mx-bluesky 1.5.2__py3-none-any.whl → 1.5.4__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.
- mx_bluesky/_version.py +16 -3
- mx_bluesky/beamlines/aithre_lasershaping/__init__.py +2 -0
- mx_bluesky/beamlines/aithre_lasershaping/beamline_safe.py +17 -0
- mx_bluesky/beamlines/i04/__init__.py +7 -3
- mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -8
- mx_bluesky/beamlines/i04/thawing_plan.py +3 -3
- mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +10 -10
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DetStage.edl +1 -1
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +68 -68
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/ME14E-GeneralPurpose.edl +120 -120
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +135 -135
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +2 -2
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/Shutter_Control.edl +3 -3
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/nudgechip.edl +24 -24
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +12 -12
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +13 -12
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +10 -10
- mx_bluesky/beamlines/i24/serial/parameters/utils.py +1 -1
- mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
- mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +142 -135
- mx_bluesky/common/device_setup_plans/manipulate_sample.py +2 -2
- mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py +2 -2
- mx_bluesky/common/experiment_plans/inner_plans/udc_default_state.py +65 -0
- mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
- mx_bluesky/common/experiment_plans/oav_snapshot_plan.py +2 -2
- mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
- mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
- mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
- mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py +35 -17
- mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +31 -6
- mx_bluesky/common/external_interaction/config_server.py +151 -54
- mx_bluesky/common/parameters/constants.py +27 -8
- mx_bluesky/common/parameters/gridscan.py +1 -1
- mx_bluesky/hyperion/__main__.py +50 -178
- mx_bluesky/hyperion/baton_handler.py +130 -69
- mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
- mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -1
- mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +17 -5
- mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
- mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +6 -2
- mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +2 -3
- mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
- mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
- mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
- mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +9 -0
- mx_bluesky/hyperion/external_interaction/callbacks/alert_on_container_change.py +54 -0
- mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
- mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
- mx_bluesky/hyperion/parameters/cli.py +15 -3
- mx_bluesky/hyperion/parameters/components.py +7 -5
- mx_bluesky/hyperion/parameters/constants.py +20 -4
- mx_bluesky/hyperion/parameters/gridscan.py +22 -14
- mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
- mx_bluesky/hyperion/parameters/robot_load.py +1 -4
- mx_bluesky/hyperion/parameters/rotation.py +1 -2
- mx_bluesky/hyperion/plan_runner.py +78 -0
- mx_bluesky/hyperion/runner.py +189 -0
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/METADATA +4 -3
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/RECORD +64 -55
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/entry_points.txt +0 -2
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/WHEEL +0 -0
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/licenses/LICENSE +0 -0
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/top_level.txt +0 -0
|
@@ -24,7 +24,10 @@ from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import (
|
|
|
24
24
|
RotationScanComposite,
|
|
25
25
|
rotation_scan_internal,
|
|
26
26
|
)
|
|
27
|
-
from mx_bluesky.hyperion.
|
|
27
|
+
from mx_bluesky.hyperion.external_interaction.config_server import (
|
|
28
|
+
get_hyperion_config_client,
|
|
29
|
+
)
|
|
30
|
+
from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
|
|
28
31
|
from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
|
|
29
32
|
from mx_bluesky.hyperion.parameters.rotation import RotationScanPerSweep
|
|
30
33
|
|
|
@@ -51,7 +54,8 @@ def load_centre_collect_full(
|
|
|
51
54
|
* If X-ray centring finds a diffracting centre then move to that centre and
|
|
52
55
|
* do a collection with the specified parameters.
|
|
53
56
|
"""
|
|
54
|
-
|
|
57
|
+
|
|
58
|
+
get_hyperion_config_client().refresh_cache()
|
|
55
59
|
|
|
56
60
|
if not oav_params:
|
|
57
61
|
oav_params = OAVParameters(context="xrayCentring")
|
|
@@ -60,8 +64,16 @@ def load_centre_collect_full(
|
|
|
60
64
|
@set_run_key_decorator(CONST.PLAN.LOAD_CENTRE_COLLECT)
|
|
61
65
|
@run_decorator(
|
|
62
66
|
md={
|
|
63
|
-
"metadata": {
|
|
64
|
-
|
|
67
|
+
"metadata": {
|
|
68
|
+
"sample_id": parameters.sample_id,
|
|
69
|
+
"visit": parameters.visit,
|
|
70
|
+
"container": parameters.sample_puck,
|
|
71
|
+
},
|
|
72
|
+
"activate_callbacks": [
|
|
73
|
+
"BeamDrawingCallback",
|
|
74
|
+
"SampleHandlingCallback",
|
|
75
|
+
"AlertOnContainerChange",
|
|
76
|
+
],
|
|
65
77
|
"with_snapshot": parameters.multi_rotation_scan.model_dump_json(
|
|
66
78
|
include=WithSnapshot.model_fields.keys() # type: ignore
|
|
67
79
|
),
|
|
@@ -117,7 +129,7 @@ def load_centre_collect_full(
|
|
|
117
129
|
|
|
118
130
|
multi_rotation.rotation_scans.clear()
|
|
119
131
|
|
|
120
|
-
is_alternating =
|
|
132
|
+
is_alternating = I03Constants.ALTERNATE_ROTATION_DIRECTION
|
|
121
133
|
|
|
122
134
|
generator = rotation_scan_generator(is_alternating)
|
|
123
135
|
next(generator)
|
|
@@ -155,7 +155,7 @@ def pin_tip_centre_plan(
|
|
|
155
155
|
tip = yield from move_pin_into_view(pin_tip_detect, smargon)
|
|
156
156
|
yield from offset_and_move(tip)
|
|
157
157
|
|
|
158
|
-
yield from bps.mvr(smargon.omega, 90)
|
|
158
|
+
yield from bps.mvr(smargon.omega, -90)
|
|
159
159
|
|
|
160
160
|
# need to wait for the OAV image to update
|
|
161
161
|
# See #673 for improvements
|
|
@@ -12,7 +12,7 @@ from blueapi.core import BlueskyContext
|
|
|
12
12
|
from bluesky.utils import Msg
|
|
13
13
|
from dodal.devices.aperturescatterguard import ApertureScatterguard
|
|
14
14
|
from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
|
|
15
|
-
from dodal.devices.backlight import Backlight,
|
|
15
|
+
from dodal.devices.backlight import Backlight, InOut
|
|
16
16
|
from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
|
|
17
17
|
from dodal.devices.i03.dcm import DCM
|
|
18
18
|
from dodal.devices.i03.undulator_dcm import UndulatorDCM
|
|
@@ -123,7 +123,11 @@ def robot_load_and_snapshots(
|
|
|
123
123
|
thawing_time: float,
|
|
124
124
|
demand_energy_ev: float | None,
|
|
125
125
|
):
|
|
126
|
-
yield from bps.abs_set(composite.backlight,
|
|
126
|
+
yield from bps.abs_set(composite.backlight, InOut.IN, group="snapshot")
|
|
127
|
+
|
|
128
|
+
yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_PRE_LOAD)
|
|
129
|
+
yield from bps.read(composite.robot)
|
|
130
|
+
yield from bps.save()
|
|
127
131
|
|
|
128
132
|
robot_load_plan = do_robot_load(
|
|
129
133
|
composite,
|
|
@@ -58,7 +58,7 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
|
|
|
58
58
|
setup_zebra_for_rotation,
|
|
59
59
|
tidy_up_zebra_after_rotation_scan,
|
|
60
60
|
)
|
|
61
|
-
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
61
|
+
from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
|
|
62
62
|
from mx_bluesky.hyperion.parameters.rotation import (
|
|
63
63
|
RotationScan,
|
|
64
64
|
SingleRotationScan,
|
|
@@ -133,7 +133,7 @@ def calculate_motion_profile(
|
|
|
133
133
|
direction = params.rotation_direction
|
|
134
134
|
start_scan_deg = params.omega_start_deg
|
|
135
135
|
|
|
136
|
-
if
|
|
136
|
+
if I03Constants.OMEGA_FLIP:
|
|
137
137
|
# If omega_flip is True then the motor omega axis is inverted with respect to the
|
|
138
138
|
# hyperion coordinate system.
|
|
139
139
|
start_scan_deg = -start_scan_deg
|
|
@@ -386,7 +386,6 @@ def rotation_scan_internal(
|
|
|
386
386
|
parameters: RotationScan,
|
|
387
387
|
oav_params: OAVParameters | None = None,
|
|
388
388
|
) -> MsgGenerator:
|
|
389
|
-
parameters.features.update_self_from_server()
|
|
390
389
|
if not oav_params:
|
|
391
390
|
oav_params = OAVParameters(context="xrayCentring")
|
|
392
391
|
eiger: EigerDetector = composite.eiger
|
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
import re
|
|
4
4
|
import traceback
|
|
5
5
|
from collections.abc import Sequence
|
|
6
|
+
from enum import StrEnum
|
|
6
7
|
from os import path
|
|
7
8
|
from typing import Any, TypeVar
|
|
8
9
|
|
|
@@ -14,6 +15,7 @@ from pydantic_extra_types.semantic_version import SemanticVersion
|
|
|
14
15
|
|
|
15
16
|
from mx_bluesky.common.parameters.components import (
|
|
16
17
|
PARAMETER_VERSION,
|
|
18
|
+
MxBlueskyParameters,
|
|
17
19
|
WithVisit,
|
|
18
20
|
)
|
|
19
21
|
from mx_bluesky.common.parameters.constants import (
|
|
@@ -21,6 +23,7 @@ from mx_bluesky.common.parameters.constants import (
|
|
|
21
23
|
)
|
|
22
24
|
from mx_bluesky.common.utils.log import LOGGER
|
|
23
25
|
from mx_bluesky.common.utils.utils import convert_angstrom_to_eV
|
|
26
|
+
from mx_bluesky.hyperion.parameters.components import Wait
|
|
24
27
|
from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
|
|
25
28
|
|
|
26
29
|
T = TypeVar("T", bound=WithVisit)
|
|
@@ -31,8 +34,13 @@ MULTIPIN_REGEX = rf"^{MULTIPIN_PREFIX}_(\d+)x(\d+(?:\.\d+)?)\+(\d+(?:\.\d+)?)$"
|
|
|
31
34
|
MX_GENERAL_ROOT_REGEX = r"^/dls/(?P<beamline>[^/]+)/data/[^/]*/(?P<visit>[^/]+)(?:/|$)"
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
class _InstructionType(StrEnum):
|
|
38
|
+
WAIT = "wait"
|
|
39
|
+
COLLECT = "collect"
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
@dataclasses.dataclass
|
|
35
|
-
class
|
|
43
|
+
class _PinType:
|
|
36
44
|
expected_number_of_crystals: int
|
|
37
45
|
single_well_width_um: float
|
|
38
46
|
tip_to_first_well_um: float = 0
|
|
@@ -54,7 +62,7 @@ class PinType:
|
|
|
54
62
|
)
|
|
55
63
|
|
|
56
64
|
|
|
57
|
-
class
|
|
65
|
+
class _SinglePin(_PinType):
|
|
58
66
|
def __init__(self):
|
|
59
67
|
super().__init__(1, GridscanParamConstants.WIDTH_UM)
|
|
60
68
|
|
|
@@ -63,23 +71,115 @@ class SinglePin(PinType):
|
|
|
63
71
|
return self.single_well_width_um
|
|
64
72
|
|
|
65
73
|
|
|
74
|
+
def create_parameters_from_agamemnon() -> Sequence[MxBlueskyParameters]:
|
|
75
|
+
"""Fetch the next instruction from agamemnon and convert it into one or more
|
|
76
|
+
mx-bluesky instructions.
|
|
77
|
+
Returns:
|
|
78
|
+
The generated sequence of mx-bluesky parameters, or empty list if
|
|
79
|
+
no instructions."""
|
|
80
|
+
beamline_name = get_beamline_name("i03")
|
|
81
|
+
agamemnon_instruction = _get_next_instruction(beamline_name)
|
|
82
|
+
if agamemnon_instruction:
|
|
83
|
+
match _instruction_and_data(agamemnon_instruction):
|
|
84
|
+
case (_InstructionType.COLLECT, data):
|
|
85
|
+
return _populate_parameters_from_agamemnon(data)
|
|
86
|
+
case (_InstructionType.WAIT, data):
|
|
87
|
+
return [
|
|
88
|
+
Wait.model_validate(
|
|
89
|
+
{
|
|
90
|
+
"duration_s": data,
|
|
91
|
+
"parameter_model_version": _get_param_version(),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def compare_params(load_centre_collect_params: LoadCentreCollect):
|
|
100
|
+
"""Compare the supplied parameters (as supplied from GDA) with those directly
|
|
101
|
+
created from agamemnon. Any differences are logged.
|
|
102
|
+
Args:
|
|
103
|
+
load_centre_collect_params: The parameters from GDA to compare."""
|
|
104
|
+
try:
|
|
105
|
+
lcc_requests = create_parameters_from_agamemnon()
|
|
106
|
+
# Log differences against GDA populated parameters
|
|
107
|
+
if not lcc_requests:
|
|
108
|
+
LOGGER.info("Agamemnon returned no instructions")
|
|
109
|
+
else:
|
|
110
|
+
differences = DeepDiff(
|
|
111
|
+
lcc_requests[0], load_centre_collect_params, math_epsilon=1e-5
|
|
112
|
+
)
|
|
113
|
+
if differences:
|
|
114
|
+
LOGGER.info(
|
|
115
|
+
f"Different parameters found when directly reading from Hyperion: {differences}"
|
|
116
|
+
)
|
|
117
|
+
except (ValueError, KeyError):
|
|
118
|
+
LOGGER.warning(f"Failed to compare parameters: {traceback.format_exc()}")
|
|
119
|
+
except Exception:
|
|
120
|
+
LOGGER.warning(
|
|
121
|
+
f"Unexpected error occurred. Failed to compare parameters: {traceback.format_exc()}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def update_params_from_agamemnon(parameters: T) -> T:
|
|
126
|
+
"""Update the supplied parameters with additional information from agamemnon.
|
|
127
|
+
This is currently necessary for multipin processing and called when Hyperion is invoked
|
|
128
|
+
from GDA.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
parameters: The LoadCentreCollectParameters that will be updated with additional info,
|
|
132
|
+
such as multipin dimensions, number of crystals.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
beamline_name = get_beamline_name("i03")
|
|
136
|
+
agamemnon_params = _get_next_instruction(beamline_name)
|
|
137
|
+
instruction, collect_params = _instruction_and_data(agamemnon_params)
|
|
138
|
+
assert instruction == _InstructionType.COLLECT, (
|
|
139
|
+
"Unable to augment GDA parameters from agamemnon, agamemnon reports 'wait'"
|
|
140
|
+
)
|
|
141
|
+
pin_type = _get_pin_type_from_agamemnon_collect_parameters(collect_params)
|
|
142
|
+
if isinstance(parameters, LoadCentreCollect):
|
|
143
|
+
parameters.robot_load_then_centre.tip_offset_um = pin_type.full_width / 2
|
|
144
|
+
parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
|
|
145
|
+
parameters.select_centres.n = pin_type.expected_number_of_crystals
|
|
146
|
+
if pin_type != _SinglePin():
|
|
147
|
+
# Rotation snapshots will be generated from the gridscan snapshots,
|
|
148
|
+
# no need to specify snapshot omega.
|
|
149
|
+
parameters.multi_rotation_scan.snapshot_omegas_deg = []
|
|
150
|
+
parameters.multi_rotation_scan.use_grid_snapshots = True
|
|
151
|
+
except (ValueError, ValidationError) as e:
|
|
152
|
+
LOGGER.warning(f"Failed to update parameters: {e}")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
LOGGER.warning(f"Unexpected error occurred. Failed to update parameters: {e}")
|
|
155
|
+
|
|
156
|
+
return parameters
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _instruction_and_data(agamemnon_instruction: dict) -> tuple[str, Any]:
|
|
160
|
+
instruction, data = next(iter(agamemnon_instruction.items()))
|
|
161
|
+
if instruction not in _InstructionType.__members__.values():
|
|
162
|
+
raise KeyError(
|
|
163
|
+
f"Unexpected instruction from agamemnon: {agamemnon_instruction}"
|
|
164
|
+
)
|
|
165
|
+
return instruction, data
|
|
166
|
+
|
|
167
|
+
|
|
66
168
|
def _get_parameters_from_url(url: str) -> dict:
|
|
67
169
|
response = requests.get(url, headers={"Accept": "application/json"})
|
|
68
170
|
response.raise_for_status()
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
return response_json["collect"]
|
|
72
|
-
except KeyError as e:
|
|
73
|
-
raise KeyError(f"Unexpected json from agamemnon: {response_json}") from e
|
|
171
|
+
return json.loads(response.content)
|
|
74
172
|
|
|
75
173
|
|
|
76
|
-
def
|
|
77
|
-
|
|
174
|
+
def _get_pin_type_from_agamemnon_collect_parameters(
|
|
175
|
+
collect_parameters: dict,
|
|
176
|
+
) -> _PinType:
|
|
177
|
+
loop_type_name: str | None = collect_parameters["sample"]["loopType"]
|
|
78
178
|
if loop_type_name:
|
|
79
179
|
regex_search = re.search(MULTIPIN_REGEX, loop_type_name)
|
|
80
180
|
if regex_search:
|
|
81
181
|
wells, well_size, tip_to_first_well = regex_search.groups()
|
|
82
|
-
return
|
|
182
|
+
return _PinType(int(wells), float(well_size), float(tip_to_first_well))
|
|
83
183
|
else:
|
|
84
184
|
loop_type_message = (
|
|
85
185
|
f"Agamemnon loop type of {loop_type_name} not recognised"
|
|
@@ -87,14 +187,14 @@ def get_pin_type_from_agamemnon_parameters(parameters: dict) -> PinType:
|
|
|
87
187
|
if loop_type_name.startswith(MULTIPIN_PREFIX):
|
|
88
188
|
raise ValueError(f"{loop_type_message}. {MULTIPIN_FORMAT_DESC}")
|
|
89
189
|
LOGGER.warning(f"{loop_type_message}, assuming single pin")
|
|
90
|
-
return
|
|
190
|
+
return _SinglePin()
|
|
91
191
|
|
|
92
192
|
|
|
93
|
-
def
|
|
193
|
+
def _get_next_instruction(beamline: str) -> dict:
|
|
94
194
|
return _get_parameters_from_url(AGAMEMNON_URL + f"getnextcollect/{beamline}")
|
|
95
195
|
|
|
96
196
|
|
|
97
|
-
def
|
|
197
|
+
def _get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
|
|
98
198
|
try:
|
|
99
199
|
prefix = parameters["prefix"]
|
|
100
200
|
collection = parameters["collection"]
|
|
@@ -113,7 +213,7 @@ def get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
|
|
|
113
213
|
)
|
|
114
214
|
|
|
115
215
|
|
|
116
|
-
def
|
|
216
|
+
def _get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
|
|
117
217
|
try:
|
|
118
218
|
first_collection: dict = parameters["collection"][0]
|
|
119
219
|
wavelength = first_collection.get("wavelength")
|
|
@@ -124,21 +224,29 @@ def get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]
|
|
|
124
224
|
return {"demand_energy_ev": None}
|
|
125
225
|
|
|
126
226
|
|
|
127
|
-
def
|
|
227
|
+
def _get_param_version() -> SemanticVersion:
|
|
128
228
|
return SemanticVersion.validate_from_str(str(PARAMETER_VERSION))
|
|
129
229
|
|
|
130
230
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
231
|
+
def _populate_parameters_from_agamemnon(
|
|
232
|
+
agamemnon_params,
|
|
233
|
+
) -> Sequence[LoadCentreCollect]:
|
|
234
|
+
if not agamemnon_params:
|
|
235
|
+
# Empty dict means no instructions
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
visit, detector_distance = _get_withvisit_parameters_from_agamemnon(
|
|
239
|
+
agamemnon_params
|
|
240
|
+
)
|
|
241
|
+
with_energy_params = _get_withenergy_parameters_from_agamemnon(agamemnon_params)
|
|
242
|
+
pin_type = _get_pin_type_from_agamemnon_collect_parameters(agamemnon_params)
|
|
135
243
|
collections = agamemnon_params["collection"]
|
|
136
244
|
visit_directory, file_name = path.split(agamemnon_params["prefix"])
|
|
137
245
|
|
|
138
246
|
return [
|
|
139
247
|
LoadCentreCollect.model_validate(
|
|
140
248
|
{
|
|
141
|
-
"parameter_model_version":
|
|
249
|
+
"parameter_model_version": _get_param_version(),
|
|
142
250
|
"visit": visit,
|
|
143
251
|
"detector_distance_mm": detector_distance,
|
|
144
252
|
"sample_id": agamemnon_params["sample"]["id"],
|
|
@@ -148,7 +256,6 @@ def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreC
|
|
|
148
256
|
"name": "TopNByMaxCount",
|
|
149
257
|
"n": pin_type.expected_number_of_crystals,
|
|
150
258
|
},
|
|
151
|
-
"features": {"use_gpu_results": True},
|
|
152
259
|
"robot_load_then_centre": {
|
|
153
260
|
"storage_directory": str(visit_directory) + "/xraycentring",
|
|
154
261
|
"file_name": file_name,
|
|
@@ -186,55 +293,3 @@ def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreC
|
|
|
186
293
|
)
|
|
187
294
|
for collection in collections
|
|
188
295
|
]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def create_parameters_from_agamemnon() -> Sequence[LoadCentreCollect]:
|
|
192
|
-
beamline_name = get_beamline_name("i03")
|
|
193
|
-
agamemnon_params = get_next_instruction(beamline_name)
|
|
194
|
-
return (
|
|
195
|
-
populate_parameters_from_agamemnon(agamemnon_params) if agamemnon_params else []
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def compare_params(load_centre_collect_params: LoadCentreCollect):
|
|
200
|
-
try:
|
|
201
|
-
lcc_requests = create_parameters_from_agamemnon()
|
|
202
|
-
# Log differences against GDA populated parameters
|
|
203
|
-
if not lcc_requests:
|
|
204
|
-
LOGGER.info("Agamemnon returned no instructions")
|
|
205
|
-
else:
|
|
206
|
-
differences = DeepDiff(
|
|
207
|
-
lcc_requests[0], load_centre_collect_params, math_epsilon=1e-5
|
|
208
|
-
)
|
|
209
|
-
if differences:
|
|
210
|
-
LOGGER.info(
|
|
211
|
-
f"Different parameters found when directly reading from Hyperion: {differences}"
|
|
212
|
-
)
|
|
213
|
-
except (ValueError, KeyError):
|
|
214
|
-
LOGGER.warning(f"Failed to compare parameters: {traceback.format_exc()}")
|
|
215
|
-
except Exception:
|
|
216
|
-
LOGGER.warning(
|
|
217
|
-
f"Unexpected error occurred. Failed to compare parameters: {traceback.format_exc()}"
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def update_params_from_agamemnon(parameters: T) -> T:
|
|
222
|
-
try:
|
|
223
|
-
beamline_name = get_beamline_name("i03")
|
|
224
|
-
agamemnon_params = get_next_instruction(beamline_name)
|
|
225
|
-
pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
|
|
226
|
-
if isinstance(parameters, LoadCentreCollect):
|
|
227
|
-
parameters.robot_load_then_centre.tip_offset_um = pin_type.full_width / 2
|
|
228
|
-
parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
|
|
229
|
-
parameters.select_centres.n = pin_type.expected_number_of_crystals
|
|
230
|
-
if pin_type != SinglePin():
|
|
231
|
-
# Rotation snapshots will be generated from the gridscan snapshots,
|
|
232
|
-
# no need to specify snapshot omega.
|
|
233
|
-
parameters.multi_rotation_scan.snapshot_omegas_deg = []
|
|
234
|
-
parameters.multi_rotation_scan.use_grid_snapshots = True
|
|
235
|
-
except (ValueError, ValidationError) as e:
|
|
236
|
-
LOGGER.warning(f"Failed to update parameters: {e}")
|
|
237
|
-
except Exception as e:
|
|
238
|
-
LOGGER.warning(f"Unexpected error occurred. Failed to update parameters: {e}")
|
|
239
|
-
|
|
240
|
-
return parameters
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Subjects(StrEnum):
|
|
5
|
+
UDC_STARTED = "UDC Started"
|
|
6
|
+
UDC_BATON_PASSED = "UDC Baton was passed"
|
|
7
|
+
UDC_RESUMED_OPERATION = "UDC Resumed operation"
|
|
8
|
+
UDC_SUSPENDED_OPERATION = "UDC Suspended operation"
|
|
9
|
+
NEW_CONTAINER = "Hyperion is collecting from a new container"
|
|
10
|
+
NEW_VISIT = "Hyperion has changed visit"
|
|
11
|
+
SAMPLE_ERROR = "Hyperion has encountered a sample error"
|
|
12
|
+
BEAMLINE_ERROR = "Hyperion has encountered a beamline error"
|
|
@@ -8,6 +8,10 @@ from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
|
|
|
8
8
|
from dodal.log import LOGGER as dodal_logger
|
|
9
9
|
from dodal.log import set_up_all_logging_handlers
|
|
10
10
|
|
|
11
|
+
from mx_bluesky.common.external_interaction.alerting import set_alerting_service
|
|
12
|
+
from mx_bluesky.common.external_interaction.alerting.log_based_service import (
|
|
13
|
+
LoggingAlertService,
|
|
14
|
+
)
|
|
11
15
|
from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import (
|
|
12
16
|
LogUidTaggingCallback,
|
|
13
17
|
)
|
|
@@ -29,6 +33,9 @@ from mx_bluesky.common.utils.log import (
|
|
|
29
33
|
_get_logging_dirs,
|
|
30
34
|
tag_filter,
|
|
31
35
|
)
|
|
36
|
+
from mx_bluesky.hyperion.external_interaction.callbacks.alert_on_container_change import (
|
|
37
|
+
AlertOnContainerChange,
|
|
38
|
+
)
|
|
32
39
|
from mx_bluesky.hyperion.external_interaction.callbacks.robot_actions.ispyb_callback import (
|
|
33
40
|
RobotLoadISPyBCallback,
|
|
34
41
|
)
|
|
@@ -85,6 +92,7 @@ def setup_callbacks() -> list[CallbackBase]:
|
|
|
85
92
|
LogUidTaggingCallback(),
|
|
86
93
|
RobotLoadISPyBCallback(),
|
|
87
94
|
SampleHandlingCallback(),
|
|
95
|
+
AlertOnContainerChange(),
|
|
88
96
|
]
|
|
89
97
|
|
|
90
98
|
|
|
@@ -156,6 +164,7 @@ class HyperionCallbackRunner:
|
|
|
156
164
|
def __init__(self, dev_mode) -> None:
|
|
157
165
|
setup_logging(dev_mode)
|
|
158
166
|
log_info("Hyperion callback process started.")
|
|
167
|
+
set_alerting_service(LoggingAlertService(CONST.GRAYLOG_STREAM_ID))
|
|
159
168
|
|
|
160
169
|
self.callbacks = setup_callbacks()
|
|
161
170
|
self.proxy, self.dispatcher, start_proxy, start_dispatcher = setup_threads()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from dodal.utils import get_beamline_name
|
|
2
|
+
from event_model import Event, EventDescriptor, RunStart
|
|
3
|
+
|
|
4
|
+
from mx_bluesky.common.external_interaction.alerting import (
|
|
5
|
+
Metadata,
|
|
6
|
+
get_alerting_service,
|
|
7
|
+
)
|
|
8
|
+
from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import (
|
|
9
|
+
PlanReactiveCallback,
|
|
10
|
+
)
|
|
11
|
+
from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER
|
|
12
|
+
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AlertOnContainerChange(PlanReactiveCallback):
|
|
16
|
+
"""Sends an alert to beamline staff when a pin from a new puck has been loaded.
|
|
17
|
+
This tends to be used as a heartbeat so we know that UDC is running."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER)
|
|
21
|
+
self._new_container = None
|
|
22
|
+
self._visit = None
|
|
23
|
+
self._sample_id = None
|
|
24
|
+
self.descriptors: dict[str, EventDescriptor] = {}
|
|
25
|
+
|
|
26
|
+
def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
|
|
27
|
+
self.descriptors[doc["uid"]] = doc
|
|
28
|
+
return super().activity_gated_descriptor(doc)
|
|
29
|
+
|
|
30
|
+
def activity_gated_event(self, doc: Event) -> Event | None:
|
|
31
|
+
event_descriptor = self.descriptors.get(doc["descriptor"])
|
|
32
|
+
if (
|
|
33
|
+
event_descriptor
|
|
34
|
+
and event_descriptor.get("name") == CONST.DESCRIPTORS.ROBOT_PRE_LOAD
|
|
35
|
+
):
|
|
36
|
+
current_container = int(doc["data"]["robot-current_puck"])
|
|
37
|
+
if self._new_container != current_container:
|
|
38
|
+
beamline = get_beamline_name("")
|
|
39
|
+
get_alerting_service().raise_alert(
|
|
40
|
+
f"UDC moved on to puck {self._new_container} on {beamline}",
|
|
41
|
+
f"Hyperion finished container {current_container} and moved on to {self._new_container}",
|
|
42
|
+
{
|
|
43
|
+
Metadata.SAMPLE_ID: str(self._sample_id),
|
|
44
|
+
Metadata.VISIT: self._visit or "",
|
|
45
|
+
Metadata.CONTAINER: str(self._new_container),
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
return doc
|
|
49
|
+
|
|
50
|
+
def activity_gated_start(self, doc: RunStart):
|
|
51
|
+
metadata = doc.get("metadata", {})
|
|
52
|
+
self._new_container = metadata.get("container")
|
|
53
|
+
self._sample_id = metadata.get("sample_id")
|
|
54
|
+
self._visit = metadata.get("visit")
|
|
@@ -15,7 +15,7 @@ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
|
|
|
15
15
|
)
|
|
16
16
|
from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter
|
|
17
17
|
from mx_bluesky.common.utils.log import NEXUS_LOGGER
|
|
18
|
-
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
18
|
+
from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
|
|
19
19
|
from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
@@ -103,6 +103,6 @@ class RotationNexusFileCallback(PlanReactiveCallback):
|
|
|
103
103
|
full_num_of_images=self.full_num_of_images,
|
|
104
104
|
meta_data_run_number=self.meta_data_run_number,
|
|
105
105
|
axis_direction=AxisDirection.NEGATIVE
|
|
106
|
-
if
|
|
106
|
+
if I03Constants.OMEGA_FLIP
|
|
107
107
|
else AxisDirection.POSITIVE,
|
|
108
108
|
)
|
|
@@ -1,35 +1,16 @@
|
|
|
1
1
|
from functools import cache
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from mx_bluesky.common.external_interaction.config_server import MXConfigClient
|
|
4
|
+
from mx_bluesky.hyperion.parameters.constants import (
|
|
5
|
+
HyperionFeatureSetting,
|
|
6
|
+
HyperionFeatureSettingources,
|
|
7
|
+
)
|
|
4
8
|
|
|
5
|
-
from mx_bluesky.common.external_interaction.config_server import FeatureFlags
|
|
6
|
-
from mx_bluesky.common.utils.log import LOGGER
|
|
7
|
-
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
use_gpu_results: If True then GPU result processing is enabled
|
|
17
|
-
and the GPU result is taken.
|
|
18
|
-
set_stub_offsets: If True then set the stub offsets after moving to the crystal (ignored for
|
|
19
|
-
multi-centre)
|
|
20
|
-
omega_flip: If True then invert the smargon omega motor rotation commands with respect to
|
|
21
|
-
the hyperion request. See "Hyperion Coordinate Systems" in the documentation.
|
|
22
|
-
alternate_rotation_direction: If True then the for multi-sample pins the rotation direction of
|
|
23
|
-
successive rotation scans is alternated between positive and negative.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
@staticmethod
|
|
27
|
-
@cache
|
|
28
|
-
def get_config_server() -> ConfigServer:
|
|
29
|
-
return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
|
|
30
|
-
|
|
31
|
-
use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
|
|
32
|
-
use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
|
|
33
|
-
set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
|
|
34
|
-
omega_flip: bool = CONST.I03.OMEGA_FLIP
|
|
35
|
-
alternate_rotation_direction: bool = CONST.I03.ALTERNATE_ROTATION_DIRECTION
|
|
10
|
+
@cache
|
|
11
|
+
def get_hyperion_config_client() -> MXConfigClient[HyperionFeatureSetting]:
|
|
12
|
+
return MXConfigClient(
|
|
13
|
+
feature_sources=HyperionFeatureSettingources,
|
|
14
|
+
feature_dc=HyperionFeatureSetting,
|
|
15
|
+
url="https://daq-config.diamond.ac.uk",
|
|
16
|
+
)
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
from enum import StrEnum
|
|
2
3
|
|
|
3
4
|
from pydantic.dataclasses import dataclass
|
|
4
5
|
|
|
5
6
|
from mx_bluesky._version import version
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
class HyperionMode(StrEnum):
|
|
10
|
+
GDA = "gda"
|
|
11
|
+
UDC = "udc"
|
|
12
|
+
|
|
13
|
+
|
|
8
14
|
@dataclass
|
|
9
15
|
class HyperionArgs:
|
|
16
|
+
mode: HyperionMode
|
|
10
17
|
dev_mode: bool = False
|
|
11
18
|
|
|
12
19
|
|
|
@@ -39,7 +46,12 @@ def parse_cli_args() -> HyperionArgs:
|
|
|
39
46
|
action="version",
|
|
40
47
|
version=version,
|
|
41
48
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--mode",
|
|
51
|
+
help="Launch in the specified mode (default is 'gda')",
|
|
52
|
+
default=HyperionMode.GDA,
|
|
53
|
+
type=HyperionMode,
|
|
54
|
+
choices=HyperionMode.__members__.values(),
|
|
45
55
|
)
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
return HyperionArgs(dev_mode=args.dev or False, mode=args.mode)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
from
|
|
1
|
+
from mx_bluesky.common.parameters.components import MxBlueskyParameters
|
|
2
2
|
|
|
3
|
-
from mx_bluesky.common.parameters.components import WithPandaGridScan
|
|
4
|
-
from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags
|
|
5
3
|
|
|
4
|
+
class Wait(MxBlueskyParameters):
|
|
5
|
+
"""Represents an instruction from Agamemnon for Hyperion to wait for a specified time
|
|
6
|
+
Attributes:
|
|
7
|
+
duration_s: duration to wait in seconds
|
|
8
|
+
"""
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
features: HyperionFeatureFlags = Field(default=HyperionFeatureFlags())
|
|
10
|
+
duration_s: float
|