dls-dodal 1.29.4__py3-none-any.whl → 1.31.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 (103) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +29 -44
  2. dls_dodal-1.31.0.dist-info/RECORD +134 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.31.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamline_specific_utils/i03.py +1 -4
  8. dodal/beamlines/__init__.py +7 -1
  9. dodal/beamlines/i03.py +34 -29
  10. dodal/beamlines/i04.py +39 -16
  11. dodal/beamlines/i13_1.py +66 -0
  12. dodal/beamlines/i22.py +22 -22
  13. dodal/beamlines/i24.py +1 -1
  14. dodal/beamlines/p38.py +21 -21
  15. dodal/beamlines/p45.py +18 -16
  16. dodal/beamlines/p99.py +61 -0
  17. dodal/beamlines/training_rig.py +64 -0
  18. dodal/cli.py +6 -3
  19. dodal/common/beamlines/beamline_parameters.py +7 -6
  20. dodal/common/beamlines/beamline_utils.py +15 -14
  21. dodal/common/maths.py +1 -3
  22. dodal/common/types.py +6 -5
  23. dodal/common/udc_directory_provider.py +39 -21
  24. dodal/common/visit.py +60 -62
  25. dodal/devices/CTAB.py +22 -17
  26. dodal/devices/aperture.py +1 -1
  27. dodal/devices/aperturescatterguard.py +139 -209
  28. dodal/devices/areadetector/adaravis.py +8 -6
  29. dodal/devices/areadetector/adsim.py +2 -3
  30. dodal/devices/areadetector/adutils.py +20 -12
  31. dodal/devices/areadetector/plugins/MJPG.py +2 -1
  32. dodal/devices/backlight.py +12 -1
  33. dodal/devices/cryostream.py +19 -7
  34. dodal/devices/dcm.py +1 -1
  35. dodal/devices/detector/__init__.py +13 -2
  36. dodal/devices/detector/det_dim_constants.py +2 -2
  37. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  38. dodal/devices/detector/detector.py +33 -32
  39. dodal/devices/detector/detector_motion.py +38 -31
  40. dodal/devices/eiger.py +11 -15
  41. dodal/devices/eiger_odin.py +9 -10
  42. dodal/devices/fast_grid_scan.py +18 -27
  43. dodal/devices/fluorescence_detector_motion.py +13 -4
  44. dodal/devices/focusing_mirror.py +6 -6
  45. dodal/devices/hutch_shutter.py +4 -4
  46. dodal/devices/i22/dcm.py +5 -4
  47. dodal/devices/i22/fswitch.py +10 -6
  48. dodal/devices/i22/nxsas.py +55 -43
  49. dodal/devices/i24/aperture.py +1 -1
  50. dodal/devices/i24/beamstop.py +1 -1
  51. dodal/devices/i24/dcm.py +1 -1
  52. dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +1 -1
  53. dodal/devices/i24/pmac.py +67 -12
  54. dodal/devices/ipin.py +7 -4
  55. dodal/devices/linkam3.py +12 -6
  56. dodal/devices/logging_ophyd_device.py +1 -1
  57. dodal/devices/motors.py +32 -6
  58. dodal/devices/oav/grid_overlay.py +1 -0
  59. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  60. dodal/devices/oav/oav_detector.py +2 -1
  61. dodal/devices/oav/oav_parameters.py +18 -10
  62. dodal/devices/oav/oav_to_redis_forwarder.py +129 -0
  63. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  64. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  65. dodal/devices/oav/utils.py +2 -2
  66. dodal/devices/p99/__init__.py +0 -0
  67. dodal/devices/p99/sample_stage.py +43 -0
  68. dodal/devices/robot.py +31 -20
  69. dodal/devices/scatterguard.py +1 -1
  70. dodal/devices/scintillator.py +8 -5
  71. dodal/devices/slits.py +1 -1
  72. dodal/devices/smargon.py +4 -4
  73. dodal/devices/status.py +2 -31
  74. dodal/devices/tetramm.py +23 -19
  75. dodal/devices/thawer.py +5 -3
  76. dodal/devices/training_rig/__init__.py +0 -0
  77. dodal/devices/training_rig/sample_stage.py +10 -0
  78. dodal/devices/turbo_slit.py +1 -1
  79. dodal/devices/undulator.py +1 -1
  80. dodal/devices/undulator_dcm.py +6 -8
  81. dodal/devices/util/adjuster_plans.py +3 -3
  82. dodal/devices/util/epics_util.py +5 -7
  83. dodal/devices/util/lookup_tables.py +2 -3
  84. dodal/devices/util/save_panda.py +87 -0
  85. dodal/devices/util/test_utils.py +17 -0
  86. dodal/devices/webcam.py +3 -3
  87. dodal/devices/xbpm_feedback.py +1 -25
  88. dodal/devices/xspress3/xspress3.py +1 -1
  89. dodal/devices/zebra.py +15 -10
  90. dodal/devices/zebra_controlled_shutter.py +26 -11
  91. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  92. dodal/devices/zocalo/zocalo_results.py +36 -19
  93. dodal/log.py +46 -15
  94. dodal/plans/check_topup.py +65 -10
  95. dodal/plans/data_session_metadata.py +8 -9
  96. dodal/plans/motor_util_plans.py +117 -0
  97. dodal/utils.py +65 -22
  98. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  99. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  100. dodal/devices/beamstop.py +0 -8
  101. dodal/devices/qbpm1.py +0 -8
  102. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
  103. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
