lima2.client 4.1.8__tar.gz → 4.2.0__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 (149) hide show
  1. {lima2_client-4.1.8/src/lima2_client.egg-info → lima2_client-4.2.0}/PKG-INFO +1 -1
  2. {lima2_client-4.1.8 → lima2_client-4.2.0}/conda/recipe.yaml +3 -0
  3. lima2_client-4.2.0/docs/assets/lima2-conductor-monitor-webpage.png +0 -0
  4. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/features.md +12 -3
  5. lima2_client-4.2.0/docs/monitor.md +12 -0
  6. lima2_client-4.2.0/src/lima2/client/_utils.py +132 -0
  7. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/client/services.py +78 -40
  8. lima2_client-4.2.0/src/lima2/common/devencoded/common.py +26 -0
  9. lima2_client-4.2.0/src/lima2/common/devencoded/data_array.py +208 -0
  10. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/devencoded/smx_sparse_frame.py +3 -4
  11. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/devencoded/sparse_frame.py +6 -4
  12. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/devencoded/structured_array.py +1 -1
  13. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/pipelines.py +24 -1
  14. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/types.py +22 -12
  15. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/acquisition.py +14 -0
  16. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/acquisition_system.py +77 -20
  17. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/detector.py +0 -2
  18. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/__init__.py +2 -0
  19. lima2_client-4.2.0/src/lima2/conductor/processing/fai.py +75 -0
  20. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/master_file.py +64 -32
  21. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/pipeline.py +142 -10
  22. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/reduced_data.py +2 -5
  23. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/xpcs.py +0 -1
  24. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/tango/container.py +3 -0
  25. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/tango/control.py +1 -1
  26. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/tango/processing.py +13 -1
  27. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/tango/receiver.py +1 -1
  28. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/tango/utils.py +1 -2
  29. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/topology.py +2 -2
  30. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/acquisition.py +28 -21
  31. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/benchmark.py +3 -3
  32. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/detector.py +17 -6
  33. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/favicon.ico +0 -0
  34. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/fonts/Roboto-Italic.woff2 +0 -0
  35. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/fonts/Roboto-Medium.woff2 +0 -0
  36. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/fonts/Roboto-MediumItalic.woff2 +0 -0
  37. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/fonts/Roboto-Regular.woff2 +0 -0
  38. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/fonts.css +43 -0
  39. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/index.html +288 -0
  40. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/milligram.css +633 -0
  41. lima2_client-4.2.0/src/lima2/conductor/webservice/monitor/normalize.css +350 -0
  42. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/pipeline.py +108 -28
  43. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/webapp.py +105 -30
  44. {lima2_client-4.1.8 → lima2_client-4.2.0/src/lima2_client.egg-info}/PKG-INFO +1 -1
  45. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2_client.egg-info/SOURCES.txt +16 -2
  46. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/conftest.py +20 -1
  47. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/test_failing.py +4 -0
  48. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/test_legacy.py +170 -69
  49. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/client/test_pipeline.py +94 -45
  50. lima2_client-4.2.0/tests/unit/client/test_utils.py +160 -0
  51. lima2_client-4.1.8/tests/unit/common/test_dense_frame.py → lima2_client-4.2.0/tests/unit/common/test_data_array.py +69 -34
  52. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/mock_devices.py +18 -13
  53. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_acquisition.py +4 -3
  54. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_acquisition_system.py +74 -16
  55. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_master_file.py +30 -1
  56. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_pipeline.py +159 -27
  57. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_processing.py +1 -0
  58. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_reduced_data.py +1 -0
  59. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_tango_devices.py +60 -3
  60. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_topology.py +2 -1
  61. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/test_acquisition.py +14 -1
  62. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/test_pipelines.py +86 -10
  63. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/test_webapp.py +24 -2
  64. lima2_client-4.1.8/docs/monitor.md +0 -9
  65. lima2_client-4.1.8/src/lima2/common/devencoded/dense_frame.py +0 -153
  66. {lima2_client-4.1.8 → lima2_client-4.2.0}/.gitignore +0 -0
  67. {lima2_client-4.1.8 → lima2_client-4.2.0}/.gitlab-ci.yml +0 -0
  68. {lima2_client-4.1.8 → lima2_client-4.2.0}/LICENSE +0 -0
  69. {lima2_client-4.1.8 → lima2_client-4.2.0}/README.md +0 -0
  70. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/assets/lima2-components-and-repos.svg +0 -0
  71. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/assets/lima2-conductor-communication-model.svg +0 -0
  72. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/assets/lima2-conductor-modules.svg +0 -0
  73. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/assets/logo-transparent-crop.png +0 -0
  74. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/concepts.md +0 -0
  75. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/contact.md +0 -0
  76. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/dev/conductor_client.md +0 -0
  77. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/dev/conductor_design.md +0 -0
  78. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/dev/conductor_requirements.md +0 -0
  79. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/dev/index.md +0 -0
  80. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/getting_started.md +0 -0
  81. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/index.md +0 -0
  82. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/license.md +0 -0
  83. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/acquisition.md +0 -0
  84. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/detector.md +0 -0
  85. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/exceptions.md +0 -0
  86. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/index.md +0 -0
  87. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/pipeline.md +0 -0
  88. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/services.md +0 -0
  89. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/reference/session.md +0 -0
  90. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/release_notes.md +0 -0
  91. {lima2_client-4.1.8 → lima2_client-4.2.0}/docs/shell.md +0 -0
  92. {lima2_client-4.1.8 → lima2_client-4.2.0}/flake8.ini +0 -0
  93. {lima2_client-4.1.8 → lima2_client-4.2.0}/mkdocs.yml +0 -0
  94. {lima2_client-4.1.8 → lima2_client-4.2.0}/pyproject.toml +0 -0
  95. {lima2_client-4.1.8 → lima2_client-4.2.0}/scripts/cleanup_processing_ds.py +0 -0
  96. {lima2_client-4.1.8 → lima2_client-4.2.0}/setup.cfg +0 -0
  97. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/client/__init__.py +0 -0
  98. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/client/exceptions.py +0 -0
  99. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/client/py.typed +0 -0
  100. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/client/session.py +0 -0
  101. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/client/shell.py +0 -0
  102. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/__init__.py +0 -0
  103. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/devencoded/__init__.py +0 -0
  104. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/devencoded/exception.py +0 -0
  105. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/exceptions.py +0 -0
  106. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/progress_counter.py +0 -0
  107. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/py.typed +0 -0
  108. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/common/state.py +0 -0
  109. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/__init__.py +0 -0
  110. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/failing.py +0 -0
  111. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/legacy.py +0 -0
  112. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/processing/smx.py +0 -0
  113. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/py.typed +0 -0
  114. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/tango/__init__.py +0 -0
  115. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/utils.py +0 -0
  116. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/__init__.py +0 -0
  117. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/benchmark_generators/__init__.py +0 -0
  118. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/benchmark_generators/rois.py +0 -0
  119. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/cli.py +0 -0
  120. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/main.py +0 -0
  121. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2/conductor/webservice/utils.py +1 -1
  122. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2_client.egg-info/dependency_links.txt +0 -0
  123. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2_client.egg-info/entry_points.txt +0 -0
  124. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2_client.egg-info/requires.txt +0 -0
  125. {lima2_client-4.1.8 → lima2_client-4.2.0}/src/lima2_client.egg-info/top_level.txt +0 -0
  126. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/__init__.py +0 -0
  127. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/__init__.py +0 -0
  128. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/tango_db/simulator_ctl.yaml +0 -0
  129. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/tango_db/simulator_rcv1.yaml +0 -0
  130. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/integration/tango_db/simulator_rcv2.yaml +0 -0
  131. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/__init__.py +0 -0
  132. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/client/__init__.py +0 -0
  133. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/client/conftest.py +0 -0
  134. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/client/test_acquisition.py +1 -1
  135. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/client/test_detector.py +0 -0
  136. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/client/test_session.py +0 -0
  137. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/common/__init__.py +0 -0
  138. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/common/test_progress_counter.py +0 -0
  139. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/common/test_smx_sparse_frame.py +0 -0
  140. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/common/test_sparse_frame.py +0 -0
  141. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/common/test_state.py +0 -0
  142. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/__init__.py +0 -0
  143. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/conftest.py +0 -0
  144. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/mock_devencoded.py +0 -0
  145. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/test_utils.py +0 -0
  146. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/__init__.py +0 -0
  147. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/test_benchmark.py +0 -0
  148. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/test_detector.py +0 -0
  149. {lima2_client-4.1.8 → lima2_client-4.2.0}/tests/unit/conductor/webservice/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lima2-client
