dls-dodal 1.44.0__py3-none-any.whl → 1.46.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 (61) hide show
  1. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/RECORD +56 -46
  3. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +2 -0
  6. dodal/beamlines/b07.py +27 -0
  7. dodal/beamlines/b07_1.py +25 -0
  8. dodal/beamlines/i03.py +4 -4
  9. dodal/beamlines/i04.py +1 -1
  10. dodal/beamlines/i09.py +25 -0
  11. dodal/beamlines/i09_1.py +25 -0
  12. dodal/beamlines/i10.py +19 -35
  13. dodal/beamlines/i18.py +7 -4
  14. dodal/beamlines/i19_1.py +2 -1
  15. dodal/beamlines/i19_2.py +2 -1
  16. dodal/beamlines/i20_1.py +2 -1
  17. dodal/beamlines/i22.py +3 -3
  18. dodal/beamlines/i23.py +67 -2
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/p60.py +21 -0
  21. dodal/common/beamlines/beamline_utils.py +5 -0
  22. dodal/common/visit.py +1 -41
  23. dodal/devices/common_dcm.py +77 -0
  24. dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
  25. dodal/devices/detector/detector.py +2 -1
  26. dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
  27. dodal/devices/electron_analyser/abstract_region.py +112 -0
  28. dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
  29. dodal/devices/electron_analyser/specs_region.py +26 -0
  30. dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
  31. dodal/devices/electron_analyser/vgscienta_region.py +90 -0
  32. dodal/devices/{dcm.py → i03/dcm.py} +8 -12
  33. dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
  34. dodal/devices/i10/diagnostics.py +239 -0
  35. dodal/devices/i10/slits.py +93 -6
  36. dodal/devices/i13_1/merlin.py +3 -4
  37. dodal/devices/i13_1/merlin_controller.py +1 -1
  38. dodal/devices/i19/blueapi_device.py +102 -0
  39. dodal/devices/i19/shutter.py +5 -43
  40. dodal/devices/i22/dcm.py +10 -12
  41. dodal/devices/i24/dcm.py +8 -17
  42. dodal/devices/motors.py +21 -0
  43. dodal/devices/tetramm.py +3 -4
  44. dodal/devices/turbo_slit.py +10 -4
  45. dodal/devices/undulator.py +9 -7
  46. dodal/devices/util/adjuster_plans.py +1 -2
  47. dodal/devices/util/lookup_tables.py +38 -0
  48. dodal/devices/util/test_utils.py +1 -0
  49. dodal/devices/zebra/zebra.py +4 -0
  50. dodal/plan_stubs/data_session.py +10 -1
  51. dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
  52. dodal/plans/verify_undulator_gap.py +2 -2
  53. dodal/devices/electron_analyser/base_region.py +0 -64
  54. dodal/devices/electron_analyser/specs/specs_region.py +0 -24
  55. dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
  56. dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +0 -77
  57. dodal/devices/util/motor_utils.py +0 -6
  58. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/entry_points.txt +0 -0
  59. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/licenses/LICENSE +0 -0
  60. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/top_level.txt +0 -0
  61. /dodal/{devices/electron_analyser/specs → plan_stubs/electron_analyser}/__init__.py +0 -0
dodal/devices/i22/dcm.py CHANGED
@@ -5,7 +5,6 @@ from bluesky.protocols import Reading
5
5
  from event_model.documents.event_descriptor import DataKey
6
6
  from ophyd_async.core import (
7
7
  Array1D,
8
- StandardReadable,
9
8
  StandardReadableFormat,
10
9
  soft_signal_r_and_setter,
11
10
  )
@@ -13,13 +12,18 @@ from ophyd_async.epics.core import epics_signal_r
13
12
  from ophyd_async.epics.motor import Motor
14
13
 
15
14
  from dodal.common.crystal_metadata import CrystalMetadata
15
+ from dodal.devices.common_dcm import (
16
+ BaseDCM,
17
+ PitchAndRollCrystal,
18
+ RollCrystal,
19
+ )
16
20
 
17
21
  # Conversion constant for energy and wavelength, taken from the X-Ray data booklet
18
22
  # Converts between energy in KeV and wavelength in angstrom
19
23
  _CONVERSION_CONSTANT = 12.3984
20
24
 
21
25
 
