dls-dodal 1.53.0__py3-none-any.whl → 1.55.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 (74) hide show
  1. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/METADATA +4 -6
  2. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/RECORD +74 -66
  3. dodal/_version.py +16 -3
  4. dodal/beamline_specific_utils/i05_shared.py +11 -0
  5. dodal/beamlines/__init__.py +1 -0
  6. dodal/beamlines/aithre.py +2 -3
  7. dodal/beamlines/b01_1.py +2 -2
  8. dodal/beamlines/b07.py +6 -3
  9. dodal/beamlines/b07_1.py +17 -6
  10. dodal/beamlines/b16.py +0 -1
  11. dodal/beamlines/b21.py +4 -6
  12. dodal/beamlines/i03.py +10 -31
  13. dodal/beamlines/i04.py +19 -45
  14. dodal/beamlines/i05.py +22 -0
  15. dodal/beamlines/i05_1.py +22 -0
  16. dodal/beamlines/i09.py +7 -3
  17. dodal/beamlines/i09_1.py +6 -3
  18. dodal/beamlines/i13_1.py +0 -1
  19. dodal/beamlines/i19_1.py +1 -2
  20. dodal/beamlines/i19_2.py +0 -1
  21. dodal/beamlines/i19_optics.py +1 -4
  22. dodal/beamlines/i20_1.py +38 -9
  23. dodal/beamlines/i23.py +1 -2
  24. dodal/beamlines/i24.py +0 -12
  25. dodal/beamlines/p60.py +13 -3
  26. dodal/common/beamlines/beamline_parameters.py +1 -1
  27. dodal/common/beamlines/device_helpers.py +0 -33
  28. dodal/devices/aithre_lasershaping/__init__.py +0 -0
  29. dodal/devices/aithre_lasershaping/goniometer.py +3 -26
  30. dodal/devices/aithre_lasershaping/laser_robot.py +2 -2
  31. dodal/devices/b07/__init__.py +2 -2
  32. dodal/devices/b07/enums.py +15 -0
  33. dodal/devices/b07_1/__init__.py +10 -1
  34. dodal/devices/b07_1/ccmc.py +79 -0
  35. dodal/devices/b07_1/enums.py +3 -0
  36. dodal/devices/electron_analyser/abstract/base_driver_io.py +25 -48
  37. dodal/devices/electron_analyser/abstract/base_region.py +9 -11
  38. dodal/devices/electron_analyser/abstract/types.py +12 -0
  39. dodal/devices/electron_analyser/specs/detector.py +9 -9
  40. dodal/devices/electron_analyser/specs/driver_io.py +54 -21
  41. dodal/devices/electron_analyser/specs/region.py +13 -8
  42. dodal/devices/electron_analyser/types.py +15 -6
  43. dodal/devices/electron_analyser/vgscienta/detector.py +18 -8
  44. dodal/devices/electron_analyser/vgscienta/driver_io.py +62 -24
  45. dodal/devices/electron_analyser/vgscienta/region.py +33 -16
  46. dodal/devices/focusing_mirror.py +1 -1
  47. dodal/devices/i03/undulator_dcm.py +8 -3
  48. dodal/devices/i05/__init__.py +3 -0
  49. dodal/devices/i05/enums.py +8 -0
  50. dodal/devices/i09/__init__.py +2 -2
  51. dodal/devices/i09/enums.py +16 -0
  52. dodal/devices/i09_1/__init__.py +2 -2
  53. dodal/devices/i09_1/enums.py +13 -0
  54. dodal/devices/i10/mirrors.py +2 -6
  55. dodal/devices/i13_1/merlin_controller.py +1 -1
  56. dodal/devices/i19/beamstop.py +2 -2
  57. dodal/devices/i24/aperture.py +1 -1
  58. dodal/devices/motors.py +75 -1
  59. dodal/devices/oav/oav_to_redis_forwarder.py +1 -1
  60. dodal/devices/oav/pin_image_recognition/__init__.py +1 -2
  61. dodal/devices/p60/__init__.py +8 -2
  62. dodal/devices/p60/enums.py +16 -0
  63. dodal/devices/robot.py +7 -4
  64. dodal/devices/tetramm.py +1 -2
  65. dodal/devices/webcam.py +2 -1
  66. dodal/devices/zebra/zebra.py +1 -1
  67. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  68. dodal/devices/zocalo/__init__.py +2 -0
  69. dodal/devices/zocalo/zocalo_results.py +16 -26
  70. dodal/utils.py +3 -10
  71. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/WHEEL +0 -0
  72. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/entry_points.txt +0 -0
  73. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/licenses/LICENSE +0 -0
  74. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
