Skip to content

Commit 35310a2

Browse files
jokasimrjl-wynen
andauthored
feat: pulse shaping (#211)
* feat: pulse shaping * fix * fix: load chopper settings in all modes - truncate time to limits of event_time_offset * data: add updated simulation files to pooch, and add provider for parameters * docs * fix: make functions public * docs * refactor: extract mode param mapping * Update src/ess/beer/conversions.py Co-authored-by: Jan-Lukas Wynen <jan-lukas.wynen@ess.eu> * fix: unnecessary mod * docs * refactor: change name to signal it makes a graph * docs: change quotes to double * test * fix: remove mod period provider from pulse shaping workflow * docs: improve names and add comment explaining the 'lambda' parameter --------- Co-authored-by: Jan-Lukas Wynen <jan-lukas.wynen@ess.eu>
1 parent d60e080 commit 35310a2

8 files changed

Lines changed: 324 additions & 67 deletions

File tree

src/ess/beer/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .io import load_beer_mcstas
1111
from .peakfinding import dhkl_peaks_from_cif
1212
from .workflow import (
13+
BeerMcStasWorkflowPulseShaping,
1314
BeerModMcStasWorkflow,
1415
BeerModMcStasWorkflowKnownPeaks,
1516
default_parameters,
@@ -23,6 +24,7 @@
2324
del importlib
2425

2526
__all__ = [
27+
'BeerMcStasWorkflowPulseShaping',
2628
'BeerModMcStasWorkflow',
2729
'BeerModMcStasWorkflowKnownPeaks',
2830
'__version__',

src/ess/beer/clustering.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from scippneutron.conversion.tof import dspacing_from_tof
33
from scipy.signal import find_peaks, medfilt
44

5+
from .conversions import t0_estimate, time_of_arrival
56
from .types import RawDetector, RunType, StreakClusteredData
67

78

@@ -10,12 +11,17 @@ def cluster_events_by_streak(da: RawDetector[RunType]) -> StreakClusteredData[Ru
1011
return sc.DataGroup({k: cluster_events_by_streak(v) for k, v in da.items()})
1112
da = da.copy(deep=False)
1213

13-
# TODO: approximate t0 should depend on chopper information
14-
approximate_t0 = sc.scalar(6e-3, unit='s')
14+
t = time_of_arrival(
15+
da.coords['event_time_offset'],
16+
da.coords['tc'].to(unit=da.coords['event_time_offset'].unit),
17+
)
18+
approximate_t0 = t0_estimate(
19+
da.coords['wavelength_estimate'], da.coords['L0'], da.coords['Ltotal']
20+
).to(unit=t.unit)
1521

1622
da.coords['coarse_d'] = dspacing_from_tof(
17-
tof=da.coords['event_time_offset'] - approximate_t0,
18-
Ltotal=da.coords['L0'],
23+
tof=t - approximate_t0,
24+
Ltotal=da.coords['Ltotal'],
1925
two_theta=da.coords['two_theta'],
2026
).to(unit='angstrom')
2127

src/ess/beer/conversions.py

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def compute_tof_in_each_cluster(
1818
da: StreakClusteredData[RunType],
1919
mod_period: ModulationPeriod,
2020
) -> TofDetector[RunType]:
21-
'''Fits a line through each cluster, the intercept of the line is t0.
21+
"""Fits a line through each cluster, the intercept of the line is t0.
2222
The line is fitted using linear regression with an outlier removal procedure.
2323
2424
The algorithm is:
@@ -30,15 +30,18 @@ def compute_tof_in_each_cluster(
3030
of the points in the cluster, and probably should belong to another cluster or
3131
are part of the background.
3232
3. Go back to 1) and iterate until convergence. A few iterations should be enough.
33-
'''
33+
"""
3434
if isinstance(da, sc.DataGroup):
3535
return sc.DataGroup(
3636
{k: compute_tof_in_each_cluster(v, mod_period) for k, v in da.items()}
3737
)
3838

3939
max_distance_from_streak_line = mod_period / 3
4040
sin_theta_L = sc.sin(da.bins.coords['two_theta'] / 2) * da.bins.coords['Ltotal']
41-
t = da.bins.coords['event_time_offset']
41+
t = time_of_arrival(
42+
da.bins.coords['event_time_offset'],
43+
da.coords['tc'].to(unit=da.bins.coords['event_time_offset'].unit),
44+
)
4245
for _ in range(15):
4346
s, t0 = _linear_regression_by_bin(sin_theta_L, t, da.data)
4447

@@ -57,10 +60,10 @@ def compute_tof_in_each_cluster(
5760
def _linear_regression_by_bin(
5861
x: sc.Variable, y: sc.Variable, w: sc.Variable
5962
) -> tuple[sc.Variable, sc.Variable]:
60-
'''Performs a weighted linear regression of the points
63+
"""Performs a weighted linear regression of the points
6164
in the binned variables ``x`` and ``y`` weighted by ``w``.
6265
Returns ``b1`` and ``b0`` such that ``y = b1 * x + b0``.
63-
'''
66+
"""
6467
w = sc.values(w)
6568
tot_w = w.bins.sum()
6669

@@ -77,7 +80,7 @@ def _linear_regression_by_bin(
7780

7881

7982
def _compute_d(
80-
event_time_offset: sc.Variable,
83+
time_of_arrival: sc.Variable,
8184
theta: sc.Variable,
8285
dhkl_list: sc.Variable,
8386
pulse_length: sc.Variable,
@@ -87,15 +90,15 @@ def _compute_d(
8790
given a list of known peaks."""
8891
# Source: https://www2.mcstas.org/download/components/3.4/contrib/NPI_tof_dhkl_detector.comp
8992
sinth = sc.sin(theta)
90-
t = event_time_offset
93+
t = time_of_arrival
9194

9295
d = sc.empty(dims=sinth.dims, shape=sinth.shape, unit=dhkl_list[0].unit)
9396
d[:] = sc.scalar(float('nan'), unit=dhkl_list[0].unit)
9497
dtfound = sc.empty(dims=sinth.dims, shape=sinth.shape, dtype='float64', unit=t.unit)
9598
dtfound[:] = sc.scalar(float('nan'), unit=t.unit)
9699

97100
const = (2 * sinth * L0 / (scipp.constants.h / scipp.constants.m_n)).to(
98-
unit=f'{event_time_offset.unit}/angstrom'
101+
unit=f'{time_of_arrival.unit}/angstrom'
99102
)
100103

101104
for dhkl in dhkl_list:
@@ -112,36 +115,49 @@ def _compute_d(
112115
return d
113116

114117

115-
def _tof_from_dhkl(
118+
def time_of_arrival(
116119
event_time_offset: sc.Variable,
120+
tc: sc.Variable,
121+
):
122+
"""Does frame unwrapping for pulse shaping chopper modes.
123+
124+
Events before the "cutoff time" `tc` are assumed to come from the previous pulse."""
125+
_eto = event_time_offset
126+
T = sc.scalar(1 / 14, unit='s').to(unit=_eto.unit)
127+
tc = tc.to(unit=_eto.unit)
128+
return sc.where(_eto >= tc, _eto, _eto + T)
129+
130+
131+
def _tof_from_dhkl(
132+
time_of_arrival: sc.Variable,
117133
theta: sc.Variable,
118134
coarse_dhkl: sc.Variable,
119135
Ltotal: sc.Variable,
120136
mod_period: sc.Variable,
121137
time0: sc.Variable,
122138
) -> sc.Variable:
123-
'''Computes tof for BEER given the dhkl peak that the event belongs to'''
139+
"""Computes tof for BEER given the dhkl peak that the event belongs to"""
124140
# Source: https://www2.mcstas.org/download/components/3.4/contrib/NPI_tof_dhkl_detector.comp
125141
# tref = 2 * d_hkl * sin(theta) / hm * Ltotal
126-
# tc = event_time_zero - time0 - tref
142+
# tc = time_of_arrival - time0 - tref
127143
# dt = floor(tc / mod_period + 0.5) * mod_period + time0
128-
# tof = event_time_offset - dt
144+
# tof = time_of_arrival - dt
129145
c = (-2 * 1.0 / (scipp.constants.h / scipp.constants.m_n)).to(
130-
unit=f'{event_time_offset.unit}/m/angstrom'
146+
unit=f'{time_of_arrival.unit}/m/angstrom'
131147
)
132148
out = sc.sin(theta)
133149
out *= c
134150
out *= coarse_dhkl
135151
out *= Ltotal
136-
out += event_time_offset
152+
out += time_of_arrival
137153
out -= time0
138154
out /= mod_period
139155
out += 0.5
140156
sc.floor(out, out=out)
141157
out *= mod_period
142158
out += time0
143159
out *= -1
144-
out += event_time_offset
160+
out += time_of_arrival
145161
return out
146162

147163

@@ -151,20 +167,23 @@ def tof_from_known_dhkl_graph(
151167
time0: WavelengthDefinitionChopperDelay,
152168
dhkl_list: DHKLList,
153169
) -> TofCoordTransformGraph:
170+
"""Graph computing ``tof`` in modulation chopper modes using
171+
list of peak positions."""
172+
154173
def _compute_coarse_dspacing(
155-
event_time_offset,
174+
time_of_arrival: sc.Variable,
156175
theta: sc.Variable,
157176
pulse_length: sc.Variable,
158-
L0,
177+
L0: sc.Variable,
159178
):
160-
'''To capture dhkl_list, otherwise it causes an error when
179+
"""To capture dhkl_list, otherwise it causes an error when
161180
``.transform_coords`` is called unless it is called with
162181
``keep_indermediates=False``.
163182
The error happens because data arrays do not allow coordinates
164183
with dimensions not present on the data.
165-
'''
184+
"""
166185
return _compute_d(
167-
event_time_offset=event_time_offset,
186+
time_of_arrival=time_of_arrival,
168187
theta=theta,
169188
pulse_length=pulse_length,
170189
L0=L0,
@@ -176,19 +195,56 @@ def _compute_coarse_dspacing(
176195
'mod_period': lambda: mod_period,
177196
'time0': lambda: time0,
178197
'tof': _tof_from_dhkl,
198+
'time_of_arrival': time_of_arrival,
179199
'coarse_dhkl': _compute_coarse_dspacing,
180200
'theta': lambda two_theta: two_theta / 2,
181201
}
182202

183203

184-
def compute_tof_from_known_peaks(
204+
def t0_estimate(
205+
wavelength_estimate: sc.Variable,
206+
L0: sc.Variable,
207+
Ltotal: sc.Variable,
208+
) -> sc.Variable:
209+
"""Estimates the time-at-chopper by assuming the wavelength."""
210+
return (
211+
sc.constants.m_n
212+
/ sc.constants.h
213+
* wavelength_estimate
214+
* (L0 - Ltotal).to(unit=wavelength_estimate.unit)
215+
).to(unit='s')
216+
217+
218+
def _tof_from_t0(
219+
time_of_arrival: sc.Variable,
220+
t0: sc.Variable,
221+
) -> sc.Variable:
222+
"""Computes time-of-flight by subtracting a start time."""
223+
return time_of_arrival - t0
224+
225+
226+
def tof_from_t0_estimate_graph() -> TofCoordTransformGraph:
227+
"""Graph for computing ``tof`` in pulse shaping chopper modes."""
228+
return {
229+
't0': t0_estimate,
230+
'tof': _tof_from_t0,
231+
'time_of_arrival': time_of_arrival,
232+
}
233+
234+
235+
def compute_tof(
185236
da: RawDetector[RunType], graph: TofCoordTransformGraph
186237
) -> TofDetector[RunType]:
238+
"""Uses the transformation graph to compute ``tof``."""
187239
return da.transform_coords(('tof',), graph=graph)
188240

189241

190242
convert_from_known_peaks_providers = (
191243
tof_from_known_dhkl_graph,
192-
compute_tof_from_known_peaks,
244+
compute_tof,
245+
)
246+
convert_pulse_shaping = (
247+
tof_from_t0_estimate_graph,
248+
compute_tof,
193249
)
194250
providers = (compute_tof_in_each_cluster,)

src/ess/beer/data.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@
2828
"silicon-dhkl.tab": "md5:59ee9ed57a7c039ce416c8df886da9cc",
2929
"duplex-dhkl.tab": "md5:b4c6c2fcd66466ad291f306b2d6b346e",
3030
"dhkl_quartz_nc.tab": "md5:40887d736e3acf859e44488bfd9a9213",
31+
# Simulations from new model with corrected(?) L0.
32+
# For correct reduction you need to use
33+
# beer.io.mcstas_chopper_delay_from_mode_new_simulations
34+
# to obtain the correct WavelengthDefinitionChopperDelay for these files.
35+
"silicon-mode10-new-model.h5": "md5:98500830f27700fc719634e1acd49944",
36+
"silicon-mode16-new-model.h5": "md5:393f9287e7d3f97ceedbe64343918413",
37+
"silicon-mode3-new-model.h5": "md5:260fc811352b96c1dcdf431d91c11787",
38+
"silicon-mode4-new-model.h5": "md5:625561f6a57c6e6892e1bbd4043d71ee",
39+
"silicon-mode5-new-model.h5": "md5:315d457d136bb597d7f7ab596497324b",
40+
"silicon-mode6-new-model.h5": "md5:be9b48ee9db9fcf1808ed2f2e35f594b",
41+
"silicon-mode7-new-model.h5": "md5:d2070d3132722bb551d99b243c62752f",
42+
"silicon-mode8-new-model.h5": "md5:d6dfdf7e87eccedf4f83c67ec552ca22",
43+
"silicon-mode9-new-model.h5": "md5:694a17fb616b7f1c20e94d9da113d201",
3144
},
3245
)
3346

@@ -54,6 +67,13 @@ def mcstas_silicon_medium_resolution() -> Path:
5467
return _registry.get_path('silicon-mode09.h5')
5568

5669

70+
def mcstas_silicon_new_model(mode: int) -> Path:
71+
"""
72+
Simulated intensity from duplex sample with ``mode`` chopper configuration.
73+
"""
74+
return _registry.get_path(f'silicon-mode{mode}-new-model.h5')
75+
76+
5777
def duplex_peaks() -> Path:
5878
return _registry.get_path('duplex-dhkl.tab')
5979

0 commit comments

Comments
 (0)