essreduce 25.2.4__tar.gz → 25.2.6__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 (137) hide show
  1. {essreduce-25.2.4/src/essreduce.egg-info → essreduce-25.2.6}/PKG-INFO +1 -1
  2. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/live/raw.py +34 -65
  3. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/nexus/_nexus_loader.py +9 -4
  4. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/streaming.py +114 -3
  5. {essreduce-25.2.4 → essreduce-25.2.6/src/essreduce.egg-info}/PKG-INFO +1 -1
  6. {essreduce-25.2.4 → essreduce-25.2.6}/tests/live/raw_test.py +2 -3
  7. {essreduce-25.2.4 → essreduce-25.2.6}/tests/live/roi_test.py +4 -5
  8. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/nexus_loader_test.py +49 -15
  9. {essreduce-25.2.4 → essreduce-25.2.6}/tests/streaming_test.py +125 -5
  10. {essreduce-25.2.4 → essreduce-25.2.6}/.copier-answers.ess.yml +0 -0
  11. {essreduce-25.2.4 → essreduce-25.2.6}/.copier-answers.yml +0 -0
  12. {essreduce-25.2.4 → essreduce-25.2.6}/.github/ISSUE_TEMPLATE/blank.md +0 -0
  13. {essreduce-25.2.4 → essreduce-25.2.6}/.github/ISSUE_TEMPLATE/high-level-requirement.yml +0 -0
  14. {essreduce-25.2.4 → essreduce-25.2.6}/.github/dependabot.yml +0 -0
  15. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/ci.yml +0 -0
  16. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/docs.yml +0 -0
  17. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/nightly_at_main.yml +0 -0
  18. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/nightly_at_release.yml +0 -0
  19. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/python-version-ci +0 -0
  20. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/release.yml +0 -0
  21. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/test.yml +0 -0
  22. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/unpinned.yml +0 -0
  23. {essreduce-25.2.4 → essreduce-25.2.6}/.github/workflows/weekly_windows_macos.yml +0 -0
  24. {essreduce-25.2.4 → essreduce-25.2.6}/.gitignore +0 -0
  25. {essreduce-25.2.4 → essreduce-25.2.6}/.pre-commit-config.yaml +0 -0
  26. {essreduce-25.2.4 → essreduce-25.2.6}/.python-version +0 -0
  27. {essreduce-25.2.4 → essreduce-25.2.6}/CODE_OF_CONDUCT.md +0 -0
  28. {essreduce-25.2.4 → essreduce-25.2.6}/CONTRIBUTING.md +0 -0
  29. {essreduce-25.2.4 → essreduce-25.2.6}/LICENSE +0 -0
  30. {essreduce-25.2.4 → essreduce-25.2.6}/MANIFEST.in +0 -0
  31. {essreduce-25.2.4 → essreduce-25.2.6}/README.md +0 -0
  32. {essreduce-25.2.4 → essreduce-25.2.6}/conda/meta.yaml +0 -0
  33. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_static/anaconda-icon.js +0 -0
  34. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_static/favicon.svg +0 -0
  35. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_static/logo-dark.svg +0 -0
  36. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_static/logo.svg +0 -0
  37. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_templates/class-template.rst +0 -0
  38. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_templates/doc_version.html +0 -0
  39. {essreduce-25.2.4 → essreduce-25.2.6}/docs/_templates/module-template.rst +0 -0
  40. {essreduce-25.2.4 → essreduce-25.2.6}/docs/about/index.md +0 -0
  41. {essreduce-25.2.4 → essreduce-25.2.6}/docs/api-reference/index.md +0 -0
  42. {essreduce-25.2.4 → essreduce-25.2.6}/docs/conf.py +0 -0
  43. {essreduce-25.2.4 → essreduce-25.2.6}/docs/developer/coding-conventions.md +0 -0
  44. {essreduce-25.2.4 → essreduce-25.2.6}/docs/developer/dependency-management.md +0 -0
  45. {essreduce-25.2.4 → essreduce-25.2.6}/docs/developer/getting-started.md +0 -0
  46. {essreduce-25.2.4 → essreduce-25.2.6}/docs/developer/gui.ipynb +0 -0
  47. {essreduce-25.2.4 → essreduce-25.2.6}/docs/developer/index.md +0 -0
  48. {essreduce-25.2.4 → essreduce-25.2.6}/docs/index.md +0 -0
  49. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/index.md +0 -0
  50. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/reduction-workflow-guidelines.md +0 -0
  51. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/tof/dream.ipynb +0 -0
  52. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/tof/frame-unwrapping.ipynb +0 -0
  53. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/tof/index.md +0 -0
  54. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/tof/wfm.ipynb +0 -0
  55. {essreduce-25.2.4 → essreduce-25.2.6}/docs/user-guide/widget.md +0 -0
  56. {essreduce-25.2.4 → essreduce-25.2.6}/pyproject.toml +0 -0
  57. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/base.in +0 -0
  58. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/base.txt +0 -0
  59. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/basetest.in +0 -0
  60. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/basetest.txt +0 -0
  61. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/ci.in +0 -0
  62. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/ci.txt +0 -0
  63. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/dev.in +0 -0
  64. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/dev.txt +0 -0
  65. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/docs.in +0 -0
  66. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/docs.txt +0 -0
  67. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/make_base.py +0 -0
  68. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/mypy.in +0 -0
  69. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/mypy.txt +0 -0
  70. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/nightly.in +0 -0
  71. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/nightly.txt +0 -0
  72. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/static.in +0 -0
  73. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/static.txt +0 -0
  74. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/test.in +0 -0
  75. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/test.txt +0 -0
  76. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/wheels.in +0 -0
  77. {essreduce-25.2.4 → essreduce-25.2.6}/requirements/wheels.txt +0 -0
  78. {essreduce-25.2.4 → essreduce-25.2.6}/resources/logo.svg +0 -0
  79. {essreduce-25.2.4 → essreduce-25.2.6}/setup.cfg +0 -0
  80. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/__init__.py +0 -0
  81. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/data.py +0 -0
  82. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/live/__init__.py +0 -0
  83. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/live/roi.py +0 -0
  84. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/live/workflow.py +0 -0
  85. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/logging.py +0 -0
  86. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/nexus/__init__.py +0 -0
  87. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/nexus/json_generator.py +0 -0
  88. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/nexus/json_nexus.py +0 -0
  89. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/nexus/types.py +0 -0
  90. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/nexus/workflow.py +0 -0
  91. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/parameter.py +0 -0
  92. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/py.typed +0 -0
  93. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/scripts/grow_nexus.py +0 -0
  94. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/time_of_flight/__init__.py +0 -0
  95. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/time_of_flight/fakes.py +0 -0
  96. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/time_of_flight/simulation.py +0 -0
  97. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/time_of_flight/to_events.py +0 -0
  98. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/time_of_flight/toa_to_tof.py +0 -0
  99. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/time_of_flight/types.py +0 -0
  100. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/ui.py +0 -0
  101. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/uncertainty.py +0 -0
  102. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/__init__.py +0 -0
  103. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_base.py +0 -0
  104. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_binedges_widget.py +0 -0
  105. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_bounds_widget.py +0 -0
  106. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_config.py +0 -0
  107. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_filename_widget.py +0 -0
  108. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_linspace_widget.py +0 -0
  109. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_optional_widget.py +0 -0
  110. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_spinner.py +0 -0
  111. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_string_widget.py +0 -0
  112. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_switchable_widget.py +0 -0
  113. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/widgets/_vector_widget.py +0 -0
  114. {essreduce-25.2.4 → essreduce-25.2.6}/src/ess/reduce/workflow.py +0 -0
  115. {essreduce-25.2.4 → essreduce-25.2.6}/src/essreduce.egg-info/SOURCES.txt +0 -0
  116. {essreduce-25.2.4 → essreduce-25.2.6}/src/essreduce.egg-info/dependency_links.txt +0 -0
  117. {essreduce-25.2.4 → essreduce-25.2.6}/src/essreduce.egg-info/entry_points.txt +0 -0
  118. {essreduce-25.2.4 → essreduce-25.2.6}/src/essreduce.egg-info/requires.txt +0 -0
  119. {essreduce-25.2.4 → essreduce-25.2.6}/src/essreduce.egg-info/top_level.txt +0 -0
  120. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_generator_test.py +0 -0
  121. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/array_dataset.json +0 -0
  122. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/dataset.json +0 -0
  123. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/detector.json +0 -0
  124. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/entry.json +0 -0
  125. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/event_data.json +0 -0
  126. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/instrument.json +0 -0
  127. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_examples/log.json +0 -0
  128. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/json_nexus_test.py +0 -0
  129. {essreduce-25.2.4 → essreduce-25.2.6}/tests/nexus/workflow_test.py +0 -0
  130. {essreduce-25.2.4 → essreduce-25.2.6}/tests/package_test.py +0 -0
  131. {essreduce-25.2.4 → essreduce-25.2.6}/tests/scripts/test_grow_nexus.py +0 -0
  132. {essreduce-25.2.4 → essreduce-25.2.6}/tests/time_of_flight/to_events_test.py +0 -0
  133. {essreduce-25.2.4 → essreduce-25.2.6}/tests/time_of_flight/unwrap_test.py +0 -0
  134. {essreduce-25.2.4 → essreduce-25.2.6}/tests/time_of_flight/wfm_test.py +0 -0
  135. {essreduce-25.2.4 → essreduce-25.2.6}/tests/uncertainty_test.py +0 -0
  136. {essreduce-25.2.4 → essreduce-25.2.6}/tests/widget_test.py +0 -0
  137. {essreduce-25.2.4 → essreduce-25.2.6}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: essreduce
