dls-dodal 1.51.0__py3-none-any.whl → 1.52.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 (48) hide show
  1. {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/METADATA +3 -2
  2. {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/RECORD +45 -41
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/b07.py +7 -6
  5. dodal/beamlines/b07_1.py +7 -6
  6. dodal/beamlines/i03.py +4 -1
  7. dodal/beamlines/i04.py +2 -2
  8. dodal/beamlines/i09.py +7 -5
  9. dodal/beamlines/i09_1.py +6 -3
  10. dodal/beamlines/i09_2.py +2 -2
  11. dodal/beamlines/i22.py +11 -0
  12. dodal/beamlines/i24.py +2 -2
  13. dodal/beamlines/p60.py +6 -4
  14. dodal/beamlines/p99.py +14 -0
  15. dodal/common/device_utils.py +45 -0
  16. dodal/devices/attenuator/attenuator.py +5 -3
  17. dodal/devices/b07/__init__.py +2 -2
  18. dodal/devices/b07/enums.py +24 -0
  19. dodal/devices/b07_1/__init__.py +2 -2
  20. dodal/devices/b07_1/enums.py +18 -0
  21. dodal/devices/electron_analyser/abstract/__init__.py +4 -0
  22. dodal/devices/electron_analyser/abstract/base_driver_io.py +21 -4
  23. dodal/devices/electron_analyser/abstract/base_region.py +20 -7
  24. dodal/devices/electron_analyser/detector.py +1 -1
  25. dodal/devices/electron_analyser/specs/detector.py +18 -4
  26. dodal/devices/electron_analyser/specs/driver_io.py +17 -5
  27. dodal/devices/electron_analyser/specs/region.py +9 -5
  28. dodal/devices/electron_analyser/types.py +21 -5
  29. dodal/devices/electron_analyser/vgscienta/detector.py +16 -7
  30. dodal/devices/electron_analyser/vgscienta/driver_io.py +13 -4
  31. dodal/devices/electron_analyser/vgscienta/region.py +11 -5
  32. dodal/devices/i09/__init__.py +2 -2
  33. dodal/devices/i09/enums.py +15 -0
  34. dodal/devices/i09_1/__init__.py +3 -0
  35. dodal/devices/i09_1/enums.py +19 -0
  36. dodal/devices/linkam3.py +25 -81
  37. dodal/devices/oav/pin_image_recognition/__init__.py +11 -14
  38. dodal/devices/p60/__init__.py +3 -2
  39. dodal/devices/p60/enums.py +10 -0
  40. dodal/devices/tetramm.py +134 -150
  41. dodal/devices/xbpm_feedback.py +6 -3
  42. dodal/devices/b07/grating.py +0 -9
  43. dodal/devices/b07_1/grating.py +0 -10
  44. dodal/devices/i09/grating.py +0 -7
  45. {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/WHEEL +0 -0
  46. {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/entry_points.txt +0 -0
  47. {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/licenses/LICENSE +0 -0
  48. {dls_dodal-1.51.0.dist-info → dls_dodal-1.52.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,10 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+
4
+ class LensMode(StrictEnum):
5
+ TRANSMISSION = "Transmission"
6
+ ANGULAR14 = "Angular14"
7
+ ANGULAR7NF = "Angular7NF"
8
+ ANGULAR30 = "Angular30"
9
+ ANGULAR30_SMALLSPOT = "Angular30_SmallSpot"
10
+ ANGULAR14_SMALLSPOT = "Angular14_SmallSpot"
dodal/devices/tetramm.py CHANGED
@@ -1,12 +1,16 @@
1
1
  import asyncio
2
+ from collections.abc import Sequence
3
+ from typing import Annotated as A
2
4
 
3
- from bluesky.protocols import Hints
4
5
  from ophyd_async.core import (
6
+ DEFAULT_TIMEOUT,
7
+ AsyncStatus,
5
8
  DatasetDescriber,
6
9
  DetectorController,
7
10
  DetectorTrigger,
8
- Device,
9
11
  PathProvider,
12
+ SignalR,
13
+ SignalRW,
10
14
  StandardDetector,
11
15
  StrictEnum,
12
16
  TriggerInfo,
@@ -15,15 +19,12 @@ from ophyd_async.core import (
15
19
  )
16
20
  from ophyd_async.epics.adcore import (
17
21
  ADHDFWriter,
22
+ NDArrayBaseIO,
18
23
  NDFileHDFIO,
19
24
  NDPluginBaseIO,
20
25
  stop_busy_record,
21
26
  )
22
- from ophyd_async.epics.core import (
23
- epics_signal_r,
24
- epics_signal_rw,
25
- epics_signal_rw_rbv,
26
- )
27
+ from ophyd_async.epics.core import PvSuffix
27
28
 
28
29
 
29
30
  class TetrammRange(StrictEnum):
@@ -54,208 +55,191 @@ class TetrammGeometry(StrictEnum):
54
55
  SQUARE = "Square"
55
56
 
56
57
 
57
- class TetrammDriver(Device):
58
- def __init__(
59
- self,
60
- prefix: str,
61
- name: str = "",
62
- ):
63
- self._prefix = prefix
64
- self.range = epics_signal_rw_rbv(TetrammRange, prefix + "Range")
65
- self.sample_time = epics_signal_r(float, prefix + "SampleTime_RBV")
66
-
67
- self.values_per_reading = epics_signal_rw_rbv(int, prefix + "ValuesPerRead")
68
- self.averaging_time = epics_signal_rw_rbv(float, prefix + "AveragingTime")
69
- self.to_average = epics_signal_r(int, prefix + "NumAverage_RBV")
70
- self.averaged = epics_signal_r(int, prefix + "NumAveraged_RBV")
71
-
72
- self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
73
-
74
- # this PV is special, for some reason it doesn't have a _RBV suffix...
75
- self.overflows = epics_signal_r(int, prefix + "RingOverflows")
76
-
77
- self.num_channels = epics_signal_rw_rbv(TetrammChannels, prefix + "NumChannels")
78
- self.resolution = epics_signal_rw_rbv(TetrammResolution, prefix + "Resolution")
79
- self.trigger_mode = epics_signal_rw_rbv(TetrammTrigger, prefix + "TriggerMode")
80
- self.bias = epics_signal_rw_rbv(bool, prefix + "BiasState")
81
- self.bias_volts = epics_signal_rw_rbv(float, prefix + "BiasVoltage")
82
- self.geometry = epics_signal_rw_rbv(TetrammGeometry, prefix + "Geometry")
83
- self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
84
-
85
- super().__init__(name=name)
58
+ class TetrammDriver(NDArrayBaseIO):
59
+ range = A[SignalRW[TetrammRange], PvSuffix.rbv("Range")]
60
+ sample_time: A[SignalR[float], PvSuffix("SampleTime_RBV")]
61
+ values_per_reading: A[SignalRW[int], PvSuffix.rbv("ValuesPerRead")]
62
+ averaging_time: A[SignalRW[float], PvSuffix.rbv("AveragingTime")]
63
+ to_average: A[SignalR[int], PvSuffix("NumAverage_RBV")]
64
+ averaged: A[SignalR[int], PvSuffix("NumAveraged_RBV")]
65
+ overflows: A[SignalR[int], PvSuffix("RingOverflows")]
66
+ num_channels: A[SignalRW[TetrammChannels], PvSuffix.rbv("NumChannels")]
67
+ resolution: A[SignalRW[TetrammResolution], PvSuffix.rbv("Resolution")]
68
+ trigger_mode: A[SignalRW[TetrammTrigger], PvSuffix.rbv("TriggerMode")]
69
+ bias: A[SignalRW[bool], PvSuffix.rbv("BiasState")]
70
+ bias_volts: A[SignalRW[float], PvSuffix.rbv("BiasVoltage")]
71
+ geometry: A[SignalRW[TetrammGeometry], PvSuffix.rbv("Geometry")]
72
+ read_format: A[SignalRW[bool], PvSuffix.rbv("ReadFormat")]
86
73
 
87
74
 
88
75
  class TetrammController(DetectorController):
89
- """Controller for a TetrAMM current monitor
90
-
91
- Attributes:
92
- base_sample_rate (int): Fixed in hardware
93
-
94
- Args:
95
- drv (TetrammDriver): A configured driver for the device
96
- maximum_readings_per_frame (int): Maximum number of readings per frame: actual readings may be lower if higher frame rate is required
97
- minimum_values_per_reading (int): Lower bound on the values that will be averaged to create a single reading
98
- readings_per_frame (int): Actual number of readings per frame.
99
-
76
+ """Controller for a TetrAMM current monitor"""
77
+
78
+ _supported_trigger_types = {
79
+ DetectorTrigger.EDGE_TRIGGER: TetrammTrigger.EXT_TRIGGER,
80
+ DetectorTrigger.CONSTANT_GATE: TetrammTrigger.EXT_TRIGGER,
81
+ }
82
+ """"On the TetrAMM ASCII mode requires a minimum value of ValuesPerRead of 500,
83
+ [...] binary mode the minimum value of ValuesPerRead is 5."
84
+ https://millenia.cars.aps.anl.gov/software/epics/quadEMDoc.html
100
85
  """
101
-
102
- base_sample_rate: int = 100_000
86
+ _minimal_values_per_reading = {0: 5, 1: 500}
87
+ """The TetrAMM always digitizes at 100 kHz"""
88
+ _base_sample_rate: int = 100_000
103
89
 
104
90
  def __init__(
105
91
  self,
106
- drv: TetrammDriver,
107
- minimum_values_per_reading: int = 5,
108
- maximum_readings_per_frame: int = 1_000,
109
- readings_per_frame: int = 1_000,
110
- ):
111
- # TODO: Are any of these also fixed by hardware constraints?
112
- self._drv = drv
113
- self.maximum_readings_per_frame = maximum_readings_per_frame
114
- self.minimum_values_per_reading = minimum_values_per_reading
115
- self.readings_per_frame = readings_per_frame
92
+ driver: TetrammDriver,
93
+ ) -> None:
94
+ self.driver = driver
95
+ self._arm_status: AsyncStatus | None = None
116
96
 
117
97
  def get_deadtime(self, exposure: float | None) -> float:
118
98
  # 2 internal clock cycles. Best effort approximation
119
- return 2 / self.base_sample_rate
99
+ return 2 / self._base_sample_rate
120
100
 
121
- async def prepare(self, trigger_info: TriggerInfo):
122
- self._validate_trigger(trigger_info.trigger)
123
- assert trigger_info.livetime is not None
101
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
102
+ if trigger_info.trigger not in self._supported_trigger_types:
103
+ raise TypeError(
104
+ f"{self.__class__.__name__} only supports the following trigger "
105
+ f"types: {[k.name for k in self._supported_trigger_types]} but was asked to "
106
+ f"use {trigger_info.trigger}"
107
+ )
108
+ if trigger_info.livetime is None:
109
+ raise ValueError(f"{self.__class__.__name__} requires that livetime is set")
124
110
 
125
111
  # trigger mode must be set first and on its own!
126
- await self._drv.trigger_mode.set(TetrammTrigger.EXT_TRIGGER)
127
-
112
+ await self.driver.trigger_mode.set(
113
+ self._supported_trigger_types[trigger_info.trigger]
114
+ )
128
115
  await asyncio.gather(
129
- self._drv.averaging_time.set(trigger_info.livetime),
116
+ self.driver.averaging_time.set(trigger_info.livetime),
130
117
  self.set_exposure(trigger_info.livetime),
131
118
  )
132
119
 
133
120
  async def arm(self):
134
- self._arm_status = await set_and_wait_for_value(
135
- self._drv.acquire, True, wait_for_set_completion=False
136
- )
121
+ self._arm_status = await self.start_acquiring_driver_and_ensure_status()
137
122
 
138
123
  async def wait_for_idle(self):
139
124
  if self._arm_status and not self._arm_status.done:
140
125
  await self._arm_status
141
126
  self._arm_status = None
142
127
 
143
- def _validate_trigger(self, trigger: DetectorTrigger) -> None:
144
- supported_trigger_types = {
145
- DetectorTrigger.EDGE_TRIGGER,
146
- DetectorTrigger.CONSTANT_GATE,
147
- }
148
-
149
- if trigger not in supported_trigger_types:
150
- raise ValueError(
151
- f"{self.__class__.__name__} only supports the following trigger "
152
- f"types: {supported_trigger_types} but was asked to "
153
- f"use {trigger}"
154
- )
155
-
156
128
  async def disarm(self):
157
- await stop_busy_record(self._drv.acquire, False, timeout=1)
129
+ # We can't use caput callback as we already used it in arm() and we can't have
130
+ # 2 or they will deadlock
131
+ await stop_busy_record(self.driver.acquire, False, timeout=1)
158
132
 
159
- async def set_exposure(self, exposure: float):
160
- """Tries to set the exposure time of a single frame.
133
+ async def set_exposure(self, exposure: float) -> None:
134
+ """Set the exposure time and acquire period.
161
135
 
162
136
  As during the exposure time, the device must collect an integer number
163
137
  of readings, in the case where the exposure is not a multiple of the base
164
138
  sample rate, it will be lowered to the prior multiple ot ensure triggers
165
139
  are not missed.
166
140
 
167
- Args:
168
- exposure (float): The time for a single frame in seconds
169
-
170
- Raises:
171
- ValueError: If exposure is too low to collect the required number
172
- of readings per frame.
141
+ :param exposure: Desired exposure time.
142
+ :type exposure: How long to wait for the exposure time and acquire
143
+ period to be set.
173
144
  """
145
+ sample_time = await self.driver.sample_time.get_value()
146
+ minimum_samples = self._minimal_values_per_reading[
147
+ await self.driver.read_format.get_value()
148
+ ]
149
+ samples_per_reading = int(exposure / sample_time)
150
+ if samples_per_reading < minimum_samples:
151
+ raise ValueError(
152
+ "Tetramm exposure time must be at least "
153
+ f"{minimum_samples * sample_time}s, asked to set it to {exposure}s"
154
+ )
155
+ await self.driver.averaging_time.set(samples_per_reading * sample_time)
174
156
 
175
- # Set up the number of readings across the exposure period to scale with
176
- # the exposure time
177
- self._set_minimum_exposure(exposure)
178
- values_per_reading: int = int(
179
- exposure * self.base_sample_rate / self.readings_per_frame
180
- )
181
-
182
- await self._drv.values_per_reading.set(values_per_reading)
157
+ async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus:
158
+ """Start acquiring driver, raising ValueError if the detector is in a bad state.
183
159
 
184
- @property
185
- def max_frame_rate(self) -> float:
186
- """Max frame rate in Hz for the current configuration"""
187
- return 1 / self.minimum_exposure
160
+ This sets driver.acquire to True, and waits for it to be True up to a timeout.
161
+ Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES,
162
+ and otherwise raises a ValueError.
188
163
 
189
- @max_frame_rate.setter
190
- def max_frame_rate(self, mfr: float):
191
- self._set_minimum_exposure(1 / mfr)
164
+ :returns AsyncStatus:
165
+ An AsyncStatus that can be awaited to set driver.acquire to True and perform
166
+ subsequent raising (if applicable) due to detector state.
167
+ """
168
+ status = await set_and_wait_for_value(
169
+ self.driver.acquire,
170
+ True,
171
+ timeout=DEFAULT_TIMEOUT,
172
+ wait_for_set_completion=False,
173
+ )
192
174
 
193
- @property
194
- def minimum_exposure(self) -> float:
195
- """Smallest amount of time needed to take a frame"""
196
- time_per_reading = self.minimum_values_per_reading / self.base_sample_rate
197
- return self.readings_per_frame * time_per_reading
175
+ async def complete_acquisition() -> None:
176
+ # NOTE: possible race condition here between the callback from
177
+ # set_and_wait_for_value and the detector state updating.
178
+ await status
198
179
 
199
- def _set_minimum_exposure(self, exposure: float):
200
- time_per_reading = self.minimum_values_per_reading / self.base_sample_rate
201
- if exposure < time_per_reading:
202
- raise ValueError(
203
- "Tetramm exposure time must be at least "
204
- f"{time_per_reading}s, asked to set it to {exposure}s"
205
- )
206
- self.readings_per_frame = int(
207
- min(self.maximum_readings_per_frame, exposure / time_per_reading)
208
- )
180
+ return AsyncStatus(complete_acquisition())
209
181
 
210
182
 
211
183
  class TetrammDatasetDescriber(DatasetDescriber):
212
- max_channels = 11
213
-
214
- def __init__(self, controller: TetrammController) -> None:
215
- self.controller = controller
184
+ def __init__(self, driver: TetrammDriver) -> None:
185
+ self._driver = driver
216
186
 
217
187
  async def np_datatype(self) -> str:
218
188
  return "<f8" # IEEE 754 double precision floating point
219
189
 
220
190
  async def shape(self) -> tuple[int, int]:
221
- return (self.max_channels, self.controller.readings_per_frame)
191
+ return (
192
+ int(await self._driver.num_channels.get_value()),
193
+ int(
194
+ await self._driver.averaging_time.get_value()
195
+ / await self._driver.sample_time.get_value(),
196
+ ),
197
+ )
222
198
 
223
199
 
224
- # TODO: Support MeanValue signals https://github.com/DiamondLightSource/dodal/issues/337
225
200
  class TetrammDetector(StandardDetector):
226
201
  def __init__(
227
202
  self,
228
203
  prefix: str,
229
204
  path_provider: PathProvider,
205
+ drv_suffix: str = "DRV:",
206
+ fileio_suffix: str = "HDF5:",
230
207
  name: str = "",
231
- type: str | None = None,
232
208
  plugins: dict[str, NDPluginBaseIO] | None = None,
233
- ) -> None:
234
- self.drv = TetrammDriver(prefix + "DRV:")
235
- self.hdf = NDFileHDFIO(prefix + "HDF5:")
236
- controller = TetrammController(self.drv)
237
- config_signals = [
238
- self.drv.values_per_reading,
239
- self.drv.averaging_time,
240
- self.drv.sample_time,
209
+ config_sigs: Sequence[SignalR] = (),
210
+ type: str | None = None,
211
+ ):
212
+ self.driver = TetrammDriver(prefix + drv_suffix)
213
+ self.file_io = NDFileHDFIO(prefix + fileio_suffix)
214
+ controller = TetrammController(self.driver)
215
+
216
+ writer = ADHDFWriter(
217
+ fileio=self.file_io,
218
+ path_provider=path_provider,
219
+ dataset_describer=TetrammDatasetDescriber(self.driver),
220
+ plugins=plugins,
221
+ )
222
+
223
+ config_sigs = [
224
+ self.driver.values_per_reading,
225
+ self.driver.averaging_time,
226
+ self.driver.sample_time,
227
+ *config_sigs,
241
228
  ]
229
+
242
230
  if type:
243
231
  self.type, _ = soft_signal_r_and_setter(str, type)
244
- config_signals.append(self.type)
232
+ config_sigs.append(self.type)
245
233
  else:
246
234
  self.type = None
235
+
236
+ if plugins is not None:
237
+ for plugin_name, plugin in plugins.items():
238
+ setattr(self, plugin_name, plugin)
239
+
247
240
  super().__init__(
248
- controller,
249
- ADHDFWriter(
250
- fileio=self.hdf,
251
- path_provider=path_provider,
252
- dataset_describer=TetrammDatasetDescriber(controller),
253
- plugins=plugins,
254
- ),
255
- config_signals,
256
- name,
241
+ controller=controller,
242
+ writer=writer,
243
+ name=name,
244
+ config_sigs=config_sigs,
257
245
  )
258
-
259
- @property
260
- def hints(self) -> Hints:
261
- return {"fields": [self.name]}
@@ -2,6 +2,8 @@ from bluesky.protocols import Triggerable
2
2
  from ophyd_async.core import AsyncStatus, Device, StrictEnum, observe_value
3
3
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
4
4
 
5
+ from dodal.common.device_utils import periodic_reminder
6
+
5
7
 
6
8
  class Pause(StrictEnum):
7
9
  PAUSE = "Paused" # 0
@@ -22,6 +24,7 @@ class XBPMFeedback(Device, Triggerable):
22
24
 
23
25
  @AsyncStatus.wrap
24
26
  async def trigger(self):
25
- async for value in observe_value(self.pos_stable):
26
- if value:
27
- return
27
+ async with periodic_reminder("Waiting for XBPM"):
28
+ async for value in observe_value(self.pos_stable):
29
+ if value:
30
+ return
@@ -1,9 +0,0 @@
1
- from ophyd_async.core import StrictEnum
2
-
3
-
4
- class B07BGrating(StrictEnum):
5
- NI_400 = "400 l/mm Ni"
6
- NI_1000 = "1000 l/mm Ni"
7
- PT_600 = "BAD 600 l/mm Pt"
8
- AU_600 = "600 l/mm Au"
9
- NO_GRATING = "No Grating"
@@ -1,10 +0,0 @@
1
- from ophyd_async.core import StrictEnum
2
-
3
-
4
- class B07CGrating(StrictEnum):
5
- AU_400 = "400 l/mm Au"
6
- AU_600 = "600 l/mm Au"
7
- PT_600 = "600 l/mm Pt"
8
- AU_1200 = "1200 l/mm Au"
9
- ML_1200 = "1200 l/mm ML"
10
- NO_GRATING = "No Grating"
@@ -1,7 +0,0 @@
1
- from ophyd_async.core import StrictEnum
2
-
3
-
4
- class I09Grating(StrictEnum):
5
- G_300 = "300 lines/mm"
6
- G_400 = "400 lines/mm"
7
- G_800 = "800 lines/mm"