dls-dodal 1.43.0__py3-none-any.whl → 1.45.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 (70) hide show
  1. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/METADATA +4 -3
  2. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/RECORD +66 -49
  3. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +2 -0
  6. dodal/beamlines/b01_1.py +8 -0
  7. dodal/beamlines/b07.py +27 -0
  8. dodal/beamlines/b07_1.py +25 -0
  9. dodal/beamlines/i03.py +11 -0
  10. dodal/beamlines/i09.py +25 -0
  11. dodal/beamlines/i09_1.py +25 -0
  12. dodal/beamlines/i10.py +19 -35
  13. dodal/beamlines/i13_1.py +22 -48
  14. dodal/beamlines/i19_1.py +17 -5
  15. dodal/beamlines/i19_2.py +13 -3
  16. dodal/beamlines/i19_optics.py +4 -2
  17. dodal/beamlines/i20_1.py +2 -1
  18. dodal/beamlines/i23.py +10 -0
  19. dodal/beamlines/p60.py +21 -0
  20. dodal/common/data_util.py +20 -0
  21. dodal/common/signal_utils.py +43 -4
  22. dodal/common/visit.py +1 -41
  23. dodal/devices/aperturescatterguard.py +3 -3
  24. dodal/devices/baton.py +17 -0
  25. dodal/devices/current_amplifiers/current_amplifier.py +1 -6
  26. dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
  27. dodal/devices/current_amplifiers/femto.py +0 -5
  28. dodal/devices/current_amplifiers/sr570.py +0 -5
  29. dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
  30. dodal/devices/detector/detector.py +2 -1
  31. dodal/devices/electron_analyser/__init__.py +0 -0
  32. dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
  33. dodal/devices/electron_analyser/abstract_region.py +112 -0
  34. dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
  35. dodal/devices/electron_analyser/specs_region.py +26 -0
  36. dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
  37. dodal/devices/electron_analyser/vgscienta_region.py +90 -0
  38. dodal/devices/fast_grid_scan.py +2 -2
  39. dodal/devices/i03/beamstop.py +2 -2
  40. dodal/devices/i10/diagnostics.py +239 -0
  41. dodal/devices/i10/slits.py +93 -6
  42. dodal/devices/i13_1/merlin.py +1 -2
  43. dodal/devices/i13_1/merlin_controller.py +12 -8
  44. dodal/devices/i19/beamstop.py +30 -0
  45. dodal/devices/i19/blueapi_device.py +102 -0
  46. dodal/devices/i19/hutch_access.py +2 -0
  47. dodal/devices/i19/shutter.py +24 -40
  48. dodal/devices/i22/nxsas.py +1 -3
  49. dodal/devices/i24/focus_mirrors.py +3 -3
  50. dodal/devices/i24/pilatus_metadata.py +2 -2
  51. dodal/devices/motors.py +21 -0
  52. dodal/devices/oav/oav_detector.py +7 -9
  53. dodal/devices/oav/snapshots/snapshot.py +21 -0
  54. dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
  55. dodal/devices/turbo_slit.py +8 -2
  56. dodal/devices/undulator.py +9 -7
  57. dodal/devices/util/adjuster_plans.py +1 -2
  58. dodal/devices/util/lookup_tables.py +38 -0
  59. dodal/devices/util/test_utils.py +1 -0
  60. dodal/plan_stubs/electron_analyser/__init__.py +0 -0
  61. dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
  62. dodal/plan_stubs/motor_utils.py +10 -12
  63. dodal/utils.py +0 -7
  64. dodal/devices/i13_1/merlin_io.py +0 -17
  65. dodal/devices/oav/microns_for_zoom_levels.json +0 -55
  66. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
  67. dodal/devices/util/motor_utils.py +0 -6
  68. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/entry_points.txt +0 -0
  69. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info/licenses}/LICENSE +0 -0
  70. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,47 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TypeVar
