dls-dodal 1.45.0__py3-none-any.whl → 1.47.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 (81) hide show
  1. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/RECORD +76 -64
  3. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +0 -1
  6. dodal/beamlines/b07.py +2 -6
  7. dodal/beamlines/b07_1.py +1 -3
  8. dodal/beamlines/i03.py +16 -19
  9. dodal/beamlines/i04.py +49 -17
  10. dodal/beamlines/i09.py +1 -3
  11. dodal/beamlines/i09_1.py +1 -3
  12. dodal/beamlines/i18.py +7 -4
  13. dodal/beamlines/i22.py +3 -3
  14. dodal/beamlines/i23.py +75 -4
  15. dodal/beamlines/p38.py +4 -4
  16. dodal/beamlines/p60.py +2 -6
  17. dodal/beamlines/p99.py +48 -4
  18. dodal/common/beamlines/beamline_parameters.py +1 -2
  19. dodal/common/beamlines/beamline_utils.py +5 -0
  20. dodal/common/data_util.py +4 -0
  21. dodal/devices/aperturescatterguard.py +47 -47
  22. dodal/devices/common_dcm.py +77 -0
  23. dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
  24. dodal/devices/diamond_filter.py +5 -17
  25. dodal/devices/eiger.py +1 -1
  26. dodal/devices/electron_analyser/__init__.py +8 -0
  27. dodal/devices/electron_analyser/abstract/__init__.py +28 -0
  28. dodal/devices/electron_analyser/abstract/base_detector.py +210 -0
  29. dodal/devices/electron_analyser/abstract/base_driver_io.py +121 -0
  30. dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +2 -9
  31. dodal/devices/electron_analyser/specs/__init__.py +11 -0
  32. dodal/devices/electron_analyser/specs/detector.py +29 -0
  33. dodal/devices/electron_analyser/specs/driver_io.py +64 -0
  34. dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
  35. dodal/devices/electron_analyser/types.py +6 -0
  36. dodal/devices/electron_analyser/util.py +13 -0
  37. dodal/devices/electron_analyser/vgscienta/__init__.py +12 -0
  38. dodal/devices/electron_analyser/vgscienta/detector.py +36 -0
  39. dodal/devices/electron_analyser/vgscienta/driver_io.py +39 -0
  40. dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -1
  41. dodal/devices/fast_grid_scan.py +7 -9
  42. dodal/devices/i03/__init__.py +3 -0
  43. dodal/devices/{dcm.py → i03/dcm.py} +8 -12
  44. dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
  45. dodal/devices/i04/__init__.py +3 -0
  46. dodal/devices/i04/constants.py +9 -0
  47. dodal/devices/i04/murko_results.py +195 -0
  48. dodal/devices/i10/diagnostics.py +9 -61
  49. dodal/devices/i13_1/merlin.py +3 -4
  50. dodal/devices/i13_1/merlin_controller.py +1 -1
  51. dodal/devices/i22/dcm.py +10 -12
  52. dodal/devices/i24/dcm.py +8 -17
  53. dodal/devices/i24/focus_mirrors.py +9 -13
  54. dodal/devices/i24/pilatus_metadata.py +9 -9
  55. dodal/devices/i24/pmac.py +19 -14
  56. dodal/devices/{i03 → mx_phase1}/beamstop.py +6 -12
  57. dodal/devices/oav/oav_calculations.py +2 -2
  58. dodal/devices/oav/oav_detector.py +32 -22
  59. dodal/devices/oav/utils.py +2 -2
  60. dodal/devices/p99/andor2_point.py +41 -0
  61. dodal/devices/positioner.py +49 -0
  62. dodal/devices/tetramm.py +8 -6
  63. dodal/devices/turbo_slit.py +2 -2
  64. dodal/devices/util/adjuster_plans.py +1 -1
  65. dodal/devices/zebra/zebra.py +4 -0
  66. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  67. dodal/devices/zocalo/__init__.py +0 -3
  68. dodal/devices/zocalo/zocalo_results.py +6 -32
  69. dodal/log.py +14 -14
  70. dodal/plan_stubs/data_session.py +10 -1
  71. dodal/plan_stubs/electron_analyser/__init__.py +3 -0
  72. dodal/plan_stubs/electron_analyser/{configure_controller.py → configure_driver.py} +30 -18
  73. dodal/plans/verify_undulator_gap.py +2 -2
  74. dodal/common/signal_utils.py +0 -88
  75. dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
  76. dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
  77. dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
  78. dodal/devices/logging_ophyd_device.py +0 -17
  79. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/entry_points.txt +0 -0
  80. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/licenses/LICENSE +0 -0
  81. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i09_1.py CHANGED