3
- Version: 25.2.4
3
+ Version: 25.2.6
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License: BSD 3-Clause License
@@ -15,14 +15,12 @@ options:
15
15
  - `'xy_plane'`: Project the data onto the x-y plane, i.e., perpendicular to the beam.
16
16
  - `'cylinder_mantle_z'`: Project the data onto the mantle of a cylinder aligned with the
17
17
  z-axis.
18
- - `LogicalView`: Not a projection in the traditional sense, but a way to select and
19
- flatten dimensions of the data.
18
+ - A callable, e.g., to select and flatten dimensions of the data.
20
19
  """
21
20
 
22
21
  from __future__ import annotations
23
22
 
24
23
  from collections.abc import Callable, Sequence
25
- from dataclasses import dataclass, field
26
24
  from math import ceil
27
25
  from typing import Literal, NewType
28
26
 
@@ -140,44 +138,6 @@ class Histogrammer:
140
138
  return self._hist(replicated, coords=self._coords) / self.replicas
141
139
 
142
140
 
143
- @dataclass
144
- class LogicalView:
145
- """
146
- Logical view of a multi-dimensional detector.
147
-
148
- Instances can be used as a "projection" function for a detector view.
149
-
150
- Parameters
151
- ----------
152
- fold:
153
- Dimensions to fold. This is useful is the raw data has a single dimension that
154
- corresponds to multiple dimensions in the logical view.
155
- transpose:
156
- Dimensions to transpose. This is useful for reordering dimensions.
157
- select:
158
- Dimensions with associated index to select from the data. This extracts a slice
159
- of the data for each given dimension.
160
- flatten:
161
- Dimensions to flatten.
162
- """
163
-
164
- fold: dict[str, int] | None = None
165
- transpose: tuple[str, ...] | None = None
166
- select: dict[str, int] = field(default_factory=dict)
167
- flatten: dict[str, list[str]] = field(default_factory=dict)
168
-
169
- def __call__(self, da: sc.DataArray) -> sc.DataArray:
170
- if self.fold is not None:
171
- da = da.fold(da.dim, sizes=self.fold)
172
- if self.transpose is not None:
173
- da = da.transpose(self.transpose)
174
- for dim, index in self.select.items():
175
- da = da[dim, index]
176
- for to, dims in self.flatten.items():
177
- da = da.flatten(dims, to=to)
178
- return da.copy()
179
-
180
-
181
141
  class Detector:
182
142
  def __init__(self, detector_number: sc.Variable):
183
143
  self._data = sc.DataArray(
@@ -295,7 +255,7 @@ class RollingDetectorView(Detector):
295
255
  else:
296
256
  indices = sc.ones(sizes=self.data.sizes, dtype='int32', unit=None)
297
257
  indices = sc.cumsum(indices, mode='exclusive')
298
- if isinstance(self._projection, LogicalView):
258
+ if self._projection is not None:
299
259
  indices = self._projection(indices)
300
260
  return roi.ROIFilter(indices=indices, norm=norm)
301
261
 
@@ -357,17 +317,23 @@ class RollingDetectorView(Detector):
357
317
  )
358
318
 
359
319
  @staticmethod
360
- def from_detector_and_logical_view(
361
- detector: CalibratedDetector[SampleRun],
362
- window: RollingDetectorViewWindow,
363
- projection: LogicalView,
364
- ) -> RollingDetectorView:
365
- """Helper for constructing via a Sciline workflow."""
366
- return RollingDetectorView(
367
- detector_number=detector.coords['detector_number'],
368
- window=window,
369
- projection=projection,
370
- )
320
+ def from_detector_with_projection(
321
+ projection: Callable[[sc.DataArray], sc.DataArray] | None,
322
+ ) -> Callable[
323
+ [CalibratedDetector[SampleRun], RollingDetectorViewWindow], RollingDetectorView
324
+ ]:
325
+ def factory(
326
+ detector: CalibratedDetector[SampleRun],
327
+ window: RollingDetectorViewWindow,
328
+ ) -> RollingDetectorView:
329
+ """Helper for constructing via a Sciline workflow."""
330
+ return RollingDetectorView(
331
+ detector_number=detector.coords['detector_number'],
332
+ window=window,
333
+ projection=projection,
334
+ )
335
+
336
+ return factory
371
337
 
372
338
  @staticmethod
373
339
  def from_nexus(
@@ -375,7 +341,9 @@ class RollingDetectorView(Detector):
375
341
  *,
376
342
  detector_name: str,
377
343
  window: int,
378
- projection: Literal['xy_plane', 'cylinder_mantle_z'] | LogicalView,
344
+ projection: Literal['xy_plane', 'cylinder_mantle_z']
345
+ | Callable[[sc.DataArray], sc.DataArray]
346
+ | None = None,
379
347
  resolution: dict[str, int] | None = None,
380
348
  pixel_noise: Literal['cylindrical'] | sc.Variable | None = None,
381
349
  ) -> RollingDetectorView:
@@ -396,10 +364,12 @@ class RollingDetectorView(Detector):
396
364
  Size of the rolling window.
397
365
  projection:
398
366
  Projection to use for the detector data. This can be a string selecting a
399
- predefined projection or a LogicalView instance.
367
+ predefined projection or a function that takes a DataArray and returns a
368
+ DataArray. The predefined projections are 'xy_plane' and
369
+ 'cylinder_mantle_z'.
400
370
  resolution:
401
- Resolution to use for histogramming the detector data. Not required when the
402
- projection is a LogicalView.
371
+ Resolution to use for histogramming the detector data. Only required for
372
+ 'xy_plane' and 'cylinder_mantle_z' projections.
403
373
  pixel_noise:
404
374
  Noise to add to the pixel positions. This can be a scalar value to add
405
375
  Gaussian noise to the pixel positions or the string 'cylindrical' to add
@@ -413,13 +383,7 @@ class RollingDetectorView(Detector):
413
383
  noise_replica_count = 16
414
384
  wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[])
415
385
  wf[RollingDetectorViewWindow] = window
416
- if isinstance(projection, LogicalView):
417
- wf[LogicalView] = projection
418
- wf[NeXusTransformation[snx.NXdetector, SampleRun]] = NeXusTransformation[
419
- snx.NXdetector, SampleRun
420
- ](sc.scalar(1))
421
- wf.insert(RollingDetectorView.from_detector_and_logical_view)
422
- elif projection == 'cylinder_mantle_z':
386
+ if projection == 'cylinder_mantle_z':
423
387
  wf.insert(make_cylinder_mantle_coords)
424
388
  wf.insert(RollingDetectorView.from_detector_and_histogrammer)
425
389
  wf[DetectorViewResolution] = resolution
@@ -428,7 +392,12 @@ class RollingDetectorView(Detector):
428
392
  wf.insert(RollingDetectorView.from_detector_and_histogrammer)
429
393
  wf[DetectorViewResolution] = resolution
430
394
  else:
431
- raise ValueError(f"Invalid {projection=}.")
395
+ wf[NeXusTransformation[snx.NXdetector, SampleRun]] = NeXusTransformation[
396
+ snx.NXdetector, SampleRun
397
+ ](sc.scalar(1))
398
+ wf.insert(
399
+ RollingDetectorView.from_detector_with_projection(projection=projection)
400
+ )
432
401
  if isinstance(pixel_noise, sc.Variable):
433
402
  wf.insert(gaussian_position_noise)
434
403
  wf[PositionNoiseSigma] = pixel_noise
@@ -180,10 +180,15 @@ def _attempt_to_open_without_locking(
180
180
  # HDF5 tracks file locking flags internally within a single process.
181
181
  # If the same file is opened multiple times, we can get a flag mismatch.
182
182
  # We can try opening without locking, maybe this matches the original flags.
183
- if "file locking flag values don't match" in err.args[0]:
184
- return True
185
- if "file locking 'ignore disabled locks' flag values don't match" in err.args[0]:
186
- return True
183
+ error_message = err.args[0]
184
+ if isinstance(error_message, str):
185
+ if "file locking flag values don't match" in error_message:
186
+ return True
187
+ if (
188
+ "file locking 'ignore disabled locks' flag values don't match"
189
+ in error_message
190
+ ):
191
+ return True
187
192
  return False
188
193
 
189
194
 
@@ -68,7 +68,18 @@ class Accumulator(ABC, Generic[T]):
68
68
  def _do_push(self, value: T) -> None: ...
69
69
 
70
70
  @property
71
- @abstractmethod
71
+ def is_empty(self) -> bool:
72
+ """
73
+ Check if the accumulator is empty.
74
+
75
+ Returns
76
+ -------
77
+ :
78
+ True if the accumulator is empty, False otherwise.
79
+ """
80
+ return False
81
+
82
+ @property
72
83
  def value(self) -> T:
73
84
  """
