Skip to content

Commit e0d5a17

Browse files
Add DualFastShutter (#1795)
* Add DualFastShutter * Add doc strings to shutters * Add tests for dual_fast_shutter * Added tests for read * Use asycio.gather in test * Improve I09 beamline device layout * Added doc string to SourceSelector and get_obj_from_selected_source * Added test_selectable_source * Added additional tests for selectable_source * minor test update * Added read only signals for shutter_device_name * Fixed tests * Fix test flakeyness * Fix controller arg * Fixed flakey test * Update doc string * Remove pyproject.toml changes as now added to ophyd-async main * Imrpove doc strings * Setup new test names * Revert remaining changes to pyproject.toml * Added dual_fast_shutter_fixture * Added code coverage * Restored pyproject.toml
1 parent 1fdd7db commit e0d5a17

File tree

18 files changed

+605
-142
lines changed

18 files changed

+605
-142
lines changed

src/dodal/beamlines/i09.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from ophyd_async.core import InOut
2+
13
from dodal.common.beamlines.beamline_utils import (
24
device_factory,
35
)
@@ -7,10 +9,14 @@
79
PitchAndRollCrystal,
810
StationaryCrystal,
911
)
10-
from dodal.devices.electron_analyser.base import DualEnergySource
12+
from dodal.devices.electron_analyser.base import (
13+
DualEnergySource,
14+
)
1115
from dodal.devices.electron_analyser.vgscienta import VGScientaDetector
16+
from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter
1217
from dodal.devices.i09 import Grating, LensMode, PassEnergy, PsuMode
1318
from dodal.devices.pgm import PlaneGratingMonochromator
19+
from dodal.devices.selectable_source import SourceSelector
1420
from dodal.devices.synchrotron import Synchrotron
1521
from dodal.devices.temperture_controller import (
1622
Lakeshore336,
@@ -19,7 +25,8 @@
1925
from dodal.utils import BeamlinePrefix, get_beamline_name
2026

2127
BL = get_beamline_name("i09")
22-
PREFIX = BeamlinePrefix(BL)
28+
I_PREFIX = BeamlinePrefix(BL, suffix="I")
29+
J_PREFIX = BeamlinePrefix(BL, suffix="J")
2330
set_log_beamline(BL)
2431
set_utils_beamline(BL)
2532

@@ -29,36 +36,65 @@ def synchrotron() -> Synchrotron:
2936
return Synchrotron()
3037

3138

39+
@device_factory()
40+
def source_selector() -> SourceSelector:
41+
return SourceSelector()
42+
43+
3244
@device_factory()
3345
def pgm() -> PlaneGratingMonochromator:
3446
return PlaneGratingMonochromator(
35-
prefix=f"{BeamlinePrefix(BL, suffix='J').beamline_prefix}-MO-PGM-01:",
36-
grating=Grating,
47+
prefix=f"{J_PREFIX.beamline_prefix}-MO-PGM-01:", grating=Grating
3748
)
3849

3950

4051
@device_factory()
4152
def dcm() -> DoubleCrystalMonochromatorWithDSpacing:
4253
return DoubleCrystalMonochromatorWithDSpacing(
43-
f"{PREFIX.beamline_prefix}-MO-DCM-01:", PitchAndRollCrystal, StationaryCrystal
54+
f"{I_PREFIX.beamline_prefix}-MO-DCM-01:", PitchAndRollCrystal, StationaryCrystal
55+
)
56+
57+
58+
@device_factory()
59+
def dual_energy_source() -> DualEnergySource:
60+
return DualEnergySource(
61+
dcm().energy_in_eV,
62+
pgm().energy.user_readback,
63+
source_selector().selected_source,
64+
)
65+
66+
67+
@device_factory()
68+
def fsi1() -> GenericFastShutter[InOut]:
69+
return GenericFastShutter[InOut](
70+
f"{I_PREFIX.beamline_prefix}-EA-FSHTR-01:CTRL", InOut.OUT, InOut.IN
71+
)
72+
73+
74+
@device_factory()
75+
def fsj1() -> GenericFastShutter[InOut]:
76+
return GenericFastShutter[InOut](
77+
f"{J_PREFIX.beamline_prefix}-EA-FSHTR-01:CTRL", InOut.OUT, InOut.IN
4478
)
4579

4680

4781
@device_factory()
48-
def energy_source() -> DualEnergySource:
49-
return DualEnergySource(dcm().energy_in_eV, pgm().energy.user_readback)
82+
def dual_fast_shutter() -> DualFastShutter[InOut]:
83+
return DualFastShutter[InOut](fsi1(), fsj1(), source_selector().selected_source)
5084

5185

5286
# Connect will work again after this work completed
5387
# https://jira.diamond.ac.uk/browse/I09-651
5488
@device_factory()
5589
def ew4000() -> VGScientaDetector[LensMode, PsuMode, PassEnergy]:
5690
return VGScientaDetector[LensMode, PsuMode, PassEnergy](
57-
prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:",
91+
prefix=f"{I_PREFIX.beamline_prefix}-EA-DET-01:CAM:",
5892
lens_mode_type=LensMode,
5993
psu_mode_type=PsuMode,
6094
pass_energy_type=PassEnergy,
61-
energy_source=energy_source(),
95+
energy_source=dual_energy_source(),
96+
shutter=dual_fast_shutter(),
97+
source_selector=source_selector(),
6298
)
6399

64100

src/dodal/beamlines/p60.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
PassEnergy,
1212
PsuMode,
1313
)
14+
from dodal.devices.selectable_source import SourceSelector
1415
from dodal.log import set_beamline as set_log_beamline
1516
from dodal.utils import BeamlinePrefix, get_beamline_name
1617

