dls-dodal 1.59.1__py3-none-any.whl → 1.61.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 (46) hide show
  1. {dls_dodal-1.59.1.dist-info → dls_dodal-1.61.0.dist-info}/METADATA +2 -3
  2. {dls_dodal-1.59.1.dist-info → dls_dodal-1.61.0.dist-info}/RECORD +46 -25
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/__init__.py +1 -0
  5. dodal/beamlines/i15.py +242 -0
  6. dodal/beamlines/i15_1.py +156 -0
  7. dodal/beamlines/i19_1.py +3 -1
  8. dodal/beamlines/i19_2.py +1 -1
  9. dodal/devices/apple2_undulator.py +85 -52
  10. dodal/devices/areadetector/__init__.py +0 -0
  11. dodal/devices/areadetector/plugins/__init__.py +0 -0
  12. dodal/devices/attenuator/__init__.py +0 -0
  13. dodal/devices/electron_analyser/abstract/__init__.py +2 -2
  14. dodal/devices/electron_analyser/abstract/base_detector.py +13 -26
  15. dodal/devices/electron_analyser/abstract/base_driver_io.py +5 -4
  16. dodal/devices/electron_analyser/abstract/base_region.py +28 -13
  17. dodal/devices/electron_analyser/detector.py +19 -31
  18. dodal/devices/electron_analyser/specs/driver_io.py +0 -1
  19. dodal/devices/electron_analyser/vgscienta/driver_io.py +0 -1
  20. dodal/devices/fast_grid_scan.py +14 -11
  21. dodal/devices/i04/murko_results.py +24 -12
  22. dodal/devices/i10/i10_apple2.py +15 -15
  23. dodal/devices/i10/rasor/__init__.py +0 -0
  24. dodal/devices/i11/__init__.py +0 -0
  25. dodal/devices/i15/__init__.py +0 -0
  26. dodal/devices/i15/dcm.py +77 -0
  27. dodal/devices/i15/focussing_mirror.py +55 -0
  28. dodal/devices/i15/jack.py +31 -0
  29. dodal/devices/i15/laue.py +14 -0
  30. dodal/devices/i15/motors.py +27 -0
  31. dodal/devices/i15/multilayer_mirror.py +21 -0
  32. dodal/devices/i15/rail.py +13 -0
  33. dodal/devices/i18/__init__.py +0 -0
  34. dodal/devices/i22/__init__.py +0 -0
  35. dodal/devices/i24/commissioning_jungfrau.py +9 -1
  36. dodal/devices/motors.py +52 -1
  37. dodal/devices/mx_phase1/__init__.py +0 -0
  38. dodal/devices/oav/snapshots/__init__.py +0 -0
  39. dodal/devices/slits.py +18 -0
  40. dodal/devices/v2f.py +7 -7
  41. dodal/devices/xspress3/__init__.py +0 -0
  42. dodal/parameters/__init__.py +0 -0
  43. {dls_dodal-1.59.1.dist-info → dls_dodal-1.61.0.dist-info}/WHEEL +0 -0
  44. {dls_dodal-1.59.1.dist-info → dls_dodal-1.61.0.dist-info}/entry_points.txt +0 -0
  45. {dls_dodal-1.59.1.dist-info → dls_dodal-1.61.0.dist-info}/licenses/LICENSE +0 -0
  46. {dls_dodal-1.59.1.dist-info → dls_dodal-1.61.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i19_1.py CHANGED
@@ -58,6 +58,8 @@ def oav() -> OAVBeamCentreFile:
58
58
  )
59
59
 
60
60
 
61
+ # NOTE EH1 uses the Zebra 2 box. While a Zebra 1 box exists and is connected
62
+ # on the beamline, it is currently not in use
61
63
  @device_factory()
62
64
  def zebra() -> Zebra:
63
65
  """Get the i19-1 zebra device, instantiate it if it hasn't already been.
@@ -65,7 +67,7 @@ def zebra() -> Zebra:
65
67
  """
66
68
  return Zebra(
67
69
  mapping=I19_1_ZEBRA_MAPPING,
68
- prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-03:",
70
+ prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-02:",
69
71
  )
70
72
 
71
73
 
