dls-dodal 1.43.0__py3-none-any.whl → 1.44.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.
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/METADATA +4 -3
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/RECORD +42 -34
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/b01_1.py +8 -0
- dodal/beamlines/i03.py +11 -0
- dodal/beamlines/i13_1.py +22 -48
- dodal/beamlines/i19_1.py +16 -5
- dodal/beamlines/i19_2.py +12 -3
- dodal/beamlines/i19_optics.py +4 -2
- dodal/common/data_util.py +20 -0
- dodal/common/signal_utils.py +43 -4
- dodal/devices/aperturescatterguard.py +3 -3
- dodal/devices/baton.py +17 -0
- dodal/devices/current_amplifiers/current_amplifier.py +1 -6
- dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
- dodal/devices/current_amplifiers/femto.py +0 -5
- dodal/devices/current_amplifiers/sr570.py +0 -5
- dodal/devices/electron_analyser/__init__.py +0 -0
- dodal/devices/electron_analyser/base_region.py +64 -0
- dodal/devices/electron_analyser/specs/__init__.py +0 -0
- dodal/devices/electron_analyser/specs/specs_region.py +24 -0
- dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
- dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +77 -0
- dodal/devices/fast_grid_scan.py +2 -2
- dodal/devices/i03/beamstop.py +2 -2
- dodal/devices/i13_1/merlin.py +1 -2
- dodal/devices/i13_1/merlin_controller.py +12 -8
- dodal/devices/i19/beamstop.py +30 -0
- dodal/devices/i19/hutch_access.py +2 -0
- dodal/devices/i19/shutter.py +52 -30
- dodal/devices/i22/nxsas.py +1 -3
- dodal/devices/i24/focus_mirrors.py +3 -3
- dodal/devices/i24/pilatus_metadata.py +2 -2
- dodal/devices/oav/oav_detector.py +7 -9
- dodal/devices/oav/snapshots/snapshot.py +21 -0
- dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
- dodal/plan_stubs/motor_utils.py +10 -12
- dodal/utils.py +0 -7
- dodal/devices/i13_1/merlin_io.py +0 -17
- dodal/devices/oav/microns_for_zoom_levels.json +0 -55
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info/licenses}/LICENSE +0 -0
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/top_level.txt +0 -0
|
@@ -23,7 +23,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
|
|
|
23
23
|
super().__init__(name)
|
|
24
24
|
|
|
25
25
|
@abstractmethod
|
|
26
|
-
@AsyncStatus.wrap
|
|
27
26
|
async def increase_gain(self, value: int = 1) -> None:
|
|
28
27
|
"""Increase gain, increment by 1 by default.
|
|
29
28
|
|
|
@@ -31,7 +30,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
|
|
|
31
30
|
bool: True if success.
|
|
32
31
|
"""
|
|
33
32
|
|
|
34
|
-
@AsyncStatus.wrap
|
|
35
33
|
@abstractmethod
|
|
36
34
|
async def decrease_gain(self, value: int = 1) -> None:
|
|
37
35
|
"""Decrease gain, decrement by 1 by default.
|
|
@@ -40,21 +38,18 @@ class CurrentAmp(ABC, StandardReadable, Movable):
|
|
|
40
38
|
bool: True if success.
|
|
41
39
|
"""
|
|
42
40
|
|
|
43
|
-
@AsyncStatus.wrap
|
|
44
41
|
@abstractmethod
|
|
45
|
-
async def get_gain(self) ->
|
|
42
|
+
async def get_gain(self) -> Enum:
|
|
46
43
|
"""Get the current gain setting
|
|
47
44
|
|
|
48
45
|
Returns:
|
|
49
46
|
Enum: The member name of the current gain setting in gain_conversion_table.
|
|
50
47
|
"""
|
|
51
48
|
|
|
52
|
-
@AsyncStatus.wrap
|
|
53
49
|
@abstractmethod
|
|
54
50
|
async def get_upperlimit(self) -> float:
|
|
55
51
|
"""Get the upper limit of the current amplifier"""
|
|
56
52
|
|
|
57
|
-
@AsyncStatus.wrap
|
|
58
53
|
@abstractmethod
|
|
59
54
|
async def get_lowerlimit(self) -> float:
|
|
60
55
|
"""Get the lower limit of the current amplifier"""
|
|
@@ -92,8 +92,8 @@ class CurrentAmpDet(StandardReadable, Preparable):
|
|
|
92
92
|
self.current_amp().get_gain(),
|
|
93
93
|
self.counter().get_voltage_per_sec(),
|
|
94
94
|
)
|
|
95
|
-
|
|
96
|
-
corrected_current = voltage_per_sec /
|
|
95
|
+
assert isinstance(current_gain.value, float)
|
|
96
|
+
corrected_current = voltage_per_sec / current_gain.value
|
|
97
97
|
return corrected_current
|
|
98
98
|
|
|
99
99
|
@AsyncStatus.wrap
|
|
@@ -114,7 +114,6 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
114
114
|
# wait for current amplifier's bandpass filter to settle.
|
|
115
115
|
await asyncio.sleep(self.raise_timetable[SEN_setting].value)
|
|
116
116
|
|
|
117
|
-
@AsyncStatus.wrap
|
|
118
117
|
async def increase_gain(self, value: int = 1) -> None:
|
|
119
118
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
120
119
|
current_gain += value
|
|
@@ -122,7 +121,6 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
122
121
|
raise ValueError("Gain at max value")
|
|
123
122
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
124
123
|
|
|
125
|
-
@AsyncStatus.wrap
|
|
126
124
|
async def decrease_gain(self, value: int = 1) -> None:
|
|
127
125
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
128
126
|
current_gain -= value
|
|
@@ -130,14 +128,11 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
130
128
|
raise ValueError("Gain at min value")
|
|
131
129
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
132
130
|
|
|
133
|
-
@AsyncStatus.wrap
|
|
134
131
|
async def get_gain(self) -> Enum:
|
|
135
132
|
return self.gain_conversion_table[(await self.gain.get_value()).name]
|
|
136
133
|
|
|
137
|
-
@AsyncStatus.wrap
|
|
138
134
|
async def get_upperlimit(self) -> float:
|
|
139
135
|
return self.upperlimit
|
|
140
136
|
|
|
141
|
-
@AsyncStatus.wrap
|
|
142
137
|
async def get_lowerlimit(self) -> float:
|
|
143
138
|
return self.lowerlimit
|
|
@@ -178,7 +178,6 @@ class SR570(CurrentAmp):
|
|
|
178
178
|
)
|
|
179
179
|
await asyncio.sleep(self.raise_timetable[coarse_gain.name].value)
|
|
180
180
|
|
|
181
|
-
@AsyncStatus.wrap
|
|
182
181
|
async def increase_gain(self, value=3) -> None:
|
|
183
182
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
184
183
|
current_gain += value
|
|
@@ -189,7 +188,6 @@ class SR570(CurrentAmp):
|
|
|
189
188
|
raise ValueError("Gain at max value")
|
|
190
189
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
191
190
|
|
|
192
|
-
@AsyncStatus.wrap
|
|
193
191
|
async def decrease_gain(self, value=3) -> None:
|
|
194
192
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
195
193
|
current_gain -= value
|
|
@@ -198,17 +196,14 @@ class SR570(CurrentAmp):
|
|
|
198
196
|
raise ValueError("Gain at min value")
|
|
199
197
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
200
198
|
|
|
201
|
-
@AsyncStatus.wrap
|
|
202
199
|
async def get_gain(self) -> Enum:
|
|
203
200
|
result = await asyncio.gather(
|
|
204
201
|
self.coarse_gain.get_value(), self.fine_gain.get_value()
|
|
205
202
|
)
|
|
206
203
|
return self.gain_conversion_table[self.combined_table(result).name]
|
|
207
204
|
|
|
208
|
-
@AsyncStatus.wrap
|
|
209
205
|
async def get_upperlimit(self) -> float:
|
|
210
206
|
return self.upperlimit
|
|
211
207
|
|
|
212
|
-
@AsyncStatus.wrap
|
|
213
208
|
async def get_lowerlimit(self) -> float:
|
|
214
209
|
return self.lowerlimit
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
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()]
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
)
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -23,7 +23,7 @@ from ophyd_async.epics.core import (
|
|
|
23
23
|
from pydantic import field_validator
|
|
24
24
|
from pydantic.dataclasses import dataclass
|
|
25
25
|
|
|
26
|
-
from dodal.common.signal_utils import
|
|
26
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
27
27
|
from dodal.log import LOGGER
|
|
28
28
|
from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
|
|
29
29
|
|
|
@@ -203,7 +203,7 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
203
203
|
self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
|
|
204
204
|
self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
|
|
205
205
|
|
|
206
|
-
self.expected_images =
|
|
206
|
+
self.expected_images = create_r_hardware_backed_soft_signal(
|
|
207
207
|
float, self._calculate_expected_images
|
|
208
208
|
)
|
|
209
209
|
|
dodal/devices/i03/beamstop.py
CHANGED
|
@@ -5,7 +5,7 @@ from ophyd_async.core import StandardReadable, StrictEnum
|
|
|
5
5
|
from ophyd_async.epics.motor import Motor
|
|
6
6
|
|
|
7
7
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
8
|
-
from dodal.common.signal_utils import
|
|
8
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BeamstopPositions(StrictEnum):
|
|
@@ -53,7 +53,7 @@ class Beamstop(StandardReadable):
|
|
|
53
53
|
self.x_mm = Motor(prefix + "X")
|
|
54
54
|
self.y_mm = Motor(prefix + "Y")
|
|
55
55
|
self.z_mm = Motor(prefix + "Z")
|
|
56
|
-
self.selected_pos =
|
|
56
|
+
self.selected_pos = create_r_hardware_backed_soft_signal(
|
|
57
57
|
BeamstopPositions, self._get_selected_position
|
|
58
58
|
)
|
|
59
59
|
|
dodal/devices/i13_1/merlin.py
CHANGED
|
@@ -3,7 +3,6 @@ from ophyd_async.epics import adcore
|
|
|
3
3
|
|
|
4
4
|
from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
|
|
5
5
|
from dodal.devices.i13_1.merlin_controller import MerlinController
|
|
6
|
-
from dodal.devices.i13_1.merlin_io import MerlinDriverIO
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class Merlin(StandardDetector):
|
|
@@ -18,7 +17,7 @@ class Merlin(StandardDetector):
|
|
|
18
17
|
fileio_suffix=HDF5_SUFFIX,
|
|
19
18
|
name: str = "",
|
|
20
19
|
):
|
|
21
|
-
self.drv =
|
|
20
|
+
self.drv = adcore.ADBaseIO(prefix + drv_suffix)
|
|
22
21
|
self.hdf = adcore.NDFileHDFIO(prefix + fileio_suffix)
|
|
23
22
|
|
|
24
23
|
super().__init__(
|
|
@@ -6,17 +6,21 @@ from ophyd_async.core import (
|
|
|
6
6
|
AsyncStatus,
|
|
7
7
|
TriggerInfo,
|
|
8
8
|
)
|
|
9
|
-
from ophyd_async.epics import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
from ophyd_async.epics.adcore import (
|
|
10
|
+
DEFAULT_GOOD_STATES,
|
|
11
|
+
ADBaseController,
|
|
12
|
+
ADBaseIO,
|
|
13
|
+
ADImageMode,
|
|
14
|
+
ADState,
|
|
15
|
+
stop_busy_record,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class MerlinController(ADBaseController):
|
|
16
20
|
def __init__(
|
|
17
21
|
self,
|
|
18
|
-
driver:
|
|
19
|
-
good_states: frozenset[
|
|
22
|
+
driver: ADBaseIO,
|
|
23
|
+
good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
|
|
20
24
|
) -> None:
|
|
21
25
|
self.driver = driver
|
|
22
26
|
self.good_states = good_states
|
|
@@ -34,7 +38,7 @@ class MerlinController(ADBaseController):
|
|
|
34
38
|
)
|
|
35
39
|
await asyncio.gather(
|
|
36
40
|
self.driver.num_images.set(trigger_info.total_number_of_triggers),
|
|
37
|
-
self.driver.image_mode.set(
|
|
41
|
+
self.driver.image_mode.set(ADImageMode.MULTIPLE),
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
async def wait_for_idle(self):
|
|
@@ -44,4 +48,4 @@ class MerlinController(ADBaseController):
|
|
|
44
48
|
async def disarm(self):
|
|
45
49
|
# We can't use caput callback as we already used it in arm() and we can't have
|
|
46
50
|
# 2 or they will deadlock
|
|
47
|
-
await
|
|
51
|
+
await stop_busy_record(self.driver.acquire, False, timeout=1)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw, epics_signal_x
|
|
3
|
+
from ophyd_async.epics.motor import Motor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HomeGroup(StrictEnum):
|
|
7
|
+
NONE = "none"
|
|
8
|
+
ALL = "All"
|
|
9
|
+
X = "X"
|
|
10
|
+
Y = "Y"
|
|
11
|
+
Z = "Z"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HomingControl(StandardReadable):
|
|
15
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
16
|
+
self.homing_group = epics_signal_rw(HomeGroup, f"{prefix}:HMGRP")
|
|
17
|
+
self.home = epics_signal_x(f"{prefix}:HOME")
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BeamStop(StandardReadable):
|
|
22
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
23
|
+
with self.add_children_as_readables():
|
|
24
|
+
self.x = Motor(f"{prefix}X")
|
|
25
|
+
self.y = Motor(f"{prefix}Y")
|
|
26
|
+
self.z = Motor(f"{prefix}Z")
|
|
27
|
+
|
|
28
|
+
self.homing = HomingControl(f"{prefix}HM", name)
|
|
29
|
+
|
|
30
|
+
super().__init__(name)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from ophyd_async.core import StandardReadable
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_r
|
|
3
3
|
|
|
4
|
+
ACCESS_DEVICE_NAME = "access_control" # Device name in i19-blueapi
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class HutchAccessControl(StandardReadable):
|
|
6
8
|
def __init__(self, prefix: str, name: str = "") -> None:
|
dodal/devices/i19/shutter.py
CHANGED
|
@@ -1,57 +1,79 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
+
from aiohttp import ClientSession
|
|
3
4
|
from bluesky.protocols import Movable
|
|
4
|
-
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
5
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
|
|
5
6
|
from ophyd_async.epics.core import epics_signal_r
|
|
6
7
|
|
|
7
|
-
from dodal.devices.hutch_shutter import
|
|
8
|
+
from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
|
|
9
|
+
from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
|
|
8
10
|
from dodal.log import LOGGER
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
class HutchInvalidError(Exception):
|
|
12
|
-
pass
|
|
12
|
+
OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class HutchState(str, Enum):
|
|
16
16
|
EH1 = "EH1"
|
|
17
17
|
EH2 = "EH2"
|
|
18
|
-
INVALID = "INVALID"
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
class
|
|
20
|
+
class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
|
|
22
21
|
""" I19-specific device to operate the hutch shutter.
|
|
23
22
|
|
|
24
|
-
This device
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
This device will send a REST call to the blueapi instance controlling the optics \
|
|
24
|
+
hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
|
|
25
|
+
the hutch sending the request and decide if the plan will be run or not.
|
|
27
26
|
As the two hutches are located in series, checking the hutch in use is necessary to \
|
|
28
27
|
avoid accidentally operating the shutter from one hutch while the other has beamtime.
|
|
29
28
|
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
The name of the hutch that wants to operate the shutter should be passed to the \
|
|
30
|
+
device upon instantiation.
|
|
31
|
+
|
|
32
|
+
For details see the architecture described in \
|
|
33
|
+
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
self.hutch_state = epics_signal_r(str, f"{bl_prefix}-OP-STAT-01:EHStatus.VALA")
|
|
37
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
38
|
+
self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
|
|
40
39
|
self.hutch_request = hutch
|
|
40
|
+
self.url = OPTICS_BLUEAPI_URL
|
|
41
41
|
super().__init__(name)
|
|
42
42
|
|
|
43
43
|
@AsyncStatus.wrap
|
|
44
44
|
async def set(self, value: ShutterDemand):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
REQUEST_PARAMS = {
|
|
46
|
+
"name": "operate_shutter_plan",
|
|
47
|
+
"params": {
|
|
48
|
+
"experiment_hutch": self.hutch_request.value,
|
|
49
|
+
"access_device": ACCESS_DEVICE_NAME,
|
|
50
|
+
"shutter_demand": value,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
async with ClientSession(base_url=self.url, raise_for_status=True) as session:
|
|
54
|
+
# First submit the plan to the worker
|
|
55
|
+
async with session.post("/tasks", data=REQUEST_PARAMS) as response:
|
|
56
|
+
LOGGER.debug(
|
|
57
|
+
f"Task submitted to the worker, response status: {response.status}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
data = await response.json()
|
|
62
|
+
task_id = data["task_id"]
|
|
63
|
+
except Exception as e:
|
|
64
|
+
LOGGER.error(
|
|
65
|
+
f"Failed to get task_id from {self.url}/tasks POST. ({e})"
|
|
66
|
+
)
|
|
67
|
+
raise
|
|
68
|
+
# Then set the task as active and run asap
|
|
69
|
+
async with session.put(
|
|
70
|
+
"/worker/tasks", data={"task_id": task_id}
|
|
71
|
+
) as response:
|
|
72
|
+
if not response.ok:
|
|
73
|
+
LOGGER.error(
|
|
74
|
+
f"""Unable to operate the shutter.
|
|
75
|
+
Session PUT responded with {response.status}: {response.reason}.
|
|
76
|
+
"""
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
LOGGER.debug(f"Run operate shutter plan, task_id: {task_id}")
|
dodal/devices/i22/nxsas.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TypeVar
|
|
|
6
6
|
from bluesky.protocols import Reading
|
|
7
7
|
from event_model.documents.event_descriptor import DataKey
|
|
8
8
|
from ophyd_async.core import PathProvider
|
|
9
|
-
from ophyd_async.epics.adaravis import
|
|
9
|
+
from ophyd_async.epics.adaravis import AravisDetector
|
|
10
10
|
from ophyd_async.epics.adpilatus import PilatusDetector
|
|
11
11
|
|
|
12
12
|
ValueAndUnits = tuple[float, str]
|
|
@@ -149,7 +149,6 @@ class NXSasOAV(AravisDetector):
|
|
|
149
149
|
fileio_suffix: str,
|
|
150
150
|
metadata_holder: NXSasMetadataHolder,
|
|
151
151
|
name: str = "",
|
|
152
|
-
gpio_number: AravisController.GPIO_NUMBER = 1,
|
|
153
152
|
):
|
|
154
153
|
"""Extends detector with configuration metadata required or desired
|
|
155
154
|
to comply with the NXsas application definition.
|
|
@@ -162,7 +161,6 @@ class NXSasOAV(AravisDetector):
|
|
|
162
161
|
drv_suffix=drv_suffix,
|
|
163
162
|
fileio_suffix=fileio_suffix,
|
|
164
163
|
name=name,
|
|
165
|
-
gpio_number=gpio_number,
|
|
166
164
|
)
|
|
167
165
|
self._metadata_holder = metadata_holder
|
|
168
166
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from ophyd_async.core import StandardReadable, StrictEnum
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_rw
|
|
3
3
|
|
|
4
|
-
from dodal.common.signal_utils import
|
|
4
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class HFocusMode(StrictEnum):
|
|
@@ -40,10 +40,10 @@ class FocusMirrorsMode(StandardReadable):
|
|
|
40
40
|
self.vertical = epics_signal_rw(VFocusMode, prefix + "G0:TARGETAPPLY")
|
|
41
41
|
|
|
42
42
|
with self.add_children_as_readables():
|
|
43
|
-
self.beam_size_x =
|
|
43
|
+
self.beam_size_x = create_r_hardware_backed_soft_signal(
|
|
44
44
|
int, self._get_beam_size_x, units="um"
|
|
45
45
|
)
|
|
46
|
-
self.beam_size_y =
|
|
46
|
+
self.beam_size_y = create_r_hardware_backed_soft_signal(
|
|
47
47
|
int, self._get_beam_size_y, units="um"
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
from ophyd_async.core import StandardReadable
|
|
6
6
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
7
7
|
|
|
8
|
-
from dodal.common.signal_utils import
|
|
8
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class PilatusMetadata(StandardReadable):
|
|
@@ -14,7 +14,7 @@ class PilatusMetadata(StandardReadable):
|
|
|
14
14
|
self.template = epics_signal_r(str, prefix + "cam1:FileTemplate_RBV")
|
|
15
15
|
self.filenumber = epics_signal_r(int, prefix + "cam1:FileNumber_RBV")
|
|
16
16
|
with self.add_children_as_readables():
|
|
17
|
-
self.filename_template =
|
|
17
|
+
self.filename_template = create_r_hardware_backed_soft_signal(
|
|
18
18
|
str, self._get_full_filename_template
|
|
19
19
|
)
|
|
20
20
|
super().__init__(name)
|
|
@@ -9,10 +9,10 @@ from ophyd_async.core import (
|
|
|
9
9
|
)
|
|
10
10
|
from ophyd_async.epics.core import epics_signal_rw
|
|
11
11
|
|
|
12
|
-
from dodal.common.signal_utils import
|
|
12
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
13
13
|
from dodal.devices.areadetector.plugins.CAM import Cam
|
|
14
14
|
from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
|
|
15
|
-
from dodal.devices.oav.snapshots.
|
|
15
|
+
from dodal.devices.oav.snapshots.snapshot import Snapshot
|
|
16
16
|
from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
|
|
17
17
|
|
|
18
18
|
|
|
@@ -63,24 +63,22 @@ class OAV(StandardReadable):
|
|
|
63
63
|
|
|
64
64
|
with self.add_children_as_readables():
|
|
65
65
|
self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
|
|
66
|
-
self.microns_per_pixel_x =
|
|
66
|
+
self.microns_per_pixel_x = create_r_hardware_backed_soft_signal(
|
|
67
67
|
float,
|
|
68
68
|
lambda: self._get_microns_per_pixel(Coords.X),
|
|
69
69
|
)
|
|
70
|
-
self.microns_per_pixel_y =
|
|
70
|
+
self.microns_per_pixel_y = create_r_hardware_backed_soft_signal(
|
|
71
71
|
float,
|
|
72
72
|
lambda: self._get_microns_per_pixel(Coords.Y),
|
|
73
73
|
)
|
|
74
|
-
self.beam_centre_i =
|
|
74
|
+
self.beam_centre_i = create_r_hardware_backed_soft_signal(
|
|
75
75
|
int, lambda: self._get_beam_position(Coords.X)
|
|
76
76
|
)
|
|
77
|
-
self.beam_centre_j =
|
|
77
|
+
self.beam_centre_j = create_r_hardware_backed_soft_signal(
|
|
78
78
|
int, lambda: self._get_beam_position(Coords.Y)
|
|
79
79
|
)
|
|
80
|
-
self.snapshot =
|
|
80
|
+
self.snapshot = Snapshot(
|
|
81
81
|
f"{self._prefix}MJPG:",
|
|
82
|
-
self.beam_centre_i,
|
|
83
|
-
self.beam_centre_j,
|
|
84
82
|
self._name,
|
|
85
83
|
)
|
|
86
84
|
|