dls-dodal 1.69.0__py3-none-any.whl → 2.0.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 (282) hide show
  1. {dls_dodal-1.69.0.dist-info → dls_dodal-2.0.0.dist-info}/METADATA +1 -1
  2. dls_dodal-2.0.0.dist-info/RECORD +354 -0
  3. {dls_dodal-1.69.0.dist-info → dls_dodal-2.0.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +10 -17
  6. dodal/beamlines/adsim.py +17 -17
  7. dodal/beamlines/b01_1.py +11 -0
  8. dodal/beamlines/b07.py +17 -21
  9. dodal/beamlines/b07_1.py +20 -22
  10. dodal/beamlines/b07_shared.py +12 -0
  11. dodal/beamlines/b16.py +1 -1
  12. dodal/beamlines/b21.py +15 -6
  13. dodal/beamlines/i02_1.py +3 -3
  14. dodal/beamlines/i02_2.py +1 -1
  15. dodal/beamlines/i03.py +4 -4
  16. dodal/beamlines/i04.py +16 -8
  17. dodal/beamlines/i05.py +7 -45
  18. dodal/beamlines/i05_1.py +4 -13
  19. dodal/beamlines/i05_shared.py +51 -0
  20. dodal/beamlines/i06_1.py +7 -5
  21. dodal/beamlines/{i06.py → i06_shared.py} +25 -14
  22. dodal/beamlines/i07.py +14 -16
  23. dodal/beamlines/i09.py +54 -51
  24. dodal/beamlines/i09_1.py +25 -64
  25. dodal/beamlines/i09_1_shared.py +61 -0
  26. dodal/beamlines/i09_2.py +6 -100
  27. dodal/beamlines/i09_2_shared.py +110 -0
  28. dodal/beamlines/i10.py +60 -54
  29. dodal/beamlines/i10_1.py +99 -10
  30. dodal/beamlines/{i10_optics.py → i10_shared.py} +80 -66
  31. dodal/beamlines/i11.py +31 -18
  32. dodal/beamlines/i13_1.py +1 -1
  33. dodal/beamlines/i15.py +6 -6
  34. dodal/beamlines/i15_1.py +6 -6
  35. dodal/beamlines/i17.py +37 -28
  36. dodal/beamlines/i18.py +3 -4
  37. dodal/beamlines/i19_1.py +95 -34
  38. dodal/beamlines/i19_2.py +68 -52
  39. dodal/beamlines/i19_optics.py +26 -13
  40. dodal/beamlines/i20_1.py +6 -14
  41. dodal/beamlines/i21.py +35 -28
  42. dodal/beamlines/i22.py +19 -4
  43. dodal/beamlines/i23.py +1 -2
  44. dodal/beamlines/i24.py +11 -10
  45. dodal/beamlines/k07.py +99 -5
  46. dodal/beamlines/p38.py +3 -3
  47. dodal/beamlines/p60.py +28 -17
  48. dodal/beamlines/p99.py +16 -15
  49. dodal/beamlines/training_rig.py +20 -12
  50. dodal/cli.py +36 -2
  51. dodal/common/beamlines/beamline_parameters.py +2 -1
  52. dodal/common/beamlines/beamline_utils.py +11 -9
  53. dodal/common/beamlines/commissioning_mode.py +6 -3
  54. dodal/common/coordination.py +12 -14
  55. dodal/common/crystal_metadata.py +5 -8
  56. dodal/common/device_utils.py +4 -3
  57. dodal/common/maths.py +28 -40
  58. dodal/common/udc_directory_provider.py +13 -8
  59. dodal/common/visit.py +18 -21
  60. dodal/common/watcher_utils.py +13 -12
  61. dodal/device_manager.py +94 -54
  62. dodal/devices/aperturescatterguard.py +26 -27
  63. dodal/devices/areadetector/plugins/cam.py +1 -3
  64. dodal/devices/areadetector/plugins/mjpg.py +6 -5
  65. dodal/devices/attenuator/attenuator.py +12 -11
  66. dodal/devices/beamlines/b07/__init__.py +3 -0
  67. dodal/devices/{b07_1 → beamlines/b07_1}/__init__.py +2 -2
  68. dodal/devices/{b07_1 → beamlines/b07_1}/ccmc.py +5 -10
  69. dodal/devices/{b16 → beamlines/b16}/detector.py +2 -3
  70. dodal/devices/{i02_1 → beamlines/i02_1}/fast_grid_scan.py +2 -3
  71. dodal/devices/{i02_1 → beamlines/i02_1}/sample_motors.py +1 -1
  72. dodal/devices/{i03 → beamlines/i03}/beamsize.py +11 -7
  73. dodal/devices/{i03 → beamlines/i03}/dcm.py +1 -2
  74. dodal/devices/{i03 → beamlines/i03}/undulator_dcm.py +4 -5
  75. dodal/devices/beamlines/i04/beam_centre.py +151 -0
  76. dodal/devices/{i04 → beamlines/i04}/beamsize.py +11 -7
  77. dodal/devices/{i04 → beamlines/i04}/murko_results.py +5 -5
  78. dodal/devices/{i04 → beamlines/i04}/transfocator.py +10 -15
  79. dodal/devices/beamlines/i05/__init__.py +3 -0
  80. dodal/devices/beamlines/i06_shared/__init__.py +3 -0
  81. dodal/devices/beamlines/i06_shared/i06_enum.py +7 -0
  82. dodal/devices/{i07 → beamlines/i07}/dcm.py +2 -3
  83. dodal/devices/{i07 → beamlines/i07}/id.py +8 -9
  84. dodal/devices/beamlines/i09/__init__.py +3 -0
  85. dodal/devices/{i09_1_shared → beamlines/i09_1_shared}/hard_energy.py +5 -6
  86. dodal/devices/{i09_1_shared → beamlines/i09_1_shared}/hard_undulator_functions.py +19 -16
  87. dodal/devices/{i10 → beamlines/i10}/diagnostics.py +4 -3
  88. dodal/devices/{i10 → beamlines/i10}/i10_apple2.py +31 -45
  89. dodal/devices/{i10 → beamlines/i10}/rasor/rasor_current_amp.py +1 -24
  90. dodal/devices/{i10 → beamlines/i10}/rasor/rasor_motors.py +2 -2
  91. dodal/devices/{i10 → beamlines/i10}/slits.py +5 -3
  92. dodal/devices/beamlines/i10_1/__init__.py +9 -0
  93. dodal/devices/beamlines/i10_1/electromagnet/magnet.py +16 -0
  94. dodal/devices/beamlines/i10_1/electromagnet/stages.py +14 -0
  95. dodal/devices/beamlines/i10_1/scaler_cards.py +13 -0
  96. dodal/devices/{i11 → beamlines/i11}/cyberstar_blower.py +1 -1
  97. dodal/devices/{i11 → beamlines/i11}/diff_stages.py +4 -6
  98. dodal/devices/{i11 → beamlines/i11}/mythen.py +3 -4
  99. dodal/devices/{i11 → beamlines/i11}/nx100robot.py +6 -6
  100. dodal/devices/{i11 → beamlines/i11}/spinner.py +1 -1
  101. dodal/devices/{i13_1 → beamlines/i13_1}/merlin.py +1 -1
  102. dodal/devices/{i15 → beamlines/i15}/dcm.py +1 -2
  103. dodal/devices/{i15 → beamlines/i15}/focussing_mirror.py +5 -5
  104. dodal/devices/{i15 → beamlines/i15}/jack.py +2 -2
  105. dodal/devices/{i15 → beamlines/i15}/multilayer_mirror.py +1 -1
  106. dodal/devices/{i17 → beamlines/i17}/i17_apple2.py +10 -16
  107. dodal/devices/{i18 → beamlines/i18}/diode.py +1 -1
  108. dodal/devices/{i19 → beamlines/i19}/access_controlled/attenuator_motor_squad.py +12 -8
  109. dodal/devices/{i19 → beamlines/i19}/access_controlled/blueapi_device.py +16 -15
  110. dodal/devices/beamlines/i19/access_controlled/piezo_control.py +72 -0
  111. dodal/devices/{i19 → beamlines/i19}/access_controlled/shutter.py +11 -9
  112. dodal/devices/{i19 → beamlines/i19}/backlight.py +3 -1
  113. dodal/devices/{i19 → beamlines/i19}/mapt_configuration.py +2 -1
  114. dodal/devices/{i19 → beamlines/i19}/pin_col_stages.py +11 -8
  115. dodal/devices/beamlines/i19/pin_tip.py +32 -0
  116. dodal/devices/beamlines/i21/__init__.py +3 -0
  117. dodal/devices/{i22 → beamlines/i22}/dcm.py +1 -2
  118. dodal/devices/{i22 → beamlines/i22}/fswitch.py +1 -3
  119. dodal/devices/{i22 → beamlines/i22}/nxsas.py +5 -4
  120. dodal/devices/{i24 → beamlines/i24}/beam_center.py +1 -1
  121. dodal/devices/{i24 → beamlines/i24}/beamstop.py +2 -2
  122. dodal/devices/{i24 → beamlines/i24}/commissioning_jungfrau.py +3 -2
  123. dodal/devices/{i24 → beamlines/i24}/dcm.py +1 -3
  124. dodal/devices/{i24 → beamlines/i24}/dual_backlight.py +3 -3
  125. dodal/devices/{i24 → beamlines/i24}/pmac.py +9 -7
  126. dodal/devices/{p60 → beamlines/p60}/lab_xray_source.py +1 -1
  127. dodal/devices/beamlines/p99/__init__.py +0 -0
  128. dodal/devices/{p99 → beamlines/p99}/andor2_point.py +11 -15
  129. dodal/devices/bimorph_mirror.py +22 -20
  130. dodal/devices/collimation_table.py +3 -2
  131. dodal/devices/common_dcm.py +30 -20
  132. dodal/devices/controllers.py +2 -2
  133. dodal/devices/cryostream.py +8 -0
  134. dodal/devices/current_amplifiers/current_amplifier.py +16 -18
  135. dodal/devices/current_amplifiers/current_amplifier_detector.py +9 -10
  136. dodal/devices/current_amplifiers/femto.py +8 -9
  137. dodal/devices/current_amplifiers/sr570.py +16 -16
  138. dodal/devices/current_amplifiers/struck_scaler_counter.py +5 -5
  139. dodal/devices/detector/det_resolution.py +9 -8
  140. dodal/devices/detector/detector.py +4 -2
  141. dodal/devices/diamond_filter.py +3 -4
  142. dodal/devices/eiger.py +3 -3
  143. dodal/devices/eiger_odin.py +1 -1
  144. dodal/devices/electron_analyser/base/base_controller.py +13 -13
  145. dodal/devices/electron_analyser/base/base_detector.py +15 -20
  146. dodal/devices/electron_analyser/base/base_driver_io.py +39 -46
  147. dodal/devices/electron_analyser/base/base_region.py +27 -30
  148. dodal/devices/electron_analyser/base/base_util.py +18 -16
  149. dodal/devices/electron_analyser/base/energy_sources.py +13 -19
  150. dodal/devices/eurotherm.py +3 -2
  151. dodal/devices/fast_grid_scan.py +31 -34
  152. dodal/devices/fast_shutter.py +24 -21
  153. dodal/devices/flux.py +1 -1
  154. dodal/devices/focusing_mirror.py +29 -11
  155. dodal/devices/hutch_shutter.py +6 -6
  156. dodal/devices/insertion_device/__init__.py +8 -0
  157. dodal/devices/insertion_device/apple2_controller.py +51 -60
  158. dodal/devices/insertion_device/apple2_undulator.py +40 -64
  159. dodal/devices/insertion_device/apple_knot_controller.py +222 -0
  160. dodal/devices/insertion_device/energy.py +100 -27
  161. dodal/devices/insertion_device/energy_motor_lookup.py +20 -27
  162. dodal/devices/insertion_device/lookup_table_models.py +45 -50
  163. dodal/devices/insertion_device/polarisation.py +1 -1
  164. dodal/devices/ipin.py +1 -1
  165. dodal/devices/linkam3.py +7 -5
  166. dodal/devices/motors.py +107 -19
  167. dodal/devices/mx_phase1/beamstop.py +2 -4
  168. dodal/devices/oav/oav_calculations.py +20 -13
  169. dodal/devices/oav/oav_detector.py +28 -23
  170. dodal/devices/oav/oav_parameters.py +4 -9
  171. dodal/devices/oav/oav_to_redis_forwarder.py +22 -18
  172. dodal/devices/oav/pin_image_recognition/__init__.py +4 -6
  173. dodal/devices/oav/pin_image_recognition/manual_test.py +1 -2
  174. dodal/devices/oav/pin_image_recognition/utils.py +30 -32
  175. dodal/devices/oav/snapshots/grid_overlay.py +10 -9
  176. dodal/devices/oav/snapshots/snapshot_image_processing.py +15 -13
  177. dodal/devices/oav/utils.py +9 -12
  178. dodal/devices/p45.py +3 -9
  179. dodal/devices/pgm.py +8 -14
  180. dodal/devices/pressure_jump_cell.py +93 -32
  181. dodal/devices/qbpm.py +1 -3
  182. dodal/devices/robot.py +12 -4
  183. dodal/devices/s4_slit_gaps.py +1 -1
  184. dodal/devices/selectable_source.py +5 -2
  185. dodal/devices/slits.py +2 -5
  186. dodal/devices/smargon.py +2 -3
  187. dodal/devices/temperture_controller/lakeshore/lakeshore.py +38 -64
  188. dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +21 -35
  189. dodal/devices/tetramm.py +7 -7
  190. dodal/devices/turbo_slit.py +8 -7
  191. dodal/devices/undulator.py +42 -56
  192. dodal/devices/util/adjuster_plans.py +2 -3
  193. dodal/devices/util/epics_util.py +10 -10
  194. dodal/devices/util/lookup_tables.py +17 -18
  195. dodal/devices/v2f.py +2 -3
  196. dodal/devices/watsonmarlow323_pump.py +1 -1
  197. dodal/devices/xbpm_feedback.py +3 -2
  198. dodal/devices/xspress3/xspress3.py +8 -11
  199. dodal/devices/xspress3/xspress3_channel.py +3 -6
  200. dodal/devices/zebra/zebra.py +6 -7
  201. dodal/devices/zebra/zebra_constants_mapping.py +11 -7
  202. dodal/devices/zebra/zebra_controlled_shutter.py +2 -1
  203. dodal/devices/zocalo/zocalo_interaction.py +14 -14
  204. dodal/devices/zocalo/zocalo_results.py +33 -33
  205. dodal/log.py +23 -20
  206. dodal/plan_stubs/check_topup.py +15 -15
  207. dodal/plan_stubs/data_session.py +6 -6
  208. dodal/plan_stubs/motor_utils.py +22 -18
  209. dodal/plan_stubs/pressure_jump_cell.py +18 -0
  210. dodal/plan_stubs/wrapped.py +40 -55
  211. dodal/plans/bimorph.py +63 -52
  212. dodal/plans/device_setup_plans/__init__.py +5 -0
  213. dodal/plans/device_setup_plans/setup_pin_tip_params.py +63 -0
  214. dodal/plans/preprocessors/verify_undulator_gap.py +10 -8
  215. dodal/plans/spec_path.py +3 -5
  216. dodal/plans/verify_undulator_gap.py +1 -2
  217. dodal/plans/wrapped.py +4 -3
  218. dodal/testing/electron_analyser/device_factory.py +5 -7
  219. dodal/testing/fixtures/devices/apple2.py +38 -0
  220. dodal/testing/fixtures/run_engine.py +3 -7
  221. dodal/testing/fixtures/utils.py +1 -2
  222. dodal/utils.py +60 -58
  223. dls_dodal-1.69.0.dist-info/RECORD +0 -338
  224. dodal/beamline_specific_utils/i05_shared.py +0 -14
  225. dodal/devices/b07/__init__.py +0 -3
  226. dodal/devices/i04/beam_centre.py +0 -84
  227. dodal/devices/i05/__init__.py +0 -3
  228. dodal/devices/i09/__init__.py +0 -3
  229. dodal/devices/i21/__init__.py +0 -5
  230. {dls_dodal-1.69.0.dist-info → dls_dodal-2.0.0.dist-info}/entry_points.txt +0 -0
  231. {dls_dodal-1.69.0.dist-info → dls_dodal-2.0.0.dist-info}/licenses/LICENSE +0 -0
  232. {dls_dodal-1.69.0.dist-info → dls_dodal-2.0.0.dist-info}/top_level.txt +0 -0
  233. /dodal/{beamline_specific_utils → devices/beamlines}/__init__.py +0 -0
  234. /dodal/devices/{b07 → beamlines/b07}/enums.py +0 -0
  235. /dodal/devices/{b07_1 → beamlines/b07_1}/enums.py +0 -0
  236. /dodal/devices/{b16 → beamlines/b16}/__init__.py +0 -0
  237. /dodal/devices/{i02_1 → beamlines/i02_1}/__init__.py +0 -0
  238. /dodal/devices/{i02_2 → beamlines/i02_2}/__init__.py +0 -0
  239. /dodal/devices/{i03 → beamlines/i03}/__init__.py +0 -0
  240. /dodal/devices/{i03 → beamlines/i03}/constants.py +0 -0
  241. /dodal/devices/{i04 → beamlines/i04}/__init__.py +0 -0
  242. /dodal/devices/{i04 → beamlines/i04}/constants.py +0 -0
  243. /dodal/devices/{i04 → beamlines/i04}/max_pixel.py +0 -0
  244. /dodal/devices/{i05 → beamlines/i05}/enums.py +0 -0
  245. /dodal/devices/{i07 → beamlines/i07}/__init__.py +0 -0
  246. /dodal/devices/{i09 → beamlines/i09}/enums.py +0 -0
  247. /dodal/devices/{i09_1 → beamlines/i09_1}/__init__.py +0 -0
  248. /dodal/devices/{i09_1 → beamlines/i09_1}/enums.py +0 -0
  249. /dodal/devices/{i09_1_shared → beamlines/i09_1_shared}/__init__.py +0 -0
  250. /dodal/devices/{i09_2_shared → beamlines/i09_2_shared}/__init__.py +0 -0
  251. /dodal/devices/{i09_2_shared → beamlines/i09_2_shared}/i09_apple2.py +0 -0
  252. /dodal/devices/{i10 → beamlines/i10}/__init__.py +0 -0
  253. /dodal/devices/{i10 → beamlines/i10}/i10_setting_data.py +0 -0
  254. /dodal/devices/{i10 → beamlines/i10}/mirrors.py +0 -0
  255. /dodal/devices/{i10 → beamlines/i10}/rasor/__init__.py +0 -0
  256. /dodal/devices/{i10 → beamlines/i10}/rasor/rasor_scaler_cards.py +0 -0
  257. /dodal/devices/{i11 → beamlines/i10_1/electromagnet}/__init__.py +0 -0
  258. /dodal/devices/{i13_1 → beamlines/i11}/__init__.py +0 -0
  259. /dodal/devices/{i15 → beamlines/i13_1}/__init__.py +0 -0
  260. /dodal/devices/{i13_1 → beamlines/i13_1}/merlin_controller.py +0 -0
  261. /dodal/devices/{i17 → beamlines/i15}/__init__.py +0 -0
  262. /dodal/devices/{i15 → beamlines/i15}/laue.py +0 -0
  263. /dodal/devices/{i15 → beamlines/i15}/motors.py +0 -0
  264. /dodal/devices/{i15 → beamlines/i15}/rail.py +0 -0
  265. /dodal/devices/{i18 → beamlines/i17}/__init__.py +0 -0
  266. /dodal/devices/{i19 → beamlines/i18}/__init__.py +0 -0
  267. /dodal/devices/{i18 → beamlines/i18}/kb_mirror.py +0 -0
  268. /dodal/devices/{i19/access_controlled → beamlines/i19}/__init__.py +0 -0
  269. /dodal/devices/{i20_1 → beamlines/i19/access_controlled}/__init__.py +0 -0
  270. /dodal/devices/{i19 → beamlines/i19}/access_controlled/hutch_access.py +0 -0
  271. /dodal/devices/{i19 → beamlines/i19}/beamstop.py +0 -0
  272. /dodal/devices/{i19 → beamlines/i19}/diffractometer.py +0 -0
  273. /dodal/devices/{i22 → beamlines/i20_1}/__init__.py +0 -0
  274. /dodal/devices/{i21 → beamlines/i21}/enums.py +0 -0
  275. /dodal/devices/{i24 → beamlines/i22}/__init__.py +0 -0
  276. /dodal/devices/{p99 → beamlines/i24}/__init__.py +0 -0
  277. /dodal/devices/{i24 → beamlines/i24}/aperture.py +0 -0
  278. /dodal/devices/{i24 → beamlines/i24}/focus_mirrors.py +0 -0
  279. /dodal/devices/{i24 → beamlines/i24}/vgonio.py +0 -0
  280. /dodal/devices/{p60 → beamlines/p60}/__init__.py +0 -0
  281. /dodal/devices/{p60 → beamlines/p60}/enums.py +0 -0
  282. /dodal/devices/{p99 → beamlines/p99}/sample_stage.py +0 -0
@@ -0,0 +1,222 @@
1
+ from math import isclose
2
+ from typing import Generic
3
+
4
+ from numpy import sign
5
+
6
+ from dodal.common import Rectangle2D
7
+ from dodal.devices.insertion_device import (
8
+ Apple2,
9
+ Apple2Controller,
10
+ Apple2Val,
11
+ EnergyMotorConvertor,
12
+ )
13
+ from dodal.devices.insertion_device.apple2_undulator import (
14
+ Apple2LockedPhasesVal,
15
+ PhaseAxesType,
16
+ )
17
+ from dodal.devices.insertion_device.enum import Pol
18
+ from dodal.log import LOGGER
19
+
20
+ APPLE_KNOT_MAXIMUM_GAP_MOTOR_POSITION = 100.0
21
+ APPLE_KNOT_MAXIMUM_PHASE_MOTOR_POSITION = 70.0
22
+
23
+
24
+ class AppleKnotPathFinder:
25
+ """Class to find a safe path for AppleKnot undulator moves that avoids the exclusion
26
+ zone around 0-0 gap-phase. We rely on axis-aligned (manhattan) moves and splitting
27
+ moves that cross zero phase into two segments via an intermediate point at zero
28
+ phase and a safe gap value. We ASSUME the exclusion zones are rectangles aligned
29
+ with the axes in a shape of hanoi tower centered at (0,0).
30
+ Gap and phase motors are NOT moved together but instead are moved sequentially.
31
+ Sequential move guarantees safe pass avoiding exslusion zones.
32
+ We can not use asynchronous move of gap and phase because we can not currently rely
33
+ on gap and phase motors relative speed.
34
+ See https://confluence.diamond.ac.uk/x/vQENAg for more details.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ exclusion_zone: tuple[Rectangle2D, ...],
40
+ ) -> None:
41
+ # Define the exclusion zone rectangles around (0,0)
42
+ self.exclusion_zone = exclusion_zone
43
+
44
+ def get_apple_knot_val_path(
45
+ self, start_val: Apple2Val, end_val: Apple2Val
46
+ ) -> tuple[Apple2Val, ...]:
47
+ """Get a list of Apple2Val representing the path from start to end avoiding
48
+ exclusion zones.
49
+ """
50
+ apple_knot_val_path = ()
51
+ # Defensive checks for no movement
52
+ if (
53
+ start_val.gap == end_val.gap
54
+ and start_val.phase.top_outer == end_val.phase.top_outer
55
+ ):
56
+ LOGGER.warning("Start point same as end point, no path calculated.")
57
+ return apple_knot_val_path
58
+ for zone in self.exclusion_zone:
59
+ for value in (start_val, end_val):
60
+ if zone.contains(value.phase.top_outer, value.gap):
61
+ LOGGER.warning(
62
+ "Start point is inside exclusion zone, no path calculated."
63
+ )
64
+ return apple_knot_val_path
65
+ apple_knot_val_path += (start_val,)
66
+ # Split the move if start and end are on opposite sides of zero phase
67
+ # TBD This can be potentially improved to always have max 3 sections in path!
68
+ # Currently in copies java class logic
69
+ if (
70
+ sign(start_val.phase.top_outer) == (-1) * sign(end_val.phase.top_outer)
71
+ and sign(start_val.phase.top_outer) != 0
72
+ ):
73
+ apple_knot_val_path += (
74
+ self._get_zero_phase_crossing_point(start_val, end_val),
75
+ )
76
+ apple_knot_val_path += (end_val,)
77
+ return self._apple_knot_manhattan_path(apple_knot_val_path)
78
+
79
+ def _apple_knot_manhattan_path(
80
+ self, apple_knot_val_path: tuple[Apple2Val, ...]
81
+ ) -> tuple[Apple2Val, ...]:
82
+ """Convert a list of Apple2Val into a manhattan path avoiding exclusion zones.
83
+ Here all moves are done in axis-aligned steps (gap first then phase or vice
84
+ versa).
85
+ List of points is expanded to include intermediate points as needed so each move
86
+ happens within one sign of gap and phase (including zero phase).
87
+ For convenience we define:phase increase as West-East axis and gap increase
88
+ as South-North axis. Only SW move in negative phase region and SE move in
89
+ positive phase region need a PHASE first then GAP move, the rest needs GAP
90
+ first then PHASE move or there is no difference in order.
91
+ """
92
+ final_path = []
93
+ for i in range(len(apple_knot_val_path) - 1):
94
+ start_val = apple_knot_val_path[i]
95
+ end_val = apple_knot_val_path[i + 1]
96
+ final_path.append(start_val)
97
+ # Direct move along one axis, no intermediate point needed
98
+ if (
99
+ end_val.phase.top_outer == start_val.phase.top_outer
100
+ or end_val.gap == start_val.gap
101
+ ):
102
+ continue
103
+ # Determine move order based on quadrant rules
104
+ if end_val.gap <= start_val.gap and abs(end_val.phase.top_outer) > abs(
105
+ start_val.phase.top_outer
106
+ ):
107
+ # Move PHASE first then GAP (SW move in negative phase or SE move in positive phase)
108
+ intermediate_val = Apple2Val(gap=start_val.gap, phase=end_val.phase)
109
+ final_path.append(intermediate_val)
110
+ else:
111
+ # Move GAP first then PHASE (other moves)
112
+ intermediate_val = Apple2Val(gap=end_val.gap, phase=start_val.phase)
113
+ final_path.append(intermediate_val)
114
+ final_path.append(apple_knot_val_path[-1])
115
+ return tuple(final_path)
116
+
117
+ def _get_zero_phase_crossing_point(
118
+ self, start_val: Apple2Val, end_val: Apple2Val
119
+ ) -> Apple2Val:
120
+ # Calculate the point where phase crosses zero
121
+ max_exclusion_gap = (
122
+ max([zone.get_max_y() for zone in self.exclusion_zone])
123
+ if self.exclusion_zone
124
+ else 0.0
125
+ )
126
+ return Apple2Val(
127
+ gap=max(
128
+ (start_val.gap + end_val.gap) / 2, max_exclusion_gap
129
+ ), # Ensure gap is above a minimum value
130
+ phase=Apple2LockedPhasesVal(
131
+ top_outer=0.0,
132
+ btm_inner=0.0,
133
+ ),
134
+ )
135
+
136
+
137
+ class AppleKnotController(
138
+ Apple2Controller[Apple2[PhaseAxesType]], Generic[PhaseAxesType]
139
+ ):
140
+ """Controller for Apple Knot undulator with unique feature of calculating a move
141
+ path through gap and phase space avoiding the exclusion zone around 0-0 gap-phase.
142
+ See https://confluence.diamond.ac.uk/x/vQENAg for more details.
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ apple: Apple2[PhaseAxesType],
148
+ gap_energy_motor_converter: EnergyMotorConvertor,
149
+ phase_energy_motor_converter: EnergyMotorConvertor,
150
+ path_finder: AppleKnotPathFinder,
151
+ maximum_gap_motor_position: float = APPLE_KNOT_MAXIMUM_GAP_MOTOR_POSITION,
152
+ maximum_phase_motor_position: float = APPLE_KNOT_MAXIMUM_PHASE_MOTOR_POSITION,
153
+ units: str = "eV",
154
+ name: str = "",
155
+ ) -> None:
156
+ self.path_finder = path_finder
157
+ super().__init__(
158
+ apple2=apple,
159
+ gap_energy_motor_converter=gap_energy_motor_converter,
160
+ phase_energy_motor_converter=phase_energy_motor_converter,
161
+ maximum_gap_motor_position=maximum_gap_motor_position,
162
+ maximum_phase_motor_position=maximum_phase_motor_position,
163
+ units=units,
164
+ name=name,
165
+ )
166
+
167
+ async def _set_energy(self, energy: float) -> None:
168
+ await self.check_top_bottom_phase_match()
169
+ pol = await self._check_and_get_pol_setpoint()
170
+ await self._combined_move(energy, pol)
171
+ self._energy_set(energy)
172
+
173
+ async def check_top_bottom_phase_match(self) -> None:
174
+ """Check that the top and bottom phase motors are in sync.
175
+ Raise an error if they are not within tolerance.
176
+ """
177
+ current_phase_top = float(
178
+ await self.apple2().phase().top_outer.user_readback.get_value()
179
+ )
180
+ current_phase_bottom = float(
181
+ await self.apple2().phase().btm_inner.user_readback.get_value()
182
+ )
183
+ if not isclose(current_phase_top, current_phase_bottom, abs_tol=5e-2):
184
+ raise RuntimeError(
185
+ f"Upper phase {current_phase_top} and lower phase {current_phase_bottom} values are not close enough."
186
+ )
187
+
188
+ async def _combined_move(self, energy: float, pol: Pol) -> None:
189
+ # get current apple2 value
190
+ current_phase_top = float(
191
+ await self.apple2().phase().top_outer.user_readback.get_value()
192
+ )
193
+ current_gap = float(await self.apple2().gap().user_readback.get_value())
194
+ current_apple2_val = self._get_apple2_value(
195
+ current_gap, current_phase_top, Pol.NONE
196
+ )
197
+ # get target apple2 value
198
+ target_gap = self.gap_energy_motor_converter(energy=energy, pol=pol)
199
+ target_phase = self.phase_energy_motor_converter(energy=energy, pol=pol)
200
+ target_apple2_val = self._get_apple2_value(target_gap, target_phase, pol)
201
+ # get path avoiding exclusion zone
202
+ manhattan_path = self.path_finder.get_apple_knot_val_path(
203
+ current_apple2_val, target_apple2_val
204
+ )
205
+ if manhattan_path == ():
206
+ raise RuntimeError("No valid path found for move avoiding exclusion zones.")
207
+ # execute the moves along the path
208
+ for apple2_val in manhattan_path:
209
+ LOGGER.info(f"Moving to apple2 values: {apple2_val}")
210
+ await self.apple2().set(id_motor_values=apple2_val)
211
+
212
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
213
+ apple2_val = Apple2Val(
214
+ gap=gap,
215
+ phase=Apple2LockedPhasesVal(
216
+ top_outer=phase,
217
+ btm_inner=phase,
218
+ ),
219
+ )
220
+ LOGGER.info(f"Getting apple2 value for gap={gap}, phase={phase}.")
221
+ LOGGER.info(f"Apple2 motor values: {apple2_val}.")
222
+ return apple2_val
@@ -1,18 +1,25 @@
1
1
  import abc
