dls-dodal 1.61.0__py3-none-any.whl → 1.62.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dls-dodal
3
- Version: 1.61.0
3
+ Version: 1.62.0
4
4
  Summary: Ophyd devices and other utils that could be used across DLS beamlines
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>, Joseph Ware <joseph.ware@diamond.ac.uk>, Oliver Silvester <Oliver.Silvester@diamond.ac.uk>, Noemi Frisina <noemi.frisina@diamond.ac.uk>
6
6
  License: Apache License
@@ -1,7 +1,7 @@
1
- dls_dodal-1.61.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
1
+ dls_dodal-1.62.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
2
2
  dodal/__init__.py,sha256=Ksms_WJF8LTkbm38gEpm1jBpGqcQ8NGvmb2ZJlOE1j8,198
3
3
  dodal/__main__.py,sha256=kP2S2RPitnOWpNGokjZ1Yq-1umOtp5sNOZk2B3tBPLM,111
4
- dodal/_version.py,sha256=R0nxjIENITg3eHoaeIFi4EMDLFAHK-E5jkMwqc20ybI,706
4
+ dodal/_version.py,sha256=5VUVRHNcXnR6NHNHVw0jy5canKWEf1jsJRKy9oQveHA,706
5
5
  dodal/cli.py,sha256=yi8dXOp0hqzlg4ZZXCRGU-LpDa_ydaropDjyREWbZ5Y,4152
6
6
  dodal/log.py,sha256=Rt5O3hFZfMnJvQueZvgagQuXnPqHrFxhponOvVkpfrk,9871
7
7
  dodal/utils.py,sha256=abGitd4FLpLnmckF7lUqOKYUL88r5Ex_NGSVgO4gOf4,19305
@@ -20,7 +20,7 @@ dodal/beamlines/b18.py,sha256=ryxrGtcCdwoFgZ8ljWYgr1g9gKvoA7nxkARVxl1IE78,1189
20
20
  dodal/beamlines/b21.py,sha256=QGn0b88Od5-PCbleKwR6CSOleAY0vriybxcWlc58NS0,3851
21
21
  dodal/beamlines/i02_1.py,sha256=SwRm9v1U6CiT0fwLbSTECbJ6OF1BPEx4TJ0cEReuGZA,4017
22
22
  dodal/beamlines/i03.py,sha256=S2mJikFdZiV0e3VLNwHp38OCk_rL9wnWD009a7tH5kE,16203
23
- dodal/beamlines/i04.py,sha256=b42_K8Fe2GonF7SUMwac8oEWnJyLOuhYQ12wdys_oQQ,13757
23
+ dodal/beamlines/i04.py,sha256=QyprATHFxnw_cP2gak1i2_ywj-i26vrGlvOYM6RmyTY,13758
24
24
  dodal/beamlines/i05.py,sha256=v4QKd8-neh4Og205oovm6NDRnAU6Oktu1WrxalXsI40,656
25
25
  dodal/beamlines/i05_1.py,sha256=R6JFFg8Bj-Izw355mx3mOd4IDvJb5ipB4p7_S0I_4Z0,670
26
26
  dodal/beamlines/i09.py,sha256=E7PgMfJAixwvxd5bKzqX8ifVNExbzVMUYFN22TTTAFU,1594
@@ -35,7 +35,7 @@ dodal/beamlines/i15_1.py,sha256=TomeTLsYSvL5en8GPvI8driQa5nMObrB5DrMKckHig8,3898
35
35
  dodal/beamlines/i17.py,sha256=Nickt8CKQ9JcQ1D_ulNICUT4jjLF1Aib7D9jblSnzA4,987
36
36
  dodal/beamlines/i18.py,sha256=FuU8G-q1piu6BRou-Shj3BQEbNtsF7CUsSIqqkvCKZc,3615
37
37
  dodal/beamlines/i19_1.py,sha256=xAI9B3fyUKtoO-tnAF7wWQelrXQ6QrYNgEyncHqVzn4,3057
38
- dodal/beamlines/i19_2.py,sha256=x-k6Dy2_Jy_9Z4Jh5ytcQWsOQlk4MRAsLOX55Fjz4t0,3171
38
+ dodal/beamlines/i19_2.py,sha256=gY4YBYW_uble5rr74fxoW7490UXJdsFVDWALT_Zdp3Y,3594
39
39
  dodal/beamlines/i19_optics.py,sha256=8hdlDAAMgFrhcXrp5xCPZtLUlrDUEC9VwKnnuUAMbbU,1150
40
40
  dodal/beamlines/i20_1.py,sha256=Zsr1lsH7ySbOgK7RhMVMWzNWZAV-fuYW0iAjSEJZicY,2625
41
41
  dodal/beamlines/i21.py,sha256=5v6iiTlY4kWlWvQ_uNidJSotvkdNF3qdjR49l7sIYPc,728
@@ -74,13 +74,14 @@ dodal/devices/baton.py,sha256=315I_0V73_DYYVT0PBs0luVy4CMqdPo0kLvHBi12MIU,606
74
74
  dodal/devices/bimorph_mirror.py,sha256=OGe6aCczG0gVco4OvIRLJVxn2kw5F2QG1e06uqhFLTw,4609
75
75
  dodal/devices/collimation_table.py,sha256=64HunSPJH-L2gZdfIj_RYdOlOuwRFEfMHfLHzu4BAKI,1681
76
76
  dodal/devices/common_dcm.py,sha256=8QSRE6Z10RQjfL3g4JZhyHRNI_aCKxWlzKSsDgiJHhE,3049
77
- dodal/devices/controllers.py,sha256=un-12-ts65rKJ4Az6gwnIWIUGvAH6KxAQhZ2u2iJ1O4,542
77
+ dodal/devices/controllers.py,sha256=W_Ras1c6xLjcOMKPSHN8Z1eCUCH-ktaah2hzQXyuFLk,652
78
78
  dodal/devices/cryostream.py,sha256=2FxCGioEZNMHItsGm_rsnkRnHjwMUIwRMAX_x8odKIw,4678
