dls-dodal 1.58.0__py3-none-any.whl → 1.60.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 (71) hide show
  1. {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/RECORD +71 -47
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/__init__.py +1 -0
  5. dodal/beamlines/b07.py +10 -5
  6. dodal/beamlines/b07_1.py +10 -5
  7. dodal/beamlines/b21.py +22 -0
  8. dodal/beamlines/i02_1.py +80 -0
  9. dodal/beamlines/i03.py +5 -3
  10. dodal/beamlines/i04.py +5 -3
  11. dodal/beamlines/i09.py +10 -9
  12. dodal/beamlines/i09_1.py +10 -5
  13. dodal/beamlines/i10-1.py +25 -0
  14. dodal/beamlines/i10.py +17 -1
  15. dodal/beamlines/i11.py +0 -17
  16. dodal/beamlines/i15.py +242 -0
  17. dodal/beamlines/i15_1.py +156 -0
  18. dodal/beamlines/i19_1.py +3 -1
  19. dodal/beamlines/i19_2.py +12 -1
  20. dodal/beamlines/i21.py +27 -0
  21. dodal/beamlines/i22.py +12 -2
  22. dodal/beamlines/i24.py +32 -3
  23. dodal/beamlines/k07.py +31 -0
  24. dodal/beamlines/p60.py +10 -9
  25. dodal/common/watcher_utils.py +1 -1
  26. dodal/devices/apple2_undulator.py +18 -142
  27. dodal/devices/attenuator/attenuator.py +48 -2
  28. dodal/devices/attenuator/filter.py +3 -0
  29. dodal/devices/attenuator/filter_selections.py +26 -0
  30. dodal/devices/eiger.py +2 -1
  31. dodal/devices/electron_analyser/__init__.py +4 -0
  32. dodal/devices/electron_analyser/abstract/base_driver_io.py +30 -18
  33. dodal/devices/electron_analyser/energy_sources.py +101 -0
  34. dodal/devices/electron_analyser/specs/detector.py +6 -6
  35. dodal/devices/electron_analyser/specs/driver_io.py +7 -15
  36. dodal/devices/electron_analyser/vgscienta/detector.py +6 -6
  37. dodal/devices/electron_analyser/vgscienta/driver_io.py +7 -14
  38. dodal/devices/fast_grid_scan.py +130 -64
  39. dodal/devices/focusing_mirror.py +30 -0
  40. dodal/devices/i02_1/__init__.py +0 -0
  41. dodal/devices/i02_1/fast_grid_scan.py +61 -0
  42. dodal/devices/i02_1/sample_motors.py +19 -0
  43. dodal/devices/i04/murko_results.py +69 -23
  44. dodal/devices/i10/i10_apple2.py +282 -140
  45. dodal/devices/i15/dcm.py +77 -0
  46. dodal/devices/i15/focussing_mirror.py +71 -0
  47. dodal/devices/i15/jack.py +39 -0
  48. dodal/devices/i15/laue.py +18 -0
  49. dodal/devices/i15/motors.py +27 -0
  50. dodal/devices/i15/multilayer_mirror.py +25 -0
  51. dodal/devices/i15/rail.py +17 -0
  52. dodal/devices/i21/__init__.py +3 -0
  53. dodal/devices/i21/enums.py +8 -0
  54. dodal/devices/i22/nxsas.py +2 -0
  55. dodal/devices/i24/commissioning_jungfrau.py +114 -0
  56. dodal/devices/motors.py +52 -1
  57. dodal/devices/slits.py +18 -0
  58. dodal/devices/smargon.py +0 -56
  59. dodal/devices/temperture_controller/__init__.py +3 -0
  60. dodal/devices/temperture_controller/lakeshore/__init__.py +0 -0
  61. dodal/devices/temperture_controller/lakeshore/lakeshore.py +204 -0
  62. dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +112 -0
  63. dodal/devices/tetramm.py +38 -16
  64. dodal/devices/v2f.py +39 -0
  65. dodal/devices/zebra/zebra.py +1 -0
  66. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  67. dodal/parameters/experiment_parameter_base.py +1 -5
  68. {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/WHEEL +0 -0
  69. {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/entry_points.txt +0 -0
  70. {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/licenses/LICENSE +0 -0
  71. {dls_dodal-1.58.0.dist-info → dls_dodal-1.60.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,101 @@
1
+ from abc import abstractmethod
2
+
3
+ from ophyd_async.core import (
4
+ Reference,
5
+ SignalR,
6
+ StandardReadable,
7
+ StandardReadableFormat,
8
+ derived_signal_r,
9
+ soft_signal_r_and_setter,
10
+ soft_signal_rw,
11
+ )
12
+
13
+ from dodal.devices.electron_analyser.enums import SelectedSource
14
+
15
+
16
+ class AbstractEnergySource(StandardReadable):
17
+ """
18
+ Abstract device that wraps an energy source signal and provides common interface via
19
+ a energy signal.
20
+ """
21
+
22
+ def __init__(self, name: str = "") -> None:
23
+ super().__init__(name)
24
+
25
+ @property
26
+ @abstractmethod
27
+ def energy(self) -> SignalR[float]:
28
+ """
29
+ Signal to provide the excitation energy value in eV.
30
+ """
31
+
32
+
33
+ class EnergySource(AbstractEnergySource):
34
+ """
35
+ Wraps a signal that relates to energy and provides common interface via energy
36
+ signal. It provides the name of the wrapped signal as a child signal in the
37
+ read_configuration via wrapped_device_name and adds the signal as a readable.
38
+ """
39
+
40
+ def __init__(self, source: SignalR[float], name: str = "") -> None:
41
+ self.add_readables([source])
42
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
43
+ self.wrapped_device_name, _ = soft_signal_r_and_setter(
44
+ str, initial_value=source.name
45
+ )
46
+ self._source_ref = Reference(source)
47
+ super().__init__(name)
48
+
49
+ @property
50
+ def energy(self) -> SignalR[float]:
51
+ return self._source_ref()
52
+
53
+
54
+ class DualEnergySource(AbstractEnergySource):
55
+ """
56
+ Holds two EnergySource devices and provides a signal to read energy depending on
57
+ which source is selected. This is controlled by a selected_source signal which can
58
+ switch source using SelectedSource enum. Both sources energy is recorded in the
59
+ read, the energy signal is used as a helper signal to know which source is being
60
+ used.
61
+ """
62
+
63
+ def __init__(
64
+ self, source1: SignalR[float], source2: SignalR[float], name: str = ""
65
+ ):
66
+ """
67
+ Args:
68
+ source1: Default energy signal to select.
69
+ source2: Secondary energy signal to select.
70
+ name: name of this device.
71
+ """
72
+
73
+ with self.add_children_as_readables():
74
+ self.selected_source = soft_signal_rw(
75
+ SelectedSource, initial_value=SelectedSource.SOURCE1
76
+ )
77
+ self.source1 = EnergySource(source1)
78
+ self.source2 = EnergySource(source2)
79
+
80
+ self._selected_energy = derived_signal_r(
81
+ self._get_excitation_energy,
82
+ "eV",
83
+ selected_source=self.selected_source,
84
+ source1=self.source1.energy,
85
+ source2=self.source2.energy,
86
+ )
87
+
88
+ super().__init__(name)
89
+
90
+ def _get_excitation_energy(
91
+ self, selected_source: SelectedSource, source1: float, source2: float
92
+ ) -> float:
93
+ match selected_source:
94
+ case SelectedSource.SOURCE1:
95
+ return source1
96
+ case SelectedSource.SOURCE2:
97
+ return source2
98
+
99
+ @property
100
+ def energy(self) -> SignalR[float]:
101
+ return self._selected_energy
@@ -1,13 +1,13 @@
1
- from collections.abc import Mapping
2
1
  from typing import Generic
3
2
 
4
- from ophyd_async.core import SignalR
5
-
6
3
  from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
7
4
  from dodal.devices.electron_analyser.detector import (
8
5
  ElectronAnalyserDetector,
9
6
  )
10
- from dodal.devices.electron_analyser.enums import SelectedSource
7
+ from dodal.devices.electron_analyser.energy_sources import (
8
+ DualEnergySource,
9
+ EnergySource,
10
+ )
11
11
  from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
12
12
  from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
13
13
 
@@ -25,10 +25,10 @@ class SpecsDetector(
25
25
  prefix: str,
26
26
  lens_mode_type: type[TLensMode],
27
27
  psu_mode_type: type[TPsuMode],
28
- energy_sources: Mapping[SelectedSource, SignalR[float]],
28
+ energy_source: DualEnergySource | EnergySource,
29
29
  name: str = "",
30
30
  ):
31
31
  driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](
32
- prefix, lens_mode_type, psu_mode_type, energy_sources
32
+ prefix, lens_mode_type, psu_mode_type, energy_source
33
33
  )
34
34
  super().__init__(SpecsSequence[lens_mode_type, psu_mode_type], driver, name)
@@ -1,11 +1,9 @@
1
1
  import asyncio
2
- from collections.abc import Mapping
3
2
  from typing import Generic
4
3
 
5
4
  import numpy as np
6
5
  from ophyd_async.core import (
7
6
  Array1D,
8
- AsyncStatus,
9
7
  SignalR,
10
8
  StandardReadableFormat,
11
9
  derived_signal_r,
@@ -16,7 +14,10 @@ from dodal.devices.electron_analyser.abstract.base_driver_io import (
16
14
  AbstractAnalyserDriverIO,
17
15
  )
18
16
  from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
19
- from dodal.devices.electron_analyser.enums import EnergyMode, SelectedSource
17
+ from dodal.devices.electron_analyser.energy_sources import (
18
+ DualEnergySource,
19
+ EnergySource,
20
+ )
20
21
  from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
21
22
  from dodal.devices.electron_analyser.specs.region import SpecsRegion
22
23
 
@@ -36,7 +37,7 @@ class SpecsAnalyserDriverIO(
36
37
  prefix: str,
37
38
  lens_mode_type: type[TLensMode],
38
39
  psu_mode_type: type[TPsuMode],
39
- energy_sources: Mapping[SelectedSource, SignalR[float]],
40
+ energy_source: EnergySource | DualEnergySource,
40
41
  name: str = "",
41
42
  ) -> None:
42
43
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
@@ -58,18 +59,11 @@ class SpecsAnalyserDriverIO(
58
59
  lens_mode_type=lens_mode_type,
59
60
  psu_mode_type=psu_mode_type,
60
61
  pass_energy_type=float,
61
- energy_sources=energy_sources,
62
+ energy_source=energy_source,
62
63
  name=name,
63
64
  )
64
65
 
65
- @AsyncStatus.wrap
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
69
- # Copy region so doesn't alter the actual region and switch to kinetic energy
70
- ke_region = region.model_copy()
71
- ke_region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
72
-
66
+ async def _set_region(self, ke_region: SpecsRegion[TLensMode, TPsuMode]):
73
67
  await asyncio.gather(
74
68
  self.region_name.set(ke_region.name),
75
69
  self.energy_mode.set(ke_region.energy_mode),
@@ -81,8 +75,6 @@ class SpecsAnalyserDriverIO(
81
75
  self.pass_energy.set(ke_region.pass_energy),
82
76
  self.iterations.set(ke_region.iterations),
83
77
  self.acquisition_mode.set(ke_region.acquisition_mode),
84
- self.excitation_energy.set(excitation_energy),
85
- self.excitation_energy_source.set(source.name),
86
78
  self.snapshot_values.set(ke_region.values),
87
79
  self.psu_mode.set(ke_region.psu_mode),
88
80
  )
@@ -1,8 +1,5 @@
1
- from collections.abc import Mapping
2
1
  from typing import Generic
3
2
 
4
- from ophyd_async.core import SignalR
5
-
6
3
  from dodal.devices.electron_analyser.abstract.types import (
7
4
  TLensMode,
8
5
  TPassEnergyEnum,
@@ -11,7 +8,10 @@ from dodal.devices.electron_analyser.abstract.types import (
11
8
  from dodal.devices.electron_analyser.detector import (
12
9
  ElectronAnalyserDetector,
13
10
  )
14
- from dodal.devices.electron_analyser.enums import SelectedSource
11
+ from dodal.devices.electron_analyser.energy_sources import (
12
+ DualEnergySource,
13
+ EnergySource,
14
+ )
15
15
  from dodal.devices.electron_analyser.vgscienta.driver_io import (
16
16
  VGScientaAnalyserDriverIO,
17
17
  )
@@ -35,11 +35,11 @@ class VGScientaDetector(
35
35
  lens_mode_type: type[TLensMode],
36
36
  psu_mode_type: type[TPsuMode],
37
37
  pass_energy_type: type[TPassEnergyEnum],
38
- energy_sources: Mapping[SelectedSource, SignalR[float]],
38
+ energy_source: DualEnergySource | EnergySource,
39
39
  name: str = "",
40
40
  ):
41
41
  driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum](
42
- prefix, lens_mode_type, psu_mode_type, pass_energy_type, energy_sources
42
+ prefix, lens_mode_type, psu_mode_type, pass_energy_type, energy_source
43
43
  )
44
44
  super().__init__(
45
45
  VGScientaSequence[lens_mode_type, psu_mode_type, pass_energy_type],
@@ -1,11 +1,9 @@
1
1
  import asyncio
2
- from collections.abc import Mapping
3
2
  from typing import Generic
4
3
 
5
4
  import numpy as np
6
5
  from ophyd_async.core import (
7
6
  Array1D,
8
- AsyncStatus,
9
7
  SignalR,
10
8
  StandardReadableFormat,
11
9
  )
@@ -19,7 +17,10 @@ from dodal.devices.electron_analyser.abstract.types import (
19
17
  TPassEnergyEnum,
20
18
  TPsuMode,
21
19
  )
22
- from dodal.devices.electron_analyser.enums import EnergyMode, SelectedSource
20
+ from dodal.devices.electron_analyser.energy_sources import (
21
+ DualEnergySource,
22
+ EnergySource,
23
+ )
23
24
  from dodal.devices.electron_analyser.vgscienta.enums import (
24
25
  AcquisitionMode,
25
26
  DetectorMode,
@@ -45,7 +46,7 @@ class VGScientaAnalyserDriverIO(
45
46
  lens_mode_type: type[TLensMode],
46
47
  psu_mode_type: type[TPsuMode],
47
48
  pass_energy_type: type[TPassEnergyEnum],
48
- energy_sources: Mapping[SelectedSource, SignalR[float]],
49
+ energy_source: EnergySource | DualEnergySource,
49
50
  name: str = "",
50
51
  ) -> None:
51
52
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
@@ -66,17 +67,11 @@ class VGScientaAnalyserDriverIO(
66
67
  lens_mode_type,
67
68
  psu_mode_type,
68
69
  pass_energy_type,
69
- energy_sources,
70
+ energy_source,
70
71
  name,
71
72
  )
72
73
 
73
- @AsyncStatus.wrap
74
- async def set(self, region: VGScientaRegion[TLensMode, TPassEnergyEnum]):
75
- source = self._get_energy_source(region.excitation_energy_source)
76
- excitation_energy = await source.get_value() # eV
77
- # Copy region so doesn't alter the actual region and switch to kinetic energy
78
- ke_region = region.model_copy()
79
- ke_region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
74
+ async def _set_region(self, ke_region: VGScientaRegion[TLensMode, TPassEnergyEnum]):
80
75
  await asyncio.gather(
81
76
  self.region_name.set(ke_region.name),
82
77
  self.energy_mode.set(ke_region.energy_mode),
@@ -89,8 +84,6 @@ class VGScientaAnalyserDriverIO(
89
84
  self.iterations.set(ke_region.iterations),
90
85
  self.acquire_time.set(ke_region.acquire_time),
91
86
  self.acquisition_mode.set(ke_region.acquisition_mode),
92
- self.excitation_energy.set(excitation_energy),
93
- self.excitation_energy_source.set(source.name),
94
87
  self.energy_step.set(ke_region.energy_step),
95
88
  self.detector_mode.set(ke_region.detector_mode),
96
89
  self.region_min_x.set(ke_region.min_x),
@@ -9,9 +9,11 @@ from ophyd_async.core import (
9
9
  AsyncStatus,
10
10
  Device,
11
11
  Signal,
12
+ SignalR,
12
13
  SignalRW,
13
14
  StandardReadable,
14
15
  derived_signal_r,
16
+ soft_signal_r_and_setter,
15
17
  wait_for_value,
16
18
  )
17
19
  from ophyd_async.epics.core import (
@@ -20,7 +22,7 @@ from ophyd_async.epics.core import (
20
22
  epics_signal_rw_rbv,
21
23
  epics_signal_x,
22
24
  )
23
- from pydantic import field_validator
25
+ from pydantic import BaseModel, field_validator
24
26
  from pydantic.dataclasses import dataclass
25
27
 
26
28
  from dodal.log import LOGGER
@@ -63,7 +65,7 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
63
65
  """
64
66
  Common holder class for the parameters of a grid scan in a similar
65
67
  layout to EPICS. The parameters and functions of this class are common
66
- to both the zebra and panda triggered fast grid scans.
68
+ to both the zebra and panda triggered fast grid scans in 2d or 3d.
67
69
 
68
70
  The grid specified is where data is taken e.g. it can be assumed the first frame is
69
71
  at x_start, y1_start, z1_start and subsequent frames are N*step_size away.
@@ -71,15 +73,11 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
71
73
 
72
74
  x_steps: int = 1
73
75
  y_steps: int = 1
74
- z_steps: int = 0
75
76
  x_step_size_mm: float = 0.1
76
77
  y_step_size_mm: float = 0.1
77
- z_step_size_mm: float = 0.1
78
78
  x_start_mm: float = 0.1
79
79
  y1_start_mm: float = 0.1
80
- y2_start_mm: float = 0.1
81
80
  z1_start_mm: float = 0.1
82
- z2_start_mm: float = 0.1
83
81
 
84
82
  # Whether to set the stub offsets after centering
85
83
  set_stub_offsets: bool = False
@@ -92,16 +90,10 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
92
90
  def y_axis(self) -> GridAxis:
93
91
  return GridAxis(self.y1_start_mm, self.y_step_size_mm, self.y_steps)
94
92
 
93
+ # In 2D grid scans, z axis is just the start position
95
94
  @property
96
95
  def z_axis(self) -> GridAxis:
97
- return GridAxis(self.z2_start_mm, self.z_step_size_mm, self.z_steps)
98
-
99
- def get_num_images(self):
100
- return self.x_steps * (self.y_steps + self.z_steps)
101
-
102
- @property
103
- def is_3d_grid_scan(self):
104
- return self.z_steps > 0
96
+ return GridAxis(self.z1_start_mm, 0, 1)
105
97
 
106
98
  def grid_position_to_motor_position(self, grid_position: ndarray) -> ndarray:
107
99
  """Converts a grid position, given as steps in the x, y, z grid,
@@ -130,15 +122,33 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
130
122
  )
131
123
 
132
124
 
133
- ParamType = TypeVar("ParamType", bound=GridScanParamsCommon)
134
-
125
+ class GridScanParamsThreeD(GridScanParamsCommon):
126
+ """Additional parameters required to do a 3 dimensional gridscan.
135
127
 
136
- class ZebraGridScanParams(GridScanParamsCommon):
137
- """
138
- Params for standard Zebra FGS. Adds on the dwell time
128
+ A 3D gridscan works by doing two 2D gridscans. The first of these grids is x_steps by
129
+ y_steps. The sample is then rotated by 90 degrees, and then the second grid is
130
+ x_steps by z_steps.
139
131
  """
140
132
 
141
- dwell_time_ms: float = 10
133
+ # Start position for z and y during the second gridscan
134
+ z2_start_mm: float = 0.1
135
+ y2_start_mm: float = 0.1
136
+
137
+ z_step_size_mm: float = 0.1
138
+
139
+ # Number of vertical steps during the second grid scan, after the rotation in omega
140
+ z_steps: int = 1
141
+
142
+ @property
143
+ def z_axis(self) -> GridAxis:
144
+ return GridAxis(self.z2_start_mm, self.z_step_size_mm, self.z_steps)
145
+
146
+
147
+ ParamType = TypeVar("ParamType", bound=GridScanParamsCommon, covariant=True)
148
+
149
+
150
+ class WithDwellTime(BaseModel):
151
+ dwell_time_ms: float = 213
142
152
 
143
153
  @field_validator("dwell_time_ms")
144
154
  @classmethod
@@ -154,7 +164,14 @@ class ZebraGridScanParams(GridScanParamsCommon):
154
164
  return dwell_time_ms
155
165
 
156
166
 
157
- class PandAGridScanParams(GridScanParamsCommon):
167
+ class ZebraGridScanParamsThreeD(GridScanParamsThreeD, WithDwellTime):
168
+ """
169
+ Params for standard Zebra FGS. Adds on the dwell time, which is really the time
170
+ between trigger positions.
171
+ """
172
+
173
+
174
+ class PandAGridScanParams(GridScanParamsThreeD):
158
175
  """
159
176
  Params for panda constant-motion scan. Adds on the goniometer run-up distance
160
177
  """
@@ -163,53 +180,50 @@ class PandAGridScanParams(GridScanParamsCommon):
163
180
 
164
181
 
165
182
  class MotionProgram(Device):
166
- def __init__(self, prefix: str, name: str = "") -> None:
183
+ def __init__(self, prefix: str, name: str = "", has_prog_num=True) -> None:
167
184
  super().__init__(name)
168
185
  self.running = epics_signal_r(int, prefix + "PROGBITS")
169
- self.program_number = epics_signal_r(float, prefix + "CS1:PROG_NUM")
186
+ if has_prog_num:
187
+ self.program_number = epics_signal_r(float, prefix + "CS1:PROG_NUM")
188
+ else:
189
+ # Prog number PV doesn't currently exist for i02-1
190
+ self.program_number = soft_signal_r_and_setter(float, -1)[0]
170
191
 
171
192
 
172
193
  class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
173
- """Device for a general fast grid scan
194
+ """Device containing the minimal signals for a general fast grid scan.
174
195
 
175
196
  When the motion program is started, the goniometer will move in a snake-like grid trajectory,
176
- with X as the fast axis and Y as the slow axis. If Z steps isn't 0, the goniometer will
177
- then rotate in the omega direction such that it moves from the X-Y, to the X-Z plane then
178
- do a second grid scan. The detector is triggered after every x step.
179
- See https://github.com/DiamondLightSource/hyperion/wiki/Coordinate-Systems for more
197
+ with X as the fast axis and Y as the slow axis.
198
+
199
+ See ZebraFastGridScanThreeD as an example of how to implement.
180
200
  """
181
201
 
182
- def __init__(self, prefix: str, smargon_prefix: str, name: str = "") -> None:
202
+ def __init__(
203
+ self, prefix: str, motion_controller_prefix: str, name: str = ""
204
+ ) -> None:
205
+ super().__init__(name)
183
206
  self.x_steps = epics_signal_rw_rbv(int, f"{prefix}X_NUM_STEPS")
184
207
  self.y_steps = epics_signal_rw_rbv(
185
208
  int, f"{prefix}Y_NUM_STEPS"
186
209
  ) # Number of vertical steps during the first grid scan
187
- self.z_steps = epics_signal_rw_rbv(
188
- int, f"{prefix}Z_NUM_STEPS"
189
- ) # Number of vertical steps during the second grid scan, after the rotation in omega
190
210
  self.x_step_size = epics_signal_rw_rbv(float, f"{prefix}X_STEP_SIZE")
191
211
  self.y_step_size = epics_signal_rw_rbv(float, f"{prefix}Y_STEP_SIZE")
192
- self.z_step_size = epics_signal_rw_rbv(float, f"{prefix}Z_STEP_SIZE")
193
212
  self.x_start = epics_signal_rw_rbv(float, f"{prefix}X_START")
194
213
  self.y1_start = epics_signal_rw_rbv(float, f"{prefix}Y_START")
195
- self.y2_start = epics_signal_rw_rbv(float, f"{prefix}Y2_START")
196
214
  self.z1_start = epics_signal_rw_rbv(float, f"{prefix}Z_START")
197
- self.z2_start = epics_signal_rw_rbv(float, f"{prefix}Z2_START")
198
215
 
199
- self.scan_invalid = epics_signal_r(float, f"{prefix}SCAN_INVALID")
216
+ # This can be created like a regular signal instead of an abstract method
217
+ # once https://github.com/DiamondLightSource/mx-bluesky/issues/1203 is done
218
+ self.scan_invalid = self._create_scan_invalid_signal(prefix)
200
219
 
201
220
  self.run_cmd = epics_signal_x(f"{prefix}RUN.PROC")
202
221
  self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
203
222
  self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
204
223
 
205
- self.expected_images = derived_signal_r(
206
- self._calculate_expected_images,
207
- x=self.x_steps,
208
- y=self.y_steps,
209
- z=self.z_steps,
210
- )
224
+ self.expected_images = self._create_expected_images_signal()
211
225
 
212
- self.motion_program = MotionProgram(smargon_prefix)
226
+ self.motion_program = self._create_motion_program(motion_controller_prefix)
213
227
 
214
228
  self.position_counter = self._create_position_counter(prefix)
215
229
 
@@ -221,23 +235,12 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
221
235
  self.movable_params: dict[str, Signal] = {
222
236
  "x_steps": self.x_steps,
223
237
  "y_steps": self.y_steps,
224
- "z_steps": self.z_steps,
225
238
  "x_step_size_mm": self.x_step_size,
226
239
  "y_step_size_mm": self.y_step_size,
227
- "z_step_size_mm": self.z_step_size,
228
240
  "x_start_mm": self.x_start,
229
241
  "y1_start_mm": self.y1_start,
230
- "y2_start_mm": self.y2_start,
231
242
  "z1_start_mm": self.z1_start,
232
- "z2_start_mm": self.z2_start,
233
243
  }
234
- super().__init__(name)
235
-
236
- def _calculate_expected_images(self, x: int, y: int, z: int) -> int:
237
- LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
238
- first_grid = x * y
239
- second_grid = x * z
240
- return first_grid + second_grid
241
244
 
242
245
  @AsyncStatus.wrap
243
246
  async def kickoff(self):
@@ -266,25 +269,84 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
266
269
  raise
267
270
 
268
271
  @abstractmethod
269
- def _create_position_counter(self, prefix: str) -> SignalRW[int]:
270
- pass
272
+ def _create_expected_images_signal(self) -> SignalR[int]: ...
271
273
 
274
+ @abstractmethod
275
+ def _create_position_counter(self, prefix: str) -> SignalRW[int]: ...
272
276
 
273
- class ZebraFastGridScan(FastGridScanCommon[ZebraGridScanParams]):
274
- """Device for standard Zebra FGS. In this scan, the goniometer's velocity profile follows a parabolic shape between X steps,
275
- with the slowest points occuring at each X step.
277
+ # This can be created within init rather than as a separate method after https://github.com/DiamondLightSource/mx-bluesky/issues/1203
278
+ @abstractmethod
279
+ def _create_scan_invalid_signal(self, prefix: str) -> SignalR[float]: ...
280
+
281
+ # This can be created within init rather than as a separate method after https://github.com/DiamondLightSource/mx-bluesky/issues/1203
282
+ @abstractmethod
283
+ def _create_motion_program(
284
+ self, motion_controller_prefix: str
285
+ ) -> MotionProgram: ...
286
+
287
+
288
+ class FastGridScanThreeD(FastGridScanCommon[ParamType]):
289
+ """Device for standard 3D FGS.
290
+
291
+ After completeing the first grid, if Z steps isn't 0, the goniometer will
292
+ rotate in the omega direction such that it moves from the X-Y, to the X-Z plane then
293
+ do a second grid scan. The detector is triggered after every x step.
294
+ See https://github.com/DiamondLightSource/hyperion/wiki/Coordinate-Systems for more.
295
+
296
+ Subclasses must implement _create_position_counter.
276
297
  """
277
298
 
278
299
  def __init__(self, prefix: str, name: str = "") -> None:
279
300
  full_prefix = prefix + "FGS:"
280
- # Time taken to travel between X steps
281
- self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}DWELL_TIME")
282
301
 
302
+ # Number of vertical steps during the second grid scan, after the rotation in omega
303
+ self.z_steps = epics_signal_rw_rbv(int, f"{prefix}Z_NUM_STEPS")
304
+ self.z_step_size = epics_signal_rw_rbv(float, f"{prefix}Z_STEP_SIZE")
305
+ self.z2_start = epics_signal_rw_rbv(float, f"{prefix}Z2_START")
306
+ self.y2_start = epics_signal_rw_rbv(float, f"{prefix}Y2_START")
283
307
  self.x_counter = epics_signal_r(int, f"{full_prefix}X_COUNTER")
284
308
  self.y_counter = epics_signal_r(int, f"{full_prefix}Y_COUNTER")
285
309
 
286
310
  super().__init__(full_prefix, prefix, name)
287
311
 
312
+ self.movable_params["z_step_size_mm"] = self.z_step_size
313
+ self.movable_params["z2_start_mm"] = self.z2_start
314
+ self.movable_params["y2_start_mm"] = self.y2_start
315
+ self.movable_params["z_steps"] = self.z_steps
316
+
317
+ def _create_expected_images_signal(self):
318
+ return derived_signal_r(
319
+ self._calculate_expected_images,
320
+ x=self.x_steps,
321
+ y=self.y_steps,
322
+ z=self.z_steps,
323
+ )
324
+
325
+ def _calculate_expected_images(self, x: int, y: int, z: int) -> int:
326
+ LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
327
+ first_grid = x * y
328
+ second_grid = x * z
329
+ return first_grid + second_grid
330
+
331
+ def _create_scan_invalid_signal(self, prefix: str) -> SignalR[float]:
332
+ return epics_signal_r(float, f"{prefix}SCAN_INVALID")
333
+
334
+ def _create_motion_program(self, motion_controller_prefix: str):
335
+ return MotionProgram(motion_controller_prefix)
336
+
337
+
338
+ class ZebraFastGridScanThreeD(FastGridScanThreeD[ZebraGridScanParamsThreeD]):
339
+ """Device for standard Zebra 3D FGS.
340
+
341
+ In this scan, the goniometer's velocity profile follows a parabolic shape between X steps,
342
+ with the slowest points occuring at each X step.
343
+ """
344
+
345
+ def __init__(self, prefix: str, name: str = "") -> None:
346
+ full_prefix = prefix + "FGS:"
347
+ # Time taken to travel between X steps
348
+ self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}DWELL_TIME")
349
+ super().__init__(prefix, name)
288
350
  self.movable_params["dwell_time_ms"] = self.dwell_time_ms
289
351
 
290
352
  def _create_position_counter(self, prefix: str):
@@ -293,8 +355,12 @@ class ZebraFastGridScan(FastGridScanCommon[ZebraGridScanParams]):
293
355
  )
294
356
 
295
357
 
296
- class PandAFastGridScan(FastGridScanCommon[PandAGridScanParams]):
297
- """Device for panda constant-motion scan"""
358
+ class PandAFastGridScan(FastGridScanThreeD[PandAGridScanParams]):
359
+ """Device for panda constant-motion scan.
360
+
361
+ In this scan, the goniometer's velocity
362
+ is constant through each row. It doesn't slow down when going through trigger points.
363
+ """
298
364
 
299
365
  def __init__(self, prefix: str, name: str = "") -> None:
300
366
  full_prefix = prefix + "PGS:"
@@ -309,7 +375,7 @@ class PandAFastGridScan(FastGridScanCommon[PandAGridScanParams]):
309
375
  self.run_up_distance_mm = epics_signal_rw_rbv(
310
376
  float, f"{full_prefix}RUNUP_DISTANCE"
311
377
  )
312
- super().__init__(full_prefix, prefix, name)
378
+ super().__init__(prefix, name)
313
379
 
314
380
  self.movable_params["run_up_distance_mm"] = self.run_up_distance_mm
315
381
 
@@ -17,6 +17,7 @@ from ophyd_async.epics.core import (
17
17
  )
18
18
  from ophyd_async.epics.motor import Motor
19
19
 
20
+ from dodal.devices.motors import XYPitchStage
20
21
  from dodal.log import LOGGER
21
22
 
22
23
  VOLTAGE_POLLING_DELAY_S = 0.5
@@ -134,6 +135,35 @@ class MirrorVoltages(StandardReadable):
134
135
  )
135
136
 
136
137
 
138
+ class SimpleMirror(XYPitchStage):
139
+ """Simple Focusing Mirror"""
140
+
141
+ def __init__(
142
+ self,
143
+ prefix: str,
144
+ name: str = "",
145
+ x_infix: str = "X",
146
+ y_infix: str = "Y",
147
+ pitch_infix: str = "PITCH",
148
+ ):
149
+ super().__init__(
150
+ name=name,
151
+ prefix=prefix,
152
+ x_infix=x_infix,
153
+ y_infix=y_infix,
154
+ pitch_infix=pitch_infix,
155
+ )
156
+
157
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
158
+ self.type, _ = soft_signal_r_and_setter(MirrorType, MirrorType.SINGLE)
159
+
160
+ with self.add_children_as_readables():
161
+ self.yaw = Motor(prefix + "YAW")
162
+ self.bend = Motor(prefix + "BEND")
163
+ self.jack1 = Motor(prefix + "J1")
164
+ self.jack2 = Motor(prefix + "J2")
165
+
166
+
137
167
  class FocusingMirror(StandardReadable):
138
168
  """Focusing Mirror"""
139
169
 
File without changes