dls-dodal 1.55.1__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.1.dist-info → dls_dodal-1.56.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/RECORD +97 -83
- dodal/_version.py +16 -3
- dodal/beamlines/b01_1.py +6 -1
- 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/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/thawer.py +6 -11
- dodal/devices/undulator.py +3 -8
- dodal/devices/util/epics_util.py +1 -1
- 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.1.dist-info → dls_dodal-1.56.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Annotated as A
|
|
3
|
+
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
DetectorTrigger,
|
|
6
|
+
PathProvider,
|
|
7
|
+
SignalRW,
|
|
8
|
+
StrictEnum,
|
|
9
|
+
TriggerInfo,
|
|
10
|
+
)
|
|
11
|
+
from ophyd_async.epics.adcore import (
|
|
12
|
+
ADBaseController,
|
|
13
|
+
ADHDFWriter,
|
|
14
|
+
ADImageMode,
|
|
15
|
+
ADWriter,
|
|
16
|
+
AreaDetector,
|
|
17
|
+
NDArrayBaseIO,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.epics.core import PvSuffix
|
|
20
|
+
|
|
21
|
+
from dodal.common.beamlines.device_helpers import DET_SUFFIX
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Mythen3TriggerMode(StrictEnum):
|
|
25
|
+
INTERNAL = "Internal"
|
|
26
|
+
EXTERNAL = "External"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Mythen3ModeSetting(StrictEnum):
|
|
30
|
+
STANDARD = "standard"
|
|
31
|
+
FAST = "fast"
|
|
32
|
+
HIGHGAIN = "highgain"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Mythen3DetectorState(StrictEnum):
|
|
36
|
+
"""Default set of states of an Mythen3 driver."""
|
|
37
|
+
|
|
38
|
+
IDLE = "Idle"
|
|
39
|
+
ERROR = "Error"
|
|
40
|
+
WAITING = "Waiting"
|
|
41
|
+
FINISHED = "Finished"
|
|
42
|
+
TRASMITTING = "Transmitting"
|
|
43
|
+
RUNNING = "Running"
|
|
44
|
+
STOPPED = "Stopped"
|
|
45
|
+
INITIALIZING = "Initializing"
|
|
46
|
+
DISCONNECTED = "Disconnected"
|
|
47
|
+
ABORTED = "Aborted"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Mythen3Driver(NDArrayBaseIO):
|
|
51
|
+
acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")]
|
|
52
|
+
acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")]
|
|
53
|
+
num_images: A[SignalRW[int], PvSuffix.rbv("NumImages")]
|
|
54
|
+
image_mode: A[SignalRW[ADImageMode], PvSuffix.rbv("ImageMode")]
|
|
55
|
+
|
|
56
|
+
# Non-specific PV's but with mythen3 specific values
|
|
57
|
+
detector_state: A[SignalRW[Mythen3DetectorState], PvSuffix("DetectorState_RBV")]
|
|
58
|
+
trigger_mode: A[SignalRW[Mythen3TriggerMode], PvSuffix.rbv("TriggerMode")]
|
|
59
|
+
|
|
60
|
+
# mythen3 specific PV's
|
|
61
|
+
mode_setting: A[SignalRW[Mythen3ModeSetting], PvSuffix.rbv("Setting")]
|
|
62
|
+
bit_depth: A[SignalRW[int], PvSuffix.rbv("BitDepth")]
|
|
63
|
+
beam_energy: A[SignalRW[int], PvSuffix.rbv("BeamEnergy")]
|
|
64
|
+
threshold: A[SignalRW[int], PvSuffix.rbv("Counter1Threshold")]
|
|
65
|
+
threshold2: A[SignalRW[int], PvSuffix.rbv("Counter2Threshold")]
|
|
66
|
+
threshold3: A[SignalRW[int], PvSuffix.rbv("Counter3Threshold")]
|
|
67
|
+
global_threshold: A[SignalRW[int], PvSuffix.rbv("ThresholdEnergy")]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
Re. _DEADTIMES
|
|
72
|
+
|
|
73
|
+
see: https://doi.org/10.1107/S1600577525000438
|
|
74
|
+
Journal of Synchrotron Radiation, Volume 32, Part 2, March 2025, Pages 365-377
|
|
75
|
+
See Section 3.3, Table 2, Section 3.4 and Table 3
|
|
76
|
+
|
|
77
|
+
Note: Maximum frame rate of MYTHEN III is a function of the number of counters and of
|
|
78
|
+
the bit depth.
|
|
79
|
+
|
|
80
|
+
These numbers neglect possible limitations arising from the DAQ computing and network
|
|
81
|
+
system. Expect ~1 order of magnitude less
|
|
82
|
+
|
|
83
|
+
Our mythen3 is currently set up to use 3 counters, and 32bit depth. The deadtimes are
|
|
84
|
+
for 3 counters only. We can run faster with less counters/bit depth
|
|
85
|
+
|
|
86
|
+
The maximum frame rate of the detector scales both with the number of counters being
|
|
87
|
+
read out and with the bit depth, which can be configured to 24 (streamed out as 32 bit),
|
|
88
|
+
16 or 8 bits see Table 3.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
_DEADTIMES = {
|
|
92
|
+
32: 1 / (30 * 1000), # values reported in frame rate (kHz)
|
|
93
|
+
24: 1 / (40 * 1000),
|
|
94
|
+
16: 1 / (60 * 1000),
|
|
95
|
+
8: 1 / (120 * 1000),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_BIT_DEPTH = 24
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Mythen3Controller(ADBaseController):
|
|
102
|
+
"""ADBaseController` for a Mythen3"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, driver: Mythen3Driver):
|
|
105
|
+
self._driver = driver
|
|
106
|
+
super().__init__(driver=self._driver)
|
|
107
|
+
|
|
108
|
+
def get_deadtime(self, exposure: float | None) -> float:
|
|
109
|
+
return _DEADTIMES[_BIT_DEPTH]
|
|
110
|
+
|
|
111
|
+
async def prepare(self, trigger_info: TriggerInfo) -> None:
|
|
112
|
+
if (exposure := trigger_info.livetime) is not None:
|
|
113
|
+
await self._driver.acquire_time.set(exposure)
|
|
114
|
+
|
|
115
|
+
if trigger_info.trigger is DetectorTrigger.INTERNAL:
|
|
116
|
+
await self._driver.trigger_mode.set(Mythen3TriggerMode.INTERNAL)
|
|
117
|
+
elif trigger_info.trigger in {
|
|
118
|
+
DetectorTrigger.CONSTANT_GATE,
|
|
119
|
+
DetectorTrigger.EDGE_TRIGGER,
|
|
120
|
+
DetectorTrigger.VARIABLE_GATE,
|
|
121
|
+
}:
|
|
122
|
+
await self._driver.trigger_mode.set(Mythen3TriggerMode.EXTERNAL)
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Mythen3 does not support {trigger_info.trigger}")
|
|
125
|
+
|
|
126
|
+
if trigger_info.total_number_of_exposures == 0:
|
|
127
|
+
image_mode = ADImageMode.CONTINUOUS
|
|
128
|
+
else:
|
|
129
|
+
image_mode = ADImageMode.MULTIPLE
|
|
130
|
+
await asyncio.gather(
|
|
131
|
+
self._driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
132
|
+
self._driver.image_mode.set(image_mode),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Mythen3(AreaDetector[Mythen3Controller]):
|
|
137
|
+
"""
|
|
138
|
+
The detector may be configured for an external trigger on a GPIO port,
|
|
139
|
+
which must be done prior to preparing the detector
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
prefix: str,
|
|
145
|
+
path_provider: PathProvider,
|
|
146
|
+
drv_suffix: str = DET_SUFFIX,
|
|
147
|
+
writer_cls: type[ADWriter] = ADHDFWriter,
|
|
148
|
+
fileio_suffix: str | None = "HDF:",
|
|
149
|
+
name: str = "",
|
|
150
|
+
):
|
|
151
|
+
self.driver = Mythen3Driver(prefix + drv_suffix)
|
|
152
|
+
self.controller = Mythen3Controller(driver=self.driver)
|
|
153
|
+
|
|
154
|
+
self.writer = writer_cls.with_io(
|
|
155
|
+
prefix,
|
|
156
|
+
path_provider,
|
|
157
|
+
dataset_source=self.driver,
|
|
158
|
+
fileio_suffix=fileio_suffix,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
super().__init__(
|
|
162
|
+
controller=self.controller,
|
|
163
|
+
writer=self.writer,
|
|
164
|
+
name=name,
|
|
165
|
+
) # plugins=plugins # config_sigs=config_sigs
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import Locatable, Location, Pausable, Stoppable
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
AsyncStatus,
|
|
7
|
+
StandardReadable,
|
|
8
|
+
StrictEnum,
|
|
9
|
+
set_and_wait_for_value,
|
|
10
|
+
)
|
|
11
|
+
from ophyd_async.epics.core import epics_signal_rw, epics_signal_rw_rbv, epics_signal_x
|
|
12
|
+
|
|
13
|
+
from dodal.log import LOGGER
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RobotJobs(StrictEnum):
|
|
17
|
+
RECOVER = "RECOVER" # Recover from unknown state
|
|
18
|
+
PICK_CAROUSEL = "PICKC" # Pick a sample from the carousel.
|
|
19
|
+
PLACE_CAROUSEL = "PLACEC" # Place a sample onto the carousel
|
|
20
|
+
PICK_DIFFRACTOMETER = "PICKD" # Pick a sample from the diffractometer.
|
|
21
|
+
PLACE_DIFFRACTOMETER = "PLACED" # Place a sample onto the diffractometer.
|
|
22
|
+
GRIPO = "GRIPO"
|
|
23
|
+
GRIPC = "GRIPC"
|
|
24
|
+
TABLEIN = "TABLEIN"
|
|
25
|
+
TABLEOUT = "TABLEOUT"
|
|
26
|
+
UNLOAD = "UNLOAD"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RobotSampleState(float, Enum):
|
|
30
|
+
CAROUSEL = 0.0 # Sample is on carousel
|
|
31
|
+
ONGRIP = 1.0 # Sample is on the gripper
|
|
32
|
+
DIFFRACTOMETER = 2.0 # Sample is on the diffractometer
|
|
33
|
+
UNKNOWN = 3.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NX100Robot(StandardReadable, Locatable[int], Stoppable, Pausable):
|
|
37
|
+
"""
|
|
38
|
+
This is a Yaskawa Motoman that uses an NX100 controller. It consists of a robot arm
|
|
39
|
+
with a gripper and a carousel for sample handling. It can pick and place samples
|
|
40
|
+
from the carousel to the diffractometer and vice versa.
|
|
41
|
+
|
|
42
|
+
Has set, pause, resume, stop, locate, stage methods.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
MAX_NUMBER_OF_SAMPLES = 200
|
|
46
|
+
MIN_NUMBER_OF_SAMPLES = 1
|
|
47
|
+
|
|
48
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
49
|
+
self.start = epics_signal_x(prefix + "START")
|
|
50
|
+
self.hold = epics_signal_rw(bool, prefix + "HOLD")
|
|
51
|
+
self.job = epics_signal_rw(RobotJobs, prefix + "JOB")
|
|
52
|
+
self.servo_on = epics_signal_rw(bool, prefix + "SVON") # Servo on/off
|
|
53
|
+
self.err = epics_signal_rw(int, prefix + "ERR")
|
|
54
|
+
|
|
55
|
+
self.robot_sample_state = epics_signal_rw(float, prefix + "D010")
|
|
56
|
+
self.next_sample_position = epics_signal_rw_rbv(
|
|
57
|
+
int, prefix + "D011", read_suffix=":RBV"
|
|
58
|
+
)
|
|
59
|
+
self.current_sample_position = epics_signal_rw_rbv(
|
|
60
|
+
int, prefix + "D012", read_suffix=":RBV"
|
|
61
|
+
)
|
|
62
|
+
self.door_latch_state = epics_signal_rw(int, prefix + "NEEDRECOVER")
|
|
63
|
+
|
|
64
|
+
super().__init__(name=name)
|
|
65
|
+
|
|
66
|
+
async def recover(self):
|
|
67
|
+
await asyncio.gather(
|
|
68
|
+
self.start.trigger(),
|
|
69
|
+
set_and_wait_for_value(self.job, RobotJobs.RECOVER),
|
|
70
|
+
set_and_wait_for_value(self.err, False),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def clear_sample(self, table_in: bool = True):
|
|
74
|
+
"""clears the sample from the diffractometer and places it on the carousel.
|
|
75
|
+
if table_in is True, it will also move the sample holder to the table in position."""
|
|
76
|
+
|
|
77
|
+
sample_state = await self.robot_sample_state.get_value()
|
|
78
|
+
if sample_state == RobotSampleState.DIFFRACTOMETER:
|
|
79
|
+
await asyncio.gather(
|
|
80
|
+
set_and_wait_for_value(self.job, RobotJobs.PICK_DIFFRACTOMETER),
|
|
81
|
+
set_and_wait_for_value(self.job, RobotJobs.PLACE_CAROUSEL),
|
|
82
|
+
)
|
|
83
|
+
elif sample_state == RobotSampleState.ONGRIP:
|
|
84
|
+
await set_and_wait_for_value(self.job, RobotJobs.PLACE_CAROUSEL)
|
|
85
|
+
elif sample_state == RobotSampleState.CAROUSEL:
|
|
86
|
+
pass
|
|
87
|
+
elif sample_state == RobotSampleState.UNKNOWN:
|
|
88
|
+
LOGGER.error("UNKNOWN sample state from robot, exit")
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(f"Unknown sample state: {sample_state}")
|
|
91
|
+
|
|
92
|
+
if table_in:
|
|
93
|
+
await set_and_wait_for_value(self.job, RobotJobs.TABLEIN)
|
|
94
|
+
|
|
95
|
+
LOGGER.info("Sample cleared from diffractometer")
|
|
96
|
+
|
|
97
|
+
async def load_sample(self, sample_location: int):
|
|
98
|
+
"""Loads a sample from the carousel to the diffractometer."""
|
|
99
|
+
sample_state = await self.robot_sample_state.get_value()
|
|
100
|
+
if sample_state == RobotSampleState.CAROUSEL:
|
|
101
|
+
await set_and_wait_for_value(self.job, RobotJobs.PICK_CAROUSEL)
|
|
102
|
+
await set_and_wait_for_value(self.job, RobotJobs.PLACE_DIFFRACTOMETER)
|
|
103
|
+
elif sample_state == RobotSampleState.ONGRIP:
|
|
104
|
+
await set_and_wait_for_value(self.job, RobotJobs.PLACE_DIFFRACTOMETER)
|
|
105
|
+
elif sample_state == RobotSampleState.DIFFRACTOMETER:
|
|
106
|
+
pass
|
|
107
|
+
elif sample_state == RobotSampleState.UNKNOWN:
|
|
108
|
+
LOGGER.warning(f"No sample at sample holder position {sample_location}")
|
|
109
|
+
LOGGER.error("UNKNOWN sample state from robot, exit")
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError(f"Unknown sample state: {sample_state}")
|
|
112
|
+
|
|
113
|
+
@AsyncStatus.wrap
|
|
114
|
+
async def set(self, sample_location: int) -> None:
|
|
115
|
+
if (
|
|
116
|
+
sample_location < self.MIN_NUMBER_OF_SAMPLES
|
|
117
|
+
or sample_location > self.MAX_NUMBER_OF_SAMPLES
|
|
118
|
+
):
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Sample location must be between {self.MIN_NUMBER_OF_SAMPLES} and {self.MAX_NUMBER_OF_SAMPLES}, got {sample_location}"
|
|
121
|
+
)
|
|
122
|
+
if await self.current_sample_position.get_value() == sample_location:
|
|
123
|
+
LOGGER.info(f"Robot already at position {sample_location}")
|
|
124
|
+
else:
|
|
125
|
+
await self.clear_sample(table_in=False)
|
|
126
|
+
await self.next_sample_position.set(sample_location, wait=True)
|
|
127
|
+
await self.load_sample(sample_location)
|
|
128
|
+
await self.current_sample_position.set(sample_location)
|
|
129
|
+
|
|
130
|
+
async def pause(self):
|
|
131
|
+
await set_and_wait_for_value(self.hold, True)
|
|
132
|
+
|
|
133
|
+
async def resume(self):
|
|
134
|
+
await set_and_wait_for_value(self.hold, False)
|
|
135
|
+
|
|
136
|
+
async def stop(self, success: bool = True):
|
|
137
|
+
await set_and_wait_for_value(self.hold, True)
|
|
138
|
+
if not success:
|
|
139
|
+
await set_and_wait_for_value(self.hold, False)
|
|
140
|
+
await self.clear_sample()
|
|
141
|
+
|
|
142
|
+
@AsyncStatus.wrap
|
|
143
|
+
async def stage(self) -> None:
|
|
144
|
+
"""Set up the device for acquisition."""
|
|
145
|
+
await asyncio.gather(
|
|
146
|
+
set_and_wait_for_value(self.servo_on, True),
|
|
147
|
+
set_and_wait_for_value(self.hold, False),
|
|
148
|
+
self.start.trigger(),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def locate(self) -> Location[int]:
|
|
152
|
+
location = await self.current_sample_position.locate()
|
|
153
|
+
return location
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from bluesky.protocols import Pausable
|
|
2
|
+
from ophyd_async.core import (
|
|
3
|
+
EnabledDisabled,
|
|
4
|
+
StandardReadable,
|
|
5
|
+
set_and_wait_for_value,
|
|
6
|
+
)
|
|
7
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Spinner(StandardReadable, Pausable):
|
|
11
|
+
"""This is a simple sample spinner, that has enable and speed (%)"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
prefix: str,
|
|
16
|
+
name: str = "",
|
|
17
|
+
disable_suffix: str = "SPIN:DISABLE",
|
|
18
|
+
speed_suffix: str = "SPIN:SPEED",
|
|
19
|
+
):
|
|
20
|
+
with self.add_children_as_readables():
|
|
21
|
+
self.enable = epics_signal_rw(EnabledDisabled, prefix + disable_suffix)
|
|
22
|
+
self.speed = epics_signal_rw(float, prefix + speed_suffix)
|
|
23
|
+
|
|
24
|
+
super().__init__(name=name)
|
|
25
|
+
|
|
26
|
+
async def pause(self):
|
|
27
|
+
await set_and_wait_for_value(self.enable, EnabledDisabled.DISABLED)
|
|
28
|
+
|
|
29
|
+
async def resume(self):
|
|
30
|
+
await set_and_wait_for_value(self.enable, EnabledDisabled.ENABLED)
|
|
@@ -8,15 +8,16 @@ from ophyd_async.core import (
|
|
|
8
8
|
)
|
|
9
9
|
from ophyd_async.epics.adcore import (
|
|
10
10
|
DEFAULT_GOOD_STATES,
|
|
11
|
-
ADBaseController,
|
|
12
11
|
ADBaseIO,
|
|
13
12
|
ADImageMode,
|
|
14
13
|
ADState,
|
|
15
14
|
)
|
|
16
15
|
from ophyd_async.epics.core import stop_busy_record
|
|
17
16
|
|
|
17
|
+
from dodal.devices.controllers import ConstantDeadTimeController
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
class MerlinController(ConstantDeadTimeController):
|
|
20
21
|
def __init__(
|
|
21
22
|
self,
|
|
22
23
|
driver: ADBaseIO,
|
|
@@ -29,8 +30,7 @@ class MerlinController(ADBaseController):
|
|
|
29
30
|
for drv_child in self.driver.children():
|
|
30
31
|
logging.debug(drv_child)
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
return 0.002
|
|
33
|
+
super().__init__(driver, 0.002)
|
|
34
34
|
|
|
35
35
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
36
36
|
self.frame_timeout = (
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from ophyd_async.epics.motor import Motor
|
|
2
|
+
|
|
3
|
+
from dodal.devices.motors import Stage, XYZStage
|
|
4
|
+
|
|
5
|
+
CIRC = "-MO-CIRC-02:" # for phi, kappa, omega, 2theta and det_z
|
|
6
|
+
SAMP = "-MO-SAMP-02:" # for x,y,z
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DetectorMotion(Stage):
|
|
10
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
11
|
+
with self.add_children_as_readables():
|
|
12
|
+
self.det_z = Motor(f"{prefix}DET")
|
|
13
|
+
self.two_theta = Motor(f"{prefix}2THETA")
|
|
14
|
+
super().__init__(name=name)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Collision model needs to be included
|
|
18
|
+
# See https://github.com/DiamondLightSource/dodal/issues/1073
|
|
19
|
+
class FourCircleDiffractometer(XYZStage):
|
|
20
|
+
"""Newport 4-circle diffractometer device."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
prefix: str,
|
|
25
|
+
name: str = "",
|
|
26
|
+
circ_infix: str = CIRC,
|
|
27
|
+
samp_infix: str = SAMP,
|
|
28
|
+
):
|
|
29
|
+
with self.add_children_as_readables():
|
|
30
|
+
self.phi = Motor(f"{prefix}{circ_infix}SAM:PHI")
|
|
31
|
+
self.omega = Motor(f"{prefix}{circ_infix}SAM:OMEGA")
|
|
32
|
+
self.kappa = Motor(f"{prefix}{circ_infix}SAM:KAPPA")
|
|
33
|
+
self.det_stage = DetectorMotion(f"{prefix}{circ_infix}SAM:", name)
|
|
34
|
+
super().__init__(f"{prefix}{samp_infix}SAM:", name)
|
dodal/devices/i19/shutter.py
CHANGED
|
@@ -22,10 +22,19 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
|
|
|
22
22
|
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
prefix: str,
|
|
28
|
+
hutch: HutchState,
|
|
29
|
+
instrument_session: str = "",
|
|
30
|
+
name: str = "",
|
|
31
|
+
) -> None:
|
|
32
|
+
# For instrument session addition to request parameters
|
|
33
|
+
# see https://github.com/DiamondLightSource/blueapi/issues/1187
|
|
26
34
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
27
35
|
self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
|
|
28
36
|
self.hutch_request = hutch
|
|
37
|
+
self.instrument_session = instrument_session
|
|
29
38
|
super().__init__(name)
|
|
30
39
|
|
|
31
40
|
@AsyncStatus.wrap
|
|
@@ -37,5 +46,6 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
|
|
|
37
46
|
"access_device": ACCESS_DEVICE_NAME,
|
|
38
47
|
"shutter_demand": value.value,
|
|
39
48
|
},
|
|
49
|
+
"instrument_session": self.instrument_session,
|
|
40
50
|
}
|
|
41
51
|
await super().set(REQUEST_PARAMS)
|
dodal/devices/i22/dcm.py
CHANGED
|
@@ -38,7 +38,7 @@ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
|
|
|
38
38
|
temperature_prefix: str,
|
|
39
39
|
crystal_1_metadata: CrystalMetadata,
|
|
40
40
|
crystal_2_metadata: CrystalMetadata,
|
|
41
|
-
prefix: str
|
|
41
|
+
prefix: str,
|
|
42
42
|
name: str = "",
|
|
43
43
|
) -> None:
|
|
44
44
|
with self.add_children_as_readables():
|
dodal/devices/i22/fswitch.py
CHANGED
|
@@ -7,20 +7,11 @@ from ophyd_async.core import (
|
|
|
7
7
|
DeviceVector,
|
|
8
8
|
StandardReadable,
|
|
9
9
|
StandardReadableFormat,
|
|
10
|
-
StrictEnum,
|
|
11
10
|
soft_signal_r_and_setter,
|
|
12
11
|
)
|
|
13
12
|
from ophyd_async.epics.core import epics_signal_r
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
class FilterState(StrictEnum):
|
|
17
|
-
"""
|
|
18
|
-
Note that the in/out here refers to the internal rocker
|
|
19
|
-
position so a PV value of IN implies a filter OUT of beam
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
IN_BEAM = "OUT"
|
|
23
|
-
OUT_BEAM = "IN"
|
|
14
|
+
from dodal.common.enums import InOutUpper
|
|
24
15
|
|
|
25
16
|
|
|
26
17
|
class FSwitch(StandardReadable):
|
|
@@ -50,7 +41,7 @@ class FSwitch(StandardReadable):
|
|
|
50
41
|
) -> None:
|
|
51
42
|
self.filters = DeviceVector(
|
|
52
43
|
{
|
|
53
|
-
i: epics_signal_r(
|
|
44
|
+
i: epics_signal_r(InOutUpper, f"{prefix}FILTER-{i:03}:STATUS_RBV")
|
|
54
45
|
for i in range(FSwitch.NUM_FILTERS)
|
|
55
46
|
}
|
|
56
47
|
)
|
|
@@ -91,7 +82,7 @@ class FSwitch(StandardReadable):
|
|
|
91
82
|
result = await asyncio.gather(
|
|
92
83
|
*(filter.get_value() for filter in self.filters.values())
|
|
93
84
|
)
|
|
94
|
-
num_in = sum(r.value ==
|
|
85
|
+
num_in = sum(r.value == InOutUpper.IN for r in result)
|
|
95
86
|
default_reading = await super().read()
|
|
96
87
|
return {
|
|
97
88
|
FSwitch.NUM_LENSES_FIELD_NAME: Reading(value=num_in, timestamp=time.time()),
|
dodal/devices/i24/aperture.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from ophyd_async.core import StrictEnum
|
|
1
|
+
from ophyd_async.core import InOut, StrictEnum
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_rw
|
|
3
3
|
|
|
4
4
|
from dodal.devices.motors import XYStage
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class AperturePositions(StrictEnum):
|
|
8
|
-
IN =
|
|
9
|
-
OUT =
|
|
8
|
+
IN = InOut.IN.value
|
|
9
|
+
OUT = InOut.OUT.value
|
|
10
10
|
ROBOT = "Robot"
|
|
11
11
|
MANUAL = "Manual Mounting"
|
|
12
12
|
|
dodal/devices/i24/dcm.py
CHANGED
|
@@ -12,22 +12,18 @@ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
|
|
|
12
12
|
A double crystal monocromator device, used to select the beam energy.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
def __init__(self, prefix: str, name: str = "") -> None:
|
|
15
|
+
def __init__(self, prefix: str, motion_prefix: str, name: str = "") -> None:
|
|
16
16
|
with self.add_children_as_readables():
|
|
17
17
|
# Temperatures
|
|
18
|
-
self.xtal1_temp = epics_signal_r(float, prefix + "
|
|
19
|
-
self.xtal1_heater_temp = epics_signal_r(
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
self.xtal2_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-4")
|
|
23
|
-
self.xtal2_heater_temp = epics_signal_r(
|
|
24
|
-
float, prefix + "-DI-DCM-01:PT100-5"
|
|
25
|
-
)
|
|
18
|
+
self.xtal1_temp = epics_signal_r(float, prefix + "PT100-1")
|
|
19
|
+
self.xtal1_heater_temp = epics_signal_r(float, prefix + "PT100-2")
|
|
20
|
+
self.xtal2_temp = epics_signal_r(float, prefix + "PT100-4")
|
|
21
|
+
self.xtal2_heater_temp = epics_signal_r(float, prefix + "PT100-5")
|
|
26
22
|
|
|
27
|
-
self.roll_plate_temp = epics_signal_r(float, prefix + "
|
|
28
|
-
self.pitch_plate_temp = epics_signal_r(float, prefix + "
|
|
29
|
-
self.backplate_temp = epics_signal_r(float, prefix + "
|
|
30
|
-
self.b1_plate_temp = epics_signal_r(float, prefix + "
|
|
31
|
-
self.gap_temp = epics_signal_r(float, prefix + "
|
|
23
|
+
self.roll_plate_temp = epics_signal_r(float, prefix + "PT100-3")
|
|
24
|
+
self.pitch_plate_temp = epics_signal_r(float, prefix + "PT100-6")
|
|
25
|
+
self.backplate_temp = epics_signal_r(float, prefix + "PT100-7")
|
|
26
|
+
self.b1_plate_temp = epics_signal_r(float, prefix + "PT100-7")
|
|
27
|
+
self.gap_temp = epics_signal_r(float, prefix + "TC-1")
|
|
32
28
|
|
|
33
|
-
super().__init__(
|
|
29
|
+
super().__init__(motion_prefix, RollCrystal, PitchAndRollCrystal, name)
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
from ophyd_async.core import AsyncStatus, StandardReadable, StrictEnum
|
|
1
|
+
from ophyd_async.core import AsyncStatus, InOut, StandardReadable, StrictEnum
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_rw
|
|
3
3
|
|
|
4
|
+
from dodal.common.enums import OnOffUpper
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class BacklightPositions(StrictEnum):
|
|
6
|
-
OUT =
|
|
7
|
-
IN =
|
|
8
|
+
OUT = InOut.OUT.value
|
|
9
|
+
IN = InOut.IN.value
|
|
8
10
|
LOAD_CHECK = "LoadCheck"
|
|
9
11
|
OAV2 = "OAV2"
|
|
10
12
|
DIODE = "Diode"
|
|
11
13
|
WHITE_IN = "White In"
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class LEDStatus(StrictEnum):
|
|
15
|
-
OFF = "OFF"
|
|
16
|
-
ON = "ON"
|
|
17
|
-
|
|
18
|
-
|
|
19
16
|
class BacklightPositioner(StandardReadable):
|
|
20
17
|
"""Device to control the backlight position."""
|
|
21
18
|
|
|
@@ -45,16 +42,18 @@ class DualBacklight(StandardReadable):
|
|
|
45
42
|
"""
|
|
46
43
|
|
|
47
44
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
48
|
-
self.backlight_state = epics_signal_rw(
|
|
45
|
+
self.backlight_state = epics_signal_rw(OnOffUpper, prefix + "-DI-LED-01:TOGGLE")
|
|
49
46
|
self.backlight_position = BacklightPositioner(prefix + "-MO-BL-01:", name)
|
|
50
47
|
|
|
51
|
-
self.frontlight_state = epics_signal_rw(
|
|
48
|
+
self.frontlight_state = epics_signal_rw(
|
|
49
|
+
OnOffUpper, prefix + "-DI-LED-02:TOGGLE"
|
|
50
|
+
)
|
|
52
51
|
super().__init__(name)
|
|
53
52
|
|
|
54
53
|
@AsyncStatus.wrap
|
|
55
54
|
async def set(self, value: BacklightPositions):
|
|
56
55
|
await self.backlight_position.set(value)
|
|
57
56
|
if value == BacklightPositions.OUT:
|
|
58
|
-
await self.backlight_state.set(
|
|
57
|
+
await self.backlight_state.set(OnOffUpper.OFF, wait=True)
|
|
59
58
|
else:
|
|
60
|
-
await self.backlight_state.set(
|
|
59
|
+
await self.backlight_state.set(OnOffUpper.ON, wait=True)
|
dodal/devices/i24/pmac.py
CHANGED
|
@@ -17,8 +17,9 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
|
17
17
|
|
|
18
18
|
from dodal.devices.motors import XYZStage
|
|
19
19
|
|
|
20
|
-
HOME_STR = r"\#
|
|
20
|
+
HOME_STR = r"\#5hmz\#6hmz\#7hmz" # Command to home the PMAC motors
|
|
21
21
|
ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
|
|
22
|
+
CS_STR = "&2"
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class ScanState(IntEnum):
|
|
@@ -196,12 +197,12 @@ class PMAC(XYZStage):
|
|
|
196
197
|
"""Device to control the chip stage on I24."""
|
|
197
198
|
|
|
198
199
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
199
|
-
self.pmac_string = epics_signal_rw(str, prefix
|
|
200
|
+
self.pmac_string = epics_signal_rw(str, f"{prefix}-MO-IOC-13:PMAC:console")
|
|
200
201
|
self.home = PMACStringMove(
|
|
201
202
|
self.pmac_string,
|
|
202
|
-
HOME_STR,
|
|
203
|
+
f"{CS_STR}{HOME_STR}",
|
|
203
204
|
)
|
|
204
|
-
self.to_xyz_zero = PMACStringMove(self.pmac_string, ZERO_STR)
|
|
205
|
+
self.to_xyz_zero = PMACStringMove(self.pmac_string, f"{CS_STR}{ZERO_STR}")
|
|
205
206
|
|
|
206
207
|
self.laser = PMACStringLaser(self.pmac_string)
|
|
207
208
|
|
|
@@ -211,8 +212,8 @@ class PMAC(XYZStage):
|
|
|
211
212
|
|
|
212
213
|
# These next signals are readback values on PVARS which are set by the motion
|
|
213
214
|
# program.
|
|
214
|
-
self.scanstatus = epics_signal_r(float, "
|
|
215
|
-
self.counter = epics_signal_r(float, "
|
|
215
|
+
self.scanstatus = epics_signal_r(float, f"{prefix}-MO-STEP-13:pmac:read:P2401")
|
|
216
|
+
self.counter = epics_signal_r(float, f"{prefix}-MO-STEP-13:pmac:read:P2402")
|
|
216
217
|
|
|
217
218
|
# A couple of soft signals for running a collection: program number to send to
|
|
218
219
|
# the PMAC_STRING and expected collection time.
|
|
@@ -228,4 +229,4 @@ class PMAC(XYZStage):
|
|
|
228
229
|
)
|
|
229
230
|
self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
|
|
230
231
|
|
|
231
|
-
super().__init__(prefix, name)
|
|
232
|
+
super().__init__(f"{prefix}-MO-CHIP-01:", name)
|
|
@@ -18,9 +18,6 @@ class BeamstopPositions(StrictEnum):
|
|
|
18
18
|
robot load however all 3 resolution positions are the same. We also
|
|
19
19
|
do not use the robot load position in Hyperion.
|
|
20
20
|
|
|
21
|
-
Until we support moving the beamstop it is only necessary to check whether the
|
|
22
|
-
beamstop is in beam or not.
|
|
23
|
-
|
|
24
21
|
See Also:
|
|
25
22
|
https://github.com/DiamondLightSource/mx-bluesky/issues/484
|
|
26
23
|
|
|
@@ -86,11 +83,13 @@ class Beamstop(StandardReadable):
|
|
|
86
83
|
return BeamstopPositions.UNKNOWN
|
|
87
84
|
|
|
88
85
|
async def _set_selected_position(self, position: BeamstopPositions) -> None:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
self.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
match position:
|
|
87
|
+
case BeamstopPositions.DATA_COLLECTION:
|
|
88
|
+
# Move z first as it could be under the table
|
|
89
|
+
await self.z_mm.set(self._in_beam_xyz_mm[2])
|
|
90
|
+
await asyncio.gather(
|
|
91
|
+
self.x_mm.set(self._in_beam_xyz_mm[0]),
|
|
92
|
+
self.y_mm.set(self._in_beam_xyz_mm[1]),
|
|
93
|
+
)
|
|
94
|
+
case _:
|
|
95
|
+
raise ValueError(f"Cannot set beamstop to position {position}")
|