@@ -20,6 +21,11 @@
2021
set_utils_beamline(BL)
2122

2223

24+
@device_factory()
25+
def source_selector() -> SourceSelector:
26+
return SourceSelector()
27+
28+
2329
@device_factory()
2430
def al_kalpha_source() -> LabXraySourceReadable:
2531
return LabXraySourceReadable(LabXraySource.AL_KALPHA)
@@ -32,7 +38,11 @@ def mg_kalpha_source() -> LabXraySourceReadable:
3238

3339
@device_factory()
3440
def energy_source() -> DualEnergySource:
35-
return DualEnergySource(al_kalpha_source().energy_ev, mg_kalpha_source().energy_ev)
41+
return DualEnergySource(
42+
al_kalpha_source().energy_ev,
43+
mg_kalpha_source().energy_ev,
44+
source_selector().selected_source,
45+
)
3646

3747

3848
# Connect will work again after this work completed

src/dodal/devices/electron_analyser/base/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
GenericAnalyserDriverIO,
1616
TAbstractAnalyserDriverIO,
1717
)
18-
from .base_enums import EnergyMode, SelectedSource
18+
from .base_enums import EnergyMode
1919
from .base_region import (
2020
AbstractBaseRegion,
2121
AbstractBaseSequence,
@@ -27,7 +27,7 @@
2727
TLensMode,
2828
)
2929
from .base_util import to_binding_energy, to_kinetic_energy
30-
from .energy_sources import DualEnergySource, EnergySource
30+
from .energy_sources import AbstractEnergySource, DualEnergySource, EnergySource
3131

3232
__all__ = [
3333
"ElectronAnalyserController",
@@ -42,7 +42,6 @@
4242
"GenericAnalyserDriverIO",
4343
"TAbstractAnalyserDriverIO",
4444
"EnergyMode",
45-
"SelectedSource",
4645
"AbstractBaseRegion",
4746
"AbstractBaseSequence",
4847
"GenericRegion",
@@ -53,6 +52,7 @@
5352
"TLensMode",
5453
"to_binding_energy",
5554
"to_kinetic_energy",
55+
"AbstractEnergySource",
5656
"DualEnergySource",
5757
"EnergySource",
5858
]

src/dodal/devices/electron_analyser/base/base_controller.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
GenericRegion,
1313
TAbstractBaseRegion,
1414
)
15-
from dodal.devices.electron_analyser.base.energy_sources import (
16-
AbstractEnergySource,
17-
DualEnergySource,
18-
)
15+
from dodal.devices.electron_analyser.base.energy_sources import AbstractEnergySource
16+
from dodal.devices.fast_shutter import FastShutter
17+
from dodal.devices.selectable_source import SourceSelector
1918

