dls-dodal 1.58.0__py3-none-any.whl → 1.59.1__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.59.1.dist-info}/METADATA +2 -1
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/RECORD +58 -43
- dodal/_version.py +2 -2
- 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/i19_2.py +11 -0
- 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/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/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.59.1.dist-info}/WHEEL +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.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/i22/nxsas.py
CHANGED
|
@@ -7,6 +7,7 @@ from bluesky.protocols import Reading
|
|
|
7
7
|
from event_model.documents.event_descriptor import DataKey
|
|
8
8
|
from ophyd_async.core import PathProvider
|
|
9
9
|
from ophyd_async.epics.adaravis import AravisDetector
|
|
10
|
+
from ophyd_async.epics.adcore import NDPluginBaseIO
|
|
10
11
|
from ophyd_async.epics.adpilatus import PilatusDetector
|
|
11
12
|
|
|
12
13
|
ValueAndUnits = tuple[float, str]
|
|
@@ -106,6 +107,7 @@ class NXSasPilatus(PilatusDetector):
|
|
|
106
107
|
fileio_suffix: str,
|
|
107
108
|
metadata_holder: NXSasMetadataHolder,
|
|
108
109
|
name: str = "",
|
|
110
|
+
plugins: dict[str, NDPluginBaseIO] | None = None,
|
|
109
111
|
):
|
|
110
112
|
"""Extends detector with configuration metadata required or desired
|
|
111
113
|
to comply with the NXsas application definition.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from bluesky.protocols import StreamAsset
|
|
6
|
+
from event_model import DataKey # type: ignore
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
AutoIncrementingPathProvider,
|
|
9
|
+
DetectorWriter,
|
|
10
|
+
StandardDetector,
|
|
11
|
+
StandardReadable,
|
|
12
|
+
StaticPathProvider,
|
|
13
|
+
observe_value,
|
|
14
|
+
wait_for_value,
|
|
15
|
+
)
|
|
16
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv
|
|
17
|
+
from ophyd_async.fastcs.jungfrau._controller import JungfrauController
|
|
18
|
+
from ophyd_async.fastcs.jungfrau._signals import JungfrauDriverIO
|
|
19
|
+
|
|
20
|
+
from dodal.log import LOGGER
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
|
|
24
|
+
"""Implementation of the temporary filewriter used for Jungfrau commissioning on i24.
|
|
25
|
+
|
|
26
|
+
The PVs on this device are responsible for writing files of a specified name
|
|
27
|
+
to a specified path, marking itself as "ready to write", and having a counter of
|
|
28
|
+
frames written, which must be zero'd at the ophyd level
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
prefix,
|
|
34
|
+
path_provider: AutoIncrementingPathProvider | StaticPathProvider,
|
|
35
|
+
name="",
|
|
36
|
+
) -> None:
|
|
37
|
+
with self.add_children_as_readables():
|
|
38
|
+
self._path_info = path_provider
|
|
39
|
+
self.frame_counter = epics_signal_rw(int, f"{prefix}NumCaptured")
|
|
40
|
+
self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName")
|
|
41
|
+
self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath")
|
|
42
|
+
self.writer_ready = epics_signal_r(int, f"{prefix}Ready_RBV")
|
|
43
|
+
super().__init__(name)
|
|
44
|
+
|
|
45
|
+
async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
|
|
46
|
+
self._exposures_per_event = exposures_per_event
|
|
47
|
+
_path_info = self._path_info()
|
|
48
|
+
|
|
49
|
+
# Commissioning Jungfrau plans allow you to override path, so check to see if file exists
|
|
50
|
+
requested_filepath = Path(_path_info.directory_path) / _path_info.filename
|
|
51
|
+
if requested_filepath.exists():
|
|
52
|
+
raise FileExistsError(
|
|
53
|
+
f"Jungfrau was requested to write to {requested_filepath}, but this file already exists!"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
await asyncio.gather(
|
|
57
|
+
self.file_name.set(_path_info.filename),
|
|
58
|
+
self.file_path.set(str(_path_info.directory_path)),
|
|
59
|
+
self.frame_counter.set(0),
|
|
60
|
+
)
|
|
61
|
+
LOGGER.info(
|
|
62
|
+
f"Jungfrau writing to folder {_path_info.directory_path} with filename {_path_info.filename}"
|
|
63
|
+
)
|
|
64
|
+
await wait_for_value(self.writer_ready, 1, timeout=10)
|
|
65
|
+
return await self._describe()
|
|
66
|
+
|
|
67
|
+
async def _describe(self) -> dict[str, DataKey]:
|
|
68
|
+
# Dummy function, doesn't actually describe the dataset
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"data": DataKey(
|
|
72
|
+
source="Commissioning writer",
|
|
73
|
+
shape=[-1],
|
|
74
|
+
dtype="array",
|
|
75
|
+
dtype_numpy="<u2",
|
|
76
|
+
external="STREAM:",
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async def observe_indices_written(
|
|
81
|
+
self, timeout: float
|
|
82
|
+
) -> AsyncGenerator[int, None]:
|
|
83
|
+
timeout = timeout * 2 # This filewriter is slow
|
|
84
|
+
async for num_captured in observe_value(self.frame_counter, timeout):
|
|
85
|
+
yield num_captured // (self._exposures_per_event)
|
|
86
|
+
|
|
87
|
+
async def get_indices_written(self) -> int:
|
|
88
|
+
return await self.frame_counter.get_value() // self._exposures_per_event
|
|
89
|
+
|
|
90
|
+
def collect_stream_docs(
|
|
91
|
+
self, name: str, indices_written: int
|
|
92
|
+
) -> AsyncIterator[StreamAsset]:
|
|
93
|
+
raise NotImplementedError()
|
|
94
|
+
|
|
95
|
+
async def close(self) -> None: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CommissioningJungfrau(
|
|
99
|
+
StandardDetector[JungfrauController, JunfrauCommissioningWriter]
|
|
100
|
+
):
|
|
101
|
+
"""Ophyd-async implementation of a Jungfrau 9M Detector, using a temporary
|
|
102
|
+
filewriter in place of Odin"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
prefix: str,
|
|
107
|
+
writer_prefix: str,
|
|
108
|
+
path_provider: AutoIncrementingPathProvider | StaticPathProvider,
|
|
109
|
+
name="",
|
|
110
|
+
):
|
|
111
|
+
self.drv = JungfrauDriverIO(prefix)
|
|
112
|
+
writer = JunfrauCommissioningWriter(writer_prefix, path_provider)
|
|
113
|
+
controller = JungfrauController(self.drv)
|
|
114
|
+
super().__init__(controller, writer, name=name)
|