dls-dodal 1.55.1__py3-none-any.whl → 1.57.0__py3-none-any.whl

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 (104) hide show
  1. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/RECORD +101 -87
  3. dodal/_version.py +16 -3
  4. dodal/beamlines/b01_1.py +6 -1
  5. dodal/beamlines/b07.py +2 -1
  6. dodal/beamlines/b07_1.py +2 -1
  7. dodal/beamlines/b21.py +4 -24
  8. dodal/beamlines/i03.py +53 -53
  9. dodal/beamlines/i04.py +16 -38
  10. dodal/beamlines/i09.py +3 -2
  11. dodal/beamlines/i09_1.py +2 -1
  12. dodal/beamlines/i11.py +143 -0
  13. dodal/beamlines/i17.py +37 -0
  14. dodal/beamlines/i19_1.py +1 -0
  15. dodal/beamlines/i19_2.py +7 -0
  16. dodal/beamlines/i22.py +5 -5
  17. dodal/beamlines/i23.py +3 -3
  18. dodal/beamlines/i24.py +6 -33
  19. dodal/beamlines/p38.py +1 -0
  20. dodal/beamlines/p60.py +3 -2
  21. dodal/cli.py +11 -1
  22. dodal/common/__init__.py +4 -0
  23. dodal/common/beamlines/beamline_parameters.py +1 -1
  24. dodal/common/beamlines/beamline_utils.py +5 -1
  25. dodal/common/enums.py +19 -0
  26. dodal/common/watcher_utils.py +83 -0
  27. dodal/devices/aithre_lasershaping/laser_robot.py +4 -9
  28. dodal/devices/aperturescatterguard.py +52 -12
  29. dodal/devices/apple2_undulator.py +0 -1
  30. dodal/devices/b16/detector.py +1 -10
  31. dodal/devices/backlight.py +8 -20
  32. dodal/devices/bimorph_mirror.py +4 -7
  33. dodal/devices/collimation_table.py +36 -0
  34. dodal/devices/controllers.py +21 -0
  35. dodal/devices/cryostream.py +97 -7
  36. dodal/devices/current_amplifiers/femto.py +1 -1
  37. dodal/devices/detector/detector_motion.py +1 -7
  38. dodal/devices/eiger.py +22 -8
  39. dodal/devices/eiger_odin.py +2 -0
  40. dodal/devices/electron_analyser/__init__.py +2 -1
  41. dodal/devices/electron_analyser/abstract/__init__.py +0 -1
  42. dodal/devices/electron_analyser/abstract/base_detector.py +3 -25
  43. dodal/devices/electron_analyser/abstract/base_driver_io.py +18 -9
  44. dodal/devices/electron_analyser/abstract/base_region.py +34 -3
  45. dodal/devices/electron_analyser/detector.py +24 -0
  46. dodal/devices/electron_analyser/enums.py +5 -0
  47. dodal/devices/electron_analyser/specs/detector.py +2 -1
  48. dodal/devices/electron_analyser/specs/driver_io.py +21 -26
  49. dodal/devices/electron_analyser/specs/region.py +1 -1
  50. dodal/devices/electron_analyser/util.py +20 -0
  51. dodal/devices/electron_analyser/vgscienta/__init__.py +3 -3
  52. dodal/devices/electron_analyser/vgscienta/detector.py +2 -1
  53. dodal/devices/electron_analyser/vgscienta/driver_io.py +24 -32
  54. dodal/devices/electron_analyser/vgscienta/enums.py +0 -8
  55. dodal/devices/electron_analyser/vgscienta/region.py +2 -31
  56. dodal/devices/eurotherm.py +126 -0
  57. dodal/devices/fluorescence_detector_motion.py +3 -10
  58. dodal/devices/focusing_mirror.py +1 -1
  59. dodal/devices/i03/undulator_dcm.py +0 -1
  60. dodal/devices/i09/enums.py +8 -8
  61. dodal/devices/i10/diagnostics.py +4 -4
  62. dodal/devices/i10/i10_apple2.py +3 -6
  63. dodal/devices/i11/cyberstar_blower.py +34 -0
  64. dodal/devices/i11/diff_stages.py +55 -0
  65. dodal/devices/i11/mythen.py +165 -0
  66. dodal/devices/i11/nx100robot.py +153 -0
  67. dodal/devices/i11/spinner.py +30 -0
  68. dodal/devices/i13_1/merlin_controller.py +4 -4
  69. dodal/devices/i19/diffractometer.py +34 -0
  70. dodal/devices/i19/shutter.py +11 -1
  71. dodal/devices/i22/dcm.py +1 -1
  72. dodal/devices/i22/fswitch.py +3 -12
  73. dodal/devices/i24/aperture.py +3 -3
  74. dodal/devices/i24/beam_center.py +1 -2
  75. dodal/devices/i24/dcm.py +11 -15
  76. dodal/devices/i24/dual_backlight.py +11 -12
  77. dodal/devices/i24/pmac.py +8 -7
  78. dodal/devices/mx_phase1/beamstop.py +10 -11
  79. dodal/devices/oav/pin_image_recognition/__init__.py +0 -3
  80. dodal/devices/p60/enums.py +8 -8
  81. dodal/devices/p60/lab_xray_source.py +3 -2
  82. dodal/devices/pressure_jump_cell.py +77 -123
  83. dodal/devices/scintillator.py +76 -4
  84. dodal/devices/smargon.py +35 -18
  85. dodal/devices/synchrotron.py +1 -2
  86. dodal/devices/thawer.py +22 -15
  87. dodal/devices/undulator.py +3 -8
  88. dodal/devices/util/epics_util.py +1 -1
  89. dodal/devices/watsonmarlow323_pump.py +7 -7
  90. dodal/devices/webcam.py +1 -0
  91. dodal/devices/xbpm_feedback.py +4 -6
  92. dodal/devices/xspress3/xspress3.py +0 -5
  93. dodal/devices/zocalo/zocalo_results.py +1 -3
  94. dodal/testing/__init__.py +3 -0
  95. dodal/testing/electron_analyser/__init__.py +6 -0
  96. dodal/testing/electron_analyser/device_factory.py +59 -0
  97. dodal/testing/setup.py +67 -0
  98. dodal/devices/CTAB.py +0 -41
  99. dodal/devices/i24/pilatus_metadata.py +0 -44
  100. dodal/devices/util/test_utils.py +0 -37
  101. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/WHEEL +0 -0
  102. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/entry_points.txt +0 -0
  103. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/licenses/LICENSE +0 -0
  104. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,36 @@
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.motor import Motor
3
+
4
+
5
+ class CollimationTable(StandardReadable):
6
+ """Basic collimation table device for motion plus the motion disable signal
7
+ when laser curtain triggered and hutch not locked.
8
+
9
+ The table has 3 physical vertical motors, the jacks. 1 upstream and 2 downstream.
10
+ The two downstream jacks are labelled as outboard (away from the ring) and
11
+ inboard (towards the ring).
12
+ Together these 3 jacks provide compound motion for vertical motion and pitch/roll.
13
+ There are 2 physical horizontal motors 1 upstream, 1 downstream. These provide yaw.
14
+
15
+ Table motion is disabled by an object being within the laser curtain area and can be
16
+ overridden by use of the dead man's handle device or locking the hutch. The effect of
17
+ these disabling systems is to cut power to the motors - signal for this is crate_power
18
+ """
19
+
20
+ def __init__(self, prefix: str, name: str = ""):
21
+ with self.add_children_as_readables():
22
+ self.inboard_y = Motor(f"{prefix}:INBOARDY")
23
+ self.outboard_y = Motor(f"{prefix}:OUTBOARDY")
24
+ self.upstream_y = Motor(f"{prefix}:UPSTREAMY")
25
+ self.combined_downstream_y = Motor(f"{prefix}:DOWNSTREAMY")
26
+ self.combined_all_y = Motor(f"{prefix}:Y")
27
+
28
+ self.downstream_x = Motor(f"{prefix}:DOWNSTREAMX")
29
+ self.upstream_x = Motor(f"{prefix}:UPSTREAMX")
30
+ self.combined_all_x = Motor(f"{prefix}:X")
31
+
32
+ self.pitch = Motor(f"{prefix}:PITCH")
33
+ self.roll = Motor(f"{prefix}:ROLL")
34
+ self.yaw = Motor(f"{prefix}:YAW")
35
+
36
+ super().__init__(name)
@@ -0,0 +1,21 @@
1
+ from typing import TypeVar
2
+
3
+ from ophyd_async.epics.adcore import (
4
+ ADBaseController,
5
+ ADBaseIO,
6
+ )
7
+
8
+ ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
9
+
10
+
11
+ class ConstantDeadTimeController(ADBaseController[ADBaseIOT]):
12
+ """
13
+ ADBaseController with a configured constant deadtime for a driver of type ADBaseIO.
14
+ """
15
+
16
+ def __init__(self, driver: ADBaseIOT, deadtime: float):
17
+ super().__init__(driver)
18
+ self.deadtime = deadtime
19
+
20
+ def get_deadtime(self, exposure: float | None) -> float:
21
+ return self.deadtime
@@ -1,13 +1,21 @@
1
- from ophyd_async.core import StandardReadable, StrictEnum
2
- from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
3
-
4
-
5
- class InOut(StrictEnum):
6
- IN = "In"
7
- OUT = "Out"
1
+ from ophyd_async.core import (
2
+ EnabledDisabled,
3
+ InOut,
4
+ StandardReadable,
5
+ StandardReadableFormat,
6
+ StrictEnum,
7
+ )
8
+ from ophyd_async.epics.core import (
9
+ epics_signal_r,
10
+ epics_signal_rw,
11
+ epics_signal_x,
12
+ )
8
13
 
