dls-dodal 1.62.0__py3-none-any.whl → 1.64.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.62.0.dist-info → dls_dodal-1.64.0.dist-info}/METADATA +3 -3
- {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/RECORD +89 -76
- dls_dodal-1.64.0.dist-info/entry_points.txt +3 -0
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +1 -0
- dodal/beamlines/adsim.py +5 -3
- dodal/beamlines/b21.py +3 -1
- dodal/beamlines/i02_2.py +32 -0
- dodal/beamlines/i03.py +9 -0
- dodal/beamlines/i07.py +21 -0
- dodal/beamlines/i09.py +11 -4
- dodal/beamlines/i09_1.py +10 -4
- dodal/beamlines/i09_2.py +30 -0
- dodal/beamlines/i10.py +7 -69
- dodal/beamlines/i10_1.py +35 -0
- dodal/beamlines/i10_optics.py +231 -0
- dodal/beamlines/i15_1.py +5 -5
- dodal/beamlines/i17.py +60 -1
- dodal/beamlines/i18.py +15 -9
- dodal/beamlines/i19_1.py +3 -3
- dodal/beamlines/i19_2.py +2 -2
- dodal/beamlines/i19_optics.py +4 -1
- dodal/beamlines/i21.py +31 -1
- dodal/beamlines/i24.py +3 -3
- dodal/cli.py +7 -7
- dodal/common/visit.py +4 -4
- dodal/devices/aperturescatterguard.py +6 -4
- dodal/devices/apple2_undulator.py +225 -126
- dodal/devices/attenuator/filter_selections.py +6 -6
- dodal/devices/b07_1/ccmc.py +1 -1
- dodal/devices/common_dcm.py +63 -16
- dodal/devices/current_amplifiers/femto.py +4 -4
- dodal/devices/current_amplifiers/sr570.py +3 -3
- dodal/devices/fast_grid_scan.py +4 -4
- dodal/devices/fast_shutter.py +19 -7
- dodal/devices/i02_2/__init__.py +0 -0
- dodal/devices/i03/dcm.py +4 -2
- dodal/devices/i03/undulator_dcm.py +1 -1
- dodal/devices/i04/murko_results.py +35 -14
- dodal/devices/i07/__init__.py +0 -0
- dodal/devices/i07/dcm.py +33 -0
- dodal/devices/i09/__init__.py +1 -2
- dodal/devices/i09_1_shared/__init__.py +3 -0
- dodal/devices/i09_1_shared/hard_undulator_functions.py +111 -0
- dodal/devices/i10/__init__.py +29 -0
- dodal/devices/i10/diagnostics.py +37 -5
- dodal/devices/i10/i10_apple2.py +125 -229
- dodal/devices/i10/slits.py +38 -6
- dodal/devices/i15/dcm.py +7 -46
- dodal/devices/i17/__init__.py +0 -0
- dodal/devices/i17/i17_apple2.py +51 -0
- dodal/devices/i19/access_controlled/__init__.py +0 -0
- dodal/devices/i19/{shutter.py → access_controlled/shutter.py} +7 -4
- dodal/devices/i22/dcm.py +3 -3
- dodal/devices/i24/dcm.py +2 -2
- dodal/devices/oav/oav_detector.py +1 -1
- dodal/devices/oav/oav_parameters.py +4 -4
- dodal/devices/oav/oav_to_redis_forwarder.py +4 -4
- dodal/devices/oav/pin_image_recognition/__init__.py +3 -3
- dodal/devices/oav/pin_image_recognition/utils.py +1 -1
- dodal/devices/oav/snapshots/snapshot.py +1 -1
- dodal/devices/oav/snapshots/snapshot_image_processing.py +12 -12
- dodal/devices/oav/snapshots/snapshot_with_grid.py +1 -1
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/pgm.py +3 -3
- dodal/devices/robot.py +5 -5
- dodal/devices/tetramm.py +9 -5
- dodal/devices/thawer.py +0 -4
- dodal/devices/util/lookup_tables.py +8 -2
- dodal/devices/v2f.py +2 -2
- dodal/devices/zebra/zebra_constants_mapping.py +2 -2
- dodal/devices/zocalo/__init__.py +4 -4
- dodal/devices/zocalo/zocalo_results.py +4 -4
- dodal/log.py +9 -9
- dodal/plan_stubs/motor_utils.py +4 -4
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -2
- dodal/plans/save_panda.py +7 -7
- dodal/plans/verify_undulator_gap.py +4 -4
- dodal/testing/fixtures/__init__.py +0 -0
- dodal/testing/fixtures/run_engine.py +46 -0
- dodal/testing/fixtures/utils.py +57 -0
- dls_dodal-1.62.0.dist-info/entry_points.txt +0 -3
- dodal/beamlines/i10-1.py +0 -25
- dodal/devices/i09/dcm.py +0 -26
- {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.62.0.dist-info → dls_dodal-1.64.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/areadetector/plugins/{CAM.py → cam.py} +0 -0
- /dodal/devices/areadetector/plugins/{MJPG.py → mjpg.py} +0 -0
- /dodal/devices/i18/{KBMirror.py → kb_mirror.py} +0 -0
- /dodal/devices/i19/{blueapi_device.py → access_controlled/blueapi_device.py} +0 -0
- /dodal/devices/i19/{hutch_access.py → access_controlled/hutch_access.py} +0 -0
dodal/devices/common_dcm.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import Generic, TypeVar
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
StandardReadable,
|
|
5
|
+
derived_signal_r,
|
|
5
6
|
)
|
|
6
7
|
from ophyd_async.epics.core import epics_signal_r
|
|
7
8
|
from ophyd_async.epics.motor import Motor
|
|
@@ -31,42 +32,39 @@ Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
|
|
|
31
32
|
Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class
|
|
35
|
+
class DoubleCrystalMonochromatorBase(StandardReadable, Generic[Xtal_1, Xtal_2]):
|
|
35
36
|
"""
|
|
36
|
-
|
|
37
|
+
Base device for the double crystal monochromator (DCM), used to select the energy of the beam.
|
|
37
38
|
|
|
38
39
|
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
|
|
39
40
|
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
|
|
40
41
|
This base device accounts for all combinations of this.
|
|
41
42
|
|
|
42
|
-
This device should act as a parent for beamline-specific DCM's
|
|
43
|
+
This device should act as a parent for beamline-specific DCM's which do not match the standard EPICS interface, it provides
|
|
44
|
+
only energy and the crystal configuration. Most beamlines should use DoubleCrystalMonochromator instead
|
|
43
45
|
|
|
44
46
|
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
|
|
45
|
-
which only requires one crystal which can roll should be typed
|
|
47
|
+
which only requires one crystal which can roll should be typed
|
|
48
|
+
'def my_plan(dcm: DoubleCrystalMonochromatorBase[RollCrystal, StationaryCrystal])`
|
|
46
49
|
"""
|
|
47
50
|
|
|
48
51
|
def __init__(
|
|
49
52
|
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
50
53
|
) -> None:
|
|
51
54
|
with self.add_children_as_readables():
|
|
52
|
-
# Virtual motor PV's which set the physical motors so that the DCM produces requested
|
|
53
|
-
|
|
54
|
-
self.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# Real motors
|
|
58
|
-
self.bragg_in_degrees = Motor(prefix + "BRAGG")
|
|
59
|
-
# Offset ensures that the beam exits the DCM at the same point, regardless of energy.
|
|
60
|
-
self.offset_in_mm = Motor(prefix + "OFFSET")
|
|
61
|
-
|
|
62
|
-
self.crystal_metadata_d_spacing_a = epics_signal_r(
|
|
63
|
-
float, prefix + "DSPACING:RBV"
|
|
55
|
+
# Virtual motor PV's which set the physical motors so that the DCM produces requested energy
|
|
56
|
+
self.energy_in_keV = Motor(prefix + "ENERGY")
|
|
57
|
+
self.energy_in_eV = derived_signal_r(
|
|
58
|
+
self._convert_keV_to_eV, energy_signal=self.energy_in_keV.user_readback
|
|
64
59
|
)
|
|
65
60
|
|
|
66
61
|
self._make_crystals(prefix, xtal_1, xtal_2)
|
|
67
62
|
|
|
68
63
|
super().__init__(name)
|
|
69
64
|
|
|
65
|
+
def _convert_keV_to_eV(self, energy_signal: float) -> float: # noqa: N802
|
|
66
|
+
return energy_signal * 1000
|
|
67
|
+
|
|
70
68
|
# Prefix convention is different depending on whether there are one or two controllable crystals
|
|
71
69
|
def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
|
|
72
70
|
if StationaryCrystal not in [xtal_1, xtal_2]:
|
|
@@ -75,3 +73,52 @@ class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
|
|
|
75
73
|
else:
|
|
76
74
|
self.xtal_1 = xtal_1(prefix)
|
|
77
75
|
self.xtal_2 = xtal_2(prefix)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DoubleCrystalMonochromator(
|
|
79
|
+
DoubleCrystalMonochromatorBase, Generic[Xtal_1, Xtal_2]
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
|
|
83
|
+
|
|
84
|
+
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
|
|
85
|
+
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
|
|
86
|
+
This base device accounts for all combinations of this.
|
|
87
|
+
|
|
88
|
+
This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
|
|
89
|
+
|
|
90
|
+
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan which only
|
|
91
|
+
requires one crystal which can roll should be typed 'def my_plan(dcm: DoubleCrystalMonochromator[RollCrystal, StationaryCrystal])`
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
96
|
+
) -> None:
|
|
97
|
+
super().__init__(prefix, xtal_1, xtal_2, name)
|
|
98
|
+
with self.add_children_as_readables():
|
|
99
|
+
# Virtual motor PV's which set the physical motors so that the DCM produces requested
|
|
100
|
+
# wavelength
|
|
101
|
+
self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
|
|
102
|
+
|
|
103
|
+
# Real motors
|
|
104
|
+
self.bragg_in_degrees = Motor(prefix + "BRAGG")
|
|
105
|
+
# Offset ensures that the beam exits the DCM at the same point, regardless of energy.
|
|
106
|
+
self.offset_in_mm = Motor(prefix + "OFFSET")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DoubleCrystalMonochromatorWithDSpacing(
|
|
110
|
+
DoubleCrystalMonochromator, Generic[Xtal_1, Xtal_2]
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Adds crystal D-spacing metadata to the DoubleCrystalMonochromator class. This should be used in preference to the
|
|
114
|
+
DoubleCrystalMonochromator on beamlines which have a "DSPACING:RBV" pv on their DCM.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
119
|
+
) -> None:
|
|
120
|
+
super().__init__(prefix, xtal_1, xtal_2, name)
|
|
121
|
+
with self.add_children_as_readables():
|
|
122
|
+
self.crystal_metadata_d_spacing_a = epics_signal_r(
|
|
123
|
+
float, prefix + "DSPACING:RBV"
|
|
124
|
+
)
|
|
@@ -104,15 +104,15 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
104
104
|
+ "\n Available gain:"
|
|
105
105
|
+ f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
|
|
106
106
|
)
|
|
107
|
-
|
|
108
|
-
LOGGER.info(f"{self.name} gain change to {
|
|
107
|
+
sensitivity_setting = self.gain_conversion_table(value).name
|
|
108
|
+
LOGGER.info(f"{self.name} gain change to {sensitivity_setting}:{value}")
|
|
109
109
|
|
|
110
110
|
await self.gain.set(
|
|
111
|
-
value=self.gain_table[
|
|
111
|
+
value=self.gain_table[sensitivity_setting],
|
|
112
112
|
timeout=self.timeout,
|
|
113
113
|
)
|
|
114
114
|
# wait for current amplifier's bandpass filter to settle.
|
|
115
|
-
await asyncio.sleep(self.raise_timetable[
|
|
115
|
+
await asyncio.sleep(self.raise_timetable[sensitivity_setting].value)
|
|
116
116
|
|
|
117
117
|
async def increase_gain(self, value: int = 1) -> None:
|
|
118
118
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
@@ -79,7 +79,7 @@ class SR570FullGainTable(Enum):
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
class SR570GainToCurrentTable(float, Enum):
|
|
82
|
-
"""Conversion table for gain(
|
|
82
|
+
"""Conversion table for gain (sensitivity) to current"""
|
|
83
83
|
|
|
84
84
|
SEN_1 = 1e3
|
|
85
85
|
SEN_2 = 2e3
|
|
@@ -168,10 +168,10 @@ class SR570(CurrentAmp):
|
|
|
168
168
|
+ "\n Available gain:"
|
|
169
169
|
+ f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
|
|
170
170
|
)
|
|
171
|
-
|
|
171
|
+
sensitivity_setting = self.gain_conversion_table(value).name
|
|
172
172
|
LOGGER.info(f"{self.name} gain change to {value}")
|
|
173
173
|
|
|
174
|
-
coarse_gain, fine_gain = self.combined_table[
|
|
174
|
+
coarse_gain, fine_gain = self.combined_table[sensitivity_setting].value
|
|
175
175
|
await asyncio.gather(
|
|
176
176
|
self.fine_gain.set(value=fine_gain, timeout=self.timeout),
|
|
177
177
|
self.coarse_gain.set(value=coarse_gain, timeout=self.timeout),
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -31,7 +31,7 @@ from dodal.log import LOGGER
|
|
|
31
31
|
from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class
|
|
34
|
+
class GridScanInvalidError(RuntimeError):
|
|
35
35
|
"""Raised when the gridscan parameters are not valid."""
|
|
36
36
|
|
|
37
37
|
|
|
@@ -302,7 +302,7 @@ class FastGridScanCommon(
|
|
|
302
302
|
value: the gridscan parameters
|
|
303
303
|
|
|
304
304
|
Raises:
|
|
305
|
-
|
|
305
|
+
GridScanInvalidError: if the gridscan parameters were not valid
|
|
306
306
|
"""
|
|
307
307
|
set_statuses = []
|
|
308
308
|
|
|
@@ -326,7 +326,7 @@ class FastGridScanCommon(
|
|
|
326
326
|
self.scan_invalid, 0.0, timeout=self.VALIDITY_CHECK_TIMEOUT
|
|
327
327
|
)
|
|
328
328
|
except TimeoutError as e:
|
|
329
|
-
raise
|
|
329
|
+
raise GridScanInvalidError(
|
|
330
330
|
f"Gridscan parameters not validated after {self.VALIDITY_CHECK_TIMEOUT}s"
|
|
331
331
|
) from e
|
|
332
332
|
|
|
@@ -470,6 +470,6 @@ def set_fast_grid_scan_params(scan: FastGridScanCommon[ParamType], params: Param
|
|
|
470
470
|
params: The parameters to set
|
|
471
471
|
|
|
472
472
|
Raises:
|
|
473
|
-
|
|
473
|
+
GridScanInvalidError: if the grid scan parameters are not valid
|
|
474
474
|
"""
|
|
475
475
|
yield from prepare(scan, params, wait=True)
|
dodal/devices/fast_shutter.py
CHANGED
|
@@ -21,27 +21,27 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
|
21
21
|
await shutter.set(shutter.open_state)
|
|
22
22
|
await shutter.set(shutter.close_state)
|
|
23
23
|
OR
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
run_engine(bps.mv(shutter, shutter.open_state))
|
|
25
|
+
run_engine(bps.mv(shutter, shutter.close_state))
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
def __init__(
|
|
29
29
|
self,
|
|
30
|
-
|
|
30
|
+
pv: str,
|
|
31
31
|
open_state: StrictEnumT,
|
|
32
32
|
close_state: StrictEnumT,
|
|
33
33
|
name: str = "",
|
|
34
34
|
):
|
|
35
35
|
"""
|
|
36
36
|
Arguments:
|
|
37
|
-
|
|
37
|
+
pv: The pv to connect to the shutter device.
|
|
38
38
|
open_state: The enum value that corresponds with opening the shutter.
|
|
39
39
|
close_state: The enum value that corresponds with closing the shutter.
|
|
40
40
|
"""
|
|
41
41
|
self.open_state = open_state
|
|
42
42
|
self.close_state = close_state
|
|
43
43
|
with self.add_children_as_readables():
|
|
44
|
-
self.state = epics_signal_rw(type(self.open_state),
|
|
44
|
+
self.state = epics_signal_rw(type(self.open_state), pv)
|
|
45
45
|
super().__init__(name)
|
|
46
46
|
|
|
47
47
|
@AsyncStatus.wrap
|
|
@@ -49,9 +49,21 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
|
49
49
|
await self.state.set(value)
|
|
50
50
|
|
|
51
51
|
async def is_open(self) -> bool:
|
|
52
|
-
"""
|
|
52
|
+
"""
|
|
53
|
+
Checks to see if shutter is in open_state. Should not be used directly inside a
|
|
54
|
+
plan. A user should use the following instead in a plan:
|
|
55
|
+
|
|
56
|
+
from bluesky import plan_stubs as bps
|
|
57
|
+
is_open = yield from bps.rd(shutter.state) == shutter.open_state
|
|
58
|
+
"""
|
|
53
59
|
return await self.state.get_value() == self.open_state
|
|
54
60
|
|
|
55
61
|
async def is_closed(self) -> bool:
|
|
56
|
-
"""
|
|
62
|
+
"""
|
|
63
|
+
Checks to see if shutter is in close_state. Should not be used directly inside a
|
|
64
|
+
plan. A user should use the following instead in a plan:
|
|
65
|
+
|
|
66
|
+
from bluesky import plan_stubs as bps
|
|
67
|
+
is_closed = yield from bps.rd(shutter.state) == shutter.close_state
|
|
68
|
+
"""
|
|
57
69
|
return await self.state.get_value() == self.close_state
|
|
File without changes
|
dodal/devices/i03/dcm.py
CHANGED
|
@@ -9,13 +9,15 @@ from dodal.common.crystal_metadata import (
|
|
|
9
9
|
make_crystal_metadata_from_material,
|
|
10
10
|
)
|
|
11
11
|
from dodal.devices.common_dcm import (
|
|
12
|
-
|
|
12
|
+
DoubleCrystalMonochromatorWithDSpacing,
|
|
13
13
|
PitchAndRollCrystal,
|
|
14
14
|
StationaryCrystal,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class DCM(
|
|
18
|
+
class DCM(
|
|
19
|
+
DoubleCrystalMonochromatorWithDSpacing[PitchAndRollCrystal, StationaryCrystal]
|
|
20
|
+
):
|
|
19
21
|
"""
|
|
20
22
|
A double crystal monochromator (DCM), used to select the energy of the beam.
|
|
21
23
|
|
|
@@ -58,7 +58,7 @@ class UndulatorDCM(StandardReadable, Movable[float]):
|
|
|
58
58
|
async def set(self, value: float):
|
|
59
59
|
await self.undulator_ref().raise_if_not_enabled()
|
|
60
60
|
await asyncio.gather(
|
|
61
|
-
self.dcm_ref().
|
|
61
|
+
self.dcm_ref().energy_in_keV.set(value, timeout=ENERGY_TIMEOUT_S),
|
|
62
62
|
self.undulator_ref().set(value),
|
|
63
63
|
)
|
|
64
64
|
|
|
@@ -43,7 +43,7 @@ class Coord(Enum):
|
|
|
43
43
|
|
|
44
44
|
@dataclass
|
|
45
45
|
class MurkoResult:
|
|
46
|
-
|
|
46
|
+
chosen_point_px: tuple[int, int]
|
|
47
47
|
x_dist_mm: float
|
|
48
48
|
y_dist_mm: float
|
|
49
49
|
omega: float
|
|
@@ -51,7 +51,7 @@ class MurkoResult:
|
|
|
51
51
|
metadata: MurkoMetadata
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
class
|
|
54
|
+
class NoResultsFoundError(ValueError):
|
|
55
55
|
pass
|
|
56
56
|
|
|
57
57
|
|
|
@@ -73,6 +73,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
73
73
|
|
|
74
74
|
TIMEOUT_S = 2
|
|
75
75
|
PERCENTAGE_TO_USE = 25
|
|
76
|
+
LEFTMOST_PIXEL_TO_USE = 10
|
|
76
77
|
NUMBER_OF_WRONG_RESULTS_TO_LOG = 5
|
|
77
78
|
|
|
78
79
|
def __init__(
|
|
@@ -129,7 +130,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
129
130
|
await self.process_batch(message, sample_id)
|
|
130
131
|
|
|
131
132
|
if not self._results:
|
|
132
|
-
raise
|
|
133
|
+
raise NoResultsFoundError("No results retrieved from Murko")
|
|
133
134
|
|
|
134
135
|
for result in self._results:
|
|
135
136
|
LOGGER.debug(result)
|
|
@@ -186,16 +187,16 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
186
187
|
else:
|
|
187
188
|
shape = result["original_shape"] # Dimensions of image in pixels
|
|
188
189
|
# Murko returns coords as y, x
|
|
189
|
-
|
|
190
|
+
chosen_point_px = (coords[1] * shape[1], coords[0] * shape[0])
|
|
190
191
|
|
|
191
192
|
beam_dist_px = calculate_beam_distance(
|
|
192
193
|
(metadata["beam_centre_i"], metadata["beam_centre_j"]),
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
chosen_point_px[0],
|
|
195
|
+
chosen_point_px[1],
|
|
195
196
|
)
|
|
196
197
|
self._results.append(
|
|
197
198
|
MurkoResult(
|
|
198
|
-
|
|
199
|
+
chosen_point_px=chosen_point_px,
|
|
199
200
|
x_dist_mm=beam_dist_px[0] * metadata["microns_per_x_pixel"] / 1000,
|
|
200
201
|
y_dist_mm=beam_dist_px[1] * metadata["microns_per_y_pixel"] / 1000,
|
|
201
202
|
omega=omega,
|
|
@@ -209,10 +210,27 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
209
210
|
"""Whilst murko is not fully trained it often gives us poor results.
|
|
210
211
|
When it is wrong it usually picks up the base of the pin, rather than the tip,
|
|
211
212
|
meaning that by keeping only a percentage of the results with the smallest X we
|
|
212
|
-
remove many of the outliers.
|
|
213
|
+
remove many of the outliers. Murko also occasionally picks a point in the bottom
|
|
214
|
+
left corner, which can be removed by filtering results with a small x pixel.
|
|
213
215
|
"""
|
|
216
|
+
|
|
214
217
|
LOGGER.info(f"Number of results before filtering: {len(self._results)}")
|
|
215
|
-
sorted_results = sorted(self._results, key=lambda item: item.
|
|
218
|
+
sorted_results = sorted(self._results, key=lambda item: item.chosen_point_px[0])
|
|
219
|
+
|
|
220
|
+
results_without_tiny_x = [
|
|
221
|
+
result
|
|
222
|
+
for result in sorted_results
|
|
223
|
+
if result.chosen_point_px[0] >= self.LEFTMOST_PIXEL_TO_USE
|
|
224
|
+
]
|
|
225
|
+
result_uuids_with_tiny_x = [
|
|
226
|
+
result.uuid
|
|
227
|
+
for result in sorted_results
|
|
228
|
+
if result not in results_without_tiny_x
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
LOGGER.info(
|
|
232
|
+
f"Results with tiny x have been removed: {result_uuids_with_tiny_x}"
|
|
233
|
+
)
|
|
216
234
|
|
|
217
235
|
worst_results = [
|
|
218
236
|
r.uuid for r in sorted_results[-self.NUMBER_OF_WRONG_RESULTS_TO_LOG :]
|
|
@@ -221,14 +239,17 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
221
239
|
LOGGER.info(
|
|
222
240
|
f"Worst {self.NUMBER_OF_WRONG_RESULTS_TO_LOG} murko results were {worst_results}"
|
|
223
241
|
)
|
|
242
|
+
|
|
224
243
|
cutoff = max(1, int(len(sorted_results) * self.PERCENTAGE_TO_USE / 100))
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
cutoff = min(cutoff, len(results_without_tiny_x))
|
|
245
|
+
|
|
246
|
+
best_x = results_without_tiny_x[:cutoff]
|
|
227
247
|
|
|
228
|
-
|
|
248
|
+
for result in sorted_results:
|
|
249
|
+
result.metadata["used_for_centring"] = result in best_x
|
|
229
250
|
|
|
230
|
-
LOGGER.info(f"Number of results after filtering: {len(
|
|
231
|
-
return
|
|
251
|
+
LOGGER.info(f"Number of results after filtering: {len(best_x)}")
|
|
252
|
+
return best_x
|
|
232
253
|
|
|
233
254
|
|
|
234
255
|
def get_yz_least_squares(vertical_dists: list, omegas: list) -> tuple[float, float]:
|
|
File without changes
|
dodal/devices/i07/dcm.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
2
|
+
from ophyd_async.epics.motor import Motor
|
|
3
|
+
|
|
4
|
+
from dodal.devices.common_dcm import (
|
|
5
|
+
DoubleCrystalMonochromator,
|
|
6
|
+
PitchAndRollCrystal,
|
|
7
|
+
StationaryCrystal,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DCM(DoubleCrystalMonochromator[PitchAndRollCrystal, StationaryCrystal]):
|
|
12
|
+
"""
|
|
13
|
+
Device for i07's DCM, including temperature monitors and vertical motor which were
|
|
14
|
+
included in GDA.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
motor_prefix: str,
|
|
20
|
+
xtal_prefix: str,
|
|
21
|
+
name: str = "",
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(motor_prefix, PitchAndRollCrystal, StationaryCrystal, name)
|
|
24
|
+
with self.add_children_as_readables():
|
|
25
|
+
self.vertical_in_mm = Motor(motor_prefix + "PERP")
|
|
26
|
+
|
|
27
|
+
# temperatures
|
|
28
|
+
self.xtal1_temp = epics_signal_r(float, xtal_prefix + "PT100-2")
|
|
29
|
+
self.xtal2_temp = epics_signal_r(float, xtal_prefix + "PT100-3")
|
|
30
|
+
self.xtal1_holder_temp = epics_signal_r(float, xtal_prefix + "PT100-1")
|
|
31
|
+
self.xtal2_holder_temp = epics_signal_r(float, xtal_prefix + "PT100-4")
|
|
32
|
+
self.gap_motor = epics_signal_r(float, xtal_prefix + "TC-1")
|
|
33
|
+
self.white_beam_stop_temp = epics_signal_r(float, xtal_prefix + "WBS:TEMP")
|
dodal/devices/i09/__init__.py
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from dodal.devices.util.lookup_tables import energy_distance_table
|
|
4
|
+
from dodal.log import LOGGER
|
|
5
|
+
|
|
6
|
+
LUT_COMMENTS = ["#"]
|
|
7
|
+
HU_SKIP_ROWS = 3
|
|
8
|
+
|
|
9
|
+
# Physics constants
|
|
10
|
+
ELECTRON_REST_ENERGY_MEV = 0.510999
|
|
11
|
+
|
|
12
|
+
# Columns in the lookup table
|
|
13
|
+
RING_ENERGY_COLUMN = 1
|
|
14
|
+
MAGNET_FIELD_COLUMN = 2
|
|
15
|
+
MIN_ENERGY_COLUMN = 3
|
|
16
|
+
MAX_ENERGY_COLUMN = 4
|
|
17
|
+
GAP_OFFSET_COLUMN = 7
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def get_hu_lut_as_dict(lut_path: str) -> dict:
|
|
21
|
+
lut_dict: dict = {}
|
|
22
|
+
_lookup_table: np.ndarray = await energy_distance_table(
|
|
23
|
+
lut_path,
|
|
24
|
+
comments=LUT_COMMENTS,
|
|
25
|
+
skiprows=HU_SKIP_ROWS,
|
|
26
|
+
)
|
|
27
|
+
for i in range(_lookup_table.shape[0]):
|
|
28
|
+
lut_dict[_lookup_table[i][0]] = _lookup_table[i]
|
|
29
|
+
LOGGER.debug(f"Loaded lookup table:\n {lut_dict}")
|
|
30
|
+
return lut_dict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def calculate_gap_i09_hu(
|
|
34
|
+
photon_energy_kev: float,
|
|
35
|
+
look_up_table: dict[int, "np.ndarray"],
|
|
36
|
+
order: int = 1,
|
|
37
|
+
gap_offset: float = 0.0,
|
|
38
|
+
undulator_period_mm: int = 27,
|
|
39
|
+
) -> float:
|
|
40
|
+
"""
|
|
41
|
+
Calculate the undulator gap required to produce a given energy at a given harmonic order.
|
|
42
|
+
This algorithm was provided by the I09 beamline scientists, and is based on the physics of undulator radiation.
|
|
43
|
+
https://cxro.lbl.gov//PDF/X-Ray-Data-Booklet.pdf
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
photon_energy_kev (float): Requested photon energy in keV.
|
|
47
|
+
look_up_table (dict[int, np.ndarray]): Lookup table containing undulator and beamline parameters for each harmonic order.
|
|
48
|
+
order (int, optional): Harmonic order for which to calculate the gap. Defaults to 1.
|
|
49
|
+
gap_offset (float, optional): Additional gap offset to apply (in mm). Defaults to 0.0.
|
|
50
|
+
undulator_period_mm (int, optional): Undulator period in mm. Defaults to 27.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
float: Calculated undulator gap in millimeters.
|
|
54
|
+
"""
|
|
55
|
+
magnet_blocks_per_period = 4
|
|
56
|
+
magnet_block_height_mm = 16
|
|
57
|
+
|
|
58
|
+
if order not in look_up_table.keys():
|
|
59
|
+
raise ValueError(f"Order parameter {order} not found in lookup table")
|
|
60
|
+
|
|
61
|
+
gamma = 1000 * look_up_table[order][RING_ENERGY_COLUMN] / ELECTRON_REST_ENERGY_MEV
|
|
62
|
+
|
|
63
|
+
# Constructive interference of radiation emitted at different poles
|
|
64
|
+
# lamda = (lambda_u/2*gamma^2)*(1+K^2/2 + gamma^2*theta^2)/n for n=1,2,3...
|
|
65
|
+
# theta is the observation angle, assumed to be 0 here.
|
|
66
|
+
# Rearranging for K (the undulator parameter, related to magnetic field and gap)
|
|
67
|
+
# gives K^2 = 2*((2*n*gamma^2*lamda/lambda_u)-1)
|
|
68
|
+
|
|
69
|
+
undulator_parameter_sqr = (
|
|
70
|
+
4.959368e-6
|
|
71
|
+
* (order * gamma * gamma / (undulator_period_mm * photon_energy_kev))
|
|
72
|
+
- 2
|
|
73
|
+
)
|
|
74
|
+
if undulator_parameter_sqr < 0:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Diffraction parameter squared must be positive! Calculated value {undulator_parameter_sqr}."
|
|
77
|
+
)
|
|
78
|
+
undulator_parameter = np.sqrt(undulator_parameter_sqr)
|
|
79
|
+
|
|
80
|
+
# Undulator_parameter K is also defined as K = 0.934*B0[T]*lambda_u[cm],
|
|
81
|
+
# where B0[T] is a peak magnetic field that must depend on gap,
|
|
82
|
+
# but in our LUT it is does not depend on gap, so it's a factor,
|
|
83
|
+
# leading to K = 0.934*B0[T]*lambda_u[cm]*exp(-pi*gap/lambda_u) or
|
|
84
|
+
# K = undulator_parameter_max*exp(-pi*gap/lambda_u)
|
|
85
|
+
# Calculating undulator_parameter_max gives:
|
|
86
|
+
undulator_parameter_max = (
|
|
87
|
+
(
|
|
88
|
+
2
|
|
89
|
+
* 0.0934
|
|
90
|
+
* undulator_period_mm
|
|
91
|
+
* look_up_table[order][MAGNET_FIELD_COLUMN]
|
|
92
|
+
* magnet_blocks_per_period
|
|
93
|
+
/ np.pi
|
|
94
|
+
)
|
|
95
|
+
* np.sin(np.pi / magnet_blocks_per_period)
|
|
96
|
+
* (1 - np.exp(-2 * np.pi * magnet_block_height_mm / undulator_period_mm))
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Finnaly, rearranging the equation:
|
|
100
|
+
# undulator_parameter = undulator_parameter_max*exp(-pi*gap/lambda_u) for gap gives
|
|
101
|
+
gap = (
|
|
102
|
+
(undulator_period_mm / np.pi)
|
|
103
|
+
* np.log(undulator_parameter_max / undulator_parameter)
|
|
104
|
+
+ look_up_table[order][GAP_OFFSET_COLUMN]
|
|
105
|
+
+ gap_offset
|
|
106
|
+
)
|
|
107
|
+
LOGGER.debug(
|
|
108
|
+
f"Calculated gap is {gap}mm for energy {photon_energy_kev}keV at order {order}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return gap
|
dodal/devices/i10/__init__.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .diagnostics import (
|
|
2
|
+
I10Diagnostic,
|
|
3
|
+
I10Diagnostic5ADet,
|
|
4
|
+
I10JDiagnostic,
|
|
5
|
+
I10PneumaticStage,
|
|
6
|
+
I10SharedDiagnostic,
|
|
7
|
+
)
|
|
8
|
+
from .mirrors import PiezoMirror
|
|
9
|
+
from .slits import (
|
|
10
|
+
I10JSlits,
|
|
11
|
+
I10SharedSlits,
|
|
12
|
+
I10SharedSlitsDrainCurrent,
|
|
13
|
+
I10Slits,
|
|
14
|
+
I10SlitsDrainCurrent,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"I10Diagnostic",
|
|
19
|
+
"I10Diagnostic5ADet",
|
|
20
|
+
"I10PneumaticStage",
|
|
21
|
+
"PiezoMirror",
|
|
22
|
+
"I10Slits",
|
|
23
|
+
"I10SlitsDrainCurrent",
|
|
24
|
+
"I10SharedDiagnostic",
|
|
25
|
+
"I10SharedSlits",
|
|
26
|
+
"I10JSlits",
|
|
27
|
+
"I10SharedSlitsDrainCurrent",
|
|
28
|
+
"I10JDiagnostic",
|
|
29
|
+
]
|
dodal/devices/i10/diagnostics.py
CHANGED
|
@@ -28,7 +28,7 @@ class D3Position(StrictEnum):
|
|
|
28
28
|
GRID = "Grid"
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class
|
|
31
|
+
class CellPosition(StrictEnum):
|
|
32
32
|
CELL_IN = "Cell In"
|
|
33
33
|
CELL_OUT = "Cell Out"
|
|
34
34
|
|
|
@@ -68,6 +68,21 @@ class InStateReadBackTable(StrictEnum):
|
|
|
68
68
|
OUT_OF_BEAM = "Out of Beam"
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class D2jPosition(StrictEnum):
|
|
72
|
+
OUT_OF_THE_BEAM = "Out of the beam"
|
|
73
|
+
DIODE = "Diode"
|
|
74
|
+
BLADE = "Blade"
|
|
75
|
+
LA = "La ref"
|
|
76
|
+
GD = "Gd ref"
|
|
77
|
+
YB = "Yb ref"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class D3jPosition(StrictEnum):
|
|
81
|
+
OUT_OF_THE_BEAM = "Out of the beam"
|
|
82
|
+
DIODE_IN = "Diode In"
|
|
83
|
+
DIAMOND_WINDOW = "Diamond window"
|
|
84
|
+
|
|
85
|
+
|
|
71
86
|
class I10PneumaticStage(StandardReadable):
|
|
72
87
|
"""Pneumatic stage only has two real positions in or out.
|
|
73
88
|
Use for fluorescent screen which can be insert into the x-ray beam.
|
|
@@ -138,9 +153,7 @@ class FullDiagnostic(Device):
|
|
|
138
153
|
super().__init__(name)
|
|
139
154
|
|
|
140
155
|
|
|
141
|
-
class
|
|
142
|
-
"""Collection of all the diagnostic stage on i10."""
|
|
143
|
-
|
|
156
|
+
class I10SharedDiagnostic(Device):
|
|
144
157
|
def __init__(self, prefix, name: str = "") -> None:
|
|
145
158
|
self.d1 = ScreenCam(prefix=prefix + "PHDGN-01:")
|
|
146
159
|
self.d2 = ScreenCam(prefix=prefix + "PHDGN-02:")
|
|
@@ -149,8 +162,15 @@ class I10Diagnostic(Device):
|
|
|
149
162
|
positioner_enum=D3Position,
|
|
150
163
|
positioner_suffix="DET:X",
|
|
151
164
|
)
|
|
165
|
+
super().__init__(name)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class I10Diagnostic(Device):
|
|
169
|
+
"""Collection of all the diagnostic stage on i10."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, prefix, name: str = "") -> None:
|
|
152
172
|
self.d4 = ScreenCam(prefix=prefix + "PHDGN-04:")
|
|
153
|
-
self.d5 = create_positioner(
|
|
173
|
+
self.d5 = create_positioner(CellPosition, f"{prefix}IONC-01:Y")
|
|
154
174
|
self.d5A = create_positioner(D5APosition, f"{prefix}PHDGN-06:DET:X")
|
|
155
175
|
self.d6 = FullDiagnostic(f"{prefix}PHDGN-05:", D6Position, "DET:X")
|
|
156
176
|
self.d7 = create_positioner(D7Position, f"{prefix}PHDGN-07:Y")
|
|
@@ -158,6 +178,18 @@ class I10Diagnostic(Device):
|
|
|
158
178
|
super().__init__(name)
|
|
159
179
|
|
|
160
180
|
|
|
181
|
+
class I10JDiagnostic(Device):
|
|
182
|
+
"""Collection of all the diagnostic stage on i10-1."""
|
|
183
|
+
|
|
184
|
+
def __init__(self, prefix, name: str = "") -> None:
|
|
185
|
+
self.dj1 = ScreenCam(prefix=prefix + "PHDGN-01:")
|
|
186
|
+
self.dj2 = create_positioner(CellPosition, f"{prefix}IONC-01:Y")
|
|
187
|
+
self.dj2A = create_positioner(D2jPosition, f"{prefix}PHDGN-03:DET:X")
|
|
188
|
+
self.dj3 = FullDiagnostic(f"{prefix}PHDGN-02:", D3jPosition, "DET:X")
|
|
189
|
+
|
|
190
|
+
super().__init__(name)
|
|
191
|
+
|
|
192
|
+
|
|
161
193
|
class I10Diagnostic5ADet(Device):
|
|
162
194
|
"""Diagnostic 5a detection with drain current and photo diode"""
|
|
163
195
|
|