Skip to content

Commit 90aa070

Browse files
authored
Merge pull request #264 from ISISComputingGroup/polref
Reflectometry: add flood-map correction & change `Guassian` fit to use centre-of-mass as an initial `x0` guess
2 parents c1bfb2c + a8e5f14 commit 90aa070

File tree

8 files changed

+160
-37
lines changed

8 files changed

+160
-37
lines changed

doc/plans/reflectometry/detector_mapping_alignment.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ The plans in this module expect:
99
{py:obj}`ibex_bluesky_core.devices.simpledae.PeriodSpecIntegralsReducer`.
1010
- An angle map, as a `numpy` array of type `float64`, which has the same dimensionality as the set of selected detectors. This
1111
maps each configured detector pixel to its angular position.
12+
- An optional flood map, as a {external+scipp:py:obj}`scipp.Variable`. This should have a dimension label of "spectrum"
13+
and have the same dimensionality as the set of selected detectors. This array may have variances. This is used to
14+
normalise pixel efficiencies: raw counts are divided by the flood to get scaled counts. If no flood map is provided, no
15+
normalisation will be performed.
1216

1317
## Angle scan
1418

src/ibex_bluesky_core/callbacks/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,11 @@ def start(self, doc: RunStart) -> None:
229229
# where a fit result can be returned before
230230
# the QtAwareCallback has had a chance to process it.
231231
self._subs.append(self._live_fit)
232-
self._subs.append(LiveFitPlot(livefit=self._live_fit, ax=ax))
232+
233+
# Sample 5000 points as this strikes a reasonable balance between displaying
234+
# 'enough' points for almost any scan (even after zooming in on a peak), while
235+
# not taking 'excessive' compute time to generate these samples.
236+
self._subs.append(LiveFitPlot(livefit=self._live_fit, ax=ax, num_points=5000))
233237
else:
234238
self._subs.append(self._live_fit)
235239

src/ibex_bluesky_core/callbacks/_plotting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def __init__(
206206
y: str,
207207
ax: Axes,
208208
postfix: str,
209-
output_dir: str | os.PathLike[str] | None,
209+
output_dir: str | os.PathLike[str] | None = None,
210210
) -> None:
211211
"""Initialise the PlotPNGSaver callback.
212212

src/ibex_bluesky_core/callbacks/reflectometry/_det_map.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ class DetMapHeightScanLiveDispatcher(LiveDispatcher):
4646
monitor spectrum used for normalization).
4747
"""
4848

49-
def __init__(self, *, mon_name: str, det_name: str, out_name: str) -> None:
49+
def __init__(
50+
self, *, mon_name: str, det_name: str, out_name: str, flood: sc.Variable | None = None
51+
) -> None:
5052
"""Init."""
5153
super().__init__()
5254
self._mon_name = mon_name
5355
self._det_name = det_name
5456
self._out_name = out_name
57+
self._flood = flood if flood is not None else sc.scalar(value=1, dtype="float64")
5558

5659
def event(self, doc: Event, **kwargs: dict[str, Any]) -> Event:
5760
"""Process an event."""
@@ -62,6 +65,8 @@ def event(self, doc: Event, **kwargs: dict[str, Any]) -> Event:
6265
det = sc.Variable(dims=["spectrum"], values=det_data, variances=det_data, dtype="float64")
6366
mon = sc.Variable(dims=["spectrum"], values=mon_data, variances=mon_data, dtype="float64")
6467

68+
det /= self._flood
69+
6570
det_sum = det.sum()
6671
mon_sum = mon.sum()
6772

