dls-dodal 1.42.0__py3-none-any.whl → 1.44.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 (65) hide show
  1. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/METADATA +5 -4
  2. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/RECORD +62 -54
  3. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/b01_1.py +8 -0
  6. dodal/beamlines/i03.py +11 -0
  7. dodal/beamlines/i13_1.py +22 -48
  8. dodal/beamlines/i19_1.py +16 -5
  9. dodal/beamlines/i19_2.py +12 -3
  10. dodal/beamlines/i19_optics.py +4 -2
  11. dodal/beamlines/training_rig.py +5 -1
  12. dodal/common/beamlines/beamline_utils.py +6 -9
  13. dodal/common/data_util.py +20 -0
  14. dodal/common/signal_utils.py +43 -4
  15. dodal/common/visit.py +41 -1
  16. dodal/devices/aperturescatterguard.py +4 -4
  17. dodal/devices/apple2_undulator.py +10 -8
  18. dodal/devices/attenuator/attenuator.py +1 -1
  19. dodal/devices/backlight.py +1 -1
  20. dodal/devices/baton.py +17 -0
  21. dodal/devices/bimorph_mirror.py +2 -2
  22. dodal/devices/current_amplifiers/current_amplifier.py +1 -6
  23. dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
  24. dodal/devices/current_amplifiers/femto.py +0 -5
  25. dodal/devices/current_amplifiers/sr570.py +0 -5
  26. dodal/devices/electron_analyser/__init__.py +0 -0
  27. dodal/devices/electron_analyser/base_region.py +64 -0
  28. dodal/devices/electron_analyser/specs/__init__.py +0 -0
  29. dodal/devices/electron_analyser/specs/specs_region.py +24 -0
  30. dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
  31. dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +77 -0
  32. dodal/devices/fast_grid_scan.py +2 -2
  33. dodal/devices/hutch_shutter.py +1 -1
  34. dodal/devices/i03/beamstop.py +2 -2
  35. dodal/devices/i10/i10_apple2.py +3 -3
  36. dodal/devices/i13_1/merlin.py +1 -2
  37. dodal/devices/i13_1/merlin_controller.py +12 -8
  38. dodal/devices/i19/beamstop.py +30 -0
  39. dodal/devices/i19/hutch_access.py +2 -0
  40. dodal/devices/i19/shutter.py +52 -30
  41. dodal/devices/i22/nxsas.py +1 -3
  42. dodal/devices/i24/focus_mirrors.py +3 -3
  43. dodal/devices/i24/pilatus_metadata.py +2 -2
  44. dodal/devices/i24/pmac.py +2 -2
  45. dodal/devices/oav/oav_detector.py +8 -10
  46. dodal/devices/oav/snapshots/snapshot.py +21 -0
  47. dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
  48. dodal/devices/pressure_jump_cell.py +7 -3
  49. dodal/devices/robot.py +1 -1
  50. dodal/devices/thawer.py +4 -4
  51. dodal/devices/undulator.py +1 -1
  52. dodal/devices/undulator_dcm.py +1 -1
  53. dodal/devices/util/epics_util.py +1 -1
  54. dodal/devices/zebra/zebra.py +4 -3
  55. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  56. dodal/plan_stubs/data_session.py +17 -9
  57. dodal/plan_stubs/motor_utils.py +10 -12
  58. dodal/plan_stubs/wrapped.py +10 -12
  59. dodal/utils.py +0 -7
  60. dodal/devices/i13_1/merlin_io.py +0 -17
  61. dodal/devices/oav/microns_for_zoom_levels.json +0 -55
  62. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
  63. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/entry_points.txt +0 -0
  64. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info/licenses}/LICENSE +0 -0
  65. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/top_level.txt +0 -0
@@ -2,22 +2,26 @@ from collections.abc import Callable, Coroutine
2
2
  from typing import Any
3
3
 
4
4
  from bluesky.protocols import Reading
