dls-dodal 1.32.0__py3-none-any.whl → 1.34.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.
Files changed (55) hide show
  1. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/RECORD +53 -43
  3. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/WHEEL +1 -1
  4. dodal/__init__.py +8 -0
  5. dodal/_version.py +2 -2
  6. dodal/beamline_specific_utils/i03.py +6 -2
  7. dodal/beamlines/__init__.py +2 -3
  8. dodal/beamlines/b01_1.py +77 -0
  9. dodal/beamlines/i03.py +41 -9
  10. dodal/beamlines/i04.py +26 -4
  11. dodal/beamlines/i10.py +257 -0
  12. dodal/beamlines/i22.py +1 -2
  13. dodal/beamlines/i24.py +7 -7
  14. dodal/beamlines/p38.py +1 -2
  15. dodal/common/signal_utils.py +53 -0
  16. dodal/common/types.py +2 -7
  17. dodal/devices/aperturescatterguard.py +12 -15
  18. dodal/devices/apple2_undulator.py +602 -0
  19. dodal/devices/areadetector/plugins/CAM.py +31 -0
  20. dodal/devices/areadetector/plugins/MJPG.py +51 -106
  21. dodal/devices/backlight.py +7 -6
  22. dodal/devices/diamond_filter.py +47 -0
  23. dodal/devices/eiger.py +6 -2
  24. dodal/devices/eiger_odin.py +48 -39
  25. dodal/devices/focusing_mirror.py +14 -8
  26. dodal/devices/i10/i10_apple2.py +398 -0
  27. dodal/devices/i10/i10_setting_data.py +7 -0
  28. dodal/devices/i22/dcm.py +7 -8
  29. dodal/devices/i24/dual_backlight.py +5 -5
  30. dodal/devices/oav/oav_calculations.py +22 -0
  31. dodal/devices/oav/oav_detector.py +118 -97
  32. dodal/devices/oav/oav_parameters.py +50 -104
  33. dodal/devices/oav/oav_to_redis_forwarder.py +75 -34
  34. dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +0 -43
  35. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
  36. dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
  37. dodal/devices/oav/utils.py +26 -25
  38. dodal/devices/pgm.py +41 -0
  39. dodal/devices/qbpm.py +18 -0
  40. dodal/devices/robot.py +2 -2
  41. dodal/devices/smargon.py +2 -2
  42. dodal/devices/tetramm.py +2 -2
  43. dodal/devices/undulator.py +2 -1
  44. dodal/devices/util/adjuster_plans.py +1 -1
  45. dodal/devices/util/lookup_tables.py +4 -5
  46. dodal/devices/zebra.py +5 -2
  47. dodal/devices/zocalo/zocalo_results.py +13 -10
  48. dodal/plans/data_session_metadata.py +2 -2
  49. dodal/plans/motor_util_plans.py +11 -9
  50. dodal/utils.py +7 -0
  51. dodal/beamlines/i04_1.py +0 -140
  52. dodal/devices/oav/oav_errors.py +0 -35
  53. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/LICENSE +0 -0
  54. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/entry_points.txt +0 -0
  55. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,398 @@
