From 8af5b19a635e5f177e09373cd8a2a6babee6924a Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Mon, 9 Feb 2026 11:50:40 +0100 Subject: [PATCH 01/27] feat: load 3D detector geometry --- src/ess/beer/data.py | 7 ++++++ src/ess/beer/io.py | 34 ++++++++++++++++++++++++----- tests/beer/mcstas_reduction_test.py | 14 +++++++++++- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/ess/beer/data.py b/src/ess/beer/data.py index 1e144080..21cd6dc5 100644 --- a/src/ess/beer/data.py +++ b/src/ess/beer/data.py @@ -41,6 +41,9 @@ "silicon-mode7-new-model.h5": "md5:d2070d3132722bb551d99b243c62752f", "silicon-mode8-new-model.h5": "md5:d6dfdf7e87eccedf4f83c67ec552ca22", "silicon-mode9-new-model.h5": "md5:694a17fb616b7f1c20e94d9da113d201", + # Simulation with 3D detector model - almost no events + # - only used to verify we can load the 3D geometry. + "few_neutrons_3d_detector_example.h5": "md5:88cbe29cb539c8acebf9fd7cee9d3c57", }, ) @@ -74,6 +77,10 @@ def mcstas_silicon_new_model(mode: int) -> Path: return _registry.get_path(f'silicon-mode{mode}-new-model.h5') +def mcstas_few_neutrons_3d_detector_example(): + return _registry.get_path('few_neutrons_3d_detector_example.h5') + + def duplex_peaks() -> Path: return _registry.get_path('duplex-dhkl.tab') diff --git a/src/ess/beer/io.py b/src/ess/beer/io.py index 464961ec..d6f9c816 100644 --- a/src/ess/beer/io.py +++ b/src/ess/beer/io.py @@ -2,6 +2,7 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) import re from pathlib import Path +from typing import Literal import h5py import numpy as np @@ -127,7 +128,7 @@ def _effective_chopper_position_from_mode( raise ValueError(f'Unkonwn chopper mode {mode}.') -def _load_beer_mcstas(f, bank=1): +def _load_beer_mcstas(f, north_or_south=None, *, number): positions = { name: f'/entry1/instrument/components/{key}/Position' for key in f['/entry1/instrument/components'] @@ -147,8 +148,16 @@ def _load_beer_mcstas(f, bank=1): mcc_pos, ) = _load_h5( f, - f'NXentry/NXdetector/bank{bank:02}_events_dat_list_p_x_y_n_id_t', - f'NXentry/NXdetector/bank{bank:02}_events_dat_list_p_x_y_n_id_t/events', + ( + f'NXentry/NXdetector/bank_{north_or_south}{number}_events_dat_list_p_x_y_n_id_t' + if north_or_south is not None + else f'NXentry/NXdetector/bank{number:02}_events_dat_list_p_x_y_n_id_t' + ), + ( + f'NXentry/NXdetector/bank_{north_or_south}{number}_events_dat_list_p_x_y_n_id_t/events' + if north_or_south is not None + else f'NXentry/NXdetector/bank{number:02}_events_dat_list_p_x_y_n_id_t/events' # noqa: E501 + ), 'NXentry/simulation/Param', positions['sampleMantid'], positions['PSC1'], @@ -162,7 +171,10 @@ def _load_beer_mcstas(f, bank=1): 'Rotation' ] detector_rotation = _find_h5( - f['/entry1/instrument/components'], f'.*nD_Mantid_?{bank}.*' + f['/entry1/instrument/components'], + f'.*nD_Mantid_?{north_or_south}_{number}.*' + if north_or_south is not None + else f'.*nD_Mantid_?{number}.*', )['Rotation'] events = events[()] @@ -286,7 +298,9 @@ def _not_between(x, a, b): return (x < a) | (b < x) -def load_beer_mcstas(f: str | Path | h5py.File, bank: int) -> sc.DataArray: +def load_beer_mcstas( + f: str | Path | h5py.File, bank: Literal['north', 'south'] | int +) -> sc.DataArray: '''Load beer McStas data from a file to a data group with one data array for each bank. ''' @@ -294,7 +308,15 @@ def load_beer_mcstas(f: str | Path | h5py.File, bank: int) -> sc.DataArray: with h5py.File(f) as ff: return load_beer_mcstas(ff, bank=bank) - return _load_beer_mcstas(f, bank=bank) + if bank in {'north', 'south'}: + return sc.concat( + [ + _load_beer_mcstas(f, north_or_south=bank, number=number) + for number in range(1, 13) + ], + dim='panel', + ) + return _load_beer_mcstas(f, north_or_south=None, number=bank) def load_beer_mcstas_provider( diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index f19ee808..7596844c 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -8,7 +8,13 @@ BeerModMcStasWorkflow, BeerModMcStasWorkflowKnownPeaks, ) -from ess.beer.data import duplex_peaks_array, mcstas_duplex, mcstas_silicon_new_model +from ess.beer.data import ( + duplex_peaks_array, + mcstas_duplex, + mcstas_few_neutrons_3d_detector_example, + mcstas_silicon_new_model, +) +from ess.beer.io import load_beer_mcstas from ess.beer.types import DetectorBank, DHKLList from ess.reduce.nexus.types import Filename, SampleRun from ess.reduce.time_of_flight.types import TofDetector @@ -71,3 +77,9 @@ def test_pulse_shaping_workflow(): sc.scalar(1.6374, unit='angstrom'), atol=sc.scalar(1e-2, unit='angstrom'), ) + + +def test_can_load_3d_detector(): + load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), 'north') + da = load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), 'south') + assert 'panel' in da.dims From 4baea39ee048101fd328025a4d885bd7c0a37f02 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Mon, 9 Feb 2026 13:49:19 +0100 Subject: [PATCH 02/27] feat: load monitor --- src/ess/beer/io.py | 39 +++++++++++++++++++++++++++++ tests/beer/mcstas_reduction_test.py | 10 +++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/ess/beer/io.py b/src/ess/beer/io.py index d6f9c816..32e48857 100644 --- a/src/ess/beer/io.py +++ b/src/ess/beer/io.py @@ -319,6 +319,45 @@ def load_beer_mcstas( return _load_beer_mcstas(f, north_or_south=None, number=bank) +def load_beer_mcstas_monitor(f: str | Path | h5py.File): + if isinstance(f, str | Path): + with h5py.File(f) as ff: + return load_beer_mcstas_monitor(ff) + ( + monitor, + wavelengths, + data, + errors, + ncount, + ) = _load_h5( + f, + 'NXentry/NXdetector/Lmon_hereon_dat', + 'NXentry/NXdetector/Lmon_hereon_dat/Wavelength__AA_', + 'NXentry/NXdetector/Lmon_hereon_dat/data', + 'NXentry/NXdetector/Lmon_hereon_dat/errors', + 'NXentry/NXdetector/Lmon_hereon_dat/ncount', + ) + da = sc.DataArray( + sc.array( + dims=['wavelength'], values=data[:], variances=errors[:], unit='counts' + ), + coords={ + 'wavelength': sc.array( + dims=['wavelength'], values=wavelengths[:], unit='angstrom' + ), + 'ncount': sc.array(dims=['wavelength'], values=ncount[:], unit='counts'), + }, + ) + for name, value in monitor.attrs.items(): + if name in ('position',): + da.coords[name] = sc.scalar(value.decode()) + + da.coords['position'] = sc.vector( + list(map(float, da.coords.pop('position').value.split(' '))), unit='m' + ) + return da + + def load_beer_mcstas_provider( fname: Filename[SampleRun], bank: DetectorBank, diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index 7596844c..113dbd47 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -14,7 +14,7 @@ mcstas_few_neutrons_3d_detector_example, mcstas_silicon_new_model, ) -from ess.beer.io import load_beer_mcstas +from ess.beer.io import load_beer_mcstas, load_beer_mcstas_monitor from ess.beer.types import DetectorBank, DHKLList from ess.reduce.nexus.types import Filename, SampleRun from ess.reduce.time_of_flight.types import TofDetector @@ -83,3 +83,11 @@ def test_can_load_3d_detector(): load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), 'north') da = load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), 'south') assert 'panel' in da.dims + + +def test_can_load_monitor(): + da = load_beer_mcstas_monitor(mcstas_few_neutrons_3d_detector_example()) + assert 'wavelength' in da.coords + assert 'position' in da.coords + assert da.coords['position'].dtype == sc.DType.vector3 + assert da.coords['position'].unit == 'm' From faa76751101a3a973644ebd181f730f0c0497e2f Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Mon, 9 Feb 2026 15:08:07 +0100 Subject: [PATCH 03/27] fix: same interface for loading old and new files + replace DetectorBank index with enum --- .../beer/beer_modulation_mcstas.ipynb | 30 ++++++++-------- src/ess/beer/io.py | 35 ++++++++++++------- src/ess/beer/types.py | 7 +++- tests/beer/mcstas_reduction_test.py | 10 +++--- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/user-guide/beer/beer_modulation_mcstas.ipynb b/docs/user-guide/beer/beer_modulation_mcstas.ipynb index d269ced1..39227ea9 100644 --- a/docs/user-guide/beer/beer_modulation_mcstas.ipynb +++ b/docs/user-guide/beer/beer_modulation_mcstas.ipynb @@ -161,7 +161,7 @@ "outputs": [], "source": [ "wf = BeerModMcStasWorkflowKnownPeaks()\n", - "wf[DetectorBank] = 2\n", + "wf[DetectorBank] = DetectorBank.north\n", "wf[Filename[SampleRun]] = mcstas_silicon_medium_resolution()\n", "da = wf.compute(RawDetector[SampleRun])\n", "da.masks.clear()\n", @@ -187,7 +187,7 @@ "wf[Filename[SampleRun]] = mcstas_silicon_medium_resolution()\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -218,7 +218,7 @@ "wf[Filename[SampleRun]] = mcstas_silicon_medium_resolution()\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -277,7 +277,7 @@ "outputs": [], "source": [ "wf = BeerModMcStasWorkflowKnownPeaks()\n", - "wf[DetectorBank] = 1\n", + "wf[DetectorBank] = DetectorBank.south\n", "wf[Filename[SampleRun]] = mcstas_duplex(8)\n", "wf.compute(RawDetector[SampleRun]).hist(two_theta=400, event_time_offset=1000).plot(norm='log', cmin=1.0e-2)" ] @@ -300,7 +300,7 @@ "wf[DHKLList] = duplex_peaks_array()\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -331,7 +331,7 @@ "wf[Filename[SampleRun]] = mcstas_duplex(8)\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -390,7 +390,7 @@ "outputs": [], "source": [ "wf = BeerModMcStasWorkflowKnownPeaks()\n", - "wf[DetectorBank] = 1\n", + "wf[DetectorBank] = DetectorBank.south\n", "wf[Filename[SampleRun]] = mcstas_duplex(9)\n", "wf.compute(RawDetector[SampleRun]).hist(two_theta=400, event_time_offset=1000).plot(norm='log', cmin=1.0e-3)" ] @@ -413,7 +413,7 @@ "wf[DHKLList] = duplex_peaks_array()\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -444,7 +444,7 @@ "wf[Filename[SampleRun]] = mcstas_duplex(9)\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -503,7 +503,7 @@ "outputs": [], "source": [ "wf = BeerModMcStasWorkflowKnownPeaks()\n", - "wf[DetectorBank] = 1\n", + "wf[DetectorBank] = DetectorBank.south\n", "wf[Filename[SampleRun]] = mcstas_duplex(10)\n", "wf.compute(RawDetector[SampleRun]).hist(two_theta=400, event_time_offset=1000).plot(norm='log', cmin=1.0e-3)" ] @@ -526,7 +526,7 @@ "wf[DHKLList] = duplex_peaks_array()\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -557,7 +557,7 @@ "wf[Filename[SampleRun]] = mcstas_duplex(10)\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -616,7 +616,7 @@ "outputs": [], "source": [ "wf = BeerModMcStasWorkflowKnownPeaks()\n", - "wf[DetectorBank] = 1\n", + "wf[DetectorBank] = DetectorBank.south\n", "wf[Filename[SampleRun]] = mcstas_duplex(16)\n", "wf.compute(RawDetector[SampleRun]).hist(two_theta=400, event_time_offset=1000).plot(norm='log', cmin=1.0e-3)" ] @@ -639,7 +639,7 @@ "wf[DHKLList] = duplex_peaks_array()\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", @@ -670,7 +670,7 @@ "wf[Filename[SampleRun]] = mcstas_duplex(16)\n", "\n", "results = {}\n", - "for bank in (1, 2):\n", + "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", " da = wf.compute(TofDetector[SampleRun])\n", " results[bank] = (\n", diff --git a/src/ess/beer/io.py b/src/ess/beer/io.py index 32e48857..91282dbe 100644 --- a/src/ess/beer/io.py +++ b/src/ess/beer/io.py @@ -2,7 +2,6 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) import re from pathlib import Path -from typing import Literal import h5py import numpy as np @@ -40,7 +39,7 @@ def _find_h5(group: h5py.Group, matches): if re.match(matches, p): return group[p] else: - raise RuntimeError(f'Could not find "{matches}" in {group}.') + raise ValueError(f'Could not find "{matches}" in {group}.') def _load_h5(group: h5py.Group | str, *paths: str): @@ -298,25 +297,35 @@ def _not_between(x, a, b): return (x < a) | (b < x) -def load_beer_mcstas( - f: str | Path | h5py.File, bank: Literal['north', 'south'] | int -) -> sc.DataArray: +def load_beer_mcstas(f: str | Path | h5py.File, bank: DetectorBank) -> sc.DataArray: '''Load beer McStas data from a file to a data group with one data array for each bank. ''' + if not isinstance(bank, DetectorBank): + raise ValueError( + '"bank" must be either ``DetectorBank.north`` or ``DetectorBank.south``' + ) + if isinstance(f, str | Path): with h5py.File(f) as ff: return load_beer_mcstas(ff, bank=bank) - if bank in {'north', 'south'}: - return sc.concat( - [ - _load_beer_mcstas(f, north_or_south=bank, number=number) - for number in range(1, 13) - ], - dim='panel', + try: + _find_h5(f['/entry1/instrument/components'], '.*nD_Mantid_?south_1.*') + except ValueError: + # The file did not have a detector named 'south'-something. + # Load old 2D structure where banks were not named 'north' and 'south'. + return _load_beer_mcstas( + f, north_or_south=None, number=1 if bank == DetectorBank.south else 2 ) - return _load_beer_mcstas(f, north_or_south=None, number=bank) + + return sc.concat( + [ + _load_beer_mcstas(f, north_or_south=bank.name, number=number) + for number in range(1, 13) + ], + dim='panel', + ) def load_beer_mcstas_monitor(f: str | Path | h5py.File): diff --git a/src/ess/beer/types.py b/src/ess/beer/types.py index 144cfbb7..c4655bf5 100644 --- a/src/ess/beer/types.py +++ b/src/ess/beer/types.py @@ -7,6 +7,7 @@ pipeline. """ +from enum import Enum from typing import NewType import sciline @@ -25,7 +26,11 @@ class StreakClusteredData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): SampleRun = SampleRun TofDetector = TofDetector -DetectorBank = NewType('DetectorBank', int) + +class DetectorBank(Enum): + north = 'north' + south = 'south' + TwoThetaLimits = NewType("TwoThetaLimits", tuple[sc.Variable, sc.Variable]) diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index 113dbd47..40caec52 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -23,7 +23,7 @@ def test_can_reduce_using_known_peaks_workflow(): wf = BeerModMcStasWorkflowKnownPeaks() wf[DHKLList] = duplex_peaks_array() - wf[DetectorBank] = 1 + wf[DetectorBank] = DetectorBank.north wf[Filename[SampleRun]] = mcstas_duplex(7) da = wf.compute(TofDetector[SampleRun]) assert 'tof' in da.bins.coords @@ -44,7 +44,7 @@ def test_can_reduce_using_known_peaks_workflow(): def test_can_reduce_using_unknown_peaks_workflow(): wf = BeerModMcStasWorkflow() wf[Filename[SampleRun]] = mcstas_duplex(7) - wf[DetectorBank] = 1 + wf[DetectorBank] = DetectorBank.north da = wf.compute(TofDetector[SampleRun]) da = da.transform_coords( ('dspacing',), @@ -62,7 +62,7 @@ def test_can_reduce_using_unknown_peaks_workflow(): def test_pulse_shaping_workflow(): wf = BeerMcStasWorkflowPulseShaping() wf[Filename[SampleRun]] = mcstas_silicon_new_model(6) - wf[DetectorBank] = 1 + wf[DetectorBank] = DetectorBank.north da = wf.compute(TofDetector[SampleRun]) assert 'tof' in da.bins.coords # assert dataarray has all coords required to compute dspacing @@ -80,8 +80,8 @@ def test_pulse_shaping_workflow(): def test_can_load_3d_detector(): - load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), 'north') - da = load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), 'south') + load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), DetectorBank.north) + da = load_beer_mcstas(mcstas_few_neutrons_3d_detector_example(), DetectorBank.south) assert 'panel' in da.dims From dcab70123501465d66df62de4e9822a03d7f61b5 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 25 Feb 2026 21:12:55 +0100 Subject: [PATCH 04/27] start removing tof from powder dream workflow --- src/ess/dream/workflows.py | 16 +++---- src/ess/powder/conversion.py | 89 +++++++++++++++++------------------- src/ess/powder/types.py | 25 +++++----- 3 files changed, 60 insertions(+), 70 deletions(-) diff --git a/src/ess/dream/workflows.py b/src/ess/dream/workflows.py index 5b629172..0f1ec112 100644 --- a/src/ess/dream/workflows.py +++ b/src/ess/dream/workflows.py @@ -7,9 +7,9 @@ import sciline import scipp as sc import scippnexus as snx +from ess.reduce.kinematics import GenericWavelengthWorkflow from ess.reduce.nexus.types import DetectorBankSizes, NeXusName from ess.reduce.parameter import parameter_mappers -from ess.reduce.time_of_flight import GenericTofWorkflow from ess.reduce.workflow import register_workflow from scippneutron.metadata import Software @@ -23,12 +23,12 @@ CaveMonitorPosition, # Should this be a DREAM-only parameter? EmptyCanRun, KeepEvents, + LookupTableFilename, LookupTableRelativeErrorThreshold, PixelMaskFilename, Position, ReducerSoftware, SampleRun, - TimeOfFlightLookupTableFilename, TofMask, TwoThetaMask, VanadiumRun, @@ -73,7 +73,7 @@ def _get_lookup_table_filename_from_configuration( configuration: InstrumentConfiguration, -) -> TimeOfFlightLookupTableFilename: +) -> LookupTableFilename: from .data import tof_lookup_table_high_flux match configuration: @@ -84,13 +84,13 @@ def _get_lookup_table_filename_from_configuration( case InstrumentConfiguration.high_resolution: raise NotImplementedError("High resolution configuration not yet supported") - return TimeOfFlightLookupTableFilename(out) + return LookupTableFilename(out) def _collect_reducer_software() -> ReducerSoftware: return ReducerSoftware( [ - Software.from_package_metadata('essdiffraction'), + # Software.from_package_metadata('essdiffraction'), Software.from_package_metadata('scippneutron'), Software.from_package_metadata('scipp'), ] @@ -100,7 +100,7 @@ def _collect_reducer_software() -> ReducerSoftware: def DreamWorkflow(**kwargs) -> sciline.Pipeline: """ Dream generic workflow with default parameters. - The workflow is based on the GenericTofWorkflow. + The workflow is based on the GenericWavelengthWorkflow. It can load data from a NeXus file recorded on the DREAM instrument, and can compute time-of-flight for the neutron events. @@ -111,9 +111,9 @@ def DreamWorkflow(**kwargs) -> sciline.Pipeline: ---------- kwargs: Additional keyword arguments are forwarded to the base - :func:`GenericTofWorkflow`. + :func:`GenericWavelengthWorkflow`. """ - wf = GenericTofWorkflow( + wf = GenericWavelengthWorkflow( run_types=[SampleRun, VanadiumRun, EmptyCanRun], monitor_types=[BunkerMonitor, CaveMonitor], **kwargs, diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index 42f21743..fb27a726 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -14,6 +14,8 @@ from .types import ( CalibrationData, CorrectedDetector, + DspacingDetector, + DspacingMonitor, ElasticCoordTransformGraph, EmptyCanSubtractedIntensityTof, EmptyCanSubtractedIofDspacing, @@ -25,8 +27,8 @@ Position, RunType, SampleRun, - TofDetector, - TofMonitor, + # TofDetector, + # TofMonitor, WavelengthDetector, WavelengthMonitor, ) @@ -98,31 +100,21 @@ def _consume_positions(position, sample_position, source_position): def to_dspacing_with_calibration( data: sc.DataArray, calibration: sc.Dataset, + graph: dict, ) -> sc.DataArray: """ Transform coordinates to d-spacing from calibration parameters. - - Computes d-spacing from time-of-flight stored in `data`. - - Attention - --------- - `data` may have a wavelength coordinate and dimension, - but those are discarded. - Only the stored time-of-flight is used, that is, any modifications to - the wavelength coordinate after it was computed from time-of-flight are lost. - - Raises - ------ - KeyError - If `data` does not contain a 'tof' coordinate. + Computes d-spacing from wavelength stored in `data`. Parameters ---------- data: - Input data in tof or wavelength dimension. - Must have a tof coordinate. + Input data in wavelength dimension. + Must have a wavelength coordinate. calibration: Calibration data. + graph: + Graph for the coordinate transformation, used to restore tof from wavelength. Returns ------- @@ -134,9 +126,10 @@ def to_dspacing_with_calibration( ess.powder.conversions.dspacing_from_diff_calibration """ out = merge_calibration(into=data, calibration=calibration) - out = _restore_tof_if_in_wavelength(out) + # Restore tof from wavelength + out = out.transform_coords("tof", graph=graph, keep_intermediate=False) - graph = {"dspacing": _dspacing_from_diff_calibration} + pos_graph = {"dspacing": _dspacing_from_diff_calibration} # `_dspacing_from_diff_calibration` does not need positions but conceptually, # the conversion maps from positions to d-spacing. # The mechanism with `_tag_positions_consumed` is meant to ensure that, @@ -145,10 +138,10 @@ def to_dspacing_with_calibration( if "position" in out.coords or ( out.bins is not None and "position" in out.bins.coords ): - graph["_tag_positions_consumed"] = _consume_positions + pos_graph["_tag_positions_consumed"] = _consume_positions else: - graph["_tag_positions_consumed"] = lambda: sc.scalar(0) - out = out.transform_coords("dspacing", graph=graph, keep_intermediate=False) + pos_graph["_tag_positions_consumed"] = lambda: sc.scalar(0) + out = out.transform_coords("dspacing", graph=pos_graph, keep_intermediate=False) out.coords.pop("_tag_positions_consumed", None) return CorrectedDetector[RunType](out) @@ -178,7 +171,7 @@ def powder_coordinate_transformation_graph( return ElasticCoordTransformGraph( { **scn.conversion.graph.beamline.beamline(scatter=True), - **scn.conversion.graph.tof.elastic("tof"), + **scn.conversion.graph.tof.elastic("kinematics"), 'source_position': lambda: source_position, 'sample_position': lambda: sample_position, 'gravity': lambda: gravity, @@ -186,27 +179,27 @@ def powder_coordinate_transformation_graph( ) -def _restore_tof_if_in_wavelength(data: sc.DataArray) -> sc.DataArray: - out = data.copy(deep=False) - outer = out.coords.get("wavelength", None) - if out.bins is not None: - binned = out.bins.coords.get("wavelength", None) - else: - binned = None +# def _restore_tof_from_wavelength(data: sc.DataArray) -> sc.DataArray: +# out = data.copy(deep=False) +# outer = out.coords.get("wavelength", None) +# if out.bins is not None: +# binned = out.bins.coords.get("wavelength", None) +# else: +# binned = None - if outer is not None or binned is not None: - get_logger().info("Discarded coordinate 'wavelength' in favor of 'tof'.") +# if outer is not None or binned is not None: +# get_logger().info("Discarded coordinate 'wavelength' in favor of 'tof'.") - if "wavelength" in out.dims: - out = out.rename_dims(wavelength="tof") - return out +# if "wavelength" in out.dims: +# out = out.rename_dims(wavelength="tof") +# return out def add_scattering_coordinates_from_positions( - data: TofDetector[RunType], + data: WavelengthDetector[RunType], graph: ElasticCoordTransformGraph[RunType], calibration: CalibrationData, -) -> WavelengthDetector[RunType]: +) -> DspacingDetector[RunType]: """ Add ``wavelength``, ``two_theta`` and ``dspacing`` coordinates to the data. The input ``data`` must have a ``tof`` coordinate, as well as the necessary @@ -226,7 +219,7 @@ def add_scattering_coordinates_from_positions( keep_intermediate=False, ) out = convert_to_dspacing(out, graph, calibration) - return WavelengthDetector[RunType](out) + return DspacingDetector[RunType](out) def convert_to_dspacing( @@ -237,7 +230,7 @@ def convert_to_dspacing( if calibration is None: out = data.transform_coords(["dspacing"], graph=graph, keep_intermediate=False) else: - out = to_dspacing_with_calibration(data, calibration=calibration) + out = to_dspacing_with_calibration(data, calibration=calibration, graph=graph) for key in ("wavelength", "two_theta"): if key in out.coords.keys(): out.coords.set_aligned(key, False) @@ -301,20 +294,20 @@ def powder_monitor_coordinate_transformation_graph( ) -def convert_monitor_to_wavelength( - monitor: TofMonitor[RunType, MonitorType], - graph: MonitorCoordTransformGraph[RunType], -) -> WavelengthMonitor[RunType, MonitorType]: - return WavelengthMonitor[RunType, MonitorType]( - monitor.transform_coords("wavelength", graph=graph, keep_intermediate=False) - ) +# def convert_monitor_to_wavelength( +# monitor: TofMonitor[RunType, MonitorType], +# graph: MonitorCoordTransformGraph[RunType], +# ) -> WavelengthMonitor[RunType, MonitorType]: +# return WavelengthMonitor[RunType, MonitorType]( +# monitor.transform_coords("wavelength", graph=graph, keep_intermediate=False) +# ) providers = ( add_scattering_coordinates_from_positions, convert_reduced_to_tof, convert_reduced_to_empty_can_subtracted_tof, - convert_monitor_to_wavelength, + # convert_monitor_to_wavelength, powder_coordinate_transformation_graph, powder_monitor_coordinate_transformation_graph, ) diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index db5b8065..d53fe588 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -14,12 +14,11 @@ import sciline import scipp as sc -from scippneutron.io import cif -from scippneutron.metadata import Person, Software - +from ess.reduce.kinematics import types as kin_t from ess.reduce.nexus import types as reduce_t -from ess.reduce.time_of_flight import types as tof_t from ess.reduce.uncertainty import UncertaintyBroadcastMode as _UncertaintyBroadcastMode +from scippneutron.io import cif +from scippneutron.metadata import Person, Software EmptyDetector = reduce_t.EmptyDetector EmptyMonitor = reduce_t.EmptyMonitor @@ -36,12 +35,12 @@ DetectorBankSizes = reduce_t.DetectorBankSizes -TofDetector = tof_t.TofDetector -TofMonitor = tof_t.TofMonitor -PulseStrideOffset = tof_t.PulseStrideOffset -TimeOfFlightLookupTable = tof_t.TimeOfFlightLookupTable -TimeOfFlightLookupTableFilename = tof_t.TimeOfFlightLookupTableFilename -LookupTableRelativeErrorThreshold = tof_t.LookupTableRelativeErrorThreshold +WavelengthDetector = kin_t.WavelengthDetector +WavelengthMonitor = kin_t.WavelengthMonitor +PulseStrideOffset = kin_t.PulseStrideOffset +LookupTable = kin_t.LookupTable +LookupTableFilename = kin_t.LookupTableFilename +LookupTableRelativeErrorThreshold = kin_t.LookupTableRelativeErrorThreshold SampleRun = reduce_t.SampleRun VanadiumRun = reduce_t.VanadiumRun @@ -95,7 +94,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """Detector calibration data.""" -class WavelengthDetector(sciline.Scope[RunType, sc.DataArray], sc.DataArray): +class DspacingDetector(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Data with scattering coordinates computed for all events: wavelength, 2theta, d-spacing.""" @@ -170,9 +169,7 @@ class MonitorFilename(sciline.Scope[RunType, Path], Path): """ -class WavelengthMonitor( - sciline.Scope[RunType, MonitorType, sc.DataArray], sc.DataArray -): +class DspacingMonitor(sciline.Scope[RunType, MonitorType, sc.DataArray], sc.DataArray): """Monitor histogram in wavelength.""" From 82aebc5b8272c10d3e2f9c0eaf4d786a9d5dacbc Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Thu, 26 Feb 2026 08:46:51 +0100 Subject: [PATCH 05/27] update notebooks --- .../dream-advanced-powder-reduction.ipynb | 14 ++++- .../dream/dream-make-tof-lookup-table.ipynb | 38 +++++++------ .../dream/dream-powder-reduction.ipynb | 53 +++++++++++++------ 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb b/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb index bb3ca987..cb98646e 100644 --- a/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb @@ -65,6 +65,7 @@ "workflow[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -186,6 +187,7 @@ "workflow[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -261,6 +263,7 @@ "workflow[CalibrationFilename] = None\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -375,6 +378,7 @@ "workflow[CalibrationFilename] = None\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", "# Edges for binning in d-spacing:\n", @@ -610,6 +614,14 @@ " vmin=1e-3,\n", ")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd340637-933f-4c73-b996-285b7394bd03", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -628,7 +640,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.12.12" } }, "nbformat": 4, diff --git a/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb b/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb index 3a1515d8..8edefb6b 100644 --- a/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb +++ b/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb @@ -5,9 +5,9 @@ "id": "0", "metadata": {}, "source": [ - "# Create a time-of-flight lookup table for DREAM\n", + "# Create a wavelength lookup table for DREAM\n", "\n", - "This notebook shows how to create a time-of-flight lookup table for the DREAM instrument." + "This notebook shows how to create a wavelength lookup table for the DREAM instrument." ] }, { @@ -18,7 +18,7 @@ "outputs": [], "source": [ "import scipp as sc\n", - "from ess.reduce import time_of_flight\n", + "from ess.reduce import kinematics as kin\n", "from ess.reduce.nexus.types import AnyRun\n", "from ess.dream.beamline import InstrumentConfiguration, choppers" ] @@ -60,17 +60,17 @@ "metadata": {}, "outputs": [], "source": [ - "wf = time_of_flight.TofLookupTableWorkflow()\n", + "wf = kin.LookupTableWorkflow()\n", "\n", - "wf[time_of_flight.LtotalRange] = sc.scalar(5.0, unit=\"m\"), sc.scalar(80.0, unit=\"m\")\n", - "wf[time_of_flight.NumberOfSimulatedNeutrons] = 200_000 # Increase this number for more reliable results\n", - "wf[time_of_flight.SourcePosition] = sc.vector([0, 0, 0], unit='m')\n", - "wf[time_of_flight.DiskChoppers[AnyRun]] = disk_choppers\n", - "wf[time_of_flight.DistanceResolution] = sc.scalar(0.1, unit=\"m\")\n", - "wf[time_of_flight.TimeResolution] = sc.scalar(250.0, unit='us')\n", - "wf[time_of_flight.PulsePeriod] = 1.0 / sc.scalar(14.0, unit=\"Hz\")\n", - "wf[time_of_flight.PulseStride] = 1\n", - "wf[time_of_flight.PulseStrideOffset] = None" + "wf[kin.LtotalRange] = sc.scalar(5.0, unit=\"m\"), sc.scalar(80.0, unit=\"m\")\n", + "wf[kin.NumberOfSimulatedNeutrons] = 5_000_000 # Increase this number for more reliable results\n", + "wf[kin.SourcePosition] = sc.vector([0, 0, 0], unit='m')\n", + "wf[kin.DiskChoppers[AnyRun]] = disk_choppers\n", + "wf[kin.DistanceResolution] = sc.scalar(0.1, unit=\"m\")\n", + "wf[kin.TimeResolution] = sc.scalar(250.0, unit='us')\n", + "wf[kin.PulsePeriod] = 1.0 / sc.scalar(14.0, unit=\"Hz\")\n", + "wf[kin.PulseStride] = 1\n", + "wf[kin.PulseStrideOffset] = None" ] }, { @@ -88,7 +88,7 @@ "metadata": {}, "outputs": [], "source": [ - "table = wf.compute(time_of_flight.TimeOfFlightLookupTable)\n", + "table = wf.compute(kin.LookupTable)\n", "table.array" ] }, @@ -117,8 +117,16 @@ "metadata": {}, "outputs": [], "source": [ - "table.save_hdf5('DREAM-high-flux-tof-lut-5m-80m.h5')" + "table.save_hdf5('DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5')" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a43b15e7-a225-4e73-b037-78f01dbefcf7", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/docs/user-guide/dream/dream-powder-reduction.ipynb b/docs/user-guide/dream/dream-powder-reduction.ipynb index b067bca1..18c65f51 100644 --- a/docs/user-guide/dream/dream-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-powder-reduction.ipynb @@ -56,7 +56,7 @@ "metadata": {}, "outputs": [], "source": [ - "workflow = dream.DreamGeant4Workflow(\n", + "wf = dream.DreamGeant4Workflow(\n", " run_norm=powder.RunNormalization.monitor_histogram,\n", ")" ] @@ -77,26 +77,27 @@ "metadata": {}, "outputs": [], "source": [ - "workflow[Filename[SampleRun]] = dream.data.simulated_diamond_sample()\n", - "workflow[Filename[VanadiumRun]] = dream.data.simulated_vanadium_sample()\n", - "workflow[Filename[EmptyCanRun]] = dream.data.simulated_empty_can()\n", - "workflow[CalibrationFilename] = None\n", + "wf[Filename[SampleRun]] = dream.data.simulated_diamond_sample()\n", + "wf[Filename[VanadiumRun]] = dream.data.simulated_vanadium_sample()\n", + "wf[Filename[EmptyCanRun]] = dream.data.simulated_empty_can()\n", + "wf[CalibrationFilename] = None\n", "\n", - "workflow[MonitorFilename[SampleRun]] = dream.data.simulated_monitor_diamond_sample()\n", - "workflow[MonitorFilename[VanadiumRun]] = dream.data.simulated_monitor_vanadium_sample()\n", - "workflow[MonitorFilename[EmptyCanRun]] = dream.data.simulated_monitor_empty_can()\n", - "workflow[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", + "wf[MonitorFilename[SampleRun]] = dream.data.simulated_monitor_diamond_sample()\n", + "wf[MonitorFilename[VanadiumRun]] = dream.data.simulated_monitor_vanadium_sample()\n", + "wf[MonitorFilename[EmptyCanRun]] = dream.data.simulated_monitor_empty_can()\n", + "wf[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", - "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "wf[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "wf[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", - "workflow[NeXusDetectorName] = \"mantle\"\n", + "wf[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", - "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", + "wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", "# Edges for binning in d-spacing:\n", - "workflow[DspacingBins] = sc.linspace(\"dspacing\", 0.3, 2.3434, 201, unit=\"angstrom\")\n", + "wf[DspacingBins] = sc.linspace(\"dspacing\", 0.3, 2.3434, 201, unit=\"angstrom\")\n", "\n", "# Do not mask any pixels / voxels:\n", - "workflow = powder.with_pixel_mask_filenames(workflow, [])" + "wf = powder.with_pixel_mask_filenames(wf, [])" ] }, { @@ -114,6 +115,26 @@ "If we didn't want to subtract an empty can measurement from the sample measurement, we would instead request `IofDspacing[SampleRun]` and `ReducedTofCIF`." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c212c16-08dc-4582-891c-7938b2cc75bd", + "metadata": {}, + "outputs": [], + "source": [ + "wf.compute(LookupTableRelativeErrorThreshold)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40e1be11-41f0-4839-9efa-e3e1de650808", + "metadata": {}, + "outputs": [], + "source": [ + "wf.visualize(ReducedEmptyCanSubtractedTofCIF, graph_attr={\"rankdir\": \"LR\"})" + ] + }, { "cell_type": "code", "execution_count": null, @@ -121,7 +142,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = workflow.compute([\n", + "results = wf.compute([\n", " EmptyCanSubtractedIofDspacing,\n", " ReducedEmptyCanSubtractedTofCIF\n", "])\n", @@ -229,7 +250,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.10" + "version": "3.12.12" } }, "nbformat": 4, From edb1ad983fd1adbcf80002fccf39ce12a5bd29d0 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 13 Mar 2026 15:39:09 +0100 Subject: [PATCH 06/27] updates for latest version of workflow --- src/ess/dream/workflows.py | 8 ++++---- src/ess/powder/conversion.py | 4 ++-- src/ess/powder/types.py | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ess/dream/workflows.py b/src/ess/dream/workflows.py index c5984e26..7d344535 100644 --- a/src/ess/dream/workflows.py +++ b/src/ess/dream/workflows.py @@ -6,9 +6,9 @@ import sciline import scipp as sc import scippnexus as snx -from ess.reduce.kinematics import GenericWavelengthWorkflow from ess.reduce.nexus.types import DetectorBankSizes, NeXusName from ess.reduce.parameter import parameter_mappers +from ess.reduce.unwrap import GenericUnwrapWorkflow from ess.reduce.workflow import register_workflow from scippneutron.metadata import Software @@ -99,7 +99,7 @@ def _collect_reducer_software() -> ReducerSoftware: def DreamWorkflow(**kwargs) -> sciline.Pipeline: """ Dream generic workflow with default parameters. - The workflow is based on the GenericWavelengthWorkflow. + The workflow is based on the GenericUnwrapWorkflow. It can load data from a NeXus file recorded on the DREAM instrument, and can compute time-of-flight for the neutron events. @@ -110,9 +110,9 @@ def DreamWorkflow(**kwargs) -> sciline.Pipeline: ---------- kwargs: Additional keyword arguments are forwarded to the base - :func:`GenericWavelengthWorkflow`. + :func:`GenericUnwrapWorkflow`. """ - wf = GenericWavelengthWorkflow( + wf = GenericUnwrapWorkflow( run_types=[SampleRun, VanadiumRun, EmptyCanRun], monitor_types=[BunkerMonitor, CaveMonitor], **kwargs, diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index b647c5cb..6008b42c 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -171,7 +171,7 @@ def powder_coordinate_transformation_graph( return ElasticCoordTransformGraph( { **scn.conversion.graph.beamline.beamline(scatter=True), - **scn.conversion.graph.kinematics.elastic("wavelength"), + **scn.conversion.graph.tof.elastic("wavelength"), 'source_position': lambda: source_position, 'sample_position': lambda: sample_position, 'gravity': lambda: gravity, @@ -286,7 +286,7 @@ def powder_monitor_coordinate_transformation_graph( return MonitorCoordTransformGraph( { **scn.conversion.graph.beamline.beamline(scatter=False), - **scn.conversion.graph.kinematics.elastic("wavelength"), + **scn.conversion.graph.tof.elastic("wavelength"), 'source_position': lambda: source_position, 'sample_position': lambda: sample_position, 'gravity': lambda: gravity, diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index d53fe588..65d12b2e 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -14,9 +14,9 @@ import sciline import scipp as sc -from ess.reduce.kinematics import types as kin_t from ess.reduce.nexus import types as reduce_t from ess.reduce.uncertainty import UncertaintyBroadcastMode as _UncertaintyBroadcastMode +from ess.reduce.unwrap import types as unwrap_t from scippneutron.io import cif from scippneutron.metadata import Person, Software @@ -35,12 +35,12 @@ DetectorBankSizes = reduce_t.DetectorBankSizes -WavelengthDetector = kin_t.WavelengthDetector -WavelengthMonitor = kin_t.WavelengthMonitor -PulseStrideOffset = kin_t.PulseStrideOffset -LookupTable = kin_t.LookupTable -LookupTableFilename = kin_t.LookupTableFilename -LookupTableRelativeErrorThreshold = kin_t.LookupTableRelativeErrorThreshold +WavelengthDetector = unwrap_t.WavelengthDetector +WavelengthMonitor = unwrap_t.WavelengthMonitor +PulseStrideOffset = unwrap_t.PulseStrideOffset +LookupTable = unwrap_t.LookupTable +LookupTableFilename = unwrap_t.LookupTableFilename +LookupTableRelativeErrorThreshold = unwrap_t.LookupTableRelativeErrorThreshold SampleRun = reduce_t.SampleRun VanadiumRun = reduce_t.VanadiumRun From b18d2666eec374a0c0f18a20b4d6b8f3d4f774c8 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 14:21:18 +0100 Subject: [PATCH 07/27] add new wavelength tables to data registry and update advanced notebook --- .../dream-advanced-powder-reduction.ipynb | 12 ------- src/ess/dream/data.py | 36 +++++++++++++++++++ src/ess/dream/workflows.py | 6 ++-- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb b/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb index cb98646e..f02589fe 100644 --- a/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb @@ -65,7 +65,6 @@ "workflow[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", - "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -187,7 +186,6 @@ "workflow[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", - "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -263,7 +261,6 @@ "workflow[CalibrationFilename] = None\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", - "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -378,7 +375,6 @@ "workflow[CalibrationFilename] = None\n", "\n", "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", - "workflow[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", "# Edges for binning in d-spacing:\n", @@ -614,14 +610,6 @@ " vmin=1e-3,\n", ")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd340637-933f-4c73-b996-285b7394bd03", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/src/ess/dream/data.py b/src/ess/dream/data.py index 1e8136eb..33f43908 100644 --- a/src/ess/dream/data.py +++ b/src/ess/dream/data.py @@ -40,6 +40,9 @@ # `shrink_nexus.py` script in the `tools` folder at the top level of the # `essdiffraction` repository. "TEST_DREAM_nexus_sorted-2023-12-07.nxs": "md5:599b426a93c46a7b4b09a874bf288c53", # noqa: E501 + # Wavelength lookup tables + "DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5": "md5:10c80c9de311cfa246f7b2c165eb0b49", # noqa: E501 + "DREAM-high-flux-wavelength-lut-5m-80m-bc240.h5": "md5:9741176f8da9b34c2a15967a43e21462", # noqa: E501 }, ) @@ -293,3 +296,36 @@ def tof_lookup_table_high_flux(bc: Literal[215, 240] = 215) -> Path: return get_path("DREAM-high-flux-tof-lut-5m-80m-bc240.h5") case _: raise ValueError(f"Unsupported band-control chopper (BC) value: {bc}") + + +def lookup_table_high_flux(bc: Literal[215, 240] = 215) -> Path: + """Path to a HDF5 file containing a wavelength lookup table for high-flux mode. + + The table was created using the ``tof`` package and the chopper settings for the + DREAM instrument in high-resolution mode. + Can return tables for two different band-control chopper (BC) settings: + - ``bc=215``: corresponds to the settings of the choppers in the tutorial data. + - ``bc=240``: a setting with less time overlap between frames. + + Note that the phase of the band-control chopper (BCC) was set to 215 degrees in the + Geant4 simulation which generated the data used in the documentation notebooks. + This has since been found to be non-optimal as it leads to time overlap between the + two frames, and a value of 240 degrees is now recommended. + + This table was computed using `Create a wavelength lookup table for DREAM + <../../user-guide/dream/dream-make-wavelength-lookup-table.rst>`_ + with ``NumberOfSimulatedNeutrons = 5_000_000``. + + Parameters + ---------- + bc: + Band-control chopper (BC) setting. The default is 215, which corresponds to the + settings of the choppers in the tutorial data. + """ + match bc: + case 215: + return get_path("DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5") + case 240: + return get_path("DREAM-high-flux-wavelength-lut-5m-80m-bc240.h5") + case _: + raise ValueError(f"Unsupported band-control chopper (BC) value: {bc}") diff --git a/src/ess/dream/workflows.py b/src/ess/dream/workflows.py index 7d344535..e8124fdb 100644 --- a/src/ess/dream/workflows.py +++ b/src/ess/dream/workflows.py @@ -73,13 +73,13 @@ def _get_lookup_table_filename_from_configuration( configuration: InstrumentConfiguration, ) -> LookupTableFilename: - from .data import tof_lookup_table_high_flux + from .data import lookup_table_high_flux match configuration: case InstrumentConfiguration.high_flux_BC215: - out = tof_lookup_table_high_flux(bc=215) + out = lookup_table_high_flux(bc=215) case InstrumentConfiguration.high_flux_BC240: - out = tof_lookup_table_high_flux(bc=240) + out = lookup_table_high_flux(bc=240) case InstrumentConfiguration.high_resolution: raise NotImplementedError("High resolution configuration not yet supported") From 7cb2f461c1b62af472c5a3bd43cbc3e10cd81313 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 14:23:28 +0100 Subject: [PATCH 08/27] replace notebook to generate lookup table --- ... dream-make-wavelength-lookup-table.ipynb} | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) rename docs/user-guide/dream/{dream-make-tof-lookup-table.ipynb => dream-make-wavelength-lookup-table.ipynb} (71%) diff --git a/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb b/docs/user-guide/dream/dream-make-wavelength-lookup-table.ipynb similarity index 71% rename from docs/user-guide/dream/dream-make-tof-lookup-table.ipynb rename to docs/user-guide/dream/dream-make-wavelength-lookup-table.ipynb index 8edefb6b..05047add 100644 --- a/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb +++ b/docs/user-guide/dream/dream-make-wavelength-lookup-table.ipynb @@ -18,7 +18,7 @@ "outputs": [], "source": [ "import scipp as sc\n", - "from ess.reduce import kinematics as kin\n", + "from ess.reduce import unwrap\n", "from ess.reduce.nexus.types import AnyRun\n", "from ess.dream.beamline import InstrumentConfiguration, choppers" ] @@ -40,7 +40,7 @@ "metadata": {}, "outputs": [], "source": [ - "disk_choppers = choppers(InstrumentConfiguration.high_flux_BC215)" + "disk_choppers = choppers(InstrumentConfiguration.high_flux_BC240)" ] }, { @@ -60,17 +60,17 @@ "metadata": {}, "outputs": [], "source": [ - "wf = kin.LookupTableWorkflow()\n", + "wf = unwrap.LookupTableWorkflow()\n", "\n", - "wf[kin.LtotalRange] = sc.scalar(5.0, unit=\"m\"), sc.scalar(80.0, unit=\"m\")\n", - "wf[kin.NumberOfSimulatedNeutrons] = 5_000_000 # Increase this number for more reliable results\n", - "wf[kin.SourcePosition] = sc.vector([0, 0, 0], unit='m')\n", - "wf[kin.DiskChoppers[AnyRun]] = disk_choppers\n", - "wf[kin.DistanceResolution] = sc.scalar(0.1, unit=\"m\")\n", - "wf[kin.TimeResolution] = sc.scalar(250.0, unit='us')\n", - "wf[kin.PulsePeriod] = 1.0 / sc.scalar(14.0, unit=\"Hz\")\n", - "wf[kin.PulseStride] = 1\n", - "wf[kin.PulseStrideOffset] = None" + "wf[unwrap.LtotalRange] = sc.scalar(5.0, unit=\"m\"), sc.scalar(80.0, unit=\"m\")\n", + "wf[unwrap.NumberOfSimulatedNeutrons] = 200_000 # Increase this number for more reliable results\n", + "wf[unwrap.SourcePosition] = sc.vector([0, 0, 0], unit='m')\n", + "wf[unwrap.DiskChoppers[AnyRun]] = disk_choppers\n", + "wf[unwrap.DistanceResolution] = sc.scalar(0.1, unit=\"m\")\n", + "wf[unwrap.TimeResolution] = sc.scalar(250.0, unit='us')\n", + "wf[unwrap.PulsePeriod] = 1.0 / sc.scalar(14.0, unit=\"Hz\")\n", + "wf[unwrap.PulseStride] = 1\n", + "wf[unwrap.PulseStrideOffset] = None" ] }, { @@ -88,7 +88,7 @@ "metadata": {}, "outputs": [], "source": [ - "table = wf.compute(kin.LookupTable)\n", + "table = wf.compute(unwrap.LookupTable)\n", "table.array" ] }, @@ -117,16 +117,8 @@ "metadata": {}, "outputs": [], "source": [ - "table.save_hdf5('DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5')" + "table.save_hdf5('DREAM-high-flux-wavelength-lut-5m-80m-bc240.h5')" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a43b15e7-a225-4e73-b037-78f01dbefcf7", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 5533122b48abc4c5884ca498b52fab7ed9aab0e0 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 14:25:57 +0100 Subject: [PATCH 09/27] cleanup simple powder notebook --- .../dream/dream-powder-reduction.ipynb | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/docs/user-guide/dream/dream-powder-reduction.ipynb b/docs/user-guide/dream/dream-powder-reduction.ipynb index 18c65f51..5a3425f9 100644 --- a/docs/user-guide/dream/dream-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-powder-reduction.ipynb @@ -88,7 +88,6 @@ "wf[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", "wf[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", - "wf[LookupTableFilename] = \"DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5\"\n", "# Select a detector bank:\n", "wf[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", @@ -115,26 +114,6 @@ "If we didn't want to subtract an empty can measurement from the sample measurement, we would instead request `IofDspacing[SampleRun]` and `ReducedTofCIF`." ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "5c212c16-08dc-4582-891c-7938b2cc75bd", - "metadata": {}, - "outputs": [], - "source": [ - "wf.compute(LookupTableRelativeErrorThreshold)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "40e1be11-41f0-4839-9efa-e3e1de650808", - "metadata": {}, - "outputs": [], - "source": [ - "wf.visualize(ReducedEmptyCanSubtractedTofCIF, graph_attr={\"rankdir\": \"LR\"})" - ] - }, { "cell_type": "code", "execution_count": null, @@ -166,9 +145,7 @@ "outputs": [], "source": [ "histogram = intensity.hist()\n", - "fig = histogram.plot(title=intensity.coords['detector'].value.capitalize())\n", - "fig.ax.set_ylabel(f\"I(d) [{histogram.unit}]\")\n", - "fig" + "histogram.plot(title=intensity.coords['detector'].value.capitalize(), ylabel=f\"I(d) [{histogram.unit}]\")" ] }, { From 2b6da84ca939ba7b42bf246dde3a7ad8c0d4cf3a Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 14:27:56 +0100 Subject: [PATCH 10/27] cleanup conversion file --- src/ess/powder/conversion.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index 6008b42c..234e9aed 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -10,12 +10,10 @@ from .calibration import OutputCalibrationData from .correction import merge_calibration -from .logging import get_logger from .types import ( CalibrationData, CorrectedDetector, DspacingDetector, - DspacingMonitor, ElasticCoordTransformGraph, EmptyCanSubtractedIntensityTof, EmptyCanSubtractedIofDspacing, @@ -23,14 +21,10 @@ IntensityDspacing, IntensityTof, MonitorCoordTransformGraph, - MonitorType, Position, RunType, SampleRun, - # TofDetector, - # TofMonitor, WavelengthDetector, - WavelengthMonitor, ) @@ -179,22 +173,6 @@ def powder_coordinate_transformation_graph( ) -# def _restore_tof_from_wavelength(data: sc.DataArray) -> sc.DataArray: -# out = data.copy(deep=False) -# outer = out.coords.get("wavelength", None) -# if out.bins is not None: -# binned = out.bins.coords.get("wavelength", None) -# else: -# binned = None - -# if outer is not None or binned is not None: -# get_logger().info("Discarded coordinate 'wavelength' in favor of 'tof'.") - -# if "wavelength" in out.dims: -# out = out.rename_dims(wavelength="tof") -# return out - - def add_scattering_coordinates_from_positions( data: WavelengthDetector[RunType], graph: ElasticCoordTransformGraph[RunType], @@ -294,20 +272,10 @@ def powder_monitor_coordinate_transformation_graph( ) -# def convert_monitor_to_wavelength( -# monitor: TofMonitor[RunType, MonitorType], -# graph: MonitorCoordTransformGraph[RunType], -# ) -> WavelengthMonitor[RunType, MonitorType]: -# return WavelengthMonitor[RunType, MonitorType]( -# monitor.transform_coords("wavelength", graph=graph, keep_intermediate=False) -# ) - - providers = ( add_scattering_coordinates_from_positions, convert_reduced_to_tof, convert_reduced_to_empty_can_subtracted_tof, - # convert_monitor_to_wavelength, powder_coordinate_transformation_graph, powder_monitor_coordinate_transformation_graph, ) From 0ab33eb8f2b5c80d1e3cca1b70ca0962c6bd9e35 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 14:47:05 +0100 Subject: [PATCH 11/27] fix types in beer and start fixing calibration to use unwrapped eto --- src/ess/beer/types.py | 9 ++++-- src/ess/powder/conversion.py | 10 ++++--- tests/beer/mcstas_reduction_test.py | 5 ++-- tests/dream/geant4_reduction_test.py | 41 ++++++++++++++-------------- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/ess/beer/types.py b/src/ess/beer/types.py index c4655bf5..cd29ca54 100644 --- a/src/ess/beer/types.py +++ b/src/ess/beer/types.py @@ -12,9 +12,7 @@ import sciline import scipp as sc - from ess.reduce.nexus.types import Filename, RawDetector, RunType, SampleRun -from ess.reduce.time_of_flight.types import TofDetector class StreakClusteredData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): @@ -24,7 +22,6 @@ class StreakClusteredData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): RawDetector = RawDetector Filename = Filename SampleRun = SampleRun -TofDetector = TofDetector class DetectorBank(Enum): @@ -58,3 +55,9 @@ class DetectorBank(Enum): CIFPeaksMinIntensity = NewType("CIFPeaksMinIntensity", sc.Variable) """Minimum peak intensity for peaks from CIF file to be included in :py:`DHKLList`.""" + + +class TofDetector(sciline.Scope[RunType, sc.DataArray], sc.DataArray): + """ + Detector with a time-of-flight coordinate + """ diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index 234e9aed..eda605bb 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -60,7 +60,7 @@ def _dspacing_from_diff_calibration_a0_impl(t, t0, c): def _dspacing_from_diff_calibration( - tof: sc.Variable, + unwrapped_eto: sc.Variable, tzero: sc.Variable, difa: sc.Variable, difc: sc.Variable, @@ -71,7 +71,7 @@ def _dspacing_from_diff_calibration( d-spacing is the positive solution of - .. math:: \mathsf{tof} = \mathsf{DIFA} * d^2 + \mathsf{DIFC} * d + t_0 + .. math:: \mathsf{eto} = \mathsf{DIFA} * d^2 + \mathsf{DIFC} * d + t_0 This function can be used with :func:`scipp.transform_coords`. @@ -80,8 +80,10 @@ def _dspacing_from_diff_calibration( ess.powder.conversions.to_dspacing_with_calibration """ if sc.all(difa == sc.scalar(0.0, unit=difa.unit)).value: - return _dspacing_from_diff_calibration_a0_impl(tof, tzero, difc) - return _dspacing_from_diff_calibration_generic_impl(tof, tzero, difa, difc) + return _dspacing_from_diff_calibration_a0_impl(unwrapped_eto, tzero, difc) + return _dspacing_from_diff_calibration_generic_impl( + unwrapped_eto, tzero, difa, difc + ) def _consume_positions(position, sample_position, source_position): diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index 40caec52..32ac0875 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -1,6 +1,7 @@ import numpy as np import scipp as sc import scippneutron as scn +from ess.reduce.nexus.types import Filename, SampleRun from scipp.testing import assert_allclose from ess.beer import ( @@ -15,9 +16,7 @@ mcstas_silicon_new_model, ) from ess.beer.io import load_beer_mcstas, load_beer_mcstas_monitor -from ess.beer.types import DetectorBank, DHKLList -from ess.reduce.nexus.types import Filename, SampleRun -from ess.reduce.time_of_flight.types import TofDetector +from ess.beer.types import DetectorBank, DHKLList, TofDetector def test_can_reduce_using_known_peaks_workflow(): diff --git a/tests/dream/geant4_reduction_test.py b/tests/dream/geant4_reduction_test.py index e4a11ddc..6aee136d 100644 --- a/tests/dream/geant4_reduction_test.py +++ b/tests/dream/geant4_reduction_test.py @@ -8,6 +8,9 @@ import sciline import scipp as sc import scipp.testing +from ess.reduce import unwrap +from ess.reduce import workflow as reduce_workflow +from ess.reduce.nexus.types import AnyRun from scippneutron import metadata from scippneutron._utils import elem_unit @@ -31,12 +34,12 @@ IntensityDspacingTwoTheta, IntensityTof, KeepEvents, + LookupTable, + LookupTableFilename, MonitorFilename, NeXusDetectorName, ReducedTofCIF, SampleRun, - TimeOfFlightLookupTable, - TimeOfFlightLookupTableFilename, TofMask, TwoThetaBins, TwoThetaMask, @@ -44,9 +47,6 @@ VanadiumRun, WavelengthMask, ) -from ess.reduce import time_of_flight -from ess.reduce import workflow as reduce_workflow -from ess.reduce.nexus.types import AnyRun params = { Filename[SampleRun]: dream.data.simulated_diamond_sample(small=True), @@ -59,8 +59,10 @@ CalibrationFilename: None, UncertaintyBroadcastMode: UncertaintyBroadcastMode.drop, DspacingBins: sc.linspace('dspacing', 0.0, 2.3434, 201, unit='angstrom'), - TofMask: lambda x: (x < sc.scalar(0.0, unit='us').to(unit=elem_unit(x))) - | (x > sc.scalar(86e3, unit='us').to(unit=elem_unit(x))), + TofMask: lambda x: ( + (x < sc.scalar(0.0, unit='us').to(unit=elem_unit(x))) + | (x > sc.scalar(86e3, unit='us').to(unit=elem_unit(x))) + ), TwoThetaMask: None, WavelengthMask: None, CIFAuthors: CIFAuthors( @@ -112,7 +114,7 @@ def test_pipeline_can_compute_dspacing_result_without_empty_can(workflow): def test_pipeline_can_compute_dspacing_result_using_lookup_table_filename(workflow): workflow = powder.with_pixel_mask_filenames(workflow, []) - workflow[TimeOfFlightLookupTableFilename] = dream.data.tof_lookup_table_high_flux() + workflow[LookupTableFilename] = dream.data.tof_lookup_table_high_flux() result = workflow.compute(EmptyCanSubtractedIofDspacing) assert result.sizes == {'dspacing': len(params[DspacingBins]) - 1} assert sc.identical(result.coords['dspacing'], params[DspacingBins]) @@ -120,29 +122,28 @@ def test_pipeline_can_compute_dspacing_result_using_lookup_table_filename(workfl @pytest.fixture(scope="module") def dream_tof_lookup_table(): - lut_wf = time_of_flight.TofLookupTableWorkflow() - lut_wf[time_of_flight.DiskChoppers[AnyRun]] = dream.beamline.choppers( + lut_wf = unwrap.LookupTableWorkflow() + lut_wf[unwrap.DiskChoppers[AnyRun]] = dream.beamline.choppers( dream.beamline.InstrumentConfiguration.high_flux_BC215 ) - lut_wf[time_of_flight.SourcePosition] = sc.vector(value=[0, 0, -76.55], unit="m") - lut_wf[time_of_flight.NumberOfSimulatedNeutrons] = 500_000 - lut_wf[time_of_flight.SimulationSeed] = 555 - lut_wf[time_of_flight.PulseStride] = 1 - lut_wf[time_of_flight.LtotalRange] = ( + lut_wf[unwrap.SourcePosition] = sc.vector(value=[0, 0, -76.55], unit="m") + lut_wf[unwrap.NumberOfSimulatedNeutrons] = 500_000 + lut_wf[unwrap.SimulationSeed] = 555 + lut_wf[unwrap.PulseStride] = 1 + lut_wf[unwrap.LtotalRange] = ( sc.scalar(60.0, unit="m"), sc.scalar(80.0, unit="m"), ) - lut_wf[time_of_flight.DistanceResolution] = sc.scalar(0.1, unit="m") - lut_wf[time_of_flight.TimeResolution] = sc.scalar(250.0, unit='us') - lut_wf[time_of_flight.LookupTableRelativeErrorThreshold] = 0.02 - return lut_wf.compute(time_of_flight.TimeOfFlightLookupTable) + lut_wf[unwrap.DistanceResolution] = sc.scalar(0.1, unit="m") + lut_wf[unwrap.TimeResolution] = sc.scalar(250.0, unit='us') + return lut_wf.compute(unwrap.LookupTable) def test_pipeline_can_compute_dspacing_result_using_custom_built_tof_lookup( workflow, dream_tof_lookup_table ): workflow = powder.with_pixel_mask_filenames(workflow, []) - workflow[TimeOfFlightLookupTable] = dream_tof_lookup_table + workflow[LookupTable] = dream_tof_lookup_table result = workflow.compute(IntensityDspacing[SampleRun]) assert result.sizes == {'dspacing': len(params[DspacingBins]) - 1} From b426ce164eab25c5fe55884420a77950756494c6 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 16:02:50 +0100 Subject: [PATCH 12/27] add manual conversion to wavelength for powgen --- src/ess/powder/conversion.py | 16 ++++++++----- src/ess/powder/types.py | 2 +- src/ess/snspowder/powgen/data.py | 40 ++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index eda605bb..826793ef 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -60,7 +60,9 @@ def _dspacing_from_diff_calibration_a0_impl(t, t0, c): def _dspacing_from_diff_calibration( - unwrapped_eto: sc.Variable, + # TODO: should not be tof here but a time-of-arrival + # See https://github.com/scipp/essdiffraction/issues/255 + tof: sc.Variable, tzero: sc.Variable, difa: sc.Variable, difc: sc.Variable, @@ -71,7 +73,7 @@ def _dspacing_from_diff_calibration( d-spacing is the positive solution of - .. math:: \mathsf{eto} = \mathsf{DIFA} * d^2 + \mathsf{DIFC} * d + t_0 + .. math:: \mathsf{tof} = \mathsf{DIFA} * d^2 + \mathsf{DIFC} * d + t_0 This function can be used with :func:`scipp.transform_coords`. @@ -80,10 +82,8 @@ def _dspacing_from_diff_calibration( ess.powder.conversions.to_dspacing_with_calibration """ if sc.all(difa == sc.scalar(0.0, unit=difa.unit)).value: - return _dspacing_from_diff_calibration_a0_impl(unwrapped_eto, tzero, difc) - return _dspacing_from_diff_calibration_generic_impl( - unwrapped_eto, tzero, difa, difc - ) + return _dspacing_from_diff_calibration_a0_impl(tof, tzero, difc) + return _dspacing_from_diff_calibration_generic_impl(tof, tzero, difa, difc) def _consume_positions(position, sample_position, source_position): @@ -122,6 +122,10 @@ def to_dspacing_with_calibration( ess.powder.conversions.dspacing_from_diff_calibration """ out = merge_calibration(into=data, calibration=calibration) + + # TODO: we should not be restoring tof here, as the calibration should be converting + # a time of arrival to d-spacing, and not a tof. + # We defer this to a later step: https://github.com/scipp/essdiffraction/issues/255 # Restore tof from wavelength out = out.transform_coords("tof", graph=graph, keep_intermediate=False) diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index 65d12b2e..d7f4afe9 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -194,7 +194,7 @@ class RawDataAndMetadata(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): TofMask = NewType("TofMask", Callable | None) -"""TofMask is a callable that returns a mask for a given TofData.""" +"""TofMask is a callable that returns a mask for masking time-of-flight regions.""" TwoThetaMask = NewType("TwoThetaMask", Callable | None) diff --git a/src/ess/snspowder/powgen/data.py b/src/ess/snspowder/powgen/data.py index 140d3a58..8be882b4 100644 --- a/src/ess/snspowder/powgen/data.py +++ b/src/ess/snspowder/powgen/data.py @@ -5,22 +5,38 @@ from pathlib import Path +import sciline as sl import scipp as sc import scippnexus as snx +from ess.reduce.data import Entry, make_registry from ess.powder.types import ( AccumulatedProtonCharge, CalibrationData, CalibrationFilename, DetectorBankSizes, + ElasticCoordTransformGraph, Filename, + MonitorCoordTransformGraph, + MonitorType, Position, ProtonCharge, RawDataAndMetadata, RunType, - TofDetector, + WavelengthDetector, + WavelengthMonitor, ) -from ess.reduce.data import Entry, make_registry + + +class TofDetector(sl.Scope[RunType, sc.DataArray], sc.DataArray): + """ + Detector with a time-of-flight coordinate + """ + + +class TofMonitor(sl.Scope[RunType, MonitorType, sc.DataArray], sc.DataArray): + """Monitor data with time-of-flight coordinate.""" + _registry = make_registry( "ess/powgen", @@ -234,6 +250,24 @@ def sample_position(dg: RawDataAndMetadata[RunType]) -> Position[snx.NXsample, R return Position[snx.NXsample, RunType](dg["data"].coords["sample_position"]) +def convert_detector_to_wavelength( + da: TofDetector[RunType], + graph: ElasticCoordTransformGraph[RunType], +) -> WavelengthDetector[RunType]: + return WavelengthDetector[RunType]( + da.transform_coords("wavelength", graph=graph, keep_intermediate=False) + ) + + +def convert_monitor_to_wavelength( + monitor: TofMonitor[RunType, MonitorType], + graph: MonitorCoordTransformGraph[RunType], +) -> WavelengthMonitor[RunType, MonitorType]: + return WavelengthMonitor[RunType, MonitorType]( + monitor.transform_coords("wavelength", graph=graph, keep_intermediate=False) + ) + + providers = ( pooch_load, pooch_load_calibration, @@ -242,5 +276,7 @@ def sample_position(dg: RawDataAndMetadata[RunType]) -> Position[snx.NXsample, R extract_raw_data, sample_position, source_position, + convert_detector_to_wavelength, + convert_monitor_to_wavelength, ) """Sciline Providers for loading POWGEN data.""" From 1cad020bed0fb3000eee9a3e57f21e0cad603541 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 20 Mar 2026 16:11:05 +0100 Subject: [PATCH 13/27] fix dream geant4 tests --- tests/dream/geant4_reduction_test.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/dream/geant4_reduction_test.py b/tests/dream/geant4_reduction_test.py index 6aee136d..a82c5486 100644 --- a/tests/dream/geant4_reduction_test.py +++ b/tests/dream/geant4_reduction_test.py @@ -59,10 +59,7 @@ CalibrationFilename: None, UncertaintyBroadcastMode: UncertaintyBroadcastMode.drop, DspacingBins: sc.linspace('dspacing', 0.0, 2.3434, 201, unit='angstrom'), - TofMask: lambda x: ( - (x < sc.scalar(0.0, unit='us').to(unit=elem_unit(x))) - | (x > sc.scalar(86e3, unit='us').to(unit=elem_unit(x))) - ), + TofMask: None, TwoThetaMask: None, WavelengthMask: None, CIFAuthors: CIFAuthors( @@ -114,14 +111,14 @@ def test_pipeline_can_compute_dspacing_result_without_empty_can(workflow): def test_pipeline_can_compute_dspacing_result_using_lookup_table_filename(workflow): workflow = powder.with_pixel_mask_filenames(workflow, []) - workflow[LookupTableFilename] = dream.data.tof_lookup_table_high_flux() + workflow[LookupTableFilename] = dream.data.lookup_table_high_flux() result = workflow.compute(EmptyCanSubtractedIofDspacing) assert result.sizes == {'dspacing': len(params[DspacingBins]) - 1} assert sc.identical(result.coords['dspacing'], params[DspacingBins]) @pytest.fixture(scope="module") -def dream_tof_lookup_table(): +def dream_lookup_table(): lut_wf = unwrap.LookupTableWorkflow() lut_wf[unwrap.DiskChoppers[AnyRun]] = dream.beamline.choppers( dream.beamline.InstrumentConfiguration.high_flux_BC215 @@ -140,10 +137,10 @@ def dream_tof_lookup_table(): def test_pipeline_can_compute_dspacing_result_using_custom_built_tof_lookup( - workflow, dream_tof_lookup_table + workflow, dream_lookup_table ): workflow = powder.with_pixel_mask_filenames(workflow, []) - workflow[LookupTable] = dream_tof_lookup_table + workflow[LookupTable] = dream_lookup_table result = workflow.compute(IntensityDspacing[SampleRun]) assert result.sizes == {'dspacing': len(params[DspacingBins]) - 1} From 6a4cacc1b3ebfb8c9be47455af9b046026f850ec Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 23 Mar 2026 14:26:53 +0100 Subject: [PATCH 14/27] make graph internally to to_dspacing_with_calibration function, and fix some tests --- src/ess/powder/conversion.py | 13 +++++-------- tests/powder/conversion_test.py | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index 826793ef..a02beb42 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -94,13 +94,11 @@ def _consume_positions(position, sample_position, source_position): def to_dspacing_with_calibration( - data: sc.DataArray, - calibration: sc.Dataset, - graph: dict, + data: sc.DataArray, calibration: sc.Dataset ) -> sc.DataArray: """ - Transform coordinates to d-spacing from calibration parameters. - Computes d-spacing from wavelength stored in `data`. + Transform coordinates from a detector time of arrival offset to d-spacing using + calibration parameters. Parameters ---------- @@ -109,8 +107,6 @@ def to_dspacing_with_calibration( Must have a wavelength coordinate. calibration: Calibration data. - graph: - Graph for the coordinate transformation, used to restore tof from wavelength. Returns ------- @@ -127,6 +123,7 @@ def to_dspacing_with_calibration( # a time of arrival to d-spacing, and not a tof. # We defer this to a later step: https://github.com/scipp/essdiffraction/issues/255 # Restore tof from wavelength + graph = {"tof": scn.conversion.tof.tof_from_wavelength} out = out.transform_coords("tof", graph=graph, keep_intermediate=False) pos_graph = {"dspacing": _dspacing_from_diff_calibration} @@ -214,7 +211,7 @@ def convert_to_dspacing( if calibration is None: out = data.transform_coords(["dspacing"], graph=graph, keep_intermediate=False) else: - out = to_dspacing_with_calibration(data, calibration=calibration, graph=graph) + out = to_dspacing_with_calibration(data, calibration=calibration) for key in ("wavelength", "two_theta"): if key in out.coords.keys(): out.coords.set_aligned(key, False) diff --git a/tests/powder/conversion_test.py b/tests/powder/conversion_test.py index ed917514..c6c9e057 100644 --- a/tests/powder/conversion_test.py +++ b/tests/powder/conversion_test.py @@ -56,6 +56,12 @@ def test_dspacing_with_calibration_roundtrip(calibration): tzero = calibration['tzero'].data recomputed_tof = difa * d**2 + difc * d + tzero recomputed_tof = recomputed_tof.rename_dims({'dspacing': 'tof'}) + # Note that here, the recomputed_tof is 2D (spectrum, tof) while the initial_tof + # is 1D (tof), but the values should be the same along the spectrum dimension for + # recomputed_tof. The allclose check takes the difference between the 2 arrays and + # checks that all values are close to zero. In that process, the 1D initial_tof is + # automatically broadcast to the shape of recomputed_tof, so the check is + # effectively comparing each spectrum's recomputed_tof to the same initial_tof. assert sc.allclose(recomputed_tof, initial_tof.coords['tof']) @@ -77,11 +83,13 @@ def test_dspacing_with_calibration_roundtrip_with_wavelength(calibration): difc = calibration['difc'].data tzero = calibration['tzero'].data recomputed_tof = difa * d**2 + difc * d + tzero - recomputed_tof = recomputed_tof.rename_dims({'dspacing': 'tof'}) - assert sc.allclose( - recomputed_tof, - initial_wavelength.coords['tof'].rename_dims({'wavelength': 'tof'}), - ) + # Note that here, the recomputed_tof is 2D (spectrum, tof) while the initial_tof + # is 1D (tof), but the values should be the same along the spectrum dimension for + # recomputed_tof. The allclose check takes the difference between the 2 arrays and + # checks that all values are close to zero. In that process, the 1D initial_tof is + # automatically broadcast to the shape of recomputed_tof, so the check is + # effectively comparing each spectrum's recomputed_tof to the same initial_tof. + assert sc.allclose(recomputed_tof, initial_wavelength.coords['tof']) def test_dspacing_with_calibration_consumes_positions(calibration): From 9568b9e7f3e53e5442fe358eaee690ff578608ec Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 23 Mar 2026 16:31:40 +0100 Subject: [PATCH 15/27] fix powgen tests and apply formatting --- src/ess/beer/types.py | 1 + src/ess/dream/workflows.py | 8 +-- src/ess/powder/conversion.py | 4 +- src/ess/powder/types.py | 5 +- src/ess/snspowder/powgen/data.py | 77 +++++++++++++++++++++++++++- tests/beer/mcstas_reduction_test.py | 2 +- tests/dream/geant4_reduction_test.py | 7 ++- 7 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/ess/beer/types.py b/src/ess/beer/types.py index cd29ca54..6ef7ce95 100644 --- a/src/ess/beer/types.py +++ b/src/ess/beer/types.py @@ -12,6 +12,7 @@ import sciline import scipp as sc + from ess.reduce.nexus.types import Filename, RawDetector, RunType, SampleRun diff --git a/src/ess/dream/workflows.py b/src/ess/dream/workflows.py index e8124fdb..02342982 100644 --- a/src/ess/dream/workflows.py +++ b/src/ess/dream/workflows.py @@ -6,10 +6,6 @@ import sciline import scipp as sc import scippnexus as snx -from ess.reduce.nexus.types import DetectorBankSizes, NeXusName -from ess.reduce.parameter import parameter_mappers -from ess.reduce.unwrap import GenericUnwrapWorkflow -from ess.reduce.workflow import register_workflow from scippneutron.metadata import Software from ess.powder import providers as powder_providers @@ -33,6 +29,10 @@ VanadiumRun, WavelengthMask, ) +from ess.reduce.nexus.types import DetectorBankSizes, NeXusName +from ess.reduce.parameter import parameter_mappers +from ess.reduce.unwrap import GenericUnwrapWorkflow +from ess.reduce.workflow import register_workflow from .beamline import InstrumentConfiguration from .io.cif import ( diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index a02beb42..45cdd429 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -165,7 +165,7 @@ def powder_coordinate_transformation_graph( : A dictionary with the graph for the transformation. """ - return ElasticCoordTransformGraph( + return ElasticCoordTransformGraph[RunType]( { **scn.conversion.graph.beamline.beamline(scatter=True), **scn.conversion.graph.tof.elastic("wavelength"), @@ -264,7 +264,7 @@ def powder_monitor_coordinate_transformation_graph( : A dictionary with the graph for the transformation. """ - return MonitorCoordTransformGraph( + return MonitorCoordTransformGraph[RunType]( { **scn.conversion.graph.beamline.beamline(scatter=False), **scn.conversion.graph.tof.elastic("wavelength"), diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index d7f4afe9..f38af15a 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -14,11 +14,12 @@ import sciline import scipp as sc +from scippneutron.io import cif +from scippneutron.metadata import Person, Software + from ess.reduce.nexus import types as reduce_t from ess.reduce.uncertainty import UncertaintyBroadcastMode as _UncertaintyBroadcastMode from ess.reduce.unwrap import types as unwrap_t -from scippneutron.io import cif -from scippneutron.metadata import Person, Software EmptyDetector = reduce_t.EmptyDetector EmptyMonitor = reduce_t.EmptyMonitor diff --git a/src/ess/snspowder/powgen/data.py b/src/ess/snspowder/powgen/data.py index 8be882b4..d25c636c 100644 --- a/src/ess/snspowder/powgen/data.py +++ b/src/ess/snspowder/powgen/data.py @@ -7,8 +7,8 @@ import sciline as sl import scipp as sc +import scippneutron as scn import scippnexus as snx -from ess.reduce.data import Entry, make_registry from ess.powder.types import ( AccumulatedProtonCharge, @@ -17,6 +17,7 @@ DetectorBankSizes, ElasticCoordTransformGraph, Filename, + GravityVector, MonitorCoordTransformGraph, MonitorType, Position, @@ -26,6 +27,7 @@ WavelengthDetector, WavelengthMonitor, ) +from ess.reduce.data import Entry, make_registry class TofDetector(sl.Scope[RunType, sc.DataArray], sc.DataArray): @@ -250,6 +252,77 @@ def sample_position(dg: RawDataAndMetadata[RunType]) -> Position[snx.NXsample, R return Position[snx.NXsample, RunType](dg["data"].coords["sample_position"]) +def _coordinate_transformation_graph( + source_position: sc.Variable, + sample_position: sc.Variable, + gravity: sc.Variable, + scatter: bool, +) -> dict: + return { + **scn.conversion.graph.beamline.beamline(scatter=scatter), + **scn.conversion.graph.tof.elastic("tof"), + 'source_position': lambda: source_position, + 'sample_position': lambda: sample_position, + 'gravity': lambda: gravity, + } + + +def detector_coordinate_transformation_graph( + source_position: Position[snx.NXsource, RunType], + sample_position: Position[snx.NXsample, RunType], + gravity: GravityVector, +) -> ElasticCoordTransformGraph[RunType]: + """Generate a coordinate transformation graph for detectors. + + Parameters + ---------- + source_position: + Position of the neutron source. + sample_position: + Position of the sample. + gravity: + Gravity vector. + + Returns + ------- + : + A dictionary graph for the transformation. + """ + return ElasticCoordTransformGraph[RunType]( + _coordinate_transformation_graph( + source_position, sample_position, gravity, scatter=True + ) + ) + + +def monitor_coordinate_transformation_graph( + source_position: Position[snx.NXsource, RunType], + sample_position: Position[snx.NXsample, RunType], + gravity: GravityVector, +) -> MonitorCoordTransformGraph[RunType]: + """Generate a coordinate transformation graph for monitors. + + Parameters + ---------- + source_position: + Position of the neutron source. + sample_position: + Position of the sample. + gravity: + Gravity vector. + + Returns + ------- + : + A dictionary graph for the transformation. + """ + return MonitorCoordTransformGraph[RunType]( + _coordinate_transformation_graph( + source_position, sample_position, gravity, scatter=False + ) + ) + + def convert_detector_to_wavelength( da: TofDetector[RunType], graph: ElasticCoordTransformGraph[RunType], @@ -276,6 +349,8 @@ def convert_monitor_to_wavelength( extract_raw_data, sample_position, source_position, + detector_coordinate_transformation_graph, + monitor_coordinate_transformation_graph, convert_detector_to_wavelength, convert_monitor_to_wavelength, ) diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index 32ac0875..526a56fd 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -1,7 +1,6 @@ import numpy as np import scipp as sc import scippneutron as scn -from ess.reduce.nexus.types import Filename, SampleRun from scipp.testing import assert_allclose from ess.beer import ( @@ -17,6 +16,7 @@ ) from ess.beer.io import load_beer_mcstas, load_beer_mcstas_monitor from ess.beer.types import DetectorBank, DHKLList, TofDetector +from ess.reduce.nexus.types import Filename, SampleRun def test_can_reduce_using_known_peaks_workflow(): diff --git a/tests/dream/geant4_reduction_test.py b/tests/dream/geant4_reduction_test.py index a82c5486..15f87249 100644 --- a/tests/dream/geant4_reduction_test.py +++ b/tests/dream/geant4_reduction_test.py @@ -8,11 +8,7 @@ import sciline import scipp as sc import scipp.testing -from ess.reduce import unwrap -from ess.reduce import workflow as reduce_workflow -from ess.reduce.nexus.types import AnyRun from scippneutron import metadata -from scippneutron._utils import elem_unit import ess.dream.data # noqa: F401 from ess import dream, powder @@ -47,6 +43,9 @@ VanadiumRun, WavelengthMask, ) +from ess.reduce import unwrap +from ess.reduce import workflow as reduce_workflow +from ess.reduce.nexus.types import AnyRun params = { Filename[SampleRun]: dream.data.simulated_diamond_sample(small=True), From 8c0efbff8343cf099d73de71e5664400c954f7a2 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 23 Mar 2026 17:00:59 +0100 Subject: [PATCH 16/27] uncomment essdiff software --- src/ess/dream/workflows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ess/dream/workflows.py b/src/ess/dream/workflows.py index 7dbf93d4..eac6f435 100644 --- a/src/ess/dream/workflows.py +++ b/src/ess/dream/workflows.py @@ -90,7 +90,7 @@ def _get_lookup_table_filename_from_configuration( def _collect_reducer_software() -> ReducerSoftware: return ReducerSoftware( [ - # Software.from_package_metadata('essdiffraction'), + Software.from_package_metadata('essdiffraction'), Software.from_package_metadata('scippneutron'), Software.from_package_metadata('scipp'), ] From 12c73996048f1f322b1489c28404759bf6f1e827 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 14:01:08 +0100 Subject: [PATCH 17/27] remove old tof luts --- src/ess/dream/data.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/src/ess/dream/data.py b/src/ess/dream/data.py index 33f43908..8b32a077 100644 --- a/src/ess/dream/data.py +++ b/src/ess/dream/data.py @@ -24,11 +24,6 @@ "DREAM_simple_pwd_workflow/Cave_TOF_Monitor_diam_in_can.dat": "md5:ef24f4a4186c628574046e6629e31611", # noqa: E501 "DREAM_simple_pwd_workflow/Cave_TOF_Monitor_van_can.dat": "md5:2cdef7ad9912652149b7e687381d2e99", # noqa: E501 "DREAM_simple_pwd_workflow/Cave_TOF_Monitor_vana_inc_coh.dat": "md5:701d66792f20eb283a4ce76bae0c8f8f", # noqa: E501 - # Time-of-flight lookup tables - "DREAM-high-flux-tof-lookup-table.h5": "md5:1b95a359fa7b0d8b4277806ece9bf279", - "DREAM-high-flux-tof-lookup-table-BC240-new0.h5": "md5:2cc9dc802082101933429a2ea3624126", # noqa: E501 - "DREAM-high-flux-tof-lut-5m-80m.h5": "md5:0db099795027e283f70cb48f738a1c44", - "DREAM-high-flux-tof-lut-5m-80m-bc240.h5": "md5:85c0a8acd7ed7f9793ef29f47776f63f", # noqa: E501 # Smaller files for unit tests "DREAM_simple_pwd_workflow/TEST_data_dream_diamond_vana_container_sample_union.csv.zip": "md5:405df9b5ade9d61ab71fe8d8c19bb51b", # noqa: E501 "DREAM_simple_pwd_workflow/TEST_data_dream_vana_container_sample_union.csv.zip": "md5:20186119d1debfb0c2352f9db384cd0a", # noqa: E501 @@ -265,39 +260,6 @@ def simulated_monitor_empty_can() -> Path: return get_path("DREAM_simple_pwd_workflow/Cave_TOF_Monitor_van_can.dat") -def tof_lookup_table_high_flux(bc: Literal[215, 240] = 215) -> Path: - """Path to a HDF5 file containing a lookup table for high-flux ToF. - - The table was created using the ``tof`` package and the chopper settings for the - DREAM instrument in high-resolution mode. - Can return tables for two different band-control chopper (BC) settings: - - ``bc=215``: corresponds to the settings of the choppers in the tutorial data. - - ``bc=240``: a setting with less time overlap between frames. - - Note that the phase of the band-control chopper (BCC) was set to 215 degrees in the - Geant4 simulation which generated the data used in the documentation notebooks. - This has since been found to be non-optimal as it leads to time overlap between the - two frames, and a value of 240 degrees is now recommended. - - This table was computed using `Create a time-of-flight lookup table for DREAM - <../../user-guide/dream/dream-make-tof-lookup-table.rst>`_ - with ``NumberOfSimulatedNeutrons = 5_000_000``. - - Parameters - ---------- - bc: - Band-control chopper (BC) setting. The default is 215, which corresponds to the - settings of the choppers in the tutorial data. - """ - match bc: - case 215: - return get_path("DREAM-high-flux-tof-lut-5m-80m.h5") - case 240: - return get_path("DREAM-high-flux-tof-lut-5m-80m-bc240.h5") - case _: - raise ValueError(f"Unsupported band-control chopper (BC) value: {bc}") - - def lookup_table_high_flux(bc: Literal[215, 240] = 215) -> Path: """Path to a HDF5 file containing a wavelength lookup table for high-flux mode. From e12e26950641f50baf4eff6e2e2e4fb9ace69f51 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 14:03:33 +0100 Subject: [PATCH 18/27] apply suggestions from review --- src/ess/powder/conversion.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ess/powder/conversion.py b/src/ess/powder/conversion.py index 45cdd429..ada1cf00 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -182,22 +182,21 @@ def add_scattering_coordinates_from_positions( calibration: CalibrationData, ) -> DspacingDetector[RunType]: """ - Add ``wavelength``, ``two_theta`` and ``dspacing`` coordinates to the data. - The input ``data`` must have a ``tof`` coordinate, as well as the necessary - positions of the beamline components (source, sample, detectors) to compute - the scattering coordinates. + Add ``two_theta`` and ``dspacing`` coordinates to the data. + + The input ``data`` must have a ``wavelength`` coordinate. + The positions of the required beamline components (source, sample, detectors) + can be provided either by the graph or as coordinates. Parameters ---------- data: - Input data with a ``tof`` coordinate. + Input data with a ``wavelength`` coordinate. graph: Coordinate transformation graph. """ out = data.transform_coords( - ["two_theta", "wavelength", "Ltotal"], - graph=graph, - keep_intermediate=False, + ["two_theta", "Ltotal"], graph=graph, keep_intermediate=False ) out = convert_to_dspacing(out, graph, calibration) return DspacingDetector[RunType](out) From 21873785de57752a6621c8e34dc6b4fd53bbd794 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 14:04:28 +0100 Subject: [PATCH 19/27] bump essreduce --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70018b3f..95dae781 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ requires-python = ">=3.11" # Make sure to list one dependency per line. dependencies = [ "dask>=2022.1.0", - "essreduce>=26.3.1", + "essreduce>=26.4.0", "graphviz", "numpy>=2", "plopp>=26.2.0", From af19bf1bd3d4853e00f4e269ac8cc9bf90a129be Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 14:30:04 +0100 Subject: [PATCH 20/27] update deps --- requirements/base.in | 2 +- requirements/base.txt | 20 ++++++++++---------- requirements/basetest.txt | 2 +- requirements/ci.txt | 12 ++++++------ requirements/dev.txt | 10 +++++----- requirements/docs.txt | 4 ++-- requirements/nightly.txt | 10 +++++----- requirements/static.txt | 8 ++++---- 8 files changed, 34 insertions(+), 34 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index 4bb09572..f4dae313 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -3,7 +3,7 @@ # --- END OF CUSTOM SECTION --- # The following was generated by 'tox -e deps', DO NOT EDIT MANUALLY! dask>=2022.1.0 -essreduce>=26.3.1 +essreduce>=26.4.0 graphviz numpy>=2 plopp>=26.2.0 diff --git a/requirements/base.txt b/requirements/base.txt index 2c895c98..0d673892 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:81c10bea2bf8b09c721c1bb7b501d35596a04095 +# SHA1:d9fbbf694f7784dc50f913e8e6d0bcc452239f7a # # This file was generated by pip-compile-multi. # To update, run: @@ -7,13 +7,13 @@ # annotated-types==0.7.0 # via pydantic -ase==3.27.0 +ase==3.28.0 # via ncrystal asttokens==3.0.1 # via stack-data certifi==2026.2.25 # via requests -charset-normalizer==3.4.5 +charset-normalizer==3.4.6 # via requests click==8.3.1 # via dask @@ -27,7 +27,7 @@ cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib -dask==2026.1.2 +dask==2026.3.0 # via -r base.in decorator==5.2.1 # via ipython @@ -35,11 +35,11 @@ dnspython==2.8.0 # via email-validator email-validator==2.3.0 # via scippneutron -essreduce==26.3.1 +essreduce==26.4.0 # via -r base.in executing==2.2.1 # via stack-data -fonttools==4.62.0 +fonttools==4.62.1 # via matplotlib fsspec==2026.2.0 # via dask @@ -55,7 +55,7 @@ idna==3.11 # via # email-validator # requests -importlib-metadata==8.7.1 +importlib-metadata==9.0.0 # via dask ipydatawidgets==4.3.5 # via pythreejs @@ -127,7 +127,7 @@ pillow==12.1.1 # via matplotlib platformdirs==4.9.4 # via pooch -plopp==26.3.0 +plopp==26.3.1 # via # -r base.in # scippneutron @@ -164,7 +164,7 @@ sciline==25.11.1 # via # -r base.in # essreduce -scipp==26.3.0 +scipp==26.3.1 # via # -r base.in # essreduce @@ -193,7 +193,7 @@ spglib==2.6.0 # ncrystal stack-data==0.6.3 # via ipython -tof==26.1.0 +tof==26.3.0 # via -r base.in toolz==1.1.0 # via diff --git a/requirements/basetest.txt b/requirements/basetest.txt index 4b147bc4..ae531535 100644 --- a/requirements/basetest.txt +++ b/requirements/basetest.txt @@ -9,7 +9,7 @@ asttokens==3.0.1 # via stack-data certifi==2026.2.25 # via requests -charset-normalizer==3.4.5 +charset-normalizer==3.4.6 # via requests comm==0.2.3 # via ipywidgets diff --git a/requirements/ci.txt b/requirements/ci.txt index 21b87c34..dedea3a9 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -5,17 +5,17 @@ # # requirements upgrade # -cachetools==7.0.4 +cachetools==7.0.5 # via tox certifi==2026.2.25 # via requests -charset-normalizer==3.4.5 +charset-normalizer==3.4.6 # via requests colorama==0.4.6 # via tox distlib==0.4.0 # via virtualenv -filelock==3.25.0 +filelock==3.25.2 # via # python-discovery # tox @@ -40,7 +40,7 @@ pluggy==1.6.0 # via tox pyproject-api==1.10.0 # via tox -python-discovery==1.1.1 +python-discovery==1.2.0 # via virtualenv requests==2.32.5 # via -r ci.in @@ -48,9 +48,9 @@ smmap==5.0.3 # via gitdb tomli-w==1.2.0 # via tox -tox==4.49.0 +tox==4.50.3 # via -r ci.in urllib3==2.6.3 # via requests -virtualenv==21.1.0 +virtualenv==21.2.0 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index c90cd996..e75b76a4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,7 +12,7 @@ -r static.txt -r test.txt -r wheels.txt -anyio==4.12.1 +anyio==4.13.0 # via # httpx # jupyter-server @@ -22,11 +22,11 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi arrow==1.4.0 # via isoduration -async-lru==2.2.0 +async-lru==2.3.0 # via jupyterlab cffi==2.0.0 # via argon2-cffi-bindings -copier==9.13.1 +copier==9.14.0 # via -r dev.in dunamai==1.26.0 # via copier @@ -46,7 +46,7 @@ jinja2-ansible-filters==1.3.2 # via copier json5==0.13.0 # via jupyterlab-server -jsonpointer==3.0.0 +jsonpointer==3.1.1 # via jsonschema jsonschema[format-nongpl]==4.26.0 # via @@ -65,7 +65,7 @@ jupyter-server==2.17.0 # notebook-shim jupyter-server-terminals==0.5.4 # via jupyter-server -jupyterlab==4.5.5 +jupyterlab==4.5.6 # via -r dev.in jupyterlab-server==2.28.0 # via jupyterlab diff --git a/requirements/docs.txt b/requirements/docs.txt index d7da0669..08b44829 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -10,7 +10,7 @@ accessible-pygments==0.0.5 # via pydata-sphinx-theme alabaster==1.0.0 # via sphinx -attrs==25.4.0 +attrs==26.1.0 # via # jsonschema # referencing @@ -171,7 +171,7 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx tinycss2==1.4.0 # via bleach -tornado==6.5.4 +tornado==6.5.5 # via # ipykernel # jupyter-client diff --git a/requirements/nightly.txt b/requirements/nightly.txt index c8cc0026..578efdcd 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -10,13 +10,13 @@ annotated-types==0.7.0 # via pydantic -ase==3.27.0 +ase==3.28.0 # via ncrystal asttokens==3.0.1 # via stack-data certifi==2026.2.25 # via requests -charset-normalizer==3.4.5 +charset-normalizer==3.4.6 # via requests click==8.3.1 # via dask @@ -30,7 +30,7 @@ cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib -dask==2026.1.2 +dask==2026.3.0 # via -r nightly.in decorator==5.2.1 # via ipython @@ -42,7 +42,7 @@ essreduce @ git+https://github.com/scipp/essreduce@main # via -r nightly.in executing==2.2.1 # via stack-data -fonttools==4.62.0 +fonttools==4.62.1 # via matplotlib fsspec==2026.2.0 # via dask @@ -58,7 +58,7 @@ idna==3.11 # via # email-validator # requests -importlib-metadata==8.7.1 +importlib-metadata==9.0.0 # via dask iniconfig==2.3.0 # via pytest diff --git a/requirements/static.txt b/requirements/static.txt index 7f8cd35f..02cb746f 100644 --- a/requirements/static.txt +++ b/requirements/static.txt @@ -9,11 +9,11 @@ cfgv==3.5.0 # via pre-commit distlib==0.4.0 # via virtualenv -filelock==3.25.0 +filelock==3.25.2 # via # python-discovery # virtualenv -identify==2.6.17 +identify==2.6.18 # via pre-commit nodeenv==1.10.0 # via pre-commit @@ -23,9 +23,9 @@ platformdirs==4.9.4 # virtualenv pre-commit==4.5.1 # via -r static.in -python-discovery==1.1.1 +python-discovery==1.2.0 # via virtualenv pyyaml==6.0.3 # via pre-commit -virtualenv==21.1.0 +virtualenv==21.2.0 # via pre-commit From 7b30aeab20891214fac0557f7144cd0805fac62f Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 15:13:49 +0100 Subject: [PATCH 21/27] remove not so useful test --- tests/powder/conversion_test.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/powder/conversion_test.py b/tests/powder/conversion_test.py index c6c9e057..a4f5374d 100644 --- a/tests/powder/conversion_test.py +++ b/tests/powder/conversion_test.py @@ -145,29 +145,3 @@ def test_dspacing_with_calibration_does_not_use_positions(calibration): assert sc.allclose( dspacing_no_pos.coords['dspacing'], dspacing_pos.coords['dspacing'] ) - - -def test_add_scattering_coordinates_from_positions(): - position = sc.vectors( - dims=['spectrum'], values=np.arange(14 * 3).reshape((14, 3)), unit='m' - ) - sample_position = sc.vector([0.0, 0.0, 0.01], unit='m') - source_position = sc.vector([0.0, 0.0, -11.3], unit='m') - tof = sc.DataArray( - sc.ones(dims=['spectrum', 'tof'], shape=[14, 27]), - coords={ - 'position': position, - 'tof': sc.linspace('tof', 1.0, 1000.0, 27, unit='us'), - 'sample_position': sample_position, - 'source_position': source_position, - }, - ) - graph = { - **scn.conversion.graph.beamline.beamline(scatter=True), - **scn.conversion.graph.tof.elastic('tof'), - } - - result = add_scattering_coordinates_from_positions(tof, graph, calibration=None) - - assert 'wavelength' in result.coords - assert 'two_theta' in result.coords From b6a3720d02de264966245d8abbbf55585659eaf1 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 15:14:29 +0100 Subject: [PATCH 22/27] linting --- tests/powder/conversion_test.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/powder/conversion_test.py b/tests/powder/conversion_test.py index a4f5374d..89c62c1b 100644 --- a/tests/powder/conversion_test.py +++ b/tests/powder/conversion_test.py @@ -4,12 +4,8 @@ import pytest import scipp as sc import scipp.testing -import scippneutron as scn -from ess.powder.conversion import ( - add_scattering_coordinates_from_positions, - to_dspacing_with_calibration, -) +from ess.powder.conversion import to_dspacing_with_calibration @pytest.fixture(params=['random', 'zero']) From 9ce3c3985e5626f3a99346e7133e37d804873caa Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 18:00:32 +0100 Subject: [PATCH 23/27] revert changes wf -> workflow --- .../dream/dream-powder-reduction.ipynb | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/user-guide/dream/dream-powder-reduction.ipynb b/docs/user-guide/dream/dream-powder-reduction.ipynb index 5a3425f9..63fbce22 100644 --- a/docs/user-guide/dream/dream-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-powder-reduction.ipynb @@ -56,7 +56,7 @@ "metadata": {}, "outputs": [], "source": [ - "wf = dream.DreamGeant4Workflow(\n", + "workflow = dream.DreamGeant4Workflow(\n", " run_norm=powder.RunNormalization.monitor_histogram,\n", ")" ] @@ -77,26 +77,26 @@ "metadata": {}, "outputs": [], "source": [ - "wf[Filename[SampleRun]] = dream.data.simulated_diamond_sample()\n", - "wf[Filename[VanadiumRun]] = dream.data.simulated_vanadium_sample()\n", - "wf[Filename[EmptyCanRun]] = dream.data.simulated_empty_can()\n", - "wf[CalibrationFilename] = None\n", + "workflow[Filename[SampleRun]] = dream.data.simulated_diamond_sample()\n", + "workflow[Filename[VanadiumRun]] = dream.data.simulated_vanadium_sample()\n", + "workflow[Filename[EmptyCanRun]] = dream.data.simulated_empty_can()\n", + "workflow[CalibrationFilename] = None\n", "\n", - "wf[MonitorFilename[SampleRun]] = dream.data.simulated_monitor_diamond_sample()\n", - "wf[MonitorFilename[VanadiumRun]] = dream.data.simulated_monitor_vanadium_sample()\n", - "wf[MonitorFilename[EmptyCanRun]] = dream.data.simulated_monitor_empty_can()\n", - "wf[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", + "workflow[MonitorFilename[SampleRun]] = dream.data.simulated_monitor_diamond_sample()\n", + "workflow[MonitorFilename[VanadiumRun]] = dream.data.simulated_monitor_vanadium_sample()\n", + "workflow[MonitorFilename[EmptyCanRun]] = dream.data.simulated_monitor_empty_can()\n", + "workflow[CaveMonitorPosition] = sc.vector([0.0, 0.0, -4220.0], unit=\"mm\")\n", "\n", - "wf[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", + "workflow[dream.InstrumentConfiguration] = dream.InstrumentConfiguration.high_flux_BC215\n", "# Select a detector bank:\n", - "wf[NeXusDetectorName] = \"mantle\"\n", + "workflow[NeXusDetectorName] = \"mantle\"\n", "# We drop uncertainties where they would otherwise lead to correlations:\n", - "wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", "# Edges for binning in d-spacing:\n", - "wf[DspacingBins] = sc.linspace(\"dspacing\", 0.3, 2.3434, 201, unit=\"angstrom\")\n", + "workflow[DspacingBins] = sc.linspace(\"dspacing\", 0.3, 2.3434, 201, unit=\"angstrom\")\n", "\n", "# Do not mask any pixels / voxels:\n", - "wf = powder.with_pixel_mask_filenames(wf, [])" + "workflow = powder.with_pixel_mask_filenames(workflow, [])" ] }, { @@ -121,7 +121,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = wf.compute([\n", + "results = workflow.compute([\n", " EmptyCanSubtractedIofDspacing,\n", " ReducedEmptyCanSubtractedTofCIF\n", "])\n", @@ -227,7 +227,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.11.10" } }, "nbformat": 4, From 6e5ac5538833853c5e2e28b937ebef86f5a1cfce Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 24 Mar 2026 18:46:09 +0100 Subject: [PATCH 24/27] fix powgen notebook --- .../user-guide/sns-instruments/POWGEN_data_reduction.ipynb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/sns-instruments/POWGEN_data_reduction.ipynb b/docs/user-guide/sns-instruments/POWGEN_data_reduction.ipynb index 01148411..92b1a1e0 100644 --- a/docs/user-guide/sns-instruments/POWGEN_data_reduction.ipynb +++ b/docs/user-guide/sns-instruments/POWGEN_data_reduction.ipynb @@ -239,7 +239,7 @@ "source": [ "results = workflow.compute(\n", " (\n", - " TofDetector[SampleRun],\n", + " WavelengthDetector[SampleRun],\n", " CorrectedDetector[SampleRun],\n", " )\n", ")" @@ -252,7 +252,7 @@ "metadata": {}, "outputs": [], "source": [ - "results[TofDetector[SampleRun]]" + "results[WavelengthDetector[SampleRun]]" ] }, { @@ -377,7 +377,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.12.12" } }, "nbformat": 4, From 920b4a5cb372e77380af9c87f1ff2ca6f0d92133 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Wed, 25 Mar 2026 10:22:29 +0100 Subject: [PATCH 25/27] refactor: tofectomy --- .../beer/beer_modulation_mcstas.ipynb | 20 ++-- src/ess/beer/conversions.py | 101 +++++++++--------- src/ess/beer/types.py | 16 +-- tests/beer/mcstas_reduction_test.py | 10 +- 4 files changed, 73 insertions(+), 74 deletions(-) diff --git a/docs/user-guide/beer/beer_modulation_mcstas.ipynb b/docs/user-guide/beer/beer_modulation_mcstas.ipynb index 39227ea9..b338fbfc 100644 --- a/docs/user-guide/beer/beer_modulation_mcstas.ipynb +++ b/docs/user-guide/beer/beer_modulation_mcstas.ipynb @@ -189,7 +189,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -220,7 +220,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -302,7 +302,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -333,7 +333,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -415,7 +415,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -446,7 +446,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -528,7 +528,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -559,7 +559,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -641,7 +641,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", @@ -672,7 +672,7 @@ "results = {}\n", "for bank in DetectorBank:\n", " wf[DetectorBank] = bank\n", - " da = wf.compute(TofDetector[SampleRun])\n", + " da = wf.compute(WavelengthDetector[SampleRun])\n", " results[bank] = (\n", " da\n", " .transform_coords(('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'),)\n", diff --git a/src/ess/beer/conversions.py b/src/ess/beer/conversions.py index 152f559b..83fa2e04 100644 --- a/src/ess/beer/conversions.py +++ b/src/ess/beer/conversions.py @@ -3,6 +3,7 @@ from scippneutron.conversion import graph from .types import ( + CoordTransformGraph, DHKLList, GeometryCoordTransformGraph, ModulationPeriod, @@ -10,16 +11,16 @@ RawDetector, RunType, StreakClusteredData, - TofCoordTransformGraph, - TofDetector, WavelengthDefinitionChopperDelay, + WavelengthDetector, ) -def compute_tof_in_each_cluster( +def compute_wavelength_in_each_cluster( da: StreakClusteredData[RunType], mod_period: ModulationPeriod, -) -> TofDetector[RunType]: + graph: CoordTransformGraph, +) -> WavelengthDetector[RunType]: """Fits a line through each cluster, the intercept of the line is t0. The line is fitted using linear regression with an outlier removal procedure. @@ -32,10 +33,14 @@ def compute_tof_in_each_cluster( of the points in the cluster, and probably should belong to another cluster or are part of the background. 3. Go back to 1) and iterate until convergence. A few iterations should be enough. + 4. Finally, round the found intercept t0 to the closest chopper opening time. """ if isinstance(da, sc.DataGroup): return sc.DataGroup( - {k: compute_tof_in_each_cluster(v, mod_period) for k, v in da.items()} + { + k: compute_wavelength_in_each_cluster(v, mod_period) + for k, v in da.items() + } ) max_distance_from_streak_line = mod_period / 3 @@ -56,6 +61,7 @@ def compute_tof_in_each_cluster( da = da.assign_coords(t0=sc.values(t0)) da = da.bins.assign_coords(tof=(t - sc.values(t0))) + da = da.transform_coords(('wavelength',), graph=graph) return da @@ -161,8 +167,37 @@ def _tof_from_dhkl( return out +def t0_estimate( + wavelength_estimate: sc.Variable, + L0: sc.Variable, + Ltotal: sc.Variable, +) -> sc.Variable: + """Estimates the time-at-chopper by assuming the wavelength.""" + return ( + sc.constants.m_n + / sc.constants.h + * wavelength_estimate + * (L0 - Ltotal).to(unit=wavelength_estimate.unit) + ).to(unit='s') + + +def tof_from_t0_estimate_graph( + gg: GeometryCoordTransformGraph, +) -> CoordTransformGraph: + """Graph for computing ``wavelength`` in pulse shaping chopper modes.""" + return { + **gg, + 't0': t0_estimate, + 'tof': lambda time_of_arrival, t0: time_of_arrival - t0, + 'time_of_arrival': time_of_arrival, + } + + def geometry_graph() -> GeometryCoordTransformGraph: - return graph.beamline.beamline(scatter=True) + return { + **graph.beamline.beamline(scatter=True), + **graph.tof.elastic("tof"), + } def tof_from_known_dhkl_graph( @@ -171,7 +206,7 @@ def tof_from_known_dhkl_graph( time0: WavelengthDefinitionChopperDelay, dhkl_list: DHKLList, gg: GeometryCoordTransformGraph, -) -> TofCoordTransformGraph: +) -> CoordTransformGraph: """Graph computing ``tof`` in modulation chopper modes using list of peak positions.""" @@ -197,7 +232,6 @@ def _compute_coarse_dspacing( return { **gg, - **graph.tof.elastic("tof"), 'pulse_length': lambda: pulse_length, 'mod_period': lambda: mod_period, 'time0': lambda: time0, @@ -208,56 +242,21 @@ def _compute_coarse_dspacing( } -def t0_estimate( - wavelength_estimate: sc.Variable, - L0: sc.Variable, - Ltotal: sc.Variable, -) -> sc.Variable: - """Estimates the time-at-chopper by assuming the wavelength.""" - return ( - sc.constants.m_n - / sc.constants.h - * wavelength_estimate - * (L0 - Ltotal).to(unit=wavelength_estimate.unit) - ).to(unit='s') - - -def _tof_from_t0( - time_of_arrival: sc.Variable, - t0: sc.Variable, -) -> sc.Variable: - """Computes time-of-flight by subtracting a start time.""" - return time_of_arrival - t0 - - -def tof_from_t0_estimate_graph( - gg: GeometryCoordTransformGraph, -) -> TofCoordTransformGraph: - """Graph for computing ``tof`` in pulse shaping chopper modes.""" - return { - **gg, - **graph.tof.elastic("tof"), - 't0': t0_estimate, - 'tof': _tof_from_t0, - 'time_of_arrival': time_of_arrival, - } - - -def compute_tof( - da: RawDetector[RunType], graph: TofCoordTransformGraph -) -> TofDetector[RunType]: - """Uses the transformation graph to compute ``tof``.""" - return da.transform_coords(('tof',), graph=graph) +def wavelength_detector( + da: RawDetector[RunType], graph: CoordTransformGraph +) -> WavelengthDetector[RunType]: + """Applies the transformation graph to compute ``wavelength``.""" + return da.transform_coords(('wavelength',), graph=graph) convert_from_known_peaks_providers = ( geometry_graph, tof_from_known_dhkl_graph, - compute_tof, + wavelength_detector, ) convert_pulse_shaping = ( geometry_graph, tof_from_t0_estimate_graph, - compute_tof, + wavelength_detector, ) -providers = (compute_tof_in_each_cluster, geometry_graph) +providers = (compute_wavelength_in_each_cluster, geometry_graph) diff --git a/src/ess/beer/types.py b/src/ess/beer/types.py index c4655bf5..d4d0f07f 100644 --- a/src/ess/beer/types.py +++ b/src/ess/beer/types.py @@ -13,18 +13,18 @@ import sciline import scipp as sc -from ess.reduce.nexus.types import Filename, RawDetector, RunType, SampleRun -from ess.reduce.time_of_flight.types import TofDetector +from ess.reduce.nexus import types as nexus_t +from ess.reduce.unwrap import types as unwrap_t -class StreakClusteredData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): +class StreakClusteredData(sciline.Scope[nexus_t.RunType, sc.DataArray], sc.DataArray): """Detector data binned by streak""" -RawDetector = RawDetector -Filename = Filename -SampleRun = SampleRun -TofDetector = TofDetector +RawDetector = nexus_t.RawDetector +Filename = nexus_t.Filename +SampleRun = nexus_t.SampleRun +WavelengthDetector = unwrap_t.WavelengthDetector class DetectorBank(Enum): @@ -34,7 +34,7 @@ class DetectorBank(Enum): TwoThetaLimits = NewType("TwoThetaLimits", tuple[sc.Variable, sc.Variable]) -TofCoordTransformGraph = NewType("TofCoordTransformGraph", dict) +CoordTransformGraph = NewType("CoordTransformGraph", dict) GeometryCoordTransformGraph = NewType("GeometryCoordTransformGraph", dict) PulseLength = NewType("PulseLength", sc.Variable) diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index 40caec52..f9a48ed5 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -17,7 +17,7 @@ from ess.beer.io import load_beer_mcstas, load_beer_mcstas_monitor from ess.beer.types import DetectorBank, DHKLList from ess.reduce.nexus.types import Filename, SampleRun -from ess.reduce.time_of_flight.types import TofDetector +from ess.reduce.time_of_flight.types import WavelengthDetector def test_can_reduce_using_known_peaks_workflow(): @@ -25,7 +25,7 @@ def test_can_reduce_using_known_peaks_workflow(): wf[DHKLList] = duplex_peaks_array() wf[DetectorBank] = DetectorBank.north wf[Filename[SampleRun]] = mcstas_duplex(7) - da = wf.compute(TofDetector[SampleRun]) + da = wf.compute(WavelengthDetector[SampleRun]) assert 'tof' in da.bins.coords # assert dataarray has all coords required to compute dspacing da = da.transform_coords( @@ -45,7 +45,7 @@ def test_can_reduce_using_unknown_peaks_workflow(): wf = BeerModMcStasWorkflow() wf[Filename[SampleRun]] = mcstas_duplex(7) wf[DetectorBank] = DetectorBank.north - da = wf.compute(TofDetector[SampleRun]) + da = wf.compute(WavelengthDetector[SampleRun]) da = da.transform_coords( ('dspacing',), graph=scn.conversion.graph.tof.elastic('tof'), @@ -63,8 +63,8 @@ def test_pulse_shaping_workflow(): wf = BeerMcStasWorkflowPulseShaping() wf[Filename[SampleRun]] = mcstas_silicon_new_model(6) wf[DetectorBank] = DetectorBank.north - da = wf.compute(TofDetector[SampleRun]) - assert 'tof' in da.bins.coords + da = wf.compute(WavelengthDetector[SampleRun]) + assert 'wavelength' in da.bins.coords # assert dataarray has all coords required to compute dspacing da = da.transform_coords( ('dspacing',), From 602a5f7c940a13ed8a192f530111b7037d2627af Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Wed, 25 Mar 2026 10:49:55 +0100 Subject: [PATCH 26/27] fix --- src/ess/beer/conversions.py | 2 +- src/ess/beer/types.py | 1 + tests/beer/mcstas_reduction_test.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ess/beer/conversions.py b/src/ess/beer/conversions.py index 83fa2e04..0f2cffa4 100644 --- a/src/ess/beer/conversions.py +++ b/src/ess/beer/conversions.py @@ -19,7 +19,7 @@ def compute_wavelength_in_each_cluster( da: StreakClusteredData[RunType], mod_period: ModulationPeriod, - graph: CoordTransformGraph, + graph: GeometryCoordTransformGraph, ) -> WavelengthDetector[RunType]: """Fits a line through each cluster, the intercept of the line is t0. The line is fitted using linear regression with an outlier removal procedure. diff --git a/src/ess/beer/types.py b/src/ess/beer/types.py index d4d0f07f..ab861d26 100644 --- a/src/ess/beer/types.py +++ b/src/ess/beer/types.py @@ -21,6 +21,7 @@ class StreakClusteredData(sciline.Scope[nexus_t.RunType, sc.DataArray], sc.DataA """Detector data binned by streak""" +RunType = nexus_t.RunType RawDetector = nexus_t.RawDetector Filename = nexus_t.Filename SampleRun = nexus_t.SampleRun diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index f9a48ed5..23619278 100644 --- a/tests/beer/mcstas_reduction_test.py +++ b/tests/beer/mcstas_reduction_test.py @@ -17,7 +17,7 @@ from ess.beer.io import load_beer_mcstas, load_beer_mcstas_monitor from ess.beer.types import DetectorBank, DHKLList from ess.reduce.nexus.types import Filename, SampleRun -from ess.reduce.time_of_flight.types import WavelengthDetector +from ess.reduce.unwrap.types import WavelengthDetector def test_can_reduce_using_known_peaks_workflow(): From 2b91577ece9dd6526abc560ee3509bb9dd1e767f Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Wed, 25 Mar 2026 11:48:37 +0100 Subject: [PATCH 27/27] docs: tweak wording --- src/ess/beer/conversions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ess/beer/conversions.py b/src/ess/beer/conversions.py index 0f2cffa4..a22faf16 100644 --- a/src/ess/beer/conversions.py +++ b/src/ess/beer/conversions.py @@ -33,7 +33,7 @@ def compute_wavelength_in_each_cluster( of the points in the cluster, and probably should belong to another cluster or are part of the background. 3. Go back to 1) and iterate until convergence. A few iterations should be enough. - 4. Finally, round the found intercept t0 to the closest chopper opening time. + 4. Finally, round the estimated t0 to the closest known chopper opening time. """ if isinstance(da, sc.DataGroup): return sc.DataGroup(