79
79
  dodal/devices/diamond_filter.py,sha256=hySd7HnLdplpPNvBrLddLjO_3LqgD8-99Zr__Sy_GbI,689
80
80
  dodal/devices/eiger.py,sha256=ZKaRXF-YnYWuMW94BIg-gmdKW7JKmtMUvehF3S5XaLY,16670
81
81
  dodal/devices/eiger_odin.py,sha256=1JoqPppTp99IZCiFOXJZB3h62xXRYKZINxTYXdLnT8c,7480
82
82
  dodal/devices/eurotherm.py,sha256=rdLldmWYP1PZBckoya6svPy1mDxHYaa1IfMleMPGzD4,3832
83
- dodal/devices/fast_grid_scan.py,sha256=4HHOVAPwhUf4u1nC_kNhCAUzCqYuEsJ15lqcnxfdrVg,14659
83
+ dodal/devices/fast_grid_scan.py,sha256=2k2Ub-ywwfwCfmlofjnhkumXk07bidDfF86DPN7hcrA,17365
84
+ dodal/devices/fast_shutter.py,sha256=kgYafhBURRTN6edrhv0xKggxkSEWGXg2rhWz_ewLZhc,1837
84
85
  dodal/devices/fluorescence_detector_motion.py,sha256=hJ1M9Zs6Dlw8DDL9APh7yVePlS4tU2hnlySd16hqfwE,346
85
86
  dodal/devices/flux.py,sha256=1CDsq9yU2-ho8MfYBl50Tl9ABZwpUBHnV486PQXKNoQ,462
86
87
  dodal/devices/focusing_mirror.py,sha256=2UWVrY6bs6_0i-h6JJ_LAUZPX_FgMzIx04JqZafdtYU,7464
@@ -160,7 +161,7 @@ dodal/devices/electron_analyser/vgscienta/driver_io.py,sha256=c1v3AOSGKp6DlVmE82
160
161
  dodal/devices/electron_analyser/vgscienta/enums.py,sha256=3vmX67ExATU8clueVp_mCzw3OUQx799oZMc8gyHtMJE,205
161
162
  dodal/devices/electron_analyser/vgscienta/region.py,sha256=6k6Eah6_I74Pi2_a0KfyMYFfwHwJbh1ndqy0fuq5o14,2184
162
163
  dodal/devices/i02_1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
163
- dodal/devices/i02_1/fast_grid_scan.py,sha256=eyWE-urpxY10_KZK4Vg48Rau0rTK3O2Ul4IcK6P2Geg,2428
164
+ dodal/devices/i02_1/fast_grid_scan.py,sha256=bfiumNayrISFGYqdcd3Bh6lEZvkOxrqyX_B24LYW1nU,2429
164
165
  dodal/devices/i02_1/sample_motors.py,sha256=fAHAyeuP4hjOnYsp2x5VQNrTh8Di35ezJV1si2YmKPY,607
165
166
  dodal/devices/i03/__init__.py,sha256=Kvukapy4a5lUQ20qaCqYCJzKNaqJn2DfXP5nKZ_Pec8,118
166
167
  dodal/devices/i03/dcm.py,sha256=zDcgxOdMRVOQZBGDsLaIlr7o4UJIK2vehPWHxAxt6VA,2268
@@ -196,7 +197,7 @@ dodal/devices/i13_1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
196
197
  dodal/devices/i13_1/merlin.py,sha256=mgTFSMJftRzLL-HXAUuJkOYxtyA3Rp8YX0L46JCb30Y,1019
197
198
  dodal/devices/i13_1/merlin_controller.py,sha256=myfmByOEXyMrlJZfsjOxDHeGQVwZGfsRtzrfSy2001o,1495
198
199
  dodal/devices/i15/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
- dodal/devices/i15/dcm.py,sha256=QLqnKhNbkcVuCFEy9yB-2Ga4fAR0EOGZQshfSoVN8-w,3113
200
+ dodal/devices/i15/dcm.py,sha256=MQK2oHQOPB1pUyc4uWwIvzb3WWP8PuzaPe5O4Zqt0TM,3089
200
201
  dodal/devices/i15/focussing_mirror.py,sha256=E6T_c7M2osgHLa7u8eUfQJlXlJbZHtOp95FhP3qZ4gY,1739
201
202
  dodal/devices/i15/jack.py,sha256=VafCNx-uqkIy0LxbBAhSm_tuC8_SbGCrnTbvQCExAzA,962
202
203
  dodal/devices/i15/laue.py,sha256=H0nLPH8gqJejBZtZeY0lv84EaE2lqdL3CmXvT9iHhpk,496
@@ -212,6 +213,8 @@ dodal/devices/i19/beamstop.py,sha256=JkcvkEmcC3eY3GHrvYNGqv2yDwrfgdpWKVZJWSadWW8
212
213
  dodal/devices/i19/blueapi_device.py,sha256=Tsl4vsREz7FM2d-kKJK-9tGrYbyKq4SLxnMlEKIM-g8,3966
213
214
  dodal/devices/i19/diffractometer.py,sha256=QCEi0Gko6Ja9_ec2vfdazwMspknvX63jcz8hQ2XW1xo,1182
214
215
  dodal/devices/i19/hutch_access.py,sha256=hnClUWCL1qTYzuBMmhXX85jiNak7mbYfyHEh54tZ27U,377
216
+ dodal/devices/i19/mapt_configuration.py,sha256=0zUzCSDofQORdaqbgD5NU-Vuf-EljgTKfvFDRDA9IjQ,1717
217
+ dodal/devices/i19/pin_col_stages.py,sha256=SAdokjoTmlRcCQQSVTz4jCkBibSsKCye7s5aYtfhsfg,6691
215
218
  dodal/devices/i19/shutter.py,sha256=B4KP0Ruc8Ex47OfCskbjYSSu81jz4tK9GC_7pxMIkgI,2140