74
85
  Get the accumulated value.
@@ -77,6 +88,24 @@ class Accumulator(ABC, Generic[T]):
77
88
  -------
78
89
  :
79
90
  Accumulated value.
91
+
92
+ Raises
93
+ ------
94
+ ValueError
95
+ If the accumulator is empty.
96
+ """
97
+ if self.is_empty:
98
+ raise ValueError("Cannot get value from empty accumulator")
99
+ return self._get_value()
100
+
101
+ @abstractmethod
102
+ def _get_value(self) -> T:
103
+ """Return the accumulated value, assuming it exists."""
104
+
105
+ @abstractmethod
106
+ def clear(self) -> None:
107
+ """
108
+ Clear the accumulator, resetting it to its initial state.
80
109
  """
81
110
 
82
111
 
@@ -92,7 +121,10 @@ class EternalAccumulator(Accumulator[T]):
92
121
  self._value: T | None = None
93
122
 
94
123
  @property
95
- def value(self) -> T:
124
+ def is_empty(self) -> bool:
125
+ return self._value is None
126
+
127
+ def _get_value(self) -> T:
96
128
  return deepcopy(self._value)
97
129
 
98
130
  def _do_push(self, value: T) -> None:
@@ -101,6 +133,10 @@ class EternalAccumulator(Accumulator[T]):
101
133
  else:
102
134
  self._value += value
103
135
 
136
+ def clear(self) -> None:
137
+ """Clear the accumulated value."""
138
+ self._value = None
139
+
104
140
 
105
141
  class RollingAccumulator(Accumulator[T]):
106
142
  """
