lima2.client 3.0a0__tar.gz → 3.1.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 (50) hide show
  1. {lima2_client-3.0a0/src/lima2_client.egg-info → lima2_client-3.1.0}/PKG-INFO +2 -2
  2. {lima2_client-3.0a0 → lima2_client-3.1.0}/pyproject.toml +1 -1
  3. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/client.py +81 -3
  4. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/detector.py +38 -10
  5. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/pipeline.py +276 -154
  6. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/pipelines/legacy.py +4 -2
  7. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/pipelines/smx.py +53 -10
  8. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/pipelines/xpcs.py +20 -9
  9. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/state_machine.py +3 -0
  10. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/topology.py +20 -15
  11. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/utils.py +23 -3
  12. lima2_client-3.1.0/src/lima2/master_file/generate.py +815 -0
  13. lima2_client-3.1.0/src/lima2/master_file/py.typed +0 -0
  14. {lima2_client-3.0a0 → lima2_client-3.1.0/src/lima2_client.egg-info}/PKG-INFO +2 -2
  15. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2_client.egg-info/SOURCES.txt +1 -0
  16. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2_client.egg-info/requires.txt +1 -1
  17. lima2_client-3.0a0/src/lima2/master_file/generate.py +0 -247
  18. {lima2_client-3.0a0 → lima2_client-3.1.0}/LICENSE +0 -0
  19. {lima2_client-3.0a0 → lima2_client-3.1.0}/README.md +0 -0
  20. {lima2_client-3.0a0 → lima2_client-3.1.0}/setup.cfg +0 -0
  21. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/__init__.py +0 -0
  22. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/draw_state_diagram.py +0 -0
  23. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/examples/__init__.py +0 -0
  24. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/examples/async_github_pull_request.py +0 -0
  25. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/examples/async_turnstile.py +0 -0
  26. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/examples/boolean_field.py +0 -0
  27. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/examples/github_pull_request.py +0 -0
  28. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/examples/turnstile.py +0 -0
  29. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/exceptions.py +0 -0
  30. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/finite_state_machine/state_machine.py +0 -0
  31. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/__init__.py +0 -0
  32. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/convert.py +0 -0
  33. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/devencoded/__init__.py +0 -0
  34. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/devencoded/dense_frame.py +0 -0
  35. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/devencoded/exception.py +0 -0
  36. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/devencoded/smx_sparse_frame.py +0 -0
  37. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/devencoded/sparse_frame.py +0 -0
  38. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/devencoded/structured_array.py +0 -0
  39. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/pipelines/__init__.py +0 -0
  40. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/pipelines/failing.py +0 -0
  41. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/progress_counter.py +0 -0
  42. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/py.typed +0 -0
  43. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/client/shell.py +0 -0
  44. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/server/__init__.py +0 -0
  45. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/server/control.py +0 -0
  46. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/server/receiver.py +0 -0
  47. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2/server/timer.py +0 -0
  48. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2_client.egg-info/dependency_links.txt +0 -0
  49. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2_client.egg-info/entry_points.txt +0 -0
  50. {lima2_client-3.0a0 → lima2_client-3.1.0}/src/lima2_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lima2-client
3
- Version: 3.0a0
3
+ Version: 3.1.0
4
4
  Summary: Python client library for Lima2 detector
5
5
  Author: BCU (ESRF)
6
6
  License: Copyright (c) 2020-2024 Beamline Control Unit, ESRF
@@ -24,7 +24,7 @@ Requires-Dist: gevent
24
24
  Requires-Dist: jsonschema
25
25
  Requires-Dist: jsonschema-default
26
26
  Requires-Dist: beartype
27
- Requires-Dist: numpy
27
+ Requires-Dist: numpy<2.3
28
28
  Requires-Dist: pyyaml
29
29
  Requires-Dist: typing-extensions
30
30
  Requires-Dist: h5py