22
- class DoubleCrystalMonochromator(StandardReadable):
26
+ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
23
27
  """
24
28
  A double crystal monochromator (DCM), used to select the energy of the beam.
25
29
 
@@ -39,13 +43,7 @@ class DoubleCrystalMonochromator(StandardReadable):
39
43
  ) -> None:
40
44
  with self.add_children_as_readables():
41
45
  # Positionable Parameters
42
- self.bragg = Motor(prefix + "BRAGG")
43
- self.offset = Motor(prefix + "OFFSET")
44
46
  self.perp = Motor(prefix + "PERP")
45
- self.energy = Motor(prefix + "ENERGY")
46
- self.crystal_1_roll = Motor(prefix + "XTAL1:ROLL")
47
- self.crystal_2_roll = Motor(prefix + "XTAL2:ROLL")
48
- self.crystal_2_pitch = Motor(prefix + "XTAL2:PITCH")
49
47
 
50
48
  # Temperatures
51
49
  self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
@@ -93,12 +91,12 @@ class DoubleCrystalMonochromator(StandardReadable):
93
91
  units=crystal_2_metadata.d_spacing[1],
94
92
  )
95
93
 
96
- super().__init__(name)
94
+ super().__init__(prefix, RollCrystal, PitchAndRollCrystal, name)
97
95
 
98
96
  async def describe(self) -> dict[str, DataKey]:
99
97
  default_describe = await super().describe()
100
98
  return {
101
- f"{self.name}-wavelength": DataKey(
99
+ f"{self.name}-wavelength_in_a": DataKey(
102
100
  dtype="number",
103
101
  shape=[],
104
102
  source=self.name,
@@ -109,7 +107,7 @@ class DoubleCrystalMonochromator(StandardReadable):
109
107
 
110
108
  async def read(self) -> dict[str, Reading]:
111
109
  default_reading = await super().read()
112
- energy: float = default_reading[f"{self.name}-energy"]["value"]
110
+ energy: float = default_reading[f"{self.name}-energy_in_kev"]["value"]
113
111
  if energy > 0.0:
114
112
  wavelength = _CONVERSION_CONSTANT / energy
115
113
  else:
@@ -117,7 +115,7 @@ class DoubleCrystalMonochromator(StandardReadable):
117
115
 
118
116
  return {
119
117
  **default_reading,
120
- f"{self.name}-wavelength": Reading(
118
+ f"{self.name}-wavelength_in_a": Reading(
121
119
  value=wavelength,
122
120
  timestamp=time.time(),
123
121
  ),
dodal/devices/i24/dcm.py CHANGED
@@ -1,28 +1,19 @@
1
- from ophyd_async.core import StandardReadable
2
1
  from ophyd_async.epics.core import epics_signal_r
3
- from ophyd_async.epics.motor import Motor
4
2
 
3
+ from dodal.devices.common_dcm import (
4
+ BaseDCM,
5
+ PitchAndRollCrystal,
6
+ RollCrystal,
7
+ )
5
8
 
6
- class DCM(StandardReadable):
9
+
10
+ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
7
11
  """
8
12
  A double crystal monocromator device, used to select the beam energy.
