mx-bluesky 0.0.2__py3-none-any.whl → 1.1.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.
Files changed (150) hide show
  1. mx_bluesky/__main__.py +1 -2
  2. mx_bluesky/_version.py +14 -2
  3. mx_bluesky/beamlines/i04/__init__.py +3 -0
  4. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +45 -0
  5. mx_bluesky/beamlines/i04/thawing_plan.py +85 -0
  6. mx_bluesky/beamlines/i24/serial/__init__.py +49 -0
  7. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +12 -0
  8. mx_bluesky/{I24 → beamlines/i24}/serial/dcid.py +53 -41
  9. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/DetStage.edl +3 -4
  10. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +28 -32
  11. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/microdrop_alignment.edl +0 -1
  12. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +516 -0
  13. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +3 -4
  14. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DetStage.edl +3 -4
  15. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +273 -223
  16. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/ME14E-GeneralPurpose.edl +0 -1
  17. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +12 -13
  18. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +0 -1
  19. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/Shutter_Control.edl +0 -1
  20. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/microdrop_alignment.edl +0 -1
  21. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/nudgechip.edl +0 -1
  22. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +273 -143
  23. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/short1-laser.png +0 -0
  24. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/short2-laser.png +0 -0
  25. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/ft_utils.py +24 -1
  26. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +808 -0
  27. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +377 -416
  28. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +34 -40
  29. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +328 -0
  30. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_moveonclick.py +66 -48
  31. mx_bluesky/{I24 → beamlines/i24}/serial/log.py +66 -19
  32. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +15 -0
  33. mx_bluesky/beamlines/i24/serial/parameters/constants.py +47 -0
  34. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +103 -0
  35. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +9 -0
  36. mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/cs/motor_direction.txt +1 -1
  37. mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/minichip-oxford.pvar +1 -1
  38. mx_bluesky/beamlines/i24/serial/parameters/utils.py +42 -0
  39. mx_bluesky/beamlines/i24/serial/run_extruder.sh +19 -0
  40. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +22 -0
  41. mx_bluesky/beamlines/i24/serial/run_serial.py +36 -0
  42. mx_bluesky/{I24 → beamlines/i24}/serial/set_visit_directory.sh +6 -1
  43. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/pv.py +1 -62
  44. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/pv_abstract.py +6 -7
  45. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/setup_beamline.py +90 -269
  46. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/setup_detector.py +47 -40
  47. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +459 -0
  48. mx_bluesky/beamlines/i24/serial/start_blueapi.sh +28 -0
  49. mx_bluesky/beamlines/i24/serial/write_nexus.py +105 -0
  50. mx_bluesky/example.py +4 -4
  51. mx_bluesky/hyperion/__init__.py +1 -0
  52. mx_bluesky/hyperion/__main__.py +374 -0
  53. mx_bluesky/hyperion/device_setup_plans/__init__.py +0 -0
  54. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +134 -0
  55. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +110 -0
  56. mx_bluesky/hyperion/device_setup_plans/position_detector.py +16 -0
  57. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +60 -0
  58. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +87 -0
  59. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +210 -0
  60. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +214 -0
  61. mx_bluesky/hyperion/device_setup_plans/smargon.py +25 -0
  62. mx_bluesky/hyperion/device_setup_plans/utils.py +44 -0
  63. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +93 -0
  64. mx_bluesky/hyperion/exceptions.py +47 -0
  65. mx_bluesky/hyperion/experiment_plans/__init__.py +30 -0
  66. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +84 -0
  67. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +528 -0
  68. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +209 -0
  69. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +173 -0
  70. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +81 -0
  71. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +463 -0
  72. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +119 -0
  73. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +164 -0
  74. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +322 -0
  75. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +436 -0
  76. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +68 -0
  77. mx_bluesky/hyperion/external_interaction/__init__.py +9 -0
  78. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +10 -0
  79. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +148 -0
  80. mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py +22 -0
  81. mx_bluesky/hyperion/external_interaction/callbacks/common/__init__.py +0 -0
  82. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +46 -0
  83. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +70 -0
  84. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +88 -0
  85. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +203 -0
  86. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +20 -0
  87. mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py +29 -0
  88. mx_bluesky/hyperion/external_interaction/callbacks/plan_reactive_callback.py +101 -0
  89. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +88 -0
  90. mx_bluesky/hyperion/external_interaction/callbacks/rotation/__init__.py +0 -0
  91. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +174 -0
  92. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +17 -0
  93. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +102 -0
  94. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/__init__.py +0 -0
  95. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +269 -0
  96. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py +53 -0
  97. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +95 -0
  98. mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py +92 -0
  99. mx_bluesky/hyperion/external_interaction/config_server.py +35 -0
  100. mx_bluesky/hyperion/external_interaction/exceptions.py +13 -0
  101. mx_bluesky/hyperion/external_interaction/ispyb/__init__.py +0 -0
  102. mx_bluesky/hyperion/external_interaction/ispyb/data_model.py +95 -0
  103. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +125 -0
  104. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +276 -0
  105. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py +29 -0
  106. mx_bluesky/hyperion/external_interaction/nexus/__init__.py +0 -0
  107. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +148 -0
  108. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +114 -0
  109. mx_bluesky/hyperion/log.py +99 -0
  110. mx_bluesky/hyperion/parameters/__init__.py +2 -0
  111. mx_bluesky/hyperion/parameters/cli.py +68 -0
  112. mx_bluesky/hyperion/parameters/components.py +253 -0
  113. mx_bluesky/hyperion/parameters/constants.py +158 -0
  114. mx_bluesky/hyperion/parameters/gridscan.py +216 -0
  115. mx_bluesky/hyperion/parameters/rotation.py +160 -0
  116. mx_bluesky/hyperion/resources/panda/panda-gridscan.yaml +964 -0
  117. mx_bluesky/hyperion/tracing.py +28 -0
  118. mx_bluesky/hyperion/utils/context.py +84 -0
  119. mx_bluesky/hyperion/utils/utils.py +25 -0
  120. mx_bluesky/hyperion/utils/validation.py +196 -0
  121. mx_bluesky/jupyter_example.ipynb +3 -2
  122. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/METADATA +53 -32
  123. mx_bluesky-1.1.0.dist-info/RECORD +136 -0
  124. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/WHEEL +1 -1
  125. mx_bluesky-1.1.0.dist-info/entry_points.txt +8 -0
  126. mx_bluesky/I24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +0 -476
  127. mx_bluesky/I24/serial/fixed_target/FT-gui-edm/ME14E-motors.edl +0 -1874
  128. mx_bluesky/I24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +0 -706
  129. mx_bluesky/I24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +0 -463
  130. mx_bluesky/I24/serial/parameters/__init__.py +0 -5
  131. mx_bluesky/I24/serial/parameters/constants.py +0 -39
  132. mx_bluesky/I24/serial/parameters/fixed_target/cs/cs_maker.json +0 -9
  133. mx_bluesky/I24/serial/parameters/fixed_target/cs/fiducial_1.txt +0 -4
  134. mx_bluesky/I24/serial/parameters/fixed_target/cs/fiducial_2.txt +0 -4
  135. mx_bluesky/I24/serial/parameters/fixed_target/litemaps/currentchip.map +0 -81
  136. mx_bluesky/I24/serial/parameters/fixed_target/parameters.txt +0 -13
  137. mx_bluesky/I24/serial/run_serial.py +0 -52
  138. mx_bluesky/I24/serial/write_nexus.py +0 -113
  139. mx_bluesky-0.0.2.dist-info/RECORD +0 -58
  140. mx_bluesky-0.0.2.dist-info/entry_points.txt +0 -4
  141. /mx_bluesky/{I24 → beamlines}/__init__.py +0 -0
  142. /mx_bluesky/{I24/serial → beamlines/i24}/__init__.py +0 -0
  143. /mx_bluesky/{I24 → beamlines/i24}/serial/extruder/__init__.py +0 -0
  144. /mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/__init__.py +0 -0
  145. /mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/oxford.pvar +0 -0
  146. /mx_bluesky/{I24 → beamlines/i24}/serial/run_ssx.sh +0 -0
  147. /mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/__init__.py +0 -0
  148. /mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/ca.py +0 -0
  149. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/LICENSE +0 -0
  150. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,374 @@
