essreduce 25.11.0__tar.gz → 25.11.2__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.
Files changed (147) hide show
  1. {essreduce-25.11.0 → essreduce-25.11.2}/PKG-INFO +1 -1
  2. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/base.txt +1 -1
  3. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/basetest.txt +1 -1
  4. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/nexus/__init__.py +2 -0
  5. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/nexus/_nexus_loader.py +26 -1
  6. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/nexus/workflow.py +59 -17
  7. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/__init__.py +2 -0
  8. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/eto_to_tof.py +99 -8
  9. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/fakes.py +1 -1
  10. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/lut.py +1 -1
  11. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/types.py +12 -0
  12. {essreduce-25.11.0 → essreduce-25.11.2}/src/essreduce.egg-info/PKG-INFO +1 -1
  13. {essreduce-25.11.0 → essreduce-25.11.2}/tests/conftest.py +2 -2
  14. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/workflow_test.py +76 -2
  15. {essreduce-25.11.0 → essreduce-25.11.2}/tests/time_of_flight/unwrap_test.py +66 -1
  16. {essreduce-25.11.0 → essreduce-25.11.2}/.copier-answers.ess.yml +0 -0
  17. {essreduce-25.11.0 → essreduce-25.11.2}/.copier-answers.yml +0 -0
  18. {essreduce-25.11.0 → essreduce-25.11.2}/.github/ISSUE_TEMPLATE/high-level-requirement.yml +0 -0
  19. {essreduce-25.11.0 → essreduce-25.11.2}/.github/dependabot.yml +0 -0
  20. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/ci.yml +0 -0
  21. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/docs.yml +0 -0
  22. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/nightly_at_main.yml +0 -0
  23. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/nightly_at_main_lower_bound.yml +0 -0
  24. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/nightly_at_release.yml +0 -0
  25. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/python-version-ci +0 -0
  26. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/release.yml +0 -0
  27. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/test.yml +0 -0
  28. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/unpinned.yml +0 -0
  29. {essreduce-25.11.0 → essreduce-25.11.2}/.github/workflows/weekly_windows_macos.yml +0 -0
  30. {essreduce-25.11.0 → essreduce-25.11.2}/.gitignore +0 -0
  31. {essreduce-25.11.0 → essreduce-25.11.2}/.pre-commit-config.yaml +0 -0
  32. {essreduce-25.11.0 → essreduce-25.11.2}/.python-version +0 -0
  33. {essreduce-25.11.0 → essreduce-25.11.2}/CODE_OF_CONDUCT.md +0 -0
  34. {essreduce-25.11.0 → essreduce-25.11.2}/CONTRIBUTING.md +0 -0
  35. {essreduce-25.11.0 → essreduce-25.11.2}/LICENSE +0 -0
  36. {essreduce-25.11.0 → essreduce-25.11.2}/MANIFEST.in +0 -0
  37. {essreduce-25.11.0 → essreduce-25.11.2}/README.md +0 -0
  38. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_static/anaconda-icon.js +0 -0
  39. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_static/favicon.svg +0 -0
  40. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_static/logo-dark.svg +0 -0
  41. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_static/logo.svg +0 -0
  42. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_templates/class-template.rst +0 -0
  43. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_templates/doc_version.html +0 -0
  44. {essreduce-25.11.0 → essreduce-25.11.2}/docs/_templates/module-template.rst +0 -0
  45. {essreduce-25.11.0 → essreduce-25.11.2}/docs/about/index.md +0 -0
  46. {essreduce-25.11.0 → essreduce-25.11.2}/docs/api-reference/index.md +0 -0
  47. {essreduce-25.11.0 → essreduce-25.11.2}/docs/conf.py +0 -0
  48. {essreduce-25.11.0 → essreduce-25.11.2}/docs/developer/coding-conventions.md +0 -0
  49. {essreduce-25.11.0 → essreduce-25.11.2}/docs/developer/dependency-management.md +0 -0
  50. {essreduce-25.11.0 → essreduce-25.11.2}/docs/developer/getting-started.md +0 -0
  51. {essreduce-25.11.0 → essreduce-25.11.2}/docs/developer/gui.ipynb +0 -0
  52. {essreduce-25.11.0 → essreduce-25.11.2}/docs/developer/index.md +0 -0
  53. {essreduce-25.11.0 → essreduce-25.11.2}/docs/index.md +0 -0
  54. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/index.md +0 -0
  55. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/installation.md +0 -0
  56. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/reduction-workflow-guidelines.md +0 -0
  57. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/tof/dream.ipynb +0 -0
  58. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/tof/frame-unwrapping.ipynb +0 -0
  59. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/tof/index.md +0 -0
  60. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/tof/wfm.ipynb +0 -0
  61. {essreduce-25.11.0 → essreduce-25.11.2}/docs/user-guide/widget.md +0 -0
  62. {essreduce-25.11.0 → essreduce-25.11.2}/pyproject.toml +0 -0
  63. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/base.in +0 -0
  64. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/basetest.in +0 -0
  65. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/ci.in +0 -0
  66. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/ci.txt +0 -0
  67. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/dev.in +0 -0
  68. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/dev.txt +0 -0
  69. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/docs.in +0 -0
  70. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/docs.txt +0 -0
  71. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/make_base.py +0 -0
  72. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/mypy.in +0 -0
  73. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/mypy.txt +0 -0
  74. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/nightly.in +0 -0
  75. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/nightly.txt +0 -0
  76. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/static.in +0 -0
  77. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/static.txt +0 -0
  78. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/test.in +0 -0
  79. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/test.txt +0 -0
  80. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/wheels.in +0 -0
  81. {essreduce-25.11.0 → essreduce-25.11.2}/requirements/wheels.txt +0 -0
  82. {essreduce-25.11.0 → essreduce-25.11.2}/resources/logo.svg +0 -0
  83. {essreduce-25.11.0 → essreduce-25.11.2}/setup.cfg +0 -0
  84. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/__init__.py +0 -0
  85. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/data/__init__.py +0 -0
  86. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/data/_registry.py +0 -0
  87. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/live/__init__.py +0 -0
  88. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/live/raw.py +0 -0
  89. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/live/roi.py +0 -0
  90. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/live/workflow.py +0 -0
  91. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/logging.py +0 -0
  92. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/nexus/json_generator.py +0 -0
  93. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/nexus/json_nexus.py +0 -0
  94. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/nexus/types.py +0 -0
  95. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/parameter.py +0 -0
  96. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/py.typed +0 -0
  97. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/scripts/grow_nexus.py +0 -0
  98. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/streaming.py +0 -0
  99. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/interpolator_numba.py +0 -0
  100. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/interpolator_scipy.py +0 -0
  101. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/resample.py +0 -0
  102. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/time_of_flight/workflow.py +0 -0
  103. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/ui.py +0 -0
  104. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/uncertainty.py +0 -0
  105. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/__init__.py +0 -0
  106. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_base.py +0 -0
  107. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_binedges_widget.py +0 -0
  108. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_bounds_widget.py +0 -0
  109. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_config.py +0 -0
  110. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_filename_widget.py +0 -0
  111. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_linspace_widget.py +0 -0
  112. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_optional_widget.py +0 -0
  113. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_spinner.py +0 -0
  114. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_string_widget.py +0 -0
  115. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_switchable_widget.py +0 -0
  116. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/widgets/_vector_widget.py +0 -0
  117. {essreduce-25.11.0 → essreduce-25.11.2}/src/ess/reduce/workflow.py +0 -0
  118. {essreduce-25.11.0 → essreduce-25.11.2}/src/essreduce.egg-info/SOURCES.txt +0 -0
  119. {essreduce-25.11.0 → essreduce-25.11.2}/src/essreduce.egg-info/dependency_links.txt +0 -0
  120. {essreduce-25.11.0 → essreduce-25.11.2}/src/essreduce.egg-info/entry_points.txt +0 -0
  121. {essreduce-25.11.0 → essreduce-25.11.2}/src/essreduce.egg-info/requires.txt +0 -0
  122. {essreduce-25.11.0 → essreduce-25.11.2}/src/essreduce.egg-info/top_level.txt +0 -0
  123. {essreduce-25.11.0 → essreduce-25.11.2}/tests/accumulators_test.py +0 -0
  124. {essreduce-25.11.0 → essreduce-25.11.2}/tests/live/raw_test.py +0 -0
  125. {essreduce-25.11.0 → essreduce-25.11.2}/tests/live/roi_test.py +0 -0
  126. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_generator_test.py +0 -0
  127. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/array_dataset.json +0 -0
  128. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/dataset.json +0 -0
  129. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/detector.json +0 -0
  130. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/entry.json +0 -0
  131. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/event_data.json +0 -0
  132. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/instrument.json +0 -0
  133. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_examples/log.json +0 -0
  134. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/json_nexus_test.py +0 -0
  135. {essreduce-25.11.0 → essreduce-25.11.2}/tests/nexus/nexus_loader_test.py +0 -0
  136. {essreduce-25.11.0 → essreduce-25.11.2}/tests/package_test.py +0 -0
  137. {essreduce-25.11.0 → essreduce-25.11.2}/tests/scripts/test_grow_nexus.py +0 -0
  138. {essreduce-25.11.0 → essreduce-25.11.2}/tests/streaming_test.py +0 -0
  139. {essreduce-25.11.0 → essreduce-25.11.2}/tests/time_of_flight/interpolator_test.py +0 -0
  140. {essreduce-25.11.0 → essreduce-25.11.2}/tests/time_of_flight/lut_test.py +0 -0
  141. {essreduce-25.11.0 → essreduce-25.11.2}/tests/time_of_flight/resample_tests.py +0 -0
  142. {essreduce-25.11.0 → essreduce-25.11.2}/tests/time_of_flight/wfm_test.py +0 -0
  143. {essreduce-25.11.0 → essreduce-25.11.2}/tests/time_of_flight/workflow_test.py +0 -0
  144. {essreduce-25.11.0 → essreduce-25.11.2}/tests/uncertainty_test.py +0 -0
  145. {essreduce-25.11.0 → essreduce-25.11.2}/tests/widget_test.py +0 -0
  146. {essreduce-25.11.0 → essreduce-25.11.2}/tools/shrink_nexus.py +0 -0
  147. {essreduce-25.11.0 → essreduce-25.11.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: essreduce
3
- Version: 25.11.0
3
+ Version: 25.11.2
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License-Expression: BSD-3-Clause
@@ -68,7 +68,7 @@ python-dateutil==2.9.0.post0
68
68
  # scippnexus
69
69
  sciline==25.11.0
70
70
  # via -r base.in
71
- scipp==25.8.0
71
+ scipp==25.11.0
72
72
  # via
73
73
  # -r base.in
74
74
  # scippneutron
@@ -97,7 +97,7 @@ python-dateutil==2.9.0.post0
97
97
  # via matplotlib
98
98
  requests==2.32.5
99
99
  # via pooch
100
- scipp==25.8.0
100
+ scipp==25.11.0
101
101
  # via tof
102
102
  scipy==1.16.3
103
103
  # via
@@ -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
  *,
@@ -385,7 +385,11 @@ def get_calibrated_detector(
385
385
  # If the NXdetector in the file is not 1-D, we want to match the order of dims.
386
386
  # zip_pixel_offsets otherwise yields a vector with dimensions in the order given
387
387
  # by the x/y/z offsets.
388
- offsets = snx.zip_pixel_offsets(da.coords).transpose(da.dims).copy()
388
+ offsets = snx.zip_pixel_offsets(da.coords)
389
+ # Get the dims in the order of the detector data array, but filter out dims that
390
+ # don't exist in the offsets (e.g. the detector data may have a 'time' dimension).
391
+ dims = [dim for dim in da.dims if dim in offsets.dims]
392
+ offsets = offsets.transpose(dims).copy()
389
393
  # We use the unit of the offsets as this is likely what the user expects.
390
394
  if transform.value.unit is not None and transform.value.unit != '':
391
395
  transform_value = transform.value.to(unit=offsets.unit)
@@ -399,7 +403,7 @@ def get_calibrated_detector(
399
403
 
400
404
  def assemble_detector_data(
401
405
  detector: EmptyDetector[RunType],
402
- event_data: NeXusData[snx.NXdetector, RunType],
406
+ neutron_data: NeXusData[snx.NXdetector, RunType],
403
407
  ) -> RawDetector[RunType]:
404
408
  """
405
409
  Assemble a detector data array with event data.
@@ -410,14 +414,15 @@ def assemble_detector_data(
410
414
  ----------
411
415
  detector:
412
416
  Calibrated detector data array.
413
- event_data:
414
- Event data array.
417
+ neutron_data:
418
+ Neutron data array (events or histogram).
415
419
  """
416
- grouped = nexus.group_event_data(
417
- event_data=event_data, detector_number=detector.coords['detector_number']
418
- )
420
+ if neutron_data.bins is not None:
421
+ neutron_data = nexus.group_event_data(
422
+ event_data=neutron_data, detector_number=detector.coords['detector_number']
423
+ )
419
424
  return RawDetector[RunType](
420
- _add_variances(grouped)
425
+ _add_variances(neutron_data)
421
426
  .assign_coords(detector.coords)
422
427
  .assign_masks(detector.masks)
423
428
  )
@@ -504,6 +509,19 @@ def _drop(
504
509
  }
505
510
 
506
511
 
512
+ class _EmptyField:
513
+ """Empty field that can replace a missing detector_number in NXdetector."""
514
+
515
+ def __init__(self, sizes: dict[str, int]):
516
+ self.attrs = {}
517
+ self.sizes = sizes.copy()
518
+ self.dims = tuple(sizes.keys())
519
+ self.shape = tuple(sizes.values())
520
+
521
+ def __getitem__(self, key: Any) -> sc.Variable:
522
+ return sc.zeros(dims=self.dims, shape=self.shape, unit=None, dtype='int32')
523
+
524
+
507
525
  class _StrippedDetector(snx.NXdetector):
508
526
  """Detector definition without large geometry or event data for ScippNexus.
509
527
 
@@ -513,8 +531,36 @@ class _StrippedDetector(snx.NXdetector):
513
531
  def __init__(
514
532
  self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group]
515
533
  ):
516
- children = _drop(children, (snx.NXoff_geometry, snx.NXevent_data))
517
- children['data'] = children['detector_number']
534
+ if 'detector_number' in children:
535
+ data = children['detector_number']
536
+ else:
537
+ # We get the 'data' sizes before the NXdata is dropped
538
+ if 'data' not in children:
539
+ raise KeyError(
540
+ "StrippedDetector: Cannot determine shape of the detector. "
541
+ "No 'detector_number' was found, and the 'data' entry is missing."
542
+ )
543
+ if 'value' not in children['data']:
544
+ raise KeyError(
545
+ "StrippedDetector: Cannot determine shape of the detector. "
546
+ "The 'data' entry has no 'value'."
547
+ )
548
+ # We drop any time-related dimension from the data sizes, as they are not
549
+ # relevant for the detector geometry/shape.
550
+ data = _EmptyField(
551
+ sizes={
552
+ dim: size
553
+ for dim, size in children['data']['value'].sizes.items()
554
+ if dim not in ('time', 'frame_time')
555
+ }
556
+ )
557
+
558
+ children = _drop(
559
+ children, (snx.NXoff_geometry, snx.NXevent_data, snx.NXdata, snx.NXlog)
560
+ )
561
+
562
+ children['data'] = data
563
+
518
564
  super().__init__(attrs=attrs, children=children)
519
565
 
520
566
 
@@ -528,7 +574,7 @@ class _DummyField:
528
574
  self.shape = (0,)
529
575
 
530
576
  def __getitem__(self, key: Any) -> sc.Variable:
531
- return sc.empty(dims=self.dims, shape=self.shape, unit=None)
577
+ return sc.zeros(dims=self.dims, shape=self.shape, unit=None, dtype='int32')
532
578
 
533
579
 
534
580
  class _StrippedMonitor(snx.NXmonitor):
@@ -645,16 +691,12 @@ def LoadMonitorWorkflow(
645
691
 
646
692
 
647
693
  def LoadDetectorWorkflow(
648
- *,
649
- run_types: Iterable[sciline.typing.Key],
650
- monitor_types: Iterable[sciline.typing.Key],
694
+ *, run_types: Iterable[sciline.typing.Key]
651
695
  ) -> sciline.Pipeline:
652
696
  """Generic workflow for loading detector data from a NeXus file."""
653
697
  wf = sciline.Pipeline(
654
698
  (*_common_providers, *_detector_providers),
655
- constraints=_gather_constraints(
656
- run_types=run_types, monitor_types=monitor_types
657
- ),
699
+ constraints=_gather_constraints(run_types=run_types, monitor_types=[]),
658
700
  )
659
701
  wf[DetectorBankSizes] = DetectorBankSizes({})
660
702
  wf[PreopenNeXusFile] = PreopenNeXusFile(False)
@@ -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 _time_of_flight_data_events(
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
- ) -> sc.DataArray:
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
- # Compute time-of-flight for all neutrons using the interpolator
263
- tofs = interp(
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
- event_time_offset=etos,
266
- pulse_index=pulse_index,
267
- pulse_period=pulse_period,
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
  )
@@ -56,7 +56,7 @@ class FakeBeamline:
56
56
  else tof_pkg.Clockwise,
57
57
  open=ch.slit_begin,
58
58
  close=ch.slit_end,
59
- phase=abs(ch.phase),
59
+ phase=ch.phase if ch.frequency.value > 0.0 else -ch.phase,
60
60
  distance=sc.norm(ch.axle_position - source_position),
61
61
  name=name,
62
62
  )
@@ -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=abs(ch.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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: essreduce
3
- Version: 25.11.0
3
+ Version: 25.11.2
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License-Expression: BSD-3-Clause
@@ -63,9 +63,9 @@ def tbl_registry() -> Registry:
63
63
  return make_registry(
64
64
  'ess/tbl',
65
65
  files={
66
- "857127_00000112_small.hdf": "md5:6f3059e0e5e111a2a8f1b368f24c6f93",
66
+ "857127_00000112_small.hdf": "md5:0db89493b859dbb2f7354c3711ed7fbd",
67
67
  },
68
- version='1',
68
+ version='2',
69
69
  )
70
70
 
71
71
 
@@ -4,15 +4,17 @@ 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,
15
16
  EmptyBeamRun,
17
+ EmptyDetector,
16
18
  Filename,
17
19
  FrameMonitor0,
18
20
  FrameMonitor1,
@@ -20,6 +22,8 @@ from ess.reduce.nexus.types import (
20
22
  Measurement,
21
23
  MonitorType,
22
24
  NeXusComponentLocationSpec,
25
+ NeXusFileSpec,
26
+ NeXusLocationSpec,
23
27
  NeXusName,
24
28
  NeXusTransformation,
25
29
  PreopenNeXusFile,
@@ -576,7 +580,7 @@ def test_load_histogram_monitor_workflow(dream_coda_test_file: Path) -> None:
576
580
 
577
581
 
578
582
  def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None:
579
- wf = LoadDetectorWorkflow(run_types=[SampleRun], monitor_types=[])
583
+ wf = LoadDetectorWorkflow(run_types=[SampleRun])
580
584
  wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250
581
585
  wf[NeXusName[snx.NXdetector]] = 'larmor_detector'
582
586
  da = wf.compute(RawDetector[SampleRun])
@@ -585,6 +589,31 @@ def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None:
585
589
  assert da.dims == ('detector_number',)
586
590
 
587
591
 
592
+ def test_load_histogram_detector_workflow(tbl_commissioning_orca_file: Path) -> None:
593
+ wf = LoadDetectorWorkflow(run_types=[SampleRun])
594
+ wf[Filename[SampleRun]] = tbl_commissioning_orca_file
595
+ wf[NeXusName[snx.NXdetector]] = 'orca_detector'
596
+ da = wf.compute(RawDetector[SampleRun])
597
+ assert 'position' in da.coords
598
+ assert da.bins is None
599
+ assert 'time' in da.dims
600
+ assert da.ndim == 3
601
+
602
+
603
+ def test_load_empty_histogram_detector_workflow(
604
+ tbl_commissioning_orca_file: Path,
605
+ ) -> None:
606
+ wf = LoadDetectorWorkflow(run_types=[SampleRun])
607
+ wf[Filename[SampleRun]] = tbl_commissioning_orca_file
608
+ wf[NeXusName[snx.NXdetector]] = 'orca_detector'
609
+ da = wf.compute(EmptyDetector[SampleRun])
610
+ assert 'position' in da.coords
611
+ assert da.bins is None
612
+ # The empty detector has no time dimension, only the dimensions of the geometry
613
+ assert 'time' not in da.dims
614
+ assert da.ndim == 2
615
+
616
+
588
617
  @pytest.mark.parametrize('preopen', [True, False])
589
618
  def test_generic_nexus_workflow(
590
619
  preopen: bool, loki_tutorial_sample_run_60250: Path
@@ -750,3 +779,48 @@ def assert_not_contains_type_arg(node: object, excluded: set[type]) -> None:
750
779
  assert not any(
751
780
  arg in excluded for arg in getattr(node, "__args__", ())
752
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'
@@ -9,7 +9,12 @@ from scippneutron.conversion.graph.tof import elastic as elastic_graph
9
9
 
10
10
  from ess.reduce import time_of_flight
11
11
  from ess.reduce.nexus.types import AnyRun, RawDetector, SampleRun
12
- from ess.reduce.time_of_flight import GenericTofWorkflow, TofLookupTableWorkflow, fakes
12
+ from ess.reduce.time_of_flight import (
13
+ GenericTofWorkflow,
14
+ PulsePeriod,
15
+ TofLookupTableWorkflow,
16
+ fakes,
17
+ )
13
18
 
14
19
  sl = pytest.importorskip("sciline")
15
20
 
@@ -441,3 +446,63 @@ def test_unwrap_int(dtype, lut_workflow_psc_choppers) -> None:
441
446
  _validate_result_events(
442
447
  tofs=tofs, ref=ref, percentile=100, diff_threshold=0.02, rtol=0.05
443
448
  )
449
+
450
+
451
+ def test_compute_toa():
452
+ distance = sc.scalar(80.0, unit="m")
453
+ choppers = fakes.psc_choppers()
454
+
455
+ lut_wf = make_lut_workflow(
456
+ choppers=choppers, neutrons=500_000, seed=1234, pulse_stride=1
457
+ )
458
+
459
+ pl, _ = _make_workflow_event_mode(
460
+ distance=distance,
461
+ choppers=choppers,
462
+ lut_workflow=lut_wf,
463
+ seed=2,
464
+ pulse_stride_offset=0,
465
+ error_threshold=0.1,
466
+ )
467
+
468
+ toas = pl.compute(time_of_flight.ToaDetector[SampleRun])
469
+
470
+ assert "toa" in toas.bins.coords
471
+ raw = pl.compute(RawDetector[SampleRun])
472
+ assert sc.allclose(toas.bins.coords["toa"], raw.bins.coords["event_time_offset"])
473
+
474
+
475
+ def test_compute_toa_pulse_skipping():
476
+ distance = sc.scalar(100.0, unit="m")
477
+ choppers = fakes.pulse_skipping_choppers()
478
+
479
+ lut_wf = make_lut_workflow(
480
+ choppers=choppers, neutrons=500_000, seed=1234, pulse_stride=2
481
+ )
482
+
483
+ pl, _ = _make_workflow_event_mode(
484
+ distance=distance,
485
+ choppers=choppers,
486
+ lut_workflow=lut_wf,
487
+ seed=2,
488
+ pulse_stride_offset=1,
489
+ error_threshold=0.1,
490
+ )
491
+
492
+ raw = pl.compute(RawDetector[SampleRun])
493
+
494
+ toas = pl.compute(time_of_flight.ToaDetector[SampleRun])
495
+
496
+ assert "toa" in toas.bins.coords
497
+ pulse_period = lut_wf.compute(PulsePeriod)
498
+ hist = toas.bins.concat().hist(
499
+ toa=sc.array(
500
+ dims=["toa"],
501
+ values=[0, pulse_period.value, pulse_period.value * 2],
502
+ unit=pulse_period.unit,
503
+ ).to(unit=toas.bins.coords["toa"].unit)
504
+ )
505
+ # There should be counts in both bins
506
+ n = raw.sum().value
507
+ assert hist.data[0].value > n / 5
508
+ assert hist.data[1].value > n / 5
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes