Chopper Cascades#

Overview#

For the purpose of frame unwrapping as well as techniques such as wavelength-frame multiplication (WFM), we need to compute bounds of neutron arrival times. This can be computed from the source pulse structure, the chopper configuration, and the instrument geometry.

The chopper_cascade module provides utilities for this purpose. It is currently under development and not fully functional.

Example: WFM chopper cascade#

As an example, consider the WFM chopper cascade from the tof package documentation. The choppers are defined in the fakes module of the tof package:

[1]:
from scippneutron.tof import chopper_cascade, fakes
import scipp as sc
import matplotlib as mpl

mpl.rcParams['figure.dpi'] = 300
choppers = fakes.wfm_choppers

We can now initialize a frame-sequence and apply the chopper cascade to it:

[2]:
frames = chopper_cascade.FrameSequence.from_source_pulse(
    time_min=sc.scalar(0.0, unit='ms'),
    time_max=sc.scalar(4.0, unit='ms'),  # ESS pulse is 3 ms, but it has a tail
    wavelength_min=sc.scalar(0.0, unit='angstrom'),
    wavelength_max=sc.scalar(10.0, unit='angstrom'),
)
frames = frames.chop(choppers.values())
at_sample = frames.propagate_to(sc.scalar(26.0, unit='m'))

at_sample.draw()
[2]:
(<Figure size 1920x1440 with 1 Axes>,
 <Axes: title={'center': 'Frame propagation through chopper cascade'}, xlabel='ms', ylabel='Å'>)
../../_images/user-guide_chopper_chopper-cascade_4_1.png

We can also draw a chopper acceptance diagram, which is essentially the same as above, but propagated back to the source pulse distance:

[3]:
frames.acceptance_diagram()
[3]:
(<Figure size 1920x1440 with 1 Axes>,
 <Axes: title={'center': 'Chopper acceptance diagram'}, xlabel='Å', ylabel='ms'>)
../../_images/user-guide_chopper_chopper-cascade_6_1.png

Frame unwrapping#

For unwrapping frames, we need the bounds of the entire from, to determine times at which to cut. Since \(L_2\) can be different for every detector, this cutting time is different for every detector. We can compute the frame bounds at a common distance, e.g., the sample, and propagate the bounds to the detectors:

[4]:
bounds = at_sample[-1].bounds()
chopper_cascade.propagate_times(
    time=bounds['time'],
    wavelength=bounds['wavelength'],
    distance=sc.linspace('L2', 1.0, 2.0, 100, unit='m'),
)
[4]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (1.81 KB)
    • (L2: 100, bound: 2)
      float64
      s
      0.017, 0.065, ..., 0.017, 0.067
      Values:
      array([[0.01656897, 0.06503264], [0.01657409, 0.06505697], [0.01657921, 0.0650813 ], [0.01658434, 0.06510563], [0.01658946, 0.06512996], [0.01659458, 0.06515429], [0.01659971, 0.06517862], [0.01660483, 0.06520295], [0.01660995, 0.06522728], [0.01661508, 0.06525161], [0.0166202 , 0.06527594], [0.01662532, 0.06530027], [0.01663045, 0.0653246 ], [0.01663557, 0.06534892], [0.01664069, 0.06537325], [0.01664582, 0.06539758], [0.01665094, 0.06542191], [0.01665606, 0.06544624], [0.01666119, 0.06547057], [0.01666631, 0.0654949 ], [0.01667143, 0.06551923], [0.01667656, 0.06554356], [0.01668168, 0.06556789], [0.0166868 , 0.06559222], [0.01669193, 0.06561655], [0.01669705, 0.06564088], [0.01670217, 0.06566521], [0.0167073 , 0.06568954], [0.01671242, 0.06571387], [0.01671754, 0.0657382 ], [0.01672267, 0.06576253], [0.01672779, 0.06578685], [0.01673291, 0.06581118], [0.01673804, 0.06583551], [0.01674316, 0.06585984], [0.01674828, 0.06588417], [0.01675341, 0.0659085 ], [0.01675853, 0.06593283], [0.01676365, 0.06595716], [0.01676878, 0.06598149], [0.0167739 , 0.06600582], [0.01677902, 0.06603015], [0.01678415, 0.06605448], [0.01678927, 0.06607881], [0.01679439, 0.06610314], [0.01679952, 0.06612747], [0.01680464, 0.0661518 ], [0.01680976, 0.06617613], [0.01681488, 0.06620046], [0.01682001, 0.06622478], [0.01682513, 0.06624911], [0.01683025, 0.06627344], [0.01683538, 0.06629777], [0.0168405 , 0.0663221 ], [0.01684562, 0.06634643], [0.01685075, 0.06637076], [0.01685587, 0.06639509], [0.01686099, 0.06641942], [0.01686612, 0.06644375], [0.01687124, 0.06646808], [0.01687636, 0.06649241], [0.01688149, 0.06651674], [0.01688661, 0.06654107], [0.01689173, 0.0665654 ], [0.01689686, 0.06658973], [0.01690198, 0.06661406], [0.0169071 , 0.06663839], [0.01691223, 0.06666272], [0.01691735, 0.06668704], [0.01692247, 0.06671137], [0.0169276 , 0.0667357 ], [0.01693272, 0.06676003], [0.01693784, 0.06678436], [0.01694297, 0.06680869], [0.01694809, 0.06683302], [0.01695321, 0.06685735], [0.01695834, 0.06688168], [0.01696346, 0.06690601], [0.01696858, 0.06693034], [0.01697371, 0.06695467], [0.01697883, 0.066979 ], [0.01698395, 0.06700333], [0.01698908, 0.06702766], [0.0169942 , 0.06705199], [0.01699932, 0.06707632], [0.01700445, 0.06710065], [0.01700957, 0.06712497], [0.01701469, 0.0671493 ], [0.01701982, 0.06717363], [0.01702494, 0.06719796], [0.01703006, 0.06722229], [0.01703519, 0.06724662], [0.01704031, 0.06727095], [0.01704543, 0.06729528], [0.01705056, 0.06731961], [0.01705568, 0.06734394], [0.0170608 , 0.06736827], [0.01706593, 0.0673926 ], [0.01707105, 0.06741693], [0.01707617, 0.06744126]])