216
219
  dodal/devices/i20_1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
220
  dodal/devices/i21/__init__.py,sha256=1H0Ov9s8K7nu6e20WtQDH39wgSKWz2ChRVAUzytIyzQ,67
@@ -280,7 +283,7 @@ dodal/plan_stubs/motor_utils.py,sha256=Mf8utOA_xmxUa2dLmQ1uRkdfyDTip7D8YcKeCBCQL
280
283
  dodal/plan_stubs/wrapped.py,sha256=kC8HH7bx3-sLYu2oieY_502tAdT2OECF8n-fqoL5Bfc,4266
281
284
  dodal/plans/__init__.py,sha256=nH1jNxw3DzDMg9O8Uda0kqKIalRVEWBrq07OLY6Ey38,93
282
285
  dodal/plans/bimorph.py,sha256=JxDmZDiEvZnz5f22tlaoyivpnaNGiX8kSL82qz5uvMM,11738
283
- dodal/plans/configure_arm_trigger_and_disarm_detector.py,sha256=Ndg6Tuqvu4WesTxtDlzzgeJh0BP7aWJ-TbYlforDkjM,5454
286
+ dodal/plans/configure_arm_trigger_and_disarm_detector.py,sha256=VLsNhHx8NPFLhNeUd_EjlbOPqe18C-SEZEWGGmXtf_Y,6170
284
287
  dodal/plans/save_panda.py,sha256=1fumH7Ih8uDIv8ahAtgQ_vUuR3dz0sfUs4n9TEtEbSs,3053
285
288
  dodal/plans/scanspec.py,sha256=Q0AcvTKRT401iGMRDSqK-D523UX5_ofiVMZ_rNXKOx8,2074
286
289
  dodal/plans/verify_undulator_gap.py,sha256=OcDN09-eCoMzsmhKGxvzsH5EapG2zYz0yGCqUtQxLSc,568
@@ -291,8 +294,8 @@ dodal/testing/__init__.py,sha256=AUYZKAvVOs7ZvxO1dVhL0pDTleRO34FQlO5MNe_cwgU,96
291
294
  dodal/testing/setup.py,sha256=8cQnrzE5MQD4Etf0eqMarmtr-opsUOMQww-k1V7DzIQ,2442
292
295
  dodal/testing/electron_analyser/__init__.py,sha256=-lc1opD2dCv0x678-J-ApOhHtvEvcslfOQ7E613U8-Y,118
293
296
  dodal/testing/electron_analyser/device_factory.py,sha256=tkMY6fW3iI02DTD1XXHi4lH6sjo8RHHZBGDHSuTdmNU,2243
