mx-bluesky 1.2.0__py3-none-any.whl → 1.4.1a0__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 (94) hide show
  1. mx_bluesky/__init__.py +8 -3
  2. mx_bluesky/__main__.py +12 -7
  3. mx_bluesky/_version.py +2 -2
  4. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +14 -4
  5. mx_bluesky/beamlines/i04/thawing_plan.py +49 -11
  6. mx_bluesky/beamlines/i24/serial/__init__.py +3 -0
  7. mx_bluesky/beamlines/i24/serial/dcid.py +19 -21
  8. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +69 -91
  9. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +2 -5
  10. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +111 -143
  12. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +141 -222
  13. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +7 -216
  14. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +18 -17
  15. mx_bluesky/beamlines/i24/serial/log.py +58 -49
  16. mx_bluesky/beamlines/i24/serial/parameters/constants.py +0 -1
  17. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +3 -3
  18. mx_bluesky/beamlines/i24/serial/run_extruder.sh +30 -5
  19. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +30 -5
  20. mx_bluesky/beamlines/i24/serial/run_serial.py +24 -8
  21. mx_bluesky/beamlines/i24/serial/setup_beamline/ca.py +0 -2
  22. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +79 -81
  23. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +9 -20
  24. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +26 -28
  25. mx_bluesky/beamlines/i24/serial/write_nexus.py +11 -11
  26. mx_bluesky/common/__init__.py +0 -0
  27. mx_bluesky/common/device_setup_plans/read_hardware_for_setup.py +14 -0
  28. mx_bluesky/common/external_interaction/config_server.py +46 -0
  29. mx_bluesky/common/parameters/components.py +258 -0
  30. mx_bluesky/common/parameters/constants.py +138 -0
  31. mx_bluesky/common/parameters/gridscan.py +94 -0
  32. mx_bluesky/common/parameters/robot_load.py +16 -0
  33. mx_bluesky/common/plans/__init__.py +1 -0
  34. mx_bluesky/common/plans/do_fgs.py +121 -0
  35. mx_bluesky/common/utils/log.py +118 -0
  36. mx_bluesky/{hyperion → common/utils}/tracing.py +2 -2
  37. mx_bluesky/hyperion/__main__.py +13 -10
  38. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +31 -26
  39. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +6 -12
  40. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +6 -12
  41. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -6
  42. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +49 -18
  43. mx_bluesky/hyperion/device_setup_plans/smargon.py +6 -6
  44. mx_bluesky/hyperion/device_setup_plans/utils.py +2 -2
  45. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +4 -4
  46. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  47. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  48. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  49. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  50. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +145 -161
  51. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +56 -22
  52. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +52 -10
  53. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +21 -20
  54. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +11 -14
  55. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  56. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +40 -21
  57. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +19 -19
  58. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +21 -21
  59. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +51 -13
  60. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +24 -7
  61. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +5 -6
  62. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +1 -2
  63. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  64. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  65. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +30 -25
  66. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +29 -12
  67. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +1 -1
  68. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +1 -1
  69. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +7 -4
  70. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  71. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +28 -20
  72. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  73. mx_bluesky/hyperion/external_interaction/config_server.py +11 -28
  74. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +1 -1
  75. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +1 -1
  76. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +2 -2
  77. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +1 -1
  78. mx_bluesky/hyperion/log.py +0 -84
  79. mx_bluesky/hyperion/parameters/components.py +4 -251
  80. mx_bluesky/hyperion/parameters/constants.py +22 -119
  81. mx_bluesky/hyperion/parameters/gridscan.py +35 -74
  82. mx_bluesky/hyperion/parameters/load_centre_collect.py +16 -11
  83. mx_bluesky/hyperion/parameters/rotation.py +23 -10
  84. mx_bluesky/hyperion/utils/utils.py +17 -0
  85. mx_bluesky/hyperion/utils/validation.py +5 -6
  86. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/METADATA +36 -33
  87. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/RECORD +91 -81
  88. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/WHEEL +1 -1
  89. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -161
  90. mx_bluesky/example.py +0 -19
  91. mx_bluesky/hyperion/parameters/robot_load.py +0 -16
  92. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/LICENSE +0 -0
  93. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/entry_points.txt +0 -0
  94. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/top_level.txt +0 -0