2
2
  import asyncio
3
3
 
4
- from bluesky.protocols import Movable
4
+ from bluesky.protocols import Flyable, Movable, Preparable
5
5
  from ophyd_async.core import (
6
6
  AsyncStatus,
7
+ FlyMotorInfo,
7
8
  Reference,
8
9
  SignalRW,
9
10
  StandardReadable,
10
11
  StandardReadableFormat,
12
+ WatchableAsyncStatus,
13
+ error_if_none,
11
14
  soft_signal_rw,
12
15
  )
13
16
  from ophyd_async.epics.motor import Motor
14
17
 
15
- from dodal.devices.insertion_device import MAXIMUM_MOVE_TIME, Apple2Controller
18
+ from dodal.devices.insertion_device import (
19
+ MAXIMUM_MOVE_TIME,
20
+ Apple2Controller,
21
+ Apple2Type,
22
+ )
16
23
  from dodal.log import LOGGER
17
24
 
18
25
 
@@ -28,26 +35,76 @@ class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
28
35
  async def set(self, energy: float) -> None: ...
29
36
 
30
37
 
31
- class BeamEnergy(StandardReadable, Movable[float]):
32
- """
33
- Compound device to set both ID and energy motor at the same time with an option to add an offset.
34
- """
38
+ class InsertionDeviceEnergy(InsertionDeviceEnergyBase, Preparable, Flyable):
39
+ """Apple2 ID energy movable device."""
35
40
 
