lima2.client 4.2.0__tar.gz → 4.3.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 (152) hide show
  1. {lima2_client-4.2.0 → lima2_client-4.3.0}/.gitlab-ci.yml +2 -2
  2. {lima2_client-4.2.0/src/lima2_client.egg-info → lima2_client-4.3.0}/PKG-INFO +2 -1
  3. {lima2_client-4.2.0 → lima2_client-4.3.0}/conda/recipe.yaml +1 -0
  4. lima2_client-4.3.0/docs/reference/accumulation.md +13 -0
  5. {lima2_client-4.2.0 → lima2_client-4.3.0}/mkdocs.yml +1 -0
  6. {lima2_client-4.2.0 → lima2_client-4.3.0}/pyproject.toml +1 -0
  7. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/_utils.py +16 -0
  8. lima2_client-4.3.0/src/lima2/client/accumulation.py +150 -0
  9. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/services.py +62 -1
  10. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/pipelines.py +1 -1
  11. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/acquisition.py +49 -14
  12. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/acquisition_system.py +7 -0
  13. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/master_file.py +327 -58
  14. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/pipeline.py +157 -36
  15. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/reduced_data.py +24 -7
  16. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/utils.py +1 -1
  17. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/acquisition.py +73 -2
  18. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/main.py +0 -11
  19. {lima2_client-4.2.0 → lima2_client-4.3.0/src/lima2_client.egg-info}/PKG-INFO +2 -1
  20. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2_client.egg-info/SOURCES.txt +5 -0
  21. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2_client.egg-info/requires.txt +1 -0
  22. lima2_client-4.3.0/src/lima2_client.egg-info/scm_file_list.json +146 -0
  23. lima2_client-4.3.0/src/lima2_client.egg-info/scm_version.json +8 -0
  24. lima2_client-4.3.0/tests/unit/client/test_accumulation.py +178 -0
  25. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/test_acquisition.py +30 -0
  26. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/test_utils.py +13 -0
  27. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/mock_devices.py +1 -0
  28. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_acquisition.py +90 -26
  29. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_master_file.py +252 -35
  30. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_pipeline.py +147 -40
  31. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_reduced_data.py +29 -0
  32. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/test_acquisition.py +127 -16
  33. {lima2_client-4.2.0 → lima2_client-4.3.0}/.gitignore +0 -0
  34. {lima2_client-4.2.0 → lima2_client-4.3.0}/LICENSE +0 -0
  35. {lima2_client-4.2.0 → lima2_client-4.3.0}/README.md +0 -0
  36. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/assets/lima2-components-and-repos.svg +0 -0
  37. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/assets/lima2-conductor-communication-model.svg +0 -0
  38. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/assets/lima2-conductor-modules.svg +0 -0
  39. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/assets/lima2-conductor-monitor-webpage.png +0 -0
  40. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/assets/logo-transparent-crop.png +0 -0
  41. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/concepts.md +0 -0
  42. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/contact.md +0 -0
  43. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/dev/conductor_client.md +0 -0
  44. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/dev/conductor_design.md +0 -0
  45. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/dev/conductor_requirements.md +0 -0
  46. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/dev/index.md +0 -0
  47. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/features.md +0 -0
  48. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/getting_started.md +0 -0
  49. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/index.md +0 -0
  50. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/license.md +0 -0
  51. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/monitor.md +0 -0
  52. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/acquisition.md +0 -0
  53. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/detector.md +0 -0
  54. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/exceptions.md +0 -0
  55. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/index.md +0 -0
  56. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/pipeline.md +0 -0
  57. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/services.md +0 -0
  58. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/reference/session.md +0 -0
  59. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/release_notes.md +0 -0
  60. {lima2_client-4.2.0 → lima2_client-4.3.0}/docs/shell.md +0 -0
  61. {lima2_client-4.2.0 → lima2_client-4.3.0}/flake8.ini +0 -0
  62. {lima2_client-4.2.0 → lima2_client-4.3.0}/scripts/cleanup_processing_ds.py +0 -0
  63. {lima2_client-4.2.0 → lima2_client-4.3.0}/setup.cfg +0 -0
  64. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/__init__.py +0 -0
  65. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/exceptions.py +0 -0
  66. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/py.typed +0 -0
  67. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/session.py +0 -0
  68. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/client/shell.py +0 -0
  69. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/__init__.py +0 -0
  70. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/__init__.py +0 -0
  71. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/common.py +0 -0
  72. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/data_array.py +0 -0
  73. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/exception.py +0 -0
  74. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/smx_sparse_frame.py +0 -0
  75. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/sparse_frame.py +0 -0
  76. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/devencoded/structured_array.py +0 -0
  77. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/exceptions.py +0 -0
  78. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/progress_counter.py +0 -0
  79. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/py.typed +0 -0
  80. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/state.py +0 -0
  81. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/common/types.py +0 -0
  82. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/__init__.py +0 -0
  83. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/detector.py +0 -0
  84. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/__init__.py +0 -0
  85. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/fai.py +0 -0
  86. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/failing.py +0 -0
  87. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/legacy.py +0 -0
  88. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/smx.py +0 -0
  89. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/processing/xpcs.py +0 -0
  90. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/py.typed +0 -0
  91. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/tango/__init__.py +0 -0
  92. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/tango/container.py +0 -0
  93. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/tango/control.py +0 -0
  94. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/tango/processing.py +0 -0
  95. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/tango/receiver.py +0 -0
  96. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/tango/utils.py +0 -0
  97. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/topology.py +0 -0
  98. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/__init__.py +0 -0
  99. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/benchmark.py +0 -0
  100. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/benchmark_generators/__init__.py +0 -0
  101. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/benchmark_generators/rois.py +0 -0
  102. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/cli.py +0 -0
  103. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/detector.py +0 -0
  104. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/favicon.ico +0 -0
  105. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/fonts/Roboto-Italic.woff2 +0 -0
  106. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/fonts/Roboto-Medium.woff2 +0 -0
  107. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/fonts/Roboto-MediumItalic.woff2 +0 -0
  108. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/fonts/Roboto-Regular.woff2 +0 -0
  109. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/fonts.css +0 -0
  110. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/index.html +0 -0
  111. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/milligram.css +0 -0
  112. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/monitor/normalize.css +0 -0
  113. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/pipeline.py +0 -0
  114. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/utils.py +0 -0
  115. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2/conductor/webservice/webapp.py +0 -0
  116. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2_client.egg-info/dependency_links.txt +0 -0
  117. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2_client.egg-info/entry_points.txt +0 -0
  118. {lima2_client-4.2.0 → lima2_client-4.3.0}/src/lima2_client.egg-info/top_level.txt +0 -0
  119. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/__init__.py +0 -0
  120. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/__init__.py +0 -0
  121. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/conftest.py +0 -0
  122. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/tango_db/simulator_ctl.yaml +0 -0
  123. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/tango_db/simulator_rcv1.yaml +0 -0
  124. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/tango_db/simulator_rcv2.yaml +0 -0
  125. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/test_failing.py +0 -0
  126. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/integration/test_legacy.py +0 -0
  127. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/__init__.py +0 -0
  128. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/__init__.py +0 -0
  129. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/conftest.py +0 -0
  130. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/test_detector.py +0 -0
  131. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/test_pipeline.py +0 -0
  132. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/client/test_session.py +0 -0
  133. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/common/__init__.py +0 -0
  134. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/common/test_data_array.py +0 -0
  135. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/common/test_progress_counter.py +0 -0
  136. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/common/test_smx_sparse_frame.py +0 -0
  137. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/common/test_sparse_frame.py +0 -0
  138. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/common/test_state.py +0 -0
  139. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/__init__.py +0 -0
  140. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/conftest.py +0 -0
  141. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/mock_devencoded.py +0 -0
  142. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_acquisition_system.py +0 -0
  143. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_processing.py +0 -0
  144. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_tango_devices.py +0 -0
  145. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_topology.py +0 -0
  146. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/test_utils.py +0 -0
  147. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/__init__.py +0 -0
  148. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/test_benchmark.py +0 -0
  149. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/test_detector.py +0 -0
  150. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/test_pipelines.py +0 -0
  151. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/test_utils.py +0 -0
  152. {lima2_client-4.2.0 → lima2_client-4.3.0}/tests/unit/conductor/webservice/test_webapp.py +0 -0
