dls-dodal 1.64.0__py3-none-any.whl → 1.66.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 (75) hide show
  1. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +72 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamline_specific_utils/i05_shared.py +6 -3
  5. dodal/beamlines/aithre.py +21 -2
  6. dodal/beamlines/b01_1.py +1 -1
  7. dodal/beamlines/b07.py +6 -3
  8. dodal/beamlines/b07_1.py +6 -3
  9. dodal/beamlines/i03.py +32 -4
  10. dodal/beamlines/i04.py +18 -3
  11. dodal/beamlines/i05.py +30 -3
  12. dodal/beamlines/i05_1.py +2 -2
  13. dodal/beamlines/i06.py +62 -0
  14. dodal/beamlines/i07.py +20 -0
  15. dodal/beamlines/i09.py +3 -3
  16. dodal/beamlines/i09_1.py +12 -1
  17. dodal/beamlines/i09_2.py +6 -3
  18. dodal/beamlines/i10_optics.py +21 -11
  19. dodal/beamlines/i17.py +3 -3
  20. dodal/beamlines/i18.py +3 -3
  21. dodal/beamlines/i19_2.py +22 -0
  22. dodal/beamlines/i21.py +3 -3
  23. dodal/beamlines/i22.py +3 -20
  24. dodal/beamlines/k07.py +6 -3
  25. dodal/beamlines/p38.py +3 -3
  26. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  27. dodal/devices/aperturescatterguard.py +3 -2
  28. dodal/devices/apple2_undulator.py +89 -44
  29. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  30. dodal/devices/beamsize/__init__.py +0 -0
  31. dodal/devices/beamsize/beamsize.py +6 -0
  32. dodal/devices/cryostream.py +21 -0
  33. dodal/devices/detector/det_resolution.py +4 -2
  34. dodal/devices/fast_grid_scan.py +14 -2
  35. dodal/devices/i03/beamsize.py +35 -0
  36. dodal/devices/i03/constants.py +7 -0
  37. dodal/devices/i03/undulator_dcm.py +2 -2
  38. dodal/devices/i04/beamsize.py +45 -0
  39. dodal/devices/i04/murko_results.py +36 -26
  40. dodal/devices/i04/transfocator.py +23 -29
  41. dodal/devices/i07/id.py +38 -0
  42. dodal/devices/i09_1_shared/__init__.py +6 -2
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i10/i10_apple2.py +22 -316
  45. dodal/devices/i17/i17_apple2.py +7 -4
  46. dodal/devices/i22/nxsas.py +5 -24
  47. dodal/devices/ipin.py +20 -2
  48. dodal/devices/motors.py +19 -3
  49. dodal/devices/mx_phase1/beamstop.py +31 -12
  50. dodal/devices/oav/oav_calculations.py +9 -4
  51. dodal/devices/oav/oav_detector.py +65 -7
  52. dodal/devices/oav/oav_parameters.py +3 -1
  53. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  54. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  55. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  56. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  57. dodal/devices/oav/utils.py +16 -6
  58. dodal/devices/pgm.py +1 -1
  59. dodal/devices/robot.py +17 -7
  60. dodal/devices/scintillator.py +40 -14
  61. dodal/devices/smargon.py +2 -3
  62. dodal/devices/thawer.py +7 -45
  63. dodal/devices/undulator.py +178 -66
  64. dodal/devices/util/lookup_tables_apple2.py +390 -0
  65. dodal/plan_stubs/__init__.py +3 -0
  66. dodal/plans/load_panda_yaml.py +9 -0
  67. dodal/plans/verify_undulator_gap.py +2 -2
  68. dodal/testing/fixtures/run_engine.py +79 -7
  69. dodal/beamline_specific_utils/i03.py +0 -17
  70. dodal/testing/__init__.py +0 -3
  71. dodal/testing/setup.py +0 -67
  72. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
  73. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
  74. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
  75. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import math
3
2
 