5
- from ophyd_async.core import SignalDatatypeT, SignalR, SoftSignalBackend
5
+ from ophyd_async.core import SignalDatatypeT, SignalR, SignalRW, SoftSignalBackend
6
+
7
+ SetHardwareType = Callable[[SignalDatatypeT], Coroutine[Any, Any, None]]
6
8
 
7
9
 
8
10
  class HardwareBackedSoftSignalBackend(SoftSignalBackend[SignalDatatypeT]):
9
11
  def __init__(
10
12
  self,
11
13
  get_from_hardware_func: Callable[[], Coroutine[Any, Any, SignalDatatypeT]],
14
+ set_to_hardware_func: SetHardwareType | None = None,
12
15
  *args,
13
16
  **kwargs,
14
17
  ) -> None:
15
18
  self.get_from_hardware_func = get_from_hardware_func
19
+ self.set_to_hardware_func = set_to_hardware_func
16
20
  super().__init__(*args, **kwargs)
17
21
 
18
22
  async def _update_value(self):
19
23
  new_value = await self.get_from_hardware_func()
20
- await self.put(new_value, True)
24
+ self.set_value(new_value)
21
25
 
22
26
  async def get_reading(self) -> Reading:
23
27
  await self._update_value()
@@ -27,8 +31,39 @@ class HardwareBackedSoftSignalBackend(SoftSignalBackend[SignalDatatypeT]):
27
31
  await self._update_value()
28
32
  return await super().get_value()
29
33
 
34
+ async def put(self, value: SignalDatatypeT | None, wait: bool) -> None:
35
+ if self.set_to_hardware_func:
36
+ write_value = self.initial_value if value is None else value
37
+ await self.set_to_hardware_func(write_value)
38
+
39
+
40
+ def create_rw_hardware_backed_soft_signal(
41
+ datatype: type[SignalDatatypeT],
42
+ get_from_hardware_func: Callable[[], Coroutine[Any, Any, SignalDatatypeT]],
43
+ set_to_hardware_func: SetHardwareType,
44
+ units: str | None = None,
45
+ precision: int | None = None,
46
+ ):
47
+ """Creates a soft signal that, when read will call the function passed into
48
+ `get_from_hardware_func` and return this. When set it will call `set_to_hardware_func`
49
+ and send something to the hardware.
50
+
51
+ This will allow you to make soft signals derived from arbitrary hardware signals.
52
+ However, calling subscribe on this signal does not give you a sensible value. See https://github.com/bluesky/ophyd-async/issues/525
53
+ for a more full solution.
54
+ """
55
+ return SignalRW(
56
+ backend=HardwareBackedSoftSignalBackend(
57
+ get_from_hardware_func,
58
+ set_to_hardware_func,
59
+ datatype,
60
+ units=units,
61
+ precision=precision,
62
+ )
63
+ )
64
+
30
65
 
