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.
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/METADATA +3 -3
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/RECORD +101 -87
- 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/i17.py +37 -0
- dodal/beamlines/i19_1.py +1 -0
- dodal/beamlines/i19_2.py +7 -0
- dodal/beamlines/i22.py +5 -5
- dodal/beamlines/i23.py +3 -3
- dodal/beamlines/i24.py +6 -33
- 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/beam_center.py +1 -2
- 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 +35 -18
- dodal/devices/synchrotron.py +1 -2
- dodal/devices/thawer.py +22 -15
- dodal/devices/undulator.py +3 -8
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/watsonmarlow323_pump.py +7 -7
- dodal/devices/webcam.py +1 -0
- 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 +3 -0
- dodal/testing/electron_analyser/__init__.py +6 -0
- dodal/testing/electron_analyser/device_factory.py +59 -0
- dodal/testing/setup.py +67 -0
- dodal/devices/CTAB.py +0 -41
- dodal/devices/i24/pilatus_metadata.py +0 -44
- dodal/devices/util/test_utils.py +0 -37
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from ophyd_async.epics.motor import Motor
|
|
2
|
+
|
|
3
|
+
from dodal.devices.motors import Stage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DiffractometerStage(Stage):
|
|
7
|
+
"""
|
|
8
|
+
This is the diffractometer stage which contains both detectors,
|
|
9
|
+
it allows for rotations and also sample position. Contains:
|
|
10
|
+
theta, delta, two_theta, sample_position
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
prefix: str,
|
|
16
|
+
name: str = "",
|
|
17
|
+
theta_suffix: str = "THETA",
|
|
18
|
+
delta_suffix: str = "DELTA",
|
|
19
|
+
two_theta_suffix: str = "2THETA",
|
|
20
|
+
sample_pos_suffix: str = "SPOS",
|
|
21
|
+
):
|
|
22
|
+
with self.add_children_as_readables():
|
|
23
|
+
self.theta = Motor(prefix + theta_suffix)
|
|
24
|
+
self.delta = Motor(prefix + delta_suffix)
|
|
25
|
+
self.two_theta = Motor(prefix + two_theta_suffix)
|
|
26
|
+
self.sample_position = Motor(prefix + sample_pos_suffix)
|
|
27
|
+
|
|
28
|
+
super().__init__(name=name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DiffractometerBase(Stage):
|
|
32
|
+
"""
|
|
33
|
+
This is the diffractometer stage which contains both detectors,
|
|
34
|
+
it allows for translation about x and y and also sample position. Contains:
|
|
35
|
+
x1, x2, y1, y2, y3. Used for aligning the detector to the beam/sample
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
prefix: str,
|
|
41
|
+
name: str = "",
|
|
42
|
+
x1_suffix: str = "X1",
|
|
43
|
+
x2_suffix: str = "X2",
|
|
44
|
+
y1_suffix: str = "Y1",
|
|
45
|
+
y2_suffix: str = "Y2",
|
|
46
|
+
y3_suffix: str = "Y3",
|
|
47
|
+
):
|
|
48
|
+
with self.add_children_as_readables():
|
|
49
|
+
self.x1 = Motor(prefix + x1_suffix)
|
|
50
|
+
self.x2 = Motor(prefix + x2_suffix)
|
|
51
|
+
self.y1 = Motor(prefix + y1_suffix)
|
|
52
|
+
self.y2 = Motor(prefix + y2_suffix)
|
|
53
|
+
self.y3 = Motor(prefix + y3_suffix)
|
|
54
|
+
|
|
55
|
+
super().__init__(name=name)
|
|
@@ -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/beam_center.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""A small temporary device to get the beam center positions
|
|
2
|
-
eiger or pilatus detector on i24"""
|
|
1
|
+
"""A small temporary device to get the beam center positions on i24"""
|
|
3
2
|
|
|
4
3
|
from ophyd_async.core import StandardReadable
|
|
5
4
|
from ophyd_async.epics.core import epics_signal_rw
|
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)
|