dls-dodal 1.62.0__py3-none-any.whl → 1.64.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 (92) hide show
  1. {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/RECORD +89 -76
  3. dls_dodal-1.64.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/i07.py +21 -0
  11. dodal/beamlines/i09.py +11 -4
  12. dodal/beamlines/i09_1.py +10 -4
  13. dodal/beamlines/i09_2.py +30 -0
  14. dodal/beamlines/i10.py +7 -69
  15. dodal/beamlines/i10_1.py +35 -0
  16. dodal/beamlines/i10_optics.py +231 -0
  17. dodal/beamlines/i15_1.py +5 -5
  18. dodal/beamlines/i17.py +60 -1
  19. dodal/beamlines/i18.py +15 -9
  20. dodal/beamlines/i19_1.py +3 -3
  21. dodal/beamlines/i19_2.py +2 -2
  22. dodal/beamlines/i19_optics.py +4 -1
  23. dodal/beamlines/i21.py +31 -1
  24. dodal/beamlines/i24.py +3 -3
  25. dodal/cli.py +7 -7
  26. dodal/common/visit.py +4 -4
  27. dodal/devices/aperturescatterguard.py +6 -4
  28. dodal/devices/apple2_undulator.py +225 -126
  29. dodal/devices/attenuator/filter_selections.py +6 -6
  30. dodal/devices/b07_1/ccmc.py +1 -1
  31. dodal/devices/common_dcm.py +63 -16
  32. dodal/devices/current_amplifiers/femto.py +4 -4
  33. dodal/devices/current_amplifiers/sr570.py +3 -3
  34. dodal/devices/fast_grid_scan.py +4 -4
  35. dodal/devices/fast_shutter.py +19 -7
  36. dodal/devices/i02_2/__init__.py +0 -0
  37. dodal/devices/i03/dcm.py +4 -2
  38. dodal/devices/i03/undulator_dcm.py +1 -1
  39. dodal/devices/i04/murko_results.py +35 -14
  40. dodal/devices/i07/__init__.py +0 -0
  41. dodal/devices/i07/dcm.py +33 -0
  42. dodal/devices/i09/__init__.py +1 -2
  43. dodal/devices/i09_1_shared/__init__.py +3 -0
  44. dodal/devices/i09_1_shared/hard_undulator_functions.py +111 -0
  45. dodal/devices/i10/__init__.py +29 -0
  46. dodal/devices/i10/diagnostics.py +37 -5
  47. dodal/devices/i10/i10_apple2.py +125 -229
  48. dodal/devices/i10/slits.py +38 -6
  49. dodal/devices/i15/dcm.py +7 -46
  50. dodal/devices/i17/__init__.py +0 -0
  51. dodal/devices/i17/i17_apple2.py +51 -0
  52. dodal/devices/i19/access_controlled/__init__.py +0 -0
  53. dodal/devices/i19/{shutter.py → access_controlled/shutter.py} +7 -4
  54. dodal/devices/i22/dcm.py +3 -3
  55. dodal/devices/i24/dcm.py +2 -2
  56. dodal/devices/oav/oav_detector.py +1 -1
  57. dodal/devices/oav/oav_parameters.py +4 -4
  58. dodal/devices/oav/oav_to_redis_forwarder.py +4 -4
  59. dodal/devices/oav/pin_image_recognition/__init__.py +3 -3
  60. dodal/devices/oav/pin_image_recognition/utils.py +1 -1
  61. dodal/devices/oav/snapshots/snapshot.py +1 -1
  62. dodal/devices/oav/snapshots/snapshot_image_processing.py +12 -12
  63. dodal/devices/oav/snapshots/snapshot_with_grid.py +1 -1
  64. dodal/devices/oav/utils.py +2 -2
  65. dodal/devices/pgm.py +3 -3
  66. dodal/devices/robot.py +5 -5
  67. dodal/devices/tetramm.py +9 -5
  68. dodal/devices/thawer.py +0 -4
  69. dodal/devices/util/lookup_tables.py +8 -2
  70. dodal/devices/v2f.py +2 -2
  71. dodal/devices/zebra/zebra_constants_mapping.py +2 -2
  72. dodal/devices/zocalo/__init__.py +4 -4
  73. dodal/devices/zocalo/zocalo_results.py +4 -4
  74. dodal/log.py +9 -9
  75. dodal/plan_stubs/motor_utils.py +4 -4
  76. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -2
  77. dodal/plans/save_panda.py +7 -7
  78. dodal/plans/verify_undulator_gap.py +4 -4
  79. dodal/testing/fixtures/__init__.py +0 -0
  80. dodal/testing/fixtures/run_engine.py +46 -0
  81. dodal/testing/fixtures/utils.py +57 -0
  82. dls_dodal-1.62.0.dist-info/entry_points.txt +0 -3
  83. dodal/beamlines/i10-1.py +0 -25
  84. dodal/devices/i09/dcm.py +0 -26
  85. {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/WHEEL +0 -0
  86. {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/licenses/LICENSE +0 -0
  87. {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/top_level.txt +0 -0
  88. /dodal/devices/areadetector/plugins/{CAM.py → cam.py} +0 -0
  89. /dodal/devices/areadetector/plugins/{MJPG.py → mjpg.py} +0 -0
  90. /dodal/devices/i18/{KBMirror.py → kb_mirror.py} +0 -0
  91. /dodal/devices/i19/{blueapi_device.py → access_controlled/blueapi_device.py} +0 -0
  92. /dodal/devices/i19/{hutch_access.py → access_controlled/hutch_access.py} +0 -0
dodal/cli.py CHANGED
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
 
5
5
  import click
6
6
  from bluesky.run_engine import RunEngine
7
- from ophyd_async.core import NotConnected, StaticPathProvider, UUIDFilenameProvider
7
+ from ophyd_async.core import NotConnectedError, StaticPathProvider, UUIDFilenameProvider
8
8
  from ophyd_async.plan_stubs import ensure_connected
9
9
 
10
10
  from dodal.beamlines import all_beamline_names, module_name_for_beamline
@@ -58,7 +58,7 @@ def connect(beamline: str, all: bool, sim_backend: bool) -> None:
58
58
 
59
59
  # We need to make a RunEngine to allow ophyd-async devices to connect.
60
60
  # See https://blueskyproject.io/ophyd-async/main/explanations/event-loop-choice.html
61
- RE = RunEngine(call_returns_result=True)
61
+ run_engine = RunEngine(call_returns_result=True)
62
62
 
63
63
  print(f"Attempting connection to {beamline} (using {full_module_path})")
64
64
 
@@ -71,7 +71,7 @@ def connect(beamline: str, all: bool, sim_backend: bool) -> None:
71
71
  fake_with_ophyd_sim=sim_backend,
72
72
  wait_for_connection=False,
73
73
  )
74
- devices, connect_exceptions = _connect_devices(RE, devices, sim_backend)
74
+ devices, connect_exceptions = _connect_devices(run_engine, devices, sim_backend)
75
75
 
76
76
  # Inform user of successful connections
77
77
  _report_successful_devices(devices, sim_backend)
@@ -79,7 +79,7 @@ def connect(beamline: str, all: bool, sim_backend: bool) -> None:
79
79
  # If exceptions have occurred, this will print details of the relevant PVs
80
80
  exceptions = {**instance_exceptions, **connect_exceptions}
81
81
  if len(exceptions) > 0:
82
- raise NotConnected(exceptions)
82
+ raise NotConnectedError(exceptions)
83
83
 
84
84
 
85
85
  def _report_successful_devices(
@@ -96,7 +96,7 @@ def _report_successful_devices(
96
96
 
97
97
 
98
98
  def _connect_devices(
99
- RE: RunEngine,
99
+ run_engine: RunEngine,
100
100
  devices: Mapping[str, AnyDevice],
101
101
  sim_backend: bool,
102
102
  ) -> tuple[Mapping[str, AnyDevice], Mapping[str, Exception]]:
@@ -112,8 +112,8 @@ def _connect_devices(
112
112
 
113
113
  # Connect ophyd-async devices
114
114
  try:
115
- RE(ensure_connected(*ophyd_async_devices.values(), mock=sim_backend))
116
- except NotConnected as ex:
115
+ run_engine(ensure_connected(*ophyd_async_devices.values(), mock=sim_backend))
116
+ except NotConnectedError as ex:
117
117
  exceptions = {**exceptions, **ex.sub_errors}
118
118
 
119
119
  # Only return the subset of devices that haven't raised an exception
dodal/common/visit.py CHANGED
@@ -4,7 +4,7 @@ from typing import Literal
4
4
 
5
5
  from aiohttp import ClientSession
6
6
  from ophyd_async.core import FilenameProvider, PathInfo
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, Field
8
8
 
9
9
  from dodal.common.types import UpdatingPathProvider
10
10
  from dodal.log import LOGGER
@@ -20,7 +20,7 @@ class DataCollectionIdentifier(BaseModel):
20
20
  Should be always incrementing, unique per-visit, co-ordinated with any other scan engines.
21
21
  """
22
22
 
23
- collectionNumber: int
23
+ collection_number: int = Field(alias="collectionNumber")
24
24
 
25
25
 
26
26
  class DirectoryServiceClient(ABC):
@@ -46,7 +46,7 @@ class DiamondFilenameProvider(FilenameProvider):
46
46
  def __call__(self, device_name: str | None = None):
47
47
  assert device_name, "Diamond filename requires device_name to be passed"
48
48
  assert self.collectionId is not None
49
- return f"{self._beamline}-{self.collectionId.collectionNumber}-{device_name}"
49
+ return f"{self._beamline}-{self.collectionId.collection_number}-{device_name}"
50
50
 
51
51
 
52
52
  class RemoteDirectoryServiceClient(DirectoryServiceClient):
@@ -143,7 +143,7 @@ class StaticVisitPathProvider(UpdatingPathProvider):
143
143
 
144
144
  async def data_session(self) -> str:
145
145
  collection = await self._client.get_current_collection()
146
- return f"{self._beamline}-{collection.collectionNumber}"
146
+ return f"{self._beamline}-{collection.collection_number}"
147
147
 
148
148
  def __call__(self, device_name: str | None = None) -> PathInfo:
149
149
  assert device_name, "Must call PathProvider with device_name"
@@ -18,7 +18,7 @@ from dodal.devices.aperture import Aperture
18
18
  from dodal.devices.motors import XYStage
19
19
 
20
20
 
21
- class InvalidApertureMove(Exception):
21
+ class InvalidApertureMoveError(Exception):
22
22
  pass
23
23
 
24
24
 
@@ -242,7 +242,7 @@ class ApertureScatterguard(StandardReadable, Preparable):
242
242
  diff_on_z = abs(current_ap_z - expected_z_position)
243
243
  aperture_z_tolerance = self._tolerances.aperture_z
244
244
  if diff_on_z > aperture_z_tolerance:
245
- raise InvalidApertureMove(
245
+ raise InvalidApertureMoveError(
246
246
  f"Current aperture z ({current_ap_z}), outside of tolerance ({aperture_z_tolerance}) from target ({expected_z_position})."
247
247
  )
248
248
 
@@ -256,7 +256,7 @@ class ApertureScatterguard(StandardReadable, Preparable):
256
256
  for axis in all_axes:
257
257
  axis_stationary = await axis.motor_done_move.get_value()
258
258
  if not axis_stationary:
259
- raise InvalidApertureMove(
259
+ raise InvalidApertureMoveError(
260
260
  f"{axis.name} is still moving. Wait for it to finish before"
261
261
  "triggering another move."
262
262
  )
@@ -294,7 +294,9 @@ class ApertureScatterguard(StandardReadable, Preparable):
294
294
  ):
295
295
  return ApertureValue.OUT_OF_BEAM
296
296
 
297
- raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
297
+ raise InvalidApertureMoveError(
298
+ "Current aperture/scatterguard state unrecognised"
299
+ )
298
300
 
299
301
  async def _safe_move_whilst_in_beam(self, position: AperturePosition):
300
302
  """
@@ -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
 
@@ -326,38 +326,39 @@ class Apple2Motors(StandardReadable, Movable):
326
326
  Name of the device.
327
327
  """
328
328
  with self.add_children_as_readables():
329
- self.gap = id_gap
330
- self.phase = id_phase
329
+ self.gap = Reference(id_gap)
330
+ self.phase = Reference(id_phase)
331
331
  super().__init__(name=name)
332
332
 
333
333
  @AsyncStatus.wrap
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.
342
- await self.gap.raise_if_cannot_move()
341
+ await self.gap().raise_if_cannot_move()
343
342
  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),
343
+ self.phase().top_outer.user_setpoint.set(value=id_motor_values.top_outer),
344
+ self.phase().top_inner.user_setpoint.set(value=id_motor_values.top_inner),
345
+ self.phase().btm_inner.user_setpoint.set(value=id_motor_values.btm_inner),
346
+ self.phase().btm_outer.user_setpoint.set(value=id_motor_values.btm_outer),
347
+ self.gap().user_setpoint.set(value=id_motor_values.gap),
349
348
  )
350
349
  timeout = np.max(
351
- await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
350
+ await asyncio.gather(self.gap().get_timeout(), self.phase().get_timeout())
352
351
  )
353
352
  LOGGER.info(
354
353
  f"Moving f{self.name} apple2 motors to {id_motor_values}, timeout = {timeout}"
355
354
  )
356
355
  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),
356
+ self.gap().set_move.set(value=1, wait=False, timeout=timeout),
357
+ self.phase().set_move.set(value=1, wait=False, timeout=timeout),
358
+ )
359
+ await wait_for_value(
360
+ self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
359
361
  )
360
- await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
361
362
 
362
363
 
363
364
  class EnergyMotorConvertor(Protocol):
@@ -366,80 +367,76 @@ class EnergyMotorConvertor(Protocol):
366
367
  ...
367
368
 
368
369
 
369
- class Apple2(abc.ABC, StandardReadable, Movable):
370
- """
371
- Apple2 Undulator Device
370
+ Apple2Type = TypeVar("Apple2Type", bound="Apple2")
372
371
 
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.
376
372
 
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.
373
+ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
374
+ """
375
+
376
+ Abstract base class for controlling an Apple2 undulator device.
379
377
 
380
- The class is abstract and requires beamline-specific implementations for _set motor
381
- positions based on energy and polarisation.
378
+ This class manages the undulator's gap and phase motors, and provides an interface
379
+ for controlling polarisation and energy settings. It exposes derived signals for
380
+ energy and polarisation, and handles conversion between energy/polarisation and
381
+ motor positions via a user-supplied conversion callable.
382
382
 
383
383
  Attributes
384
384
  ----------
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.
385
+ apple2 : Reference[Apple2Type]
386
+ Reference to the Apple2 device containing gap and phase motors.
387
+ energy : derived_signal_rw
388
+ Derived signal for moving and reading back energy.
389
389
  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.
390
+ Soft signal for the polarisation setpoint.
391
+ polarisation : derived_signal_rw
392
+ Hardware-backed signal for polarisation readback and control.
395
393
  energy_to_motor : EnergyMotorConvertor
396
- A callable that converts energy and polarisation to motor positions.
394
+ Callable that converts energy and polarisation to motor positions.
397
395
 
398
396
  Abstract Methods
399
397
  ----------------
400
- _set(value: float) -> None
398
+ _set_motors_from_energy(value: float) -> None
401
399
  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.
400
+ energy_to_motor : EnergyMotorConvertor
401
+ A callable that converts energy and polarisation to motor positions.
407
402
 
408
403
  Notes
409
404
  -----
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),
405
+ - Subclasses must implement `_set_motors_from_energy` for beamline-specific logic.
406
+ - LH3 polarisation is indistinguishable from LH in hardware; special handling is provided.
407
+ - Supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
412
408
  positive circular (PC), negative circular (NC), and linear arbitrary (LA).
413
409
 
414
- For more detail see
415
- `UML </_images/apple2_design.png>`__ for detail.
416
-
417
- .. figure:: /explanations/umls/apple2_design.png
418
-
419
410
  """
420
411
 
421
412
  def __init__(
422
413
  self,
423
- apple2_motors: Apple2Motors,
424
- energy_motor_convertor: EnergyMotorConvertor,
414
+ apple2: Apple2Type,
415
+ energy_to_motor_converter: EnergyMotorConvertor,
425
416
  name: str = "",
426
417
  ) -> None:
427
418
  """
428
419
 
429
420
  Parameters
430
421
  ----------
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.
422
+ apple2: Apple2
423
+ An Apple2 device.
424
+ name: str
425
+ Name of the device.
435
426
  """
427
+ self.energy_to_motor = energy_to_motor_converter
428
+ self.apple2 = Reference(apple2)
436
429
 
437
- self.motors = apple2_motors
438
- self.energy_to_motor = energy_motor_convertor
430
+ # Store the set energy for readback.
431
+ self._energy, self._energy_set = soft_signal_r_and_setter(
432
+ float, initial_value=None, units="eV"
433
+ )
439
434
  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
435
+ self.energy = derived_signal_rw(
436
+ raw_to_derived=self._read_energy,
437
+ set_derived=self._set_energy,
438
+ energy=self._energy,
439
+ derived_units="eV",
443
440
  )
444
441
 
445
442
  # Store the polarisation for setpoint. And provide readback for LH3.
@@ -447,61 +444,65 @@ class Apple2(abc.ABC, StandardReadable, Movable):
447
444
  self.polarisation_setpoint, self._polarisation_setpoint_set = (
448
445
  soft_signal_r_and_setter(Pol)
449
446
  )
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
- )
447
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
448
+ # Hardware backed read/write for polarisation.
449
+ self.polarisation = derived_signal_rw(
450
+ raw_to_derived=self._read_pol,
451
+ set_derived=self._set_pol,
452
+ pol=self.polarisation_setpoint,
453
+ top_outer=self.apple2().phase().top_outer.user_readback,
454
+ top_inner=self.apple2().phase().top_inner.user_readback,
455
+ btm_inner=self.apple2().phase().btm_inner.user_readback,
456
+ btm_outer=self.apple2().phase().btm_outer.user_readback,
457
+ gap=self.apple2().gap().user_readback,
458
+ )
462
459
  super().__init__(name)
463
460
 
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:
461
+ @abc.abstractmethod
462
+ async def _set_motors_from_energy(self, value: float) -> None:
463
+ """
464
+ This method should be implemented by the beamline specific ID class as the
465
+ motor positions will be different for each beamline depending on the
466
+ undulator design and the lookup table used.
480
467
  """
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
468
 
469
+ async def _set_energy(self, energy: float) -> None:
470
+ await self._set_motors_from_energy(energy)
471
+ self._energy_set(energy)
485
472
 
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.")
473
+ def _read_energy(self, energy: float) -> float:
474
+ """Readback for energy is just the set value."""
475
+ return energy
494
476
 
495
- @abc.abstractmethod
496
- async def _set(self, value: float) -> None:
477
+ async def _check_and_get_pol_setpoint(self) -> Pol:
497
478
  """
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.
479
+ Check the polarisation setpoint and if it is NONE try to read it from
480
+ hardware.
503
481
  """
504
482
 
483
+ pol = await self.polarisation_setpoint.get_value()
484
+
485
+ if pol == Pol.NONE:
486
+ LOGGER.warning(
487
+ "Found no setpoint for polarisation. Attempting to"
488
+ " determine polarisation from hardware..."
489
+ )
490
+ pol = await self.polarisation.get_value()
491
+ if pol == Pol.NONE:
492
+ raise ValueError(
493
+ f"Polarisation cannot be determined from hardware for {self.name}"
494
+ )
495
+ self._polarisation_setpoint_set(pol)
496
+ return pol
497
+
498
+ async def _set_pol(
499
+ self,
500
+ value: Pol,
501
+ ) -> None:
502
+ # This changes the pol setpoint and then changes polarisation via set energy.
503
+ self._polarisation_setpoint_set(value)
504
+ await self.energy.set(await self.energy.get_value())
505
+
505
506
  def _read_pol(
506
507
  self,
507
508
  pol: Pol,
@@ -605,3 +606,101 @@ class Apple2(abc.ABC, StandardReadable, Movable):
605
606
 
606
607
  LOGGER.warning("Unable to determine polarisation. Defaulting to NONE.")
607
608
  return Pol.NONE, 0.0
609
+
610
+
611
+ class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
612
+ """Base class for ID energy movable device."""
613
+
614
+ def __init__(self, name: str = "") -> None:
615
+ self.energy: Reference[SignalRW[float]]
616
+ super().__init__(name=name)
617
+
618
+ @abc.abstractmethod
619
+ @AsyncStatus.wrap
620
+ async def set(self, energy: float) -> None: ...
621
+
622
+
623
+ class BeamEnergy(StandardReadable, Movable[float]):
624
+ """
625
+ Compound device to set both ID and energy motor at the same time with an option to add an offset.
626
+ """
627
+
628
+ def __init__(
629
+ self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
630
+ ) -> None:
631
+ """
632
+ Parameters
633
+ ----------
634
+
635
+ id_energy: InsertionDeviceEnergy
636
+ An InsertionDeviceEnergy device.
637
+ mono: Motor
638
+ A Motor(energy) device.
639
+ name:
640
+ New device name.
641
+ """
642
+ super().__init__(name=name)
643
+ self._id_energy = Reference(id_energy)
644
+ self._mono_energy = Reference(mono)
645
+
646
+ self.add_readables(
647
+ [
648
+ self._id_energy().energy(),
649
+ self._mono_energy().user_readback,
650
+ ],
651
+ StandardReadableFormat.HINTED_SIGNAL,
652
+ )
653
+
654
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
655
+ self.id_energy_offset = soft_signal_rw(float, initial_value=0)
656
+
657
+ @AsyncStatus.wrap
658
+ async def set(self, energy: float) -> None:
659
+ LOGGER.info(f"Moving f{self.name} energy to {energy}.")
660
+ await asyncio.gather(
661
+ self._id_energy().set(
662
+ energy=energy + await self.id_energy_offset.get_value()
663
+ ),
664
+ self._mono_energy().set(energy),
665
+ )
666
+
667
+
668
+ class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
669
+ """Apple2 ID energy movable device."""
670
+
671
+ def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
672
+ self.energy = Reference(id_controller.energy)
673
+ super().__init__(name=name)
674
+
675
+ self.add_readables(
676
+ [
677
+ self.energy(),
678
+ ],
679
+ StandardReadableFormat.HINTED_SIGNAL,
680
+ )
681
+
682
+ @AsyncStatus.wrap
683
+ async def set(self, energy: float) -> None:
684
+ await self.energy().set(energy)
685
+
686
+
687
+ class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
688
+ """Apple2 ID polarisation movable device."""
689
+
690
+ def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
691
+ self.polarisation = Reference(id_controller.polarisation)
692
+ self.polarisation_setpoint = Reference(id_controller.polarisation_setpoint)
693
+ super().__init__(name=name)
694
+
695
+ self.add_readables([self.polarisation()], StandardReadableFormat.HINTED_SIGNAL)
696
+
697
+ @AsyncStatus.wrap
698
+ async def set(self, pol: Pol) -> None:
699
+ await self.polarisation().set(pol)
700
+
701
+ async def locate(self) -> Location[Pol]:
702
+ """Return the current polarisation"""
703
+ setpoint, readback = await asyncio.gather(
704
+ self.polarisation_setpoint().get_value(), self.polarisation().get_value()
705
+ )
706
+ 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"
@@ -62,7 +62,7 @@ class ChannelCutMonochromator(
62
62
  )
63
63
 
64
64
  # energy derived signal as property
65
- self.energy_in_ev = derived_signal_r(
65
+ self.energy_in_eV = derived_signal_r(
66
66
  self._convert_pos_to_ev, pos_signal=self.crystal
67
67
  )
68
68
  super().__init__(name=name)