dodal/beamlines/i19_2.py CHANGED
@@ -57,7 +57,7 @@ def zebra() -> Zebra:
57
57
  """
58
58
  return Zebra(
59
59
  mapping=I19_2_ZEBRA_MAPPING,
60
- prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-01:",
60
+ prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-03:",
61
61
  )
62
62
 
63
63
 
@@ -301,9 +301,68 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
301
301
  )
302
302
 
303
303
 
304
+ class Apple2Motors(StandardReadable, Movable):
305
+ """
306
+ Device representing the combined motor controls for an Apple2 undulator.
307
+
308
+ Attributes
309
+ ----------
310
+ gap : UndulatorGap
311
+ The undulator gap motor device.
312
+ phase : UndulatorPhaseAxes
313
+ The undulator phase axes device, consisting of four phase motors.
314
+ """
315
+
316
+ def __init__(self, id_gap: UndulatorGap, id_phase: UndulatorPhaseAxes, name=""):
317
+ """
318
+ Parameters
319
+ ----------
320
+
321
+ id_gap: UndulatorGap
322
+ An UndulatorGap device.
323
+ id_phase: UndulatorPhaseAxes
324
+ An UndulatorPhaseAxes device.
325
+ name: str
326
+ Name of the device.
327
+ """
328
+ with self.add_children_as_readables():
329
+ self.gap = id_gap
330
+ self.phase = id_phase
331
+ super().__init__(name=name)
332
+
333
+ @AsyncStatus.wrap
334
+ async def set(self, id_motor_values: Apple2Val) -> None:
335
+ """
336
+ Check ID is in a movable state and set all the demand value before moving them
337
+ all at the same time. This should be modified by the beamline specific ID
338
+ class, if the ID motors has to move in a specific order.
339
+ """
340
+
341
+ # Only need to check gap as the phase motors share both fault and gate with gap.
342
+ await self.gap.raise_if_cannot_move()
343
+ await asyncio.gather(
344
+ self.phase.top_outer.user_setpoint.set(value=id_motor_values.top_outer),
345
+ self.phase.top_inner.user_setpoint.set(value=id_motor_values.top_inner),
346
+ self.phase.btm_inner.user_setpoint.set(value=id_motor_values.btm_inner),
347
+ self.phase.btm_outer.user_setpoint.set(value=id_motor_values.btm_outer),
348
+ self.gap.user_setpoint.set(value=id_motor_values.gap),
349
+ )
350
+ timeout = np.max(
351
+ await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
352
+ )
353
+ LOGGER.info(
354
+ f"Moving f{self.name} apple2 motors to {id_motor_values}, timeout = {timeout}"
355
+ )
356
+ await asyncio.gather(
357
+ self.gap.set_move.set(value=1, wait=False, timeout=timeout),
358
+ self.phase.set_move.set(value=1, wait=False, timeout=timeout),
359
+ )
360
+ await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
361
+
362
+
304
363
  class EnergyMotorConvertor(Protocol):
305
364
  def __call__(self, energy: float, pol: Pol) -> tuple[float, float]:
306
- """Protocol to provide energy to motor position convertion"""
365
+ """Protocol to provide energy to motor position conversion"""
307
366
  ...
308
367
 
309
368
 
@@ -318,15 +377,13 @@ class Apple2(abc.ABC, StandardReadable, Movable):
318
377
  The class is designed to manage the undulator's gap, phase motors, and polarisation settings, while
319
378
  abstracting hardware interactions and providing a high-level interface for beamline operations.
320
379
 
321
- The class is abstract and requires beamline-specific implementations for set motor
380
+ The class is abstract and requires beamline-specific implementations for _set motor
322
381
  positions based on energy and polarisation.
323
382
 
324
383
  Attributes
325
384
  ----------
326
- gap : UndulatorGap
327
- The gap control device for the undulator.
328
- phase : UndulatorPhaseAxes
329
- The phase control device, consisting of four phase motors.
385
+ apple2_motors : Apple2Motors
386
+ A collection of gap and phase motor devices.
330
387
  energy : SignalR
331
388
  A soft signal for the current energy readback.
332
389
  polarisation_setpoint : SignalR
@@ -340,7 +397,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
340
397
 
341
398
  Abstract Methods
342
399
  ----------------
343
- set(value: float) -> None
400
+ _set(value: float) -> None
344
401
  Abstract method to set motor positions for a given energy and polarisation.
345
402
 
346
403
  Methods
@@ -363,8 +420,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
363
420
 
364
421
  def __init__(
365
422
  self,
366
- id_gap: UndulatorGap,
367
- id_phase: UndulatorPhaseAxes,
423
+ apple2_motors: Apple2Motors,
368
424
  energy_motor_convertor: EnergyMotorConvertor,
369
425
  name: str = "",
370
426
  ) -> None:
@@ -378,8 +434,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
378
434
  name: Name of the device.
379
435
  """
