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.
Files changed (59) hide show
  1. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +56 -50
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +23 -3
  6. dodal/beamlines/i04.py +18 -3
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +7 -2
  11. dodal/beamlines/i10_optics.py +18 -8
  12. dodal/beamlines/i18.py +3 -3
  13. dodal/beamlines/i22.py +3 -3
  14. dodal/beamlines/p38.py +3 -3
  15. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  16. dodal/devices/aperturescatterguard.py +3 -2
  17. dodal/devices/apple2_undulator.py +89 -44
  18. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  19. dodal/devices/beamsize/__init__.py +0 -0
  20. dodal/devices/beamsize/beamsize.py +6 -0
  21. dodal/devices/detector/det_resolution.py +4 -2
  22. dodal/devices/fast_grid_scan.py +14 -2
  23. dodal/devices/i03/beamsize.py +35 -0
  24. dodal/devices/i03/constants.py +7 -0
  25. dodal/devices/i03/undulator_dcm.py +2 -2
  26. dodal/devices/i04/beamsize.py +45 -0
  27. dodal/devices/i04/murko_results.py +36 -26
  28. dodal/devices/i04/transfocator.py +23 -29
  29. dodal/devices/i07/id.py +38 -0
  30. dodal/devices/i09_1_shared/__init__.py +6 -2
  31. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  32. dodal/devices/i10/i10_apple2.py +22 -316
  33. dodal/devices/i17/i17_apple2.py +7 -4
  34. dodal/devices/ipin.py +20 -2
  35. dodal/devices/motors.py +19 -3
  36. dodal/devices/mx_phase1/beamstop.py +31 -12
  37. dodal/devices/oav/oav_calculations.py +9 -4
  38. dodal/devices/oav/oav_detector.py +65 -7
  39. dodal/devices/oav/oav_parameters.py +3 -1
  40. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  41. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  42. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  43. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  44. dodal/devices/oav/utils.py +16 -6
  45. dodal/devices/robot.py +17 -7
  46. dodal/devices/scintillator.py +36 -14
  47. dodal/devices/smargon.py +2 -3
  48. dodal/devices/thawer.py +7 -45
  49. dodal/devices/undulator.py +152 -68
  50. dodal/devices/util/lookup_tables_apple2.py +390 -0
  51. dodal/plans/load_panda_yaml.py +9 -0
  52. dodal/plans/verify_undulator_gap.py +2 -2
  53. dodal/beamline_specific_utils/i03.py +0 -17
  54. dodal/testing/__init__.py +0 -3
  55. dodal/testing/setup.py +0 -67
  56. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
  57. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
  58. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
  59. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,7 @@
1
- import csv
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
- # data class to store the lookup table configuration that is use in convert_csv_to_lookup
40
- @dataclass
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
- I10Apple2 device is an apple2 with extra jaw phase motor.
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
- lookuptable_dir: str,
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
- parameters
367
- ----------
78
+ Parameters:
79
+ -----------
368
80
  apple2 : I10Apple2
369
81
  An I10Apple2 device.
370
- lookuptable_dir : str
371
- The path to look up table.
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.lookup_table_client = I10EnergyMotorLookup(
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.lookup_table_client.get_motor_from_energy,
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)
@@ -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(XYZStage):
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 = "OMEGA",
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 all(
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
- # Move z first as it could be under the table
89
- await self.z_mm.set(self._in_beam_xyz_mm[2])
90
- await asyncio.gather(
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 = -horizontal
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 = -vertical * cosine
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