294
- dls_dodal-1.61.0.dist-info/METADATA,sha256=CzbkDC58MYPwQdjxT6ArCdVqQWB9qWGQSHRUC8iWB7A,16941
295
- dls_dodal-1.61.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
296
- dls_dodal-1.61.0.dist-info/entry_points.txt,sha256=bycw_EKUzup_rxfCetOwcauXV4kLln_OPpPT8jEnr-I,94
297
- dls_dodal-1.61.0.dist-info/top_level.txt,sha256=xIozdmZk_wmMV4wugpq9-6eZs0vgADNUKz3j2UAwlhc,6
298
- dls_dodal-1.61.0.dist-info/RECORD,,
297
+ dls_dodal-1.62.0.dist-info/METADATA,sha256=oUaFqOHwl4hc_7ifCVwr1lskpYdxni_oGRG1eihyPog,16941
298
+ dls_dodal-1.62.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
299
+ dls_dodal-1.62.0.dist-info/entry_points.txt,sha256=bycw_EKUzup_rxfCetOwcauXV4kLln_OPpPT8jEnr-I,94
300
+ dls_dodal-1.62.0.dist-info/top_level.txt,sha256=xIozdmZk_wmMV4wugpq9-6eZs0vgADNUKz3j2UAwlhc,6
301
+ dls_dodal-1.62.0.dist-info/RECORD,,
dodal/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.61.0'
32
- __version_tuple__ = version_tuple = (1, 61, 0)
31
+ __version__ = version = '1.62.0'
32
+ __version_tuple__ = version_tuple = (1, 62, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
dodal/beamlines/i04.py CHANGED
@@ -234,7 +234,7 @@ def undulator() -> Undulator:
234
234
  """
235
235
  return Undulator(
236
236
  prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
237
- id_gap_lookup_table_path="/dls_sw/i04/software/gda/config/lookupTables/BeamLine_Undulator_toGap.txt",
237
+ id_gap_lookup_table_path="/dls_sw/i04/software/daq_configuration/lookup/BeamLine_Undulator_toGap.txt",
238
238
  )
239
239
 
240
240
 
dodal/beamlines/i19_2.py CHANGED
@@ -11,6 +11,7 @@ from dodal.devices.i19.backlight import BacklightPosition
11
11
  from dodal.devices.i19.beamstop import BeamStop
12
12
  from dodal.devices.i19.blueapi_device import HutchState
13
13
  from dodal.devices.i19.diffractometer import FourCircleDiffractometer
14
+ from dodal.devices.i19.pin_col_stages import PinholeCollimatorControl
14
15
  from dodal.devices.i19.shutter import AccessControlledShutter
15
16
  from dodal.devices.synchrotron import Synchrotron
16
17
  from dodal.devices.zebra.zebra import Zebra
@@ -81,6 +82,15 @@ def synchrotron() -> Synchrotron:
81
82
  return Synchrotron()
82
83
 
83
84
 
85
+ @device_factory()
86
+ def pinhole_and_collimator() -> PinholeCollimatorControl:
87
+ """Get the i19-2 pinhole and collimator control device, instantiate it if it
88
+ hasn't already been. If this is called when already instantiated in i19-2,
89
+ it will return the existing object.
90
+ """
91
+ return PinholeCollimatorControl(prefix=PREFIX.beamline_prefix)
92
+
93
+
84
94
  @device_factory()
85
95
  def backlight() -> BacklightPosition:
86
96
  """Get the i19-2 backlight device, instantiate it if it hasn't already been.
@@ -1,9 +1,6 @@
1
1
  from typing import TypeVar
2
2
 
3
- from ophyd_async.epics.adcore import (
4
- ADBaseController,
5
- ADBaseIO,
6
- )
3
+ from ophyd_async.epics.adcore import ADBaseController, ADBaseIO, ADImageMode
7
4
 
8
5
  ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
9
6
 
@@ -13,8 +10,13 @@ class ConstantDeadTimeController(ADBaseController[ADBaseIOT]):
13
10
  ADBaseController with a configured constant deadtime for a driver of type ADBaseIO.
14
11
  """
15
12
 
16
- def __init__(self, driver: ADBaseIOT, deadtime: float):
17
- super().__init__(driver)
13
+ def __init__(
14
+ self,
15
+ driver: ADBaseIOT,
16
+ deadtime: float,
17
+ image_mode: ADImageMode = ADImageMode.MULTIPLE,
18
+ ):
19
+ super().__init__(driver, image_mode=image_mode)
18
20
  self.deadtime = deadtime
19
21
 
20
22
  def get_deadtime(self, exposure: float | None) -> float:
@@ -1,9 +1,10 @@
1
+ import asyncio
1
2
  from abc import ABC, abstractmethod
2
3
  from typing import Generic, TypeVar
3
4
 
4
5
  import numpy as np
5
- from bluesky.plan_stubs import mv
6
- from bluesky.protocols import Flyable
6
+ from bluesky.plan_stubs import prepare
7
+ from bluesky.protocols import Flyable, Preparable
7
8
  from numpy import ndarray
8
9
  from ophyd_async.core import (
9
10
  AsyncStatus,
@@ -13,6 +14,7 @@ from ophyd_async.core import (
13
14
  SignalRW,
14
15
  StandardReadable,
15
16
  derived_signal_r,
17
+ set_and_wait_for_value,
16
18
  soft_signal_r_and_setter,
17
19
  wait_for_value,
18
20
  )
@@ -29,6 +31,10 @@ from dodal.log import LOGGER
29
31
  from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
30
32
 
31
33
 
34
+ class GridScanInvalidException(RuntimeError):
35
+ """Raised when the gridscan parameters are not valid."""
36
+
37
+
32
38
  @dataclass
33
39
  class GridAxis:
34
40
  start: float
@@ -144,7 +150,7 @@ class GridScanParamsThreeD(GridScanParamsCommon):
144
150
  return GridAxis(self.z2_start_mm, self.z_step_size_mm, self.z_steps)
145
151
 
146
152
 
147
- ParamType = TypeVar("ParamType", bound=GridScanParamsCommon, covariant=True)
153
+ ParamType = TypeVar("ParamType", bound=GridScanParamsCommon)
148
154
 
149
155
 
150
156
  class WithDwellTime(BaseModel):
@@ -190,7 +196,9 @@ class MotionProgram(Device):
190
196
  self.program_number = soft_signal_r_and_setter(float, -1)[0]
191
197
 
192
198
 
193
- class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
199
+ class FastGridScanCommon(
200
+ StandardReadable, Flyable, ABC, Preparable, Generic[ParamType]
201
+ ):
194
202
  """Device containing the minimal signals for a general fast grid scan.
195
203
 
196
204
  When the motion program is started, the goniometer will move in a snake-like grid trajectory,
@@ -231,8 +239,9 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
231
239
  self.KICKOFF_TIMEOUT: float = 5.0
232
240
 
233
241
  self.COMPLETE_STATUS: float = 60.0
242
+ self.VALIDITY_CHECK_TIMEOUT = 0.5
234
243
 
235
- self.movable_params: dict[str, Signal] = {
244
+ self._movable_params: dict[str, Signal] = {
236
245
  "x_steps": self.x_steps,
237
246
  "y_steps": self.y_steps,
238
247
  "x_step_size_mm": self.x_step_size,
@@ -284,6 +293,45 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
284
293
  self, motion_controller_prefix: str
285
294
  ) -> MotionProgram: ...
286
295
 
296
+ @AsyncStatus.wrap
297
+ async def prepare(self, value: ParamType):
298
+ """
299
+ Submit the gridscan parameters to the device for validation prior to
300
+ gridscan kickoff
301
+ Args:
302
+ value: the gridscan parameters
303
+
304
+ Raises:
305
+ GridScanInvalidException: if the gridscan parameters were not valid
306
+ """
307
+ set_statuses = []
308
+
309
+ LOGGER.info("Applying gridscan parameters...")
310
+ # Create arguments for bps.mv
311
+ for key, signal in self._movable_params.items():
312
+ param_value = value.__dict__[key]
313
+ set_statuses.append(await set_and_wait_for_value(signal, param_value)) # type: ignore
314
+
315
+ # Counter should always start at 0
316
+ set_statuses.append(await set_and_wait_for_value(self.position_counter, 0))
317
+
318
+ LOGGER.info("Gridscan parameters applied, waiting for sets to complete...")
319
+
320
+ # wait for parameter sets to complete
321
+ await asyncio.gather(*set_statuses)
322
+
323
+ LOGGER.info("Sets confirmed, waiting for validity checks to pass...")
324
+ try:
325
+ await wait_for_value(
326
+ self.scan_invalid, 0.0, timeout=self.VALIDITY_CHECK_TIMEOUT
327
+ )
328
+ except TimeoutError as e:
329
+ raise GridScanInvalidException(
330
+ f"Gridscan parameters not validated after {self.VALIDITY_CHECK_TIMEOUT}s"
331
+ ) from e
332
+
333
+ LOGGER.info("Gridscan validity confirmed, gridscan is now prepared.")
334
+
287
335
 
288
336
  class FastGridScanThreeD(FastGridScanCommon[ParamType]):
289
337
  """Device for standard 3D FGS.
@@ -309,10 +357,10 @@ class FastGridScanThreeD(FastGridScanCommon[ParamType]):
309
357
 
310
358
  super().__init__(full_prefix, prefix, name)
311
359
 
312
- self.movable_params["z_step_size_mm"] = self.z_step_size
313
- self.movable_params["z2_start_mm"] = self.z2_start
314
- self.movable_params["y2_start_mm"] = self.y2_start
315
- self.movable_params["z_steps"] = self.z_steps
360
+ self._movable_params["z_step_size_mm"] = self.z_step_size
361
+ self._movable_params["z2_start_mm"] = self.z2_start
362
+ self._movable_params["y2_start_mm"] = self.y2_start
363
+ self._movable_params["z_steps"] = self.z_steps
316
364
 
317
365
  def _create_expected_images_signal(self):
318
366
  return derived_signal_r(
@@ -329,7 +377,35 @@ class FastGridScanThreeD(FastGridScanCommon[ParamType]):
329
377
  return first_grid + second_grid
330
378
 
331
379
  def _create_scan_invalid_signal(self, prefix: str) -> SignalR[float]:
332
- return epics_signal_r(float, f"{prefix}SCAN_INVALID")
380
+ self.x_scan_valid = epics_signal_r(float, f"{prefix}X_SCAN_VALID")
381
+ self.y_scan_valid = epics_signal_r(float, f"{prefix}Y_SCAN_VALID")
382
+ self.z_scan_valid = epics_signal_r(float, f"{prefix}Z_SCAN_VALID")
383
+ self.device_scan_invalid = epics_signal_r(float, f"{prefix}SCAN_INVALID")
384
+
385
+ def compute_derived_value(
386
+ x_scan_valid: float,
387
+ y_scan_valid: float,
388
+ z_scan_valid: float,
389
+ device_scan_invalid: float,
390
+ ) -> float:
391
+ return (
392
+ 1.0
393
+ if not (
394
+ x_scan_valid
395
+ and y_scan_valid
396
+ and z_scan_valid
397
+ and not device_scan_invalid
398
+ )
399
+ else 0.0
400
+ )
401
+
402
+ return derived_signal_r(
403
+ compute_derived_value,
404
+ x_scan_valid=self.x_scan_valid,
405
+ y_scan_valid=self.y_scan_valid,
406
+ z_scan_valid=self.z_scan_valid,
407
+ device_scan_invalid=self.device_scan_invalid,
408
+ )
333
409
 
334
410
  def _create_motion_program(self, motion_controller_prefix: str):
335
411
  return MotionProgram(motion_controller_prefix)
@@ -349,7 +425,7 @@ class ZebraFastGridScanThreeD(FastGridScanThreeD[ZebraGridScanParamsThreeD]):
349
425
  self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}DWELL_TIME")
350
426
  self.x_counter = epics_signal_r(int, f"{full_prefix}X_COUNTER")
351
427
  super().__init__(prefix, infix, name)
352
- self.movable_params["dwell_time_ms"] = self.dwell_time_ms
428
+ self._movable_params["dwell_time_ms"] = self.dwell_time_ms
353
429
 
354
430
  def _create_position_counter(self, prefix: str):
355
431
  return epics_signal_rw(
@@ -380,20 +456,20 @@ class PandAFastGridScan(FastGridScanThreeD[PandAGridScanParams]):
380
456
  )
381
457
  super().__init__(prefix, infix, name)
382
458
 
383
- self.movable_params["run_up_distance_mm"] = self.run_up_distance_mm
459
+ self._movable_params["run_up_distance_mm"] = self.run_up_distance_mm
384
460
 
385
461
  def _create_position_counter(self, prefix: str):
386
462
  return epics_signal_rw(int, f"{prefix}Y_COUNTER")
387
463
 
388
464
 
389
465
  def set_fast_grid_scan_params(scan: FastGridScanCommon[ParamType], params: ParamType):
390
- to_move = []
391
-
392
- # Create arguments for bps.mv
393
- for key in scan.movable_params.keys():
394
- to_move.extend([scan.movable_params[key], params.__dict__[key]])
395
-
396
- # Counter should always start at 0
397
- to_move.extend([scan.position_counter, 0])
466
+ """
467
+ Apply the fast grid scan parameters to the grid scan device and validate them
468
+ Args:
469
+ scan: The fast grid scan device
470
+ params: The parameters to set
398
471
 
399
- yield from mv(*to_move)
472
+ Raises:
473
+ GridScanInvalidException: if the grid scan parameters are not valid
474
+ """
475
+ yield from prepare(scan, params, wait=True)
@@ -0,0 +1,57 @@
1
+ from typing import TypeVar
2
+
3
+ from bluesky.protocols import Movable
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ EnumTypes,
7
+ StandardReadable,
8
+ )
9
+ from ophyd_async.epics.core import epics_signal_rw
10
+
11
+ StrictEnumT = TypeVar("StrictEnumT", bound=EnumTypes)
12
+
13
+
14
+ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
15
+ """
16
+ Basic enum device specialised for a fast shutter with configured open_state and
17
+ close_state so it is generic enough to be used with any device or plan without
18
+ knowing the specific enum to use.
19
+
20
+ For example:
21
+ await shutter.set(shutter.open_state)
22
+ await shutter.set(shutter.close_state)
23
+ OR
24
+ RE(bps.mv(shutter, shutter.open_state))
25
+ RE(bps.mv(shutter, shutter.close_state))
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ prefix: str,
31
+ open_state: StrictEnumT,
32
+ close_state: StrictEnumT,
33
+ name: str = "",
34
+ ):
35
+ """
36
+ Arguments:
37
+ prefix: The prefix for the shutter device.
38
+ open_state: The enum value that corresponds with opening the shutter.
39
+ close_state: The enum value that corresponds with closing the shutter.
40
+ """
41
+ self.open_state = open_state
42
+ self.close_state = close_state
43
+ with self.add_children_as_readables():
44
+ self.state = epics_signal_rw(type(self.open_state), prefix)
45
+ super().__init__(name)
46
+
47
+ @AsyncStatus.wrap
48
+ async def set(self, value: StrictEnumT) -> None:
49
+ await self.state.set(value)
50
+
51
+ async def is_open(self) -> bool:
52
+ """Checks to see if shutter is currently open"""
53
+ return await self.state.get_value() == self.open_state
54
+
55
+ async def is_closed(self) -> bool:
56
+ """Checks to see if shutter is currently closed"""
57
+ return await self.state.get_value() == self.close_state
@@ -35,7 +35,7 @@ class ZebraFastGridScanTwoD(FastGridScanCommon[ZebraGridScanParamsTwoD]):
35
35
  # See https://github.com/DiamondLightSource/mx-bluesky/issues/1203
36
36
  self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}EXPOSURE_TIME")