@@ -9,8 +9,6 @@ the edm screen, while on the schematics they are 0 indexed. Thus, `Soft In 1` fr
9
9
  schematics corresponds to soft_in_2 in the code.
10
10
  """
11
11
 
12
- import logging
13
-
14
12
  import bluesky.plan_stubs as bps
15
13
  from dodal.devices.zebra import (
16
14
  AND3,
@@ -35,6 +33,8 @@ from dodal.devices.zebra import (
35
33
  Zebra,
36
34
  )
37
35
 
36
+ from mx_bluesky.beamlines.i24.serial.log import SSX_LOGGER
37
+
38
38
  # Detector specific outs
39
39
  TTL_EIGER = 1
40
40
  TTL_PILATUS = 2
@@ -48,8 +48,6 @@ SHUTTER_MODE = {
48
48
  GATE_START = 1.0
49
49
  SHUTTER_OPEN_TIME = 0.05 # For pp with long delays
50
50
 
51
- logger = logging.getLogger("I24ssx.setup_zebra")
52
-
53
51
 
54
52
  def get_zebra_settings_for_extruder(
55
53
  exp_time: float,
@@ -70,29 +68,29 @@ def get_zebra_settings_for_extruder(
70
68
 
71
69
 
72
70
  def arm_zebra(zebra: Zebra):
73
- yield from bps.abs_set(zebra.pc.arm, ArmDemand.ARM, wait=True)
74
- logger.info("Zebra armed.")
71
+ yield from bps.abs_set(zebra.pc.arm, ArmDemand.ARM, wait=True) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
72
+ SSX_LOGGER.info("Zebra armed.")
75
73
 
76
74
 
77
75
  def disarm_zebra(zebra: Zebra):
78
- yield from bps.abs_set(zebra.pc.arm, ArmDemand.DISARM, wait=True)
79
- logger.info("Zebra disarmed.")
76
+ yield from bps.abs_set(zebra.pc.arm, ArmDemand.DISARM, wait=True) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
77
+ SSX_LOGGER.info("Zebra disarmed.")
80
78
 
81
79
 
82
80
  def open_fast_shutter(zebra: Zebra):
83
81
  yield from bps.abs_set(zebra.inputs.soft_in_2, SoftInState.YES, wait=True)
84
- logger.info("Fast shutter open.")
82
+ SSX_LOGGER.info("Fast shutter open.")
85
83
 
86
84
 
87
85
  def close_fast_shutter(zebra: Zebra):
88
86
  yield from bps.abs_set(zebra.inputs.soft_in_2, SoftInState.NO, wait=True)
89
- logger.info("Fast shutter closed.")
87
+ SSX_LOGGER.info("Fast shutter closed.")
90
88
 
91
89
 
92
90
  def set_shutter_mode(zebra: Zebra, mode: str):
93
91
  # SOFT_IN:B0 has to be disabled for manual mode
94
92
  yield from bps.abs_set(zebra.inputs.soft_in_1, SHUTTER_MODE[mode], wait=True)
95
- logger.info(f"Shutter mode set to {mode}.")
93
+ SSX_LOGGER.info(f"Shutter mode set to {mode}.")
96
94
 
97
95
 
98
96
  def setup_pc_sources(
@@ -124,12 +122,12 @@ def setup_zebra_for_quickshot_plan(
124
122
  exp_time (float): Collection exposure time, in s.
125
123
  num_images (float): Number of images to be collected.
126
124
  """
127
- logger.info("Setup ZEBRA for quickshot collection.")
125
+ SSX_LOGGER.info("Setup ZEBRA for quickshot collection.")
128
126
  yield from bps.abs_set(zebra.pc.arm_source, ArmSource.SOFT, group=group)
129
127
  yield from setup_pc_sources(zebra, TrigSource.TIME, TrigSource.EXTERNAL)
130
128
 
131
129
  gate_width = exp_time * num_images + 0.5
132
- logger.info(f"Gate start set to {GATE_START}, with width {gate_width}.")
130
+ SSX_LOGGER.info(f"Gate start set to {GATE_START}, with width {gate_width}.")
133
131
  yield from bps.abs_set(zebra.pc.gate_start, GATE_START, group=group)
134
132
  yield from bps.abs_set(zebra.pc.gate_width, gate_width, group=group)
135
133
 