@@ -121,7 +157,10 @@ class RollingAccumulator(Accumulator[T]):
121
157
  self._values: list[T] = []
122
158
 
123
159
  @property
124
- def value(self) -> T:
160
+ def is_empty(self) -> bool:
161
+ return len(self._values) == 0
162
+
163
+ def _get_value(self) -> T:
125
164
  # Naive and potentially slow implementation if values and/or window are large!
126
165
  return sc.reduce(self._values).sum()
127
166
 
@@ -130,6 +169,68 @@ class RollingAccumulator(Accumulator[T]):
130
169
  if len(self._values) > self._window:
131
170
  self._values.pop(0)
132
171
 
172
+ def clear(self) -> None:
173
+ """Clear the accumulated values."""
174
+ self._values = []
175
+
176
+
177
+ class MinAccumulator(Accumulator):
178
+ """Keeps the minimum value seen so far.
179
+
180
+ Only supports scalar values.
181
+ """
182
+
183
+ def __init__(self, **kwargs: Any) -> None:
184
+ super().__init__(**kwargs)
185
+ self._cur_min: sc.Variable | None = None
186
+
187
+ def _do_push(self, value: sc.Variable) -> None:
188
+ if self._cur_min is None:
189
+ self._cur_min = value
190
+ else:
191
+ self._cur_min = min(self._cur_min, value)
192
+
193
+ @property
194
+ def is_empty(self) -> bool:
195
+ """Check if the accumulator has collected a minimum value."""
196
+ return self._cur_min is None
197
+
198
+ def _get_value(self) -> Any:
199
+ return self._cur_min
200
+
201
+ def clear(self) -> None:
202
+ """Clear the accumulated minimum value."""
203
+ self._cur_min = None
204
+
205
+
206
+ class MaxAccumulator(Accumulator):
207
+ """Keeps the maximum value seen so far.
208
+
209
+ Only supports scalar values.
210
+ """
211
+
212
+ def __init__(self, **kwargs: Any) -> None:
213
+ super().__init__(**kwargs)
214
+ self._cur_max: sc.Variable | None = None
215
+
216
+ @property
217
+ def is_empty(self) -> bool:
218
+ """Check if the accumulator has collected a maximum value."""
219
+ return self._cur_max is None
220
+
221
+ def _do_push(self, value: sc.Variable) -> None:
222
+ if self._cur_max is None:
223
+ self._cur_max = value
224
+ else:
225
+ self._cur_max = max(self._cur_max, value)
226
+
227
+ def _get_value(self) -> sc.Variable | None:
228
+ return self._cur_max
229
+
230
+ def clear(self) -> None:
231
+ """Clear the accumulated maximum value."""
232
+ self._cur_max = None
233
+
133
234
 