@@ -24,7 +24,7 @@ dependencies = [
24
24
  "jsonschema",
25
25
  "jsonschema-default",
26
26
  "beartype",
27
- "numpy",
27
+ "numpy<2.3", # CI runner (CentOS 7) has glibc2.17, numpy>=2.3 needs glibc2.28+
28
28
  "pyyaml",
29
29
  "typing-extensions",
30
30
  "h5py",
@@ -34,6 +34,8 @@ from lima2.client.progress_counter import ProgressCounter, SingleCounter
34
34
  from lima2.client.topology import TopologyKind
35
35
  from lima2.client.utils import docstring_from
36
36
 
37
+ from lima2.master_file import generate as mfg
38
+
37
39
  logger = logging.getLogger(__name__)
38
40
 
39
41
 
@@ -77,6 +79,32 @@ class Client:
77
79
 
78
80
  self._tango_db = tg.Database()
79
81
 
82
+ # Show master file generation info messages
83
+ mfg.logger.setLevel(logging.INFO)
84
+
85
+ self.master_file_tasks: dict[UUID, dict[str, mfg.MasterFileTask]] = {}
86
+ """Master file generation tasks for each pipeline UUID.
87
+
88
+ Each entry corresponds to a pipeline, and holds a dict structured like
89
+ {saving channel name: list of tasks}.
90
+ """
91
+
92
+ def cleanup(self):
93
+ """Unregister all state loggers and tango event handlers.
94
+
95
+ This method serves only in specific cases where event handlers must for
96
+ some reason be unregistered. In a normal use case, a Client is
97
+ instantiated once per Lima2 instance, and no cleanup is necessary.
98
+
99
+ NOTE: After this call, the Client instance should no longer be used.
100
+ """
101
+ self._detector.unregister_events()
102
+ self._detector = None
103
+
104
+ for pipeline in self.__pipelines.values():
105
+ pipeline.unregister_events()
106
+ self.__pipelines.clear()
107
+
80
108
  @classmethod
81
109
  def from_yaml(
82
110
  cls,
@@ -147,16 +175,30 @@ class Client:
147
175
  ####################################################################################
148
176
 
149
177
  def prepare_acq(
150
- self, uuid: UUID, ctrl_params: dict, acq_params: dict, proc_params: dict
178
+ self,
179
+ uuid: UUID,
180
+ ctrl_params: dict,
181
+ acq_params: dict,
182
+ proc_params: dict,
151
183
  ) -> None:
152
184
  """Prepare acquisition with a given uuid and set of params."""
153
185
 
186
+ # Check parameters required for master file generation
187
+ checked_params = mfg.check_generation(
188
+ ctrl_params=ctrl_params,
189
+ recv_params=acq_params,
190
+ proc_params=proc_params,
191
+ topology_kind=self.topology_kind,
192
+ )
193
+ ctrl_params, acq_params, proc_params = checked_params
194
+
154
195
  # Adjust params for distributed acquisition
155
196
  ctl, acq, proc = topology.distribute_acq(
156
197
  ctl_params=ctrl_params,
157
198
  acq_params=acq_params,
158
199
  proc_params=proc_params,
159
200
  num_receivers=len(self.receivers),
201
+ topology_kind=self.topology_kind,
160
202
  )
161
203
 
162
204
  self.detector.prepare_acq(uuid, ctl, acq, proc)
@@ -174,9 +216,40 @@ class Client:
174
216
 
175
217
  self.current_pipeline.register_on_error(on_error)
176
218
 
219
+ # Remove master file tasks of previous acquisitions if done or not started
220
+ for prev_uuid in list(self.master_file_tasks.keys()):
221
+ tasks = self.master_file_tasks[prev_uuid].values()
222
+ if all([not task.started() or task.ready() for task in tasks]):
223
+ logger.debug(
224
+ "All master file tasks done (or never started) for "
225
+ f"{prev_uuid} -> cleaning up"
226
+ )
227
+ self.master_file_tasks.pop(prev_uuid)
228
+ else:
229
+ logger.debug(f"Not all master file tasks done for {prev_uuid}...")
230
+
231
+ self.master_file_tasks[uuid] = mfg.enable_generation(
232
+ pipeline=self.current_pipeline,
233
+ num_frames=ctrl_params["acq"]["nb_frames"],
234
+ ctrl_params=ctl,
235
+ recv_params=acq[0],
236
+ proc_params=proc[0],
237
+ topology_kind=self.topology_kind,
238
+ )
239
+
177
240
  @docstring_from(Detector.start_acq)
178
241
  def start_acq(self) -> None:
179
- self.detector.start_acq()
242
+ pipeline = self.current_pipeline
243
+ pipeline.register_on_finished(pipeline.unregister_events)
244
+
245
+ try:
246
+ self.detector.start_acq()
247
+ except Exception:
248
+ pipeline.unregister_events()
249
+ raise
250
+
251
+ for task in self.master_file_tasks[self.current_pipeline.uuid].values():
252
+ task.start()
180
253
 
181
254
  @docstring_from(Detector.trigger)
182
255
  def trigger(self) -> None:
@@ -293,9 +366,14 @@ class Client:
293
366
 
294
367
  Raises if the pipeline is absent from a receiver.
295
368
  """
369
+
370
+ # First unregister Tango events
371
+ pipeline = self.__pipelines.pop(uuid, None)
372
+ if pipeline is not None:
373
+ pipeline.unregister_events()
374
+
296
375
  for recv in self.receivers:
297
376
  recv.erasePipeline(str(uuid))
298
- self.__pipelines.pop(uuid, None)
299
377
 
300
378
  def clear_previous_pipelines(self) -> None:
301
379
  """Erase all pipelines except the current one."""
@@ -24,6 +24,8 @@ import logging
24
24
  import uuid
25
25
  from collections.abc import Iterator
26
26
 
27
+ import gevent
28
+
27
29
  import tango
28
30
  from beartype import beartype
29
31
  from gevent.lock import BoundedSemaphore
@@ -71,6 +73,7 @@ class Detector:
71
73
 
72
74
  self.__ctrl = ctrl_dev
73
75
  self.__recvs = list(recv_devs)
76
+ self._state_event_ids: dict[tango.DeviceProxy, int] = {}
74
77
 
75
78
  self.ping()
76
79
 
@@ -98,6 +101,17 @@ class Detector:
98
101
  d.set_green_mode(tango.GreenMode.Gevent)
99
102
  d.set_timeout_millis(timeout * 1000)
100
103
 
104
+ def unregister_events(self):
105
+ """Unregister all state loggers and tango event handlers.
106
+
107
+ This method serves only in specific cases where event handlers must for
108
+ some reason be unregistered. In a normal use case, a Client is
109
+ instantiated once per Lima2 instance, and no cleanup is necessary.
110
+ """
111
+ for device, event_id in self._state_event_ids.items():
112
+ device.unsubscribe_event(event_id)
113
+ self._state_event_ids.clear()
114
+
101
115
  @contextlib.contextmanager
102
116
  def _locked_fsm(self) -> Iterator[fsm.StateMachine]:
103
117
  self.__fsm_lock.acquire()
@@ -406,12 +420,18 @@ class Detector:
406
420
  f"prepare_acq({uuid}, {ctrl_params}, {acq_params}, {proc_params})"
407
421
  )
408
422
 
409
- validate(
410
- instance=ctrl_params,
411
- schema=self.params_schema[self.ctrl.name()]["acq_params"],
412
- )
423
+ def parallel_exec(ctrl_fn, recv_fn):
424
+ tasks = [gevent.spawn(ctrl_fn, self.__ctrl)]
425
+ tasks += [gevent.spawn(recv_fn, i, r) for i, r in enumerate(self.__recvs)]
426
+ gevent.joinall(tasks, raise_error=True)
427
+
428
+ def validate_ctrl(ctrl):
429
+ validate(
430
+ instance=ctrl_params,
431
+ schema=self.params_schema[ctrl.name()]["acq_params"],
432
+ )
413
433
 
414
- for i, recv in enumerate(self.__recvs):
434
+ def validate_recv(i, recv):
415
435
  validate(
416
436
  instance=acq_params[i],
417
437
  schema=self.params_schema[recv.name()]["acq_params"],
@@ -420,13 +440,19 @@ class Detector:
420
440
  pipeline_class = pipelines.get_class(proc_params[i]["class_name"])
421
441
  validate(instance=proc_params[i], schema=pipeline_class.params_schema)
422
442
 
443
+ parallel_exec(validate_ctrl, validate_recv)
444
+
423
445
  # Update parameters on the device servers
424
- # TODO: Rename acq_params to ctrl_params
425
- self.__ctrl.acq_params = json.dumps(ctrl_params)
426
- for i, recv in enumerate(self.__recvs):
446
+ def update_ctrl(ctrl):
447
+ # TODO: Rename acq_params to ctrl_params
448
+ ctrl.acq_params = json.dumps(ctrl_params)
449
+
450
+ def update_recv(i, recv):
427
451
  recv.acq_params = json.dumps(acq_params[i])
428
452
  recv.proc_params = json.dumps(proc_params[i])
429
453
 
454
+ parallel_exec(update_ctrl, update_recv)
455
+
430
456
  # Raises and reverts to IDLE on exception
431
457
  with self._locked_fsm() as fsm:
432
458
  fsm.prepare(str(uuid))
@@ -619,12 +645,14 @@ class Detector:
619
645
  finally:
620
646
  _logger.info(f"Current state: {fsm.state}")
621
647
 
622
- for dev in self.devs:
623
- dev.subscribe_event(
648
+ self._state_event_ids = {
649
+ dev: dev.subscribe_event(
624
650
  "acq_state",
625
651
  tango.EventType.CHANGE_EVENT,
626
652
  on_acq_state_change,
627
653
  )
654
+ for dev in self.devs
655
+ }
628
656
 
629
657
  def _register_on_end_acq_handlers(self):
630
658
  """Register an "end of acquisition" callback on each receiver.