dls-dodal 1.65.0__py3-none-any.whl → 1.67.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 (85) hide show
  1. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +102 -198
  6. dodal/beamlines/i04.py +40 -4
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +32 -3
  11. dodal/beamlines/i09_2.py +57 -2
  12. dodal/beamlines/i10_optics.py +46 -17
  13. dodal/beamlines/i17.py +7 -3
  14. dodal/beamlines/i18.py +3 -3
  15. dodal/beamlines/i19_1.py +26 -14
  16. dodal/beamlines/i19_2.py +49 -38
  17. dodal/beamlines/i21.py +2 -2
  18. dodal/beamlines/i22.py +19 -4
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/training_rig.py +0 -16
  21. dodal/cli.py +26 -12
  22. dodal/common/coordination.py +3 -2
  23. dodal/device_manager.py +604 -0
  24. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  25. dodal/devices/aperturescatterguard.py +3 -2
  26. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  27. dodal/devices/beamsize/__init__.py +0 -0
  28. dodal/devices/beamsize/beamsize.py +6 -0
  29. dodal/devices/cryostream.py +28 -57
  30. dodal/devices/detector/det_resolution.py +4 -2
  31. dodal/devices/eiger.py +26 -18
  32. dodal/devices/fast_grid_scan.py +14 -2
  33. dodal/devices/i03/beamsize.py +35 -0
  34. dodal/devices/i03/constants.py +7 -0
  35. dodal/devices/i03/undulator_dcm.py +2 -2
  36. dodal/devices/i04/beamsize.py +45 -0
  37. dodal/devices/i04/max_pixel.py +38 -0
  38. dodal/devices/i04/murko_results.py +36 -26
  39. dodal/devices/i04/transfocator.py +23 -29
  40. dodal/devices/i07/id.py +38 -0
  41. dodal/devices/i09_1_shared/__init__.py +13 -2
  42. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i09_2_shared/__init__.py +0 -0
  45. dodal/devices/i09_2_shared/i09_apple2.py +86 -0
  46. dodal/devices/i10/i10_apple2.py +39 -331
  47. dodal/devices/i17/i17_apple2.py +37 -22
  48. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  49. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  50. dodal/devices/i19/access_controlled/shutter.py +2 -4
  51. dodal/devices/insertion_device/__init__.py +0 -0
  52. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
  53. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  54. dodal/devices/insertion_device/lookup_table_models.py +287 -0
  55. dodal/devices/ipin.py +20 -2
  56. dodal/devices/motors.py +33 -3
  57. dodal/devices/mx_phase1/beamstop.py +31 -12
  58. dodal/devices/oav/oav_calculations.py +9 -4
  59. dodal/devices/oav/oav_detector.py +65 -7
  60. dodal/devices/oav/oav_parameters.py +3 -1
  61. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  62. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  63. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  64. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  65. dodal/devices/oav/utils.py +16 -6
  66. dodal/devices/robot.py +33 -18
  67. dodal/devices/scintillator.py +36 -14
  68. dodal/devices/smargon.py +2 -3
  69. dodal/devices/thawer.py +7 -45
  70. dodal/devices/undulator.py +152 -68
  71. dodal/plans/__init__.py +1 -1
  72. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  73. dodal/plans/load_panda_yaml.py +9 -0
  74. dodal/plans/verify_undulator_gap.py +2 -2
  75. dodal/testing/fixtures/devices/__init__.py +0 -0
  76. dodal/testing/fixtures/devices/apple2.py +78 -0
  77. dodal/utils.py +6 -3
  78. dodal/beamline_specific_utils/i03.py +0 -17
  79. dodal/testing/__init__.py +0 -3
  80. dodal/testing/setup.py +0 -67
  81. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
  82. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
  83. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
  84. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
  85. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -0,0 +1,38 @@