134
235
  class StreamProcessor:
135
236
  """
@@ -299,6 +400,16 @@ class StreamProcessor:
299
400
  self._finalize_workflow[key] = self._accumulators[key].value
300
401
  return self._finalize_workflow.compute(self._target_keys)
301
402
 
403
+ def clear(self) -> None:
404
+ """
405
+ Clear all accumulators, resetting them to their initial state.
406
+
407
+ This is useful for restarting a streaming computation without
408
+ creating a new StreamProcessor instance.
409
+ """
410
+ for accumulator in self._accumulators.values():
411
+ accumulator.clear()
412
+
302
413
 
303
414
  def _find_descendants(
304
415
  workflow: sciline.Pipeline, keys: tuple[sciline.typing.Key, ...]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: essreduce
3
- Version: 25.2.4
3
+ Version: 25.2.6
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License: BSD 3-Clause License
@@ -354,13 +354,12 @@ def test_ROIFilter_from_trivial_RollingDetectorView() -> None:
354
354
  assert sc.identical(scale, sc.ones(dims=['detector_number'], shape=[2]))
355
355
 
356
356
 
357
- def test_ROIFilter_from_RollingDetectorView_with_LogicalView() -> None:
358
- logical_view = raw.LogicalView(select={'z': 0})
357
+ def test_ROIFilter_from_RollingDetectorView_with_custom_projection() -> None:
359
358
  detector_number = sc.array(
360
359
  dims=['x', 'y', 'z'], values=[[[1, 2], [3, 4]], [[5, 6], [7, 8]]], unit=None
361
360
  )
362
361
  view = raw.RollingDetectorView(
363
- detector_number=detector_number, window=2, projection=logical_view
362
+ detector_number=detector_number, window=2, projection=lambda da: da['z', 0]
364
363
  )
365
364
  roi_filter = view.make_roi_filter()
366
365
  data = detector_number.copy()
@@ -3,7 +3,7 @@
3
3
  import pytest
4
4
  import scipp as sc
5
5
 
6
- from ess.reduce.live import raw, roi
6
+ from ess.reduce.live import roi
7
7
 
8
8
 
9
9
  @pytest.fixture
@@ -146,13 +146,12 @@ def test_apply_selection_fails_with_out_of_bounds_index():
146
146
  roi.apply_selection(data, selection=selection)
147
147
 
148
148
 
149
- @pytest.fixture
150
- def logical_view():
151
- return raw.LogicalView(fold={'x': 3, 'y': 4, 'z': 2}, select={'z': 0})
149
+ def logical_view(da: sc.DataArray) -> sc.DataArray:
150
+ return da.fold(da.dim, sizes={'x': 3, 'y': 4, 'z': 2})['z', 0]
152
151
 
153
152
 
154
153
  @pytest.fixture
155
- def roi_filter(logical_view: raw.LogicalView):
154
+ def roi_filter() -> roi.ROIFilter:
156
155
  indices = sc.ones(sizes={'detector_number': 24}, dtype='int32', unit=None)
157
156
  indices = sc.cumsum(indices, mode='exclusive')
158
157
  return roi.ROIFilter(logical_view(indices))
@@ -1,6 +1,7 @@
1
1
  # SPDX-License-Identifier: BSD-3-Clause
2
2
  # Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
3
-
3
+ import os
4
+ import sys
4
5
  from contextlib import contextmanager
5
6
  from io import BytesIO
6
7
  from pathlib import Path
@@ -661,17 +662,34 @@ def test_open_nexus_file_multiple_times(tmp_path: Path, locks: tuple[Any, Any])
661
662
  assert f1.name == f2.name
662
663
 
663
664
 
665
+ def _in_conda_env():
666
+ return 'CONDA_PREFIX' in os.environ
667
+
668
+
669
+ def _test_open_nexus_file_with_mismatched_locking(
670
+ tmp_path: Path, locks: tuple[Any, Any]
671
+ ) -> None:
672
+ from ess.reduce.nexus._nexus_loader import _open_nexus_file
673
+
674
+ path = FilePath(tmp_path / "file.nxs")
675
+ with snx.File(path, "w"):
676
+ pass
677
+
678
+ with _open_nexus_file(path, locking=locks[0]):
679
+ with pytest.raises(OSError, match="flag values don't match"):
680
+ _ = _open_nexus_file(path, locking=locks[1])
681
+
682
+
683
+ @pytest.mark.skipif(
684
+ sys.platform in ("darwin", "win32")
685
+ or (sys.platform == "linux" and _in_conda_env()),
686
+ reason="HDF5 has different file locking flags on MacOS, Windows and Linux(conda)",
687
+ )
664
688
  @pytest.mark.parametrize(
665
689
  "locks",
666
690
  [
667
- (True, False),
668
691
  (True, None),
669
- (False, True),
670
- (False, None),
671
692
  (None, True),
672
- (None, False),
673
- # On a read-only filesystem, this would work:
674
- (NoLockingIfNeeded, False),
675
693
  # This could be supported, but it could cause problems because the first
676
694
  # user expects the file to be locked.
677
695
  (True, NoLockingIfNeeded),
@@ -679,15 +697,31 @@ def test_open_nexus_file_multiple_times(tmp_path: Path, locks: tuple[Any, Any])
679
697
  (NoLockingIfNeeded, True),
680
698
  ],
681
699
  )
682
- def test_open_nexus_file_with_mismatched_locking(
700
+ def test_open_nexus_file_with_mismatched_locking_pypi_linux(
683
701
  tmp_path: Path, locks: tuple[Any, Any]
684
702
  ) -> None:
685
- from ess.reduce.nexus._nexus_loader import _open_nexus_file
703
+ _test_open_nexus_file_with_mismatched_locking(tmp_path, locks)
686
704
 
687
- path = FilePath(tmp_path / "file.nxs")
688
- with snx.File(path, "w"):
689
- pass
690
705
 
691
- with _open_nexus_file(path, locking=locks[0]):
692
- with pytest.raises(OSError, match="flag values don't match"):
693
- _ = _open_nexus_file(path, locking=locks[1])
706
+ @pytest.mark.parametrize(
707
+ "locks",
708
+ [
709
+ (True, False),
710
+ (False, True),
711
+ (False, None),
712
+ (None, False),
713
+ # On a read-only filesystem, this would work:
714
+ (NoLockingIfNeeded, False),
715
+ ],
716
+ )
717
+ def test_open_nexus_file_with_mismatched_locking_all(
718
+ tmp_path: Path, locks: tuple[Any, Any]
719
+ ) -> None:
720
+ _test_open_nexus_file_with_mismatched_locking(tmp_path, locks)
721
+
722
+
723
+ def test_open_nonexisting_file_raises_filenotfounderror():
724
+ from ess.reduce.nexus._nexus_loader import _open_nexus_file
725
+
726
+ with pytest.raises(FileNotFoundError):
727
+ _open_nexus_file(nexus.types.FilePath(Path("doesnotexist.hdf")))
@@ -45,6 +45,18 @@ def test_eternal_accumulator_does_not_modify_pushed_values() -> None:
45
45
  assert sc.identical(var, original)
46
46
 
47
47
 
48
+ def test_eternal_accumulator_clear() -> None:
49
+ accum = streaming.EternalAccumulator()
50
+ var = sc.linspace(dim='x', start=0, stop=1, num=10)
51
+ for i in range(10):
52
+ accum.push(var[i].copy())
53
+ assert sc.identical(accum.value, sc.sum(var))
54
+ accum.clear()
55
+ assert accum.is_empty
56
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
57
+ _ = accum.value
58
+
59
+
48
60
  def test_rolling_accumulator_sums_over_window() -> None:
49
61
  accum = streaming.RollingAccumulator(window=3)
50
62
  var = sc.linspace(dim='x', start=0, stop=1, num=10)
@@ -94,6 +106,76 @@ def test_rolling_accumulator_does_not_modify_pushed_values() -> None:
94
106
  assert sc.identical(var, original)
95
107
 
96
108
 
109
+ def test_rolling_accumulator_clear() -> None:
110
+ accum = streaming.RollingAccumulator(window=3)
111
+ var = sc.linspace(dim='x', start=0, stop=1, num=10)
112
+ for i in range(5):
113
+ accum.push(var[i].copy())
114
+ assert sc.identical(accum.value, var[2:5].sum())
115
+ accum.clear()
116
+ assert accum.is_empty
117
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
118
+ _ = accum.value
119
+
120
+
121
+ def test_eternal_accumulator_is_empty() -> None:
122
+ accum = streaming.EternalAccumulator()
123
+ assert accum.is_empty
124
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
125
+ _ = accum.value
126
+
127
+ var = sc.linspace(dim='x', start=0, stop=1, num=10)
128
+ accum.push(var[0].copy())
129
+ assert not accum.is_empty
130
+ assert sc.identical(accum.value, var[0])
131
+
132
+ accum.clear()
133
+ assert accum.is_empty
134
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
135
+ _ = accum.value
136
+
137
+
138
+ def test_rolling_accumulator_is_empty() -> None:
139
+ accum = streaming.RollingAccumulator(window=3)
140
+ assert accum.is_empty
141
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
142
+ _ = accum.value
143
+
144
+ var = sc.linspace(dim='x', start=0, stop=1, num=10)
145
+ accum.push(var[0].copy())
146
+ assert not accum.is_empty
147
+ assert sc.identical(accum.value, var[0])
148
+
149
+ accum.clear()
150
+ assert accum.is_empty
151
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
152
+ _ = accum.value
153
+
154
+
155
+ def test_min_accumulator() -> None:
156
+ accum = streaming.MinAccumulator()
157
+ var = sc.array(dims=['x'], values=[1.0, 2.0, 3.0, 2.0, 1.0])
158
+ for scalar_var in var:
159
+ accum.push(scalar_var)
160
+ assert sc.identical(accum.value, sc.min(var))
161
+ accum.clear()
162
+ assert accum.is_empty
163
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
164
+ _ = accum.value
165
+
166
+
167
+ def test_max_accumulator() -> None:
168
+ accum = streaming.MaxAccumulator()
169
+ var = sc.array(dims=['x'], values=[1.0, 2.0, 3.0, 2.0, 1.0])
170
+ for scalar_var in var:
171
+ accum.push(scalar_var)
172
+ assert sc.identical(accum.value, sc.max(var))
173
+ accum.clear()
174
+ assert accum.is_empty
175
+ with pytest.raises(ValueError, match="Cannot get value from empty accumulator"):
176
+ _ = accum.value
177
+
178
+
97
179
  DynamicA = NewType('DynamicA', float)
98
180
  DynamicB = NewType('DynamicB', float)
99
181
  DynamicC = NewType('DynamicC', float)
@@ -161,9 +243,16 @@ def test_StreamProcessor_uses_custom_accumulator() -> None:
161
243
  pass
162
244
 
163
245
  @property
164
- def value(self) -> sc.Variable:
246
+ def is_empty(self) -> bool:
247
+ return False
248
+
249
+ def _get_value(self) -> sc.Variable:
165
250
  return sc.scalar(42)
166
251
 
252
+ def clear(self) -> None:
253
+ # Nothing to clear
254
+ pass
255
+
167
256
  base_workflow = sciline.Pipeline(
168
257
  (make_static_a, make_accum_a, make_accum_b, make_target)
169
258
  )
@@ -370,10 +459,9 @@ def test_StreamProcessor_raises_given_partial_update_for_accumulator() -> None:
370
459
  accumulators=(Target, AccumC), # Target depends on both A and B
371
460
  )
372
461
  # We can update either (A, B) and/or C...
373
- result = streaming_wf.add_chunk({DynamicA: sc.scalar(1), DynamicB: sc.scalar(4)})
374
- assert sc.identical(result[Target], sc.scalar(2 * 1.0 / 4.0))
375
- assert result[AccumC] is None
376
- result = streaming_wf.add_chunk({DynamicC: sc.scalar(11)})
462
+ streaming_wf.accumulate({DynamicA: sc.scalar(1), DynamicB: sc.scalar(4)})
463
+ streaming_wf.accumulate({DynamicC: sc.scalar(11)})
464
+ result = streaming_wf.finalize()
377
465
  assert sc.identical(result[Target], sc.scalar(2 * 1.0 / 4.0))
378
466
  assert sc.identical(result[AccumC], sc.scalar(11))
379
467
  result = streaming_wf.add_chunk({DynamicA: sc.scalar(2), DynamicB: sc.scalar(5)})
@@ -428,3 +516,35 @@ def test_StreamProcessor_raises_when_trying_to_update_non_dynamic_key() -> None:
428
516
  match=r'Got non-dynamic keys: {tests.streaming_test.Target}',
429
517
  ):
430
518
  result = streaming_wf.add_chunk({Target: sc.scalar(2)})
519
+
520
+
521
+ def test_StreamProcessor_clear() -> None:
522
+ base_workflow = sciline.Pipeline(
523
+ (make_static_a, make_accum_a, make_accum_b, make_target)
524
+ )
525
+
526
+ # Reset call counter to ensure we can track it properly
527
+ make_static_a.call_count = 0
528
+
529
+ streaming_wf = streaming.StreamProcessor(
530
+ base_workflow=base_workflow,
531
+ dynamic_keys=(DynamicA, DynamicB),
532
+ target_keys=(Target,),
533
+ accumulators=(AccumA, AccumB),
534
+ )
535
+ # Add some data
536
+ result = streaming_wf.add_chunk({DynamicA: sc.scalar(1), DynamicB: sc.scalar(4)})
537
+ assert sc.identical(result[Target], sc.scalar(2 * 1.0 / 4.0))
538
+ result = streaming_wf.add_chunk({DynamicA: sc.scalar(2), DynamicB: sc.scalar(5)})
539
+ assert sc.identical(result[Target], sc.scalar(2 * 3.0 / 9.0))
540
+
541
+ # Make sure static_a was called exactly once
542
+ assert make_static_a.call_count == 1
543
+
544
+ # Clear and verify we get back to initial state
545
+ streaming_wf.clear()
546
+ result = streaming_wf.add_chunk({DynamicA: sc.scalar(1), DynamicB: sc.scalar(4)})
547
+ assert sc.identical(result[Target], sc.scalar(2 * 1.0 / 4.0))
548
+
549
+ # Static values should be preserved after clear, so call_count remains 1
550
+ assert make_static_a.call_count == 1
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
File without changes