37
37
 
38
- self.movable_params["dwell_time_ms"] = self.dwell_time_ms
38
+ self._movable_params["dwell_time_ms"] = self.dwell_time_ms
39
39
 
40
40
  def _create_expected_images_signal(self):
41
41
  return derived_signal_r(
dodal/devices/i15/dcm.py CHANGED
@@ -28,19 +28,17 @@ Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
28
28
  Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
29
29
 
30
30
 
31
- class DualCrystalMonoSimple(StandardReadable, Generic[Xtal_1, Xtal_2]):
31
+ class BaseDCMforI15(StandardReadable, Generic[Xtal_1, Xtal_2]):
32
32
  """
33
- Device for simple double crystal monochromators (DCM), which only allow energy of the beam to be selected.
33
+ Device for double crystal monochromators (DCM), which only allow energy of the beam to be selected.
34
34
 
35
35
  Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
36
36
  each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
37
- This base device accounts for all combinations of this.
37
+ This device only accounts for combinations of energy plus two crystals.
38
38
 
39
- This device is more able to act as a parent for beamline-specific DCM's, in which any other missing signals can be added,
40
- as it doesn't assume WAVELENGTH, BRAGG and OFFSET are available for all DCM deivces, as BaseDCM does.
41
-
42
- Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
43
- which only requires one crystal which can roll should be typed 'def my_plan(dcm: BaseDCM[RollCrystal, StationaryCrystal])`
39
+ This device is designed to be a drop in replacement for BaseDCM for i15, which doesn't require WAVELENGTH, BRAGG and OFFSET to
40
+ be available. Once the i15 DCM supports all of the PVs required by BaseDCM, the i15 DCM device can switch to inheriting from
41
+ BaseDCM and this class can be removed.
44
42
  """
45
43
 
46
44
  def __init__(
@@ -64,9 +62,12 @@ class DualCrystalMonoSimple(StandardReadable, Generic[Xtal_1, Xtal_2]):
64
62
  self.xtal_2 = xtal_2(prefix)
65
63
 
66
64
 
67
- class DCM(DualCrystalMonoSimple[ThetaRollYZCrystal, ThetaYCrystal]):
65
+ class DCM(BaseDCMforI15[ThetaRollYZCrystal, ThetaYCrystal]):
68
66
  """
69
67
  A double crystal monocromator device, used to select the beam energy.
68
+
69
+ Once the i15 DCM supports all of the PVs required by BaseDCM, this class can be
70
+ changed to inherit from BaseDCM and BaseDCMforI15 can be removed.
70
71
  """
71
72
 
72
73
  def __init__(self, prefix: str, name: str = "") -> None:
@@ -0,0 +1,38 @@
1
+ from ophyd_async.core import DeviceVector, SignalR, StandardReadable, SubsetEnum
2
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
3
+
4
+
5
+ class MAPTConfigurationTable(StandardReadable):
6
+ """Readable device that can be used to build the read-only MAPT (Mini Apertures)
7
+ configuration table in controls for aperture motors in the available positions.
8
+ For each aperture it sets up a readable signal with the position of all the motors
9
+ in the MAPT configuration.
10
+ This device can be used to build the table for both eh1 and eh2 on I19, in
11
+ combination with the MAPTConfigurationControl device.
12
+ """
13
+
14
+ def __init__(
15
+ self, prefix: str, motor_name: str, aperture_list: list[int], name: str = ""
16
+ ) -> None:
17
+ with self.add_children_as_readables():
18
+ self.in_positions: DeviceVector[SignalR[float]] = DeviceVector(
19
+ {
20
+ pos: epics_signal_r(float, f"{prefix}:{pos}UM:{motor_name}")
21
+ for pos in aperture_list
22
+ }
23
+ )
24
+ super().__init__(name)
25
+
26
+
27
+ class MAPTConfigurationControl(StandardReadable):
28
+ """A device to control the MAPT (Mini Aperture) configuration. It provides a signal
29
+ to set the configuration PV to the requested value and a triggerable signal that
30
+ will move all the motors to the correct position."""
31
+
32
+ def __init__(
33
+ self, prefix: str, aperture_request: type[SubsetEnum], name: str = ""
34
+ ) -> None:
35
+ with self.add_children_as_readables():
36
+ self.select_config = epics_signal_rw(aperture_request, f"{prefix}")
37
+ self.apply_selection = epics_signal_x(f"{prefix}:APPLY.PROC")
38
+ super().__init__(name)
@@ -0,0 +1,170 @@
1
+ import asyncio
2
+ from enum import StrEnum
3
+
4
+ from bluesky.protocols import Movable
5
+ from ophyd_async.core import AsyncStatus, StandardReadable, SubsetEnum
6
+ from ophyd_async.epics.core import epics_signal_r
7
+ from pydantic import BaseModel
8
+
9
+ from dodal.devices.i19.mapt_configuration import (
10
+ MAPTConfigurationControl,
11
+ MAPTConfigurationTable,
12
+ )
13
+ from dodal.devices.motors import XYStage
14
+ from dodal.log import LOGGER
15
+
16
+ _PIN = "-MO-PIN-01:"
17
+ _COL = "-MO-COL-01:"
18
+ _CONFIG = "-OP-PCOL-01:"
19
+
20
+
21
+ class PinColRequest(StrEnum):
22
+ """Aperture request positions."""
23
+
24
+ PCOL20 = "20um"
25
+ PCOL40 = "40um"
26
+ PCOL100 = "100um"
27
+ PCOL3000 = "3000um"
28
+ OUT = "OUT"
29
+
30
+
31
+ # NOTE. Using subset enum because from the OUT positions should only be used by
32
+ # the beamline scientists from the synoptic. Another option will be needed in the
33
+ # device for OUT position.
34
+ class _PinColPosition(SubsetEnum):
35
+ """Aperture request IN positions."""
36
+
37
+ PCOL20 = "20um"
38
+ PCOL40 = "40um"
39
+ PCOL100 = "100um"
40
+ PCOL3000 = "3000um"
41
+
42
+
43
+ class AperturePosition(BaseModel):
44
+ """Describe the positions of the pinhole and collimator stage motors for
45
+ one of the available apertures.
46
+
47
+ Attributes:
48
+ pinhole_x: The position of the x motor on the pinhole stage
49
+ pinhole_y: The position of the y motor on the pinhole stage
50
+ collimator_x: The position of the x motor on the collimator stage
51
+ collimator_y: The position of the y motor on the collimator stage
52
+ """
53
+
54
+ pinhole_x: float
55
+ pinhole_y: float
56
+ collimator_x: float
57
+ collimator_y: float
58
+
59
+
60
+ class PinColConfiguration(StandardReadable):
61
+ """Full MAPT configuration table, including out positions and selection for the
62
+ Pinhole and Collimator control."""
63
+
64
+ def __init__(self, prefix: str, apertures: list[int], name: str = "") -> None:
65
+ with self.add_children_as_readables():
66
+ self.configuration = MAPTConfigurationControl(prefix, _PinColPosition)
67
+ self.pin_x = MAPTConfigurationTable(prefix, "PINX", apertures)
68
+ self.pin_y = MAPTConfigurationTable(prefix, "PINY", apertures)
69
+ self.col_x = MAPTConfigurationTable(prefix, "COLX", apertures)
70
+ self.col_y = MAPTConfigurationTable(prefix, "COLY", apertures)
71
+ self.pin_x_out = epics_signal_r(float, f"{prefix}:OUT:PINX")
72
+ self.col_x_out = epics_signal_r(float, f"{prefix}:OUT:COLX")
73
+ super().__init__(name)
74
+
75
+
76
+ class PinholeCollimatorControl(StandardReadable, Movable[str]):
77
+ """Device to control the Pinhole and Collimator stages moves on I19-2, using the
78
+ MAPT configuration table to look up the positions."""
79
+
80
+ def __init__(
81
+ self,
82
+ prefix: str,
83
+ name: str = "",
84
+ pin_infix: str = _PIN,
85
+ col_infix: str = _COL,
86
+ config_infix: str = _CONFIG,
87
+ ):
88
+ self._aperture_sizes = [self._get_aperture_size(i) for i in _PinColPosition]
89
+ with self.add_children_as_readables():
90
+ self._pinhole = XYStage(f"{prefix}{pin_infix}")
91
+ self._collimator = XYStage(f"{prefix}{col_infix}")
92
+ self.mapt = PinColConfiguration(
93
+ f"{prefix}{config_infix}CONFIG", apertures=self._aperture_sizes
94
+ )
95
+ super().__init__(name=name)
96
+
97
+ def _get_aperture_size(self, ap_request: str) -> int:
98
+ return int(ap_request.strip("um"))
99
+
100
+ async def _get_motor_positions_for_requested_aperture(
101
+ self, ap_request: _PinColPosition
102
+ ) -> AperturePosition:
103
+ val = self._get_aperture_size(ap_request.value)
104
+
105
+ pinx = await self.mapt.pin_x.in_positions[val].get_value()
106
+ piny = await self.mapt.pin_y.in_positions[val].get_value()
107
+ colx = await self.mapt.col_x.in_positions[val].get_value()
108
+ coly = await self.mapt.col_y.in_positions[val].get_value()
109
+
110
+ return AperturePosition(
111
+ pinhole_x=pinx, pinhole_y=piny, collimator_x=colx, collimator_y=coly
112
+ )
113
+
114
+ async def _safe_move_out(self):
115
+ """Move the pinhole and collimator stages safely to the out position, which
116
+ involves only the x motors of the stages.
117
+ In order to avoid a collision, we have to make sure that the collimator stage is
118
+ always moved out first and the pinhole stage second.
119
+ """
120
+ LOGGER.info("Moving pinhole and collimator stages to out position")
121
+ colx_out = await self.mapt.col_x_out.get_value()
122
+ pin_x_out = await self.mapt.pin_x_out.get_value()
123
+ # First move Collimator x motor
124
+ LOGGER.debug(f"Move collimator stage x motor to {colx_out}")
125
+ await self._collimator.x.set(colx_out)
126
+ # Then move Pinhole x motor
127
+ LOGGER.debug(f"Move pinhole stage x motor to {pin_x_out}")
128
+ await self._pinhole.x.set(pin_x_out)
129
+
130
+ async def _safe_move_in(self, value: _PinColPosition):
131
+ """Move the pinhole and collimator stages safely to the in position.
132
+ In order to avoid a collision, we have to make sure that the pinhole stage is
133
+ always moved in before the collimator stage."""
134
+ LOGGER.info(
135
+ f"Moving pinhole and collimator stages to in position: {value.value}"
136
+ )
137
+ await self.mapt.configuration.select_config.set(value, wait=True)
138
+ # NOTE. The apply PV will not be used here unless fixed in controls first.
139
+ # This is to avoid collisions. A safe move in will move first the pinhole stage
140
+ # and then the collimator stage, but apply will try to move all the motors
141
+ # at the same time.
142
+ aperture_positions = await self._get_motor_positions_for_requested_aperture(
143
+ value
144
+ )
145
+ LOGGER.debug(f"Moving motors to {aperture_positions}")
146
+
147
+ # First move Pinhole motors,
148
+ LOGGER.debug("Moving pinhole stage in")
149
+ await asyncio.gather(
150
+ self._pinhole.x.set(aperture_positions.pinhole_x),
151
+ self._pinhole.y.set(aperture_positions.pinhole_y),
152
+ )
153
+ # Then move Collimator motors
154
+ LOGGER.debug("Moving collimator stage in")
155
+ await asyncio.gather(
156
+ self._collimator.x.set(aperture_positions.collimator_x),
157
+ self._collimator.y.set(aperture_positions.collimator_y),
158
+ )
159
+
160
+ @AsyncStatus.wrap
161
+ async def set(self, value: PinColRequest):
162
+ """Moves the motor stages to the position for the requested aperture while
163
+ avoiding possible collisions.
164
+ The request coming from a plan should always be one of accepted request values:
165
+ ('20um', '40um', '100um', '3000um', 'OUT').
166
+ """
167
+ if value is PinColRequest.OUT:
168
+ await self._safe_move_out()
169
+ else:
170
+ await self._safe_move_in(_PinColPosition(value))
@@ -1,12 +1,18 @@
1
1
  import time
2
+ from pathlib import PurePath
2
3
 
3
4
  import bluesky.plan_stubs as bps
4
5
  from bluesky import preprocessors as bpp
5
6
  from bluesky.run_engine import RunEngine
6
- from ophyd_async.core import DetectorTrigger, TriggerInfo
7
+ from ophyd_async.core import (
8
+ DetectorTrigger,
9
+ StaticFilenameProvider,
10
+ StaticPathProvider,
11
+ TriggerInfo,
12
+ )
7
13
  from ophyd_async.fastcs.eiger import EigerDetector
8
14
 
9
- from dodal.beamlines.i03 import fastcs_eiger
15
+ from dodal.beamlines.i03 import fastcs_eiger, set_path_provider
10
16
  from dodal.devices.detector import DetectorParams
11
17
  from dodal.log import LOGGER, do_default_logging_setup
12
18
 
@@ -28,9 +34,17 @@ def configure_arm_trigger_and_disarm_detector(
28
34
  yield from change_roi_mode(eiger, detector_params, wait=True)
29
35
  LOGGER.info(f"Changing ROI Mode: {time.time() - start}s")
30
36
  start = time.time()
31
- yield from bps.abs_set(eiger.odin.num_frames_chunks, 1)
37
+ yield from bps.abs_set(eiger.odin.num_frames_chunks, 1, wait=True)
32
38
  LOGGER.info(f"Setting # of Frame Chunks: {time.time() - start}s")
33
39
  start = time.time()
40
+ yield from bps.abs_set(
41
+ eiger.drv.detector.photon_energy, detector_params.expected_energy_ev, wait=True
42
+ )
43
+ LOGGER.info(f"Setting Photon Energy: {time.time() - start}s")
44
+ start = time.time()
45
+ yield from bps.abs_set(eiger.drv.detector.ntrigger, 1, wait=True)
46
+ LOGGER.info(f"Setting Number of Triggers: {time.time() - start}s")
47
+ start = time.time()
34
48
  yield from set_mx_settings_pvs(eiger, detector_params, wait=True)
35
49
  LOGGER.info(f"Setting MX PVs: {time.time() - start}s")
36
50
  start = time.time()
@@ -40,7 +54,7 @@ def configure_arm_trigger_and_disarm_detector(
40
54
  yield from bps.kickoff(eiger, wait=True)
41
55
  LOGGER.info(f"Kickoff Eiger: {time.time() - start}s")
42
56
  start = time.time()
43
- yield from bps.trigger(eiger.drv.detector.trigger) # type: ignore
57
+ yield from bps.trigger(eiger.drv.detector.trigger, wait=True)
44
58
  LOGGER.info(f"Triggering Eiger: {time.time() - start}s")
45
59
  start = time.time()
46
60
  yield from bps.complete(eiger, wait=True)
@@ -82,7 +96,7 @@ def change_roi_mode(
82
96
 
83
97
  yield from bps.abs_set(
84
98
  eiger.drv.detector.roi_mode,
85
- 1 if detector_params.use_roi_mode else 0,
99
+ "4M" if detector_params.use_roi_mode else "disabled",
86
100
  group=group,
87
101
  )
88
102
  yield from bps.abs_set(
@@ -143,6 +157,14 @@ def set_mx_settings_pvs(
143
157
  if __name__ == "__main__":
144
158
  RE = RunEngine()
145
159
  do_default_logging_setup()
160
+
161
+ path_provider = StaticPathProvider(
162
+ StaticFilenameProvider("eiger_test_file12.h5"),
163
+ PurePath("/dls/i03/data/2025/cm40607-2/test_new_eiger/"),
164
+ )
165
+
166
+ set_path_provider(path_provider)
167
+
146
168
  eiger = fastcs_eiger(connect_immediately=True)
147
169
  RE(
148
170
  configure_arm_trigger_and_disarm_detector(