dls-dodal 1.66.0__py3-none-any.whl → 1.68.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.66.0.dist-info → dls_dodal-1.68.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/RECORD +75 -65
- dodal/_version.py +2 -2
- dodal/beamlines/b07.py +1 -1
- dodal/beamlines/b07_1.py +1 -1
- dodal/beamlines/i03.py +92 -208
- dodal/beamlines/i04.py +22 -1
- dodal/beamlines/i05.py +1 -1
- dodal/beamlines/i06.py +1 -1
- dodal/beamlines/i09.py +1 -1
- dodal/beamlines/i09_1.py +27 -3
- dodal/beamlines/i09_2.py +58 -2
- dodal/beamlines/i10_optics.py +44 -25
- dodal/beamlines/i16.py +23 -0
- dodal/beamlines/i17.py +7 -3
- dodal/beamlines/i19_1.py +26 -14
- dodal/beamlines/i19_2.py +49 -38
- dodal/beamlines/i21.py +61 -2
- dodal/beamlines/i22.py +16 -1
- dodal/beamlines/p60.py +1 -1
- dodal/beamlines/training_rig.py +0 -16
- dodal/cli.py +26 -12
- dodal/common/coordination.py +3 -2
- dodal/device_manager.py +604 -0
- dodal/devices/cryostream.py +28 -57
- dodal/devices/eiger.py +41 -27
- dodal/devices/electron_analyser/__init__.py +0 -33
- dodal/devices/electron_analyser/base/__init__.py +58 -0
- dodal/devices/electron_analyser/base/base_controller.py +73 -0
- dodal/devices/electron_analyser/base/base_detector.py +214 -0
- dodal/devices/electron_analyser/{abstract → base}/base_driver_io.py +23 -42
- dodal/devices/electron_analyser/{abstract → base}/base_region.py +47 -11
- dodal/devices/electron_analyser/{util.py → base/base_util.py} +1 -1
- dodal/devices/electron_analyser/{energy_sources.py → base/energy_sources.py} +1 -1
- dodal/devices/electron_analyser/specs/__init__.py +4 -4
- dodal/devices/electron_analyser/specs/specs_detector.py +46 -0
- dodal/devices/electron_analyser/specs/{driver_io.py → specs_driver_io.py} +23 -26
- dodal/devices/electron_analyser/specs/{region.py → specs_region.py} +4 -3
- dodal/devices/electron_analyser/vgscienta/__init__.py +4 -4
- dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +52 -0
- dodal/devices/electron_analyser/vgscienta/{driver_io.py → vgscienta_driver_io.py} +25 -31
- dodal/devices/electron_analyser/vgscienta/{region.py → vgscienta_region.py} +6 -6
- dodal/devices/i04/max_pixel.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +8 -1
- dodal/devices/i09_1_shared/hard_energy.py +112 -0
- dodal/devices/i09_2_shared/__init__.py +0 -0
- dodal/devices/i09_2_shared/i09_apple2.py +14 -0
- dodal/devices/i10/i10_apple2.py +24 -22
- dodal/devices/i17/i17_apple2.py +32 -20
- dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
- dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
- dodal/devices/i19/access_controlled/shutter.py +2 -4
- dodal/devices/i21/__init__.py +3 -1
- dodal/devices/insertion_device/__init__.py +58 -0
- dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +102 -44
- dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
- dodal/devices/insertion_device/id_enum.py +17 -0
- dodal/devices/insertion_device/lookup_table_models.py +317 -0
- dodal/devices/motors.py +14 -0
- dodal/devices/robot.py +16 -11
- dodal/plans/__init__.py +1 -1
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
- dodal/testing/electron_analyser/device_factory.py +4 -4
- dodal/testing/fixtures/devices/__init__.py +0 -0
- dodal/testing/fixtures/devices/apple2.py +78 -0
- dodal/testing/fixtures/run_engine.py +4 -0
- dodal/utils.py +6 -3
- dodal/devices/electron_analyser/abstract/__init__.py +0 -25
- dodal/devices/electron_analyser/abstract/base_detector.py +0 -63
- dodal/devices/electron_analyser/abstract/types.py +0 -12
- dodal/devices/electron_analyser/detector.py +0 -143
- dodal/devices/electron_analyser/specs/detector.py +0 -34
- dodal/devices/electron_analyser/types.py +0 -57
- dodal/devices/electron_analyser/vgscienta/detector.py +0 -48
- dodal/devices/util/lookup_tables_apple2.py +0 -390
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/electron_analyser/{enums.py → base/base_enums.py} +0 -0
- /dodal/devices/electron_analyser/specs/{enums.py → specs_enums.py} +0 -0
- /dodal/devices/electron_analyser/vgscienta/{enums.py → vgscienta_enums.py} +0 -0
- /dodal/plans/{scanspec.py → spec_path.py} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Generic
|
|
2
|
+
|
|
3
|
+
from dodal.devices.electron_analyser.base.base_controller import (
|
|
4
|
+
ElectronAnalyserController,
|
|
5
|
+
)
|
|
6
|
+
from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector
|
|
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
|
+
)
|
|
12
|
+
from dodal.devices.electron_analyser.vgscienta.vgscienta_driver_io import (
|
|
13
|
+
VGScientaAnalyserDriverIO,
|
|
14
|
+
)
|
|
15
|
+
from dodal.devices.electron_analyser.vgscienta.vgscienta_region import (
|
|
16
|
+
TPassEnergyEnum,
|
|
17
|
+
VGScientaRegion,
|
|
18
|
+
VGScientaSequence,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VGScientaDetector(
|
|
23
|
+
ElectronAnalyserDetector[
|
|
24
|
+
VGScientaSequence[TLensMode, TPsuMode, TPassEnergyEnum],
|
|
25
|
+
VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum],
|
|
26
|
+
VGScientaRegion[TLensMode, TPassEnergyEnum],
|
|
27
|
+
],
|
|
28
|
+
Generic[TLensMode, TPsuMode, TPassEnergyEnum],
|
|
29
|
+
):
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
prefix: str,
|
|
33
|
+
lens_mode_type: type[TLensMode],
|
|
34
|
+
psu_mode_type: type[TPsuMode],
|
|
35
|
+
pass_energy_type: type[TPassEnergyEnum],
|
|
36
|
+
energy_source: DualEnergySource | EnergySource,
|
|
37
|
+
name: str = "",
|
|
38
|
+
):
|
|
39
|
+
# Save to class so takes part with connect()
|
|
40
|
+
self.driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum](
|
|
41
|
+
prefix, lens_mode_type, psu_mode_type, pass_energy_type
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
controller = ElectronAnalyserController[
|
|
45
|
+
VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum],
|
|
46
|
+
VGScientaRegion[TLensMode, TPassEnergyEnum],
|
|
47
|
+
](self.driver, energy_source, 0)
|
|
48
|
+
|
|
49
|
+
sequence_class = VGScientaSequence[
|
|
50
|
+
lens_mode_type, psu_mode_type, pass_energy_type
|
|
51
|
+
]
|
|
52
|
+
super().__init__(sequence_class, controller, name)
|
|
@@ -4,28 +4,22 @@ from typing import Generic
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from ophyd_async.core import (
|
|
6
6
|
Array1D,
|
|
7
|
+
AsyncStatus,
|
|
7
8
|
SignalR,
|
|
8
9
|
StandardReadableFormat,
|
|
9
10
|
)
|
|
10
11
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
11
12
|
|
|
12
|
-
from dodal.devices.electron_analyser.
|
|
13
|
+
from dodal.devices.electron_analyser.base.base_driver_io import (
|
|
13
14
|
AbstractAnalyserDriverIO,
|
|
14
15
|
)
|
|
15
|
-
from dodal.devices.electron_analyser.
|
|
16
|
-
|
|
17
|
-
TPassEnergyEnum,
|
|
18
|
-
TPsuMode,
|
|
19
|
-
)
|
|
20
|
-
from dodal.devices.electron_analyser.energy_sources import (
|
|
21
|
-
DualEnergySource,
|
|
22
|
-
EnergySource,
|
|
23
|
-
)
|
|
24
|
-
from dodal.devices.electron_analyser.vgscienta.enums import (
|
|
16
|
+
from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode
|
|
17
|
+
from dodal.devices.electron_analyser.vgscienta.vgscienta_enums import (
|
|
25
18
|
AcquisitionMode,
|
|
26
19
|
DetectorMode,
|
|
27
20
|
)
|
|
28
|
-
from dodal.devices.electron_analyser.vgscienta.
|
|
21
|
+
from dodal.devices.electron_analyser.vgscienta.vgscienta_region import (
|
|
22
|
+
TPassEnergyEnum,
|
|
29
23
|
VGScientaRegion,
|
|
30
24
|
)
|
|
31
25
|
|
|
@@ -46,7 +40,6 @@ class VGScientaAnalyserDriverIO(
|
|
|
46
40
|
lens_mode_type: type[TLensMode],
|
|
47
41
|
psu_mode_type: type[TPsuMode],
|
|
48
42
|
pass_energy_type: type[TPassEnergyEnum],
|
|
49
|
-
energy_source: EnergySource | DualEnergySource,
|
|
50
43
|
name: str = "",
|
|
51
44
|
) -> None:
|
|
52
45
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
@@ -67,28 +60,29 @@ class VGScientaAnalyserDriverIO(
|
|
|
67
60
|
lens_mode_type,
|
|
68
61
|
psu_mode_type,
|
|
69
62
|
pass_energy_type,
|
|
70
|
-
energy_source,
|
|
71
63
|
name,
|
|
72
64
|
)
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
@AsyncStatus.wrap
|
|
67
|
+
async def set(self, epics_region: VGScientaRegion[TLensMode, TPassEnergyEnum]):
|
|
75
68
|
await asyncio.gather(
|
|
76
|
-
self.region_name.set(
|
|
77
|
-
self.low_energy.set(
|
|
78
|
-
self.centre_energy.set(
|
|
79
|
-
self.high_energy.set(
|
|
80
|
-
self.slices.set(
|
|
81
|
-
self.lens_mode.set(
|
|
82
|
-
self.pass_energy.set(
|
|
83
|
-
self.iterations.set(
|
|
84
|
-
self.acquire_time.set(
|
|
85
|
-
self.acquisition_mode.set(
|
|
86
|
-
self.energy_step.set(
|
|
87
|
-
self.detector_mode.set(
|
|
88
|
-
self.region_min_x.set(
|
|
89
|
-
self.region_size_x.set(
|
|
90
|
-
self.region_min_y.set(
|
|
91
|
-
self.region_size_y.set(
|
|
69
|
+
self.region_name.set(epics_region.name),
|
|
70
|
+
self.low_energy.set(epics_region.low_energy),
|
|
71
|
+
self.centre_energy.set(epics_region.centre_energy),
|
|
72
|
+
self.high_energy.set(epics_region.high_energy),
|
|
73
|
+
self.slices.set(epics_region.slices),
|
|
74
|
+
self.lens_mode.set(epics_region.lens_mode),
|
|
75
|
+
self.pass_energy.set(epics_region.pass_energy),
|
|
76
|
+
self.iterations.set(epics_region.iterations),
|
|
77
|
+
self.acquire_time.set(epics_region.acquire_time),
|
|
78
|
+
self.acquisition_mode.set(epics_region.acquisition_mode),
|
|
79
|
+
self.energy_step.set(epics_region.energy_step),
|
|
80
|
+
self.detector_mode.set(epics_region.detector_mode),
|
|
81
|
+
self.region_min_x.set(epics_region.min_x),
|
|
82
|
+
self.region_size_x.set(epics_region.size_x),
|
|
83
|
+
self.region_min_y.set(epics_region.min_y),
|
|
84
|
+
self.region_size_y.set(epics_region.size_y),
|
|
85
|
+
self.energy_mode.set(epics_region.energy_mode),
|
|
92
86
|
)
|
|
93
87
|
|
|
94
88
|
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from typing import Generic
|
|
2
|
+
from typing import Generic, TypeVar
|
|
3
3
|
|
|
4
|
+
from ophyd_async.core import StrictEnum
|
|
4
5
|
from pydantic import Field, field_validator
|
|
5
6
|
|
|
6
|
-
from dodal.devices.electron_analyser.
|
|
7
|
+
from dodal.devices.electron_analyser.base.base_region import (
|
|
7
8
|
AbstractBaseRegion,
|
|
8
9
|
AbstractBaseSequence,
|
|
9
|
-
)
|
|
10
|
-
from dodal.devices.electron_analyser.abstract.types import (
|
|
11
10
|
TLensMode,
|
|
12
|
-
TPassEnergyEnum,
|
|
13
11
|
TPsuMode,
|
|
14
12
|
)
|
|
15
|
-
from dodal.devices.electron_analyser.vgscienta.
|
|
13
|
+
from dodal.devices.electron_analyser.vgscienta.vgscienta_enums import (
|
|
16
14
|
AcquisitionMode,
|
|
17
15
|
DetectorMode,
|
|
18
16
|
)
|
|
19
17
|
|
|
18
|
+
TPassEnergyEnum = TypeVar("TPassEnergyEnum", bound=StrictEnum)
|
|
19
|
+
|
|
20
20
|
|
|
21
21
|
class VGScientaRegion(
|
|
22
22
|
AbstractBaseRegion[AcquisitionMode, TLensMode, TPassEnergyEnum],
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
# kernal size describes how many of the neigbouring pixels are used for the blur,
|
|
10
|
+
# higher kernal size means more of a blur effect
|
|
11
|
+
KERNAL_SIZE = (7, 7)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaxPixel(StandardReadable, Triggerable):
|
|
15
|
+
"""Gets the max pixel (brightest pixel) from the image after some image processing."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
18
|
+
self.array_data = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
|
|
19
|
+
self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
|
|
20
|
+
super().__init__(name)
|
|
21
|
+
|
|
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
|
+
@AsyncStatus.wrap
|
|
34
|
+
async def trigger(self):
|
|
35
|
+
arr = await self._convert_to_gray_and_blur()
|
|
36
|
+
max_val = float(np.max(arr)) # np.int64
|
|
37
|
+
assert isinstance(max_val, float)
|
|
38
|
+
self._max_val_setter(max_val)
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
from .hard_energy import HardEnergy, HardInsertionDeviceEnergy
|
|
1
2
|
from .hard_undulator_functions import (
|
|
2
3
|
calculate_energy_i09_hu,
|
|
3
4
|
calculate_gap_i09_hu,
|
|
4
5
|
get_hu_lut_as_dict,
|
|
5
6
|
)
|
|
6
7
|
|
|
7
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"calculate_gap_i09_hu",
|
|
10
|
+
"get_hu_lut_as_dict",
|
|
11
|
+
"calculate_energy_i09_hu",
|
|
12
|
+
"HardInsertionDeviceEnergy",
|
|
13
|
+
"HardEnergy",
|
|
14
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from asyncio import gather
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import Locatable, Location, Movable
|
|
5
|
+
from numpy import ndarray
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
Reference,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
StandardReadableFormat,
|
|
11
|
+
derived_signal_rw,
|
|
12
|
+
soft_signal_rw,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase
|
|
16
|
+
from dodal.devices.i09_1_shared.hard_undulator_functions import (
|
|
17
|
+
MAX_ENERGY_COLUMN,
|
|
18
|
+
MIN_ENERGY_COLUMN,
|
|
19
|
+
)
|
|
20
|
+
from dodal.devices.undulator import UndulatorInMm, UndulatorOrder
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HardInsertionDeviceEnergy(StandardReadable, Movable[float]):
|
|
24
|
+
"""
|
|
25
|
+
Compound device to link hard x-ray undulator gap and order to photon energy.
|
|
26
|
+
Setting the energy adjusts the undulator gap accordingly.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
undulator_order: UndulatorOrder,
|
|
32
|
+
undulator: UndulatorInMm,
|
|
33
|
+
lut: dict[int, ndarray],
|
|
34
|
+
gap_to_energy_func: Callable[..., float],
|
|
35
|
+
energy_to_gap_func: Callable[..., float],
|
|
36
|
+
name: str = "",
|
|
37
|
+
) -> None:
|
|
38
|
+
self._lut = lut
|
|
39
|
+
self.gap_to_energy_func = gap_to_energy_func
|
|
40
|
+
self.energy_to_gap_func = energy_to_gap_func
|
|
41
|
+
self._undulator_order_ref = Reference(undulator_order)
|
|
42
|
+
self._undulator_ref = Reference(undulator)
|
|
43
|
+
|
|
44
|
+
self.add_readables([undulator_order, undulator.current_gap])
|
|
45
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
46
|
+
self.energy_demand = soft_signal_rw(float)
|
|
47
|
+
self.energy = derived_signal_rw(
|
|
48
|
+
raw_to_derived=self._read_energy,
|
|
49
|
+
set_derived=self._set_energy,
|
|
50
|
+
current_gap=self._undulator_ref().gap_motor.user_readback,
|
|
51
|
+
current_order=self._undulator_order_ref().value,
|
|
52
|
+
derived_units="keV",
|
|
53
|
+
)
|
|
54
|
+
super().__init__(name=name)
|
|
55
|
+
|
|
56
|
+
def _read_energy(self, current_gap: float, current_order: int) -> float:
|
|
57
|
+
return self.gap_to_energy_func(
|
|
58
|
+
gap=current_gap,
|
|
59
|
+
look_up_table=self._lut,
|
|
60
|
+
order=current_order,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def _set_energy(self, energy: float) -> None:
|
|
64
|
+
current_order = await self._undulator_order_ref().value.get_value()
|
|
65
|
+
min_energy, max_energy = self._lut[current_order][
|
|
66
|
+
MIN_ENERGY_COLUMN : MAX_ENERGY_COLUMN + 1
|
|
67
|
+
]
|
|
68
|
+
if not (min_energy <= energy <= max_energy):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Requested energy {energy} keV is out of range for harmonic {current_order}: "
|
|
71
|
+
f"[{min_energy}, {max_energy}] keV"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
target_gap = self.energy_to_gap_func(
|
|
75
|
+
photon_energy_kev=energy, look_up_table=self._lut, order=current_order
|
|
76
|
+
)
|
|
77
|
+
await self._undulator_ref().set(target_gap)
|
|
78
|
+
|
|
79
|
+
@AsyncStatus.wrap
|
|
80
|
+
async def set(self, value: float) -> None:
|
|
81
|
+
self.energy_demand.set(value)
|
|
82
|
+
await self.energy.set(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HardEnergy(StandardReadable, Locatable[float]):
|
|
86
|
+
"""
|
|
87
|
+
Energy compound device that provides combined change of both DCM energy and undulator gap accordingly.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
dcm: DoubleCrystalMonochromatorBase,
|
|
93
|
+
undulator_energy: HardInsertionDeviceEnergy,
|
|
94
|
+
name: str = "",
|
|
95
|
+
) -> None:
|
|
96
|
+
self._dcm_ref = Reference(dcm)
|
|
97
|
+
self._undulator_energy_ref = Reference(undulator_energy)
|
|
98
|
+
self.add_readables([undulator_energy, dcm.energy_in_keV])
|
|
99
|
+
super().__init__(name=name)
|
|
100
|
+
|
|
101
|
+
@AsyncStatus.wrap
|
|
102
|
+
async def set(self, value: float) -> None:
|
|
103
|
+
await gather(
|
|
104
|
+
self._dcm_ref().energy_in_keV.set(value),
|
|
105
|
+
self._undulator_energy_ref().set(value),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def locate(self) -> Location[float]:
|
|
109
|
+
return Location(
|
|
110
|
+
setpoint=await self._dcm_ref().energy_in_keV.user_setpoint.get_value(),
|
|
111
|
+
readback=await self._dcm_ref().energy_in_keV.user_readback.get_value(),
|
|
112
|
+
)
|
|
File without changes
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -11,19 +11,18 @@ from ophyd_async.core import (
|
|
|
11
11
|
soft_signal_rw,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
from dodal.devices.
|
|
14
|
+
from dodal.devices.insertion_device import (
|
|
15
15
|
MAXIMUM_MOVE_TIME,
|
|
16
16
|
Apple2,
|
|
17
17
|
Apple2Controller,
|
|
18
18
|
Apple2PhasesVal,
|
|
19
19
|
Apple2Val,
|
|
20
|
-
Pol,
|
|
21
20
|
UndulatorGap,
|
|
22
21
|
UndulatorJawPhase,
|
|
23
22
|
UndulatorPhaseAxes,
|
|
24
23
|
)
|
|
25
|
-
from dodal.devices.
|
|
26
|
-
from dodal.
|
|
24
|
+
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
25
|
+
from dodal.devices.insertion_device.id_enum import Pol
|
|
27
26
|
|
|
28
27
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
29
28
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -62,16 +61,18 @@ class I10Apple2(Apple2[UndulatorPhaseAxes]):
|
|
|
62
61
|
class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
63
62
|
"""
|
|
64
63
|
I10Apple2Controller is a extension of Apple2Controller which provide linear
|
|
65
|
-
|
|
64
|
+
arbitrary angle control.
|
|
66
65
|
"""
|
|
67
66
|
|
|
68
67
|
def __init__(
|
|
69
68
|
self,
|
|
70
69
|
apple2: I10Apple2,
|
|
71
|
-
|
|
70
|
+
gap_energy_motor_lut: EnergyMotorLookup,
|
|
71
|
+
phase_energy_motor_lut: EnergyMotorLookup,
|
|
72
72
|
jaw_phase_limit: float = 12.0,
|
|
73
73
|
jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
74
74
|
angle_threshold_deg=30.0,
|
|
75
|
+
units: str = "eV",
|
|
75
76
|
name: str = "",
|
|
76
77
|
) -> None:
|
|
77
78
|
"""
|
|
@@ -79,25 +80,30 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
79
80
|
-----------
|
|
80
81
|
apple2 : I10Apple2
|
|
81
82
|
An I10Apple2 device.
|
|
82
|
-
|
|
83
|
-
The class that handles the look up table logic for the insertion device.
|
|
83
|
+
gap_energy_motor_lut: EnergyMotorLookup
|
|
84
|
+
The class that handles the gap look up table logic for the insertion device.
|
|
85
|
+
phase_energy_motor_lut: EnergyMotorLookup
|
|
86
|
+
The class that handles the phase look up table logic for the insertion device.
|
|
84
87
|
jaw_phase_limit : float, optional
|
|
85
88
|
The maximum allowed jaw_phase movement., by default 12.0
|
|
86
89
|
jaw_phase_poly_param : list[float], optional
|
|
87
90
|
polynomial parameters highest power first., by default DEFAULT_JAW_PHASE_POLY_PARAMS
|
|
88
91
|
angle_threshold_deg : float, optional
|
|
89
92
|
The angle threshold to switch between 0-180 and 180-360 range., by default 30.0
|
|
93
|
+
units:
|
|
94
|
+
the units of this device. Defaults to eV.
|
|
90
95
|
name : str, optional
|
|
91
96
|
New device name.
|
|
92
97
|
"""
|
|
93
|
-
|
|
94
|
-
self.
|
|
98
|
+
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
99
|
+
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
95
100
|
super().__init__(
|
|
96
101
|
apple2=apple2,
|
|
97
|
-
|
|
102
|
+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
103
|
+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
104
|
+
units=units,
|
|
98
105
|
name=name,
|
|
99
106
|
)
|
|
100
|
-
|
|
101
107
|
self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
|
|
102
108
|
self.angle_threshold_deg = angle_threshold_deg
|
|
103
109
|
self.jaw_phase_limit = jaw_phase_limit
|
|
@@ -132,15 +138,9 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
132
138
|
await self.apple2().jaw_phase().set(jaw_phase)
|
|
133
139
|
await self._linear_arbitrary_angle.set(pol_angle)
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
"""
|
|
137
|
-
Set the undulator motors for a given energy and polarisation.
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
pol = await self._check_and_get_pol_setpoint()
|
|
141
|
-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
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
144
|
gap=f"{gap:.6f}",
|
|
145
145
|
phase=Apple2PhasesVal(
|
|
146
146
|
top_outer=f"{phase:.6f}",
|
|
@@ -150,8 +150,10 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
150
150
|
),
|
|
151
151
|
)
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
async def _set_motors_from_energy_and_polarisation(
|
|
154
|
+
self, energy: float, pol: Pol
|
|
155
|
+
) -> None:
|
|
156
|
+
await super()._set_motors_from_energy_and_polarisation(energy, pol)
|
|
155
157
|
if pol != Pol.LA:
|
|
156
158
|
await self.apple2().jaw_phase().set(0)
|
|
157
159
|
await self.apple2().jaw_phase().set_move.set(1)
|
dodal/devices/i17/i17_apple2.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from dodal.devices.apple2_undulator import (
|
|
1
|
+
from dodal.devices.insertion_device.apple2_undulator import (
|
|
2
2
|
Apple2,
|
|
3
3
|
Apple2Controller,
|
|
4
4
|
Apple2PhasesVal,
|
|
5
5
|
Apple2Val,
|
|
6
|
-
|
|
6
|
+
Pol,
|
|
7
|
+
UndulatorPhaseAxes,
|
|
7
8
|
)
|
|
8
|
-
from dodal.
|
|
9
|
+
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
9
10
|
|
|
10
11
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
11
12
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -15,7 +16,7 @@ ALPHA_OFFSET = 180
|
|
|
15
16
|
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
19
|
+
class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
|
|
19
20
|
"""
|
|
20
21
|
I10Apple2Controller is a extension of Apple2Controller which provide linear
|
|
21
22
|
arbitrary angle control.
|
|
@@ -23,32 +24,43 @@ class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
|
23
24
|
|
|
24
25
|
def __init__(
|
|
25
26
|
self,
|
|
26
|
-
apple2: Apple2,
|
|
27
|
-
|
|
27
|
+
apple2: Apple2[UndulatorPhaseAxes],
|
|
28
|
+
gap_energy_motor_lut: EnergyMotorLookup,
|
|
29
|
+
phase_energy_motor_lut: EnergyMotorLookup,
|
|
30
|
+
units: str = "eV",
|
|
28
31
|
name: str = "",
|
|
29
32
|
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Parameters:
|
|
35
|
+
-----------
|
|
36
|
+
apple2 : Apple2
|
|
37
|
+
An Apple2 device.
|
|
38
|
+
gap_energy_motor_lut: EnergyMotorLookup
|
|
39
|
+
The class that handles the gap look up table logic for the insertion device.
|
|
40
|
+
phase_energy_motor_lut: EnergyMotorLookup
|
|
41
|
+
The class that handles the phase look up table logic for the insertion device.
|
|
42
|
+
units:
|
|
43
|
+
the units of this device. Defaults to eV.
|
|
44
|
+
name : str, optional
|
|
45
|
+
New device name.
|
|
46
|
+
"""
|
|
47
|
+
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
48
|
+
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
30
49
|
super().__init__(
|
|
31
50
|
apple2=apple2,
|
|
32
|
-
|
|
51
|
+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
52
|
+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
53
|
+
units=units,
|
|
33
54
|
name=name,
|
|
34
55
|
)
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Set the undulator motors for a given energy and polarisation.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
pol = await self._check_and_get_pol_setpoint()
|
|
42
|
-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
43
|
-
id_set_val = Apple2Val(
|
|
57
|
+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
58
|
+
return Apple2Val(
|
|
44
59
|
gap=f"{gap:.6f}",
|
|
45
60
|
phase=Apple2PhasesVal(
|
|
46
61
|
top_outer=f"{phase:.6f}",
|
|
47
|
-
top_inner="0.0",
|
|
62
|
+
top_inner=f"{0.0:.6f}",
|
|
48
63
|
btm_inner=f"{phase:.6f}",
|
|
49
|
-
btm_outer="0.0",
|
|
64
|
+
btm_outer=f"{0.0:.6f}",
|
|
50
65
|
),
|
|
51
66
|
)
|
|
52
|
-
|
|
53
|
-
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
|
54
|
-
await self.apple2().set(id_motor_values=id_set_val)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Annotated, Any, Self
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import AsyncStatus
|
|
4
|
+
from pydantic import BaseModel, model_validator
|
|
5
|
+
from pydantic.types import PositiveInt, StringConstraints
|
|
6
|
+
|
|
7
|
+
from dodal.devices.i19.access_controlled.blueapi_device import (
|
|
8
|
+
OpticsBlueAPIDevice,
|
|
9
|
+
)
|
|
10
|
+
from dodal.devices.i19.access_controlled.hutch_access import ACCESS_DEVICE_NAME
|
|
11
|
+
|
|
12
|
+
PermittedKeyStr = Annotated[str, StringConstraints(pattern="^[A-Za-z0-9-_]*$")]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AttenuatorMotorPositionDemands(BaseModel):
|
|
16
|
+
continuous_demands: dict[PermittedKeyStr, float] = {}
|
|
17
|
+
indexed_demands: dict[PermittedKeyStr, PositiveInt] = {}
|
|
18
|
+
|
|
19
|
+
@model_validator(mode="after")
|
|
20
|
+
def no_keys_clash(self) -> Self:
|
|
21
|
+
common_keys = set(self.continuous_demands).intersection(self.indexed_demands)
|
|
22
|
+
common_key_count = sum(1 for _ in common_keys)
|
|
23
|
+
if common_key_count < 1:
|
|
24
|
+
return self
|
|
25
|
+
else:
|
|
26
|
+
ks: str = "key" if common_key_count == 1 else "keys"
|
|
27
|
+
error_msg = f"Common {ks} found in distinct motor demands: {common_keys}"
|
|
28
|
+
raise ValueError(error_msg)
|
|
29
|
+
|
|
30
|
+
def validated_complete_demand(self) -> dict[PermittedKeyStr, Any]:
|
|
31
|
+
return self.continuous_demands | self.indexed_demands
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AttenuatorMotorSquad(OpticsBlueAPIDevice):
|
|
35
|
+
""" I19-specific proxy device which requests absorber position changes in the x-ray attenuator.
|
|
36
|
+
|
|
37
|
+
Sends REST call to blueapi controlling optics on the I19 cluster.
|
|
38
|
+
The hutch in use is compared against the hutch which sent the REST call.
|
|
39
|
+
Only the hutch in use will be permitted to execute a plan (requesting motor moves).
|
|
40
|
+
As the two hutches are located in series, checking the hutch in use is necessary to \
|
|
41
|
+
avoid accidentally operating optics devices from one hutch while the other has beam time.
|
|
42
|
+
|
|
43
|
+
The name of the hutch that wants to operate the optics device is passed to the \
|
|
44
|
+
access controlled device upon instantiation of the latter.
|
|
45
|
+
|
|
46
|
+
For details see the architecture described in \
|
|
47
|
+
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@AsyncStatus.wrap
|
|
51
|
+
async def set(self, value: AttenuatorMotorPositionDemands):
|
|
52
|
+
request_params = {
|
|
53
|
+
"name": "operate_motor_squad_plan",
|
|
54
|
+
"params": {
|
|
55
|
+
"experiment_hutch": self._invoking_hutch,
|
|
56
|
+
"access_device": ACCESS_DEVICE_NAME,
|
|
57
|
+
"attenuator_demands": value.validated_complete_demand(),
|
|
58
|
+
},
|
|
59
|
+
"instrument_session": self.instrument_session,
|
|
60
|
+
}
|
|
61
|
+
await super().set(request_params)
|
|
@@ -29,11 +29,19 @@ class OpticsBlueAPIDevice(StandardReadable, Movable[D]):
|
|
|
29
29
|
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self, hutch: HutchState, instrument_session: str = "", name: str = ""
|
|
34
|
+
) -> None:
|
|
35
|
+
self.hutch_request = hutch
|
|
36
|
+
self.instrument_session = instrument_session
|
|
33
37
|
self.url = OPTICS_BLUEAPI_URL
|
|
34
38
|
self.headers = HEADERS
|
|
35
39
|
super().__init__(name)
|
|
36
40
|
|
|
41
|
+
@property
|
|
42
|
+
def _invoking_hutch(self) -> str:
|
|
43
|
+
return self.hutch_request.value
|
|
44
|
+
|
|
37
45
|
@AsyncStatus.wrap
|
|
38
46
|
async def set(self, value: D):
|
|
39
47
|
""" On set send a POST request to the optics blueapi with the name and \
|
|
@@ -36,16 +36,14 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
|
|
|
36
36
|
# see https://github.com/DiamondLightSource/blueapi/issues/1187
|
|
37
37
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
38
38
|
self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
|
|
39
|
-
|
|
40
|
-
self.instrument_session = instrument_session
|
|
41
|
-
super().__init__(name)
|
|
39
|
+
super().__init__(hutch=hutch, instrument_session=instrument_session, name=name)
|
|
42
40
|
|
|
43
41
|
@AsyncStatus.wrap
|
|
44
42
|
async def set(self, value: ShutterDemand):
|
|
45
43
|
request_params = {
|
|
46
44
|
"name": "operate_shutter_plan",
|
|
47
45
|
"params": {
|
|
48
|
-
"experiment_hutch": self.
|
|
46
|
+
"experiment_hutch": self._invoking_hutch,
|
|
49
47
|
"access_device": ACCESS_DEVICE_NAME,
|
|
50
48
|
"shutter_demand": value.value,
|
|
51
49
|
},
|
dodal/devices/i21/__init__.py
CHANGED