1
+ from typing import TypeVar
2
+
3
+ from ophyd_async.core import StrictEnum, SupersetEnum
4
+
5
+ TAcquisitionMode = TypeVar("TAcquisitionMode", bound=StrictEnum)
6
+ # Allow SupersetEnum. Specs analysers can connect to Lens and Psu mode separately to the
7
+ # analyser which leaves the enum to either be "Not connected" OR the available enums
8
+ # when connected.
9
+ TLensMode = TypeVar("TLensMode", bound=SupersetEnum | StrictEnum)
10
+ TPsuMode = TypeVar("TPsuMode", bound=SupersetEnum | StrictEnum)
11
+ TPassEnergy = TypeVar("TPassEnergy", bound=StrictEnum | float)
12
+ TPassEnergyEnum = TypeVar("TPassEnergyEnum", bound=StrictEnum)
@@ -3,7 +3,7 @@ from typing import Generic
3
3
 
4
4
  from ophyd_async.core import SignalR
5
5
 
6
- from dodal.devices.electron_analyser.abstract.base_driver_io import TLensMode
6
+ from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
7
7
  from dodal.devices.electron_analyser.detector import (
8
8
  ElectronAnalyserDetector,
9
9
  )
@@ -13,21 +13,21 @@ from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSeque
13
13
 
14
14
  class SpecsDetector(
15
15
  ElectronAnalyserDetector[
16
- SpecsAnalyserDriverIO[TLensMode],
17
- SpecsSequence[TLensMode],
18
- SpecsRegion[TLensMode],
16
+ SpecsAnalyserDriverIO[TLensMode, TPsuMode],
17
+ SpecsSequence[TLensMode, TPsuMode],
18
+ SpecsRegion[TLensMode, TPsuMode],
19
19
  ],
20
- Generic[TLensMode],
20
+ Generic[TLensMode, TPsuMode],
21
21
  ):
22
22
  def __init__(
23
23
  self,
24
24
  prefix: str,
25
25
  lens_mode_type: type[TLensMode],
26
+ psu_mode_type: type[TPsuMode],
26
27
  energy_sources: Mapping[str, SignalR[float]],
27
28
  name: str = "",
28
29
  ):
29
- driver = SpecsAnalyserDriverIO[TLensMode](
30
- prefix, lens_mode_type, energy_sources
30
+ driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](
31
+ prefix, lens_mode_type, psu_mode_type, energy_sources
31
32
  )
32
- seq = SpecsSequence[lens_mode_type]
33
- super().__init__(seq, driver, name)
33
+ super().__init__(SpecsSequence[lens_mode_type, psu_mode_type], driver, name)
@@ -15,51 +15,88 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
15
15
  from dodal.devices.electron_analyser.abstract.base_driver_io import (
16
16
  AbstractAnalyserDriverIO,
17
17
  )
18
- from dodal.devices.electron_analyser.abstract.base_region import TLensMode
18
+ from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
19
19
  from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
20
20
  from dodal.devices.electron_analyser.specs.region import SpecsRegion
21
+ from dodal.devices.electron_analyser.util import to_kinetic_energy
21
22
 
22
23
 
23
24
  class SpecsAnalyserDriverIO(
24
- AbstractAnalyserDriverIO[SpecsRegion, AcquisitionMode, TLensMode],
25
- Generic[TLensMode],
25
+ AbstractAnalyserDriverIO[
26
+ SpecsRegion[TLensMode, TPsuMode],
27
+ AcquisitionMode,
28
+ TLensMode,
29
+ TPsuMode,
30
+ float,
31
+ ],
32
+ Generic[TLensMode, TPsuMode],
26
33
  ):
27
34
  def __init__(
28
35
  self,
29
36
  prefix: str,
30
37
  lens_mode_type: type[TLensMode],
38
+ psu_mode_type: type[TPsuMode],
31
39
  energy_sources: Mapping[str, SignalR[float]],
32
40
  name: str = "",
33
41
  ) -> None:
34
42
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
35
43
  # Used for setting up region data acquisition.
36
- self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
37
44
  self.snapshot_values = epics_signal_rw(int, prefix + "VALUES")
38
- self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
39
45
 