1
+ import numpy as np
2
+
3
+ from dodal.devices.undulator import UndulatorInKeV, UndulatorOrder
4
+ from dodal.devices.util.lookup_tables import energy_distance_table
5
+
6
+
7
+ class InsertionDevice(UndulatorInKeV):
8
+ """
9
+ Insertion device for i07 including beamline-specific energy-gap lookup behaviour
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ name: str,
15
+ prefix: str,
16
+ harmonic: UndulatorOrder,
17
+ id_gap_lookup_table_path: str = "/dls_sw/i07/software/gda/config/lookupTables/"
18
+ + "IIDCalibrationTable.txt",
19
+ ) -> None:
20
+ super().__init__(prefix, id_gap_lookup_table_path, name=name)
21
+ self.harmonic = harmonic
22
+
23
+ async def _get_gap_to_match_energy(self, energy_kev: float) -> float:
24
+ """
25
+ i07's energy scans remain on a particular harmonic while changing energy. The
26
+ calibration table has one row for each harmonic, row contains max and min
27
+ energies and their corresponding ID gaps. The requested energy is used to
28
+ interpolate between these values, assuming a linear relationship on the relevant
29
+ scale.
30
+ """
31
+ energy_to_distance_table: np.ndarray = await energy_distance_table(
32
+ self.id_gap_lookup_table_path, comments="#", skiprows=2
33
+ )
34
+ harmonic_value: int = await self.harmonic.value.get_value()
35
+
36
+ row: np.ndarray = energy_to_distance_table[harmonic_value - 1, :]
37
+ gap = np.interp(energy_kev, [row[1], row[2]], [row[3], row[4]])
38
+ return gap
@@ -1,3 +1,14 @@
1
- from .hard_undulator_functions import calculate_gap_i09_hu, get_hu_lut_as_dict
1
+ from .hard_energy import HardEnergy, HardInsertionDeviceEnergy
2
+ from .hard_undulator_functions import (
3
+ calculate_energy_i09_hu,
4
+ calculate_gap_i09_hu,
5
+ get_hu_lut_as_dict,
6
+ )
2
7
 
3
- __all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict"]
8
+ __all__ = [
9
+ "calculate_gap_i09_hu",
10
+ "get_hu_lut_as_dict",
11
+ "calculate_energy_i09_hu",
12
+ "HardInsertionDeviceEnergy",
13
+ "HardEnergy",
14
+ ]
@@ -0,0 +1,112 @@
1
+ from asyncio import gather
2
+ from collections.abc import Callable
3
+
4
+ from bluesky.protocols import Locatable, Location, Movable
5
+ from numpy import ndarray
6
+ from ophyd_async.core import (
7
+ AsyncStatus,
8
+ Reference,
9
+ StandardReadable,
10
+ StandardReadableFormat,
11
+ derived_signal_rw,
12
+ soft_signal_rw,
13
+ )
14
+
15
+ from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase
16
+ from dodal.devices.i09_1_shared.hard_undulator_functions import (
17
+ MAX_ENERGY_COLUMN,
18
+ MIN_ENERGY_COLUMN,
19
+ )
20
+ from dodal.devices.undulator import UndulatorInMm, UndulatorOrder
21
+
22
+
23
+ class HardInsertionDeviceEnergy(StandardReadable, Movable[float]):
24
+ """
25
+ Compound device to link hard x-ray undulator gap and order to photon energy.
26
+ Setting the energy adjusts the undulator gap accordingly.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ undulator_order: UndulatorOrder,
32
+ undulator: UndulatorInMm,
33
+ lut: dict[int, ndarray],
34
+ gap_to_energy_func: Callable[..., float],
35
+ energy_to_gap_func: Callable[..., float],
36
+ name: str = "",
37
+ ) -> None:
38
+ self._lut = lut
39
+ self.gap_to_energy_func = gap_to_energy_func
40
+ self.energy_to_gap_func = energy_to_gap_func
41
+ self._undulator_order_ref = Reference(undulator_order)
42
+ self._undulator_ref = Reference(undulator)
43
+
44
+ self.add_readables([undulator_order, undulator.current_gap])
45
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
46
+ self.energy_demand = soft_signal_rw(float)
47
+ self.energy = derived_signal_rw(
48
+ raw_to_derived=self._read_energy,
49
+ set_derived=self._set_energy,
50
+ current_gap=self._undulator_ref().gap_motor.user_readback,
51
+ current_order=self._undulator_order_ref().value,
52
+ derived_units="keV",
53
+ )
54
+ super().__init__(name=name)
55
+
56
+ def _read_energy(self, current_gap: float, current_order: int) -> float:
57
+ return self.gap_to_energy_func(
58
+ gap=current_gap,
59
+ look_up_table=self._lut,
60
+ order=current_order,
61
+ )
62
+
63
+ async def _set_energy(self, energy: float) -> None:
64
+ current_order = await self._undulator_order_ref().value.get_value()
65
+ min_energy, max_energy = self._lut[current_order][
66
+ MIN_ENERGY_COLUMN : MAX_ENERGY_COLUMN + 1
67
+ ]
68
+ if not (min_energy <= energy <= max_energy):
69
+ raise ValueError(
70
+ f"Requested energy {energy} keV is out of range for harmonic {current_order}: "
71
+ f"[{min_energy}, {max_energy}] keV"
72
+ )
73
+
74
+ target_gap = self.energy_to_gap_func(
75
+ photon_energy_kev=energy, look_up_table=self._lut, order=current_order
76
+ )
77
+ await self._undulator_ref().set(target_gap)
78
+
79
+ @AsyncStatus.wrap
80
+ async def set(self, value: float) -> None:
81
+ self.energy_demand.set(value)
82
+ await self.energy.set(value)
83
+
84
+
85
+ class HardEnergy(StandardReadable, Locatable[float]):
86
+ """
87
+ Energy compound device that provides combined change of both DCM energy and undulator gap accordingly.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ dcm: DoubleCrystalMonochromatorBase,
93
+ undulator_energy: HardInsertionDeviceEnergy,
94
+ name: str = "",
95
+ ) -> None:
96
+ self._dcm_ref = Reference(dcm)
97
+ self._undulator_energy_ref = Reference(undulator_energy)
98
+ self.add_readables([undulator_energy, dcm.energy_in_keV])
99
+ super().__init__(name=name)
100
+
101
+ @AsyncStatus.wrap
102
+ async def set(self, value: float) -> None:
103
+ await gather(
104
+ self._dcm_ref().energy_in_keV.set(value),
105
+ self._undulator_energy_ref().set(value),
106
+ )
107
+
108
+ async def locate(self) -> Location[float]:
109
+ return Location(
110
+ setpoint=await self._dcm_ref().energy_in_keV.user_setpoint.get_value(),
111
+ readback=await self._dcm_ref().energy_in_keV.user_readback.get_value(),
112
+ )
@@ -14,11 +14,16 @@ RING_ENERGY_COLUMN = 1
14
14
  MAGNET_FIELD_COLUMN = 2
15
15
  MIN_ENERGY_COLUMN = 3
16
16
  MAX_ENERGY_COLUMN = 4
17
+ MIN_GAP_COLUMN = 5
18
+ MAX_GAP_COLUMN = 6
17
19
  GAP_OFFSET_COLUMN = 7
18
20
 
21
+ MAGNET_BLOCKS_PER_PERIOD = 4
22
+ MAGNTE_BLOCK_HEIGHT_MM = 16
19
23
 
20
- async def get_hu_lut_as_dict(lut_path: str) -> dict:
21
- lut_dict: dict = {}
24
+
25
+ async def get_hu_lut_as_dict(lut_path: str) -> dict[int, np.ndarray]:
26
+ lut_dict: dict[int, np.ndarray] = {}
22
27
  _lookup_table: np.ndarray = await energy_distance_table(
23
28
  lut_path,
24
29
  comments=LUT_COMMENTS,
@@ -26,13 +31,44 @@ async def get_hu_lut_as_dict(lut_path: str) -> dict:
26
31
  )
27
32
  for i in range(_lookup_table.shape[0]):
28
33
  lut_dict[_lookup_table[i][0]] = _lookup_table[i]
29
- LOGGER.debug(f"Loaded lookup table:\n {lut_dict}")
34
+ LOGGER.debug(f"Loaded lookup table: {lut_dict}")
30
35
  return lut_dict
31
36
 
32
37
 
38
+ def _validate_order(order: int, look_up_table: dict[int, "np.ndarray"]) -> None:
39
+ """Validate that the harmonic order exists in the lookup table."""
40
+ if order not in look_up_table.keys():
41
+ raise ValueError(f"Order parameter {order} not found in lookup table")
42
+
43
+
44
+ def _calculate_gamma(look_up_table: dict[int, "np.ndarray"], order: int) -> float:
45
+ """Calculate the Lorentz factor gamma from the lookup table."""
46
+ return 1000 * look_up_table[order][RING_ENERGY_COLUMN] / ELECTRON_REST_ENERGY_MEV
47
+
48
+
49
+ def _calculate_undulator_parameter_max(
50
+ magnet_field: float, undulator_period_mm: int
51
+ ) -> float:
52
+ """
53
+ Calculate the maximum undulator parameter.
54
+ """
55
+ return (
56
+ (
57
+ 2
58
+ * 0.0934
59
+ * undulator_period_mm
60
+ * magnet_field
61
+ * MAGNET_BLOCKS_PER_PERIOD
62
+ / np.pi
63
+ )
64
+ * np.sin(np.pi / MAGNET_BLOCKS_PER_PERIOD)
65
+ * (1 - np.exp(-2 * np.pi * MAGNTE_BLOCK_HEIGHT_MM / undulator_period_mm))
66
+ )
67
+
68
+
33
69
  def calculate_gap_i09_hu(
34
70
  photon_energy_kev: float,
35
- look_up_table: dict[int, "np.ndarray"],
71
+ look_up_table: dict[int, np.ndarray],
36
72
  order: int = 1,
37
73
  gap_offset: float = 0.0,
38
74
  undulator_period_mm: int = 27,
@@ -52,13 +88,9 @@ def calculate_gap_i09_hu(
52
88
  Returns:
53
89
  float: Calculated undulator gap in millimeters.
54
90
  """