1
+ import asyncio
2
+ import csv
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+ from bluesky.protocols import Movable
9
+ from ophyd_async.core import (
10
+ AsyncStatus,
11
+ HintedSignal,
12
+ StandardReadable,
13
+ soft_signal_r_and_setter,
14
+ soft_signal_rw,
15
+ )
16
+
17
+ from dodal.devices.apple2_undulator import (
18
+ Apple2,
19
+ Apple2Val,
20
+ Lookuptable,
21
+ UndulatorGap,
22
+ UndulatorJawPhase,
23
+ UndulatorPhaseAxes,
24
+ )
25
+ from dodal.devices.pgm import PGM
26
+ from dodal.log import LOGGER
27
+
28
+ ROW_PHASE_MOTOR_TOLERANCE = 0.004
29
+ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
30
+ MAXIMUM_GAP_MOTOR_POSITION = 100
31
+ DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
32
+ ALPHA_OFFSET = 180
33
+
34
+
35
+ # data class to store the lookup table configuration that is use in convert_csv_to_lookup
36
+ @dataclass
37
+ class LookupPath:
38
+ Gap: Path
39
+ Phase: Path
40
+
41
+
42
+ @dataclass
43
+ class LookupTableConfig:
44
+ path: LookupPath
45
+ source: tuple[str, str]
46
+ mode: str | None
47
+ min_energy: str | None
48
+ max_energy: str | None
49
+ poly_deg: list | None
50
+
51
+
52
+ class I10Apple2(Apple2):
53
+ """
54
+ I10Apple2 is the i10 version of Apple2 ID.
55
+ The set and update_lookuptable should be the only part that is I10 specific.
56
+
57
+ A pair of look up tables are needed to provide the conversion
58
+ between motor position and energy.
59
+ Set is in energy(eV).
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ id_gap: UndulatorGap,
65
+ id_phase: UndulatorPhaseAxes,
66
+ id_jaw_phase: UndulatorJawPhase,
67
+ energy_gap_table_path: Path,
68
+ energy_phase_table_path: Path,
69
+ source: tuple[str, str],
70
+ prefix: str = "",
71
+ mode: str = "Mode",
72
+ min_energy: str = "MinEnergy",
73
+ max_energy: str = "MaxEnergy",
74
+ poly_deg: list | None = None,
75
+ name: str = "",
76
+ ) -> None:
77
+ """
78
+ Parameters
79
+ ----------
80
+ id_gap:
81
+ An UndulatorGap device.
82
+ id_phase:
83
+ An UndulatorPhaseAxes device.
84
+ energy_gap_table_path:
85
+ The path to id gap look up table.
86
+ energy_phase_table_path:
87
+ The path to id phase look up table.
88
+ source:
89
+ The column name and the name of the source in look up table. e.g. ("source", "idu")
90
+ mode:
91
+ The column name of the mode in look up table.
92
+ min_energy:
93
+ The column name that contain the maximum energy in look up table.
94
+ max_energy:
95
+ The column name that contain the maximum energy in look up table.
96
+ poly_deg:
97
+ The column names for the parameters for the energy conversion polynomial, starting with the least significant.
98
+ prefix:
99
+ Not in use but needed for device_instantiation.
100
+ Name:
101
+ Name of the device
102
+ """
103
+
104
+ # A dataclass contains the path to the look up table and the expected column names.
105
+ self.lookup_table_config = LookupTableConfig(
106
+ path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
107
+ source=source,
108
+ mode=mode,
109
+ min_energy=min_energy,
110
+ max_energy=max_energy,
111
+ poly_deg=poly_deg,
112
+ )
113
+
114
+ super().__init__(
115
+ id_gap=id_gap,
116
+ id_phase=id_phase,
117
+ prefix=prefix,
118
+ name=name,
119
+ )
120
+ with self.add_children_as_readables():
121
+ self.id_jaw_phase = id_jaw_phase
122
+
123
+ @AsyncStatus.wrap
124
+ async def set(self, value: float) -> None:
125
+ """
126
+ Check polarisation state and use it together with the energy(value)
127
+ to calculate the required gap and phases before setting it.
128
+ """
129
+ if self.pol is None:
130
+ LOGGER.warning("Polarisation not set attempting to read from hardware")
131
+ pol, phase = await self.determinePhaseFromHardware()
132
+ if pol is None:
133
+ raise ValueError(f"Pol is not set for {self.name}")
134
+ self.pol = pol
135
+
136
+ self._polarisation_set(self.pol)
137
+ gap, phase = self._get_id_gap_phase(value)
138
+ phase3 = phase * (-1 if self.pol == "la" else (1))
139
+ id_set_val = Apple2Val(
140
+ top_outer=str(phase),
141
+ top_inner="0.0",
142
+ btm_inner=str(phase3),
143
+ btm_outer="0.0",
144
+ gap=str(gap),
145
+ )
146
+ LOGGER.info(f"Setting polarisation to {self.pol}, with {id_set_val}")
147
+ await self._set(value=id_set_val, energy=value)
148
+ if self.pol != "la":
149
+ await self.id_jaw_phase.set(0)
150
+ await self.id_jaw_phase.set_move.set(1)
151
+
152
+ def update_lookuptable(self):
153
+ """
154
+ Update the stored lookup tabled from file.
155
+
156
+ """
157
+ LOGGER.info("Updating lookup dictionary from file.")
158
+ for key, path in self.lookup_table_config.path.__dict__.items():
159
+ if path.exists():
160
+ self.lookup_tables[key] = convert_csv_to_lookup(
161
+ file=path,
162
+ source=self.lookup_table_config.source,
163
+ mode=self.lookup_table_config.mode,
164
+ min_energy=self.lookup_table_config.min_energy,
165
+ max_energy=self.lookup_table_config.max_energy,
166
+ poly_deg=self.lookup_table_config.poly_deg,
167
+ )
168
+ # ensure the importing lookup table is the correct format
169
+ Lookuptable.model_validate(self.lookup_tables[key])
170
+ else:
171
+ raise FileNotFoundError(f"{key} look up table is not in path: {path}")
172
+
173
+ self._available_pol = list(self.lookup_tables["Gap"].keys())
174
+
175
+
176
+ class I10Apple2PGM(StandardReadable, Movable):
177
+ """
178
+ Compound device to set both ID and PGM energy at the sample time,poly_deg
179
+
180
+ """
181
+
182
+ def __init__(
183
+ self, id: I10Apple2, pgm: PGM, prefix: str = "", name: str = ""
184
+ ) -> None:
185
+ """
186
+ Parameters
187
+ ----------
188
+ id:
189
+ An Apple2 device.
190
+ pgm:
191
+ A PGM/mono device.
192
+ prefix:
193
+ Not in use but needed for device_instantiation.
194
+ name:
195
+ New device name.
196
+ """
197
+ super().__init__(name=name)
198
+ with self.add_children_as_readables():
199
+ self.id = id
200
+ self.pgm = pgm
201
+ with self.add_children_as_readables(HintedSignal):
202
+ self.energy_offset = soft_signal_rw(float, initial_value=0)
203
+
204
+ @AsyncStatus.wrap
205
+ async def set(self, value: float) -> None:
206
+ LOGGER.info(f"Moving f{self.name} energy to {value}.")
207
+ await asyncio.gather(
208
+ self.id.set(value=value + await self.energy_offset.get_value()),
209
+ self.pgm.energy.set(value),
210
+ )
211
+
212
+
213
+ class I10Apple2Pol(StandardReadable, Movable):
214
+ """
215
+ Compound device to set polorisation of ID.
216
+ """
217
+
218
+ def __init__(self, id: I10Apple2, prefix: str = "", name: str = "") -> None:
219
+ """
220
+ Parameters
221
+ ----------
222
+ id:
223
+ An I10Apple2 device.
224
+ prefix:
225
+ Not in use but needed for device_instantiation.
226
+ name:
227
+ New device name.
228
+ """
229
+ super().__init__(name=name)
230
+ with self.add_children_as_readables():
231
+ self.id = id
232
+
233
+ @AsyncStatus.wrap
234
+ async def set(self, value: str) -> None:
235
+ self.id.pol = value # change polarisation.
236
+ LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
237
+ await self.id.set(
238
+ await self.id.energy.get_value()
239
+ ) # Move id to new polarisation
240
+
241
+
242
+ class LinearArbitraryAngle(StandardReadable, Movable):
243
+ """
244
+ Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
245
+ is the direction of the magnetic field which can be change by varying the jaw_phase
246
+ in (linear arbitrary (la) mode,
247
+ The angle of 0 is equivalent to linear horizontal "lh" (sigma) and
248
+ 90 is linear vertical "lv" (pi).
249
+ This device require a jaw_phase to angle conversion which is done via a polynomial.
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ id: I10Apple2,
255
+ prefix: str = "",
256
+ name: str = "",
257
+ jaw_phase_limit: float = 12.0,
258
+ jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
259
+ angle_threshold_deg=30.0,
260
+ ) -> None:
261
+ """
262
+ Parameters
263
+ ----------
264
+ id: I10Apple2
265
+ An I10Apple2 device.
266
+ prefix: str
267
+ Not in use but needed for device_instantiation.
268
+ name: str
269
+ New device name.
270
+ jaw_phase_limit: float
271
+ The maximum allowed jaw_phase movement.
272
+ jaw_phase_poly_param: list
273
+ polynomial parameters highest power first.
274
+ """
275
+ super().__init__(name=name)
276
+ with self.add_children_as_readables():
277
+ self.id = id
278
+ self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
279
+ self.angle_threshold_deg = angle_threshold_deg
280
+ self.jaw_phase_limit = jaw_phase_limit
281
+ with self.add_children_as_readables(HintedSignal):
282
+ self.angle, self._angle_set = soft_signal_r_and_setter(
283
+ float, initial_value=None
284
+ )
285
+
286
+ @AsyncStatus.wrap
287
+ async def set(self, value: float) -> None:
288
+ pol = self.id.pol
289
+ if pol != "la":
290
+ raise RuntimeError(
291
+ f"Angle control is not available in polarisation {pol} with {self.id.name}"
292
+ )
293
+ # Moving to real angle which is 210 to 30.
294
+ alpha_real = value if value > self.angle_threshold_deg else value + ALPHA_OFFSET
295
+ jaw_phase = self.jaw_phase_from_angle(alpha_real)
296
+ if abs(jaw_phase) > self.jaw_phase_limit:
297
+ raise RuntimeError(
298
+ f"jaw_phase position for angle ({value}) is outside permitted range"
299
+ f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
300
+ )
301
+ await self.id.id_jaw_phase.set(jaw_phase)
302
+ self._angle_set(value)
303
+
304
+
305
+ def convert_csv_to_lookup(
306
+ file: str,
307
+ source: tuple[str, str],
308
+ mode: str | None = "Mode",
309
+ min_energy: str | None = "MinEnergy",
310
+ max_energy: str | None = "MaxEnergy",
311
+ poly_deg: list | None = None,
312
+ ) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
313
+ """
314
+ Convert csv to a dictionary that can be read by Apple2 ID device.
315
+
316
+ Parameters
317
+ -----------
318
+ file: str
319
+ File path.
320
+ source: tuple[str, str]
321
+ Tuple(column name, source name)
322
+ e.g. ("Source", "idu").
323
+ mode: str = "Mode"
324
+ Column name for the available modes, "lv","lh","pc","nc" etc
325
+ min_energy: str = "MinEnergy":
326
+ Column name for min energy for the polynomial.
327
+ max_energy: str = "MaxEnergy",
328
+ Column name for max energy for the polynomial.
329
+ poly_deg: list | None = None,
330
+ Column names for the parameters for the polynomial, starting with the least significant.
331
+
332
+ return
333
+ ------
334
+ return a dictionary that conform to Apple2 lookup table format:
335
+
336
+ {mode: {'Energies': {Any: {'Low': float,
337
+ 'High': float,
338
+ 'Poly':np.poly1d
339
+ }
340
+ }
341
+ 'Limit': {'Minimum': float,
342
+ 'Maximum': float
343
+ }
344
+ }
345
+ }
346
+ """
347
+ if poly_deg is None:
348
+ poly_deg = [
349
+ "7th-order",
350
+ "6th-order",
351
+ "5th-order",
352
+ "4th-order",
353
+ "3rd-order",
354
+ "2nd-order",
355
+ "1st-order",
356
+ "b",
357
+ ]
358
+ look_up_table = {}
359
+ pol = []
360
+
361
+ def data2dict(row) -> None:
362
+ # logic for the conversion for each row of data.
363
+ if row[mode] not in pol:
364
+ pol.append(row[mode])
365
+ look_up_table[row[mode]] = {}
366
+ look_up_table[row[mode]] = {
367
+ "Energies": {},
368
+ "Limit": {
369
+ "Minimum": float(row[min_energy]),
370
+ "Maximum": float(row[max_energy]),
371
+ },
372
+ }
373
+
374
+ # create polynomial object for energy to gap/phase
375
+ cof = [float(row[x]) for x in poly_deg]
376
+ poly = np.poly1d(cof)
377
+
378
+ look_up_table[row[mode]]["Energies"][row[min_energy]] = {
379
+ "Low": float(row[min_energy]),
380
+ "High": float(row[max_energy]),
381
+ "Poly": poly,
382
+ }
383
+ look_up_table[row[mode]]["Limit"]["Minimum"] = min(
384
+ look_up_table[row[mode]]["Limit"]["Minimum"], float(row[min_energy])
385
+ )
386
+ look_up_table[row[mode]]["Limit"]["Maximum"] = max(
387
+ look_up_table[row[mode]]["Limit"]["Maximum"], float(row[max_energy])
388
+ )
389
+
390
+ with open(file, newline="") as csvfile:
391
+ reader = csv.DictReader(csvfile)
392
+ for row in reader:
393
+ # If there are multiple source only convert requested.
394
+ if row[source[0]] == source[1]:
395
+ data2dict(row=row)
396
+ if not look_up_table:
397
+ raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
398
+ return look_up_table
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class I10Grating(str, Enum):
5
+ Au400 = "400 line/mm Au"
6
+ Si400 = "400 line/mm Si"
7
+ Au1200 = "1200 line/mm Au"
dodal/devices/i22/dcm.py CHANGED
@@ -39,7 +39,6 @@ class DoubleCrystalMonochromator(StandardReadable):
39
39
 