36
41
  def __init__(
37
- self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
42
+ self, id_controller: Apple2Controller[Apple2Type], name: str = ""
38
43
  ) -> None:
39
- """
40
- Parameters
41
- ----------
42
-
43
- id_energy: InsertionDeviceEnergy
44
- An InsertionDeviceEnergy device.
45
- mono: Motor
46
- A Motor(energy) device.
47
- name:
48
- New device name.
49
- """
44
+ self.energy = Reference(id_controller.energy)
45
+ self._id_controller = Reference(id_controller)
50
46
  super().__init__(name=name)
47
+
48
+ self.add_readables([self.energy()], StandardReadableFormat.HINTED_SIGNAL)
49
+
50
+ @AsyncStatus.wrap
51
+ async def set(self, energy: float) -> None:
52
+ LOGGER.info(f"Setting insertion device energy to {energy}.")
53
+ await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
54
+
55
+ @AsyncStatus.wrap
56
+ async def prepare(self, value: FlyMotorInfo) -> None:
57
+ """Convert FlyMotorInfo from energy to gap motion and move phase motor to mid point."""
58
+ mid_energy = (value.start_position + value.end_position) / 2.0
59
+ LOGGER.info(
60
+ f"Preparing for fly energy scan, move {self._id_controller().apple2().phase} to {mid_energy}"
61
+ )
62
+ await self.set(energy=mid_energy)
63
+ current_pol = await self._id_controller().polarisation_setpoint.get_value()
64
+ start_position = self._id_controller().gap_energy_motor_converter(
65
+ energy=value.start_position,
66
+ pol=current_pol,
67
+ )
68
+ end_position = self._id_controller().gap_energy_motor_converter(
69
+ energy=value.end_position, pol=current_pol
70
+ )
71
+
72
+ gap_fly_motor_info = FlyMotorInfo(
73
+ start_position=start_position,
74
+ end_position=end_position,
75
+ time_for_move=value.time_for_move,
76
+ )
77
+
78
+ LOGGER.info(
79
+ f"Flyscan info in energy: {value}. "
80
+ + f"Flyscan info in gap: {gap_fly_motor_info}. "
81
+ + f"Speed: {gap_fly_motor_info.velocity}."
82
+ )
83
+ await self._id_controller().apple2().gap().prepare(value=gap_fly_motor_info)
84
+
85
+ @AsyncStatus.wrap
86
+ async def kickoff(self):
87
+ await self._id_controller().apple2().gap().kickoff()
88
+
89
+ def complete(self) -> WatchableAsyncStatus:
90
+ return self._id_controller().apple2().gap().complete()
91
+
92
+ async def get_id_acceleration_time(self) -> float:
93
+ return await self._id_controller().apple2().gap().acceleration_time.get_value()
94
+
95
+
96
+ class BeamEnergy(StandardReadable, Movable[float], Preparable, Flyable):
97
+ """Compound device to set both ID and energy motor at the same time with an option to add an offset.
98
+
99
+ Args:
100
+ id_energy (InsertionDeviceEnergy): An InsertionDeviceEnergy device.
101
+ mono (Motor): A Motor(energy) device.
102
+ name: New device name.
103
+ """
104
+
105
+ def __init__(
106
+ self, id_energy: InsertionDeviceEnergy, mono: Motor, name: str = ""
107
+ ) -> None:
51
108
  self._id_energy = Reference(id_energy)