@@ -90,19 +95,31 @@ class DetMapAngleScanLiveDispatcher(LiveDispatcher):
9095
"""
9196

9297
def __init__(
93-
self, x_data: npt.NDArray[np.float64], x_name: str, y_in_name: str, y_out_name: str
98+
self,
99+
x_data: npt.NDArray[np.float64],
100+
x_name: str,
101+
y_in_name: str,
102+
y_out_name: str,
103+
flood: sc.Variable | None = None,
94104
) -> None:
95105
"""Init."""
96106
super().__init__()
97107
self.x_data = x_data
98108
self.x_name = x_name
99109

100-
self.y_data = np.zeros_like(x_data)
110+
self.y_data = sc.array(
111+
dims=["spectrum"],
112+
values=np.zeros_like(x_data),
113+
variances=np.zeros_like(x_data),
114+
dtype="float64",
115+
)
101116
self.y_in_name: str = y_in_name
102117
self.y_out_name: str = y_out_name
103118

104119
self._descriptor_uid: str | None = None
105120

121+
self._flood = flood if flood is not None else sc.scalar(value=1, dtype="float64")
122+
106123
def descriptor(self, doc: EventDescriptor) -> None:
107124
"""Process a descriptor."""
108125
self._descriptor_uid = doc["uid"]
@@ -118,7 +135,10 @@ def event(self, doc: Event, **kwargs: dict[str, Any]) -> Event:
118135
f"Shape of data ({data.shape} does not match x_data.shape ({self.x_data.shape})"
119136
)
120137

121-
self.y_data += data
138+
scaled_data = (
139+
sc.array(dims=["spectrum"], values=data, variances=data, dtype="float64") / self._flood
140+
)
141+
self.y_data += scaled_data
122142
return doc
123143

124144
def stop(self, doc: RunStop, _md: dict[str, Any] | None = None) -> None:
@@ -128,13 +148,13 @@ def stop(self, doc: RunStop, _md: dict[str, Any] | None = None) -> None:
128148
return super().stop(doc, _md)
129149

130150
current_time = time.time()
131-
for x, y in zip(self.x_data, self.y_data, strict=True):
151+
for x, y in zip(self.x_data, self.y_data, strict=True): # type: ignore (pyright doesn't understand scipp)
132152
logger.debug("DetMapAngleScanLiveDispatcher emitting event with x=%f, y=%f", x, y)
133153
event = {
134154
"data": {
135155
self.x_name: x,
136-
self.y_out_name: y,
137-
self.y_out_name + "_err": np.sqrt(y + 0.5),
156+
self.y_out_name: y.value,
157+
self.y_out_name + "_err": np.sqrt(y.variance + 0.5),
138158
},
139159
"timestamps": {
140160
self.x_name: current_time,

src/ibex_bluesky_core/fitting.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Fitting methods used by the LiveFit callback."""
22

3+
import math
34
from abc import ABC, abstractmethod
45
from collections.abc import Callable
56

@@ -102,6 +103,19 @@ def fit(cls, *args: int) -> FitMethod:
102103
return FitMethod(model=cls.model(*args), guess=cls.guess(*args))
103104

104105

106+
def _guess_cen_and_width(
107+
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
108+
) -> tuple[float, float]:
109+
"""Guess the center and width of a positive peak."""
110+
com, total_area = center_of_mass_of_area_under_curve(x, y)
111+
y_range = np.max(y) - np.min(y)
112+
if y_range == 0.0:
113+
width = (np.max(x) - np.min(x)) / 2
114+
else:
115+
width = total_area / y_range
116+
return com, width
117+
118+
105119
class Gaussian(Fit):
106120
"""Gaussian Fitting."""
107121

@@ -130,8 +144,9 @@ def guess(
130144
def guess(
131145
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
132146
) -> dict[str, lmfit.Parameter]:
133-
mean = np.sum(x * y) / np.sum(y)
134-
sigma = np.sqrt(np.sum(y * (x - mean) ** 2) / np.sum(y))
147+
cen, width = _guess_cen_and_width(x, y)
148+
sigma = width / math.sqrt(2 * math.pi) # From expected area under gaussian
149+
135150
background = np.min(y)
136151

137152
if np.max(y) > abs(np.min(y)):
@@ -142,7 +157,7 @@ def guess(
142157
init_guess = {
143158
"amp": lmfit.Parameter("amp", amp),
144159
"sigma": lmfit.Parameter("sigma", sigma, min=0),
145-
"x0": lmfit.Parameter("x0", mean),
160+
"x0": lmfit.Parameter("x0", cen),
146161
"background": lmfit.Parameter("background", background),
147162
}
148163

@@ -547,19 +562,6 @@ def guess(
547562
return guess
548563

549564

550-
def _guess_cen_and_width(
551-
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
552-
) -> tuple[float, float]:
553-
"""Guess the center and width of a positive peak."""
554-
com, total_area = center_of_mass_of_area_under_curve(x, y)
555-
y_range = np.max(y) - np.min(y)
556-
if y_range == 0.0:
557-
width = (np.max(x) - np.min(x)) / 2
558-
else:
559-
width = total_area / y_range
560-
return com, width
561-
562-
563565
class TopHat(Fit):
564566
"""Top Hat Fitting."""
565567

0 commit comments

Comments
 (0)