dls-dodal 1.55.0__py3-none-any.whl → 1.56.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.
- {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/METADATA +3 -3
- {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/RECORD +100 -86
- dodal/_version.py +2 -2
- dodal/beamlines/b01_1.py +6 -13
- dodal/beamlines/b07.py +2 -1
- dodal/beamlines/b07_1.py +2 -1
- dodal/beamlines/b21.py +4 -24
- dodal/beamlines/i03.py +53 -53
- dodal/beamlines/i04.py +16 -38
- dodal/beamlines/i09.py +3 -2
- dodal/beamlines/i09_1.py +2 -1
- dodal/beamlines/i11.py +143 -0
- dodal/beamlines/i19_1.py +1 -0
- dodal/beamlines/i19_2.py +7 -0
- dodal/beamlines/i22.py +2 -2
- dodal/beamlines/i23.py +3 -3
- dodal/beamlines/i24.py +6 -14
- dodal/beamlines/p38.py +1 -0
- dodal/beamlines/p60.py +3 -2
- dodal/cli.py +11 -1
- dodal/common/__init__.py +4 -0
- dodal/common/beamlines/beamline_parameters.py +1 -1
- dodal/common/beamlines/beamline_utils.py +5 -1
- dodal/common/enums.py +19 -0
- dodal/common/watcher_utils.py +83 -0
- dodal/devices/aithre_lasershaping/laser_robot.py +4 -9
- dodal/devices/aperturescatterguard.py +52 -12
- dodal/devices/apple2_undulator.py +0 -1
- dodal/devices/attenuator/attenuator.py +3 -1
- dodal/devices/b16/detector.py +1 -10
- dodal/devices/backlight.py +8 -20
- dodal/devices/bimorph_mirror.py +4 -7
- dodal/devices/collimation_table.py +36 -0
- dodal/devices/controllers.py +21 -0
- dodal/devices/cryostream.py +97 -7
- dodal/devices/current_amplifiers/femto.py +1 -1
- dodal/devices/detector/detector_motion.py +1 -7
- dodal/devices/eiger.py +22 -8
- dodal/devices/eiger_odin.py +2 -0
- dodal/devices/electron_analyser/__init__.py +2 -1
- dodal/devices/electron_analyser/abstract/__init__.py +0 -1
- dodal/devices/electron_analyser/abstract/base_detector.py +3 -25
- dodal/devices/electron_analyser/abstract/base_driver_io.py +18 -9
- dodal/devices/electron_analyser/abstract/base_region.py +34 -3
- dodal/devices/electron_analyser/detector.py +24 -0
- dodal/devices/electron_analyser/enums.py +5 -0
- dodal/devices/electron_analyser/specs/detector.py +2 -1
- dodal/devices/electron_analyser/specs/driver_io.py +21 -26
- dodal/devices/electron_analyser/specs/region.py +1 -1
- dodal/devices/electron_analyser/util.py +20 -0
- dodal/devices/electron_analyser/vgscienta/__init__.py +3 -3
- dodal/devices/electron_analyser/vgscienta/detector.py +2 -1
- dodal/devices/electron_analyser/vgscienta/driver_io.py +24 -32
- dodal/devices/electron_analyser/vgscienta/enums.py +0 -8
- dodal/devices/electron_analyser/vgscienta/region.py +2 -31
- dodal/devices/eurotherm.py +126 -0
- dodal/devices/fluorescence_detector_motion.py +3 -10
- dodal/devices/focusing_mirror.py +1 -1
- dodal/devices/i03/undulator_dcm.py +0 -1
- dodal/devices/i09/enums.py +8 -8
- dodal/devices/i10/diagnostics.py +4 -4
- dodal/devices/i10/i10_apple2.py +3 -6
- dodal/devices/i11/cyberstar_blower.py +34 -0
- dodal/devices/i11/diff_stages.py +55 -0
- dodal/devices/i11/mythen.py +165 -0
- dodal/devices/i11/nx100robot.py +153 -0
- dodal/devices/i11/spinner.py +30 -0
- dodal/devices/i13_1/merlin_controller.py +4 -4
- dodal/devices/i19/diffractometer.py +34 -0
- dodal/devices/i19/shutter.py +11 -1
- dodal/devices/i22/dcm.py +1 -1
- dodal/devices/i22/fswitch.py +3 -12
- dodal/devices/i24/aperture.py +3 -3
- dodal/devices/i24/dcm.py +11 -15
- dodal/devices/i24/dual_backlight.py +11 -12
- dodal/devices/i24/pmac.py +8 -7
- dodal/devices/mx_phase1/beamstop.py +10 -11
- dodal/devices/oav/pin_image_recognition/__init__.py +0 -3
- dodal/devices/p60/enums.py +8 -8
- dodal/devices/p60/lab_xray_source.py +3 -2
- dodal/devices/pressure_jump_cell.py +77 -123
- dodal/devices/scintillator.py +76 -4
- dodal/devices/smargon.py +2 -2
- dodal/devices/synchrotron.py +1 -2
- dodal/devices/tetramm.py +13 -0
- dodal/devices/thawer.py +6 -11
- dodal/devices/undulator.py +3 -8
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/util/test_utils.py +19 -0
- dodal/devices/watsonmarlow323_pump.py +7 -7
- dodal/devices/xbpm_feedback.py +4 -6
- dodal/devices/xspress3/xspress3.py +0 -5
- dodal/devices/zocalo/zocalo_results.py +1 -3
- dodal/testing/__init__.py +0 -0
- dodal/testing/electron_analyser/__init__.py +6 -0
- dodal/testing/electron_analyser/device_factory.py +59 -0
- dodal/devices/CTAB.py +0 -41
- {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/top_level.txt +0 -0
dodal/devices/cryostream.py
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
from ophyd_async.core import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
419
|
+
self.wait_for_stale_params,
|
|
404
420
|
self._wait_for_odin_status,
|
|
405
|
-
|
|
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
|
]
|
dodal/devices/eiger_odin.py
CHANGED
|
@@ -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,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,
|
|
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
|
|
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[
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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.
|
|
131
|
+
step_time=self.acquire_time,
|
|
126
132
|
iterations=self.iterations,
|
|
127
133
|
)
|
|
128
134
|
|
|
129
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
@@ -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[
|
|
28
|
+
energy_sources: Mapping[SelectedSource, SignalR[float]],
|
|
28
29
|
name: str = "",
|
|
29
30
|
):
|
|
30
31
|
driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](
|
|
@@ -16,9 +16,9 @@ from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
|
16
16
|
AbstractAnalyserDriverIO,
|
|
17
17
|
)
|
|
18
18
|
from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
|
|
19
|
+
from dodal.devices.electron_analyser.enums import EnergyMode, SelectedSource
|
|
19
20
|
from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
|
|
20
21
|
from dodal.devices.electron_analyser.specs.region import SpecsRegion
|
|
21
|
-
from dodal.devices.electron_analyser.util import to_kinetic_energy
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class SpecsAnalyserDriverIO(
|
|
@@ -36,7 +36,7 @@ class SpecsAnalyserDriverIO(
|
|
|
36
36
|
prefix: str,
|
|
37
37
|
lens_mode_type: type[TLensMode],
|
|
38
38
|
psu_mode_type: type[TPsuMode],
|
|
39
|
-
energy_sources: Mapping[
|
|
39
|
+
energy_sources: Mapping[SelectedSource, SignalR[float]],
|
|
40
40
|
name: str = "",
|
|
41
41
|
) -> None:
|
|
42
42
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
@@ -66,36 +66,31 @@ class SpecsAnalyserDriverIO(
|
|
|
66
66
|
async def set(self, region: SpecsRegion[TLensMode, TPsuMode]):
|
|
67
67
|
source = self._get_energy_source(region.excitation_energy_source)
|
|
68
68
|
excitation_energy = await source.get_value() # eV
|
|
69
|
+
# Copy region so doesn't alter the actual region and switch to kinetic energy
|
|
70
|
+
ke_region = region.model_copy()
|
|
71
|
+
ke_region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
|
|
69
72
|
|
|
70
|
-
low_energy = to_kinetic_energy(
|
|
71
|
-
region.low_energy, region.energy_mode, excitation_energy
|
|
72
|
-
)
|
|
73
|
-
centre_energy = to_kinetic_energy(
|
|
74
|
-
region.centre_energy, region.energy_mode, excitation_energy
|
|
75
|
-
)
|
|
76
|
-
high_energy = to_kinetic_energy(
|
|
77
|
-
region.high_energy, region.energy_mode, excitation_energy
|
|
78
|
-
)
|
|
79
73
|
await asyncio.gather(
|
|
80
|
-
self.region_name.set(
|
|
81
|
-
self.energy_mode.set(
|
|
82
|
-
self.low_energy.set(low_energy),
|
|
83
|
-
self.high_energy.set(high_energy),
|
|
84
|
-
self.slices.set(
|
|
85
|
-
self.
|
|
86
|
-
self.
|
|
87
|
-
self.
|
|
88
|
-
self.
|
|
74
|
+
self.region_name.set(ke_region.name),
|
|
75
|
+
self.energy_mode.set(ke_region.energy_mode),
|
|
76
|
+
self.low_energy.set(ke_region.low_energy),
|
|
77
|
+
self.high_energy.set(ke_region.high_energy),
|
|
78
|
+
self.slices.set(ke_region.slices),
|
|
79
|
+
self.acquire_time.set(ke_region.acquire_time),
|
|
80
|
+
self.lens_mode.set(ke_region.lens_mode),
|
|
81
|
+
self.pass_energy.set(ke_region.pass_energy),
|
|
82
|
+
self.iterations.set(ke_region.iterations),
|
|
83
|
+
self.acquisition_mode.set(ke_region.acquisition_mode),
|
|
89
84
|
self.excitation_energy.set(excitation_energy),
|
|
90
85
|
self.excitation_energy_source.set(source.name),
|
|
91
|
-
self.snapshot_values.set(
|
|
92
|
-
self.psu_mode.set(
|
|
86
|
+
self.snapshot_values.set(ke_region.values),
|
|
87
|
+
self.psu_mode.set(ke_region.psu_mode),
|
|
93
88
|
)
|
|
94
|
-
if
|
|
95
|
-
await self.energy_step.set(
|
|
89
|
+
if ke_region.acquisition_mode == AcquisitionMode.FIXED_TRANSMISSION:
|
|
90
|
+
await self.energy_step.set(ke_region.energy_step)
|
|
96
91
|
|
|
97
|
-
if
|
|
98
|
-
await self.centre_energy.set(centre_energy)
|
|
92
|
+
if ke_region.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
|
|
93
|
+
await self.centre_energy.set(ke_region.centre_energy)
|
|
99
94
|
|
|
100
95
|
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
101
96
|
angle_axis = derived_signal_r(
|
|
@@ -21,7 +21,7 @@ class SpecsRegion(
|
|
|
21
21
|
low_energy: float = Field(default=800, alias="start_energy")
|
|
22
22
|
centre_energy: float = 0
|
|
23
23
|
high_energy: float = Field(default=850, alias="end_energy")
|
|
24
|
-
|
|
24
|
+
acquire_time: float = Field(default=1.0, alias="exposure_time")
|
|
25
25
|
energy_step: float = Field(default=0.1, alias="step_energy")
|
|
26
26
|
|
|
27
27
|
# Specific to this class
|