380
436
 
381
- self.gap = id_gap
382
- self.phase = id_phase
437
+ self.motors = apple2_motors
383
438
  self.energy_to_motor = energy_motor_convertor
384
439
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
385
440
  # Store the set energy for readback.
@@ -398,11 +453,11 @@ class Apple2(abc.ABC, StandardReadable, Movable):
398
453
  raw_to_derived=self._read_pol,
399
454
  set_derived=self._set_pol,
400
455
  pol=self.polarisation_setpoint,
401
- top_outer=self.phase.top_outer.user_readback,
402
- top_inner=self.phase.top_inner.user_readback,
403
- btm_inner=self.phase.btm_inner.user_readback,
404
- btm_outer=self.phase.btm_outer.user_readback,
405
- gap=id_gap.user_readback,
456
+ top_outer=self.motors.phase.top_outer.user_readback,
457
+ top_inner=self.motors.phase.top_inner.user_readback,
458
+ btm_inner=self.motors.phase.btm_inner.user_readback,
459
+ btm_outer=self.motors.phase.btm_outer.user_readback,
460
+ gap=self.motors.gap.user_readback,
406
461
  )
407
462
  super().__init__(name)
408
463
 
@@ -420,24 +475,32 @@ class Apple2(abc.ABC, StandardReadable, Movable):
420
475
  self._set_pol_setpoint(value)
421
476
  await self.set(await self.energy.get_value())
422
477
 
423
- @abc.abstractmethod
424
478
  @AsyncStatus.wrap
425
479
  async def set(self, value: float) -> None:
426
480
  """
427
481
  Set should be in energy units, this will set the energy of the ID by setting the
428
482
  gap and phase motors to the correct position for the given energy
429
483
  and polarisation.
430
- This method should be implemented by the beamline specific ID class as the
431
- motor positions will be different for each beamline depending on the
432
- undulator design and the lookup table used.
433
- _set can be used to set the motor positions for the given energy and
434
- polarisation provided that all motors can be moved at the same time.
484
+
435
485
 
436
486
  Examples
437
487
  --------
438
488
  RE( id.set(888.0)) # This will set the ID to 888 eV
439
489
  RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
440
490
  """
491
+ await self._set(value)
492
+ self._set_energy_rbv(value) # Update energy after move for readback.
493
+ LOGGER.info(f"Energy set to {value} eV successfully.")
494
+
495
+ @abc.abstractmethod
496
+ async def _set(self, value: float) -> None:
497
+ """
498
+ This method should be implemented by the beamline specific ID class as the
499
+ motor positions will be different for each beamline depending on the
500
+ undulator design and the lookup table used. The set method can be
501
+ used to set the motor positions for the given energy and polarisation
502
+ provided that all motors can be moved at the same time.
503
+ """
441
504
 