9
14
 
10
15
  class CryoStream(StandardReadable):
16
+ MAX_TEMP_K = 110
17
+ MAX_PRESSURE_BAR = 0.1
18
+
11
19
  def __init__(self, prefix: str, name: str = ""):
12
20
  self.course = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:COARSE:CTRL")
13
21
  self.fine = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:FINE:CTRL")
@@ -15,5 +23,87 @@ class CryoStream(StandardReadable):
15
23
  self.back_pressure_bar = epics_signal_r(
16
24
  float, f"{prefix}-EA-CSTRM-01:BACKPRESS"
17
25
  )
26
+ super().__init__(name)
27
+
28
+
29
+ class TurboEnum(StrictEnum):
30
+ OFF = "Off"
31
+ ON = "On"
32
+ AUTO = "Auto"
33
+
34
+
35
+ class OxfordCryoStreamController(StandardReadable):
36
+ def __init__(self, prefix: str, name: str = ""):
37
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
38
+ # Any signals that should be read once at the start of the scan
39
+ self.turbo = epics_signal_rw(str, f"{prefix}TURBO")
40
+ self.turbo_mode = epics_signal_rw(TurboEnum, f"{prefix}TURBOMODE")
41
+
42
+ self.serial_comms = epics_signal_rw(EnabledDisabled, f"{prefix}DISABLE")
43
+ self.status = epics_signal_r(str, f"{prefix}STATUS.SEVR")
44
+
45
+ with self.add_children_as_readables():
46
+ # Any signals that should be read at every point in the scan
47
+
48
+ self.purge = epics_signal_x(f"{prefix}PURGE.PROC")
49
+ self.hold = epics_signal_x(f"{prefix}HOLD.PROC")
50
+ self.start = epics_signal_x(f"{prefix}RESTART.PROC")
51
+ self.pause = epics_signal_x(f"{prefix}PAUSE.PROC")
52
+ self.resume = epics_signal_x(f"{prefix}RESUME.PROC")
53
+ self.end = epics_signal_x(f"{prefix}END.PROC")
54
+ self.stop = epics_signal_x(f"{prefix}STOP.PROC")
55
+
56
+ self.ramp_rate = epics_signal_rw(float, f"{prefix}RRATE")
57
+ self.ramp_temp = epics_signal_rw(float, f"{prefix}RTEMP")
58
+ self.ramp = epics_signal_x(f"{prefix}RAMP.PROC")
59
+
60
+ self.plat_time = epics_signal_rw(float, f"{prefix}PTIME")
61
+ self.plat = epics_signal_x(f"{prefix}PLAT.PROC")
62
+
63
+ self.cool_temp = epics_signal_rw(float, f"{prefix}CTEMP")
64
+ self.cool = epics_signal_x(f"{prefix}COOL.PROC")
65
+
66
+ self.end_rate = epics_signal_rw(float, f"{prefix}ERATE")
67
+
68
+ super().__init__(name)
69
+
70
+
71
+ class OxfordCryoStreamStatus(StandardReadable):
72
+ def __init__(self, prefix: str, name: str = ""):
73
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
74
+ # Any signals that should be read once at the start of the scan
75
+
76
+ self.pump_uptime = epics_signal_r(float, f"{prefix}RUNTIME")
77
+ self.controller_number = epics_signal_r(float, f"{prefix}CTRLNUM")
78
+ self.software_version = epics_signal_r(float, f"{prefix}VER")
79
+ self.evap_adjust = epics_signal_r(float, f"{prefix}EVAPADJUST")
80
+ self.series = epics_signal_r(str, f"{prefix}SERIES")
81
+
82
+ with self.add_children_as_readables():
83
+ # Any signals that should be read at every point in the scan
84
+ self.setpoint = epics_signal_r(float, f"{prefix}SETPOINT")
85
+ self.temp = epics_signal_r(float, f"{prefix}TEMP")
86
+ self.error = epics_signal_r(float, f"{prefix}ERROR")
87
+ self.mode = epics_signal_r(str, f"{prefix}RUNMODE")
88
+ self.phase = epics_signal_r(str, f"{prefix}PHASE")
89
+ self.ramp_rate_setpoint = epics_signal_r(float, f"{prefix}RAMPRATE")
90
+ self.target_temp = epics_signal_r(float, f"{prefix}TARGETTEMP")
91
+ self.evap_temp = epics_signal_r(float, f"{prefix}EVAPTEMP")
92
+ self.time_remaining = epics_signal_r(float, f"{prefix}REMAINING")
93
+ self.gas_flow = epics_signal_r(float, f"{prefix}GASFLOW")
94
+ self.gas_heat = epics_signal_r(float, f"{prefix}GASHEAT")
95
+ self.evap_heat = epics_signal_r(float, f"{prefix}EVAPHEAT")
96
+ self.suct_temp = epics_signal_r(float, f"{prefix}SUCTTEMP")
97
+ self.suct_heat = epics_signal_r(float, f"{prefix}SUCTHEAT")
98
+ self.back_pressure = epics_signal_r(float, f"{prefix}BACKPRESS")
99
+
100
+ super().__init__(name)
101
+
102
+
103
+ class OxfordCryoStream(StandardReadable):
104
+ def __init__(self, prefix: str, name=""):
105
+ with self.add_children_as_readables():
106
+ self.controller = OxfordCryoStreamController(prefix=prefix)
107
+ self.status = OxfordCryoStreamStatus(prefix=prefix)
18
108
 