40
40
  def __init__(
41
41
  self,
42
- motion_prefix: str,
43
42
  temperature_prefix: str,
44
43
  crystal_1_metadata: CrystalMetadata | None = None,
45
44
  crystal_2_metadata: CrystalMetadata | None = None,
@@ -48,13 +47,13 @@ class DoubleCrystalMonochromator(StandardReadable):
48
47
  ) -> None:
49
48
  with self.add_children_as_readables():
50
49
  # Positionable Parameters
51
- self.bragg = Motor(motion_prefix + "BRAGG")
52
- self.offset = Motor(motion_prefix + "OFFSET")
53
- self.perp = Motor(motion_prefix + "PERP")
54
- self.energy = Motor(motion_prefix + "ENERGY")
55
- self.crystal_1_roll = Motor(motion_prefix + "XTAL1:ROLL")
56
- self.crystal_2_roll = Motor(motion_prefix + "XTAL2:ROLL")
57
- self.crystal_2_pitch = Motor(motion_prefix + "XTAL2:PITCH")
50
+ self.bragg = Motor(prefix + "BRAGG")
51
+ self.offset = Motor(prefix + "OFFSET")
52
+ self.perp = Motor(prefix + "PERP")
53
+ self.energy = Motor(prefix + "ENERGY")
54
+ self.crystal_1_roll = Motor(prefix + "XTAL1:ROLL")
55
+ self.crystal_2_roll = Motor(prefix + "XTAL2:ROLL")
56
+ self.crystal_2_pitch = Motor(prefix + "XTAL2:PITCH")
58
57
 
