dls-dodal 1.61.0__py3-none-any.whl → 1.63.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 (84) hide show
  1. {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/METADATA +1 -1
  2. {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/RECORD +81 -73
  3. dls_dodal-1.63.0.dist-info/entry_points.txt +3 -0
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +1 -0
  6. dodal/beamlines/adsim.py +5 -3
  7. dodal/beamlines/b21.py +3 -1
  8. dodal/beamlines/i02_2.py +32 -0
  9. dodal/beamlines/i03.py +9 -0
  10. dodal/beamlines/i04.py +1 -1
  11. dodal/beamlines/i09.py +10 -3
  12. dodal/beamlines/i09_1.py +9 -3
  13. dodal/beamlines/i10.py +7 -69
  14. dodal/beamlines/i10_1.py +35 -0
  15. dodal/beamlines/i10_optics.py +205 -0
  16. dodal/beamlines/i15_1.py +5 -5
  17. dodal/beamlines/i17.py +50 -1
  18. dodal/beamlines/i18.py +15 -9
  19. dodal/beamlines/i19_1.py +3 -3
  20. dodal/beamlines/i19_2.py +12 -2
  21. dodal/beamlines/i19_optics.py +4 -1
  22. dodal/beamlines/i24.py +3 -3
  23. dodal/cli.py +4 -4
  24. dodal/common/visit.py +4 -4
  25. dodal/devices/aperturescatterguard.py +6 -4
  26. dodal/devices/apple2_undulator.py +211 -114
  27. dodal/devices/attenuator/filter_selections.py +6 -6
  28. dodal/devices/common_dcm.py +62 -15
  29. dodal/devices/controllers.py +8 -6
  30. dodal/devices/current_amplifiers/femto.py +4 -4
  31. dodal/devices/current_amplifiers/sr570.py +3 -3
  32. dodal/devices/fast_grid_scan.py +97 -21
  33. dodal/devices/fast_shutter.py +69 -0
  34. dodal/devices/i02_1/fast_grid_scan.py +1 -1
  35. dodal/devices/i02_2/__init__.py +0 -0
  36. dodal/devices/i03/dcm.py +4 -2
  37. dodal/devices/i04/murko_results.py +35 -14
  38. dodal/devices/i09/__init__.py +1 -2
  39. dodal/devices/i10/__init__.py +29 -0
  40. dodal/devices/i10/diagnostics.py +37 -5
  41. dodal/devices/i10/i10_apple2.py +125 -229
  42. dodal/devices/i10/slits.py +38 -6
  43. dodal/devices/i15/dcm.py +6 -44
  44. dodal/devices/i17/__init__.py +0 -0
  45. dodal/devices/i17/i17_apple2.py +51 -0
  46. dodal/devices/i19/access_controlled/__init__.py +0 -0
  47. dodal/devices/i19/{shutter.py → access_controlled/shutter.py} +7 -4
  48. dodal/devices/i19/mapt_configuration.py +38 -0
  49. dodal/devices/i19/pin_col_stages.py +170 -0
  50. dodal/devices/i22/dcm.py +2 -2
  51. dodal/devices/i24/dcm.py +2 -2
  52. dodal/devices/oav/oav_detector.py +1 -1
  53. dodal/devices/oav/oav_parameters.py +4 -4
  54. dodal/devices/oav/oav_to_redis_forwarder.py +4 -4
  55. dodal/devices/oav/pin_image_recognition/__init__.py +3 -3
  56. dodal/devices/oav/pin_image_recognition/utils.py +1 -1
  57. dodal/devices/oav/snapshots/snapshot.py +1 -1
  58. dodal/devices/oav/snapshots/snapshot_image_processing.py +12 -12
  59. dodal/devices/oav/snapshots/snapshot_with_grid.py +1 -1
  60. dodal/devices/oav/utils.py +2 -2
  61. dodal/devices/pgm.py +3 -3
  62. dodal/devices/robot.py +5 -5
  63. dodal/devices/tetramm.py +9 -5
  64. dodal/devices/thawer.py +0 -4
  65. dodal/devices/v2f.py +2 -2
  66. dodal/devices/zebra/zebra_constants_mapping.py +2 -2
  67. dodal/devices/zocalo/__init__.py +4 -4
  68. dodal/devices/zocalo/zocalo_results.py +4 -4
  69. dodal/log.py +9 -9
  70. dodal/plan_stubs/motor_utils.py +4 -4
  71. dodal/plans/configure_arm_trigger_and_disarm_detector.py +29 -7
  72. dodal/plans/save_panda.py +7 -7
  73. dodal/plans/verify_undulator_gap.py +2 -2
  74. dls_dodal-1.61.0.dist-info/entry_points.txt +0 -3
  75. dodal/beamlines/i10-1.py +0 -25
  76. dodal/devices/i09/dcm.py +0 -26
  77. {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/WHEEL +0 -0
  78. {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/licenses/LICENSE +0 -0
  79. {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/top_level.txt +0 -0
  80. /dodal/devices/areadetector/plugins/{CAM.py → cam.py} +0 -0
  81. /dodal/devices/areadetector/plugins/{MJPG.py → mjpg.py} +0 -0
  82. /dodal/devices/i18/{KBMirror.py → kb_mirror.py} +0 -0
  83. /dodal/devices/i19/{blueapi_device.py → access_controlled/blueapi_device.py} +0 -0
  84. /dodal/devices/i19/{hutch_access.py → access_controlled/hutch_access.py} +0 -0
@@ -5,19 +5,23 @@ from math import isclose
5
5
  from typing import Generic, Protocol, TypeVar
6
6
 
7
7
  import numpy as np
8
- from bluesky.protocols import Movable
8
+ from bluesky.protocols import Locatable, Location, Movable
9
9
  from ophyd_async.core import (
10
10
  AsyncStatus,
11
+ Reference,
11
12
  SignalR,
13
+ SignalRW,
12
14
  SignalW,
13
15
  StandardReadable,
14
16
  StandardReadableFormat,
15
17
  StrictEnum,
16
18
  derived_signal_rw,
17
19
  soft_signal_r_and_setter,
20
+ soft_signal_rw,
18
21
  wait_for_value,
19
22
  )
20
23
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
24
+ from ophyd_async.epics.motor import Motor
21
25
 
22
26
  from dodal.log import LOGGER
23
27
 
@@ -40,12 +44,8 @@ class Apple2PhasesVal:
40
44
 
41
45
 
42
46
  @dataclass
43
- class Apple2Val:
47
+ class Apple2Val(Apple2PhasesVal):
44
48
  gap: str
45
- top_outer: str
46
- top_inner: str
47
- btm_inner: str
48
- btm_outer: str
49
49
 
50
50
 
51
51
  class Pol(StrictEnum):
@@ -185,24 +185,24 @@ class UndulatorPhaseMotor(StandardReadable):
185
185
  name : str
186
186
  Name of the Id phase device
187
187
  """
188
- fullPV = f"{prefix}BL{infix}"
189
- self.user_setpoint = epics_signal_w(str, fullPV + "SET")
190
- self.user_setpoint_readback = epics_signal_r(float, fullPV + "DMD")
191
- fullPV = fullPV + "MTR"
188
+ full_pv = f"{prefix}BL{infix}"
189
+ self.user_setpoint = epics_signal_w(str, full_pv + "SET")
190
+ self.user_setpoint_readback = epics_signal_r(float, full_pv + "DMD")
191
+ full_pv = full_pv + "MTR"
192
192
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
193
- self.user_readback = epics_signal_r(float, fullPV + ".RBV")
193
+ self.user_readback = epics_signal_r(float, full_pv + ".RBV")
194
194
 
195
195
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
196
- self.motor_egu = epics_signal_r(str, fullPV + ".EGU")
197
- self.velocity = epics_signal_rw(float, fullPV + ".VELO")
198
-
199
- self.max_velocity = epics_signal_r(float, fullPV + ".VMAX")
200
- self.acceleration_time = epics_signal_rw(float, fullPV + ".ACCL")
201
- self.precision = epics_signal_r(int, fullPV + ".PREC")
202
- self.deadband = epics_signal_r(float, fullPV + ".RDBD")
203
- self.motor_done_move = epics_signal_r(int, fullPV + ".DMOV")
204
- self.low_limit_travel = epics_signal_rw(float, fullPV + ".LLM")
205
- self.high_limit_travel = epics_signal_rw(float, fullPV + ".HLM")
196
+ self.motor_egu = epics_signal_r(str, full_pv + ".EGU")
197
+ self.velocity = epics_signal_rw(float, full_pv + ".VELO")
198
+
199
+ self.max_velocity = epics_signal_r(float, full_pv + ".VMAX")
200
+ self.acceleration_time = epics_signal_rw(float, full_pv + ".ACCL")
201
+ self.precision = epics_signal_r(int, full_pv + ".PREC")
202
+ self.deadband = epics_signal_r(float, full_pv + ".RDBD")
203
+ self.motor_done_move = epics_signal_r(int, full_pv + ".DMOV")
204
+ self.low_limit_travel = epics_signal_rw(float, full_pv + ".LLM")
205
+ self.high_limit_travel = epics_signal_rw(float, full_pv + ".HLM")
206
206
  super().__init__(name=name)
207
207
 
208
208
 
@@ -301,7 +301,7 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
301
301
  )
302
302
 
303
303
 
304
- class Apple2Motors(StandardReadable, Movable):
304
+ class Apple2(StandardReadable, Movable):
305
305
  """
306
306
  Device representing the combined motor controls for an Apple2 undulator.
307
307
 
@@ -334,8 +334,7 @@ class Apple2Motors(StandardReadable, Movable):
334
334
  async def set(self, id_motor_values: Apple2Val) -> None:
335
335
  """
336
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.
337
+ all at the same time.
339
338
  """
340
339
 
341
340
  # Only need to check gap as the phase motors share both fault and gate with gap.
@@ -366,80 +365,76 @@ class EnergyMotorConvertor(Protocol):
366
365
  ...
367
366
 
368
367
 
369
- class Apple2(abc.ABC, StandardReadable, Movable):
370
- """
371
- Apple2 Undulator Device
368
+ Apple2Type = TypeVar("Apple2Type", bound="Apple2")
369
+
372
370
 
373
- The `Apple2` class represents an Apple 2 insertion device (undulator) used in synchrotron beamlines.
374
- This device provides additional degrees of freedom compared to standard undulators, allowing independent
375
- movement of magnet banks to produce X-rays with various polarisations and energies.
371
+ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
372
+ """
376
373
 
377
- The class is designed to manage the undulator's gap, phase motors, and polarisation settings, while
378
- abstracting hardware interactions and providing a high-level interface for beamline operations.
374
+ Abstract base class for controlling an Apple2 undulator device.
379
375
 
380
- The class is abstract and requires beamline-specific implementations for _set motor
381
- positions based on energy and polarisation.
376
+ This class manages the undulator's gap and phase motors, and provides an interface
377
+ for controlling polarisation and energy settings. It exposes derived signals for
378
+ energy and polarisation, and handles conversion between energy/polarisation and
379
+ motor positions via a user-supplied conversion callable.
382
380
 
383
381
  Attributes
384
382
  ----------
385
- apple2_motors : Apple2Motors
386
- A collection of gap and phase motor devices.
387
- energy : SignalR
388
- A soft signal for the current energy readback.
383
+ apple2 : Reference[Apple2Type]
384
+ Reference to the Apple2 device containing gap and phase motors.
385
+ energy : derived_signal_rw
386
+ Derived signal for moving and reading back energy.
389
387
  polarisation_setpoint : SignalR
390
- A soft signal for the polarisation setpoint.
391
- polarisation : SignalRW
392
- A hardware-backed signal for polarisation readback and control.
393
- lookup_tables : dict
394
- A dictionary storing lookup tables for gap and phase motor positions, used for energy and polarisation conversion.
388
+ Soft signal for the polarisation setpoint.
389
+ polarisation : derived_signal_rw
390
+ Hardware-backed signal for polarisation readback and control.
395
391
  energy_to_motor : EnergyMotorConvertor
396
- A callable that converts energy and polarisation to motor positions.
392
+ Callable that converts energy and polarisation to motor positions.
397
393
 
398
394
  Abstract Methods
399
395
  ----------------
400
- _set(value: float) -> None
396
+ _set_motors_from_energy(value: float) -> None
401
397
  Abstract method to set motor positions for a given energy and polarisation.
402
-
403
- Methods
404
- -------
405
- determine_phase_from_hardware(...) -> tuple[Pol, float]
406
- Determines the polarisation and phase value based on motor positions.
398
+ energy_to_motor : EnergyMotorConvertor
399
+ A callable that converts energy and polarisation to motor positions.
407
400
 
408
401
  Notes
409
402
  -----
410
- - This class requires beamline-specific implementations of the abstract methods.
411
- - The device supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
403
+ - Subclasses must implement `_set_motors_from_energy` for beamline-specific logic.
404
+ - LH3 polarisation is indistinguishable from LH in hardware; special handling is provided.
405
+ - Supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
412
406
  positive circular (PC), negative circular (NC), and linear arbitrary (LA).
413
407
 
414
- For more detail see
415
- `UML </_images/apple2_design.png>`__ for detail.
416
-
417
- .. figure:: /explanations/umls/apple2_design.png
418
-
419
408
  """
420
409
 
421
410
  def __init__(
422
411
  self,
423
- apple2_motors: Apple2Motors,
424
- energy_motor_convertor: EnergyMotorConvertor,
412
+ apple2: Apple2Type,
413
+ energy_to_motor_converter: EnergyMotorConvertor,
425
414
  name: str = "",
426
415
  ) -> None:
427
416
  """
428
417
 
429
418
  Parameters
430
419
  ----------
431
- id_gap: An UndulatorGap device.
432
- id_phase: An UndulatorPhaseAxes device.
433
- energy_motor_convertor: A callable that converts energy and polarisation to motor positions.
434
- name: Name of the device.
420
+ apple2: Apple2
421
+ An Apple2 device.
422
+ name: str
423
+ Name of the device.
435
424
  """
425
+ self.energy_to_motor = energy_to_motor_converter
426
+ self.apple2 = Reference(apple2)
436
427
 
437
- self.motors = apple2_motors
438
- self.energy_to_motor = energy_motor_convertor
428
+ # Store the set energy for readback.
429
+ self._energy, self._energy_set = soft_signal_r_and_setter(
430
+ float, initial_value=None, units="eV"
431
+ )
439
432
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
440
- # Store the set energy for readback.
441
- self.energy, self._set_energy_rbv = soft_signal_r_and_setter(
442
- float, initial_value=None
433
+ self.energy = derived_signal_rw(
434
+ raw_to_derived=self._read_energy,
435
+ set_derived=self._set_energy,
436
+ energy=self._energy,
437
+ derived_units="eV",
443
438
  )
444
439
 
445
440
  # Store the polarisation for setpoint. And provide readback for LH3.
@@ -447,61 +442,65 @@ class Apple2(abc.ABC, StandardReadable, Movable):
447
442
  self.polarisation_setpoint, self._polarisation_setpoint_set = (
448
443
  soft_signal_r_and_setter(Pol)
449
444
  )
450
-
451
- # Hardware backed read/write for polarisation.
452
- self.polarisation = derived_signal_rw(
453
- raw_to_derived=self._read_pol,
454
- set_derived=self._set_pol,
455
- pol=self.polarisation_setpoint,
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,
461
- )
445
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
446
+ # Hardware backed read/write for polarisation.
447
+ self.polarisation = derived_signal_rw(
448
+ raw_to_derived=self._read_pol,
449
+ set_derived=self._set_pol,
450
+ pol=self.polarisation_setpoint,
451
+ top_outer=self.apple2().phase.top_outer.user_readback,
452
+ top_inner=self.apple2().phase.top_inner.user_readback,
453
+ btm_inner=self.apple2().phase.btm_inner.user_readback,
454
+ btm_outer=self.apple2().phase.btm_outer.user_readback,
455
+ gap=self.apple2().gap.user_readback,
456
+ )
462
457
  super().__init__(name)
463
458
 
464
- def _set_pol_setpoint(self, pol: Pol) -> None:
465
- """Set the polarisation setpoint without moving hardware. The polarisation
466
- setpoint is used to determine the gap and phase motor positions when
467
- setting the energy/polarisation of the undulator."""
468
- self._polarisation_setpoint_set(pol)
469
-
470
- async def _set_pol(
471
- self,
472
- value: Pol,
473
- ) -> None:
474
- # This changes the pol setpoint and then changes polarisation via set energy.
475
- self._set_pol_setpoint(value)
476
- await self.set(await self.energy.get_value())
477
-
478
- @AsyncStatus.wrap
479
- async def set(self, value: float) -> None:
459
+ @abc.abstractmethod
460
+ async def _set_motors_from_energy(self, value: float) -> None:
461
+ """
462
+ This method should be implemented by the beamline specific ID class as the
463
+ motor positions will be different for each beamline depending on the
464
+ undulator design and the lookup table used.
480
465
  """
481
- Set should be in energy units, this will set the energy of the ID by setting the
482
- gap and phase motors to the correct position for the given energy
483
- and polarisation.
484
466
 
467
+ async def _set_energy(self, energy: float) -> None:
468
+ await self._set_motors_from_energy(energy)
469
+ self._energy_set(energy)
485
470
 
486
- Examples
487
- --------
488
- RE( id.set(888.0)) # This will set the ID to 888 eV
489
- RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
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.")
471
+ def _read_energy(self, energy: float) -> float:
472
+ """Readback for energy is just the set value."""
473
+ return energy
494
474
 
495
- @abc.abstractmethod
496
- async def _set(self, value: float) -> None:
475
+ async def _check_and_get_pol_setpoint(self) -> Pol:
497
476
  """
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.
477
+ Check the polarisation setpoint and if it is NONE try to read it from
478
+ hardware.
503
479
  """
504
480
 
481
+ pol = await self.polarisation_setpoint.get_value()
482
+
483
+ if pol == Pol.NONE:
484
+ LOGGER.warning(
485
+ "Found no setpoint for polarisation. Attempting to"
486
+ " determine polarisation from hardware..."
487
+ )
488
+ pol = await self.polarisation.get_value()
489
+ if pol == Pol.NONE:
490
+ raise ValueError(
491
+ f"Polarisation cannot be determined from hardware for {self.name}"
492
+ )
493
+ self._polarisation_setpoint_set(pol)
494
+ return pol
495
+
496
+ async def _set_pol(
497
+ self,
498
+ value: Pol,
499
+ ) -> None:
500
+ # This changes the pol setpoint and then changes polarisation via set energy.
501
+ self._polarisation_setpoint_set(value)
502
+ await self.energy.set(await self.energy.get_value())
503
+
505
504
  def _read_pol(
506
505
  self,
507
506
  pol: Pol,
@@ -605,3 +604,101 @@ class Apple2(abc.ABC, StandardReadable, Movable):
605
604
 
606
605
  LOGGER.warning("Unable to determine polarisation. Defaulting to NONE.")
607
606
  return Pol.NONE, 0.0
607
+
608
+
609
+ class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
610
+ """Base class for ID energy movable device."""
611
+
612
+ def __init__(self, name: str = "") -> None:
613
+ self.energy: Reference[SignalRW[float]]
614
+ super().__init__(name=name)
615
+
616
+ @abc.abstractmethod
617
+ @AsyncStatus.wrap
618
+ async def set(self, energy: float) -> None: ...
619
+
620
+
621
+ class BeamEnergy(StandardReadable, Movable[float]):
622
+ """
623
+ Compound device to set both ID and energy motor at the same time with an option to add an offset.
624
+ """
625
+
626
+ def __init__(
627
+ self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
628
+ ) -> None:
629
+ """
630
+ Parameters
631
+ ----------
632
+
633
+ id_energy: InsertionDeviceEnergy
634
+ An InsertionDeviceEnergy device.
635
+ mono: Motor
636
+ A Motor(energy) device.
637
+ name:
638
+ New device name.
639
+ """
640
+ super().__init__(name=name)
641
+ self._id_energy = Reference(id_energy)
642
+ self._mono_energy = Reference(mono)
643
+
644
+ self.add_readables(
645
+ [
646
+ self._id_energy().energy(),
647
+ self._mono_energy().user_readback,
648
+ ],
649
+ StandardReadableFormat.HINTED_SIGNAL,
650
+ )
651
+
652
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
653
+ self.id_energy_offset = soft_signal_rw(float, initial_value=0)
654
+
655
+ @AsyncStatus.wrap
656
+ async def set(self, energy: float) -> None:
657
+ LOGGER.info(f"Moving f{self.name} energy to {energy}.")
658
+ await asyncio.gather(
659
+ self._id_energy().set(
660
+ energy=energy + await self.id_energy_offset.get_value()
661
+ ),
662
+ self._mono_energy().set(energy),
663
+ )
664
+
665
+
666
+ class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
667
+ """Apple2 ID energy movable device."""
668
+
669
+ def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
670
+ self.energy = Reference(id_controller.energy)
671
+ super().__init__(name=name)
672
+
673
+ self.add_readables(
674
+ [
675
+ self.energy(),
676
+ ],
677
+ StandardReadableFormat.HINTED_SIGNAL,
678
+ )
679
+
680
+ @AsyncStatus.wrap
681
+ async def set(self, energy: float) -> None:
682
+ await self.energy().set(energy)
683
+
684
+
685
+ class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
686
+ """Apple2 ID polarisation movable device."""
687
+
688
+ def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
689
+ self.polarisation = Reference(id_controller.polarisation)
690
+ self.polarisation_setpoint = Reference(id_controller.polarisation_setpoint)
691
+ super().__init__(name=name)
692
+
693
+ self.add_readables([self.polarisation()], StandardReadableFormat.HINTED_SIGNAL)
694
+
695
+ @AsyncStatus.wrap
696
+ async def set(self, pol: Pol) -> None:
697
+ await self.polarisation().set(pol)
698
+
699
+ async def locate(self) -> Location[Pol]:
700
+ """Return the current polarisation"""
701
+ setpoint, readback = await asyncio.gather(
702
+ self.polarisation_setpoint().get_value(), self.polarisation().get_value()
703
+ )
704
+ return Location(setpoint=setpoint, readback=readback)
@@ -20,7 +20,7 @@ class P99FilterSelections(SubsetEnum):
20
20
  USER = "User"
21
21
 
22
22
 
23
- class I02_1FilterOneSelections(SubsetEnum):
23
+ class I02_1FilterOneSelections(SubsetEnum): # noqa: N801
24
24
  EMPTY = "Empty"
25
25
  AL8 = "Al8"
26
26
  AL15 = "Al15"
@@ -33,7 +33,7 @@ class I02_1FilterOneSelections(SubsetEnum):
33
33
  TWO_TIMES_TI500 = "2xTi500"
34
34
 
35
35
 
36
- class I02_1FilterTwoSelections(SubsetEnum):
36
+ class I02_1FilterTwoSelections(SubsetEnum): # noqa: N801
37
37
  EMPTY = "Empty"
38
38
  AL50 = "Al50"
39
39
  AL100 = "Al100"
@@ -46,7 +46,7 @@ class I02_1FilterTwoSelections(SubsetEnum):
46
46
  TWO_TIMES_TI500 = "2xTi500"
47
47
 
48
48
 
49
- class I02_1FilterThreeSelections(SubsetEnum):
49
+ class I02_1FilterThreeSelections(SubsetEnum): # noqa: N801
50
50
  EMPTY = "Empty"
51
51
  AL15 = "Al15"
52
52
  AL25 = "Al25"
@@ -59,7 +59,7 @@ class I02_1FilterThreeSelections(SubsetEnum):
59
59
  TI200 = "Ti200"
60
60
 
61
61
 
62
- class I02_1FilterFourSelections(SubsetEnum):
62
+ class I02_1FilterFourSelections(SubsetEnum): # noqa: N801
63
63
  EMPTY = "Empty"
64
64
  AL15 = "Al15"
65
65
  AL25 = "Al25"
@@ -72,7 +72,7 @@ class I02_1FilterFourSelections(SubsetEnum):
72
72
  TI500 = "Ti500"
73
73
 
74
74
 
75
- class I24_FilterOneSelections(SubsetEnum):
75
+ class I24FilterOneSelections(SubsetEnum):
76
76
  EMPTY = "Empty"
77
77
  AL12_5 = "Al12.5"
78
78
  AL25 = "Al25"
@@ -85,7 +85,7 @@ class I24_FilterOneSelections(SubsetEnum):
85
85
  TI500 = "Ti500"
86
86
 
87
87
 
88
- class I24_FilterTwoSelections(SubsetEnum):
88
+ class I24FilterTwoSelections(SubsetEnum):
89
89
  EMPTY = "Empty"
90
90
  AL100 = "Al100"
91
91
  AL200 = "Al200"
@@ -2,6 +2,7 @@ from typing import Generic, TypeVar
2
2
 
3
3
  from ophyd_async.core import (
4
4
  StandardReadable,
5
+ derived_signal_r,
5
6
  )
6
7
  from ophyd_async.epics.core import epics_signal_r
7
8
  from ophyd_async.epics.motor import Motor
@@ -31,42 +32,39 @@ Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
31
32
  Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
32
33
 
33
34
 
34
- class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
35
+ class DoubleCrystalMonochromatorBase(StandardReadable, Generic[Xtal_1, Xtal_2]):
35
36
  """
36
- Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
37
+ Base device for the double crystal monochromator (DCM), used to select the energy of the beam.
37
38
 
38
39
  Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
39
40
  each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
40
41
  This base device accounts for all combinations of this.
41
42
 
42
- This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
43
+ This device should act as a parent for beamline-specific DCM's which do not match the standard EPICS interface, it provides
44
+ only energy and the crystal configuration. Most beamlines should use DoubleCrystalMonochromator instead
43
45
 
44
46
  Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
45
- which only requires one crystal which can roll should be typed 'def my_plan(dcm: BaseDCM[RollCrystal, StationaryCrystal])`
47
+ which only requires one crystal which can roll should be typed
48
+ 'def my_plan(dcm: DoubleCrystalMonochromatorBase[RollCrystal, StationaryCrystal])`
46
49
  """
47
50
 
48
51
  def __init__(
49
52
  self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
50
53
  ) -> None:
51
54
  with self.add_children_as_readables():
52
- # Virtual motor PV's which set the physical motors so that the DCM produces requested
53
- # wavelength/energy
55
+ # Virtual motor PV's which set the physical motors so that the DCM produces requested energy
54
56
  self.energy_in_kev = Motor(prefix + "ENERGY")
55
- self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
56
-
57
- # Real motors
58
- self.bragg_in_degrees = Motor(prefix + "BRAGG")
59
- # Offset ensures that the beam exits the DCM at the same point, regardless of energy.
60
- self.offset_in_mm = Motor(prefix + "OFFSET")
61
-
62
- self.crystal_metadata_d_spacing_a = epics_signal_r(
63
- float, prefix + "DSPACING:RBV"
57
+ self.energy_in_ev = derived_signal_r(
58
+ self._convert_keV_to_eV, energy_signal=self.energy_in_kev.user_readback
64
59
  )
65
60
 
66
61
  self._make_crystals(prefix, xtal_1, xtal_2)
67
62
 
68
63
  super().__init__(name)
69
64
 
65
+ def _convert_keV_to_eV(self, energy_signal: float) -> float: # noqa: N802
66
+ return energy_signal * 1000
67
+
70
68
  # Prefix convention is different depending on whether there are one or two controllable crystals
71
69
  def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
72
70
  if StationaryCrystal not in [xtal_1, xtal_2]:
@@ -75,3 +73,52 @@ class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
75
73
  else:
76
74
  self.xtal_1 = xtal_1(prefix)
77
75
  self.xtal_2 = xtal_2(prefix)
76
+
77
+
78
+ class DoubleCrystalMonochromator(
79
+ DoubleCrystalMonochromatorBase, Generic[Xtal_1, Xtal_2]
80
+ ):
81
+ """
82
+ Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
83
+
84
+ Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
85
+ each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
86
+ This base device accounts for all combinations of this.
87
+
88
+ This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
89
+
90
+ Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan which only
91
+ requires one crystal which can roll should be typed 'def my_plan(dcm: DoubleCrystalMonochromator[RollCrystal, StationaryCrystal])`
92
+ """
93
+
94
+ def __init__(
95
+ self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
96
+ ) -> None:
97
+ super().__init__(prefix, xtal_1, xtal_2, name)
98
+ with self.add_children_as_readables():
99
+ # Virtual motor PV's which set the physical motors so that the DCM produces requested
100
+ # wavelength
101
+ self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
102
+
103
+ # Real motors
104
+ self.bragg_in_degrees = Motor(prefix + "BRAGG")
105
+ # Offset ensures that the beam exits the DCM at the same point, regardless of energy.
106
+ self.offset_in_mm = Motor(prefix + "OFFSET")
107
+
108
+
109
+ class DoubleCrystalMonochromatorWithDSpacing(
110
+ DoubleCrystalMonochromator, Generic[Xtal_1, Xtal_2]
111
+ ):
112
+ """
113
+ Adds crystal D-spacing metadata to the DoubleCrystalMonochromator class. This should be used in preference to the
114
+ DoubleCrystalMonochromator on beamlines which have a "DSPACING:RBV" pv on their DCM.
115
+ """
116
+
117
+ def __init__(
118
+ self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
119
+ ) -> None:
120
+ super().__init__(prefix, xtal_1, xtal_2, name)
121
+ with self.add_children_as_readables():
122
+ self.crystal_metadata_d_spacing_a = epics_signal_r(
123
+ float, prefix + "DSPACING:RBV"
124
+ )
@@ -1,9 +1,6 @@
1
1
  from typing import TypeVar
2
2
 
3
- from ophyd_async.epics.adcore import (
4
- ADBaseController,
5
- ADBaseIO,
6
- )
3
+ from ophyd_async.epics.adcore import ADBaseController, ADBaseIO, ADImageMode
7
4
 
8
5
  ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
9
6
 
@@ -13,8 +10,13 @@ class ConstantDeadTimeController(ADBaseController[ADBaseIOT]):
13
10
  ADBaseController with a configured constant deadtime for a driver of type ADBaseIO.
14
11
  """
15
12
 
16
- def __init__(self, driver: ADBaseIOT, deadtime: float):
17
- super().__init__(driver)
13
+ def __init__(
14
+ self,
15
+ driver: ADBaseIOT,
16
+ deadtime: float,
17
+ image_mode: ADImageMode = ADImageMode.MULTIPLE,
18
+ ):
19
+ super().__init__(driver, image_mode=image_mode)
18
20
  self.deadtime = deadtime
19
21
 
20
22
  def get_deadtime(self, exposure: float | None) -> float:
@@ -104,15 +104,15 @@ class FemtoDDPCA(CurrentAmp):
104
104
  + "\n Available gain:"
105
105
  + f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
106
106
  )
107
- SEN_setting = self.gain_conversion_table(value).name
108
- LOGGER.info(f"{self.name} gain change to {SEN_setting}:{value}")
107
+ sensitivity_setting = self.gain_conversion_table(value).name
108
+ LOGGER.info(f"{self.name} gain change to {sensitivity_setting}:{value}")
109
109
 
110
110
  await self.gain.set(
111
- value=self.gain_table[SEN_setting],
111
+ value=self.gain_table[sensitivity_setting],
112
112
  timeout=self.timeout,
113
113
  )
114
114
  # wait for current amplifier's bandpass filter to settle.
115
- await asyncio.sleep(self.raise_timetable[SEN_setting].value)
115
+ await asyncio.sleep(self.raise_timetable[sensitivity_setting].value)
116
116
 
117
117
  async def increase_gain(self, value: int = 1) -> None:
118
118
  current_gain = int((await self.get_gain()).name.split("_")[-1])
@@ -79,7 +79,7 @@ class SR570FullGainTable(Enum):
79
79
 
80
80
 
81
81
  class SR570GainToCurrentTable(float, Enum):
82
- """Conversion table for gain(sen) to current"""
82
+ """Conversion table for gain (sensitivity) to current"""
83
83
 
84
84
  SEN_1 = 1e3
85
85
  SEN_2 = 2e3
@@ -168,10 +168,10 @@ class SR570(CurrentAmp):
168
168
  + "\n Available gain:"
169
169
  + f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
170
170
  )
171
- SEN_setting = self.gain_conversion_table(value).name
171
+ sensitivity_setting = self.gain_conversion_table(value).name
172
172
  LOGGER.info(f"{self.name} gain change to {value}")
173
173
 
174
- coarse_gain, fine_gain = self.combined_table[SEN_setting].value
174
+ coarse_gain, fine_gain = self.combined_table[sensitivity_setting].value
175
175
  await asyncio.gather(
176
176
  self.fine_gain.set(value=fine_gain, timeout=self.timeout),
177
177
  self.coarse_gain.set(value=coarse_gain, timeout=self.timeout),