3
+
4
+ from ophyd_async.core import StandardReadable
5
+ from ophyd_async.epics.core import epics_signal_rw
6
+
7
+ from dodal.devices.electron_analyser.abstract_region import EnergyMode
8
+
9
+
10
+ class AbstractAnalyserDriverIO(ABC, StandardReadable):
11
+ """
12
+ Generic device to configure electron analyser with new region settings.
13
+ Electron analysers should inherit from this class for further specialisation.
14
+ """
15
+
16
+ def __init__(self, prefix: str, name: str = "") -> None:
17
+ with self.add_children_as_readables():
18
+ self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
19
+ self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
20
+ self.slices = epics_signal_rw(int, prefix + "SLICES")
21
+ self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
22
+ self.pass_energy = epics_signal_rw(
23
+ self.pass_energy_type, prefix + "PASS_ENERGY"
24
+ )
25
+ self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
26
+ self.iterations = epics_signal_rw(int, prefix + "NumExposures")
27
+ self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE")
28
+
29
+ super().__init__(name)
30
+
31
+ def to_kinetic_energy(
32
+ self, value: float, excitation_energy: float, mode: EnergyMode
33
+ ) -> float:
34
+ return excitation_energy - value if mode == EnergyMode.BINDING else value
35
+
36
+ @property
37
+ @abstractmethod
38
+ def pass_energy_type(self) -> type:
39
+ """
40
+ Return the type the pass_energy should be. Each one is unfortunately different
41
+ for the underlying analyser software and cannot be changed on epics side.
42
+ """
43
+
44
+
45
+ TAbstractAnalyserDriverIO = TypeVar(
46
+ "TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
47
+ )
@@ -0,0 +1,112 @@
1
+ import re
2
+ from abc import ABC
3
+ from collections.abc import Callable
4
+ from enum import Enum
5
+ from typing import Generic, TypeVar
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+
10
+ def java_to_python_case(java_str: str) -> str:
11
+ """
12
+ Convert a camelCase Java-style string to a snake_case Python-style string.
13
+
14
+ :param java_str: The Java-style camelCase string.
15
+ :return: The Python-style snake_case string.
16
+ """
17
+ new_value = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", java_str)
18
+ new_value = re.sub("([a-z0-9])([A-Z])", r"\1_\2", new_value).lower()
19
+ return new_value
20
+
21
+
22
+ def switch_case_validation(data: dict, f: Callable[[str], str]) -> dict:
23
+ return {f(key): value for key, value in data.items()}
24
+
25
+
26
+ class JavaToPythonModel(BaseModel):
27
+ @model_validator(mode="before")
28
+ @classmethod
29
+ def before_validation(cls, data: dict) -> dict:
30
+ data = switch_case_validation(data, java_to_python_case)
31
+ return data
32
+
33
+
34
+ def energy_mode_validation(data: dict) -> dict:
35
+ # Convert binding_energy to energy_mode to make base region more generic
36
+ if "binding_energy" in data:
37
+ is_binding_energy = data["binding_energy"]
38
+ del data["binding_energy"]
39
+ data["energy_mode"] = (
40
+ EnergyMode.BINDING if is_binding_energy else EnergyMode.KINETIC
41
+ )
42
+ return data
43
+
44
+
45
+ class EnergyMode(str, Enum):
46
+ KINETIC = "Kinetic"
47
+ BINDING = "Binding"
48
+
49
+
50
+ class AbstractBaseRegion(ABC, JavaToPythonModel):
51
+ """
52
+ Generic region model that holds the data. Specialised region models should inherit
53
+ this to extend functionality. All energy units are assumed to be in eV.
54
+ """
55
+
56
+ name: str = "New_region"
57
+ enabled: bool = False
58
+ slices: int = 1
59
+ iterations: int = 1
60
+ # These ones we need subclasses to provide default values
61
+ lens_mode: str
62
+ pass_energy: int
63
+ acquisition_mode: str
64
+ low_energy: float
65
+ high_energy: float
66
+ step_time: float
67
+ energy_step: float # in eV
68
+ energy_mode: EnergyMode = EnergyMode.KINETIC
69
+
70
+ def is_binding_energy(self) -> bool:
71
+ return self.energy_mode == EnergyMode.BINDING
72
+
73
+ def is_kinetic_energy(self) -> bool:
74
+ return self.energy_mode == EnergyMode.KINETIC
75
+
76
+ def to_kinetic_energy(self, value: float, excitation_energy: float) -> float:
77
+ return value if self.is_binding_energy() else excitation_energy - value
78
+
79
+ @model_validator(mode="before")
80
+ @classmethod
81
+ def before_validation(cls, data: dict) -> dict:
82
+ data = switch_case_validation(data, java_to_python_case)
83
+ return energy_mode_validation(data)
84
+
85
+
86
+ TAbstractBaseRegion = TypeVar("TAbstractBaseRegion", bound=AbstractBaseRegion)
87
+
88
+
89
+ class AbstractBaseSequence(ABC, JavaToPythonModel, Generic[TAbstractBaseRegion]):
90
+ """
91
+ Generic sequence model that holds the list of region data. Specialised sequence
92
+ models should inherit this to extend functionality and define type of region to
93
+ hold.
94
+ """
95
+
96
+ version: float = 0.1 # If file format changes within prod, increment this number!
97
+ regions: list[TAbstractBaseRegion] = Field(default_factory=lambda: [])
98
+
99
+ def get_enabled_regions(self) -> list[TAbstractBaseRegion]:
100
+ return [r for r in self.regions if r.enabled]
101
+
102
+ def get_region_names(self) -> list[str]:
103
+ return [r.name for r in self.regions]
104
+
105
+ def get_enabled_region_names(self) -> list[str]:
106
+ return [r.name for r in self.get_enabled_regions()]
107
+
108
+ def get_region_by_name(self, name: str) -> TAbstractBaseRegion | None:
109
+ return next((region for region in self.regions if region.name == name), None)
110
+
111
+
112
+ TAbstractBaseSequence = TypeVar("TAbstractBaseSequence", bound=AbstractBaseSequence)
@@ -0,0 +1,19 @@
1
+ from ophyd_async.epics.core import epics_signal_rw
2
+
3
+ from dodal.devices.electron_analyser.abstract_analyser_io import (
4
+ AbstractAnalyserDriverIO,
5
+ )
6
+
7
+
8
+ class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO):
9
+ def __init__(self, prefix: str, name: str = "") -> None:
10
+ with self.add_children_as_readables():
11
+ self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
12
+ self.values = epics_signal_rw(int, prefix + "VALUES")
13
+ self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
14
+
15
+ super().__init__(prefix, name)
16
+
17
+ @property
18
+ def pass_energy_type(self) -> type:
19
+ return float
@@ -0,0 +1,26 @@
1
+ from pydantic import Field
2
+
3
+ from dodal.devices.electron_analyser.abstract_region import (
4
+ AbstractBaseRegion,
5
+ AbstractBaseSequence,
6
+ )
7
+
8
+
9
+ class SpecsRegion(AbstractBaseRegion):
10
+ # Override base class with defaults
11
+ lens_mode: str = "SmallArea"
12
+ pass_energy: int = 5
13
+ acquisition_mode: str = "Fixed Transmission"
14
+ low_energy: float = Field(default=800, alias="start_energy")
15
+ high_energy: float = Field(default=850, alias="end_energy")
16
+ step_time: float = Field(default=1.0, alias="exposure_time")
17
+ energy_step: float = Field(default=0.1, alias="step_energy")
18
+ # Specific to this class
19
+ values: int = 1
20
+ centre_energy: float = 0
21
+ psu_mode: str = "1.5keV"
22
+ estimated_time_in_ms: float = 0
23
+
24
+
25
+ class SpecsSequence(AbstractBaseSequence[SpecsRegion]):
26
+ regions: list[SpecsRegion] = Field(default_factory=lambda: [])
@@ -0,0 +1,26 @@
1
+ from ophyd_async.epics.core import epics_signal_rw
2
+
3
+ from dodal.devices.electron_analyser.abstract_analyser_io import (
4
+ AbstractAnalyserDriverIO,
5
+ )
6
+ from dodal.devices.electron_analyser.vgscienta_region import (
7
+ DetectorMode,
8
+ )
9
+
10
+
11
+ class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO):
12
+ def __init__(self, prefix: str, name: str = "") -> None:
13
+ with self.add_children_as_readables():
14
+ self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
15
+ self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
16
+ self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
17
+ self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
18
+ self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
19
+ self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
20
+ self.image_mode = epics_signal_rw(str, prefix + "ImageMode")
21
+
22
+ super().__init__(prefix, name)
23
+
24
+ @property
25
+ def pass_energy_type(self) -> type:
26
+ return str
@@ -0,0 +1,90 @@
1
+ import uuid
2
+ from enum import Enum
3
+
4
+ from ophyd_async.core import StrictEnum
5
+ from pydantic import Field
6
+
7
+ from dodal.devices.electron_analyser.abstract_region import (
8
+ AbstractBaseRegion,
9
+ AbstractBaseSequence,
10
+ JavaToPythonModel,
11
+ )
12
+
13
+
14
+ class Status(str, Enum):
15
+ READY = "Ready"
16
+ RUNNING = "Running"
17
+ COMPLETED = "Completed"
18
+ INVALID = "Invalid"
19
+ ABORTED = "Aborted"
20
+
21
+
22
+ class DetectorMode(StrictEnum):
23
+ ADC = "ADC"
24
+ PULSE_COUNTING = "Pulse Counting"
25
+
26
+
27
+ class AcquisitionMode(str, Enum):
28
+ SWEPT = "Swept"
29
+ FIXED = "Fixed"
30
+
31
+
32
+ class VGScientaRegion(AbstractBaseRegion):
33
+ # Override defaults of base region class
34
+ lens_mode: str = "Angular45"
35
+ pass_energy: int = 5
36
+ acquisition_mode: str = AcquisitionMode.SWEPT
37
+ low_energy: float = 8.0
38
+ high_energy: float = 10.0
39
+ step_time: float = 1.0
40
+ energy_step: float = Field(default=200.0)
41
+ # Specific to this class
42
+ id: str = Field(default=str(uuid.uuid4()), alias="region_id")
43
+ excitation_energy_source: str = "source1"
44
+ fix_energy: float = 9.0
45
+ total_steps: float = 13.0
46
+ total_time: float = 13.0
47
+ exposure_time: float = 1.0
48
+ first_x_channel: int = 1
49
+ last_x_channel: int = 1000
50
+ first_y_channel: int = 101
51
+ last_y_channel: int = 800
52
+ detector_mode: DetectorMode = DetectorMode.ADC
53
+ status: Status = Status.READY
54
+
55
+ def x_channel_size(self) -> int:
56
+ return self.last_x_channel - self.first_x_channel + 1
57
+
58
+ def y_channel_size(self) -> int:
59
+ return self.last_y_channel - self.first_y_channel + 1
60
+
61
+
62
+ class VGScientaExcitationEnergySource(JavaToPythonModel):
63
+ name: str = "source1"
64
+ device_name: str = Field(default="", alias="scannable_name")
65
+ value: float = 0
66
+
67
+
68
+ class VGScientaSequence(AbstractBaseSequence[VGScientaRegion]):
69
+ element_set: str = Field(default="Unknown")
70
+ excitation_energy_sources: list[VGScientaExcitationEnergySource] = Field(
71
+ default_factory=lambda: []
72
+ )
73
+ regions: list[VGScientaRegion] = Field(default_factory=lambda: [])
74
+
75
+ def get_excitation_energy_source_by_region(
76
+ self, region: VGScientaRegion
77
+ ) -> VGScientaExcitationEnergySource:
78
+ value = next(
79
+ (
80
+ e
81
+ for e in self.excitation_energy_sources
82
+ if region.excitation_energy_source == e.name
83
+ ),
84
+ None,
85
+ )
86
+ if value is None:
87
+ raise ValueError(
88
+ f'Unable to find excitation energy source using region "{region.name}"'
89
+ )
90
+ return value
@@ -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
 