9
13
  """
10
14
 
11
15
  def __init__(self, prefix: str, name: str = "") -> None:
12
16
  with self.add_children_as_readables():
13
- # Motors
14
- self.bragg_in_degrees = Motor(prefix + "-MO-DCM-01:BRAGG")
15
- self.x_translation_in_mm = Motor(prefix + "-MO-DCM-01:X")
16
- self.offset_in_mm = Motor(prefix + "-MO-DCM-01:OFFSET")
17
- self.gap_in_mm = Motor(prefix + "-MO-DCM-01:GAP")
18
- self.energy_in_kev = Motor(prefix + "-MO-DCM-01:ENERGY")
19
- self.xtal1_roll = Motor(prefix + "-MO-DCM-01:XTAL1:ROLL")
20
- self.xtal2_roll = Motor(prefix + "-MO-DCM-01:XTAL2:ROLL")
21
- self.xtal2_pitch = Motor(prefix + "-MO-DCM-01:XTAL2:PITCH")
22
-
23
- # Wavelength is calculated in epics from the energy
24
- self.wavelength_in_a = epics_signal_r(float, prefix + "-MO-DCM-01:LAMBDA")
25
-
26
17
  # Temperatures
27
18
  self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
28
19
  self.xtal1_heater_temp = epics_signal_r(
@@ -39,4 +30,4 @@ class DCM(StandardReadable):
39
30
  self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
40
31
  self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
41
32
 
42
- super().__init__(name)
33
+ super().__init__(prefix + "-MO-DCM-01:", RollCrystal, PitchAndRollCrystal, name)
dodal/devices/motors.py CHANGED
@@ -38,3 +38,24 @@ class XYZPositioner(StandardReadable):
38
38
  self.y = Motor(prefix + infix[1])
39
39
  self.z = Motor(prefix + infix[2])
40
40
  super().__init__(name=name)
41
+
42
+
43
+ class SixAxisGonio(XYZPositioner):
44
+ def __init__(
45
+ self,
46
+ prefix: str,
47
+ name: str = "",
48
+ infix: tuple[str, str, str, str, str, str] = (
49
+ "X",
50
+ "Y",
51
+ "Z",
52
+ "KAPPA",
53
+ "PHI",
54
+ "OMEGA",
55
+ ),
56
+ ):
57
+ with self.add_children_as_readables():
58
+ self.kappa = Motor(prefix + infix[3])
59
+ self.phi = Motor(prefix + infix[4])
60
+ self.omega = Motor(prefix + infix[5])
61
+ super().__init__(name=name, prefix=prefix, infix=infix[0:3])
dodal/devices/tetramm.py CHANGED
@@ -244,10 +244,9 @@ class TetrammDetector(StandardDetector):
244
244
  super().__init__(
245
245
  controller,
246
246
  ADHDFWriter(
247
- self.hdf,
248
- path_provider,
249
- lambda: self.name,
250
- TetrammDatasetDescriber(controller),
247
+ fileio=self.hdf,
248
+ path_provider=path_provider,
249
+ dataset_describer=TetrammDatasetDescriber(controller),
251
250
  plugins=plugins,
252
251
  ),
253
252
  config_signals,
@@ -1,8 +1,9 @@
1
- from ophyd_async.core import StandardReadable
1
+ from bluesky.protocols import Movable
2
+ from ophyd_async.core import AsyncStatus, StandardReadable
2
3
  from ophyd_async.epics.motor import Motor
3
4
 
4
5
 
5
- class TurboSlit(StandardReadable):
6
+ class TurboSlit(StandardReadable, Movable[float]):
6
7
  """
7
8
  This collection of motors coordinates time resolved XAS experiments.
8
9
  It selects a beam out of the polychromatic fan.
@@ -12,9 +13,9 @@ class TurboSlit(StandardReadable):
12
13
  The xfine motor can move the slit in x direction at high frequencies for different scans.
13
14
  These slits can be scanned continously or in step mode.
14
15
  The relationship between the three motors is as follows:
15
- - arc - position of the middle of the gap (coarse/ macro) extension
16
+ - arc - position of the middle of the gap in degrees (coarse/ macro) extension
16
17
  - gap - width in mm, provides energy resolution
