essreduce 25.5.3__py3-none-any.whl → 25.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,46 +6,54 @@ Utilities for computing real neutron time-of-flight from chopper settings and
6
6
  neutron time-of-arrival at the detectors.
7
7
  """
8
8
 
9
- from .eto_to_tof import default_parameters, providers
10
- from .simulation import simulate_beamline
11
- from .types import (
12
- DetectorLtotal,
13
- DetectorTofData,
9
+ from ..nexus.types import DiskChoppers
10
+ from .eto_to_tof import providers
11
+ from .lut import (
14
12
  DistanceResolution,
15
13
  LookupTableRelativeErrorThreshold,
16
14
  LtotalRange,
17
- MonitorLtotal,
18
- MonitorTofData,
15
+ NumberOfSimulatedNeutrons,
19
16
  PulsePeriod,
20
17
  PulseStride,
21
- PulseStrideOffset,
22
18
  SimulationResults,
19
+ SimulationSeed,
20
+ SourcePosition,
21
+ TimeResolution,
22
+ TofLookupTableWorkflow,
23
+ simulate_chopper_cascade_using_tof,
24
+ )
25
+ from .types import (
26
+ DetectorLtotal,
27
+ DetectorTofData,
28
+ MonitorLtotal,
29
+ MonitorTofData,
30
+ PulseStrideOffset,
23
31
  TimeOfFlightLookupTable,
24
32
  TimeOfFlightLookupTableFilename,
25
- TimeResolution,
26
33
  )
27
- from .workflow import GenericTofWorkflow, TofLutProvider
34
+ from .workflow import GenericTofWorkflow
28
35
 
29
36
  __all__ = [
30
37
  "DetectorLtotal",
31
38
  "DetectorTofData",
32
- "DetectorTofData",
39
+ "DiskChoppers",
33
40
  "DistanceResolution",
34
41
  "GenericTofWorkflow",
35
42
  "LookupTableRelativeErrorThreshold",
36
43
  "LtotalRange",
37
44
  "MonitorLtotal",
38
45
  "MonitorTofData",
39
- "MonitorTofData",
46
+ "NumberOfSimulatedNeutrons",
40
47
  "PulsePeriod",
41
48
  "PulseStride",
42
49
  "PulseStrideOffset",
43
50
  "SimulationResults",
51
+ "SimulationSeed",
52
+ "SourcePosition",
44
53
  "TimeOfFlightLookupTable",
45
54
  "TimeOfFlightLookupTableFilename",
46
55
  "TimeResolution",
47
- "TofLutProvider",
48
- "default_parameters",
56
+ "TofLookupTableWorkflow",
49
57
  "providers",
50
- "simulate_beamline",
58
+ "simulate_chopper_cascade_using_tof",
51
59
  ]
@@ -31,278 +31,13 @@ from .resample import rebin_strictly_increasing
31
31
  from .types import (
32
32
  DetectorLtotal,
33
33
  DetectorTofData,
34
- DistanceResolution,
35
- LookupTableRelativeErrorThreshold,
36
- LtotalRange,
37
34
  MonitorLtotal,
38
35
  MonitorTofData,
39
- PulsePeriod,
40
- PulseStride,
41
36
  PulseStrideOffset,
42
- SimulationResults,
43
37
  TimeOfFlightLookupTable,
44
- TimeResolution,
45
38
  )
46
39
 
47
40
 
48
- def _mask_large_uncertainty(table: sc.DataArray, error_threshold: float):
49
- """
50
- Mask regions with large uncertainty with NaNs.
51
- The values are modified in place in the input table.
52
-
53
- Parameters
54
- ----------
55
- table:
56
- Lookup table with time-of-flight as a function of distance and time-of-arrival.
57
- error_threshold:
58
- Threshold for the relative standard deviation (coefficient of variation) of the
59
- projected time-of-flight above which values are masked.
60
- """
61
- # Finally, mask regions with large uncertainty with NaNs.
62
- relative_error = sc.stddevs(table.data) / sc.values(table.data)
63
- mask = relative_error > sc.scalar(error_threshold)
64
- # Use numpy for indexing as table is 2D
65
- table.values[mask.values] = np.nan
66
-
67
-
68
- def _compute_mean_tof_in_distance_range(
69
- simulation: SimulationResults,
70
- distance_bins: sc.Variable,
71
- time_bins: sc.Variable,
72
- distance_unit: str,
73
- time_unit: str,
74
- frame_period: sc.Variable,
75
- time_bins_half_width: sc.Variable,
76
- ) -> sc.DataArray:
77
- """
78
- Compute the mean time-of-flight inside event_time_offset bins for a given range of
79
- distances.
80
-
81
- Parameters
82
- ----------
83
- simulation:
84
- Results of a time-of-flight simulation used to create a lookup table.
85
- distance_bins:
86
- Bin edges for the distance axis in the lookup table.
87
- time_bins:
88
- Bin edges for the event_time_offset axis in the lookup table.
89
- distance_unit:
90
- Unit of the distance axis.
91
- time_unit:
92
- Unit of the event_time_offset axis.
93
- frame_period:
94
- Period of the source pulses, i.e., time between consecutive pulse starts.
95
- time_bins_half_width:
96
- Half width of the time bins in the event_time_offset axis.
97
- """
98
- simulation_distance = simulation.distance.to(unit=distance_unit)
99
- distances = sc.midpoints(distance_bins)
100
- # Compute arrival and flight times for all neutrons
101
- toas = simulation.time_of_arrival + (distances / simulation.speed).to(
102
- unit=time_unit, copy=False
103
- )
104
- dist = distances + simulation_distance
105
- tofs = dist * (sc.constants.m_n / sc.constants.h) * simulation.wavelength
106
-
107
- data = sc.DataArray(
108
- data=sc.broadcast(simulation.weight, sizes=toas.sizes),
109
- coords={
110
- "toa": toas,
111
- "tof": tofs.to(unit=time_unit, copy=False),
112
- "distance": dist,
113
- },
114
- ).flatten(to="event")
115
-
116
- # Add the event_time_offset coordinate, wrapped to the frame_period
117
- data.coords['event_time_offset'] = data.coords['toa'] % frame_period
118
-
119
- # Because we staggered the mesh by half a bin width, we want the values above
120
- # the last bin edge to wrap around to the first bin.
121
- # Technically, those values should end up between -0.5*bin_width and 0, but
122
- # a simple modulo also works here because even if they end up between 0 and
123
- # 0.5*bin_width, we are (below) computing the mean between -0.5*bin_width and
124
- # 0.5*bin_width and it yields the same result.
125
- # data.coords['event_time_offset'] %= pulse_period - time_bins_half_width
126
- data.coords['event_time_offset'] %= frame_period - time_bins_half_width
127
-
128
- binned = data.bin(
129
- distance=distance_bins + simulation_distance, event_time_offset=time_bins
130
- )
131
-
132
- # Weighted mean of tof inside each bin
133
- mean_tof = (
134
- binned.bins.data * binned.bins.coords["tof"]
135
- ).bins.sum() / binned.bins.sum()
136
- # Compute the variance of the tofs to track regions with large uncertainty
137
- variance = (
138
- binned.bins.data * (binned.bins.coords["tof"] - mean_tof) ** 2
139
- ).bins.sum() / binned.bins.sum()
140
-
141
- mean_tof.variances = variance.values
142
- return mean_tof
143
-
144
-
145
- def compute_tof_lookup_table(
146
- simulation: SimulationResults,
147
- ltotal_range: LtotalRange,
148
- distance_resolution: DistanceResolution,
149
- time_resolution: TimeResolution,
150
- pulse_period: PulsePeriod,
151
- pulse_stride: PulseStride,
152
- error_threshold: LookupTableRelativeErrorThreshold,
153
- ) -> TimeOfFlightLookupTable:
154
- """
155
- Compute a lookup table for time-of-flight as a function of distance and
156
- time-of-arrival.
157
-
158
- Parameters
159
- ----------
160
- simulation:
161
- Results of a time-of-flight simulation used to create a lookup table.
162
- The results should be a flat table with columns for time-of-arrival, speed,
163
- wavelength, and weight.
164
- ltotal_range:
165
- Range of total flight path lengths from the source to the detector.
166
- distance_resolution:
167
- Resolution of the distance axis in the lookup table.
168
- time_resolution:
169
- Resolution of the time-of-arrival axis in the lookup table. Must be an integer.
170
- pulse_period:
171
- Period of the source pulses, i.e., time between consecutive pulse starts.
172
- pulse_stride:
173
- Stride of used pulses. Usually 1, but may be a small integer when
174
- pulse-skipping.
175
- error_threshold:
176
- Threshold for the relative standard deviation (coefficient of variation) of the
177
- projected time-of-flight above which values are masked.
178
-
179
- Notes
180
- -----
181
-
182
- Below are some details about the binning and wrapping around frame period in the
183
- time dimension.
184
-
185
- We have some simulated ``toa`` (events) from a Tof/McStas simulation.
186
- Those are absolute ``toa``, unwrapped.
187
- First we compute the usual ``event_time_offset = toa % frame_period``.
188
-
189
- Now, we want to ensure periodic boundaries. If we make a bin centered around 0,
190
- and a bin centered around 71ms: the first bin will use events between 0 and
191
- ``0.5 * dt`` (where ``dt`` is the bin width).
192
- The last bin will use events between ``frame_period - 0.5*dt`` and
193
- ``frame_period + 0.5 * dt``. So when we compute the mean inside those two bins,
194
- they will not yield the same results.
195
- It is as if the first bin is missing the events it should have between
196
- ``-0.5 * dt`` and 0 (because of the modulo we computed above).
197
-
198
- To fix this, we do not make a last bin around 71ms (the bins stop at
199
- ``frame_period - 0.5*dt``). Instead, we compute modulo a second time,
200
- but this time using ``event_time_offset %= (frame_period - 0.5*dt)``.
201
- (we cannot directly do ``event_time_offset = toa % (frame_period - 0.5*dt)`` in a
202
- single step because it would introduce a gradual shift,
203
- as the pulse number increases).
204
-
205
- This second modulo effectively takes all the events that would have gone in the
206
- last bin (between ``frame_period - 0.5*dt`` and ``frame_period``) and puts them in
207
- the first bin. Instead of placing them between ``-0.5*dt`` and 0,
208
- it places them between 0 and ``0.5*dt``, but this does not really matter,
209
- because we then take the mean inside the first bin.
210
- Whether the events are on the left or right side of zero does not matter.
211
-
212
- Finally, we make a copy of the left edge, and append it to the right of the table,
213
- thus ensuring that the values on the right edge are strictly the same as on the
214
- left edge.
215
- """
216
- distance_unit = "m"
217
- time_unit = simulation.time_of_arrival.unit
218
- res = distance_resolution.to(unit=distance_unit)
219
- pulse_period = pulse_period.to(unit=time_unit)
220
- frame_period = pulse_period * pulse_stride
221
-
222
- min_dist, max_dist = (
223
- x.to(unit=distance_unit) - simulation.distance.to(unit=distance_unit)
224
- for x in ltotal_range
225
- )
226
- # We need to bin the data below, to compute the weighted mean of the wavelength.
227
- # This results in data with bin edges.
228
- # However, the 2d interpolator expects bin centers.
229
- # We want to give the 2d interpolator a table that covers the requested range,
230
- # hence we need to extend the range by at least half a resolution in each direction.
231
- # Then, we make the choice that the resolution in distance is the quantity that
232
- # should be preserved. Because the difference between min and max distance is
233
- # not necessarily an integer multiple of the resolution, we need to add a pad to
234
- # ensure that the last bin is not cut off. We want the upper edge to be higher than
235
- # the maximum distance, hence we pad with an additional 1.5 x resolution.
236
- pad = 2.0 * res
237
- distance_bins = sc.arange('distance', min_dist - pad, max_dist + pad, res)
238
-
239
- # Create some time bins for event_time_offset.
240
- # We want our final table to strictly cover the range [0, frame_period].
241
- # However, binning the data associates mean values inside the bins to the bin
242
- # centers. Instead, we stagger the mesh by half a bin width so we are computing
243
- # values for the final mesh edges (the bilinear interpolation needs values on the
244
- # edges/corners).
245
- nbins = int(frame_period / time_resolution.to(unit=time_unit)) + 1
246
- time_bins = sc.linspace(
247
- 'event_time_offset', 0.0, frame_period.value, nbins + 1, unit=pulse_period.unit
248
- )
249
- time_bins_half_width = 0.5 * (time_bins[1] - time_bins[0])
250
- time_bins -= time_bins_half_width
251
-
252
- # To avoid a too large RAM usage, we compute the table in chunks, and piece them
253
- # together at the end.
254
- ndist = len(distance_bins) - 1
255
- max_size = 2e7
256
- total_size = ndist * len(simulation.time_of_arrival)
257
- nchunks = total_size / max_size
258
- chunk_size = int(ndist / nchunks) + 1
259
- pieces = []
260
- for i in range(int(nchunks) + 1):
261
- dist_edges = distance_bins[i * chunk_size : (i + 1) * chunk_size + 1]
262
-
263
- pieces.append(
264
- _compute_mean_tof_in_distance_range(
265
- simulation=simulation,
266
- distance_bins=dist_edges,
267
- time_bins=time_bins,
268
- distance_unit=distance_unit,
269
- time_unit=time_unit,
270
- frame_period=frame_period,
271
- time_bins_half_width=time_bins_half_width,
272
- )
273
- )
274
-
275
- table = sc.concat(pieces, 'distance')
276
- table.coords["distance"] = sc.midpoints(table.coords["distance"])
277
- table.coords["event_time_offset"] = sc.midpoints(table.coords["event_time_offset"])
278
-
279
- # Copy the left edge to the right to create periodic boundary conditions
280
- table = sc.DataArray(
281
- data=sc.concat(
282
- [table.data, table.data['event_time_offset', 0]], dim='event_time_offset'
283
- ),
284
- coords={
285
- "distance": table.coords["distance"],
286
- "event_time_offset": sc.concat(
287
- [table.coords["event_time_offset"], frame_period],
288
- dim='event_time_offset',
289
- ),
290
- "pulse_period": pulse_period,
291
- "pulse_stride": sc.scalar(pulse_stride, unit=None),
292
- "distance_resolution": table.coords["distance"][1]
293
- - table.coords["distance"][0],
294
- "time_resolution": table.coords["event_time_offset"][1]
295
- - table.coords["event_time_offset"][0],
296
- "error_threshold": sc.scalar(error_threshold),
297
- },
298
- )
299
-
300
- # In-place masking for better performance
301
- _mask_large_uncertainty(table, error_threshold)
302
-
303
- return TimeOfFlightLookupTable(table)
304
-
305
-
306
41
  class TofInterpolator:
307
42
  def __init__(self, lookup: sc.DataArray, distance_unit: str, time_unit: str):
308
43
  self._distance_unit = distance_unit
@@ -360,8 +95,8 @@ def _time_of_flight_data_histogram(
360
95
  da: sc.DataArray, lookup: sc.DataArray, ltotal: sc.Variable
361
96
  ) -> sc.DataArray:
362
97
  # In NeXus, 'time_of_flight' is the canonical name in NXmonitor, but in some files,
363
- # it may be called 'tof'.
364
- key = next(iter(set(da.coords.keys()) & {"time_of_flight", "tof"}))
98
+ # it may be called 'tof' or 'frame_time'.
99
+ key = next(iter(set(da.coords.keys()) & {"time_of_flight", "tof", "frame_time"}))
365
100
  raw_eto = da.coords[key].to(dtype=float, copy=False)
366
101
  eto_unit = raw_eto.unit
367
102
  pulse_period = lookup.coords["pulse_period"].to(unit=eto_unit)
@@ -387,7 +122,9 @@ def _time_of_flight_data_histogram(
387
122
  pulse_period=pulse_period,
388
123
  )
389
124
 
390
- return rebinned.assign_coords(tof=tofs)
125
+ return rebinned.assign_coords(tof=tofs).drop_coords(
126
+ list({key} & {"time_of_flight", "frame_time"})
127
+ )
391
128
 
392
129
 
393
130
  def _guess_pulse_stride_offset(
@@ -529,7 +266,8 @@ def _time_of_flight_data_events(
529
266
 
530
267
  parts = da.bins.constituents
531
268
  parts["data"] = tofs
532
- return da.bins.assign_coords(tof=sc.bins(**parts, validate_indices=False))
269
+ result = da.bins.assign_coords(tof=sc.bins(**parts, validate_indices=False))
270
+ return result.bins.drop_coords("event_time_offset")
533
271
 
534
272
 
535
273
  def detector_ltotal_from_straight_line_approximation(
@@ -663,26 +401,11 @@ def monitor_time_of_flight_data(
663
401
  )
664
402
 
665
403
 
666
- def default_parameters() -> dict:
667
- """
668
- Default parameters of the time-of-flight workflow.
669
- """
670
- return {
671
- PulsePeriod: 1.0 / sc.scalar(14.0, unit="Hz"),
672
- PulseStride: 1,
673
- PulseStrideOffset: None,
674
- DistanceResolution: sc.scalar(0.1, unit="m"),
675
- TimeResolution: sc.scalar(250.0, unit='us'),
676
- LookupTableRelativeErrorThreshold: 0.1,
677
- }
678
-
679
-
680
404
  def providers() -> tuple[Callable]:
681
405
  """