52
109
  self._mono_energy = Reference(mono)
53
110
 
@@ -61,6 +118,7 @@ class BeamEnergy(StandardReadable, Movable[float]):
61
118
 
62
119
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
63
120
  self.id_energy_offset = soft_signal_rw(float, initial_value=0)
121
+ super().__init__(name=name)
64
122
 
65
123
  @AsyncStatus.wrap
66
124
  async def set(self, energy: float) -> None:
@@ -72,17 +130,32 @@ class BeamEnergy(StandardReadable, Movable[float]):
72
130
  self._mono_energy().set(energy),
73
131
  )
74
132
 
133
+ @AsyncStatus.wrap
134
+ async def prepare(self, value: FlyMotorInfo) -> None:
135
+ await asyncio.gather(
136
+ self._id_energy().prepare(value), self._mono_energy().prepare(value)
137
+ )
75
138
 
76
- class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
77
- """Apple2 ID energy movable device."""
139
+ @AsyncStatus.wrap
140
+ async def kickoff(self):
141
+ pgm_acceleration_time, gap_acceleration_time = await asyncio.gather(
142
+ self._mono_energy().acceleration_time.get_value(),
143
+ self._id_energy().get_id_acceleration_time(),
144
+ )
145
+ start_offset_time = pgm_acceleration_time - gap_acceleration_time
78
146
 