2019

2120
class ElectronAnalyserController(
@@ -32,7 +31,9 @@ def __init__(
3231
self,
3332
driver: TAbstractAnalyserDriverIO,
3433
energy_source: AbstractEnergySource,
35-
deadtime: float,
34+
shutter: FastShutter | None = None,
35+
source_selector: SourceSelector | None = None,
36+
deadtime: float = 0,
3637
image_mode: ADImageMode = ADImageMode.SINGLE,
3738
):
3839
"""
@@ -45,13 +46,19 @@ def __init__(
4546
image_mode: The image mode to configure the driver with before measuring.
4647
"""
4748
self.energy_source = energy_source
49+
self.shutter = shutter
50+
self.source_selector = source_selector
4851
super().__init__(driver, deadtime, image_mode)
4952

50-
async def setup_with_region(self, region: TAbstractBaseRegion):
53+
async def setup_with_region(self, region: TAbstractBaseRegion) -> None:
5154
"""Logic to set the driver with a region."""
55+
if self.source_selector is not None:
56+
await self.source_selector.set(region.excitation_energy_source)
57+
58+
# Should this be moved to a VGScientController only?
59+
if self.shutter is not None:
60+
await self.shutter.set(self.shutter.close_state)
5261

53-
if isinstance(self.energy_source, DualEnergySource):
54-
self.energy_source.selected_source.set(region.excitation_energy_source)
5562
excitation_energy = await self.energy_source.energy.get_value()
5663
epics_region = region.prepare_for_epics(excitation_energy)
5764
await self.driver.set(epics_region)
@@ -62,6 +69,10 @@ async def prepare(self, trigger_info: TriggerInfo) -> None:
6269
# axis calculation.
6370
excitation_energy = await self.energy_source.energy.get_value()
6471
await self.driver.cached_excitation_energy.set(excitation_energy)
72+
73+
if self.shutter is not None:
74+
await self.shutter.set(self.shutter.open_state)
75+
6576
await super().prepare(trigger_info)
6677

6778

src/dodal/devices/electron_analyser/base/base_enums.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,3 @@
44
class EnergyMode(StrictEnum):
55
KINETIC = "Kinetic"
66
BINDING = "Binding"
7-
8-
9-
class SelectedSource(StrictEnum):
10-
SOURCE1 = "source1"
11-
SOURCE2 = "source2"

src/dodal/devices/electron_analyser/base/base_region.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
from ophyd_async.core import StrictEnum, SupersetEnum
77
from pydantic import BaseModel, Field, model_validator
88

9-
from dodal.devices.electron_analyser.base.base_enums import EnergyMode, SelectedSource
9+
from dodal.devices.electron_analyser.base.base_enums import EnergyMode
1010
from dodal.devices.electron_analyser.base.base_util import (
1111
to_binding_energy,
1212
to_kinetic_energy,
1313
)
14+
from dodal.devices.selectable_source import SelectedSource
1415

1516
AnyAcqMode: TypeAlias = StrictEnum
1617
AnyLensMode: TypeAlias = SupersetEnum | StrictEnum

src/dodal/devices/electron_analyser/base/energy_sources.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
from ophyd_async.core import (
44
Reference,
55
SignalR,
6+
SignalRW,
67
StandardReadable,
78
StandardReadableFormat,
89
derived_signal_r,
910
soft_signal_r_and_setter,
10-
soft_signal_rw,
1111
)
1212

13-
from dodal.devices.electron_analyser.base.base_enums import SelectedSource
13+
from dodal.devices.selectable_source import SelectedSource, get_obj_from_selected_source
1414

1515

1616
class AbstractEnergySource(StandardReadable):
@@ -51,51 +51,52 @@ def energy(self) -> SignalR[float]:
5151
return self._source_ref()
5252

5353

54+
def get_float_from_selected_source(
55+
selected: SelectedSource, s1: float, s2: float
56+
) -> float:
57+
"""Wrapper function to provide type hints for derived signal."""
58+
return get_obj_from_selected_source(selected, s1, s2)
59+
60+
5461
class DualEnergySource(AbstractEnergySource):
5562
"""
5663
Holds two EnergySource devices and provides a signal to read energy depending on
57-
which source is selected. This is controlled by a selected_source signal which can
58-
switch source using SelectedSource enum. Both sources energy is recorded in the
59-
read, the energy signal is used as a helper signal to know which source is being
60-
used.
64+
which source is selected. The energy is the one that corrosponds to the
65+
selected_source signal. For example, selected_source is source1 if selected_source
66+
is at SelectedSource.SOURCE1 and vise versa for source2 and SelectedSource.SOURCE2.
6167
"""
6268

6369
def __init__(
64-
self, source1: SignalR[float], source2: SignalR[float], name: str = ""
70+
self,
71+
source1: SignalR[float],
72+
source2: SignalR[float],
73+
selected_source: SignalRW[SelectedSource],
74+
name: str = "",
6575
):
6676
"""
6777
Args:
68-
source1: Default energy signal to select.
69-
source2: Secondary energy signal to select.
70-
name: name of this device.
78+
source1: Energy source that corrosponds to SelectedSource.SOURCE1.
79+
source2: Energy source that corrosponds to SelectedSource.SOURCE2.
80+
selected_source: Signal that decides the active energy source.
81+
name: Name of this device.
7182
"""
7283

84+
self.selected_source_ref = Reference(selected_source)
7385
with self.add_children_as_readables():
74-
self.selected_source = soft_signal_rw(
75-
SelectedSource, initial_value=SelectedSource.SOURCE1
76-
)
7786
self.source1 = EnergySource(source1)
7887
self.source2 = EnergySource(source2)
7988

8089
self._selected_energy = derived_signal_r(
81-
self._get_excitation_energy,
90+
get_float_from_selected_source,
8291
"eV",
83-
selected_source=self.selected_source,
84-
source1=self.source1.energy,
85-
source2=self.source2.energy,
92+
selected=self.selected_source_ref(),
93+
s1=self.source1.energy,
94+
s2=self.source2.energy,
8695
)
96+
self.add_readables([selected_source])
8797

8898
super().__init__(name)
8999

90-
def _get_excitation_energy(
91-
self, selected_source: SelectedSource, source1: float, source2: float
92-
) -> float:
93-
match selected_source:
94-
case SelectedSource.SOURCE1:
95-
return source1
96-
case SelectedSource.SOURCE2:
97-
return source2
98-
99100
@property
100101
def energy(self) -> SignalR[float]:
101102
return self._selected_energy

src/dodal/devices/electron_analyser/specs/specs_detector.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55
)
66
from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector
77
from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode
8-
from dodal.devices.electron_analyser.base.energy_sources import (
9-
DualEnergySource,
10-
EnergySource,
11-
)
8+
from dodal.devices.electron_analyser.base.energy_sources import AbstractEnergySource
129
from dodal.devices.electron_analyser.specs.specs_driver_io import SpecsAnalyserDriverIO
1310
from dodal.devices.electron_analyser.specs.specs_region import (
1411
SpecsRegion,
1512
SpecsSequence,
1613
)
14+
from dodal.devices.fast_shutter import FastShutter
15+
from dodal.devices.selectable_source import SourceSelector
1716

1817

1918
class SpecsDetector(
@@ -29,7 +28,9 @@ def __init__(
2928
prefix: str,
3029
lens_mode_type: type[TLensMode],
3130
psu_mode_type: type[TPsuMode],
32-
energy_source: DualEnergySource | EnergySource,
31+
energy_source: AbstractEnergySource,
32+
shutter: FastShutter | None = None,
33+
source_selector: SourceSelector | None = None,
3334
name: str = "",
3435
):
3536
# Save to class so takes part with connect()
@@ -39,7 +40,7 @@ def __init__(
3940

4041
controller = ElectronAnalyserController[
4142
SpecsAnalyserDriverIO[TLensMode, TPsuMode], SpecsRegion[TLensMode, TPsuMode]
42-
](self.driver, energy_source, 0)
43+
](self.driver, energy_source, shutter, source_selector)
4344

4445
sequence_class = SpecsSequence[lens_mode_type, psu_mode_type]
4546

0 commit comments

Comments
 (0)