4
3
  from ophyd_async.core import (
5
4
  AsyncStatus,
@@ -9,6 +8,7 @@ from ophyd_async.core import (
9
8
  )
10
9
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
11
10
 
11
+ from dodal.common.device_utils import periodic_reminder
12
12
  from dodal.log import LOGGER
13
13
 
14
14
 
@@ -23,15 +23,15 @@ class Transfocator(StandardReadable):
23
23
  """
24
24
 
25
25
  def __init__(self, prefix: str, name: str = ""):
26
+ self._vert_size_calc_sp = epics_signal_rw(float, prefix + "VERT_REQ")
27
+ self._num_lenses_calc_rbv = epics_signal_r(float, prefix + "LENS_PRED")
28
+ self.start = epics_signal_rw(int, prefix + "START.PROC")
29
+ self.start_rbv = epics_signal_r(int, prefix + "START_RBV")
30
+
26
31
  with self.add_children_as_readables():
27
- self.beamsize_set_microns = epics_signal_rw(float, prefix + "VERT_REQ")
28
- self.predicted_vertical_num_lenses = epics_signal_rw(
29
- float, prefix + "LENS_PRED"
30
- )
31
32
  self.number_filters_sp = epics_signal_rw(int, prefix + "NUM_FILTERS")
32
- self.start = epics_signal_rw(int, prefix + "START.PROC")
33
- self.start_rbv = epics_signal_r(int, prefix + "START_RBV")
34
- self.vertical_lens_rbv = epics_signal_r(float, prefix + "VER")
33
+ self.current_horizontal_size_rbv = epics_signal_r(float, prefix + "HOR")
34
+ self.current_vertical_size_rbv = epics_signal_r(float, prefix + "VER")
35
35
 
36
36
  self.TIMEOUT = 120
37
37
 
@@ -41,14 +41,10 @@ class Transfocator(StandardReadable):
41
41
  # We can only put an integer number of lenses in the beam but the
42
42
  # calculation in the IOC returns the theoretical float number of lenses
43
43
  value = round(value)
44
- LOGGER.info(f"Transfocator setting {value} filters")
45
44
  await self.number_filters_sp.set(value)
46
45
  await self.start.set(1)
47
- LOGGER.info("Waiting for start_rbv to change to 1")
48
46
  await wait_for_value(self.start_rbv, 1, self.TIMEOUT)
49
- LOGGER.info("Waiting for start_rbv to change to 0")
50
47
  await wait_for_value(self.start_rbv, 0, self.TIMEOUT)
51
- self.latest_pred_vertical_num_lenses = value
52
48
 
53
49
  @AsyncStatus.wrap
54
50
  async def set(self, value: float):
@@ -59,29 +55,27 @@ class Transfocator(StandardReadable):
59
55
  4. Start the device moving
60
56
  5. Wait for the start_rbv goes high and low again
61
57
  """
62
- self.latest_pred_vertical_num_lenses = (
63
- await self.predicted_vertical_num_lenses.get_value()
64
- )
65
-
66
58
  LOGGER.info(f"Transfocator setting {value} beamsize")
67
59
 
68
- if await self.beamsize_set_microns.get_value() != value:
69
- # Logic in the IOC calculates predicted_vertical_num_lenses when beam_set_microns changes
60
+ # Logic in the IOC calculates _num_lenses_calc_rbv when _vert_size_calc_sp changes
61
+
62
+ # Register an observer before setting _vert_size_calc_sp to ensure we don't miss changes
63
+ num_lenses_calc_iterator = observe_value(
64
+ self._num_lenses_calc_rbv, timeout=self.TIMEOUT
65
+ )
66
+
67
+ await anext(num_lenses_calc_iterator)
68
+ await self._vert_size_calc_sp.set(value)
69
+ calc_lenses = await anext(num_lenses_calc_iterator)
70
70
 
71
- # Register an observer before setting beamsize_set_microns to ensure we don't miss changes
72
- predicted_vertical_num_lenses_iterator = observe_value(
73
- self.predicted_vertical_num_lenses, timeout=self.TIMEOUT
74
- )
75
- # Keep initial prediction before setting to later compare with change after setting
76
- current_prediction = await anext(predicted_vertical_num_lenses_iterator)
77
- await self.beamsize_set_microns.set(value)
78
- accepted_prediction = await anext(predicted_vertical_num_lenses_iterator)
79
- if not math.isclose(current_prediction, accepted_prediction, abs_tol=1e-8):
80
- await self.set_based_on_prediction(accepted_prediction)
71
+ async with periodic_reminder(
72
+ f"Waiting for transfocator to insert {calc_lenses} into beam"
73
+ ):
74
+ await self.set_based_on_prediction(calc_lenses)
81
75
 
82
76
  number_filters_rbv, vertical_lens_size_rbv = await asyncio.gather(
83
77
  self.number_filters_sp.get_value(),
84
- self.vertical_lens_rbv.get_value(),
78
+ self.current_vertical_size_rbv.get_value(),
85
79
  )
86
80
 
87
81
  LOGGER.info(
@@ -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,7 @@
1
- from .hard_undulator_functions import calculate_gap_i09_hu, get_hu_lut_as_dict
1
+ from .hard_undulator_functions import (
2
+ calculate_energy_i09_hu,
3
+ calculate_gap_i09_hu,
4
+ get_hu_lut_as_dict,
5
+ )
2
6
 
3
- __all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict"]
7
+ __all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict", "calculate_energy_i09_hu"]
@@ -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