@@ -2,9 +2,7 @@ from dodal.common.beamlines.beamline_utils import (
2
2
  device_factory,
3
3
  )
4
4
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5
- from dodal.devices.electron_analyser.specs_analyser_io import (
6
- SpecsAnalyserDriverIO,
7
- )
5
+ from dodal.devices.electron_analyser.specs import SpecsAnalyserDriverIO
8
6
  from dodal.devices.synchrotron import Synchrotron
9
7
  from dodal.log import set_beamline as set_log_beamline
10
8
  from dodal.utils import BeamlinePrefix, get_beamline_name
dodal/beamlines/i18.py CHANGED
@@ -12,7 +12,7 @@ from dodal.common.visit import (
12
12
  LocalDirectoryServiceClient,
13
13
  StaticVisitPathProvider,
14
14
  )
15
- from dodal.devices.dcm import DCM
15
+ from dodal.devices.common_dcm import BaseDCM, PitchAndRollCrystal, RollCrystal
16
16
  from dodal.devices.i18.diode import Diode
17
17
  from dodal.devices.i18.KBMirror import KBMirror
18
18
  from dodal.devices.i18.table import Table
@@ -54,12 +54,15 @@ def undulator() -> Undulator:
54
54
  return Undulator(f"{PREFIX.insertion_prefix}-MO-SERVC-01:")
55
55
 
56
56
 
57
- @device_factory()
58
- def dcm() -> DCM:
57
+ # See https://github.com/DiamondLightSource/dodal/issues/1180
58
+ @device_factory(skip=True)
59
+ def dcm() -> BaseDCM[RollCrystal, PitchAndRollCrystal]:
59
60
  # once spacing is added Si111 d-spacing is 3.135 angsterm , and Si311 is 1.637
60
61
  # calculations are in gda/config/lookupTables/Si111/eV_Deg_converter.xml