3
- Version: 4.1.8
3
+ Version: 4.2.0
4
4
  Summary: The Python client library of the Lima2 system
5
5
  Author: BCU (ESRF)
6
6
  License-Expression: MIT
@@ -50,6 +50,9 @@ tests:
50
50
  - lima2.client.services
51
51
  - lima2.common
52
52
  pip_check: true
53
+ - package_contents:
54
+ site_packages:
55
+ - lima2/conductor/webservice/monitor/*
53
56
  - script: lima2-shell --help
54
57
  - script: lima2-conductor --help
55
58
 
@@ -32,9 +32,18 @@ Both modes have a prefetched variant, where the frames are prefetched in memory
32
32
 
33
33
  Lima2 implements the following processing pipelines:
34
34
 
35
- - Legacy (aka Classic): a standard, CPU-based processing pipeline for generic image acquisition with optional [ROIs](./concepts.md#region-of-interest-roi).
36
- - [Smx (Serial Macromolecular Crystallography)](https://www.esrf.fr/id29): involves GPU-based processing to perform online azimuthal integration.
37
- - [XPCS (X-ray Photon Correlation Spectroscopy)](https://www.esrf.fr/UsersAndScience/Experiments/CBS/ID10): a pipeline dedicated to extracting temporal correlations from the images.
35
+ - **Classic (aka Legacy)**: a general-purpose, CPU-based pipeline that can perform
36
+ standard image operations (cropping, rotation, background subtraction, etc.) as well
37
+ as [ROI statistics and profiles](./concepts.md#region-of-interest-roi).
38
+ - **FAI (Fast Azimuthal Integration)**: specialized to process diffraction patterns, FAI
39
+ runs an online azimuthal integration step on GPU to efficiently find rings in
40
+ circularly-symmetric images.
41
+ - [**Smx (Serial Macromolecular Crystallography)**](https://www.esrf.fr/id29): an
42
+ extended FAI pipeline, which additionally performs peak finding and sparsification after the
43
+ azimuthal integration step.
44
+ - [**XPCS (X-ray Photon Correlation
45
+ Spectroscopy)**](https://www.esrf.fr/UsersAndScience/Experiments/CBS/ID10): a pipeline
46
+ dedicated to extracting temporal correlations from the images.
38
47
 
39
48
  Naturally, depending on the chosen processing type, the kinds of frames and
40
49
  [reduced data](./concepts.md#reduced-data) available to consumers changes.
@@ -0,0 +1,12 @@
1
+ # Status monitor
2
+
3
+ The conductor serves a status monitoring webpage at its root endpoint ("/"). This page
4
+ can be accessed in any browser, using the conductor's hostname and port.
5
+
6
+ As long as the conductor is running, the page will periodically poll to update the
7
+ displayed information.
8
+
9
+ ![Lima2 status webpage screenshot](assets/lima2-conductor-monitor-webpage.png)
10
+
11
+ This page is mainly aimed at Lima2 administrators, and can be used to get an overview of
12
+ the system's state (detector, receivers, ...).
@@ -0,0 +1,132 @@
1
+ # This file is part of the Lima2 project
2
+ #
3
+ # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
4
+ # Distributed under the MIT licence. See LICENSE for more info.
5
+
6
+ """Lima2 client private utility functions."""
7
+
8
+ import logging
9
+
10
+ import h5py
11
+ import tango as tg
12
+
13
+ from lima2.common import types
14
+ from lima2.common.devencoded.common import Frame
15
+ from lima2.common.devencoded.data_array import Vector
16
+ from lima2.common.devencoded.smx_sparse_frame import SmxSparseFrame
17
+ from lima2.common.devencoded.sparse_frame import SparseFrame
18
+ from lima2.common.types import FrameType
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def tango_get_frame(
24
+ url: str,
25
+ command_name: str,
26
+ lookup_idx: int,
27
+ ) -> Frame | Vector | SparseFrame | SmxSparseFrame:
28
+ """Fetch a frame from a Lima2 receiver over tango, and decode it."""
29
+
30
+ device = tg.DeviceProxy(url)
31
+
32
+ try:
33
+ raw_data: tuple[str, bytes] = device.command_inout(
34
+ name=command_name, cmd_param=lookup_idx
35
+ )
36
+ except tg.DevFailed as e:
37
+ logger.error(
38
+ f"Call to {command_name}({lookup_idx}) on {device.dev_name()} failed"
39
+ )
40
+ raise RuntimeError(
41
+ f"Unable to get frame {lookup_idx} from {device.dev_name()}:\n{e}"
42
+ ) from e
43
+
44
+ try:
45
+ type_id, payload = raw_data
46
+ except ValueError:
47
+ raise RuntimeError(
48
+ f"Unexpected data received from Lima2 device: {raw_data!r}"
49
+ ) from None
50
+
51
+ if type_id not in types.decoder_by_id:
52
+ raise NotImplementedError(f"No decoder for '{type_id}'")
53
+
54
+ decoder = types.decoder_by_id[type_id]
55
+
56
+ return decoder(payload)
57
+
58
+
59
+ def hdf5_get_frame(
60
+ url: str,
61
+ frame_idx: int,
62
+ lookup_idx: int,
63
+ frame_type: FrameType,
64
+ shape: tuple[int, ...],
65
+ ) -> Frame | Vector | SparseFrame | SmxSparseFrame:
66
+ """Fetch a frame from an HDF5 file."""
67
+ try:
68
+ filepath, datapath = url.removeprefix("file://").split(":")
69
+ except ValueError as e:
70
+ raise RuntimeError(
71
+ f"Invalid hdf5 url format: '{url}'. "
72
+ f"Expected 'file://file/path.h5:data/path"
73
+ ) from e
74
+
75
+ frm: Frame | Vector | SparseFrame | SmxSparseFrame
76
+
77
+ with h5py.File(filepath, "r") as f:
78
+ if frame_type == FrameType.DENSE:
79
+ try:
80
+ frm_data = f[f"{datapath}/data"][lookup_idx]
81
+ except IndexError as e:
82
+ raise RuntimeError(
83
+ f"Can't access element {lookup_idx} at {url}."
84
+ ) from e
85
+ frm = Frame(data=frm_data, idx=frame_idx)
86
+
87
+ elif frame_type == FrameType.SPARSE:
88
+ try:
89
+ index = f[f"{datapath}/index"][lookup_idx]
90
+ intensity = f[f"{datapath}/intensity"][lookup_idx]
91
+ except IndexError as e:
92
+ raise RuntimeError(
93
+ f"Can't access element {lookup_idx} at {url}."
94
+ ) from e
95
+ frm = SparseFrame(
96
+ index=index, intensity=intensity, idx=frame_idx, shape=shape
97
+ )
98
+
99
+ elif frame_type == FrameType.SMX_SPARSE:
100
+ try:
101
+ index = f[f"{datapath}/index"][lookup_idx]
102
+ intensity = f[f"{datapath}/intensity"][lookup_idx]
103
+ background_avg = f[f"{datapath}/background_avg"][lookup_idx]
104
+ background_std = f[f"{datapath}/background_std"][lookup_idx]
105
+ except IndexError as e:
106
+ raise RuntimeError(
107
+ f"Can't access element {lookup_idx} at {url}."
108
+ ) from e
109
+
110
+ frm = SmxSparseFrame(
111
+ index=index,
112
+ intensity=intensity,
113
+ background_avg=background_avg,
114
+ background_std=background_std,
115
+ idx=frame_idx,
116
+ shape=shape,
117
+ )
118
+
119
+ elif frame_type == FrameType.VECTOR:
120
+ try:
121
+ vector_data = f[f"{datapath}/data"][lookup_idx]
122
+ except IndexError as e:
123
+ raise RuntimeError(
124
+ f"Can't access element {lookup_idx} at {url}."
125
+ ) from e
126
+
127
+ frm = Vector(data=vector_data, idx=frame_idx)
128
+
129
+ else:
130
+ raise NotImplementedError(repr(frame_type))
131
+
132
+ return frm
@@ -6,6 +6,7 @@
6
6
  """Conductor HTTP services wrapped into functions."""
7
7
 
8
8
  import logging
9
+ import warnings
9
10
  from dataclasses import dataclass
10
11
  from enum import Enum, auto
11
12
  from importlib.metadata import version
@@ -14,19 +15,19 @@ from uuid import UUID
14
15
 
15
16
  import numpy as np
16
17
  import numpy.typing as npt
17
- import tango as tg
18
18
  from packaging.version import Version
19
19
 
20
+ from lima2.client import _utils
20
21
  from lima2.client.exceptions import ConductorConnectionError, ConductorEndpointNotFound
21
22
  from lima2.client.session import ConductorSession
22
- from lima2.common import pipelines, types
23
- from lima2.common.devencoded.dense_frame import Frame
23
+ from lima2.common.devencoded.common import Frame
24
+ from lima2.common.devencoded.data_array import Vector
24
25
  from lima2.common.devencoded.smx_sparse_frame import SmxSparseFrame
25
26
  from lima2.common.devencoded.sparse_frame import SparseFrame
26
- from lima2.common.exceptions import Lima2LookupError, Lima2NotFound
27
+ from lima2.common.exceptions import Lima2LookupError, Lima2NotFound, Lima2ValueError
27
28
  from lima2.common.progress_counter import ProgressCounter, SingleCounter
28
29
  from lima2.common.state import RunState, State
29
- from lima2.common.types import FrameInfo
30
+ from lima2.common.types import FrameInfo, FrameType
30
31
 
31
32
  logger = logging.getLogger(__name__)
32
33
 
@@ -43,14 +44,19 @@ class Acquisition:
43
44
  control: dict[str, Any],
44
45
  receiver: dict[str, Any],
45
46
  processing: dict[str, Any],
47
+ masterfile_metadata: dict[str, Any] | None = None,
46
48
  ) -> UUID:
47
49
  """Prepare for a new acquisition given control, receiver and processing params."""
50
+ if masterfile_metadata is None:
51
+ masterfile_metadata = {}
52
+
48
53
  uuid_str = self.session.post(
49
54
  "/acquisition/prepare",
50
55
  json={
51
56
  "control": control,
52
57
  "receiver": receiver,
53
58
  "processing": processing,
59
+ "masterfile_metadata": masterfile_metadata,
54
60
  },
55
61
  ).json()
56
62
  return UUID(uuid_str)
@@ -334,58 +340,90 @@ class Pipeline:
334
340
  def lookup(self, frame_idx: int, uuid: str | UUID = "current") -> tuple[int, str]:
335
341
  """Look up a frame by index.
336
342
 
343
+ Warning: Deprecated
344
+ This function will be removed in a future release.
345
+
337
346
  Returns a tuple (frame index, receiver url).
338
347
  """
348
+ warnings.warn(
349
+ "This function is deprecated and will be removed in a future release.",
350
+ stacklevel=2,
351
+ )
339
352
  json = self.session.get(f"/pipeline/{str(uuid)}/lookup/{frame_idx}").json()
340
353
 
341
354
  return (json["frame_idx"], json["receiver_url"])
342
355
 
343
356
  def get_frame(
344
357
  self, frame_idx: int, source: str, uuid: str | UUID = "current"
345
- ) -> Frame | SparseFrame | SmxSparseFrame:
346
- """Fetch a frame directly from a receiver device.
358
+ ) -> Frame | SparseFrame | SmxSparseFrame | Vector:
359
+ """Fetch a frame by absolute index.
347
360
 
348
- Raises:
349
- ValueError: Invalid frame source.
350
- RuntimeError: The frame isn't available.
351
- """
352
- try:
353
- fidx, rcv_url = self.lookup(frame_idx=frame_idx, uuid=uuid)
354
- except Lima2LookupError as e:
355
- raise RuntimeError(f"Can't find {source} {frame_idx}: {e}") from None
361
+ If frame_idx == -1, returns the most recent frame acquired.
356
362
 
357
- class_name = self.pipeline(uuid=uuid)["type"]
358
- try:
359
- pipeline_info = pipelines.by_name[class_name]
360
- except KeyError as e:
361
- raise NotImplementedError(class_name) from e
363
+ Requires a source name, which depends on the type of processing pipeline.
364
+ Valid source names can be queried with
365
+ [`frame_channels()`][lima2.client.services.Pipeline.frame_channels].
362
366
 
363
- try:
364
- frame_source = pipeline_info.frame_sources[source]
365
- except KeyError as e:
366
- raise ValueError(
367
- f"Invalid frame source '{source}' for pipeline {class_name}."
368
- ) from e
367
+ This function will first make a lookup request to determine the frame's
368
+ location, which could be either on disk or inside a receiver's memory.
369
369
 
370
- device = tg.DeviceProxy(rcv_url)
370
+ NOTE: some types of frames cannot be accessed online since they aren't buffered
371
+ in memory, e.g. raw frames. Such a frame will become available once the file it
372
+ belongs to has been closed.
371
373
 
372
- getter = getattr(device, frame_source.getter_name)
374
+ On the other hand, some types of frames are only available from memory buffers,
375
+ and are never saved, e.g. "input frames". Such a frame will no longer be
376
+ available when the (circular) buffer that contains it has wrapped around.
373
377
 
378
+ Raises:
379
+ ValueError: Invalid frame source.
380
+ RuntimeError: The frame can't be retrieved.
381
+ """
374
382
  try:
375
- raw_data: tuple[str, bytes] = getter(frame_idx)
376
- except tg.DevFailed as e:
377
- logger.error(
378
- f"Failed to get {source} {frame_idx} (resolved to {fidx} on {device.dev_name()})"
383
+ lookup_result = self.session.get(
384
+ f"/pipeline/{str(uuid)}/lookup/{source}/{frame_idx}"
385
+ ).json()
386
+ except Lima2ValueError as e:
387
+ raise ValueError(f"{e}") from None
388
+ except Lima2LookupError as e:
389
+ raise RuntimeError(f"{e}") from None
390
+
391
+ h5_loc = lookup_result["hdf5"]
392
+ buffer_loc = lookup_result["tango"]
393
+
394
+ if h5_loc is not None:
395
+ try:
396
+ return _utils.hdf5_get_frame(
397
+ url=h5_loc["url"],
398
+ frame_idx=frame_idx,
399
+ lookup_idx=h5_loc["idx"],
400
+ frame_type=FrameType[h5_loc["frame_type"]],
401
+ shape=tuple(h5_loc["shape"]),
402
+ )
403
+ except OSError as e:
404
+ # We cannot be too specific with the exception type here,
405
+ # see https://docs.h5py.org/en/stable/faq.html#exceptions.
406
+
407
+ # The file is likely still open for writing.
408
+ # Absorb the IO error and fall through to try online access.
409
+ logger.debug(
410
+ f"Can't read frame from HDF5 file {h5_loc} (error: {e}) "
411
+ f"-> falling back to tango access"
412
+ )
413
+
414
+ if buffer_loc is not None:
415
+ return _utils.tango_get_frame(
416
+ url=buffer_loc["url"],
417
+ command_name=buffer_loc["command_name"],
418
+ lookup_idx=buffer_loc["idx"],
379
419
  )
380
- raise RuntimeError(
381
- f"Unable to get frame {fidx} from {device.dev_name()}:\n{e}"
382
- ) from e
383
-
384
- decoder = types.decoder_by_type[frame_source.frame_type]
385
420
 
386
- frm = decoder(raw_data)
387
-
388
- return frm
421
+ # NOTE: if the frame source isn't buffered AND it hasn't been saved to disk,
422
+ # we end up with no way to fetch it.
423
+ raise RuntimeError(
424
+ f"Requested {source} {frame_idx} currently has no known location "
425
+ f"in either memory or disk."
426
+ )
389
427
 
390
428
 
391
429
  class ConnectionState(Enum):
@@ -0,0 +1,26 @@
1
+ # This file is part of the Lima2 project
2
+ #
3
+ # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
4
+ # Distributed under the MIT license. See LICENSE for more info.
5
+
6
+ """Definitions used by multiple DevEncoded deserialization modules."""
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ import numpy.typing as npt
12
+
13
+ DevEncoded = bytes | tuple[str, bytes]
14
+
15
+
16
+ @dataclass
17
+ class Frame:
18
+ """
19
+ Provide data frame from Lima including few metadata
20
+ """
21
+
22
+ data: npt.NDArray[Any]
23
+ """Data of the frame"""
24
+
25
+ idx: int
26
+ """Index of the frame where 0 is the first frame"""
@@ -0,0 +1,208 @@
1
+ # This file is part of the Lima2 project
2
+ #
3
+ # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
4
+ # Distributed under the MIT license. See LICENSE for more info.
5
+
6
+ """Decoding of DevEncoded data in the DATA_ARRAY format.
7
+
8
+ This includes multiple categories of payloads, for instance dense frames (2D) and
9
+ vectors (1D).
10
+ """
11
+
12
+ import struct
13
+ from dataclasses import dataclass
14
+ from enum import IntEnum
15
+ from typing import Any, TypeVar
16
+
17
+ import numpy as np
18
+ import numpy.typing as npt
19
+
20
+ from lima2.common.devencoded.common import DevEncoded, Frame
21
+ from lima2.common.devencoded.exception import DevEncodedFormatNotSupported
22
+
23
+ T = TypeVar("T", bound=np.generic)
24
+
25
+
26
+ class DataArrayCategory(IntEnum):
27
+ """Analogue to lima::tango::category_enum. Values must match."""
28
+
29
+ SCALAR_STACK = 0
30
+ SPECTRUM = 1
31
+ IMAGE = 2
32
+ SPECTRUM_STACK = 3
33
+ IMAGE_STACK = 4
34
+
35
+
36
+ class DataArrayDType(IntEnum):
37
+ """Analogue to lima::tango::data_type_enum. Values must match."""
38
+
39
+ UINT8 = 0
40
+ UINT16 = 1
41
+ UINT32 = 2
42
+ UINT64 = 3
43
+ INT8 = 4
44
+ INT16 = 5
45
+ INT32 = 6
46
+ INT64 = 7
47
+ FLOAT32 = 8
48
+ FLOAT64 = 9
49
+
50
+
51
+ # Mapping used for direct conversion from raw data to numpy array
52
+ DTYPE_TO_NUMPY: dict[DataArrayDType, type] = {
53
+ DataArrayDType.UINT8: np.uint8,
54
+ DataArrayDType.UINT16: np.uint16,
55
+ DataArrayDType.UINT32: np.uint32,
56
+ DataArrayDType.UINT64: np.uint64,
57
+ DataArrayDType.INT8: np.int8,
58
+ DataArrayDType.INT16: np.int16,
59
+ DataArrayDType.INT32: np.int32,
60
+ DataArrayDType.INT64: np.int64,
61
+ DataArrayDType.FLOAT32: np.float32,
62
+ DataArrayDType.FLOAT64: np.float64,
63
+ }
64
+
65
+ HEADER_FORMAT = "<IHHIIHHHHHHHHIIIIIIQ"
66
+ MAGIC = struct.unpack(">I", b"DTAY")[0]
67
+ HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
68
+
69
+
70
+ @dataclass
71
+ class DataArrayHeader:
72
+ magic: int
73
+ version: int
74
+ header_size: int
75
+ category: int
76
+ data_type: int
77
+ endianness: int
78
+ nb_dim: int
79
+ dim1: int
80
+ dim2: int
81
+ dim3: int
82
+ dim4: int
83
+ dim5: int
84
+ dim6: int
85
+ dim_step1: int
86
+ dim_step2: int
87
+ dim_step3: int
88
+ dim_step4: int
89
+ dim_step5: int
90
+ dim_step6: int
91
+ frame_idx: int
92
+
93
+ @staticmethod
94
+ def from_bytes(raw: bytes) -> "DataArrayHeader":
95
+ return DataArrayHeader(*struct.unpack_from(HEADER_FORMAT, raw))
96
+
97
+
98
+ def unpack_frame(
99
+ payload: bytes, shape: tuple[int, int, int], dtype: np.dtype[T], frame_idx: int
100
+ ) -> Frame:
101
+ """Instantiate a Frame from a data array header and payload."""
102
+
103
+ data = np.frombuffer(payload, dtype=dtype).reshape(shape)
104
+
105
+ # Create a memory copy only if it is needed
106
+ if not data.flags.writeable:
107
+ data = np.array(data)
108
+
109
+ return Frame(data=data, idx=frame_idx)
110
+
111
+
112
+ @dataclass
113
+ class Vector:
114
+ """1D data, aka spectrum."""
115
+
116
+ data: npt.NDArray[Any]
117
+ idx: int
118
+
119
+
120
+ def unpack_vector(payload: bytes, dtype: np.dtype[T], frame_idx: int) -> Vector:
121
+ """Instantiate a Vector from a data array header and payload."""
122
+
123
+ data = np.frombuffer(payload, dtype=dtype)
124
+
125
+ # Create a memory copy only if it is needed
126
+ if not data.flags.writeable:
127
+ data = np.array(data)
128
+
129
+ return Vector(data=data, idx=frame_idx)
130
+
131
+
132
+ def decode(raw_data: DevEncoded) -> Frame | Vector:
133
+ """Decode a DevEncoded DATA_ARRAY and unpack the payload.
134
+
135
+ The resulting data depends on the header's category field.
136
+ """
137
+
138
+ if isinstance(raw_data, tuple):
139
+ # Support the direct output from proxy.readImage
140
+ if raw_data[0] not in ["DATA_ARRAY"]:
141
+ raise DevEncodedFormatNotSupported(
142
+ "Data type DATA_ARRAY expected (found %s)." % raw_data[0]
143
+ )
144
+ raw_data = raw_data[1]
145
+
146
+ header = DataArrayHeader.from_bytes(raw=raw_data)
147
+
148
+ if header.magic != MAGIC:
149
+ raise DevEncodedFormatNotSupported(
150
+ "Magic header not supported (found 0x%x)." % header.magic
151
+ )
152
+
153
+ # Assume backward-compatible incremental versioning
154
+ if header.version < 1:
155
+ raise DevEncodedFormatNotSupported(
156
+ f"Data array header version {header.version} not supported (expected >= 1)."
157
+ )
158
+
159
+ if header.header_size != HEADER_SIZE:
160
+ raise DevEncodedFormatNotSupported(
161
+ f"Invalid header size {header.header_size} " f"(expected {HEADER_SIZE})."
162
+ )
163
+
164
+ if header.endianness != 0:
165
+ raise DevEncodedFormatNotSupported(
166
+ "Unsupported endianness (found %s)." % header.endianness
167
+ )
168
+
169
+ try:
170
+ data_type = DataArrayDType(header.data_type)
171
+ except ValueError:
172
+ raise DevEncodedFormatNotSupported(
173
+ f"Data array data_type {header.data_type} not supported"
174
+ ) from None
175
+
176
+ if data_type not in DTYPE_TO_NUMPY:
177
+ raise NotImplementedError(
178
+ f"Missing dtype association for {data_type}"
179
+ ) from None
180
+
181
+ dtype = DTYPE_TO_NUMPY[data_type]
182
+
183
+ match header.category:
184
+ case DataArrayCategory.IMAGE:
185
+ if header.nb_dim != 3:
186
+ raise DevEncodedFormatNotSupported(
187
+ f"Expected frame nb_dim == 3, got {header.nb_dim}"
188
+ )
189
+ return unpack_frame(
190
+ payload=raw_data[header.header_size :],
191
+ shape=(header.dim3, header.dim2, header.dim1),
192
+ dtype=np.dtype(dtype),
193
+ frame_idx=header.frame_idx,
194
+ )
195
+ case DataArrayCategory.SPECTRUM:
196
+ if header.nb_dim != 1:
197
+ raise DevEncodedFormatNotSupported(
198
+ f"Expected vector nb_dim == 1, got {header.nb_dim}"
199
+ )
200
+ return unpack_vector(
201
+ payload=raw_data[header.header_size :],
202
+ dtype=np.dtype(dtype),
203
+ frame_idx=header.frame_idx,
204
+ )
205
+ case _:
206
+ raise NotImplementedError(
207
+ f"No unpack function for data array category {header.category}"
208
+ )
@@ -12,9 +12,8 @@ from typing import Any
12
12
  import numpy as np
13
13
  import numpy.typing as npt
14
14
 
15
- from lima2.common.devencoded.dense_frame import EncodedFrame, Frame
16
-
17
- from .exception import DevEncodedFormatNotSupported
15
+ from lima2.common.devencoded.common import DevEncoded, Frame
16
+ from lima2.common.devencoded.exception import DevEncodedFormatNotSupported
18
17
 
19
18
  DATA_HEADER_FORMAT = "<HHIII"
20
19
  DATA_HEADER_SIZE = struct.calcsize(DATA_HEADER_FORMAT)
@@ -91,7 +90,7 @@ class SmxSparseFrame:
91
90
  return self.densify_peaks()
92
91
 
93
92
 
94
- def decode(raw_data: EncodedFrame) -> SmxSparseFrame:
93
+ def decode(raw_data: DevEncoded) -> SmxSparseFrame:
95
94
  """Decode data provided by Lima2
96
95
 
97
96
  Argument:
@@ -12,9 +12,8 @@ from typing import Any
12
12
  import numpy as np
13
13
  import numpy.typing as npt
14
14
 
15
- from lima2.common.devencoded.dense_frame import EncodedFrame, Frame
16
-
17
- from .exception import DevEncodedFormatNotSupported
15
+ from lima2.common.devencoded.common import Frame
16
+ from lima2.common.devencoded.exception import DevEncodedFormatNotSupported
18
17
 
19
18
  DATA_HEADER_FORMAT = "<HHII"
20
19
  DATA_HEADER_SIZE = struct.calcsize(DATA_HEADER_FORMAT)
@@ -53,7 +52,10 @@ class SparseFrame:
53
52
  return Frame(dense, self.idx)
54
53
 
55
54
 
56
- def decode(raw_data: EncodedFrame) -> SparseFrame:
55
+ EncodedSparseFrame = bytes | tuple[str, bytes]
56
+
57
+
58
+ def decode(raw_data: EncodedSparseFrame) -> SparseFrame:
57
59
  """Decode data provided by Lima2
58
60
 
59
61
  Argument:
@@ -11,7 +11,7 @@ from typing import Any
11
11
  import numpy
12
12
  import numpy.typing as npt
13
13
 
14
- from .exception import DevEncodedFormatNotSupported
14
+ from lima2.common.devencoded.exception import DevEncodedFormatNotSupported
15
15
 
16
16
  DATA_HEADER_FORMAT = "<IHHIIHHHHHHHHIIIIIIQ"
17
17
  DATA_MAGIC = struct.unpack(">I", b"DTAY")[0]