19
109
  super().__init__(name)
@@ -108,7 +108,7 @@ class FemtoDDPCA(CurrentAmp):
108
108
  LOGGER.info(f"{self.name} gain change to {SEN_setting}:{value}")
109
109
 
110
110
  await self.gain.set(
111
- value=self.gain_table[SEN_setting].value,
111
+ value=self.gain_table[SEN_setting],
112
112
  timeout=self.timeout,
113
113
  )
114
114
  # wait for current amplifier's bandpass filter to settle.
@@ -11,13 +11,7 @@ class ShutterState(StrictEnum):
11
11
 
12
12
 
13
13
  class DetectorMotion(XYZStage):
14
- _device_prefix = "-MO-DET-01:"
15
- _pmac_prefix = "-MO-PMAC-02:"
16
-
17
- def __init__(self, prefix: str, name: str = ""):
18
- device_prefix = f"{prefix}{self._device_prefix}"
19
- pmac_prefix = f"{prefix}{self._pmac_prefix}"
20
-
14
+ def __init__(self, device_prefix: str, pmac_prefix: str, name: str = ""):
21
15
  self.upstream_x = Motor(f"{device_prefix}UPSTREAMX")
22
16
  self.downstream_x = Motor(f"{device_prefix}DOWNSTREAMX")
