dls-dodal 1.65.0__py3-none-any.whl → 1.66.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.66.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +56 -50
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +23 -3
- dodal/beamlines/i04.py +18 -3
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +7 -2
- dodal/beamlines/i10_optics.py +18 -8
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i22.py +3 -3
- dodal/beamlines/p38.py +3 -3
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/apple2_undulator.py +89 -44
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/detector/det_resolution.py +4 -2
- 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/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 +6 -2
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i10/i10_apple2.py +22 -316
- dodal/devices/i17/i17_apple2.py +7 -4
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +19 -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 +17 -7
- 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/devices/util/lookup_tables_apple2.py +390 -0
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- 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.66.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +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,17 +10,19 @@ 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
14
|
from dodal.devices.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
|
)
|
|
25
|
+
from dodal.devices.util.lookup_tables_apple2 import EnergyMotorLookup
|
|
29
26
|
from dodal.log import LOGGER
|
|
30
27
|
|
|
31
28
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
@@ -33,290 +30,11 @@ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
|
33
30
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
34
31
|
DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
|
|
35
32
|
ALPHA_OFFSET = 180
|
|
36
|
-
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
37
33
|
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class LookupPath:
|
|
42
|
-
Gap: Path
|
|
43
|
-
Phase: Path
|
|
35
|
+
class I10Apple2(Apple2[UndulatorPhaseAxes]):
|
|
36
|
+
"""I10Apple2 device is an apple2 with extra jaw phase motor."""
|
|
44
37
|
|
|
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
38
|
def __init__(
|
|
321
39
|
self,
|
|
322
40
|
id_gap: UndulatorGap,
|
|
@@ -325,11 +43,8 @@ class I10Apple2(Apple2):
|
|
|
325
43
|
name: str = "",
|
|
326
44
|
) -> None:
|
|
327
45
|
"""
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Parameters
|
|
331
|
-
----------
|
|
332
|
-
|
|
46
|
+
Parameters:
|
|
47
|
+
------------
|
|
333
48
|
id_gap : UndulatorJawPhase
|
|
334
49
|
The gap motor of the undulator.
|
|
335
50
|
id_phase : UndulatorJawPhase
|
|
@@ -353,26 +68,19 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
353
68
|
def __init__(
|
|
354
69
|
self,
|
|
355
70
|
apple2: I10Apple2,
|
|
356
|
-
|
|
357
|
-
source: tuple[str, str],
|
|
358
|
-
config_client: ConfigServer,
|
|
71
|
+
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,
|
|
362
75
|
name: str = "",
|
|
363
76
|
) -> None:
|
|
364
77
|
"""
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
----------
|
|
78
|
+
Parameters:
|
|
79
|
+
-----------
|
|
368
80
|
apple2 : I10Apple2
|
|
369
81
|
An I10Apple2 device.
|
|
370
|
-
|
|
371
|
-
The
|
|
372
|
-
source : tuple[str, str]
|
|
373
|
-
The column name and the name of the source in look up table. e.g. ( "source", "idu")
|
|
374
|
-
config_client : ConfigServer
|
|
375
|
-
The config server client to fetch the look up table.
|
|
82
|
+
energy_motor_lut: EnergyMotorLookup
|
|
83
|
+
The class that handles the look up table logic for the insertion device.
|
|
376
84
|
jaw_phase_limit : float, optional
|
|
377
85
|
The maximum allowed jaw_phase movement., by default 12.0
|
|
378
86
|
jaw_phase_poly_param : list[float], optional
|
|
@@ -383,14 +91,10 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
383
91
|
New device name.
|
|
384
92
|
"""
|
|
385
93
|
|
|
386
|
-
self.
|
|
387
|
-
lookuptable_dir=lookuptable_dir,
|
|
388
|
-
source=source,
|
|
389
|
-
config_client=config_client,
|
|
390
|
-
)
|
|
94
|
+
self.energy_motor_lut = energy_motor_lut
|
|
391
95
|
super().__init__(
|
|
392
96
|
apple2=apple2,
|
|
393
|
-
energy_to_motor_converter=self.
|
|
97
|
+
energy_to_motor_converter=self.energy_motor_lut.get_motor_from_energy,
|
|
394
98
|
name=name,
|
|
395
99
|
)
|
|
396
100
|
|
|
@@ -437,11 +141,13 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
437
141
|
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
438
142
|
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
439
143
|
id_set_val = Apple2Val(
|
|
440
|
-
top_outer=f"{phase:.6f}",
|
|
441
|
-
top_inner="0.0",
|
|
442
|
-
btm_inner=f"{phase3:.6f}",
|
|
443
|
-
btm_outer="0.0",
|
|
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
153
|
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
|
@@ -486,4 +192,4 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
|
486
192
|
|
|
487
193
|
@AsyncStatus.wrap
|
|
488
194
|
async def set(self, angle: float) -> None:
|
|
489
|
-
await self.linear_arbitrary_angle().set(angle)
|
|
195
|
+
await self.linear_arbitrary_angle().set(angle, timeout=MAXIMUM_MOVE_TIME)
|
dodal/devices/i17/i17_apple2.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dodal.devices.apple2_undulator import (
|
|
2
2
|
Apple2,
|
|
3
3
|
Apple2Controller,
|
|
4
|
+
Apple2PhasesVal,
|
|
4
5
|
Apple2Val,
|
|
5
6
|
EnergyMotorConvertor,
|
|
6
7
|
)
|
|
@@ -40,11 +41,13 @@ class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
|
40
41
|
pol = await self._check_and_get_pol_setpoint()
|
|
41
42
|
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
42
43
|
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",
|
|
47
44
|
gap=f"{gap:.6f}",
|
|
45
|
+
phase=Apple2PhasesVal(
|
|
46
|
+
top_outer=f"{phase:.6f}",
|
|
47
|
+
top_inner="0.0",
|
|
48
|
+
btm_inner=f"{phase:.6f}",
|
|
49
|
+
btm_outer="0.0",
|
|
50
|
+
),
|
|
48
51
|
)
|
|
49
52
|
|
|
50
53
|
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
dodal/devices/ipin.py
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
from ophyd_async.core import StandardReadable, StandardReadableFormat
|
|
2
|
-
from ophyd_async.epics.core import epics_signal_r
|
|
1
|
+
from ophyd_async.core import StandardReadable, StandardReadableFormat, SubsetEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class IPinGain(SubsetEnum):
|
|
6
|
+
GAIN_10E3_LOW_NOISE = "10^3 low noise"
|
|
7
|
+
GAIN_10E4_LOW_NOISE = "10^4 low noise"
|
|
8
|
+
GAIN_10E5_LOW_NOISE = "10^5 low noise"
|
|
9
|
+
GAIN_10E6_LOW_NOISE = "10^6 low noise"
|
|
10
|
+
GAIN_10E7_LOW_NOISE = "10^7 low noise"
|
|
11
|
+
GAIN_10E8_LOW_NOISE = "10^8 low noise"
|
|
12
|
+
GAIN_10E9_LOW_NOISE = "10^9 low noise"
|
|
13
|
+
GAIN_10E5_HIGH_SPEED = "10^5 high speed"
|
|
14
|
+
GAIN_10E6_HIGH_SPEED = "10^6 high speed"
|
|
15
|
+
GAIN_10E7_HIGH_SPEED = "10^7 high speed"
|
|
16
|
+
GAIN_10E8_HIGH_SPEED = "10^8 high speed"
|
|
17
|
+
GAIN_10E9_HIGH_SPEED = "10^9 high speed"
|
|
18
|
+
GAIN_10E10_HIGH_SPEED = "10^10 high spd"
|
|
19
|
+
GAIN_10E11_HIGH_SPEED = "10^11 high spd"
|
|
3
20
|
|
|
4
21
|
|
|
5
22
|
class IPin(StandardReadable):
|
|
@@ -10,4 +27,5 @@ class IPin(StandardReadable):
|
|
|
10
27
|
format=StandardReadableFormat.HINTED_SIGNAL
|
|
11
28
|
):
|
|
12
29
|
self.pin_readback = epics_signal_r(float, prefix + "I")
|
|
30
|
+
self.gain = epics_signal_rw(IPinGain, prefix + "GAIN")
|
|
13
31
|
super().__init__(name)
|
dodal/devices/motors.py
CHANGED
|
@@ -7,6 +7,8 @@ from ophyd_async.epics.motor import Motor
|
|
|
7
7
|
|
|
8
8
|
_X, _Y, _Z = "X", "Y", "Z"
|
|
9
9
|
|
|
10
|
+
_OMEGA = "OMEGA"
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class Stage(StandardReadable, ABC):
|
|
12
14
|
"""
|
|
@@ -77,6 +79,21 @@ class XYZThetaStage(XYZStage):
|
|
|
77
79
|
super().__init__(prefix, name, x_infix, y_infix, z_infix)
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
class XYZOmegaStage(XYZStage):
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
prefix: str,
|
|
86
|
+
name: str = "",
|
|
87
|
+
x_infix: str = _X,
|
|
88
|
+
y_infix: str = _Y,
|
|
89
|
+
z_infix: str = _Z,
|
|
90
|
+
omega_infix: str = _OMEGA,
|
|
91
|
+
) -> None:
|
|
92
|
+
with self.add_children_as_readables():
|
|
93
|
+
self.omega = Motor(prefix + omega_infix)
|
|
94
|
+
super().__init__(prefix, name, x_infix, y_infix, z_infix)
|
|
95
|
+
|
|
96
|
+
|
|
80
97
|
class XYPhiStage(XYStage):
|
|
81
98
|
def __init__(
|
|
82
99
|
self,
|
|
@@ -141,7 +158,7 @@ class XYZPitchYawRollStage(XYZStage):
|
|
|
141
158
|
super().__init__(prefix, name, x_infix, y_infix, z_infix)
|
|
142
159
|
|
|
143
160
|
|
|
144
|
-
class SixAxisGonio(
|
|
161
|
+
class SixAxisGonio(XYZOmegaStage):
|
|
145
162
|
def __init__(
|
|
146
163
|
self,
|
|
147
164
|
prefix: str,
|
|
@@ -151,7 +168,7 @@ class SixAxisGonio(XYZStage):
|
|
|
151
168
|
z_infix: str = _Z,
|
|
152
169
|
kappa_infix: str = "KAPPA",
|
|
153
170
|
phi_infix: str = "PHI",
|
|
154
|
-
omega_infix: str =
|
|
171
|
+
omega_infix: str = _OMEGA,
|
|
155
172
|
):
|
|
156
173
|
"""Six-axis goniometer with a standard xyz stage and three axes of rotation:
|
|
157
174
|
kappa, phi and omega.
|
|
@@ -159,7 +176,6 @@ class SixAxisGonio(XYZStage):
|
|
|
159
176
|
with self.add_children_as_readables():
|
|
160
177
|
self.kappa = Motor(prefix + kappa_infix)
|
|
161
178
|
self.phi = Motor(prefix + phi_infix)
|
|
162
|
-
self.omega = Motor(prefix + omega_infix)
|
|
163
179
|
super().__init__(prefix, name, x_infix, y_infix, z_infix)
|
|
164
180
|
|
|
165
181
|
self.vertical_in_lab_space = create_axis_perp_to_rotation(
|
|
@@ -10,6 +10,8 @@ from ophyd_async.epics.motor import Motor
|
|
|
10
10
|
|
|
11
11
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
12
12
|
|
|
13
|
+
_BEAMSTOP_OUT_DELTA_Y_MM = -2
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
class BeamstopPositions(StrictEnum):
|
|
15
17
|
"""
|
|
@@ -28,6 +30,7 @@ class BeamstopPositions(StrictEnum):
|
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
DATA_COLLECTION = "Data Collection"
|
|
33
|
+
OUT_OF_BEAM = "Out"
|
|
31
34
|
UNKNOWN = "Unknown"
|
|
32
35
|
|
|
33
36
|
|
|
@@ -63,6 +66,10 @@ class Beamstop(StandardReadable):
|
|
|
63
66
|
float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
|
|
64
67
|
for axis in ("x", "y", "z")
|
|
65
68
|
]
|
|
69
|
+
|
|
70
|
+
self._out_of_beam_xyz_mm = list(self._in_beam_xyz_mm)
|
|
71
|
+
self._out_of_beam_xyz_mm[1] += _BEAMSTOP_OUT_DELTA_Y_MM
|
|
72
|
+
|
|
66
73
|
self._xyz_tolerance_mm = [
|
|
67
74
|
float(beamline_parameters[f"bs_{axis}_tolerance"])
|
|
68
75
|
for axis in ("x", "y", "z")
|
|
@@ -72,24 +79,36 @@ class Beamstop(StandardReadable):
|
|
|
72
79
|
|
|
73
80
|
def _get_selected_position(self, x: float, y: float, z: float) -> BeamstopPositions:
|
|
74
81
|
current_pos = [x, y, z]
|
|
75
|
-
if
|
|
76
|
-
isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
|
|
77
|
-
for axis_pos, axis_in_beam, axis_tolerance in zip(
|
|
78
|
-
current_pos, self._in_beam_xyz_mm, self._xyz_tolerance_mm, strict=False
|
|
79
|
-
)
|
|
80
|
-
):
|
|
82
|
+
if self._is_near_position(current_pos, self._in_beam_xyz_mm):
|
|
81
83
|
return BeamstopPositions.DATA_COLLECTION
|
|
84
|
+
elif self._is_near_position(current_pos, self._out_of_beam_xyz_mm):
|
|
85
|
+
return BeamstopPositions.OUT_OF_BEAM
|
|
82
86
|
else:
|
|
83
87
|
return BeamstopPositions.UNKNOWN
|
|
84
88
|
|
|
89
|
+
def _is_near_position(
|
|
90
|
+
self, current_pos: list[float], target_pos: list[float]
|
|
91
|
+
) -> bool:
|
|
92
|
+
return all(
|
|
93
|
+
isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
|
|
94
|
+
for axis_pos, axis_in_beam, axis_tolerance in zip(
|
|
95
|
+
current_pos, target_pos, self._xyz_tolerance_mm, strict=False
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
85
99
|
async def _set_selected_position(self, position: BeamstopPositions) -> None:
|
|
86
100
|
match position:
|
|
87
101
|
case BeamstopPositions.DATA_COLLECTION:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await
|
|
91
|
-
self.x_mm.set(self._in_beam_xyz_mm[0]),
|
|
92
|
-
self.y_mm.set(self._in_beam_xyz_mm[1]),
|
|
93
|
-
)
|
|
102
|
+
await self._safe_move_above_table(self._in_beam_xyz_mm)
|
|
103
|
+
case BeamstopPositions.OUT_OF_BEAM:
|
|
104
|
+
await self._safe_move_above_table(self._out_of_beam_xyz_mm)
|
|
94
105
|
case _:
|
|
95
106
|
raise ValueError(f"Cannot set beamstop to position {position}")
|
|
107
|
+
|
|
108
|
+
async def _safe_move_above_table(self, pos: list[float]):
|
|
109
|
+
# Move z first as it could be under the table
|
|
110
|
+
await self.z_mm.set(pos[2])
|
|
111
|
+
await asyncio.gather(
|
|
112
|
+
self.x_mm.set(pos[0]),
|
|
113
|
+
self.y_mm.set(pos[1]),
|
|
114
|
+
)
|
|
@@ -7,6 +7,9 @@ def camera_coordinates_to_xyz_mm(
|
|
|
7
7
|
omega: float,
|
|
8
8
|
microns_per_i_pixel: float,
|
|
9
9
|
microns_per_j_pixel: float,
|
|
10
|
+
x_horizontal_sign: int,
|
|
11
|
+
y_vertical_sign: int,
|
|
12
|
+
z_vertical_sign: int,
|
|
10
13
|
) -> np.ndarray:
|
|
11
14
|
"""
|
|
12
15
|
Converts from (horizontal,vertical) pixel measurements from the OAV camera into to (x, y, z) motor coordinates in millimetres.
|
|
@@ -18,13 +21,16 @@ def camera_coordinates_to_xyz_mm(
|
|
|
18
21
|
omega (float): The omega angle of the smargon that the horizontal, vertical measurements were obtained at.
|
|
19
22
|
microns_per_i_pixel (float): The number of microns per i pixel, adjusted for the zoom level horizontal was measured at.
|
|
20
23
|
microns_per_j_pixel (float): The number of microns per j pixel, adjusted for the zoom level vertical was measured at.
|
|
24
|
+
x_horizontal_sign (int): Direction mapping for x, positive means the oav and motor are on same direction, default from hyperion
|
|
25
|
+
y_vertical_sign (int): Direction mapping for y
|
|
26
|
+
z_vertical_sign (int): Direction mapping for z
|
|
21
27
|
"""
|
|
22
28
|
# Convert the vertical and horizontal into mm.
|
|
23
29
|
horizontal *= microns_per_i_pixel * 1e-3
|
|
24
30
|
vertical *= microns_per_j_pixel * 1e-3
|
|
25
31
|
|
|
26
32
|
# +ve x in the OAV camera becomes -ve x in the smargon motors.
|
|
27
|
-
x =
|
|
33
|
+
x = x_horizontal_sign * horizontal
|
|
28
34
|
|
|
29
35
|
# Rotating the camera causes the position on the vertical horizontal to change by raising or lowering the centre.
|
|
30
36
|
# We can negate this change by multiplying sin and cosine of the omega.
|
|
@@ -33,9 +39,8 @@ def camera_coordinates_to_xyz_mm(
|
|
|
33
39
|
sine = np.sin(radians)
|
|
34
40
|
|
|
35
41
|
# +ve y in the OAV camera becomes -ve y in the smargon motors/
|
|
36
|
-
y =
|
|
37
|
-
|
|
38
|
-
z = vertical * sine
|
|
42
|
+
y = y_vertical_sign * vertical * cosine
|
|
43
|
+
z = z_vertical_sign * vertical * sine
|
|
39
44
|
return np.array([x, y, z], dtype=np.float64)
|
|
40
45
|
|
|
41
46
|
|