dls-dodal 1.65.0__py3-none-any.whl → 1.67.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.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +102 -198
- dodal/beamlines/i04.py +40 -4
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +32 -3
- dodal/beamlines/i09_2.py +57 -2
- dodal/beamlines/i10_optics.py +46 -17
- dodal/beamlines/i17.py +7 -3
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i19_1.py +26 -14
- dodal/beamlines/i19_2.py +49 -38
- dodal/beamlines/i21.py +2 -2
- dodal/beamlines/i22.py +19 -4
- dodal/beamlines/p38.py +3 -3
- dodal/beamlines/training_rig.py +0 -16
- dodal/cli.py +26 -12
- dodal/common/coordination.py +3 -2
- dodal/device_manager.py +604 -0
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/cryostream.py +28 -57
- dodal/devices/detector/det_resolution.py +4 -2
- dodal/devices/eiger.py +26 -18
- dodal/devices/fast_grid_scan.py +14 -2
- dodal/devices/i03/beamsize.py +35 -0
- dodal/devices/i03/constants.py +7 -0
- dodal/devices/i03/undulator_dcm.py +2 -2
- dodal/devices/i04/beamsize.py +45 -0
- dodal/devices/i04/max_pixel.py +38 -0
- dodal/devices/i04/murko_results.py +36 -26
- dodal/devices/i04/transfocator.py +23 -29
- dodal/devices/i07/id.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +13 -2
- dodal/devices/i09_1_shared/hard_energy.py +112 -0
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i09_2_shared/__init__.py +0 -0
- dodal/devices/i09_2_shared/i09_apple2.py +86 -0
- dodal/devices/i10/i10_apple2.py +39 -331
- dodal/devices/i17/i17_apple2.py +37 -22
- dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
- dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
- dodal/devices/i19/access_controlled/shutter.py +2 -4
- dodal/devices/insertion_device/__init__.py +0 -0
- dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
- dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
- dodal/devices/insertion_device/lookup_table_models.py +287 -0
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +33 -3
- dodal/devices/mx_phase1/beamstop.py +31 -12
- dodal/devices/oav/oav_calculations.py +9 -4
- dodal/devices/oav/oav_detector.py +65 -7
- dodal/devices/oav/oav_parameters.py +3 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
- dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
- dodal/devices/oav/pin_image_recognition/utils.py +23 -1
- dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
- dodal/devices/oav/utils.py +16 -6
- dodal/devices/robot.py +33 -18
- dodal/devices/scintillator.py +36 -14
- dodal/devices/smargon.py +2 -3
- dodal/devices/thawer.py +7 -45
- dodal/devices/undulator.py +152 -68
- dodal/plans/__init__.py +1 -1
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/testing/fixtures/devices/__init__.py +0 -0
- dodal/testing/fixtures/devices/apple2.py +78 -0
- dodal/utils.py +6 -3
- dodal/beamline_specific_utils/i03.py +0 -17
- dodal/testing/__init__.py +0 -3
- dodal/testing/setup.py +0 -67
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
- /dodal/plans/{scanspec.py → spec_path.py} +0 -0
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import io
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any, SupportsFloat
|
|
1
|
+
from typing import SupportsFloat
|
|
6
2
|
|
|
7
3
|
import numpy as np
|
|
8
4
|
from bluesky.protocols import Movable
|
|
9
|
-
from daq_config_server.client import ConfigServer
|
|
10
5
|
from ophyd_async.core import (
|
|
11
6
|
AsyncStatus,
|
|
12
7
|
Reference,
|
|
@@ -15,308 +10,30 @@ from ophyd_async.core import (
|
|
|
15
10
|
derived_signal_rw,
|
|
16
11
|
soft_signal_rw,
|
|
17
12
|
)
|
|
18
|
-
from pydantic import BaseModel, ConfigDict, RootModel
|
|
19
13
|
|
|
20
|
-
from dodal.devices.apple2_undulator import (
|
|
14
|
+
from dodal.devices.insertion_device.apple2_undulator import (
|
|
15
|
+
MAXIMUM_MOVE_TIME,
|
|
21
16
|
Apple2,
|
|
22
17
|
Apple2Controller,
|
|
18
|
+
Apple2PhasesVal,
|
|
23
19
|
Apple2Val,
|
|
24
20
|
Pol,
|
|
25
21
|
UndulatorGap,
|
|
26
22
|
UndulatorJawPhase,
|
|
27
23
|
UndulatorPhaseAxes,
|
|
28
24
|
)
|
|
29
|
-
from dodal.
|
|
25
|
+
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
30
26
|
|
|
31
27
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
32
28
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
33
29
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
34
30
|
DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
|
|
35
31
|
ALPHA_OFFSET = 180
|
|
36
|
-
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
37
32
|
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class LookupPath:
|
|
42
|
-
Gap: Path
|
|
43
|
-
Phase: Path
|
|
34
|
+
class I10Apple2(Apple2[UndulatorPhaseAxes]):
|
|
35
|
+
"""I10Apple2 device is an apple2 with extra jaw phase motor."""
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
@dataclass
|
|
47
|
-
class LookupTableConfig:
|
|
48
|
-
path: LookupPath
|
|
49
|
-
source: tuple[str, str]
|
|
50
|
-
mode: str | None
|
|
51
|
-
min_energy: str | None
|
|
52
|
-
max_energy: str | None
|
|
53
|
-
poly_deg: list | None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class EnergyMinMax(BaseModel):
|
|
57
|
-
Minimum: float
|
|
58
|
-
Maximum: float
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class EnergyCoverageEntry(BaseModel):
|
|
62
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
63
|
-
Low: float
|
|
64
|
-
High: float
|
|
65
|
-
Poly: np.poly1d
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class EnergyCoverage(RootModel):
|
|
69
|
-
root: dict[str, EnergyCoverageEntry]
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class LookupTableEntries(BaseModel):
|
|
73
|
-
Energies: EnergyCoverage
|
|
74
|
-
Limit: EnergyMinMax
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class Lookuptable(RootModel):
|
|
78
|
-
"""BaseModel class for the lookup table.
|
|
79
|
-
Apple2 lookup table should be in this format.
|
|
80
|
-
|
|
81
|
-
{mode: {'Energies': {Any: {'Low': float,
|
|
82
|
-
'High': float,
|
|
83
|
-
'Poly':np.poly1d
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
'Limit': {'Minimum': float,
|
|
87
|
-
'Maximum': float
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
root: dict[str, LookupTableEntries]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class I10EnergyMotorLookup:
|
|
97
|
-
"""
|
|
98
|
-
Handles lookup tables for I10 Apple2 ID, converting energy and polarisation to gap
|
|
99
|
-
and phase. Fetches and parses lookup tables from a config server, supports dynamic
|
|
100
|
-
updates, and validates input.
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
def __init__(
|
|
104
|
-
self,
|
|
105
|
-
lookuptable_dir: str,
|
|
106
|
-
source: tuple[str, str],
|
|
107
|
-
config_client: ConfigServer,
|
|
108
|
-
mode: str = "Mode",
|
|
109
|
-
min_energy: str = "MinEnergy",
|
|
110
|
-
max_energy: str = "MaxEnergy",
|
|
111
|
-
gap_file_name: str = "IDEnergy2GapCalibrations.csv",
|
|
112
|
-
phase_file_name: str = "IDEnergy2PhaseCalibrations.csv",
|
|
113
|
-
poly_deg: list | None = None,
|
|
114
|
-
):
|
|
115
|
-
"""Initialise the I10EnergyMotorLookup class with lookup table headers provided.
|
|
116
|
-
|
|
117
|
-
Parameters
|
|
118
|
-
----------
|
|
119
|
-
look_up_table_dir:
|
|
120
|
-
The path to look up table.
|
|
121
|
-
source:
|
|
122
|
-
The column name and the name of the source in look up table. e.g. ( "source", "idu")
|
|
123
|
-
config_client:
|
|
124
|
-
The config server client to fetch the look up table.
|
|
125
|
-
mode:
|
|
126
|
-
The column name of the mode in look up table.
|
|
127
|
-
min_energy:
|
|
128
|
-
The column name that contain the maximum energy in look up table.
|
|
129
|
-
max_energy:
|
|
130
|
-
The column name that contain the maximum energy in look up table.
|
|
131
|
-
poly_deg:
|
|
132
|
-
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
133
|
-
|
|
134
|
-
"""
|
|
135
|
-
self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
|
|
136
|
-
"Gap": {},
|
|
137
|
-
"Phase": {},
|
|
138
|
-
}
|
|
139
|
-
energy_gap_table_path = Path(lookuptable_dir, gap_file_name)
|
|
140
|
-
energy_phase_table_path = Path(lookuptable_dir, phase_file_name)
|
|
141
|
-
self.lookup_table_config = LookupTableConfig(
|
|
142
|
-
path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
|
|
143
|
-
source=source,
|
|
144
|
-
mode=mode,
|
|
145
|
-
min_energy=min_energy,
|
|
146
|
-
max_energy=max_energy,
|
|
147
|
-
poly_deg=poly_deg,
|
|
148
|
-
)
|
|
149
|
-
self.config_client = config_client
|
|
150
|
-
self._available_pol = []
|
|
151
|
-
|
|
152
|
-
@property
|
|
153
|
-
def available_pol(self) -> list[str | None]:
|
|
154
|
-
return self._available_pol
|
|
155
|
-
|
|
156
|
-
@available_pol.setter
|
|
157
|
-
def available_pol(self, value: list[str | None]) -> None:
|
|
158
|
-
self._available_pol = value
|
|
159
|
-
|
|
160
|
-
def update_lookuptable(self):
|
|
161
|
-
"""
|
|
162
|
-
Update lookup tables from files and validate their format.
|
|
163
|
-
"""
|
|
164
|
-
LOGGER.info("Updating lookup dictionary from file.")
|
|
165
|
-
for key, path in self.lookup_table_config.path.__dict__.items():
|
|
166
|
-
self.lookup_tables[key] = self.convert_csv_to_lookup(
|
|
167
|
-
file=path,
|
|
168
|
-
source=self.lookup_table_config.source,
|
|
169
|
-
mode=self.lookup_table_config.mode,
|
|
170
|
-
min_energy=self.lookup_table_config.min_energy,
|
|
171
|
-
max_energy=self.lookup_table_config.max_energy,
|
|
172
|
-
poly_deg=self.lookup_table_config.poly_deg,
|
|
173
|
-
)
|
|
174
|
-
Lookuptable.model_validate(self.lookup_tables[key])
|
|
175
|
-
|
|
176
|
-
self.available_pol = list(self.lookup_tables["Gap"].keys())
|
|
177
|
-
|
|
178
|
-
def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
|
|
179
|
-
"""
|
|
180
|
-
Convert energy and polarisation to gap and phase motor positions.
|
|
181
|
-
|
|
182
|
-
Parameters
|
|
183
|
-
----------
|
|
184
|
-
energy : float
|
|
185
|
-
Desired energy in eV.
|
|
186
|
-
pol : Pol
|
|
187
|
-
Polarisation mode.
|
|
188
|
-
|
|
189
|
-
Returns
|
|
190
|
-
-------
|
|
191
|
-
tuple[float, float]
|
|
192
|
-
(gap, phase) motor positions.
|
|
193
|
-
|
|
194
|
-
"""
|
|
195
|
-
if self.available_pol == []:
|
|
196
|
-
self.update_lookuptable()
|
|
197
|
-
|
|
198
|
-
gap_poly = self._get_poly(
|
|
199
|
-
lookup_table=self.lookup_tables["Gap"], energy=energy, pol=pol
|
|
200
|
-
)
|
|
201
|
-
phase_poly = self._get_poly(
|
|
202
|
-
lookup_table=self.lookup_tables["Phase"], energy=energy, pol=pol
|
|
203
|
-
)
|
|
204
|
-
return gap_poly(energy), phase_poly(energy)
|
|
205
|
-
|
|
206
|
-
def _get_poly(
|
|
207
|
-
self,
|
|
208
|
-
energy: float,
|
|
209
|
-
pol: Pol,
|
|
210
|
-
lookup_table: dict[str | None, dict[str, dict[str, Any]]],
|
|
211
|
-
) -> np.poly1d:
|
|
212
|
-
"""
|
|
213
|
-
Get polynomial for a given energy and polarisation.
|
|
214
|
-
|
|
215
|
-
Raises
|
|
216
|
-
------
|
|
217
|
-
ValueError
|
|
218
|
-
If energy is out of bounds or coefficients are missing.
|
|
219
|
-
"""
|
|
220
|
-
if (
|
|
221
|
-
energy < lookup_table[pol]["Limit"]["Minimum"]
|
|
222
|
-
or energy > lookup_table[pol]["Limit"]["Maximum"]
|
|
223
|
-
):
|
|
224
|
-
raise ValueError(
|
|
225
|
-
"Demanding energy must lie between {} and {} eV!".format(
|
|
226
|
-
lookup_table[pol]["Limit"]["Minimum"],
|
|
227
|
-
lookup_table[pol]["Limit"]["Maximum"],
|
|
228
|
-
)
|
|
229
|
-
)
|
|
230
|
-
else:
|
|
231
|
-
for energy_range in lookup_table[pol]["Energies"].values():
|
|
232
|
-
if energy >= energy_range["Low"] and energy < energy_range["High"]:
|
|
233
|
-
return energy_range["Poly"]
|
|
234
|
-
|
|
235
|
-
raise ValueError(
|
|
236
|
-
"""Cannot find polynomial coefficients for your requested energy.
|
|
237
|
-
There might be gap in the calibration lookup table."""
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
def convert_csv_to_lookup(
|
|
241
|
-
self,
|
|
242
|
-
file: str,
|
|
243
|
-
source: tuple[str, str],
|
|
244
|
-
mode: str | None = "Mode",
|
|
245
|
-
min_energy: str | None = "MinEnergy",
|
|
246
|
-
max_energy: str | None = "MaxEnergy",
|
|
247
|
-
poly_deg: list | None = None,
|
|
248
|
-
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
249
|
-
"""
|
|
250
|
-
Convert a CSV file to a lookup table dictionary.
|
|
251
|
-
|
|
252
|
-
Returns
|
|
253
|
-
-------
|
|
254
|
-
dict
|
|
255
|
-
Dictionary in Apple2 lookup table format.
|
|
256
|
-
|
|
257
|
-
Raises
|
|
258
|
-
------
|
|
259
|
-
RuntimeError
|
|
260
|
-
If the CSV cannot be converted.
|
|
261
|
-
|
|
262
|
-
"""
|
|
263
|
-
if poly_deg is None:
|
|
264
|
-
poly_deg = [
|
|
265
|
-
"7th-order",
|
|
266
|
-
"6th-order",
|
|
267
|
-
"5th-order",
|
|
268
|
-
"4th-order",
|
|
269
|
-
"3rd-order",
|
|
270
|
-
"2nd-order",
|
|
271
|
-
"1st-order",
|
|
272
|
-
"b",
|
|
273
|
-
]
|
|
274
|
-
lookup_table = {}
|
|
275
|
-
polarisations = set()
|
|
276
|
-
|
|
277
|
-
def process_row(row: dict) -> None:
|
|
278
|
-
"""Process a single row from the CSV file and update the lookup table."""
|
|
279
|
-
mode_value = row[mode]
|
|
280
|
-
if mode_value not in polarisations:
|
|
281
|
-
polarisations.add(mode_value)
|
|
282
|
-
lookup_table[mode_value] = {
|
|
283
|
-
"Energies": {},
|
|
284
|
-
"Limit": {
|
|
285
|
-
"Minimum": float(row[min_energy]),
|
|
286
|
-
"Maximum": float(row[max_energy]),
|
|
287
|
-
},
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
# Create polynomial object for energy-to-gap/phase conversion
|
|
291
|
-
coefficients = [float(row[coef]) for coef in poly_deg]
|
|
292
|
-
polynomial = np.poly1d(coefficients)
|
|
293
|
-
|
|
294
|
-
lookup_table[mode_value]["Energies"][row[min_energy]] = {
|
|
295
|
-
"Low": float(row[min_energy]),
|
|
296
|
-
"High": float(row[max_energy]),
|
|
297
|
-
"Poly": polynomial,
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
# Update energy limits
|
|
301
|
-
lookup_table[mode_value]["Limit"]["Minimum"] = min(
|
|
302
|
-
lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
|
|
303
|
-
)
|
|
304
|
-
lookup_table[mode_value]["Limit"]["Maximum"] = max(
|
|
305
|
-
lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
csv_file = self.config_client.get_file_contents(file, reset_cached_result=True)
|
|
309
|
-
reader = csv.DictReader(io.StringIO(csv_file))
|
|
310
|
-
for row in reader:
|
|
311
|
-
# If there are multiple source only convert requested.
|
|
312
|
-
if row[source[0]] == source[1]:
|
|
313
|
-
process_row(row=row)
|
|
314
|
-
if not lookup_table:
|
|
315
|
-
raise RuntimeError(f"Unable to convert lookup table:\t{file}")
|
|
316
|
-
return lookup_table
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
class I10Apple2(Apple2):
|
|
320
37
|
def __init__(
|
|
321
38
|
self,
|
|
322
39
|
id_gap: UndulatorGap,
|
|
@@ -325,11 +42,8 @@ class I10Apple2(Apple2):
|
|
|
325
42
|
name: str = "",
|
|
326
43
|
) -> None:
|
|
327
44
|
"""
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Parameters
|
|
331
|
-
----------
|
|
332
|
-
|
|
45
|
+
Parameters:
|
|
46
|
+
------------
|
|
333
47
|
id_gap : UndulatorJawPhase
|
|
334
48
|
The gap motor of the undulator.
|
|
335
49
|
id_phase : UndulatorJawPhase
|
|
@@ -347,53 +61,49 @@ class I10Apple2(Apple2):
|
|
|
347
61
|
class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
348
62
|
"""
|
|
349
63
|
I10Apple2Controller is a extension of Apple2Controller which provide linear
|
|
350
|
-
|
|
64
|
+
arbitrary angle control.
|
|
351
65
|
"""
|
|
352
66
|
|
|
353
67
|
def __init__(
|
|
354
68
|
self,
|
|
355
69
|
apple2: I10Apple2,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
config_client: ConfigServer,
|
|
70
|
+
gap_energy_motor_lut: EnergyMotorLookup,
|
|
71
|
+
phase_energy_motor_lut: EnergyMotorLookup,
|
|
359
72
|
jaw_phase_limit: float = 12.0,
|
|
360
73
|
jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
361
74
|
angle_threshold_deg=30.0,
|
|
75
|
+
units: str = "eV",
|
|
362
76
|
name: str = "",
|
|
363
77
|
) -> None:
|
|
364
78
|
"""
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
----------
|
|
79
|
+
Parameters:
|
|
80
|
+
-----------
|
|
368
81
|
apple2 : I10Apple2
|
|
369
82
|
An I10Apple2 device.
|
|
370
|
-
|
|
371
|
-
The
|
|
372
|
-
|
|
373
|
-
The
|
|
374
|
-
config_client : ConfigServer
|
|
375
|
-
The config server client to fetch the look up table.
|
|
83
|
+
gap_energy_motor_lut: EnergyMotorLookup
|
|
84
|
+
The class that handles the gap look up table logic for the insertion device.
|
|
85
|
+
phase_energy_motor_lut: EnergyMotorLookup
|
|
86
|
+
The class that handles the phase look up table logic for the insertion device.
|
|
376
87
|
jaw_phase_limit : float, optional
|
|
377
88
|
The maximum allowed jaw_phase movement., by default 12.0
|
|
378
89
|
jaw_phase_poly_param : list[float], optional
|
|
379
90
|
polynomial parameters highest power first., by default DEFAULT_JAW_PHASE_POLY_PARAMS
|
|
380
91
|
angle_threshold_deg : float, optional
|
|
381
92
|
The angle threshold to switch between 0-180 and 180-360 range., by default 30.0
|
|
93
|
+
units:
|
|
94
|
+
the units of this device. Defaults to eV.
|
|
382
95
|
name : str, optional
|
|
383
96
|
New device name.
|
|
384
97
|
"""
|
|
385
|
-
|
|
386
|
-
self.
|
|
387
|
-
lookuptable_dir=lookuptable_dir,
|
|
388
|
-
source=source,
|
|
389
|
-
config_client=config_client,
|
|
390
|
-
)
|
|
98
|
+
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
99
|
+
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
391
100
|
super().__init__(
|
|
392
101
|
apple2=apple2,
|
|
393
|
-
|
|
102
|
+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
103
|
+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
104
|
+
units=units,
|
|
394
105
|
name=name,
|
|
395
106
|
)
|
|
396
|
-
|
|
397
107
|
self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
|
|
398
108
|
self.angle_threshold_deg = angle_threshold_deg
|
|
399
109
|
self.jaw_phase_limit = jaw_phase_limit
|
|
@@ -428,24 +138,22 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
428
138
|
await self.apple2().jaw_phase().set(jaw_phase)
|
|
429
139
|
await self._linear_arbitrary_angle.set(pol_angle)
|
|
430
140
|
|
|
431
|
-
|
|
432
|
-
"""
|
|
433
|
-
Set the undulator motors for a given energy and polarisation.
|
|
434
|
-
"""
|
|
435
|
-
|
|
436
|
-
pol = await self._check_and_get_pol_setpoint()
|
|
437
|
-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
141
|
+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
438
142
|
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
439
|
-
|
|
440
|
-
top_outer=f"{phase:.6f}",
|
|
441
|
-
top_inner="0.0",
|
|
442
|
-
btm_inner=f"{phase3:.6f}",
|
|
443
|
-
btm_outer="0.0",
|
|
143
|
+
return Apple2Val(
|
|
444
144
|
gap=f"{gap:.6f}",
|
|
145
|
+
phase=Apple2PhasesVal(
|
|
146
|
+
top_outer=f"{phase:.6f}",
|
|
147
|
+
top_inner="0.0",
|
|
148
|
+
btm_inner=f"{phase3:.6f}",
|
|
149
|
+
btm_outer="0.0",
|
|
150
|
+
),
|
|
445
151
|
)
|
|
446
152
|
|
|
447
|
-
|
|
448
|
-
|
|
153
|
+
async def _set_motors_from_energy_and_polarisation(
|
|
154
|
+
self, energy: float, pol: Pol
|
|
155
|
+
) -> None:
|
|
156
|
+
await super()._set_motors_from_energy_and_polarisation(energy, pol)
|
|
449
157
|
if pol != Pol.LA:
|
|
450
158
|
await self.apple2().jaw_phase().set(0)
|
|
451
159
|
await self.apple2().jaw_phase().set_move.set(1)
|
|
@@ -486,4 +194,4 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
|
486
194
|
|
|
487
195
|
@AsyncStatus.wrap
|
|
488
196
|
async def set(self, angle: float) -> None:
|
|
489
|
-
await self.linear_arbitrary_angle().set(angle)
|
|
197
|
+
await self.linear_arbitrary_angle().set(angle, timeout=MAXIMUM_MOVE_TIME)
|
dodal/devices/i17/i17_apple2.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
from dodal.devices.apple2_undulator import (
|
|
1
|
+
from dodal.devices.insertion_device.apple2_undulator import (
|
|
2
2
|
Apple2,
|
|
3
3
|
Apple2Controller,
|
|
4
|
+
Apple2PhasesVal,
|
|
4
5
|
Apple2Val,
|
|
5
|
-
|
|
6
|
+
Pol,
|
|
7
|
+
UndulatorPhaseAxes,
|
|
6
8
|
)
|
|
7
|
-
from dodal.
|
|
9
|
+
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
8
10
|
|
|
9
11
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
10
12
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -14,7 +16,7 @@ ALPHA_OFFSET = 180
|
|
|
14
16
|
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
19
|
+
class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
|
|
18
20
|
"""
|
|
19
21
|
I10Apple2Controller is a extension of Apple2Controller which provide linear
|
|
20
22
|
arbitrary angle control.
|
|
@@ -22,30 +24,43 @@ class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
|
22
24
|
|
|
23
25
|
def __init__(
|
|
24
26
|
self,
|
|
25
|
-
apple2: Apple2,
|
|
26
|
-
|
|
27
|
+
apple2: Apple2[UndulatorPhaseAxes],
|
|
28
|
+
gap_energy_motor_lut: EnergyMotorLookup,
|
|
29
|
+
phase_energy_motor_lut: EnergyMotorLookup,
|
|
30
|
+
units: str = "eV",
|
|
27
31
|
name: str = "",
|
|
28
32
|
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Parameters:
|
|
35
|
+
-----------
|
|
36
|
+
apple2 : Apple2
|
|
37
|
+
An Apple2 device.
|
|
38
|
+
gap_energy_motor_lut: EnergyMotorLookup
|
|
39
|
+
The class that handles the gap look up table logic for the insertion device.
|
|
40
|
+
phase_energy_motor_lut: EnergyMotorLookup
|
|
41
|
+
The class that handles the phase look up table logic for the insertion device.
|
|
42
|
+
units:
|
|
43
|
+
the units of this device. Defaults to eV.
|
|
44
|
+
name : str, optional
|
|
45
|
+
New device name.
|
|
46
|
+
"""
|
|
47
|
+
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
48
|
+
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
29
49
|
super().__init__(
|
|
30
50
|
apple2=apple2,
|
|
31
|
-
|
|
51
|
+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
52
|
+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
53
|
+
units=units,
|
|
32
54
|
name=name,
|
|
33
55
|
)
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Set the undulator motors for a given energy and polarisation.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
pol = await self._check_and_get_pol_setpoint()
|
|
41
|
-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
42
|
-
id_set_val = Apple2Val(
|
|
43
|
-
top_outer=f"{phase:.6f}",
|
|
44
|
-
top_inner="0.0",
|
|
45
|
-
btm_inner=f"{phase:.6f}",
|
|
46
|
-
btm_outer="0.0",
|
|
57
|
+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
58
|
+
return Apple2Val(
|
|
47
59
|
gap=f"{gap:.6f}",
|
|
60
|
+
phase=Apple2PhasesVal(
|
|
61
|
+
top_outer=f"{phase:.6f}",
|
|
62
|
+
top_inner=f"{0.0:.6f}",
|
|
63
|
+
btm_inner=f"{phase:.6f}",
|
|
64
|
+
btm_outer=f"{0.0:.6f}",
|
|
65
|
+
),
|
|
48
66
|
)
|
|
49
|
-
|
|
50
|
-
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
|
51
|
-
await self.apple2().set(id_motor_values=id_set_val)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Annotated, Any, Self
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import AsyncStatus
|
|
4
|
+
from pydantic import BaseModel, model_validator
|
|
5
|
+
from pydantic.types import PositiveInt, StringConstraints
|
|
6
|
+
|
|
7
|
+
from dodal.devices.i19.access_controlled.blueapi_device import (
|
|
8
|
+
OpticsBlueAPIDevice,
|
|
9
|
+
)
|
|
10
|
+
from dodal.devices.i19.access_controlled.hutch_access import ACCESS_DEVICE_NAME
|
|
11
|
+
|
|
12
|
+
PermittedKeyStr = Annotated[str, StringConstraints(pattern="^[A-Za-z0-9-_]*$")]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AttenuatorMotorPositionDemands(BaseModel):
|
|
16
|
+
continuous_demands: dict[PermittedKeyStr, float] = {}
|
|
17
|
+
indexed_demands: dict[PermittedKeyStr, PositiveInt] = {}
|
|
18
|
+
|
|
19
|
+
@model_validator(mode="after")
|
|
20
|
+
def no_keys_clash(self) -> Self:
|
|
21
|
+
common_keys = set(self.continuous_demands).intersection(self.indexed_demands)
|
|
22
|
+
common_key_count = sum(1 for _ in common_keys)
|
|
23
|
+
if common_key_count < 1:
|
|
24
|
+
return self
|
|
25
|
+
else:
|
|
26
|
+
ks: str = "key" if common_key_count == 1 else "keys"
|
|
27
|
+
error_msg = f"Common {ks} found in distinct motor demands: {common_keys}"
|
|
28
|
+
raise ValueError(error_msg)
|
|
29
|
+
|
|
30
|
+
def validated_complete_demand(self) -> dict[PermittedKeyStr, Any]:
|
|
31
|
+
return self.continuous_demands | self.indexed_demands
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AttenuatorMotorSquad(OpticsBlueAPIDevice):
|
|
35
|
+
""" I19-specific proxy device which requests absorber position changes in the x-ray attenuator.
|
|
36
|
+
|
|
37
|
+
Sends REST call to blueapi controlling optics on the I19 cluster.
|
|
38
|
+
The hutch in use is compared against the hutch which sent the REST call.
|
|
39
|
+
Only the hutch in use will be permitted to execute a plan (requesting motor moves).
|
|
40
|
+
As the two hutches are located in series, checking the hutch in use is necessary to \
|
|
41
|
+
avoid accidentally operating optics devices from one hutch while the other has beam time.
|
|
42
|
+
|
|
43
|
+
The name of the hutch that wants to operate the optics device is passed to the \
|
|
44
|
+
access controlled device upon instantiation of the latter.
|
|
45
|
+
|
|
46
|
+
For details see the architecture described in \
|
|
47
|
+
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@AsyncStatus.wrap
|
|
51
|
+
async def set(self, value: AttenuatorMotorPositionDemands):
|
|
52
|
+
request_params = {
|
|
53
|
+
"name": "operate_motor_squad_plan",
|
|
54
|
+
"params": {
|
|
55
|
+
"experiment_hutch": self._invoking_hutch,
|
|
56
|
+
"access_device": ACCESS_DEVICE_NAME,
|
|
57
|
+
"attenuator_demands": value.validated_complete_demand(),
|
|
58
|
+
},
|
|
59
|
+
"instrument_session": self.instrument_session,
|
|
60
|
+
}
|
|
61
|
+
await super().set(request_params)
|
|
@@ -29,11 +29,19 @@ class OpticsBlueAPIDevice(StandardReadable, Movable[D]):
|
|
|
29
29
|
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self, hutch: HutchState, instrument_session: str = "", name: str = ""
|
|
34
|
+
) -> None:
|
|
35
|
+
self.hutch_request = hutch
|
|
36
|
+
self.instrument_session = instrument_session
|
|
33
37
|
self.url = OPTICS_BLUEAPI_URL
|
|
34
38
|
self.headers = HEADERS
|
|
35
39
|
super().__init__(name)
|
|
36
40
|
|
|
41
|
+
@property
|
|
42
|
+
def _invoking_hutch(self) -> str:
|
|
43
|
+
return self.hutch_request.value
|
|
44
|
+
|
|
37
45
|
@AsyncStatus.wrap
|
|
38
46
|
async def set(self, value: D):
|
|
39
47
|
""" On set send a POST request to the optics blueapi with the name and \
|
|
@@ -36,16 +36,14 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
|
|
|
36
36
|
# see https://github.com/DiamondLightSource/blueapi/issues/1187
|
|
37
37
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
38
38
|
self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
|
|
39
|
-
|
|
40
|
-
self.instrument_session = instrument_session
|
|
41
|
-
super().__init__(name)
|
|
39
|
+
super().__init__(hutch=hutch, instrument_session=instrument_session, name=name)
|
|
42
40
|
|
|
43
41
|
@AsyncStatus.wrap
|
|
44
42
|
async def set(self, value: ShutterDemand):
|
|
45
43
|
request_params = {
|
|
46
44
|
"name": "operate_shutter_plan",
|
|
47
45
|
"params": {
|
|
48
|
-
"experiment_hutch": self.
|
|
46
|
+
"experiment_hutch": self._invoking_hutch,
|
|
49
47
|
"access_device": ACCESS_DEVICE_NAME,
|
|
50
48
|
"shutter_demand": value.value,
|
|
51
49
|
},
|
|
File without changes
|