17
- - xfine selects the energy as part of the high frequency scan
18
+ - xfine - main scanning axis in mm, selects the energy as part of the high frequency scan
18
19
  """
19
20
 
20
21
  def __init__(self, prefix: str, name: str = ""):
@@ -23,3 +24,8 @@ class TurboSlit(StandardReadable):
23
24
  self.arc = Motor(prefix=prefix + "ARC")
24
25
  self.xfine = Motor(prefix=prefix + "XFINE")
25
26
  super().__init__(name=name)
27
+
28
+ @AsyncStatus.wrap
29
+ async def set(self, value: float):
30
+ """This will move the default XFINE"""
31
+ await self.xfine.set(value)
@@ -2,7 +2,7 @@ import os
2
2
 
3
3
  import numpy as np
4
4
  from bluesky.protocols import Movable
5
- from numpy import argmin, ndarray
5
+ from numpy import ndarray
6
6
  from ophyd_async.core import (
7
7
  AsyncStatus,
8
8
  StandardReadable,
@@ -38,12 +38,12 @@ class UndulatorGapAccess(StrictEnum):
38
38
  DISABLED = "DISABLED"
39
39
 
40
40
 
41
- def _get_closest_gap_for_energy(
41
+ def _get_gap_for_energy(
42
42
  dcm_energy_ev: float, energy_to_distance_table: ndarray
43
43
  ) -> float:
44
- table = energy_to_distance_table.transpose()
45
- idx = argmin(np.abs(table[0] - dcm_energy_ev))
46
- return table[1][idx]
44
+ return np.interp(
45
+ dcm_energy_ev, energy_to_distance_table[:, 0], energy_to_distance_table[:, 1]
46
+ )
47
47
 
48
48
 
49
49
  class Undulator(StandardReadable, Movable[float]):
@@ -115,8 +115,10 @@ class Undulator(StandardReadable, Movable[float]):
115
115
 
116
116
  async def _set_undulator_gap(self, energy_kev: float) -> None:
117
117
  await self.raise_if_not_enabled()
118
- LOGGER.info(f"Setting undulator gap to {energy_kev:.2f} kev")
119
118
  target_gap = await self._get_gap_to_match_energy(energy_kev)
119
+ LOGGER.info(
120
+ f"Setting undulator gap to {target_gap:.3f}mm based on {energy_kev:.2f}kev"
121
+ )
120
122
 
121
123
  # Check if undulator gap is close enough to the value from the DCM
122
124
  current_gap = await self.current_gap.get_value()
@@ -152,7 +154,7 @@ class Undulator(StandardReadable, Movable[float]):
152
154
  )
153
155
 
154
156
  # Use the lookup table to get the undulator gap associated with this dcm energy
155
- return _get_closest_gap_for_energy(
157
+ return _get_gap_for_energy(
156
158
  energy_kev * 1000,
157
159
  energy_to_distance_table,
158
160
  )
@@ -7,14 +7,13 @@ from collections.abc import Callable, Generator
7
7
 
8
8
  from bluesky import plan_stubs as bps
9
9
  from bluesky.utils import Msg
10
- from ophyd.epics_motor import EpicsMotor
11
10
  from ophyd_async.epics.motor import Motor
12
11
 
13
12
  from dodal.log import LOGGER
14
13
 
15
14
 
16
15
  def lookup_table_adjuster(
17
- lookup_table: Callable[[float], float], output_device: EpicsMotor | Motor, input
16
+ lookup_table: Callable[[float], float], output_device: Motor, input
18
17
  ):
19
18
  """Returns a callable that adjusts a value according to a lookup table"""
20
19
 
@@ -64,3 +64,41 @@ def linear_interpolation_lut(
64
64
  return float(interp(s, s_values, t_values))
65
65
 
66
66
  return s_to_t2
67
+
68
+
69
+ def linear_extrapolation_lut(
70
+ s_values: Sequence[float], t_values: Sequence[float]
71
+ ) -> Callable[[float], float]:
72
+ """
73
+ Return a callable that implements f(s) = t according to the conversion table data
74
+ supplied, with linear extrapolation outside that range. Inside the range of the table,
75
+ the function is equivalent to that returned by linear_interpolation_lut
76
+
77
+ Args:
78
+ s_values: Values of the independent axis
79
+ t_values: Values of the dependent axis
80
+
81
+ Returns:
82
+ A callable that returns t for the given s
83
+ """
84
+ assert len(s_values) == len(t_values), (
85
+ "Lookup table does not have the same number of values for each axis"
86
+ )
87
+ assert len(s_values) > 1, "Need at least 2 points in the lookup table"
88
+ interp = linear_interpolation_lut(s_values, t_values)
89
+ s_min = s_values[0]
90
+ s_max = s_values[-1]
91
+
92
+ def s_to_t(s: float) -> float:
93
+ if s < s_min:
94
+ return t_values[0] + (s - s_min) * (t_values[1] - t_values[0]) / (
95
+ s_values[1] - s_values[0]
96
+ )
97
+ elif s > s_max:
98
+ return t_values[-1] + (s - s_max) * (t_values[-1] - t_values[-2]) / (
99
+ s_values[-1] - s_values[-2]
100
+ )
101
+ else:
102
+ return interp(s)
103
+
104
+ return s_to_t
@@ -11,6 +11,7 @@ def patch_motor(motor: Motor, initial_position=0):
11
11
  set_mock_value(motor.deadband, 0.001)
12
12
  set_mock_value(motor.motor_done_move, 1)
13
13
  set_mock_value(motor.velocity, 3)
14
+ set_mock_value(motor.max_velocity, 5)
14
15
  return callback_on_mock_put(
15
16
  motor.user_setpoint,
16
17
  lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
@@ -51,6 +51,10 @@ class I24Axes:
51
51
  VGON_YH = EncEnum.ENC4
52
52
 
53
53
 
54
+ class I23Axes:
55
+ OMEGA = EncEnum.ENC1
56
+
57
+
54
58
  class RotationDirection(StrictEnum):
55
59
  """
