dls-dodal 1.36.3__py3-none-any.whl → 1.37.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 (43) hide show
  1. {dls_dodal-1.36.3.dist-info → dls_dodal-1.37.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.36.3.dist-info → dls_dodal-1.37.0.dist-info}/RECORD +43 -29
  3. {dls_dodal-1.36.3.dist-info → dls_dodal-1.37.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/i02_1.py +37 -0
  6. dodal/beamlines/i03.py +20 -3
  7. dodal/beamlines/i04.py +3 -3
  8. dodal/beamlines/i10.py +105 -0
  9. dodal/beamlines/i22.py +15 -0
  10. dodal/beamlines/i24.py +1 -1
  11. dodal/beamlines/p99.py +6 -2
  12. dodal/common/crystal_metadata.py +3 -3
  13. dodal/common/udc_directory_provider.py +3 -1
  14. dodal/devices/aperturescatterguard.py +3 -0
  15. dodal/devices/{attenuator.py → attenuator/attenuator.py} +29 -1
  16. dodal/devices/attenuator/filter.py +11 -0
  17. dodal/devices/attenuator/filter_selections.py +72 -0
  18. dodal/devices/bimorph_mirror.py +151 -0
  19. dodal/devices/current_amplifiers/__init__.py +34 -0
  20. dodal/devices/current_amplifiers/current_amplifier.py +103 -0
  21. dodal/devices/current_amplifiers/current_amplifier_detector.py +109 -0
  22. dodal/devices/current_amplifiers/femto.py +143 -0
  23. dodal/devices/current_amplifiers/sr570.py +214 -0
  24. dodal/devices/current_amplifiers/struck_scaler_counter.py +79 -0
  25. dodal/devices/detector/det_dim_constants.py +15 -0
  26. dodal/devices/eiger_odin.py +3 -3
  27. dodal/devices/fast_grid_scan.py +8 -3
  28. dodal/devices/i03/beamstop.py +85 -0
  29. dodal/devices/i04/transfocator.py +67 -53
  30. dodal/devices/i10/rasor/rasor_current_amp.py +72 -0
  31. dodal/devices/i10/rasor/rasor_motors.py +62 -0
  32. dodal/devices/i10/rasor/rasor_scaler_cards.py +12 -0
  33. dodal/devices/p99/sample_stage.py +2 -28
  34. dodal/devices/robot.py +2 -2
  35. dodal/devices/undulator_dcm.py +9 -11
  36. dodal/devices/zebra.py +6 -1
  37. dodal/devices/zocalo/zocalo_interaction.py +2 -1
  38. dodal/devices/zocalo/zocalo_results.py +22 -2
  39. dodal/log.py +2 -2
  40. dodal/plans/wrapped.py +3 -3
  41. {dls_dodal-1.36.3.dist-info → dls_dodal-1.37.0.dist-info}/LICENSE +0 -0
  42. {dls_dodal-1.36.3.dist-info → dls_dodal-1.37.0.dist-info}/entry_points.txt +0 -0
  43. {dls_dodal-1.36.3.dist-info → dls_dodal-1.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ import asyncio
2
+ from enum import Enum
3
+
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ StandardReadableFormat,
7
+ StrictEnum,
8
+ )
9
+ from ophyd_async.epics.core import epics_signal_rw
10
+
11
+ from dodal.devices.current_amplifiers.current_amplifier import CurrentAmp
12
+ from dodal.log import LOGGER
13
+
14
+
15
+ class SR570GainTable(StrictEnum):
16
+ """Coarse/unit sensitivity setting for SR570 current amplifier"""
17
+
18
+ SEN_1 = "mA/V"
19
+ SEN_2 = "uA/V"
20
+ SEN_3 = "nA/V"
21
+ SEN_4 = "pA/V"
22
+
23
+
24
+ class SR570FineGainTable(StrictEnum):
25
+ """Fine sensitivity setting for SR570 current amplifier"""
26
+
27
+ SEN_1 = "1"
28
+ SEN_2 = "2"
29
+ SEN_3 = "5"
30
+ SEN_4 = "10"
31
+ SEN_5 = "20"
32
+ SEN_6 = "50"
33
+ SEN_7 = "100"
34
+ SEN_8 = "200"
35
+ SEN_9 = "500"
36
+
37
+
38
+ class SR570RaiseTimeTable(float, Enum):
39
+ """These are the gain dependent raise time(s) for SR570 current amplifier"""
40
+
41
+ SEN_1 = 1e-4
42
+ SEN_2 = 1e-2
43
+ SEN_3 = 0.15
44
+ SEN_4 = 0.2
45
+
46
+
47
+ class SR570FullGainTable(Enum):
48
+ """Combined gain table, as each gain step is a combination of both coarse gain and
49
+ fine gain setting"""
50
+
51
+ SEN_1 = [SR570GainTable.SEN_1, SR570FineGainTable.SEN_1]
52
+ SEN_2 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_9]
53
+ SEN_3 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_8]
54
+ SEN_4 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_7]
55
+ SEN_5 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_6]
56
+ SEN_6 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_5]
57
+ SEN_7 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_4]
58
+ SEN_8 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_3]
59
+ SEN_9 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_2]
60
+ SEN_10 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_1]
61
+ SEN_11 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_9]
62
+ SEN_12 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_8]
63
+ SEN_13 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_7]
64
+ SEN_14 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_6]
65
+ SEN_15 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_5]
66
+ SEN_16 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_4]
67
+ SEN_17 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_3]
68
+ SEN_18 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_2]
69
+ SEN_19 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_1]
70
+ SEN_20 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_9]
71
+ SEN_21 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_8]
72
+ SEN_22 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_7]
73
+ SEN_23 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_6]
74
+ SEN_24 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_5]
75
+ SEN_25 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_4]
76
+ SEN_26 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_3]
77
+ SEN_27 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_2]
78
+ SEN_28 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_1]
79
+
80
+
81
+ class SR570GainToCurrentTable(float, Enum):
82
+ """Conversion table for gain(sen) to current"""
83
+
84
+ SEN_1 = 1e3
85
+ SEN_2 = 2e3
86
+ SEN_3 = 5e3
87
+ SEN_4 = 1e4
88
+ SEN_5 = 2e4
89
+ SEN_6 = 5e4
90
+ SEN_7 = 1e5
91
+ SEN_8 = 2e5
92
+ SEN_9 = 5e5
93
+ SEN_10 = 1e6
94
+ SEN_11 = 2e6
95
+ SEN_12 = 5e6
96
+ SEN_13 = 1e7
97
+ SEN_14 = 2e7
98
+ SEN_15 = 5e7
99
+ SEN_16 = 1e8
100
+ SEN_17 = 2e8
101
+ SEN_18 = 5e8
102
+ SEN_19 = 1e9
103
+ SEN_20 = 2e9
104
+ SEN_21 = 5e9
105
+ SEN_22 = 1e10
106
+ SEN_23 = 2e10
107
+ SEN_24 = 5e10
108
+ SEN_25 = 1e11
109
+ SEN_26 = 2e11
110
+ SEN_27 = 5e11
111
+ SEN_28 = 1e12
112
+
113
+
114
+ class SR570(CurrentAmp):
115
+ """
116
+ SR570 current amplifier device. This is similar to Femto with the only different
117
+ is SR570 has two gain setting fine and coarse, therefore it requires extra
118
+ gain tables.
119
+ Attributes:
120
+ fine_gain (SignalRW): This is the epic signal that control SR570 fine gain.
121
+ coarse_gain (SignalRW): This is the epic signal that control SR570 coarse gain.
122
+ fine_gain_table (strictEnum): The table that fine_gain use to set gain.
123
+ coarse_gain_table (strictEnum): The table that coarse_gain use to set gain.
124
+ timeout (float): Maximum waiting time in second for setting gain.
125
+ raise_timetable (Enum): Table contain the amount of time to wait after
126
+ setting gain.
127
+ combined_table (Enum): Table that combine fine and coarse table into one.
128
+ gain (SignalRW([str]): Soft signal to store the member name of the current gain
129
+ setting in gain_conversion_table.
130
+ upperlimit (float): upperlimit of the current amplifier
131
+ lowerlimit (float): lowerlimit of the current amplifier
132
+
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ prefix: str,
138
+ suffix: str,
139
+ fine_gain_table: type[StrictEnum],
140
+ coarse_gain_table: type[StrictEnum],
141
+ combined_table: type[Enum],
142
+ gain_to_current_table: type[Enum],
143
+ raise_timetable: type[Enum],
144
+ upperlimit: float = 4.8,
145
+ lowerlimit: float = 0.4,
146
+ timeout: float = 1,
147
+ name: str = "",
148
+ ) -> None:
149
+ super().__init__(name=name, gain_conversion_table=gain_to_current_table)
150
+
151
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
152
+ self.fine_gain = epics_signal_rw(fine_gain_table, prefix + suffix + "1")
153
+ self.coarse_gain = epics_signal_rw(coarse_gain_table, prefix + suffix + "2")
154
+
155
+ self.fine_gain_table = fine_gain_table
156
+ self.coarse_gain_table = coarse_gain_table
157
+ self.timeout = timeout
158
+ self.raise_timetable = raise_timetable
159
+ self.combined_table = combined_table
160
+ self.upperlimit = upperlimit
161
+ self.lowerlimit = lowerlimit
162
+
163
+ @AsyncStatus.wrap
164
+ async def set(self, value) -> None:
165
+ if value not in [item.value for item in self.gain_conversion_table]:
166
+ raise ValueError(
167
+ f"Gain value {value} is not within {self.name} range."
168
+ + "\n Available gain:"
169
+ + f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
170
+ )
171
+ SEN_setting = self.gain_conversion_table(value).name
172
+ LOGGER.info(f"{self.name} gain change to {value}")
173
+
174
+ coarse_gain, fine_gain = self.combined_table[SEN_setting].value
175
+ await asyncio.gather(
176
+ self.fine_gain.set(value=fine_gain, timeout=self.timeout),
177
+ self.coarse_gain.set(value=coarse_gain, timeout=self.timeout),
178
+ )
179
+ await asyncio.sleep(self.raise_timetable[coarse_gain.name].value)
180
+
181
+ @AsyncStatus.wrap
182
+ async def increase_gain(self, value=3) -> None:
183
+ current_gain = int((await self.get_gain()).name.split("_")[-1])
184
+ current_gain += value
185
+ if current_gain > len(self.combined_table):
186
+ await self.set(
187
+ self.gain_conversion_table[f"SEN_{len(self.combined_table)}"]
188
+ )
189
+ raise ValueError("Gain at max value")
190
+ await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
191
+
192
+ @AsyncStatus.wrap
193
+ async def decrease_gain(self, value=3) -> None:
194
+ current_gain = int((await self.get_gain()).name.split("_")[-1])
195
+ current_gain -= value
196
+ if current_gain < 1:
197
+ await self.set(self.gain_conversion_table["SEN_1"])
198
+ raise ValueError("Gain at min value")
199
+ await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
200
+
201
+ @AsyncStatus.wrap
202
+ async def get_gain(self) -> Enum:
203
+ result = await asyncio.gather(
204
+ self.coarse_gain.get_value(), self.fine_gain.get_value()
205
+ )
206
+ return self.gain_conversion_table[self.combined_table(result).name]
207
+
208
+ @AsyncStatus.wrap
209
+ async def get_upperlimit(self) -> float:
210
+ return self.upperlimit
211
+
212
+ @AsyncStatus.wrap
213
+ async def get_lowerlimit(self) -> float:
214
+ return self.lowerlimit
@@ -0,0 +1,79 @@
1
+ from ophyd_async.core import (
2
+ AsyncStatus,
3
+ StandardReadableFormat,
4
+ StrictEnum,
5
+ set_and_wait_for_other_value,
6
+ )
7
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
8
+
9
+ from dodal.devices.current_amplifiers import (
10
+ CurrentAmpCounter,
11
+ )
12
+
13
+
14
+ class CountMode(StrictEnum):
15
+ AUTO = "AutoCount"
16
+ ONE_SHOT = "OneShot"
17
+
18
+
19
+ class CountState(StrictEnum):
20
+ DONE = "Done"
21
+ COUNT = "Count" # type: ignore
22
+
23
+
24
+ COUNT_PER_VOLTAGE = 100000
25
+
26
+
27
+ class StruckScaler(CurrentAmpCounter):
28
+ """
29
+ StruckScaler is a counting card that record the output signal from a wide
30
+ range of detectors. This class contains the basic control to run the struckscaler
31
+ card together with a current amplifier. It has functions that provide conversion
32
+ between count and voltage.
33
+ Attributes:
34
+ readout(SignalR): Scaler card output.
35
+ count_mode (SignalR[CountMode]): Counting card setting.
36
+ count_time (SignalRW([float]): Count time.
37
+ """
38
+
39
+ def __init__(
40
+ self, prefix: str, suffix: str, count_per_volt=COUNT_PER_VOLTAGE, name: str = ""
41
+ ):
42
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
43
+ self.readout = epics_signal_r(float, prefix + suffix)
44
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
45
+ self.count_mode = epics_signal_rw(CountMode, prefix + ":AutoCount")
46
+ self.count_time = epics_signal_rw(float, prefix + ".TP")
47
+ self.trigger_start = epics_signal_rw(CountState, prefix + ".CNT")
48
+ super().__init__(count_per_volt=count_per_volt, name=name)
49
+
50
+ @AsyncStatus.wrap
51
+ async def stage(self) -> None:
52
+ await self.count_mode.set(CountMode.ONE_SHOT)
53
+
54
+ @AsyncStatus.wrap
55
+ async def unstage(self) -> None:
56
+ await self.count_mode.set(CountMode.AUTO)
57
+
58
+ @AsyncStatus.wrap
59
+ async def trigger(self) -> None:
60
+ await set_and_wait_for_other_value(
61
+ set_signal=self.trigger_start,
62
+ set_value=CountState.COUNT,
63
+ match_signal=self.trigger_start,
64
+ match_value=CountState.COUNT,
65
+ )
66
+
67
+ @AsyncStatus.wrap
68
+ async def prepare(self, value) -> None:
69
+ await self.count_time.set(value)
70
+
71
+ async def get_count(self) -> float:
72
+ await self.trigger()
73
+ return await self.readout.get_value()
74
+
75
+ async def get_count_per_sec(self) -> float:
76
+ return await self.get_count() / await self.count_time.get_value()
77
+
78
+ async def get_voltage_per_sec(self) -> float:
79
+ return await self.get_count_per_sec() / self.count_per_volt
@@ -26,6 +26,21 @@ class DetectorSizeConstants:
26
26
  ALL_DETECTORS[self.det_type_string] = self
27
27
 
28
28
 
29
+ PILATUS_TYPE_6M = "PILATUS_6M"
30
+ PILATUS_6M_DIMENSION_X = 423.636
31
+ PILATUS_6M_DIMENSION_Y = 434.644
32
+ PILATUS_6M_DIMENSION = DetectorSize(PILATUS_6M_DIMENSION_X, PILATUS_6M_DIMENSION_Y)
33
+ PIXELS_X_PILATUS_6M = 2463
34
+ PIXELS_Y_PILATUS_6M = 2527
35
+ PIXELS_PILATUS_6M = DetectorSize(PIXELS_X_PILATUS_6M, PIXELS_Y_PILATUS_6M)
36
+ PILATUS_6M_SIZE = DetectorSizeConstants(
37
+ PILATUS_TYPE_6M,
38
+ PILATUS_6M_DIMENSION,
39
+ PIXELS_PILATUS_6M,
40
+ PILATUS_6M_DIMENSION,
41
+ PIXELS_PILATUS_6M,
42
+ )
43
+
29
44
  EIGER_TYPE_EIGER2_X_4M = "EIGER2_X_4M"
30
45
  EIGER2_X_4M_DIMENSION_X = 155.1
31
46
  EIGER2_X_4M_DIMENSION_Y = 162.15
@@ -91,10 +91,10 @@ class OdinNodesStatus(Device):
91
91
  def wait_for_no_errors(self, timeout) -> dict[SubscriptionStatus, str]:
92
92
  errors = {}
93
93
  for node_number, node_pv in enumerate(self.nodes):
94
- errors[
95
- await_value(node_pv.error_status, False, timeout)
96
- ] = f"Filewriter {node_number} is in an error state with error message\
94
+ errors[await_value(node_pv.error_status, False, timeout)] = (
95
+ f"Filewriter {node_number} is in an error state with error message\
97
96
  - {node_pv.error_message.get()}"
97
+ )
98
98
 
99
99
  return errors
100
100
 
@@ -97,9 +97,14 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
97
97
  """Converts a grid position, given as steps in the x, y, z grid,
98
98
  to a real motor position.
99
99
 
100
- :param grid_position: The x, y, z position in grid steps
101
- :return: The motor position this corresponds to.
102
- :raises: IndexError if the desired position is outside the grid."""
100
+ Args:
101
+ grid_position: The x, y, z position in grid steps. The origin is at the
102
+ centre of the first grid box
103
+ Returns:
104
+ The motor position this corresponds to.
105
+ Raises:
106
+ IndexError if the desired position is outside the grid.
107
+ """
103
108
  for position, axis in zip(
104
109
  grid_position, [self.x_axis, self.y_axis, self.z_axis], strict=False
105
110
  ):
@@ -0,0 +1,85 @@
1
+ from asyncio import gather
2
+ from math import isclose
3
+
4
+ from ophyd_async.core import StandardReadable, StrictEnum
5
+ from ophyd_async.epics.motor import Motor
6
+
7
+ from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
8
+ from dodal.common.signal_utils import create_hardware_backed_soft_signal
9
+
10
+
11
+ class BeamstopPositions(StrictEnum):
12
+ """
13
+ Beamstop positions.
14
+ GDA supports Standard/High/Low resolution positions, as well as parked and
15
+ robot load however all 3 resolution positions are the same. We also
16
+ do not use the robot load position in Hyperion.
17
+
18
+ Until we support moving the beamstop it is only necessary to check whether the
19
+ beamstop is in beam or not.
20
+
21
+ See Also:
22
+ https://github.com/DiamondLightSource/mx-bluesky/issues/484
23
+
24
+ Attributes:
25
+ DATA_COLLECTION: The beamstop is in beam ready for data collection
26
+ UNKNOWN: The beamstop is in some other position, check the device motor
27
+ positions to determine it.
28
+ """
29
+
30
+ DATA_COLLECTION = "Data Collection"
31
+ UNKNOWN = "Unknown"
32
+
33
+
34
+ class Beamstop(StandardReadable):
35
+ """
36
+ Beamstop for I03.
37
+
38
+ Attributes:
39
+ x: beamstop x position in mm
40
+ y: beamstop y position in mm
41
+ z: beamstop z position in mm
42
+ selected_pos: Get the current position of the beamstop as an enum. Currently this
43
+ is read-only.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ prefix: str,
49
+ beamline_parameters: GDABeamlineParameters,
50
+ name: str = "",
51
+ ):
52
+ with self.add_children_as_readables():
53
+ self.x_mm = Motor(prefix + "X")
54
+ self.y_mm = Motor(prefix + "Y")
55
+ self.z_mm = Motor(prefix + "Z")
56
+ self.selected_pos = create_hardware_backed_soft_signal(
57
+ BeamstopPositions, self._get_selected_position
58
+ )
59
+
60
+ self._in_beam_xyz_mm = [
61
+ float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
62
+ for axis in ("x", "y", "z")
63
+ ]
64
+ self._xyz_tolerance_mm = [
65
+ float(beamline_parameters[f"bs_{axis}_tolerance"])
66
+ for axis in ("x", "y", "z")
67
+ ]
68
+
69
+ super().__init__(name)
70
+
71
+ async def _get_selected_position(self) -> BeamstopPositions:
72
+ current_pos = await gather(
73
+ self.x_mm.user_readback.get_value(),
74
+ self.y_mm.user_readback.get_value(),
75
+ self.z_mm.user_readback.get_value(),
76
+ )
77
+ if all(
78
+ isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
79
+ for axis_pos, axis_in_beam, axis_tolerance in zip(
80
+ current_pos, self._in_beam_xyz_mm, self._xyz_tolerance_mm, strict=False
81
+ )
82
+ ):
83
+ return BeamstopPositions.DATA_COLLECTION
84
+ else:
85
+ return BeamstopPositions.UNKNOWN
@@ -1,14 +1,18 @@
1
+ import asyncio
1
2
  import math
2
- from time import sleep, time
3
3
 
4
- from ophyd import Component as Cpt
5
- from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind
6
- from ophyd.status import DeviceStatus
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ StandardReadable,
7
+ observe_value,
8
+ wait_for_value,
9
+ )
10
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
11
 
8
12
  from dodal.log import LOGGER
9
13
 
10
14
 
11
- class Transfocator(Device):
15
+ class Transfocator(StandardReadable):
12
16
  """The transfocator is a device that puts a number of lenses in the beam to change
13
17
  its shape.
14
18
 
@@ -18,34 +22,50 @@ class Transfocator(Device):
18
22
  my_transfocator.set(vert_beamsize_microns)
19
23
  """
20
24
 
21
- beamsize_set_microns = Cpt(EpicsSignal, "VERT_REQ", kind=Kind.hinted)
22
- predicted_vertical_num_lenses = Cpt(EpicsSignal, "LENS_PRED")
23
-
24
- number_filters_sp = Cpt(EpicsSignal, "NUM_FILTERS")
25
-
26
- start = Cpt(EpicsSignal, "START.PROC")
27
- start_rbv = Cpt(EpicsSignalRO, "START_RBV")
28
-
29
- vertical_lens_rbv = Cpt(EpicsSignalRO, "VER", kind=Kind.hinted)
25
+ def __init__(self, prefix: str, name: str = ""):
26
+ with self.add_children_as_readables():
27
+ self.beamsize_set_microns = epics_signal_rw(float, prefix + "VERT_REQ")
28
+ self.predicted_vertical_num_lenses = epics_signal_rw(
29
+ float, prefix + "LENS_PRED"
30
+ )
31
+ self.number_filters_sp = epics_signal_rw(int, prefix + "NUM_FILTERS")
32
+ self.start = epics_signal_rw(int, prefix + "START.PROC")
33
+ self.start_rbv = epics_signal_r(int, prefix + "START_RBV")
34
+ self.vertical_lens_rbv = epics_signal_r(float, prefix + "VER")
30
35
 
31
- TIMEOUT = 120
32
- _POLLING_WAIT = 0.01
36
+ self.TIMEOUT = 120
33
37
 
34
- def polling_wait_on_start_rbv(self, for_value):
35
- # For some reason couldn't get monitors working on START_RBV
36
- # (See https://github.com/DiamondLightSource/dodal/issues/152)
37
- start_time = time()
38
- while time() < start_time + self.TIMEOUT:
39
- RBV_value = self.start_rbv.get()
40
- if RBV_value == for_value:
41
- return
42
- sleep(self._POLLING_WAIT)
38
+ super().__init__(name=name)
43
39
 
44
- # last try
45
- if self.start_rbv.get() != for_value:
46
- raise TimeoutError()
40
+ async def _observe_beamsize_microns(self):
41
+ is_set_filters_done = False
47
42
 
48
- def set(self, beamsize_microns: float) -> DeviceStatus:
43
+ async def set_based_on_prediction(value: float):
44
+ if not math.isclose(
45
+ self.latest_pred_vertical_num_lenses, value, abs_tol=1e-8
46
+ ):
47
+ # We can only put an integer number of lenses in the beam but the
48
+ # calculation in the IOC returns the theoretical float number of lenses
49
+ nonlocal is_set_filters_done
50
+ value = round(value)
51
+ LOGGER.info(f"Transfocator setting {value} filters")
52
+ await self.number_filters_sp.set(value)
53
+ await self.start.set(1)
54
+ LOGGER.info("Waiting for start_rbv to change to 1")
55
+ await wait_for_value(self.start_rbv, 1, self.TIMEOUT)
56
+ LOGGER.info("Waiting for start_rbv to change to 0")
57
+ await wait_for_value(self.start_rbv, 0, self.TIMEOUT)
58
+ self.latest_pred_vertical_num_lenses = value
59
+ is_set_filters_done = True
60
+
61
+ # The value hasn't changed so assume the device is already set up correctly
62
+ async for value in observe_value(self.predicted_vertical_num_lenses):
63
+ await set_based_on_prediction(value)
64
+ if is_set_filters_done:
65
+ break
66
+
67
+ @AsyncStatus.wrap
68
+ async def set(self, value: float):
49
69
  """To set the beamsize on the transfocator we must:
50
70
  1. Set the beamsize in the calculator part of the transfocator
51
71
  2. Get the predicted number of lenses needed from this calculator
@@ -53,30 +73,24 @@ class Transfocator(Device):
53
73
  4. Start the device moving
54
74
  5. Wait for the start_rbv goes high and low again
55
75
  """
56
- subscriber: int
57
- status = DeviceStatus(self, timeout=self.TIMEOUT)
76
+ self.latest_pred_vertical_num_lenses = (
77
+ await self.predicted_vertical_num_lenses.get_value()
78
+ )
58
79
 
59
- def set_based_on_predicition(old_value, value, *args, **kwargs):
60
- if not math.isclose(old_value, value, abs_tol=1e-8):
61
- self.predicted_vertical_num_lenses.unsubscribe(subscriber)
80
+ LOGGER.info(f"Transfocator setting {value} beamsize")
62
81
 
63
- # We can only put an integer number of lenses in the beam but the
64
- # calculation in the IOC returns the theoretical float number of lenses
65
- value = round(value)
66
- LOGGER.info(f"Transfocator setting {value} filters")
67
- self.number_filters_sp.set(value).wait()
68
- self.start.set(1).wait()
69
- self.polling_wait_on_start_rbv(1)
70
- self.polling_wait_on_start_rbv(0)
71
- # The value hasn't changed so assume the device is already set up correctly
72
- status.set_finished()
73
-
74
- LOGGER.info(f"Transfocator setting {beamsize_microns} beamsize")
75
- if self.beamsize_set_microns.get() != beamsize_microns:
76
- subscriber = self.predicted_vertical_num_lenses.subscribe(
77
- set_based_on_predicition, run=False
82
+ if await self.beamsize_set_microns.get_value() != value:
83
+ # Logic in the IOC calculates predicted_vertical_num_lenses when beam_set_microns changes
84
+ await asyncio.gather(
85
+ self.beamsize_set_microns.set(value),
86
+ self._observe_beamsize_microns(),
78
87
  )
79
- self.beamsize_set_microns.set(beamsize_microns)
80
- else:
81
- status.set_finished()
82
- return status
88
+
89
+ number_filters_rbv, vertical_lens_size_rbv = await asyncio.gather(
90
+ self.number_filters_sp.get_value(),
91
+ self.vertical_lens_rbv.get_value(),
92
+ )
93
+
94
+ LOGGER.info(
95
+ f"Transfocator set complete. Number of filters is: {number_filters_rbv} and Vertical beam size is: {vertical_lens_size_rbv}"
96
+ )
@@ -0,0 +1,72 @@
1
+ from ophyd_async.core import Device
2
+
3
+ from dodal.devices.current_amplifiers import (
4
+ SR570,
5
+ Femto3xxGainTable,
6
+ Femto3xxGainToCurrentTable,
7
+ Femto3xxRaiseTime,
8
+ FemtoDDPCA,
9
+ SR570FineGainTable,
10
+ SR570FullGainTable,
11
+ SR570GainTable,
12
+ SR570GainToCurrentTable,
13
+ SR570RaiseTimeTable,
14
+ )
15
+
16
+
17
+ class RasorFemto(Device):
18
+ def __init__(self, prefix: str, suffix: str = "GAIN", name: str = "") -> None:
19
+ self.ca1 = FemtoDDPCA(
20
+ prefix + "-01:",
21
+ suffix=suffix,
22
+ gain_table=Femto3xxGainTable,
23
+ gain_to_current_table=Femto3xxGainToCurrentTable,
24
+ raise_timetable=Femto3xxRaiseTime,
25
+ )
26
+ self.ca2 = FemtoDDPCA(
27
+ prefix + "-02:",
28
+ suffix=suffix,
29
+ gain_table=Femto3xxGainTable,
30
+ gain_to_current_table=Femto3xxGainToCurrentTable,
31
+ raise_timetable=Femto3xxRaiseTime,
32
+ )
33
+ self.ca3 = FemtoDDPCA(
34
+ prefix + "-03:",
35
+ suffix=suffix,
36
+ gain_table=Femto3xxGainTable,
37
+ gain_to_current_table=Femto3xxGainToCurrentTable,
38
+ raise_timetable=Femto3xxRaiseTime,
39
+ )
40
+ super().__init__(name)
41
+
42
+
43
+ class RasorSR570(Device):
44
+ def __init__(self, prefix: str, suffix: str = "SENS:SEL", name: str = "") -> None:
45
+ self.ca1 = SR570(
46
+ prefix + "-04:",
47
+ suffix=suffix,
48
+ fine_gain_table=SR570FineGainTable,
49
+ coarse_gain_table=SR570GainTable,
50
+ combined_table=SR570FullGainTable,
51
+ gain_to_current_table=SR570GainToCurrentTable,
52
+ raise_timetable=SR570RaiseTimeTable,
53
+ )
54
+ self.ca2 = SR570(
55
+ prefix + "-05:",
56
+ suffix=suffix,
57
+ fine_gain_table=SR570FineGainTable,
58
+ coarse_gain_table=SR570GainTable,
59
+ combined_table=SR570FullGainTable,
60
+ gain_to_current_table=SR570GainToCurrentTable,
61
+ raise_timetable=SR570RaiseTimeTable,
62
+ )
63
+ self.ca3 = SR570(
64
+ prefix + "-06:",
65
+ suffix=suffix,
66
+ fine_gain_table=SR570FineGainTable,
67
+ coarse_gain_table=SR570GainTable,
68
+ combined_table=SR570FullGainTable,
69
+ gain_to_current_table=SR570GainToCurrentTable,
70
+ raise_timetable=SR570RaiseTimeTable,
71
+ )
72
+ super().__init__(name)