dls-dodal 1.36.2__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 (54) hide show
  1. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/METADATA +4 -4
  2. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/RECORD +54 -38
  3. {dls_dodal-1.36.2.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 +179 -7
  9. dodal/beamlines/i22.py +15 -0
  10. dodal/beamlines/i24.py +2 -2
  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/apple2_undulator.py +9 -9
  16. dodal/devices/{attenuator.py → attenuator/attenuator.py} +29 -1
  17. dodal/devices/attenuator/filter.py +11 -0
  18. dodal/devices/attenuator/filter_selections.py +72 -0
  19. dodal/devices/bimorph_mirror.py +151 -0
  20. dodal/devices/current_amplifiers/__init__.py +34 -0
  21. dodal/devices/current_amplifiers/current_amplifier.py +103 -0
  22. dodal/devices/current_amplifiers/current_amplifier_detector.py +109 -0
  23. dodal/devices/current_amplifiers/femto.py +143 -0
  24. dodal/devices/current_amplifiers/sr570.py +214 -0
  25. dodal/devices/current_amplifiers/struck_scaler_counter.py +79 -0
  26. dodal/devices/detector/det_dim_constants.py +15 -0
  27. dodal/devices/eiger_odin.py +3 -3
  28. dodal/devices/fast_grid_scan.py +8 -3
  29. dodal/devices/i03/beamstop.py +85 -0
  30. dodal/devices/i04/transfocator.py +67 -53
  31. dodal/devices/i10/i10_setting_data.py +3 -3
  32. dodal/devices/i10/mirrors.py +24 -0
  33. dodal/devices/i10/rasor/rasor_current_amp.py +72 -0
  34. dodal/devices/i10/rasor/rasor_motors.py +62 -0
  35. dodal/devices/i10/rasor/rasor_scaler_cards.py +12 -0
  36. dodal/devices/i10/slits.py +37 -0
  37. dodal/devices/i24/dual_backlight.py +1 -0
  38. dodal/devices/i24/focus_mirrors.py +12 -12
  39. dodal/devices/linkam3.py +2 -2
  40. dodal/devices/p99/sample_stage.py +2 -28
  41. dodal/devices/robot.py +2 -2
  42. dodal/devices/slits.py +29 -7
  43. dodal/devices/tetramm.py +16 -16
  44. dodal/devices/undulator_dcm.py +9 -11
  45. dodal/devices/util/test_utils.py +2 -2
  46. dodal/devices/xspress3/xspress3.py +3 -3
  47. dodal/devices/zebra.py +19 -14
  48. dodal/devices/zocalo/zocalo_interaction.py +2 -1
  49. dodal/devices/zocalo/zocalo_results.py +22 -2
  50. dodal/log.py +2 -2
  51. dodal/plans/wrapped.py +3 -3
  52. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/LICENSE +0 -0
  53. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/entry_points.txt +0 -0
  54. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/p99.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from dodal.common.beamlines.beamline_utils import device_factory, set_beamline
2
+ from dodal.devices.attenuator.filter import FilterMotor
3
+ from dodal.devices.attenuator.filter_selections import P99FilterSelections
2
4
  from dodal.devices.motors import XYZPositioner
3
- from dodal.devices.p99.sample_stage import FilterMotor, SampleAngleStage
5
+ from dodal.devices.p99.sample_stage import SampleAngleStage
4
6
  from dodal.log import set_beamline as set_log_beamline
5
7
  from dodal.utils import BeamlinePrefix, get_beamline_name
6
8
 
@@ -17,7 +19,9 @@ def angle_stage() -> SampleAngleStage:
17
19
 
18
20
  @device_factory()
19
21
  def filter() -> FilterMotor:
20
- return FilterMotor(f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:SELECT")
22
+ return FilterMotor(
23
+ f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:SELECT", P99FilterSelections
24
+ )
21
25
 
22
26
 
23
27
  @device_factory()
@@ -55,7 +55,7 @@ def make_crystal_metadata_from_material(
55
55
  d_spacing = d_spacing_param or CrystalMetadata.calculate_default_d_spacing(
56
56
  material.value.lattice_parameter, reflection_plane
57
57
  )
58
- assert all(
59
- isinstance(i, int) and i > 0 for i in reflection_plane
60
- ), "Reflection plane indices must be positive integers"
58
+ assert all(isinstance(i, int) and i > 0 for i in reflection_plane), (
59
+ "Reflection plane indices must be positive integers"
60
+ )
61
61
  return CrystalMetadata(usage, material.value.name, reflection_plane, d_spacing)
@@ -46,7 +46,9 @@ class PandASubpathProvider(UpdatingPathProvider):
46
46
  self._filename_provider.suffix = suffix
47
47
 
48
48
  def __call__(self, device_name: str | None = None) -> PathInfo:
49
- assert self._output_directory, "Directory unknown for PandA to write into, update() needs to be called at least once"
49
+ assert self._output_directory, (
50
+ "Directory unknown for PandA to write into, update() needs to be called at least once"
51
+ )
50
52
  return PathInfo(
51
53
  directory_path=self._output_directory,
52
54
  filename=self._filename_provider(device_name),
@@ -87,6 +87,9 @@ class ApertureValue(StrictEnum):
87
87
  MEDIUM = "MEDIUM_APERTURE"
88
88
  LARGE = "LARGE_APERTURE"
89
89
 
90
+ def __str__(self):
91
+ return self.name.capitalize()
92
+
90
93
 
91
94
  def load_positions_from_beamline_parameters(
92
95
  params: GDABeamlineParameters,
@@ -21,8 +21,8 @@ from dodal.log import LOGGER
21
21
 
22
22
 
23
23
  class UndulatorGateStatus(StrictEnum):
24
- open = "Open"
25
- close = "Closed"
24
+ OPEN = "Open"
25
+ CLOSE = "Closed"
26
26
 
27
27
 
28
28
  @dataclass
@@ -146,7 +146,7 @@ class UndulatorGap(StandardReadable, Movable):
146
146
  timeout = await self._cal_timeout()
147
147
  LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
148
148
  await self.set_move.set(value=1, timeout=timeout)
149
- await wait_for_value(self.gate, UndulatorGateStatus.close, timeout=timeout)
149
+ await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
150
150
 
151
151
  async def _cal_timeout(self) -> float:
152
152
  vel = await self.velocity.get_value()
@@ -157,7 +157,7 @@ class UndulatorGap(StandardReadable, Movable):
157
157
  async def check_id_status(self) -> None:
158
158
  if await self.fault.get_value() != 0:
159
159
  raise RuntimeError(f"{self.name} is in fault state")
160
- if await self.gate.get_value() == UndulatorGateStatus.open:
160
+ if await self.gate.get_value() == UndulatorGateStatus.OPEN:
161
161
  raise RuntimeError(f"{self.name} is already in motion.")
162
162
 
163
163
  async def get_timeout(self) -> float:
@@ -251,7 +251,7 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
251
251
  )
252
252
  timeout = await self._cal_timeout()
253
253
  await self.set_move.set(value=1, timeout=timeout)
254
- await wait_for_value(self.gate, UndulatorGateStatus.close, timeout=timeout)
254
+ await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
255
255
 
256
256
  async def _cal_timeout(self) -> float:
257
257
  """
@@ -283,7 +283,7 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
283
283
  async def check_id_status(self) -> None:
284
284
  if await self.fault.get_value() != 0:
285
285
  raise RuntimeError(f"{self.name} is in fault state")
286
- if await self.gate.get_value() == UndulatorGateStatus.open:
286
+ if await self.gate.get_value() == UndulatorGateStatus.OPEN:
287
287
  raise RuntimeError(f"{self.name} is already in motion.")
288
288
 
289
289
  async def get_timeout(self) -> float:
@@ -325,7 +325,7 @@ class UndulatorJawPhase(StandardReadable, Movable):
325
325
  )
326
326
  timeout = await self._cal_timeout()
327
327
  await self.set_move.set(value=1, timeout=timeout)
328
- await wait_for_value(self.gate, UndulatorGateStatus.close, timeout=timeout)
328
+ await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
329
329
 
330
330
  async def _cal_timeout(self) -> float:
331
331
  """
@@ -345,7 +345,7 @@ class UndulatorJawPhase(StandardReadable, Movable):
345
345
  async def check_id_status(self) -> None:
346
346
  if await self.fault.get_value() != 0:
347
347
  raise RuntimeError(f"{self.name} is in fault state")
348
- if await self.gate.get_value() == UndulatorGateStatus.open:
348
+ if await self.gate.get_value() == UndulatorGateStatus.OPEN:
349
349
  raise RuntimeError(f"{self.name} is already in motion.")
350
350
 
351
351
  async def get_timeout(self) -> float:
@@ -458,7 +458,7 @@ class Apple2(StandardReadable, Movable):
458
458
  self.phase().set_move.set(value=1, timeout=timeout),
459
459
  )
460
460
  await wait_for_value(
461
- self.gap().gate, UndulatorGateStatus.close, timeout=timeout
461
+ self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
462
462
  )
463
463
  self._energy_set(energy) # Update energy for after move for readback.
464
464
 
@@ -7,10 +7,12 @@ from ophyd_async.core import (
7
7
  DeviceVector,
8
8
  SignalR,
9
9
  StandardReadable,
10
+ SubsetEnum,
10
11
  wait_for_value,
11
12
  )
12
13
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
13
14
 
15
+ from dodal.devices.attenuator.filter import FilterMotor
14
16
  from dodal.log import LOGGER
15
17
 
16
18
 
@@ -27,8 +29,9 @@ class ReadOnlyAttenuator(StandardReadable):
27
29
  super().__init__(name)
28
30
 
29
31
 
30
- class Attenuator(ReadOnlyAttenuator, Movable):
32
+ class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable):
31
33
  """The attenuator will insert filters into the beam to reduce its transmission.
34
+ In this attenuator, each filter can be in one of two states: IN or OUT
32
35
 
33
36
  This device should be set with:
34
37
  yield from bps.set(attenuator, desired_transmission)
@@ -83,3 +86,28 @@ class Attenuator(ReadOnlyAttenuator, Movable):
83
86
  for i in range(16)
84
87
  ]
85
88
  )
89
+
90
+
91
+ class EnumFilterAttenuator(ReadOnlyAttenuator):
92
+ """The attenuator will insert filters into the beam to reduce its transmission.
93
+
94
+ This device is currently working, but feature incomplete. See https://github.com/DiamondLightSource/dodal/issues/972
95
+
96
+ In this attenuator, the state of a filter corresponds to the selected material,
97
+ e.g Ag50, in contrast to being either 'IN' or 'OUT'; see BinaryFilterAttenuator.
98
+ """
99
+
100
+ def __init__(
101
+ self,
102
+ prefix: str,
103
+ filter_selection: tuple[type[SubsetEnum], ...],
104
+ name: str = "",
105
+ ):
106
+ with self.add_children_as_readables():
107
+ self.filters: DeviceVector[FilterMotor] = DeviceVector(
108
+ {
109
+ index: FilterMotor(f"{prefix}MP{index + 1}:", filter, name)
110
+ for index, filter in enumerate(filter_selection)
111
+ }
112
+ )
113
+ super().__init__(prefix, name=name)
@@ -0,0 +1,11 @@
1
+ from ophyd_async.core import StandardReadable, SubsetEnum
2
+ from ophyd_async.epics.core import epics_signal_rw
3
+
4
+
5
+ class FilterMotor(StandardReadable):
6
+ def __init__(
7
+ self, prefix: str, filter_selections: type[SubsetEnum], name: str = ""
8
+ ):
9
+ with self.add_children_as_readables():
10
+ self.user_setpoint = epics_signal_rw(filter_selections, f"{prefix}SELECT")
11
+ super().__init__(name=name)
@@ -0,0 +1,72 @@
1
+ from ophyd_async.core import SubsetEnum
2
+
3
+
4
+ class P99FilterSelections(SubsetEnum):
5
+ EMPTY = "Empty"
6
+ MN5UM = "Mn 5um"
7
+ FE = "Fe (empty)"
8
+ CO5UM = "Co 5um"
9
+ NI5UM = "Ni 5um"
10
+ CU5UM = "Cu 5um"
11
+ ZN5UM = "Zn 5um"
12
+ ZR = "Zr (empty)"
13
+ MO = "Mo (empty)"
14
+ RH = "Rh (empty)"
15
+ PD = "Pd (empty)"
16
+ AG = "Ag (empty)"
17
+ CD25UM = "Cd 25um"
18
+ W = "W (empty)"
19
+ PT = "Pt (empty)"
20
+ USER = "User"
21
+
22
+
23
+ class I02_1FilterOneSelections(SubsetEnum):
24
+ EMPTY = "Empty"
25
+ AL8 = "Al8"
26
+ AL15 = "Al15"
27
+ AL25 = "Al25"
28
+ AL1000 = "Al1000"
29
+ TI50 = "Ti50"
30
+ TI100 = "Ti100"
31
+ TI200 = "Ti200"
32
+ TI400 = "Ti400"
33
+ TWO_TIMES_TI500 = "2xTi500"
34
+
35
+
36
+ class I02_1FilterTwoSelections(SubsetEnum):
37
+ EMPTY = "Empty"
38
+ AL50 = "Al50"
39
+ AL100 = "Al100"
40
+ AL125 = "Al125"
41
+ AL250 = "Al250"
42
+ AL500 = "Al500"
43
+ AL1000 = "Al1000"
44
+ TI50 = "Ti50"
45
+ TI100 = "Ti100"
46
+ TWO_TIMES_TI500 = "2xTi500"
47
+
48
+
49
+ class I02_1FilterThreeSelections(SubsetEnum):
50
+ EMPTY = "Empty"
51
+ AL15 = "Al15"
52
+ AL25 = "Al25"
53
+ AL50 = "Al50"
54
+ AL100 = "Al100"
55
+ AL250 = "Al250"
56
+ AL1000 = "Al1000"
57
+ TI50 = "Ti50"
58
+ TI100 = "Ti100"
59
+ TI200 = "Ti200"
60
+
61
+
62
+ class I02_1FilterFourSelections(SubsetEnum):
63
+ EMPTY = "Empty"
64
+ AL15 = "Al15"
65
+ AL25 = "Al25"
66
+ AL50 = "Al50"
67
+ AL100 = "Al100"
68
+ AL250 = "Al250"
69
+ AL500 = "Al500"
70
+ TI300 = "Ti300"
71
+ TI400 = "Ti400"
72
+ TI500 = "Ti500"
@@ -0,0 +1,151 @@
1
+ import asyncio
2
+ from collections.abc import Mapping
3
+ from typing import Annotated as A
4
+
5
+ from bluesky.protocols import Movable
6
+ from ophyd_async.core import (
7
+ DEFAULT_TIMEOUT,
8
+ AsyncStatus,
9
+ DeviceVector,
10
+ SignalR,
11
+ SignalRW,
12
+ SignalW,
13
+ StandardReadable,
14
+ StrictEnum,
15
+ wait_for_value,
16
+ )
17
+ from ophyd_async.core import StandardReadableFormat as Format
18
+ from ophyd_async.epics.core import (
19
+ EpicsDevice,
20
+ PvSuffix,
21
+ epics_signal_r,
22
+ epics_signal_w,
23
+ epics_signal_x,
24
+ )
25
+
26
+
27
+ class BimorphMirrorOnOff(StrictEnum):
28
+ ON = "ON"
29
+ OFF = "OFF"
30
+
31
+
32
+ class BimorphMirrorMode(StrictEnum):
33
+ HI = "HI"
34
+ NORMAL = "NORMAL"
35
+ FAST = "FAST"
36
+
37
+
38
+ class BimorphMirrorStatus(StrictEnum):
39
+ IDLE = "Idle"
40
+ BUSY = "Busy"
41
+ ERROR = "Error"
42
+
43
+
44
+ class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
45
+ """Collection of PVs comprising a single bimorph channel.
46
+
47
+ Attributes:
48
+ target_voltage: Float RW_RBV for target voltage, which can be set using parent mirror's all target proc
49
+ output_voltage: Float RW_RBV for current voltage on bimorph
50
+ status: BimorphMirrorOnOff readable for ON/OFF status of channel
51
+ shift: Float writeable shifting channel voltage
52
+ """
53
+
54
+ target_voltage: A[SignalRW[float], PvSuffix.rbv("VTRGT"), Format.CONFIG_SIGNAL]
55
+ output_voltage: A[SignalRW[float], PvSuffix.rbv("VOUT"), Format.HINTED_SIGNAL]
56
+ status: A[SignalR[BimorphMirrorOnOff], PvSuffix("STATUS"), Format.CONFIG_SIGNAL]
57
+ shift: A[SignalW[float], PvSuffix("SHIFT")]
58
+
59
+ @AsyncStatus.wrap
60
+ async def set(self, value: float):
61
+ """Sets channel's VOUT to given value.
62
+
63
+ Args:
64
+ value: float to set VOUT to
65
+ """
66
+ await self.output_voltage.set(value)
67
+
68
+
69
+ class BimorphMirror(StandardReadable, Movable):
70
+ """Class to represent CAENels Bimorph Mirrors.
71
+
72
+ Attributes:
73
+ channels: DeviceVector of BimorphMirrorChannel, indexed from 1, for each channel
74
+ enabled: Writeable BimorphOnOff
75
+ commit_target_voltages: Procable signal that writes values in each channel's VTRGT to VOUT
76
+ status: Readable BimorphMirrorStatus Busy/Idle status
77
+ err: Alarm status"""
78
+
79
+ def __init__(self, prefix: str, number_of_channels: int, name=""):
80
+ """
81
+ Args:
82
+ prefix: str PV prefix
83
+ number_of_channels: int number of channels on bimorph mirror (can be zero)
84
+ name: str name of device
85
+
86
+ Raises:
87
+ ValueError: number_of_channels is less than zero"""
88
+
89
+ if number_of_channels < 0:
90
+ raise ValueError(f"Number of channels is below zero: {number_of_channels}")
91
+
92
+ with self.add_children_as_readables():
93
+ self.channels = DeviceVector(
94
+ {
95
+ i: BimorphMirrorChannel(f"{prefix}C{i}:")
96
+ for i in range(1, number_of_channels + 1)
97
+ }
98
+ )
99
+ self.enabled = epics_signal_w(BimorphMirrorOnOff, f"{prefix}ONOFF")
100
+ self.commit_target_voltages = epics_signal_x(f"{prefix}ALLTRGT.PROC")
101
+ self.status = epics_signal_r(BimorphMirrorStatus, f"{prefix}STATUS")
102
+ self.err = epics_signal_r(str, f"{prefix}ERR")
103
+ super().__init__(name=name)
104
+
105
+ @AsyncStatus.wrap
106
+ async def set(self, value: Mapping[int, float], tolerance: float = 0.0001) -> None:
107
+ """Sets bimorph voltages in parrallel via target voltage and all proc.
108
+
109
+ Args:
110
+ value: Dict of channel numbers to target voltages
111
+
112
+ Raises:
113
+ ValueError: On set to non-existent channel"""
114
+
115
+ if any(key not in self.channels for key in value):
116
+ raise ValueError(
117
+ f"Attempting to put to non-existent channels: {[key for key in value if (key not in self.channels)]}"
118
+ )
119
+
120
+ # Write target voltages:
121
+ await asyncio.gather(
122
+ *[
123
+ self.channels[i].target_voltage.set(target, wait=True)
124
+ for i, target in value.items()
125
+ ]
126
+ )
127
+
128
+ # Trigger set target voltages:
129
+ await self.commit_target_voltages.trigger()
130
+
131
+ # Wait for values to propogate to voltage out rbv:
132
+ await asyncio.gather(
133
+ *[
134
+ wait_for_value(
135
+ self.channels[i].output_voltage,
136
+ tolerance_func_builder(tolerance, target),
137
+ timeout=DEFAULT_TIMEOUT,
138
+ )
139
+ for i, target in value.items()
140
+ ],
141
+ wait_for_value(
142
+ self.status, BimorphMirrorStatus.IDLE, timeout=DEFAULT_TIMEOUT
143
+ ),
144
+ )
145
+
146
+
147
+ def tolerance_func_builder(tolerance: float, target_value: float):
148
+ def is_within_value(x):
149
+ return abs(x - target_value) <= tolerance
150
+
151
+ return is_within_value
@@ -0,0 +1,34 @@
1
+ from .current_amplifier import CurrentAmp
2
+ from .current_amplifier_detector import CurrentAmpCounter, CurrentAmpDet
3
+ from .femto import (
4
+ Femto3xxGainTable,
5
+ Femto3xxGainToCurrentTable,
6
+ Femto3xxRaiseTime,
7
+ FemtoDDPCA,
8
+ )
9
+ from .sr570 import (
10
+ SR570,
11
+ SR570FineGainTable,
12
+ SR570FullGainTable,
13
+ SR570GainTable,
14
+ SR570GainToCurrentTable,
15
+ SR570RaiseTimeTable,
16
+ )
17
+ from .struck_scaler_counter import StruckScaler
18
+
19
+ __all__ = [
20
+ "FemtoDDPCA",
21
+ "Femto3xxGainTable",
22
+ "Femto3xxRaiseTime",
23
+ "CurrentAmp",
24
+ "Femto3xxGainToCurrentTable",
25
+ "CurrentAmpCounter",
26
+ "CurrentAmpDet",
27
+ "SR570",
28
+ "SR570GainTable",
29
+ "SR570FineGainTable",
30
+ "SR570FullGainTable",
31
+ "SR570GainToCurrentTable",
32
+ "SR570RaiseTimeTable",
33
+ "StruckScaler",
34
+ ]
@@ -0,0 +1,103 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+
4
+ from bluesky.protocols import (
5
+ Movable,
6
+ Preparable,
7
+ )
8
+ from ophyd_async.core import AsyncStatus, StandardReadable
9
+
10
+
11
+ class CurrentAmp(ABC, StandardReadable, Movable):
12
+ """
13
+ Base class for current amplifier, it contains the minimal functionality
14
+ a current amplifier needs:
15
+
16
+ Attributes:
17
+ gain_conversion_table (Enum): The conversion table between current
18
+ and gain setting.
19
+ """
20
+
21
+ def __init__(self, gain_conversion_table: type[Enum], name: str = "") -> None:
22
+ self.gain_conversion_table = gain_conversion_table
23
+ super().__init__(name)
24
+
25
+ @abstractmethod
26
+ @AsyncStatus.wrap
27
+ async def increase_gain(self, value: int = 1) -> None:
28
+ """Increase gain, increment by 1 by default.
29
+
30
+ Returns:
31
+ bool: True if success.
32
+ """
33
+
34
+ @AsyncStatus.wrap
35
+ @abstractmethod
36
+ async def decrease_gain(self, value: int = 1) -> None:
37
+ """Decrease gain, decrement by 1 by default.
38
+
39
+ Returns:
40
+ bool: True if success.
41
+ """
42
+
43
+ @AsyncStatus.wrap
44
+ @abstractmethod
45
+ async def get_gain(self) -> type[Enum]:
46
+ """Get the current gain setting
47
+
48
+ Returns:
49
+ Enum: The member name of the current gain setting in gain_conversion_table.
50
+ """
51
+
52
+ @AsyncStatus.wrap
53
+ @abstractmethod
54
+ async def get_upperlimit(self) -> float:
55
+ """Get the upper limit of the current amplifier"""
56
+
57
+ @AsyncStatus.wrap
58
+ @abstractmethod
59
+ async def get_lowerlimit(self) -> float:
60
+ """Get the lower limit of the current amplifier"""
61
+
62
+
63
+ class CurrentAmpCounter(ABC, StandardReadable, Preparable):
64
+ """
65
+ Base class for current amplifier counter, it contain the minimal implementations
66
+ required for a counter/detector to function with CurrentAmpDet:
67
+
68
+ Attributes:
69
+ count_per_volt (float): The conversion factor between counter output and voltage.
70
+ """
71
+
72
+ def __init__(self, count_per_volt: float, name: str = ""):
73
+ self.count_per_volt = count_per_volt
74
+ super().__init__(name)
75
+
76
+ @abstractmethod
77
+ async def get_count(self) -> float:
78
+ """ "Get count
79
+
80
+ Returns:
81
+ float: Current count
82
+ """
83
+
84
+ @abstractmethod
85
+ async def get_count_per_sec(self) -> float:
86
+ """Get count per second
87
+
88
+ Returns:
89
+ float: Current count per second
90
+ """
91
+
92
+ @abstractmethod
93
+ async def get_voltage_per_sec(self) -> float:
94
+ """Get count per second in voltage
95
+
96
+ Returns:
97
+ float: Current count in volt per second
98
+ """
99
+
100
+ @abstractmethod
101
+ @AsyncStatus.wrap
102
+ async def prepare(self, value: float) -> None:
103
+ """Prepare method for setting up the counter"""
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+
3
+ from bluesky.protocols import Preparable, Reading
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ Reference,
7
+ StandardReadable,
8
+ StandardReadableFormat,
9
+ soft_signal_r_and_setter,
10
+ soft_signal_rw,
11
+ )
12
+
13
+ from dodal.devices.current_amplifiers.current_amplifier import (
14
+ CurrentAmp,
15
+ CurrentAmpCounter,
16
+ )
17
+ from dodal.log import LOGGER
18
+
19
+
20
+ class CurrentAmpDet(StandardReadable, Preparable):
21
+ """
22
+ CurrentAmpDet composed of a CurrentAmp and a CurrentAmpCounter. It provides
23
+ the option for automatically changing the CurrentAmp gain to within the optimal
24
+ range. It also converts the currentAmp/counter output back into the detector
25
+ current output in Amp.
26
+ Attributes:
27
+ current_amp (currentAmp): Current amplifier type device.
28
+ counter (CurrentAmpCounter): Counter that capture the current amplifier output.
29
+ current (SignalRW([float]): Soft signal to store the corrected current.
30
+ auto_mode (signalR([bool])): Soft signal to store the flag for auto gain.
31
+ name (str): Name of the device.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ current_amp: CurrentAmp,
37
+ counter: CurrentAmpCounter,
38
+ name: str = "",
39
+ ) -> None:
40
+ self.current_amp = Reference(current_amp)
41
+ self.counter = Reference(counter)
42
+ with self.add_children_as_readables():
43
+ self.current, self._set_current = soft_signal_r_and_setter(
44
+ float, initial_value=None, units="Amp"
45
+ )
46
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
47
+ self.auto_mode = soft_signal_rw(bool, initial_value=True)
48
+ super().__init__(name)
49
+
50
+ async def read(self) -> dict[str, Reading]:
51
+ """
52
+ Read is modified so that if auto_mode is true it will optimise gain before
53
+ taking the final reading
54
+ """
55
+ if await self.auto_mode.get_value():
56
+ LOGGER.info(f"{self.name}-Attempting auto-gain")
57
+ status = self.auto_gain()
58
+ try:
59
+ await status
60
+ LOGGER.info(
61
+ f"{self.name} new gain = {await self.current_amp().get_gain()}."
62
+ )
63
+ except ValueError as ex:
64
+ LOGGER.warning(f"{self.name} gain went outside limits")
65
+ # Further details are provided to the user in the logged exception
66
+ LOGGER.exception(ex)
67
+ current = await self.get_corrected_current()
68
+ self._set_current(current)
69
+ return await super().read()
70
+
71
+ @AsyncStatus.wrap
72
+ async def auto_gain(self) -> None:
73
+ within_limits = False
74
+ while not within_limits:
75
+ reading = abs(await self.counter().get_voltage_per_sec())
76
+ upper_limit, lower_limit = await asyncio.gather(
77
+ self.current_amp().get_upperlimit(),
78
+ self.current_amp().get_lowerlimit(),
79
+ )
80
+ if reading > upper_limit:
81
+ await self.current_amp().decrease_gain()
82
+ elif reading < lower_limit:
83
+ await self.current_amp().increase_gain()
84
+ else:
85
+ within_limits = True
86
+
87
+ async def get_corrected_current(self) -> float:
88
+ """
89
+ Convert the output(count and gain) back into the read detector output in Amp.
90
+ """
91
+ current_gain, voltage_per_sec = await asyncio.gather(
92
+ self.current_amp().get_gain(),
93
+ self.counter().get_voltage_per_sec(),
94
+ )
95
+ correction_factor = current_gain.value
96
+ corrected_current = voltage_per_sec / correction_factor
97
+ return corrected_current
98
+
99
+ @AsyncStatus.wrap
100
+ async def stage(self) -> None:
101
+ await self.counter().stage()
102
+
103
+ @AsyncStatus.wrap
104
+ async def unstage(self) -> None:
105
+ await self.counter().unstage()
106
+
107
+ @AsyncStatus.wrap
108
+ async def prepare(self, value) -> None:
109
+ await self.counter().prepare(value=value)