dls-dodal 1.58.0__py3-none-any.whl → 1.60.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.58.0.dist-info → dls_dodal-1.60.0.dist-info}/METADATA +3 -3
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/RECORD +71 -47
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +1 -0
- dodal/beamlines/b07.py +10 -5
- dodal/beamlines/b07_1.py +10 -5
- dodal/beamlines/b21.py +22 -0
- dodal/beamlines/i02_1.py +80 -0
- dodal/beamlines/i03.py +5 -3
- dodal/beamlines/i04.py +5 -3
- dodal/beamlines/i09.py +10 -9
- dodal/beamlines/i09_1.py +10 -5
- dodal/beamlines/i10-1.py +25 -0
- dodal/beamlines/i10.py +17 -1
- dodal/beamlines/i11.py +0 -17
- dodal/beamlines/i15.py +242 -0
- dodal/beamlines/i15_1.py +156 -0
- dodal/beamlines/i19_1.py +3 -1
- dodal/beamlines/i19_2.py +12 -1
- dodal/beamlines/i21.py +27 -0
- dodal/beamlines/i22.py +12 -2
- dodal/beamlines/i24.py +32 -3
- dodal/beamlines/k07.py +31 -0
- dodal/beamlines/p60.py +10 -9
- dodal/common/watcher_utils.py +1 -1
- dodal/devices/apple2_undulator.py +18 -142
- dodal/devices/attenuator/attenuator.py +48 -2
- dodal/devices/attenuator/filter.py +3 -0
- dodal/devices/attenuator/filter_selections.py +26 -0
- dodal/devices/eiger.py +2 -1
- dodal/devices/electron_analyser/__init__.py +4 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +30 -18
- dodal/devices/electron_analyser/energy_sources.py +101 -0
- dodal/devices/electron_analyser/specs/detector.py +6 -6
- dodal/devices/electron_analyser/specs/driver_io.py +7 -15
- dodal/devices/electron_analyser/vgscienta/detector.py +6 -6
- dodal/devices/electron_analyser/vgscienta/driver_io.py +7 -14
- dodal/devices/fast_grid_scan.py +130 -64
- dodal/devices/focusing_mirror.py +30 -0
- dodal/devices/i02_1/__init__.py +0 -0
- dodal/devices/i02_1/fast_grid_scan.py +61 -0
- dodal/devices/i02_1/sample_motors.py +19 -0
- dodal/devices/i04/murko_results.py +69 -23
- dodal/devices/i10/i10_apple2.py +282 -140
- dodal/devices/i15/dcm.py +77 -0
- dodal/devices/i15/focussing_mirror.py +71 -0
- dodal/devices/i15/jack.py +39 -0
- dodal/devices/i15/laue.py +18 -0
- dodal/devices/i15/motors.py +27 -0
- dodal/devices/i15/multilayer_mirror.py +25 -0
- dodal/devices/i15/rail.py +17 -0
- dodal/devices/i21/__init__.py +3 -0
- dodal/devices/i21/enums.py +8 -0
- dodal/devices/i22/nxsas.py +2 -0
- dodal/devices/i24/commissioning_jungfrau.py +114 -0
- dodal/devices/motors.py +52 -1
- dodal/devices/slits.py +18 -0
- dodal/devices/smargon.py +0 -56
- dodal/devices/temperture_controller/__init__.py +3 -0
- dodal/devices/temperture_controller/lakeshore/__init__.py +0 -0
- dodal/devices/temperture_controller/lakeshore/lakeshore.py +204 -0
- dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +112 -0
- dodal/devices/tetramm.py +38 -16
- dodal/devices/v2f.py +39 -0
- dodal/devices/zebra/zebra.py +1 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -1
- dodal/parameters/experiment_parameter_base.py +1 -5
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/top_level.txt +0 -0
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import csv
|
|
3
|
+
import io
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, SupportsFloat
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
from bluesky.protocols import Movable
|
|
10
|
+
from daq_config_server.client import ConfigServer
|
|
9
11
|
from ophyd_async.core import (
|
|
10
12
|
AsyncStatus,
|
|
11
13
|
Device,
|
|
@@ -15,13 +17,14 @@ from ophyd_async.core import (
|
|
|
15
17
|
soft_signal_r_and_setter,
|
|
16
18
|
soft_signal_rw,
|
|
17
19
|
)
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, RootModel
|
|
18
21
|
|
|
19
22
|
from dodal.log import LOGGER
|
|
20
23
|
|
|
21
24
|
from ..apple2_undulator import (
|
|
22
25
|
Apple2,
|
|
23
26
|
Apple2Val,
|
|
24
|
-
|
|
27
|
+
EnergyMotorConvertor,
|
|
25
28
|
Pol,
|
|
26
29
|
UndulatorGap,
|
|
27
30
|
UndulatorJawPhase,
|
|
@@ -34,6 +37,7 @@ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
|
34
37
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
35
38
|
DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
|
|
36
39
|
ALPHA_OFFSET = 180
|
|
40
|
+
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
# data class to store the lookup table configuration that is use in convert_csv_to_lookup
|
|
@@ -53,36 +57,75 @@ class LookupTableConfig:
|
|
|
53
57
|
poly_deg: list | None
|
|
54
58
|
|
|
55
59
|
|
|
56
|
-
class
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
class EnergyMinMax(BaseModel):
|
|
61
|
+
Minimum: float
|
|
62
|
+
Maximum: float
|
|
59
63
|
|
|
60
|
-
A pair of look up tables are needed to provide the conversion betwApple 2 ID/undulator has 4 extra degrees of freedom compare to the standard Undulator,
|
|
61
|
-
each bank of magnet can move independently to each other,
|
|
62
|
-
which allow the production of different x-ray polarisation as well as energy.
|
|
63
|
-
This type of ID is use on I10, I21, I09, I17 and I06 for soft x-ray.een motor position and energy.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
class EnergyCoverageEntry(BaseModel):
|
|
66
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
67
|
+
Low: float
|
|
68
|
+
High: float
|
|
69
|
+
Poly: np.poly1d
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EnergyCoverage(RootModel):
|
|
73
|
+
root: dict[str, EnergyCoverageEntry]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LookupTableEntries(BaseModel):
|
|
77
|
+
Energies: EnergyCoverage
|
|
78
|
+
Limit: EnergyMinMax
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Lookuptable(RootModel):
|
|
82
|
+
"""BaseModel class for the lookup table.
|
|
83
|
+
Apple2 lookup table should be in this format.
|
|
84
|
+
|
|
85
|
+
{mode: {'Energies': {Any: {'Low': float,
|
|
86
|
+
'High': float,
|
|
87
|
+
'Poly':np.poly1d
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
'Limit': {'Minimum': float,
|
|
91
|
+
'Maximum': float
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
root: dict[str, LookupTableEntries]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class I10EnergyMotorLookup:
|
|
101
|
+
"""
|
|
102
|
+
Handles lookup tables for I10 Apple2 ID, converting energy and polarisation to gap
|
|
103
|
+
and phase. Fetches and parses lookup tables from a config server, supports dynamic
|
|
104
|
+
updates, and validates input.
|
|
66
105
|
"""
|
|
67
106
|
|
|
68
107
|
def __init__(
|
|
69
108
|
self,
|
|
70
109
|
look_up_table_dir: str,
|
|
71
110
|
source: tuple[str, str],
|
|
72
|
-
|
|
111
|
+
config_client: ConfigServer,
|
|
73
112
|
mode: str = "Mode",
|
|
74
113
|
min_energy: str = "MinEnergy",
|
|
75
114
|
max_energy: str = "MaxEnergy",
|
|
115
|
+
gap_file_name: str = "IDEnergy2GapCalibrations.csv",
|
|
116
|
+
phase_file_name: str = "IDEnergy2PhaseCalibrations.csv",
|
|
76
117
|
poly_deg: list | None = None,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
):
|
|
119
|
+
"""Initialise the I10EnergyMotorLookup class with lookup table headers provided.
|
|
120
|
+
|
|
80
121
|
Parameters
|
|
81
122
|
----------
|
|
82
123
|
look_up_table_dir:
|
|
83
124
|
The path to look up table.
|
|
84
125
|
source:
|
|
85
|
-
The column name and the name of the source in look up table. e.g. ("source", "idu")
|
|
126
|
+
The column name and the name of the source in look up table. e.g. ( "source", "idu")
|
|
127
|
+
config_client:
|
|
128
|
+
The config server client to fetch the look up table.
|
|
86
129
|
mode:
|
|
87
130
|
The column name of the mode in look up table.
|
|
88
131
|
min_energy:
|
|
@@ -91,17 +134,14 @@ class I10Apple2(Apple2):
|
|
|
91
134
|
The column name that contain the maximum energy in look up table.
|
|
92
135
|
poly_deg:
|
|
93
136
|
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
94
|
-
prefix:
|
|
95
|
-
epic pv for id
|
|
96
|
-
Name:
|
|
97
|
-
Name of the device
|
|
98
|
-
"""
|
|
99
137
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
"""
|
|
139
|
+
self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
|
|
140
|
+
"Gap": {},
|
|
141
|
+
"Phase": {},
|
|
142
|
+
}
|
|
143
|
+
energy_gap_table_path = Path(look_up_table_dir, gap_file_name)
|
|
144
|
+
energy_phase_table_path = Path(look_up_table_dir, phase_file_name)
|
|
105
145
|
self.lookup_table_config = LookupTableConfig(
|
|
106
146
|
path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
|
|
107
147
|
source=source,
|
|
@@ -110,6 +150,212 @@ class I10Apple2(Apple2):
|
|
|
110
150
|
max_energy=max_energy,
|
|
111
151
|
poly_deg=poly_deg,
|
|
112
152
|
)
|
|
153
|
+
self.config_client = config_client
|
|
154
|
+
self._available_pol = []
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def available_pol(self) -> list[str | None]:
|
|
158
|
+
return self._available_pol
|
|
159
|
+
|
|
160
|
+
@available_pol.setter
|
|
161
|
+
def available_pol(self, value: list[str | None]) -> None:
|
|
162
|
+
self._available_pol = value
|
|
163
|
+
|
|
164
|
+
def update_lookuptable(self):
|
|
165
|
+
"""
|
|
166
|
+
Update lookup tables from files and validate their format.
|
|
167
|
+
"""
|
|
168
|
+
LOGGER.info("Updating lookup dictionary from file.")
|
|
169
|
+
for key, path in self.lookup_table_config.path.__dict__.items():
|
|
170
|
+
self.lookup_tables[key] = self.convert_csv_to_lookup(
|
|
171
|
+
file=path,
|
|
172
|
+
source=self.lookup_table_config.source,
|
|
173
|
+
mode=self.lookup_table_config.mode,
|
|
174
|
+
min_energy=self.lookup_table_config.min_energy,
|
|
175
|
+
max_energy=self.lookup_table_config.max_energy,
|
|
176
|
+
poly_deg=self.lookup_table_config.poly_deg,
|
|
177
|
+
)
|
|
178
|
+
Lookuptable.model_validate(self.lookup_tables[key])
|
|
179
|
+
|
|
180
|
+
self.available_pol = list(self.lookup_tables["Gap"].keys())
|
|
181
|
+
|
|
182
|
+
def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
|
|
183
|
+
"""
|
|
184
|
+
Convert energy and polarisation to gap and phase motor positions.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
energy : float
|
|
189
|
+
Desired energy in eV.
|
|
190
|
+
pol : Pol
|
|
191
|
+
Polarisation mode.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
tuple[float, float]
|
|
196
|
+
(gap, phase) motor positions.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
if self.available_pol == []:
|
|
200
|
+
self.update_lookuptable()
|
|
201
|
+
|
|
202
|
+
gap_poly = self._get_poly(
|
|
203
|
+
lookup_table=self.lookup_tables["Gap"], energy=energy, pol=pol
|
|
204
|
+
)
|
|
205
|
+
phase_poly = self._get_poly(
|
|
206
|
+
lookup_table=self.lookup_tables["Phase"], energy=energy, pol=pol
|
|
207
|
+
)
|
|
208
|
+
return gap_poly(energy), phase_poly(energy)
|
|
209
|
+
|
|
210
|
+
def _get_poly(
|
|
211
|
+
self,
|
|
212
|
+
energy: float,
|
|
213
|
+
pol: Pol,
|
|
214
|
+
lookup_table: dict[str | None, dict[str, dict[str, Any]]],
|
|
215
|
+
) -> np.poly1d:
|
|
216
|
+
"""
|
|
217
|
+
Get polynomial for a given energy and polarisation.
|
|
218
|
+
|
|
219
|
+
Raises
|
|
220
|
+
------
|
|
221
|
+
ValueError
|
|
222
|
+
If energy is out of bounds or coefficients are missing.
|
|
223
|
+
"""
|
|
224
|
+
if (
|
|
225
|
+
energy < lookup_table[pol]["Limit"]["Minimum"]
|
|
226
|
+
or energy > lookup_table[pol]["Limit"]["Maximum"]
|
|
227
|
+
):
|
|
228
|
+
raise ValueError(
|
|
229
|
+
"Demanding energy must lie between {} and {} eV!".format(
|
|
230
|
+
lookup_table[pol]["Limit"]["Minimum"],
|
|
231
|
+
lookup_table[pol]["Limit"]["Maximum"],
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
for energy_range in lookup_table[pol]["Energies"].values():
|
|
236
|
+
if energy >= energy_range["Low"] and energy < energy_range["High"]:
|
|
237
|
+
return energy_range["Poly"]
|
|
238
|
+
|
|
239
|
+
raise ValueError(
|
|
240
|
+
"""Cannot find polynomial coefficients for your requested energy.
|
|
241
|
+
There might be gap in the calibration lookup table."""
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def convert_csv_to_lookup(
|
|
245
|
+
self,
|
|
246
|
+
file: str,
|
|
247
|
+
source: tuple[str, str],
|
|
248
|
+
mode: str | None = "Mode",
|
|
249
|
+
min_energy: str | None = "MinEnergy",
|
|
250
|
+
max_energy: str | None = "MaxEnergy",
|
|
251
|
+
poly_deg: list | None = None,
|
|
252
|
+
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
253
|
+
"""
|
|
254
|
+
Convert a CSV file to a lookup table dictionary.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
dict
|
|
259
|
+
Dictionary in Apple2 lookup table format.
|
|
260
|
+
|
|
261
|
+
Raises
|
|
262
|
+
------
|
|
263
|
+
RuntimeError
|
|
264
|
+
If the CSV cannot be converted.
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
if poly_deg is None:
|
|
268
|
+
poly_deg = [
|
|
269
|
+
"7th-order",
|
|
270
|
+
"6th-order",
|
|
271
|
+
"5th-order",
|
|
272
|
+
"4th-order",
|
|
273
|
+
"3rd-order",
|
|
274
|
+
"2nd-order",
|
|
275
|
+
"1st-order",
|
|
276
|
+
"b",
|
|
277
|
+
]
|
|
278
|
+
lookup_table = {}
|
|
279
|
+
polarisations = set()
|
|
280
|
+
|
|
281
|
+
def process_row(row: dict) -> None:
|
|
282
|
+
"""Process a single row from the CSV file and update the lookup table."""
|
|
283
|
+
mode_value = row[mode]
|
|
284
|
+
if mode_value not in polarisations:
|
|
285
|
+
polarisations.add(mode_value)
|
|
286
|
+
lookup_table[mode_value] = {
|
|
287
|
+
"Energies": {},
|
|
288
|
+
"Limit": {
|
|
289
|
+
"Minimum": float(row[min_energy]),
|
|
290
|
+
"Maximum": float(row[max_energy]),
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Create polynomial object for energy-to-gap/phase conversion
|
|
295
|
+
coefficients = [float(row[coef]) for coef in poly_deg]
|
|
296
|
+
polynomial = np.poly1d(coefficients)
|
|
297
|
+
|
|
298
|
+
lookup_table[mode_value]["Energies"][row[min_energy]] = {
|
|
299
|
+
"Low": float(row[min_energy]),
|
|
300
|
+
"High": float(row[max_energy]),
|
|
301
|
+
"Poly": polynomial,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Update energy limits
|
|
305
|
+
lookup_table[mode_value]["Limit"]["Minimum"] = min(
|
|
306
|
+
lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
|
|
307
|
+
)
|
|
308
|
+
lookup_table[mode_value]["Limit"]["Maximum"] = max(
|
|
309
|
+
lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
csv_file = self.config_client.get_file_contents(file, reset_cached_result=True)
|
|
313
|
+
reader = csv.DictReader(io.StringIO(csv_file))
|
|
314
|
+
for row in reader:
|
|
315
|
+
# If there are multiple source only convert requested.
|
|
316
|
+
if row[source[0]] == source[1]:
|
|
317
|
+
process_row(row=row)
|
|
318
|
+
if not lookup_table:
|
|
319
|
+
raise RuntimeError(f"Unable to convert lookup table:\t{file}")
|
|
320
|
+
return lookup_table
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class I10Apple2(Apple2):
|
|
324
|
+
"""I10Apple2 is the i10 version of Apple2 ID, set and energy_motor_convertor
|
|
325
|
+
should be the only part that is I10 specific.
|
|
326
|
+
|
|
327
|
+
A EnergyMotorConvertor function is needed to provide the conversion between
|
|
328
|
+
x-ray motor position and energy.
|
|
329
|
+
|
|
330
|
+
Set is in energy(eV).
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(
|
|
334
|
+
self,
|
|
335
|
+
prefix: str,
|
|
336
|
+
energy_motor_convertor: EnergyMotorConvertor,
|
|
337
|
+
name: str = "",
|
|
338
|
+
) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
look_up_table_dir:
|
|
343
|
+
The path to look up table.
|
|
344
|
+
source:
|
|
345
|
+
The column name and the name of the source in look up table. e.g. ("source", "idu")
|
|
346
|
+
mode:
|
|
347
|
+
The column name of the mode in look up table.
|
|
348
|
+
min_energy:
|
|
349
|
+
The column name that contain the maximum energy in look up table.
|
|
350
|
+
max_energy:
|
|
351
|
+
The column name that contain the maximum energy in look up table.
|
|
352
|
+
poly_deg:
|
|
353
|
+
The column names for the parameters for the energy conversion polynomial, starting with the least significant.
|
|
354
|
+
prefix:
|
|
355
|
+
epic pv for id
|
|
356
|
+
Name:
|
|
357
|
+
Name of the device
|
|
358
|
+
"""
|
|
113
359
|
|
|
114
360
|
with self.add_children_as_readables():
|
|
115
361
|
super().__init__(
|
|
@@ -122,6 +368,7 @@ class I10Apple2(Apple2):
|
|
|
122
368
|
btm_inner="RPQ3",
|
|
123
369
|
btm_outer="RPQ4",
|
|
124
370
|
),
|
|
371
|
+
energy_motor_convertor=energy_motor_convertor,
|
|
125
372
|
name=name,
|
|
126
373
|
)
|
|
127
374
|
self.id_jaw_phase = UndulatorJawPhase(
|
|
@@ -150,7 +397,7 @@ class I10Apple2(Apple2):
|
|
|
150
397
|
)
|
|
151
398
|
|
|
152
399
|
self._set_pol_setpoint(pol)
|
|
153
|
-
gap, phase =
|
|
400
|
+
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
154
401
|
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
155
402
|
id_set_val = Apple2Val(
|
|
156
403
|
top_outer=f"{phase:.6f}",
|
|
@@ -167,29 +414,6 @@ class I10Apple2(Apple2):
|
|
|
167
414
|
await self.id_jaw_phase.set_move.set(1)
|
|
168
415
|
LOGGER.info(f"Energy set to {value} eV successfully.")
|
|
169
416
|
|
|
170
|
-
def update_lookuptable(self):
|
|
171
|
-
"""
|
|
172
|
-
Update the stored lookup tabled from file.
|
|
173
|
-
|
|
174
|
-
"""
|
|
175
|
-
LOGGER.info("Updating lookup dictionary from file.")
|
|
176
|
-
for key, path in self.lookup_table_config.path.__dict__.items():
|
|
177
|
-
if path.exists():
|
|
178
|
-
self.lookup_tables[key] = convert_csv_to_lookup(
|
|
179
|
-
file=path,
|
|
180
|
-
source=self.lookup_table_config.source,
|
|
181
|
-
mode=self.lookup_table_config.mode,
|
|
182
|
-
min_energy=self.lookup_table_config.min_energy,
|
|
183
|
-
max_energy=self.lookup_table_config.max_energy,
|
|
184
|
-
poly_deg=self.lookup_table_config.poly_deg,
|
|
185
|
-
)
|
|
186
|
-
# ensure the importing lookup table is the correct format
|
|
187
|
-
Lookuptable.model_validate(self.lookup_tables[key])
|
|
188
|
-
else:
|
|
189
|
-
raise FileNotFoundError(f"{key} look up table is not in path: {path}")
|
|
190
|
-
|
|
191
|
-
self._available_pol = list(self.lookup_tables["Gap"].keys())
|
|
192
|
-
|
|
193
417
|
|
|
194
418
|
class EnergySetter(StandardReadable, Movable[float]):
|
|
195
419
|
"""
|
|
@@ -250,7 +474,8 @@ class I10Apple2Pol(StandardReadable, Movable[Pol]):
|
|
|
250
474
|
@AsyncStatus.wrap
|
|
251
475
|
async def set(self, value: Pol) -> None:
|
|
252
476
|
LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
|
|
253
|
-
|
|
477
|
+
# Timeout is determined internally by the set method later, so we set it to max here.
|
|
478
|
+
await self.id_ref().polarisation.set(value, timeout=MAXIMUM_MOVE_TIME)
|
|
254
479
|
|
|
255
480
|
|
|
256
481
|
class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
@@ -320,6 +545,7 @@ class I10Id(Device):
|
|
|
320
545
|
prefix: str,
|
|
321
546
|
look_up_table_dir: str,
|
|
322
547
|
source: tuple[str, str],
|
|
548
|
+
config_client: ConfigServer,
|
|
323
549
|
jaw_phase_limit=12.0,
|
|
324
550
|
jaw_phase_poly_param=DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
325
551
|
angle_threshold_deg=30.0,
|
|
@@ -341,12 +567,16 @@ class I10Id(Device):
|
|
|
341
567
|
linear_arbitrary_angle : LinearArbitraryAngle
|
|
342
568
|
A device for controlling the linear arbitrary polarization angle.
|
|
343
569
|
"""
|
|
570
|
+
self.lookup_table_client = I10EnergyMotorLookup(
|
|
571
|
+
look_up_table_dir=look_up_table_dir,
|
|
572
|
+
source=source,
|
|
573
|
+
config_client=config_client,
|
|
574
|
+
)
|
|
344
575
|
self.energy = EnergySetter(
|
|
345
576
|
id=I10Apple2(
|
|
346
|
-
look_up_table_dir=look_up_table_dir,
|
|
347
|
-
name="id_energy",
|
|
348
|
-
source=source,
|
|
349
577
|
prefix=prefix,
|
|
578
|
+
energy_motor_convertor=self.lookup_table_client.get_motor_from_energy,
|
|
579
|
+
name="id_energy",
|
|
350
580
|
),
|
|
351
581
|
pgm=pgm,
|
|
352
582
|
name="energy",
|
|
@@ -361,91 +591,3 @@ class I10Id(Device):
|
|
|
361
591
|
)
|
|
362
592
|
|
|
363
593
|
super().__init__(name=name)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def convert_csv_to_lookup(
|
|
367
|
-
file: str,
|
|
368
|
-
source: tuple[str, str],
|
|
369
|
-
mode: str | None = "Mode",
|
|
370
|
-
min_energy: str | None = "MinEnergy",
|
|
371
|
-
max_energy: str | None = "MaxEnergy",
|
|
372
|
-
poly_deg: list | None = None,
|
|
373
|
-
) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
|
|
374
|
-
"""
|
|
375
|
-
Convert a CSV file to a dictionary compatible with the Apple2 lookup table format.
|
|
376
|
-
|
|
377
|
-
Parameters
|
|
378
|
-
----------
|
|
379
|
-
file : str
|
|
380
|
-
Path to the CSV file.
|
|
381
|
-
source : tuple[str, str]
|
|
382
|
-
Tuple specifying the column name and source name (e.g., ("Source", "idu")).
|
|
383
|
-
mode : str, optional
|
|
384
|
-
Column name for the available modes (e.g., "lv", "lh", "pc", "nc"), by default "Mode".
|
|
385
|
-
min_energy : str, optional
|
|
386
|
-
Column name for the minimum energy, by default "MinEnergy".
|
|
387
|
-
max_energy : str, optional
|
|
388
|
-
Column name for the maximum energy, by default "MaxEnergy".
|
|
389
|
-
poly_deg : list, optional
|
|
390
|
-
Column names for polynomial coefficients, starting with the least significant term.
|
|
391
|
-
|
|
392
|
-
Returns
|
|
393
|
-
-------
|
|
394
|
-
dict
|
|
395
|
-
A dictionary conforming to the Apple2 lookup table format.
|
|
396
|
-
|
|
397
|
-
"""
|
|
398
|
-
if poly_deg is None:
|
|
399
|
-
poly_deg = [
|
|
400
|
-
"7th-order",
|
|
401
|
-
"6th-order",
|
|
402
|
-
"5th-order",
|
|
403
|
-
"4th-order",
|
|
404
|
-
"3rd-order",
|
|
405
|
-
"2nd-order",
|
|
406
|
-
"1st-order",
|
|
407
|
-
"b",
|
|
408
|
-
]
|
|
409
|
-
lookup_table = {}
|
|
410
|
-
polarisations = set()
|
|
411
|
-
|
|
412
|
-
def process_row(row: dict) -> None:
|
|
413
|
-
"""Process a single row from the CSV file and update the lookup table."""
|
|
414
|
-
mode_value = row[mode]
|
|
415
|
-
if mode_value not in polarisations:
|
|
416
|
-
polarisations.add(mode_value)
|
|
417
|
-
lookup_table[mode_value] = {
|
|
418
|
-
"Energies": {},
|
|
419
|
-
"Limit": {
|
|
420
|
-
"Minimum": float(row[min_energy]),
|
|
421
|
-
"Maximum": float(row[max_energy]),
|
|
422
|
-
},
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
# Create polynomial object for energy-to-gap/phase conversion
|
|
426
|
-
coefficients = [float(row[coef]) for coef in poly_deg]
|
|
427
|
-
polynomial = np.poly1d(coefficients)
|
|
428
|
-
|
|
429
|
-
lookup_table[mode_value]["Energies"][row[min_energy]] = {
|
|
430
|
-
"Low": float(row[min_energy]),
|
|
431
|
-
"High": float(row[max_energy]),
|
|
432
|
-
"Poly": polynomial,
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
# Update energy limits
|
|
436
|
-
lookup_table[mode_value]["Limit"]["Minimum"] = min(
|
|
437
|
-
lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
|
|
438
|
-
)
|
|
439
|
-
lookup_table[mode_value]["Limit"]["Maximum"] = max(
|
|
440
|
-
lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
with open(file, newline="") as csvfile:
|
|
444
|
-
reader = csv.DictReader(csvfile)
|
|
445
|
-
for row in reader:
|
|
446
|
-
# If there are multiple source only convert requested.
|
|
447
|
-
if row[source[0]] == source[1]:
|
|
448
|
-
process_row(row=row)
|
|
449
|
-
if not lookup_table:
|
|
450
|
-
raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
|
|
451
|
-
return lookup_table
|
dodal/devices/i15/dcm.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import StandardReadable
|
|
4
|
+
from ophyd_async.epics.motor import Motor
|
|
5
|
+
|
|
6
|
+
from dodal.devices.common_dcm import (
|
|
7
|
+
StationaryCrystal,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ThetaYCrystal(StationaryCrystal):
|
|
12
|
+
def __init__(self, prefix):
|
|
13
|
+
with self.add_children_as_readables():
|
|
14
|
+
self.theta = Motor(prefix + "THETA")
|
|
15
|
+
self.y = Motor(prefix + "Y")
|
|
16
|
+
super().__init__(prefix)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ThetaRollYZCrystal(ThetaYCrystal):
|
|
20
|
+
def __init__(self, prefix):
|
|
21
|
+
with self.add_children_as_readables():
|
|
22
|
+
self.roll = Motor(prefix + "ROLL")
|
|
23
|
+
self.z = Motor(prefix + "Z")
|
|
24
|
+
super().__init__(prefix)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
|
|
28
|
+
Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DualCrystalMonoSimple(StandardReadable, Generic[Xtal_1, Xtal_2]):
|
|
32
|
+
"""
|
|
33
|
+
Device for simple double crystal monochromators (DCM), which only allow energy of the beam to be selected.
|
|
34
|
+
|
|
35
|
+
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
|
|
36
|
+
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
|
|
37
|
+
This base device accounts for all combinations of this.
|
|
38
|
+
|
|
39
|
+
This device is more able to act as a parent for beamline-specific DCM's, in which any other missing signals can be added,
|
|
40
|
+
as it doesn't assume WAVELENGTH, BRAGG and OFFSET are available for all DCM deivces, as BaseDCM does.
|
|
41
|
+
|
|
42
|
+
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
|
|
43
|
+
which only requires one crystal which can roll should be typed 'def my_plan(dcm: BaseDCM[RollCrystal, StationaryCrystal])`
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
48
|
+
) -> None:
|
|
49
|
+
with self.add_children_as_readables():
|
|
50
|
+
# Virtual motor PV's which set the physical motors so that the DCM produces requested
|
|
51
|
+
# wavelength/energy
|
|
52
|
+
self.energy_in_kev = Motor(prefix + "ENERGY")
|
|
53
|
+
self._make_crystals(prefix, xtal_1, xtal_2)
|
|
54
|
+
|
|
55
|
+
super().__init__(name)
|
|
56
|
+
|
|
57
|
+
# Prefix convention is different depending on whether there are one or two controllable crystals
|
|
58
|
+
def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
|
|
59
|
+
if StationaryCrystal not in [xtal_1, xtal_2]:
|
|
60
|
+
self.xtal_1 = xtal_1(f"{prefix}XTAL1:")
|
|
61
|
+
self.xtal_2 = xtal_2(f"{prefix}XTAL2:")
|
|
62
|
+
else:
|
|
63
|
+
self.xtal_1 = xtal_1(prefix)
|
|
64
|
+
self.xtal_2 = xtal_2(prefix)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DCM(DualCrystalMonoSimple[ThetaRollYZCrystal, ThetaYCrystal]):
|
|
68
|
+
"""
|
|
69
|
+
A double crystal monocromator device, used to select the beam energy.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
73
|
+
with self.add_children_as_readables():
|
|
74
|
+
self.calibrated_energy_in_kev = Motor(prefix + "CAL")
|
|
75
|
+
self.x1 = Motor(prefix + "X1")
|
|
76
|
+
|
|
77
|
+
super().__init__(prefix, ThetaRollYZCrystal, ThetaYCrystal, name)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.epics.motor import Motor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FocusingMirrorBase(StandardReadable):
|
|
6
|
+
"""Focusing Mirror with curve, ellip & pitch"""
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
prefix: str,
|
|
11
|
+
name: str = "",
|
|
12
|
+
):
|
|
13
|
+
with self.add_children_as_readables():
|
|
14
|
+
self.curve = Motor(prefix + "CURVE")
|
|
15
|
+
self.ellipticity = Motor(prefix + "ELLIP")
|
|
16
|
+
self.pitch = Motor(prefix + "PITCH")
|
|
17
|
+
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FocusingMirrorHorizontal(FocusingMirrorBase):
|
|
22
|
+
"""Focusing Mirror with curve, ellip, pitch & X"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
prefix: str,
|
|
27
|
+
name: str = "",
|
|
28
|
+
):
|
|
29
|
+
with self.add_children_as_readables():
|
|
30
|
+
self.x = Motor(prefix + "X")
|
|
31
|
+
|
|
32
|
+
super().__init__(prefix, name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FocusingMirrorVertical(FocusingMirrorBase):
|
|
36
|
+
"""Focusing Mirror with curve, ellip, pitch & Y"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
prefix: str,
|
|
41
|
+
name: str = "",
|
|
42
|
+
):
|
|
43
|
+
with self.add_children_as_readables():
|
|
44
|
+
self.y = Motor(prefix + "Y")
|
|
45
|
+
|
|
46
|
+
super().__init__(prefix, name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FocusingMirror(FocusingMirrorBase):
|
|
50
|
+
"""Focusing Mirror with curve, ellip, pitch, yaw, X & Y"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
prefix: str,
|
|
55
|
+
name: str = "",
|
|
56
|
+
):
|
|
57
|
+
with self.add_children_as_readables():
|
|
58
|
+
self.yaw = Motor(prefix + "YAW")
|
|
59
|
+
self.x = Motor(prefix + "X")
|
|
60
|
+
self.y = Motor(prefix + "Y")
|
|
61
|
+
|
|
62
|
+
super().__init__(prefix, name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FocusingMirrorWithRoll(FocusingMirror):
|
|
66
|
+
"""Focusing Mirror with curve, ellip, pitch, roll, yaw, X & Y"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
69
|
+
with self.add_children_as_readables():
|
|
70
|
+
self.roll = Motor(prefix + "ROLL")
|
|
71
|
+
super().__init__(prefix, name)
|