@@ -47,7 +47,7 @@ class DetectorDistanceToBeamXYConverter:
47
47
 
48
48
  def parse_table(self) -> list:
49
49
  rows = loadtxt(self.lookup_file, delimiter=" ", comments=["#", "Units"])
50
- columns = list(zip(*rows))
50
+ columns = list(zip(*rows, strict=False))
51
51
 
52
52
  return columns
53
53
 
@@ -1,7 +1,7 @@
1
1
  from enum import Enum, auto
2
- from typing import Any, Tuple
2
+ from pathlib import Path
3
3
 
4
- from pydantic import BaseModel, root_validator, validator
4
+ from pydantic import BaseModel, Field, field_serializer, field_validator
5
5
 
6
6
  from dodal.devices.detector.det_dim_constants import (
7
7
  EIGER2_X_16M_SIZE,
@@ -28,9 +28,12 @@ class DetectorParams(BaseModel):
28
28
  """Holds parameters for the detector. Provides access to a list of Dectris detector
29
29
  sizes and a converter for distance to beam centre."""
30
30
 
31
+ # https://github.com/pydantic/pydantic/issues/8379
32
+ # Must use model_dump(by_alias=True) if serialising!
33
+
31
34
  expected_energy_ev: float | None = None
32
35
  exposure_time: float
33
- directory: str
36
+ directory: str # : Path https://github.com/DiamondLightSource/dodal/issues/774
34
37
  prefix: str
35
38
  detector_distance: float
36
39
  omega_start: float
@@ -39,48 +42,46 @@ class DetectorParams(BaseModel):
39
42
  num_triggers: int
40
43
  use_roi_mode: bool
41
44
  det_dist_to_beam_converter_path: str
45
+ override_run_number: int | None = Field(default=None, alias="run_number")
42
46
  trigger_mode: TriggerMode = TriggerMode.SET_FRAMES
43
47
  detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE
44
- beam_xy_converter: DetectorDistanceToBeamXYConverter
45
- run_number: int
46
48
  enable_dev_shm: bool = (
47
49
  False # Remove in https://github.com/DiamondLightSource/hyperion/issues/1395
48
50
  )
49
51
 
50
- class Config:
51
- arbitrary_types_allowed = True
52
- json_encoders = {
53
- DetectorDistanceToBeamXYConverter: lambda _: None,
54
- DetectorSizeConstants: lambda d: d.det_type_string,
55
- }
56
-
57
- # should be replaced with model_validator once move to pydantic 2 is complete
58
- @root_validator(pre=True)
59
- def create_beamxy_and_runnumber(cls, values: dict[str, Any]) -> dict[str, Any]:
60
- values["beam_xy_converter"] = DetectorDistanceToBeamXYConverter(
61
- values["det_dist_to_beam_converter_path"]
52
+ @property
53
+ def beam_xy_converter(self) -> DetectorDistanceToBeamXYConverter:
54
+ return DetectorDistanceToBeamXYConverter(self.det_dist_to_beam_converter_path)
55
+
56
+ @property
57
+ def run_number(self) -> int:
58
+ return (
59
+ get_run_number(self.directory, self.prefix)
60
+ if self.override_run_number is None
61
+ else self.override_run_number
62
62
  )
63
- if values.get("run_number") is None:
64
- values["run_number"] = get_run_number(values["directory"], values["prefix"])
65
- return values
66
-
67
- @validator("detector_size_constants", pre=True)
68
- def _parse_detector_size_constants(
69
- cls, det_type: str, values: dict[str, Any]
70
- ) -> DetectorSizeConstants:
63
+
64
+ @field_serializer("detector_size_constants")
65
+ def serialize_detector_size_constants(self, size: DetectorSizeConstants):
66
+ return size.det_type_string
67
+
68
+ @field_validator("detector_size_constants", mode="before")
69
+ @classmethod
70
+ def _parse_detector_size_constants(cls, det_type: str) -> DetectorSizeConstants:
71
71
  return (
72
72
  det_type
73
73
  if isinstance(det_type, DetectorSizeConstants)
74
74
  else constants_from_type(det_type)
75
75
  )
76
76
 
77
- @validator("directory", pre=True)
78
- def _parse_directory(cls, directory: str, values: dict[str, Any]) -> str:
79
- if not directory.endswith("/"):
80
- directory += "/"
81
- return directory
77
+ @field_validator("directory", mode="before")
78
+ @classmethod
79
+ def _parse_directory(cls, directory: str | Path) -> str:
80
+ path = Path(directory)
81
+ assert path.is_dir()
82
+ return str(path)
82
83
 
83
- def get_beam_position_mm(self, detector_distance: float) -> Tuple[float, float]:
84
+ def get_beam_position_mm(self, detector_distance: float) -> tuple[float, float]:
84
85
  x_beam_mm = self.beam_xy_converter.get_beam_xy_from_det_dist(
85
86
  detector_distance, Axis.X_AXIS
86
87
  )
@@ -105,7 +106,7 @@ class DetectorParams(BaseModel):
105
106
  roi_size = self.detector_size_constants.roi_size_pixels
106
107
  return roi_size if self.use_roi_mode else full_size
107
108
 
108
- def get_beam_position_pixels(self, detector_distance: float) -> Tuple[float, float]:
109
+ def get_beam_position_pixels(self, detector_distance: float) -> tuple[float, float]:
109
110
  full_size_pixels = self.detector_size_constants.det_size_pixels
110
111
  roi_size_pixels = self.get_detector_size_pizels()
111
112
 
@@ -1,37 +1,44 @@
1
- from enum import IntEnum
1
+ from enum import Enum
2
2
 
3
- from ophyd import Component as Cpt
4
- from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO
3
+ from ophyd_async.core import Device
4
+ from ophyd_async.epics.motor import Motor
5
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
5
6
 
6
7
 
7
- class ShutterState(IntEnum):
8
- CLOSED = 0
9
- OPEN = 1
8
+ class ShutterState(str, Enum):
9
+ CLOSED = "Closed"
10
+ OPEN = "Open"
10
11
 
11
12
 
12
13
  class DetectorMotion(Device):
13
- """Physical motion and interlocks for detector travel"""
14
-
15
- upstream_x = Cpt(EpicsMotor, "-MO-DET-01:UPSTREAMX")
16
- downstream_x = Cpt(EpicsMotor, "-MO-DET-01:DOWNSTREAMX")
17
- x = Cpt(EpicsMotor, "-MO-DET-01:X")
18
- y = Cpt(EpicsMotor, "-MO-DET-01:Y")
19
- z = Cpt(EpicsMotor, "-MO-DET-01:Z")
20
- yaw = Cpt(EpicsMotor, "-MO-DET-01:YAW")
21
- shutter = Cpt(EpicsSignal, "-MO-DET-01:SET_SHUTTER_STATE") # 0=closed, 1=open
22
- # monitors
23
- shutter_closed_lim = Cpt(
24
- EpicsSignalRO, "-MO-DET-01:CLOSE_LIMIT"
25
- ) # on limit = 1, off = 0
26
- shutter_open_lim = Cpt(
27
- EpicsSignalRO, "-MO-DET-01:OPEN_LIMIT"
28
- ) # on limit = 1, off = 0
29
- z_disabled = Cpt(
30
- EpicsSignalRO, "-MO-DET-01:Z:DISABLED"
31
- ) # robot interlock, 0=ok to move, 1=blocked
32
- crate_power = Cpt(
33
- EpicsSignalRO, "-MO-PMAC-02:CRATE2_HEALTHY"
34
- ) # returns 0 if no power
35
- in_robot_load_safe_position = Cpt(
36
- EpicsSignalRO, "-MO-PMAC-02:GPIO_INP_BITS.B2"
37
- ) # returns 1 if safe
14
+ def __init__(self, prefix: str, name: str = ""):
15
+ device_prefix = "-MO-DET-01:"
16
+ pmac_prefix = "-MO-PMAC-02:"
17
+
18
+ self.upstream_x = Motor(f"{prefix}{device_prefix}UPSTREAMX")
19
+ self.downstream_x = Motor(f"{prefix}{device_prefix}DOWNSTREAMX")
20
+ self.x = Motor(f"{prefix}{device_prefix}X")
21
+ self.y = Motor(f"{prefix}{device_prefix}Y")
22
+ self.z = Motor(f"{prefix}{device_prefix}Z")
23
+ self.yaw = Motor(f"{prefix}{device_prefix}YAW")
24
+
25
+ self.shutter = epics_signal_rw(
26
+ ShutterState, f"{prefix}{device_prefix}SET_SHUTTER_STATE"
27
+ )
28
+ self.shutter_closed_lim = epics_signal_r(
29
+ float, f"{prefix}{device_prefix}CLOSE_LIMIT"
30
+ ) # on limit = 1, off = 0
31
+ self.shutter_open_lim = epics_signal_r(
32
+ float, f"{prefix}{device_prefix}OPEN_LIMIT"
33
+ ) # on limit = 1, off = 0
34
+ self.z_disabled = epics_signal_r(
35
+ float, f"{prefix}{device_prefix}Z:DISABLED"
36
+ ) # robot interlock, 0=ok to move, 1=blocked
37
+ self.crate_power = epics_signal_r(
38
+ float, f"{prefix}{pmac_prefix}CRATE2_HEALTHY"
39
+ ) # returns 0 if no power
40
+ self.in_robot_load_safe_position = epics_signal_r(
41
+ int, f"{prefix}{pmac_prefix}GPIO_INP_BITS.B2"
42
+ ) # returns 1 if safe
43
+
44
+ super().__init__(name)
dodal/devices/eiger.py CHANGED
@@ -1,3 +1,4 @@
1
+ # type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
1
2
  from enum import Enum
2
3
 
3
4
  from ophyd import Component, Device, EpicsSignalRO, Signal
@@ -12,10 +13,6 @@ from dodal.log import LOGGER
12
13
 
13
14
  FREE_RUN_MAX_IMAGES = 1000000
14
15
 
15
- # TODO present for testing purposes, remove
16
- TEST_1169_FIX = True
17
- TEST_1169_INJECT = False
18
-
19
16
 
20
17
  class InternalEigerTriggerMode(Enum):
21
18
  INTERNAL_SERIES = 0
@@ -56,17 +53,15 @@ class EigerDetector(Device):
56
53
  cls,
57
54
  params: DetectorParams,
58
55
  name: str = "EigerDetector",
59
- *args,
60
- **kwargs,
61
56
  ):
62
- det = cls(name=name, *args, **kwargs)
57
+ det = cls(name=name)
63
58
  det.set_detector_parameters(params)
64
59
  return det
65
60
 
66
61
  def set_detector_parameters(self, detector_params: DetectorParams):
67
62
  self.detector_params = detector_params
68
63
  if self.detector_params is None:
69
- raise Exception("Parameters for scan must be specified")
64
+ raise ValueError("Parameters for scan must be specified")
70
65
 
71
66
  to_check = [
72
67
  (
@@ -103,7 +98,7 @@ class EigerDetector(Device):
103
98
 
104
99
  def stage(self):
105
100
  self.wait_on_arming_if_started()
106
- if TEST_1169_INJECT or not self.is_armed():
101
+ if not self.is_armed():
107
102
  LOGGER.info("Eiger not armed, arming")
108
103
 
109
104
  self.async_stage().wait(timeout=self.ARMING_TIMEOUT)
@@ -318,7 +313,9 @@ class EigerDetector(Device):
318
313
 
319
314
  def _finish_arm(self) -> Status:
320
315
  LOGGER.info("Eiger staging: Finishing arming")
321
- return Status(done=True, success=True)
316
+ status = Status()
317
+ status.set_finished()
318
+ return status
322
319
 
323
320
  def forward_bit_depth_to_filewriter(self):
324
321
  bit_depth = self.bit_depth.get()
@@ -341,6 +338,10 @@ class EigerDetector(Device):
341
338
  functions_to_do_arm.append(self.enable_roi_mode)
342
339
 
343
340
  arming_sequence_funcs = [
341
+ # If a beam dump occurs after arming the eiger but prior to eiger staging,
342
+ # the odin may timeout which will cause the arming sequence to be retried;
343
+ # if this previously completed successfully we must reset the odin first
344
+ self.odin.stop,
344
345
  lambda: self.change_dev_shm(detector_params.enable_dev_shm),
345
346
  lambda: self.set_detector_threshold(detector_params.expected_energy_ev),
346
347
  self.set_cam_pvs,
@@ -354,11 +355,6 @@ class EigerDetector(Device):
354
355
  self._wait_fan_ready,
355
356
  self._finish_arm,
356
357
  ]
357
- if TEST_1169_FIX:
358
- # If a beam dump occurs after arming the eiger but prior to eiger staging,
359
- # the odin may timeout which will cause the arming sequence to be retried;
360
- # if this previously completed successfully we must reset the odin first
361
- arming_sequence_funcs.insert(0, self.odin.stop)
362
358
 
363
359
  functions_to_do_arm.extend(arming_sequence_funcs)
364
360
 
@@ -1,5 +1,4 @@
1
- from typing import List, Tuple
2
-
1
+ # type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
3
2
  from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
4
3
  from ophyd.areadetector.plugins import HDF5Plugin_V22
5
4
  from ophyd.sim import NullStatus
@@ -59,12 +58,12 @@ class OdinNodesStatus(Device):
59
58
  node_3 = Component(OdinNode, "OD4:")
60
59
 
61
60
  @property
62
- def nodes(self) -> List[OdinNode]:
61
+ def nodes(self) -> list[OdinNode]:
63
62
  return [self.node_0, self.node_1, self.node_2, self.node_3]
64
63
 
65
64
  def check_node_frames_from_attr(
66
65
  self, node_get_func, error_message_verb: str
67
- ) -> Tuple[bool, str]:
66
+ ) -> tuple[bool, str]:
68
67
  nodes_frames_values = [0] * len(self.nodes)
69
68
  frames_details = []
70
69
  for node_number, node_pv in enumerate(self.nodes):
@@ -75,17 +74,17 @@ class OdinNodesStatus(Device):
75
74
  bad_frames = any(v != 0 for v in nodes_frames_values)
76
75
  return bad_frames, "\n".join(frames_details)
77
76
 
78
- def check_frames_timed_out(self) -> Tuple[bool, str]:
77
+ def check_frames_timed_out(self) -> tuple[bool, str]:
79
78
  return self.check_node_frames_from_attr(
80
79
  lambda node: node.frames_timed_out.get(), "timed out"
81
80
  )
82
81
 
83
- def check_frames_dropped(self) -> Tuple[bool, str]:
82
+ def check_frames_dropped(self) -> tuple[bool, str]:
84
83
  return self.check_node_frames_from_attr(
85
84
  lambda node: node.frames_dropped.get(), "dropped"
86
85
  )
87
86
 
88
- def get_error_state(self) -> Tuple[bool, str]:
87
+ def get_error_state(self) -> tuple[bool, str]:
89
88
  is_error = []
90
89
  error_messages = []
91
90
  for node_number, node_pv in enumerate(self.nodes):
@@ -99,7 +98,7 @@ class OdinNodesStatus(Device):
99
98
 
100
99
  def get_init_state(self) -> bool:
101
100
  is_initialised = []
102
- for node_number, node_pv in enumerate(self.nodes):
101
+ for node_pv in self.nodes:
103
102
  is_initialised.append(node_pv.fr_initialised.get())
104
103
  is_initialised.append(node_pv.fp_initialised.get())
105
104
  return all(is_initialised)
@@ -132,7 +131,7 @@ class EigerOdin(Device):
132
131
  frames_timed_out, frames_timed_out_details = self.nodes.check_frames_timed_out()
133
132
 
134
133
  if not is_initialised:
135
- raise Exception(error_message)
134
+ raise RuntimeError(error_message)
136
135
  if frames_dropped:
137
136
  self.log.error(f"Frames dropped: {frames_dropped_details}")
138
137
  if frames_timed_out:
@@ -140,7 +139,7 @@ class EigerOdin(Device):
140
139
 
141
140
  return is_initialised and not frames_dropped and not frames_timed_out
142
141
 
143
- def check_odin_initialised(self) -> Tuple[bool, str]:
142
+ def check_odin_initialised(self) -> tuple[bool, str]:
144
143
  is_error_state, error_messages = self.nodes.get_error_state()
145
144
  to_check = [
146
145
  (not self.fan.consumers_connected.get(), "EigerFan is not connected"),
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Generic, TypeVar
2
+ from typing import Generic, TypeVar
3
3
 
4
4
  import numpy as np
5
5
  from bluesky.plan_stubs import mv
@@ -20,7 +20,7 @@ from ophyd_async.epics.signal import (
20
20
  epics_signal_rw_rbv,
21
21
  epics_signal_x,
22
22
  )
23
- from pydantic import validator
23
+ from pydantic import field_validator
24
24
  from pydantic.dataclasses import dataclass
25
25
 
26
26
  from dodal.log import LOGGER
@@ -69,9 +69,6 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
69
69
  y2_start: float = 0.1
70
70
  z1_start: float = 0.1
71
71
  z2_start: float = 0.1
72
- x_axis: GridAxis = GridAxis(0, 0, 0)
73
- y_axis: GridAxis = GridAxis(0, 0, 0)
74
- z_axis: GridAxis = GridAxis(0, 0, 0)
75
72
 
76
73
  # Whether to set the stub offsets after centering
77
74
  set_stub_offsets: bool = False
@@ -91,28 +88,20 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
91
88
  "z2_start": self.z2_start,
92
89
  }
93
90
 
94
- class Config:
95
- arbitrary_types_allowed = True
96
- fields = {
97
- "x_axis": {"exclude": True},
98
- "y_axis": {"exclude": True},
99
- "z_axis": {"exclude": True},
100
- }
101
-
102
- @validator("x_axis", always=True)
103
- def _get_x_axis(cls, x_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
104
- return GridAxis(values["x_start"], values["x_step_size"], values["x_steps"])
91
+ @property
92
+ def x_axis(self) -> GridAxis:
93
+ return GridAxis(self.x_start, self.x_step_size, self.x_steps)
105
94
 
106
- @validator("y_axis", always=True)
107
- def _get_y_axis(cls, y_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
108
- return GridAxis(values["y1_start"], values["y_step_size"], values["y_steps"])
95
+ @property
96
+ def y_axis(self) -> GridAxis:
97
+ return GridAxis(self.y1_start, self.y_step_size, self.y_steps)
109
98
 
110
- @validator("z_axis", always=True)
111
- def _get_z_axis(cls, z_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
112
- return GridAxis(values["z2_start"], values["z_step_size"], values["z_steps"])
99
+ @property
100
+ def z_axis(self) -> GridAxis:
101
+ return GridAxis(self.z2_start, self.z_step_size, self.z_steps)
113
102
 
114
103
  def get_num_images(self):
115
- return self.x_steps * self.y_steps + self.x_steps * self.z_steps
104
+ return self.x_steps * (self.y_steps + self.z_steps)
116
105
 
117
106
  @property
118
107
  def is_3d_grid_scan(self):
@@ -126,7 +115,7 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
126
115
  :return: The motor position this corresponds to.
127
116
  :raises: IndexError if the desired position is outside the grid."""
128
117
  for position, axis in zip(
129
- grid_position, [self.x_axis, self.y_axis, self.z_axis]
118
+ grid_position, [self.x_axis, self.y_axis, self.z_axis], strict=False
130
119
  ):
131
120
  if not axis.is_within(position):
132
121
  raise IndexError(f"{grid_position} is outside the bounds of the grid")
@@ -155,7 +144,8 @@ class ZebraGridScanParams(GridScanParamsCommon):
155
144
  param_positions["dwell_time_ms"] = self.dwell_time_ms
156
145
  return param_positions
157
146
 
158
- @validator("dwell_time_ms", always=True, check_fields=True)
147
+ @field_validator("dwell_time_ms")
148
+ @classmethod
159
149
  def non_integer_dwell_time(cls, dwell_time_ms: float) -> float:
160
150
  dwell_time_floor_rounded = np.floor(dwell_time_ms)
161
151
  dwell_time_is_close = np.isclose(
@@ -191,9 +181,10 @@ class MotionProgram(Device):
191
181
  class ExpectedImages(SignalR[int]):
192
182
  def __init__(self, parent: "FastGridScanCommon") -> None:
193
183
  super().__init__(SoftSignalBackend(int))
194
- self.parent: "FastGridScanCommon" = parent
184
+ self.parent = parent
195
185
 
196
- async def get_value(self):
186
+ async def get_value(self, cached: bool | None = None):
187
+ assert isinstance(self.parent, FastGridScanCommon)
197
188
  x = await self.parent.x_steps.get_value()
198
189
  y = await self.parent.y_steps.get_value()
199
190
  z = await self.parent.z_steps.get_value()
@@ -1,9 +1,18 @@
1
- from ophyd import Component as Cpt
2
- from ophyd import Device, EpicsSignal
1
+ from enum import Enum
3
2
 
3
+ from ophyd_async.core import StandardReadable
4
+ from ophyd_async.epics.signal import epics_signal_r
4
5
 
5
- class FluorescenceDetector(Device):
6
+
7
+ class FluorescenceDetectorControlState(Enum):
6
8
  OUT = 0
7
9
  IN = 1
8
10
 
9
- pos = Cpt(EpicsSignal, "-EA-FLU-01:CTRL")
11
+
12
+ class FluorescenceDetector(StandardReadable):
13
+ def __init__(self, prefix: str, name: str = ""):
14
+ with self.add_children_as_readables():
15
+ self.pos = epics_signal_r(
16
+ FluorescenceDetectorControlState, prefix + "-EA-FLU-01:CTRL"
17
+ )
18
+ super().__init__(name)
@@ -2,13 +2,15 @@ from enum import Enum
2
2
 
3
3
  from ophyd_async.core import (
4
4
  AsyncStatus,
5
+ ConfigSignal,
5
6
  Device,
6
7
  DeviceVector,
8
+ HintedSignal,
7
9
  StandardReadable,
8
10
  observe_value,
11
+ soft_signal_r_and_setter,
9
12
  )
10
- from ophyd_async.core.signal import soft_signal_r_and_setter
11
- from ophyd_async.epics.motion import Motor
13
+ from ophyd_async.epics.motor import Motor
12
14
  from ophyd_async.epics.signal import (
13
15
  epics_signal_r,
14
16
  epics_signal_rw,
@@ -144,10 +146,8 @@ class FocusingMirror(StandardReadable):
144
146
  # regardless of orientation of the mirror
145
147
  self.incident_angle = Motor(prefix + "PITCH")
146
148
 
147
- self.set_readable_signals(
148
- read=[self.incident_angle.user_readback],
149
- config=[self.type],
150
- )
149
+ self.add_readables([self.incident_angle.user_readback], wrapper=HintedSignal)
150
+ self.add_readables([self.type], wrapper=ConfigSignal)
151
151
  super().__init__(name)
152
152
 
153
153
 
@@ -74,20 +74,20 @@ class HutchShutter(StandardReadable, Movable):
74
74
  super().__init__(name)
75
75
 
76
76
  @AsyncStatus.wrap
77
- async def set(self, position_demand: ShutterDemand):
77
+ async def set(self, value: ShutterDemand):
78
78
  interlock_state = await self.interlock.shutter_safe_to_operate()
79
79
  if not interlock_state:
80
80
  raise ShutterNotSafeToOperateError(
81
81
  "The hutch has not been locked, not operating shutter."
82
82
  )
83
- if position_demand == ShutterDemand.OPEN:
83
+ if value == ShutterDemand.OPEN:
84
84
  await self.control.set(ShutterDemand.RESET, wait=True)
85
- await self.control.set(position_demand, wait=True)
85
+ await self.control.set(value, wait=True)
86
86
  return await wait_for_value(
87
87
  self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
88
88
  )
89
89
  else:
90
- await self.control.set(position_demand, wait=True)
90
+ await self.control.set(value, wait=True)
91
91
  return await wait_for_value(
92
92
  self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
93
93
  )
dodal/devices/i22/dcm.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import time
2
+ from collections.abc import Sequence
2
3
  from dataclasses import dataclass
3
- from typing import Dict, Literal, Sequence
4
+ from typing import Literal
4
5
 
5
6
  from bluesky.protocols import Reading
6
7
  from event_model.documents.event_descriptor import DataKey
7
8
  from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
8
- from ophyd_async.epics.motion import Motor
9
+ from ophyd_async.epics.motor import Motor
9
10
  from ophyd_async.epics.signal import epics_signal_r
10
11
 
11
12
  # Conversion constant for energy and wavelength, taken from the X-Ray data booklet
@@ -127,7 +128,7 @@ class DoubleCrystalMonochromator(StandardReadable):
127
128
 
128
129
  super().__init__(name)
129
130
 
130
- async def describe(self) -> Dict[str, DataKey]:
131
+ async def describe(self) -> dict[str, DataKey]:
131
132
  default_describe = await super().describe()
132
133
  return {
133
134
  f"{self.name}-wavelength": DataKey(
@@ -139,7 +140,7 @@ class DoubleCrystalMonochromator(StandardReadable):
139
140
  **default_describe,
140
141
  }
141
142
 
142
- async def read(self) -> Dict[str, Reading]:
143
+ async def read(self) -> dict[str, Reading]:
143
144
  default_reading = await super().read()
144
145
  energy: float = default_reading[f"{self.name}-energy"]["value"]
145
146
  if energy > 0.0:
@@ -1,11 +1,15 @@
1
1
  import asyncio
2
2
  import time
3
3
  from enum import Enum
4
- from typing import Dict
5
4
 
6
- from bluesky.protocols import DataKey, Reading
7
- from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
8
- from ophyd_async.core.device import DeviceVector
5
+ from bluesky.protocols import Reading
6
+ from event_model import DataKey
7
+ from ophyd_async.core import (
8
+ ConfigSignal,
9
+ DeviceVector,
10
+ StandardReadable,
11
+ soft_signal_r_and_setter,
12
+ )
9
13
  from ophyd_async.epics.signal import epics_signal_r
10
14
 
11
15
 
@@ -74,7 +78,7 @@ class FSwitch(StandardReadable):
74
78
 
75
79
  super().__init__(name)
76
80
 
77
- async def describe(self) -> Dict[str, DataKey]:
81
+ async def describe(self) -> dict[str, DataKey]:
78
82
  default_describe = await super().describe()
79
83
  return {
80
84
  FSwitch.NUM_LENSES_FIELD_NAME: DataKey(
@@ -83,7 +87,7 @@ class FSwitch(StandardReadable):
83
87
  **default_describe,
84
88
  }
85
89
 
86
- async def read(self) -> Dict[str, Reading]:
90
+ async def read(self) -> dict[str, Reading]:
87
91
  result = await asyncio.gather(
88
92
  *(filter.get_value() for filter in self.filters.values())
89
93
  )