@@ -88,6 +88,7 @@ integration-test:
88
88
  script:
89
89
  - mamba create -n lima2_env -c conda-forge -c esrf-bcu --yes
90
90
  "esrf-bcu::lima2-core>=1.0.0,<2"
91
+ conda-forge::pip
91
92
  conda-forge::git # for setuptools_scm
92
93
  - mamba run -n lima2_env pip install .[dev]
93
94
  - export LIMA2_PLUGIN_FOLDER="$(conda info --base)/envs/lima2_env/lib/plugins"
@@ -175,9 +176,8 @@ deploy-registry:
175
176
 
176
177
  pages:
177
178
  stage: doc
178
- before_script:
179
- - pip install .[docs]
180
179
  script:
180
+ - pip install .[docs]
181
181
  - mkdocs build
182
182
  - mv site public
183
183
  tags:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lima2-client
3
- Version: 4.2.0
3
+ Version: 4.3.0
4
4
  Summary: The Python client library of the Lima2 system
5
5
  Author: BCU (ESRF)
6
6
  License-Expression: MIT
@@ -17,6 +17,7 @@ Requires-Dist: packaging
17
17
  Requires-Dist: pytango
18
18
  Requires-Dist: requests
19
19
  Provides-Extra: conductor
20
+ Requires-Dist: hdf5plugin; extra == "conductor"
20
21
  Requires-Dist: jsonschema-default; extra == "conductor"