@@ -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
 
@@ -0,0 +1,239 @@
1
+ from bluesky.protocols import Movable
2
+ from ophyd_async.core import (
3
+ AsyncStatus,
4
+ Device,
5
+ StandardReadable,
6
+ StrictEnum,
7
+ )
8
+ from ophyd_async.core import StandardReadableFormat as Format
9
+ from ophyd_async.core._device import DeviceConnector
10
+ from ophyd_async.epics.adaravis import AravisDriverIO
11
+ from ophyd_async.epics.adcore import SingleTriggerDetector
12
+ from ophyd_async.epics.core import (
13
+ epics_signal_r,
14
+ epics_signal_rw,
15
+ )
16
+ from ophyd_async.epics.motor import Motor
17
+
18
+ from dodal.devices.current_amplifiers import (
19
+ CurrentAmpDet,
20
+ Femto3xxGainTable,
21
+ Femto3xxGainToCurrentTable,
22
+ Femto3xxRaiseTime,
23
+ FemtoDDPCA,
24
+ StruckScaler,
25
+ )
26
+
27
+
28
+ class D3Position(StrictEnum):
29
+ NOTHING = "Nothing"
30
+ GRID = "Grid"
31
+
32
+
33
+ class D5Position(StrictEnum):
34
+ CELL_IN = "Cell In"
35
+ CELL_OUT = "Cell Out"
36
+
37
+
38
+ class D5APosition(StrictEnum):
39
+ OUT_OF_THE_BEAM = "Out of the beam"
40
+ DIODE = "Diode"
41
+ BLADE = "Blade"
42
+ LA = "La ref"
43
+ GD = "Gd ref"
44
+ YB = "Yb ref"
45
+ GRID = "Grid"
46
+
47
+
48
+ class D6Position(StrictEnum):
49
+ DIODE_OUT = "Diode Out"
50
+ DIODE_IN = "Diode In"
51
+ AU_MESH = "Au Mesh"
52
+
53
+
54
+ class D7Position(StrictEnum):
55
+ OUT = "Out"
56
+ SHUTTER = "Shutter"
57
+
58
+
59
+ class InOutTable(StrictEnum):
60
+ MOVE_IN = "Move In"
61
+ MOVE_OUT = "Move Out"
62
+ RESET = "Reset"
63
+
64
+
65
+ class InOutReadBackTable(StrictEnum):
66
+ MOVE_IN = "Moving In"
67
+ MOVE_OUT = "Moving Out"
68
+ IN_BEAM = "In Beam"
69
+ FAULT = "Fault"
70
+ OUT_OF_BEAM = "Out of Beam"
71
+
72
+
73
+ class Positioner(StandardReadable, Movable):
74
+ """1D stage with a enum table to select positions."""
75
+
76
+ def __init__(
77
+ self,
78
+ prefix: str,
79
+ positioner_enum: type[StrictEnum],
80
+ positioner_suffix: str = "",
81
+ Positioner_pv_suffix: str = ":MP:SELECT",
82
+ name: str = "",
83
+ ) -> None:
84
+ self._stage_motion = Motor(prefix=prefix + positioner_suffix)
85
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
86
+ self.stage_position = epics_signal_rw(
87
+ positioner_enum,
88
+ read_pv=prefix + positioner_suffix + Positioner_pv_suffix,
89
+ )
90
+ super().__init__(name=name)
91
+ self.positioner_enum = positioner_enum
92
+
93
+ @AsyncStatus.wrap
94
+ async def set(self, value: StrictEnum) -> None:
95
+ if value in self.positioner_enum:
96
+ await self.stage_position.set(value=value)
97
+ else:
98
+ raise ValueError(
99
+ f"{value} is not an allow position. Position must be: {self.positioner_enum}"
100
+ )
101
+
102
+
103
+ class I10PneumaticStage(StandardReadable):
104
+ """Pneumatic stage only has two real positions in or out.
105
+ Use for fluorescent screen which can be insert into the x-ray beam.
106
+ Most often use in conjunction with a webcam to locate the x-ray beam."""
107
+
108
+ def __init__(
109
+ self,
110
+ prefix: str,
111
+ name: str = "",
112
+ ) -> None:
113
+ with self.add_children_as_readables(Format.HINTED_SIGNAL):
114
+ self.stage_position_set = epics_signal_rw(
115
+ InOutTable,
116
+ read_pv=prefix + "CON",
117
+ )
118
+ self.stage_position_readback = epics_signal_r(
119
+ InOutReadBackTable,
120
+ read_pv=prefix + "STA",
121
+ )
122
+ super().__init__(name=name)
123
+
124
+
125
+ class ScreenCam(Device):
126
+ """Compound device of pneumatic stage(fluorescent screen) and webcam"""
127
+
128
+ def __init__(
129
+ self,
130
+ prefix: str,
131
+ cam_infix="DCAM:",
132
+ name: str = "",
133
+ ) -> None:
134
+ self.screen_stage = I10PneumaticStage(
135
+ prefix=prefix,
136
+ )
137
+ cam_pv = prefix + cam_infix
138
+ self.centroid_x = epics_signal_r(float, read_pv=f"{cam_pv}STAT:CentroidX_RBV")
139
+ self.centroid_y = epics_signal_r(float, read_pv=f"{cam_pv}STAT:CentroidY_RBV")
140
+ self.single_trigger_centroid = SingleTriggerDetector(
141
+ drv=AravisDriverIO(prefix=cam_pv + "CAM:"),
142
+ read_uncached=[
143
+ self.centroid_x,
144
+ self.centroid_y,
145
+ ],
146
+ )
147
+ super().__init__(name=name)
148
+
149
+
150
+ class FullDiagnostic(Device):
151
+ """Compound device of a diagnostic with screen, webcam and Positioner stage."""
152
+
153
+ def __init__(
154
+ self,
155
+ prefix: str,
156
+ positioner_enum: type[StrictEnum],
157
+ positioner_suffix: str = "",
158
+ Positioner_pv_suffix: str = ":MP:SELECT",
159
+ cam_infix: str = "DCAM:",
160
+ name: str = "",
161
+ ) -> None:
162
+ self.positioner = Positioner(
163
+ prefix=prefix,
164
+ positioner_enum=positioner_enum,
165
+ positioner_suffix=positioner_suffix,
166
+ Positioner_pv_suffix=Positioner_pv_suffix,
167
+ )
168
+ self.screen = ScreenCam(
169
+ prefix,
170
+ cam_infix,
171
+ name,
172
+ )
173
+ super().__init__(name)
174
+
175
+
176
+ class I10Diagnostic(Device):
177
+ """Collection of all the diagnostic stage on i10."""
178
+
179
+ def __init__(self, prefix, name: str = "") -> None:
180
+ self.d1 = ScreenCam(prefix=prefix + "PHDGN-01:")
181
+ self.d2 = ScreenCam(prefix=prefix + "PHDGN-02:")
182
+ self.d3 = FullDiagnostic(
183
+ prefix=prefix + "PHDGN-03:",
184
+ positioner_enum=D3Position,
185
+ positioner_suffix="DET:X",
186
+ )
187
+ self.d4 = ScreenCam(prefix=prefix + "PHDGN-04:")
188
+ self.d5 = Positioner(
189
+ prefix=prefix + "IONC-01:",
190
+ positioner_enum=D5Position,
191
+ positioner_suffix="Y",
192
+ )
193
+
194
+ self.d5A = Positioner(
195
+ prefix=prefix + "PHDGN-06:",
196
+ positioner_enum=D5APosition,
197
+ positioner_suffix="DET:X",
198
+ )
199
+
200
+ self.d6 = FullDiagnostic(
201
+ prefix=prefix + "PHDGN-05:",
202
+ positioner_enum=D6Position,
203
+ positioner_suffix="DET:X",
204
+ )
205
+ self.d7 = Positioner(
206
+ prefix=prefix + "PHDGN-07:",
207
+ positioner_enum=D7Position,
208
+ positioner_suffix="Y",
209
+ )
210
+ super().__init__(name)
211
+
212
+
213
+ class I10Diagnostic5ADet(Device):
214
+ """Diagnostic 5a detection with drain current and photo diode"""
215
+
216
+ def __init__(
217
+ self, prefix: str, name: str = "", connector: DeviceConnector | None = None
218
+ ) -> None:
219
+ self.drain_current = CurrentAmpDet(
220
+ current_amp=FemtoDDPCA(
221
+ prefix=prefix + "IAMP-06:",
222
+ suffix="GAIN",
223
+ gain_table=Femto3xxGainTable,
224
+ gain_to_current_table=Femto3xxGainToCurrentTable,
225
+ raise_timetable=Femto3xxRaiseTime,
226
+ ),
227
+ counter=StruckScaler(prefix=prefix + "SCLR-02:SCALER2", suffix=".S17"),
228
+ )
229
+ self.diode = CurrentAmpDet(
230
+ FemtoDDPCA(
231
+ prefix=prefix + "IAMP-05:",
232
+ suffix="GAIN",
233
+ gain_table=Femto3xxGainTable,
234
+ gain_to_current_table=Femto3xxGainToCurrentTable,
235
+ raise_timetable=Femto3xxRaiseTime,
236
+ ),
237
+ counter=StruckScaler(prefix=prefix + "SCLR-02:SCALER2", suffix=".S18"),
238
+ )
239
+ super().__init__(name, connector)