Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 30 additions & 6 deletions packages/essreduce/src/ess/reduce/unwrap/lut.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,34 @@ def _to_component_reading(component) -> BeamlineComponentReading:
)


def chopper_distance_along_beam(
axle_position: sc.Variable,
source_position: sc.Variable,
) -> sc.Variable:
"""Flight distance from the source to a chopper along the incident beam.

A disk chopper's axle is offset from the beam (the disk sits above, below, or
to the side of it), so the straight-line distance
``norm(axle_position - source_position)`` overestimates the flight distance,
and for closely-spaced choppers can even place them in the wrong order along
the cascade. The neutron crosses the disk where the incident beam intersects
it, so the flight distance is the projection of the axle position onto the
incident-beam direction. Assuming the incident beam runs along the z axis,
this is the z-component of the offset from the source. This matches the
straight-line ``Ltotal`` used for detectors and monitors, where the component
lies on the beam and the projection reduces to the norm.

Parameters
----------
axle_position:
Position of the chopper's rotation axle.
source_position:
Position of the neutron source.
"""
source_position = source_position.to(unit=axle_position.unit)
return (axle_position - source_position).fields.z


def simulate_chopper_cascade_using_tof(
choppers: DiskChoppers[RunType],
source_position: Position[snx.NXsource, RunType],
Expand Down Expand Up @@ -471,9 +499,7 @@ def simulate_chopper_cascade_using_tof(
tof_choppers = []
for name, ch in choppers.items():
chop = tof.Chopper.from_diskchopper(ch, name=name)
chop.distance = sc.norm(
ch.axle_position - source_position.to(unit=ch.axle_position.unit)
)
chop.distance = chopper_distance_along_beam(ch.axle_position, source_position)
tof_choppers.append(chop)

source = tof.Source(
Expand Down Expand Up @@ -657,9 +683,7 @@ def compute_frame_sequence(

chops = {
key: chopper_cascade.Chopper(
distance=sc.norm(
ch.axle_position - source_position.to(unit=ch.axle_position.unit)
),
distance=chopper_distance_along_beam(ch.axle_position, source_position),
time_open=ch.time_offset_open(
pulse_frequency=frequency_for_chopper_rotation
),
Expand Down
44 changes: 44 additions & 0 deletions packages/essreduce/tests/unwrap/lut_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ess.reduce import unwrap
from ess.reduce.nexus.types import AnyRun, FrameMonitor0, Position
from ess.reduce.unwrap import GenericUnwrapWorkflow, LookupTableWorkflow, SourceBounds
from ess.reduce.unwrap.lut import chopper_distance_along_beam

sl = pytest.importorskip("sciline")

Expand Down Expand Up @@ -167,6 +168,49 @@ def _make_choppers():
}


def _single_slit_chopper(axle_position: sc.Variable) -> DiskChopper:
return DiskChopper(
frequency=sc.scalar(-14.0, unit='Hz'),
beam_position=sc.scalar(0.0, unit='deg'),
phase=sc.scalar(0.0, unit='deg'),
axle_position=axle_position,
slit_begin=sc.array(dims=['cutout'], values=[0.0], unit='deg'),
slit_end=sc.array(dims=['cutout'], values=[10.0], unit='deg'),
slit_height=sc.scalar(10.0, unit='cm'),
radius=sc.scalar(30.0, unit='cm'),
)


def test_chopper_distance_along_beam_projects_offset_axle_onto_beam():
source = sc.vector([0, 0, 0], unit='m')
# Axle sits 3 m above the beam and 10 m downstream of the source.
axle = sc.vector([0, 3, 10], unit='m')
distance = chopper_distance_along_beam(axle, source)
assert sc.isclose(distance, sc.scalar(10.0, unit='m'))
# The straight-line distance to the offset axle would overestimate this.
assert sc.norm(axle - source) > sc.scalar(10.0, unit='m')


def test_chopper_cascade_orders_offset_axle_by_beam_projection():
# Two closely-spaced choppers; the upstream one's axle is offset far enough
# from the beam that ``norm(axle - source)`` (6.185 m) exceeds the downstream
# chopper's distance (6.155 m). Ordering must follow the beam-projected
# distance, otherwise the upstream chopper is wrongly applied last (or, for a
# narrow distance range, never at all).
wf = _make_workflow("analytical")
wf[Position[snx.NXsource, AnyRun]] = sc.vector([0, 0, 0], unit='m')
wf[unwrap.PulseStride[AnyRun]] = 1
wf[unwrap.DiskChoppers[AnyRun]] = {
'near': _single_slit_chopper(sc.vector([0, 0.7, 6.145], unit='m')),
'far': _single_slit_chopper(sc.vector([0, 0, 6.155], unit='m')),
}

frames = wf.compute(unwrap.ChopperFrameSequence[AnyRun])

distances = sorted(f.distance.to(unit='m').value for f in frames)
assert distances == pytest.approx([0.0, 6.145, 6.155])


@pytest.mark.parametrize("detector_or_monitor", ["detector", "monitor"])
@pytest.mark.parametrize("wavelength_from", ["analytical", "simulation"])
def test_lut_workflow_computes_table_with_choppers(
Expand Down