mx-bluesky 1.4.5__py3-none-any.whl → 1.4.7__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 +9 -4
- mx_bluesky/beamlines/aithre_lasershaping/__init__.py +13 -0
- mx_bluesky/beamlines/aithre_lasershaping/check_goniometer_performance.py +29 -0
- mx_bluesky/beamlines/aithre_lasershaping/goniometer_controls.py +18 -0
- mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +45 -28
- mx_bluesky/beamlines/i04/thawing_plan.py +19 -14
- mx_bluesky/beamlines/i24/serial/__init__.py +14 -0
- mx_bluesky/beamlines/i24/serial/dcid.py +3 -1
- mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +12 -12
- mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +31 -30
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +16 -14
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +19 -21
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +11 -4
- mx_bluesky/beamlines/i24/serial/parameters/constants.py +1 -1
- mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
- mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +16 -16
- mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +48 -49
- mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +2 -2
- mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +11 -9
- mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +109 -0
- mx_bluesky/beamlines/i24/serial/write_nexus.py +5 -4
- mx_bluesky/common/device_setup_plans/xbpm_feedback.py +45 -0
- mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +2 -4
- mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
- mx_bluesky/common/external_interaction/callbacks/common/plan_reactive_callback.py +2 -2
- mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +18 -15
- mx_bluesky/common/external_interaction/callbacks/sample_handling/__init__.py +0 -0
- mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/sample_handling_callback.py +29 -12
- mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +43 -7
- mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py +1 -1
- mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +2 -1
- mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -0
- mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +6 -2
- mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +21 -1
- mx_bluesky/common/external_interaction/nexus/nexus_utils.py +1 -1
- mx_bluesky/common/parameters/constants.py +3 -1
- mx_bluesky/common/parameters/gridscan.py +36 -1
- mx_bluesky/common/plans/do_fgs.py +4 -6
- mx_bluesky/common/plans/read_hardware.py +78 -0
- mx_bluesky/common/plans/write_sample_status.py +46 -0
- mx_bluesky/common/preprocessors/__init__.py +0 -0
- mx_bluesky/common/preprocessors/preprocessors.py +105 -0
- mx_bluesky/common/protocols/__init__.py +0 -0
- mx_bluesky/common/protocols/protocols.py +10 -0
- mx_bluesky/common/utils/context.py +68 -0
- mx_bluesky/{hyperion/experiment_plans/common → common}/xrc_result.py +16 -0
- mx_bluesky/hyperion/__main__.py +7 -9
- mx_bluesky/hyperion/baton_handler.py +84 -0
- mx_bluesky/hyperion/device_setup_plans/setup_oav.py +5 -5
- mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -1
- mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +2 -2
- mx_bluesky/hyperion/device_setup_plans/smargon.py +6 -6
- mx_bluesky/hyperion/device_setup_plans/utils.py +2 -2
- mx_bluesky/hyperion/experiment_plans/__init__.py +0 -4
- mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +12 -31
- mx_bluesky/hyperion/experiment_plans/experiment_registry.py +0 -7
- mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +44 -97
- mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +6 -6
- mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +8 -6
- mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +11 -11
- mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +5 -5
- mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +1 -1
- mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +2 -4
- mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +15 -13
- mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +10 -10
- mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +1 -29
- mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +30 -27
- mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +25 -6
- mx_bluesky/hyperion/external_interaction/agamemnon.py +242 -0
- mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +12 -6
- mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +1 -1
- mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +107 -0
- mx_bluesky/hyperion/external_interaction/config_server.py +6 -6
- mx_bluesky/hyperion/parameters/device_composites.py +49 -0
- mx_bluesky/hyperion/parameters/gridscan.py +3 -3
- mx_bluesky/hyperion/parameters/rotation.py +1 -1
- mx_bluesky/hyperion/utils/__init__.py +1 -0
- mx_bluesky/hyperion/utils/context.py +0 -65
- mx_bluesky/hyperion/utils/validation.py +3 -3
- {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/METADATA +6 -5
- {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/RECORD +86 -72
- {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/WHEEL +1 -1
- {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/entry_points.txt +1 -0
- mx_bluesky/common/device_setup_plans/read_hardware_for_setup.py +0 -14
- mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py +0 -22
- mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +0 -54
- mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +0 -103
- /mx_bluesky/{hyperion/external_interaction/callbacks/sample_handling → beamlines/i24/serial/web_gui_plans}/__init__.py +0 -0
- {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info/licenses}/LICENSE +0 -0
- {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from os import path
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from deepdiff.diff import DeepDiff
|
|
9
|
+
from dodal.utils import get_beamline_name
|
|
10
|
+
from jsonschema import ValidationError
|
|
11
|
+
from pydantic_extra_types.semantic_version import SemanticVersion
|
|
12
|
+
|
|
13
|
+
from mx_bluesky.common.parameters.components import (
|
|
14
|
+
PARAMETER_VERSION,
|
|
15
|
+
MxBlueskyParameters,
|
|
16
|
+
TopNByMaxCountSelection,
|
|
17
|
+
WithCentreSelection,
|
|
18
|
+
WithOptionalEnergyChange,
|
|
19
|
+
WithSample,
|
|
20
|
+
WithVisit,
|
|
21
|
+
)
|
|
22
|
+
from mx_bluesky.common.parameters.constants import (
|
|
23
|
+
GridscanParamConstants,
|
|
24
|
+
)
|
|
25
|
+
from mx_bluesky.common.utils.log import LOGGER
|
|
26
|
+
from mx_bluesky.common.utils.utils import convert_angstrom_to_eV
|
|
27
|
+
from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
|
|
28
|
+
from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
|
|
29
|
+
from mx_bluesky.hyperion.parameters.robot_load import RobotLoadThenCentre
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T", bound=WithVisit)
|
|
32
|
+
AGAMEMNON_URL = "http://agamemnon.diamond.ac.uk/"
|
|
33
|
+
MULTIPIN_PREFIX = "multipin"
|
|
34
|
+
MULTIPIN_FORMAT_DESC = "Expected multipin format is multipin_{number_of_wells}x{well_size}+{distance_between_tip_and_first_well}"
|
|
35
|
+
MULTIPIN_REGEX = rf"^{MULTIPIN_PREFIX}_(\d+)x(\d+(?:\.\d+)?)\+(\d+(?:\.\d+)?)$"
|
|
36
|
+
MX_GENERAL_ROOT_REGEX = r"^/dls/(?P<beamline>[^/]+)/data/[^/]*/(?P<visit>[^/]+)(?:/|$)"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AgamemnonLoadCentreCollect(
|
|
40
|
+
MxBlueskyParameters,
|
|
41
|
+
WithVisit,
|
|
42
|
+
WithSample,
|
|
43
|
+
WithCentreSelection,
|
|
44
|
+
WithHyperionUDCFeatures,
|
|
45
|
+
WithOptionalEnergyChange,
|
|
46
|
+
):
|
|
47
|
+
"""Experiment parameters to compare against GDA populated LoadCentreCollect."""
|
|
48
|
+
|
|
49
|
+
robot_load_then_centre: RobotLoadThenCentre
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclasses.dataclass
|
|
53
|
+
class PinType:
|
|
54
|
+
expected_number_of_crystals: int
|
|
55
|
+
single_well_width_um: float
|
|
56
|
+
tip_to_first_well_um: float = 0
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def full_width(self) -> float:
|
|
60
|
+
"""This is the "width" of the area where there may be samples.
|
|
61
|
+
|
|
62
|
+
From a pin perspective this is along the length of the pin but we use width here as
|
|
63
|
+
we mount the sample at 90 deg to the optical camera.
|
|
64
|
+
|
|
65
|
+
We calculate the full width by adding all the gaps between wells then assuming
|
|
66
|
+
there is a buffer of {tip_to_first_well_um} either side too. In reality the
|
|
67
|
+
calculation does not need to be very exact as long as we get a width that's good
|
|
68
|
+
enough to use for optical centring and XRC grid size.
|
|
69
|
+
"""
|
|
70
|
+
return (self.expected_number_of_crystals - 1) * self.single_well_width_um + (
|
|
71
|
+
2 * self.tip_to_first_well_um
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SinglePin(PinType):
|
|
76
|
+
def __init__(self):
|
|
77
|
+
super().__init__(1, GridscanParamConstants.WIDTH_UM)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def full_width(self) -> float:
|
|
81
|
+
return self.single_well_width_um
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_parameters_from_url(url: str) -> dict:
|
|
85
|
+
response = requests.get(url, headers={"Accept": "application/json"})
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
response_json = json.loads(response.content)
|
|
88
|
+
try:
|
|
89
|
+
return response_json["collect"]
|
|
90
|
+
except KeyError as e:
|
|
91
|
+
raise KeyError(f"Unexpected json from agamemnon: {response_json}") from e
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_pin_type_from_agamemnon_parameters(parameters: dict) -> PinType:
|
|
95
|
+
loop_type_name: str | None = parameters["sample"]["loopType"]
|
|
96
|
+
if loop_type_name:
|
|
97
|
+
regex_search = re.search(MULTIPIN_REGEX, loop_type_name)
|
|
98
|
+
if regex_search:
|
|
99
|
+
wells, well_size, tip_to_first_well = regex_search.groups()
|
|
100
|
+
return PinType(int(wells), float(well_size), float(tip_to_first_well))
|
|
101
|
+
else:
|
|
102
|
+
loop_type_message = (
|
|
103
|
+
f"Agamemnon loop type of {loop_type_name} not recognised"
|
|
104
|
+
)
|
|
105
|
+
if loop_type_name.startswith(MULTIPIN_PREFIX):
|
|
106
|
+
raise ValueError(f"{loop_type_message}. {MULTIPIN_FORMAT_DESC}")
|
|
107
|
+
LOGGER.warning(f"{loop_type_message}, assuming single pin")
|
|
108
|
+
return SinglePin()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_next_instruction(beamline: str) -> dict:
|
|
112
|
+
return _get_parameters_from_url(AGAMEMNON_URL + f"getnextcollect/{beamline}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
|
|
116
|
+
try:
|
|
117
|
+
prefix = parameters["prefix"]
|
|
118
|
+
collection = parameters["collection"]
|
|
119
|
+
# Assuming distance is identical for multiple collections. Remove after https://github.com/DiamondLightSource/mx-bluesky/issues/773
|
|
120
|
+
detector_distance = collection[0]["distance"]
|
|
121
|
+
except KeyError as e:
|
|
122
|
+
raise KeyError("Unexpected json from agamemnon") from e
|
|
123
|
+
|
|
124
|
+
match = re.match(MX_GENERAL_ROOT_REGEX, prefix) if prefix else None
|
|
125
|
+
|
|
126
|
+
if match:
|
|
127
|
+
return (match.group("visit"), detector_distance)
|
|
128
|
+
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Agamemnon prefix '{prefix}' does not match MX-General root structure"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_withsample_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
|
|
135
|
+
assert parameters.get("sample"), "instruction does not have a sample"
|
|
136
|
+
return {
|
|
137
|
+
"sample_id": parameters["sample"]["id"],
|
|
138
|
+
"sample_puck": parameters["sample"]["container"],
|
|
139
|
+
"sample_pin": parameters["sample"]["position"],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
|
|
144
|
+
try:
|
|
145
|
+
first_collection: dict = parameters["collection"][0]
|
|
146
|
+
wavelength = first_collection.get("wavelength")
|
|
147
|
+
assert isinstance(wavelength, float)
|
|
148
|
+
demand_energy_ev = convert_angstrom_to_eV(wavelength)
|
|
149
|
+
return {"demand_energy_ev": demand_energy_ev}
|
|
150
|
+
except (KeyError, IndexError, AttributeError, TypeError):
|
|
151
|
+
return {"demand_energy_ev": None}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_param_version() -> SemanticVersion:
|
|
155
|
+
return SemanticVersion.validate_from_str(str(PARAMETER_VERSION))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_robot_load_then_centre_params_from_agamemnon(
|
|
159
|
+
parameters: dict,
|
|
160
|
+
) -> RobotLoadThenCentre:
|
|
161
|
+
visit, detector_distance = get_withvisit_parameters_from_agamemnon(parameters)
|
|
162
|
+
with_sample_params = get_withsample_parameters_from_agamemnon(parameters)
|
|
163
|
+
with_energy_params = get_withenergy_parameters_from_agamemnon(parameters)
|
|
164
|
+
visit_directory, file_name = path.split(parameters["prefix"])
|
|
165
|
+
return RobotLoadThenCentre(
|
|
166
|
+
parameter_model_version=get_param_version(),
|
|
167
|
+
storage_directory=visit_directory + "/xraycentring",
|
|
168
|
+
visit=visit,
|
|
169
|
+
detector_distance_mm=detector_distance,
|
|
170
|
+
snapshot_directory=visit_directory + "/snapshots",
|
|
171
|
+
file_name=file_name,
|
|
172
|
+
**with_energy_params,
|
|
173
|
+
**with_sample_params,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def populate_parameters_from_agamemnon(agamemnon_params):
|
|
178
|
+
visit, detector_distance = get_withvisit_parameters_from_agamemnon(agamemnon_params)
|
|
179
|
+
with_sample_params = get_withsample_parameters_from_agamemnon(agamemnon_params)
|
|
180
|
+
with_energy_params = get_withenergy_parameters_from_agamemnon(agamemnon_params)
|
|
181
|
+
pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
|
|
182
|
+
robot_load_params = create_robot_load_then_centre_params_from_agamemnon(
|
|
183
|
+
agamemnon_params
|
|
184
|
+
)
|
|
185
|
+
return AgamemnonLoadCentreCollect(
|
|
186
|
+
parameter_model_version=SemanticVersion.validate_from_str(
|
|
187
|
+
str(PARAMETER_VERSION)
|
|
188
|
+
),
|
|
189
|
+
visit=visit,
|
|
190
|
+
detector_distance_mm=detector_distance,
|
|
191
|
+
select_centres=TopNByMaxCountSelection(n=pin_type.expected_number_of_crystals),
|
|
192
|
+
robot_load_then_centre=robot_load_params,
|
|
193
|
+
**with_sample_params,
|
|
194
|
+
**with_energy_params,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def create_parameters_from_agamemnon() -> AgamemnonLoadCentreCollect:
|
|
199
|
+
beamline_name = get_beamline_name("i03")
|
|
200
|
+
agamemnon_params = get_next_instruction(beamline_name)
|
|
201
|
+
|
|
202
|
+
return populate_parameters_from_agamemnon(agamemnon_params)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def compare_params(load_centre_collect_params):
|
|
206
|
+
try:
|
|
207
|
+
parameters = create_parameters_from_agamemnon()
|
|
208
|
+
|
|
209
|
+
# Log differences against GDA populated parameters
|
|
210
|
+
differences = DeepDiff(
|
|
211
|
+
parameters, load_centre_collect_params, math_epsilon=1e-5
|
|
212
|
+
)
|
|
213
|
+
if differences:
|
|
214
|
+
LOGGER.info(
|
|
215
|
+
f"Different parameters found when directly reading from Hyperion: {differences}"
|
|
216
|
+
)
|
|
217
|
+
except (ValueError, KeyError) as e:
|
|
218
|
+
LOGGER.warning(f"Failed to compare parameters: {e}")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
LOGGER.warning(f"Unexpected error occurred. Failed to compare parameters: {e}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def update_params_from_agamemnon(parameters: T) -> T:
|
|
224
|
+
try:
|
|
225
|
+
beamline_name = get_beamline_name("i03")
|
|
226
|
+
agamemnon_params = get_next_instruction(beamline_name)
|
|
227
|
+
pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
|
|
228
|
+
if isinstance(parameters, LoadCentreCollect):
|
|
229
|
+
parameters.robot_load_then_centre.tip_offset_um = pin_type.full_width / 2
|
|
230
|
+
parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
|
|
231
|
+
parameters.select_centres.n = pin_type.expected_number_of_crystals
|
|
232
|
+
if pin_type != SinglePin():
|
|
233
|
+
# Snapshots between each collection take a lot of time.
|
|
234
|
+
# Before we do https://github.com/DiamondLightSource/mx-bluesky/issues/226
|
|
235
|
+
# this will give no snapshots but that's preferable
|
|
236
|
+
parameters.multi_rotation_scan.snapshot_omegas_deg = []
|
|
237
|
+
except (ValueError, ValidationError) as e:
|
|
238
|
+
LOGGER.warning(f"Failed to update parameters: {e}")
|
|
239
|
+
except Exception as e:
|
|
240
|
+
LOGGER.warning(f"Unexpected error occurred. Failed to update parameters: {e}")
|
|
241
|
+
|
|
242
|
+
return parameters
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections.abc import Callable, Sequence
|
|
3
3
|
from threading import Thread
|
|
4
|
-
from time import sleep
|
|
5
4
|
|
|
5
|
+
import bluesky.plan_stubs as bps
|
|
6
6
|
from bluesky.callbacks import CallbackBase
|
|
7
7
|
from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
|
|
8
8
|
from dodal.log import LOGGER as dodal_logger
|
|
@@ -14,6 +14,9 @@ from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callbac
|
|
|
14
14
|
from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import (
|
|
15
15
|
ZocaloCallback,
|
|
16
16
|
)
|
|
17
|
+
from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import (
|
|
18
|
+
SampleHandlingCallback,
|
|
19
|
+
)
|
|
17
20
|
from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
|
|
18
21
|
GridscanISPyBCallback,
|
|
19
22
|
)
|
|
@@ -35,8 +38,8 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback
|
|
|
35
38
|
from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
|
|
36
39
|
RotationNexusFileCallback,
|
|
37
40
|
)
|
|
38
|
-
from mx_bluesky.hyperion.external_interaction.callbacks.
|
|
39
|
-
|
|
41
|
+
from mx_bluesky.hyperion.external_interaction.callbacks.snapshot_callback import (
|
|
42
|
+
BeamDrawingCallback,
|
|
40
43
|
)
|
|
41
44
|
from mx_bluesky.hyperion.parameters.cli import parse_callback_dev_mode_arg
|
|
42
45
|
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
@@ -67,15 +70,18 @@ def create_rotation_callbacks() -> tuple[
|
|
|
67
70
|
return (
|
|
68
71
|
RotationNexusFileCallback(),
|
|
69
72
|
RotationISPyBCallback(
|
|
70
|
-
emit=ZocaloCallback(CONST.PLAN.
|
|
73
|
+
emit=ZocaloCallback(CONST.PLAN.ROTATION_MULTI, CONST.ZOCALO_ENV)
|
|
71
74
|
),
|
|
72
75
|
)
|
|
73
76
|
|
|
74
77
|
|
|
75
78
|
def setup_callbacks() -> list[CallbackBase]:
|
|
79
|
+
rot_nexus_cb, rot_ispyb_cb = create_rotation_callbacks()
|
|
80
|
+
snapshot_cb = BeamDrawingCallback(emit=rot_ispyb_cb)
|
|
76
81
|
return [
|
|
77
82
|
*create_gridscan_callbacks(),
|
|
78
|
-
|
|
83
|
+
rot_nexus_cb,
|
|
84
|
+
snapshot_cb,
|
|
79
85
|
LogUidTaggingCallback(),
|
|
80
86
|
RobotLoadISPyBCallback(),
|
|
81
87
|
SampleHandlingCallback(),
|
|
@@ -134,7 +140,7 @@ def wait_for_threads_forever(threads: Sequence[Thread]):
|
|
|
134
140
|
try:
|
|
135
141
|
log_debug("Trying to wait forever on callback and dispatcher threads")
|
|
136
142
|
while all(alive):
|
|
137
|
-
sleep(LIVENESS_POLL_SECONDS)
|
|
143
|
+
yield from bps.sleep(LIVENESS_POLL_SECONDS)
|
|
138
144
|
alive = [t.is_alive() for t in threads]
|
|
139
145
|
except KeyboardInterrupt:
|
|
140
146
|
log_info("Main thread received interrupt - exiting.")
|
|
@@ -136,7 +136,7 @@ class RotationISPyBCallback(BaseISPyBCallback):
|
|
|
136
136
|
"handle_ispyb_hardware_read triggered before activity_gated_start"
|
|
137
137
|
)
|
|
138
138
|
motor_positions_um = [position * 1000 for position in motor_positions_mm]
|
|
139
|
-
comment = f"Sample position (µm): ({motor_positions_um[0]:.0f}, {motor_positions_um[1]:.0f}, {motor_positions_um[2]:.0f})
|
|
139
|
+
comment = f"Sample position (µm): ({motor_positions_um[0]:.0f}, {motor_positions_um[1]:.0f}, {motor_positions_um[2]:.0f})"
|
|
140
140
|
scan_data_infos[0].data_collection_info.comments = comment
|
|
141
141
|
return scan_data_infos
|
|
142
142
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from dodal.devices.oav.snapshots.snapshot_image_processing import (
|
|
4
|
+
compute_beam_centre_pixel_xy_for_mm_position,
|
|
5
|
+
draw_crosshair,
|
|
6
|
+
)
|
|
7
|
+
from event_model import Event, EventDescriptor, RunStart
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import (
|
|
11
|
+
PlanReactiveCallback,
|
|
12
|
+
)
|
|
13
|
+
from mx_bluesky.common.parameters.constants import DocDescriptorNames
|
|
14
|
+
from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER as CALLBACK_LOGGER
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BeamDrawingCallback(PlanReactiveCallback):
|
|
18
|
+
"""
|
|
19
|
+
Callback that monitors for OAV_ROTATION_SNAPSHOT_TRIGGERED events and
|
|
20
|
+
draws a crosshair at the beam centre, saving the snapshot to a file.
|
|
21
|
+
The callback assumes an OAV device "oav"
|
|
22
|
+
Examples:
|
|
23
|
+
Take a snapshot at the current location
|
|
24
|
+
>>> from bluesky.run_engine import RunEngine
|
|
25
|
+
>>> import bluesky.preprocessors as bpp
|
|
26
|
+
>>> import bluesky.plan_stubs as bps
|
|
27
|
+
>>> from dodal.devices.oav.oav_detector import OAV
|
|
28
|
+
>>> from mx_bluesky.common.parameters.components import WithSnapshot
|
|
29
|
+
>>> def take_snapshot(params: WithSnapshot, oav: OAV, run_engine: RunEngine):
|
|
30
|
+
... run_engine.subscribe(BeamDrawingCallback())
|
|
31
|
+
... @bpp.run_decorator(md={
|
|
32
|
+
... "activate_callbacks": ["BeamDrawingCallback"],
|
|
33
|
+
... "with_snapshot": params.model_dump_json(),
|
|
34
|
+
... })
|
|
35
|
+
... def inner_plan():
|
|
36
|
+
... yield from bps.abs_set(oav.snapshot.directory, "/path/to/snapshot_folder", wait=True)
|
|
37
|
+
... yield from bps.abs_set(oav.snapshot.filename, "my_snapshot_prefix", wait=True)
|
|
38
|
+
... yield from bps.trigger(oav.snapshot, wait=True)
|
|
39
|
+
... yield from bps.create(DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED)
|
|
40
|
+
... yield from bps.read(oav)
|
|
41
|
+
... yield from bps.save()
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
super().__init__(*args, log=CALLBACK_LOGGER, **kwargs)
|
|
46
|
+
self._snapshot_files: list[str] = []
|
|
47
|
+
self._microns_per_pixel: tuple[float, float]
|
|
48
|
+
self._beam_centre: tuple[int, int]
|
|
49
|
+
self._rotation_snapshot_descriptor: str = ""
|
|
50
|
+
|
|
51
|
+
def activity_gated_start(self, doc: RunStart):
|
|
52
|
+
if self.activity_uid == doc.get("uid"):
|
|
53
|
+
with_snapshot_json = doc.get("with_snapshot") # type: ignore
|
|
54
|
+
assert with_snapshot_json, (
|
|
55
|
+
"run start event did not have expected snapshot json"
|
|
56
|
+
)
|
|
57
|
+
return doc
|
|
58
|
+
|
|
59
|
+
def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
|
|
60
|
+
if doc.get("name") == DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED:
|
|
61
|
+
self._rotation_snapshot_descriptor = doc["uid"]
|
|
62
|
+
return doc
|
|
63
|
+
|
|
64
|
+
def activity_gated_event(self, doc: Event) -> Event:
|
|
65
|
+
if doc["descriptor"] == self._rotation_snapshot_descriptor:
|
|
66
|
+
self._handle_rotation_snapshot(doc)
|
|
67
|
+
return doc
|
|
68
|
+
|
|
69
|
+
def _extract_base_snapshot_params(self, doc: Event):
|
|
70
|
+
data = doc["data"]
|
|
71
|
+
self._snapshot_files.append(data["oav-snapshot-last_saved_path"])
|
|
72
|
+
self._microns_per_pixel = (
|
|
73
|
+
data["oav-microns_per_pixel_x"],
|
|
74
|
+
data["oav-microns_per_pixel_y"],
|
|
75
|
+
)
|
|
76
|
+
self._beam_centre = (data["oav-beam_centre_i"], data["oav-beam_centre_j"])
|
|
77
|
+
|
|
78
|
+
def _handle_rotation_snapshot(self, doc: Event):
|
|
79
|
+
self._extract_base_snapshot_params(doc)
|
|
80
|
+
data = doc["data"]
|
|
81
|
+
snapshot_path = data["oav-snapshot-last_saved_path"]
|
|
82
|
+
match = re.match("(.*)\\.png", snapshot_path)
|
|
83
|
+
assert match, f"Snapshot {snapshot_path} was not a .png file"
|
|
84
|
+
snapshot_base = match.groups()[0]
|
|
85
|
+
output_snapshot_path = f"{snapshot_base}_with_beam_centre.png"
|
|
86
|
+
self._generate_snapshot_at(snapshot_path, output_snapshot_path, 0, 0)
|
|
87
|
+
data["oav-snapshot-last_saved_path"] = output_snapshot_path
|
|
88
|
+
return doc
|
|
89
|
+
|
|
90
|
+
def _generate_snapshot_at(
|
|
91
|
+
self, input_snapshot_path: str, output_snapshot_path: str, x_mm: int, y_mm: int
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Save a snapshot to the specified path, with an annotated crosshair at the specified
|
|
95
|
+
position
|
|
96
|
+
Args:
|
|
97
|
+
input_snapshot_path: The non-annotated image path.
|
|
98
|
+
output_snapshot_path: The path to the image that will be annotated.
|
|
99
|
+
x_mm: Relative x location of the sample to the original image (mm)
|
|
100
|
+
y_mm: Relative y location of the sample to the original image (mm)
|
|
101
|
+
"""
|
|
102
|
+
image = Image.open(input_snapshot_path)
|
|
103
|
+
x_px, y_px = compute_beam_centre_pixel_xy_for_mm_position(
|
|
104
|
+
(x_mm, y_mm), self._beam_centre, self._microns_per_pixel
|
|
105
|
+
)
|
|
106
|
+
draw_crosshair(image, x_px, y_px)
|
|
107
|
+
image.save(output_snapshot_path, format="png")
|
|
@@ -29,15 +29,15 @@ class HyperionFeatureFlags(FeatureFlags):
|
|
|
29
29
|
def get_config_server() -> ConfigServer:
|
|
30
30
|
return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
|
|
31
31
|
|
|
32
|
-
use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
|
|
33
|
-
compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO
|
|
34
|
-
use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
|
|
35
|
-
set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
|
|
36
|
-
omega_flip: bool = CONST.I03.OMEGA_FLIP
|
|
37
|
-
|
|
38
32
|
@model_validator(mode="after")
|
|
39
33
|
def use_gpu_and_compare_cannot_both_be_true(self):
|
|
40
34
|
assert not (self.use_gpu_results and self.compare_cpu_and_gpu_zocalo), (
|
|
41
35
|
"Cannot both use GPU results and compare them to CPU"
|
|
42
36
|
)
|
|
43
37
|
return self
|
|
38
|
+
|
|
39
|
+
use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
|
|
40
|
+
compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO
|
|
41
|
+
use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
|
|
42
|
+
set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
|
|
43
|
+
omega_flip: bool = CONST.I03.OMEGA_FLIP
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pydantic
|
|
4
|
+
from dodal.devices.aperturescatterguard import (
|
|
5
|
+
ApertureScatterguard,
|
|
6
|
+
)
|
|
7
|
+
from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
|
|
8
|
+
from dodal.devices.backlight import Backlight
|
|
9
|
+
from dodal.devices.dcm import DCM
|
|
10
|
+
from dodal.devices.eiger import EigerDetector
|
|
11
|
+
from dodal.devices.fast_grid_scan import (
|
|
12
|
+
PandAFastGridScan,
|
|
13
|
+
ZebraFastGridScan,
|
|
14
|
+
)
|
|
15
|
+
from dodal.devices.flux import Flux
|
|
16
|
+
from dodal.devices.robot import BartRobot
|
|
17
|
+
from dodal.devices.s4_slit_gaps import S4SlitGaps
|
|
18
|
+
from dodal.devices.smargon import Smargon
|
|
19
|
+
from dodal.devices.synchrotron import Synchrotron
|
|
20
|
+
from dodal.devices.undulator import Undulator
|
|
21
|
+
from dodal.devices.xbpm_feedback import XBPMFeedback
|
|
22
|
+
from dodal.devices.zebra.zebra import Zebra
|
|
23
|
+
from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
|
|
24
|
+
from dodal.devices.zocalo import ZocaloResults
|
|
25
|
+
from ophyd_async.fastcs.panda import HDFPanda
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
|
|
29
|
+
class HyperionFlyScanXRayCentreComposite:
|
|
30
|
+
"""All devices which are directly or indirectly required by this plan"""
|
|
31
|
+
|
|
32
|
+
aperture_scatterguard: ApertureScatterguard
|
|
33
|
+
attenuator: BinaryFilterAttenuator
|
|
34
|
+
backlight: Backlight
|
|
35
|
+
dcm: DCM
|
|
36
|
+
eiger: EigerDetector
|
|
37
|
+
zebra_fast_grid_scan: ZebraFastGridScan
|
|
38
|
+
flux: Flux
|
|
39
|
+
s4_slit_gaps: S4SlitGaps
|
|
40
|
+
smargon: Smargon
|
|
41
|
+
undulator: Undulator
|
|
42
|
+
synchrotron: Synchrotron
|
|
43
|
+
xbpm_feedback: XBPMFeedback
|
|
44
|
+
zebra: Zebra
|
|
45
|
+
zocalo: ZocaloResults
|
|
46
|
+
panda: HDFPanda
|
|
47
|
+
panda_fast_grid_scan: PandAFastGridScan
|
|
48
|
+
robot: BartRobot
|
|
49
|
+
sample_shutter: ZebraShutter
|
|
@@ -36,7 +36,7 @@ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
|
|
|
36
36
|
return DetectorParams(
|
|
37
37
|
detector_size_constants=I03Constants.DETECTOR,
|
|
38
38
|
expected_energy_ev=self.demand_energy_ev,
|
|
39
|
-
|
|
39
|
+
exposure_time_s=self.exposure_time_s,
|
|
40
40
|
directory=self.storage_directory,
|
|
41
41
|
prefix=self.file_name,
|
|
42
42
|
detector_distance=self.detector_distance_mm,
|
|
@@ -53,7 +53,7 @@ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
class HyperionSpecifiedThreeDGridScan(
|
|
56
|
+
class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGridScan):
|
|
57
57
|
"""Hyperion's 3D grid scan deviates from the common class due to: optionally using a PandA, optionally using dev_shm for GPU analysis, and using a config server for features"""
|
|
58
58
|
|
|
59
59
|
# These detector params only exist so that we can properly select enable_dev_shm. Remove in
|
|
@@ -73,7 +73,7 @@ class HyperionSpecifiedThreeDGridScan(SpecifiedThreeDGridScan, WithHyperionUDCFe
|
|
|
73
73
|
return DetectorParams(
|
|
74
74
|
detector_size_constants=I03Constants.DETECTOR,
|
|
75
75
|
expected_energy_ev=self.demand_energy_ev,
|
|
76
|
-
|
|
76
|
+
exposure_time_s=self.exposure_time_s,
|
|
77
77
|
directory=self.storage_directory,
|
|
78
78
|
prefix=self.file_name,
|
|
79
79
|
detector_distance=self.detector_distance_mm,
|
|
@@ -75,7 +75,7 @@ class RotationExperiment(DiffractionExperimentWithSample, WithHyperionUDCFeature
|
|
|
75
75
|
return DetectorParams(
|
|
76
76
|
detector_size_constants=I03Constants.DETECTOR,
|
|
77
77
|
expected_energy_ev=self.demand_energy_ev,
|
|
78
|
-
|
|
78
|
+
exposure_time_s=self.exposure_time_s,
|
|
79
79
|
directory=self.storage_directory,
|
|
80
80
|
prefix=self.file_name,
|
|
81
81
|
detector_distance=self.detector_distance_mm,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# placeholder file to start layout
|
|
@@ -1,74 +1,9 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
from typing import Any, ClassVar, Protocol, TypeVar, get_type_hints
|
|
3
|
-
|
|
4
1
|
from blueapi.core import BlueskyContext
|
|
5
|
-
from blueapi.core.bluesky_types import Device
|
|
6
2
|
from dodal.utils import get_beamline_based_on_environment_variable
|
|
7
3
|
|
|
8
4
|
import mx_bluesky.hyperion.experiment_plans as hyperion_plans
|
|
9
5
|
from mx_bluesky.common.utils.log import LOGGER
|
|
10
6
|
|
|
11
|
-
T = TypeVar("T", bound=Device)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class _IsDataclass(Protocol):
|
|
15
|
-
"""Protocol followed by any dataclass"""
|
|
16
|
-
|
|
17
|
-
__dataclass_fields__: ClassVar[dict]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
DT = TypeVar("DT", bound=_IsDataclass)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def find_device_in_context(
|
|
24
|
-
context: BlueskyContext,
|
|
25
|
-
name: str,
|
|
26
|
-
# Typing in here is wrong (see https://github.com/microsoft/pyright/issues/7228#issuecomment-1934500232)
|
|
27
|
-
# but this whole thing will go away when we do https://github.com/DiamondLightSource/hyperion/issues/868
|
|
28
|
-
expected_type: type[T] = Device, # type: ignore
|
|
29
|
-
) -> T:
|
|
30
|
-
LOGGER.debug(f"Looking for device {name} of type {expected_type} in context")
|
|
31
|
-
|
|
32
|
-
device = context.find_device(name)
|
|
33
|
-
if device is None:
|
|
34
|
-
raise ValueError(
|
|
35
|
-
f"Cannot find device named '{name}' in bluesky context {context.devices}."
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
if not isinstance(device, expected_type):
|
|
39
|
-
raise ValueError(
|
|
40
|
-
f"Found device named '{name}' and expected it to be a '{expected_type}' but it was a '{device.__class__.__name__}'"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
LOGGER.debug(f"Found matching device {device}")
|
|
44
|
-
return device
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def device_composite_from_context(context: BlueskyContext, dc: type[DT]) -> DT:
|
|
48
|
-
"""
|
|
49
|
-
Initializes all of the devices referenced in a given dataclass from a provided
|
|
50
|
-
context, checking that the types of devices returned by the context are compatible
|
|
51
|
-
with the type annotations of the dataclass.
|
|
52
|
-
|
|
53
|
-
Note that if the context was not created with `wait_for_connection=True` devices may
|
|
54
|
-
still be unconnected.
|
|
55
|
-
"""
|
|
56
|
-
LOGGER.debug(
|
|
57
|
-
f"Attempting to initialize devices referenced in dataclass {dc} from blueapi context"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
devices: dict[str, Any] = {}
|
|
61
|
-
dc_type_hints: dict[str, Any] = get_type_hints(dc)
|
|
62
|
-
|
|
63
|
-
for field in dataclasses.fields(dc):
|
|
64
|
-
device = find_device_in_context(
|
|
65
|
-
context, field.name, expected_type=dc_type_hints.get(field.name, Device)
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
devices[field.name] = device
|
|
69
|
-
|
|
70
|
-
return dc(**devices)
|
|
71
|
-
|
|
72
7
|
|
|
73
8
|
def setup_context(wait_for_connection: bool = True) -> BlueskyContext:
|
|
74
9
|
context = BlueskyContext()
|
|
@@ -11,8 +11,8 @@ from dodal.beamlines import i03
|
|
|
11
11
|
from dodal.devices.oav.oav_parameters import OAVConfig
|
|
12
12
|
from ophyd_async.testing import set_mock_value
|
|
13
13
|
|
|
14
|
-
from mx_bluesky.
|
|
15
|
-
|
|
14
|
+
from mx_bluesky.common.plans.read_hardware import (
|
|
15
|
+
standard_read_hardware_during_collection,
|
|
16
16
|
)
|
|
17
17
|
from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import (
|
|
18
18
|
RotationScanComposite,
|
|
@@ -67,7 +67,7 @@ def fake_rotation_scan(
|
|
|
67
67
|
}
|
|
68
68
|
)
|
|
69
69
|
def plan():
|
|
70
|
-
yield from
|
|
70
|
+
yield from standard_read_hardware_during_collection(
|
|
71
71
|
rotation_devices.aperture_scatterguard,
|
|
72
72
|
rotation_devices.attenuator,
|
|
73
73
|
rotation_devices.flux,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mx-bluesky
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.7
|
|
4
4
|
Summary: Bluesky tools for MX Beamlines at DLS
|
|
5
5
|
Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
|
|
6
6
|
License: Apache License
|
|
@@ -238,8 +238,8 @@ Requires-Dist: blueapi>=0.5.0
|
|
|
238
238
|
Requires-Dist: daq-config-server>=0.1.1
|
|
239
239
|
Requires-Dist: ophyd==1.9.0
|
|
240
240
|
Requires-Dist: ophyd-async>=0.9.0a2
|
|
241
|
-
Requires-Dist: bluesky>=1.13
|
|
242
|
-
Requires-Dist: dls-dodal==1.
|
|
241
|
+
Requires-Dist: bluesky>=1.13.1
|
|
242
|
+
Requires-Dist: dls-dodal==1.44.0
|
|
243
243
|
Provides-Extra: dev
|
|
244
244
|
Requires-Dist: black; extra == "dev"
|
|
245
245
|
Requires-Dist: build; extra == "dev"
|
|
@@ -266,6 +266,7 @@ Requires-Dist: tox-direct; extra == "dev"
|
|
|
266
266
|
Requires-Dist: tox; extra == "dev"
|
|
267
267
|
Requires-Dist: types-mock; extra == "dev"
|
|
268
268
|
Requires-Dist: types-requests; extra == "dev"
|
|
269
|
+
Dynamic: license-file
|
|
269
270
|
|
|
270
271
|
mx-bluesky
|
|
271
272
|
===========================
|
|
@@ -284,7 +285,7 @@ Releases https://github.com/DiamondLightSource/mx-bluesky/releases
|
|
|
284
285
|
Getting Started
|
|
285
286
|
===============
|
|
286
287
|
|
|
287
|
-
To get started with developing this repo at DLS run
|
|
288
|
+
To get started with developing this repo at DLS run ``./utility_scripts/dls_dev_env.sh``.
|
|
288
289
|
|
|
289
290
|
If you want to develop interactively at the beamline we recommend using jupyter notebooks. You can get started with this by running::
|
|
290
291
|
|