442
505
  def _read_pol(
443
506
  self,
@@ -468,36 +531,6 @@ class Apple2(abc.ABC, StandardReadable, Movable):
468
531
 
469
532
  return read_pol
470
533
 
471
- async def _set(self, value: Apple2Val, energy: float) -> None:
472
- """
473
- Check ID is in a movable state and set all the demand value before moving them
474
- all at the same time. This should be modified by the beamline specific ID class
475
- , if the ID motors has to move in a specific order.
476
- """
477
-
478
- # Only need to check gap as the phase motors share both fault and gate with gap.
479
- await self.gap.raise_if_cannot_move()
480
- await asyncio.gather(
481
- self.phase.top_outer.user_setpoint.set(value=value.top_outer),
482
- self.phase.top_inner.user_setpoint.set(value=value.top_inner),
483
- self.phase.btm_inner.user_setpoint.set(value=value.btm_inner),
484
- self.phase.btm_outer.user_setpoint.set(value=value.btm_outer),
485
- self.gap.user_setpoint.set(value=value.gap),
486
- )
487
- timeout = np.max(
488
- await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
489
- )
490
- LOGGER.info(
491
- f"Moving f{self.name} energy and polorisation to {energy}, {await self.polarisation.get_value()}"
492
- + f"with motor position {value}, timeout = {timeout}"
493
- )
494
- await asyncio.gather(
495
- self.gap.set_move.set(value=1, wait=False, timeout=timeout),
496
- self.phase.set_move.set(value=1, wait=False, timeout=timeout),
497
- )
498
- await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
499
- self._set_energy_rbv(energy) # Update energy after move for readback.
500
-
501
534
  def determine_phase_from_hardware(
502
535
  self,
503
536
  top_outer: float,
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
1
  from .base_detector import (
2
- AbstractElectronAnalyserDetector,
2
+ BaseElectronAnalyserDetector,
3
3
  )
4
4
  from .base_driver_io import AbstractAnalyserDriverIO, TAbstractAnalyserDriverIO
5
5
  from .base_region import (
@@ -19,7 +19,7 @@ __all__ = [
19
19
  "TAcquisitionMode",
20
20
  "TLensMode",
21
21
  "AbstractAnalyserDriverIO",
22
- "AbstractElectronAnalyserDetector",
22
+ "BaseElectronAnalyserDetector",
23
23
  "AbstractAnalyserDriverIO",
24
24
  "TAbstractAnalyserDriverIO",
25
25
  ]
@@ -1,4 +1,3 @@
1
- from abc import abstractmethod
2
1
  from typing import Generic
3
2
 
4
3
  from bluesky.protocols import Reading, Triggerable
@@ -9,14 +8,14 @@ from ophyd_async.core import (
9
8
  AsyncStatus,
10
9
  Device,
11
10
  )
11
+ from ophyd_async.epics.adcore import ADBaseController
12
12
 
13
- from dodal.devices.controllers import ConstantDeadTimeController
14
13
  from dodal.devices.electron_analyser.abstract.base_driver_io import (
15
14
  TAbstractAnalyserDriverIO,
16
15
  )
17
16
 
18
17
 
19
- class AbstractElectronAnalyserDetector(
18
+ class BaseElectronAnalyserDetector(
20
19
  Device,
21
20
  Triggerable,
22
21
  AsyncReadable,
@@ -34,43 +33,31 @@ class AbstractElectronAnalyserDetector(
34
33
 
35
34
  def __init__(
36
35
  self,
37
- driver: TAbstractAnalyserDriverIO,
36
+ controller: ADBaseController[TAbstractAnalyserDriverIO],
38
37
  name: str = "",
39
38
  ):
40
- self.controller = ConstantDeadTimeController(driver, 0)
39
+ self._controller = controller
41
40
  super().__init__(name)
42
41
 
43
42
  @AsyncStatus.wrap
44
43
  async def trigger(self) -> None:
45
- await self.controller.arm()
46
- await self.controller.wait_for_idle()
44
+ await self._controller.arm()
45
+ await self._controller.wait_for_idle()
47
46
 
48
47
  async def read(self) -> dict[str, Reading]:
49
- return await self.driver.read()
48
+ return await self._controller.driver.read()
50
49
 
51
50
  async def describe(self) -> dict[str, DataKey]:
52
- data = await self.driver.describe()
51
+ data = await self._controller.driver.describe()
53
52
  # Correct the shape for image
54
- prefix = self.driver.name + "-"
55
- energy_size = len(await self.driver.energy_axis.get_value())
56
- angle_size = len(await self.driver.angle_axis.get_value())
53
+ prefix = self._controller.driver.name + "-"
54
+ energy_size = len(await self._controller.driver.energy_axis.get_value())
55
+ angle_size = len(await self._controller.driver.angle_axis.get_value())
57
56
  data[prefix + "image"]["shape"] = [angle_size, energy_size]
58
57
  return data
59
58
 
60
59
  async def read_configuration(self) -> dict[str, Reading]:
61
- return await self.driver.read_configuration()
60
+ return await self._controller.driver.read_configuration()
62
61
 
63
62
  async def describe_configuration(self) -> dict[str, DataKey]:
64
- return await self.driver.describe_configuration()
65
-
66
- @property
67
- @abstractmethod
68
- def driver(self) -> TAbstractAnalyserDriverIO:
69
- """
70
- Define common property for all implementations to access the driver. Some
71
- implementations will store this as a reference so it doesn't have conflicting
72
- parents.
73
-
74
- Returns:
75
- instance of the driver.
76
- """
63
+ return await self._controller.driver.describe_configuration()
@@ -153,11 +153,12 @@ class AbstractAnalyserDriverIO(
153
153
  self.energy_source.selected_source.set(region.excitation_energy_source)
154
154
  excitation_energy = await self.energy_source.energy.get_value()
155
155
 
156
- # Copy region so doesn't alter the actual region and switch to kinetic energy
157
- ke_region = region.model_copy()
158
- ke_region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
159
-
156
+ # Switch to kinetic energy as epics doesn't support BINDING.
157
+ ke_region = region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
160
158
  await self._set_region(ke_region)
159
+ # Set the true energy mode from original region so binding_energy_axis can be
160
+ # calculated correctly.
161
+ await self.energy_mode.set(region.energy_mode)
161
162
 
162
163
  @abstractmethod
163
164
  async def _set_region(self, ke_region: TAbstractBaseRegion):
@@ -1,7 +1,7 @@
1
1
  import re
2
2
  from abc import ABC
3
3
  from collections.abc import Callable
4
- from typing import Generic, TypeVar
4
+ from typing import Generic, Self, TypeVar
5
5
 
6
6
  from pydantic import BaseModel, Field, model_validator
7
7
 
@@ -88,28 +88,43 @@ class AbstractBaseRegion(
88
88
  return self.energy_mode == EnergyMode.KINETIC
89
89
 
90
90
  def switch_energy_mode(
91
- self, energy_mode: EnergyMode, excitation_energy: float
92
- ) -> None:
91
+ self, energy_mode: EnergyMode, excitation_energy: float, copy: bool = True
92
+ ) -> Self:
93
93
  """
94
- Switch region to new energy mode: Kinetic or Binding. Updates the low_energy,
95
- centre_energy, high_energy, and energy_mode, only if it switches to a new one.
94
+ Switch region with to a new energy mode with a new energy mode: Kinetic or Binding.
95
+ It caculates new values for low_energy, centre_energy, high_energy, via the
96
+ excitation enerrgy. It doesn't calculate anything if the region is already of
97
+ the same energy mode.
96
98
 
97
99
  Parameters:
98
- energy_mode: mode you want to switch the region to.
99
- excitation_energy: the energy to calculate the new values of low_energy,
100
- centre_energy, and high_energy.
100
+ energy_mode: Mode you want to switch the region to.
101
+ excitation_energy: Energy conversion for low_energy, centre_energy, and
102
+ high_energy for new energy mode.
103
+ copy: Defaults to True. If true, create a copy of this region for the new
104
+ energy_mode and return it. If False, alter this region for the
105
+ energy_mode and return it self.
106
+
107
+ Returns:
108
+ Region with selected energy mode and new calculated energy values.
101
109
  """
110
+ switched_r = self.model_copy() if copy else self
102
111
  conv = (
103
112
  to_binding_energy
104
113
  if energy_mode == EnergyMode.BINDING
105
114
  else to_kinetic_energy
106
115
  )
107
- self.low_energy = conv(self.low_energy, self.energy_mode, excitation_energy)
108
- self.centre_energy = conv(
109
- self.centre_energy, self.energy_mode, excitation_energy
116
+ switched_r.low_energy = conv(
117
+ switched_r.low_energy, switched_r.energy_mode, excitation_energy
110
118
  )
111
- self.high_energy = conv(self.high_energy, self.energy_mode, excitation_energy)
112
- self.energy_mode = energy_mode
119
+ switched_r.centre_energy = conv(
120
+ switched_r.centre_energy, switched_r.energy_mode, excitation_energy
121
+ )
122
+ switched_r.high_energy = conv(
123
+ switched_r.high_energy, switched_r.energy_mode, excitation_energy
124
+ )
125
+ switched_r.energy_mode = energy_mode
126
+
127
+ return switched_r
113
128
 
114
129
  @model_validator(mode="before")
115
130
  @classmethod
@@ -1,14 +1,13 @@
1
1
  from typing import Generic, TypeVar
2
2
 
3
3
  from bluesky.protocols import Stageable
4
- from ophyd_async.core import (
5
- AsyncStatus,
6
- Reference,
7
- )
4
+ from ophyd_async.core import AsyncStatus
5
+ from ophyd_async.epics.adcore import ADBaseController
8
6
 
9
7
  from dodal.common.data_util import load_json_file_to_class
8
+ from dodal.devices.controllers import ConstantDeadTimeController
10
9
  from dodal.devices.electron_analyser.abstract.base_detector import (
11
- AbstractElectronAnalyserDetector,
10
+ BaseElectronAnalyserDetector,
12
11
  )
13
12
  from dodal.devices.electron_analyser.abstract.base_driver_io import (
14
13
  TAbstractAnalyserDriverIO,
@@ -20,35 +19,27 @@ from dodal.devices.electron_analyser.abstract.base_region import (
20
19
 
21
20
 
22
21
  class ElectronAnalyserRegionDetector(
23
- AbstractElectronAnalyserDetector[TAbstractAnalyserDriverIO],
22
+ BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
24
23
  Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
25
24
  ):
26
25
  """
27
26
  Extends electron analyser detector to configure specific region settings before data
28
- acqusition. This object must be passed in a driver and store it as a reference. It
29
- is designed to only exist inside a plan.
27
+ acquisition. It is designed to only exist inside a plan.
30
28
  """
31
29
 
32
30
  def __init__(
33
31
  self,
34
- driver: TAbstractAnalyserDriverIO,
32
+ controller: ADBaseController[TAbstractAnalyserDriverIO],
35
33
  region: TAbstractBaseRegion,
36
34
  name: str = "",
37
35
  ):
38
- self._driver_ref = Reference(driver)
39
36
  self.region = region
40
- super().__init__(driver, name)
41
-
42
- @property
43
- def driver(self) -> TAbstractAnalyserDriverIO:
44
- # Store as a reference, this implementation will be given a driver so needs to
45
- # make sure we don't get conflicting parents.
46
- return self._driver_ref()
37
+ super().__init__(controller, name)
47
38
 
48
39
  @AsyncStatus.wrap
49
40
  async def trigger(self) -> None:
50
41
  # Configure region parameters on the driver first before data collection.
51
- await self.driver.set(self.region)
42
+ await self._controller.driver.set(self.region)
52
43
  await super().trigger()
53
44
 
54
45
 
@@ -59,7 +50,7 @@ TElectronAnalyserRegionDetector = TypeVar(
59
50
 
60
51
 
61
52
  class ElectronAnalyserDetector(
62
- AbstractElectronAnalyserDetector[TAbstractAnalyserDriverIO],
53
+ BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
63
54
  Stageable,
64
55
  Generic[
65
56
  TAbstractAnalyserDriverIO,
@@ -79,16 +70,11 @@ class ElectronAnalyserDetector(
79
70
  driver: TAbstractAnalyserDriverIO,
80
71
  name: str = "",
81
72
  ):
82
- # Pass in driver
83
- self._driver = driver
73
+ # Save driver as direct child so participates with connect()
74
+ self.driver = driver
84
75
  self._sequence_class = sequence_class
85
- super().__init__(self.driver, name)
86
-
87
- @property
88
- def driver(self) -> TAbstractAnalyserDriverIO:
89
- # This implementation creates the driver and wants this to be the parent so it
90
- # can be used with connect() method.
91
- return self._driver
76
+ controller = ConstantDeadTimeController[TAbstractAnalyserDriverIO](driver, 0)
77
+ super().__init__(controller, name)
92
78
 
93
79
  @AsyncStatus.wrap
94
80
  async def stage(self) -> None:
@@ -103,13 +89,13 @@ class ElectronAnalyserDetector(
103
89
  Raises:
104
90
  Any exceptions raised by the driver's stage or controller's disarm methods.
105
91
  """
106
- await self.controller.disarm()
92
+ await self._controller.disarm()
107
93
  await self.driver.stage()
108
94
 
109
95
  @AsyncStatus.wrap
110
96
  async def unstage(self) -> None:
111
97
  """Disarm the detector."""
112
- await self.controller.disarm()
98
+ await self._controller.disarm()
113
99
  await self.driver.unstage()
114
100
 
115
101
  def load_sequence(self, filename: str) -> TAbstractBaseSequence:
@@ -144,7 +130,9 @@ class ElectronAnalyserDetector(
144
130
  seq = self.load_sequence(filename)
145
131
  regions = seq.get_enabled_regions() if enabled_only else seq.regions
146
132
  return [
147
- ElectronAnalyserRegionDetector(self.driver, r, self.name + "_" + r.name)
133
+ ElectronAnalyserRegionDetector(
134
+ self._controller, r, self.name + "_" + r.name
135
+ )
148
136
  for r in regions
149
137
  ]
150
138
 
@@ -66,7 +66,6 @@ class SpecsAnalyserDriverIO(
66
66
  async def _set_region(self, ke_region: SpecsRegion[TLensMode, TPsuMode]):
67
67
  await asyncio.gather(
68
68
  self.region_name.set(ke_region.name),
69
- self.energy_mode.set(ke_region.energy_mode),
70
69
  self.low_energy.set(ke_region.low_energy),
71
70
  self.high_energy.set(ke_region.high_energy),
72
71
  self.slices.set(ke_region.slices),
@@ -74,7 +74,6 @@ class VGScientaAnalyserDriverIO(
74
74
  async def _set_region(self, ke_region: VGScientaRegion[TLensMode, TPassEnergyEnum]):
75
75
  await asyncio.gather(
76
76
  self.region_name.set(ke_region.name),
77
- self.energy_mode.set(ke_region.energy_mode),
78
77
  self.low_energy.set(ke_region.low_energy),
79
78
  self.centre_energy.set(ke_region.centre_energy),
80
79
  self.high_energy.set(ke_region.high_energy),
@@ -296,15 +296,15 @@ class FastGridScanThreeD(FastGridScanCommon[ParamType]):
296
296
  Subclasses must implement _create_position_counter.
297
297
  """
298
298
 
299
- def __init__(self, prefix: str, name: str = "") -> None:
300
- full_prefix = prefix + "FGS:"
299
+ def __init__(self, prefix: str, infix: str, name: str = "") -> None:
300
+ full_prefix = prefix + infix
301
301
 
302
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")
307
- self.x_counter = epics_signal_r(int, f"{full_prefix}X_COUNTER")
303
+ self.z_steps = epics_signal_rw_rbv(int, f"{full_prefix}Z_NUM_STEPS")
304
+ self.z_step_size = epics_signal_rw_rbv(float, f"{full_prefix}Z_STEP_SIZE")
305
+ self.z2_start = epics_signal_rw_rbv(float, f"{full_prefix}Z2_START")
306
+ self.y2_start = epics_signal_rw_rbv(float, f"{full_prefix}Y2_START")
307
+ # panda does not have x counter
308
308
  self.y_counter = epics_signal_r(int, f"{full_prefix}Y_COUNTER")
309
309
 
310
310
  super().__init__(full_prefix, prefix, name)
@@ -343,10 +343,12 @@ class ZebraFastGridScanThreeD(FastGridScanThreeD[ZebraGridScanParamsThreeD]):
343
343
  """
344
344
 
345
345
  def __init__(self, prefix: str, name: str = "") -> None:
346
- full_prefix = prefix + "FGS:"
346
+ infix = "FGS:"
347
+ full_prefix = prefix + infix
347
348
  # Time taken to travel between X steps
348
349
  self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}DWELL_TIME")
349
- super().__init__(prefix, name)
350
+ self.x_counter = epics_signal_r(int, f"{full_prefix}X_COUNTER")
351
+ super().__init__(prefix, infix, name)
350
352
  self.movable_params["dwell_time_ms"] = self.dwell_time_ms
351
353
 
352
354
  def _create_position_counter(self, prefix: str):
@@ -363,7 +365,8 @@ class PandAFastGridScan(FastGridScanThreeD[PandAGridScanParams]):
363
365
  """
364
366
 
365
367
  def __init__(self, prefix: str, name: str = "") -> None:
366
- full_prefix = prefix + "PGS:"
368
+ infix = "PGS:"
369
+ full_prefix = prefix + infix
367
370
  self.time_between_x_steps_ms = (
368
371
  epics_signal_rw_rbv( # Used by motion controller to set goniometer velocity
369
372
  float, f"{full_prefix}TIME_BETWEEN_X_STEPS"
@@ -375,7 +378,7 @@ class PandAFastGridScan(FastGridScanThreeD[PandAGridScanParams]):
375
378
  self.run_up_distance_mm = epics_signal_rw_rbv(
376
379
  float, f"{full_prefix}RUNUP_DISTANCE"
377
380
  )
378
- super().__init__(prefix, name)
381
+ super().__init__(prefix, infix, name)
379
382
 
380
383
  self.movable_params["run_up_distance_mm"] = self.run_up_distance_mm
381
384