dls-dodal 1.68.0__py3-none-any.whl → 1.69.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.68.0.dist-info → dls_dodal-1.69.0.dist-info}/METADATA +1 -31
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/RECORD +57 -49
- dodal/_version.py +2 -2
- dodal/beamlines/adsim.py +30 -23
- dodal/beamlines/i02_1.py +14 -42
- dodal/beamlines/i02_2.py +5 -11
- dodal/beamlines/i03.py +4 -1
- dodal/beamlines/i03_supervisor.py +19 -0
- dodal/beamlines/i04.py +74 -179
- dodal/beamlines/i05.py +8 -0
- dodal/beamlines/i06_1.py +24 -0
- dodal/beamlines/i09.py +53 -9
- dodal/beamlines/i09_1.py +8 -0
- dodal/beamlines/i09_2.py +4 -4
- dodal/beamlines/i16.py +11 -0
- dodal/beamlines/i20_1.py +14 -0
- dodal/beamlines/i21.py +12 -4
- dodal/beamlines/i23.py +19 -25
- dodal/beamlines/i24.py +55 -105
- dodal/beamlines/p60.py +11 -1
- dodal/common/__init__.py +2 -1
- dodal/common/maths.py +80 -0
- dodal/devices/eiger.py +29 -14
- dodal/devices/electron_analyser/base/__init__.py +3 -3
- dodal/devices/electron_analyser/base/base_controller.py +19 -8
- dodal/devices/electron_analyser/base/base_enums.py +0 -5
- dodal/devices/electron_analyser/base/base_region.py +2 -1
- dodal/devices/electron_analyser/base/energy_sources.py +27 -26
- dodal/devices/electron_analyser/specs/specs_detector.py +7 -6
- dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +7 -6
- dodal/devices/fast_shutter.py +108 -25
- dodal/devices/i04/beam_centre.py +84 -0
- dodal/devices/i04/max_pixel.py +4 -17
- dodal/devices/i04/murko_results.py +18 -3
- dodal/devices/i10/i10_apple2.py +6 -6
- dodal/devices/i17/i17_apple2.py +6 -6
- dodal/devices/i24/commissioning_jungfrau.py +9 -10
- dodal/devices/insertion_device/__init__.py +12 -8
- dodal/devices/insertion_device/apple2_controller.py +380 -0
- dodal/devices/insertion_device/apple2_undulator.py +152 -531
- dodal/devices/insertion_device/energy.py +88 -0
- dodal/devices/insertion_device/energy_motor_lookup.py +1 -1
- dodal/devices/insertion_device/lookup_table_models.py +2 -2
- dodal/devices/insertion_device/polarisation.py +36 -0
- dodal/devices/oav/oav_detector.py +66 -1
- dodal/devices/oav/utils.py +17 -0
- dodal/devices/robot.py +35 -18
- dodal/devices/selectable_source.py +38 -0
- dodal/devices/zebra/zebra.py +15 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
- dodal/testing/__init__.py +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/insertion_device/{id_enum.py → enum.py} +0 -0
|
@@ -12,10 +12,9 @@ from dodal.devices.electron_analyser.base.base_region import (
|
|
|
12
12
|
GenericRegion,
|
|
13
13
|
TAbstractBaseRegion,
|
|
14
14
|
)
|
|
15
|
-
from dodal.devices.electron_analyser.base.energy_sources import
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
class ElectronAnalyserController(
|
|
@@ -32,7 +31,9 @@ class ElectronAnalyserController(
|
|
|
32
31
|
self,
|
|
33
32
|
driver: TAbstractAnalyserDriverIO,
|
|
34
33
|
energy_source: AbstractEnergySource,
|
|
35
|
-
|
|
34
|
+
shutter: FastShutter | None = None,
|
|
35
|
+
source_selector: SourceSelector | None = None,
|
|
36
|
+
deadtime: float = 0,
|
|
36
37
|
image_mode: ADImageMode = ADImageMode.SINGLE,
|
|
37
38
|
):
|
|
38
39
|
"""
|
|
@@ -45,13 +46,19 @@ class ElectronAnalyserController(
|
|
|
45
46
|
image_mode: The image mode to configure the driver with before measuring.
|
|
46
47
|
"""
|
|
47
48
|
self.energy_source = energy_source
|
|
49
|
+
self.shutter = shutter
|
|
50
|
+
self.source_selector = source_selector
|
|
48
51
|
super().__init__(driver, deadtime, image_mode)
|
|
49
52
|
|
|
50
|
-
async def setup_with_region(self, region: TAbstractBaseRegion):
|
|
53
|
+
async def setup_with_region(self, region: TAbstractBaseRegion) -> None:
|
|
51
54
|
"""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)
|
|
52
61
|
|
|
53
|
-
if isinstance(self.energy_source, DualEnergySource):
|
|
54
|
-
self.energy_source.selected_source.set(region.excitation_energy_source)
|
|
55
62
|
excitation_energy = await self.energy_source.energy.get_value()
|
|
56
63
|
epics_region = region.prepare_for_epics(excitation_energy)
|
|
57
64
|
await self.driver.set(epics_region)
|
|
@@ -62,6 +69,10 @@ class ElectronAnalyserController(
|
|
|
62
69
|
# axis calculation.
|
|
63
70
|
excitation_energy = await self.energy_source.energy.get_value()
|
|
64
71
|
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
|
+
|
|
65
76
|
await super().prepare(trigger_info)
|
|
66
77
|
|
|
67
78
|
|
|
@@ -6,11 +6,12 @@ from typing import Generic, Self, TypeAlias, TypeVar
|
|
|
6
6
|
from ophyd_async.core import StrictEnum, SupersetEnum
|
|
7
7
|
from pydantic import BaseModel, Field, model_validator
|
|
8
8
|
|
|
9
|
-
from dodal.devices.electron_analyser.base.base_enums import EnergyMode
|
|
9
|
+
from dodal.devices.electron_analyser.base.base_enums import EnergyMode
|
|
10
10
|
from dodal.devices.electron_analyser.base.base_util import (
|
|
11
11
|
to_binding_energy,
|
|
12
12
|
to_kinetic_energy,
|
|
13
13
|
)
|
|
14
|
+
from dodal.devices.selectable_source import SelectedSource
|
|
14
15
|
|
|
15
16
|
AnyAcqMode: TypeAlias = StrictEnum
|
|
16
17
|
AnyLensMode: TypeAlias = SupersetEnum | StrictEnum
|
|
@@ -3,14 +3,14 @@ from abc import abstractmethod
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
Reference,
|
|
5
5
|
SignalR,
|
|
6
|
+
SignalRW,
|
|
6
7
|
StandardReadable,
|
|
7
8
|
StandardReadableFormat,
|
|
8
9
|
derived_signal_r,
|
|
9
10
|
soft_signal_r_and_setter,
|
|
10
|
-
soft_signal_rw,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
from dodal.devices.
|
|
13
|
+
from dodal.devices.selectable_source import SelectedSource, get_obj_from_selected_source
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class AbstractEnergySource(StandardReadable):
|
|
@@ -51,51 +51,52 @@ class EnergySource(AbstractEnergySource):
|
|
|
51
51
|
return self._source_ref()
|
|
52
52
|
|
|
53
53
|
|
|
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
|
+
|
|
54
61
|
class DualEnergySource(AbstractEnergySource):
|
|
55
62
|
"""
|
|
56
63
|
Holds two EnergySource devices and provides a signal to read energy depending on
|
|
57
|
-
which source is selected.
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
61
67
|
"""
|
|
62
68
|
|
|
63
69
|
def __init__(
|
|
64
|
-
self,
|
|
70
|
+
self,
|
|
71
|
+
source1: SignalR[float],
|
|
72
|
+
source2: SignalR[float],
|
|
73
|
+
selected_source: SignalRW[SelectedSource],
|
|
74
|
+
name: str = "",
|
|
65
75
|
):
|
|
66
76
|
"""
|
|
67
77
|
Args:
|
|
68
|
-
source1:
|
|
69
|
-
source2:
|
|
70
|
-
|
|
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.
|
|
71
82
|
"""
|
|
72
83
|
|
|
84
|
+
self.selected_source_ref = Reference(selected_source)
|
|
73
85
|
with self.add_children_as_readables():
|
|
74
|
-
self.selected_source = soft_signal_rw(
|
|
75
|
-
SelectedSource, initial_value=SelectedSource.SOURCE1
|
|
76
|
-
)
|
|
77
86
|
self.source1 = EnergySource(source1)
|
|
78
87
|
self.source2 = EnergySource(source2)
|
|
79
88
|
|
|
80
89
|
self._selected_energy = derived_signal_r(
|
|
81
|
-
|
|
90
|
+
get_float_from_selected_source,
|
|
82
91
|
"eV",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
selected=self.selected_source_ref(),
|
|
93
|
+
s1=self.source1.energy,
|
|
94
|
+
s2=self.source2.energy,
|
|
86
95
|
)
|
|
96
|
+
self.add_readables([selected_source])
|
|
87
97
|
|
|
88
98
|
super().__init__(name)
|
|
89
99
|
|
|
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
|
-
|
|
99
100
|
@property
|
|
100
101
|
def energy(self) -> SignalR[float]:
|
|
101
102
|
return self._selected_energy
|
|
@@ -5,15 +5,14 @@ from dodal.devices.electron_analyser.base.base_controller import (
|
|
|
5
5
|
)
|
|
6
6
|
from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector
|
|
7
7
|
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
|
|
12
9
|
from dodal.devices.electron_analyser.specs.specs_driver_io import SpecsAnalyserDriverIO
|
|
13
10
|
from dodal.devices.electron_analyser.specs.specs_region import (
|
|
14
11
|
SpecsRegion,
|
|
15
12
|
SpecsSequence,
|
|
16
13
|
)
|
|
14
|
+
from dodal.devices.fast_shutter import FastShutter
|
|
15
|
+
from dodal.devices.selectable_source import SourceSelector
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class SpecsDetector(
|
|
@@ -29,7 +28,9 @@ class SpecsDetector(
|
|
|
29
28
|
prefix: str,
|
|
30
29
|
lens_mode_type: type[TLensMode],
|
|
31
30
|
psu_mode_type: type[TPsuMode],
|
|
32
|
-
energy_source:
|
|
31
|
+
energy_source: AbstractEnergySource,
|
|
32
|
+
shutter: FastShutter | None = None,
|
|
33
|
+
source_selector: SourceSelector | None = None,
|
|
33
34
|
name: str = "",
|
|
34
35
|
):
|
|
35
36
|
# Save to class so takes part with connect()
|
|
@@ -39,7 +40,7 @@ class SpecsDetector(
|
|
|
39
40
|
|
|
40
41
|
controller = ElectronAnalyserController[
|
|
41
42
|
SpecsAnalyserDriverIO[TLensMode, TPsuMode], SpecsRegion[TLensMode, TPsuMode]
|
|
42
|
-
](self.driver, energy_source,
|
|
43
|
+
](self.driver, energy_source, shutter, source_selector)
|
|
43
44
|
|
|
44
45
|
sequence_class = SpecsSequence[lens_mode_type, psu_mode_type]
|
|
45
46
|
|
|
@@ -5,10 +5,7 @@ from dodal.devices.electron_analyser.base.base_controller import (
|
|
|
5
5
|
)
|
|
6
6
|
from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector
|
|
7
7
|
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
|
|
12
9
|
from dodal.devices.electron_analyser.vgscienta.vgscienta_driver_io import (
|
|
13
10
|
VGScientaAnalyserDriverIO,
|
|
14
11
|
)
|
|
@@ -17,6 +14,8 @@ from dodal.devices.electron_analyser.vgscienta.vgscienta_region import (
|
|
|
17
14
|
VGScientaRegion,
|
|
18
15
|
VGScientaSequence,
|
|
19
16
|
)
|
|
17
|
+
from dodal.devices.fast_shutter import FastShutter
|
|
18
|
+
from dodal.devices.selectable_source import SourceSelector
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class VGScientaDetector(
|
|
@@ -33,7 +32,9 @@ class VGScientaDetector(
|
|
|
33
32
|
lens_mode_type: type[TLensMode],
|
|
34
33
|
psu_mode_type: type[TPsuMode],
|
|
35
34
|
pass_energy_type: type[TPassEnergyEnum],
|
|
36
|
-
energy_source:
|
|
35
|
+
energy_source: AbstractEnergySource,
|
|
36
|
+
shutter: FastShutter | None = None,
|
|
37
|
+
source_selector: SourceSelector | None = None,
|
|
37
38
|
name: str = "",
|
|
38
39
|
):
|
|
39
40
|
# Save to class so takes part with connect()
|
|
@@ -44,7 +45,7 @@ class VGScientaDetector(
|
|
|
44
45
|
controller = ElectronAnalyserController[
|
|
45
46
|
VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum],
|
|
46
47
|
VGScientaRegion[TLensMode, TPassEnergyEnum],
|
|
47
|
-
](self.driver, energy_source,
|
|
48
|
+
](self.driver, energy_source, shutter, source_selector)
|
|
48
49
|
|
|
49
50
|
sequence_class = VGScientaSequence[
|
|
50
51
|
lens_mode_type, psu_mode_type, pass_energy_type
|
dodal/devices/fast_shutter.py
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
from typing import TypeVar
|
|
1
|
+
from typing import Generic, Protocol, TypeVar
|
|
2
2
|
|
|
3
3
|
from bluesky.protocols import Movable
|
|
4
4
|
from ophyd_async.core import (
|
|
5
5
|
AsyncStatus,
|
|
6
6
|
EnumTypes,
|
|
7
|
+
Reference,
|
|
8
|
+
SignalRW,
|
|
7
9
|
StandardReadable,
|
|
10
|
+
StandardReadableFormat,
|
|
11
|
+
derived_signal_rw,
|
|
12
|
+
soft_signal_r_and_setter,
|
|
8
13
|
)
|
|
9
14
|
from ophyd_async.epics.core import epics_signal_rw
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
from dodal.devices.selectable_source import SelectedSource, get_obj_from_selected_source
|
|
12
17
|
|
|
18
|
+
EnumTypesT = TypeVar("EnumTypesT", bound=EnumTypes)
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
|
|
21
|
+
class FastShutter(Movable[EnumTypesT], Protocol, Generic[EnumTypesT]):
|
|
15
22
|
"""
|
|
16
|
-
|
|
23
|
+
Enum device specialised for a fast shutter with configured open_state and
|
|
17
24
|
close_state so it is generic enough to be used with any device or plan without
|
|
18
25
|
knowing the specific enum to use.
|
|
19
26
|
|
|
@@ -25,11 +32,28 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
|
25
32
|
run_engine(bps.mv(shutter, shutter.close_state))
|
|
26
33
|
"""
|
|
27
34
|
|
|
35
|
+
open_state: EnumTypesT
|
|
36
|
+
close_state: EnumTypesT
|
|
37
|
+
shutter_state: SignalRW[EnumTypesT]
|
|
38
|
+
|
|
39
|
+
@AsyncStatus.wrap
|
|
40
|
+
async def set(self, state: EnumTypesT):
|
|
41
|
+
await self.shutter_state.set(state)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GenericFastShutter(
|
|
45
|
+
StandardReadable, FastShutter[EnumTypesT], Generic[EnumTypesT]
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Implementation of fast shutter that connects to an epics pv. This pv is an enum that
|
|
49
|
+
controls the open and close state of the shutter.
|
|
50
|
+
"""
|
|
51
|
+
|
|
28
52
|
def __init__(
|
|
29
53
|
self,
|
|
30
54
|
pv: str,
|
|
31
|
-
open_state:
|
|
32
|
-
close_state:
|
|
55
|
+
open_state: EnumTypesT,
|
|
56
|
+
close_state: EnumTypesT,
|
|
33
57
|
name: str = "",
|
|
34
58
|
):
|
|
35
59
|
"""
|
|
@@ -41,29 +65,88 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
|
41
65
|
self.open_state = open_state
|
|
42
66
|
self.close_state = close_state
|
|
43
67
|
with self.add_children_as_readables():
|
|
44
|
-
self.
|
|
68
|
+
self.shutter_state = epics_signal_rw(type(self.open_state), pv)
|
|
45
69
|
super().__init__(name)
|
|
46
70
|
|
|
47
|
-
@AsyncStatus.wrap
|
|
48
|
-
async def set(self, value: StrictEnumT) -> None:
|
|
49
|
-
await self.state.set(value)
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
class DualFastShutter(StandardReadable, FastShutter[EnumTypesT], Generic[EnumTypesT]):
|
|
73
|
+
"""
|
|
74
|
+
A fast shutter device that handles the positions of two other fast shutters. The
|
|
75
|
+
"active" shutter is the one that corrosponds to the selected_shutter signal. For
|
|
76
|
+
example, active shutter is shutter1 if selected_source is at SelectedSource.SOURCE1
|
|
77
|
+
and vise versa for shutter2 and SelectedSource.SOURCE2. Whenever a move is done on
|
|
78
|
+
this device, the inactive shutter is always set to the close_state.
|
|
79
|
+
"""
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
shutter1: GenericFastShutter[EnumTypesT],
|
|
84
|
+
shutter2: GenericFastShutter[EnumTypesT],
|
|
85
|
+
selected_source: SignalRW[SelectedSource],
|
|
86
|
+
name: str = "",
|
|
87
|
+
):
|
|
58
88
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
Arguments:
|
|
90
|
+
shutter1: Active shutter that corrosponds to SelectedSource.SOURCE1.
|
|
91
|
+
shutter2: Active shutter that corrosponds to SelectedSource.SOURCE2.
|
|
92
|
+
selected_source: Signal that decides the active shutter.
|
|
93
|
+
name: Name of this device.
|
|
62
94
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
95
|
+
self._validate_shutter_states(shutter1.open_state, shutter2.open_state)
|
|
96
|
+
self._validate_shutter_states(shutter1.close_state, shutter2.close_state)
|
|
97
|
+
self.open_state = shutter1.open_state
|
|
98
|
+
self.close_state = shutter1.close_state
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
self._shutter1_ref = Reference(shutter1)
|
|
101
|
+
self._shutter2_ref = Reference(shutter2)
|
|
102
|
+
self._selected_shutter_ref = Reference(selected_source)
|
|
103
|
+
|
|
104
|
+
with self.add_children_as_readables():
|
|
105
|
+
self.shutter_state = derived_signal_rw(
|
|
106
|
+
self._read_shutter_state,
|
|
107
|
+
self._set_shutter_state,
|
|
108
|
+
selected_shutter=selected_source,
|
|
109
|
+
shutter1=shutter1.shutter_state,
|
|
110
|
+
shutter2=shutter2.shutter_state,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
114
|
+
self.shutter1_device_name, _ = soft_signal_r_and_setter(
|
|
115
|
+
str, initial_value=shutter1.name
|
|
116
|
+
)
|
|
117
|
+
self.shutter2_device_name, _ = soft_signal_r_and_setter(
|
|
118
|
+
str, initial_value=shutter2.name
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.add_readables([shutter1, shutter2, selected_source])
|
|
122
|
+
|
|
123
|
+
super().__init__(name)
|
|
124
|
+
|
|
125
|
+
def _validate_shutter_states(self, state1: EnumTypesT, state2: EnumTypesT) -> None:
|
|
126
|
+
if state1 is not state2:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"{state1} is not same value as {state2}. They must be the same to be compatible."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _read_shutter_state(
|
|
132
|
+
self,
|
|
133
|
+
selected_shutter: SelectedSource,
|
|
134
|
+
shutter1: EnumTypesT,
|
|
135
|
+
shutter2: EnumTypesT,
|
|
136
|
+
) -> EnumTypesT:
|
|
137
|
+
return get_obj_from_selected_source(selected_shutter, shutter1, shutter2)
|
|
138
|
+
|
|
139
|
+
async def _set_shutter_state(self, value: EnumTypesT):
|
|
140
|
+
selected_shutter = await self._selected_shutter_ref().get_value()
|
|
141
|
+
active_shutter = get_obj_from_selected_source(
|
|
142
|
+
selected_shutter,
|
|
143
|
+
self._shutter1_ref(),
|
|
144
|
+
self._shutter2_ref(),
|
|
145
|
+
)
|
|
146
|
+
inactive_shutter = get_obj_from_selected_source(
|
|
147
|
+
selected_shutter,
|
|
148
|
+
self._shutter2_ref(),
|
|
149
|
+
self._shutter1_ref(),
|
|
150
|
+
)
|
|
151
|
+
await inactive_shutter.set(inactive_shutter.close_state)
|
|
152
|
+
await active_shutter.set(value)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
from bluesky.protocols import Triggerable
|
|
4
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
|
|
5
|
+
from ophyd_async.epics.core import (
|
|
6
|
+
epics_signal_r,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from dodal.devices.oav.utils import convert_to_gray_and_blur
|
|
10
|
+
from dodal.log import LOGGER
|
|
11
|
+
|
|
12
|
+
# Constant was chosen from trial and error with test images
|
|
13
|
+
ADDITIONAL_BINARY_THRESH = 20
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def convert_image_to_binary(image: np.ndarray):
|
|
17
|
+
"""
|
|
18
|
+
Creates a binary image from OAV image array data.
|
|
19
|
+
|
|
20
|
+
Pixels of the input image are converted to one of two values (a high and a low value).
|
|
21
|
+
Otsu's method is used for automatic thresholding.
|
|
22
|
+
See https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html.
|
|
23
|
+
The threshold is increased by ADDITIONAL_BINARY_THRESH in order to get more of
|
|
24
|
+
the centre of the beam.
|
|
25
|
+
"""
|
|
26
|
+
max_pixel_value = 255
|
|
27
|
+
|
|
28
|
+
blurred_image = convert_to_gray_and_blur(image)
|
|
29
|
+
|
|
30
|
+
threshold_value, _ = cv2.threshold(
|
|
31
|
+
blurred_image, 0, max_pixel_value, cv2.THRESH_BINARY + cv2.THRESH_OTSU
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Adjusting because the inner beam is less noisy compared to the outer
|
|
35
|
+
threshold_value += ADDITIONAL_BINARY_THRESH
|
|
36
|
+
|
|
37
|
+
_, thresholded_image = cv2.threshold(
|
|
38
|
+
blurred_image, threshold_value, max_pixel_value, cv2.THRESH_BINARY
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
LOGGER.info(f"Image binarised with threshold of {threshold_value}")
|
|
42
|
+
return thresholded_image
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CentreEllipseMethod(StandardReadable, Triggerable):
|
|
46
|
+
"""
|
|
47
|
+
Upon triggering, fits an ellipse to a binary image from the area detector defined by
|
|
48
|
+
the prefix.
|
|
49
|
+
|
|
50
|
+
This is used, in conjunction with a scintillator, to determine the centre of the beam
|
|
51
|
+
on the image.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
55
|
+
self.oav_array_signal = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
|
|
56
|
+
|
|
57
|
+
self.center_x_val, self._center_x_val_setter = soft_signal_r_and_setter(float)
|
|
58
|
+
self.center_y_val, self._center_y_val_setter = soft_signal_r_and_setter(float)
|
|
59
|
+
super().__init__(name)
|
|
60
|
+
|
|
61
|
+
def _fit_ellipse(self, binary_img: cv2.typing.MatLike) -> cv2.typing.RotatedRect:
|
|
62
|
+
contours, _ = cv2.findContours(
|
|
63
|
+
binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
|
|
64
|
+
)
|
|
65
|
+
if not contours:
|
|
66
|
+
raise ValueError("No contours found in image.")
|
|
67
|
+
|
|
68
|
+
largest_contour = max(contours, key=cv2.contourArea)
|
|
69
|
+
if len(largest_contour) < 5:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Not enough points to fit an ellipse. Found {largest_contour} points and need at least 5."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return cv2.fitEllipse(largest_contour)
|
|
75
|
+
|
|
76
|
+
@AsyncStatus.wrap
|
|
77
|
+
async def trigger(self):
|
|
78
|
+
array_data = await self.oav_array_signal.get_value()
|
|
79
|
+
binary = convert_image_to_binary(array_data)
|
|
80
|
+
ellipse_fit = self._fit_ellipse(binary)
|
|
81
|
+
centre_x = ellipse_fit[0][0]
|
|
82
|
+
centre_y = ellipse_fit[0][1]
|
|
83
|
+
self._center_x_val_setter(centre_x)
|
|
84
|
+
self._center_y_val_setter(centre_y)
|
dodal/devices/i04/max_pixel.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import cv2
|
|
2
1
|
import numpy as np
|
|
3
2
|
from bluesky.protocols import Triggerable
|
|
4
3
|
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
|
|
@@ -6,9 +5,7 @@ from ophyd_async.epics.core import (
|
|
|
6
5
|
epics_signal_r,
|
|
7
6
|
)
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
# higher kernal size means more of a blur effect
|
|
11
|
-
KERNAL_SIZE = (7, 7)
|
|
8
|
+
from dodal.devices.oav.utils import convert_to_gray_and_blur
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
class MaxPixel(StandardReadable, Triggerable):
|
|
@@ -19,20 +16,10 @@ class MaxPixel(StandardReadable, Triggerable):
|
|
|
19
16
|
self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
|
|
20
17
|
super().__init__(name)
|
|
21
18
|
|
|
22
|
-
async def _convert_to_gray_and_blur(self):
|
|
23
|
-
"""
|
|
24
|
-
Preprocess the image array data (convert to grayscale and apply a gaussian blur)
|
|
25
|
-
Image is converted to grayscale (using a weighted mean as green contributes more to brightness)
|
|
26
|
-
as we aren't interested in data relating to colour. A blur is then applied to mitigate
|
|
27
|
-
errors due to rogue hot pixels.
|
|
28
|
-
"""
|
|
29
|
-
data = await self.array_data.get_value()
|
|
30
|
-
gray_arr = cv2.cvtColor(data, cv2.COLOR_BGR2GRAY)
|
|
31
|
-
return cv2.GaussianBlur(gray_arr, KERNAL_SIZE, 0)
|
|
32
|
-
|
|
33
19
|
@AsyncStatus.wrap
|
|
34
20
|
async def trigger(self):
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
img_data = await self.array_data.get_value()
|
|
22
|
+
arr = convert_to_gray_and_blur(img_data)
|
|
23
|
+
max_val = float(np.max(arr))
|
|
37
24
|
assert isinstance(max_val, float)
|
|
38
25
|
self._max_val_setter(max_val)
|
|
@@ -13,7 +13,7 @@ from ophyd_async.core import (
|
|
|
13
13
|
soft_signal_r_and_setter,
|
|
14
14
|
soft_signal_rw,
|
|
15
15
|
)
|
|
16
|
-
from redis.asyncio import StrictRedis
|
|
16
|
+
from redis.asyncio import ConnectionError, StrictRedis
|
|
17
17
|
|
|
18
18
|
from dodal.devices.i04.constants import RedisConstants
|
|
19
19
|
from dodal.devices.oav.oav_calculations import (
|
|
@@ -103,13 +103,25 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
103
103
|
self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
|
|
104
104
|
super().__init__(name=name)
|
|
105
105
|
|
|
106
|
+
async def _check_redis_connection(self):
|
|
107
|
+
try:
|
|
108
|
+
await self.redis_client.ping() # type: ignore
|
|
109
|
+
return True
|
|
110
|
+
except ConnectionError:
|
|
111
|
+
LOGGER.warning(
|
|
112
|
+
f"Failed to connect to redis: {self.redis_client}. Murko results device will not trigger"
|
|
113
|
+
)
|
|
114
|
+
return False
|
|
115
|
+
|
|
106
116
|
def _reset(self):
|
|
107
117
|
self._last_omega = None
|
|
108
118
|
self._results: list[MurkoResult] = []
|
|
109
119
|
|
|
110
120
|
@AsyncStatus.wrap
|
|
111
121
|
async def stage(self):
|
|
112
|
-
await self.
|
|
122
|
+
self.redis_connected = await self._check_redis_connection()
|
|
123
|
+
if self.redis_connected:
|
|
124
|
+
await self.pubsub.subscribe("murko-results")
|
|
113
125
|
self._x_mm_setter(0)
|
|
114
126
|
self._y_mm_setter(0)
|
|
115
127
|
self._z_mm_setter(0)
|
|
@@ -117,10 +129,13 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
117
129
|
@AsyncStatus.wrap
|
|
118
130
|
async def unstage(self):
|
|
119
131
|
self._reset()
|
|
120
|
-
|
|
132
|
+
if self.redis_connected:
|
|
133
|
+
await self.pubsub.unsubscribe()
|
|
121
134
|
|
|
122
135
|
@AsyncStatus.wrap
|
|
123
136
|
async def trigger(self):
|
|
137
|
+
if not self.redis_connected:
|
|
138
|
+
return
|
|
124
139
|
sample_id = await self.sample_id.get_value()
|
|
125
140
|
t_last_result = time.time()
|
|
126
141
|
while True:
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -22,7 +22,7 @@ from dodal.devices.insertion_device import (
|
|
|
22
22
|
UndulatorPhaseAxes,
|
|
23
23
|
)
|
|
24
24
|
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
25
|
-
from dodal.devices.insertion_device.
|
|
25
|
+
from dodal.devices.insertion_device.enum import Pol
|
|
26
26
|
|
|
27
27
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
28
28
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -141,12 +141,12 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
141
141
|
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
142
142
|
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
143
143
|
return Apple2Val(
|
|
144
|
-
gap=
|
|
144
|
+
gap=gap,
|
|
145
145
|
phase=Apple2PhasesVal(
|
|
146
|
-
top_outer=
|
|
147
|
-
top_inner=
|
|
148
|
-
btm_inner=
|
|
149
|
-
btm_outer=
|
|
146
|
+
top_outer=phase,
|
|
147
|
+
top_inner=0.0,
|
|
148
|
+
btm_inner=phase3,
|
|
149
|
+
btm_outer=0.0,
|
|
150
150
|
),
|
|
151
151
|
)
|
|
152
152
|
|
dodal/devices/i17/i17_apple2.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dodal.devices.insertion_device
|
|
1
|
+
from dodal.devices.insertion_device import (
|
|
2
2
|
Apple2,
|
|
3
3
|
Apple2Controller,
|
|
4
4
|
Apple2PhasesVal,
|
|
@@ -56,11 +56,11 @@ class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
|
|
|
56
56
|
|
|
57
57
|
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
58
58
|
return Apple2Val(
|
|
59
|
-
gap=
|
|
59
|
+
gap=gap,
|
|
60
60
|
phase=Apple2PhasesVal(
|
|
61
|
-
top_outer=
|
|
62
|
-
top_inner=
|
|
63
|
-
btm_inner=
|
|
64
|
-
btm_outer=
|
|
61
|
+
top_outer=phase,
|
|
62
|
+
top_inner=0.0,
|
|
63
|
+
btm_inner=phase,
|
|
64
|
+
btm_outer=0.0,
|
|
65
65
|
),
|
|
66
66
|
)
|