dls-dodal 1.58.0__py3-none-any.whl → 1.59.1__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 (58) hide show
  1. {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/METADATA +2 -1
  2. {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/RECORD +58 -43
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/b07.py +10 -5
  5. dodal/beamlines/b07_1.py +10 -5
  6. dodal/beamlines/b21.py +22 -0
  7. dodal/beamlines/i02_1.py +80 -0
  8. dodal/beamlines/i03.py +5 -3
  9. dodal/beamlines/i04.py +5 -3
  10. dodal/beamlines/i09.py +10 -9
  11. dodal/beamlines/i09_1.py +10 -5
  12. dodal/beamlines/i10-1.py +25 -0
  13. dodal/beamlines/i10.py +17 -1
  14. dodal/beamlines/i11.py +0 -17
  15. dodal/beamlines/i19_2.py +11 -0
  16. dodal/beamlines/i21.py +27 -0
  17. dodal/beamlines/i22.py +12 -2
  18. dodal/beamlines/i24.py +32 -3
  19. dodal/beamlines/k07.py +31 -0
  20. dodal/beamlines/p60.py +10 -9
  21. dodal/common/watcher_utils.py +1 -1
  22. dodal/devices/apple2_undulator.py +18 -142
  23. dodal/devices/attenuator/attenuator.py +48 -2
  24. dodal/devices/attenuator/filter.py +3 -0
  25. dodal/devices/attenuator/filter_selections.py +26 -0
  26. dodal/devices/eiger.py +2 -1
  27. dodal/devices/electron_analyser/__init__.py +4 -0
  28. dodal/devices/electron_analyser/abstract/base_driver_io.py +30 -18
  29. dodal/devices/electron_analyser/energy_sources.py +101 -0
  30. dodal/devices/electron_analyser/specs/detector.py +6 -6
  31. dodal/devices/electron_analyser/specs/driver_io.py +7 -15
  32. dodal/devices/electron_analyser/vgscienta/detector.py +6 -6
  33. dodal/devices/electron_analyser/vgscienta/driver_io.py +7 -14
  34. dodal/devices/fast_grid_scan.py +130 -64
  35. dodal/devices/focusing_mirror.py +30 -0
  36. dodal/devices/i02_1/__init__.py +0 -0
  37. dodal/devices/i02_1/fast_grid_scan.py +61 -0
  38. dodal/devices/i02_1/sample_motors.py +19 -0
  39. dodal/devices/i04/murko_results.py +69 -23
  40. dodal/devices/i10/i10_apple2.py +282 -140
  41. dodal/devices/i21/__init__.py +3 -0
  42. dodal/devices/i21/enums.py +8 -0
  43. dodal/devices/i22/nxsas.py +2 -0
  44. dodal/devices/i24/commissioning_jungfrau.py +114 -0
  45. dodal/devices/smargon.py +0 -56
  46. dodal/devices/temperture_controller/__init__.py +3 -0
  47. dodal/devices/temperture_controller/lakeshore/__init__.py +0 -0
  48. dodal/devices/temperture_controller/lakeshore/lakeshore.py +204 -0
  49. dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +112 -0
  50. dodal/devices/tetramm.py +38 -16
  51. dodal/devices/v2f.py +39 -0
  52. dodal/devices/zebra/zebra.py +1 -0
  53. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  54. dodal/parameters/experiment_parameter_base.py +1 -5
  55. {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/WHEEL +0 -0
  56. {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/entry_points.txt +0 -0
  57. {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/licenses/LICENSE +0 -0
  58. {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/top_level.txt +0 -0
dodal/beamlines/i10.py CHANGED
@@ -6,6 +6,8 @@ note:
6
6
  idd == id1, idu == id2.
7
7
  """
8
8
 
9
+ from daq_config_server.client import ConfigServer
10
+
9
11
  from dodal.common.beamlines.beamline_utils import device_factory
10
12
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
11
13
  from dodal.devices.current_amplifiers import CurrentAmpDet
@@ -25,6 +27,9 @@ from dodal.devices.i10.rasor.rasor_scaler_cards import RasorScalerCard1
25
27
  from dodal.devices.i10.slits import I10Slits, I10SlitsDrainCurrent
26
28
  from dodal.devices.motors import XYStage, XYZStage
27
29
  from dodal.devices.pgm import PGM
30
+ from dodal.devices.temperture_controller import (
31
+ Lakeshore340,
32
+ )
28
33
  from dodal.log import set_beamline as set_log_beamline
29
34
  from dodal.utils import BeamlinePrefix, get_beamline_name
30
35
 
@@ -33,8 +38,10 @@ set_log_beamline(BL)
33
38
  set_utils_beamline(BL)
34
39
  PREFIX = BeamlinePrefix(BL)
35
40
 
41
+ I10_CONF_CLIENT = ConfigServer(url="https://daq-config.diamond.ac.uk")
36
42
 
37
- LOOK_UPTABLE_DIR = "/dls_sw/i10/software/blueapi/scratch/i10-config/lookupTables/"
43
+
44
+ LOOK_UPTABLE_DIR = "/dls_sw/i10/software/gda/workspace_git/gda-diamond.git/configurations/i10-shared/lookupTables/"
38
45
 
39
46
 
40
47
  @device_factory()
@@ -60,6 +67,7 @@ def idd() -> I10Id:
60
67
  pgm=pgm(),
61
68
  look_up_table_dir=LOOK_UPTABLE_DIR,
62
69
  source=("Source", "idd"),
70
+ config_client=I10_CONF_CLIENT,
63
71
  )
64
72
 
65
73
 
@@ -76,6 +84,7 @@ def idu() -> I10Id:
76
84
  pgm=pgm(),
77
85
  look_up_table_dir=LOOK_UPTABLE_DIR,
78
86
  source=("Source", "idu"),
87
+ config_client=I10_CONF_CLIENT,
79
88
  )
80
89
 
81
90
 
@@ -153,6 +162,13 @@ def sample_stage() -> XYZStage:
153
162
  return XYZStage(prefix="ME01D-MO-CRYO-01:")
154
163
 
155
164
 
165
+ @device_factory()
166
+ def rasor_temperature_controller() -> Lakeshore340:
167
+ return Lakeshore340(
168
+ prefix="ME01D-EA-TCTRL-01:",
169
+ )
170
+
171
+
156
172
  @device_factory()
157
173
  def rasor_femto() -> RasorFemto:
158
174
  return RasorFemto(
dodal/beamlines/i11.py CHANGED
@@ -1,13 +1,9 @@
1
- from pathlib import Path
2
-
3
1
  from dodal.common.beamlines.beamline_utils import (
4
2
  device_factory,
5
3
  get_path_provider,
6
- set_path_provider,
7
4
  )
8
5
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
9
6
  from dodal.common.beamlines.device_helpers import DET_SUFFIX
10
- from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
11
7
  from dodal.devices.cryostream import OxfordCryoStream
12
8
  from dodal.devices.eurotherm import (
13
9
  EurothermGeneral,
@@ -34,19 +30,6 @@ PREFIX = BeamlinePrefix(BL)
34
30
  set_log_beamline(BL)
35
31
  set_utils_beamline(BL)
36
32
 
37
- # Currently we must hard-code the visit, determining the visit at runtime requires
38
- # infrastructure that is still WIP.
39
- # Communication with GDA is also WIP so for now we determine an arbitrary scan number
40
- # locally and write the commissioning directory. The scan number is not guaranteed to
41
- # be unique and the data is at risk - this configuration is for testing only.
42
- set_path_provider(
43
- StaticVisitPathProvider(
44
- BL,
45
- Path(f"/dls/{BL}/data/2025/cm40625-3/bluesky"),
46
- client=RemoteDirectoryServiceClient(f"http://{BL}-control:8088/api"),
47
- )
48
- )
49
-
50
33
 
51
34
  @device_factory()
52
35
  def mythen3() -> Mythen3:
dodal/beamlines/i19_2.py CHANGED
@@ -1,5 +1,8 @@
1
+ from ophyd_async.fastcs.panda import HDFPanda
2
+
1
3
  from dodal.common.beamlines.beamline_utils import (
2
4
  device_factory,
5
+ get_path_provider,
3
6
  )
4
7
  from dodal.common.beamlines.beamline_utils import (
5
8
  set_beamline as set_utils_beamline,
@@ -84,3 +87,11 @@ def backlight() -> BacklightPosition:
84
87
  If this is called when already instantiated in i19-2, it will return the existing object.
85
88
  """
86
89
  return BacklightPosition(prefix=f"{PREFIX.beamline_prefix}-EA-IOC-12:")
90
+
91
+
92
+ @device_factory()
93
+ def panda() -> HDFPanda:
94
+ return HDFPanda(
95
+ prefix=f"{PREFIX.beamline_prefix}-EA-PANDA-01:",
96
+ path_provider=get_path_provider(),
97
+ )
dodal/beamlines/i21.py ADDED
@@ -0,0 +1,27 @@
1
+ from dodal.common.beamlines.beamline_utils import (
2
+ device_factory,
3
+ )
4
+ from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5
+ from dodal.devices.i21 import Grating
6
+ from dodal.devices.pgm import PGM
7
+ from dodal.devices.synchrotron import Synchrotron
8
+ from dodal.log import set_beamline as set_log_beamline
9
+ from dodal.utils import BeamlinePrefix, get_beamline_name
10
+
11
+ BL = get_beamline_name("i21")
12
+ PREFIX = BeamlinePrefix(BL, suffix="I")
13
+ set_log_beamline(BL)
14
+ set_utils_beamline(BL)
15
+
16
+
17
+ @device_factory()
18
+ def synchrotron() -> Synchrotron:
19
+ return Synchrotron()
20
+
21
+
22
+ @device_factory()
23
+ def pgm() -> PGM:
24
+ return PGM(
25
+ prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:",
26
+ grating=Grating,
27
+ )
dodal/beamlines/i22.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from ophyd_async.epics.adaravis import AravisDetector
4
- from ophyd_async.epics.adcore import NDPluginBaseIO
4
+ from ophyd_async.epics.adcore import NDPluginBaseIO, NDPluginStatsIO
5
5
  from ophyd_async.epics.adpilatus import PilatusDetector
6
6
  from ophyd_async.fastcs.panda import HDFPanda
7
7
 
@@ -68,6 +68,11 @@ def saxs() -> PilatusDetector:
68
68
  drv_suffix=CAM_SUFFIX,
69
69
  fileio_suffix=HDF5_SUFFIX,
70
70
  metadata_holder=metadata_holder,
71
+ plugins={
72
+ "stats": NDPluginStatsIO(
73
+ prefix=f"{PREFIX.beamline_prefix}-EA-PILAT-01:STAT:"
74
+ )
75
+ },
71
76
  )
72
77
 
73
78
 
@@ -93,6 +98,11 @@ def waxs() -> PilatusDetector:
93
98
  drv_suffix=CAM_SUFFIX,
94
99
  fileio_suffix=HDF5_SUFFIX,
95
100
  metadata_holder=metadata_holder,
101
+ plugins={
102
+ "stats": NDPluginStatsIO(
103
+ prefix=f"{PREFIX.beamline_prefix}-EA-PILAT-03:STAT:"
104
+ )
105
+ },
96
106
  )
97
107
 
98
108
 
@@ -272,7 +282,7 @@ def linkam() -> Linkam3:
272
282
  return Linkam3(prefix=f"{PREFIX.beamline_prefix}-EA-TEMPC-05:")
273
283
 
274
284
 
275
- @device_factory()
285
+ @device_factory(skip=True)
276
286
  def ppump() -> WatsonMarlow323Pump:
277
287
  """Sample Environment Peristaltic Pump"""
278
288
  return WatsonMarlow323Pump(f"{PREFIX.beamline_prefix}-EA-PUMP-01:")
dodal/beamlines/i24.py CHANGED
@@ -1,13 +1,22 @@
1
+ from pathlib import PurePath
2
+
3
+ from ophyd_async.core import AutoIncrementingPathProvider, StaticFilenameProvider
4
+
1
5
  from dodal.common.beamlines.beamline_utils import (
2
6
  BL,
3
7
  device_factory,
4
8
  )
5
9
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
6
- from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator
10
+ from dodal.devices.attenuator.attenuator import EnumFilterAttenuator
11
+ from dodal.devices.attenuator.filter_selections import (
12
+ I24_FilterOneSelections,
13
+ I24_FilterTwoSelections,
14
+ )
7
15
  from dodal.devices.hutch_shutter import HutchShutter
8
16
  from dodal.devices.i24.aperture import Aperture
9
17
  from dodal.devices.i24.beam_center import DetectorBeamCenter
10
18
  from dodal.devices.i24.beamstop import Beamstop
19
+ from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
11
20
  from dodal.devices.i24.dcm import DCM
12
21
  from dodal.devices.i24.dual_backlight import DualBacklight
13
22
  from dodal.devices.i24.focus_mirrors import FocusMirrorsMode
@@ -44,12 +53,13 @@ PREFIX = BeamlinePrefix(BL)
44
53
 
45
54
 
46
55
  @device_factory()
47
- def attenuator() -> ReadOnlyAttenuator:
56
+ def attenuator() -> EnumFilterAttenuator:
48
57
  """Get a read-only attenuator device for i24, instantiate it if it hasn't already
49
58
  been. If this is called when already instantiated in i24, it will return the
50
59
  existing object."""
51
- return ReadOnlyAttenuator(
60
+ return EnumFilterAttenuator(
52
61
  f"{PREFIX.beamline_prefix}-OP-ATTN-01:",
62
+ filter_selection=(I24_FilterOneSelections, I24_FilterTwoSelections),
53
63
  )
54
64
 
55
65
 
@@ -187,3 +197,22 @@ def eiger_beam_center() -> DetectorBeamCenter:
187
197
  f"{PREFIX.beamline_prefix}-EA-EIGER-01:CAM:",
188
198
  "eiger_bc",
189
199
  )
200
+
201
+
202
+ @device_factory()
203
+ def commissioning_jungfrau(
204
+ path_to_dir: str = "/tmp/jf", # Device factory doesn't allow for required args,
205
+ filename: str = "jf_output", # but these should be manually entered when commissioning
206
+ ) -> CommissioningJungfrau:
207
+ """Get the commissionning Jungfrau 9M device, which uses a temporary filewriter
208
+ device in place of Odin while the detector is in commissioning.
209
+ Instantiates the device if it hasn't already been.
210
+ If this is called when already instantiated, it will return the existing object."""
211
+
212
+ return CommissioningJungfrau(
213
+ f"{PREFIX.beamline_prefix}-EA-JFRAU-01:",
214
+ f"{PREFIX.beamline_prefix}-JUNGFRAU-META:FD:",
215
+ AutoIncrementingPathProvider(
216
+ StaticFilenameProvider(filename), PurePath(path_to_dir)
217
+ ),
218
+ )
dodal/beamlines/k07.py ADDED
@@ -0,0 +1,31 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+ from dodal.common.beamlines.beamline_utils import (
4
+ device_factory,
5
+ )
6
+ from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
7
+ from dodal.devices.pgm import PGM
8
+ from dodal.devices.synchrotron import Synchrotron
9
+ from dodal.log import set_beamline as set_log_beamline
10
+ from dodal.utils import BeamlinePrefix, get_beamline_name
11
+
12
+ BL = get_beamline_name("k07")
13
+ PREFIX = BeamlinePrefix(BL)
14
+ set_log_beamline(BL)
15
+ set_utils_beamline(BL)
16
+
17
+
18
+ @device_factory()
19
+ def synchrotron() -> Synchrotron:
20
+ return Synchrotron()
21
+
22
+
23
+ # Grating does not exist yet - this class is a placeholder for when it does
24
+ class Grating(StrictEnum):
25
+ NO_GRATING = "No Grating"
26
+
27
+
28
+ # Grating does not exist yet - this class is a placeholder for when it does
29
+ @device_factory(skip=True)
30
+ def pgm() -> PGM:
31
+ return PGM(prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:", grating=Grating)
dodal/beamlines/p60.py CHANGED
@@ -2,8 +2,8 @@ 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 import SelectedSource
6
- from dodal.devices.electron_analyser.vgscienta import VGScientaAnalyserDriverIO
5
+ from dodal.devices.electron_analyser import DualEnergySource
6
+ from dodal.devices.electron_analyser.vgscienta import VGScientaDetector
7
7
  from dodal.devices.p60 import (
8
8
  LabXraySource,
9
9
  LabXraySourceReadable,
@@ -30,18 +30,19 @@ def mg_kalpha_source() -> LabXraySourceReadable:
30
30
  return LabXraySourceReadable(LabXraySource.MG_KALPHA)
31
31
 
32
32
 
33
+ @device_factory()
34
+ def energy_source() -> DualEnergySource:
35
+ return DualEnergySource(al_kalpha_source().energy_ev, mg_kalpha_source().energy_ev)
36
+
37
+
33
38
  # Connect will work again after this work completed
34
39
  # https://jira.diamond.ac.uk/browse/P60-13
35
40
  @device_factory()
36
- def analyser_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]:
37
- energy_sources = {
38
- SelectedSource.SOURCE1: al_kalpha_source().energy_ev,
39
- SelectedSource.SOURCE2: mg_kalpha_source().energy_ev,
40
- }
41
- return VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy](
41
+ def r4000() -> VGScientaDetector[LensMode, PsuMode, PassEnergy]:
42
+ return VGScientaDetector[LensMode, PsuMode, PassEnergy](
42
43
  prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:",
43
44
  lens_mode_type=LensMode,
44
45
  psu_mode_type=PsuMode,
45
46
  pass_energy_type=PassEnergy,
46
- energy_sources=energy_sources,
47
+ energy_source=energy_source(),
47
48
  )
@@ -12,7 +12,6 @@ class _LogOnPercentageProgressWatcher(Watcher[Number]):
12
12
  message_prefix: str,
13
13
  percent_interval: Number = 25,
14
14
  ):
15
- status.watch(self)
16
15
  self.percent_interval = percent_interval
17
16
  self._current_percent_interval = 0
18
17
  self.message_prefix = message_prefix
@@ -20,6 +19,7 @@ class _LogOnPercentageProgressWatcher(Watcher[Number]):
20
19
  raise ValueError(
21
20
  f"Percent interval on class _LogOnPercentageProgressWatcher must be a positive number, but received {self.percent_interval}"
22
21
  )
22
+ status.watch(self)
23
23
 
24
24
  def __call__(
25
25
  self,
@@ -2,7 +2,7 @@ import abc
2
2
  import asyncio
3
3
  from dataclasses import dataclass
4
4
  from math import isclose
5
- from typing import Any, Generic, TypeVar
5
+ from typing import Generic, Protocol, TypeVar
6
6
 
7
7
  import numpy as np
8
8
  from bluesky.protocols import Movable
@@ -18,7 +18,6 @@ from ophyd_async.core import (
18
18
  wait_for_value,
19
19
  )
20
20
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
21
- from pydantic import BaseModel, ConfigDict, RootModel
22
21
 
23
22
  from dodal.log import LOGGER
24
23
 
@@ -49,46 +48,6 @@ class Apple2Val:
49
48
  btm_outer: str
50
49
 
51
50
 
52
- class EnergyMinMax(BaseModel):
53
- Minimum: float
54
- Maximum: float
55
-
56
-
57
- class EnergyCoverageEntry(BaseModel):
58
- model_config = ConfigDict(arbitrary_types_allowed=True)
59
- Low: float
60
- High: float
61
- Poly: np.poly1d
62
-
63
-
64
- class EnergyCoverage(RootModel):
65
- root: dict[str, EnergyCoverageEntry]
66
-
67
-
68
- class LookupTableEntries(BaseModel):
69
- Energies: EnergyCoverage
70
- Limit: EnergyMinMax
71
-
72
-
73
- class Lookuptable(RootModel):
74
- """BaseModel class for the lookup table.
75
- Apple2 lookup table should be in this format.
76
-
77
- {mode: {'Energies': {Any: {'Low': float,
78
- 'High': float,
79
- 'Poly':np.poly1d
80
- }
81
- }
82
- 'Limit': {'Minimum': float,
83
- 'Maximum': float
84
- }
85
- }
86
- }
87
- """
88
-
89
- root: dict[str, LookupTableEntries]
90
-
91
-
92
51
  class Pol(StrictEnum):
93
52
  NONE = "None"
94
53
  LH = "lh"
@@ -342,6 +301,12 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
342
301
  )
343
302
 
344
303
 
304
+ class EnergyMotorConvertor(Protocol):
305
+ def __call__(self, energy: float, pol: Pol) -> tuple[float, float]:
306
+ """Protocol to provide energy to motor position convertion"""
307
+ ...
308
+
309
+
345
310
  class Apple2(abc.ABC, StandardReadable, Movable):
346
311
  """
347
312
  Apple2 Undulator Device
@@ -353,9 +318,8 @@ class Apple2(abc.ABC, StandardReadable, Movable):
353
318
  The class is designed to manage the undulator's gap, phase motors, and polarisation settings, while
354
319
  abstracting hardware interactions and providing a high-level interface for beamline operations.
355
320
 
356
-
357
- A pair of look up tables are needed to provide the conversion between motor position
358
- and energy.
321
+ The class is abstract and requires beamline-specific implementations for set motor
322
+ positions based on energy and polarisation.
359
323
 
360
324
  Attributes
361
325
  ----------
@@ -371,27 +335,22 @@ class Apple2(abc.ABC, StandardReadable, Movable):
371
335
  A hardware-backed signal for polarisation readback and control.
372
336
  lookup_tables : dict
373
337
  A dictionary storing lookup tables for gap and phase motor positions, used for energy and polarisation conversion.
374
- _available_pol : list
375
- A list of available polarisations supported by the device.
338
+ energy_to_motor : EnergyMotorConvertor
339
+ A callable that converts energy and polarisation to motor positions.
376
340
 
377
341
  Abstract Methods
378
342
  ----------------
379
343
  set(value: float) -> None
380
344
  Abstract method to set motor positions for a given energy and polarisation.
381
- update_lookuptable() -> None
382
- Abstract method to load and validate lookup tables from external sources.
383
345
 
384
346
  Methods
385
347
  -------
386
- _set_pol_setpoint(pol: Pol) -> None
387
- Sets the polarisation setpoint without moving hardware.
388
348
  determine_phase_from_hardware(...) -> tuple[Pol, float]
389
349
  Determines the polarisation and phase value based on motor positions.
390
350
 
391
351
  Notes
392
352
  -----
393
353
  - This class requires beamline-specific implementations of the abstract methods.
394
- - The lookup tables must follow the `Lookuptable` format and be validated before use.
395
354
  - The device supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
396
355
  positive circular (PC), negative circular (NC), and linear arbitrary (LA).
397
356
 
@@ -406,6 +365,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
406
365
  self,
407
366
  id_gap: UndulatorGap,
408
367
  id_phase: UndulatorPhaseAxes,
368
+ energy_motor_convertor: EnergyMotorConvertor,
409
369
  name: str = "",
410
370
  ) -> None:
411
371
  """
@@ -414,16 +374,13 @@ class Apple2(abc.ABC, StandardReadable, Movable):
414
374
  ----------
415
375
  id_gap: An UndulatorGap device.
416
376
  id_phase: An UndulatorPhaseAxes device.
417
- prefix: Not in use but needed for device_instantiation.
377
+ energy_motor_convertor: A callable that converts energy and polarisation to motor positions.
418
378
  name: Name of the device.
419
379
  """
420
- super().__init__(name)
421
380
 
422
- # Attributes are set after super call so they are not renamed to
423
- # <name>-undulator, etc.
424
381
  self.gap = id_gap
425
382
  self.phase = id_phase
426
-
383
+ self.energy_to_motor = energy_motor_convertor
427
384
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
428
385
  # Store the set energy for readback.
429
386
  self.energy, self._set_energy_rbv = soft_signal_r_and_setter(
@@ -435,11 +392,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
435
392
  self.polarisation_setpoint, self._polarisation_setpoint_set = (
436
393
  soft_signal_r_and_setter(Pol)
437
394
  )
438
- # This store two lookup tables, Gap and Phase in the Lookuptable format
439
- self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
440
- "Gap": {},
441
- "Phase": {},
442
- }
395
+
443
396
  # Hardware backed read/write for polarisation.
444
397
  self.polarisation = derived_signal_rw(
445
398
  raw_to_derived=self._read_pol,
@@ -451,13 +404,7 @@ class Apple2(abc.ABC, StandardReadable, Movable):
451
404
  btm_outer=self.phase.btm_outer.user_readback,
452
405
  gap=id_gap.user_readback,
453
406
  )
454
-
455
- self._available_pol = []
456
- """
457
- Abstract method that run at start up to load lookup tables into self.lookup_tables
458
- and set available_pol.
459
- """
460
- self.update_lookuptable()
407
+ super().__init__(name)
461
408
 
462
409
  def _set_pol_setpoint(self, pol: Pol) -> None:
463
410
  """Set the polarisation setpoint without moving hardware. The polarisation
@@ -488,8 +435,8 @@ class Apple2(abc.ABC, StandardReadable, Movable):
488
435
 
489
436
  Examples
490
437
  --------
491
- >>> RE( id.set(888.0)) # This will set the ID to 888 eV
492
- >>> RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
438
+ RE( id.set(888.0)) # This will set the ID to 888 eV
439
+ RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
493
440
  """
494
441
 
495
442
  def _read_pol(
@@ -551,77 +498,6 @@ class Apple2(abc.ABC, StandardReadable, Movable):
551
498
  await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
552
499
  self._set_energy_rbv(energy) # Update energy after move for readback.
553
500
 
554
- async def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
555
- """
556
- Converts energy and polarisation to gap and phase.
557
- """
558
- gap_poly = await self._get_poly(
559
- lookup_table=self.lookup_tables["Gap"], new_energy=energy
560
- )
561
- phase_poly = await self._get_poly(
562
- lookup_table=self.lookup_tables["Phase"], new_energy=energy
563
- )
564
- return gap_poly(energy), phase_poly(energy)
565
-
566
- async def _get_poly(
567
- self,
568
- new_energy: float,
569
- lookup_table: dict[str | None, dict[str, dict[str, Any]]],
570
- ) -> np.poly1d:
571
- """
572
- Get the correct polynomial for a given energy form lookuptable
573
- for the current polarisation setpoint.
574
- Parameters
575
- ----------
576
- new_energy : float
577
- The energy in eV for which the polynomial is requested.
578
- lookup_table : dict[str | None, dict[str, dict[str, Any]]]
579
- The lookup table containing polynomial coefficients for different energies
580
- and polarisations.
581
- Returns
582
- -------
583
- np.poly1d
584
- The polynomial coefficients for the requested energy and polarisation.
585
- Raises
586
- ------
587
- ValueError
588
- If the requested energy is outside the limits defined in the lookup table
589
- or if no polynomial coefficients are found for the requested energy.
590
- """
591
- pol = await self.polarisation_setpoint.get_value()
592
- if (
593
- new_energy < lookup_table[pol]["Limit"]["Minimum"]
594
- or new_energy > lookup_table[pol]["Limit"]["Maximum"]
595
- ):
596
- raise ValueError(
597
- "Demanding energy must lie between {} and {} eV!".format(
598
- lookup_table[pol]["Limit"]["Minimum"],
599
- lookup_table[pol]["Limit"]["Maximum"],
600
- )
601
- )
602
- else:
603
- for energy_range in lookup_table[pol]["Energies"].values():
604
- if (
605
- new_energy >= energy_range["Low"]
606
- and new_energy < energy_range["High"]
607
- ):
608
- return energy_range["Poly"]
609
-
610
- raise ValueError(
611
- """Cannot find polynomial coefficients for your requested energy.
612
- There might be gap in the calibration lookup table."""
613
- )
614
-
615
- @abc.abstractmethod
616
- def update_lookuptable(self) -> None:
617
- """
618
- Abstract method to update the stored lookup tabled from file.
619
- This function should include check to ensure the lookuptable is in the correct format:
620
- # ensure the importing lookup table is the correct format
621
- Lookuptable.model_validate(<loockuptable>)
622
-
623
- """
624
-
625
501
  def determine_phase_from_hardware(
626
502
  self,
627
503
  top_outer: float,
@@ -7,6 +7,7 @@ from ophyd_async.core import (
7
7
  DeviceVector,
8
8
  SignalR,
9
9
  StandardReadable,
10
+ StrictEnum,
10
11
  SubsetEnum,
11
12
  wait_for_value,
12
13
  )
@@ -26,6 +27,9 @@ class ReadOnlyAttenuator(StandardReadable):
26
27
 
27
28
  def __init__(self, prefix: str, name: str = "") -> None:
28
29
  with self.add_children_as_readables():
30
+ # Closest obtainable transmission to the current desired transmission given the specific
31
+ # set of filters in the attenuator. This value updates immediately after setting desired
32
+ # transmission, before the motors may have finished moving. It is not a readback value.
29
33
  self.actual_transmission = epics_signal_r(float, prefix + "MATCH")
30
34
 
31
35
  super().__init__(name)
@@ -92,7 +96,18 @@ class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
92
96
  )
93
97
 
94
98
 
95
- class EnumFilterAttenuator(ReadOnlyAttenuator):
99
+ # Replace with ophyd async enum after https://github.com/bluesky/ophyd-async/pull/1067
100
+ class YesNo(StrictEnum):
101
+ YES = "YES"
102
+ NO = "NO"
103
+
104
+
105
+ # Time given to allow for motors to begin moving after the desired transmission has been set,
106
+ # so that we can work out when the set is complete.
107
+ ENUM_ATTENUATOR_SETTLE_TIME_S = 0.15
108
+
109
+
110
+ class EnumFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
96
111
  """The attenuator will insert filters into the beam to reduce its transmission.
97
112
 
98
113
  This device is currently working, but feature incomplete. See https://github.com/DiamondLightSource/dodal/issues/972
@@ -107,11 +122,42 @@ class EnumFilterAttenuator(ReadOnlyAttenuator):
107
122
  filter_selection: tuple[type[SubsetEnum], ...],
108
123
  name: str = "",
109
124
  ):
125
+ self._auto_move_on_desired_transmission_set = epics_signal_rw(
126
+ YesNo, prefix + "AUTOMOVE"
127
+ )
128
+ self._desired_transmission = epics_signal_rw(float, prefix + "T2A:SETVAL1")
129
+ self._use_current_energy = epics_signal_x(prefix + "E2WL:USECURRENTENERGY.PROC")
130
+
110
131
  with self.add_children_as_readables():
111
- self.filters: DeviceVector[FilterMotor] = DeviceVector(
132
+ self._filters: DeviceVector[FilterMotor] = DeviceVector(
112
133
  {
113
134
  index: FilterMotor(f"{prefix}MP{index + 1}:", filter, name)
114
135
  for index, filter in enumerate(filter_selection)
115
136
  }
116
137
  )
117
138
  super().__init__(prefix, name=name)
139
+
140
+ @AsyncStatus.wrap
141
+ async def set(self, value: float):
142
+ """Set the transmission to the fractional (0-1) value given.
143
+
144
+ The attenuator IOC will then insert filters to reach the desired transmission for
145
+ the current beamline energy, the set will only complete when they have all been
146
+ applied.
147
+ """
148
+
149
+ # auto move should normally be on, but check here incase it was manually turned off
150
+ await self._auto_move_on_desired_transmission_set.set(YesNo.YES)
151
+
152
+ # Currently uncertain if _use_current_energy correctly waits for completion: https://github.com/DiamondLightSource/dodal/issues/1588
153
+ await self._use_current_energy.trigger()
154
+ await self._desired_transmission.set(value)
155
+
156
+ # Give EPICS a chance to start moving the filter motors. Not needed after
157
+ # a transmission readback PV is added at the controls level: https://jira.diamond.ac.uk/browse/I24-725
158
+ await asyncio.sleep(ENUM_ATTENUATOR_SETTLE_TIME_S)
159
+ coros = [
160
+ wait_for_value(self._filters[i].done_move, 1, timeout=DEFAULT_TIMEOUT)
161
+ for i in self._filters
162
+ ]
163
+ await asyncio.gather(*coros)
@@ -8,4 +8,7 @@ class FilterMotor(StandardReadable):
8
8
  ):
9
9
  with self.add_children_as_readables():
10
10
  self.user_setpoint = epics_signal_rw(filter_selections, f"{prefix}SELECT")
11
+ self.done_move = epics_signal_rw(
12
+ int, f"{prefix}DMOV"
13
+ ) # 1 for yes, 0 for no
11
14
  super().__init__(name=name)