59
58
  # Temperatures
60
59
  self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
@@ -26,8 +26,8 @@ class BacklightPositioner(StandardReadable):
26
26
  super().__init__(name)
27
27
 
28
28
  @AsyncStatus.wrap
29
- async def set(self, position: BacklightPositions):
30
- await self.pos_level.set(position, wait=True)
29
+ async def set(self, value: BacklightPositions):
30
+ await self.pos_level.set(value, wait=True)
31
31
 
32
32
 
33
33
  class DualBacklight(StandardReadable):
@@ -53,9 +53,9 @@ class DualBacklight(StandardReadable):
53
53
  super().__init__(name)
54
54
 
55
55
  @AsyncStatus.wrap
56
- async def set(self, position: BacklightPositions):
57
- await self.backlight_position.set(position)
58
- if position == BacklightPositions.OUT:
56
+ async def set(self, value: BacklightPositions):
57
+ await self.backlight_position.set(value)
58
+ if value == BacklightPositions.OUT:
59
59
  await self.backlight_state.set(LEDStatus.OFF, wait=True)
60
60
  else:
61
61
  await self.backlight_state.set(LEDStatus.ON, wait=True)
@@ -37,3 +37,25 @@ def camera_coordinates_to_xyz(
37
37
 
38
38
  z = vertical * sine
39
39
  return np.array([x, y, z], dtype=np.float64)
40
+
41
+
42
+ def calculate_beam_distance(
43
+ beam_centre: tuple[int, int],
44
+ horizontal_pixels: int,
45
+ vertical_pixels: int,
46
+ ) -> tuple[int, int]:
47
+ """
48
+ Calculates the distance between the beam centre and the given (horizontal, vertical).
49
+
50
+ Args:
51
+ horizontal_pixels (int): The x (camera coordinates) value in pixels.
52
+ vertical_pixels (int): The y (camera coordinates) value in pixels.
53
+ Returns:
54
+ The distance between the beam centre and the (horizontal, vertical) point in pixels as a tuple
55
+ (horizontal_distance, vertical_distance).
56
+ """
57
+ beam_x, beam_y = beam_centre
58
+ return (
59
+ beam_x - horizontal_pixels,
60
+ beam_y - vertical_pixels,
61
+ )