1
+ import atexit
2
+ import json
3
+ import threading
4
+ from collections.abc import Callable
5
+ from dataclasses import asdict
6
+ from queue import Queue
7
+ from traceback import format_exception
8
+ from typing import Any
9
+
10
+ from blueapi.core import BlueskyContext, MsgGenerator
11
+ from bluesky.callbacks.zmq import Publisher
12
+ from bluesky.run_engine import RunEngine
13
+ from flask import Flask, request
14
+ from flask_restful import Api, Resource
15
+ from pydantic.dataclasses import dataclass
16
+
17
+ from mx_bluesky.hyperion.exceptions import WarningException
18
+ from mx_bluesky.hyperion.experiment_plans.experiment_registry import (
19
+ PLAN_REGISTRY,
20
+ PlanNotFound,
21
+ )
22
+ from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import (
23
+ setup_logging as setup_callback_logging,
24
+ )
25
+ from mx_bluesky.hyperion.external_interaction.callbacks.aperture_change_callback import (
26
+ ApertureChangeCallback,
27
+ )
28
+ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import (
29
+ CallbacksFactory,
30
+ )
31
+ from mx_bluesky.hyperion.external_interaction.callbacks.log_uid_tag_callback import (
32
+ LogUidTaggingCallback,
33
+ )
34
+ from mx_bluesky.hyperion.external_interaction.callbacks.logging_callback import (
35
+ VerbosePlanExecutionLoggingCallback,
36
+ )
37
+ from mx_bluesky.hyperion.log import (
38
+ LOGGER,
39
+ do_default_logging_setup,
40
+ flush_debug_handler,
41
+ )
42
+ from mx_bluesky.hyperion.parameters.cli import parse_cli_args
43
+ from mx_bluesky.hyperion.parameters.components import HyperionParameters
44
+ from mx_bluesky.hyperion.parameters.constants import CONST, Actions, Status
45
+ from mx_bluesky.hyperion.tracing import TRACER
46
+ from mx_bluesky.hyperion.utils.context import setup_context
47
+
48
+ VERBOSE_EVENT_LOGGING: bool | None = None
49
+
50
+
51
+ @dataclass
52
+ class Command:
53
+ action: Actions
54
+ devices: Any | None = None
55
+ experiment: Callable[[Any, Any], MsgGenerator] | None = None
56
+ parameters: HyperionParameters | None = None
57
+ callbacks: CallbacksFactory | None = None
58
+
59
+
60
+ @dataclass
61
+ class StatusAndMessage:
62
+ status: str
63
+ message: str = ""
64
+
65
+ def __init__(self, status: Status, message: str = "") -> None:
66
+ self.status = status.value
67
+ self.message = message
68
+
69
+
70
+ @dataclass
71
+ class ErrorStatusAndMessage(StatusAndMessage):
72
+ exception_type: str = ""
73
+
74
+
75
+ def make_error_status_and_message(exception: Exception):
76
+ return ErrorStatusAndMessage(
77
+ status=Status.FAILED.value,
78
+ message=repr(exception),
79
+ exception_type=type(exception).__name__,
80
+ )
81
+
82
+
83
+ class BlueskyRunner:
84
+ def __init__(
85
+ self,
86
+ RE: RunEngine,
87
+ context: BlueskyContext,
88
+ skip_startup_connection=False,
89
+ use_external_callbacks: bool = False,
90
+ ) -> None:
91
+ self.command_queue: Queue[Command] = Queue()
92
+ self.current_status: StatusAndMessage = StatusAndMessage(Status.IDLE)
93
+ self.last_run_aborted: bool = False
94
+ self.aperture_change_callback = ApertureChangeCallback()
95
+ self.logging_uid_tag_callback = LogUidTaggingCallback()
96
+ self.context: BlueskyContext
97
+
98
+ self.RE = RE
99
+ self.context = context
100
+ self.subscribed_per_plan_callbacks: list[int] = []
101
+ RE.subscribe(self.aperture_change_callback)
102
+ RE.subscribe(self.logging_uid_tag_callback)
103
+
104
+ self.use_external_callbacks = use_external_callbacks
105
+ if self.use_external_callbacks:
106
+ LOGGER.info("Connecting to external callback ZMQ proxy...")
107
+ self.publisher = Publisher(f"localhost:{CONST.CALLBACK_0MQ_PROXY_PORTS[0]}")
108
+ RE.subscribe(self.publisher)
109
+
110
+ if VERBOSE_EVENT_LOGGING:
111
+ RE.subscribe(VerbosePlanExecutionLoggingCallback())
112
+
113
+ self.skip_startup_connection = skip_startup_connection
114
+ if not self.skip_startup_connection:
115
+ LOGGER.info("Initialising dodal devices...")
116
+ for plan_name in PLAN_REGISTRY:
117
+ PLAN_REGISTRY[plan_name]["setup"](context)
118
+
119
+ def start(
120
+ self,
121
+ experiment: Callable,
122
+ parameters: HyperionParameters,
123
+ plan_name: str,
124
+ callbacks: CallbacksFactory | None,
125
+ ) -> StatusAndMessage:
126
+ LOGGER.info(f"Started with parameters: {parameters.model_dump_json(indent=2)}")
127
+
128
+ devices: Any = PLAN_REGISTRY[plan_name]["setup"](self.context)
129
+
130
+ if (
131
+ self.current_status.status == Status.BUSY.value
132
+ or self.current_status.status == Status.ABORTING.value
133
+ ):
134
+ return StatusAndMessage(Status.FAILED, "Bluesky already running")
135
+ else:
136
+ self.current_status = StatusAndMessage(Status.BUSY)
137
+ self.command_queue.put(
138
+ Command(
139
+ action=Actions.START,
140
+ devices=devices,
141
+ experiment=experiment,
142
+ parameters=parameters,
143
+ callbacks=callbacks,
144
+ )
145
+ )
146
+ return StatusAndMessage(Status.SUCCESS)
147
+
148
+ def stopping_thread(self):
149
+ try:
150
+ self.RE.abort()
151
+ self.current_status = StatusAndMessage(Status.IDLE)
152
+ except Exception as e:
153
+ self.current_status = make_error_status_and_message(e)
154
+
155
+ def stop(self) -> StatusAndMessage:
156
+ if self.current_status.status == Status.IDLE.value:
157
+ return StatusAndMessage(Status.FAILED, "Bluesky not running")
158
+ elif self.current_status.status == Status.ABORTING.value:
159
+ return StatusAndMessage(Status.FAILED, "Bluesky already stopping")
160
+ else:
161
+ self.current_status = StatusAndMessage(Status.ABORTING)
162
+ stopping_thread = threading.Thread(target=self.stopping_thread)
163
+ stopping_thread.start()
164
+ self.last_run_aborted = True
165
+ return StatusAndMessage(Status.ABORTING)
166
+
167
+ def shutdown(self):
168
+ """Stops the run engine and the loop waiting for messages."""
169
+ print("Shutting down: Stopping the run engine gracefully")
170
+ self.stop()
171
+ self.command_queue.put(Command(action=Actions.SHUTDOWN))
172
+
173
+ def wait_on_queue(self):
174
+ while True:
175
+ command = self.command_queue.get()
176
+ if command.action == Actions.SHUTDOWN:
177
+ return
178
+ elif command.action == Actions.START:
179
+ if command.experiment is None:
180
+ raise ValueError("No experiment provided for START")
181
+ try:
182
+ if (
183
+ not self.use_external_callbacks
184
+ and command.callbacks
185
+ and (cbs := command.callbacks())
186
+ ):
187
+ LOGGER.info(
188
+ f"Using callbacks for this plan: {not self.use_external_callbacks} - {cbs}"
189
+ )
190
+ self.subscribed_per_plan_callbacks += [
191
+ self.RE.subscribe(cb) for cb in cbs
192
+ ]
193
+ with TRACER.start_span("do_run"):
194
+ self.RE(command.experiment(command.devices, command.parameters))
195
+
196
+ self.current_status = StatusAndMessage(
197
+ Status.IDLE,
198
+ self.aperture_change_callback.last_selected_aperture,
199
+ )
200
+
201
+ self.last_run_aborted = False
202
+ except WarningException as exception:
203
+ LOGGER.warning("Warning Exception", exc_info=True)
204
+ self.current_status = make_error_status_and_message(exception)
205
+ except Exception as exception:
206
+ LOGGER.error("Exception on running plan", exc_info=True)
207
+
208
+ if self.last_run_aborted:
209
+ # Aborting will cause an exception here that we want to swallow
210
+ self.last_run_aborted = False
211
+ else:
212
+ self.current_status = make_error_status_and_message(exception)
213
+ finally:
214
+ [
215
+ self.RE.unsubscribe(cb)
216
+ for cb in self.subscribed_per_plan_callbacks
217
+ ]
218
+
219
+
220
+ def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions):
221
+ experiment_registry_entry = PLAN_REGISTRY.get(plan_name)
222
+ if experiment_registry_entry is None:
223
+ raise PlanNotFound(f"Experiment plan '{plan_name}' not found in registry.")
224
+
225
+ experiment_internal_param_type = experiment_registry_entry.get("param_type")
226
+ callback_type = experiment_registry_entry.get("callback_collection_type")
227
+ plan = context.plan_functions.get(plan_name)
228
+ if experiment_internal_param_type is None:
229
+ raise PlanNotFound(
230
+ f"Corresponding internal param type for '{plan_name}' not found in registry."
231
+ )
232
+ if plan is None:
233
+ raise PlanNotFound(
234
+ f"Experiment plan '{plan_name}' not found in context. Context has {context.plan_functions.keys()}"
235
+ )
236
+ try:
237
+ parameters = experiment_internal_param_type(**json.loads(request.data))
238
+ if parameters.model_extra:
239
+ raise ValueError(f"Extra fields not allowed {parameters.model_extra}")
240
+ except Exception as e:
241
+ raise ValueError(
242
+ f"Supplied parameters don't match the plan for this endpoint {request.data}"
243
+ ) from e
244
+ return plan, parameters, plan_name, callback_type
245
+
246
+
247
+ class RunExperiment(Resource):
248
+ def __init__(self, runner: BlueskyRunner, context: BlueskyContext) -> None:
249
+ super().__init__()
250
+ self.runner = runner
251
+ self.context = context
252
+
253
+ def put(self, plan_name: str, action: Actions):
254
+ status_and_message = StatusAndMessage(Status.FAILED, f"{action} not understood")
255
+ if action == Actions.START.value:
256
+ try:
257
+ plan, params, plan_name, callback_type = compose_start_args(
258
+ self.context, plan_name, action
259
+ )
260
+ status_and_message = self.runner.start(
261
+ plan, params, plan_name, callback_type
262
+ )
263
+ except Exception as e:
264
+ status_and_message = make_error_status_and_message(e)
265
+ LOGGER.error(format_exception(e))
266
+
267
+ elif action == Actions.STOP.value:
268
+ status_and_message = self.runner.stop()
269
+ # no idea why mypy gives an attribute error here but nowhere else for this
270
+ # exact same situation...
271
+ return asdict(status_and_message) # type: ignore
272
+
273
+
274
+ class StopOrStatus(Resource):
275
+ def __init__(self, runner: BlueskyRunner) -> None:
276
+ super().__init__()
277
+ self.runner: BlueskyRunner = runner
278
+
279
+ def put(self, action):
280
+ status_and_message = StatusAndMessage(Status.FAILED, f"{action} not understood")
281
+ if action == Actions.STOP.value:
282
+ status_and_message = self.runner.stop()
283
+ return asdict(status_and_message)
284
+
285
+ def get(self, **kwargs):
286
+ action = kwargs.get("action")
287
+ status_and_message = StatusAndMessage(Status.FAILED, f"{action} not understood")
288
+ if action == Actions.STATUS.value:
289
+ LOGGER.debug(
290
+ f"Runner received status request - state of the runner object is: {self.runner.__dict__} - state of the RE is: {self.runner.RE.__dict__}"
291
+ )
292
+ status_and_message = self.runner.current_status
293
+ return asdict(status_and_message)
294
+
295
+
296
+ class FlushLogs(Resource):
297
+ def put(self, **kwargs):
298
+ try:
299
+ status_and_message = StatusAndMessage(
300
+ Status.SUCCESS, f"Flushed debug log to {flush_debug_handler()}"
301
+ )
302
+ except Exception as e:
303
+ status_and_message = StatusAndMessage(
304
+ Status.FAILED, f"Failed to flush debug log: {e}"
305
+ )
306
+ return asdict(status_and_message)
307
+
308
+
309
+ def create_app(
310
+ test_config=None,
311
+ RE: RunEngine = RunEngine({}),
312
+ skip_startup_connection: bool = False,
313
+ use_external_callbacks: bool = False,
314
+ ) -> tuple[Flask, BlueskyRunner]:
315
+ context = setup_context(
316
+ wait_for_connection=not skip_startup_connection,
317
+ )
318
+ runner = BlueskyRunner(
319
+ RE,
320
+ context=context,
321
+ use_external_callbacks=use_external_callbacks,
322
+ skip_startup_connection=skip_startup_connection,
323
+ )
324
+ app = Flask(__name__)
325
+ if test_config:
326
+ app.config.update(test_config)
327
+ api = Api(app)
328
+ api.add_resource(
329
+ RunExperiment,
330
+ "/<string:plan_name>/<string:action>",
331
+ resource_class_args=[runner, context],
332
+ )
333
+ api.add_resource(
334
+ FlushLogs,
335
+ "/flush_debug_log",
336
+ )
337
+ api.add_resource(
338
+ StopOrStatus,
339
+ "/<string:action>",
340
+ resource_class_args=[runner],
341
+ )
342
+ return app, runner
343
+
344
+
345
+ def create_targets():
346
+ hyperion_port = 5005
347
+ args = parse_cli_args()
348
+ do_default_logging_setup(dev_mode=args.dev_mode)
349
+ if not args.use_external_callbacks:
350
+ setup_callback_logging(args.dev_mode)
351
+ app, runner = create_app(
352
+ skip_startup_connection=args.skip_startup_connection,
353
+ use_external_callbacks=args.use_external_callbacks,
354
+ )
355
+ return app, runner, hyperion_port, args.dev_mode
356
+
357
+
358
+ def main():
359
+ app, runner, port, dev_mode = create_targets()
360
+ atexit.register(runner.shutdown)
361
+ flask_thread = threading.Thread(
362
+ target=lambda: app.run(
363
+ host="0.0.0.0", port=port, debug=True, use_reloader=False
364
+ ),
365
+ daemon=True,
366
+ )
367
+ flask_thread.start()
368
+ LOGGER.info(f"Hyperion now listening on {port} ({'IN DEV' if dev_mode else ''})")
369
+ runner.wait_on_queue()
370
+ flask_thread.join()
371
+
372
+
373
+ if __name__ == "__main__":
374
+ main()
File without changes
@@ -0,0 +1,134 @@
1
+ import json
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from dodal.devices.focusing_mirror import (
5
+ FocusingMirrorWithStripes,
6
+ MirrorStripe,
7
+ VFMMirrorVoltages,
8
+ )
9
+ from dodal.devices.undulator_dcm import UndulatorDCM
10
+ from dodal.devices.util.adjuster_plans import lookup_table_adjuster
11
+ from dodal.devices.util.lookup_tables import (
12
+ linear_interpolation_lut,
13
+ )
14
+
15
+ from mx_bluesky.hyperion.log import LOGGER
16
+
17
+ MIRROR_VOLTAGE_GROUP = "MIRROR_VOLTAGE_GROUP"
18
+ DCM_GROUP = "DCM_GROUP"
19
+
20
+
21
+ def _apply_and_wait_for_voltages_to_settle(
22
+ stripe: MirrorStripe,
23
+ mirror: FocusingMirrorWithStripes,
24
+ mirror_voltages: VFMMirrorVoltages,
25
+ ):
26
+ with open(mirror_voltages.voltage_lookup_table_path) as lut_file:
27
+ json_obj = json.load(lut_file)
28
+
29
+ # sample mode is the only mode supported
30
+ sample_data = json_obj["sample"]
31
+ mirror_key = mirror.name.lower()
32
+ if stripe == MirrorStripe.BARE:
33
+ stripe_key = "bare"
34
+ elif stripe == MirrorStripe.RHODIUM:
35
+ stripe_key = "rh"
36
+ elif stripe == MirrorStripe.PLATINUM:
37
+ stripe_key = "pt"
38
+ else:
39
+ raise ValueError(f"Unsupported stripe '{stripe}'")
40
+
41
+ required_voltages = sample_data[stripe_key][mirror_key]
42
+ for voltage_channel, required_voltage in zip(
43
+ mirror_voltages.voltage_channels.values(), required_voltages, strict=False
44
+ ):
45
+ LOGGER.debug(
46
+ f"Applying and waiting for voltage {voltage_channel.name} = {required_voltage}"
47
+ )
48
+ yield from bps.abs_set(
49
+ voltage_channel, required_voltage, group=MIRROR_VOLTAGE_GROUP
50
+ )
51
+
52
+ yield from bps.wait(group=MIRROR_VOLTAGE_GROUP)
53
+
54
+
55
+ def adjust_mirror_stripe(
56
+ energy_kev, mirror: FocusingMirrorWithStripes, mirror_voltages: VFMMirrorVoltages
57
+ ):
58
+ """Feedback should be OFF prior to entry, in order to prevent
59
+ feedback from making unnecessary corrections while beam is being adjusted."""
60
+ stripe = mirror.energy_to_stripe(energy_kev)
61
+
62
+ LOGGER.info(
63
+ f"Adjusting mirror stripe for {energy_kev}keV selecting {stripe} stripe"
64
+ )
65
+ yield from bps.abs_set(mirror.stripe, stripe, wait=True)
66
+ yield from bps.trigger(mirror.apply_stripe)
67
+
68
+ LOGGER.info("Adjusting mirror voltages...")
69
+ yield from _apply_and_wait_for_voltages_to_settle(stripe, mirror, mirror_voltages)
70
+
71
+
72
+ def adjust_dcm_pitch_roll_vfm_from_lut(
73
+ undulator_dcm: UndulatorDCM,
74
+ vfm: FocusingMirrorWithStripes,
75
+ vfm_mirror_voltages: VFMMirrorVoltages,
76
+ energy_kev,
77
+ ):
78
+ """Beamline energy-change post-adjustments : Adjust DCM and VFM directly from lookup tables.
79
+ Lookups are performed against the Bragg angle which will have been automatically set by EPICS as a side-effect of the
80
+ energy change prior to calling this function.
81
+ Feedback should be OFF prior to entry, in order to prevent
82
+ feedback from making unnecessary corrections while beam is being adjusted."""
83
+
84
+ # DCM Pitch
85
+ dcm = undulator_dcm.dcm
86
+ LOGGER.info(f"Adjusting DCM and VFM for {energy_kev} keV")
87
+ bragg_deg = yield from bps.rd(dcm.bragg_in_degrees.user_readback)
88
+ LOGGER.info(f"Read Bragg angle = {bragg_deg} degrees")
89
+ dcm_pitch_adjuster = lookup_table_adjuster(
90
+ linear_interpolation_lut(undulator_dcm.pitch_energy_table_path),
91
+ dcm.pitch_in_mrad,
92
+ bragg_deg,
93
+ )
94
+ yield from dcm_pitch_adjuster(DCM_GROUP)
95
+ # It's possible we can remove these waits but we need to check
96
+ LOGGER.info("Waiting for DCM pitch adjust to complete...")
97
+
98
+ # DCM Roll
99
+ dcm_roll_adjuster = lookup_table_adjuster(
100
+ linear_interpolation_lut(undulator_dcm.roll_energy_table_path),
101
+ dcm.roll_in_mrad,
102
+ bragg_deg,
103
+ )
104
+ yield from dcm_roll_adjuster(DCM_GROUP)
105
+ LOGGER.info("Waiting for DCM roll adjust to complete...")
106
+
107
+ # DCM Perp pitch
108
+ offset_mm = undulator_dcm.dcm_fixed_offset_mm
109
+ LOGGER.info(f"Adjusting DCM offset to {offset_mm} mm")
110
+ yield from bps.abs_set(dcm.offset_in_mm, offset_mm, group=DCM_GROUP)
111
+
112
+ #
113
+ # Adjust mirrors
114
+ #
115
+
116
+ # No need to change HFM
117
+
118
+ # Assumption is focus mode is already set to "sample"
119
+ # not sure how we check this
120
+
121
+ # VFM Stripe selection
122
+ yield from adjust_mirror_stripe(energy_kev, vfm, vfm_mirror_voltages)
123
+ yield from bps.wait(DCM_GROUP)
124
+
125
+ # VFM Adjust - for I03 this table always returns the same value
126
+ vfm_lut = vfm.bragg_to_lat_lookup_table_path
127
+ assert vfm_lut is not None
128
+ vfm_x_adjuster = lookup_table_adjuster(
129
+ linear_interpolation_lut(vfm_lut),
130
+ vfm.x_mm,
131
+ bragg_deg,
132
+ )
133
+ LOGGER.info("Waiting for VFM Lat (Horizontal Translation) to complete...")
134
+ yield from vfm_x_adjuster()
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from dodal.devices.aperturescatterguard import (
5
+ ApertureScatterguard,
6
+ ApertureValue,
7
+ )
8
+ from dodal.devices.backlight import Backlight, BacklightPosition
9
+ from dodal.devices.detector.detector_motion import DetectorMotion
10
+ from dodal.devices.smargon import Smargon
11
+
12
+ from mx_bluesky.hyperion.log import LOGGER
13
+
14
+ LOWER_DETECTOR_SHUTTER_AFTER_SCAN = True
15
+
16
+
17
+ def setup_sample_environment(
18
+ aperture_scatterguard: ApertureScatterguard,
19
+ aperture_position_gda_name: str | None,
20
+ backlight: Backlight,
21
+ group="setup_senv",
22
+ ):
23
+ """Move the aperture into required position, move out the backlight."""
24
+ aperture_value = (
25
+ None
26
+ if not aperture_position_gda_name
27
+ else ApertureValue(aperture_position_gda_name)
28
+ )
29
+ yield from move_aperture_if_required(
30
+ aperture_scatterguard, aperture_value, group=group
31
+ )
32
+ yield from bps.abs_set(backlight, BacklightPosition.OUT, group=group)
33
+
34
+
35
+ def move_aperture_if_required(
36
+ aperture_scatterguard: ApertureScatterguard,
37
+ aperture_value: ApertureValue | None,
38
+ group="move_aperture",
39
+ ):
40
+ if not aperture_value:
41
+ previous_aperture_position = yield from bps.rd(aperture_scatterguard)
42
+ assert isinstance(previous_aperture_position, ApertureValue)
43
+ LOGGER.info(
44
+ f"Using previously set aperture position {previous_aperture_position}"
45
+ )
46
+
47
+ else:
48
+ LOGGER.info(f"Setting aperture position to {aperture_value}")
49
+ yield from bps.abs_set(
50
+ aperture_scatterguard,
51
+ aperture_value,
52
+ group=group,
53
+ )
54
+
55
+
56
+ def cleanup_sample_environment(
57
+ detector_motion: DetectorMotion,
58
+ group="cleanup_senv",
59
+ ):
60
+ """Put the detector shutter back down"""
61
+
62
+ yield from bps.abs_set(
63
+ detector_motion.shutter,
64
+ int(not LOWER_DETECTOR_SHUTTER_AFTER_SCAN),
65
+ group=group,
66
+ )
67
+
68
+
69
+ def move_x_y_z(
70
+ smargon: Smargon,
71
+ x_mm: float | None = None,
72
+ y_mm: float | None = None,
73
+ z_mm: float | None = None,
74
+ wait=False,
75
+ group="move_x_y_z",
76
+ ):
77
+ """Move the x, y, and z axes of the given smargon to the specified position. All
78
+ axes are optional."""
79
+
80
+ LOGGER.info(f"Moving smargon to x, y, z: {(x_mm, y_mm, z_mm)}")
81
+ if x_mm:
82
+ yield from bps.abs_set(smargon.x, x_mm, group=group)
83
+ if y_mm:
84
+ yield from bps.abs_set(smargon.y, y_mm, group=group)
85
+ if z_mm:
86
+ yield from bps.abs_set(smargon.z, z_mm, group=group)
87
+ if wait:
88
+ yield from bps.wait(group)
89
+
90
+
91
+ def move_phi_chi_omega(
92
+ smargon: Smargon,
93
+ phi: float | None = None,
94
+ chi: float | None = None,
95
+ omega: float | None = None,
96
+ wait=False,
97
+ group="move_phi_chi_omega",
98
+ ):
99
+ """Move the x, y, and z axes of the given smargon to the specified position. All
100
+ axes are optional."""
101
+
102
+ LOGGER.info(f"Moving smargon to phi, chi, omega: {(phi, chi, omega)}")
103
+ if phi:
104
+ yield from bps.abs_set(smargon.phi, phi, group=group)
105
+ if chi:
106
+ yield from bps.abs_set(smargon.chi, chi, group=group)
107
+ if omega:
108
+ yield from bps.abs_set(smargon.omega, omega, group=group)
109
+ if wait:
110
+ yield from bps.wait(group)
@@ -0,0 +1,16 @@
1
+ from bluesky import plan_stubs as bps
2
+ from dodal.devices.detector.detector_motion import DetectorMotion, ShutterState
3
+
4
+ from mx_bluesky.hyperion.log import LOGGER
5
+
6
+
7
+ def set_detector_z_position(
8
+ detector_motion: DetectorMotion, detector_position: float, group=None
9
+ ):
10
+ LOGGER.info(f"Moving detector to {detector_position} ({group})")
11
+ yield from bps.abs_set(detector_motion.z, detector_position, group=group)
12
+
13
+
14
+ def set_shutter(detector_motion: DetectorMotion, state: ShutterState, group=None):
15
+ LOGGER.info(f"Setting shutter to {state} ({group})")
16
+ yield from bps.abs_set(detector_motion.shutter, state, group=group)
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
5
+ from dodal.devices.attenuator import Attenuator
6
+ from dodal.devices.dcm import DCM
7
+ from dodal.devices.eiger import EigerDetector
8
+ from dodal.devices.flux import Flux
9
+ from dodal.devices.robot import BartRobot
10
+ from dodal.devices.s4_slit_gaps import S4SlitGaps
11
+ from dodal.devices.smargon import Smargon
12
+ from dodal.devices.synchrotron import Synchrotron
13
+ from dodal.devices.undulator import Undulator
14
+
15
+ from mx_bluesky.hyperion.log import LOGGER
16
+ from mx_bluesky.hyperion.parameters.constants import CONST
17
+
18
+
19
+ def read_hardware_pre_collection(
20
+ undulator: Undulator,
21
+ synchrotron: Synchrotron,
22
+ s4_slit_gaps: S4SlitGaps,
23
+ robot: BartRobot,
24
+ smargon: Smargon,
25
+ ):
26
+ LOGGER.info("Reading status of beamline for callbacks, pre collection.")
27
+ yield from bps.create(
28
+ name=CONST.DESCRIPTORS.HARDWARE_READ_PRE
29
+ ) # gives name to event *descriptor* document
30
+ yield from bps.read(undulator.current_gap)
31
+ yield from bps.read(synchrotron.synchrotron_mode)
32
+ yield from bps.read(s4_slit_gaps.xgap)
33
+ yield from bps.read(s4_slit_gaps.ygap)
34
+ yield from bps.read(smargon.x)
35
+ yield from bps.read(smargon.y)
36
+ yield from bps.read(smargon.z)
37
+ yield from bps.save()
38
+
39
+
40
+ def read_hardware_during_collection(
41
+ aperture_scatterguard: ApertureScatterguard,
42
+ attenuator: Attenuator,
43
+ flux: Flux,
44
+ dcm: DCM,
45
+ detector: EigerDetector,
46
+ ):
47
+ LOGGER.info("Reading status of beamline for callbacks, during collection.")
48
+ yield from bps.create(name=CONST.DESCRIPTORS.HARDWARE_READ_DURING)
49
+ yield from bps.read(aperture_scatterguard)
50
+ yield from bps.read(attenuator.actual_transmission)
51
+ yield from bps.read(flux.flux_reading)
52
+ yield from bps.read(dcm.energy_in_kev)
53
+ yield from bps.read(detector.bit_depth)
54
+ yield from bps.save()
55
+
56
+
57
+ def read_hardware_for_zocalo(detector: EigerDetector):
58
+ yield from bps.create(name=CONST.DESCRIPTORS.ZOCALO_HW_READ)
59
+ yield from bps.read(detector.odin.file_writer.id)
60
+ yield from bps.save()