dls-dodal 1.47.0__py3-none-any.whl → 1.48.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 (42) hide show
  1. {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/RECORD +40 -40
  3. {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/aithre.py +6 -0
  6. dodal/beamlines/b01_1.py +1 -1
  7. dodal/beamlines/i03.py +21 -6
  8. dodal/beamlines/i04.py +17 -10
  9. dodal/beamlines/i18.py +1 -1
  10. dodal/beamlines/i19_1.py +9 -6
  11. dodal/beamlines/i24.py +5 -5
  12. dodal/common/beamlines/beamline_parameters.py +2 -28
  13. dodal/devices/aithre_lasershaping/goniometer.py +36 -2
  14. dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
  15. dodal/devices/electron_analyser/__init__.py +10 -0
  16. dodal/devices/electron_analyser/abstract/__init__.py +0 -6
  17. dodal/devices/electron_analyser/abstract/base_detector.py +69 -56
  18. dodal/devices/electron_analyser/abstract/base_driver_io.py +114 -5
  19. dodal/devices/electron_analyser/abstract/base_region.py +1 -0
  20. dodal/devices/electron_analyser/specs/__init__.py +1 -2
  21. dodal/devices/electron_analyser/specs/detector.py +5 -21
  22. dodal/devices/electron_analyser/specs/driver_io.py +27 -2
  23. dodal/devices/electron_analyser/vgscienta/__init__.py +1 -2
  24. dodal/devices/electron_analyser/vgscienta/detector.py +8 -22
  25. dodal/devices/electron_analyser/vgscienta/driver_io.py +31 -3
  26. dodal/devices/electron_analyser/vgscienta/region.py +0 -1
  27. dodal/devices/fast_grid_scan.py +1 -1
  28. dodal/devices/i04/murko_results.py +93 -96
  29. dodal/devices/i18/diode.py +37 -4
  30. dodal/devices/mx_phase1/beamstop.py +23 -6
  31. dodal/devices/oav/oav_detector.py +61 -23
  32. dodal/devices/oav/oav_parameters.py +46 -16
  33. dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
  34. dodal/devices/robot.py +20 -1
  35. dodal/devices/smargon.py +43 -4
  36. dodal/devices/zebra/zebra.py +8 -0
  37. dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
  38. dodal/plan_stubs/electron_analyser/__init__.py +0 -3
  39. dodal/plan_stubs/electron_analyser/configure_driver.py +0 -92
  40. {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/entry_points.txt +0 -0
  41. {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/licenses/LICENSE +0 -0
  42. {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import pickle
3
- from collections import OrderedDict
4
3
  from enum import Enum
5
4
  from typing import TypedDict
6
5
 
@@ -17,10 +16,11 @@ from redis.asyncio import StrictRedis
17
16
  from dodal.devices.i04.constants import RedisConstants
18
17
  from dodal.devices.oav.oav_calculations import (
19
18
  calculate_beam_distance,
20
- camera_coordinates_to_xyz_mm,
21
19
  )
22
20
  from dodal.log import LOGGER
23
21
 
22
+ NO_MURKO_RESULT = (-1, -1)
23
+
24
24
  MurkoResult = dict
25
25
  FullMurkoResults = dict[str, list[MurkoResult]]
26
26
 
@@ -43,12 +43,19 @@ class Coord(Enum):
43
43
 
44
44
 
45
45
  class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
46
- """Device that takes crystal centre coords from Murko and uses them to set the
46
+ """Device that takes crystal centre values from Murko and uses them to set the
47
47
  x, y, z coordinate of the sample to be in line with the beam centre.
48
- (x, z) coords can be read at 90°, and (x, y) at 180° (or the closest omega angle to
49
- 90° and 180°). The average of the x values at these angles is taken, and sin(omega)z
50
- and cosine(omega)y are taken to account for the rotation. This value is used to
51
- calculate a number of mm the sample needs to move to be in line with the beam centre.
48
+ The most_likely_click[1] value from Murko corresponds with the x coordinate of the
49
+ sample. The most_likely_click[0] value from Murko corresponds with a component of
50
+ the y and z coordinates of the sample, depending on the omega angle, as the sample
51
+ is rotated around the x axis.
52
+
53
+ Given a most_likely_click value at a certain omega angle θ:
54
+ most_likely_click[1] = x
55
+ most_likely_click[0] = cos(θ)y - sin(θ)z
56
+
57
+ A value for x can be found by averaging all most_likely_click[1] values, and
58
+ solutions for y and z can be calculated using numpy's linear algebra library.
52
59
  """
53
60
 
54
61
  TIMEOUT_S = 2
@@ -59,6 +66,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
59
66
  redis_password=RedisConstants.REDIS_PASSWORD,
60
67
  redis_db=RedisConstants.MURKO_REDIS_DB,
61
68
  name="",
69
+ stop_angle=350,
62
70
  ):
63
71
  self.redis_client = StrictRedis(
64
72
  host=redis_host,
@@ -67,17 +75,11 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
67
75
  )
68
76
  self.pubsub = self.redis_client.pubsub()
69
77
  self._last_omega = 0
70
- self._last_result = None
71
78
  self.sample_id = soft_signal_rw(str) # Should get from redis
72
- self.coords = {"x": {}, "y": {}, "z": {}}
73
- self.search_angles = OrderedDict(
74
- [ # Angles to search and dimensions to gather at each angle
75
- (90, ("x", "z")),
76
- (180, ("x", "y")),
77
- (270, ()), # Stop searching here
78
- ]
79
- )
80
- self.angles_to_search = list(self.search_angles.keys())
79
+ self.stop_angle = stop_angle
80
+ self.x_dists_mm = []
81
+ self.y_dists_mm = []
82
+ self.omegas = []
81
83
 
82
84
  with self.add_children_as_readables():
83
85
  # Diffs from current x/y/z
@@ -101,95 +103,90 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
101
103
  async def trigger(self):
102
104
  # Wait for results
103
105
  sample_id = await self.sample_id.get_value()
104
- final_message = None
105
- while self.angles_to_search:
106
- # waits here for next batch to be recieved
106
+ while self._last_omega < self.stop_angle:
107
+ # waits here for next batch to be received
107
108
  message = await self.pubsub.get_message(timeout=self.TIMEOUT_S)
108
109
  if message is None: # No more messages to process
109
- await self.process_batch(
110
- final_message, sample_id
111
- ) # Process final message again
112
110
  break
113
111
  await self.process_batch(message, sample_id)
114
- final_message = message
115
- x_values = list(self.coords["x"].values())
116
- y_values = list(self.coords["y"].values())
117
- z_values = list(self.coords["z"].values())
118
- assert x_values, "No x values"
119
- assert z_values, "No z values"
120
- assert y_values, "No y values"
121
- self._x_mm_setter(float(np.mean(x_values)))
122
- self._y_mm_setter(float(np.mean(y_values)))
123
- self._z_mm_setter(float(np.mean(z_values)))
112
+
113
+ for i in range(len(self.omegas)):
114
+ LOGGER.debug(
115
+ f"omega: {round(self.omegas[i], 2)}, x: {round(self.x_dists_mm[i], 2)}, y: {round(self.y_dists_mm[i], 2)}"
116
+ )
117
+
118
+ LOGGER.info(f"Using average of x beam distances: {self.x_dists_mm}")
119
+ avg_x = float(np.mean(self.x_dists_mm))
120
+ LOGGER.info(f"Finding least square y and z from y distances: {self.y_dists_mm}")
121
+ best_y, best_z = get_yz_least_squares(self.y_dists_mm, self.omegas)
122
+ # x, y, z are relative to beam centre. Need to move negative these values to get centred.
123
+ self._x_mm_setter(-avg_x)
124
+ self._y_mm_setter(-best_y)
125
+ self._z_mm_setter(-best_z)
124
126
 
125
127
  async def process_batch(self, message: dict | None, sample_id: str):
126
128
  if message and message["type"] == "message":
127
- batch_results = pickle.loads(message["data"])
129
+ batch_results: list[dict] = pickle.loads(message["data"])
128
130
  for results in batch_results:
129
- LOGGER.info(f"Got {results} from redis")
130
131
  for uuid, result in results.items():
131
- metadata_str = await self.redis_client.hget( # type: ignore
132
+ if metadata_str := await self.redis_client.hget( # type: ignore
132
133
  f"murko:{sample_id}:metadata", uuid
133
- )
134
- if metadata_str and self.angles_to_search:
135
- self.process_result(result, uuid, metadata_str)
136
-
137
- def process_result(
138
- self, result: dict, uuid: int, metadata_str: str
139
- ) -> float | None:
140
- metadata = MurkoMetadata(json.loads(metadata_str))
141
- omega_angle = metadata["omega_angle"]
142
- LOGGER.info(f"Got angle {omega_angle}")
143
- # Find closest to next search angle
144
- movement = self.get_coords_if_at_angle(metadata, result, omega_angle)
145
- if movement is not None:
146
- LOGGER.info(f"Using result {uuid}, {metadata_str}, {result}")
147
- search_angle = self.angles_to_search.pop(0)
148
- for coord in self.search_angles[search_angle]:
149
- self.coords[coord][omega_angle] = movement[Coord[coord].value]
150
- LOGGER.info(f"Found {coord} at {movement}, angle = {omega_angle}")
151
- self._last_omega = omega_angle
152
- self._last_result = result
153
-
154
- def get_coords_if_at_angle(
155
- self, metadata: MurkoMetadata, result: MurkoResult, omega: float
156
- ) -> np.ndarray | None:
157
- """Gets the 'most_likely_click' coordinates from Murko if omega or the last
158
- omega are the closest angle to the search angle. Otherwise returns None.
134
+ ):
135
+ LOGGER.info(
136
+ f"Found metadata for uuid {uuid}, processing result"
137
+ )
138
+ self.process_result(
139
+ result, MurkoMetadata(json.loads(metadata_str))
140
+ )
141
+ else:
142
+ LOGGER.info(f"Found no metadata for uuid {uuid}")
143
+
144
+ def process_result(self, result: dict, metadata: MurkoMetadata):
145
+ """Uses the 'most_likely_click' coordinates from Murko to calculate the
146
+ horizontal and vertical distances from the beam centre, and store these values
147
+ as well as the omega angle the image was taken at.
159
148
  """
160
- search_angle = self.angles_to_search[0]
161
- LOGGER.info(f"Compare {omega}, {search_angle}, {self._last_omega}")
162
- if ( # if last omega is closest
163
- abs(omega - search_angle) >= abs(self._last_omega - search_angle)
164
- and self._last_result is not None
165
- ):
166
- closest_result = self._last_result
167
- closest_omega = self._last_omega
168
- elif omega - search_angle >= 0: # if this omega is closest
169
- closest_result = result
170
- closest_omega = omega
149
+ omega = metadata["omega_angle"]
150
+ coords = result["most_likely_click"] # As proportion from top, left of image
151
+ LOGGER.info(f"Got most_likely_click: {coords} at angle {omega}")
152
+ if (
153
+ tuple(coords) == NO_MURKO_RESULT
154
+ ): # See https://github.com/MartinSavko/murko/issues/9
155
+ LOGGER.info("Murko didn't produce a result, moving on")
171
156
  else:
172
- return None
173
- coords = closest_result[
174
- "most_likely_click"
175
- ] # As proportion from top, left of image
176
- shape = closest_result["original_shape"] # Dimensions of image in pixels
177
- # Murko returns coords as y, x
178
- centre_px = (coords[1] * shape[1], coords[0] * shape[0])
179
- LOGGER.info(
180
- f"Using image taken at {closest_omega}, which found xtal at {centre_px}"
181
- )
182
-
183
- beam_dist_px = calculate_beam_distance(
184
- (metadata["beam_centre_i"], metadata["beam_centre_j"]),
185
- centre_px[0],
186
- centre_px[1],
187
- )
157
+ shape = result["original_shape"] # Dimensions of image in pixels
158
+ # Murko returns coords as y, x
159
+ centre_px = (coords[1] * shape[1], coords[0] * shape[0])
160
+
161
+ beam_dist_px = calculate_beam_distance(
162
+ (metadata["beam_centre_i"], metadata["beam_centre_j"]),
163
+ centre_px[0],
164
+ centre_px[1],
165
+ )
166
+ self.x_dists_mm.append(
167
+ beam_dist_px[0] * metadata["microns_per_x_pixel"] / 1000
168
+ )
169
+ self.y_dists_mm.append(
170
+ beam_dist_px[1] * metadata["microns_per_y_pixel"] / 1000
171
+ )
172
+ self.omegas.append(omega)
173
+ self._last_omega = omega
174
+
175
+
176
+ def get_yz_least_squares(vertical_dists: list, omegas: list) -> tuple[float, float]:
177
+ """Get the least squares solution for y and z from the vertical distances and omega angles.
178
+
179
+ Args:
180
+ v_dists (list): List of vertical distances from beam centre. Any units
181
+ omegas (list): List of omega angles in degrees.
182
+
183
+ Returns:
184
+ tuple[float, float]: y, z distances from centre, in whichever units
185
+ v_dists came as.
186
+ """
187
+ thetas = np.radians(omegas)
188
+ matrix = np.column_stack([np.cos(thetas), -np.sin(thetas)])
188
189
 
189
- return camera_coordinates_to_xyz_mm(
190
- beam_dist_px[0],
191
- beam_dist_px[1],
192
- closest_omega,
193
- metadata["microns_per_x_pixel"],
194
- metadata["microns_per_y_pixel"],
195
- )
190
+ yz, residuals, rank, s = np.linalg.lstsq(matrix, vertical_dists, rcond=None)
191
+ y, z = yz
192
+ return y, z
@@ -1,8 +1,36 @@
1
- from ophyd_async.core import (
2
- StandardReadable,
3
- )
1
+ from ophyd_async.core import StandardReadable, StrictEnum
4
2
  from ophyd_async.epics.core import epics_signal_r
5
3
 
4
+ from dodal.devices.positioner import create_positioner
5
+
6
+
7
+ class FilterAValues(StrictEnum):
8
+ """Maps from a short usable name to the string name in EPICS"""
9
+
10
+ AL_2MM = "2 mm Al"
11
+ AL_1_5MM = "1.5 mm Al"
12
+ AL_1_25MM = "1.25 mm Al"
13
+ AL_0_8MM = "0.8 mm Al"
14
+ AL_0_55MM = "0.55 mm Al"
15
+ AL_0_5MM = "0.5 mm Al"
16
+ AL_0_3MM = "0.3 mm Al"
17
+ AL_0_25MM = "0.25 mm Al"
18
+ AL_0_15MM = "0.15 mm Al"
19
+ AL_0_1MM = "0.1 mm Al"
20
+ AL_0_05MM = "0.05 mm Al"
21
+ AL_0_025MM = "0.025 mm Al"
22
+ AL_GAP = "Gap"
23
+
24
+
25
+ class FilterBValues(StrictEnum):
26
+ DIAMOND_THIN = "Diamond thin"
27
+ DIAMOND_THICK = "Diamond thick"
28
+ NI_DRAIN = "ni drain"
29
+ AU_DRAIN = "au drain"
30
+ AL_DRAIN = "al drain"
31
+ GAP = "Gap"
32
+ IN_LINE_DIODE = "in line diode"
33
+
6
34
 
7
35
  class Diode(StandardReadable):
8
36
  def __init__(
@@ -10,8 +38,13 @@ class Diode(StandardReadable):
10
38
  prefix: str,
11
39
  name: str = "",
12
40
  ):
13
- self._prefix = prefix
14
41
  with self.add_children_as_readables():
15
42
  self.signal = epics_signal_r(float, prefix + "B:DIODE:I")
43
+ self.positioner_a = create_positioner(
44
+ FilterAValues, prefix + "A:MP", positioner_pv_suffix=":SELECT"
45
+ ) # more complex, will be fixed on Tuesday 20.05.2025
46
+ self.positioner_b = create_positioner(
47
+ FilterBValues, prefix + "B:MP", positioner_pv_suffix=":SELECT"
48
+ )
16
49
 
17
50
  super().__init__(name=name)
@@ -1,6 +1,11 @@
1
+ import asyncio
1
2
  from math import isclose
2
3
 
3
- from ophyd_async.core import StandardReadable, StrictEnum, derived_signal_r
4
+ from ophyd_async.core import (
5
+ StandardReadable,
6
+ StrictEnum,
7
+ derived_signal_rw,
8
+ )
4
9
  from ophyd_async.epics.motor import Motor
5
10
 
6
11
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
@@ -37,8 +42,7 @@ class Beamstop(StandardReadable):
37
42
  x: beamstop x position in mm
38
43
  y: beamstop y position in mm
39
44
  z: beamstop z position in mm
40
- selected_pos: Get the current position of the beamstop as an enum. Currently this
41
- is read-only.
45
+ selected_pos: Get or set the current position of the beamstop as an enum.
42
46
  """
43
47
 
44
48
  def __init__(
@@ -51,10 +55,13 @@ class Beamstop(StandardReadable):
51
55
  self.x_mm = Motor(prefix + "X")
52
56
  self.y_mm = Motor(prefix + "Y")
53
57
  self.z_mm = Motor(prefix + "Z")
54
- self.selected_pos = derived_signal_r(
55
- self._get_selected_position, x=self.x_mm, y=self.y_mm, z=self.z_mm
58
+ self.selected_pos = derived_signal_rw(
59
+ self._get_selected_position,
60
+ self._set_selected_position,
61
+ x=self.x_mm,
62
+ y=self.y_mm,
63
+ z=self.z_mm,
56
64
  )
57
-
58
65
  self._in_beam_xyz_mm = [
59
66
  float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
60
67
  for axis in ("x", "y", "z")
@@ -77,3 +84,13 @@ class Beamstop(StandardReadable):
77
84
  return BeamstopPositions.DATA_COLLECTION
78
85
  else:
79
86
  return BeamstopPositions.UNKNOWN
87
+
88
+ async def _set_selected_position(self, position: BeamstopPositions) -> None:
89
+ if position == BeamstopPositions.DATA_COLLECTION:
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
+ self.z_mm.set(self._in_beam_xyz_mm[2]),
94
+ )
95
+ elif position == BeamstopPositions.UNKNOWN:
96
+ raise ValueError(f"Cannot set beamstop to position {position}")
@@ -5,14 +5,20 @@ from ophyd_async.core import (
5
5
  DEFAULT_TIMEOUT,
6
6
  AsyncStatus,
7
7
  LazyMock,
8
+ SignalR,
8
9
  StandardReadable,
9
10
  derived_signal_r,
10
11
  soft_signal_rw,
11
12
  )
12
- from ophyd_async.epics.core import epics_signal_rw
13
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
13
14
 
14
15
  from dodal.devices.areadetector.plugins.CAM import Cam
15
- from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
16
+ from dodal.devices.oav.oav_parameters import (
17
+ DEFAULT_OAV_WINDOW,
18
+ OAVConfig,
19
+ OAVConfigBase,
20
+ OAVConfigBeamCentre,
21
+ )
16
22
  from dodal.devices.oav.snapshots.snapshot import Snapshot
17
23
  from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
18
24
 
@@ -53,7 +59,10 @@ class ZoomController(StandardReadable, Movable[str]):
53
59
 
54
60
 
55
61
  class OAV(StandardReadable):
56
- def __init__(self, prefix: str, config: OAVConfig, name: str = ""):
62
+ beam_centre_i: SignalR[int]
63
+ beam_centre_j: SignalR[int]
64
+
65
+ def __init__(self, prefix: str, config: OAVConfigBase, name: str = ""):
57
66
  self.oav_config = config
58
67
  self._prefix = prefix
59
68
  self._name = name
@@ -79,18 +88,6 @@ class OAV(StandardReadable):
79
88
  size=self.sizes[Coords.Y],
80
89
  coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
81
90
  )
82
- self.beam_centre_i = derived_signal_r(
83
- self._get_beam_position,
84
- zoom_level=self.zoom_controller.level,
85
- size=self.sizes[Coords.X],
86
- coord=soft_signal_rw(datatype=int, initial_value=Coords.X.value),
87
- )
88
- self.beam_centre_j = derived_signal_r(
89
- self._get_beam_position,
90
- zoom_level=self.zoom_controller.level,
91
- size=self.sizes[Coords.Y],
92
- coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
93
- )
94
91
  self.snapshot = Snapshot(
95
92
  f"{self._prefix}MJPG:",
96
93
  self._name,
@@ -107,14 +104,6 @@ class OAV(StandardReadable):
107
104
  value = self.parameters[_zoom].microns_per_pixel[coord]
108
105
  return value * DEFAULT_OAV_WINDOW[coord] / size
109
106
 
110
- def _get_beam_position(self, zoom_level: str, size: int, coord: int) -> int:
111
- """Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
112
- zoom level. """
113
- _zoom = self._read_current_zoom(zoom_level)
114
- value = self.parameters[_zoom].crosshair[coord]
115
-
116
- return int(value * size / DEFAULT_OAV_WINDOW[coord])
117
-
118
107
  async def connect(
119
108
  self,
120
109
  mock: bool | LazyMock = False,
@@ -124,3 +113,52 @@ class OAV(StandardReadable):
124
113
  self.parameters = self.oav_config.get_parameters()
125
114
 
126
115
  return await super().connect(mock, timeout, force_reconnect)
116
+
117
+
118
+ class OAVBeamCentreFile(OAV):
119
+ """OAV device that reads its beam centre values from a file. The config parameter
120
+ must be a OAVConfigBeamCentre object, as this contains a filepath to where the beam
121
+ centre values are stored.
122
+ """
123
+
124
+ def __init__(self, prefix: str, config: OAVConfigBeamCentre, name: str = ""):
125
+ super().__init__(prefix, config, name)
126
+
127
+ with self.add_children_as_readables():
128
+ self.beam_centre_i = derived_signal_r(
129
+ self._get_beam_position,
130
+ zoom_level=self.zoom_controller.level,
131
+ size=self.sizes[Coords.X],
132
+ coord=soft_signal_rw(datatype=int, initial_value=Coords.X.value),
133
+ )
134
+ self.beam_centre_j = derived_signal_r(
135
+ self._get_beam_position,
136
+ zoom_level=self.zoom_controller.level,
137
+ size=self.sizes[Coords.Y],
138
+ coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
139
+ )
140
+ # Set name so that new child signals get correct name
141
+ self.set_name(self.name)
142
+
143
+ def _get_beam_position(self, zoom_level: str, size: int, coord: int) -> int:
144
+ """Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
145
+ zoom level. """
146
+ _zoom = self._read_current_zoom(zoom_level)
147
+ value = self.parameters[_zoom].crosshair[coord]
148
+ return int(value * size / DEFAULT_OAV_WINDOW[coord])
149
+
150
+
151
+ class OAVBeamCentrePV(OAV):
152
+ """OAV device that reads its beam centre values from PVs."""
153
+
154
+ def __init__(
155
+ self, prefix: str, config: OAVConfig, name: str = "", overlay_channel: int = 1
156
+ ):
157
+ with self.add_children_as_readables():
158
+ self.beam_centre_i = epics_signal_r(
159
+ int, prefix + f"OVER:{overlay_channel}:CenterX"
160
+ )
161
+ self.beam_centre_j = epics_signal_r(
162
+ int, prefix + f"OVER:{overlay_channel}:CenterY"
163
+ )
164
+ super().__init__(prefix, config, name)
@@ -1,8 +1,9 @@
1
1
  import json
2
2
  import xml.etree.ElementTree as et
3
+ from abc import abstractmethod
3
4
  from collections import ChainMap
4
5
  from dataclasses import dataclass
5
- from typing import Any
6
+ from typing import Any, Generic, TypeVar
6
7
  from xml.etree.ElementTree import Element
7
8
 
8
9
  # GDA currently assumes this aspect ratio for the OAV window size.
@@ -107,22 +108,19 @@ class OAVParameters:
107
108
  @dataclass
108
109
  class ZoomParams:
109
110
  microns_per_pixel: tuple[float, float]
111
+
112
+
113
+ @dataclass
114
+ class ZoomParamsCrosshair(ZoomParams):
110
115
  crosshair: tuple[int, int]
111
116
 
112
117
 
113
- class OAVConfig:
114
- """ Read the OAV config files and return a dictionary of {'zoom_level': ZoomParams}\
115
- with information about microns per pixels and crosshairs.
116
- """
118
+ ParamType = TypeVar("ParamType", bound="ZoomParams")
117
119
 
118
- def __init__(self, zoom_params_file: str, display_config_file: str):
119
- self.zoom_params = self._get_zoom_params(zoom_params_file)
120
- self.display_config = self._get_display_config(display_config_file)
121
120
 
122
- def _get_display_config(self, display_config_file: str):
123
- with open(display_config_file) as f:
124
- file_lines = f.readlines()
125
- return file_lines
121
+ class OAVConfigBase(Generic[ParamType]):
122
+ def __init__(self, zoom_params_file: str):
123
+ self.zoom_params = self._get_zoom_params(zoom_params_file)
126
124
 
127
125
  def _get_zoom_params(self, zoom_params_file: str):
128
126
  tree = et.parse(zoom_params_file)
@@ -138,6 +136,39 @@ class OAVConfig:
138
136
  um_per_pix[zoom] = (um_pix_x, um_pix_y)
139
137
  return um_per_pix
140
138
 
139
+ @abstractmethod
140
+ def get_parameters(self) -> dict[str, ParamType]: ...
141
+
142
+
143
+ class OAVConfig(OAVConfigBase[ZoomParams]):
144
+ def get_parameters(self) -> dict[str, ZoomParams]:
145
+ config = {}
146
+ um_xy = self._read_zoom_params()
147
+ for zoom_key in list(um_xy.keys()):
148
+ config[zoom_key] = ZoomParams(
149
+ microns_per_pixel=um_xy[zoom_key],
150
+ )
151
+ return config
152
+
153
+
154
+ class OAVConfigBeamCentre(OAVConfigBase[ZoomParamsCrosshair]):
155
+ """ Read the OAV config files and return a dictionary of {'zoom_level': ZoomParams}\
156
+ with information about microns per pixels and crosshairs.
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ zoom_params_file: str,
162
+ display_config_file: str,
163
+ ):
164
+ self.display_config = self._get_display_config(display_config_file)
165
+ super().__init__(zoom_params_file)
166
+
167
+ def _get_display_config(self, display_config_file: str):
168
+ with open(display_config_file) as f:
169
+ file_lines = f.readlines()
170
+ return file_lines
171
+
141
172
  def _read_display_config(self) -> dict:
142
173
  crosshairs = {}
143
174
  for i in range(len(self.display_config)):
@@ -148,13 +179,12 @@ class OAVConfig:
148
179
  crosshairs[zoom] = (x, y)
149
180
  return crosshairs
150
181
 
151
- def get_parameters(self) -> dict[str, ZoomParams]:
182
+ def get_parameters(self) -> dict[str, ZoomParamsCrosshair]:
152
183
  config = {}
153
184
  um_xy = self._read_zoom_params()
154
185
  bc_xy = self._read_display_config()
155
186
  for zoom_key in list(bc_xy.keys()):
156
- config[zoom_key] = ZoomParams(
157
- microns_per_pixel=um_xy[zoom_key],
158
- crosshair=bc_xy[zoom_key],
187
+ config[zoom_key] = ZoomParamsCrosshair(
188
+ microns_per_pixel=um_xy[zoom_key], crosshair=bc_xy[zoom_key]
159
189
  )
160
190
  return config
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  from collections.abc import Awaitable, Callable
3
3
  from datetime import timedelta
4
- from enum import Enum
4
+ from enum import IntEnum
5
5
  from uuid import uuid4
6
6
 
7
7
  from aiohttp import ClientResponse, ClientSession
@@ -29,7 +29,7 @@ async def get_next_jpeg(response: ClientResponse) -> bytes:
29
29
  return line + await response.content.readuntil(JPEG_STOP_BYTE)
30
30
 
31
31
 
32
- class Source(Enum):
32
+ class Source(IntEnum):
33
33
  FULL_SCREEN = 0
34
34
  ROI = 1
35
35
 
dodal/devices/robot.py CHANGED
@@ -11,7 +11,12 @@ from ophyd_async.core import (
11
11
  set_and_wait_for_value,
12
12
  wait_for_value,
13
13
  )
14
- from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_signal_x
14
+ from ophyd_async.epics.core import (
15
+ epics_signal_r,
16
+ epics_signal_rw,
17
+ epics_signal_rw_rbv,
18
+ epics_signal_x,
19
+ )
15
20
 
16
21
  from dodal.log import LOGGER
17
22
 
@@ -88,6 +93,20 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
88
93
  self.controller_error = ErrorStatus(prefix + "CNTL")
89
94
 
90
95
  self.reset = epics_signal_x(prefix + "RESET.PROC")
96
+ self.stop = epics_signal_x(prefix + "ABORT.PROC")
97
+ self.init = epics_signal_x(prefix + "INIT.PROC")
98
+ self.soak = epics_signal_x(prefix + "SOAK.PROC")
99
+ self.home = epics_signal_x(prefix + "GOHM.PROC")
100
+ self.unload = epics_signal_x(prefix + "UNLD.PROC")
101
+ self.dry = epics_signal_x(prefix + "DRY.PROC")
102
+ self.open = epics_signal_x(prefix + "COLO.PROC")
103
+ self.close = epics_signal_x(prefix + "COLC.PROC")
104
+ self.cryomode_rbv = epics_signal_r(float, prefix + "CRYO_MODE_RBV")
105
+ self.cryomode = epics_signal_rw(str, prefix + "CRYO_MODE_CTRL")
106
+ self.gripper_temp = epics_signal_r(float, prefix + "GRIPPER_TEMP")
107
+ self.dewar_lid_temperature = epics_signal_rw(
108
+ float, prefix + "DW_1_TEMP", prefix + "DW_1_SET_POINT"
109
+ )
91
110
  super().__init__(name=name)
92
111
 
93
112
  async def pin_mounted_or_no_pin_found(self):