21
22
  Requires-Dist: jsonschema-rs; extra == "conductor"
22
23
  Requires-Dist: orjson; extra == "conductor"
@@ -32,6 +32,7 @@ requirements:
32
32
  - termcolor # extra: shell
33
33
  - ipython # extra: shell
34
34
  - typer # extra: shell, conductor
35
+ - hdf5plugin # extra: conductor
35
36
  - jsonschema-default # extra: conductor
36
37
  - jsonschema-rs # extra: conductor
37
38
  - orjson # extra: conductor
@@ -0,0 +1,13 @@
1
+ # Accumulation
2
+
3
+ ::: lima2.client.accumulation
4
+
5
+ options:
6
+ toc_label: lima2.client.accumulation
7
+ show_root_heading: true
8
+ show_if_no_docstring: true
9
+ summary:
10
+ functions: true
11
+ filters:
12
+ - "!^_"
13
+ members_order: source
@@ -63,6 +63,7 @@ nav:
63
63
  - reference/services.md
64
64
  - reference/session.md
65
65
  - reference/exceptions.md
66
+ - reference/accumulation.md
66
67
  - contact.md
67
68
  - release_notes.md
68
69
  - Design docs:
@@ -30,6 +30,7 @@ lima2-shell = "lima2.client.shell:app [dev]"
30
30
 
31
31
  [project.optional-dependencies]
