Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions src/scippneutron/chopper/disk_chopper.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,16 @@ def from_nexus(

Keys in the input correspond to the fields of `NXdisk_chopper
<https://manual.nexusformat.org/classes/base_classes/NXdisk_chopper.html>`_.
The values have to be post-processed, e.g., with
:func:`~scippneutron.chopper.nexus_chopper.post_process_disk_chopper`.
See its documentation for the required steps.
Also, note the class docs of :class:`DiskChopper`
about time-dependent fields.

The input data group must contain a `rotation_speed_setpoint`. If this is not
available, a single (scalar) value must be computed from processing the
time-dependent `rotation_speed` data. This can be done with
:func:`~scippneutron.chopper.nexus_chopper.post_process_disk_chopper`
(see its documentation for the required steps). The resulting single value
can then be passed as `rotation_speed_setpoint`.

If a `phase` field is not present, it is computed from the `delay` field
and the `rotation_speed_setpoint` as `phase = 2*pi*frequency*delay`.

Parameters
----------
Expand All @@ -332,11 +337,28 @@ def from_nexus(
'Class DiskChopper only supports single choppers,'
f'got chopper type {typ}'
)

if "rotation_speed_setpoint" in chopper:
# `rotation_speed_setpoint` is preferred if available (ESS files)
frequency = _get_0d_variable(chopper, 'rotation_speed_setpoint')
else:
# Fallback to `rotation_speed` (standard NeXus)
frequency = _get_0d_variable(chopper, 'rotation_speed')
if "phase" in chopper:
phase = _get_0d_variable(chopper, 'phase')
else:
if "delay" not in chopper:
raise ValueError(
"DiskChopper.from_nexus: Chopper field 'phase' is missing and "
"cannot be computed because field 'delay' is also missing."
)
omega = 2 * sc.constants.pi * frequency * sc.scalar(1.0, unit='rad')
phase = (chopper["delay"].to(dtype=float) * omega).to(unit='rad')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the notebook in the docs to reflect this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See update

return DiskChopper(
axle_position=chopper['position'],
frequency=_get_1d_variable(chopper, 'rotation_speed'),
beam_position=_get_1d_variable(chopper, 'beam_position'),
phase=_get_1d_variable(chopper, 'phase'),
frequency=frequency,
beam_position=_get_0d_variable(chopper, 'beam_position'),
phase=phase,
slit_height=chopper.get('slit_height'),
radius=chopper.get('radius'),
**_get_edges_from_nexus(chopper),
Expand Down Expand Up @@ -557,6 +579,10 @@ def _source_phase_factor(self, pulse_frequency: sc.Variable) -> int:
# of the slits, so use `max` here:
return round(max(quot.value, 1))

def as_dict(self) -> dict[str, Any]:
"""Return the DiskChopper fields as a dictionary."""
return dataclasses.asdict(self)


def _field_eq(a: Any, b: Any) -> bool:
if isinstance(a, sc.Variable | sc.DataArray):
Expand Down Expand Up @@ -627,13 +653,7 @@ def _get_edges_from_nexus(
}


def _len_or_1(x: sc.Variable) -> int:
if x.ndim == 0:
return 1
return len(x)


def _get_1d_variable(
def _get_0d_variable(
dg: Mapping[str, sc.Variable | sc.DataArray], name: str
) -> sc.Variable:
if (val := dg.get(name)) is None:
Expand All @@ -642,9 +662,11 @@ def _get_1d_variable(
msg = (
"Chopper field '{name}' must be a scalar variable, {got}. "
"See the chopper user-guide for more information: "
"https://scipp.github.io/scippneutron/user-guide/chopper/pre-processing.html"
"https://scipp.github.io/scippneutron/user-guide/chopper/processing-nexus-choppers.html"
)

if isinstance(val, sc.DataArray):
val = val.data
if not isinstance(val, sc.Variable):
raise TypeError(msg.format(name=name, got=f'got a {type(val)}'))
if val.ndim != 0:
Expand Down
102 changes: 79 additions & 23 deletions tests/chopper/disk_chopper_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ def deg_angle_to_time_factor(frequency: sc.Variable) -> sc.Variable:
return sc.to_unit(to_rad / angular_frequency, 's/deg')


@pytest.fixture
def nexus_chopper():
@pytest.fixture(params=['rotation_speed', 'rotation_speed_setpoint'])
def nexus_chopper(request):
rotation_speed_key = request.param
return sc.DataGroup(
{
'type': DiskChopperType.single,
'position': sc.vector([0.0, 0.0, 2.0], unit='m'),
'rotation_speed': sc.scalar(12.0, unit='Hz'),
rotation_speed_key: sc.scalar(12.0, unit='Hz'),
'beam_position': sc.scalar(45.0, unit='deg'),
'phase': sc.scalar(-20.0, unit='deg'),
'slit_edges': sc.array(
Expand All @@ -38,6 +39,14 @@ def nexus_chopper():
)


def _get_rotation_speed_key(nexus_chopper):
return (
'rotation_speed_setpoint'
if 'rotation_speed_setpoint' in nexus_chopper
else 'rotation_speed'
)


@pytest.mark.parametrize(
'typ',
[
Expand All @@ -54,7 +63,7 @@ def test_chopper_supports_only_single(nexus_chopper, typ):


def test_frequency_must_be_frequency(nexus_chopper):
nexus_chopper['rotation_speed'] = sc.scalar(1.0, unit='m/s')
nexus_chopper[_get_rotation_speed_key(nexus_chopper)] = sc.scalar(1.0, unit='m/s')
with pytest.raises(sc.UnitError):
DiskChopper.from_nexus(nexus_chopper)

Expand All @@ -78,6 +87,11 @@ def test_eq(nexus_chopper):
)
def test_neq(nexus_chopper, replacement):
ch1 = DiskChopper.from_nexus(nexus_chopper)
if (
replacement[0] == 'rotation_speed'
and 'rotation_speed_setpoint' in nexus_chopper
):
replacement = ('rotation_speed_setpoint', replacement[1])
ch2 = DiskChopper.from_nexus({**nexus_chopper, replacement[0]: replacement[1]})
assert ch1 != ch2

Expand Down Expand Up @@ -212,7 +226,7 @@ def test_time_offset_angle_at_beam_no_phase_zero_beam_pos_clockwise_single_angle
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit=beam_position_unit),
'phase': sc.scalar(0.0, unit=phase_unit),
'rotation_speed': sc.scalar(-2.3, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-2.3, unit='Hz'),
}
)
omega = 2 * pi * 2.3
Expand Down Expand Up @@ -250,7 +264,7 @@ def test_time_offset_angle_at_beam_no_phase_zero_beam_pos_anti_clockwise_single_
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit=beam_position_unit),
'phase': sc.scalar(0.0, unit=phase_unit),
'rotation_speed': sc.scalar(2.3, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(2.3, unit='Hz'),
}
)
omega = 2 * pi * 2.3
Expand Down Expand Up @@ -287,7 +301,7 @@ def test_time_offset_angle_at_beam_no_phase_zero_beam_pos_clockwise_multi_angle(
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit=beam_position_unit),
'phase': sc.scalar(0.0, unit=phase_unit),
'rotation_speed': sc.scalar(-4.4, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-4.4, unit='Hz'),
}
)
omega = 2 * pi * 4.4
Expand All @@ -312,7 +326,7 @@ def test_time_offset_angle_at_beam_no_phase_zero_beam_pos_anti_clockwise_multi_a
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit=beam_position_unit),
'phase': sc.scalar(0.0, unit=phase_unit),
'rotation_speed': sc.scalar(4.4, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(4.4, unit='Hz'),
}
)
omega = 2 * pi * 4.4
Expand All @@ -335,7 +349,7 @@ def test_time_offset_angle_at_beam_no_phase_with_beam_pos_clockwise(
**nexus_chopper,
'beam_position': sc.scalar(1.8, unit='rad'),
'phase': sc.scalar(0.0, unit=phase_unit),
'rotation_speed': sc.scalar(-2.3, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-2.3, unit='Hz'),
}
)
omega = 2 * pi * 2.3
Expand Down Expand Up @@ -368,7 +382,7 @@ def test_time_offset_angle_at_beam_no_phase_with_beam_pos_anti_clockwise(
**nexus_chopper,
'beam_position': sc.scalar(1.8, unit='rad'),
'phase': sc.scalar(0.0, unit=phase_unit),
'rotation_speed': sc.scalar(2.3, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(2.3, unit='Hz'),
}
)
omega = 2 * pi * 2.3
Expand Down Expand Up @@ -401,7 +415,7 @@ def test_time_offset_angle_at_beam_with_phase_zero_beam_pos_clockwise(
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit=beam_position_unit),
'phase': sc.scalar(-0.7, unit='rad'),
'rotation_speed': sc.scalar(-1.1, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-1.1, unit='Hz'),
}
)
omega = 2 * pi * 1.1
Expand Down Expand Up @@ -435,7 +449,7 @@ def test_time_offset_angle_at_beam_with_phase_zero_beam_pos_anti_clockwise(
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit=beam_position_unit),
'phase': sc.scalar(-0.7, unit='rad'),
'rotation_speed': sc.scalar(1.1, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(1.1, unit='Hz'),
}
)
omega = 2 * pi * 1.1
Expand Down Expand Up @@ -492,7 +506,9 @@ def test_time_offset_open_close_only_slit(nexus_chopper, rotation_speed):
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit='rad'),
'phase': sc.scalar(0.0, unit='rad'),
'rotation_speed': sc.scalar(rotation_speed, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(
rotation_speed, unit='Hz'
),
'slit_edges': sc.array(
dims=['slit'],
values=[0.0, 360.0],
Expand Down Expand Up @@ -538,7 +554,7 @@ def test_time_offset_open_close_one_slit_clockwise(nexus_chopper, phase, beam_po
**nexus_chopper,
'beam_position': beam_position,
'phase': phase,
'rotation_speed': sc.scalar(-7.21, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-7.21, unit='Hz'),
'slit_edges': sc.array(dims=['slit'], values=[87.0, 177.0], unit='deg'),
}
)
Expand Down Expand Up @@ -584,7 +600,7 @@ def test_time_offset_open_close_one_slit_anticlockwise(
**nexus_chopper,
'beam_position': beam_position,
'phase': phase,
'rotation_speed': sc.scalar(7.21, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(7.21, unit='Hz'),
'slit_edges': sc.array(dims=['slit'], values=[87.0, 177.0], unit='deg'),
}
)
Expand Down Expand Up @@ -630,7 +646,7 @@ def test_time_offset_open_close_one_slit_across_tdc_clockwise(
**nexus_chopper,
'beam_position': beam_position,
'phase': phase,
'rotation_speed': sc.scalar(-7.21, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-7.21, unit='Hz'),
'slit_edges': sc.array(dims=['slit'], values=[330.0, 380.0], unit='deg'),
}
)
Expand Down Expand Up @@ -676,7 +692,7 @@ def test_time_offset_open_close_one_slit_across_tdc_anticlockwise(
**nexus_chopper,
'beam_position': beam_position,
'phase': phase,
'rotation_speed': sc.scalar(7.21, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(7.21, unit='Hz'),
'slit_edges': sc.array(dims=['slit'], values=[330.0, 380.0], unit='deg'),
}
)
Expand Down Expand Up @@ -704,7 +720,7 @@ def test_time_offset_open_close_two_slits_clockwise(nexus_chopper):
**nexus_chopper,
'beam_position': sc.scalar(-32.0, unit='deg'),
'phase': sc.scalar(0.0, unit='deg'),
'rotation_speed': sc.scalar(-11.2, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-11.2, unit='Hz'),
'slit_edges': sc.array(
dims=['slit'],
values=[87.0, 177.0, 280.0, 342.0],
Expand Down Expand Up @@ -743,7 +759,7 @@ def test_time_offset_open_close_two_slits_anticlockwise(nexus_chopper):
**nexus_chopper,
'beam_position': sc.scalar(-32.0, unit='deg'),
'phase': sc.scalar(0.0, unit='deg'),
'rotation_speed': sc.scalar(11.2, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(11.2, unit='Hz'),
'slit_edges': sc.array(
dims=['slit'],
values=[87.0, 177.0, 280.0, 342.0],
Expand Down Expand Up @@ -782,7 +798,7 @@ def test_time_offset_open_close_two_slits_clockwise_two_pulses(nexus_chopper):
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit='deg'),
'phase': sc.scalar(60.0, unit='deg'),
'rotation_speed': sc.scalar(-14.0, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-14.0, unit='Hz'),
'slit_edges': sc.array(
dims=['slit'],
values=[87.0, 177.0, 200.0, 240.0],
Expand Down Expand Up @@ -837,7 +853,7 @@ def test_time_offset_open_close_two_slits_anticlockwise_two_pulses(nexus_chopper
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit='deg'),
'phase': sc.scalar(60.0, unit='deg'),
'rotation_speed': sc.scalar(14.0, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(14.0, unit='Hz'),
'slit_edges': sc.array(
dims=['slit'],
values=[87.0, 177.0, 200.0, 240.0],
Expand Down Expand Up @@ -892,7 +908,7 @@ def test_time_offset_open_close_two_slits_clockwise_half_pulse(nexus_chopper):
**nexus_chopper,
'beam_position': sc.scalar(0.0, unit='deg'),
'phase': sc.scalar(60.0, unit='deg'),
'rotation_speed': sc.scalar(-3.5, unit='Hz'),
_get_rotation_speed_key(nexus_chopper): sc.scalar(-3.5, unit='Hz'),
'slit_edges': sc.array(
dims=['slit'],
values=[87.0, 177.0, 200.0, 240.0],
Expand Down Expand Up @@ -928,7 +944,10 @@ def test_time_offset_open_close_two_slits_clockwise_half_pulse(nexus_chopper):

def test_time_offset_open_close_source_frequency_not_multiple_of_chopper(nexus_chopper):
ch = DiskChopper.from_nexus(
{**nexus_chopper, 'rotation_speed': sc.scalar(4.52, unit='Hz')}
{
**nexus_chopper,
_get_rotation_speed_key(nexus_chopper): sc.scalar(4.52, unit='Hz'),
}
)
with pytest.raises(ValueError, match='out of phase'):
ch.time_offset_open(pulse_frequency=sc.scalar(4.3, unit='Hz'))
Expand All @@ -945,3 +964,40 @@ def test_disk_chopper_svg_custom_dim_names(nexus_chopper):
nexus_chopper['slit_edges'] = nexus_chopper['slit_edges'].rename_dims(slit='dim_0')
ch = DiskChopper.from_nexus(nexus_chopper)
assert ch.make_svg()


def test_phase_derived_from_delay_and_frequency(nexus_chopper):
delay = sc.scalar(5e7, unit='ns')
frequency = sc.scalar(14.0, unit='Hz')
expected_phase = (2 * pi * frequency * delay * sc.scalar(1.0, unit='rad')).to(
unit='deg'
)

chopper_params = nexus_chopper.copy()
del chopper_params['phase']

ch = DiskChopper.from_nexus(
{
**chopper_params,
'delay': delay,
_get_rotation_speed_key(nexus_chopper): frequency,
}
)
assert sc.allclose(ch.phase.to(unit='deg'), expected_phase)


def test_missing_both_phase_and_delay_raises(nexus_chopper):
chopper_params = nexus_chopper.copy()
del chopper_params['phase']

with pytest.raises(
ValueError,
match="DiskChopper.from_nexus: Chopper field 'phase' is missing and "
"cannot be computed because field 'delay' is also missing.",
):
DiskChopper.from_nexus(
{
**chopper_params,
_get_rotation_speed_key(nexus_chopper): sc.scalar(14.0, unit='Hz'),
}
)
Loading