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/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb b/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb index bb3ca987..f02589fe 100644 --- a/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-advanced-powder-reduction.ipynb @@ -628,7 +628,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-wavelength-lookup-table.ipynb similarity index 68% 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 3a1515d8..05047add 100644 --- a/docs/user-guide/dream/dream-make-tof-lookup-table.ipynb +++ b/docs/user-guide/dream/dream-make-wavelength-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 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 = time_of_flight.TofLookupTableWorkflow()\n", + "wf = unwrap.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[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(time_of_flight.TimeOfFlightLookupTable)\n", + "table = wf.compute(unwrap.LookupTable)\n", "table.array" ] }, @@ -117,7 +117,7 @@ "metadata": {}, "outputs": [], "source": [ - "table.save_hdf5('DREAM-high-flux-tof-lut-5m-80m.h5')" + "table.save_hdf5('DREAM-high-flux-wavelength-lut-5m-80m-bc240.h5')" ] } ], diff --git a/docs/user-guide/dream/dream-powder-reduction.ipynb b/docs/user-guide/dream/dream-powder-reduction.ipynb index b067bca1..63fbce22 100644 --- a/docs/user-guide/dream/dream-powder-reduction.ipynb +++ b/docs/user-guide/dream/dream-powder-reduction.ipynb @@ -145,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}]\")" ] }, { 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, 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", 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 diff --git a/src/ess/beer/conversions.py b/src/ess/beer/conversions.py index 152f559b..a22faf16 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: 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. @@ -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 estimated t0 to the closest known 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..d8949225 100644 --- a/src/ess/beer/types.py +++ b/src/ess/beer/types.py @@ -14,7 +14,7 @@ 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.unwrap.types import WavelengthDetector class StreakClusteredData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): @@ -24,7 +24,7 @@ class StreakClusteredData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): RawDetector = RawDetector Filename = Filename SampleRun = SampleRun -TofDetector = TofDetector +WavelengthDetector = 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/src/ess/dream/data.py b/src/ess/dream/data.py index 1e8136eb..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 @@ -40,6 +35,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 }, ) @@ -262,8 +260,8 @@ 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. +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. @@ -276,8 +274,8 @@ def tof_lookup_table_high_flux(bc: Literal[215, 240] = 215) -> Path: 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>`_ + 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 @@ -288,8 +286,8 @@ def tof_lookup_table_high_flux(bc: Literal[215, 240] = 215) -> Path: """ match bc: case 215: - return get_path("DREAM-high-flux-tof-lut-5m-80m.h5") + return get_path("DREAM-high-flux-wavelength-lut-5m-80m-bc215.h5") case 240: - return get_path("DREAM-high-flux-tof-lut-5m-80m-bc240.h5") + 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 dc1596fd..eac6f435 100644 --- a/src/ess/dream/workflows.py +++ b/src/ess/dream/workflows.py @@ -18,13 +18,13 @@ CaveMonitorPosition, # Should this be a DREAM-only parameter? EmptyCanRun, KeepEvents, + LookupTableFilename, LookupTableRelativeErrorThreshold, Measurement, PixelMaskFilename, Position, ReducerSoftware, SampleRun, - TimeOfFlightLookupTableFilename, TofMask, TwoThetaMask, VanadiumRun, @@ -32,7 +32,7 @@ ) 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.unwrap import GenericUnwrapWorkflow from ess.reduce.workflow import register_workflow from .beamline import InstrumentConfiguration @@ -73,18 +73,18 @@ def _get_lookup_table_filename_from_configuration( configuration: InstrumentConfiguration, -) -> TimeOfFlightLookupTableFilename: - from .data import tof_lookup_table_high_flux +) -> LookupTableFilename: + 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") - return TimeOfFlightLookupTableFilename(out) + return LookupTableFilename(out) def _collect_reducer_software() -> ReducerSoftware: @@ -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 GenericUnwrapWorkflow. 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:`GenericUnwrapWorkflow`. """ - wf = GenericTofWorkflow( + 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 42f21743..ada1cf00 100644 --- a/src/ess/powder/conversion.py +++ b/src/ess/powder/conversion.py @@ -10,10 +10,10 @@ from .calibration import OutputCalibrationData from .correction import merge_calibration -from .logging import get_logger from .types import ( CalibrationData, CorrectedDetector, + DspacingDetector, ElasticCoordTransformGraph, EmptyCanSubtractedIntensityTof, EmptyCanSubtractedIofDspacing, @@ -21,14 +21,10 @@ IntensityDspacing, IntensityTof, MonitorCoordTransformGraph, - MonitorType, Position, RunType, SampleRun, - TofDetector, - TofMonitor, WavelengthDetector, - WavelengthMonitor, ) @@ -64,6 +60,8 @@ def _dspacing_from_diff_calibration_a0_impl(t, t0, c): def _dspacing_from_diff_calibration( + # 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, @@ -96,31 +94,17 @@ def _consume_positions(position, sample_position, source_position): def to_dspacing_with_calibration( - data: sc.DataArray, - calibration: sc.Dataset, + data: sc.DataArray, calibration: sc.Dataset ) -> 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. + Transform coordinates from a detector time of arrival offset to d-spacing using + calibration parameters. 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. @@ -134,9 +118,15 @@ 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) - graph = {"dspacing": _dspacing_from_diff_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 + 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} # `_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 +135,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) @@ -175,10 +165,10 @@ 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("tof"), + **scn.conversion.graph.tof.elastic("wavelength"), 'source_position': lambda: source_position, 'sample_position': lambda: sample_position, 'gravity': lambda: gravity, @@ -186,47 +176,30 @@ 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 - - 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: 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 - 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 WavelengthDetector[RunType](out) + return DspacingDetector[RunType](out) def convert_to_dspacing( @@ -290,10 +263,10 @@ 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("tof"), + **scn.conversion.graph.tof.elastic("wavelength"), 'source_position': lambda: source_position, 'sample_position': lambda: sample_position, 'gravity': lambda: gravity, @@ -301,20 +274,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, ) diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index aeace7df..37a757dd 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -18,8 +18,8 @@ from scippneutron.metadata import Person, Software 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 ess.reduce.unwrap import types as unwrap_t EmptyDetector = reduce_t.EmptyDetector EmptyMonitor = reduce_t.EmptyMonitor @@ -36,13 +36,13 @@ DetectorBankSizes = reduce_t.DetectorBankSizes -DetectorLtotal = tof_t.DetectorLtotal -TofDetector = tof_t.TofDetector -TofMonitor = tof_t.TofMonitor -PulseStrideOffset = tof_t.PulseStrideOffset -TimeOfFlightLookupTable = tof_t.TimeOfFlightLookupTable -TimeOfFlightLookupTableFilename = tof_t.TimeOfFlightLookupTableFilename -LookupTableRelativeErrorThreshold = tof_t.LookupTableRelativeErrorThreshold +DetectorLtotal = unwrap_t.DetectorLtotal +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 @@ -96,7 +96,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.""" @@ -175,9 +175,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.""" @@ -202,7 +200,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/powder/workflow.py b/src/ess/powder/workflow.py index 8fbe0b3d..a0ca3828 100644 --- a/src/ess/powder/workflow.py +++ b/src/ess/powder/workflow.py @@ -2,17 +2,17 @@ from .masking import apply_masks from .types import ( CorrectedDetector, + DspacingDetector, MaskedDetectorIDs, RunType, TofMask, TwoThetaMask, - WavelengthDetector, WavelengthMask, ) def add_masks_and_corrections( - da: WavelengthDetector[RunType], + da: DspacingDetector[RunType], masked_pixel_ids: MaskedDetectorIDs, tof_mask_func: TofMask, wavelength_mask_func: WavelengthMask, diff --git a/src/ess/snspowder/powgen/data.py b/src/ess/snspowder/powgen/data.py index 140d3a58..d25c636c 100644 --- a/src/ess/snspowder/powgen/data.py +++ b/src/ess/snspowder/powgen/data.py @@ -5,7 +5,9 @@ from pathlib import Path +import sciline as sl import scipp as sc +import scippneutron as scn import scippnexus as snx from ess.powder.types import ( @@ -13,15 +15,31 @@ CalibrationData, CalibrationFilename, DetectorBankSizes, + ElasticCoordTransformGraph, Filename, + GravityVector, + 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", version="1", @@ -234,6 +252,95 @@ 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], +) -> 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 +349,9 @@ def sample_position(dg: RawDataAndMetadata[RunType]) -> Position[snx.NXsample, R extract_raw_data, sample_position, source_position, + detector_coordinate_transformation_graph, + monitor_coordinate_transformation_graph, + convert_detector_to_wavelength, + convert_monitor_to_wavelength, ) """Sciline Providers for loading POWGEN data.""" diff --git a/tests/beer/mcstas_reduction_test.py b/tests/beer/mcstas_reduction_test.py index 40caec52..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 TofDetector +from ess.reduce.unwrap.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',), diff --git a/tests/dream/geant4_reduction_test.py b/tests/dream/geant4_reduction_test.py index e4a11ddc..15f87249 100644 --- a/tests/dream/geant4_reduction_test.py +++ b/tests/dream/geant4_reduction_test.py @@ -9,7 +9,6 @@ import scipp as sc import scipp.testing from scippneutron import metadata -from scippneutron._utils import elem_unit import ess.dream.data # noqa: F401 from ess import dream, powder @@ -31,12 +30,12 @@ IntensityDspacingTwoTheta, IntensityTof, KeepEvents, + LookupTable, + LookupTableFilename, MonitorFilename, NeXusDetectorName, ReducedTofCIF, SampleRun, - TimeOfFlightLookupTable, - TimeOfFlightLookupTableFilename, TofMask, TwoThetaBins, TwoThetaMask, @@ -44,7 +43,7 @@ VanadiumRun, WavelengthMask, ) -from ess.reduce import time_of_flight +from ess.reduce import unwrap from ess.reduce import workflow as reduce_workflow from ess.reduce.nexus.types import AnyRun @@ -59,8 +58,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( @@ -112,37 +110,36 @@ 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.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(): - lut_wf = time_of_flight.TofLookupTableWorkflow() - lut_wf[time_of_flight.DiskChoppers[AnyRun]] = dream.beamline.choppers( +def dream_lookup_table(): + 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, dream_lookup_table ): workflow = powder.with_pixel_mask_filenames(workflow, []) - workflow[TimeOfFlightLookupTable] = dream_tof_lookup_table + workflow[LookupTable] = dream_lookup_table result = workflow.compute(IntensityDspacing[SampleRun]) assert result.sizes == {'dspacing': len(params[DspacingBins]) - 1} diff --git a/tests/powder/conversion_test.py b/tests/powder/conversion_test.py index ed917514..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']) @@ -56,6 +52,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 +79,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): @@ -137,29 +141,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