dls-dodal 1.68.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 (292) hide show
  1. {dls_dodal-1.68.0.dist-info → dls_dodal-2.0.0.dist-info}/METADATA +1 -31
  2. dls_dodal-2.0.0.dist-info/RECORD +354 -0
  3. {dls_dodal-1.68.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 +40 -33
  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 +17 -45
  14. dodal/beamlines/i02_2.py +6 -12
  15. dodal/beamlines/i03.py +8 -5
  16. dodal/beamlines/i03_supervisor.py +19 -0
  17. dodal/beamlines/i04.py +87 -184
  18. dodal/beamlines/i05.py +9 -39
  19. dodal/beamlines/i05_1.py +4 -13
  20. dodal/beamlines/i05_shared.py +51 -0
  21. dodal/beamlines/i06_1.py +26 -0
  22. dodal/beamlines/{i06.py → i06_shared.py} +25 -14
  23. dodal/beamlines/i07.py +14 -16
  24. dodal/beamlines/i09.py +76 -29
  25. dodal/beamlines/i09_1.py +25 -56
  26. dodal/beamlines/i09_1_shared.py +61 -0
  27. dodal/beamlines/i09_2.py +6 -100
  28. dodal/beamlines/i09_2_shared.py +110 -0
  29. dodal/beamlines/i10.py +60 -54
  30. dodal/beamlines/i10_1.py +99 -10
  31. dodal/beamlines/{i10_optics.py → i10_shared.py} +80 -66
  32. dodal/beamlines/i11.py +31 -18
  33. dodal/beamlines/i13_1.py +1 -1
  34. dodal/beamlines/i15.py +6 -6
  35. dodal/beamlines/i15_1.py +6 -6
  36. dodal/beamlines/i16.py +11 -0
  37. dodal/beamlines/i17.py +37 -28
  38. dodal/beamlines/i18.py +3 -4
  39. dodal/beamlines/i19_1.py +95 -34
  40. dodal/beamlines/i19_2.py +68 -52
  41. dodal/beamlines/i19_optics.py +26 -13
  42. dodal/beamlines/i20_1.py +17 -11
  43. dodal/beamlines/i21.py +44 -29
  44. dodal/beamlines/i22.py +19 -4
  45. dodal/beamlines/i23.py +20 -27
  46. dodal/beamlines/i24.py +64 -113
  47. dodal/beamlines/k07.py +99 -5
  48. dodal/beamlines/p38.py +3 -3
  49. dodal/beamlines/p60.py +35 -14
  50. dodal/beamlines/p99.py +16 -15
  51. dodal/beamlines/training_rig.py +20 -12
  52. dodal/cli.py +36 -2
  53. dodal/common/__init__.py +2 -1
  54. dodal/common/beamlines/beamline_parameters.py +2 -1
  55. dodal/common/beamlines/beamline_utils.py +11 -9
  56. dodal/common/beamlines/commissioning_mode.py +6 -3
  57. dodal/common/coordination.py +12 -14
  58. dodal/common/crystal_metadata.py +5 -8
  59. dodal/common/device_utils.py +4 -3
  60. dodal/common/maths.py +87 -19
  61. dodal/common/udc_directory_provider.py +13 -8
  62. dodal/common/visit.py +18 -21
  63. dodal/common/watcher_utils.py +13 -12
  64. dodal/device_manager.py +94 -54
  65. dodal/devices/aperturescatterguard.py +26 -27
  66. dodal/devices/areadetector/plugins/cam.py +1 -3
  67. dodal/devices/areadetector/plugins/mjpg.py +6 -5
  68. dodal/devices/attenuator/attenuator.py +12 -11
  69. dodal/devices/beamlines/b07/__init__.py +3 -0
  70. dodal/devices/{b07_1 → beamlines/b07_1}/__init__.py +2 -2
  71. dodal/devices/{b07_1 → beamlines/b07_1}/ccmc.py +5 -10
  72. dodal/devices/{b16 → beamlines/b16}/detector.py +2 -3
  73. dodal/devices/{i02_1 → beamlines/i02_1}/fast_grid_scan.py +2 -3
  74. dodal/devices/{i02_1 → beamlines/i02_1}/sample_motors.py +1 -1
  75. dodal/devices/{i03 → beamlines/i03}/beamsize.py +11 -7
  76. dodal/devices/{i03 → beamlines/i03}/dcm.py +1 -2
  77. dodal/devices/{i03 → beamlines/i03}/undulator_dcm.py +4 -5
  78. dodal/devices/beamlines/i04/beam_centre.py +151 -0
  79. dodal/devices/{i04 → beamlines/i04}/beamsize.py +11 -7
  80. dodal/devices/beamlines/i04/max_pixel.py +25 -0
  81. dodal/devices/{i04 → beamlines/i04}/murko_results.py +23 -8
  82. dodal/devices/{i04 → beamlines/i04}/transfocator.py +10 -15
  83. dodal/devices/beamlines/i05/__init__.py +3 -0
  84. dodal/devices/beamlines/i06_shared/__init__.py +3 -0
  85. dodal/devices/beamlines/i06_shared/i06_enum.py +7 -0
  86. dodal/devices/{i07 → beamlines/i07}/dcm.py +2 -3
  87. dodal/devices/{i07 → beamlines/i07}/id.py +8 -9
  88. dodal/devices/beamlines/i09/__init__.py +3 -0
  89. dodal/devices/{i09_1_shared → beamlines/i09_1_shared}/hard_energy.py +5 -6
  90. dodal/devices/{i09_1_shared → beamlines/i09_1_shared}/hard_undulator_functions.py +19 -16
  91. dodal/devices/{i10 → beamlines/i10}/diagnostics.py +4 -3
  92. dodal/devices/{i10 → beamlines/i10}/i10_apple2.py +37 -51
  93. dodal/devices/{i10 → beamlines/i10}/rasor/rasor_current_amp.py +1 -24
  94. dodal/devices/{i10 → beamlines/i10}/rasor/rasor_motors.py +2 -2
  95. dodal/devices/{i10 → beamlines/i10}/slits.py +5 -3
  96. dodal/devices/beamlines/i10_1/__init__.py +9 -0
  97. dodal/devices/beamlines/i10_1/electromagnet/magnet.py +16 -0
  98. dodal/devices/beamlines/i10_1/electromagnet/stages.py +14 -0
  99. dodal/devices/beamlines/i10_1/scaler_cards.py +13 -0
  100. dodal/devices/{i11 → beamlines/i11}/cyberstar_blower.py +1 -1
  101. dodal/devices/{i11 → beamlines/i11}/diff_stages.py +4 -6
  102. dodal/devices/{i11 → beamlines/i11}/mythen.py +3 -4
  103. dodal/devices/{i11 → beamlines/i11}/nx100robot.py +6 -6
  104. dodal/devices/{i11 → beamlines/i11}/spinner.py +1 -1
  105. dodal/devices/{i13_1 → beamlines/i13_1}/merlin.py +1 -1
  106. dodal/devices/{i15 → beamlines/i15}/dcm.py +1 -2
  107. dodal/devices/{i15 → beamlines/i15}/focussing_mirror.py +5 -5
  108. dodal/devices/{i15 → beamlines/i15}/jack.py +2 -2
  109. dodal/devices/{i15 → beamlines/i15}/multilayer_mirror.py +1 -1
  110. dodal/devices/{i17 → beamlines/i17}/i17_apple2.py +16 -22
  111. dodal/devices/{i18 → beamlines/i18}/diode.py +1 -1
  112. dodal/devices/{i19 → beamlines/i19}/access_controlled/attenuator_motor_squad.py +12 -8
  113. dodal/devices/{i19 → beamlines/i19}/access_controlled/blueapi_device.py +16 -15
  114. dodal/devices/beamlines/i19/access_controlled/piezo_control.py +72 -0
  115. dodal/devices/{i19 → beamlines/i19}/access_controlled/shutter.py +11 -9
  116. dodal/devices/{i19 → beamlines/i19}/backlight.py +3 -1
  117. dodal/devices/{i19 → beamlines/i19}/mapt_configuration.py +2 -1
  118. dodal/devices/{i19 → beamlines/i19}/pin_col_stages.py +11 -8
  119. dodal/devices/beamlines/i19/pin_tip.py +32 -0
  120. dodal/devices/beamlines/i21/__init__.py +3 -0
  121. dodal/devices/{i22 → beamlines/i22}/dcm.py +1 -2
  122. dodal/devices/{i22 → beamlines/i22}/fswitch.py +1 -3
  123. dodal/devices/{i22 → beamlines/i22}/nxsas.py +5 -4
  124. dodal/devices/{i24 → beamlines/i24}/beam_center.py +1 -1
  125. dodal/devices/{i24 → beamlines/i24}/beamstop.py +2 -2
  126. dodal/devices/{i24 → beamlines/i24}/commissioning_jungfrau.py +12 -12
  127. dodal/devices/{i24 → beamlines/i24}/dcm.py +1 -3
  128. dodal/devices/{i24 → beamlines/i24}/dual_backlight.py +3 -3
  129. dodal/devices/{i24 → beamlines/i24}/pmac.py +9 -7
  130. dodal/devices/{p60 → beamlines/p60}/lab_xray_source.py +1 -1
  131. dodal/devices/beamlines/p99/__init__.py +0 -0
  132. dodal/devices/{p99 → beamlines/p99}/andor2_point.py +11 -15
  133. dodal/devices/bimorph_mirror.py +22 -20
  134. dodal/devices/collimation_table.py +3 -2
  135. dodal/devices/common_dcm.py +30 -20
  136. dodal/devices/controllers.py +2 -2
  137. dodal/devices/cryostream.py +8 -0
  138. dodal/devices/current_amplifiers/current_amplifier.py +16 -18
  139. dodal/devices/current_amplifiers/current_amplifier_detector.py +9 -10
  140. dodal/devices/current_amplifiers/femto.py +8 -9
  141. dodal/devices/current_amplifiers/sr570.py +16 -16
  142. dodal/devices/current_amplifiers/struck_scaler_counter.py +5 -5
  143. dodal/devices/detector/det_resolution.py +9 -8
  144. dodal/devices/detector/detector.py +4 -2
  145. dodal/devices/diamond_filter.py +3 -4
  146. dodal/devices/eiger.py +32 -17
  147. dodal/devices/eiger_odin.py +1 -1
  148. dodal/devices/electron_analyser/base/__init__.py +3 -3
  149. dodal/devices/electron_analyser/base/base_controller.py +32 -21
  150. dodal/devices/electron_analyser/base/base_detector.py +15 -20
  151. dodal/devices/electron_analyser/base/base_driver_io.py +39 -46
  152. dodal/devices/electron_analyser/base/base_enums.py +0 -5
  153. dodal/devices/electron_analyser/base/base_region.py +29 -31
  154. dodal/devices/electron_analyser/base/base_util.py +18 -16
  155. dodal/devices/electron_analyser/base/energy_sources.py +35 -40
  156. dodal/devices/electron_analyser/specs/specs_detector.py +7 -6
  157. dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +7 -6
  158. dodal/devices/eurotherm.py +3 -2
  159. dodal/devices/fast_grid_scan.py +31 -34
  160. dodal/devices/fast_shutter.py +125 -39
  161. dodal/devices/flux.py +1 -1
  162. dodal/devices/focusing_mirror.py +29 -11
  163. dodal/devices/hutch_shutter.py +6 -6
  164. dodal/devices/insertion_device/__init__.py +20 -8
  165. dodal/devices/insertion_device/apple2_controller.py +371 -0
  166. dodal/devices/insertion_device/apple2_undulator.py +184 -587
  167. dodal/devices/insertion_device/apple_knot_controller.py +222 -0
  168. dodal/devices/insertion_device/energy.py +161 -0
  169. dodal/devices/insertion_device/energy_motor_lookup.py +21 -28
  170. dodal/devices/insertion_device/lookup_table_models.py +47 -52
  171. dodal/devices/insertion_device/polarisation.py +36 -0
  172. dodal/devices/ipin.py +1 -1
  173. dodal/devices/linkam3.py +7 -5
  174. dodal/devices/motors.py +107 -19
  175. dodal/devices/mx_phase1/beamstop.py +2 -4
  176. dodal/devices/oav/oav_calculations.py +20 -13
  177. dodal/devices/oav/oav_detector.py +92 -22
  178. dodal/devices/oav/oav_parameters.py +4 -9
  179. dodal/devices/oav/oav_to_redis_forwarder.py +22 -18
  180. dodal/devices/oav/pin_image_recognition/__init__.py +4 -6
  181. dodal/devices/oav/pin_image_recognition/manual_test.py +1 -2
  182. dodal/devices/oav/pin_image_recognition/utils.py +30 -32
  183. dodal/devices/oav/snapshots/grid_overlay.py +10 -9
  184. dodal/devices/oav/snapshots/snapshot_image_processing.py +15 -13
  185. dodal/devices/oav/utils.py +20 -6
  186. dodal/devices/p45.py +3 -9
  187. dodal/devices/pgm.py +8 -14
  188. dodal/devices/pressure_jump_cell.py +93 -32
  189. dodal/devices/qbpm.py +1 -3
  190. dodal/devices/robot.py +45 -20
  191. dodal/devices/s4_slit_gaps.py +1 -1
  192. dodal/devices/selectable_source.py +41 -0
  193. dodal/devices/slits.py +2 -5
  194. dodal/devices/smargon.py +2 -3
  195. dodal/devices/temperture_controller/lakeshore/lakeshore.py +38 -64
  196. dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +21 -35
  197. dodal/devices/tetramm.py +7 -7
  198. dodal/devices/turbo_slit.py +8 -7
  199. dodal/devices/undulator.py +42 -56
  200. dodal/devices/util/adjuster_plans.py +2 -3
  201. dodal/devices/util/epics_util.py +10 -10
  202. dodal/devices/util/lookup_tables.py +17 -18
  203. dodal/devices/v2f.py +2 -3
  204. dodal/devices/watsonmarlow323_pump.py +1 -1
  205. dodal/devices/xbpm_feedback.py +3 -2
  206. dodal/devices/xspress3/xspress3.py +8 -11
  207. dodal/devices/xspress3/xspress3_channel.py +3 -6
  208. dodal/devices/zebra/zebra.py +21 -7
  209. dodal/devices/zebra/zebra_constants_mapping.py +12 -7
  210. dodal/devices/zebra/zebra_controlled_shutter.py +2 -1
  211. dodal/devices/zocalo/zocalo_interaction.py +14 -14
  212. dodal/devices/zocalo/zocalo_results.py +33 -33
  213. dodal/log.py +23 -20
  214. dodal/plan_stubs/check_topup.py +15 -15
  215. dodal/plan_stubs/data_session.py +6 -6
  216. dodal/plan_stubs/motor_utils.py +22 -18
  217. dodal/plan_stubs/pressure_jump_cell.py +18 -0
  218. dodal/plan_stubs/wrapped.py +40 -55
  219. dodal/plans/bimorph.py +63 -52
  220. dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
  221. dodal/plans/device_setup_plans/__init__.py +5 -0
  222. dodal/plans/device_setup_plans/setup_pin_tip_params.py +63 -0
  223. dodal/plans/preprocessors/verify_undulator_gap.py +10 -8
  224. dodal/plans/spec_path.py +3 -5
  225. dodal/plans/verify_undulator_gap.py +1 -2
  226. dodal/plans/wrapped.py +4 -3
  227. dodal/testing/__init__.py +0 -0
  228. dodal/testing/electron_analyser/device_factory.py +5 -7
  229. dodal/testing/fixtures/devices/apple2.py +38 -0
  230. dodal/testing/fixtures/run_engine.py +3 -7
  231. dodal/testing/fixtures/utils.py +1 -2
  232. dodal/utils.py +60 -58
  233. dls_dodal-1.68.0.dist-info/RECORD +0 -330
  234. dodal/beamline_specific_utils/i05_shared.py +0 -14
  235. dodal/devices/b07/__init__.py +0 -3
  236. dodal/devices/i04/max_pixel.py +0 -38
  237. dodal/devices/i05/__init__.py +0 -3
  238. dodal/devices/i09/__init__.py +0 -3
  239. dodal/devices/i21/__init__.py +0 -5
  240. {dls_dodal-1.68.0.dist-info → dls_dodal-2.0.0.dist-info}/entry_points.txt +0 -0
  241. {dls_dodal-1.68.0.dist-info → dls_dodal-2.0.0.dist-info}/licenses/LICENSE +0 -0
  242. {dls_dodal-1.68.0.dist-info → dls_dodal-2.0.0.dist-info}/top_level.txt +0 -0
  243. /dodal/{beamline_specific_utils → devices/beamlines}/__init__.py +0 -0
  244. /dodal/devices/{b07 → beamlines/b07}/enums.py +0 -0
  245. /dodal/devices/{b07_1 → beamlines/b07_1}/enums.py +0 -0
  246. /dodal/devices/{b16 → beamlines/b16}/__init__.py +0 -0
  247. /dodal/devices/{i02_1 → beamlines/i02_1}/__init__.py +0 -0
  248. /dodal/devices/{i02_2 → beamlines/i02_2}/__init__.py +0 -0
  249. /dodal/devices/{i03 → beamlines/i03}/__init__.py +0 -0
  250. /dodal/devices/{i03 → beamlines/i03}/constants.py +0 -0
  251. /dodal/devices/{i04 → beamlines/i04}/__init__.py +0 -0
  252. /dodal/devices/{i04 → beamlines/i04}/constants.py +0 -0
  253. /dodal/devices/{i05 → beamlines/i05}/enums.py +0 -0
  254. /dodal/devices/{i07 → beamlines/i07}/__init__.py +0 -0
  255. /dodal/devices/{i09 → beamlines/i09}/enums.py +0 -0
  256. /dodal/devices/{i09_1 → beamlines/i09_1}/__init__.py +0 -0
  257. /dodal/devices/{i09_1 → beamlines/i09_1}/enums.py +0 -0
  258. /dodal/devices/{i09_1_shared → beamlines/i09_1_shared}/__init__.py +0 -0
  259. /dodal/devices/{i09_2_shared → beamlines/i09_2_shared}/__init__.py +0 -0
  260. /dodal/devices/{i09_2_shared → beamlines/i09_2_shared}/i09_apple2.py +0 -0
  261. /dodal/devices/{i10 → beamlines/i10}/__init__.py +0 -0
  262. /dodal/devices/{i10 → beamlines/i10}/i10_setting_data.py +0 -0
  263. /dodal/devices/{i10 → beamlines/i10}/mirrors.py +0 -0
  264. /dodal/devices/{i10 → beamlines/i10}/rasor/__init__.py +0 -0
  265. /dodal/devices/{i10 → beamlines/i10}/rasor/rasor_scaler_cards.py +0 -0
  266. /dodal/devices/{i11 → beamlines/i10_1/electromagnet}/__init__.py +0 -0
  267. /dodal/devices/{i13_1 → beamlines/i11}/__init__.py +0 -0
  268. /dodal/devices/{i15 → beamlines/i13_1}/__init__.py +0 -0
  269. /dodal/devices/{i13_1 → beamlines/i13_1}/merlin_controller.py +0 -0
  270. /dodal/devices/{i17 → beamlines/i15}/__init__.py +0 -0
  271. /dodal/devices/{i15 → beamlines/i15}/laue.py +0 -0
  272. /dodal/devices/{i15 → beamlines/i15}/motors.py +0 -0
  273. /dodal/devices/{i15 → beamlines/i15}/rail.py +0 -0
  274. /dodal/devices/{i18 → beamlines/i17}/__init__.py +0 -0
  275. /dodal/devices/{i19 → beamlines/i18}/__init__.py +0 -0
  276. /dodal/devices/{i18 → beamlines/i18}/kb_mirror.py +0 -0
  277. /dodal/devices/{i19/access_controlled → beamlines/i19}/__init__.py +0 -0
  278. /dodal/devices/{i20_1 → beamlines/i19/access_controlled}/__init__.py +0 -0
  279. /dodal/devices/{i19 → beamlines/i19}/access_controlled/hutch_access.py +0 -0
  280. /dodal/devices/{i19 → beamlines/i19}/beamstop.py +0 -0
  281. /dodal/devices/{i19 → beamlines/i19}/diffractometer.py +0 -0
  282. /dodal/devices/{i22 → beamlines/i20_1}/__init__.py +0 -0
  283. /dodal/devices/{i21 → beamlines/i21}/enums.py +0 -0
  284. /dodal/devices/{i24 → beamlines/i22}/__init__.py +0 -0
  285. /dodal/devices/{p99 → beamlines/i24}/__init__.py +0 -0
  286. /dodal/devices/{i24 → beamlines/i24}/aperture.py +0 -0
  287. /dodal/devices/{i24 → beamlines/i24}/focus_mirrors.py +0 -0
  288. /dodal/devices/{i24 → beamlines/i24}/vgonio.py +0 -0
  289. /dodal/devices/{p60 → beamlines/p60}/__init__.py +0 -0
  290. /dodal/devices/{p60 → beamlines/p60}/enums.py +0 -0
  291. /dodal/devices/{p99 → beamlines/p99}/sample_stage.py +0 -0
  292. /dodal/devices/insertion_device/{id_enum.py → enum.py} +0 -0
@@ -1,64 +1,58 @@
1
1
  import abc
2
2
  import asyncio
3
3
  from dataclasses import dataclass
4
- from math import isclose
5
- from typing import Generic, Protocol, TypeVar
4
+ from typing import Generic, TypeVar
6
5
 
7
6
  import numpy as np
8
- from bluesky.protocols import Locatable, Location, Movable
7
+ from bluesky.protocols import Movable
9
8
  from ophyd_async.core import (
9
+ DEFAULT_TIMEOUT,
10
10
  AsyncStatus,
11
+ Device,
12
+ FlyMotorInfo,
11
13
  Reference,
12
14
  SignalR,
13
- SignalRW,
14
15
  SignalW,
15
16
  StandardReadable,
16
17
  StandardReadableFormat,
17
- derived_signal_rw,
18
- soft_signal_r_and_setter,
19
- soft_signal_rw,
18
+ WatchableAsyncStatus,
19
+ WatcherUpdate,
20
+ observe_value,
20
21
  wait_for_value,
21
22
  )
22
- from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
23
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
23
24
  from ophyd_async.epics.motor import Motor
24
25
 
25
26
  from dodal.common.enums import EnabledDisabledUpper
26
- from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
27
- from dodal.devices.insertion_device.id_enum import Pol, UndulatorGateStatus
27
+ from dodal.devices.insertion_device.enum import UndulatorGateStatus
28
28
  from dodal.log import LOGGER
29
29
 
30
30
  T = TypeVar("T")
31
31
 
32
32
  DEFAULT_MOTOR_MIN_TIMEOUT = 10
33
- MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
34
33
 
35
34
 
36
35
  @dataclass
37
36
  class Apple2LockedPhasesVal:
38
- top_outer: str
39
- btm_inner: str
37
+ top_outer: float
38
+ btm_inner: float
40
39
 
41
40
 
42
41
  @dataclass
43
42
  class Apple2PhasesVal(Apple2LockedPhasesVal):
44
- top_inner: str
45
- btm_outer: str
43
+ top_inner: float
44
+ btm_outer: float
46
45
 
47
46
 
48
47
  @dataclass
49
48
  class Apple2Val:
50
- gap: str
49
+ gap: float
51
50
  phase: Apple2LockedPhasesVal | Apple2PhasesVal
52
51
 
53
52
  def extract_phase_val(self):
54
53
  return self.phase
55
54
 
56
55
 
57
- ROW_PHASE_MOTOR_TOLERANCE = 0.004
58
- MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
59
- MAXIMUM_GAP_MOTOR_POSITION = 100
60
-
61
-
62
56
  async def estimate_motor_timeout(
63
57
  setpoint: SignalR, curr_pos: SignalR, velocity: SignalR
64
58
  ):
@@ -68,7 +62,35 @@ async def estimate_motor_timeout(
68
62
  return abs((target_pos - cur_pos) * 2.0 / vel) + DEFAULT_MOTOR_MIN_TIMEOUT
69
63
 
70
64
 
71
- class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
65
+ class UndulatorBase(abc.ABC, Device, Generic[T]):
66
+ """Abstract base class for Apple2 undulator devices.
67
+ This class provides common functionality for undulator devices, including
68
+ gate and status signal management, safety checks before motion, and abstract
69
+ methods for setting demand positions and estimating move timeouts.
70
+ """
71
+
72
+ def __init__(self, name: str = ""):
73
+ # Gate keeper open when move is requested, closed when move is completed
74
+ self.gate: SignalR[UndulatorGateStatus]
75
+ self.status: SignalR[EnabledDisabledUpper]
76
+ super().__init__(name=name)
77
+
78
+ @abc.abstractmethod
79
+ async def set_demand_positions(self, value: T) -> None:
80
+ """Set the demand positions on the device without actually hitting move."""
81
+
82
+ @abc.abstractmethod
83
+ async def get_timeout(self) -> float:
84
+ """Get the timeout for the move based on an estimate of how long it will take."""
85
+
86
+ async def raise_if_cannot_move(self) -> None:
87
+ if await self.status.get_value() is EnabledDisabledUpper.DISABLED:
88
+ raise RuntimeError(f"{self.name} is DISABLED and cannot move.")
89
+ if await self.gate.get_value() is UndulatorGateStatus.OPEN:
90
+ raise RuntimeError(f"{self.name} is already in motion.")
91
+
92
+
93
+ class SafeUndulatorMover(StandardReadable, UndulatorBase, Generic[T]):
72
94
  """A device that will check it's safe to move the undulator before moving it and
73
95
  wait for the undulator to be safe again before calling the move complete.
74
96
  """
@@ -90,113 +112,128 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
90
112
  await self.set_move.set(value=1, timeout=timeout)
91
113
  await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
92
114
 
93
- @abc.abstractmethod
94
- async def set_demand_positions(self, value: T) -> None:
95
- """Set the demand positions on the device without actually hitting move."""
96
115
 
97
- @abc.abstractmethod
98
- async def get_timeout(self) -> float:
99
- """Get the timeout for the move based on an estimate of how long it will take."""
116
+ class UnstoppableMotor(Motor):
117
+ """A motor that does not support stop."""
100
118
 
101
- async def raise_if_cannot_move(self) -> None:
102
- if await self.status.get_value() is not EnabledDisabledUpper.ENABLED:
103
- raise RuntimeError(f"{self.name} is DISABLED and cannot move.")
104
- if await self.gate.get_value() == UndulatorGateStatus.OPEN:
105
- raise RuntimeError(f"{self.name} is already in motion.")
119
+ def __init__(self, prefix: str, name: str = ""):
120
+ super().__init__(prefix=prefix, name=name)
121
+ del self.motor_stop # Remove motor_stop from the public interface
106
122
 
123
+ async def stop(self, success=False):
124
+ LOGGER.warning(f"Stopping {self.name} is not supported.")
107
125
 
108
- class UndulatorGap(SafeUndulatorMover[float]):
109
- """A device with a collection of epics signals to set Apple 2 undulator gap motion.
110
- Only PV used by beamline are added the full list is here:
111
- /dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
112
- /dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDPhaseSoftMotor.template
113
- """
114
126
 
115
- def __init__(self, prefix: str, name: str = ""):
116
- """
127
+ class GapSafeMotorNoStop(UnstoppableMotor, UndulatorBase[float]):
128
+ """Update gap safe motor that checks it's safe to move before moving."""
117
129
 
118
- Parameters
119
- ----------
120
- prefix : str
121
- Beamline specific part of the PV
122
- name : str
123
- Name of the Id device
124
-
125
- """
130
+ def __init__(self, set_move: SignalW[int], prefix: str, name: str = ""):
131
+ # Gate keeper open when move is requested, closed when move is completed
132
+ self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
133
+ self.status = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
134
+ self.set_move = set_move
135
+ super().__init__(prefix=prefix + "BLGAPMTR", name=name)
136
+
137
+ @WatchableAsyncStatus.wrap
138
+ async def set(self, new_position: float, timeout=DEFAULT_TIMEOUT):
139
+ (
140
+ old_position,
141
+ units,
142
+ precision,
143
+ ) = await asyncio.gather(
144
+ self.user_setpoint.get_value(),
145
+ self.motor_egu.get_value(),
146
+ self.precision.get_value(),
147
+ )
148
+ LOGGER.info(f"Setting {self.name} to {new_position}")
149
+ await self.raise_if_cannot_move()
150
+ await self.set_demand_positions(new_position)
151
+ timeout = await self.get_timeout()
152
+ LOGGER.info(f"Moving {self.name} to {new_position} with timeout = {timeout}")
126
153
 
127
- # Gap demand set point and readback
128
- self.user_setpoint = epics_signal_rw(
129
- str, prefix + "GAPSET.B", prefix + "BLGSET"
154
+ await self.set_move.set(value=1, wait=True, timeout=timeout)
155
+ move_status = AsyncStatus(
156
+ wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
130
157
  )
131
- # Nothing move until this is set to 1 and it will return to 0 when done
158
+
159
+ async for current_position in observe_value(
160
+ self.user_readback, done_status=move_status
161
+ ):
162
+ yield WatcherUpdate(
163
+ current=current_position,
164
+ initial=old_position,
165
+ target=new_position,
166
+ name=self.name,
167
+ unit=units,
168
+ precision=precision,
169
+ )
170
+
171
+
172
+ class UndulatorGap(GapSafeMotorNoStop):
173
+ """Apple 2 undulator gap motor device. With PV corrections.
174
+
175
+ Args:
176
+ prefix (str): Beamline specific part of the PV
177
+ name (str): Name of the Id device
178
+ """
179
+
180
+ def __init__(self, prefix: str, name: str = ""):
132
181
  self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
182
+ # Nothing move until this is set to 1 and it will return to 0 when done.
183
+ super().__init__(self.set_move, prefix, name)
133
184
 
134
- # These are gap velocity limit.
135
185
  self.max_velocity = epics_signal_r(float, prefix + "BLGSETVEL.HOPR")
136
186
  self.min_velocity = epics_signal_r(float, prefix + "BLGSETVEL.LOPR")
137
- # These are gap limit.
138
- self.high_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.HLM")
139
- self.low_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.LLM")
140
-
141
- # This is calculated acceleration from speed
142
- self.acceleration_time = epics_signal_r(float, prefix + "IDGSETACC")
143
-
144
- with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
145
- # Unit
146
- self.motor_egu = epics_signal_r(str, prefix + "BLGAPMTR.EGU")
147
- # Gap velocity
148
- self.velocity = epics_signal_rw(float, prefix + "BLGSETVEL")
149
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
150
- # Gap readback value
151
- self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
152
- super().__init__(self.set_move, prefix, name)
187
+ self.user_setpoint = epics_signal_rw(str, prefix + "BLGSET")
188
+ """ Clear the motor config_signal as we need new PV for velocity."""
189
+ self._describe_config_funcs = ()
190
+ self._read_config_funcs = ()
191
+ self.velocity = epics_signal_rw(float, prefix + "BLGSETVEL")
192
+ self.add_readables(
193
+ [self.velocity, self.motor_egu, self.offset],
194
+ format=StandardReadableFormat.CONFIG_SIGNAL,
195
+ )
153
196
 
154
- async def set_demand_positions(self, value: float) -> None:
155
- await self.user_setpoint.set(str(value))
197
+ @AsyncStatus.wrap
198
+ async def prepare(self, value: FlyMotorInfo) -> None:
199
+ """Prepare for a fly scan by moving to the run-up position at max velocity.
200
+ Stores fly info for later use in kickoff.
201
+ """
202
+ max_velocity, min_velocity, egu = await asyncio.gather(
203
+ self.max_velocity.get_value(),
204
+ self.min_velocity.get_value(),
205
+ self.motor_egu.get_value(),
206
+ )
207
+ velocity = abs(value.velocity)
208
+ if not (min_velocity <= velocity <= max_velocity):
209
+ raise ValueError(
210
+ f"Requested velocity {velocity} {egu}/s is out of bounds: "
211
+ f"must be between {min_velocity} and {max_velocity} {egu}/s."
212
+ )
213
+ await super().prepare(value)
156
214
 
157
215
  async def get_timeout(self) -> float:
158
216
  return await estimate_motor_timeout(
159
217
  self.user_setpoint, self.user_readback, self.velocity
160
218
  )
161
219
 
220
+ async def set_demand_positions(self, value: float) -> None:
221
+ await self.user_setpoint.set(str(value))
222
+
162
223
 
163
- class UndulatorPhaseMotor(StandardReadable):
164
- """A collection of epics signals for ID phase motion.
165
- Only PV used by beamline are added the full list is here:
166
- /dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDPhaseSoftMotor.template
224
+ class UndulatorPhaseMotor(UnstoppableMotor):
225
+ """Phase motor that will not stop.
226
+
227
+ Args:
228
+ prefix (str): The setting prefix PV.
229
+ name (str, optional): Name of the Id phase device.
167
230
  """
168
231
 
169
- def __init__(self, prefix: str, infix: str, name: str = ""):
170
- """
171
- Parameters
172
- ----------
173
-
174
- prefix : str
175
- The setting prefix PV.
176
- infix: str
177
- Collection of pv that are different between beamlines
178
- name : str
179
- Name of the Id phase device
180
- """
181
- full_pv = f"{prefix}BL{infix}"
182
- self.user_setpoint = epics_signal_w(str, full_pv + "SET")
183
- self.user_setpoint_readback = epics_signal_r(float, full_pv + "DMD")
184
- full_pv = full_pv + "MTR"
185
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
186
- self.user_readback = epics_signal_r(float, full_pv + ".RBV")
187
-
188
- with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
189
- self.motor_egu = epics_signal_r(str, full_pv + ".EGU")
190
- self.velocity = epics_signal_rw(float, full_pv + ".VELO")
191
-
192
- self.max_velocity = epics_signal_r(float, full_pv + ".VMAX")
193
- self.acceleration_time = epics_signal_rw(float, full_pv + ".ACCL")
194
- self.precision = epics_signal_r(int, full_pv + ".PREC")
195
- self.deadband = epics_signal_r(float, full_pv + ".RDBD")
196
- self.motor_done_move = epics_signal_r(int, full_pv + ".DMOV")
197
- self.low_limit_travel = epics_signal_rw(float, full_pv + ".LLM")
198
- self.high_limit_travel = epics_signal_rw(float, full_pv + ".HLM")
199
- super().__init__(name=name)
232
+ def __init__(self, prefix: str, name: str = ""):
233
+ motor_pv = f"{prefix}MTR"
234
+ super().__init__(prefix=motor_pv, name=name)
235
+ self.user_setpoint = epics_signal_rw(str, prefix + "SET")
236
+ self.user_setpoint_readback = epics_signal_r(float, prefix + "DMD")
200
237
 
201
238
 
202
239
  Apple2PhaseValType = TypeVar("Apple2PhaseValType", bound=Apple2LockedPhasesVal)
@@ -214,8 +251,8 @@ class UndulatorLockedPhaseAxes(SafeUndulatorMover[Apple2PhaseValType]):
214
251
  ):
215
252
  # Gap demand set point and readback
216
253
  with self.add_children_as_readables():
217
- self.top_outer = UndulatorPhaseMotor(prefix=prefix, infix=top_outer)
218
- self.btm_inner = UndulatorPhaseMotor(prefix=prefix, infix=btm_inner)
254
+ self.top_outer = UndulatorPhaseMotor(prefix=f"{prefix}BL{top_outer}")
255
+ self.btm_inner = UndulatorPhaseMotor(prefix=f"{prefix}BL{btm_inner}")
219
256
  # Nothing move until this is set to 1 and it will return to 0 when done.
220
257
  self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
221
258
  self.axes = [self.top_outer, self.btm_inner]
@@ -223,15 +260,14 @@ class UndulatorLockedPhaseAxes(SafeUndulatorMover[Apple2PhaseValType]):
223
260
 
224
261
  async def set_demand_positions(self, value: Apple2PhaseValType) -> None:
225
262
  await asyncio.gather(
226
- self.top_outer.user_setpoint.set(value=value.top_outer),
227
- self.btm_inner.user_setpoint.set(value=value.btm_inner),
263
+ self.top_outer.user_setpoint.set(value=str(value.top_outer)),
264
+ self.btm_inner.user_setpoint.set(value=str(value.btm_inner)),
228
265
  )
229
266
 
230
267
  async def get_timeout(self) -> float:
268
+ """Get all motor speed, current positions and target positions to calculate
269
+ required timeout.
231
270
  """
232
- Get all four motor speed, current positions and target positions to calculate required timeout.
233
- """
234
-
235
271
  timeouts = await asyncio.gather(
236
272
  *[
237
273
  estimate_motor_timeout(
@@ -244,20 +280,19 @@ class UndulatorLockedPhaseAxes(SafeUndulatorMover[Apple2PhaseValType]):
244
280
  )
245
281
  """A 2.0 multiplier is required to prevent premature motor timeouts in phase
246
282
  axes as it is a master-slave system, where the slave's movement,
247
- being dependent on the master, can take up to twice as long to complete.
248
- """
283
+ being dependent on the master, can take up to twice as long to complete."""
249
284
  return np.max(timeouts) * 2.0
250
285
 
251
286
 
252
287
  class UndulatorPhaseAxes(UndulatorLockedPhaseAxes[Apple2PhasesVal]):
253
- """
254
- A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
255
- e.g. top_outer == Q1
256
- top_inner == Q2
257
- btm_inner == q3
258
- btm_outer == q4
288
+ """A collection of 4 phase Motor to make up the full id phase motion. We are using
289
+ the diamond PV convention. e.g.::
259
290
 
260
- """
291
+ top_outer == Q1
292
+ top_inner == Q2
293
+ btm_inner == q3
294
+ btm_outer == q4
295
+ """ # noqa D415
261
296
 
262
297
  def __init__(
263
298
  self,
@@ -270,25 +305,24 @@ class UndulatorPhaseAxes(UndulatorLockedPhaseAxes[Apple2PhasesVal]):
270
305
  ):
271
306
  # Gap demand set point and readback
272
307
  with self.add_children_as_readables():
273
- self.top_inner = UndulatorPhaseMotor(prefix=prefix, infix=top_inner)
274
- self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
308
+ self.top_inner = UndulatorPhaseMotor(prefix=f"{prefix}BL{top_inner}")
309
+ self.btm_outer = UndulatorPhaseMotor(prefix=f"{prefix}BL{btm_outer}")
275
310
 
276
311
  super().__init__(prefix, top_outer=top_outer, btm_inner=btm_inner, name=name)
277
312
  self.axes.extend([self.top_inner, self.btm_outer])
278
313
 
279
314
  async def set_demand_positions(self, value: Apple2PhasesVal) -> None:
280
315
  await asyncio.gather(
281
- self.top_outer.user_setpoint.set(value=value.top_outer),
282
- self.top_inner.user_setpoint.set(value=value.top_inner),
283
- self.btm_inner.user_setpoint.set(value=value.btm_inner),
284
- self.btm_outer.user_setpoint.set(value=value.btm_outer),
316
+ self.top_outer.user_setpoint.set(value=str(value.top_outer)),
317
+ self.top_inner.user_setpoint.set(value=str(value.top_inner)),
318
+ self.btm_inner.user_setpoint.set(value=str(value.btm_inner)),
319
+ self.btm_outer.user_setpoint.set(value=str(value.btm_outer)),
285
320
  )
286
321
 
287
322
 
288
323
  class UndulatorJawPhase(SafeUndulatorMover[float]):
289
- """
290
- A JawPhase movable, this is use for moving the jaw phase which is use to control the
291
- linear arbitrary polarisation but only on some of the beamline.
324
+ """A JawPhase movable, this is use for moving the jaw phase which is use to control
325
+ the linear arbitrary polarisation but only on some of the beamline.
292
326
  """
293
327
 
294
328
  def __init__(
@@ -300,7 +334,7 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
300
334
  ):
301
335
  # Gap demand set point and readback
302
336
  with self.add_children_as_readables():
303
- self.jaw_phase = UndulatorPhaseMotor(prefix=prefix, infix=jaw_phase)
337
+ self.jaw_phase = UndulatorPhaseMotor(prefix=f"{prefix}BL{jaw_phase}")
304
338
  # Nothing move until this is set to 1 and it will return to 0 when done
305
339
  self.set_move = epics_signal_rw(int, f"{prefix}BL{move_pv}" + "MOVE")
306
340
 
@@ -310,8 +344,8 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
310
344
  await self.jaw_phase.user_setpoint.set(value=str(value))
311
345
 
312
346
  async def get_timeout(self) -> float:
313
- """
314
- Get motor speed, current position and target position to calculate required timeout.
347
+ """Get motor speed, current position and target position to calculate required
348
+ timeout.
315
349
  """
316
350
  return await estimate_motor_timeout(
317
351
  self.jaw_phase.user_setpoint_readback,
@@ -324,29 +358,20 @@ PhaseAxesType = TypeVar("PhaseAxesType", bound=UndulatorLockedPhaseAxes)
324
358
 
325
359
 
326
360
  class Apple2(StandardReadable, Movable[Apple2Val], Generic[PhaseAxesType]):
327
- """
328
- Device representing the combined motor controls for an Apple2 undulator.
329
-
330
- Attributes
331
- ----------
332
- gap : UndulatorGap
333
- The undulator gap motor device.
334
- phase : UndulatorPhaseAxes
335
- The undulator phase axes device, consisting of four phase motors.
361
+ """Device representing the combined motor controls for an Apple2 undulator.
362
+
363
+ Attributes:
364
+ gap (UndulatorGap): The undulator gap motor device.
365
+ phase (UndulatorPhaseAxes): The undulator phase axes device, consisting of four
366
+ phase motors.
367
+
368
+ Args:
369
+ id_gap (UndulatorGap): An UndulatorGap device.
370
+ id_phase (UndulatorPhaseAxes): An UndulatorPhaseAxes device.
371
+ name (str, optional): Name of the device.
336
372
  """
337
373
 
338
374
  def __init__(self, id_gap: UndulatorGap, id_phase: PhaseAxesType, name=""):
339
- """
340
- Parameters
341
- ----------
342
-
343
- id_gap: UndulatorGap
344
- An UndulatorGap device.
345
- id_phase: UndulatorPhaseAxes
346
- An UndulatorPhaseAxes device.
347
- name: str
348
- Name of the device.
349
- """
350
375
  with self.add_children_as_readables():
351
376
  self.gap = Reference(id_gap)
352
377
  self.phase = Reference(id_phase)
@@ -354,12 +379,10 @@ class Apple2(StandardReadable, Movable[Apple2Val], Generic[PhaseAxesType]):
354
379
 
355
380
  @AsyncStatus.wrap
356
381
  async def set(self, id_motor_values: Apple2Val) -> None:
382
+ """Check ID is in a movable state and set all the demand value before moving
383
+ them all at the same time.
357
384
  """
358
- Check ID is in a movable state and set all the demand value before moving them
359
- all at the same time.
360
- """
361
-
362
- # Only need to check gap as the phase motors share both fault and gate with gap.
385
+ # Only need to check gap as the phase motors share both status and gate with gap.
363
386
  await self.gap().raise_if_cannot_move()
364
387
 
365
388
  await asyncio.gather(
@@ -372,7 +395,7 @@ class Apple2(StandardReadable, Movable[Apple2Val], Generic[PhaseAxesType]):
372
395
  await asyncio.gather(self.gap().get_timeout(), self.phase().get_timeout())
373
396
  )
374
397
  LOGGER.info(
375
- f"Moving f{self.name} apple2 motors to {id_motor_values}, timeout = {timeout}"
398
+ f"Moving {self.name} apple2 motors to {id_motor_values}, timeout = {timeout}"
376
399
  )
377
400
  await asyncio.gather(
378
401
  self.gap().set_move.set(value=1, wait=False, timeout=timeout),
@@ -381,429 +404,3 @@ class Apple2(StandardReadable, Movable[Apple2Val], Generic[PhaseAxesType]):
381
404
  await wait_for_value(
382
405
  self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
383
406
  )
384
-
385
-
386
- class EnergyMotorConvertor(Protocol):
387
- def __call__(self, energy: float, pol: Pol) -> float:
388
- """Protocol to provide energy to motor position conversion"""
389
- ...
390
-
391
-
392
- Apple2Type = TypeVar("Apple2Type", bound=Apple2)
393
-
394
-
395
- class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
396
- """
397
-
398
- Abstract base class for controlling an Apple2 undulator device.
399
-
400
- This class manages the undulator's gap and phase motors, and provides an interface
401
- for controlling polarisation and energy settings. It exposes derived signals for
402
- energy and polarisation, and handles conversion between energy/polarisation and
403
- motor positions via a user-supplied conversion callable.
404
-
405
- Attributes
406
- ----------
407
- apple2 : Reference[Apple2Type]
408
- Reference to the Apple2 device containing gap and phase motors.
409
- energy : derived_signal_rw
410
- Derived signal for moving and reading back energy.
411
- polarisation_setpoint : SignalR
412
- Soft signal for the polarisation setpoint.
413
- polarisation : derived_signal_rw
414
- Hardware-backed signal for polarisation readback and control.
415
- gap_energy_to_motor_converter : EnergyMotorConvertor
416
- Callable that converts energy and polarisation to gap motor positions.
417
- phase_energy_to_motor_converter : EnergyMotorConvertor
418
- Callable that converts energy and polarisation to phase motor positions.
419
-
420
- Abstract Methods
421
- ----------------
422
- _get_apple2_value(gap: float, phase: float) -> Apple2Val
423
- Abstract method to return the Apple2Val used to set the apple2 with.
424
- Notes
425
- -----
426
- - Subclasses must implement `_get_apple2_value` for beamline-specific logic.
427
- - LH3 polarisation is indistinguishable from LH in hardware; special handling is provided.
428
- - Supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
429
- positive circular (PC), negative circular (NC), and linear arbitrary (LA).
430
-
431
- """
432
-
433
- def __init__(
434
- self,
435
- apple2: Apple2Type,
436
- gap_energy_motor_converter: EnergyMotorConvertor,
437
- phase_energy_motor_converter: EnergyMotorConvertor,
438
- units: str = "eV",
439
- name: str = "",
440
- ) -> None:
441
- """
442
-
443
- Parameters
444
- ----------
445
- apple2: Apple2
446
- An Apple2 device.
447
- name: str
448
- Name of the device.
449
- """
450
- self.apple2 = Reference(apple2)
451
- self.gap_energy_motor_converter = gap_energy_motor_converter
452
- self.phase_energy_motor_converter = phase_energy_motor_converter
453
-
454
- # Store the set energy for readback.
455
- self._energy, self._energy_set = soft_signal_r_and_setter(
456
- float, initial_value=None, units=units
457
- )
458
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
459
- self.energy = derived_signal_rw(
460
- raw_to_derived=self._read_energy,
461
- set_derived=self._set_energy,
462
- energy=self._energy,
463
- derived_units=units,
464
- )
465
-
466
- # Store the polarisation for setpoint. And provide readback for LH3.
467
- # LH3 is a special case as it is indistinguishable from LH in the hardware.
468
- self.polarisation_setpoint, self._polarisation_setpoint_set = (
469
- soft_signal_r_and_setter(Pol)
470
- )
471
- phase = self.apple2().phase()
472
- # check if undulator phase is unlocked.
473
- if isinstance(phase, UndulatorPhaseAxes):
474
- top_inner = phase.top_inner.user_readback
475
- btm_outer = phase.btm_outer.user_readback
476
- else:
477
- # If locked phase axes make the locked phase 0.
478
- top_inner = btm_outer = soft_signal_rw(float, initial_value=0.0)
479
-
480
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
481
- # Hardware backed read/write for polarisation.
482
-
483
- self.polarisation = derived_signal_rw(
484
- raw_to_derived=self._read_pol,
485
- set_derived=self._set_pol,
486
- pol=self.polarisation_setpoint,
487
- top_outer=phase.top_outer.user_readback,
488
- top_inner=top_inner,
489
- btm_inner=phase.btm_inner.user_readback,
490
- btm_outer=btm_outer,
491
- gap=self.apple2().gap().user_readback,
492
- )
493
- super().__init__(name)
494
-
495
- @abc.abstractmethod
496
- def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
497
- """
498
- This method should be implemented by the beamline specific ID class as the
499
- motor positions will be different for each beamline depending on the
500
- undulator design.
501
- """
502
-
503
- async def _set_motors_from_energy_and_polarisation(
504
- self, energy: float, pol: Pol
505
- ) -> None:
506
- """Set the undulator motors for a given energy and polarisation."""
507
- gap = self.gap_energy_motor_converter(energy=energy, pol=pol)
508
- phase = self.phase_energy_motor_converter(energy=energy, pol=pol)
509
- apple2_val = self._get_apple2_value(gap, phase, pol)
510
- LOGGER.info(f"Setting polarisation to {pol}, with values: {apple2_val}")
511
- await self.apple2().set(id_motor_values=apple2_val)
512
-
513
- async def _set_energy(self, energy: float) -> None:
514
- pol = await self._check_and_get_pol_setpoint()
515
- await self._set_motors_from_energy_and_polarisation(energy, pol)
516
- self._energy_set(energy)
517
-
518
- def _read_energy(self, energy: float) -> float:
519
- """Readback for energy is just the set value."""
520
- return energy
521
-
522
- async def _check_and_get_pol_setpoint(self) -> Pol:
523
- """
524
- Check the polarisation setpoint and if it is NONE try to read it from
525
- hardware.
526
- """
527
- pol = await self.polarisation_setpoint.get_value()
528
-
529
- if pol == Pol.NONE:
530
- LOGGER.warning(
531
- "Found no setpoint for polarisation. Attempting to"
532
- " determine polarisation from hardware..."
533
- )
534
- pol = await self.polarisation.get_value()
535
- if pol == Pol.NONE:
536
- raise ValueError(
537
- f"Polarisation cannot be determined from hardware for {self.name}"
538
- )
539
- self._polarisation_setpoint_set(pol)
540
- return pol
541
-
542
- async def _set_pol(
543
- self,
544
- value: Pol,
545
- ) -> None:
546
- # This changes the pol setpoint and then changes polarisation via set energy.
547
- self._polarisation_setpoint_set(value)
548
- await self.energy.set(await self.energy.get_value(), timeout=MAXIMUM_MOVE_TIME)
549
-
550
- def _read_pol(
551
- self,
552
- pol: Pol,
553
- top_outer: float,
554
- top_inner: float,
555
- btm_inner: float,
556
- btm_outer: float,
557
- gap: float,
558
- ) -> Pol:
559
- LOGGER.info(
560
- f"Reading polarisation setpoint from hardware: "
561
- f"top_outer={top_outer}, top_inner={top_inner}, "
562
- f"btm_inner={btm_inner}, btm_outer={btm_outer}, gap={gap}."
563
- )
564
-
565
- read_pol, _ = self.determine_phase_from_hardware(
566
- top_outer, top_inner, btm_inner, btm_outer, gap
567
- )
568
- # LH3 is indistinguishable from LH see determine_phase_from_hardware's docString
569
- # so we return LH3 if the setpoint is LH3 and the readback is LH.
570
- if pol == Pol.LH3 and read_pol == Pol.LH:
571
- LOGGER.info(
572
- "The hardware cannot distinguish between LH and LH3."
573
- " Returning the last commanded polarisation value"
574
- )
575
- return Pol.LH3
576
-
577
- return read_pol
578
-
579
- def determine_phase_from_hardware(
580
- self,
581
- top_outer: float,
582
- top_inner: float,
583
- btm_inner: float,
584
- btm_outer: float,
585
- gap: float,
586
- ) -> tuple[Pol, float]:
587
- """
588
- Determine polarisation and phase value using motor position patterns.
589
- However there is no way to return lh3 polarisation or higher harmonic setting.
590
- (May be for future one can use the inverse poly to work out the energy and try to match it with the current energy
591
- to workout the polarisation but during my test the inverse poly is too unstable for general use.)
592
- """
593
- if gap > MAXIMUM_GAP_MOTOR_POSITION:
594
- raise RuntimeError(
595
- f"{self.name} is not in use, close gap or set polarisation to use this ID"
596
- )
597
-
598
- if all(
599
- isclose(x, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
600
- for x in [top_outer, top_inner, btm_inner, btm_outer]
601
- ):
602
- LOGGER.info("Determined polarisation: LH (Linear Horizontal).")
603
- return Pol.LH, 0.0
604
- if (
605
- isclose(
606
- top_outer,
607
- MAXIMUM_ROW_PHASE_MOTOR_POSITION,
608
- abs_tol=ROW_PHASE_MOTOR_TOLERANCE,
609
- )
610
- and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
611
- and isclose(
612
- btm_inner,
613
- MAXIMUM_ROW_PHASE_MOTOR_POSITION,
614
- abs_tol=ROW_PHASE_MOTOR_TOLERANCE,
615
- )
616
- and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
617
- ):
618
- LOGGER.info("Determined polarisation: LV (Linear Vertical).")
619
- return Pol.LV, MAXIMUM_ROW_PHASE_MOTOR_POSITION
620
- if (
621
- isclose(top_outer, btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
622
- and top_outer > 0.0
623
- and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
624
- and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
625
- ):
626
- LOGGER.info("Determined polarisation: PC (Positive Circular).")
627
- return Pol.PC, top_outer
628
- if (
629
- isclose(top_outer, btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
630
- and top_outer < 0.0
631
- and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
632
- and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
633
- ):
634
- LOGGER.info("Determined polarisation: NC (Negative Circular).")
635
- return Pol.NC, top_outer
636
- if (
637
- isclose(top_outer, -btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
638
- and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
639
- and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
640
- ):
641
- LOGGER.info("Determined polarisation: LA (Positive Linear Arbitrary).")
642
- return Pol.LA, top_outer
643
- if (
644
- isclose(top_inner, -btm_outer, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
645
- and isclose(top_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
646
- and isclose(btm_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
647
- ):
648
- LOGGER.info("Determined polarisation: LA (Negative Linear Arbitrary).")
649
- return Pol.LA, top_inner
650
-
651
- LOGGER.warning("Unable to determine polarisation. Defaulting to NONE.")
652
- return Pol.NONE, 0.0
653
-
654
-
655
- class Apple2EnforceLHMoveController(Apple2Controller[Apple2]):
656
- """The latest Apple2 version allows unrestricted motor movement.
657
- However, because of the high forces involved in polarization changes,
658
- all movements must be performed using the Linear Horizontal (LH) mode.
659
- A look-up table must also be used to determine the highest energy that can
660
- be reached in LH mode."""
661
-
662
- def __init__(
663
- self,
664
- apple2: Apple2,
665
- gap_energy_motor_lut: EnergyMotorLookup,
666
- phase_energy_motor_lut: EnergyMotorLookup,
667
- units: str = "eV",
668
- name: str = "",
669
- ) -> None:
670
- self.gap_energy_motor_lu = gap_energy_motor_lut
671
- self.phase_energy_motor_lu = phase_energy_motor_lut
672
- super().__init__(
673
- apple2=apple2,
674
- gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
675
- phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
676
- units=units,
677
- name=name,
678
- )
679
-
680
- def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
681
- apple2_val = Apple2Val(
682
- gap=f"{gap:.6f}",
683
- phase=Apple2PhasesVal(
684
- top_outer=f"{phase:.6f}",
685
- top_inner=f"{0.0:.6f}",
686
- btm_inner=f"{phase:.6f}",
687
- btm_outer=f"{0.0:.6f}",
688
- ),
689
- )
690
- LOGGER.info(f"Getting apple2 value for pol={pol}, gap={gap}, phase={phase}.")
691
- LOGGER.info(f"Apple2 motor values: {apple2_val}.")
692
-
693
- return apple2_val
694
-
695
- async def _set_pol(
696
- self,
697
- value: Pol,
698
- ) -> None:
699
- # I09/I21 require all polarisation change to go via LH.
700
- current_pol = await self.polarisation.get_value()
701
- if current_pol == value:
702
- LOGGER.info(f"Polarisation already at {value}")
703
- else:
704
- target_energy = await self.energy.get_value()
705
- if (value is not Pol.LH) and (current_pol is not Pol.LH):
706
- self._polarisation_setpoint_set(Pol.LH)
707
- max_lh_energy = float(
708
- self.gap_energy_motor_lu.lut.root[Pol("lh")].max_energy
709
- )
710
- lh_setpoint = (
711
- max_lh_energy if target_energy > max_lh_energy else target_energy
712
- )
713
- LOGGER.info(f"Changing polarisation to {value} via {Pol.LH}")
714
- await self.energy.set(lh_setpoint, timeout=MAXIMUM_MOVE_TIME)
715
- self._polarisation_setpoint_set(value)
716
- await self.energy.set(target_energy, timeout=MAXIMUM_MOVE_TIME)
717
-
718
-
719
- class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
720
- """Base class for ID energy movable device."""
721
-
722
- def __init__(self, name: str = "") -> None:
723
- self.energy: Reference[SignalRW[float]]
724
- super().__init__(name=name)
725
-
726
- @abc.abstractmethod
727
- @AsyncStatus.wrap
728
- async def set(self, energy: float) -> None: ...
729
-
730
-
731
- class BeamEnergy(StandardReadable, Movable[float]):
732
- """
733
- Compound device to set both ID and energy motor at the same time with an option to add an offset.
734
- """
735
-
736
- def __init__(
737
- self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
738
- ) -> None:
739
- """
740
- Parameters
741
- ----------
742
-
743
- id_energy: InsertionDeviceEnergy
744
- An InsertionDeviceEnergy device.
745
- mono: Motor
746
- A Motor(energy) device.
747
- name:
748
- New device name.
749
- """
750
- super().__init__(name=name)
751
- self._id_energy = Reference(id_energy)
752
- self._mono_energy = Reference(mono)
753
-
754
- self.add_readables(
755
- [
756
- self._id_energy().energy(),
757
- self._mono_energy().user_readback,
758
- ],
759
- StandardReadableFormat.HINTED_SIGNAL,
760
- )
761
-
762
- with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
763
- self.id_energy_offset = soft_signal_rw(float, initial_value=0)
764
-
765
- @AsyncStatus.wrap
766
- async def set(self, energy: float) -> None:
767
- LOGGER.info(f"Moving f{self.name} energy to {energy}.")
768
- await asyncio.gather(
769
- self._id_energy().set(
770
- energy=energy + await self.id_energy_offset.get_value()
771
- ),
772
- self._mono_energy().set(energy),
773
- )
774
-
775
-
776
- class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
777
- """Apple2 ID energy movable device."""
778
-
779
- def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
780
- self.energy = Reference(id_controller.energy)
781
- super().__init__(name=name)
782
-
783
- self.add_readables([self.energy()], StandardReadableFormat.HINTED_SIGNAL)
784
-
785
- @AsyncStatus.wrap
786
- async def set(self, energy: float) -> None:
787
- await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
788
-
789
-
790
- class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
791
- """Apple2 ID polarisation movable device."""
792
-
793
- def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
794
- self.polarisation = Reference(id_controller.polarisation)
795
- self.polarisation_setpoint = Reference(id_controller.polarisation_setpoint)
796
- super().__init__(name=name)
797
-
798
- self.add_readables([self.polarisation()], StandardReadableFormat.HINTED_SIGNAL)
799
-
800
- @AsyncStatus.wrap
801
- async def set(self, pol: Pol) -> None:
802
- await self.polarisation().set(pol, timeout=MAXIMUM_MOVE_TIME)
803
-
804
- async def locate(self) -> Location[Pol]:
805
- """Return the current polarisation"""
806
- setpoint, readback = await asyncio.gather(
807
- self.polarisation_setpoint().get_value(), self.polarisation().get_value()
808
- )
809
- return Location(setpoint=setpoint, readback=readback)