31
- def create_hardware_backed_soft_signal(
66
+ def create_r_hardware_backed_soft_signal(
32
67
  datatype: type[SignalDatatypeT],
33
68
  get_from_hardware_func: Callable[[], Coroutine[Any, Any, SignalDatatypeT]],
34
69
  units: str | None = None,
@@ -44,6 +79,10 @@ def create_hardware_backed_soft_signal(
44
79
  """
45
80
  return SignalR(
46
81
  backend=HardwareBackedSoftSignalBackend(
47
- get_from_hardware_func, datatype, units=units, precision=precision
82
+ get_from_hardware_func,
83
+ None,
84
+ datatype,
85
+ units=units,
86
+ precision=precision,
48
87
  )
49
88
  )
dodal/common/visit.py CHANGED
@@ -3,7 +3,8 @@ from pathlib import Path
3
3
  from typing import Literal
4
4
 
5
5
  from aiohttp import ClientSession
6
- from ophyd_async.core import FilenameProvider, PathInfo
6
+ from event_model import RunStart
7
+ from ophyd_async.core import FilenameProvider, PathInfo, PathProvider
7
8
  from pydantic import BaseModel
8
9
 
9
10
  from dodal.common.types import UpdatingPathProvider
@@ -150,3 +151,42 @@ class StaticVisitPathProvider(UpdatingPathProvider):
150
151
  return PathInfo(
151
152
  directory_path=self._root, filename=self._filename_provider(device_name)
152
153
  )
154
+
155
+
156
+ DEFAULT_TEMPLATE = "{device_name}-{instrument}-{scan_id}"
157
+
158
+
159
+ class StartDocumentPathProvider(PathProvider):
160
+ """A PathProvider that sources from metadata in a RunStart document.
161
+
162
+ This uses metadata from a RunStart document to determine file names and data session
163
+ directories. The file naming defaults to "{device_name}-{instrument}-{scan_id}", so
164
+ the file name is incremented by scan number. A template can be included in the
165
+ StartDocument to allow for custom naming conventions.
166
+
167
+ """
168
+
169
+ def __init__(self) -> None:
170
+ self._doc = {}
171
+
172
+ def update_run(self, name: str, start_doc: RunStart) -> None:
173
+ """Cache a start document.
174
+
175
+ This can be plugged into the run engine's subscribe method.
176
+ """
177
+ if name == "start":
178
+ self._doc = start_doc
179
+
180
+ def __call__(self, device_name: str | None = None) -> PathInfo:
181
+ """Returns the directory path and filename for a given data_session.
182
+
183
+ The default template for file naming is: "{device_name}-{instrument}-{scan_id}"
184
+ however, this can be changed by providing a template in the start document. For
185
+ example: "template": "custom-{device_name}--{scan_id}".
186
+
187
+ If you do not provide a data_session_directory it will default to "/tmp".
188
+ """
189
+ template = self._doc.get("template", DEFAULT_TEMPLATE)
190
+ sub_path = template.format_map(self._doc | {"device_name": device_name})
191
+ data_session_directory = Path(self._doc.get("data_session_directory", "/tmp"))
192
+ return PathInfo(directory_path=data_session_directory, filename=sub_path)
@@ -12,7 +12,7 @@ from ophyd_async.core import (
12
12
  from pydantic import BaseModel, Field
13
13
 
14
14
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
15
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
15
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
16
16
  from dodal.devices.aperture import Aperture
17
17
  from dodal.devices.scatterguard import Scatterguard
18
18
 
@@ -123,7 +123,7 @@ def load_positions_from_beamline_parameters(
123
123
  }
124
124
 
125
125
 
126
- class ApertureScatterguard(StandardReadable, Movable, Preparable):
126
+ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable):
127
127
  """Move the aperture and scatterguard assembly in a safe way. There are two ways to
128
128
  interact with the device depending on if you want simplicity or move flexibility.
129
129
 
@@ -164,7 +164,7 @@ class ApertureScatterguard(StandardReadable, Movable, Preparable):
164
164
  ) -> None:
165
165
  self.aperture = Aperture(prefix + "-MO-MAPT-01:")
166
166
  self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
167
- self.radius = create_hardware_backed_soft_signal(
167
+ self.radius = create_r_hardware_backed_soft_signal(
168
168
  float, self._get_current_radius, units="µm"
169
169
  )
170
170
  self._loaded_positions = loaded_positions
@@ -181,7 +181,7 @@ class ApertureScatterguard(StandardReadable, Movable, Preparable):
181
181
  )
182
182
 
183
183
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
184
- self.selected_aperture = create_hardware_backed_soft_signal(
184
+ self.selected_aperture = create_r_hardware_backed_soft_signal(
185
185
  ApertureValue, self._get_current_aperture_position
186
186
  )
187
187
 
@@ -1,7 +1,7 @@
1
1
  import abc
2
2
  import asyncio
3
3
  from dataclasses import dataclass
4
- from typing import Any
4
+ from typing import Any, Generic, TypeVar
5
5
 
6
6
  import numpy as np
7
7
  from bluesky.protocols import Movable
@@ -21,6 +21,8 @@ from pydantic import BaseModel, ConfigDict, RootModel
21
21
 
22
22
  from dodal.log import LOGGER
23
23
 
24
+ T = TypeVar("T")
25
+
24
26
 
25
27
  class UndulatorGateStatus(StrictEnum):
26
28
  OPEN = "Open"
@@ -99,7 +101,7 @@ async def estimate_motor_timeout(
99
101
  return abs((target_pos - cur_pos) * 2.0 / vel) + 1
100
102
 
101
103
 
102
- class SafeUndulatorMover(StandardReadable, Movable):
104
+ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
103
105
  """A device that will check it's safe to move the undulator before moving it and
104
106
  wait for the undulator to be safe again before calling the move complete.
105
107
  """
@@ -115,7 +117,7 @@ class SafeUndulatorMover(StandardReadable, Movable):
115
117
  super().__init__(name)
116
118
 
117
119
  @AsyncStatus.wrap
118
- async def set(self, value) -> None:
120
+ async def set(self, value: T) -> None:
119
121
  LOGGER.info(f"Setting {self.name} to {value}")
120
122
  await self.raise_if_cannot_move()
121
123
  await self._set_demand_positions(value)
@@ -125,7 +127,7 @@ class SafeUndulatorMover(StandardReadable, Movable):
125
127
  await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
126
128
 
127
129
  @abc.abstractmethod
128
- async def _set_demand_positions(self, value) -> None:
130
+ async def _set_demand_positions(self, value: T) -> None:
129
131
  """Set the demand positions on the device without actually hitting move."""
130
132
 
131
133
  @abc.abstractmethod
@@ -139,7 +141,7 @@ class SafeUndulatorMover(StandardReadable, Movable):
139
141
  raise RuntimeError(f"{self.name} is already in motion.")
140
142
 
141
143
 
142
- class UndulatorGap(SafeUndulatorMover):
144
+ class UndulatorGap(SafeUndulatorMover[float]):
143
145
  """A device with a collection of epics signals to set Apple 2 undulator gap motion.
144
146
  Only PV used by beamline are added the full list is here:
145
147
  /dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
@@ -185,7 +187,7 @@ class UndulatorGap(SafeUndulatorMover):
185
187
  self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
186
188
  super().__init__(self.set_move, prefix, name)
187
189
 
188
- async def _set_demand_positions(self, value) -> None:
190
+ async def _set_demand_positions(self, value: float) -> None:
189
191
  await self.user_setpoint.set(str(value))
190
192
 
191
193
  async def get_timeout(self) -> float:
@@ -234,7 +236,7 @@ class UndulatorPhaseMotor(StandardReadable):
234
236
  super().__init__(name=name)
235
237
 
236
238
 
237
- class UndulatorPhaseAxes(SafeUndulatorMover):
239
+ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
238
240
  """
239
241
  A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
240
242
  e.g. top_outer == Q1
@@ -290,7 +292,7 @@ class UndulatorPhaseAxes(SafeUndulatorMover):
290
292
  return np.max(timeouts)
291
293
 
292
294
 
293
- class UndulatorJawPhase(SafeUndulatorMover):
295
+ class UndulatorJawPhase(SafeUndulatorMover[float]):
294
296
  """
295
297
  A JawPhase movable, this is use for moving the jaw phase which is use to control the
296
298
  linear arbitrary polarisation but only one some of the beamline.
@@ -29,7 +29,7 @@ class ReadOnlyAttenuator(StandardReadable):
29
29
  super().__init__(name)
30
30
 
31
31
 
32
- class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable):
32
+ class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
33
33
  """The attenuator will insert filters into the beam to reduce its transmission.
34
34
  In this attenuator, each filter can be in one of two states: IN or OUT
35
35
 
@@ -15,7 +15,7 @@ class BacklightPosition(StrictEnum):
15
15
  OUT = "Out"
16
16
 
17
17
 
18
- class Backlight(StandardReadable, Movable):
18
+ class Backlight(StandardReadable, Movable[BacklightPosition]):
19
19
  """Simple device to trigger the pneumatic in/out."""
20
20
 
21
21
  TIME_TO_MOVE_S = 1.0 # Tested using a stopwatch on the beamline 09/2024
dodal/devices/baton.py ADDED
@@ -0,0 +1,17 @@
1
+ from typing import Annotated as A
2
+
3
+ from ophyd_async.core import (
4
+ SignalRW,
5
+ StandardReadable,
6
+ )
7
+ from ophyd_async.core import StandardReadableFormat as Format
8
+ from ophyd_async.epics.core import EpicsDevice, PvSuffix
9
+
10
+
11
+ class Baton(StandardReadable, EpicsDevice):
12
+ requested_user: A[
13
+ SignalRW[str], PvSuffix("REQUESTED_USER"), Format.HINTED_UNCACHED_SIGNAL
14
+ ]
15
+ current_user: A[
16
+ SignalRW[str], PvSuffix("CURRENT_USER"), Format.HINTED_UNCACHED_SIGNAL
17
+ ]
@@ -41,7 +41,7 @@ class BimorphMirrorStatus(StrictEnum):
41
41
  ERROR = "Error"
42
42
 
43
43
 
44
- class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
44
+ class BimorphMirrorChannel(StandardReadable, Movable[float], EpicsDevice):
45
45
  """Collection of PVs comprising a single bimorph channel.
46
46
 
47
47
  Attributes:
@@ -66,7 +66,7 @@ class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
66
66
  await self.output_voltage.set(value)
67
67
 
68
68
 
69
- class BimorphMirror(StandardReadable, Movable):
69
+ class BimorphMirror(StandardReadable, Movable[Mapping[int, float]]):
70
70
  """Class to represent CAENels Bimorph Mirrors.
71
71
 
72
72
  Attributes:
@@ -23,7 +23,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
23
23
  super().__init__(name)
24
24
 
25
25
  @abstractmethod
26
- @AsyncStatus.wrap
27
26
  async def increase_gain(self, value: int = 1) -> None:
28
27
  """Increase gain, increment by 1 by default.
29
28
 
@@ -31,7 +30,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
31
30
  bool: True if success.
32
31
  """
33
32
 
34
- @AsyncStatus.wrap
35
33
  @abstractmethod
36
34
  async def decrease_gain(self, value: int = 1) -> None:
37
35
  """Decrease gain, decrement by 1 by default.
@@ -40,21 +38,18 @@ class CurrentAmp(ABC, StandardReadable, Movable):
40
38
  bool: True if success.
41
39
  """
42
40
 
43
- @AsyncStatus.wrap
44
41
  @abstractmethod
45
- async def get_gain(self) -> type[Enum]:
42
+ async def get_gain(self) -> Enum:
46
43
  """Get the current gain setting
47
44
 
48
45
  Returns:
49
46
  Enum: The member name of the current gain setting in gain_conversion_table.
50
47
  """
51
48
 
52
- @AsyncStatus.wrap
53
49
  @abstractmethod
54
50
  async def get_upperlimit(self) -> float:
55
51
  """Get the upper limit of the current amplifier"""
56
52
 
57
- @AsyncStatus.wrap
58
53
  @abstractmethod
59
54
  async def get_lowerlimit(self) -> float:
60
55
  """Get the lower limit of the current amplifier"""
@@ -92,8 +92,8 @@ class CurrentAmpDet(StandardReadable, Preparable):
92
92
  self.current_amp().get_gain(),
93
93
  self.counter().get_voltage_per_sec(),
94
94
  )
95
- correction_factor = current_gain.value
96
- corrected_current = voltage_per_sec / correction_factor
95
+ assert isinstance(current_gain.value, float)
96
+ corrected_current = voltage_per_sec / current_gain.value
97
97
  return corrected_current
98
98
 
99
99
  @AsyncStatus.wrap
@@ -114,7 +114,6 @@ class FemtoDDPCA(CurrentAmp):
114
114
  # wait for current amplifier's bandpass filter to settle.
115
115
  await asyncio.sleep(self.raise_timetable[SEN_setting].value)
116
116
 
117
- @AsyncStatus.wrap
118
117
  async def increase_gain(self, value: int = 1) -> None:
119
118
  current_gain = int((await self.get_gain()).name.split("_")[-1])
120
119
  current_gain += value
@@ -122,7 +121,6 @@ class FemtoDDPCA(CurrentAmp):
122
121
  raise ValueError("Gain at max value")
123
122
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
124
123
 
125
- @AsyncStatus.wrap
126
124
  async def decrease_gain(self, value: int = 1) -> None:
127
125
  current_gain = int((await self.get_gain()).name.split("_")[-1])
128
126
  current_gain -= value
@@ -130,14 +128,11 @@ class FemtoDDPCA(CurrentAmp):
130
128
  raise ValueError("Gain at min value")
131
129
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
132
130
 
133
- @AsyncStatus.wrap
134
131
  async def get_gain(self) -> Enum:
135
132
  return self.gain_conversion_table[(await self.gain.get_value()).name]
136
133
 
137
- @AsyncStatus.wrap
138
134
  async def get_upperlimit(self) -> float:
139
135
  return self.upperlimit
140
136
 
141
- @AsyncStatus.wrap
142
137
  async def get_lowerlimit(self) -> float:
143
138
  return self.lowerlimit
@@ -178,7 +178,6 @@ class SR570(CurrentAmp):
178
178
  )
179
179
  await asyncio.sleep(self.raise_timetable[coarse_gain.name].value)
180
180
 
181
- @AsyncStatus.wrap
182
181
  async def increase_gain(self, value=3) -> None:
183
182
  current_gain = int((await self.get_gain()).name.split("_")[-1])
184
183
  current_gain += value
@@ -189,7 +188,6 @@ class SR570(CurrentAmp):
189
188
  raise ValueError("Gain at max value")
190
189
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
191
190
 
192
- @AsyncStatus.wrap
193
191
  async def decrease_gain(self, value=3) -> None:
194
192
  current_gain = int((await self.get_gain()).name.split("_")[-1])
195
193
  current_gain -= value
@@ -198,17 +196,14 @@ class SR570(CurrentAmp):
198
196
  raise ValueError("Gain at min value")
199
197
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
200
198
 
201
- @AsyncStatus.wrap
202
199
  async def get_gain(self) -> Enum:
203
200
  result = await asyncio.gather(
204
201
  self.coarse_gain.get_value(), self.fine_gain.get_value()
205
202
  )
206
203
  return self.gain_conversion_table[self.combined_table(result).name]
207
204
 
208
- @AsyncStatus.wrap
209
205
  async def get_upperlimit(self) -> float:
210
206
  return self.upperlimit
211
207
 
212
- @AsyncStatus.wrap
213
208
  async def get_lowerlimit(self) -> float:
214
209
  return self.lowerlimit
File without changes
@@ -0,0 +1,64 @@
1
+ from enum import Enum
2
+ from typing import Any, Generic, TypeVar
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+
6
+
7
+ class EnergyMode(str, Enum):
8
+ KINETIC = "Kinetic"
9
+ BINDING = "Binding"
10
+
11
+
12
+ class BaseRegion(BaseModel):
13
+ """
14
+ Generic region model that holds the data. Specialised region models should inherit
15
+ this to extend functionality.
16
+ """
17
+
18
+ name: str = "New_region"
19
+ enabled: bool = False
20
+ slices: int = 1
21
+ iterations: int = 1
22
+ # These ones we need subclasses to provide default values
23
+ lensMode: str
24
+ passEnergy: int | float
25
+ acquisitionMode: str
26
+ lowEnergy: float
27
+ highEnergy: float
28
+ stepTime: float
29
+ energyStep: float
30
+ energyMode: EnergyMode = EnergyMode.KINETIC
31
+
32
+ @model_validator(mode="before")
33
+ @classmethod
34
+ def check_energy_mode(cls, data: Any) -> Any:
35
+ if isinstance(data, dict):
36
+ # convert bindingEnergy to energyMode to make base region more generic
37
+ if "bindingEnergy" in data:
38
+ is_binding_energy = data["bindingEnergy"]
39
+ del data["bindingEnergy"]
40
+ data["energyMode"] = (
41
+ EnergyMode.BINDING if is_binding_energy else EnergyMode.KINETIC
42
+ )
43
+ return data
44
+
45
+
46
+ TBaseRegion = TypeVar("TBaseRegion", bound=BaseRegion)
47
+
48
+
49
+ class BaseSequence(BaseModel, Generic[TBaseRegion]):
50
+ """
51
+ Generic sequence model that holds the list of region data. Specialised sequence models
52
+ should inherit this to extend functionality.
53
+ """
54
+
55
+ regions: list[TBaseRegion] = Field(default_factory=lambda: [])
56
+
57
+ def get_enabled_regions(self) -> list[BaseRegion]:
58
+ return [r for r in self.regions if r.enabled]
59
+
60
+ def get_region_names(self) -> list[str]:
61
+ return [r.name for r in self.regions]
62
+
63
+ def get_enabled_region_names(self) -> list[str]:
64
+ return [r.name for r in self.get_enabled_regions()]
File without changes
@@ -0,0 +1,24 @@
1
+ from pydantic import Field
2
+
3
+ from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
4
+
5
+
6
+ class SpecsRegion(BaseRegion):
7
+ # Override base class with defaults
8
+ lensMode: str = "SmallArea"
9
+ passEnergy: int | float = 5.0
10
+ acquisitionMode: str = "Fixed Transmission"
11
+ lowEnergy: float = Field(default=800, alias="startEnergy")
12
+ highEnergy: float = Field(default=850, alias="endEnergy")
13
+ stepTime: float = Field(default=1.0, alias="exposureTime")
14
+ energyStep: float = Field(default=0.1, alias="stepEnergy")
15
+ # Specific to this class
16
+ values: float = 1
17
+ centreEnergy: float = 0
18
+ psuMode: str = "1.5keV"
19
+ acquisitionMode: str = ""
20
+ estimatedTimeInMs: float = 0
21
+
22
+
23
+ class SpecsSequence(BaseSequence):
24
+ regions: list[SpecsRegion] = Field(default_factory=lambda: [])
File without changes
@@ -0,0 +1,77 @@
1
+ import uuid
2
+ from enum import Enum
3
+
4
+ from ophyd_async.core import StrictEnum
5
+ from pydantic import BaseModel, Field
6
+
7
+ from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
8
+
9
+
10
+ class Status(str, Enum):
11
+ READY = "Ready"
12
+ RUNNING = "Running"
13
+ COMPLETED = "Completed"
14
+ INVALID = "Invalid"
15
+ ABORTED = "Aborted"
16
+
17
+
18
+ class DetectorMode(StrictEnum):
19
+ ADC = "ADC"
20
+ PULSE_COUNTING = "Pulse Counting"
21
+
22
+
23
+ class AcquisitionMode(str, Enum):
24
+ SWEPT = "Swept"
25
+ FIXED = "Fixed"
26
+
27
+
28
+ class VGScientaRegion(BaseRegion):
29
+ # Override defaults of base region class
30
+ lensMode: str = "Angular45"
31
+ passEnergy: int | float = 5
32
+ acquisitionMode: str = AcquisitionMode.SWEPT
33
+ lowEnergy: float = 8.0
34
+ highEnergy: float = 10.0
35
+ stepTime: float = 1.0
36
+ energyStep: float = Field(default=200.0)
37
+ # Specific to this class
38
+ regionId: str = Field(default=str(uuid.uuid4()))
39
+ excitationEnergySource: str = "source1"
40
+ fixEnergy: float = 9.0
41
+ totalSteps: float = 13.0
42
+ totalTime: float = 13.0
43
+ exposureTime: float = 1.0
44
+ firstXChannel: int = 1
45
+ lastXChannel: int = 1000
46
+ firstYChannel: int = 101
47
+ lastYChannel: int = 800
48
+ detectorMode: DetectorMode = DetectorMode.ADC
49
+ status: Status = Status.READY
50
+
51
+
52
+ class VGScientaExcitationEnergySource(BaseModel):
53
+ name: str = "source1"
54
+ scannableName: str = ""
55
+ value: float = 0
56
+
57
+
58
+ class VGScientaSequence(BaseSequence):
59
+ elementSet: str = Field(default="Unknown")
60
+ excitationEnergySources: list[VGScientaExcitationEnergySource] = Field(
61
+ default_factory=lambda: []
62
+ )
63
+ regions: list[VGScientaRegion] = Field(default_factory=lambda: [])
64
+
65
+ def get_excitation_energy_source_by_region(
66
+ self, region: VGScientaRegion
67
+ ) -> VGScientaExcitationEnergySource | None:
68
+ filtered_excitation_energy_sources = [
69
+ e
70
+ for e in self.excitationEnergySources
71
+ if e.name == region.excitationEnergySource
72
+ ]
73
+ return (
74
+ filtered_excitation_energy_sources[0]
75
+ if len(filtered_excitation_energy_sources) == 1
76
+ else None
77
+ )
@@ -23,7 +23,7 @@ from ophyd_async.epics.core import (
23
23
  from pydantic import field_validator
24
24
  from pydantic.dataclasses import dataclass
25
25
 
26
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
26
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
27
27
  from dodal.log import LOGGER
28
28
  from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
29
29
 
@@ -203,7 +203,7 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
203
203
  self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
204
204
  self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
205
205
 
206
- self.expected_images = create_hardware_backed_soft_signal(
206
+ self.expected_images = create_r_hardware_backed_soft_signal(
207
207
  float, self._calculate_expected_images
208
208
  )
209
209
 
@@ -55,7 +55,7 @@ class HutchInterlock(StandardReadable):
55
55
  return interlock_state == HUTCH_SAFE_FOR_OPERATIONS
56
56
 
57
57
 
58
- class HutchShutter(StandardReadable, Movable):
58
+ class HutchShutter(StandardReadable, Movable[ShutterDemand]):
59
59
  """Device to operate the hutch shutter.
60
60
 
61
61
  When a demand is sent, the device should first check the hutch status \
@@ -5,7 +5,7 @@ from ophyd_async.core import StandardReadable, StrictEnum
5
5
  from ophyd_async.epics.motor import Motor
6
6
 
7
7
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
8
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
8
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
9
9
 
10
10
 
11
11
  class BeamstopPositions(StrictEnum):
@@ -53,7 +53,7 @@ class Beamstop(StandardReadable):
53
53
  self.x_mm = Motor(prefix + "X")
54
54
  self.y_mm = Motor(prefix + "Y")
55
55
  self.z_mm = Motor(prefix + "Z")
56
- self.selected_pos = create_hardware_backed_soft_signal(
56
+ self.selected_pos = create_r_hardware_backed_soft_signal(
57
57
  BeamstopPositions, self._get_selected_position
58
58
  )
59
59
 
@@ -175,7 +175,7 @@ class I10Apple2(Apple2):
175
175
  self._available_pol = list(self.lookup_tables["Gap"].keys())
176
176
 
177
177
 
178
- class I10Apple2PGM(StandardReadable, Movable):
178
+ class I10Apple2PGM(StandardReadable, Movable[float]):
179
179
  """
180
180
  Compound device to set both ID and PGM energy at the sample time,poly_deg
181
181
 
@@ -211,7 +211,7 @@ class I10Apple2PGM(StandardReadable, Movable):
211
211
  )
212
212
 
213
213
 
214
- class I10Apple2Pol(StandardReadable, Movable):
214
+ class I10Apple2Pol(StandardReadable, Movable[str]):
215
215
  """
216
216
  Compound device to set polorisation of ID.
217
217
  """
@@ -240,7 +240,7 @@ class I10Apple2Pol(StandardReadable, Movable):
240
240
  ) # Move id to new polarisation
241
241
 
242
242
 
243
- class LinearArbitraryAngle(StandardReadable, Movable):
243
+ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
244
244
  """
245
245
  Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
246
246
  is the direction of the magnetic field which can be change by varying the jaw_phase