For WFM, we need to compute subframe time cutting points. Again, \(L_2\) can be different for every detector, so we need to compute the cutting points for every detector:

[5]:
bounds = at_sample[-1].subbounds()
bounds
[5]:
  • time
    scipp
    Variable
    (subframe: 6, bound: 2)
    float64
    s
    0.016, 0.023, ..., 0.056, 0.063
  • wavelength
    scipp
    Variable
    (subframe: 6, bound: 2)
    float64
    Å
    2.007, 3.514, ..., 7.937, 9.529
[6]:
chopper_cascade.propagate_times(
    time=bounds['time'],
    wavelength=bounds['wavelength'],
    distance=sc.linspace('L2', 1.0, 2.0, 100, unit='m'),
)
[6]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (9.62 KB)
    • (L2: 100, subframe: 6, bound: 2)
      float64
      s
      0.017, 0.024, ..., 0.060, 0.067
      Values:
      array([[[0.01656897, 0.02398398], [0.02586316, 0.03408453], [0.0346639 , 0.04199434], [0.04247192, 0.04937471], [0.05023934, 0.05649482], [0.05767942, 0.06503264]], [[0.01657409, 0.02399295], [0.02587169, 0.03409728], [0.03467567, 0.04201005], [0.04248653, 0.04939318], [0.05025696, 0.05651591], [0.05769969, 0.06505697]], [[0.01657921, 0.02400192], [0.02588021, 0.03411003], [0.03468744, 0.04202576], [0.04250114, 0.04941164], [0.05027457, 0.056537 ], [0.05771995, 0.0650813 ]], ..., [[0.01706593, 0.02485433], [0.02669009, 0.03532142], [0.03580532, 0.04351826], [0.04388917, 0.05116595], [0.05194819, 0.05854081], [0.05964523, 0.0673926 ]], [[0.01707105, 0.0248633 ], [0.02669861, 0.03533417], [0.03581708, 0.04353397], [0.04390378, 0.05118442], [0.05196581, 0.05856191], [0.0596655 , 0.06741693]], [[0.01707617, 0.02487227], [0.02670714, 0.03534692], [0.03582885, 0.04354969], [0.04391839, 0.05120288], [0.05198342, 0.058583 ], [0.05968576, 0.06744126]]])

Note that subframes may in principle overlap, and this may depend on the detector. We therefore call subframe_bounds at a common location and propagate the result, otherwise we would get a different number of subframes for different detectors.