dls-dodal 1.33.0__py3-none-any.whl → 1.35.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.33.0.dist-info → dls_dodal-1.35.0.dist-info}/METADATA +3 -3
- dls_dodal-1.35.0.dist-info/RECORD +147 -0
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/WHEEL +1 -1
- dodal/__init__.py +8 -0
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i03.py +6 -2
- dodal/beamlines/__init__.py +2 -3
- dodal/beamlines/i03.py +41 -9
- dodal/beamlines/i04.py +26 -4
- dodal/beamlines/i10.py +257 -0
- dodal/beamlines/i22.py +25 -13
- dodal/beamlines/i24.py +11 -11
- dodal/beamlines/p38.py +24 -13
- dodal/common/beamlines/beamline_utils.py +1 -2
- dodal/common/crystal_metadata.py +61 -0
- dodal/common/signal_utils.py +10 -14
- dodal/common/types.py +2 -7
- dodal/devices/CTAB.py +1 -1
- dodal/devices/aperture.py +1 -1
- dodal/devices/aperturescatterguard.py +20 -8
- dodal/devices/apple2_undulator.py +603 -0
- dodal/devices/areadetector/plugins/CAM.py +29 -0
- dodal/devices/areadetector/plugins/MJPG.py +51 -106
- dodal/devices/attenuator.py +1 -1
- dodal/devices/backlight.py +11 -11
- dodal/devices/cryostream.py +3 -5
- dodal/devices/dcm.py +26 -2
- dodal/devices/detector/detector_motion.py +3 -5
- dodal/devices/diamond_filter.py +46 -0
- dodal/devices/eiger.py +6 -2
- dodal/devices/eiger_odin.py +48 -39
- dodal/devices/fast_grid_scan.py +1 -1
- dodal/devices/fluorescence_detector_motion.py +5 -7
- dodal/devices/focusing_mirror.py +26 -19
- dodal/devices/hutch_shutter.py +4 -5
- dodal/devices/i10/i10_apple2.py +399 -0
- dodal/devices/i10/i10_setting_data.py +7 -0
- dodal/devices/i22/dcm.py +50 -83
- dodal/devices/i22/fswitch.py +5 -5
- dodal/devices/i24/aperture.py +3 -5
- dodal/devices/i24/beamstop.py +3 -5
- dodal/devices/i24/dcm.py +1 -1
- dodal/devices/i24/dual_backlight.py +9 -11
- dodal/devices/i24/pmac.py +35 -46
- dodal/devices/i24/vgonio.py +16 -0
- dodal/devices/ipin.py +5 -3
- dodal/devices/linkam3.py +7 -7
- dodal/devices/oav/oav_calculations.py +22 -0
- dodal/devices/oav/oav_detector.py +118 -83
- dodal/devices/oav/oav_parameters.py +50 -104
- dodal/devices/oav/oav_to_redis_forwarder.py +77 -35
- dodal/devices/oav/pin_image_recognition/__init__.py +9 -7
- dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +16 -59
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
- dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
- dodal/devices/oav/utils.py +28 -27
- dodal/devices/p99/sample_stage.py +3 -5
- dodal/devices/pgm.py +40 -0
- dodal/devices/qbpm.py +18 -0
- dodal/devices/robot.py +5 -5
- dodal/devices/smargon.py +3 -3
- dodal/devices/synchrotron.py +9 -4
- dodal/devices/tetramm.py +9 -9
- dodal/devices/thawer.py +13 -7
- dodal/devices/undulator.py +7 -6
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/util/lookup_tables.py +4 -5
- dodal/devices/watsonmarlow323_pump.py +45 -0
- dodal/devices/webcam.py +9 -2
- dodal/devices/xbpm_feedback.py +3 -5
- dodal/devices/xspress3/xspress3.py +8 -9
- dodal/devices/xspress3/xspress3_channel.py +3 -5
- dodal/devices/zebra.py +12 -8
- dodal/devices/zebra_controlled_shutter.py +5 -6
- dodal/devices/zocalo/__init__.py +2 -2
- dodal/devices/zocalo/zocalo_constants.py +3 -0
- dodal/devices/zocalo/zocalo_interaction.py +2 -1
- dodal/devices/zocalo/zocalo_results.py +105 -89
- dodal/plans/data_session_metadata.py +2 -2
- dodal/plans/motor_util_plans.py +11 -9
- dodal/utils.py +11 -0
- dls_dodal-1.33.0.dist-info/RECORD +0 -136
- dodal/beamlines/i04_1.py +0 -140
- dodal/devices/i24/i24_vgonio.py +0 -17
- dodal/devices/oav/oav_errors.py +0 -35
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/top_level.txt +0 -0
dodal/devices/hutch_shutter.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
1
|
from bluesky.protocols import Movable
|
|
4
2
|
from ophyd_async.core import (
|
|
5
3
|
DEFAULT_TIMEOUT,
|
|
6
4
|
AsyncStatus,
|
|
7
5
|
StandardReadable,
|
|
6
|
+
StrictEnum,
|
|
8
7
|
wait_for_value,
|
|
9
8
|
)
|
|
10
|
-
from ophyd_async.epics.
|
|
9
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_w
|
|
11
10
|
|
|
12
11
|
HUTCH_SAFE_FOR_OPERATIONS = 0 # Hutch is locked and can't be entered
|
|
13
12
|
|
|
@@ -16,13 +15,13 @@ class ShutterNotSafeToOperateError(Exception):
|
|
|
16
15
|
pass
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
class ShutterDemand(
|
|
18
|
+
class ShutterDemand(StrictEnum):
|
|
20
19
|
OPEN = "Open"
|
|
21
20
|
CLOSE = "Close"
|
|
22
21
|
RESET = "Reset"
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
class ShutterState(
|
|
24
|
+
class ShutterState(StrictEnum):
|
|
26
25
|
FAULT = "Fault"
|
|
27
26
|
OPEN = "Open"
|
|
28
27
|
OPENING = "Opening"
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import csv
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, SupportsFloat
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from bluesky.protocols import Movable
|
|
9
|
+
from ophyd_async.core import (
|
|
10
|
+
AsyncStatus,
|
|
11
|
+
Reference,
|
|
12
|
+
StandardReadable,
|
|
13
|
+
StandardReadableFormat,
|
|
14
|
+
soft_signal_r_and_setter,
|
|
15
|
+
soft_signal_rw,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from dodal.devices.apple2_undulator import (
|
|
19
|
+
Apple2,
|
|
20
|
+
Apple2Val,
|
|
21
|
+
Lookuptable,
|
|
22
|
+
UndulatorGap,
|
|
23
|
+
UndulatorJawPhase,
|
|
24
|
+
UndulatorPhaseAxes,
|
|
25
|
+
)
|
|
26
|
+
from dodal.devices.pgm import PGM
|
|
27
|
+
from dodal.log import LOGGER
|
|
28
|
+
|
|
29
|
+
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
30
|
+
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
31
|
+
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
32
|
+
DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
|
|
33
|
+
ALPHA_OFFSET = 180
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# data class to store the lookup table configuration that is use in convert_csv_to_lookup
|
|
37
|
+
@dataclass
|
|
38
|
+
class LookupPath:
|
|
39
|
+
Gap: Path
|
|
40
|
+
Phase: Path
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class LookupTableConfig:
|
|
45
|
+
path: LookupPath
|
|
46
|
+
source: tuple[str, str]
|
|
47
|
+
mode: str | None
|
|
48
|
+
min_energy: str | None
|
|
49
|
+
max_energy: str | None
|
|
50
|
+
poly_deg: list | None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class I10Apple2(Apple2):
|
|
54
|
+
"""
|
|
55
|
+
I10Apple2 is the i10 version of Apple2 ID.
|
|
56
|
+
The set and update_lookuptable should be the only part that is I10 specific.
|
|
57
|
+
|
|
58
|
+
A pair of look up tables are needed to provide the conversion
|
|
59
|
+
between motor position and energy.
|
|
60
|
+
Set is in energy(eV).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
id_gap: UndulatorGap,
|
|
66
|
+
id_phase: UndulatorPhaseAxes,
|
|
67
|
+
id_jaw_phase: UndulatorJawPhase,
|
|
68
|
+
energy_gap_table_path: Path,
|
|
69
|
+
energy_phase_table_path: Path,
|
|
70
|
+
source: tuple[str, str],
|
|
71
|
+
prefix: str = "",
|
|
72
|
+
mode: str = "Mode",
|
|
73
|
+
min_energy: str = "MinEnergy",
|
|
74
|
+
max_energy: str = "MaxEnergy",
|
|
75
|
+
poly_deg: list | None = None,
|
|
76
|
+
name: str = "",
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
id_gap:
|
|
82
|
+
An UndulatorGap device.
|
|
83
|
+
id_phase:
|
|
84
|
+
An UndulatorPhaseAxes device.
|
|
85
|
+
energy_gap_table_path:
|
|
86
|
+
The path to id gap look up table.
|
|
87
|
+
energy_phase_table_path:
|
|
88
|
+
The path to id phase look up table.
|
|
89
|
+
source:
|
|
90
|
+
The column name and the name of the source in look up table. e.g. ("source", "idu")
|
|
91
|
+
mode:
|
|
92
|
+
The column name of the mode in look up table.
|
|
93
|
+
min_energy:
|
|
94
|
+
The column name that contain the maximum energy in look up table.
|
|
95
|
+
max_energy:
|
|
96
|
+
The column name that contain the maximum energy in look up table.
|
|
97
|
+
poly_deg:
|
|
98
|
+
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
99
|
+
prefix:
|
|
100
|
+
Not in use but needed for device_instantiation.
|
|
101
|
+
Name:
|
|
102
|
+
Name of the device
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# A dataclass contains the path to the look up table and the expected column names.
|
|
106
|
+
self.lookup_table_config = LookupTableConfig(
|
|
107
|
+
path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
|
|
108
|
+
source=source,
|
|
109
|
+
mode=mode,
|
|
110
|
+
min_energy=min_energy,
|
|
111
|
+
max_energy=max_energy,
|
|
112
|
+
poly_deg=poly_deg,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
super().__init__(
|
|
116
|
+
id_gap=id_gap,
|
|
117
|
+
id_phase=id_phase,
|
|
118
|
+
prefix=prefix,
|
|
119
|
+
name=name,
|
|
120
|
+
)
|
|
121
|
+
with self.add_children_as_readables():
|
|
122
|
+
self.id_jaw_phase = Reference(id_jaw_phase)
|
|
123
|
+
|
|
124
|
+
@AsyncStatus.wrap
|
|
125
|
+
async def set(self, value: SupportsFloat) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Check polarisation state and use it together with the energy(value)
|
|
128
|
+
to calculate the required gap and phases before setting it.
|
|
129
|
+
"""
|
|
130
|
+
value = float(value)
|
|
131
|
+
if self.pol is None:
|
|
132
|
+
LOGGER.warning("Polarisation not set attempting to read from hardware")
|
|
133
|
+
pol, phase = await self.determinePhaseFromHardware()
|
|
134
|
+
if pol is None:
|
|
135
|
+
raise ValueError(f"Pol is not set for {self.name}")
|
|
136
|
+
self.pol = pol
|
|
137
|
+
|
|
138
|
+
self._polarisation_set(self.pol)
|
|
139
|
+
gap, phase = self._get_id_gap_phase(value)
|
|
140
|
+
phase3 = phase * (-1 if self.pol == "la" else (1))
|
|
141
|
+
id_set_val = Apple2Val(
|
|
142
|
+
top_outer=str(phase),
|
|
143
|
+
top_inner="0.0",
|
|
144
|
+
btm_inner=str(phase3),
|
|
145
|
+
btm_outer="0.0",
|
|
146
|
+
gap=str(gap),
|
|
147
|
+
)
|
|
148
|
+
LOGGER.info(f"Setting polarisation to {self.pol}, with {id_set_val}")
|
|
149
|
+
await self._set(value=id_set_val, energy=value)
|
|
150
|
+
if self.pol != "la":
|
|
151
|
+
await self.id_jaw_phase().set(0)
|
|
152
|
+
await self.id_jaw_phase().set_move.set(1)
|
|
153
|
+
|
|
154
|
+
def update_lookuptable(self):
|
|
155
|
+
"""
|
|
156
|
+
Update the stored lookup tabled from file.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
LOGGER.info("Updating lookup dictionary from file.")
|
|
160
|
+
for key, path in self.lookup_table_config.path.__dict__.items():
|
|
161
|
+
if path.exists():
|
|
162
|
+
self.lookup_tables[key] = convert_csv_to_lookup(
|
|
163
|
+
file=path,
|
|
164
|
+
source=self.lookup_table_config.source,
|
|
165
|
+
mode=self.lookup_table_config.mode,
|
|
166
|
+
min_energy=self.lookup_table_config.min_energy,
|
|
167
|
+
max_energy=self.lookup_table_config.max_energy,
|
|
168
|
+
poly_deg=self.lookup_table_config.poly_deg,
|
|
169
|
+
)
|
|
170
|
+
# ensure the importing lookup table is the correct format
|
|
171
|
+
Lookuptable.model_validate(self.lookup_tables[key])
|
|
172
|
+
else:
|
|
173
|
+
raise FileNotFoundError(f"{key} look up table is not in path: {path}")
|
|
174
|
+
|
|
175
|
+
self._available_pol = list(self.lookup_tables["Gap"].keys())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class I10Apple2PGM(StandardReadable, Movable):
|
|
179
|
+
"""
|
|
180
|
+
Compound device to set both ID and PGM energy at the sample time,poly_deg
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self, id: I10Apple2, pgm: PGM, prefix: str = "", name: str = ""
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
id:
|
|
191
|
+
An Apple2 device.
|
|
192
|
+
pgm:
|
|
193
|
+
A PGM/mono device.
|
|
194
|
+
prefix:
|
|
195
|
+
Not in use but needed for device_instantiation.
|
|
196
|
+
name:
|
|
197
|
+
New device name.
|
|
198
|
+
"""
|
|
199
|
+
super().__init__(name=name)
|
|
200
|
+
self.id_ref = Reference(id)
|
|
201
|
+
self.pgm_ref = Reference(pgm)
|
|
202
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
203
|
+
self.energy_offset = soft_signal_rw(float, initial_value=0)
|
|
204
|
+
|
|
205
|
+
@AsyncStatus.wrap
|
|
206
|
+
async def set(self, value: float) -> None:
|
|
207
|
+
LOGGER.info(f"Moving f{self.name} energy to {value}.")
|
|
208
|
+
await asyncio.gather(
|
|
209
|
+
self.id_ref().set(value=value + await self.energy_offset.get_value()),
|
|
210
|
+
self.pgm_ref().energy.set(value),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class I10Apple2Pol(StandardReadable, Movable):
|
|
215
|
+
"""
|
|
216
|
+
Compound device to set polorisation of ID.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self, id: I10Apple2, prefix: str = "", name: str = "") -> None:
|
|
220
|
+
"""
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
id:
|
|
224
|
+
An I10Apple2 device.
|
|
225
|
+
prefix:
|
|
226
|
+
Not in use but needed for device_instantiation.
|
|
227
|
+
name:
|
|
228
|
+
New device name.
|
|
229
|
+
"""
|
|
230
|
+
super().__init__(name=name)
|
|
231
|
+
with self.add_children_as_readables():
|
|
232
|
+
self.id = id
|
|
233
|
+
|
|
234
|
+
@AsyncStatus.wrap
|
|
235
|
+
async def set(self, value: str) -> None:
|
|
236
|
+
self.id.pol = value # change polarisation.
|
|
237
|
+
LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
|
|
238
|
+
await self.id.set(
|
|
239
|
+
await self.id.energy.get_value()
|
|
240
|
+
) # Move id to new polarisation
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class LinearArbitraryAngle(StandardReadable, Movable):
|
|
244
|
+
"""
|
|
245
|
+
Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
|
|
246
|
+
is the direction of the magnetic field which can be change by varying the jaw_phase
|
|
247
|
+
in (linear arbitrary (la) mode,
|
|
248
|
+
The angle of 0 is equivalent to linear horizontal "lh" (sigma) and
|
|
249
|
+
90 is linear vertical "lv" (pi).
|
|
250
|
+
This device require a jaw_phase to angle conversion which is done via a polynomial.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
id: I10Apple2,
|
|
256
|
+
prefix: str = "",
|
|
257
|
+
name: str = "",
|
|
258
|
+
jaw_phase_limit: float = 12.0,
|
|
259
|
+
jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
260
|
+
angle_threshold_deg=30.0,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
id: I10Apple2
|
|
266
|
+
An I10Apple2 device.
|
|
267
|
+
prefix: str
|
|
268
|
+
Not in use but needed for device_instantiation.
|
|
269
|
+
name: str
|
|
270
|
+
New device name.
|
|
271
|
+
jaw_phase_limit: float
|
|
272
|
+
The maximum allowed jaw_phase movement.
|
|
273
|
+
jaw_phase_poly_param: list
|
|
274
|
+
polynomial parameters highest power first.
|
|
275
|
+
"""
|
|
276
|
+
super().__init__(name=name)
|
|
277
|
+
self.id_ref = Reference(id)
|
|
278
|
+
self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
|
|
279
|
+
self.angle_threshold_deg = angle_threshold_deg
|
|
280
|
+
self.jaw_phase_limit = jaw_phase_limit
|
|
281
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
282
|
+
self.angle, self._angle_set = soft_signal_r_and_setter(
|
|
283
|
+
float, initial_value=None
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@AsyncStatus.wrap
|
|
287
|
+
async def set(self, value: SupportsFloat) -> None:
|
|
288
|
+
value = float(value)
|
|
289
|
+
pol = self.id_ref().pol
|
|
290
|
+
if pol != "la":
|
|
291
|
+
raise RuntimeError(
|
|
292
|
+
f"Angle control is not available in polarisation {pol} with {self.id_ref().name}"
|
|
293
|
+
)
|
|
294
|
+
# Moving to real angle which is 210 to 30.
|
|
295
|
+
alpha_real = value if value > self.angle_threshold_deg else value + ALPHA_OFFSET
|
|
296
|
+
jaw_phase = self.jaw_phase_from_angle(alpha_real)
|
|
297
|
+
if abs(jaw_phase) > self.jaw_phase_limit:
|
|
298
|
+
raise RuntimeError(
|
|
299
|
+
f"jaw_phase position for angle ({value}) is outside permitted range"
|
|
300
|
+
f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
|
|
301
|
+
)
|
|
302
|
+
await self.id_ref().id_jaw_phase().set(jaw_phase)
|
|
303
|
+
self._angle_set(value)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def convert_csv_to_lookup(
|
|
307
|
+
file: str,
|
|
308
|
+
source: tuple[str, str],
|
|
309
|
+
mode: str | None = "Mode",
|
|
310
|
+
min_energy: str | None = "MinEnergy",
|
|
311
|
+
max_energy: str | None = "MaxEnergy",
|
|
312
|
+
poly_deg: list | None = None,
|
|
313
|
+
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
314
|
+
"""
|
|
315
|
+
Convert csv to a dictionary that can be read by Apple2 ID device.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
-----------
|
|
319
|
+
file: str
|
|
320
|
+
File path.
|
|
321
|
+
source: tuple[str, str]
|
|
322
|
+
Tuple(column name, source name)
|
|
323
|
+
e.g. ("Source", "idu").
|
|
324
|
+
mode: str = "Mode"
|
|
325
|
+
Column name for the available modes, "lv","lh","pc","nc" etc
|
|
326
|
+
min_energy: str = "MinEnergy":
|
|
327
|
+
Column name for min energy for the polynomial.
|
|
328
|
+
max_energy: str = "MaxEnergy",
|
|
329
|
+
Column name for max energy for the polynomial.
|
|
330
|
+
poly_deg: list | None = None,
|
|
331
|
+
Column names for the parameters for the polynomial, starting with the least significant.
|
|
332
|
+
|
|
333
|
+
return
|
|
334
|
+
------
|
|
335
|
+
return a dictionary that conform to Apple2 lookup table format:
|
|
336
|
+
|
|
337
|
+
{mode: {'Energies': {Any: {'Low': float,
|
|
338
|
+
'High': float,
|
|
339
|
+
'Poly':np.poly1d
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
'Limit': {'Minimum': float,
|
|
343
|
+
'Maximum': float
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
"""
|
|
348
|
+
if poly_deg is None:
|
|
349
|
+
poly_deg = [
|
|
350
|
+
"7th-order",
|
|
351
|
+
"6th-order",
|
|
352
|
+
"5th-order",
|
|
353
|
+
"4th-order",
|
|
354
|
+
"3rd-order",
|
|
355
|
+
"2nd-order",
|
|
356
|
+
"1st-order",
|
|
357
|
+
"b",
|
|
358
|
+
]
|
|
359
|
+
look_up_table = {}
|
|
360
|
+
pol = []
|
|
361
|
+
|
|
362
|
+
def data2dict(row) -> None:
|
|
363
|
+
# logic for the conversion for each row of data.
|
|
364
|
+
if row[mode] not in pol:
|
|
365
|
+
pol.append(row[mode])
|
|
366
|
+
look_up_table[row[mode]] = {}
|
|
367
|
+
look_up_table[row[mode]] = {
|
|
368
|
+
"Energies": {},
|
|
369
|
+
"Limit": {
|
|
370
|
+
"Minimum": float(row[min_energy]),
|
|
371
|
+
"Maximum": float(row[max_energy]),
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# create polynomial object for energy to gap/phase
|
|
376
|
+
cof = [float(row[x]) for x in poly_deg]
|
|
377
|
+
poly = np.poly1d(cof)
|
|
378
|
+
|
|
379
|
+
look_up_table[row[mode]]["Energies"][row[min_energy]] = {
|
|
380
|
+
"Low": float(row[min_energy]),
|
|
381
|
+
"High": float(row[max_energy]),
|
|
382
|
+
"Poly": poly,
|
|
383
|
+
}
|
|
384
|
+
look_up_table[row[mode]]["Limit"]["Minimum"] = min(
|
|
385
|
+
look_up_table[row[mode]]["Limit"]["Minimum"], float(row[min_energy])
|
|
386
|
+
)
|
|
387
|
+
look_up_table[row[mode]]["Limit"]["Maximum"] = max(
|
|
388
|
+
look_up_table[row[mode]]["Limit"]["Maximum"], float(row[max_energy])
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
with open(file, newline="") as csvfile:
|
|
392
|
+
reader = csv.DictReader(csvfile)
|
|
393
|
+
for row in reader:
|
|
394
|
+
# If there are multiple source only convert requested.
|
|
395
|
+
if row[source[0]] == source[1]:
|
|
396
|
+
data2dict(row=row)
|
|
397
|
+
if not look_up_table:
|
|
398
|
+
raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
|
|
399
|
+
return look_up_table
|
dodal/devices/i22/dcm.py
CHANGED
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
import time
|
|
2
|
-
from collections.abc import Sequence
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Literal
|
|
5
2
|
|
|
3
|
+
import numpy as np
|
|
6
4
|
from bluesky.protocols import Reading
|
|
7
5
|
from event_model.documents.event_descriptor import DataKey
|
|
8
|
-
from ophyd_async.core import
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
Array1D,
|
|
8
|
+
StandardReadable,
|
|
9
|
+
StandardReadableFormat,
|
|
10
|
+
soft_signal_r_and_setter,
|
|
11
|
+
)
|
|
12
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
9
13
|
from ophyd_async.epics.motor import Motor
|
|
10
|
-
|
|
14
|
+
|
|
15
|
+
from dodal.common.crystal_metadata import CrystalMetadata
|
|
11
16
|
|
|
12
17
|
# Conversion constant for energy and wavelength, taken from the X-Ray data booklet
|
|
13
18
|
# Converts between energy in KeV and wavelength in angstrom
|
|
14
19
|
_CONVERSION_CONSTANT = 12.3984
|
|
15
20
|
|
|
16
21
|
|
|
17
|
-
@dataclass(frozen=True, unsafe_hash=True)
|
|
18
|
-
class CrystalMetadata:
|
|
19
|
-
"""
|
|
20
|
-
Metadata used in the NeXus format,
|
|
21
|
-
see https://manual.nexusformat.org/classes/base_classes/NXcrystal.html
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
usage: Literal["Bragg", "Laue"] | None = None
|
|
25
|
-
type: str | None = None
|
|
26
|
-
reflection: tuple[int, int, int] | None = None
|
|
27
|
-
d_spacing: tuple[float, str] | None = None
|
|
28
|
-
|
|
29
|
-
|
|
30
22
|
class DoubleCrystalMonochromator(StandardReadable):
|
|
31
23
|
"""
|
|
32
24
|
A double crystal monochromator (DCM), used to select the energy of the beam.
|
|
@@ -39,22 +31,21 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
39
31
|
|
|
40
32
|
def __init__(
|
|
41
33
|
self,
|
|
42
|
-
motion_prefix: str,
|
|
43
34
|
temperature_prefix: str,
|
|
44
|
-
crystal_1_metadata: CrystalMetadata
|
|
45
|
-
crystal_2_metadata: CrystalMetadata
|
|
35
|
+
crystal_1_metadata: CrystalMetadata,
|
|
36
|
+
crystal_2_metadata: CrystalMetadata,
|
|
46
37
|
prefix: str = "",
|
|
47
38
|
name: str = "",
|
|
48
39
|
) -> None:
|
|
49
40
|
with self.add_children_as_readables():
|
|
50
41
|
# Positionable Parameters
|
|
51
|
-
self.bragg = Motor(
|
|
52
|
-
self.offset = Motor(
|
|
53
|
-
self.perp = Motor(
|
|
54
|
-
self.energy = Motor(
|
|
55
|
-
self.crystal_1_roll = Motor(
|
|
56
|
-
self.crystal_2_roll = Motor(
|
|
57
|
-
self.crystal_2_pitch = Motor(
|
|
42
|
+
self.bragg = Motor(prefix + "BRAGG")
|
|
43
|
+
self.offset = Motor(prefix + "OFFSET")
|
|
44
|
+
self.perp = Motor(prefix + "PERP")
|
|
45
|
+
self.energy = Motor(prefix + "ENERGY")
|
|
46
|
+
self.crystal_1_roll = Motor(prefix + "XTAL1:ROLL")
|
|
47
|
+
self.crystal_2_roll = Motor(prefix + "XTAL2:ROLL")
|
|
48
|
+
self.crystal_2_pitch = Motor(prefix + "XTAL2:PITCH")
|
|
58
49
|
|
|
59
50
|
# Temperatures
|
|
60
51
|
self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
|
|
@@ -70,61 +61,37 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
70
61
|
|
|
71
62
|
# Soft metadata
|
|
72
63
|
# If supplied include crystal details in output of read_configuration
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
str, initial_value=crystal_2_metadata.usage
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
self.crystal_2_usage = None
|
|
108
|
-
if crystal_2_metadata.type is not None:
|
|
109
|
-
self.crystal_2_type, _ = soft_signal_r_and_setter(
|
|
110
|
-
str, initial_value=crystal_2_metadata.type
|
|
111
|
-
)
|
|
112
|
-
else:
|
|
113
|
-
self.crystal_2_type = None
|
|
114
|
-
if crystal_2_metadata.reflection is not None:
|
|
115
|
-
self.crystal_2_reflection, _ = soft_signal_r_and_setter(
|
|
116
|
-
Sequence[int], initial_value=list(crystal_2_metadata.reflection)
|
|
117
|
-
)
|
|
118
|
-
else:
|
|
119
|
-
self.crystal_2_reflection = None
|
|
120
|
-
if crystal_2_metadata.d_spacing is not None:
|
|
121
|
-
self.crystal_2_d_spacing, _ = soft_signal_r_and_setter(
|
|
122
|
-
float,
|
|
123
|
-
initial_value=crystal_2_metadata.d_spacing[0],
|
|
124
|
-
units=crystal_2_metadata.d_spacing[1],
|
|
125
|
-
)
|
|
126
|
-
else:
|
|
127
|
-
self.crystal_2_d_spacing = None
|
|
64
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
65
|
+
self.crystal_1_usage, _ = soft_signal_r_and_setter(
|
|
66
|
+
str, initial_value=crystal_1_metadata.usage
|
|
67
|
+
)
|
|
68
|
+
self.crystal_1_type, _ = soft_signal_r_and_setter(
|
|
69
|
+
str, initial_value=crystal_1_metadata.type
|
|
70
|
+
)
|
|
71
|
+
self.crystal_1_reflection, _ = soft_signal_r_and_setter(
|
|
72
|
+
Array1D[np.int32],
|
|
73
|
+
initial_value=np.array(crystal_1_metadata.reflection),
|
|
74
|
+
)
|
|
75
|
+
self.crystal_1_d_spacing, _ = soft_signal_r_and_setter(
|
|
76
|
+
float,
|
|
77
|
+
initial_value=crystal_1_metadata.d_spacing[0],
|
|
78
|
+
units=crystal_1_metadata.d_spacing[1],
|
|
79
|
+
)
|
|
80
|
+
self.crystal_2_usage, _ = soft_signal_r_and_setter(
|
|
81
|
+
str, initial_value=crystal_2_metadata.usage
|
|
82
|
+
)
|
|
83
|
+
self.crystal_2_type, _ = soft_signal_r_and_setter(
|
|
84
|
+
str, initial_value=crystal_2_metadata.type
|
|
85
|
+
)
|
|
86
|
+
self.crystal_2_reflection, _ = soft_signal_r_and_setter(
|
|
87
|
+
Array1D[np.int32],
|
|
88
|
+
initial_value=np.array(crystal_2_metadata.reflection),
|
|
89
|
+
)
|
|
90
|
+
self.crystal_2_d_spacing, _ = soft_signal_r_and_setter(
|
|
91
|
+
float,
|
|
92
|
+
initial_value=crystal_2_metadata.d_spacing[0],
|
|
93
|
+
units=crystal_2_metadata.d_spacing[1],
|
|
94
|
+
)
|
|
128
95
|
|
|
129
96
|
super().__init__(name)
|
|
130
97
|
|
dodal/devices/i22/fswitch.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
-
from enum import Enum
|
|
4
3
|
|
|
5
4
|
from bluesky.protocols import Reading
|
|
6
5
|
from event_model import DataKey
|
|
7
6
|
from ophyd_async.core import (
|
|
8
|
-
ConfigSignal,
|
|
9
7
|
DeviceVector,
|
|
10
8
|
StandardReadable,
|
|
9
|
+
StandardReadableFormat,
|
|
10
|
+
StrictEnum,
|
|
11
11
|
soft_signal_r_and_setter,
|
|
12
12
|
)
|
|
13
|
-
from ophyd_async.epics.
|
|
13
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class FilterState(
|
|
16
|
+
class FilterState(StrictEnum):
|
|
17
17
|
"""
|
|
18
18
|
Note that the in/out here refers to the internal rocker
|
|
19
19
|
position so a PV value of IN implies a filter OUT of beam
|
|
@@ -54,7 +54,7 @@ class FSwitch(StandardReadable):
|
|
|
54
54
|
for i in range(FSwitch.NUM_FILTERS)
|
|
55
55
|
}
|
|
56
56
|
)
|
|
57
|
-
with self.add_children_as_readables(
|
|
57
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
58
58
|
if lens_geometry is not None:
|
|
59
59
|
self.lens_geometry, _ = soft_signal_r_and_setter(
|
|
60
60
|
str, initial_value=lens_geometry
|
dodal/devices/i24/aperture.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from ophyd_async.core import StandardReadable
|
|
1
|
+
from ophyd_async.core import StandardReadable, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
4
3
|
from ophyd_async.epics.motor import Motor
|
|
5
|
-
from ophyd_async.epics.signal import epics_signal_rw
|
|
6
4
|
|
|
7
5
|
|
|
8
|
-
class AperturePositions(
|
|
6
|
+
class AperturePositions(StrictEnum):
|
|
9
7
|
IN = "In"
|
|
10
8
|
OUT = "Out"
|
|
11
9
|
ROBOT = "Robot"
|
dodal/devices/i24/beamstop.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from ophyd_async.core import StandardReadable
|
|
1
|
+
from ophyd_async.core import StandardReadable, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
4
3
|
from ophyd_async.epics.motor import Motor
|
|
5
|
-
from ophyd_async.epics.signal import epics_signal_rw
|
|
6
4
|
|
|
7
5
|
|
|
8
|
-
class BeamstopPositions(
|
|
6
|
+
class BeamstopPositions(StrictEnum):
|
|
9
7
|
CHECK_BEAM = "CheckBeam"
|
|
10
8
|
DATA_COLLECTION = "Data Collection"
|
|
11
9
|
DATA_COLLECTION_FAR = "Data Collection Far"
|