Skip to content

Commit daee728

Browse files
authored
Proton current correction (#115)
* feat: proton current correction * docs: clarify that functions modify input inplace * test: proton current is added * test: correction value is expected * fix: avoid mutating input * fix: remove inplace changes
1 parent c691cae commit daee728

File tree

7 files changed

+139
-18
lines changed

7 files changed

+139
-18
lines changed

src/ess/amor/conversions.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,23 +149,27 @@ def add_masks(
149149
zlims: ZIndexLimits,
150150
bdlim: BeamDivergenceLimits,
151151
wbins: WavelengthBins,
152-
):
152+
) -> sc.DataArray:
153153
"""
154154
Masks the data by ranges in the detector
155155
coordinates ``z`` and ``y``, and by the divergence of the beam,
156156
and by wavelength.
157157
"""
158-
da.masks["stripe_range"] = _not_between(da.coords["stripe"], *ylim)
159-
da.masks['z_range'] = _not_between(da.coords["z_index"], *zlims)
160-
da.bins.masks["divergence_too_large"] = _not_between(
161-
da.bins.coords["angle_of_divergence"],
162-
bdlim[0].to(unit=da.bins.coords["angle_of_divergence"].bins.unit),
163-
bdlim[1].to(unit=da.bins.coords["angle_of_divergence"].bins.unit),
158+
da = da.assign_masks(
159+
stripe_range=_not_between(da.coords["stripe"], *ylim),
160+
z_range=_not_between(da.coords["z_index"], *zlims),
164161
)
165-
da.bins.masks['wavelength'] = _not_between(
166-
da.bins.coords['wavelength'],
167-
wbins[0],
168-
wbins[-1],
162+
da = da.bins.assign_masks(
163+
divergence_too_large=_not_between(
164+
da.bins.coords["angle_of_divergence"],
165+
bdlim[0].to(unit=da.bins.coords["angle_of_divergence"].bins.unit),
166+
bdlim[1].to(unit=da.bins.coords["angle_of_divergence"].bins.unit),
167+
),
168+
wavelength=_not_between(
169+
da.bins.coords['wavelength'],
170+
wbins[0],
171+
wbins[-1],
172+
),
169173
)
170174
return da
171175

src/ess/amor/load.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Filename,
1010
LoadedNeXusDetector,
1111
NeXusDetectorName,
12+
ProtonCurrent,
1213
RawDetectorData,
1314
RunType,
1415
SampleRotation,
@@ -43,10 +44,15 @@ def load_events(
4344
beam_size: BeamSize[RunType],
4445
angle_to_center_of_beam: AngleCenterOfIncomingToHorizon[RunType],
4546
) -> RawDetectorData[RunType]:
47+
event_data = detector["data"]
48+
if 'event_time_zero' in event_data.coords:
49+
event_data.bins.coords['event_time_zero'] = sc.bins_like(
50+
event_data, fill_value=event_data.coords['event_time_zero']
51+
)
52+
4653
detector_numbers = pixel_coordinates_in_detector_system()
4754
data = (
48-
detector["data"]
49-
.bins.constituents["data"]
55+
event_data.bins.constituents["data"]
5056
.group(detector_numbers.data.flatten(to='event_id'))
5157
.fold("event_id", sizes=detector_numbers.sizes)
5258
)
@@ -126,6 +132,15 @@ def load_amor_angle_from_horizon_to_center_of_incident_beam(
126132
)
127133

128134

135+
def load_amor_proton_current(
136+
fp: Filename[RunType],
137+
) -> ProtonCurrent[RunType]:
138+
(pc,) = load_nx(fp, 'NXentry/NXinstrument/NXdetector/proton_current')
139+
pc = pc['value']['dim_1', 0]
140+
pc.data.unit = 'mA/s'
141+
return pc
142+
143+
129144
providers = (
130145
load_detector,
131146
load_events,
@@ -136,5 +151,6 @@ def load_amor_angle_from_horizon_to_center_of_incident_beam(
136151
load_amor_sample_rotation,
137152
load_amor_detector_rotation,
138153
load_amor_angle_from_horizon_to_center_of_incident_beam,
154+
load_amor_proton_current,
139155
amor_chopper,
140156
)

src/ess/amor/workflow.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
from ..reflectometry.corrections import correct_by_footprint
1+
from ..reflectometry.conversions import (
2+
add_proton_current_coord,
3+
add_proton_current_mask,
4+
)
5+
from ..reflectometry.corrections import correct_by_footprint, correct_by_proton_current
26
from ..reflectometry.types import (
37
BeamDivergenceLimits,
8+
ProtonCurrent,
49
RawDetectorData,
510
ReducibleData,
611
RunType,
@@ -18,6 +23,7 @@ def add_coords_masks_and_apply_corrections(
1823
zlims: ZIndexLimits,
1924
bdlim: BeamDivergenceLimits,
2025
wbins: WavelengthBins,
26+
proton_current: ProtonCurrent[RunType],
2127
graph: CoordTransformationGraph,
2228
) -> ReducibleData[RunType]:
2329
"""
@@ -26,8 +32,16 @@ def add_coords_masks_and_apply_corrections(
2632
"""
2733
da = add_coords(da, graph)
2834
da = add_masks(da, ylim, zlims, bdlim, wbins)
29-
correct_by_footprint(da)
30-
return da
35+
36+
da = correct_by_footprint(da)
37+
38+
# For some older Amor files there are no entries in the proton current log
39+
if len(proton_current) != 0:
40+
da = add_proton_current_coord(da, proton_current)
41+
da = add_proton_current_mask(da)
42+
da = correct_by_proton_current(da)
43+
44+
return ReducibleData[RunType](da)
3145

3246

3347
providers = (add_coords_masks_and_apply_corrections,)

src/ess/reflectometry/conversions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from scipp.constants import pi
55
from scippneutron._utils import elem_dtype
66

7+
from .types import ProtonCurrent, RunType
8+
79

810
def reflectometry_q(wavelength: sc.Variable, theta: sc.Variable) -> sc.Variable:
911
"""
@@ -26,4 +28,37 @@ def reflectometry_q(wavelength: sc.Variable, theta: sc.Variable) -> sc.Variable:
2628
return c * sc.sin(theta.astype(dtype, copy=False)) / wavelength
2729

2830

31+
def add_proton_current_coord(
32+
da: sc.DataArray,
33+
pc: ProtonCurrent[RunType],
34+
) -> sc.DataArray:
35+
"""Find the proton current value for each event and
36+
adds it as a coord to the data array."""
37+
pc_lookup = sc.lookup(
38+
pc,
39+
dim='time',
40+
mode='previous',
41+
fill_value=sc.scalar(float('nan'), unit=pc.unit),
42+
)
43+
# Useful for comparing the proton current to what is typical
44+
da = da.assign_coords(median_proton_current=sc.median(pc).data)
45+
da.coords.set_aligned('median_proton_current', False)
46+
da = da.bins.assign_coords(
47+
proton_current=pc_lookup(da.bins.coords['event_time_zero'])
48+
)
49+
return da
50+
51+
52+
def add_proton_current_mask(da: sc.DataArray) -> sc.DataArray:
53+
"""Masks events where the proton current was too low or where
54+
the proton current is nan."""
55+
# Take inverse and use >= because we want to mask nan values
56+
da = da.bins.assign_masks(
57+
proton_current_too_low=~(
58+
da.bins.coords['proton_current'] >= da.coords['median_proton_current'] / 4
59+
)
60+
)
61+
return da
62+
63+
2964
providers = ()

src/ess/reflectometry/corrections.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ def footprint_on_sample(
3030
return sc.erf(fwhm_to_std(sample_size / size_of_beam_on_sample))
3131

3232

33-
def correct_by_footprint(da: sc.DataArray) -> None:
33+
def correct_by_footprint(da: sc.DataArray) -> sc.DataArray:
3434
"Corrects the data by the size of the footprint on the sample."
35-
da /= footprint_on_sample(
35+
return da / footprint_on_sample(
3636
da.bins.coords['theta'],
3737
da.coords['beam_size'],
3838
da.coords['sample_size'],
3939
)
40+
41+
42+
def correct_by_proton_current(da: sc.DataArray) -> sc.DataArray:
43+
"Corrects the data by the proton current during the time of data collection"
44+
return da / da.bins.coords['proton_current']

src/ess/reflectometry/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ class SampleSize(sciline.Scope[RunType, sc.Variable], sc.Variable):
9393
"""Size of the sample. If None it is assumed to be the same as the reference."""
9494

9595

96+
class ProtonCurrent(sciline.Scope[RunType, sc.DataArray], sc.DataArray):
97+
"""Proton current log from file"""
98+
99+
96100
YIndexLimits = NewType("YIndexLimits", tuple[sc.Variable, sc.Variable])
97101
"""Limit of the (logical) 'y' detector pixel index"""
98102

tests/amor/pipeline_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from ess.reflectometry import orso
1515
from ess.reflectometry.types import (
1616
Filename,
17+
ProtonCurrent,
1718
QBins,
19+
ReducibleData,
1820
ReferenceRun,
1921
ReflectivityOverQ,
2022
SampleRotation,
@@ -127,3 +129,44 @@ def test_pipeline_merging_events_result_unchanged(amor_pipeline: sciline.Pipelin
127129
assert_allclose(
128130
2 * sc.variances(result.data), sc.variances(result2.data), rtol=sc.scalar(1e-6)
129131
)
132+
133+
134+
@pytest.mark.filterwarnings("ignore:Failed to convert .* into a transformation")
135+
@pytest.mark.filterwarnings("ignore:Invalid transformation, missing attribute")
136+
def test_proton_current(amor_pipeline: sciline.Pipeline):
137+
amor_pipeline[Filename[SampleRun]] = amor.data.amor_sample_run(611)
138+
da_without_proton_current = amor_pipeline.compute(ReducibleData[SampleRun])
139+
140+
proton_current = [1, 2, 0.1]
141+
timestamps = [1699883542349602112, 1699883542349602112, 1699886864071691036]
142+
amor_pipeline[ProtonCurrent[SampleRun]] = sc.DataArray(
143+
sc.array(dims=['time'], values=proton_current),
144+
coords={
145+
'time': sc.array(
146+
dims=['time'],
147+
values=timestamps,
148+
dtype='datetime64',
149+
unit='ns',
150+
)
151+
},
152+
)
153+
da_with_proton_current = amor_pipeline.compute(ReducibleData[SampleRun])
154+
155+
assert "proton_current" in da_with_proton_current.bins.coords
156+
assert "proton_current_too_low" in da_with_proton_current.bins.masks
157+
assert da_with_proton_current.bins.masks["proton_current_too_low"].any()
158+
assert not da_with_proton_current.bins.masks["proton_current_too_low"].all()
159+
160+
assert "proton_current" not in da_without_proton_current.bins.coords
161+
assert "proton_current_too_low" not in da_without_proton_current.bins.masks
162+
163+
t = (
164+
da_with_proton_current.bins.constituents['data']
165+
.coords['event_time_zero'][0]
166+
.value.astype('uint64')
167+
)
168+
w_with = da_with_proton_current.bins.constituents['data'].data[0].value
169+
w_without = da_without_proton_current.bins.constituents['data'].data[0].value
170+
np.testing.assert_allclose(
171+
proton_current[np.searchsorted(timestamps, t) - 1], w_without / w_with
172+
)

0 commit comments

Comments
 (0)