@@ -138,7 +136,7 @@ def setup_zebra_for_quickshot_plan(
138
136
 
139
137
  if wait:
140
138
  yield from bps.wait(group)
141
- logger.info("Finished setting up zebra.")
139
+ SSX_LOGGER.info("Finished setting up zebra.")
142
140
 
143
141
 
144
142
  def set_logic_gates_for_porto_triggering(
@@ -204,7 +202,7 @@ def setup_zebra_for_extruder_with_pump_probe_plan(
204
202
  pulse1_delay (float, optional): Delay to start pulse1 (the laser control) after \
205
203
  gate start. Defaults to 0.0.
206
204
  """
207
- logger.info("Setup ZEBRA for pump probe extruder collection.")
205
+ SSX_LOGGER.info("Setup ZEBRA for pump probe extruder collection.")
208
206
 
209
207
  yield from set_shutter_mode(zebra, "manual")
210
208
 
@@ -226,7 +224,7 @@ def setup_zebra_for_extruder_with_pump_probe_plan(
226
224
  gate_width, gate_step = get_zebra_settings_for_extruder(
227
225
  exp_time, pump_exp, pump_delay
228
226
  )
229
- logger.info(
227
+ SSX_LOGGER.info(
230
228
  f"""
231
229
  Gate start set to {GATE_START}, with calculated width {gate_width}
232
230
  and step {gate_step}.
@@ -241,13 +239,13 @@ def setup_zebra_for_extruder_with_pump_probe_plan(
241
239
  # Settings for extruder pump probe:
242
240
  # PULSE1_DLY is the start (0 usually), PULSE1_WID is the laser dwell set on edm
243
241
  # PULSE2_DLY is the laser delay set on edm, PULSE2_WID is the exposure time
244
- logger.info(
242
+ SSX_LOGGER.info(
245
243
  f"Pulse1 starting at {pulse1_delay} with width set to laser dwell {pump_exp}."
246
244
  )
247
245
  yield from bps.abs_set(zebra.output.pulse_1.input, PC_GATE, group=group)
248
246
  yield from bps.abs_set(zebra.output.pulse_1.delay, pulse1_delay, group=group)
249
247
  yield from bps.abs_set(zebra.output.pulse_1.width, pump_exp, group=group)
250
- logger.info(
248
+ SSX_LOGGER.info(
251
249
  f"""
252
250
  Pulse2 starting at laser delay {pump_delay} with width set to \
253
251
  exposure time {exp_time}.
@@ -259,7 +257,7 @@ def setup_zebra_for_extruder_with_pump_probe_plan(
259
257
 
260
258
  if wait:
261
259
  yield from bps.wait(group)
262
- logger.info("Finished setting up zebra.")
260
+ SSX_LOGGER.info("Finished setting up zebra.")
263
261
 
264
262
 
265
263
  def setup_zebra_for_fastchip_plan(
@@ -302,7 +300,7 @@ def setup_zebra_for_fastchip_plan(
302
300
  start_time_offset (float): Delay on the start of the position compare. \
303
301
  Defaults to 0.0 (standard chip collection).
304
302
  """
305
- logger.info("Setup ZEBRA for a fixed target collection.")
303
+ SSX_LOGGER.info("Setup ZEBRA for a fixed target collection.")
306
304
 
307
305
  yield from set_shutter_mode(zebra, "manual")
308
306
 
@@ -341,7 +339,7 @@ def setup_zebra_for_fastchip_plan(
341
339
 
342
340
  if wait:
343
341
  yield from bps.wait(group)
344
- logger.info("Finished setting up zebra.")
342
+ SSX_LOGGER.info("Finished setting up zebra.")
345
343
 
346
344
 
347
345
  def open_fast_shutter_at_each_position_plan(
@@ -371,10 +369,10 @@ def open_fast_shutter_at_each_position_plan(
371
369
  num_exposures (int): Number of times data is collected in each aperture.
372
370
  exposure_time (float): Exposure time for each shot.
373
371
  """
374
- logger.info(
372
+ SSX_LOGGER.info(
375
373
  "ZEBRA setup for fastchip collection with long delays between exposures."
376
374
  )
377
- logger.debug("Controlling the fast shutter on PULSE2.")
375
+ SSX_LOGGER.debug("Controlling the fast shutter on PULSE2.")
378
376
  # Output panel pulse_2 settings
379
377
  yield from bps.abs_set(zebra.output.pulse_2.input, PC_GATE, group=group)
380
378
  yield from bps.abs_set(zebra.output.pulse_2.delay, 0.0, group=group)
@@ -386,7 +384,7 @@ def open_fast_shutter_at_each_position_plan(
386
384
 
387
385
  if wait:
388
386
  yield from bps.wait(group=group)
389
- logger.debug("Finished setting up for long delays.")
387
+ SSX_LOGGER.debug("Finished setting up for long delays.")
390
388
 
391
389
 
392
390
  def reset_pc_gate_and_pulse(zebra: Zebra, group: str = "reset_pc"):
@@ -444,16 +442,16 @@ def zebra_return_to_normal_plan(
444
442
 
445
443
  if wait:
446
444
  yield from bps.wait(group)
447
- logger.info("Zebra settings back to normal.")
445
+ SSX_LOGGER.info("Zebra settings back to normal.")
448
446
 
449
447
 
450
448
  def reset_zebra_when_collection_done_plan(zebra: Zebra):
451
449
  """
452
450
  End of collection zebra operations: close fast shutter, disarm and reset settings.
453
451
  """
454
- logger.debug("Close the fast shutter.")
452
+ SSX_LOGGER.debug("Close the fast shutter.")
455
453
  yield from close_fast_shutter(zebra)
456
- logger.debug("Disarm the zebra.")
454
+ SSX_LOGGER.debug("Disarm the zebra.")
457
455
  yield from disarm_zebra(zebra)
458
- logger.debug("Set zebra back to normal.")
456
+ SSX_LOGGER.debug("Set zebra back to normal.")
459
457
  yield from zebra_return_to_normal_plan(zebra, wait=True)
@@ -1,4 +1,3 @@
1
- import logging
2
1
  import os
3
2
  import pathlib
4
3
  import pprint
@@ -9,14 +8,13 @@ from typing import Literal
9
8
  import requests
10
9
 
11
10
  from mx_bluesky.beamlines.i24.serial.fixed_target.ft_utils import ChipType, MappingType
11
+ from mx_bluesky.beamlines.i24.serial.log import SSX_LOGGER
12
12
  from mx_bluesky.beamlines.i24.serial.parameters import (
13
13
  ExtruderParameters,
14
14
  FixedTargetParameters,
15
15
  )
16
16
  from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, caget, cagetstring
17
17
 
18
- logger = logging.getLogger("I24ssx.nexus_writer")
19
-
20
18
 
21
19
  def call_nexgen(
22
20
  chip_prog_dict: dict | None,
@@ -54,23 +52,23 @@ def call_nexgen(
54
52
  )
55
53
  t0 = time.time()
56
54
  max_wait = 60 # seconds
57
- logger.info(f"Watching for {meta_h5}")
55
+ SSX_LOGGER.info(f"Watching for {meta_h5}")
58
56
  while time.time() - t0 < max_wait:
59
57
  if meta_h5.exists():
60
- logger.info(f"Found {meta_h5} after {time.time() - t0:.1f} seconds")
58
+ SSX_LOGGER.info(f"Found {meta_h5} after {time.time() - t0:.1f} seconds")
61
59
  time.sleep(5)
62
60
  break
63
- logger.debug(f"Waiting for {meta_h5}")
61
+ SSX_LOGGER.debug(f"Waiting for {meta_h5}")
64
62
  time.sleep(1)
65
63
  if not meta_h5.exists():
66
- logger.warning(f"Giving up waiting for {meta_h5} after {max_wait} seconds")
64
+ SSX_LOGGER.warning(f"Giving up waiting for {meta_h5} after {max_wait} seconds")
67
65
  return False
68
66
 
69
67
  transmission = (float(caget(Eiger.pv.transmission)),)
70
68
 
71
69
  if det_type == Eiger.name:
72
70
  bit_depth = int(caget(Eiger.pv.bit_depth))
73
- logger.debug(
71
+ SSX_LOGGER.debug(
74
72
  f"Call to nexgen server with the following chip definition: \n{chip_prog_dict}"
75
73
  )
76
74
 
@@ -96,10 +94,12 @@ def call_nexgen(
96
94
  "wavelength": wavelength,
97
95
  "bit_depth": bit_depth,
98
96
  }
99
- logger.info(f"Sending POST request to {url} with payload:")
100
- logger.info(pprint.pformat(payload))
97
+ SSX_LOGGER.info(f"Sending POST request to {url} with payload:")
98
+ SSX_LOGGER.info(pprint.pformat(payload))
101
99
  response = requests.post(url, headers=headers, json=payload)
102
- logger.info(f"Response: {response.text} (status code: {response.status_code})")
100
+ SSX_LOGGER.info(
101
+ f"Response: {response.text} (status code: {response.status_code})"
102
+ )
103
103
  # the following will raise an error if the request was unsuccessful
104
104
  return response.status_code == requests.codes.ok
105
105
  return False
File without changes
@@ -0,0 +1,14 @@
1
+ import bluesky.plan_stubs as bps
2
+ from dodal.devices.eiger import EigerDetector
3
+
4
+ from mx_bluesky.common.parameters.constants import DocDescriptorNames
5
+
6
+
7
+ def read_hardware_for_zocalo(detector: EigerDetector):
8
+ """ "
9
+ If the RunEngine is subscribed to the ZocaloCallback, this plan will also trigger zocalo.
10
+ A bluesky run must be open to use this plan
11
+ """
12
+ yield from bps.create(name=DocDescriptorNames.ZOCALO_HW_READ)
13
+ yield from bps.read(detector.odin.file_writer.id) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
14
+ yield from bps.save()
@@ -0,0 +1,46 @@
1
+ from abc import ABC, abstractmethod
2
+ from functools import cache
3
+
4
+ from daq_config_server.client import ConfigServer
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+
8
+ class FeatureFlags(BaseModel, ABC):
9
+ """Abstract class to use ConfigServer to toggle features for an experiment
10
+
11
+ A module wanting to use FeatureFlags should inherit this class, add boolean features
12
+ as attributes, and implement a get_config_server method, which returns a cached creation of
13
+ ConfigServer. See HyperionFeatureFlags for an example
14
+
15
+ Values supplied upon class instantiation will always take priority over the config server. If connection to the server cannot
16
+ be made AND values were not supplied, attributes will use their default values
17
+ """
18
+
19
+ # Feature values supplied at construction will override values from the config server
20
+ overriden_features: dict = Field(default_factory=dict, exclude=True)
21
+
22
+ @staticmethod
23
+ @cache
24
+ @abstractmethod
25
+ def get_config_server() -> ConfigServer: ...
26
+
27
+ @model_validator(mode="before")
28
+ @classmethod
29
+ def mark_overridden_features(cls, values):
30
+ assert isinstance(values, dict)
31
+ values["overriden_features"] = values.copy()
32
+ return values
33
+
34
+ def _get_flags(self):
35
+ flags = type(self).get_config_server().best_effort_get_all_feature_flags()
36
+ return {f: flags[f] for f in flags if f in self.model_fields.keys()}
37
+
38
+ def update_self_from_server(self):
39
+ """Used to update the feature flags from the server during a plan. Where there are flags which were explicitly set from externally supplied parameters, these values will be used instead."""
40
+ for flag, value in self._get_flags().items():
41
+ updated_value = (
42
+ value
43
+ if flag not in self.overriden_features.keys()
44
+ else self.overriden_features[flag]
45
+ )
46
+ setattr(self, flag, updated_value)
@@ -0,0 +1,258 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from abc import abstractmethod
5
+ from collections.abc import Sequence
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+ from typing import Literal, SupportsInt, cast
9
+
10
+ from dodal.devices.aperturescatterguard import ApertureValue
11
+ from dodal.devices.detector import (
12
+ DetectorParams,
13
+ TriggerMode,
14
+ )
15
+ from pydantic import (
16
+ BaseModel,
17
+ ConfigDict,
18
+ Field,
19
+ field_validator,
20
+ model_validator,
21
+ )
22
+ from pydantic_extra_types.semantic_version import SemanticVersion
23
+ from scanspec.core import AxesPoints
24
+ from semver import Version
25
+
26
+ from mx_bluesky.common.parameters.constants import (
27
+ TEST_MODE,
28
+ DetectorParamConstants,
29
+ GridscanParamConstants,
30
+ )
31
+
32
+ PARAMETER_VERSION = Version.parse("5.2.0")
33
+
34
+
35
+ class RotationAxis(StrEnum):
36
+ OMEGA = "omega"
37
+ PHI = "phi"
38
+ CHI = "chi"
39
+ KAPPA = "kappa"
40
+
41
+
42
+ class XyzAxis(StrEnum):
43
+ X = "sam_x"
44
+ Y = "sam_y"
45
+ Z = "sam_z"
46
+
47
+
48
+ class IspybExperimentType(StrEnum):
49
+ # Enum values from ispyb column data type
50
+ SAD = "SAD" # at or slightly above the peak
51
+ SAD_INVERSE_BEAM = "SAD - Inverse Beam"
52
+ OSC = "OSC" # "native" (in the absence of a heavy atom)
53
+ COLLECT_MULTIWEDGE = (
54
+ "Collect - Multiwedge" # "poorly determined" ~ EDNA complex strategy???
55
+ )
56
+ MAD = "MAD"
57
+ HELICAL = "Helical"
58
+ MULTI_POSITIONAL = "Multi-positional"
59
+ MESH = "Mesh"
60
+ BURN = "Burn"
61
+ MAD_INVERSE_BEAM = "MAD - Inverse Beam"
62
+ CHARACTERIZATION = "Characterization"
63
+ DEHYDRATION = "Dehydration"
64
+ TOMO = "tomo"
65
+ EXPERIMENT = "experiment"
66
+ EM = "EM"
67
+ PDF = "PDF"
68
+ PDF_BRAGG = "PDF+Bragg"
69
+ BRAGG = "Bragg"
70
+ SINGLE_PARTICLE = "single particle"
71
+ SERIAL_FIXED = "Serial Fixed"
72
+ SERIAL_JET = "Serial Jet"
73
+ STANDARD = "Standard" # Routine structure determination experiment
74
+ TIME_RESOLVED = "Time Resolved" # Investigate the change of a system over time
75
+ DLS_ANVIL_HP = "Diamond Anvil High Pressure" # HP sample environment pressure cell
76
+ CUSTOM = "Custom" # Special or non-standard data collection
77
+ XRF_MAP = "XRF map"
78
+ ENERGY_SCAN = "Energy scan"
79
+ XRF_SPECTRUM = "XRF spectrum"
80
+ XRF_MAP_XAS = "XRF map xas"
81
+ MESH_3D = "Mesh3D"
82
+ SCREENING = "Screening"
83
+ STILL = "Still"
84
+ SSX_CHIP = "SSX-Chip"
85
+ SSX_JET = "SSX-Jet"
86
+
87
+ # Aliases for historic hyperion experiment type mapping
88
+ ROTATION = "SAD"
89
+ GRIDSCAN_2D = "mesh"
90
+ GRIDSCAN_3D = "Mesh3D"
91
+
92
+
93
+ class MxBlueskyParameters(BaseModel):
94
+ model_config = ConfigDict(
95
+ arbitrary_types_allowed=True,
96
+ extra="allow",
97
+ )
98
+
99
+ def __hash__(self) -> int:
100
+ return self.model_dump_json().__hash__()
101
+
102
+ parameter_model_version: SemanticVersion
103
+
104
+ @field_validator("parameter_model_version")
105
+ @classmethod
106
+ def _validate_version(cls, version: Version):
107
+ assert (
108
+ version >= Version(major=PARAMETER_VERSION.major)
109
+ ), f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}"
110
+ assert (
111
+ version <= Version(major=PARAMETER_VERSION.major + 1)
112
+ ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
113
+ return version
114
+
115
+
116
+ class WithSnapshot(BaseModel):
117
+ snapshot_directory: Path
118
+ snapshot_omegas_deg: list[float] | None = None
119
+
120
+ @property
121
+ def take_snapshots(self) -> bool:
122
+ return bool(self.snapshot_omegas_deg)
123
+
124
+
125
+ class WithOptionalEnergyChange(BaseModel):
126
+ demand_energy_ev: float | None = Field(default=None, gt=0)
127
+
128
+
129
+ class WithVisit(BaseModel):
130
+ beamline: str = Field(default="BL03I", pattern=r"BL\d{2}[BIJS]")
131
+ visit: str = Field(min_length=1)
132
+ det_dist_to_beam_converter_path: str = Field(
133
+ default=DetectorParamConstants.BEAM_XY_LUT_PATH
134
+ )
135
+ insertion_prefix: str = "SR03S" if TEST_MODE else "SR03I"
136
+ detector_distance_mm: float | None = Field(default=None, gt=0)
137
+
138
+
139
+ class DiffractionExperiment(
140
+ MxBlueskyParameters, WithSnapshot, WithOptionalEnergyChange, WithVisit
141
+ ):
142
+ """For all experiments which use beam"""
143
+
144
+ file_name: str
145
+ exposure_time_s: float = Field(gt=0)
146
+ comment: str = Field(default="")
147
+ trigger_mode: TriggerMode = Field(default=TriggerMode.FREE_RUN)
148
+ run_number: int | None = Field(default=None, ge=0)
149
+ selected_aperture: ApertureValue | None = Field(default=None)
150
+ transmission_frac: float = Field(default=0.1)
151
+ ispyb_experiment_type: IspybExperimentType
152
+ storage_directory: str
153
+
154
+ @model_validator(mode="before")
155
+ @classmethod
156
+ def validate_directories(cls, values):
157
+ os.makedirs(values["storage_directory"], exist_ok=True)
158
+
159
+ values["snapshot_directory"] = values.get(
160
+ "snapshot_directory",
161
+ Path(values["storage_directory"], "snapshots").as_posix(),
162
+ )
163
+ return values
164
+
165
+ @property
166
+ def num_images(self) -> int:
167
+ return 0
168
+
169
+ @property
170
+ @abstractmethod
171
+ def detector_params(self) -> DetectorParams: ...
172
+
173
+
174
+ class WithScan(BaseModel):
175
+ """For experiments where the scan is known"""
176
+
177
+ @property
178
+ @abstractmethod
179
+ def scan_points(self) -> AxesPoints: ...
180
+
181
+ @property
182
+ @abstractmethod
183
+ def num_images(self) -> int: ...
184
+
185
+
186
+ class WithPandaGridScan(BaseModel):
187
+ """For experiments which use a PandA for constant-motion grid scans"""
188
+
189
+ panda_runup_distance_mm: float = Field(
190
+ default=GridscanParamConstants.PANDA_RUN_UP_DISTANCE_MM
191
+ )
192
+
193
+
194
+ class SplitScan(BaseModel):
195
+ @property
196
+ @abstractmethod
197
+ def scan_indices(self) -> Sequence[SupportsInt]:
198
+ """Should return the first index of each scan (i.e. for each nexus file)"""
199
+ ...
200
+
201
+
202
+ class WithSample(BaseModel):
203
+ sample_id: int
204
+ sample_puck: int | None = None
205
+ sample_pin: int | None = None
206
+
207
+
208
+ class DiffractionExperimentWithSample(DiffractionExperiment, WithSample): ...
209
+
210
+
211
+ class MultiXtalSelection(BaseModel):
212
+ name: str
213
+
214
+
215
+ class TopNByMaxCountSelection(MultiXtalSelection):
216
+ name: Literal["TopNByMaxCount"] = "TopNByMaxCount" # pyright: ignore [reportIncompatibleVariableOverride]
217
+ n: int
218
+
219
+
220
+ class WithCentreSelection(BaseModel):
221
+ select_centres: TopNByMaxCountSelection = Field(
222
+ discriminator="name", default=TopNByMaxCountSelection(n=1)
223
+ )
224
+
225
+ @property
226
+ def selection_params(self) -> MultiXtalSelection:
227
+ """A helper property because pydantic does not allow polymorphism with base classes
228
+ # only type unions"""
229
+ cast1 = cast(MultiXtalSelection, self.select_centres)
230
+ return cast1
231
+
232
+
233
+ class OptionalXyzStarts(BaseModel):
234
+ x_start_um: float | None = None
235
+ y_start_um: float | None = None
236
+ z_start_um: float | None = None
237
+
238
+
239
+ class XyzStarts(BaseModel):
240
+ x_start_um: float
241
+ y_start_um: float
242
+ z_start_um: float
243
+
244
+ def _start_for_axis(self, axis: XyzAxis) -> float:
245
+ match axis:
246
+ case XyzAxis.X:
247
+ return self.x_start_um
248
+ case XyzAxis.Y:
249
+ return self.y_start_um
250
+ case XyzAxis.Z:
251
+ return self.z_start_um
252
+
253
+
254
+ class OptionalGonioAngleStarts(BaseModel):
255
+ omega_start_deg: float | None = None
256
+ phi_start_deg: float | None = None
257
+ chi_start_deg: float | None = None
258
+ kappa_start_deg: float | None = None