682
406
  Providers of the time-of-flight workflow.
683
407
  """
684
408
  return (
685
- compute_tof_lookup_table,
686
409
  detector_time_of_flight_data,
687
410
  monitor_time_of_flight_data,
688
411
  detector_ltotal_from_straight_line_approximation,
@@ -0,0 +1,478 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3
+ """
4
+ Utilities for computing time-of-flight lookup tables from neutron simulations.
5
+ """
6
+
7
+ import math
8
+ from dataclasses import dataclass
9
+ from typing import NewType
10
+
11
+ import numpy as np
12
+ import sciline as sl
13
+ import scipp as sc
14
+
15
+ from ..nexus.types import DiskChoppers
16
+ from .types import TimeOfFlightLookupTable
17
+
18
+
19
+ @dataclass
20
+ class SimulationResults:
21
+ """
22
+ Results of a time-of-flight simulation used to create a lookup table.
23
+
24
+ The results (apart from ``distance``) should be flat lists (1d arrays) of length N
25
+ where N is the number of neutrons, containing the properties of the neutrons in the
26
+ simulation.
27
+
28
+ Parameters
29
+ ----------
30
+ time_of_arrival:
31
+ Time of arrival of the neutrons at the position where the events were recorded
32
+ (1d array of size N).
33
+ speed:
34
+ Speed of the neutrons, typically derived from the wavelength of the neutrons
35
+ (1d array of size N).
36
+ wavelength:
37
+ Wavelength of the neutrons (1d array of size N).
38
+ weight:
39
+ Weight/probability of the neutrons (1d array of size N).
40
+ distance:
41
+ Distance from the source to the position where the events were recorded
42
+ (single value; we assume all neutrons were recorded at the same position).
43
+ For a ``tof`` simulation, this is just the position of the detector where the
44
+ events are recorded. For a ``McStas`` simulation, this is the distance between
45
+ the source and the event monitor.
46
+ """
47
+
48
+ time_of_arrival: sc.Variable
49
+ speed: sc.Variable
50
+ wavelength: sc.Variable
51
+ weight: sc.Variable
52
+ distance: sc.Variable
53
+
54
+
55
+ NumberOfSimulatedNeutrons = NewType("NumberOfSimulatedNeutrons", int)
56
+ """
57
+ Number of neutrons simulated in the simulation that is used to create the lookup table.
58
+ This is typically a large number, e.g., 1e6 or 1e7.
59
+ """
60
+
61
+ LtotalRange = NewType("LtotalRange", tuple[sc.Variable, sc.Variable])
62
+ """
63
+ Range (min, max) of the total length of the flight path from the source to the detector.
64
+ This is used to create the lookup table to compute the neutron time-of-flight.
65
+ Note that the resulting table will extend slightly beyond this range, as the supplied
66
+ range is not necessarily a multiple of the distance resolution.
67
+
68
+ Note also that the range of total flight paths is supplied manually to the workflow
69
+ instead of being read from the input data, as it allows us to compute the expensive part
70
+ of the workflow in advance (the lookup table) and does not need to be repeated for each
71
+ run, or for new data coming in in the case of live data collection.
72
+ """
73
+
74
+ DistanceResolution = NewType("DistanceResolution", sc.Variable)
75
+ """
76
+ Step size of the distance axis in the lookup table.
77
+ Should be a single scalar value with a unit of length.
78
+ This is typically of the order of 1-10 cm.
79
+ """
80
+
81
+ TimeResolution = NewType("TimeResolution", sc.Variable)
82
+ """
83
+ Step size of the event_time_offset axis in the lookup table.
84
+ This is basically the 'time-of-flight' resolution of the detector.
85
+ Should be a single scalar value with a unit of time.
86
+ This is typically of the order of 0.1-0.5 ms.
87
+
88
+ Since the event_time_offset range needs to span exactly one pulse period, the final
89
+ resolution in the lookup table will be at least the supplied value here, but may be
90
+ smaller if the pulse period is not an integer multiple of the time resolution.
91
+ """
92
+
93
+
94
+ LookupTableRelativeErrorThreshold = NewType("LookupTableRelativeErrorThreshold", float)
95
+ """
96
+ Threshold for the relative standard deviation (coefficient of variation) of the
97
+ projected time-of-flight above which values are masked.
98
+ """
99
+
100
+ PulsePeriod = NewType("PulsePeriod", sc.Variable)
101
+ """
102
+ Period of the source pulses, i.e., time between consecutive pulse starts.
103
+ """
104
+
105
+ PulseStride = NewType("PulseStride", int)
106
+ """
107
+ Stride of used pulses. Usually 1, but may be a small integer when pulse-skipping.
108
+ """
109
+
110
+ SourcePosition = NewType("SourcePosition", sc.Variable)
111
+ """
112
+ Position of the neutron source in the coordinate system of the choppers.
113
+ """
114
+
115
+ SimulationSeed = NewType("SimulationSeed", int | None)
116
+ """Seed for the random number generator used in the simulation.
117
+ """
118
+
119
+ SimulationFacility = NewType("SimulationFacility", str)
120
+ """
121
+ Facility where the experiment is performed, e.g., 'ess'.
122
+ """
123
+
124
+
125
+ def _mask_large_uncertainty(table: sc.DataArray, error_threshold: float):
126
+ """
127
+ Mask regions with large uncertainty with NaNs.
128
+ The values are modified in place in the input table.
129
+
130
+ Parameters
131
+ ----------
132
+ table:
133
+ Lookup table with time-of-flight as a function of distance and time-of-arrival.
134
+ error_threshold:
135
+ Threshold for the relative standard deviation (coefficient of variation) of the
136
+ projected time-of-flight above which values are masked.
137
+ """
138
+ # Finally, mask regions with large uncertainty with NaNs.
139
+ relative_error = sc.stddevs(table.data) / sc.values(table.data)
140
+ mask = relative_error > sc.scalar(error_threshold)
141
+ # Use numpy for indexing as table is 2D
142
+ table.values[mask.values] = np.nan
143
+
144
+
145
+ def _compute_mean_tof_in_distance_range(
146
+ simulation: SimulationResults,
147
+ distance_bins: sc.Variable,
148
+ time_bins: sc.Variable,
149
+ distance_unit: str,
150
+ time_unit: str,
151
+ frame_period: sc.Variable,
152
+ time_bins_half_width: sc.Variable,
153
+ ) -> sc.DataArray:
154
+ """
155
+ Compute the mean time-of-flight inside event_time_offset bins for a given range of
156
+ distances.
157
+
158
+ Parameters
159
+ ----------
160
+ simulation:
161
+ Results of a time-of-flight simulation used to create a lookup table.
162
+ distance_bins:
163
+ Bin edges for the distance axis in the lookup table.
164
+ time_bins:
165
+ Bin edges for the event_time_offset axis in the lookup table.
166
+ distance_unit:
167
+ Unit of the distance axis.
168
+ time_unit:
169
+ Unit of the event_time_offset axis.
170
+ frame_period:
171
+ Period of the source pulses, i.e., time between consecutive pulse starts.
172
+ time_bins_half_width:
173
+ Half width of the time bins in the event_time_offset axis.
174
+ """
175
+ simulation_distance = simulation.distance.to(unit=distance_unit)
176
+ distances = sc.midpoints(distance_bins)
177
+ # Compute arrival and flight times for all neutrons
178
+ toas = simulation.time_of_arrival + (distances / simulation.speed).to(
179
+ unit=time_unit, copy=False
180
+ )
181
+ dist = distances + simulation_distance
182
+ tofs = dist * (sc.constants.m_n / sc.constants.h) * simulation.wavelength
183
+
184
+ data = sc.DataArray(
185
+ data=sc.broadcast(simulation.weight, sizes=toas.sizes),
186
+ coords={
187
+ "toa": toas,
188
+ "tof": tofs.to(unit=time_unit, copy=False),
189
+ "distance": dist,
190
+ },
191
+ ).flatten(to="event")
192
+
193
+ # Add the event_time_offset coordinate, wrapped to the frame_period
194
+ data.coords['event_time_offset'] = data.coords['toa'] % frame_period
195
+
196
+ # Because we staggered the mesh by half a bin width, we want the values above
197
+ # the last bin edge to wrap around to the first bin.
198
+ # Technically, those values should end up between -0.5*bin_width and 0, but
199
+ # a simple modulo also works here because even if they end up between 0 and
200
+ # 0.5*bin_width, we are (below) computing the mean between -0.5*bin_width and
201
+ # 0.5*bin_width and it yields the same result.
202
+ # data.coords['event_time_offset'] %= pulse_period - time_bins_half_width
203
+ data.coords['event_time_offset'] %= frame_period - time_bins_half_width
204
+
205
+ binned = data.bin(
206
+ distance=distance_bins + simulation_distance, event_time_offset=time_bins
207
+ )
208
+
209
+ # Weighted mean of tof inside each bin
210
+ mean_tof = (
211
+ binned.bins.data * binned.bins.coords["tof"]
212
+ ).bins.sum() / binned.bins.sum()
213
+ # Compute the variance of the tofs to track regions with large uncertainty
214
+ variance = (
215
+ binned.bins.data * (binned.bins.coords["tof"] - mean_tof) ** 2
216
+ ).bins.sum() / binned.bins.sum()
217
+
218
+ mean_tof.variances = variance.values
219
+ return mean_tof
220
+
221
+
222
+ def make_tof_lookup_table(
223
+ simulation: SimulationResults,
224
+ ltotal_range: LtotalRange,
225
+ distance_resolution: DistanceResolution,
226
+ time_resolution: TimeResolution,
227
+ pulse_period: PulsePeriod,
228
+ pulse_stride: PulseStride,
229
+ error_threshold: LookupTableRelativeErrorThreshold,
230
+ ) -> TimeOfFlightLookupTable:
231
+ """
232
+ Compute a lookup table for time-of-flight as a function of distance and
233
+ time-of-arrival.
234
+
235
+ Parameters
236
+ ----------
237
+ simulation:
238
+ Results of a time-of-flight simulation used to create a lookup table.
239
+ The results should be a flat table with columns for time-of-arrival, speed,
240
+ wavelength, and weight.
241
+ ltotal_range:
242
+ Range of total flight path lengths from the source to the detector.
243
+ distance_resolution:
244
+ Resolution of the distance axis in the lookup table.
245
+ time_resolution:
246
+ Resolution of the time-of-arrival axis in the lookup table. Must be an integer.
247
+ pulse_period:
248
+ Period of the source pulses, i.e., time between consecutive pulse starts.
249
+ pulse_stride:
250
+ Stride of used pulses. Usually 1, but may be a small integer when
251
+ pulse-skipping.
252
+ error_threshold:
253
+ Threshold for the relative standard deviation (coefficient of variation) of the
254
+ projected time-of-flight above which values are masked.
255
+
256
+ Notes
257
+ -----
258
+
259
+ Below are some details about the binning and wrapping around frame period in the
260
+ time dimension.
261
+
262
+ We have some simulated ``toa`` (events) from a Tof/McStas simulation.
263
+ Those are absolute ``toa``, unwrapped.
264
+ First we compute the usual ``event_time_offset = toa % frame_period``.
265
+
266
+ Now, we want to ensure periodic boundaries. If we make a bin centered around 0,
267
+ and a bin centered around 71ms: the first bin will use events between 0 and
268
+ ``0.5 * dt`` (where ``dt`` is the bin width).
269
+ The last bin will use events between ``frame_period - 0.5*dt`` and
270
+ ``frame_period + 0.5 * dt``. So when we compute the mean inside those two bins,
271
+ they will not yield the same results.
272
+ It is as if the first bin is missing the events it should have between
273
+ ``-0.5 * dt`` and 0 (because of the modulo we computed above).
274
+
275
+ To fix this, we do not make a last bin around 71ms (the bins stop at
276
+ ``frame_period - 0.5*dt``). Instead, we compute modulo a second time,
277
+ but this time using ``event_time_offset %= (frame_period - 0.5*dt)``.
278
+ (we cannot directly do ``event_time_offset = toa % (frame_period - 0.5*dt)`` in a
279
+ single step because it would introduce a gradual shift,
280
+ as the pulse number increases).
281
+
282
+ This second modulo effectively takes all the events that would have gone in the
283
+ last bin (between ``frame_period - 0.5*dt`` and ``frame_period``) and puts them in
284
+ the first bin. Instead of placing them between ``-0.5*dt`` and 0,
285
+ it places them between 0 and ``0.5*dt``, but this does not really matter,
286
+ because we then take the mean inside the first bin.
287
+ Whether the events are on the left or right side of zero does not matter.
288
+
289
+ Finally, we make a copy of the left edge, and append it to the right of the table,
290
+ thus ensuring that the values on the right edge are strictly the same as on the
291
+ left edge.
292
+ """
293
+ distance_unit = "m"
294
+ time_unit = simulation.time_of_arrival.unit
295
+ res = distance_resolution.to(unit=distance_unit)
296
+ pulse_period = pulse_period.to(unit=time_unit)
297
+ frame_period = pulse_period * pulse_stride
298
+
299
+ min_dist, max_dist = (
300
+ x.to(unit=distance_unit) - simulation.distance.to(unit=distance_unit)
301
+ for x in ltotal_range
302
+ )
303
+ # We need to bin the data below, to compute the weighted mean of the wavelength.
304
+ # This results in data with bin edges.
305
+ # However, the 2d interpolator expects bin centers.
306
+ # We want to give the 2d interpolator a table that covers the requested range,
307
+ # hence we need to extend the range by at least half a resolution in each direction.
308
+ # Then, we make the choice that the resolution in distance is the quantity that
309
+ # should be preserved. Because the difference between min and max distance is
310
+ # not necessarily an integer multiple of the resolution, we need to add a pad to
311
+ # ensure that the last bin is not cut off. We want the upper edge to be higher than
312
+ # the maximum distance, hence we pad with an additional 1.5 x resolution.
313
+ pad = 2.0 * res
314
+ distance_bins = sc.arange('distance', min_dist - pad, max_dist + pad, res)
315
+
316
+ # Create some time bins for event_time_offset.
317
+ # We want our final table to strictly cover the range [0, frame_period].
318
+ # However, binning the data associates mean values inside the bins to the bin
319
+ # centers. Instead, we stagger the mesh by half a bin width so we are computing
320
+ # values for the final mesh edges (the bilinear interpolation needs values on the
321
+ # edges/corners).
322
+ nbins = int(frame_period / time_resolution.to(unit=time_unit)) + 1
323
+ time_bins = sc.linspace(
324
+ 'event_time_offset', 0.0, frame_period.value, nbins + 1, unit=pulse_period.unit
325
+ )
326
+ time_bins_half_width = 0.5 * (time_bins[1] - time_bins[0])
327
+ time_bins -= time_bins_half_width
328
+
329
+ # To avoid a too large RAM usage, we compute the table in chunks, and piece them
330
+ # together at the end.
331
+ ndist = len(distance_bins) - 1
332
+ max_size = 2e7
333
+ total_size = ndist * len(simulation.time_of_arrival)
334
+ nchunks = math.ceil(total_size / max_size)
335
+ chunk_size = math.ceil(ndist / nchunks)
336
+ pieces = []
337
+ for i in range(nchunks):
338
+ dist_edges = distance_bins[i * chunk_size : (i + 1) * chunk_size + 1]
339
+ pieces.append(
340
+ _compute_mean_tof_in_distance_range(
341
+ simulation=simulation,
342
+ distance_bins=dist_edges,
343
+ time_bins=time_bins,
344
+ distance_unit=distance_unit,
345
+ time_unit=time_unit,
346
+ frame_period=frame_period,
347
+ time_bins_half_width=time_bins_half_width,
348
+ )
349
+ )
350
+
351
+ table = sc.concat(pieces, 'distance')
352
+ table.coords["distance"] = sc.midpoints(table.coords["distance"])
353
+ table.coords["event_time_offset"] = sc.midpoints(table.coords["event_time_offset"])
354
+
355
+ # Copy the left edge to the right to create periodic boundary conditions
356
+ table = sc.DataArray(
357
+ data=sc.concat(
358
+ [table.data, table.data['event_time_offset', 0]], dim='event_time_offset'
359
+ ),
360
+ coords={
361
+ "distance": table.coords["distance"],
362
+ "event_time_offset": sc.concat(
363
+ [table.coords["event_time_offset"], frame_period],
364
+ dim='event_time_offset',
365
+ ),
366
+ "pulse_period": pulse_period,
367
+ "pulse_stride": sc.scalar(pulse_stride, unit=None),
368
+ "distance_resolution": table.coords["distance"][1]
369
+ - table.coords["distance"][0],
370
+ "time_resolution": table.coords["event_time_offset"][1]
371
+ - table.coords["event_time_offset"][0],
372
+ "error_threshold": sc.scalar(error_threshold),
373
+ },
374
+ )
375
+
376
+ # In-place masking for better performance
377
+ _mask_large_uncertainty(table, error_threshold)
378
+
379
+ return TimeOfFlightLookupTable(table)
380
+
381
+
382
+ def simulate_chopper_cascade_using_tof(
383
+ choppers: DiskChoppers,
384
+ source_position: SourcePosition,
385
+ neutrons: NumberOfSimulatedNeutrons,
386
+ pulse_stride: PulseStride,
387
+ seed: SimulationSeed,
388
+ facility: SimulationFacility,
389
+ ) -> SimulationResults:
390
+ """
391
+ Simulate a pulse of neutrons propagating through a chopper cascade using the
392
+ ``tof`` package (https://tof.readthedocs.io).
393
+
394
+ Parameters
395
+ ----------
396
+ choppers:
397
+ A dict of DiskChopper objects representing the choppers in the beamline. See
398
+ https://scipp.github.io/scippneutron/user-guide/chopper/processing-nexus-choppers.html#Build-DiskChopper
399
+ for more information.
400
+ source_position:
401
+ A scalar variable with ``dtype=vector3`` that defines the source position.
402
+ Must be in the same coordinate system as the choppers' axle positions.
403
+ neutrons:
404
+ Number of neutrons to simulate.
405
+ pulse_stride:
406
+ The pulse strinde; we need to simulate at least enough pulses to cover the
407
+ requested stride.
408
+ seed:
409
+ Seed for the random number generator used in the simulation.
410
+ facility:
411
+ Facility where the experiment is performed.
412
+ """
413
+ import tof
414
+
415
+ tof_choppers = [
416
+ tof.Chopper(
417
+ frequency=abs(ch.frequency),
418
+ direction=tof.AntiClockwise
419
+ if (ch.frequency.value > 0.0)
420
+ else tof.Clockwise,
421
+ open=ch.slit_begin,
422
+ close=ch.slit_end,
423
+ phase=abs(ch.phase),
424
+ distance=sc.norm(
425
+ ch.axle_position - source_position.to(unit=ch.axle_position.unit)
426
+ ),
427
+ name=name,
428
+ )
429
+ for name, ch in choppers.items()
430
+ ]
431
+ source = tof.Source(
432
+ facility=facility, neutrons=neutrons, pulses=pulse_stride, seed=seed
433
+ )
434
+ if not tof_choppers:
435
+ events = source.data.squeeze().flatten(to='event')
436
+ return SimulationResults(
437
+ time_of_arrival=events.coords["birth_time"],
438
+ speed=events.coords["speed"],
439
+ wavelength=events.coords["wavelength"],
440
+ weight=events.data,
441
+ distance=0.0 * sc.units.m,
442
+ )
443
+ model = tof.Model(source=source, choppers=tof_choppers)
444
+ results = model.run()
445
+ # Find name of the furthest chopper in tof_choppers
446
+ furthest_chopper = max(tof_choppers, key=lambda c: c.distance)
447
+ events = results[furthest_chopper.name].data.squeeze().flatten(to='event')
448
+ events = events[
449
+ ~(events.masks["blocked_by_others"] | events.masks["blocked_by_me"])
450
+ ]
451
+ return SimulationResults(
452
+ time_of_arrival=events.coords["toa"],
453
+ speed=events.coords["speed"],
454
+ wavelength=events.coords["wavelength"],
455
+ weight=events.data,
456
+ distance=furthest_chopper.distance,
457
+ )
458
+
459
+
460
+ def TofLookupTableWorkflow():
461
+ """
462
+ Create a workflow for computing a time-of-flight lookup table from a
463
+ simulation of neutrons propagating through a chopper cascade.
464
+ """
465
+ wf = sl.Pipeline(
466
+ (make_tof_lookup_table, simulate_chopper_cascade_using_tof),
467
+ params={
468
+ PulsePeriod: 1.0 / sc.scalar(14.0, unit="Hz"),
469
+ PulseStride: 1,
470
+ DistanceResolution: sc.scalar(0.1, unit="m"),
471
+ TimeResolution: sc.scalar(250.0, unit='us'),
472
+ LookupTableRelativeErrorThreshold: 0.1,
473
+ NumberOfSimulatedNeutrons: 1_000_000,
474
+ SimulationSeed: None,
475
+ SimulationFacility: 'ess',
476
+ },
477
+ )
478
+ return wf
@@ -1,7 +1,6 @@
1
1
  # SPDX-License-Identifier: BSD-3-Clause
2
2
  # Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3
3
 
4
- from dataclasses import dataclass
5
4
  from typing import NewType
6
5
 
7
6
  import sciline as sl
@@ -9,81 +8,6 @@ import scipp as sc
9
8
 
10
9
  from ..nexus.types import MonitorType, RunType
11
10
 
12
-
13
- @dataclass
14
- class SimulationResults:
15
- """
16
- Results of a time-of-flight simulation used to create a lookup table.
17
-
18
- The results (apart from ``distance``) should be flat lists (1d arrays) of length N
19
- where N is the number of neutrons, containing the properties of the neutrons in the
20
- simulation.
21
-
22
- Parameters
23
- ----------
24
- time_of_arrival:
25
- Time of arrival of the neutrons at the position where the events were recorded
26
- (1d array of size N).
27
- speed:
28
- Speed of the neutrons, typically derived from the wavelength of the neutrons
29
- (1d array of size N).
30
- wavelength:
31
- Wavelength of the neutrons (1d array of size N).
32
- weight:
33
- Weight/probability of the neutrons (1d array of size N).
34
- distance:
35
- Distance from the source to the position where the events were recorded
36
- (single value; we assume all neutrons were recorded at the same position).
37
- For a ``tof`` simulation, this is just the position of the detector where the
38
- events are recorded. For a ``McStas`` simulation, this is the distance between
39
- the source and the event monitor.
40
- """
41
-
42
- time_of_arrival: sc.Variable
43
- speed: sc.Variable
44
- wavelength: sc.Variable
45
- weight: sc.Variable
46
- distance: sc.Variable
47
-
48
-
49
- NumberOfSimulatedNeutrons = NewType("NumberOfSimulatedNeutrons", int)
50
- """
51
- Number of neutrons simulated in the simulation that is used to create the lookup table.
52
- This is typically a large number, e.g., 1e6 or 1e7.
53
- """
54
-
55
- LtotalRange = NewType("LtotalRange", tuple[sc.Variable, sc.Variable])
56
- """
57
- Range (min, max) of the total length of the flight path from the source to the detector.
58
- This is used to create the lookup table to compute the neutron time-of-flight.
59
- Note that the resulting table will extend slightly beyond this range, as the supplied
60
- range is not necessarily a multiple of the distance resolution.
61
-
62
- Note also that the range of total flight paths is supplied manually to the workflow
63
- instead of being read from the input data, as it allows us to compute the expensive part
64
- of the workflow in advance (the lookup table) and does not need to be repeated for each
65
- run, or for new data coming in in the case of live data collection.
66
- """
67
-
68
- DistanceResolution = NewType("DistanceResolution", sc.Variable)
69
- """
70
- Step size of the distance axis in the lookup table.
71
- Should be a single scalar value with a unit of length.
72
- This is typically of the order of 1-10 cm.
73
- """
74
-
75
- TimeResolution = NewType("TimeResolution", sc.Variable)
76
- """
77
- Step size of the event_time_offset axis in the lookup table.
78
- This is basically the 'time-of-flight' resolution of the detector.
79
- Should be a single scalar value with a unit of time.
80
- This is typically of the order of 0.1-0.5 ms.
81
-
82
- Since the event_time_offset range needs to span exactly one pulse period, the final
83
- resolution in the lookup table will be at least the supplied value here, but may be
84
- smaller if the pulse period is not an integer multiple of the time resolution.
85
- """
86
-
87
11
  TimeOfFlightLookupTableFilename = NewType("TimeOfFlightLookupTableFilename", str)
88
12
  """Filename of the time-of-flight lookup table."""
89
13
 
@@ -93,21 +17,6 @@ TimeOfFlightLookupTable = NewType("TimeOfFlightLookupTable", sc.DataArray)
93
17
  Lookup table giving time-of-flight as a function of distance and time of arrival.
94
18
  """
95
19
 
96
- LookupTableRelativeErrorThreshold = NewType("LookupTableRelativeErrorThreshold", float)
97
- """
98
- Threshold for the relative standard deviation (coefficient of variation) of the
99
- projected time-of-flight above which values are masked.
100
- """
101
-
102
- PulsePeriod = NewType("PulsePeriod", sc.Variable)
103
- """
104
- Period of the source pulses, i.e., time between consecutive pulse starts.
105
- """
106
-
107
- PulseStride = NewType("PulseStride", int)
108
- """
109
- Stride of used pulses. Usually 1, but may be a small integer when pulse-skipping.
110
- """
111
20
 
112
21
  PulseStrideOffset = NewType("PulseStrideOffset", int | None)
113
22
  """
@@ -1,22 +1,17 @@
1
1
  # SPDX-License-Identifier: BSD-3-Clause
2
2
  # Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3
3
  from collections.abc import Iterable
4
- from enum import Enum, auto
5
4
 
6
5
  import sciline
7
6
  import scipp as sc
8
7
 
9
8
  from ..nexus import GenericNeXusWorkflow
10
- from . import eto_to_tof, simulation
11
- from .types import TimeOfFlightLookupTable, TimeOfFlightLookupTableFilename
12
-
13
-
14
- class TofLutProvider(Enum):
15
- """Provider for the time-of-flight lookup table."""
16
-
17
- FILE = auto() # From file
18
- TOF = auto() # Computed with 'tof' package from chopper settings
19
- MCSTAS = auto() # McStas simulation (not implemented yet)
9
+ from . import eto_to_tof
10
+ from .types import (
11
+ PulseStrideOffset,
12
+ TimeOfFlightLookupTable,
13
+ TimeOfFlightLookupTableFilename,
14
+ )
20
15
 
21
16
 
22
17
  def load_tof_lookup_table(
@@ -29,7 +24,6 @@ def GenericTofWorkflow(
29
24
  *,
30
25
  run_types: Iterable[sciline.typing.Key],
31
26
  monitor_types: Iterable[sciline.typing.Key],
32
- tof_lut_provider: TofLutProvider = TofLutProvider.FILE,
33
27
  ) -> sciline.Pipeline:
34
28
  """
35
29
  Generic workflow for computing the neutron time-of-flight for detector and monitor
@@ -59,11 +53,6 @@ def GenericTofWorkflow(
59
53
  List of monitor types to include in the workflow.
60
54
  Constrains the possible values of :class:`ess.reduce.nexus.types.MonitorType`
61
55
  and :class:`ess.reduce.nexus.types.Component`.
62
- tof_lut_provider:
63
- Specifies how the time-of-flight lookup table is provided:
64
- - FILE: Read from a file
65
- - TOF: Computed from chopper settings using the 'tof' package
66
- - MCSTAS: From McStas simulation (not implemented yet)
67
56
 
68
57
  Returns
69
58
  -------
@@ -75,16 +64,9 @@ def GenericTofWorkflow(
75
64
  for provider in eto_to_tof.providers():
76
65
  wf.insert(provider)
77
66
 
78
- if tof_lut_provider == TofLutProvider.FILE:
79
- wf.insert(load_tof_lookup_table)
80
- else:
81
- wf.insert(eto_to_tof.compute_tof_lookup_table)
82
- if tof_lut_provider == TofLutProvider.TOF:
83
- wf.insert(simulation.simulate_chopper_cascade_using_tof)
84
- if tof_lut_provider == TofLutProvider.MCSTAS:
85
- raise NotImplementedError("McStas simulation not implemented yet")
67
+ wf.insert(load_tof_lookup_table)
86
68
 
87
- for key, value in eto_to_tof.default_parameters().items():
88
- wf[key] = value
69
+ # Default parameters
70
+ wf[PulseStrideOffset] = None
89
71
 
90
72
  return wf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: essreduce
3
- Version: 25.5.3
3
+ Version: 25.7.1
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License: BSD 3-Clause License
@@ -18,15 +18,15 @@ ess/reduce/nexus/json_nexus.py,sha256=QrVc0p424nZ5dHX9gebAJppTw6lGZq9404P_OFl1gi
18
18
  ess/reduce/nexus/types.py,sha256=DE82JnbgpTlQnt7UN2a2Gur2N9QupV3CDL9j4Iy4lsE,9178
19
19
  ess/reduce/nexus/workflow.py,sha256=Ytt80-muk5EiXmip890ahb_m5DQqlTGRQUyaTVXRNzo,24568
20
20
  ess/reduce/scripts/grow_nexus.py,sha256=hET3h06M0xlJd62E3palNLFvJMyNax2kK4XyJcOhl-I,3387
21
- ess/reduce/time_of_flight/__init__.py,sha256=jbvLcVgODURuweuicrsDqEYqiL_GNJa_t4c5ik344Ro,1269
22
- ess/reduce/time_of_flight/eto_to_tof.py,sha256=FYujFQSqDoxOLiVbNId4YcpuhKmNdvtBMUr9nK5poIk,26070
21
+ ess/reduce/time_of_flight/__init__.py,sha256=bhteT_xvkRYBh_oeJZTzCtiPg0WSW_TRrtm_xzGbzw4,1441
22
+ ess/reduce/time_of_flight/eto_to_tof.py,sha256=pkxtj1Gg0b2aF9Ryr2vL8PeL8RdujuzWbcp1L4TMghU,14712
23
23
  ess/reduce/time_of_flight/fakes.py,sha256=0gtbSX3ZQilaM4ZP5dMr3fqbnhpyoVsZX2YEb8GgREE,4489
24
24
  ess/reduce/time_of_flight/interpolator_numba.py,sha256=wh2YS3j2rOu30v1Ok3xNHcwS7t8eEtZyZvbfXOCtgrQ,3835
25
25
  ess/reduce/time_of_flight/interpolator_scipy.py,sha256=_InoAPuMm2qhJKZQBAHOGRFqtvvuQ8TStoN7j_YgS4M,1853
26
+ ess/reduce/time_of_flight/lut.py,sha256=VlygrRd9DK_jiz2uW506kHUzu6MweWhhZXJTu7qw-_M,18734
26
27
  ess/reduce/time_of_flight/resample.py,sha256=Opmi-JA4zNH725l9VB99U4O9UlM37f5ACTCGtwBcows,3718
27
- ess/reduce/time_of_flight/simulation.py,sha256=vo-zjG6t-PLetv2_nj4dhMSTEyTQ1MsrhlM2XkhOtf8,3632
28
- ess/reduce/time_of_flight/types.py,sha256=LJlK_5u5yeFj2TLnz-LI3eApkp8vEg5q8OncHkmHjj8,4844
29
- ess/reduce/time_of_flight/workflow.py,sha256=BAIMeA1bSJlS6JSG7r2srVdtBsAK6VD0DuOiYZuQvNg,3182
28
+ ess/reduce/time_of_flight/types.py,sha256=EroKBxi4WUErNx8d200jh8kqkhwtjAGKIV7PvBUwAJs,1310
29
+ ess/reduce/time_of_flight/workflow.py,sha256=mkgESvQ5Yt3CyAsa1iewkjBOHUqrHm5rRc1EhOQRewQ,2213
30
30
  ess/reduce/widgets/__init__.py,sha256=SoSHBv8Dc3QXV9HUvPhjSYWMwKTGYZLpsWwsShIO97Q,5325
31
31
  ess/reduce/widgets/_base.py,sha256=_wN3FOlXgx_u0c-A_3yyoIH-SdUvDENGgquh9S-h5GI,4852
32
32
  ess/reduce/widgets/_binedges_widget.py,sha256=ZCQsGjYHnJr9GFUn7NjoZc1CdsnAzm_fMzyF-fTKKVY,2785
@@ -39,9 +39,9 @@ ess/reduce/widgets/_spinner.py,sha256=2VY4Fhfa7HMXox2O7UbofcdKsYG-AJGrsgGJB85nDX
39
39
  ess/reduce/widgets/_string_widget.py,sha256=iPAdfANyXHf-nkfhgkyH6gQDklia0LebLTmwi3m-iYQ,1482
40
40
  ess/reduce/widgets/_switchable_widget.py,sha256=fjKz99SKLhIF1BLgGVBSKKn3Lu_jYBwDYGeAjbJY3Q8,2390
41
41
  ess/reduce/widgets/_vector_widget.py,sha256=aTaBqCFHZQhrIoX6-sSqFWCPePEW8HQt5kUio8jP1t8,1203
42
- essreduce-25.5.3.dist-info/licenses/LICENSE,sha256=nVEiume4Qj6jMYfSRjHTM2jtJ4FGu0g-5Sdh7osfEYw,1553
43
- essreduce-25.5.3.dist-info/METADATA,sha256=WNGTtuRJz8G7G3mFV0sWWQy5ByR1uzJ3iDQi7hVyYps,3768
44
- essreduce-25.5.3.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
45
- essreduce-25.5.3.dist-info/entry_points.txt,sha256=PMZOIYzCifHMTe4pK3HbhxUwxjFaZizYlLD0td4Isb0,66
46
- essreduce-25.5.3.dist-info/top_level.txt,sha256=0JxTCgMKPLKtp14wb1-RKisQPQWX7i96innZNvHBr-s,4
47
- essreduce-25.5.3.dist-info/RECORD,,
42
+ essreduce-25.7.1.dist-info/licenses/LICENSE,sha256=nVEiume4Qj6jMYfSRjHTM2jtJ4FGu0g-5Sdh7osfEYw,1553
43
+ essreduce-25.7.1.dist-info/METADATA,sha256=wJNhY-mZpbNQwbSdKA1Ju0gTYSAU62tivdyuBmgG5Pc,3768
44
+ essreduce-25.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
+ essreduce-25.7.1.dist-info/entry_points.txt,sha256=PMZOIYzCifHMTe4pK3HbhxUwxjFaZizYlLD0td4Isb0,66
46
+ essreduce-25.7.1.dist-info/top_level.txt,sha256=0JxTCgMKPLKtp14wb1-RKisQPQWX7i96innZNvHBr-s,4
47
+ essreduce-25.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,108 +0,0 @@
1
- # SPDX-License-Identifier: BSD-3-Clause
2
- # Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3
- from collections.abc import Mapping
4
-
5
- import scipp as sc
6
- import scippnexus as snx
7
- from scippneutron.chopper import DiskChopper
8
-
9
- from ..nexus.types import DiskChoppers, Position, SampleRun
10
- from .types import NumberOfSimulatedNeutrons, SimulationResults
11
-
12
-
13
- def simulate_beamline(
14
- *,
15
- choppers: Mapping[str, DiskChopper],
16
- source_position: sc.Variable,
17
- neutrons: int = 1_000_000,
18
- pulses: int = 1,
19
- seed: int | None = None,
20
- facility: str = 'ess',
21
- ) -> SimulationResults:
22
- """
23
- Simulate a pulse of neutrons propagating through a chopper cascade using the
24
- ``tof`` package (https://tof.readthedocs.io).
25
-
26
- Parameters
27
- ----------
28
- choppers:
29
- A dict of DiskChopper objects representing the choppers in the beamline. See
30
- https://scipp.github.io/scippneutron/user-guide/chopper/processing-nexus-choppers.html#Build-DiskChopper
31
- for more information.
32
- source_position:
33
- A scalar variable with ``dtype=vector3`` that defines the source position.
34
- Must be in the same coordinate system as the choppers' axle positions.
35
- neutrons:
36
- Number of neutrons to simulate.
37
- pulses:
38
- Number of pulses to simulate.
39
- seed:
40
- Seed for the random number generator used in the simulation.
41
- facility:
42
- Facility where the experiment is performed.
43
- """
44
- import tof
45
-
46
- tof_choppers = [
47
- tof.Chopper(
48
- frequency=abs(ch.frequency),
49
- direction=tof.AntiClockwise
50
- if (ch.frequency.value > 0.0)
51
- else tof.Clockwise,
52
- open=ch.slit_begin,
53
- close=ch.slit_end,
54
- phase=abs(ch.phase),
55
- distance=sc.norm(
56
- ch.axle_position - source_position.to(unit=ch.axle_position.unit)
57
- ),
58
- name=name,
59
- )
60
- for name, ch in choppers.items()
61
- ]
62
- source = tof.Source(facility=facility, neutrons=neutrons, pulses=pulses, seed=seed)
63
- if not tof_choppers:
64
- events = source.data.squeeze().flatten(to='event')
65
- return SimulationResults(
66
- time_of_arrival=events.coords["birth_time"],
67
- speed=events.coords["speed"],
68
- wavelength=events.coords["wavelength"],
69
- weight=events.data,
70
- distance=0.0 * sc.units.m,
71
- )
72
- model = tof.Model(source=source, choppers=tof_choppers)
73
- results = model.run()
74
- # Find name of the furthest chopper in tof_choppers
75
- furthest_chopper = max(tof_choppers, key=lambda c: c.distance)
76
- events = results[furthest_chopper.name].data.squeeze().flatten(to='event')
77
- events = events[
78
- ~(events.masks["blocked_by_others"] | events.masks["blocked_by_me"])
79
- ]
80
- return SimulationResults(
81
- time_of_arrival=events.coords["toa"],
82
- speed=events.coords["speed"],
83
- wavelength=events.coords["wavelength"],
84
- weight=events.data,
85
- distance=furthest_chopper.distance,
86
- )
87
-
88
-
89
- def simulate_chopper_cascade_using_tof(
90
- choppers: DiskChoppers[SampleRun],
91
- neutrons: NumberOfSimulatedNeutrons,
92
- source_position: Position[snx.NXsource, SampleRun],
93
- ) -> SimulationResults:
94
- """
95
- Simulate neutrons traveling through the chopper cascade using the ``tof`` package.
96
-
97
- Parameters
98
- ----------
99
- choppers:
100
- Chopper settings.
101
- neutrons:
102
- Number of neutrons to simulate.
103
- source_position:
104
- Position of the source.
105
- """
106
- return simulate_beamline(
107
- choppers=choppers, neutrons=neutrons, source_position=source_position
108
- )