dls-dodal 1.63.0__py3-none-any.whl → 1.65.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 (47) hide show
  1. {dls_dodal-1.63.0.dist-info → dls_dodal-1.65.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.63.0.dist-info → dls_dodal-1.65.0.dist-info}/RECORD +47 -39
  3. dodal/_version.py +2 -2
  4. dodal/beamline_specific_utils/i05_shared.py +6 -3
  5. dodal/beamlines/b01_1.py +1 -1
  6. dodal/beamlines/b07.py +6 -3
  7. dodal/beamlines/b07_1.py +6 -3
  8. dodal/beamlines/i03.py +9 -1
  9. dodal/beamlines/i05.py +2 -2
  10. dodal/beamlines/i05_1.py +2 -2
  11. dodal/beamlines/i07.py +21 -0
  12. dodal/beamlines/i09.py +4 -4
  13. dodal/beamlines/i09_1.py +7 -1
  14. dodal/beamlines/i09_2.py +36 -3
  15. dodal/beamlines/i10_optics.py +53 -27
  16. dodal/beamlines/i17.py +21 -11
  17. dodal/beamlines/i19_2.py +22 -0
  18. dodal/beamlines/i21.py +34 -4
  19. dodal/beamlines/i22.py +0 -17
  20. dodal/beamlines/k07.py +6 -3
  21. dodal/cli.py +3 -3
  22. dodal/devices/apple2_undulator.py +19 -17
  23. dodal/devices/b07_1/ccmc.py +1 -1
  24. dodal/devices/common_dcm.py +3 -3
  25. dodal/devices/cryostream.py +21 -0
  26. dodal/devices/i03/undulator_dcm.py +1 -1
  27. dodal/devices/i07/__init__.py +0 -0
  28. dodal/devices/i07/dcm.py +33 -0
  29. dodal/devices/i09_1_shared/__init__.py +3 -0
  30. dodal/devices/i09_1_shared/hard_undulator_functions.py +111 -0
  31. dodal/devices/i10/i10_apple2.py +4 -4
  32. dodal/devices/i15/dcm.py +1 -1
  33. dodal/devices/i22/dcm.py +1 -1
  34. dodal/devices/i22/nxsas.py +5 -24
  35. dodal/devices/pgm.py +1 -1
  36. dodal/devices/scintillator.py +4 -0
  37. dodal/devices/undulator.py +29 -1
  38. dodal/devices/util/lookup_tables.py +8 -2
  39. dodal/plan_stubs/__init__.py +3 -0
  40. dodal/plans/verify_undulator_gap.py +2 -2
  41. dodal/testing/fixtures/__init__.py +0 -0
  42. dodal/testing/fixtures/run_engine.py +118 -0
  43. dodal/testing/fixtures/utils.py +57 -0
  44. {dls_dodal-1.63.0.dist-info → dls_dodal-1.65.0.dist-info}/WHEEL +0 -0
  45. {dls_dodal-1.63.0.dist-info → dls_dodal-1.65.0.dist-info}/entry_points.txt +0 -0
  46. {dls_dodal-1.63.0.dist-info → dls_dodal-1.65.0.dist-info}/licenses/LICENSE +0 -0
  47. {dls_dodal-1.63.0.dist-info → dls_dodal-1.65.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i17.py CHANGED
@@ -17,7 +17,7 @@ from dodal.devices.apple2_undulator import (
17
17
  UndulatorPhaseAxes,
18
18
  )
19
19
  from dodal.devices.i17.i17_apple2 import I17Apple2Controller
20
- from dodal.devices.pgm import PGM
20
+ from dodal.devices.pgm import PlaneGratingMonochromator
21
21
  from dodal.devices.synchrotron import Synchrotron
22
22
  from dodal.log import set_beamline as set_log_beamline
23
23
  from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -39,26 +39,36 @@ def synchrotron() -> Synchrotron:
39
39
 
40
40
 
41
41
  @device_factory(skip=True)
42
- def pgm() -> PGM:
43
- return PGM(
42
+ def pgm() -> PlaneGratingMonochromator:
43
+ return PlaneGratingMonochromator(
44
44
  prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:",
45
45
  grating=I17Grating,
46
46
  grating_pv="NLINES2",
47
47
  )
48
48
 
49
49
 
50
+ @device_factory()
51
+ def id_gap() -> UndulatorGap:
52
+ return UndulatorGap(prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:")
53
+
54
+
55
+ @device_factory()
56
+ def id_phase() -> UndulatorPhaseAxes:
57
+ return UndulatorPhaseAxes(
58
+ prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
59
+ top_outer="RPQ1",
60
+ top_inner="RPQ2",
61
+ btm_inner="RPQ3",
62
+ btm_outer="RPQ4",
63
+ )
64
+
65
+
50
66
  @device_factory(skip=True)
51
67
  def id() -> Apple2:
52
68
  """I17 insertion device:"""
53
69
  return Apple2(
54
- id_gap=UndulatorGap(prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:"),
55
- id_phase=UndulatorPhaseAxes(
56
- prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
57
- top_outer="RPQ1",
58
- top_inner="RPQ2",
59
- btm_inner="RPQ3",
60
- btm_outer="RPQ4",
61
- ),
70
+ id_gap=id_gap(),
71
+ id_phase=id_phase(),
62
72
  )
63
73
 
64
74
 
dodal/beamlines/i19_2.py CHANGED
@@ -1,12 +1,17 @@
1
+ from pathlib import Path
2
+
3
+ from ophyd_async.fastcs.eiger import EigerDetector
1
4
  from ophyd_async.fastcs.panda import HDFPanda
2
5
 
3
6
  from dodal.common.beamlines.beamline_utils import (
4
7
  device_factory,
5
8
  get_path_provider,
9
+ set_path_provider,
6
10
  )
7
11
  from dodal.common.beamlines.beamline_utils import (
8
12
  set_beamline as set_utils_beamline,
9
13
  )
14
+ from dodal.common.visit import StaticVisitPathProvider
10
15
  from dodal.devices.i19.access_controlled.blueapi_device import HutchState
11
16
  from dodal.devices.i19.access_controlled.shutter import AccessControlledShutter
12
17
  from dodal.devices.i19.backlight import BacklightPosition
@@ -31,6 +36,13 @@ PREFIX = BeamlinePrefix("i19", "I")
31
36
  set_log_beamline(BL)
32
37
  set_utils_beamline(BL)
33
38
 
39
+ set_path_provider(
40
+ StaticVisitPathProvider(
41
+ BL,
42
+ Path("/dls/i19-2/data/2025/cm40639-4/"),
43
+ )
44
+ )
45
+
34
46
 
35
47
  I19_2_ZEBRA_MAPPING = ZebraMapping(
36
48
  outputs=ZebraTTLOutputs(),
@@ -105,3 +117,13 @@ def panda() -> HDFPanda:
105
117
  prefix=f"{PREFIX.beamline_prefix}-EA-PANDA-01:",
106
118
  path_provider=get_path_provider(),
107
119
  )
120
+
121
+
122
+ @device_factory()
123
+ def eiger() -> EigerDetector:
124
+ return EigerDetector(
125
+ prefix=PREFIX.beamline_prefix,
126
+ path_provider=get_path_provider(),
127
+ drv_suffix="-EA-EIGER-01:",
128
+ hdf_suffix="-EA-EIGER-01:OD:",
129
+ )
dodal/beamlines/i21.py CHANGED
@@ -2,14 +2,19 @@ from dodal.common.beamlines.beamline_utils import (
2
2
  device_factory,
3
3
  )
4
4
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5
+ from dodal.devices.apple2_undulator import (
6
+ Apple2,
7
+ UndulatorGap,
8
+ UndulatorPhaseAxes,
9
+ )
5
10
  from dodal.devices.i21 import Grating
6
- from dodal.devices.pgm import PGM
11
+ from dodal.devices.pgm import PlaneGratingMonochromator
7
12
  from dodal.devices.synchrotron import Synchrotron
8
13
  from dodal.log import set_beamline as set_log_beamline
9
14
  from dodal.utils import BeamlinePrefix, get_beamline_name
10
15
 
11
16
  BL = get_beamline_name("i21")
12
- PREFIX = BeamlinePrefix(BL, suffix="I")
17
+ PREFIX = BeamlinePrefix(BL)
13
18
  set_log_beamline(BL)
14
19
  set_utils_beamline(BL)
15
20
 
@@ -20,8 +25,33 @@ def synchrotron() -> Synchrotron:
20
25
 
21
26
 
22
27
  @device_factory()
23
- def pgm() -> PGM:
24
- return PGM(
28
+ def pgm() -> PlaneGratingMonochromator:
29
+ return PlaneGratingMonochromator(
25
30
  prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:",
26
31
  grating=Grating,
27
32
  )
33
+
34
+
35
+ @device_factory()
36
+ def id_gap() -> UndulatorGap:
37
+ return UndulatorGap(prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:")
38
+
39
+
40
+ @device_factory()
41
+ def id_phase() -> UndulatorPhaseAxes:
42
+ return UndulatorPhaseAxes(
43
+ prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
44
+ top_outer="PUO",
45
+ top_inner="PUI",
46
+ btm_inner="PLI",
47
+ btm_outer="PLO",
48
+ )
49
+
50
+
51
+ @device_factory()
52
+ def id() -> Apple2:
53
+ """I21 insertion device."""
54
+ return Apple2(
55
+ id_gap=id_gap(),
56
+ id_phase=id_phase(),
57
+ )
dodal/beamlines/i22.py CHANGED
@@ -1,5 +1,3 @@
1
- from pathlib import Path
2
-
3
1
  from ophyd_async.epics.adaravis import AravisDetector
4
2
  from ophyd_async.epics.adcore import NDPluginBaseIO, NDPluginStatsIO
5
3
  from ophyd_async.epics.adpilatus import PilatusDetector
@@ -8,7 +6,6 @@ from ophyd_async.fastcs.panda import HDFPanda
8
6
  from dodal.common.beamlines.beamline_utils import (
9
7
  device_factory,
10
8
  get_path_provider,
11
- set_path_provider,
12
9
  )
13
10
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
14
11
  from dodal.common.beamlines.device_helpers import CAM_SUFFIX, DET_SUFFIX, HDF5_SUFFIX
@@ -16,7 +13,6 @@ from dodal.common.crystal_metadata import (
16
13
  MaterialsEnum,
17
14
  make_crystal_metadata_from_material,
18
15
  )
19
- from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
20
16
  from dodal.devices.bimorph_mirror import BimorphMirror
21
17
  from dodal.devices.focusing_mirror import FocusingMirror
22
18
  from dodal.devices.i22.dcm import DCM
@@ -37,19 +33,6 @@ PREFIX = BeamlinePrefix(BL)
37
33
  set_log_beamline(BL)
38
34
  set_utils_beamline(BL)
39
35
 
40
- # Currently we must hard-code the visit, determining the visit at runtime requires
41
- # infrastructure that is still WIP.
42
- # Communication with GDA is also WIP so for now we determine an arbitrary scan number
43
- # locally and write the commissioning directory. The scan number is not guaranteed to
44
- # be unique and the data is at risk - this configuration is for testing only.
45
- set_path_provider(
46
- StaticVisitPathProvider(
47
- BL,
48
- Path("/dls/i22/data/2025/cm40643-4/"),
49
- client=RemoteDirectoryServiceClient("http://i22-control:8088/api"),
50
- )
51
- )
52
-
53
36
 
54
37
  @device_factory()
55
38
  def saxs() -> PilatusDetector:
dodal/beamlines/k07.py CHANGED
@@ -4,7 +4,7 @@ from dodal.common.beamlines.beamline_utils import (
4
4
  device_factory,
5
5
  )
6
6
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
7
- from dodal.devices.pgm import PGM
7
+ from dodal.devices.pgm import PlaneGratingMonochromator
8
8
  from dodal.devices.synchrotron import Synchrotron
9
9
  from dodal.log import set_beamline as set_log_beamline
10
10
  from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -27,5 +27,8 @@ class Grating(StrictEnum):
27
27
 
28
28
  # Grating does not exist yet - this class is a placeholder for when it does
29
29
  @device_factory(skip=True)
30
- def pgm() -> PGM:
31
- return PGM(prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:", grating=Grating)
30
+ def pgm() -> PlaneGratingMonochromator:
31
+ return PlaneGratingMonochromator(
32
+ prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:",
33
+ grating=Grating,
34
+ )
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
@@ -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(
@@ -113,7 +113,7 @@ def _connect_devices(
113
113
  # Connect ophyd-async devices
114
114
  try:
115
115
  run_engine(ensure_connected(*ophyd_async_devices.values(), mock=sim_backend))
116
- except NotConnected as ex:
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
@@ -326,8 +326,8 @@ class Apple2(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
@@ -338,25 +338,27 @@ class Apple2(StandardReadable, Movable):
338
338
  """
339
339
 
340
340
  # Only need to check gap as the phase motors share both fault and gate with gap.
341
- await self.gap.raise_if_cannot_move()
341
+ await self.gap().raise_if_cannot_move()
342
342
  await asyncio.gather(
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),
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),
348
348
  )
349
349
  timeout = np.max(
350
- await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
350
+ await asyncio.gather(self.gap().get_timeout(), self.phase().get_timeout())
351
351
  )
352
352
  LOGGER.info(
353
353
  f"Moving f{self.name} apple2 motors to {id_motor_values}, timeout = {timeout}"
354
354
  )
355
355
  await asyncio.gather(
356
- self.gap.set_move.set(value=1, wait=False, timeout=timeout),
357
- 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
358
361
  )
359
- await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
360
362
 
361
363
 
362
364
  class EnergyMotorConvertor(Protocol):
@@ -448,11 +450,11 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
448
450
  raw_to_derived=self._read_pol,
449
451
  set_derived=self._set_pol,
450
452
  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,
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,
456
458
  )
457
459
  super().__init__(name)
458
460
 
@@ -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)
@@ -53,9 +53,9 @@ class DoubleCrystalMonochromatorBase(StandardReadable, Generic[Xtal_1, Xtal_2]):
53
53
  ) -> None:
54
54
  with self.add_children_as_readables():
55
55
  # Virtual motor PV's which set the physical motors so that the DCM produces requested energy
56
- self.energy_in_kev = Motor(prefix + "ENERGY")
57
- self.energy_in_ev = derived_signal_r(
58
- self._convert_keV_to_eV, energy_signal=self.energy_in_kev.user_readback
56
+ self.energy_in_keV = Motor(prefix + "ENERGY")
57
+ self.energy_in_eV = derived_signal_r(
58
+ self._convert_keV_to_eV, energy_signal=self.energy_in_keV.user_readback
59
59
  )
60
60
 
61
61
  self._make_crystals(prefix, xtal_1, xtal_2)
@@ -32,6 +32,11 @@ class TurboEnum(StrictEnum):
32
32
  AUTO = "Auto"
33
33
 
34
34
 
35
+ class CryoStreamSelection(StrictEnum):
36
+ CRYOJET = "CryoJet"
37
+ HC1 = "HC1"
38
+
39
+
35
40
  class OxfordCryoStreamController(StandardReadable):
36
41
  def __init__(self, prefix: str, name: str = ""):
37
42
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
@@ -107,3 +112,19 @@ class OxfordCryoStream(StandardReadable):
107
112
  self.status = OxfordCryoStreamStatus(prefix=prefix)
108
113
 
109
114
  super().__init__(name)
115
+
116
+
117
+ class CryoStreamGantry(StandardReadable):
118
+ def __init__(self, prefix: str, name: str = ""):
119
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
120
+ self.cryostream_selector = epics_signal_r(
121
+ CryoStreamSelection, f"{prefix}-EA-GANT-01:CTRL"
122
+ )
123
+ self.hc1_selected = epics_signal_r(
124
+ int, f"{prefix}-MO-STEP-02:GPIO_INP_BITS.B2"
125
+ )
126
+ self.cryostream_selected = epics_signal_r(
127
+ int, f"{prefix}-MO-STEP-02:GPIO_INP_BITS.B3"
128
+ )
129
+
130
+ super().__init__(name)
@@ -58,7 +58,7 @@ class UndulatorDCM(StandardReadable, Movable[float]):
58
58
  async def set(self, value: float):
59
59
  await self.undulator_ref().raise_if_not_enabled()
60
60
  await asyncio.gather(
61
- self.dcm_ref().energy_in_kev.set(value, timeout=ENERGY_TIMEOUT_S),
61
+ self.dcm_ref().energy_in_keV.set(value, timeout=ENERGY_TIMEOUT_S),
62
62
  self.undulator_ref().set(value),
63
63
  )
64
64
 
File without changes
@@ -0,0 +1,33 @@
1
+ from ophyd_async.epics.core import epics_signal_r
2
+ from ophyd_async.epics.motor import Motor
3
+
4
+ from dodal.devices.common_dcm import (
5
+ DoubleCrystalMonochromator,
6
+ PitchAndRollCrystal,
7
+ StationaryCrystal,
8
+ )
9
+
10
+
11
+ class DCM(DoubleCrystalMonochromator[PitchAndRollCrystal, StationaryCrystal]):
12
+ """
13
+ Device for i07's DCM, including temperature monitors and vertical motor which were
14
+ included in GDA.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ motor_prefix: str,
20
+ xtal_prefix: str,
21
+ name: str = "",
22
+ ) -> None:
23
+ super().__init__(motor_prefix, PitchAndRollCrystal, StationaryCrystal, name)
24
+ with self.add_children_as_readables():
25
+ self.vertical_in_mm = Motor(motor_prefix + "PERP")
26
+
27
+ # temperatures
28
+ self.xtal1_temp = epics_signal_r(float, xtal_prefix + "PT100-2")
29
+ self.xtal2_temp = epics_signal_r(float, xtal_prefix + "PT100-3")
30
+ self.xtal1_holder_temp = epics_signal_r(float, xtal_prefix + "PT100-1")
31
+ self.xtal2_holder_temp = epics_signal_r(float, xtal_prefix + "PT100-4")
32
+ self.gap_motor = epics_signal_r(float, xtal_prefix + "TC-1")
33
+ self.white_beam_stop_temp = epics_signal_r(float, xtal_prefix + "WBS:TEMP")
@@ -0,0 +1,3 @@
1
+ from .hard_undulator_functions import calculate_gap_i09_hu, get_hu_lut_as_dict
2
+
3
+ __all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict"]
@@ -0,0 +1,111 @@
1
+ import numpy as np
2
+
3
+ from dodal.devices.util.lookup_tables import energy_distance_table
4
+ from dodal.log import LOGGER
5
+
6
+ LUT_COMMENTS = ["#"]
7
+ HU_SKIP_ROWS = 3
8
+
9
+ # Physics constants
10
+ ELECTRON_REST_ENERGY_MEV = 0.510999
11
+
12
+ # Columns in the lookup table
13
+ RING_ENERGY_COLUMN = 1
14
+ MAGNET_FIELD_COLUMN = 2
15
+ MIN_ENERGY_COLUMN = 3
16
+ MAX_ENERGY_COLUMN = 4
17
+ GAP_OFFSET_COLUMN = 7
18
+
19
+
20
+ async def get_hu_lut_as_dict(lut_path: str) -> dict:
21
+ lut_dict: dict = {}
22
+ _lookup_table: np.ndarray = await energy_distance_table(
23
+ lut_path,
24
+ comments=LUT_COMMENTS,
25
+ skiprows=HU_SKIP_ROWS,
26
+ )
27
+ for i in range(_lookup_table.shape[0]):
28
+ lut_dict[_lookup_table[i][0]] = _lookup_table[i]
29
+ LOGGER.debug(f"Loaded lookup table:\n {lut_dict}")
30
+ return lut_dict
31
+
32
+
33
+ def calculate_gap_i09_hu(
34
+ photon_energy_kev: float,
35
+ look_up_table: dict[int, "np.ndarray"],
36
+ order: int = 1,
37
+ gap_offset: float = 0.0,
38
+ undulator_period_mm: int = 27,
39
+ ) -> float:
40
+ """
41
+ Calculate the undulator gap required to produce a given energy at a given harmonic order.
42
+ This algorithm was provided by the I09 beamline scientists, and is based on the physics of undulator radiation.
43
+ https://cxro.lbl.gov//PDF/X-Ray-Data-Booklet.pdf
44
+
45
+ Args:
46
+ photon_energy_kev (float): Requested photon energy in keV.
47
+ look_up_table (dict[int, np.ndarray]): Lookup table containing undulator and beamline parameters for each harmonic order.
48
+ order (int, optional): Harmonic order for which to calculate the gap. Defaults to 1.
49
+ gap_offset (float, optional): Additional gap offset to apply (in mm). Defaults to 0.0.
50
+ undulator_period_mm (int, optional): Undulator period in mm. Defaults to 27.
51
+
52
+ Returns:
53
+ float: Calculated undulator gap in millimeters.
54
+ """
55
+ magnet_blocks_per_period = 4
56
+ magnet_block_height_mm = 16
57
+
58
+ if order not in look_up_table.keys():
59
+ raise ValueError(f"Order parameter {order} not found in lookup table")
60
+
61
+ gamma = 1000 * look_up_table[order][RING_ENERGY_COLUMN] / ELECTRON_REST_ENERGY_MEV
62
+
63
+ # Constructive interference of radiation emitted at different poles
64
+ # lamda = (lambda_u/2*gamma^2)*(1+K^2/2 + gamma^2*theta^2)/n for n=1,2,3...
65
+ # theta is the observation angle, assumed to be 0 here.
66
+ # Rearranging for K (the undulator parameter, related to magnetic field and gap)
67
+ # gives K^2 = 2*((2*n*gamma^2*lamda/lambda_u)-1)
68
+
69
+ undulator_parameter_sqr = (
70
+ 4.959368e-6
71
+ * (order * gamma * gamma / (undulator_period_mm * photon_energy_kev))
72
+ - 2
73
+ )
74
+ if undulator_parameter_sqr < 0:
75
+ raise ValueError(
76
+ f"Diffraction parameter squared must be positive! Calculated value {undulator_parameter_sqr}."
77
+ )
78
+ undulator_parameter = np.sqrt(undulator_parameter_sqr)
79
+
80
+ # Undulator_parameter K is also defined as K = 0.934*B0[T]*lambda_u[cm],
81
+ # where B0[T] is a peak magnetic field that must depend on gap,
82
+ # but in our LUT it is does not depend on gap, so it's a factor,
83
+ # leading to K = 0.934*B0[T]*lambda_u[cm]*exp(-pi*gap/lambda_u) or
84
+ # K = undulator_parameter_max*exp(-pi*gap/lambda_u)
85
+ # Calculating undulator_parameter_max gives:
86
+ undulator_parameter_max = (
87
+ (
88
+ 2
89
+ * 0.0934
90
+ * undulator_period_mm
91
+ * look_up_table[order][MAGNET_FIELD_COLUMN]
92
+ * magnet_blocks_per_period
93
+ / np.pi
94
+ )
95
+ * np.sin(np.pi / magnet_blocks_per_period)
96
+ * (1 - np.exp(-2 * np.pi * magnet_block_height_mm / undulator_period_mm))
97
+ )
98
+
99
+ # Finnaly, rearranging the equation:
100
+ # undulator_parameter = undulator_parameter_max*exp(-pi*gap/lambda_u) for gap gives
101
+ gap = (
102
+ (undulator_period_mm / np.pi)
103
+ * np.log(undulator_parameter_max / undulator_parameter)
104
+ + look_up_table[order][GAP_OFFSET_COLUMN]
105
+ + gap_offset
106
+ )
107
+ LOGGER.debug(
108
+ f"Calculated gap is {gap}mm for energy {photon_energy_kev}keV at order {order}"
109
+ )
110
+
111
+ return gap
@@ -340,7 +340,7 @@ class I10Apple2(Apple2):
340
340
  The name of the device, by default "".
341
341
  """
342
342
  with self.add_children_as_readables():
343
- self.jaw_phase = id_jaw_phase
343
+ self.jaw_phase = Reference(id_jaw_phase)
344
344
  super().__init__(id_gap=id_gap, id_phase=id_phase, name=name)
345
345
 
346
346
 
@@ -425,7 +425,7 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
425
425
  f"jaw_phase position for angle ({pol_angle}) is outside permitted range"
426
426
  f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
427
427
  )
428
- await self.apple2().jaw_phase.set(jaw_phase)
428
+ await self.apple2().jaw_phase().set(jaw_phase)
429
429
  await self._linear_arbitrary_angle.set(pol_angle)
430
430
 
431
431
  async def _set_motors_from_energy(self, value: float) -> None:
@@ -447,8 +447,8 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
447
447
  LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
448
448
  await self.apple2().set(id_motor_values=id_set_val)
449
449
  if pol != Pol.LA:
450
- await self.apple2().jaw_phase.set(0)
451
- await self.apple2().jaw_phase.set_move.set(1)
450
+ await self.apple2().jaw_phase().set(0)
451
+ await self.apple2().jaw_phase().set_move.set(1)
452
452
 
453
453
  def _raise_if_not_la(self, pol: Pol) -> None:
454
454
  if pol != Pol.LA:
dodal/devices/i15/dcm.py CHANGED
@@ -33,7 +33,7 @@ class DCM(DoubleCrystalMonochromatorBase[ThetaRollYZCrystal, ThetaYCrystal]):
33
33
 
34
34
  def __init__(self, prefix: str, name: str = "") -> None:
35
35
  with self.add_children_as_readables():
36
- self.calibrated_energy_in_kev = Motor(prefix + "CAL")
36
+ self.calibrated_energy_in_keV = Motor(prefix + "CAL")
37
37
  self.x1 = Motor(prefix + "X1")
38
38
 
39
39
  super().__init__(prefix, ThetaRollYZCrystal, ThetaYCrystal, name)
dodal/devices/i22/dcm.py CHANGED
@@ -107,7 +107,7 @@ class DCM(DoubleCrystalMonochromatorWithDSpacing[RollCrystal, PitchAndRollCrysta
107
107
 
108
108
  async def read(self) -> dict[str, Reading]:
109
109
  default_reading = await super().read()
110
- energy: float = default_reading[f"{self.name}-energy_in_kev"]["value"]
110
+ energy: float = default_reading[f"{self.name}-energy_in_keV"]["value"]
111
111
  if energy > 0.0:
112
112
  wavelength = _CONVERSION_CONSTANT / energy
113
113
  else:
@@ -1,11 +1,9 @@
1
- import asyncio
2
- from collections.abc import Awaitable, Iterable
3
1
  from dataclasses import dataclass, fields
4
2
  from typing import TypeVar
5
3
 
6
4
  from bluesky.protocols import Reading
7
5
  from event_model.documents.event_descriptor import DataKey
8
- from ophyd_async.core import PathProvider
6
+ from ophyd_async.core import PathProvider, merge_gathered_dicts
9
7
  from ophyd_async.epics.adaravis import AravisDetector
10
8
  from ophyd_async.epics.adcore import NDPluginBaseIO
11
9
  from ophyd_async.epics.adpilatus import PilatusDetector
@@ -14,23 +12,6 @@ ValueAndUnits = tuple[float, str]
14
12
  T = TypeVar("T")
15
13
 
16
14
 
17
- # TODO: Remove this file as part of github.com/DiamondLightSource/dodal/issues/595
18
- # Until which, temporarily duplicated non-public method from ophyd_async
19
- async def _merge_gathered_dicts(
20
- coros: Iterable[Awaitable[dict[str, T]]],
21
- ) -> dict[str, T]:
22
- """Merge dictionaries produced by a sequence of coroutines.
23
-
24
- Can be used for merging ``read()`` or ``describe``. For instance::
25
-
26
- combined_read = await merge_gathered_dicts(s.read() for s in signals)
27
- """
28
- ret: dict[str, T] = {}
29
- for result in await asyncio.gather(*coros):
30
- ret.update(result)
31
- return ret
32
-
33
-
34
15
  @dataclass
35
16
  class MetadataHolder:
36
17
  # TODO: just in case this is useful more widely...
@@ -124,7 +105,7 @@ class NXSasPilatus(PilatusDetector):
124
105
  self._metadata_holder = metadata_holder
125
106
 
126
107
  async def read_configuration(self) -> dict[str, Reading]:
127
- return await _merge_gathered_dicts(
108
+ return await merge_gathered_dicts(
128
109
  r
129
110
  for r in (
130
111
  super().read_configuration(),
@@ -133,7 +114,7 @@ class NXSasPilatus(PilatusDetector):
133
114
  )
134
115
 
135
116
  async def describe_configuration(self) -> dict[str, DataKey]:
136
- return await _merge_gathered_dicts(
117
+ return await merge_gathered_dicts(
137
118
  r
138
119
  for r in (
139
120
  super().describe_configuration(),
@@ -167,7 +148,7 @@ class NXSasOAV(AravisDetector):
167
148
  self._metadata_holder = metadata_holder
168
149
 
169
150
  async def read_configuration(self) -> dict[str, Reading]:
170
- return await _merge_gathered_dicts(
151
+ return await merge_gathered_dicts(
171
152
  r
172
153
  for r in (
173
154
  super().read_configuration(),
@@ -176,7 +157,7 @@ class NXSasOAV(AravisDetector):
176
157
  )
177
158
 
178
159
  async def describe_configuration(self) -> dict[str, DataKey]:
179
- return await _merge_gathered_dicts(
160
+ return await merge_gathered_dicts(
180
161
  r
181
162
  for r in (
182
163
  super().describe_configuration(),
dodal/devices/pgm.py CHANGED
@@ -7,7 +7,7 @@ from ophyd_async.epics.core import epics_signal_rw
7
7
  from ophyd_async.epics.motor import Motor
8
8
 
9
9
 
10
- class PGM(StandardReadable):
10
+ class PlaneGratingMonochromator(StandardReadable):
11
11
  """
12
12
  Plane grating monochromator, it is use in soft x-ray beamline to generate monochromic beam.
13
13
  """