Processing NeXus Choppers#
When choppers are loaded from NeXus, they typically contain a number of fields that need to be processed before they can be used for computing wavelength ranges, etc. This guide shows how to extract the relevant data from such a NeXus chopper and create a scippneutron.chopper.DiskChopper object.
[1]:
import matplotlib.pyplot as plt
import scipp as sc
Here, we use fake data which roughly represents what a real chopper loaded from NeXus looks like. ScippNeutron has a function for generating this data:
[2]:
from scippneutron.data import chopper_mockup
chopper = chopper_mockup()
chopper
[2]:
- scippDataGroup(time: 1)
- valuescippDataArray(time: 1)int64ns3050
- positionscippVariable()vector3m[ 0. 0. 60.]
- radiusscippVariable()float64m0.35
- scippDataGroup(time: 180)
- valuescippDataArray(time: 180)float64Hz1.287e-05, 0.359, ..., 0.352, -4.904e-07
- slit_heightscippVariable()float64m0.1
- slit_edgesscippVariable(slit: 4)float64deg30.0, 160.0, 210.0, 280.0
- slitsint()2
- scippDataGroup(time: 2354)
- timescippVariable(time: 2354)datetime64ns2023-01-19T08:11:06.217830400, 2023-01-19T08:11:08.799053824, ..., 2023-01-19T08:14:39.174524416, 2023-01-19T08:14:40.640919296
We can see that there is some information about the slits and geometry of the chopper as well as some timing-related data. Take a look at the NXdisk_chopper documentation for an overview of the fields.
In this case, there already is a position
. This typically needs to be computed first, see scippnexus.compute_positions.
Some fields are nested data groups which happens when a NeXus file contains NXlog
s. We can extract the relevant arrays from them using extract_chopper_from_nexus:
[3]:
from scippneutron.chopper import extract_chopper_from_nexus
chopper = extract_chopper_from_nexus(chopper)
chopper
[3]:
- typescippneutronDiskChopperType()DiskChopperType.single
- delayscippDataArray()int64ns3050
- positionscippVariable()vector3m[ 0. 0. 60.]
- radiusscippVariable()float64m0.35
- rotation_speedscippDataArray(time: 180)float64Hz1.287e-05, 0.359, ..., 0.352, -4.904e-07
- slit_heightscippVariable()float64m0.1
- slit_edgesscippVariable(slit: 4)float64deg30.0, 160.0, 210.0, 280.0
- slitsint()2
- top_dead_centerscippVariable(time: 2354)datetime64ns2023-01-19T08:11:06.217830400, 2023-01-19T08:11:08.799053824, ..., 2023-01-19T08:14:39.174524416, 2023-01-19T08:14:40.640919296
Some data varies with time, which can complicate the data processing. Instead, we compute corresponding time-independent quantities from the raw chopper data.
Identify In-phase Regions#
Frame unwrapping is only feasible when the chopper is in-phase with the neutron source pulses because, otherwise, the wavelength frames vary pulse-by-pulse. To identify regions where the chopper is in-phase, we first find plateaus in the rotation_speed
which is the rotation frequency of the chopper.
[4]:
rotation_speed = chopper['rotation_speed']
rotation_speed.name = 'rotation_speed'
rotation_speed
[4]:
- time: 180
- time(time)datetime64ns2023-01-19T08:11:06.205830400, 2023-01-19T08:11:07.412763648, ..., 2023-01-19T08:14:41.039957504, 2023-01-19T08:14:42.246890752
Values:
array(['2023-01-19T08:11:06.205830400', '2023-01-19T08:11:07.412763648', '2023-01-19T08:11:08.619696896', '2023-01-19T08:11:09.826630400', '2023-01-19T08:11:11.033563648', '2023-01-19T08:11:12.240496896', '2023-01-19T08:11:13.447430144', '2023-01-19T08:11:14.654363392', '2023-01-19T08:11:15.861296896', '2023-01-19T08:11:17.068230144', '2023-01-19T08:11:18.275163392', '2023-01-19T08:11:19.482096640', '2023-01-19T08:11:20.689029888', '2023-01-19T08:11:21.895963392', '2023-01-19T08:11:23.102896640', '2023-01-19T08:11:24.309829888', '2023-01-19T08:11:25.516763136', '2023-01-19T08:11:26.723696384', '2023-01-19T08:11:27.930629888', '2023-01-19T08:11:29.137563136', '2023-01-19T08:11:30.344496384', '2023-01-19T08:11:31.551429632', '2023-01-19T08:11:32.758362880', '2023-01-19T08:11:33.965296128', '2023-01-19T08:11:35.172229632', '2023-01-19T08:11:36.379162880', '2023-01-19T08:11:37.586096128', '2023-01-19T08:11:38.793029376', '2023-01-19T08:11:39.999962624', '2023-01-19T08:11:41.206896128', '2023-01-19T08:11:42.413829376', '2023-01-19T08:11:43.620762624', '2023-01-19T08:11:44.827695872', '2023-01-19T08:11:46.034629120', '2023-01-19T08:11:47.241562624', '2023-01-19T08:11:48.448495872', '2023-01-19T08:11:49.655429120', '2023-01-19T08:11:50.862362368', '2023-01-19T08:11:52.069295616', '2023-01-19T08:11:53.276229120', '2023-01-19T08:11:54.483162368', '2023-01-19T08:11:55.690095616', '2023-01-19T08:11:56.897028864', '2023-01-19T08:11:58.103962112', '2023-01-19T08:11:59.310895616', '2023-01-19T08:12:00.517828864', '2023-01-19T08:12:01.724762112', '2023-01-19T08:12:02.931695360', '2023-01-19T08:12:04.138628608', '2023-01-19T08:12:05.345562112', '2023-01-19T08:12:06.552495360', '2023-01-19T08:12:07.759428608', '2023-01-19T08:12:08.966361856', '2023-01-19T08:12:10.173295104', '2023-01-19T08:12:11.380228608', '2023-01-19T08:12:12.587161856', '2023-01-19T08:12:13.794095104', '2023-01-19T08:12:15.001028352', '2023-01-19T08:12:16.207961600', '2023-01-19T08:12:17.414895104', '2023-01-19T08:12:18.621828352', '2023-01-19T08:12:19.828761600', '2023-01-19T08:12:21.035694848', '2023-01-19T08:12:22.242628096', '2023-01-19T08:12:23.449561600', '2023-01-19T08:12:24.656494848', '2023-01-19T08:12:25.863428096', '2023-01-19T08:12:27.070361344', '2023-01-19T08:12:28.277294592', '2023-01-19T08:12:29.484227840', '2023-01-19T08:12:30.691161344', '2023-01-19T08:12:31.898094592', '2023-01-19T08:12:33.105027840', '2023-01-19T08:12:34.311961088', '2023-01-19T08:12:35.518894336', '2023-01-19T08:12:36.725827840', '2023-01-19T08:12:37.932761088', '2023-01-19T08:12:39.139694336', '2023-01-19T08:12:40.346627584', '2023-01-19T08:12:41.553560832', '2023-01-19T08:12:42.760494336', '2023-01-19T08:12:43.967427584', '2023-01-19T08:12:45.174360832', '2023-01-19T08:12:46.381294080', '2023-01-19T08:12:47.588227328', '2023-01-19T08:12:48.795160832', '2023-01-19T08:12:50.002094080', '2023-01-19T08:12:51.209027328', '2023-01-19T08:12:52.415960576', '2023-01-19T08:12:53.622893824', '2023-01-19T08:12:54.829827328', '2023-01-19T08:12:56.036760576', '2023-01-19T08:12:57.243693824', '2023-01-19T08:12:58.450627072', '2023-01-19T08:12:59.657560320', '2023-01-19T08:13:00.864493824', '2023-01-19T08:13:02.071427072', '2023-01-19T08:13:03.278360320', '2023-01-19T08:13:04.485293568', '2023-01-19T08:13:05.692226816', '2023-01-19T08:13:06.899160320', '2023-01-19T08:13:08.106093568', '2023-01-19T08:13:09.313026816', '2023-01-19T08:13:10.519960064', '2023-01-19T08:13:11.726893312', '2023-01-19T08:13:12.933826816', '2023-01-19T08:13:14.140760064', '2023-01-19T08:13:15.347693312', '2023-01-19T08:13:16.554626560', '2023-01-19T08:13:17.761559808', '2023-01-19T08:13:18.968493312', '2023-01-19T08:13:20.175426560', '2023-01-19T08:13:21.382359808', '2023-01-19T08:13:22.589293056', '2023-01-19T08:13:23.796226304', '2023-01-19T08:13:25.003159552', '2023-01-19T08:13:26.210093056', '2023-01-19T08:13:27.417026304', '2023-01-19T08:13:28.623959552', '2023-01-19T08:13:29.830892800', '2023-01-19T08:13:31.037826048', '2023-01-19T08:13:32.244759552', '2023-01-19T08:13:33.451692800', '2023-01-19T08:13:34.658626048', '2023-01-19T08:13:35.865559296', '2023-01-19T08:13:37.072492544', '2023-01-19T08:13:38.279426048', '2023-01-19T08:13:39.486359296', '2023-01-19T08:13:40.693292544', '2023-01-19T08:13:41.900225792', '2023-01-19T08:13:43.107159040', '2023-01-19T08:13:44.314092544', '2023-01-19T08:13:45.521025792', '2023-01-19T08:13:46.727959040', '2023-01-19T08:13:47.934892288', '2023-01-19T08:13:49.141825536', '2023-01-19T08:13:50.348759040', '2023-01-19T08:13:51.555692288', '2023-01-19T08:13:52.762625536', '2023-01-19T08:13:53.969558784', '2023-01-19T08:13:55.176492032', '2023-01-19T08:13:56.383425536', '2023-01-19T08:13:57.590358784', '2023-01-19T08:13:58.797292032', '2023-01-19T08:14:00.004225280', '2023-01-19T08:14:01.211158528', '2023-01-19T08:14:02.418092032', '2023-01-19T08:14:03.625025280', '2023-01-19T08:14:04.831958528', '2023-01-19T08:14:06.038891776', '2023-01-19T08:14:07.245825024', '2023-01-19T08:14:08.452758528', '2023-01-19T08:14:09.659691776', '2023-01-19T08:14:10.866625024', '2023-01-19T08:14:12.073558272', '2023-01-19T08:14:13.280491520', '2023-01-19T08:14:14.487425024', '2023-01-19T08:14:15.694358272', '2023-01-19T08:14:16.901291520', '2023-01-19T08:14:18.108224768', '2023-01-19T08:14:19.315158016', '2023-01-19T08:14:20.522091264', '2023-01-19T08:14:21.729024768', '2023-01-19T08:14:22.935958016', '2023-01-19T08:14:24.142891264', '2023-01-19T08:14:25.349824512', '2023-01-19T08:14:26.556757760', '2023-01-19T08:14:27.763691264', '2023-01-19T08:14:28.970624512', '2023-01-19T08:14:30.177557760', '2023-01-19T08:14:31.384491008', '2023-01-19T08:14:32.591424256', '2023-01-19T08:14:33.798357760', '2023-01-19T08:14:35.005291008', '2023-01-19T08:14:36.212224256', '2023-01-19T08:14:37.419157504', '2023-01-19T08:14:38.626090752', '2023-01-19T08:14:39.833024256', '2023-01-19T08:14:41.039957504', '2023-01-19T08:14:42.246890752'], dtype='datetime64[ns]')
- (time)float64Hz1.287e-05, 0.359, ..., 0.352, -4.904e-07
Values:
array([ 1.28712703e-05, 3.58958178e-01, 7.17866971e-01, 1.07691359e+00, 1.43591746e+00, 1.79487320e+00, 2.15385369e+00, 2.51281987e+00, 2.87187121e+00, 3.23081226e+00, 3.58981891e+00, 3.94870842e+00, 4.30769912e+00, 4.66666365e+00, 5.02562219e+00, 5.38460756e+00, 5.74358259e+00, 6.10256219e+00, 6.46145332e+00, 6.82052313e+00, 7.17950960e+00, 7.53843141e+00, 7.89749324e+00, 8.25639496e+00, 8.61538232e+00, 8.97432454e+00, 9.33327696e+00, 9.69229285e+00, 1.00513025e+01, 1.04102649e+01, 1.07692595e+01, 1.11281847e+01, 1.14871807e+01, 1.18461841e+01, 1.22050969e+01, 1.25640798e+01, 1.29231327e+01, 1.32820439e+01, 1.36409727e+01, 1.40000353e+01, 1.40783218e+01, 1.40743732e+01, 1.40368964e+01, 1.39993821e+01, 1.39790818e+01, 1.39768099e+01, 1.39851619e+01, 1.39956710e+01, 1.40030625e+01, 1.40059890e+01, 1.40054666e+01, 1.40033109e+01, 1.40010291e+01, 1.39994742e+01, 1.39987127e+01, 1.39987323e+01, 1.39990919e+01, 1.39995409e+01, 1.39998985e+01, 1.40000804e+01, 1.40002122e+01, 1.40001767e+01, 1.40002364e+01, 1.40000597e+01, 1.40001066e+01, 1.40000218e+01, 1.40000392e+01, 1.39999286e+01, 1.40000235e+01, 1.40000074e+01, 1.39999709e+01, 1.39999340e+01, 1.39999981e+01, 1.40000250e+01, 1.40000138e+01, 1.39999544e+01, 1.40000262e+01, 1.39999248e+01, 1.39999959e+01, 1.40000356e+01, 1.39999859e+01, 1.40000064e+01, 1.40000152e+01, 1.39999306e+01, 1.40000103e+01, 1.40000394e+01, 1.40000453e+01, 1.39999621e+01, 1.39999829e+01, 1.40000255e+01, 1.40000156e+01, 1.40001212e+01, 1.40000104e+01, 1.40000177e+01, 1.40000035e+01, 1.40000708e+01, 1.39999885e+01, 1.40000239e+01, 1.39999674e+01, 1.39999966e+01, 1.40000941e+01, 1.39999697e+01, 1.40000485e+01, 1.39999898e+01, 1.39999805e+01, 1.40000370e+01, 1.39999218e+01, 1.39999741e+01, 1.40000058e+01, 1.40000634e+01, 1.39999932e+01, 1.40000127e+01, 1.39999972e+01, 1.40000141e+01, 1.40000016e+01, 1.39999793e+01, 1.39999465e+01, 1.39997833e+01, 1.39994304e+01, 1.39975883e+01, 1.39909586e+01, 1.39679657e+01, 1.39066992e+01, 1.38161344e+01, 1.37547382e+01, 1.37317718e+01, 1.37251129e+01, 1.37233104e+01, 1.37229065e+01, 1.37227973e+01, 1.37227971e+01, 1.37227141e+01, 1.37227334e+01, 1.37227890e+01, 1.37228152e+01, 1.37227866e+01, 1.37227864e+01, 1.37227676e+01, 1.37227788e+01, 1.37227187e+01, 1.37227271e+01, 1.33708923e+01, 1.30189700e+01, 1.26671450e+01, 1.23152892e+01, 1.19635346e+01, 1.16115960e+01, 1.12596457e+01, 1.09078860e+01, 1.05559769e+01, 1.02041706e+01, 9.85215034e+00, 9.50041097e+00, 9.14844059e+00, 8.79657999e+00, 8.44471935e+00, 8.09286946e+00, 7.74101219e+00, 7.38913836e+00, 7.03729080e+00, 6.68543505e+00, 6.33355645e+00, 5.98173710e+00, 5.62985157e+00, 5.27793131e+00, 4.92608422e+00, 4.57427248e+00, 4.22239068e+00, 3.87061422e+00, 3.51868112e+00, 3.16675315e+00, 2.81483757e+00, 2.46306945e+00, 2.11123897e+00, 1.75938373e+00, 1.40747555e+00, 1.05549357e+00, 7.03716252e-01, 3.51902452e-01, -4.90383720e-07])
The chopper has a long region of near-constant rotation speed surrounded by spin-up and spin-down regions:
[5]:
rotation_speed.plot(markersize=2)
[5]:
We use find_plateaus and collapse_plateaus to find those plateaus. Note the atol
and min_n_points
parameters, they need to be tuned for the specific input data.
Warning
find_plateaus
can potentially falsely identify regions with a small but steady slope as a plateau. See the function’s documentation for details.
[6]:
from scippneutron.chopper import collapse_plateaus, find_plateaus
plateaus = find_plateaus(rotation_speed, atol=sc.scalar(1e-3, unit='Hz / s'), min_n_points=10)
plateaus = collapse_plateaus(plateaus)
plateaus
[6]:
- plateau: 2
- plateau(plateau)int640, 1
Values:
array([0, 1]) - time(plateau, time [bin-edge])datetime64ns2023-01-19T08:12:10.173295104, 2023-01-19T08:13:28.623959553, 2023-01-19T08:13:39.486359296, 2023-01-19T08:13:55.176492033
Values:
array([['2023-01-19T08:12:10.173295104', '2023-01-19T08:13:28.623959553'], ['2023-01-19T08:13:39.486359296', '2023-01-19T08:13:55.176492033']], dtype='datetime64[ns]')
- (plateau)float64Hz14.000, 13.723
Values:
array([13.99993461, 13.7228163 ])
find_plateaus
found two plateaus that we can plot with the following helper function:
[7]:
def plot_plateaus(raw_data: sc.DataArray, plateaus: sc.DataArray) -> None:
fig, ax = plt.subplots(1)
raw_data.plot(ax=ax, markersize=2)
for plateau in plateaus:
i = plateau.coords['plateau'].value
da = sc.DataArray(
plateau.data.broadcast(dims=['time'], shape=[2]),
coords={'time': plateau.coords['time']},
name=f'Plateau {i}')
da.plot(ax=ax, ls='-', marker='|', c=f'C{i + 1}')
[8]:
plot_plateaus(rotation_speed, plateaus)
In this case, the source has a frequency of 14Hz which means that plateau 0 is in phase. But plateau 1 is not, it is a short region where the chopper slowed down before fully stopping.
We can use filter_in_phase to remove all out-of-phase plateaus:
[9]:
pulse_frequency = sc.scalar(14.0, unit='Hz')
[10]:
from scippneutron.chopper import filter_in_phase
frequency_in_phase = filter_in_phase(
plateaus,
reference=pulse_frequency,
rtol=sc.scalar(1e-3))
frequency_in_phase
[10]:
- plateau: 1
- plateau(plateau)int640
Values:
array([0]) - time(plateau, time [bin-edge])datetime64ns2023-01-19T08:12:10.173295104, 2023-01-19T08:13:28.623959553
Values:
array([['2023-01-19T08:12:10.173295104', '2023-01-19T08:13:28.623959553']], dtype='datetime64[ns]')
- (plateau)float64Hz14.000
Values:
array([13.99993461])
[11]:
plot_plateaus(rotation_speed, frequency_in_phase)
Extract Plateau#
Since there is only one plateau left, we can simply index into it to get the chopper frequency:
[12]:
frequency = frequency_in_phase['plateau', 0]
frequency
[12]:
- plateau()int640
Values:
array(0) - time(time [bin-edge])datetime64ns2023-01-19T08:12:10.173295104, 2023-01-19T08:13:28.623959553
Values:
array(['2023-01-19T08:12:10.173295104', '2023-01-19T08:13:28.623959553'], dtype='datetime64[ns]')
- ()float64Hz13.999934613725587
Values:
array(13.99993461)
Next, we need the TDC timestamps for the in-phase region:
[13]:
tdc = chopper['top_dead_center']
tdc
[13]:
- (time: 2354)datetime64ns2023-01-19T08:11:06.217830400, 2023-01-19T08:11:08.799053824, ..., 2023-01-19T08:14:39.174524416, 2023-01-19T08:14:40.640919296
Values:
array(['2023-01-19T08:11:06.217830400', '2023-01-19T08:11:08.799053824', '2023-01-19T08:11:09.873177088', ..., '2023-01-19T08:14:38.209520640', '2023-01-19T08:14:39.174524416', '2023-01-19T08:14:40.640919296'], dtype='datetime64[ns]')
[14]:
low = frequency.coords['time'][0]
high = frequency.coords['time'][1]
tdc_in_phase = tdc[(tdc > low) & (tdc < high)]
tdc_in_phase
[14]:
- (time: 1098)datetime64ns2023-01-19T08:12:10.228627200, 2023-01-19T08:12:10.300058880, ..., 2023-01-19T08:13:28.514769152, 2023-01-19T08:13:28.586200832
Values:
array(['2023-01-19T08:12:10.228627200', '2023-01-19T08:12:10.300058880', '2023-01-19T08:12:10.371490560', ..., '2023-01-19T08:13:28.443337984', '2023-01-19T08:13:28.514769152', '2023-01-19T08:13:28.586200832'], dtype='datetime64[ns]')
We can check that the rate at which the TDC triggers is indeed close to 14Hz.
[15]:
diff = tdc_in_phase[1:] - tdc_in_phase[:-1]
rate = 1 / diff.to(unit='s', dtype='float64')
rate.min(), rate.max()
[15]:
(<scipp.Variable> () float64 [Hz] 13.9986,
<scipp.Variable> () float64 [Hz] 14.0002)
Compute Chopper Phase#
DiskChopper
does not use TDC directly for time calculations but instead the chopper phase \(\phi\). According to the disk chopper docs, the phase is defined as
where \(t_0\) is a TDC timestamp and \(T_0\) a pulse time.
We already determined the TDC timestamps above. In practice, we would get \(T_0\) from the input NeXus file, but here, we simply make one up:
[16]:
pulse_time = sc.datetime('2023-01-19T08:12:03.442912915', unit='ns')
Note
The pulse time is typically an array of timestamps and it can be difficult to determine which pulse goes with which chopper period. While the choice is technically arbitrary, the times calculated by DiskChopper
are relative to the chosen pulse time.
If the chopper rotates at the pulse frequency or an integer multiple of it, we can select any pulse time and TDC timestamp and simply use phase = phase % (2 * sc.constants.pi)
below. This corresponds to selecting the pulse and TDC times that are closest to each other.
(We multiply by 1 rad to get the proper rad*Hz
unit in omega
.)
[17]:
omega = 2*sc.constants.pi * frequency.data * sc.scalar(1, unit='rad')
phase = omega * (tdc_in_phase[0] + chopper['delay'].data - pulse_time)
phase = phase.to(unit='rad')
phase
[17]:
- ()float64rad596.900084607312
Values:
array(596.90008461)
Build DiskChopper
#
Finally, we can assemble all data into a scippneutron.chopper.DiskChopper object.
The rotation speed gets rounded (resulting in 14Hz) because DiskChopper
requires it to be a near exact integer multiple of the pulse frequency or vice versa:
rotation_speed = N * pulse_frequency
rotation_speed = pulse_frequency / N
where N
is an integer number.
[18]:
processed = chopper.copy()
processed['rotation_speed'] = sc.round(frequency.data)
processed['phase'] = phase
The input data does not contain a beam position (the angle between the beam and TDC). This probably means that it is 0. But since DiskChopper
does not make that assumption we have to be explicit:
[19]:
processed['beam_position'] = sc.scalar(0.0, unit='rad')
[20]:
from scippneutron.chopper import DiskChopper
disk_chopper = DiskChopper.from_nexus(processed)
disk_chopper
[20]:
- axle_positionscippVariable()vector3m[ 0. 0. 60.]
- frequencyscippVariable()float64Hz14.0
- beam_positionscippVariable()float64rad0.0
- phasescippVariable()float64rad596.900084607312
- slit_beginscippVariable(slit: 2)float64deg30.0, 210.0
- slit_endscippVariable(slit: 2)float64deg160.0, 280.0
- slit_heightscippVariable(slit: 2)float64m0.1, 0.1
- radiusscippVariable()float64m0.35