23
17
  self.yaw = Motor(f"{device_prefix}YAW")
dodal/devices/eiger.py CHANGED
@@ -219,6 +219,7 @@ class EigerDetector(Device, Stageable):
219
219
  return status
220
220
 
221
221
  def set_cam_pvs(self) -> AndStatus:
222
+ LOGGER.info("Eiger arming: Setting eiger camera PVs...")
222
223
  assert self.detector_params is not None
223
224
  status = self.cam.acquire_time.set(
224
225
  self.detector_params.exposure_time_s,
@@ -241,6 +242,7 @@ class EigerDetector(Device, Stageable):
241
242
  return status
242
243
 
243
244
  def set_odin_number_of_frame_chunks(self) -> Status:
245
+ LOGGER.info("Eiger arming: Setting odin number of frames chunks...")
244
246
  assert self.detector_params is not None
245
247
  status = self.odin.file_writer.num_frames_chunks.set(
246
248
  1, timeout=self.timeouts.general_status_timeout
@@ -248,6 +250,7 @@ class EigerDetector(Device, Stageable):
248
250
  return status
249
251
 
250
252
  def set_odin_pvs(self) -> StatusBase:
253
+ LOGGER.info("Eiger arming: Setting odin PVs...")
251
254
  assert self.detector_params is not None
252
255
  file_prefix = self.detector_params.full_filename
253
256
  status = self.odin.file_writer.file_path.set(
@@ -269,6 +272,7 @@ class EigerDetector(Device, Stageable):
269
272
  return status
270
273
 
271
274
  def set_mx_settings_pvs(self):
275
+ LOGGER.info("Eiger arming: Setting mx setting PVs...")
272
276
  assert self.detector_params is not None
273
277
  beam_x_pixels, beam_y_pixels = self.detector_params.get_beam_position_pixels(
274
278
  self.detector_params.detector_distance
@@ -304,10 +308,14 @@ class EigerDetector(Device, Stageable):
304
308
 
305
309
  current_energy = self.cam.photon_energy.get()
306
310
  if abs(current_energy - energy) > tolerance:
311
+ LOGGER.info(f"Setting detector threshold to {energy}")
307
312
  return self.cam.photon_energy.set(
308
313
  energy, timeout=self.timeouts.general_status_timeout
309
314
  )
310
315
  else:
316
+ LOGGER.info(
317
+ f"Not setting detector threshold as already close to {current_energy}"
318
+ )
311
319
  status = Status()
312
320
  status.set_finished()
313
321
  return status
@@ -317,7 +325,7 @@ class EigerDetector(Device, Stageable):
317
325
  during the datacollection. The number of images is the number of images per
318
326
  trigger.
319
327
  """
320
-
328
+ LOGGER.info("Eiger arming: setting num triggers and captures...")
321
329
  assert self.detector_params is not None
322
330
  status = self.cam.num_images.set(
323
331
  self.detector_params.num_images_per_trigger,
@@ -351,7 +359,7 @@ class EigerDetector(Device, Stageable):
351
359
  status = self.odin.file_writer.capture.set(
352
360
  1, timeout=self.timeouts.general_status_timeout
353
361
  )
354
- LOGGER.info("Eiger staging: awaiting odin metadata")
362
+ LOGGER.info("Eiger arming: awaiting odin metadata")
355
363
  status &= await_value(
356
364
  self.odin.meta.ready, 1, timeout=self.timeouts.meta_file_ready_timeout
357
365
  )
@@ -359,11 +367,11 @@ class EigerDetector(Device, Stageable):
359
367
 
360
368
  def _wait_fan_ready(self) -> StatusBase:
361
369
  self.filewriters_finished = self.odin.create_finished_status()
362
- LOGGER.info("Eiger staging: awaiting odin fan ready")
370
+ LOGGER.info("Eiger arming: awaiting odin fan ready")
363
371
  return await_value(self.odin.fan.ready, 1, self.timeouts.general_status_timeout)
364
372
 
365
373
  def _finish_arm(self) -> Status:
366
- LOGGER.info("Eiger staging: Finishing arming")
374
+ LOGGER.info("Eiger arming: Finishing arming")
367
375
  status = Status()
368
376
  status.set_finished()
369
377
  return status
@@ -381,6 +389,14 @@ class EigerDetector(Device, Stageable):
381
389
  def disarm_detector(self):
382
390
  self.cam.acquire.set(0).wait(self.timeouts.general_status_timeout)
383
391
 
392
+ def wait_for_stale_params(self) -> Status:
393
+ LOGGER.info("Eiger arming: Waiting for stale params...")
394
+ return await_value(self.stale_params, 0, 60)
395
+
396
+ def set_cam_acquire(self) -> Status:
397
+ LOGGER.info("Eiger arming: Setting cam acquire...")
398
+ return self.cam.acquire.set(1, timeout=self.timeouts.general_status_timeout)
399
+
384
400
  def do_arming_chain(self) -> Status:
385
401
  functions_to_do_arm = []
386
402
  assert self.detector_params
@@ -400,11 +416,9 @@ class EigerDetector(Device, Stageable):
400
416
  self.set_odin_pvs,
401
417
  self.set_mx_settings_pvs,
402
418
  self.set_num_triggers_and_captures,
403
- lambda: await_value(self.stale_params, 0, 60),
419
+ self.wait_for_stale_params,
404
420
  self._wait_for_odin_status,
405
- lambda: self.cam.acquire.set(
406
- 1, timeout=self.timeouts.general_status_timeout
407
- ),
421
+ self.set_cam_acquire,
408
422
  self._wait_fan_ready,
409
423
  self._finish_arm,
410
424
  ]
@@ -7,6 +7,7 @@ from ophyd.sim import NullStatus
7
7
  from ophyd.status import StatusBase, SubscriptionStatus
8
8
 
9
9
  from dodal.devices.status import await_value
10
+ from dodal.log import LOGGER
10
11
 
11
12
 
12
13
  class EigerFan(Device):
@@ -167,6 +168,7 @@ class EigerOdin(Device):
167
168
 
168
169
  def stop(self) -> StatusBase:
169
170
  """Stop odin manually"""
171
+ LOGGER.info("Stopping Odin...")
170
172
  status = self.file_writer.capture.set(0)
171
173
  status &= self.meta.stop_writing.set(1)
172
174
  return status
@@ -4,7 +4,7 @@ from .detector import (
4
4
  TElectronAnalyserDetector,
5
5
  TElectronAnalyserRegionDetector,
6
6
  )
7
- from .enums import EnergyMode
7
+ from .enums import EnergyMode, SelectedSource
8
8
  from .types import (
9
9
  ElectronAnalyserDetectorImpl,
10
10
  ElectronAnalyserDriverImpl,
@@ -17,6 +17,7 @@ __all__ = [
17
17
  "to_binding_energy",
18
18
  "to_kinetic_energy",
19
19
  "EnergyMode",
20
+ "SelectedSource",
20
21
  "ElectronAnalyserDetector",
21
22
  "ElectronAnalyserDetectorImpl",
22
23
  "ElectronAnalyserDriverImpl",
@@ -1,5 +1,4 @@
1
1
  from .base_detector import (
2
- AbstractAnalyserDriverIO,
3
2
  AbstractElectronAnalyserDetector,
4
3
  )
5
4
  from .base_driver_io import AbstractAnalyserDriverIO, TAbstractAnalyserDriverIO
@@ -1,8 +1,7 @@
1
- import asyncio
2
1
  from abc import abstractmethod
3
2
  from typing import Generic
4
3
 
5
- from bluesky.protocols import Reading, Stageable, Triggerable
4
+ from bluesky.protocols import Reading, Triggerable
6
5
  from event_model import DataKey
7
6
  from ophyd_async.core import (
8
7
  AsyncConfigurable,
@@ -10,24 +9,15 @@ from ophyd_async.core import (
10
9
  AsyncStatus,
11
10
  Device,
12
11
  )
13
- from ophyd_async.epics.adcore import (
14
- ADBaseController,
15
- )
16
12
 
13
+ from dodal.devices.controllers import ConstantDeadTimeController
17
14
  from dodal.devices.electron_analyser.abstract.base_driver_io import (
18
- AbstractAnalyserDriverIO,
19
15
  TAbstractAnalyserDriverIO,
20
16
  )
21
17
 
22
18
 
23
- class ElectronAnalyserController(ADBaseController[AbstractAnalyserDriverIO]):
24
- def get_deadtime(self, exposure: float | None) -> float:
25
- return 0
26
-
27
-
28
19
  class AbstractElectronAnalyserDetector(
29
20
  Device,
30
- Stageable,
31
21
  Triggerable,
32
22
  AsyncReadable,
33
23
  AsyncConfigurable,
@@ -47,9 +37,7 @@ class AbstractElectronAnalyserDetector(
47
37
  driver: TAbstractAnalyserDriverIO,
48
38
  name: str = "",
49
39
  ):
50
- self.controller: ElectronAnalyserController = ElectronAnalyserController(
51
- driver=driver
52
- )
40
+ self.controller = ConstantDeadTimeController(driver, 0)
53
41
  super().__init__(name)
54
42
 
55
43
  @AsyncStatus.wrap
@@ -57,16 +45,6 @@ class AbstractElectronAnalyserDetector(
57
45
  await self.controller.arm()
58
46
  await self.controller.wait_for_idle()
59
47
 
60
- @AsyncStatus.wrap
61
- async def stage(self) -> None:
62
- """Make sure the detector is idle and ready to be used."""
63
- await asyncio.gather(self.controller.disarm())
64
-
65
- @AsyncStatus.wrap
66
- async def unstage(self) -> None:
67
- """Disarm the detector."""
68
- await asyncio.gather(self.controller.disarm())
69
-
70
48
  async def read(self) -> dict[str, Reading]:
71
49
  return await self.driver.read()
72
50
 
@@ -13,7 +13,7 @@ from ophyd_async.core import (
13
13
  derived_signal_r,
14
14
  soft_signal_rw,
15
15
  )
16
- from ophyd_async.epics.adcore import ADBaseIO
16
+ from ophyd_async.epics.adcore import ADBaseIO, ADImageMode
17
17
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
18
18
 
19
19
  from dodal.devices.electron_analyser.abstract.base_region import (
@@ -25,7 +25,7 @@ from dodal.devices.electron_analyser.abstract.types import (
25
25
  TPassEnergy,
26
26
  TPsuMode,
27
27
  )
28
- from dodal.devices.electron_analyser.enums import EnergyMode
28
+ from dodal.devices.electron_analyser.enums import EnergyMode, SelectedSource
29
29
  from dodal.devices.electron_analyser.util import to_binding_energy
30
30
 
31
31
 
@@ -49,7 +49,7 @@ class AbstractAnalyserDriverIO(
49
49
  lens_mode_type: type[TLensMode],
50
50
  psu_mode_type: type[TPsuMode],
51
51
  pass_energy_type: type[TPassEnergy],
52
- energy_sources: Mapping[str, SignalR[float]],
52
+ energy_sources: Mapping[SelectedSource, SignalR[float]],
53
53
  name: str = "",
54
54
  ) -> None:
55
55
  """
@@ -75,6 +75,9 @@ class AbstractAnalyserDriverIO(
75
75
  self.psu_mode_type = psu_mode_type
76
76
  self.pass_energy_type = pass_energy_type
77
77
 
78
+ # must call first to initiate parent variables
79
+ super().__init__(prefix=prefix, name=name)
80
+
78
81
  with self.add_children_as_readables():
79
82
  self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
80
83
  self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
@@ -84,7 +87,8 @@ class AbstractAnalyserDriverIO(
84
87
  self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV")
85
88
 
86
89
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
87
- # Used for setting up region data acquisition.
90
+ # Read once per scan after data acquired
91
+ # Used for setting up region data acquisition
88
92
  self.region_name = soft_signal_rw(str, initial_value="null")
89
93
  self.energy_mode = soft_signal_rw(
90
94
  EnergyMode, initial_value=EnergyMode.KINETIC
@@ -105,8 +109,11 @@ class AbstractAnalyserDriverIO(
105
109
  # analyser type to know if is moved with region settings.
106
110
  self.psu_mode = epics_signal_rw(psu_mode_type, prefix + "PSU_MODE")
107
111
 
112
+ # This is defined in the parent class, add it as readable configuration.
113
+ self.add_readables([self.acquire_time], StandardReadableFormat.CONFIG_SIGNAL)
114
+
108
115
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
109
- # Read once per scan after data acquired
116
+ # NOT used for setting up region data acquisition.
110
117
  self.energy_axis = self._create_energy_axis_signal(prefix)
111
118
  self.binding_energy_axis = derived_signal_r(
112
119
  self._calculate_binding_energy_axis,
@@ -116,17 +123,19 @@ class AbstractAnalyserDriverIO(
116
123
  energy_mode=self.energy_mode,
117
124
  )
118
125
  self.angle_axis = self._create_angle_axis_signal(prefix)
119
- self.step_time = epics_signal_r(float, prefix + "AcquireTime")
120
126
  self.total_steps = epics_signal_r(int, prefix + "TOTAL_POINTS_RBV")
121
127
  self.total_time = derived_signal_r(
122
128
  self._calculate_total_time,
123
129
  "s",
124
130
  total_steps=self.total_steps,
125
- step_time=self.step_time,
131
+ step_time=self.acquire_time,
126
132
  iterations=self.iterations,
127
133
  )
128
134
 
129
- super().__init__(prefix=prefix, name=name)
135
+ @AsyncStatus.wrap
136
+ async def stage(self) -> None:
137
+ await self.image_mode.set(ADImageMode.SINGLE)
138
+ await super().stage()
130
139
 
131
140
  @abstractmethod
132
141
  @AsyncStatus.wrap
@@ -139,7 +148,7 @@ class AbstractAnalyserDriverIO(
139
148
  region: Contains the parameters to setup the driver for a scan.
140
149
  """
141
150
 
142
- def _get_energy_source(self, alias_name: str) -> SignalR[float]:
151
+ def _get_energy_source(self, alias_name: SelectedSource) -> SignalR[float]:
143
152
  energy_source = self.energy_sources.get(alias_name)
144
153
  if energy_source is None:
145
154
  raise KeyError(
@@ -10,7 +10,8 @@ from dodal.devices.electron_analyser.abstract.types import (
10
10
  TLensMode,
11
11
  TPassEnergy,
12
12
  )
13
- from dodal.devices.electron_analyser.enums import EnergyMode
13
+ from dodal.devices.electron_analyser.enums import EnergyMode, SelectedSource
14
+ from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
14
15
 
15
16
 
16
17
  def java_to_python_case(java_str: str) -> str:
@@ -62,7 +63,7 @@ class AbstractBaseRegion(
62
63
  enabled: bool = False
63
64
  slices: int = 1
64
65
  iterations: int = 1
65
- excitation_energy_source: str = "source1"
66
+ excitation_energy_source: SelectedSource = SelectedSource.SOURCE1
66
67
  # These ones we need subclasses to provide default values
67
68
  lens_mode: TLensMode
68
69
  pass_energy: TPassEnergy
@@ -70,16 +71,46 @@ class AbstractBaseRegion(
70
71
  low_energy: float
71
72
  centre_energy: float
72
73
  high_energy: float
73
- step_time: float
74
+ acquire_time: float
74
75
  energy_step: float # in eV
75
76
  energy_mode: EnergyMode = EnergyMode.KINETIC
76
77
 
77
78
  def is_binding_energy(self) -> bool:
79
+ """
80
+ Returns true if the energy_mode is binding.
81
+ """
78
82
  return self.energy_mode == EnergyMode.BINDING
79
83
 
80
84
  def is_kinetic_energy(self) -> bool:
85
+ """
86
+ Returns true if the energy_mode is kinetic.
87
+ """
81
88
  return self.energy_mode == EnergyMode.KINETIC
82
89
 
90
+ def switch_energy_mode(
91
+ self, energy_mode: EnergyMode, excitation_energy: float
92
+ ) -> None:
93
+ """
94
+ Switch region to new energy mode: Kinetic or Binding. Updates the low_energy,
95
+ centre_energy, high_energy, and energy_mode, only if it switches to a new one.
96
+
97
+ Parameters:
98
+ energy_mode: mode you want to switch the region to.
99
+ excitation_energy: the energy to calculate the new values of low_energy,
100
+ centre_energy, and high_energy.
101
+ """
102
+ conv = (
103
+ to_binding_energy
104
+ if energy_mode == EnergyMode.BINDING
105
+ else to_kinetic_energy
106
+ )
107
+ self.low_energy = conv(self.low_energy, self.energy_mode, excitation_energy)
108
+ self.centre_energy = conv(
109
+ self.centre_energy, self.energy_mode, excitation_energy
110
+ )
111
+ self.high_energy = conv(self.high_energy, self.energy_mode, excitation_energy)
112
+ self.energy_mode = energy_mode
113
+
83
114
  @model_validator(mode="before")
84
115
  @classmethod
85
116
  def before_validation(cls, data: dict) -> dict:
@@ -1,5 +1,6 @@
1
1
  from typing import Generic, TypeVar
2
2
 
3
+ from bluesky.protocols import Stageable
3
4
  from ophyd_async.core import (
4
5
  AsyncStatus,
5
6
  Reference,
@@ -59,6 +60,7 @@ TElectronAnalyserRegionDetector = TypeVar(
59
60
 
60
61
  class ElectronAnalyserDetector(
61
62
  AbstractElectronAnalyserDetector[TAbstractAnalyserDriverIO],
63
+ Stageable,
62
64
  Generic[
63
65
  TAbstractAnalyserDriverIO,
64
66
  TAbstractBaseSequence,
@@ -88,6 +90,28 @@ class ElectronAnalyserDetector(
88
90
  # can be used with connect() method.
89
91
  return self._driver
90
92
 
93
+ @AsyncStatus.wrap
94
+ async def stage(self) -> None:
95
+ """
96
+ Prepare the detector for use by ensuring it is idle and ready.
97
+
98
+ This method asynchronously stages the detector by first disarming the controller
99
+ to ensure the detector is not actively acquiring data, then invokes the driver's
100
+ stage procedure. This ensures the detector is in a known, ready state
101
+ before use.
102
+
103
+ Raises:
104
+ Any exceptions raised by the driver's stage or controller's disarm methods.
105
+ """
106
+ await self.controller.disarm()
107
+ await self.driver.stage()
108
+
109
+ @AsyncStatus.wrap
110
+ async def unstage(self) -> None:
111
+ """Disarm the detector."""
112
+ await self.controller.disarm()
113
+ await self.driver.unstage()
114
+
91
115
  def load_sequence(self, filename: str) -> TAbstractBaseSequence:
92
116
  """
93
117
  Load the sequence data from a provided json file into a sequence class.
@@ -4,3 +4,8 @@ from ophyd_async.core import StrictEnum
4
4
  class EnergyMode(StrictEnum):
5
5
  KINETIC = "Kinetic"
6
6
  BINDING = "Binding"
7
+
8
+
9
+ class SelectedSource(StrictEnum):
10
+ SOURCE1 = "source1"
11
+ SOURCE2 = "source2"
@@ -7,6 +7,7 @@ from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
7
7
  from dodal.devices.electron_analyser.detector import (
8
8
  ElectronAnalyserDetector,
9
9
  )
10
+ from dodal.devices.electron_analyser.enums import SelectedSource
10
11
  from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
11
12
  from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
12
13
 
@@ -24,7 +25,7 @@ class SpecsDetector(
24
25
  prefix: str,
25
26
  lens_mode_type: type[TLensMode],
26
27
  psu_mode_type: type[TPsuMode],
27
- energy_sources: Mapping[str, SignalR[float]],
28
+ energy_sources: Mapping[SelectedSource, SignalR[float]],
28
29
  name: str = "",
29
30
  ):
30
31
  driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](