40
- # Used to read detector data after acqusition.
41
- self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
42
- self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
43
- self.energy_channels = epics_signal_r(
44
- int, prefix + "TOTAL_POINTS_ITERATION_RBV"
45
- )
46
+ # Used to calculate the angle axis.
47
+ self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
48
+ self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
46
49
 
47
- super().__init__(prefix, AcquisitionMode, lens_mode_type, energy_sources, name)
50
+ # Used to calculate the energy axis.
51
+ self.energy_channels = epics_signal_r(
52
+ int, prefix + "TOTAL_POINTS_ITERATION_RBV"
53
+ )
54
+
55
+ super().__init__(
56
+ prefix=prefix,
57
+ acquisition_mode_type=AcquisitionMode,
58
+ lens_mode_type=lens_mode_type,
59
+ psu_mode_type=psu_mode_type,
60
+ pass_energy_type=float,
61
+ energy_sources=energy_sources,
62
+ name=name,
63
+ )
48
64
 
49
65
  @AsyncStatus.wrap
50
- async def set(self, region: SpecsRegion[TLensMode]):
51
- await super().set(region)
66
+ async def set(self, region: SpecsRegion[TLensMode, TPsuMode]):
67
+ source = self._get_energy_source(region.excitation_energy_source)
68
+ excitation_energy = await source.get_value() # eV
52
69
 
70
+ low_energy = to_kinetic_energy(
71
+ region.low_energy, region.energy_mode, excitation_energy
72
+ )
73
+ centre_energy = to_kinetic_energy(
74
+ region.centre_energy, region.energy_mode, excitation_energy
75
+ )
76
+ high_energy = to_kinetic_energy(
77
+ region.high_energy, region.energy_mode, excitation_energy
78
+ )
53
79
  await asyncio.gather(
80
+ self.region_name.set(region.name),
81
+ self.energy_mode.set(region.energy_mode),
82
+ self.low_energy.set(low_energy),
83
+ self.high_energy.set(high_energy),
84
+ self.slices.set(region.slices),
85
+ self.lens_mode.set(region.lens_mode),
86
+ self.pass_energy.set(region.pass_energy),
87
+ self.iterations.set(region.iterations),
88
+ self.acquisition_mode.set(region.acquisition_mode),
89
+ self.excitation_energy.set(excitation_energy),
90
+ self.excitation_energy_source.set(source.name),
54
91
  self.snapshot_values.set(region.values),
55
92
  self.psu_mode.set(region.psu_mode),
56
93
  )
57
94
  if region.acquisition_mode == AcquisitionMode.FIXED_TRANSMISSION:
58
- await self.centre_energy.set(region.centre_energy)
59
-
60
- if self.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
61
95
  await self.energy_step.set(region.energy_step)
62
96
 
97
+ if region.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
98
+ await self.centre_energy.set(centre_energy)
99
+
63
100
  def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
