essreduce 25.11.1__tar.gz → 25.11.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {essreduce-25.11.1 → essreduce-25.11.3}/PKG-INFO +1 -1
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/api-reference/index.md +1 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/__init__.py +2 -2
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/nexus/__init__.py +2 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/nexus/_nexus_loader.py +26 -1
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/nexus/workflow.py +2 -6
- essreduce-25.11.3/src/ess/reduce/normalization.py +215 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/__init__.py +2 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/eto_to_tof.py +99 -8
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/fakes.py +1 -1
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/lut.py +1 -1
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/types.py +12 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/essreduce.egg-info/PKG-INFO +1 -1
- {essreduce-25.11.1 → essreduce-25.11.3}/src/essreduce.egg-info/SOURCES.txt +2 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/workflow_test.py +52 -4
- essreduce-25.11.3/tests/normalization_test.py +741 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/time_of_flight/unwrap_test.py +66 -1
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/time_of_flight/wfm_test.py +2 -2
- {essreduce-25.11.1 → essreduce-25.11.3}/.copier-answers.ess.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.copier-answers.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/ISSUE_TEMPLATE/high-level-requirement.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/dependabot.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/ci.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/docs.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/nightly_at_main.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/nightly_at_main_lower_bound.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/nightly_at_release.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/python-version-ci +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/release.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/test.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/unpinned.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.github/workflows/weekly_windows_macos.yml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.gitignore +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.pre-commit-config.yaml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/.python-version +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/CODE_OF_CONDUCT.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/CONTRIBUTING.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/LICENSE +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/MANIFEST.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/README.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_static/anaconda-icon.js +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_static/favicon.svg +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_static/logo-dark.svg +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_static/logo.svg +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_templates/class-template.rst +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_templates/doc_version.html +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/_templates/module-template.rst +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/about/index.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/conf.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/developer/coding-conventions.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/developer/dependency-management.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/developer/getting-started.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/developer/gui.ipynb +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/developer/index.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/index.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/index.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/installation.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/reduction-workflow-guidelines.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/tof/dream.ipynb +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/tof/frame-unwrapping.ipynb +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/tof/index.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/tof/wfm.ipynb +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/docs/user-guide/widget.md +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/pyproject.toml +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/base.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/base.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/basetest.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/basetest.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/ci.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/ci.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/dev.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/dev.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/docs.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/docs.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/make_base.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/mypy.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/mypy.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/nightly.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/nightly.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/static.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/static.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/test.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/test.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/wheels.in +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/requirements/wheels.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/resources/logo.svg +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/setup.cfg +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/data/__init__.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/data/_registry.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/live/__init__.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/live/raw.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/live/roi.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/live/workflow.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/logging.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/nexus/json_generator.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/nexus/json_nexus.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/nexus/types.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/parameter.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/py.typed +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/scripts/grow_nexus.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/streaming.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/interpolator_numba.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/interpolator_scipy.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/resample.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/workflow.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/ui.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/uncertainty.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/__init__.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_base.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_binedges_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_bounds_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_config.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_filename_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_linspace_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_optional_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_spinner.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_string_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_switchable_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/widgets/_vector_widget.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/ess/reduce/workflow.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/essreduce.egg-info/dependency_links.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/essreduce.egg-info/entry_points.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/essreduce.egg-info/requires.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/src/essreduce.egg-info/top_level.txt +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/accumulators_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/conftest.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/live/raw_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/live/roi_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_generator_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/array_dataset.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/dataset.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/detector.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/entry.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/event_data.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/instrument.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/log.json +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/json_nexus_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/nexus/nexus_loader_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/package_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/scripts/test_grow_nexus.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/streaming_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/time_of_flight/interpolator_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/time_of_flight/lut_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/time_of_flight/resample_tests.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/time_of_flight/workflow_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/uncertainty_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tests/widget_test.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tools/shrink_nexus.py +0 -0
- {essreduce-25.11.1 → essreduce-25.11.3}/tox.ini +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import importlib.metadata
|
|
6
6
|
|
|
7
|
-
from . import nexus, time_of_flight, uncertainty
|
|
7
|
+
from . import nexus, normalization, time_of_flight, uncertainty
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
10
|
__version__ = importlib.metadata.version("essreduce")
|
|
@@ -13,4 +13,4 @@ except importlib.metadata.PackageNotFoundError:
|
|
|
13
13
|
|
|
14
14
|
del importlib
|
|
15
15
|
|
|
16
|
-
__all__ = ["nexus", "time_of_flight", "uncertainty"]
|
|
16
|
+
__all__ = ["nexus", "normalization", "time_of_flight", "uncertainty"]
|
|
@@ -20,6 +20,7 @@ from ._nexus_loader import (
|
|
|
20
20
|
load_all_components,
|
|
21
21
|
load_component,
|
|
22
22
|
load_data,
|
|
23
|
+
load_from_path,
|
|
23
24
|
open_component_group,
|
|
24
25
|
open_nexus_file,
|
|
25
26
|
)
|
|
@@ -33,6 +34,7 @@ __all__ = [
|
|
|
33
34
|
'load_all_components',
|
|
34
35
|
'load_component',
|
|
35
36
|
'load_data',
|
|
37
|
+
'load_from_path',
|
|
36
38
|
'open_component_group',
|
|
37
39
|
'open_nexus_file',
|
|
38
40
|
'types',
|
|
@@ -8,7 +8,7 @@ from collections.abc import Generator, Mapping
|
|
|
8
8
|
from contextlib import AbstractContextManager, contextmanager, nullcontext
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from math import prod
|
|
11
|
-
from typing import TypeVar, cast
|
|
11
|
+
from typing import Any, TypeVar, cast
|
|
12
12
|
|
|
13
13
|
import scipp as sc
|
|
14
14
|
import scippnexus as snx
|
|
@@ -42,6 +42,31 @@ class NoLockingIfNeededType:
|
|
|
42
42
|
NoLockingIfNeeded = NoLockingIfNeededType()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def load_from_path(
|
|
46
|
+
location: NeXusLocationSpec,
|
|
47
|
+
definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions,
|
|
48
|
+
) -> Any:
|
|
49
|
+
"""Load a field or group from a NeXus file given its location.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
location:
|
|
54
|
+
Location of the field within the NeXus file (filename, entry name, selection).
|
|
55
|
+
definitions:
|
|
56
|
+
Application definitions to use for the file.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
:
|
|
61
|
+
The loaded field (as a variable, data array, or raw python object) or group
|
|
62
|
+
(as a data group).
|
|
63
|
+
"""
|
|
64
|
+
with open_nexus_file(location.filename, definitions=definitions) as f:
|
|
65
|
+
entry = _unique_child_group(f, snx.NXentry, location.entry_name)
|
|
66
|
+
item = entry[location.component_name]
|
|
67
|
+
return item[location.selection]
|
|
68
|
+
|
|
69
|
+
|
|
45
70
|
def load_component(
|
|
46
71
|
location: NeXusLocationSpec,
|
|
47
72
|
*,
|
|
@@ -691,16 +691,12 @@ def LoadMonitorWorkflow(
|
|
|
691
691
|
|
|
692
692
|
|
|
693
693
|
def LoadDetectorWorkflow(
|
|
694
|
-
*,
|
|
695
|
-
run_types: Iterable[sciline.typing.Key],
|
|
696
|
-
monitor_types: Iterable[sciline.typing.Key],
|
|
694
|
+
*, run_types: Iterable[sciline.typing.Key]
|
|
697
695
|
) -> sciline.Pipeline:
|
|
698
696
|
"""Generic workflow for loading detector data from a NeXus file."""
|
|
699
697
|
wf = sciline.Pipeline(
|
|
700
698
|
(*_common_providers, *_detector_providers),
|
|
701
|
-
constraints=_gather_constraints(
|
|
702
|
-
run_types=run_types, monitor_types=monitor_types
|
|
703
|
-
),
|
|
699
|
+
constraints=_gather_constraints(run_types=run_types, monitor_types=[]),
|
|
704
700
|
)
|
|
705
701
|
wf[DetectorBankSizes] = DetectorBankSizes({})
|
|
706
702
|
wf[PreopenNeXusFile] = PreopenNeXusFile(False)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
"""Normalization routines for neutron data reduction."""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
|
|
7
|
+
import scipp as sc
|
|
8
|
+
|
|
9
|
+
from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_by_monitor_histogram(
|
|
13
|
+
detector: sc.DataArray,
|
|
14
|
+
*,
|
|
15
|
+
monitor: sc.DataArray,
|
|
16
|
+
uncertainty_broadcast_mode: UncertaintyBroadcastMode,
|
|
17
|
+
) -> sc.DataArray:
|
|
18
|
+
"""Normalize detector data by a normalized histogrammed monitor.
|
|
19
|
+
|
|
20
|
+
This normalization accounts for both the (wavelength) profile of the incident beam
|
|
21
|
+
and the integrated neutron flux, meaning measurement duration and source strength.
|
|
22
|
+
|
|
23
|
+
- For *event* detectors, the monitor values are mapped to the detector
|
|
24
|
+
using :func:`scipp.lookup`. That is, for detector event :math:`d_i`,
|
|
25
|
+
:math:`m_i` is the monitor bin value at the same coordinate.
|
|
26
|
+
- For *histogram* detectors, the monitor is rebinned using to the detector
|
|
27
|
+
binning using :func:`scipp.rebin`. Thus, detector value :math:`d_i` and
|
|
28
|
+
monitor value :math:`m_i` correspond to the same bin.
|
|
29
|
+
|
|
30
|
+
In both cases, let :math:`x_i` be the lower bound of monitor bin :math:`i`
|
|
31
|
+
and let :math:`\\Delta x_i = x_{i+1} - x_i` be the width of that bin.
|
|
32
|
+
|
|
33
|
+
The detector is normalized according to
|
|
34
|
+
|
|
35
|
+
.. math::
|
|
36
|
+
|
|
37
|
+
d_i^\\text{Norm} = \\frac{d_i}{m_i} \\Delta x_i
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
detector:
|
|
42
|
+
Input detector data.
|
|
43
|
+
Must have a coordinate named ``monitor.dim``, that is, the single
|
|
44
|
+
dimension name of the **monitor**.
|
|
45
|
+
monitor:
|
|
46
|
+
A histogrammed monitor.
|
|
47
|
+
Must be one-dimensional and have a dimension coordinate, typically "wavelength".
|
|
48
|
+
uncertainty_broadcast_mode:
|
|
49
|
+
Choose how uncertainties of the monitor are broadcast to the sample data.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
:
|
|
54
|
+
``detector`` normalized by ``monitor``.
|
|
55
|
+
If the monitor has masks or contains non-finite values, the output has a mask
|
|
56
|
+
called '_monitor_mask' constructed from the monitor masks and non-finite values.
|
|
57
|
+
|
|
58
|
+
See also
|
|
59
|
+
--------
|
|
60
|
+
normalize_by_monitor_integrated:
|
|
61
|
+
Normalize by an integrated monitor.
|
|
62
|
+
"""
|
|
63
|
+
_check_monitor_range_contains_detector(monitor=monitor, detector=detector)
|
|
64
|
+
|
|
65
|
+
dim = monitor.dim
|
|
66
|
+
|
|
67
|
+
if detector.bins is None:
|
|
68
|
+
monitor = monitor.rebin({dim: detector.coords[dim]})
|
|
69
|
+
detector = _mask_detector_for_norm(detector=detector, monitor=monitor)
|
|
70
|
+
coord = monitor.coords[dim]
|
|
71
|
+
delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=monitor.masks)
|
|
72
|
+
norm = broadcast_uncertainties(
|
|
73
|
+
monitor / delta_w, prototype=detector, mode=uncertainty_broadcast_mode
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if detector.bins is None:
|
|
77
|
+
return detector / norm.rebin({dim: detector.coords[dim]})
|
|
78
|
+
return detector.bins / sc.lookup(norm, dim=dim)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def normalize_by_monitor_integrated(
|
|
82
|
+
detector: sc.DataArray,
|
|
83
|
+
*,
|
|
84
|
+
monitor: sc.DataArray,
|
|
85
|
+
uncertainty_broadcast_mode: UncertaintyBroadcastMode,
|
|
86
|
+
) -> sc.DataArray:
|
|
87
|
+
"""Normalize detector data by an integrated monitor.
|
|
88
|
+
|
|
89
|
+
This normalization accounts only for the integrated neutron flux,
|
|
90
|
+
meaning measurement duration and source strength.
|
|
91
|
+
It does *not* account for the (wavelength) profile of the incident beam.
|
|
92
|
+
For that, see :func:`normalize_by_monitor_histogram`.
|
|
93
|
+
|
|
94
|
+
Let :math:`d_i` be a detector event or the counts in a detector bin.
|
|
95
|
+
The normalized detector is
|
|
96
|
+
|
|
97
|
+
.. math::
|
|
98
|
+
|
|
99
|
+
d_i^\\text{Norm} = \\frac{d_i}{\\sum_j\\, m_j}
|
|
100
|
+
|
|
101
|
+
where :math:`m_j` is the monitor counts in bin :math:`j`.
|
|
102
|
+
Note that this is not a true integral but only a sum over monitor events.
|
|
103
|
+
|
|
104
|
+
The result depends on the range of the monitor but not its
|
|
105
|
+
binning within that range.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
detector:
|
|
110
|
+
Input detector data.
|
|
111
|
+
monitor:
|
|
112
|
+
A histogrammed monitor.
|
|
113
|
+
Must be one-dimensional and have a dimension coordinate, typically "wavelength".
|
|
114
|
+
uncertainty_broadcast_mode:
|
|
115
|
+
Choose how uncertainties of the monitor are broadcast to the sample data.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
:
|
|
120
|
+
`detector` normalized by a monitor.
|
|
121
|
+
If the monitor has masks or contains non-finite values, the output has a mask
|
|
122
|
+
called '_monitor_mask' constructed from the monitor masks and non-finite values.
|
|
123
|
+
|
|
124
|
+
See also
|
|
125
|
+
--------
|
|
126
|
+
normalize_by_monitor_histogram:
|
|
127
|
+
Normalize by a monitor histogram.
|
|
128
|
+
"""
|
|
129
|
+
_check_monitor_range_contains_detector(monitor=monitor, detector=detector)
|
|
130
|
+
detector = _mask_detector_for_norm(detector=detector, monitor=monitor)
|
|
131
|
+
norm = monitor.nansum().data
|
|
132
|
+
norm = broadcast_uncertainties(
|
|
133
|
+
norm, prototype=detector, mode=uncertainty_broadcast_mode
|
|
134
|
+
)
|
|
135
|
+
return detector / norm
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_monitor_range_contains_detector(
|
|
139
|
+
*, monitor: sc.DataArray, detector: sc.DataArray
|
|
140
|
+
) -> None:
|
|
141
|
+
dim = monitor.dim
|
|
142
|
+
if not monitor.coords.is_edges(dim):
|
|
143
|
+
raise sc.CoordError(
|
|
144
|
+
f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Prefer a bin coord over an event coord because this makes the behavior for binned
|
|
148
|
+
# and histogrammed data consistent. If we used an event coord, we might allow a
|
|
149
|
+
# monitor range that is less than the detector bins which is fine for the events,
|
|
150
|
+
# but would be wrong if the detector was subsequently histogrammed.
|
|
151
|
+
if (det_coord := detector.coords.get(dim)) is not None:
|
|
152
|
+
lo = det_coord[dim, :-1].nanmin()
|
|
153
|
+
hi = det_coord[dim, 1:].nanmax()
|
|
154
|
+
elif (det_coord := detector.bins.coords.get(dim)) is not None:
|
|
155
|
+
lo = det_coord.nanmin()
|
|
156
|
+
hi = det_coord.nanmax()
|
|
157
|
+
else:
|
|
158
|
+
raise sc.CoordError(
|
|
159
|
+
f"Missing '{dim}' coordinate in detector for monitor normalization."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if monitor.coords[dim].min() > lo or monitor.coords[dim].max() < hi:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Cannot normalize by monitor: The {dim} range of the monitor "
|
|
165
|
+
f"({monitor.coords[dim].min():c} to {monitor.coords[dim].max():c}) "
|
|
166
|
+
f"is smaller than the range of the detector ({lo:c} to {hi:c})."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _mask_detector_for_norm(
|
|
171
|
+
*, detector: sc.DataArray, monitor: sc.DataArray
|
|
172
|
+
) -> sc.DataArray:
|
|
173
|
+
"""Mask the detector where the monitor is masked.
|
|
174
|
+
|
|
175
|
+
For performance, this applies the monitor mask to the detector bins.
|
|
176
|
+
This can lead to masking more events than strictly necessary if we
|
|
177
|
+
used an event mask.
|
|
178
|
+
"""
|
|
179
|
+
dim = monitor.dim
|
|
180
|
+
|
|
181
|
+
if (monitor_mask := _monitor_mask(monitor)) is None:
|
|
182
|
+
return detector
|
|
183
|
+
|
|
184
|
+
if (detector_coord := detector.coords.get(monitor.dim)) is not None:
|
|
185
|
+
# Apply the mask to the bins or a dense detector.
|
|
186
|
+
# Use rebin to reshape the mask to the detector.
|
|
187
|
+
mask = sc.DataArray(monitor_mask, coords={dim: monitor.coords[dim]}).rebin(
|
|
188
|
+
{dim: detector_coord}
|
|
189
|
+
).data != sc.scalar(0, unit=None)
|
|
190
|
+
return detector.assign_masks({"_monitor_mask": mask})
|
|
191
|
+
|
|
192
|
+
# else: Apply the mask to the events.
|
|
193
|
+
if dim not in detector.bins.coords:
|
|
194
|
+
raise sc.CoordError(
|
|
195
|
+
f"Detector must have coordinate '{dim}' to mask by monitor."
|
|
196
|
+
)
|
|
197
|
+
event_mask = sc.lookup(
|
|
198
|
+
sc.DataArray(monitor_mask, coords={dim: monitor.coords[dim]})
|
|
199
|
+
)[detector.bins.coords[dim]]
|
|
200
|
+
return detector.bins.assign_masks({"_monitor_mask": event_mask})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _monitor_mask(monitor: sc.DataArray) -> sc.Variable | None:
|
|
204
|
+
"""Mask nonfinite and zero monitor values and combine all masks."""
|
|
205
|
+
masks = list(monitor.masks.values())
|
|
206
|
+
|
|
207
|
+
finite = sc.isfinite(monitor.data)
|
|
208
|
+
nonzero = monitor.data != sc.scalar(0, unit=monitor.unit)
|
|
209
|
+
valid = finite & nonzero
|
|
210
|
+
if not valid.all():
|
|
211
|
+
masks.append(~valid)
|
|
212
|
+
|
|
213
|
+
if not masks:
|
|
214
|
+
return None
|
|
215
|
+
return functools.reduce(sc.logical_or, masks)
|
|
@@ -28,6 +28,7 @@ from .types import (
|
|
|
28
28
|
PulseStrideOffset,
|
|
29
29
|
TimeOfFlightLookupTable,
|
|
30
30
|
TimeOfFlightLookupTableFilename,
|
|
31
|
+
ToaDetector,
|
|
31
32
|
TofDetector,
|
|
32
33
|
TofMonitor,
|
|
33
34
|
)
|
|
@@ -51,6 +52,7 @@ __all__ = [
|
|
|
51
52
|
"TimeOfFlightLookupTable",
|
|
52
53
|
"TimeOfFlightLookupTableFilename",
|
|
53
54
|
"TimeResolution",
|
|
55
|
+
"ToaDetector",
|
|
54
56
|
"TofDetector",
|
|
55
57
|
"TofLookupTableWorkflow",
|
|
56
58
|
"TofMonitor",
|
|
@@ -36,6 +36,7 @@ from .types import (
|
|
|
36
36
|
MonitorLtotal,
|
|
37
37
|
PulseStrideOffset,
|
|
38
38
|
TimeOfFlightLookupTable,
|
|
39
|
+
ToaDetector,
|
|
39
40
|
TofDetector,
|
|
40
41
|
TofMonitor,
|
|
41
42
|
)
|
|
@@ -196,12 +197,32 @@ def _guess_pulse_stride_offset(
|
|
|
196
197
|
return sorted(tofs, key=lambda x: sc.isnan(tofs[x]).sum())[0]
|
|
197
198
|
|
|
198
199
|
|
|
199
|
-
def
|
|
200
|
+
def _prepare_tof_interpolation_inputs(
|
|
200
201
|
da: sc.DataArray,
|
|
201
202
|
lookup: sc.DataArray,
|
|
202
203
|
ltotal: sc.Variable,
|
|
203
|
-
pulse_stride_offset: int,
|
|
204
|
-
) ->
|
|
204
|
+
pulse_stride_offset: int | None,
|
|
205
|
+
) -> dict:
|
|
206
|
+
"""
|
|
207
|
+
Prepare the inputs required for the time-of-flight interpolation.
|
|
208
|
+
This function is used when computing the time-of-flight for event data, and for
|
|
209
|
+
computing the time-of-arrival for event data (as they both require guessing the
|
|
210
|
+
pulse_stride_offset if not provided).
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
da:
|
|
215
|
+
Data array with event data.
|
|
216
|
+
lookup:
|
|
217
|
+
Lookup table giving time-of-flight as a function of distance and time of
|
|
218
|
+
arrival.
|
|
219
|
+
ltotal:
|
|
220
|
+
Total length of the flight path from the source to the detector.
|
|
221
|
+
pulse_stride_offset:
|
|
222
|
+
When pulse-skipping, the offset of the first pulse in the stride. This is
|
|
223
|
+
typically zero but can be a small integer < pulse_stride.
|
|
224
|
+
If None, a guess is made.
|
|
225
|
+
"""
|
|
205
226
|
etos = da.bins.coords["event_time_offset"].to(dtype=float, copy=False)
|
|
206
227
|
eto_unit = elem_unit(etos)
|
|
207
228
|
|
|
@@ -259,12 +280,34 @@ def _time_of_flight_data_events(
|
|
|
259
280
|
pulse_index += pulse_stride_offset
|
|
260
281
|
pulse_index %= pulse_stride
|
|
261
282
|
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
return {
|
|
284
|
+
"eto": etos,
|
|
285
|
+
"pulse_index": pulse_index,
|
|
286
|
+
"pulse_period": pulse_period,
|
|
287
|
+
"interp": interp,
|
|
288
|
+
"ltotal": ltotal,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _time_of_flight_data_events(
|
|
293
|
+
da: sc.DataArray,
|
|
294
|
+
lookup: sc.DataArray,
|
|
295
|
+
ltotal: sc.Variable,
|
|
296
|
+
pulse_stride_offset: int | None,
|
|
297
|
+
) -> sc.DataArray:
|
|
298
|
+
inputs = _prepare_tof_interpolation_inputs(
|
|
299
|
+
da=da,
|
|
300
|
+
lookup=lookup,
|
|
264
301
|
ltotal=ltotal,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
302
|
+
pulse_stride_offset=pulse_stride_offset,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Compute time-of-flight for all neutrons using the interpolator
|
|
306
|
+
tofs = inputs["interp"](
|
|
307
|
+
ltotal=inputs["ltotal"],
|
|
308
|
+
event_time_offset=inputs["eto"],
|
|
309
|
+
pulse_index=inputs["pulse_index"],
|
|
310
|
+
pulse_period=inputs["pulse_period"],
|
|
268
311
|
)
|
|
269
312
|
|
|
270
313
|
parts = da.bins.constituents
|
|
@@ -416,6 +459,53 @@ def monitor_time_of_flight_data(
|
|
|
416
459
|
)
|
|
417
460
|
|
|
418
461
|
|
|
462
|
+
def detector_time_of_arrival_data(
|
|
463
|
+
detector_data: RawDetector[RunType],
|
|
464
|
+
lookup: TimeOfFlightLookupTable,
|
|
465
|
+
ltotal: DetectorLtotal[RunType],
|
|
466
|
+
pulse_stride_offset: PulseStrideOffset,
|
|
467
|
+
) -> ToaDetector[RunType]:
|
|
468
|
+
"""
|
|
469
|
+
Convert the time-of-flight data to time-of-arrival data using a lookup table.
|
|
470
|
+
The output data will have a time-of-arrival coordinate.
|
|
471
|
+
The time-of-arrival is the time since the neutron was emitted from the source.
|
|
472
|
+
It is basically equal to event_time_offset + pulse_index * pulse_period.
|
|
473
|
+
|
|
474
|
+
Parameters
|
|
475
|
+
----------
|
|
476
|
+
da:
|
|
477
|
+
Raw detector data loaded from a NeXus file, e.g., NXdetector containing
|
|
478
|
+
NXevent_data.
|
|
479
|
+
lookup:
|
|
480
|
+
Lookup table giving time-of-flight as a function of distance and time of
|
|
481
|
+
arrival.
|
|
482
|
+
ltotal:
|
|
483
|
+
Total length of the flight path from the source to the detector.
|
|
484
|
+
pulse_stride_offset:
|
|
485
|
+
When pulse-skipping, the offset of the first pulse in the stride. This is
|
|
486
|
+
typically zero but can be a small integer < pulse_stride.
|
|
487
|
+
"""
|
|
488
|
+
if detector_data.bins is None:
|
|
489
|
+
raise NotImplementedError(
|
|
490
|
+
"Computing time-of-arrival in histogram mode is not implemented yet."
|
|
491
|
+
)
|
|
492
|
+
inputs = _prepare_tof_interpolation_inputs(
|
|
493
|
+
da=detector_data,
|
|
494
|
+
lookup=lookup,
|
|
495
|
+
ltotal=ltotal,
|
|
496
|
+
pulse_stride_offset=pulse_stride_offset,
|
|
497
|
+
)
|
|
498
|
+
parts = detector_data.bins.constituents
|
|
499
|
+
parts["data"] = inputs["eto"]
|
|
500
|
+
# The pulse index is None if pulse_stride == 1 (i.e., no pulse skipping)
|
|
501
|
+
if inputs["pulse_index"] is not None:
|
|
502
|
+
parts["data"] = parts["data"] + inputs["pulse_index"] * inputs["pulse_period"]
|
|
503
|
+
result = detector_data.bins.assign_coords(
|
|
504
|
+
toa=sc.bins(**parts, validate_indices=False)
|
|
505
|
+
)
|
|
506
|
+
return result
|
|
507
|
+
|
|
508
|
+
|
|
419
509
|
def providers() -> tuple[Callable]:
|
|
420
510
|
"""
|
|
421
511
|
Providers of the time-of-flight workflow.
|
|
@@ -425,4 +515,5 @@ def providers() -> tuple[Callable]:
|
|
|
425
515
|
monitor_time_of_flight_data,
|
|
426
516
|
detector_ltotal_from_straight_line_approximation,
|
|
427
517
|
monitor_ltotal_from_straight_line_approximation,
|
|
518
|
+
detector_time_of_arrival_data,
|
|
428
519
|
)
|
|
@@ -420,7 +420,7 @@ def simulate_chopper_cascade_using_tof(
|
|
|
420
420
|
else tof.Clockwise,
|
|
421
421
|
open=ch.slit_begin,
|
|
422
422
|
close=ch.slit_end,
|
|
423
|
-
phase=
|
|
423
|
+
phase=ch.phase if ch.frequency.value > 0.0 else -ch.phase,
|
|
424
424
|
distance=sc.norm(
|
|
425
425
|
ch.axle_position - source_position.to(unit=ch.axle_position.unit)
|
|
426
426
|
),
|
|
@@ -37,5 +37,17 @@ class TofDetector(sl.Scope[RunType, sc.DataArray], sc.DataArray):
|
|
|
37
37
|
"""Detector data with time-of-flight coordinate."""
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class ToaDetector(sl.Scope[RunType, sc.DataArray], sc.DataArray):
|
|
41
|
+
"""Detector data with time-of-arrival coordinate.
|
|
42
|
+
|
|
43
|
+
When the pulse stride is 1 (i.e., no pulse skipping), the time-of-arrival is the
|
|
44
|
+
same as the event_time_offset. When pulse skipping is used, the time-of-arrival is
|
|
45
|
+
the event_time_offset + pulse_offset * pulse_period.
|
|
46
|
+
This means that the time-of-arrival is basically the event_time_offset wrapped
|
|
47
|
+
over the frame period instead of the pulse period
|
|
48
|
+
(where frame_period = pulse_stride * pulse_period).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
40
52
|
class TofMonitor(sl.Scope[RunType, MonitorType, sc.DataArray], sc.DataArray):
|
|
41
53
|
"""Monitor data with time-of-flight coordinate."""
|
|
@@ -70,6 +70,7 @@ requirements/wheels.txt
|
|
|
70
70
|
resources/logo.svg
|
|
71
71
|
src/ess/reduce/__init__.py
|
|
72
72
|
src/ess/reduce/logging.py
|
|
73
|
+
src/ess/reduce/normalization.py
|
|
73
74
|
src/ess/reduce/parameter.py
|
|
74
75
|
src/ess/reduce/py.typed
|
|
75
76
|
src/ess/reduce/streaming.py
|
|
@@ -118,6 +119,7 @@ src/essreduce.egg-info/requires.txt
|
|
|
118
119
|
src/essreduce.egg-info/top_level.txt
|
|
119
120
|
tests/accumulators_test.py
|
|
120
121
|
tests/conftest.py
|
|
122
|
+
tests/normalization_test.py
|
|
121
123
|
tests/package_test.py
|
|
122
124
|
tests/streaming_test.py
|
|
123
125
|
tests/uncertainty_test.py
|
|
@@ -4,11 +4,12 @@ from datetime import UTC, datetime
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
import sciline as sl
|
|
7
8
|
import scipp as sc
|
|
8
9
|
import scippnexus as snx
|
|
9
10
|
from scipp.testing import assert_identical
|
|
10
11
|
|
|
11
|
-
from ess.reduce.nexus import compute_component_position, workflow
|
|
12
|
+
from ess.reduce.nexus import compute_component_position, load_from_path, workflow
|
|
12
13
|
from ess.reduce.nexus.types import (
|
|
13
14
|
BackgroundRun,
|
|
14
15
|
Beamline,
|
|
@@ -21,6 +22,8 @@ from ess.reduce.nexus.types import (
|
|
|
21
22
|
Measurement,
|
|
22
23
|
MonitorType,
|
|
23
24
|
NeXusComponentLocationSpec,
|
|
25
|
+
NeXusFileSpec,
|
|
26
|
+
NeXusLocationSpec,
|
|
24
27
|
NeXusName,
|
|
25
28
|
NeXusTransformation,
|
|
26
29
|
PreopenNeXusFile,
|
|
@@ -577,7 +580,7 @@ def test_load_histogram_monitor_workflow(dream_coda_test_file: Path) -> None:
|
|
|
577
580
|
|
|
578
581
|
|
|
579
582
|
def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None:
|
|
580
|
-
wf = LoadDetectorWorkflow(run_types=[SampleRun]
|
|
583
|
+
wf = LoadDetectorWorkflow(run_types=[SampleRun])
|
|
581
584
|
wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250
|
|
582
585
|
wf[NeXusName[snx.NXdetector]] = 'larmor_detector'
|
|
583
586
|
da = wf.compute(RawDetector[SampleRun])
|
|
@@ -587,7 +590,7 @@ def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None:
|
|
|
587
590
|
|
|
588
591
|
|
|
589
592
|
def test_load_histogram_detector_workflow(tbl_commissioning_orca_file: Path) -> None:
|
|
590
|
-
wf = LoadDetectorWorkflow(run_types=[SampleRun]
|
|
593
|
+
wf = LoadDetectorWorkflow(run_types=[SampleRun])
|
|
591
594
|
wf[Filename[SampleRun]] = tbl_commissioning_orca_file
|
|
592
595
|
wf[NeXusName[snx.NXdetector]] = 'orca_detector'
|
|
593
596
|
da = wf.compute(RawDetector[SampleRun])
|
|
@@ -600,7 +603,7 @@ def test_load_histogram_detector_workflow(tbl_commissioning_orca_file: Path) ->
|
|
|
600
603
|
def test_load_empty_histogram_detector_workflow(
|
|
601
604
|
tbl_commissioning_orca_file: Path,
|
|
602
605
|
) -> None:
|
|
603
|
-
wf = LoadDetectorWorkflow(run_types=[SampleRun]
|
|
606
|
+
wf = LoadDetectorWorkflow(run_types=[SampleRun])
|
|
604
607
|
wf[Filename[SampleRun]] = tbl_commissioning_orca_file
|
|
605
608
|
wf[NeXusName[snx.NXdetector]] = 'orca_detector'
|
|
606
609
|
da = wf.compute(EmptyDetector[SampleRun])
|
|
@@ -776,3 +779,48 @@ def assert_not_contains_type_arg(node: object, excluded: set[type]) -> None:
|
|
|
776
779
|
assert not any(
|
|
777
780
|
arg in excluded for arg in getattr(node, "__args__", ())
|
|
778
781
|
), f"Node {node} contains one of {excluded!r}"
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def test_generic_nexus_workflow_load_custom_field_user_affiliation(
|
|
785
|
+
loki_tutorial_sample_run_60250: Path,
|
|
786
|
+
) -> None:
|
|
787
|
+
class UserAffiliation(sl.Scope[RunType, str], str):
|
|
788
|
+
"""User affiliation."""
|
|
789
|
+
|
|
790
|
+
def load_user_affiliation(
|
|
791
|
+
file: NeXusFileSpec[RunType], path: NeXusName[UserAffiliation[RunType]]
|
|
792
|
+
) -> UserAffiliation[RunType]:
|
|
793
|
+
return UserAffiliation[RunType](
|
|
794
|
+
load_from_path(NeXusLocationSpec(filename=file.value, component_name=path))
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[])
|
|
798
|
+
wf.insert(load_user_affiliation)
|
|
799
|
+
wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250
|
|
800
|
+
# Path is relative to the top-level '/entry'
|
|
801
|
+
wf[NeXusName[UserAffiliation[SampleRun]]] = 'user_0/affiliation'
|
|
802
|
+
affiliation = wf.compute(UserAffiliation[SampleRun])
|
|
803
|
+
assert affiliation == 'ESS'
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def test_generic_nexus_workflow_load_custom_group_user(
|
|
807
|
+
loki_tutorial_sample_run_60250: Path,
|
|
808
|
+
) -> None:
|
|
809
|
+
class UserInfo(sl.Scope[RunType, str], str):
|
|
810
|
+
"""User info."""
|
|
811
|
+
|
|
812
|
+
def load_user_info(
|
|
813
|
+
file: NeXusFileSpec[RunType], path: NeXusName[UserInfo[RunType]]
|
|
814
|
+
) -> UserInfo[RunType]:
|
|
815
|
+
return UserInfo[RunType](
|
|
816
|
+
load_from_path(NeXusLocationSpec(filename=file.value, component_name=path))
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[])
|
|
820
|
+
wf.insert(load_user_info)
|
|
821
|
+
wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250
|
|
822
|
+
# Path is relative to the top-level '/entry'
|
|
823
|
+
wf[NeXusName[UserInfo]] = 'user_0'
|
|
824
|
+
user_info = wf.compute(UserInfo[SampleRun])
|
|
825
|
+
assert user_info['affiliation'] == 'ESS'
|
|
826
|
+
assert user_info['name'] == 'John Doe'
|