56
60
  Defines for a swept angle whether the scan width (sweep) is to be added or subtracted from
@@ -33,7 +33,11 @@ def attach_data_session_metadata_wrapper(
33
33
  Yields:
34
34
  Iterator[Msg]: Plan messages
35
35
  """
36
- provider = provider or get_path_provider()
36
+ try:
37
+ provider = provider or get_path_provider()
38
+ except NameError:
39
+ provider = None
40
+
37
41
  if isinstance(provider, UpdatingPathProvider):
38
42
  yield from bps.wait_for([provider.update])
39
43
  ress = yield from bps.wait_for([provider.data_session])
@@ -41,6 +45,11 @@ def attach_data_session_metadata_wrapper(
41
45
  # https://github.com/DiamondLightSource/dodal/issues/452
42
46
  # As part of 452, write each dataCollection into their own folder, then can use resource_dir directly
43
47
  yield from bpp.inject_md_wrapper(plan, md={DATA_SESSION: data_session})
48
+ elif provider is None:
49
+ logging.warning(
50
+ f"There is no PathProvider set, {attach_data_session_metadata_wrapper.__name__} will have no effect"
51
+ )
52
+ yield from plan
44
53
  else:
45
54
  logging.warning(
46
55
  f"{provider} is not an UpdatingPathProvider, {attach_data_session_metadata_wrapper.__name__} will have no effect"
@@ -0,0 +1,80 @@
1
+ from bluesky import plan_stubs as bps
2
+
3
+ from dodal.devices.electron_analyser.abstract_analyser_io import (
4
+ AbstractAnalyserDriverIO,
5
+ )
6
+ from dodal.devices.electron_analyser.abstract_region import (
7
+ AbstractBaseRegion,
8
+ )
9
+ from dodal.devices.electron_analyser.specs_analyser_io import (
10
+ SpecsAnalyserDriverIO,
11
+ )
12
+ from dodal.devices.electron_analyser.specs_region import SpecsRegion
13
+ from dodal.devices.electron_analyser.vgscienta_analyser_io import (
14
+ VGScientaAnalyserDriverIO,
15
+ )
16
+ from dodal.devices.electron_analyser.vgscienta_region import (
17
+ VGScientaRegion,
18
+ )
19
+ from dodal.log import LOGGER
20
+
21
+
22
+ def configure_analyser(
23
+ analyser: AbstractAnalyserDriverIO,
24
+ region: AbstractBaseRegion,
25
+ excitation_energy: float,
26
+ ):
27
+ LOGGER.info(f'Configuring analyser with region "{region.name}"')
28
+ low_energy = region.to_kinetic_energy(region.low_energy, excitation_energy)
29
+ high_energy = region.to_kinetic_energy(region.high_energy, excitation_energy)
30
+ pass_energy_type = analyser.pass_energy_type
31
+ pass_energy = pass_energy_type(region.pass_energy)
32
+ # Set detector settings, wait for them all to have completed
33
+ # fmt: off
34
+ yield from bps.mv(
35
+ analyser.low_energy, low_energy,
36
+ analyser.high_energy, high_energy,
37
+ analyser.slices, region.slices,
38
+ analyser.lens_mode, region.lens_mode,
39
+ analyser.pass_energy, pass_energy,
40
+ analyser.iterations, region.iterations,
41
+ analyser.acquisition_mode, region.acquisition_mode,
42
+ )
43
+ # fmt: on
44
+
45
+
46
+ def configure_specs(
47
+ analyser: SpecsAnalyserDriverIO, region: SpecsRegion, excitation_energy: float
48
+ ):
49
+ yield from configure_analyser(analyser, region, excitation_energy)
50
+ # fmt: off
51
+ yield from bps.mv(
52
+ analyser.values, region.values,
53
+ analyser.psu_mode, region.psu_mode,
54
+ )
55
+ # fmt: on
56
+ if region.acquisition_mode == "Fixed Transmission":
57
+ yield from bps.mv(analyser.centre_energy, region.centre_energy)
58
+
59
+ if region.acquisition_mode == "Fixed Energy":
60
+ yield from bps.mv(analyser.energy_step, region.energy_step)
61
+
62
+
63
+ def configure_vgscienta(
64
+ analyser: VGScientaAnalyserDriverIO, region: VGScientaRegion, excitation_energy
65
+ ):
66
+ yield from configure_analyser(analyser, region, excitation_energy)
67
+ centre_energy = region.to_kinetic_energy(region.fix_energy, excitation_energy)
68
+
69
+ # fmt: off
70
+ yield from bps.mv(
71
+ analyser.centre_energy, centre_energy,
72
+ analyser.energy_step, region.energy_step,
73
+ analyser.first_x_channel, region.first_x_channel,
74
+ analyser.first_y_channel, region.first_y_channel,
75
+ analyser.x_channel_size, region.x_channel_size(),
76
+ analyser.y_channel_size, region.y_channel_size(),
77
+ analyser.detector_mode, region.detector_mode,
78
+ analyser.image_mode, "Single",
79
+ )
80
+ # fmt: on
@@ -2,14 +2,14 @@ from typing import Protocol, runtime_checkable
2
2
 
3
3
  from bluesky import plan_stubs as bps
4
4
 
5
- from dodal.devices.dcm import DCM
5
+ from dodal.devices.common_dcm import BaseDCM
6
6
  from dodal.devices.undulator import Undulator
7
7
 
8
8
 
9
9
  @runtime_checkable
10
10
  class CheckUndulatorDevices(Protocol):
11
11
  undulator: Undulator
12
- dcm: DCM
12
+ dcm: BaseDCM
13
13
 
14
14
 
15
15
  def verify_undulator_gap(devices: CheckUndulatorDevices):
@@ -1,64 +0,0 @@
1
- from enum import Enum
2
- from typing import Any, Generic, TypeVar
3
-
4
- from pydantic import BaseModel, Field, model_validator
5
-
6
-
7
- class EnergyMode(str, Enum):
8
- KINETIC = "Kinetic"
9
- BINDING = "Binding"
10
-
11
-
12
- class BaseRegion(BaseModel):
13
- """
14
- Generic region model that holds the data. Specialised region models should inherit
15
- this to extend functionality.
16
- """
17
-
18
- name: str = "New_region"
19
- enabled: bool = False
20
- slices: int = 1
21
- iterations: int = 1
22
- # These ones we need subclasses to provide default values
23
- lensMode: str
24
- passEnergy: int | float
25
- acquisitionMode: str
26
- lowEnergy: float
27
- highEnergy: float
28
- stepTime: float
29
- energyStep: float
30
- energyMode: EnergyMode = EnergyMode.KINETIC
31
-
32
- @model_validator(mode="before")
33
- @classmethod
34
- def check_energy_mode(cls, data: Any) -> Any:
35
- if isinstance(data, dict):
36
- # convert bindingEnergy to energyMode to make base region more generic
37
- if "bindingEnergy" in data:
38
- is_binding_energy = data["bindingEnergy"]
39
- del data["bindingEnergy"]
40
- data["energyMode"] = (
41
- EnergyMode.BINDING if is_binding_energy else EnergyMode.KINETIC
42
- )
43
- return data
44
-
45
-
46
- TBaseRegion = TypeVar("TBaseRegion", bound=BaseRegion)
47
-
48
-
49
- class BaseSequence(BaseModel, Generic[TBaseRegion]):
50
- """
51
- Generic sequence model that holds the list of region data. Specialised sequence models
52
- should inherit this to extend functionality.
53
- """
54
-
55
- regions: list[TBaseRegion] = Field(default_factory=lambda: [])
56
-
57
- def get_enabled_regions(self) -> list[BaseRegion]:
58
- return [r for r in self.regions if r.enabled]
59
-
60
- def get_region_names(self) -> list[str]:
61
- return [r.name for r in self.regions]
62
-
63
- def get_enabled_region_names(self) -> list[str]:
64
- return [r.name for r in self.get_enabled_regions()]
@@ -1,24 +0,0 @@
1
- from pydantic import Field
2
-
3
- from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
4
-
5
-
6
- class SpecsRegion(BaseRegion):
7
- # Override base class with defaults
8
- lensMode: str = "SmallArea"
9
- passEnergy: int | float = 5.0
10
- acquisitionMode: str = "Fixed Transmission"
11
- lowEnergy: float = Field(default=800, alias="startEnergy")
12
- highEnergy: float = Field(default=850, alias="endEnergy")
13
- stepTime: float = Field(default=1.0, alias="exposureTime")
14
- energyStep: float = Field(default=0.1, alias="stepEnergy")
15
- # Specific to this class
16
- values: float = 1
17
- centreEnergy: float = 0
18
- psuMode: str = "1.5keV"
19
- acquisitionMode: str = ""
20
- estimatedTimeInMs: float = 0
21
-
22
-
23
- class SpecsSequence(BaseSequence):
24
- regions: list[SpecsRegion] = Field(default_factory=lambda: [])
File without changes
@@ -1,77 +0,0 @@
1
- import uuid
2
- from enum import Enum
3
-
4
- from ophyd_async.core import StrictEnum
5
- from pydantic import BaseModel, Field
6
-
7
- from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
8
-
9
-
10
- class Status(str, Enum):
11
- READY = "Ready"
12
- RUNNING = "Running"
13
- COMPLETED = "Completed"
14
- INVALID = "Invalid"
15
- ABORTED = "Aborted"
16
-
17
-
18
- class DetectorMode(StrictEnum):
19
- ADC = "ADC"
20
- PULSE_COUNTING = "Pulse Counting"
21
-
22
-
23
- class AcquisitionMode(str, Enum):
24
- SWEPT = "Swept"
25
- FIXED = "Fixed"
26
-
27
-
28
- class VGScientaRegion(BaseRegion):
29
- # Override defaults of base region class
30
- lensMode: str = "Angular45"
31
- passEnergy: int | float = 5
32
- acquisitionMode: str = AcquisitionMode.SWEPT
33
- lowEnergy: float = 8.0
34
- highEnergy: float = 10.0
35
- stepTime: float = 1.0
36
- energyStep: float = Field(default=200.0)
37
- # Specific to this class
38
- regionId: str = Field(default=str(uuid.uuid4()))
39
- excitationEnergySource: str = "source1"
40
- fixEnergy: float = 9.0
41
- totalSteps: float = 13.0
42
- totalTime: float = 13.0
43
- exposureTime: float = 1.0
44
- firstXChannel: int = 1
45
- lastXChannel: int = 1000
46
- firstYChannel: int = 101
47
- lastYChannel: int = 800
48
- detectorMode: DetectorMode = DetectorMode.ADC
49
- status: Status = Status.READY
50
-
51
-
52
- class VGScientaExcitationEnergySource(BaseModel):
53
- name: str = "source1"
54
- scannableName: str = ""
55
- value: float = 0
56
-
57
-
58
- class VGScientaSequence(BaseSequence):
59
- elementSet: str = Field(default="Unknown")
60
- excitationEnergySources: list[VGScientaExcitationEnergySource] = Field(
61
- default_factory=lambda: []
62
- )
63
- regions: list[VGScientaRegion] = Field(default_factory=lambda: [])
64
-
65
- def get_excitation_energy_source_by_region(
66
- self, region: VGScientaRegion
67
- ) -> VGScientaExcitationEnergySource | None:
68
- filtered_excitation_energy_sources = [
69
- e
70
- for e in self.excitationEnergySources
71
- if e.name == region.excitationEnergySource
72
- ]
73
- return (
74
- filtered_excitation_energy_sources[0]
75
- if len(filtered_excitation_energy_sources) == 1
76
- else None
77
- )
@@ -1,6 +0,0 @@
1
- from ophyd import Component, EpicsMotor, EpicsSignalRO
2
-
3
-
4
- class ExtendedEpicsMotor(EpicsMotor):
5
- motor_resolution: Component[EpicsSignalRO] = Component(EpicsSignalRO, ".MRES")
6
- max_velocity: Component[EpicsSignalRO] = Component(EpicsSignalRO, ".VMAX")