64
101
  angle_axis = derived_signal_r(
65
102
  self._calculate_angle_axis,
@@ -97,7 +134,3 @@ class SpecsAnalyserDriverIO(
97
134
  step = (max_energy - min_energy) / (total_points_iterations - 1)
98
135
  axis = np.array([min_energy + i * step for i in range(total_points_iterations)])
99
136
  return axis
100
-
101
- @property
102
- def pass_energy_type(self) -> type:
103
- return float
@@ -5,27 +5,32 @@ from pydantic import Field
5
5
  from dodal.devices.electron_analyser.abstract.base_region import (
6
6
  AbstractBaseRegion,
7
7
  AbstractBaseSequence,
8
- TLensMode,
9
8
  )
9
+ from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
10
10
  from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
11
11
 
12
12
 
13
- class SpecsRegion(AbstractBaseRegion[AcquisitionMode, TLensMode], Generic[TLensMode]):
13
+ class SpecsRegion(
14
+ AbstractBaseRegion[AcquisitionMode, TLensMode, float],
15
+ Generic[TLensMode, TPsuMode],
16
+ ):
14
17
  # Override base class with defaults
15
18
  lens_mode: TLensMode
16
- pass_energy: int = 5
19
+ pass_energy: float = 5
17
20
  acquisition_mode: AcquisitionMode = AcquisitionMode.FIXED_TRANSMISSION
18
21
  low_energy: float = Field(default=800, alias="start_energy")
22
+ centre_energy: float = 0
19
23
  high_energy: float = Field(default=850, alias="end_energy")
20
24
  step_time: float = Field(default=1.0, alias="exposure_time")
21
25
  energy_step: float = Field(default=0.1, alias="step_energy")
26
+
22
27
  # Specific to this class
23
28
  values: int = 1
24
- centre_energy: float = 0
25
- # ToDo - Update to an enum https://github.com/DiamondLightSource/dodal/issues/1328
26
- psu_mode: str = "1.5kV"
29
+ psu_mode: TPsuMode
27
30
  estimated_time_in_ms: float = 0
28
31
 
29
32
 
30
- class SpecsSequence(AbstractBaseSequence[SpecsRegion, TLensMode], Generic[TLensMode]):
31
- regions: list[SpecsRegion[TLensMode]] = Field(default_factory=lambda: [])
33
+ class SpecsSequence(
34
+ AbstractBaseSequence[SpecsRegion[TLensMode, TPsuMode]], Generic[TLensMode, TPsuMode]
35
+ ):
36
+ regions: list[SpecsRegion[TLensMode, TPsuMode]] = Field(default_factory=lambda: [])
@@ -22,27 +22,36 @@ from dodal.devices.electron_analyser.vgscienta.detector import (
22
22
 
23
23
  AnyAcqMode = StrictEnum
24
24
  AnyLensMode = SupersetEnum | StrictEnum
25
+ AnyPsuMode = SupersetEnum | StrictEnum
26
+ AnyPassEnergy = StrictEnum | float
27
+ AnyPassEnergyEnum = StrictEnum
25
28
 
26
29
  # Electron analyser types that encompasses all implementations, useful for tests and
27
30
  # plans
28
31
  ElectronAnalyserDetectorImpl = (
29
- VGScientaDetector[AnyLensMode] | SpecsDetector[AnyLensMode]
32
+ VGScientaDetector[AnyLensMode, AnyPsuMode, AnyPassEnergyEnum]
33
+ | SpecsDetector[AnyLensMode, AnyPsuMode]
30
34
  )
31
35
  ElectronAnalyserDriverImpl = (
32
- VGScientaAnalyserDriverIO[AnyLensMode] | SpecsAnalyserDriverIO[AnyLensMode]
36
+ VGScientaAnalyserDriverIO[AnyLensMode, AnyPsuMode, AnyPassEnergyEnum]
37
+ | SpecsAnalyserDriverIO[AnyLensMode, AnyPsuMode]
33
38
  )
34
39
 
35
40
  # Short hand the type so less verbose
36
- AbstractBaseRegion = AbstractBaseRegion[AnyAcqMode, AnyLensMode]
41
+ AbstractBaseRegion = AbstractBaseRegion[AnyAcqMode, AnyLensMode, AnyPassEnergy]
37
42
 
38
43
  # Generic electron analyser types that supports full typing with the abstract classes.
39
44
  GenericElectronAnalyserDetector = ElectronAnalyserDetector[
40
- AbstractAnalyserDriverIO[AbstractBaseRegion, AnyAcqMode, AnyLensMode],
41
- AbstractBaseSequence[AbstractBaseRegion, AnyLensMode],
45
+ AbstractAnalyserDriverIO[
46
+ AbstractBaseRegion, AnyAcqMode, AnyLensMode, AnyPsuMode, AnyPassEnergy
47
+ ],
48
+ AbstractBaseSequence[AbstractBaseRegion],
42
49
  AbstractBaseRegion,
43
50
  ]
44
51
 
45
52
  GenericElectronAnalyserRegionDetector = ElectronAnalyserRegionDetector[
46
- AbstractAnalyserDriverIO[AbstractBaseRegion, AnyAcqMode, AnyLensMode],
53
+ AbstractAnalyserDriverIO[
54
+ AbstractBaseRegion, AnyAcqMode, AnyLensMode, AnyPsuMode, AnyPassEnergy
55
+ ],
47
56
  AbstractBaseRegion,
48
57
  ]
@@ -3,7 +3,11 @@ from typing import Generic
3
3
 
4
4
  from ophyd_async.core import SignalR
5
5
 
6
- from dodal.devices.electron_analyser.abstract.base_region import TLensMode
6
+ from dodal.devices.electron_analyser.abstract.types import (
7
+ TLensMode,
8
+ TPassEnergyEnum,
9
+ TPsuMode,
10
+ )
7
11
  from dodal.devices.electron_analyser.detector import (
8
12
  ElectronAnalyserDetector,
9
13
  )
@@ -18,20 +22,26 @@ from dodal.devices.electron_analyser.vgscienta.region import (
18
22
 
19
23
  class VGScientaDetector(
20
24
  ElectronAnalyserDetector[
21
- VGScientaAnalyserDriverIO[TLensMode],
22
- VGScientaSequence[TLensMode],
23
- VGScientaRegion[TLensMode],
25
+ VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum],
26
+ VGScientaSequence[TLensMode, TPsuMode, TPassEnergyEnum],
27
+ VGScientaRegion[TLensMode, TPassEnergyEnum],
24
28
  ],
25
- Generic[TLensMode],
29
+ Generic[TLensMode, TPsuMode, TPassEnergyEnum],
26
30
  ):
27
31
  def __init__(
28
32
  self,
29
33
  prefix: str,
30
34
  lens_mode_type: type[TLensMode],
35
+ psu_mode_type: type[TPsuMode],
36
+ pass_energy_type: type[TPassEnergyEnum],
31
37
  energy_sources: Mapping[str, SignalR[float]],
32
38
  name: str = "",
33
39
  ):
34
- driver = VGScientaAnalyserDriverIO[TLensMode](
35
- prefix, lens_mode_type, energy_sources
40
+ driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum](
41
+ prefix, lens_mode_type, psu_mode_type, pass_energy_type, energy_sources
42
+ )
43
+ super().__init__(
44
+ VGScientaSequence[lens_mode_type, psu_mode_type, pass_energy_type],
45
+ driver,
46
+ name,
36
47
  )
37
- super().__init__(VGScientaSequence[lens_mode_type], driver, name)
@@ -15,54 +15,96 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
15
15
  from dodal.devices.electron_analyser.abstract.base_driver_io import (
16
16
  AbstractAnalyserDriverIO,
17
17
  )
18
- from dodal.devices.electron_analyser.abstract.base_region import TLensMode
18
+ from dodal.devices.electron_analyser.abstract.types import (
19
+ TLensMode,
20
+ TPassEnergyEnum,
21
+ TPsuMode,
22
+ )
19
23
  from dodal.devices.electron_analyser.util import to_kinetic_energy
20
- from dodal.devices.electron_analyser.vgscienta.enums import AcquisitionMode
21
- from dodal.devices.electron_analyser.vgscienta.region import (
24
+ from dodal.devices.electron_analyser.vgscienta.enums import (
25
+ AcquisitionMode,
22
26
  DetectorMode,
27
+ )
28
+ from dodal.devices.electron_analyser.vgscienta.region import (
23
29
  VGScientaRegion,
24
30
  )
25
31
 
26
32
 
27
33
  class VGScientaAnalyserDriverIO(
28
- AbstractAnalyserDriverIO[VGScientaRegion, AcquisitionMode, TLensMode],
29
- Generic[TLensMode],
34
+ AbstractAnalyserDriverIO[
35
+ VGScientaRegion[TLensMode, TPassEnergyEnum],
36
+ AcquisitionMode,
37
+ TLensMode,
38
+ TPsuMode,
39
+ TPassEnergyEnum,
40
+ ],
41
+ Generic[TLensMode, TPsuMode, TPassEnergyEnum],
30
42
  ):
31
43
  def __init__(
32
44
  self,
33
45
  prefix: str,
34
46
  lens_mode_type: type[TLensMode],
47
+ psu_mode_type: type[TPsuMode],
48
+ pass_energy_type: type[TPassEnergyEnum],
35
49
  energy_sources: Mapping[str, SignalR[float]],
36
50
  name: str = "",
37
51
  ) -> None:
38
52
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
39
53
  # Used for setting up region data acquisition.
40
- self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
41
- self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
42
- self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
43
- self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
44
- self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
45
54
  self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
46
55
 
47
- super().__init__(prefix, AcquisitionMode, lens_mode_type, energy_sources, name)
56
+ self.region_min_x = epics_signal_rw(int, prefix + "MinX")
57
+ self.region_size_x = epics_signal_rw(int, prefix + "SizeX")
58
+ self.sensor_max_size_x = epics_signal_rw(int, prefix + "MaxSizeX")
59
+
60
+ self.region_min_y = epics_signal_rw(int, prefix + "MinY")
61
+ self.region_size_y = epics_signal_rw(int, prefix + "SizeY")
62
+ self.sensor_max_size_y = epics_signal_rw(int, prefix + "MaxSizeY")
63
+
64
+ super().__init__(
65
+ prefix,
66
+ AcquisitionMode,
67
+ lens_mode_type,
68
+ psu_mode_type,
69
+ pass_energy_type,
70
+ energy_sources,
71
+ name,
72
+ )
48
73
 
49
74
  @AsyncStatus.wrap
50
- async def set(self, region: VGScientaRegion[TLensMode]):
51
- await super().set(region)
75
+ async def set(self, region: VGScientaRegion[TLensMode, TPassEnergyEnum]):
76
+ source = self._get_energy_source(region.excitation_energy_source)
77
+ excitation_energy = await source.get_value() # eV
52
78
 
53
- excitation_energy = await self.excitation_energy.get_value()
79
+ low_energy = to_kinetic_energy(
80
+ region.low_energy, region.energy_mode, excitation_energy
81
+ )
54
82
  centre_energy = to_kinetic_energy(
55
- region.fix_energy, region.energy_mode, excitation_energy
83
+ region.centre_energy, region.energy_mode, excitation_energy
84
+ )
85
+ high_energy = to_kinetic_energy(
86
+ region.high_energy, region.energy_mode, excitation_energy
56
87
  )
57
88
  await asyncio.gather(
89
+ self.region_name.set(region.name),
90
+ self.energy_mode.set(region.energy_mode),
91
+ self.low_energy.set(low_energy),
58
92
  self.centre_energy.set(centre_energy),
93
+ self.high_energy.set(high_energy),
94
+ self.slices.set(region.slices),
95
+ self.lens_mode.set(region.lens_mode),
96
+ self.pass_energy.set(region.pass_energy),
97
+ self.iterations.set(region.iterations),
98
+ self.acquisition_mode.set(region.acquisition_mode),
99
+ self.excitation_energy.set(excitation_energy),
100
+ self.excitation_energy_source.set(source.name),
59
101
  self.energy_step.set(region.energy_step),
60
- self.first_x_channel.set(region.first_x_channel),
61
- self.first_y_channel.set(region.first_y_channel),
62
- self.x_channel_size.set(region.x_channel_size()),
63
- self.y_channel_size.set(region.y_channel_size()),
64
- self.detector_mode.set(region.detector_mode),
65
102
  self.image_mode.set(ADImageMode.SINGLE),
103
+ self.detector_mode.set(region.detector_mode),
104
+ self.region_min_x.set(region.min_x),
105
+ self.region_size_x.set(region.size_x),
106
+ self.region_min_y.set(region.min_y),
107
+ self.region_size_y.set(region.size_y),
66
108
  )
67
109
 
68
110
  def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
@@ -70,7 +112,3 @@ class VGScientaAnalyserDriverIO(
70
112
 
71
113
  def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
72
114
  return epics_signal_r(Array1D[np.float64], prefix + "Y_SCALE_RBV")
73
-
74
- @property
75
- def pass_energy_type(self) -> type:
76
- return str
@@ -1,13 +1,17 @@
1
1
  import uuid
2
2
  from typing import Generic
3
3
 
4
- from pydantic import Field
4
+ from pydantic import Field, field_validator
5
5
 
6
6
  from dodal.devices.electron_analyser.abstract.base_region import (
7
7
  AbstractBaseRegion,
8
8
  AbstractBaseSequence,
9
9
  JavaToPythonModel,
10
+ )
11
+ from dodal.devices.electron_analyser.abstract.types import (
10
12
  TLensMode,
13
+ TPassEnergyEnum,
14
+ TPsuMode,
11
15
  )
12
16
  from dodal.devices.electron_analyser.vgscienta.enums import (
13
17
  AcquisitionMode,
@@ -17,33 +21,43 @@ from dodal.devices.electron_analyser.vgscienta.enums import (
17
21
 
18
22
 
19
23
  class VGScientaRegion(
20
- AbstractBaseRegion[AcquisitionMode, TLensMode], Generic[TLensMode]
24
+ AbstractBaseRegion[AcquisitionMode, TLensMode, TPassEnergyEnum],
25
+ Generic[TLensMode, TPassEnergyEnum],
21
26
  ):
22
27
  # Override defaults of base region class
23
28
  lens_mode: TLensMode
24
- pass_energy: int = 5
29
+ pass_energy: TPassEnergyEnum
25
30
  acquisition_mode: AcquisitionMode = AcquisitionMode.SWEPT
26
31
  low_energy: float = 8.0
27
32
  high_energy: float = 10.0
28
33
  step_time: float = 1.0
29
34
  energy_step: float = Field(default=200.0)
35
+ centre_energy: float = Field(alias="fix_energy", default=9)
30
36
  # Specific to this class
31
37
  id: str = Field(default=str(uuid.uuid4()), alias="region_id")
32
- fix_energy: float = 9.0
33
38
  total_steps: float = 13.0
34
39
  total_time: float = 13.0
35
- first_x_channel: int = 1
36
- last_x_channel: int = 1000
37
- first_y_channel: int = 101
38
- last_y_channel: int = 800
40
+ min_x: int = Field(alias="first_x_channel", default=1)
41
+ sensor_max_size_x: int = Field(alias="last_x_channel", default=1000)
42
+ min_y: int = Field(alias="first_y_channel", default=101)
43
+ sensor_max_size_y: int = Field(alias="last_y_channel", default=800)
39
44
  detector_mode: DetectorMode = DetectorMode.ADC
40
45
  status: Status = Status.READY
41
46
 
42
- def x_channel_size(self) -> int:
43
- return self.last_x_channel - self.first_x_channel + 1
47
+ @property
48
+ def size_x(self) -> int:
49
+ return self.sensor_max_size_x - self.min_x + 1
50
+
51
+ @property
52
+ def size_y(self) -> int:
53
+ return self.sensor_max_size_y - self.min_y + 1
44
54
 
45
- def y_channel_size(self) -> int:
46
- return self.last_y_channel - self.first_y_channel + 1
55
+ @field_validator("pass_energy", mode="before")
56
+ @classmethod
57
+ def validate_pass_energy(cls, val):
58
+ # This is needed because if the value is a number, it can't be casted to the
59
+ # enum correctly.
60
+ return str(val)
47
61
 
48
62
 
49
63
  class VGScientaExcitationEnergySource(JavaToPythonModel):
@@ -53,16 +67,19 @@ class VGScientaExcitationEnergySource(JavaToPythonModel):
53
67
 
54
68
 
55
69
  class VGScientaSequence(
56
- AbstractBaseSequence[VGScientaRegion, TLensMode], Generic[TLensMode]
70
+ AbstractBaseSequence[VGScientaRegion[TLensMode, TPassEnergyEnum]],
71
+ Generic[TLensMode, TPsuMode, TPassEnergyEnum],
57
72
  ):
58
- element_set: str = Field(default="Unknown")
73
+ psu_mode: TPsuMode = Field(alias="element_set")
59
74
  excitation_energy_sources: list[VGScientaExcitationEnergySource] = Field(
60
75
  default_factory=lambda: []
61
76
  )
62
- regions: list[VGScientaRegion[TLensMode]] = Field(default_factory=lambda: [])
77
+ regions: list[VGScientaRegion[TLensMode, TPassEnergyEnum]] = Field(
78
+ default_factory=lambda: []
79
+ )
63
80
 
64
81
  def get_excitation_energy_source_by_region(
65
- self, region: VGScientaRegion[TLensMode]
82
+ self, region: VGScientaRegion[TLensMode, TPassEnergyEnum]
66
83
  ) -> VGScientaExcitationEnergySource:
67
84
  value = next(
68
85
  (
@@ -113,7 +113,7 @@ class SingleMirrorVoltage(Device):
113
113
 
114
114
  class MirrorVoltages(StandardReadable):
115
115
  def __init__(
116
- self, name: str, prefix: str, *args, daq_configuration_path: str, **kwargs
116
+ self, prefix: str, name: str = "", *args, daq_configuration_path: str, **kwargs
117
117
  ):
118
118
  self.voltage_lookup_table_path = (
119
119
  daq_configuration_path + "/json/mirrorFocus.json"
@@ -27,6 +27,8 @@ class UndulatorDCM(StandardReadable, Movable[float]):
27
27
  instead. See https://github.com/DiamondLightSource/dodal/issues/1092
28
28
  """
29
29
 
30
+ DCM_PERP_TOLERANCE = 0.01
31
+
30
32
  def __init__(
31
33
  self,
32
34
  undulator: Undulator,
@@ -60,6 +62,9 @@ class UndulatorDCM(StandardReadable, Movable[float]):
60
62
  self.dcm_ref().energy_in_kev.set(value, timeout=ENERGY_TIMEOUT_S),
61
63
  self.undulator_ref().set(value),
62
64
  )
63
- # DCM Perp pitch
64
- LOGGER.info(f"Adjusting DCM offset to {self.dcm_fixed_offset_mm} mm")
65
- await self.dcm_ref().offset_in_mm.set(self.dcm_fixed_offset_mm)
65
+
66
+ # The DCM perp is under vacuum so for heat management it's best to only change it when required
67
+ current_offset = await self.dcm_ref().offset_in_mm.user_readback.get_value()
68
+ if abs(current_offset - self.dcm_fixed_offset_mm) > self.DCM_PERP_TOLERANCE:
69
+ LOGGER.info(f"Adjusting DCM offset to {self.dcm_fixed_offset_mm} mm")
70
+ await self.dcm_ref().offset_in_mm.set(self.dcm_fixed_offset_mm)
@@ -0,0 +1,3 @@
1
+ from dodal.devices.i05.enums import Grating
2
+
3
+ __all__ = ["Grating"]
@@ -0,0 +1,8 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+
4
+ class Grating(StrictEnum):
5
+ PT_400 = "400 lines/mm"
6
+ C_1600 = "C 1600 lines/mm"
7
+ RH_1600 = "Rh 1600 lines/mm"
8
+ PT_800 = "B 800 lines/mm"
@@ -1,4 +1,4 @@
1
1
  from dodal.devices.i09.dcm import DCM
2
- from dodal.devices.i09.enums import Grating, LensMode
2
+ from dodal.devices.i09.enums import Grating, LensMode, PassEnergy, PsuMode
3
3
 
4
- __all__ = ["DCM", "Grating", "LensMode"]
4
+ __all__ = ["DCM", "Grating", "LensMode", "PsuMode", "PassEnergy"]
@@ -13,3 +13,19 @@ class LensMode(StrictEnum):
13
13
  ANGULAR60 = "Angular60"
14
14
  ANGULAR56 = "Angular56"
15
15
  ANGULAR45VUV = "Angular45VUV"
16
+
17
+
18
+ class PsuMode(StrictEnum):
19
+ HIGH = "High"
20
+ LOW = "Low"
21
+
22
+
23
+ class PassEnergy(StrictEnum):
24
+ E5 = 5
25
+ E10 = 10
26
+ E20 = 20
27
+ E50 = 50
28
+ E70 = 70
29
+ E100 = 100
30
+ E200 = 200
31
+ E500 = 500
@@ -1,3 +1,3 @@
1
- from .enums import LensMode
1
+ from .enums import LensMode, PsuMode
2
2
 
3
- __all__ = ["LensMode"]
3
+ __all__ = ["LensMode", "PsuMode"]
@@ -16,4 +16,17 @@ class LensMode(SupersetEnum):
16
16
  LOW_MAGNIFICATION = "LowMagnification"
17
17
  HIGH_MAGNIFICATION2 = "HighMagnification2"
18
18
  RAMP_MODE = "RampMode"
19
+ # This is connected to the device separately and will only have "Not connected" as
20
+ # option if disconnected. Once it is connected, "Not connected" is replaced with the
21
+ # options above. This is also why this must be a SupersetEnum.
22
+ NOT_CONNECTED = "Not connected"
23
+
24
+
25
+ class PsuMode(SupersetEnum):
26
+ V3500 = "3.5kV"
27
+ V1500 = "1.5kV"
28
+ V400 = "400V"
29
+ # This is connected to the device separately and will only have "Not connected" as
30
+ # option if disconnected. Once it is connected, "Not connected" is replaced with the
31
+ # options above. This is also why this must be a SupersetEnum.
19
32
  NOT_CONNECTED = "Not connected"
@@ -1,19 +1,15 @@
1
1
  from ophyd_async.epics.core import epics_signal_rw
2
- from ophyd_async.epics.motor import Motor
3
2
 
4
- from dodal.devices.motors import XYZStage
3
+ from dodal.devices.motors import XYZPitchYawRollStage
5
4
 
6
5
 
7
- class PiezoMirror(XYZStage):
6
+ class PiezoMirror(XYZPitchYawRollStage):
8
7
  def __init__(
9
8
  self,
10
9
  prefix: str,
11
10
  name: str = "",
12
11
  ):
13
12
  with self.add_children_as_readables():
14
- self.yaw = Motor(prefix + "YAW")
15
- self.pitch = Motor(prefix + "PITCH")
16
- self.roll = Motor(prefix + "ROLL")
17
13
  self.fine_pitch = epics_signal_rw(
18
14
  float,
19
15
  read_pv=prefix + "FPITCH:RBV:AI",
@@ -12,8 +12,8 @@ from ophyd_async.epics.adcore import (
12
12
  ADBaseIO,
13
13
  ADImageMode,
14
14
  ADState,
15
- stop_busy_record,
16
15
  )
16
+ from ophyd_async.epics.core import stop_busy_record
17
17
 
18
18
 
19
19
  class MerlinController(ADBaseController):