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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Apple2 lookup table utilities and CSV converter.
|
|
2
|
+
|
|
3
|
+
This module provides helpers to read, validate and convert Apple2 insertion-device
|
|
4
|
+
lookup tables (energy -> gap/phase polynomials) from CSV sources into an
|
|
5
|
+
in-memory dictionary format used by the Apple2 controllers.
|
|
6
|
+
|
|
7
|
+
Data format produced
|
|
8
|
+
The lookup-table dictionary created by convert_csv_to_lookup() follows this
|
|
9
|
+
structure:
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"POL_MODE": {
|
|
13
|
+
"energy_entries": [
|
|
14
|
+
{
|
|
15
|
+
"low": <float>,
|
|
16
|
+
"high": <float>,
|
|
17
|
+
"poly": <numpy.poly1d>
|
|
18
|
+
},
|
|
19
|
+
...
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
...
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import csv
|
|
27
|
+
import io
|
|
28
|
+
from collections.abc import Generator
|
|
29
|
+
from typing import Annotated as A
|
|
30
|
+
from typing import Any, NamedTuple, Self
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
from pydantic import (
|
|
34
|
+
BaseModel,
|
|
35
|
+
ConfigDict,
|
|
36
|
+
Field,
|
|
37
|
+
RootModel,
|
|
38
|
+
field_serializer,
|
|
39
|
+
field_validator,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from dodal.devices.insertion_device.apple2_undulator import Pol
|
|
43
|
+
|
|
44
|
+
DEFAULT_POLY_DEG = [
|
|
45
|
+
"7th-order",
|
|
46
|
+
"6th-order",
|
|
47
|
+
"5th-order",
|
|
48
|
+
"4th-order",
|
|
49
|
+
"3rd-order",
|
|
50
|
+
"2nd-order",
|
|
51
|
+
"1st-order",
|
|
52
|
+
"b",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
MODE_NAME_CONVERT = {"cr": "pc", "cl": "nc"}
|
|
56
|
+
DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
|
|
57
|
+
DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
|
|
58
|
+
|
|
59
|
+
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
60
|
+
ROW_PHASE_CIRCULAR = 15
|
|
61
|
+
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
62
|
+
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
63
|
+
|
|
64
|
+
DEFAULT_POLY1D_PARAMETERS = {
|
|
65
|
+
Pol.LH: [0],
|
|
66
|
+
Pol.LV: [MAXIMUM_ROW_PHASE_MOTOR_POSITION],
|
|
67
|
+
Pol.PC: [ROW_PHASE_CIRCULAR],
|
|
68
|
+
Pol.NC: [-ROW_PHASE_CIRCULAR],
|
|
69
|
+
Pol.LH3: [0],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Source(NamedTuple):
|
|
74
|
+
column: str
|
|
75
|
+
value: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LookupTableColumnConfig(BaseModel):
|
|
79
|
+
"""Configuration on how to process a csv file columns into a LookupTable data model."""
|
|
80
|
+
|
|
81
|
+
source: A[
|
|
82
|
+
Source | None,
|
|
83
|
+
Field(
|
|
84
|
+
description="If not None, only process the row if the source column name match the value."
|
|
85
|
+
),
|
|
86
|
+
] = None
|
|
87
|
+
mode: A[str, Field(description="Polarisation mode column name.")] = "Mode"
|
|
88
|
+
min_energy: A[str, Field(description="Minimum energy column name.")] = "MinEnergy"
|
|
89
|
+
max_energy: A[str, Field(description="Maximum energy column name.")] = "MaxEnergy"
|
|
90
|
+
poly_deg: list[str] = Field(
|
|
91
|
+
description="Polynomial column names.", default_factory=lambda: DEFAULT_POLY_DEG
|
|
92
|
+
)
|
|
93
|
+
mode_name_convert: dict[str, str] = Field(
|
|
94
|
+
description="When processing polarisation mode values, map their alias values to a real value.",
|
|
95
|
+
default_factory=lambda: MODE_NAME_CONVERT,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class EnergyCoverageEntry(BaseModel):
|
|
100
|
+
model_config = ConfigDict(arbitrary_types_allowed=True) # So np.poly1d can be used.
|
|
101
|
+
min_energy: float
|
|
102
|
+
max_energy: float
|
|
103
|
+
poly: np.poly1d
|
|
104
|
+
|
|
105
|
+
@field_validator("poly", mode="before")
|
|
106
|
+
@classmethod
|
|
107
|
+
def validate_and_convert_poly(
|
|
108
|
+
cls: type[Self], value: np.poly1d | list
|
|
109
|
+
) -> np.poly1d:
|
|
110
|
+
"""If reading from serialized data, it will be using a list. Convert to np.poly1d"""
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
return np.poly1d(value)
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
@field_serializer("poly", mode="plain")
|
|
116
|
+
def serialize_poly(self, value: np.poly1d) -> list:
|
|
117
|
+
"""Allow np.poly1d to work when serializing."""
|
|
118
|
+
return value.coefficients.tolist()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class EnergyCoverage(BaseModel):
|
|
122
|
+
energy_entries: list[EnergyCoverageEntry]
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def generate(
|
|
126
|
+
cls: type[Self],
|
|
127
|
+
min_energies: list[float],
|
|
128
|
+
max_energies: list[float],
|
|
129
|
+
poly1d_params: list[list[float]],
|
|
130
|
+
) -> Self:
|
|
131
|
+
energy_entries = [
|
|
132
|
+
EnergyCoverageEntry(
|
|
133
|
+
min_energy=min_energy,
|
|
134
|
+
max_energy=max_energy,
|
|
135
|
+
poly=np.poly1d(poly_params),
|
|
136
|
+
)
|
|
137
|
+
for min_energy, max_energy, poly_params in zip(
|
|
138
|
+
min_energies, max_energies, poly1d_params, strict=True
|
|
139
|
+
)
|
|
140
|
+
]
|
|
141
|
+
return cls(energy_entries=energy_entries)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def min_energy(self) -> float:
|
|
145
|
+
return min(e.min_energy for e in self.energy_entries)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def max_energy(self) -> float:
|
|
149
|
+
return max(e.max_energy for e in self.energy_entries)
|
|
150
|
+
|
|
151
|
+
def get_poly(self, energy: float) -> np.poly1d:
|
|
152
|
+
"""
|
|
153
|
+
Return the numpy.poly1d polynomial applicable for the given energy.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
-----------
|
|
157
|
+
energy:
|
|
158
|
+
Energy value in the same units used to create the lookup table.
|
|
159
|
+
"""
|
|
160
|
+
# Cache initial values so don't do unnecessary work again
|
|
161
|
+
min_energy = self.min_energy
|
|
162
|
+
max_energy = self.max_energy
|
|
163
|
+
if energy < min_energy or energy > max_energy:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"Demanding energy must lie between {min_energy} and {max_energy}!"
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
for energy_coverage in self.energy_entries:
|
|
169
|
+
if (
|
|
170
|
+
energy >= energy_coverage.min_energy
|
|
171
|
+
and energy < energy_coverage.max_energy
|
|
172
|
+
):
|
|
173
|
+
return energy_coverage.poly
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"Cannot find polynomial coefficients for your requested energy."
|
|
176
|
+
+ " There might be gap in the calibration lookup table."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class LookupTable(RootModel[dict[Pol, EnergyCoverage]]):
|
|
181
|
+
"""
|
|
182
|
+
Specialised lookup table for insertion devices to relate the energy and polarisation
|
|
183
|
+
values to Apple2 motor positions.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
# Allow to auto specify a dict if one not provided
|
|
187
|
+
def __init__(self, root: dict[Pol, EnergyCoverage] | None = None):
|
|
188
|
+
super().__init__(root=root or {})
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def generate(
|
|
192
|
+
cls: type[Self],
|
|
193
|
+
pols: list[Pol],
|
|
194
|
+
energy_coverage: list[EnergyCoverage],
|
|
195
|
+
) -> Self:
|
|
196
|
+
"""Generate a LookupTable containing multiple EnergyCoverage
|
|
197
|
+
for provided polarisations."""
|
|
198
|
+
lut = cls()
|
|
199
|
+
for i in range(len(pols)):
|
|
200
|
+
lut.root[pols[i]] = energy_coverage[i]
|
|
201
|
+
return lut
|
|
202
|
+
|
|
203
|
+
def get_poly(
|
|
204
|
+
self,
|
|
205
|
+
energy: float,
|
|
206
|
+
pol: Pol,
|
|
207
|
+
) -> np.poly1d:
|
|
208
|
+
"""
|
|
209
|
+
Return the numpy.poly1d polynomial applicable for the given energy and polarisation.
|
|
210
|
+
|
|
211
|
+
Parameters:
|
|
212
|
+
-----------
|
|
213
|
+
energy:
|
|
214
|
+
Energy value in the same units used to create the lookup table.
|
|
215
|
+
pol:
|
|
216
|
+
Polarisation mode (Pol enum).
|
|
217
|
+
"""
|
|
218
|
+
return self.root[pol].get_poly(energy)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def convert_csv_to_lookup(
|
|
222
|
+
file_contents: str,
|
|
223
|
+
lut_config: LookupTableColumnConfig,
|
|
224
|
+
skip_line_start_with: str = "#",
|
|
225
|
+
) -> LookupTable:
|
|
226
|
+
"""
|
|
227
|
+
Convert CSV content into the Apple2 lookup-table dictionary.
|
|
228
|
+
|
|
229
|
+
Parameters:
|
|
230
|
+
-----------
|
|
231
|
+
file_contents:
|
|
232
|
+
The CSV file contents as string.
|
|
233
|
+
lut_config:
|
|
234
|
+
The configuration that how to process the file_contents into a LookupTable.
|
|
235
|
+
skip_line_start_with
|
|
236
|
+
Lines beginning with this prefix are skipped (default "#").
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
-----------
|
|
240
|
+
LookupTable
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def process_row(row: dict[str, Any], lut: LookupTable) -> None:
|
|
244
|
+
"""Process a single row from the CSV file and update the lookup table."""
|
|
245
|
+
raw_mode_value = str(row[lut_config.mode]).lower()
|
|
246
|
+
mode_value = Pol(
|
|
247
|
+
lut_config.mode_name_convert.get(raw_mode_value, raw_mode_value)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Create polynomial object for energy-to-gap/phase conversion
|
|
251
|
+
coefficients = np.poly1d([float(row[coef]) for coef in lut_config.poly_deg])
|
|
252
|
+
|
|
253
|
+
energy_entry = EnergyCoverageEntry(
|
|
254
|
+
min_energy=float(row[lut_config.min_energy]),
|
|
255
|
+
max_energy=float(row[lut_config.max_energy]),
|
|
256
|
+
poly=coefficients,
|
|
257
|
+
)
|
|
258
|
+
if mode_value not in lut.root:
|
|
259
|
+
lut.root[mode_value] = EnergyCoverage(energy_entries=[energy_entry])
|
|
260
|
+
else:
|
|
261
|
+
lut.root[mode_value].energy_entries.append(energy_entry)
|
|
262
|
+
|
|
263
|
+
reader = csv.DictReader(read_file_and_skip(file_contents, skip_line_start_with))
|
|
264
|
+
lut = LookupTable()
|
|
265
|
+
|
|
266
|
+
for row in reader:
|
|
267
|
+
source = lut_config.source
|
|
268
|
+
# If there are multiple source only convert requested.
|
|
269
|
+
if source is None or row[source.column] == source.value:
|
|
270
|
+
process_row(row=row, lut=lut)
|
|
271
|
+
|
|
272
|
+
# Check if our LookupTable is empty after processing, raise error if it is.
|
|
273
|
+
if not lut.root:
|
|
274
|
+
raise RuntimeError(
|
|
275
|
+
"LookupTable content is empty, failed to convert the file contents to "
|
|
276
|
+
"a LookupTable!"
|
|
277
|
+
)
|
|
278
|
+
return lut
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def read_file_and_skip(file: str, skip_line_start_with: str = "#") -> Generator[str]:
|
|
282
|
+
"""Yield non-comment lines from the CSV content string."""
|
|
283
|
+
for line in io.StringIO(file):
|
|
284
|
+
if line.startswith(skip_line_start_with):
|
|
285
|
+
continue
|
|
286
|
+
else:
|
|
287
|
+
yield line
|
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,
|
|
@@ -105,6 +122,20 @@ class XYPitchStage(XYStage):
|
|
|
105
122
|
super().__init__(prefix, name, x_infix, y_infix)
|
|
106
123
|
|
|
107
124
|
|
|
125
|
+
class XYRollStage(XYStage):
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
prefix: str,
|
|
129
|
+
x_infix: str = _X,
|
|
130
|
+
y_infix: str = _Y,
|
|
131
|
+
roll_infix: str = "ROLL",
|
|
132
|
+
name: str = "",
|
|
133
|
+
) -> None:
|
|
134
|
+
with self.add_children_as_readables():
|
|
135
|
+
self.roll = Motor(prefix + roll_infix)
|
|
136
|
+
super().__init__(prefix, name, x_infix, y_infix)
|
|
137
|
+
|
|
138
|
+
|
|
108
139
|
class XYZPitchYawStage(XYZStage):
|
|
109
140
|
def __init__(
|
|
110
141
|
self,
|
|
@@ -141,7 +172,7 @@ class XYZPitchYawRollStage(XYZStage):
|
|
|
141
172
|
super().__init__(prefix, name, x_infix, y_infix, z_infix)
|
|
142
173
|
|
|
143
174
|
|
|
144
|
-
class SixAxisGonio(
|
|
175
|
+
class SixAxisGonio(XYZOmegaStage):
|
|
145
176
|
def __init__(
|
|
146
177
|
self,
|
|
147
178
|
prefix: str,
|
|
@@ -151,7 +182,7 @@ class SixAxisGonio(XYZStage):
|
|
|
151
182
|
z_infix: str = _Z,
|
|
152
183
|
kappa_infix: str = "KAPPA",
|
|
153
184
|
phi_infix: str = "PHI",
|
|
154
|
-
omega_infix: str =
|
|
185
|
+
omega_infix: str = _OMEGA,
|
|
155
186
|
):
|
|
156
187
|
"""Six-axis goniometer with a standard xyz stage and three axes of rotation:
|
|
157
188
|
kappa, phi and omega.
|
|
@@ -159,7 +190,6 @@ class SixAxisGonio(XYZStage):
|
|
|
159
190
|
with self.add_children_as_readables():
|
|
160
191
|
self.kappa = Motor(prefix + kappa_infix)
|
|
161
192
|
self.phi = Motor(prefix + phi_infix)
|
|
162
|
-
self.omega = Motor(prefix + omega_infix)
|
|
163
193
|
super().__init__(prefix, name, x_infix, y_infix, z_infix)
|
|
164
194
|
|
|
165
195
|
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
|
|
|
@@ -46,9 +46,14 @@ class NullZoomController(BaseZoomController):
|
|
|
46
46
|
def __init__(self):
|
|
47
47
|
self.level = soft_signal_rw(str, "1.0x")
|
|
48
48
|
self.percentage = soft_signal_rw(float, 100)
|
|
49
|
+
super().__init__()
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
@AsyncStatus.wrap
|
|
52
|
+
async def set(self, value: str) -> None:
|
|
53
|
+
if value != "1.0x":
|
|
54
|
+
raise Exception("Attempting to set zoom level of a null zoom controller")
|
|
55
|
+
else:
|
|
56
|
+
await self.level.set(value, wait=True)
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
class ZoomController(BaseZoomController):
|
|
@@ -74,6 +79,16 @@ class ZoomController(BaseZoomController):
|
|
|
74
79
|
|
|
75
80
|
|
|
76
81
|
class OAV(StandardReadable):
|
|
82
|
+
"""
|
|
83
|
+
Class for oav device
|
|
84
|
+
|
|
85
|
+
x_direction(int): Should only be 1 or -1, with 1 indicating the oav x direction is the same with motor x
|
|
86
|
+
y_direction(int): Same with x_direction but for motor y
|
|
87
|
+
z_direction(int): Same with x_direction but for motor z
|
|
88
|
+
mjpg_x_size_pv(str): PV infix for x_size in mjpg
|
|
89
|
+
mjpg_y_size_pv(str): PV infix for y_size in mjpg
|
|
90
|
+
"""
|
|
91
|
+
|
|
77
92
|
beam_centre_i: SignalR[int]
|
|
78
93
|
beam_centre_j: SignalR[int]
|
|
79
94
|
|
|
@@ -82,7 +97,13 @@ class OAV(StandardReadable):
|
|
|
82
97
|
prefix: str,
|
|
83
98
|
config: OAVConfigBase,
|
|
84
99
|
name: str = "",
|
|
100
|
+
mjpeg_prefix: str = "MJPG",
|
|
85
101
|
zoom_controller: BaseZoomController | None = None,
|
|
102
|
+
x_direction: int = -1,
|
|
103
|
+
y_direction: int = -1,
|
|
104
|
+
z_direction: int = 1,
|
|
105
|
+
mjpg_x_size_pv: str = "ArraySize1_RBV",
|
|
106
|
+
mjpg_y_size_pv: str = "ArraySize2_RBV",
|
|
86
107
|
):
|
|
87
108
|
self.oav_config = config
|
|
88
109
|
self._prefix = prefix
|
|
@@ -98,9 +119,15 @@ class OAV(StandardReadable):
|
|
|
98
119
|
|
|
99
120
|
self.cam = Cam(f"{prefix}CAM:", name=name)
|
|
100
121
|
with self.add_children_as_readables():
|
|
101
|
-
self.grid_snapshot = SnapshotWithGrid(
|
|
122
|
+
self.grid_snapshot = SnapshotWithGrid(
|
|
123
|
+
f"{prefix}{mjpeg_prefix}:", name, mjpg_x_size_pv, mjpg_y_size_pv
|
|
124
|
+
)
|
|
102
125
|
|
|
103
126
|
self.sizes = [self.grid_snapshot.x_size, self.grid_snapshot.y_size]
|
|
127
|
+
with self.add_children_as_readables():
|
|
128
|
+
self.x_direction = soft_signal_rw(int, x_direction, name="x_direction")
|
|
129
|
+
self.y_direction = soft_signal_rw(int, y_direction, name="y_direction")
|
|
130
|
+
self.z_direction = soft_signal_rw(int, z_direction, name="z_direction")
|
|
104
131
|
|
|
105
132
|
with self.add_children_as_readables():
|
|
106
133
|
self.microns_per_pixel_x = derived_signal_r(
|
|
@@ -116,7 +143,7 @@ class OAV(StandardReadable):
|
|
|
116
143
|
coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
|
|
117
144
|
)
|
|
118
145
|
self.snapshot = Snapshot(
|
|
119
|
-
f"{self._prefix}
|
|
146
|
+
f"{self._prefix}{mjpeg_prefix}:",
|
|
120
147
|
self._name,
|
|
121
148
|
)
|
|
122
149
|
|
|
@@ -143,9 +170,16 @@ class OAV(StandardReadable):
|
|
|
143
170
|
|
|
144
171
|
|
|
145
172
|
class OAVBeamCentreFile(OAV):
|
|
146
|
-
"""
|
|
173
|
+
"""
|
|
174
|
+
OAV device that reads its beam centre values from a file. The config parameter
|
|
147
175
|
must be a OAVConfigBeamCentre object, as this contains a filepath to where the beam
|
|
148
176
|
centre values are stored.
|
|
177
|
+
|
|
178
|
+
x_direction(int): Should only be 1 or -1, with 1 indicating the oav x direction is the same with motor x
|
|
179
|
+
y_direction(int): Same with x_direction but for motor y
|
|
180
|
+
z_direction(int): Same with x_direction but for motor z
|
|
181
|
+
mjpg_x_size_pv(str): PV infix for x_size in mjpg
|
|
182
|
+
mjpg_y_size_pv(str): PV infix for y_size in mjpg
|
|
149
183
|
"""
|
|
150
184
|
|
|
151
185
|
def __init__(
|
|
@@ -153,9 +187,26 @@ class OAVBeamCentreFile(OAV):
|
|
|
153
187
|
prefix: str,
|
|
154
188
|
config: OAVConfigBeamCentre,
|
|
155
189
|
name: str = "",
|
|
190
|
+
mjpeg_prefix: str = "MJPG",
|
|
156
191
|
zoom_controller: BaseZoomController | None = None,
|
|
192
|
+
mjpg_x_size_pv: str = "ArraySize1_RBV",
|
|
193
|
+
mjpg_y_size_pv: str = "ArraySize2_RBV",
|
|
194
|
+
x_direction: int = -1,
|
|
195
|
+
y_direction: int = -1,
|
|
196
|
+
z_direction: int = 1,
|
|
157
197
|
):
|
|
158
|
-
super().__init__(
|
|
198
|
+
super().__init__(
|
|
199
|
+
prefix=prefix,
|
|
200
|
+
config=config,
|
|
201
|
+
name=name,
|
|
202
|
+
mjpeg_prefix=mjpeg_prefix,
|
|
203
|
+
zoom_controller=zoom_controller,
|
|
204
|
+
mjpg_x_size_pv=mjpg_x_size_pv,
|
|
205
|
+
mjpg_y_size_pv=mjpg_y_size_pv,
|
|
206
|
+
x_direction=x_direction,
|
|
207
|
+
y_direction=y_direction,
|
|
208
|
+
z_direction=z_direction,
|
|
209
|
+
)
|
|
159
210
|
|
|
160
211
|
with self.add_children_as_readables():
|
|
161
212
|
self.beam_centre_i = derived_signal_r(
|
|
@@ -189,6 +240,7 @@ class OAVBeamCentrePV(OAV):
|
|
|
189
240
|
prefix: str,
|
|
190
241
|
config: OAVConfig,
|
|
191
242
|
name: str = "",
|
|
243
|
+
mjpeg_prefix: str = "MJPG",
|
|
192
244
|
zoom_controller: BaseZoomController | None = None,
|
|
193
245
|
overlay_channel: int = 1,
|
|
194
246
|
):
|
|
@@ -199,4 +251,10 @@ class OAVBeamCentrePV(OAV):
|
|
|
199
251
|
self.beam_centre_j = epics_signal_r(
|
|
200
252
|
int, prefix + f"OVER:{overlay_channel}:CenterY"
|
|
201
253
|
)
|
|
202
|
-
super().__init__(
|
|
254
|
+
super().__init__(
|
|
255
|
+
prefix=prefix,
|
|
256
|
+
config=config,
|
|
257
|
+
name=name,
|
|
258
|
+
mjpeg_prefix=mjpeg_prefix,
|
|
259
|
+
zoom_controller=zoom_controller,
|
|
260
|
+
)
|
|
@@ -92,8 +92,10 @@ class OAVParameters:
|
|
|
92
92
|
self.preprocess_K_size: int = update(
|
|
93
93
|
"preProcessKSize", int
|
|
94
94
|
) # length scale for blur preprocessing
|
|
95
|
+
self.preprocess_iter: int = update("preProcessIteration", int, default=5)
|
|
95
96
|
self.detection_script_filename: str = update("filename", str)
|
|
96
|
-
self.
|
|
97
|
+
self.open_ksize: int = update("open_ksize", int, default=0)
|
|
98
|
+
self.close_ksize: int = update("close_ksize", int, default=5)
|
|
97
99
|
self.min_callback_time: float = update("min_callback_time", float, default=0.08)
|
|
98
100
|
self.direction: int = update("direction", int)
|
|
99
101
|
self.max_tip_distance: float = update("max_tip_distance", float, default=300)
|