79
- def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
80
- self.energy = Reference(id_controller.energy)
81
- super().__init__(name=name)
147
+ await self._mono_energy().kickoff()
148
+ await asyncio.sleep(start_offset_time)
149
+ await self._id_energy().kickoff()
150
+ self._fly_status = self._combined_fly_status()
82
151
 
83
- self.add_readables([self.energy()], StandardReadableFormat.HINTED_SIGNAL)
152
+ def complete(self) -> AsyncStatus:
153
+ """Stop when both pgm and id is done moving."""
154
+ fly_status = error_if_none(self._fly_status, "kickoff not called")
155
+ return fly_status
84
156
 
85
157
  @AsyncStatus.wrap
86
- async def set(self, energy: float) -> None:
87
- LOGGER.info(f"Setting insertion device energy to {energy}.")
88
- await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
158
+ async def _combined_fly_status(self):
159
+ status_pgm = self._mono_energy().complete()
160
+ status_id = self._id_energy().complete()
161
+ await asyncio.gather(status_pgm, status_id)
@@ -11,12 +11,12 @@ from dodal.devices.insertion_device.lookup_table_models import (
11
11
 
12
12
 
13
13
  class EnergyMotorLookup:
14
- """
15
- Handles a lookup table for Apple2 ID, converting energy/polarisation to a motor
14
+ """Handles a lookup table for Apple2 ID, converting energy/polarisation to a motor
16
15
  position.
17
16
 
18
- After update_lookup_table() has populated the lookup table, `find_value_in_lookup_table()`
19
- can be used to compute gap / phase for a requested energy / polarisation pair.
17
+ After update_lookup_table() has populated the lookup table,
18
+ `find_value_in_lookup_table()` can be used to compute gap / phase for a requested
19
+ energy / polarisation pair.
20
20
  """
21
21
 
22
22
  def __init__(self, lut: LookupTable | None = None):
@@ -26,24 +26,19 @@ class EnergyMotorLookup:
26
26
 
27
27
  def update_lookup_table(self) -> None:
28
28
  """Do nothing by default. Sub classes may override this method to provide logic
29
- on what updating lookup table does."""
29
+ on what updating lookup table does.
30
+ """
30
31
  pass
31
32
 
32
33
  def find_value_in_lookup_table(self, energy: float, pol: Pol) -> float:
33
- """
34
- Convert energy and polarisation to a value from the lookup table.
34
+ """Convert energy and polarisation to a value from the lookup table.
35
35
 
36
- Parameters:
37
- -----------
38
- energy : float
39
- Desired energy.
40
- pol : Pol
41
- Polarisation mode.
36
+ Args:
37
+ energy (float): Desired energy.
38
+ pol (Pol): Polarisation mode.
42
39
 
43
40
  Returns:
44
- ----------
45
- float
46
- gap / phase motor position from the lookup table.
41
+ float: gap / phase motor position from the lookup table.
47
42
  """
48
43
  # if lut is empty, force an update to pull updated lut incase subclasses have
49
44
  # implemented it.
@@ -55,7 +50,15 @@ class EnergyMotorLookup:
55
50
 
56
51
  class ConfigServerEnergyMotorLookup(EnergyMotorLookup):
57
52
  """Fetches and parses lookup table (csv) from a config server, supports dynamic
58
- updates, and validates input."""
53
+ updates, and validates input.
54
+
55
+ Args:
56
+ config_client (ConfigServer): The config server client to fetch the look up
57
+ table data.
58
+ lut_config (LookupTableColumnConfig): Configuration that defines how to
59
+ process file contents into a LookupTable.
60
+ path (Path): File path to the lookup table.
61
+ """
59
62
 
60
63
  def __init__(
61
64
  self,
@@ -63,16 +66,6 @@ class ConfigServerEnergyMotorLookup(EnergyMotorLookup):
63
66
  lut_config: LookupTableColumnConfig,
64
67
  path: Path,
65
68
  ):
66
- """
67
- Parameters:
68
- -----------
69
- config_client:
70
- The config server client to fetch the look up table data.
71
- lut_config:
72
- Configuration that defines how to process file contents into a LookupTable
73
- path:
74
- File path to the lookup table.
75
- """
76
69
  self.path = path
77
70
  self.config_client = config_client
78
71
  self.lut_config = lut_config
@@ -6,21 +6,21 @@ in-memory dictionary format used by the Apple2 controllers.
6
6
 
7
7
  Data format produced
8
8
  The lookup-table dictionary created by convert_csv_to_lookup() follows this
9
- structure:
10
-
11
- {
12
- "POL_MODE": {
13
- "energy_entries": [
14
- {
15
- "low": <float>,
16
- "high": <float>,
17
- "poly": <numpy.poly1d>
18
- },
19
- ...
20
- ]
21
- },
22
- ...
23
- }
9
+ structure::
10
+
11
+ {
12
+ "POL_MODE": {
13
+ "energy_entries": [
14
+ {
15
+ "low": <float>,
16
+ "high": <float>,
17
+ "poly": <numpy.poly1d>
18
+ },
19
+ ...
20
+ ]
21
+ },
22
+ ...
23
+ }
24
24
  """
25
25
 
26
26
  import csv
@@ -76,7 +76,9 @@ class Source(NamedTuple):
76
76
 
77
77
 
78
78
  class LookupTableColumnConfig(BaseModel):
79
- """Configuration on how to process a csv file columns into a LookupTable data model."""
79
+ """Configuration on how to process a csv file columns into a LookupTable data
80
+ model.
81
+ """
80
82
 
81
83
  source: A[
82
84
  Source | None,
@@ -113,7 +115,9 @@ class EnergyCoverageEntry(BaseModel):
113
115
  def validate_and_convert_poly(
114
116
  cls: type[Self], value: np.poly1d | list
115
117
  ) -> np.poly1d:
116
- """If reading from serialized data, it will be using a list. Convert to np.poly1d"""
118
+ """If reading from serialized data, it will be using a list. Convert to
119
+ np.poly1d.
120
+ """
117
121
  if isinstance(value, list):
118
122
  return np.poly1d(value)
119
123
  return value
@@ -164,15 +168,12 @@ class EnergyCoverage(BaseModel):
164
168
  return self.energy_entries[-1].max_energy
165
169
 
166
170
  def get_poly(self, energy: float) -> np.poly1d:
167
- """
168
- Return the numpy.poly1d polynomial applicable for the given energy.
171
+ """Return the numpy.poly1d polynomial applicable for the given energy.
169
172
 
170
- Parameters:
171
- -----------
172
- energy:
173
- Energy value in the same units used to create the lookup table.
173
+ Args:
174
+ energy (float): Energy value in the same units used to create the lookup
175
+ table.
174
176
  """
175
-
176
177
  if not self.min_energy <= energy <= self.max_energy:
177
178
  raise ValueError(
178
179
  f"Demanding energy must lie between {self.min_energy} and {self.max_energy}!"
@@ -188,7 +189,8 @@ class EnergyCoverage(BaseModel):
188
189
 
189
190
  def get_energy_index(self, energy: float) -> int | None:
190
191
  """Binary search assumes self.energy_entries is sorted by min_energy.
191
- Return index or None if not found."""
192
+ Return index or None if not found.
193
+ """
192
194
  max_index = len(self.energy_entries) - 1
193
195
  min_index = 0
194
196
  while min_index <= max_index:
@@ -204,9 +206,8 @@ class EnergyCoverage(BaseModel):
204
206
 
205
207
 
206
208
  class LookupTable(RootModel[dict[Pol, EnergyCoverage]]):
207
- """
208
- Specialised lookup table for insertion devices to relate the energy and polarisation
209
- values to Apple2 motor positions.
209
+ """Specialised lookup table for insertion devices to relate the energy and
210
+ polarisation values to Apple2 motor positions.
210
211
  """
211
212
 
212
213
  model_config = ConfigDict(frozen=True)
@@ -222,7 +223,8 @@ class LookupTable(RootModel[dict[Pol, EnergyCoverage]]):
222
223
  energy_coverage: list[EnergyCoverage],
223
224
  ) -> Self:
224
225
  """Generate a LookupTable containing multiple EnergyCoverage
225
- for provided polarisations."""
226
+ for provided polarisations.
227
+ """
226
228
  root_data = dict(zip(pols, energy_coverage, strict=False))
227
229
  return cls(root=root_data)
228
230
 
@@ -231,15 +233,12 @@ class LookupTable(RootModel[dict[Pol, EnergyCoverage]]):
231
233
  energy: float,
232
234
  pol: Pol,
233
235
  ) -> np.poly1d:
234
- """
235
- Return the numpy.poly1d polynomial applicable for the given energy and polarisation.
236
-
237
- Parameters:
238
- -----------
239
- energy:
240
- Energy value in the same units used to create the lookup table.
241
- pol:
242
- Polarisation mode (Pol enum).
236
+ """Return the numpy.poly1d polynomial applicable for the given energy and
237
+ polarisation.
238
+
239
+ Args:
240
+ energy (float): Energy value in the same units used to create the lookup table.
241
+ pol (Pol): Polarisation mode enum.
243
242
  """
244
243
  return self.root[pol].get_poly(energy)
245
244
 
@@ -249,21 +248,17 @@ def convert_csv_to_lookup(
249
248
  lut_config: LookupTableColumnConfig,
250
249
  skip_line_start_with: str = "#",
251
250
  ) -> LookupTable:
252
- """
253
- Convert CSV content into the Apple2 lookup-table dictionary.
251
+ """Convert CSV content into the Apple2 lookup-table dictionary.
254
252
 
255
- Parameters:
256
- -----------
257
- file_contents:
258
- The CSV file contents as string.
259
- lut_config:
260
- The configuration that how to process the file_contents into a LookupTable.
261
- skip_line_start_with
262
- Lines beginning with this prefix are skipped (default "#").
253
+ Args:
254
+ file_contents (str): The CSV file contents as string.
255
+ lut_config (LookupTableColumnConfig): The configuration that how to process the
256
+ file_contents into a LookupTable.
257
+ skip_line_start_with (str, optional): Lines beginning with this prefix are
258
+ skipped (default "#").
263
259
 
264
260
  Returns:
265
- -----------
266
- LookupTable
261
+ LookupTable
267
262
  """
268
263
  temp_mode_entries: dict[Pol, list[EnergyCoverageEntry]] = {}
269
264
 
@@ -29,7 +29,7 @@ class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
29
29
  await self.polarisation().set(pol, timeout=MAXIMUM_MOVE_TIME)
30
30
 
31
31
  async def locate(self) -> Location[Pol]:
32
- """Return the current polarisation"""
32
+ """Return the current polarisation."""
33
33
  setpoint, readback = await asyncio.gather(
34
34
  self.polarisation_setpoint().get_value(), self.polarisation().get_value()
35
35
  )
dodal/devices/ipin.py CHANGED
@@ -20,7 +20,7 @@ class IPinGain(SubsetEnum):
20
20
 
21
21
 
22
22
  class IPin(StandardReadable):
23
- """Simple device to get the ipin reading"""
23
+ """Simple device to get the ipin reading."""
24
24
 
25
25
  def __init__(self, prefix: str, name: str = "") -> None:
26
26
  with self.add_children_as_readables(
dodal/devices/linkam3.py CHANGED
@@ -14,15 +14,17 @@ class PumpControl(StrictEnum):
14
14
  # TODO: Make use of Status PV:
15
15
  # https://github.com/DiamondLightSource/dodal/issues/338
16
16
  class Linkam3(StandardReadable):
17
- """Device to represent a Linkam3 temperature controller
17
+ """Device to represent a Linkam3 temperature controller.
18
18
 
19
19
  Attributes:
20
- tolerance (float): Deadband around the setpoint within which the position is assumed to have been reached
21
- settle_time (int): The delay between reaching the setpoint and the move being considered complete
20
+ tolerance (float): Deadband around the setpoint within which the position is
21
+ assumed to have been reached.
22
+ settle_time (int): The delay between reaching the setpoint and the move being
23
+ considered complete.
22
24
 
23
25
  Args:
24
- prefix (str): PV prefix for this device
25
- name (str): unique name for this device
26
+ prefix (str): PV prefix for this device.
27
+ name (str): Unique name for this device.
26
28
  """
27
29
 
28
30
  tolerance: float = 0.5