55
- magnet_blocks_per_period = 4
56
- magnet_block_height_mm = 16
57
91
 
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
92
+ _validate_order(order, look_up_table)
93
+ gamma = _calculate_gamma(look_up_table, order)
62
94
 
63
95
  # Constructive interference of radiation emitted at different poles
64
96
  # lamda = (lambda_u/2*gamma^2)*(1+K^2/2 + gamma^2*theta^2)/n for n=1,2,3...
@@ -83,17 +115,8 @@ def calculate_gap_i09_hu(
83
115
  # leading to K = 0.934*B0[T]*lambda_u[cm]*exp(-pi*gap/lambda_u) or
84
116
  # K = undulator_parameter_max*exp(-pi*gap/lambda_u)
85
117
  # 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))
118
+ undulator_parameter_max = _calculate_undulator_parameter_max(
119
+ look_up_table[order][MAGNET_FIELD_COLUMN], undulator_period_mm
97
120
  )
98
121
 
99
122
  # Finnaly, rearranging the equation:
@@ -109,3 +132,44 @@ def calculate_gap_i09_hu(
109
132
  )
110
133
 
111
134
  return gap
135
+
136
+
137
+ def calculate_energy_i09_hu(
138
+ gap: float,
139
+ look_up_table: dict[int, "np.ndarray"],
140
+ order: int = 1,
141
+ gap_offset: float = 0.0,
142
+ undulator_period_mm: int = 27,
143
+ ) -> float:
144
+ """
145
+ Calculate the photon energy produced by the undulator at a given gap and harmonic order.
146
+ Reverse of the calculate_gap_i09_hu function.
147
+
148
+ Args:
149
+ gap (float): Undulator gap in millimeters.
150
+ look_up_table (dict[int, np.ndarray]): Lookup table containing undulator and beamline parameters for each harmonic order.
151
+ order (int, optional): Harmonic order for which to calculate the energy. Defaults to 1.
152
+ gap_offset (float, optional): Additional gap offset to apply (in mm). Defaults to 0.0.
153
+ undulator_period_mm (int, optional): Undulator period in mm. Defaults to 27.
154
+
155
+ Returns:
156
+ float: Calculated photon energy in keV.
157
+ """
158
+ _validate_order(order, look_up_table)
159
+
160
+ gamma = _calculate_gamma(look_up_table, order)
161
+ undulator_parameter_max = _calculate_undulator_parameter_max(
162
+ look_up_table[order][MAGNET_FIELD_COLUMN], undulator_period_mm
163
+ )
164
+
165
+ undulator_parameter = undulator_parameter_max / np.exp(
166
+ (gap - look_up_table[order][GAP_OFFSET_COLUMN] - gap_offset)
167
+ / (undulator_period_mm / np.pi)
168
+ )
169
+ energy_kev = (
170
+ 4.959368e-6
171
+ * order
172
+ * np.square(gamma)
173
+ / (undulator_period_mm * (np.square(undulator_parameter) + 2))
174
+ )
175
+ return energy_kev
File without changes
@@ -0,0 +1,86 @@
1
+ from dodal.devices.insertion_device.apple2_undulator import (
2
+ MAXIMUM_MOVE_TIME,
3
+ Apple2,
4
+ Apple2Controller,
5
+ Apple2PhasesVal,
6
+ Apple2Val,
7
+ Pol,
8
+ UndulatorPhaseAxes,
9
+ )
10
+ from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
11
+
12
+ J09_GAP_POLY_DEG_COLUMNS = [
13
+ "9th-order",
14
+ "8th-order",
15
+ "7th-order",
16
+ "6th-order",
17
+ "5th-order",
18
+ "4th-order",
19
+ "3rd-order",
20
+ "2nd-order",
21
+ "1st-order",
22
+ "0th-order",
23
+ ]
24
+
25
+ J09_PHASE_POLY_DEG_COLUMNS = ["0th-order"]
26
+
27
+
28
+ class J09Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
29
+ def __init__(
30
+ self,
31
+ apple2: Apple2[UndulatorPhaseAxes],
32
+ gap_energy_motor_lut: EnergyMotorLookup,
33
+ phase_energy_motor_lut: EnergyMotorLookup,
34
+ units: str = "keV",
35
+ name: str = "",
36
+ ) -> None:
37
+ """
38
+ Parameters:
39
+ -----------
40
+ apple2 : Apple2
41
+ An Apple2 device.
42
+ gap_energy_motor_lut: EnergyMotorLookup
43
+ The class that handles the gap look up table logic for the insertion device.
44
+ phase_energy_motor_lut: EnergyMotorLookup
45
+ The class that handles the phase look up table logic for the insertion device.
46
+ units:
47
+ the units of this device. Defaults to eV.
48
+ name : str, optional
49
+ New device name.
50
+ """
51
+ self.gap_energy_motor_lut = gap_energy_motor_lut
52
+ self.phase_energy_motor_lut = phase_energy_motor_lut
53
+ super().__init__(
54
+ apple2=apple2,
55
+ gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
56
+ phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
57
+ units=units,
58
+ name=name,
59
+ )
60
+
61
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
62
+ return Apple2Val(
63
+ gap=f"{gap:.6f}",
64
+ phase=Apple2PhasesVal(
65
+ top_outer=f"{phase:.6f}",
66
+ top_inner=f"{0.0:.6f}",
67
+ btm_inner=f"{phase:.6f}",
68
+ btm_outer=f"{0.0:.6f}",
69
+ ),
70
+ )
71
+
72
+ async def _set_pol(
73
+ self,
74
+ value: Pol,
75
+ ) -> None:
76
+ # I09 require all palarisation change to go via LH.
77
+ target_energy = await self.energy.get_value()
78
+ if value is not Pol.LH:
79
+ self._polarisation_setpoint_set(Pol.LH)
80
+ max_lh_energy = self.gap_energy_motor_lut.lut.root[Pol.LH].max_energy
81
+ lh_setpoint = (
82
+ max_lh_energy if target_energy > max_lh_energy else target_energy
83
+ )
84
+ await self.energy.set(lh_setpoint, timeout=MAXIMUM_MOVE_TIME)
85
+ self._polarisation_setpoint_set(value)
86
+ await self.energy.set(target_energy, timeout=MAXIMUM_MOVE_TIME)