61
- return DCM(
62
+ return BaseDCM(
62
63
  prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
64
+ xtal_1=RollCrystal,
65
+ xtal_2=PitchAndRollCrystal,
63
66
  )
64
67
 
65
68
 
dodal/beamlines/i22.py CHANGED
@@ -18,7 +18,7 @@ from dodal.common.crystal_metadata import (
18
18
  from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
19
19
  from dodal.devices.bimorph_mirror import BimorphMirror
20
20
  from dodal.devices.focusing_mirror import FocusingMirror
21
- from dodal.devices.i22.dcm import DoubleCrystalMonochromator
21
+ from dodal.devices.i22.dcm import DCM
22
22
  from dodal.devices.i22.fswitch import FSwitch
23
23
  from dodal.devices.i22.nxsas import NXSasMetadataHolder, NXSasOAV, NXSasPilatus
24
24
  from dodal.devices.linkam3 import Linkam3
@@ -141,8 +141,8 @@ def bimorph_vfm() -> BimorphMirror:
141
141
 
142
142
 
143
143
  @device_factory()
144
- def dcm() -> DoubleCrystalMonochromator:
145
- return DoubleCrystalMonochromator(
144
+ def dcm() -> DCM:
145
+ return DCM(
146
146
  prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
147
147
  temperature_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:",
148
148
  crystal_1_metadata=make_crystal_metadata_from_material(
dodal/beamlines/i23.py CHANGED
@@ -1,7 +1,26 @@
1
- from dodal.common.beamlines.beamline_utils import device_factory
1
+ from pathlib import Path
2
+
3
+ from ophyd_async.core import StrictEnum
4
+ from ophyd_async.epics.adpilatus import PilatusDetector
5
+
6
+ from dodal.common.beamlines.beamline_utils import (
7
+ device_factory,
8
+ get_path_provider,
9
+ set_path_provider,
10
+ )
2
11
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
12
+ from dodal.common.beamlines.device_helpers import HDF5_SUFFIX
13
+ from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
3
14
  from dodal.devices.motors import SixAxisGonio
4
15
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
16
+ from dodal.devices.positioner import Positioner1D
17
+ from dodal.devices.zebra.zebra import Zebra
18
+ from dodal.devices.zebra.zebra_constants_mapping import (
19
+ ZebraMapping,
20
+ ZebraSources,
21
+ ZebraTTLOutputs,
22
+ )
23
+ from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
5
24
  from dodal.log import set_beamline as set_log_beamline
6
25
  from dodal.utils import BeamlinePrefix, get_beamline_name, get_hostname
7
26
 
@@ -9,8 +28,27 @@ BL = get_beamline_name("i23")
9
28
  set_log_beamline(BL)
10
29
  set_utils_beamline(BL)
11
30
 
31
+ set_path_provider(
32
+ StaticVisitPathProvider(
33
+ BL,
34
+ Path("/tmp"),
35
+ client=LocalDirectoryServiceClient(),
36
+ )
37
+ )
38
+
12
39
  PREFIX = BeamlinePrefix(BL)
13
40
 
41
+ I23_ZEBRA_MAPPING = ZebraMapping(
42
+ outputs=ZebraTTLOutputs(TTL_DETECTOR=1, TTL_SHUTTER=4),
43
+ sources=ZebraSources(),
44
+ )
45
+
46
+
47
+ class I23DetectorPositions(StrictEnum):
48
+ IN = "In"
49
+ OUT = "Out"
50
+ SAMPLE_CHANGE = "sample change"
51
+
14
52
 
15
53
  def _is_i23_machine():
16
54
  """
@@ -23,7 +61,7 @@ def _is_i23_machine():
23
61
 
24
62
  @device_factory(skip=lambda: not _is_i23_machine())
25
63
  def oav_pin_tip_detection() -> PinTipDetection:
26
- """Get the i23 OAV pin-tip detection device"""
64
+ """Get the i23 OAV pin-tip detection device."""
27
65
 
28
66
  return PinTipDetection(
29
67
  f"{PREFIX.beamline_prefix}-DI-OAV-01:",
@@ -31,10 +69,43 @@ def oav_pin_tip_detection() -> PinTipDetection:
31
69
  )
32
70
 
33
71
 
72
+ @device_factory()
73
+ def shutter() -> ZebraShutter:
74
+ """Get the i23 zebra controlled shutter."""
75
+ return ZebraShutter(f"{PREFIX.beamline_prefix}-EA-SHTR-01:", "shutter")
76
+
77
+
34
78
  @device_factory()
35
79
  def gonio() -> SixAxisGonio:
36
80
  """Get the i23 goniometer"""
81
+ return SixAxisGonio(f"{PREFIX.beamline_prefix}-MO-GONIO-01:")
37
82
 
38
- return SixAxisGonio(
39
- f"{PREFIX.beamline_prefix}-MO-GONIO-01:",
83
+
84
+ @device_factory()
85
+ def zebra() -> Zebra:
86
+ """Get the i23 zebra"""
87
+ return Zebra(
88
+ name="zebra",
89
+ prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-01:ZEBRA:",
90
+ mapping=I23_ZEBRA_MAPPING,
91
+ )
92
+
93
+
94
+ @device_factory()
95
+ def pilatus() -> PilatusDetector:
96
+ """Get the i23 pilatus"""
97
+ return PilatusDetector(
98
+ prefix=f"{PREFIX.beamline_prefix}-EA-PILAT-01:",
99
+ path_provider=get_path_provider(),
100
+ drv_suffix="cam1:",
101
+ fileio_suffix=HDF5_SUFFIX,
102
+ )
103
+
104
+
105
+ @device_factory()
106
+ def detector_motion() -> Positioner1D[I23DetectorPositions]:
107
+ """Get the i23 detector"""
108
+ return Positioner1D[I23DetectorPositions](
109
+ f"{PREFIX.beamline_prefix}-EA-DET-01:Z",
110
+ datatype=I23DetectorPositions,
40
111
  )
dodal/beamlines/p38.py CHANGED
@@ -16,7 +16,7 @@ from dodal.common.crystal_metadata import (
16
16
  )
17
17
  from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
18
18
  from dodal.devices.focusing_mirror import FocusingMirror
19
- from dodal.devices.i22.dcm import DoubleCrystalMonochromator
19
+ from dodal.devices.i22.dcm import DCM
20
20
  from dodal.devices.i22.fswitch import FSwitch
21
21
  from dodal.devices.linkam3 import Linkam3
22
22
  from dodal.devices.pressure_jump_cell import PressureJumpCell
@@ -40,7 +40,7 @@ set_utils_beamline(BL)
40
40
  set_path_provider(
41
41
  StaticVisitPathProvider(
42
42
  BL,
43
- Path("/dls/p38/data/2024/cm37282-2/bluesky"),
43
+ Path("/dls/p38/data/2025/cm40650-2/bluesky"),
44
44
  client=LocalDirectoryServiceClient(),
45
45
  )
46
46
  )
@@ -143,8 +143,8 @@ def hfm() -> FocusingMirror:
143
143
 
144
144
 
145
145
  @device_factory(mock=True)
146
- def dcm() -> DoubleCrystalMonochromator:
147
- return DoubleCrystalMonochromator(
146
+ def dcm() -> DCM:
147
+ return DCM(
148
148
  temperature_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:",
149
149
  crystal_1_metadata=make_crystal_metadata_from_material(
150
150
  MaterialsEnum.Si, (1, 1, 1)
dodal/beamlines/p60.py CHANGED
@@ -2,9 +2,7 @@ from dodal.common.beamlines.beamline_utils import (
2
2
  device_factory,
3
3
  )
4
4
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5
- from dodal.devices.electron_analyser.vgscienta_analyser_io import (
6
- VGScientaAnalyserDriverIO,
7
- )
5
+ from dodal.devices.electron_analyser.vgscienta import VGScientaAnalyserDriverIO
8
6
  from dodal.log import set_beamline as set_log_beamline
9
7
  from dodal.utils import BeamlinePrefix, get_beamline_name
10
8
 
@@ -16,6 +14,4 @@ set_utils_beamline(BL)
16
14
 
17
15
  @device_factory()
18
16
  def analyser_driver() -> VGScientaAnalyserDriverIO:
19
- return VGScientaAnalyserDriverIO(
20
- name="analyser_driver", prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:"
21
- )
17
+ return VGScientaAnalyserDriverIO(prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:")
dodal/beamlines/p99.py CHANGED
@@ -1,7 +1,22 @@
1
- from dodal.common.beamlines.beamline_utils import device_factory, set_beamline
1
+ from pathlib import Path
2
+
3
+ from ophyd_async.epics.adandor import Andor2Detector
4
+
5
+ from dodal.common.beamlines.beamline_utils import (
6
+ device_factory,
7
+ get_path_provider,
8
+ set_beamline,
9
+ set_path_provider,
10
+ )
11
+ from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
12
+ from dodal.common.visit import (
13
+ LocalDirectoryServiceClient,
14
+ StaticVisitPathProvider,
15
+ )
2
16
  from dodal.devices.attenuator.filter import FilterMotor
3
17
  from dodal.devices.attenuator.filter_selections import P99FilterSelections
4
18
  from dodal.devices.motors import XYZPositioner
19
+ from dodal.devices.p99.andor2_point import Andor2Point
5
20
  from dodal.devices.p99.sample_stage import SampleAngleStage
6
21
  from dodal.log import set_beamline as set_log_beamline
7
22
  from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -19,9 +34,7 @@ def angle_stage() -> SampleAngleStage:
19
34
 
20
35
  @device_factory()
21
36
  def filter() -> FilterMotor:
22
- return FilterMotor(
23
- f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:SELECT", P99FilterSelections
24
- )
37
+ return FilterMotor(f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:", P99FilterSelections)
25
38
 
26
39
 
27
40
  @device_factory()
@@ -32,3 +45,34 @@ def sample_stage() -> XYZPositioner:
32
45
  @device_factory()
33
46
  def lab_stage() -> XYZPositioner:
34
47
  return XYZPositioner(f"{PREFIX.beamline_prefix}-MO-STAGE-02:LAB:")
48
+
49
+
50
+ set_path_provider(
51
+ StaticVisitPathProvider(
52
+ BL,
53
+ Path("/dls/p99/data/2024/cm37284-2/processing/writenData"),
54
+ client=LocalDirectoryServiceClient(), # RemoteDirectoryServiceClient("http://p99-control:8088/api"),
55
+ )
56
+ )
57
+
58
+
59
+ @device_factory()
60
+ def andor2_det() -> Andor2Detector:
61
+ """Andor model:DU897_BV."""
62
+ return Andor2Detector(
63
+ prefix=f"{PREFIX.beamline_prefix}-EA-DET-03:",
64
+ path_provider=get_path_provider(),
65
+ drv_suffix=CAM_SUFFIX,
66
+ fileio_suffix=HDF5_SUFFIX,
67
+ )
68
+
69
+
70
+ @device_factory()
71
+ def andor2_point() -> Andor2Point:
72
+ """Using the andor2 as if it is a massive point detector, read the meanValue and total after
73
+ a picture is taken."""
74
+ return Andor2Point(
75
+ prefix=f"{PREFIX.beamline_prefix}-EA-DET-03:",
76
+ drv_suffix=CAM_SUFFIX,
77
+ read_uncached={"mean": "STAT:MeanValue_RBV", "total": "STAT:Total_RBV"},
78
+ )
@@ -8,7 +8,6 @@ BEAMLINE_PARAMETER_KEYWORDS = ["FB", "FULL", "deadtime"]
8
8
  BEAMLINE_PARAMETER_PATHS = {
9
9
  "i03": "/dls_sw/i03/software/daq_configuration/domain/beamlineParameters",
10
10
  "i04": "/dls_sw/i04/software/gda_versions/gda_9_34/workspace_git/gda-mx.git/configurations/i04-config/scripts/beamlineParameters",
11
- "s03": "tests/test_data/test_beamline_parameters.txt",
12
11
  }
13
12
 
14
13
 
@@ -92,7 +91,7 @@ def get_beamline_parameters(beamline_param_path: str | None = None):
92
91
  """Loads the beamline parameters from the specified path, or according to the
93
92
  environment variable if none is given"""
94
93
  if not beamline_param_path:
95
- beamline_name = get_beamline_name("s03")
94
+ beamline_name = get_beamline_name("i03")
96
95
  beamline_param_path = BEAMLINE_PARAMETER_PATHS.get(beamline_name)
97
96
  if beamline_param_path is None:
98
97
  raise KeyError(
@@ -162,3 +162,8 @@ def set_path_provider(provider: PathProvider):
162
162
 
163
163
  def get_path_provider() -> PathProvider:
164
164
  return PATH_PROVIDER
165
+
166
+
167
+ def clear_path_provider() -> None:
168
+ global PATH_PROVIDER
169
+ del PATH_PROVIDER
dodal/common/data_util.py CHANGED
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from typing import TypeVar
2
3
 
3
4
  from pydantic import BaseModel
@@ -9,6 +10,9 @@ def load_json_file_to_class(
9
10
  t: type[TBaseModel],
10
11
  file: str,
11
12
  ) -> TBaseModel:
13
+ if not os.path.isfile(file):
14
+ raise FileNotFoundError(f"Cannot find file {file}")
15
+
12
16
  with open(file) as f:
13
17
  json_obj = f.read()
14
18
  cls = t.model_validate_json(json_obj)
@@ -2,17 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
 
5
- from bluesky.protocols import Movable, Preparable
5
+ from bluesky.protocols import Preparable
6
6
  from ophyd_async.core import (
7
7
  AsyncStatus,
8
8
  StandardReadable,
9
9
  StandardReadableFormat,
10
10
  StrictEnum,
11
+ derived_signal_r,
12
+ derived_signal_rw,
11
13
  )
12
14
  from pydantic import BaseModel, Field
13
15
 
14
16
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
15
- from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
16
17
  from dodal.devices.aperture import Aperture
17
18
  from dodal.devices.scatterguard import Scatterguard
18
19
 
@@ -123,21 +124,21 @@ def load_positions_from_beamline_parameters(
123
124
  }
124
125
 
125
126
 
126
- class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable):
127
+ class ApertureScatterguard(StandardReadable, Preparable):
127
128
  """Move the aperture and scatterguard assembly in a safe way. There are two ways to
128
129
  interact with the device depending on if you want simplicity or move flexibility.
129
130
 
130
131
  Examples:
131
132
  The simple interface is using::
132
133
 
133
- await aperture_scatterguard.set(ApertureValue.LARGE)
134
+ await aperture_scatterguard.selected_aperture.set(ApertureValue.LARGE)
134
135
 
135
136
  This will move the assembly so that the large aperture is in the beam, regardless
136
137
  of where the assembly currently is.
137
138
 
138
139
  We may also want to move the assembly out of the beam with::
139
140
 
140
- await aperture_scatterguard.set(ApertureValue.OUT_OF_BEAM)
141
+ await aperture_scatterguard.selected_aperture.set(ApertureValue.OUT_OF_BEAM)
141
142
 
142
143
  Note, to make sure we do this as quickly as possible, the scatterguard will stay
143
144
  in the same position relative to the aperture.
@@ -149,7 +150,7 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
149
150
 
150
151
  Then, at a later time, move back into the beam::
151
152
 
152
- await aperture_scatterguard.set(ApertureValue.LARGE)
153
+ await aperture_scatterguard.selected_aperture.set(ApertureValue.LARGE)
153
154
 
154
155
  Given the prepare has been done this move will now be faster as only the y is
155
156
  left to move.
@@ -164,11 +165,24 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
164
165
  ) -> None:
165
166
  self.aperture = Aperture(prefix + "-MO-MAPT-01:")
166
167
  self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
167
- self.radius = create_r_hardware_backed_soft_signal(
168
- float, self._get_current_radius, units="µm"
169
- )
170
168
  self._loaded_positions = loaded_positions
171
169
  self._tolerances = tolerances
170
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
171
+ self.selected_aperture = derived_signal_rw(
172
+ self._get_current_aperture_position,
173
+ self._set_current_aperture_position,
174
+ large=self.aperture.large,
175
+ medium=self.aperture.medium,
176
+ small=self.aperture.small,
177
+ current_ap_y=self.aperture.y.user_readback,
178
+ )
179
+
180
+ self.radius = derived_signal_r(
181
+ self._get_current_radius,
182
+ current_aperture=self.selected_aperture,
183
+ derived_units="µm",
184
+ )
185
+
172
186
  self.add_readables(
173
187
  [
174
188
  self.aperture.x.user_readback,
@@ -180,17 +194,9 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
180
194
  ],
181
195
  )
182
196
 
183
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
184
- self.selected_aperture = create_r_hardware_backed_soft_signal(
185
- ApertureValue, self._get_current_aperture_position
186
- )
187
-
188
197
  super().__init__(name)
189
198
 
190
- @AsyncStatus.wrap
191
- async def set(self, value: ApertureValue):
192
- """This set will move the aperture into the beam or move the whole assembly out"""
193
-
199
+ async def _set_current_aperture_position(self, value: ApertureValue) -> None:
194
200
  position = self._loaded_positions[value]
195
201
  await self._check_safe_to_move(position.aperture_z)
196
202
 
@@ -231,6 +237,27 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
231
237
  "triggering another move."
232
238
  )
233
239
 
240
+ def _get_current_radius(self, current_aperture: ApertureValue) -> float:
241
+ return self._loaded_positions[current_aperture].radius
242
+
243
+ def _is_out_of_beam(self, current_ap_y: float) -> bool:
244
+ out_ap_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
245
+ return current_ap_y <= out_ap_y + self._tolerances.aperture_y
246
+
247
+ def _get_current_aperture_position(
248
+ self, large: float, medium: float, small: float, current_ap_y: float
249
+ ) -> ApertureValue:
250
+ if large == 1:
251
+ return ApertureValue.LARGE
252
+ elif medium == 1:
253
+ return ApertureValue.MEDIUM
254
+ elif small == 1:
255
+ return ApertureValue.SMALL
256
+ elif self._is_out_of_beam(current_ap_y):
257
+ return ApertureValue.OUT_OF_BEAM
258
+
259
+ raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
260
+
234
261
  async def _safe_move_whilst_in_beam(self, position: AperturePosition):
235
262
  """
236
263
  Move the aperture and scatterguard combo safely to a new position.
@@ -282,33 +309,6 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
282
309
  self.scatterguard.y.set(scatterguard_y),
283
310
  )
284
311
 
285
- async def _is_out_of_beam(self) -> bool:
286
- current_ap_y = await self.aperture.y.user_readback.get_value()
287
- out_ap_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
288
- return current_ap_y <= out_ap_y + self._tolerances.aperture_y
289
-
290
- async def _get_current_aperture_position(self) -> ApertureValue:
291
- """
292
- Returns the current aperture position using readback values
293
- for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when
294
- mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
295
- If no position is found then raises InvalidApertureMove.
296
- """
297
- if await self.aperture.large.get_value(cached=False) == 1:
298
- return ApertureValue.LARGE
299
- elif await self.aperture.medium.get_value(cached=False) == 1:
300
- return ApertureValue.MEDIUM
301
- elif await self.aperture.small.get_value(cached=False) == 1:
302
- return ApertureValue.SMALL
303
- elif await self._is_out_of_beam():
304
- return ApertureValue.OUT_OF_BEAM
305
-
306
- raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
307
-
308
- async def _get_current_radius(self) -> float:
309
- current_value = await self._get_current_aperture_position()
310
- return self._loaded_positions[current_value].radius
311
-
312
312
  @AsyncStatus.wrap
313
313
  async def prepare(self, value: ApertureValue):
314
314
  """Moves the assembly to the position for the specified aperture, whilst keeping
@@ -317,7 +317,7 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
317
317
  Moving the assembly whilst out of the beam has no collision risk so we can just
318
318
  move all the motors together.
319
319
  """
320
- if await self._is_out_of_beam():
320
+ if self._is_out_of_beam(await self.aperture.y.user_readback.get_value()):
321
321
  aperture_x, _, aperture_z, scatterguard_x, scatterguard_y = (
322
322
  self._loaded_positions[value].values
323
323
  )
@@ -329,4 +329,4 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
329
329
  self.scatterguard.y.set(scatterguard_y),
330
330
  )
331
331
  else:
332
- await self.set(value)
332
+ await self.selected_aperture.set(value)
@@ -0,0 +1,77 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from ophyd_async.core import (
4
+ StandardReadable,
5
+ )
6
+ from ophyd_async.epics.core import epics_signal_r
7
+ from ophyd_async.epics.motor import Motor
8
+
9
+
10
+ class StationaryCrystal(StandardReadable):
11
+ def __init__(self, prefix):
12
+ super().__init__(prefix)
13
+
14
+
15
+ class RollCrystal(StationaryCrystal):
16
+ def __init__(self, prefix):
17
+ with self.add_children_as_readables():
18
+ self.roll_in_mrad = Motor(prefix + "ROLL")
19
+ super().__init__(prefix)
20
+
21
+
22
+ class PitchAndRollCrystal(StationaryCrystal):
23
+ def __init__(self, prefix):
24
+ with self.add_children_as_readables():
25
+ self.pitch_in_mrad = Motor(prefix + "PITCH")
26
+ self.roll_in_mrad = Motor(prefix + "ROLL")
27
+ super().__init__(prefix)
28
+
29
+
30
+ Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
31
+ Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
32
+
33
+
34
+ class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
35
+ """
36
+ Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
37
+
38
+ Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
39
+ each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
40
+ This base device accounts for all combinations of this.
41
+
42
+ This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
43
+
44
+ Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
45
+ which only requires one crystal which can roll should be typed 'def my_plan(dcm: BaseDCM[RollCrystal, StationaryCrystal])`
46
+ """
47
+
48
+ def __init__(
49
+ self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
50
+ ) -> None:
51
+ with self.add_children_as_readables():
52
+ # Virtual motor PV's which set the physical motors so that the DCM produces requested
53
+ # wavelength/energy
54
+ self.energy_in_kev = Motor(prefix + "ENERGY")
55
+ self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
56
+
57
+ # Real motors
58
+ self.bragg_in_degrees = Motor(prefix + "BRAGG")
59
+ # Offset ensures that the beam exits the DCM at the same point, regardless of energy.
60
+ self.offset_in_mm = Motor(prefix + "OFFSET")
61
+
62
+ self.crystal_metadata_d_spacing_a = epics_signal_r(
63
+ float, prefix + "DSPACING:RBV"
64
+ )
65
+
66
+ self._make_crystals(prefix, xtal_1, xtal_2)
67
+
68
+ super().__init__(name)
69
+
70
+ # Prefix convention is different depending on whether there are one or two controllable crystals
71
+ def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
72
+ if StationaryCrystal not in [xtal_1, xtal_2]:
73
+ self.xtal_1 = xtal_1(f"{prefix}XTAL1:")
74
+ self.xtal_2 = xtal_2(f"{prefix}XTAL2:")
75
+ else:
76
+ self.xtal_1 = xtal_1(prefix)
77
+ self.xtal_2 = xtal_2(prefix)
@@ -18,7 +18,7 @@ class CountMode(StrictEnum):
18
18
 
19
19
  class CountState(StrictEnum):
20
20
  DONE = "Done"
21
- COUNT = "Count" # type: ignore
21
+ COUNT = "Count"
22
22
 
23
23
 
24
24
  COUNT_PER_VOLTAGE = 100000
@@ -1,8 +1,8 @@
1
- from typing import Generic, TypeVar
1
+ from typing import TypeVar
2
2
 
3
- from ophyd_async.core import StandardReadable, StrictEnum
4
- from ophyd_async.epics.core import epics_signal_rw
5
- from ophyd_async.epics.motor import Motor
3
+ from ophyd_async.core import StrictEnum
4
+
5
+ from dodal.devices.positioner import Positioner1D
6
6
 
7
7
 
8
8
  class _Filters(StrictEnum):
@@ -25,22 +25,10 @@ class I04Filters(_Filters):
25
25
  T = TypeVar("T", bound=_Filters)
26
26
 
27
27
 
28
- class DiamondFilter(StandardReadable, Generic[T]):
28
+ class DiamondFilter(Positioner1D[T]):
29
29
  """
30
30
  A filter set that is used to reduce the heat load on the monochromator.
31
31
 
32
32
  It has 4 slots that can contain filters of different thickness. Changing the thickness
33
33
  signal will move the filter set to select this filter.
34
34
  """
35
-
36
- def __init__(
37
- self,
38
- prefix: str,
39
- data_type: type[T],
40
- name: str = "",
41
- ) -> None:
42
- with self.add_children_as_readables():
43
- self.y_motor = Motor(prefix + "Y")
44
- self.thickness = epics_signal_rw(data_type, f"{prefix}Y:MP:SELECT")
45
-
46
- super().__init__(name)
dodal/devices/eiger.py CHANGED
@@ -35,7 +35,7 @@ class InternalEigerTriggerMode(Enum):
35
35
  AVAILABLE_TIMEOUTS = {
36
36
  "i03": EigerTimeouts(
37
37
  stale_params_timeout=60,
38
- general_status_timeout=10,
38
+ general_status_timeout=20,
39
39
  meta_file_ready_timeout=30,
40
40
  all_frames_timeout=120, # Long timeout for meta file to compensate for filesystem issues
41
41
  arming_timeout=60,
@@ -0,0 +1,8 @@
1
+ from .types import EnergyMode
2
+ from .util import to_binding_energy, to_kinetic_energy
3
+
4
+ __all__ = [
5
+ "to_binding_energy",
6
+ "to_kinetic_energy",
7
+ "EnergyMode",
8
+ ]