From 2dcfc426ff07f40107f5e07e68ca3becc2377dbf Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 22 Jul 2026 14:10:59 +0200 Subject: [PATCH 1/3] convert int coords in raw data to float before rebinning, update loki LUT range to 5-35m --- .../src/ess/reduce/unwrap/to_wavelength.py | 8 +++++++- .../loki/loki-make-wavelength-lookup-table.ipynb | 4 ++-- packages/esssans/src/ess/loki/data.py | 6 ++++-- packages/esssans/src/ess/loki/workflow.py | 14 ++++++++++++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py b/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py index 4fde14197..0cdf74963 100644 --- a/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py +++ b/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py @@ -154,7 +154,13 @@ def _compute_wavelength_histogram( sc.concat([raw_eto, sc.scalar(0.0, unit=eto_unit), pulse_period], dim=key), key=key, ) - rebinned = da.rebin({key: new_bins}) + # Rebin only handles float coords, but it is possible that the coordinate to be + # rebinned is an integer type (e.g., when the time-of-flight is stored as an + # integer number of nanoseconds). In that case, we need to convert the coordinate + # to float before rebinning. + rebinned = da.assign_coords( + {key: da.coords[key].to(dtype=float, copy=False)} + ).rebin({key: new_bins}) etos = rebinned.coords[key] # Create linear interpolator diff --git a/packages/esssans/docs/user-guide/loki/loki-make-wavelength-lookup-table.ipynb b/packages/esssans/docs/user-guide/loki/loki-make-wavelength-lookup-table.ipynb index e8e8e56d8..32ff0f932 100644 --- a/packages/esssans/docs/user-guide/loki/loki-make-wavelength-lookup-table.ipynb +++ b/packages/esssans/docs/user-guide/loki/loki-make-wavelength-lookup-table.ipynb @@ -46,7 +46,7 @@ "wf[Position[snx.NXsource, AnyRun]] = source_position\n", "wf[unwrap.NumberOfSimulatedNeutrons] = 200_000 # Increase this number for more reliable results\n", "wf[unwrap.SimulationSeed] = 1234\n", - "wf[unwrap.LtotalRange[AnyRun, snx.NXdetector]] = sc.scalar(9.0, unit=\"m\"), sc.scalar(35.0, unit=\"m\")\n", + "wf[unwrap.LtotalRange[AnyRun, snx.NXdetector]] = sc.scalar(5.0, unit=\"m\"), sc.scalar(35.0, unit=\"m\")\n", "wf[unwrap.DistanceResolution] = sc.scalar(0.1, unit=\"m\")\n", "wf[unwrap.TimeResolution] = sc.scalar(250.0, unit='us')" ] @@ -116,7 +116,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/packages/esssans/src/ess/loki/data.py b/packages/esssans/src/ess/loki/data.py index 385468865..d92cff38a 100644 --- a/packages/esssans/src/ess/loki/data.py +++ b/packages/esssans/src/ess/loki/data.py @@ -59,9 +59,9 @@ # Small CODA file for testing purposes (data from only 5 pulses) 'loki-coda-5-pulses.hdf': 'md5:8368779651ccf101612b35ca075d0298', # Wavelength lookup table without choppers - 'loki-wavelength-lookup-table-no-choppers.h5': 'md5:3fb133962af6554a9999a753c1724ed4', # noqa: E501 + 'loki-wavelength-lookup-table-no-choppers.h5': 'md5:34cc975a00383653cce4b3e2356e0dc7', # noqa: E501 }, - version='2', + version='3', ) @@ -178,6 +178,8 @@ def loki_lookup_table_no_choppers() -> Path: This table was computed using `Create a wavelength lookup table for LoKI <../../user-guide/loki/loki-make-wavelength-lookup-table.ipynb>`_ with ``NumberOfSimulatedNeutrons = 5_000_000``. + + Ltotal range is 5-35 meters. """ return _registry.get_path("loki-wavelength-lookup-table-no-choppers.h5") diff --git a/packages/esssans/src/ess/loki/workflow.py b/packages/esssans/src/ess/loki/workflow.py index 61d6b96a0..41b9b2028 100644 --- a/packages/esssans/src/ess/loki/workflow.py +++ b/packages/esssans/src/ess/loki/workflow.py @@ -10,6 +10,7 @@ from ess import sans from ess.sans.parameters import typical_outputs +from ess.reduce.unwrap import WavelengthLutMode from ess.reduce.workflow import register_workflow from ..sans.types import ( @@ -73,16 +74,25 @@ def load_direct_beam(filename: DirectBeamFilename) -> DirectBeam: @register_workflow -def LokiWorkflow() -> sciline.Pipeline: +def LokiWorkflow( + wavelength_from: WavelengthLutMode = "file", +) -> sciline.Pipeline: """ Workflow with default parameters for Loki. + Parameters + ---------- + wavelength_from: + Mode for creating the wavelength lookup table. Possible values are + 'analytical', 'simulation', and 'file'. See + https://scipp.github.io/ess/reduce/user-guide/unwrap/lut-building-methods.html + Returns ------- : Loki workflow as a sciline.Pipeline """ - workflow = sans.SansWorkflow() + workflow = sans.SansWorkflow(wavelength_from=wavelength_from) for provider in loki_providers: workflow.insert(provider) for key, param in loki_default_parameters().items(): From b9f0a8445caacf3e9e5268a4d962b09d27696c70 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 22 Jul 2026 14:15:37 +0200 Subject: [PATCH 2/3] only change dtype if needed --- packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py b/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py index 0cdf74963..adb62e485 100644 --- a/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py +++ b/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py @@ -158,9 +158,9 @@ def _compute_wavelength_histogram( # rebinned is an integer type (e.g., when the time-of-flight is stored as an # integer number of nanoseconds). In that case, we need to convert the coordinate # to float before rebinning. - rebinned = da.assign_coords( - {key: da.coords[key].to(dtype=float, copy=False)} - ).rebin({key: new_bins}) + if da.coords[key].dtype not in ('float32', 'float64'): + da = da.assign_coords({key: da.coords[key].to(dtype=float)}) + rebinned = da.rebin({key: new_bins}) etos = rebinned.coords[key] # Create linear interpolator From c5b9d6e1c843d6efdecd67130d4ef75ce85b1e99 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 22 Jul 2026 14:29:05 +0200 Subject: [PATCH 3/3] add tests --- .../src/ess/reduce/unwrap/to_wavelength.py | 12 +++--- .../essreduce/tests/unwrap/unwrap_test.py | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py b/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py index adb62e485..456435cd7 100644 --- a/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py +++ b/packages/essreduce/src/ess/reduce/unwrap/to_wavelength.py @@ -154,13 +154,11 @@ def _compute_wavelength_histogram( sc.concat([raw_eto, sc.scalar(0.0, unit=eto_unit), pulse_period], dim=key), key=key, ) - # Rebin only handles float coords, but it is possible that the coordinate to be - # rebinned is an integer type (e.g., when the time-of-flight is stored as an - # integer number of nanoseconds). In that case, we need to convert the coordinate - # to float before rebinning. - if da.coords[key].dtype not in ('float32', 'float64'): - da = da.assign_coords({key: da.coords[key].to(dtype=float)}) - rebinned = da.rebin({key: new_bins}) + # Rebin only handles float64 coords, so we need to convert the coordinate + # on-the-fly if necessary. + rebinned = da.assign_coords( + {key: da.coords[key].to(dtype='float64', copy=False)} + ).rebin({key: new_bins}) etos = rebinned.coords[key] # Create linear interpolator diff --git a/packages/essreduce/tests/unwrap/unwrap_test.py b/packages/essreduce/tests/unwrap/unwrap_test.py index 71c3192e0..82bad0814 100644 --- a/packages/essreduce/tests/unwrap/unwrap_test.py +++ b/packages/essreduce/tests/unwrap/unwrap_test.py @@ -625,3 +625,44 @@ def test_unwrap_int( diff_threshold=0.02, rtol=0.2 if wavelength_from == "simulation" else 0.01, ) + + +@pytest.mark.parametrize("dtype", ["int32", "int64", "float32", "float64"]) +@pytest.mark.parametrize("wavelength_from", ["simulation", "analytical"]) +@pytest.mark.parametrize("detector_or_monitor", ["detector", "monitor"]) +def test_unwrap_histogram_other_coord_dtypes( + dtype, wavelength_from, detector_or_monitor, simulation_results_psc_choppers +) -> None: + dist = 62.0 + dim = "frame_time" + wf, ref = _make_workflow_histogram_wavelength_from( + wavelength_from=wavelength_from, + dim=dim, + distance=sc.scalar(dist, unit="m"), + choppers=fakes.psc_choppers(), + seed=378, + error_threshold=np.inf, + detector_or_monitor=detector_or_monitor, + ) + + if wavelength_from == "simulation": + wf[unwrap.SimulationResults[SampleRun]] = simulation_results_psc_choppers + + if detector_or_monitor == "detector": + da = wf.compute(RawDetector[SampleRun]) + new = da.assign_coords({dim: da.coords[dim].to(dtype=dtype)}) + wf[RawDetector[SampleRun]] = new + wavs = wf.compute(unwrap.WavelengthDetector[SampleRun]) + else: + da = wf.compute(RawMonitor[SampleRun, Monitor0]) + new = da.assign_coords({dim: da.coords[dim].to(dtype=dtype)}) + wf[RawMonitor[SampleRun, Monitor0]] = new + wavs = wf.compute(unwrap.WavelengthMonitor[SampleRun, Monitor0]) + + _validate_result_histogram_wavelength_from( + wavs=wavs, + ref=ref, + percentile=96, + diff_threshold=0.4, + rtol=0.2 if wavelength_from == "simulation" else 0.01, + )