32
32
  conductor = [
33
+ "hdf5plugin",
33
34
  "jsonschema-default",
34
35
  "jsonschema-rs",
35
36
  "orjson",
@@ -6,8 +6,10 @@
6
6
  """Lima2 client private utility functions."""
7
7
 
8
8
  import logging
9
+ from typing import Any
9
10
 
10
11
  import h5py
12
+ import numpy as np
11
13
  import tango as tg
12
14
 
13
15
  from lima2.common import types
@@ -130,3 +132,17 @@ def hdf5_get_frame(
130
132
  raise NotImplementedError(repr(frame_type))
131
133
 
132
134
  return frm
135
+
136
+
137
+ def jsonify_numpy_arrays(d: dict[str, Any]) -> dict[str, Any]:
138
+ """
139
+ Return an identical dictionary with numpy arrays replaced by JSON-ready objects.
140
+ """
141
+ ret: dict[str, Any] = {}
142
+ for k, v in d.items():
143
+ if isinstance(v, np.ndarray):
144
+ v = dict(type="numpy.ndarray", value=v.tolist(), dtype=str(v.dtype))
145
+ elif isinstance(v, dict):
146
+ v = jsonify_numpy_arrays(v)
147
+ ret[k] = v
148
+ return ret
@@ -0,0 +1,150 @@
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 accumulation helpers."""
7
+
8
+ import math
9
+ from typing import Literal, NamedTuple
10
+
11
+
12
+ class AccumulationParams(NamedTuple):
13
+ expo_time: float
14
+ nb_frames: int
15
+ nb_frames_per_trigger: int
16
+ acc_nb_frames: int
17
+
18
+
19
+ def params_livetime(
20
+ expo_time: float,
21
+ max_expo_time: float,
22
+ npoints: int,
23
+ max_nb_frames_per_trigger: int,
24
+ trigger_mode: Literal["internal", "software", "external", "gate"],
25
+ ) -> AccumulationParams:
26
+ """
27
+ Calculate an optimal set of acquisition params given an upper limit constraint on
28
+ the duration of a single exposure, adding in accumulation if necessary, and if the
29
+ detector allows us to acquire a long-enough train of frames in a single trigger
30
+ pulse.
31
+
32
+ Assumes we are working in terms of "livetime", meaning the total exposure does not
33
+ include deadtime between frames. In this mode, accumulating `N` frames corresponds
34
+ to an integrated exposure of `N * subframe_expo`.
35
+
36
+ Args:
37
+ expo_time: Desired exposure of one accumulated frame (seconds).
38
+ max_expo_time: Desired upper limit on the expo time per subframe (seconds).
39
+ npoints: Number of scan iterations.
40
+ max_nb_frames_per_trigger: Maximum number of frames that the detector is able to
41
+ acquire per trigger signal.
42
+ trigger_mode: Whether the detector is triggered by hardware (external/gate),
43
+ software, or internally.
44
+ """
45
+ if max_expo_time <= 0:
46
+ raise ValueError("max_expo_time must be positive")
47
+ if expo_time <= 0:
48
+ raise ValueError("livetime must be positive")
49
+ if npoints < 0:
50
+ raise ValueError("npoints must not be negative")
51
+
52
+ if expo_time > max_expo_time:
53
+ acc_nb_frames = int(math.ceil(expo_time / max_expo_time))
54
+ det_expo_time = expo_time / acc_nb_frames
55
+ det_nb_frames = npoints * acc_nb_frames
56
+ nb_frames_per_trigger = acc_nb_frames
57
+
58
+ if acc_nb_frames > max_nb_frames_per_trigger:
59
+ if trigger_mode == "software":
60
+ raise ValueError(
61
+ f"Can't resolve acquisition parameters in software triggering with "
62
+ f"accumulation: "
63
+ f"would need to set acc_nb_frames={acc_nb_frames} but the "
64
+ f"upper limit is {max_nb_frames_per_trigger}."
65
+ )
66
+ elif trigger_mode == "external":
67
+ nb_frames_per_trigger = 1
68
+
69
+ return AccumulationParams(
70
+ expo_time=det_expo_time,
71
+ nb_frames=det_nb_frames,
72
+ nb_frames_per_trigger=nb_frames_per_trigger,
73
+ acc_nb_frames=acc_nb_frames,
74
+ )
75
+ else:
76
+ return AccumulationParams(
77
+ expo_time=expo_time,
78
+ nb_frames=npoints,
79
+ nb_frames_per_trigger=1,
80
+ acc_nb_frames=1,
81
+ )
82
+
83
+
84
+ def params_realtime(
85
+ expo_time: float,
86
+ latency_time: float,
87
+ max_expo_time: float,
88
+ npoints: int,
89
+ max_nb_frames_per_trigger: int,
90
+ trigger_mode: Literal["internal", "software", "external", "gate"],
91
+ ) -> AccumulationParams:
92
+ """
93
+ Calculate an optimal set of acquisition params given an upper limit constraint on
94
+ the duration of a single exposure, adding in accumulation if necessary, and if the
95
+ detector allows us to acquire a long-enough train of frames in a single trigger
96
+ pulse.
97
+
98
+ Assumes we are working in terms of "realtime", where the resulting train of frames
99
+ should not exceed the requested frame time. To accomplish this, we must take into
100
+ account the dead time between exposures (latency time).
101
+
102
+ Args:
103
+ expo_time: Desired exposure of one accumulated frame (seconds).
104
+ latency_time: Delay between two subframe exposures (seconds).
105
+ max_expo_time: Desired upper limit on the expo time per subframe (seconds).
106
+ npoints: Number of scan iterations.
107
+ max_nb_frames_per_trigger: Maximum number of frames that the detector is able to
108
+ acquire per trigger signal.
109
+ trigger_mode: Whether the detector is triggered by hardware (external/gate),
110
+ software, or internally.
111
+ """
112
+ if max_expo_time <= 0:
113
+ raise ValueError("max_expo_time must be positive")
114
+ if expo_time <= 0:
115
+ raise ValueError("livetime must be positive")
116
+ if npoints < 0:
117
+ raise ValueError("npoints must not be negative")
118
+
119
+ if expo_time > max_expo_time:
120
+ acc_nb_frames = int(
121
+ math.ceil((expo_time + latency_time) / (max_expo_time + latency_time))
122
+ )
123
+ det_expo_time = (expo_time + latency_time) / acc_nb_frames - latency_time
124
+ det_nb_frames = npoints * acc_nb_frames
125
+ nb_frames_per_trigger = acc_nb_frames
126
+
127
+ if acc_nb_frames > max_nb_frames_per_trigger:
128
+ if trigger_mode == "software":
129
+ raise ValueError(
130
+ f"Can't resolve acquisition parameters in software triggering with "
131
+ f"accumulation: "
132
+ f"would need to set acc_nb_frames={acc_nb_frames} but the "
133
+ f"upper limit is {max_nb_frames_per_trigger}."
134
+ )
135
+ elif trigger_mode == "external":
136
+ nb_frames_per_trigger = 1
137
+
138
+ return AccumulationParams(
139
+ expo_time=det_expo_time,
140
+ nb_frames=det_nb_frames,
141
+ nb_frames_per_trigger=nb_frames_per_trigger,
142
+ acc_nb_frames=acc_nb_frames,
143
+ )
144
+ else:
145
+ return AccumulationParams(
146
+ expo_time=expo_time,
147
+ nb_frames=npoints,
148
+ nb_frames_per_trigger=1,
149
+ acc_nb_frames=1,
150
+ )
@@ -32,6 +32,43 @@ from lima2.common.types import FrameInfo, FrameType
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
34
 
35
+ ########################################################################################
36
+ # Acquisition results
37
+ ########################################################################################
38
+
39
+
40
+ @dataclass
41
+ class AcquisitionSuccess:
42
+ """The acquisition completed successfully, despite potential warnings."""
43
+
44
+ warnings: list[str]
45
+
46
+
47
+ @dataclass
48
+ class AcquisitionFailure:
49
+ """The acquisition failed because or acquisition or processing issues."""
50
+
51
+ acquisition_errors: list[str]
52
+ """Fatal errors during frame acquisition."""
53
+ processing_errors: list[str]
54
+ """Fatal errors during frame processing."""
55
+ warnings: list[str]
56
+ """Non-critical issues that occurred during the acquisition."""
57
+
58
+
59
+ class Unknown:
60
+ """No acquisition is ongoing, or the current acquisition is still running."""
61
+
62
+
63
+ AcquisitionResult = AcquisitionSuccess | AcquisitionFailure | Unknown
64
+ """Return type of `Acquisition.result()`. Outcome of the latest acquisition."""
65
+
66
+
67
+ ########################################################################################
68
+ # Services
69
+ ########################################################################################
70
+
71
+
35
72
  class Acquisition:
36
73
  """Conductor /acquisition service requests."""
37
74
 
@@ -56,7 +93,7 @@ class Acquisition:
56
93
  "control": control,
57
94
  "receiver": receiver,
58
95
  "processing": processing,
59
- "masterfile_metadata": masterfile_metadata,
96
+ "masterfile_metadata": _utils.jsonify_numpy_arrays(masterfile_metadata),
60
97
  },
61
98
  ).json()
62
99
  return UUID(uuid_str)
@@ -104,6 +141,30 @@ class Acquisition:
104
141
  runstate = RunState(self.session.get("/acquisition/state").json()["value"])
105
142
  return bool(runstate & RunState.FAULT)
106
143
 
144
+ def result(self) -> AcquisitionResult:
145
+ """Get information about the latest acquisition's outcome.
146
+
147
+ Returns an instance of either AcquisitionSuccess, AcquisitionFailure, or
148
+ Unknown.
149
+ """
150
+ result_json = self.session.get("/acquisition/result").json()
151
+ status = result_json["type"]
152
+
153
+ if status == "unknown":
154
+ return Unknown()
155
+ elif status == "success":
156
+ return AcquisitionSuccess(warnings=result_json["warnings"])
157
+ elif status == "failure":
158
+ acq_errors = result_json["errors"].get("acquisition", [])
159
+ proc_errors = result_json["errors"].get("processing", [])
160
+ return AcquisitionFailure(
161
+ acquisition_errors=acq_errors,
162
+ processing_errors=proc_errors,
163
+ warnings=result_json["warnings"],
164
+ )
165
+ else:
166
+ raise NotImplementedError(f"Unknown result format: {result_json}")
167
+
107
168
  def default_params(self) -> tuple[dict[str, Any], dict[str, Any]]:
108
169
  """Get default acquisition parameters.
109
170
 
@@ -84,7 +84,7 @@ smx = Pipeline(
84
84
  ),
85
85
  },
86
86
  reduced_data_sources={
87
- "peak_counter": ScalarDataSource(
87
+ "nb_peaks": ScalarDataSource(
88
88
  getter_name="popPeakCounters",
89
89
  src_dtype=PEAK_COUNTER_DTYPE,
90
90
  channel_keys=["nb_peaks"],
@@ -15,6 +15,7 @@ import traceback
15
15
  import weakref
16
16
  from dataclasses import dataclass
17
17
  from enum import Enum, auto
18
+ from pprint import pformat
18
19
  from typing import Awaitable, Callable
19
20
 
20
21
  from lima2.common.exceptions import Lima2BackendError
@@ -24,6 +25,7 @@ from lima2.conductor.processing.master_file import (
24
25
  FrameSavingChannel,
25
26
  MasterFileMetadata,
26
27
  )
28
+ from lima2.conductor.processing.pipeline import Pipeline
27
29
  from lima2.conductor.processing.reduced_data import RoiParams
28
30
  from lima2.conductor.tango.container import TangoProcessingGroup
29
31
  from lima2.conductor.tango.processing import ProcessingErrorEvent, TangoProcessing
@@ -72,6 +74,9 @@ OnEndCallback = Callable[[SuccessFlag], Awaitable[None]]
72
74
  OnFailureCallback = Callable[[], Awaitable[None]]
73
75
  """Signature of the on_failure() callback."""
74
76
 
77
+ OnProcessingFinishedCallback = Callable[[], Awaitable[None]]
78
+ """Signature of the on_processing_finished() callback."""
79
+
75
80
 
76
81
  class AcquisitionState:
77
82
  """Represents the state of a running acquisition.
@@ -94,13 +99,17 @@ class AcquisitionState:
94
99
  num_receivers: int,
95
100
  on_end: OnEndCallback,
96
101
  on_failure: OnFailureCallback,
102
+ on_processing_finished: OnProcessingFinishedCallback,
97
103
  ) -> None:
98
104
  self.num_receivers = num_receivers
99
105
 
100
- self.on_end = on_end
106
+ # NOTE: use WeakMethods to avoid cyclic Acquisition <-> AcquisitionState refs
107
+ self.on_end_ref = weakref.WeakMethod(on_end)
101
108
  """Called when all devices are done (successfully or not)."""
102
- self.on_failure = on_failure
109
+ self.on_failure_ref = weakref.WeakMethod(on_failure)
103
110
  """Called when any device fails, but only once."""
111
+ self.on_processing_finished_ref = weakref.WeakMethod(on_processing_finished)
112
+ """Called when all processing devices are finished (successfully or not)."""
104
113
 
105
114
  # Substate: we need to keep track of how many receivers and processing
106
115
  # devices are done, whether some failed, whether failure has already
@@ -247,6 +256,10 @@ class AcquisitionState:
247
256
  case AcquisitionEvent.PROC_FINISHED:
248
257
  self.proc_finished = True
249
258
 
259
+ on_processing_finished = self.on_processing_finished_ref()
260
+ if on_processing_finished:
261
+ await on_processing_finished()
262
+
250
263
  if self.acq_finished:
251
264
  events.append(AcquisitionEvent.FINISHED)
252
265
 
@@ -262,9 +275,13 @@ class AcquisitionState:
262
275
  logger.info("🔥 Pipeline finished with errors: see above 🔥")
263
276
 
264
277
  try:
265
- await self.on_end(
266
- SuccessFlag(success=not (self.rcv_failed or self.proc_failed))
267
- )
278
+ on_end = self.on_end_ref()
279
+ if on_end:
280
+ await on_end(
281
+ SuccessFlag(
282
+ success=not (self.rcv_failed or self.proc_failed)
283
+ )
284
+ )
268
285
  except BaseException:
269
286
  logger.warning(
270
287
  f"Exception in on_end() callback: {traceback.format_exc()}"
@@ -276,7 +293,9 @@ class AcquisitionState:
276
293
  if not self.failed_once:
277
294
  self.failed_once = True
278
295
  try:
279
- await self.on_failure()
296
+ on_failure = self.on_failure_ref()
297
+ if on_failure:
298
+ await on_failure()
280
299
  except BaseException:
281
300
  logger.warning(
282
301
  f"Exception in on_failure() callback: "
@@ -336,6 +355,7 @@ class Acquisition:
336
355
  on_end: OnEndCallback,
337
356
  on_failure: OnFailureCallback,
338
357
  num_frames: int,
358
+ accumulation_depth: int,
339
359
  frame_infos: dict[str, FrameInfo],
340
360
  roi_params: RoiParams,
341
361
  saving_channels: dict[str, FrameSavingChannel],
@@ -346,6 +366,7 @@ class Acquisition:
346
366
  num_receivers=len(processing_devices),
347
367
  on_end=on_end,
348
368
  on_failure=on_failure,
369
+ on_processing_finished=self.on_processing_finished,
349
370
  )
350
371
  self.proc_group = processing_devices
351
372
  """Processing device group."""
@@ -358,6 +379,7 @@ class Acquisition:
358
379
  self.pipeline = pipeline_class(
359
380
  devices=processing_devices,
360
381
  num_frames=num_frames,
382
+ accumulation_depth=accumulation_depth,
361
383
  frame_infos=frame_infos,
362
384
  roi_params=roi_params,
363
385
  saving_channels=saving_channels,
@@ -366,14 +388,30 @@ class Acquisition:
366
388
  """Processing pipeline."""
367
389
 
368
390
  self.processing_errors: list[str] = []
369
- """Holds processing error messages that occurred during the run, if any."""
391
+ """Holds processing error messages that occurred during the run, if any.
392
+
393
+ This list contains errors from the REMOTE processing devices.
394
+ """
370
395
 
371
- self.done_devices: set[TangoProcessing] = set()
372
- """Set of processing devices that have finished (successfully or not)."""
396
+ self.pipeline_result: Pipeline.PipelineResult | None = None
397
+ """Holds the return value of pipeline.close(), once processing is finished.
398
+
399
+ This result contains information about the LOCAL pipeline-related tasks, not the
400
+ online processing done by Lima2 backend devices.
401
+ """
402
+
403
+ async def on_processing_finished(self) -> None:
404
+ """Registered as the AcquisitionState's on_processing_finished() callback."""
405
+ logger.info("Closing pipeline")
406
+ self.pipeline_result = await self.pipeline.close()
407
+ logger.info(f"Pipeline result: {pformat(self.pipeline_result)}")
373
408
 
374
409
  def __del__(self) -> None:
375
410
  logger.debug(f"Acquisition instance {self.uuid} destroyed")
376
411
 
412
+ def started(self) -> bool:
413
+ return self.state.started
414
+
377
415
  def running(self) -> bool:
378
416
  return self.state.running()
379
417
 
@@ -391,6 +429,7 @@ class Acquisition:
391
429
  on_end: OnEndCallback,
392
430
  on_failure: OnFailureCallback,
393
431
  num_frames: int,
432
+ accumulation_depth: int,
394
433
  frame_infos: dict[str, FrameInfo],
395
434
  roi_params: RoiParams,
396
435
  saving_channels: dict[str, FrameSavingChannel],
@@ -405,6 +444,7 @@ class Acquisition:
405
444
  on_end=on_end,
406
445
  on_failure=on_failure,
407
446
  num_frames=num_frames,
447
+ accumulation_depth=accumulation_depth,
408
448
  frame_infos=frame_infos,
409
449
  roi_params=roi_params,
410
450
  saving_channels=saving_channels,
@@ -450,11 +490,6 @@ class Acquisition:
450
490
 
451
491
  await self_ref.notify(event=AcquisitionEvent.PROC_DEVICE_DONE)
452
492
 
453
- self_ref.done_devices.add(device)
454
- if self_ref.done_devices == set(self_ref.proc_group):
455
- logger.info("Closing pipeline")
456
- await self_ref.pipeline.close()
457
-
458
493
  async def on_device_error(evt: ProcessingErrorEvent) -> None:
459
494
  """Reports a processing error to the registered callback."""
460
495
  self_ref = wself()
@@ -385,6 +385,11 @@ class AcquisitionSystem:
385
385
  frame_types=frame_types,
386
386
  )
387
387
 
388
+ if "accumulation" in proc_params and proc_params["accumulation"]["enabled"]:
389
+ accumulation_depth = proc_params["accumulation"]["nb_frames"]
390
+ else:
391
+ accumulation_depth = 1
392
+
388
393
  logger.info(f"Frame infos:\n{pformat(frame_infos, indent=2)}")
389
394
  logger.info(f"Saving channels:\n{pformat(saving_channels, indent=2)}")
390
395
 
@@ -393,6 +398,7 @@ class AcquisitionSystem:
393
398
  on_end=self.on_acquisition_end,
394
399
  on_failure=self.on_acquisition_failure,
395
400
  num_frames=num_frames,
401
+ accumulation_depth=accumulation_depth,
396
402
  frame_infos=frame_infos,
397
403
  roi_params=roi_params,
398
404
  saving_channels=saving_channels,
@@ -620,6 +626,7 @@ class AcquisitionSystem:
620
626
  devices=proc_devices,
621
627
  frame_infos=frame_infos,
622
628
  num_frames=0,
629
+ accumulation_depth=1,
623
630
  roi_params=RoiParams(rois=[], profiles=[]),
624
631
  saving_channels={},
625
632
  masterfile_metadata=MasterFileMetadata(