mx-bluesky 1.5.11__py3-none-any.whl → 1.5.14__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 (92) hide show
  1. mx_bluesky/Getting started.ipynb +170 -0
  2. mx_bluesky/_version.py +2 -2
  3. mx_bluesky/beamlines/aithre_lasershaping/experiment_plans/__init__.py +0 -0
  4. mx_bluesky/beamlines/aithre_lasershaping/experiment_plans/robot_load_plan.py +198 -0
  5. mx_bluesky/beamlines/aithre_lasershaping/parameters/__init__.py +0 -0
  6. mx_bluesky/beamlines/aithre_lasershaping/parameters/constants.py +17 -0
  7. mx_bluesky/beamlines/aithre_lasershaping/parameters/robot_load_parameters.py +13 -0
  8. mx_bluesky/beamlines/aithre_lasershaping/pin_tip_centring.py +31 -0
  9. mx_bluesky/beamlines/aithre_lasershaping/robot_load.py +74 -0
  10. mx_bluesky/beamlines/i04/__init__.py +6 -2
  11. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +27 -12
  12. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +88 -13
  13. mx_bluesky/beamlines/i04/external_interaction/__init__.py +0 -0
  14. mx_bluesky/beamlines/i04/external_interaction/config_server.py +15 -0
  15. mx_bluesky/beamlines/i04/oav_centering_plans/__init__.py +0 -0
  16. mx_bluesky/beamlines/i04/oav_centering_plans/oav_imaging.py +115 -0
  17. mx_bluesky/beamlines/i04/parameters/__init__.py +0 -0
  18. mx_bluesky/beamlines/i04/parameters/constants.py +21 -0
  19. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +24 -1
  20. mx_bluesky/beamlines/i04/thawing_plan.py +147 -152
  21. mx_bluesky/beamlines/i24/serial/dcid.py +4 -5
  22. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +5 -2
  23. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +11 -11
  24. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DetStage.edl +3 -3
  25. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +142 -142
  26. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +135 -135
  27. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +8 -8
  28. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +13 -13
  29. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_chip_collect_py3v1.py +7 -4
  30. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_chip_manager_py3v1.py +35 -32
  31. mx_bluesky/beamlines/i24/serial/parameters/utils.py +5 -5
  32. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +113 -306
  33. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +8 -2
  34. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +1 -1
  35. mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +6 -6
  36. mx_bluesky/beamlines/i24/serial/web_gui_plans/oav_plans.py +64 -0
  37. mx_bluesky/{hyperion/device_setup_plans/smargon.py → common/device_setup_plans/gonio.py} +9 -6
  38. mx_bluesky/common/device_setup_plans/manipulate_sample.py +8 -1
  39. mx_bluesky/common/device_setup_plans/robot_load_unload.py +1 -1
  40. mx_bluesky/common/device_setup_plans/setup_oav.py +8 -0
  41. mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +0 -5
  42. mx_bluesky/common/device_setup_plans/xbpm_feedback.py +8 -1
  43. mx_bluesky/common/experiment_plans/beamstop_check.py +229 -0
  44. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +2 -0
  45. mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +5 -2
  46. mx_bluesky/common/experiment_plans/oav_snapshot_plan.py +0 -1
  47. mx_bluesky/{hyperion → common}/experiment_plans/pin_tip_centring_plan.py +20 -21
  48. mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py +5 -0
  49. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +10 -12
  50. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +3 -5
  51. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +5 -5
  52. mx_bluesky/common/external_interaction/config_server.py +2 -2
  53. mx_bluesky/common/external_interaction/ispyb/data_model.py +11 -4
  54. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +159 -2
  55. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +76 -166
  56. mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py +0 -14
  57. mx_bluesky/common/parameters/components.py +1 -0
  58. mx_bluesky/common/parameters/constants.py +5 -2
  59. mx_bluesky/common/parameters/device_composites.py +4 -2
  60. mx_bluesky/common/utils/exceptions.py +15 -0
  61. mx_bluesky/common/utils/log.py +9 -0
  62. mx_bluesky/common/utils/utils.py +48 -0
  63. mx_bluesky/hyperion/__main__.py +3 -13
  64. mx_bluesky/hyperion/baton_handler.py +23 -6
  65. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +1 -0
  66. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +5 -6
  67. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +3 -10
  68. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +4 -2
  69. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +8 -2
  70. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  71. mx_bluesky/hyperion/experiment_plans/udc_default_state.py +166 -0
  72. mx_bluesky/hyperion/external_interaction/agamemnon.py +1 -1
  73. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +48 -21
  74. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +2 -2
  75. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +1 -0
  76. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +1 -4
  77. mx_bluesky/hyperion/external_interaction/config_server.py +5 -5
  78. mx_bluesky/hyperion/parameters/constants.py +10 -3
  79. mx_bluesky/hyperion/parameters/device_composites.py +4 -2
  80. mx_bluesky/hyperion/parameters/robot_load.py +1 -9
  81. mx_bluesky/hyperion/plan_runner.py +31 -0
  82. mx_bluesky/hyperion/plan_runner_api.py +14 -1
  83. mx_bluesky/hyperion/utils/context.py +2 -2
  84. mx_bluesky/jupyter_example.ipynb +9 -1
  85. {mx_bluesky-1.5.11.dist-info → mx_bluesky-1.5.14.dist-info}/METADATA +7 -6
  86. {mx_bluesky-1.5.11.dist-info → mx_bluesky-1.5.14.dist-info}/RECORD +90 -75
  87. mx_bluesky/common/experiment_plans/inner_plans/udc_default_state.py +0 -86
  88. mx_bluesky/common/external_interaction/callbacks/common/logging_callback.py +0 -29
  89. {mx_bluesky-1.5.11.dist-info → mx_bluesky-1.5.14.dist-info}/WHEEL +0 -0
  90. {mx_bluesky-1.5.11.dist-info → mx_bluesky-1.5.14.dist-info}/entry_points.txt +0 -0
  91. {mx_bluesky-1.5.11.dist-info → mx_bluesky-1.5.14.dist-info}/licenses/LICENSE +0 -0
  92. {mx_bluesky-1.5.11.dist-info → mx_bluesky-1.5.14.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,8 @@ from math import asin
4
4
  from scanspec.core import AxesPoints, Axis
5
5
  from scipy.constants import physical_constants
6
6
 
7
+ from mx_bluesky.common.utils.log import LOGGER
8
+
7
9
  hc_in_ev_and_angstrom: float = (
8
10
  physical_constants["speed of light in vacuum"][0]
9
11
  * physical_constants["Planck constant in eV/Hz"][0]
@@ -40,3 +42,49 @@ def energy_to_bragg_angle(energy_kev: float, d_a: float) -> float:
40
42
  wavelength_a = convert_ev_to_angstrom(energy_kev * 1000)
41
43
  d = d_a
42
44
  return asin(wavelength_a / (2 * d)) * 180 / math.pi
45
+
46
+
47
+ def fix_transmission_and_exposure_time_for_current_wavelength(
48
+ current_wavelength_a: float,
49
+ assumed_wavelength_a_from_settings: float,
50
+ requested_trans_frac: float = 100,
51
+ requested_exposure_time_s: float = 0.004,
52
+ ) -> tuple[float, float]:
53
+ """
54
+ Calculates an exposure time and transmission fraction for XRC which will provide a good signal
55
+ on the detector by using a known good wavelength, comparing it to the beamlines current wavelength,
56
+ then scaling accordingly.
57
+
58
+ Args:
59
+ current_wavelength_a: Current energy of the beamline in angstroms.
60
+ assumed_wavelength_a_from_settings: The known "good" wavelength. This should be read from
61
+ 'gda.px.expttable.default.wavelength' in GDA's domain.properties, via the config server.
62
+ requested_trans_frac: Requested transmission fraction to use.
63
+ requested_exposure_time_s: Requested exposure time to use.
64
+
65
+ Returns:
66
+ The scaled transmission fraction and exposure time respectively, in a tuple.
67
+ """
68
+
69
+ wavelength_scale = (assumed_wavelength_a_from_settings / current_wavelength_a) ** 2
70
+
71
+ # Transmission frac needed to get ideal signal
72
+ ideal_trans_frac = requested_trans_frac * wavelength_scale
73
+ if ideal_trans_frac <= 1:
74
+ new_trans_frac = ideal_trans_frac
75
+ new_exposure_time_s = requested_exposure_time_s
76
+ else:
77
+ # If the scaling would result in transmission fraction > 1,
78
+ # cap it to 1, find remaining scaling needed, and apply it
79
+ # to exposure time instead.
80
+ new_trans_frac = 1
81
+ scaling_applied_to_trans = new_trans_frac / requested_trans_frac
82
+ remaining_scaling_needed = wavelength_scale / scaling_applied_to_trans
83
+ new_exposure_time_s = requested_exposure_time_s * remaining_scaling_needed
84
+
85
+ LOGGER.info(
86
+ f"Fixing transmission fraction to {new_trans_frac} and exposure time to {new_exposure_time_s}s"
87
+ )
88
+
89
+ # Exposure time in FGS IOC is in ms, and must be an integer, so round it here
90
+ return new_trans_frac, round(new_exposure_time_s, 3)
@@ -45,21 +45,13 @@ from mx_bluesky.hyperion.runner import (
45
45
  from mx_bluesky.hyperion.utils.context import setup_context
46
46
 
47
47
 
48
- def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions):
48
+ def compose_start_args(context: BlueskyContext, plan_name: str):
49
49
  experiment_registry_entry = PLAN_REGISTRY.get(plan_name)
50
50
  if experiment_registry_entry is None:
51
51
  raise PlanNotFoundError(f"Experiment plan '{plan_name}' not found in registry.")
52
52
 
53
53
  experiment_internal_param_type = experiment_registry_entry.get("param_type")
54
54
  plan = context.plan_functions.get(plan_name)
55
- if experiment_internal_param_type is None:
56
- raise PlanNotFoundError(
57
- f"Corresponding internal param type for '{plan_name}' not found in registry."
58
- )
59
- if plan is None:
60
- raise PlanNotFoundError(
61
- f"Experiment plan '{plan_name}' not found in context. Context has {context.plan_functions.keys()}"
62
- )
63
55
  try:
64
56
  parameters = experiment_internal_param_type(**json.loads(request.data))
65
57
  parameters = update_params_from_agamemnon(parameters)
@@ -80,13 +72,11 @@ class RunExperiment(Resource):
80
72
  self.runner = runner
81
73
  self.context = context
82
74
 
83
- def put(self, plan_name: str, action: Actions):
75
+ def put(self, plan_name: str, action: str):
84
76
  status_and_message = StatusAndMessage(Status.FAILED, f"{action} not understood")
85
77
  if action == Actions.START.value:
86
78
  try:
87
- plan, params, plan_name = compose_start_args(
88
- self.context, plan_name, action
89
- )
79
+ plan, params, plan_name = compose_start_args(self.context, plan_name)
90
80
  status_and_message = self.runner.start(plan, params, plan_name)
91
81
  except Exception as e:
92
82
  status_and_message = make_error_status_and_message(e)
@@ -15,10 +15,6 @@ from dodal.devices.robot import BartRobot
15
15
  from dodal.devices.smargon import Smargon
16
16
 
17
17
  from mx_bluesky.common.device_setup_plans.robot_load_unload import robot_unload
18
- from mx_bluesky.common.experiment_plans.inner_plans.udc_default_state import (
19
- UDCDefaultDevices,
20
- move_to_udc_default_state,
21
- )
22
18
  from mx_bluesky.common.external_interaction.alerting import (
23
19
  AlertService,
24
20
  get_alerting_service,
@@ -28,11 +24,16 @@ from mx_bluesky.common.utils.context import (
28
24
  device_composite_from_context,
29
25
  find_device_in_context,
30
26
  )
27
+ from mx_bluesky.common.utils.exceptions import BeamlineCheckFailureError
31
28
  from mx_bluesky.common.utils.log import LOGGER
32
29
  from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
33
30
  create_devices,
34
31
  load_centre_collect_full,
35
32
  )
33
+ from mx_bluesky.hyperion.experiment_plans.udc_default_state import (
34
+ UDCDefaultDevices,
35
+ move_to_udc_default_state,
36
+ )
36
37
  from mx_bluesky.hyperion.external_interaction.agamemnon import (
37
38
  create_parameters_from_agamemnon,
38
39
  )
@@ -100,7 +101,11 @@ def run_udc_when_requested(context: BlueskyContext, runner: PlanRunner):
100
101
  runner: The runner
101
102
  """
102
103
  _raise_udc_start_alert(get_alerting_service())
103
- yield from _move_to_udc_default_state(context)
104
+ yield from bpp.contingency_wrapper(
105
+ _move_to_udc_default_state(context),
106
+ except_plan=trap_default_state_exception,
107
+ auto_raise=False,
108
+ )
104
109
 
105
110
  # re-fetch the baton because the device has been reinstantiated
106
111
  baton = _get_baton(context)
@@ -121,8 +126,20 @@ def run_udc_when_requested(context: BlueskyContext, runner: PlanRunner):
121
126
  yield from bps.abs_set(baton.current_user, NO_USER, wait=True)
122
127
  _raise_baton_released_alert(get_alerting_service(), previous_requested_user)
123
128
 
129
+ def trap_default_state_exception(e: Exception):
130
+ yield from bps.null()
131
+ if isinstance(e, BeamlineCheckFailureError):
132
+ LOGGER.warning("Caught default state check failure:", exc_info=e)
133
+ raise PlanError("Caught default state check failure") from e
134
+ else:
135
+ LOGGER.warning("Caught unexpected exception", exc_info=e)
136
+ raise PlanError("Unexpected exception from UDC Default State plan") from e
137
+
124
138
  def collect_then_release() -> MsgGenerator:
125
- yield from bpp.contingency_wrapper(collect(), final_plan=release_baton)
139
+ yield from bpp.contingency_wrapper(
140
+ collect(),
141
+ final_plan=release_baton,
142
+ )
126
143
 
127
144
  context.run_engine(acquire_baton())
128
145
  _initialise_udc(context, runner.is_dev_mode)
@@ -63,6 +63,7 @@ def construct_hyperion_specific_features(
63
63
  xrc_composite.flux.flux_reading,
64
64
  xrc_composite.dcm.energy_in_keV,
65
65
  xrc_composite.eiger.bit_depth,
66
+ xrc_composite.beamsize,
66
67
  ]
67
68
 
68
69
  setup_trigger_plan: Callable[..., MsgGenerator]
@@ -21,6 +21,10 @@ from mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan
21
21
  from mx_bluesky.common.experiment_plans.oav_snapshot_plan import (
22
22
  setup_beamline_for_oav,
23
23
  )
24
+ from mx_bluesky.common.experiment_plans.pin_tip_centring_plan import (
25
+ PinTipCentringComposite,
26
+ pin_tip_centre_plan,
27
+ )
24
28
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
25
29
  ispyb_activation_wrapper,
26
30
  )
@@ -31,10 +35,6 @@ from mx_bluesky.common.xrc_result import XRayCentreEventHandler
31
35
  from mx_bluesky.hyperion.experiment_plans.hyperion_flyscan_xray_centre_plan import (
32
36
  construct_hyperion_specific_features,
33
37
  )
34
- from mx_bluesky.hyperion.experiment_plans.pin_tip_centring_plan import (
35
- PinTipCentringComposite,
36
- pin_tip_centre_plan,
37
- )
38
38
  from mx_bluesky.hyperion.parameters.constants import CONST
39
39
  from mx_bluesky.hyperion.parameters.device_composites import (
40
40
  HyperionGridDetectThenXRayCentreComposite,
@@ -78,8 +78,7 @@ def pin_centre_then_flyscan_plan(
78
78
 
79
79
  pin_tip_centring_composite = PinTipCentringComposite(
80
80
  oav=composite.oav,
81
- smargon=composite.smargon,
82
- backlight=composite.backlight,
81
+ gonio=composite.smargon,
83
82
  pin_tip_detection=composite.pin_tip_detection,
84
83
  )
85
84
 
@@ -20,7 +20,7 @@ from dodal.devices.motors import XYZStage
20
20
  from dodal.devices.oav.oav_detector import OAV
21
21
  from dodal.devices.robot import BartRobot, SampleLocation
22
22
  from dodal.devices.smargon import Smargon
23
- from dodal.devices.thawer import Thawer
23
+ from dodal.devices.thawer import OnOff, Thawer
24
24
  from dodal.devices.webcam import Webcam
25
25
  from dodal.devices.xbpm_feedback import XBPMFeedback
26
26
 
@@ -82,7 +82,6 @@ def do_robot_load(
82
82
  sample_location: SampleLocation,
83
83
  sample_id: int,
84
84
  demand_energy_ev: float | None,
85
- thawing_time: float,
86
85
  ):
87
86
  yield from bps.abs_set(composite.robot.next_sample_id, sample_id, wait=True)
88
87
 
@@ -96,13 +95,10 @@ def do_robot_load(
96
95
 
97
96
  yield from bps.wait("robot_load")
98
97
 
99
- yield from bps.abs_set(
100
- composite.thawer.thaw_for_time_s,
101
- thawing_time,
102
- group="thawing_finished",
103
- )
104
98
  yield from wait_for_smargon_not_disabled(composite.smargon)
105
99
 
100
+ yield from bps.mv(composite.thawer, OnOff.ON)
101
+
106
102
 
107
103
  def pin_already_loaded(
108
104
  robot: BartRobot, sample_location: SampleLocation
@@ -120,7 +116,6 @@ def robot_load_and_snapshots(
120
116
  location: SampleLocation,
121
117
  snapshot_directory: Path,
122
118
  sample_id: int,
123
- thawing_time: float,
124
119
  demand_energy_ev: float | None,
125
120
  ):
126
121
  yield from bps.abs_set(composite.backlight, InOut.IN, group="snapshot")
@@ -134,7 +129,6 @@ def robot_load_and_snapshots(
134
129
  location,
135
130
  sample_id,
136
131
  demand_energy_ev,
137
- thawing_time,
138
132
  )
139
133
 
140
134
  gonio_finished = yield from do_plan_while_lower_gonio_at_home(
@@ -173,7 +167,6 @@ def robot_load_and_change_energy_plan(
173
167
  sample_location,
174
168
  params.snapshot_directory,
175
169
  params.sample_id,
176
- params.thawing_time,
177
170
  params.demand_energy_ev,
178
171
  ),
179
172
  md={
@@ -10,6 +10,7 @@ from bluesky.utils import MsgGenerator
10
10
  from dodal.devices.aperturescatterguard import ApertureScatterguard
11
11
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
12
12
  from dodal.devices.backlight import Backlight
13
+ from dodal.devices.beamsize.beamsize import BeamsizeBase
13
14
  from dodal.devices.detector.detector_motion import DetectorMotion
14
15
  from dodal.devices.eiger import EigerDetector
15
16
  from dodal.devices.fast_grid_scan import PandAFastGridScan, ZebraFastGridScanThreeD
@@ -26,7 +27,7 @@ from dodal.devices.s4_slit_gaps import S4SlitGaps
26
27
  from dodal.devices.smargon import Smargon
27
28
  from dodal.devices.synchrotron import Synchrotron
28
29
  from dodal.devices.thawer import Thawer
29
- from dodal.devices.undulator import Undulator
30
+ from dodal.devices.undulator import UndulatorInKeV
30
31
  from dodal.devices.webcam import Webcam
31
32
  from dodal.devices.xbpm_feedback import XBPMFeedback
32
33
  from dodal.devices.zebra.zebra import Zebra
@@ -70,6 +71,7 @@ class RobotLoadThenCentreComposite:
70
71
  # HyperionGridDetectThenXRayCentreComposite fields
71
72
  aperture_scatterguard: ApertureScatterguard
72
73
  backlight: Backlight
74
+ beamsize: BeamsizeBase
73
75
  detector_motion: DetectorMotion
74
76
  eiger: EigerDetector
75
77
  zebra_fast_grid_scan: ZebraFastGridScanThreeD
@@ -79,7 +81,7 @@ class RobotLoadThenCentreComposite:
79
81
  smargon: Smargon
80
82
  synchrotron: Synchrotron
81
83
  s4_slit_gaps: S4SlitGaps
82
- undulator: Undulator
84
+ undulator: UndulatorInKeV
83
85
  zebra: Zebra
84
86
  zocalo: ZocaloResults
85
87
  panda: HDFPanda
@@ -10,6 +10,7 @@ from bluesky.utils import MsgGenerator
10
10
  from dodal.devices.aperturescatterguard import ApertureScatterguard
11
11
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
12
12
  from dodal.devices.backlight import Backlight
13
+ from dodal.devices.beamsize.beamsize import BeamsizeBase
13
14
  from dodal.devices.detector.detector_motion import DetectorMotion
14
15
  from dodal.devices.eiger import EigerDetector
15
16
  from dodal.devices.flux import Flux
@@ -21,7 +22,8 @@ from dodal.devices.robot import BartRobot
21
22
  from dodal.devices.s4_slit_gaps import S4SlitGaps
22
23
  from dodal.devices.smargon import CombinedMove, Smargon
23
24
  from dodal.devices.synchrotron import Synchrotron
24
- from dodal.devices.undulator import Undulator
25
+ from dodal.devices.thawer import Thawer
26
+ from dodal.devices.undulator import UndulatorInKeV
25
27
  from dodal.devices.xbpm_feedback import XBPMFeedback
26
28
  from dodal.devices.zebra.zebra import RotationDirection, Zebra
27
29
  from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
@@ -74,6 +76,7 @@ class RotationScanComposite(OavSnapshotComposite):
74
76
  aperture_scatterguard: ApertureScatterguard
75
77
  attenuator: BinaryFilterAttenuator
76
78
  backlight: Backlight
79
+ beamsize: BeamsizeBase
77
80
  beamstop: Beamstop
78
81
  dcm: DCM
79
82
  detector_motion: DetectorMotion
@@ -81,13 +84,14 @@ class RotationScanComposite(OavSnapshotComposite):
81
84
  flux: Flux
82
85
  robot: BartRobot
83
86
  smargon: Smargon
84
- undulator: Undulator
87
+ undulator: UndulatorInKeV
85
88
  synchrotron: Synchrotron
86
89
  s4_slit_gaps: S4SlitGaps
87
90
  sample_shutter: ZebraShutter
88
91
  zebra: Zebra
89
92
  oav: OAV
90
93
  xbpm_feedback: XBPMFeedback
94
+ thawer: Thawer
91
95
 
92
96
 
93
97
  def create_devices(context: BlueskyContext) -> RotationScanComposite:
@@ -258,6 +262,7 @@ def rotation_scan_plan(
258
262
  composite.aperture_scatterguard,
259
263
  params.selected_aperture,
260
264
  composite.backlight,
265
+ composite.thawer,
261
266
  group=CONST.WAIT.ROTATION_READY_FOR_DC,
262
267
  )
263
268
 
@@ -301,6 +306,7 @@ def rotation_scan_plan(
301
306
  composite.flux,
302
307
  composite.dcm,
303
308
  composite.eiger,
309
+ composite.beamsize,
304
310
  )
305
311
 
306
312
  yield from _rotation_scan_plan(motion_values, composite)
@@ -12,7 +12,7 @@ from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
12
12
  from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
13
13
  from dodal.devices.i03.dcm import DCM
14
14
  from dodal.devices.i03.undulator_dcm import UndulatorDCM
15
- from dodal.devices.undulator import Undulator
15
+ from dodal.devices.undulator import UndulatorInKeV
16
16
  from dodal.devices.xbpm_feedback import XBPMFeedback
17
17
 
18
18
  from mx_bluesky.common.parameters.constants import PlanNameConstants
@@ -39,7 +39,7 @@ class SetEnergyComposite:
39
39
  # Remove composite after https://github.com/DiamondLightSource/dodal/issues/1092
40
40
  @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
41
41
  class XBPMWrapperComposite:
42
- undulator: Undulator
42
+ undulator: UndulatorInKeV
43
43
  xbpm_feedback: XBPMFeedback
44
44
  attenuator: BinaryFilterAttenuator
45
45
  dcm: DCM
@@ -0,0 +1,166 @@
1
+ import bluesky.plan_stubs as bps
2
+ import pydantic
3
+ from bluesky.utils import MsgGenerator
4
+ from dodal.common.beamlines.beamline_parameters import (
5
+ get_beamline_parameters,
6
+ )
7
+ from dodal.devices.aperturescatterguard import ApertureValue
8
+ from dodal.devices.collimation_table import CollimationTable
9
+ from dodal.devices.cryostream import (
10
+ CryoStreamGantry,
11
+ CryoStreamSelection,
12
+ OxfordCryoJet,
13
+ OxfordCryoStream,
14
+ )
15
+ from dodal.devices.cryostream import InOut as CryoInOut
16
+ from dodal.devices.fluorescence_detector_motion import FluorescenceDetector
17
+ from dodal.devices.fluorescence_detector_motion import InOut as FlouInOut
18
+ from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
19
+ from dodal.devices.mx_phase1.beamstop import BeamstopPositions
20
+ from dodal.devices.robot import BartRobot, PinMounted
21
+ from dodal.devices.scintillator import InOut as ScinInOut
22
+ from dodal.devices.scintillator import Scintillator
23
+ from dodal.devices.smargon import Smargon
24
+ from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutterState
25
+
26
+ from mx_bluesky.common.experiment_plans.beamstop_check import (
27
+ BeamstopCheckDevices,
28
+ move_beamstop_in_and_verify_using_diode,
29
+ )
30
+ from mx_bluesky.common.utils.exceptions import BeamlineCheckFailureError
31
+ from mx_bluesky.common.utils.log import LOGGER
32
+ from mx_bluesky.hyperion.external_interaction.config_server import (
33
+ get_hyperion_config_client,
34
+ )
35
+ from mx_bluesky.hyperion.parameters.constants import CONST, HyperionFeatureSettings
36
+
37
+ _GROUP_PRE_BEAMSTOP_CHECK = "pre_beamstop_check"
38
+ _GROUP_POST_BEAMSTOP_CHECK = "post_beamstop_check"
39
+
40
+
41
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
42
+ class UDCDefaultDevices(BeamstopCheckDevices):
43
+ collimation_table: CollimationTable
44
+ cryojet: OxfordCryoJet
45
+ cryostream: OxfordCryoStream
46
+ cryostream_gantry: CryoStreamGantry
47
+ fluorescence_det_motion: FluorescenceDetector
48
+ hutch_shutter: HutchShutter
49
+ robot: BartRobot
50
+ scintillator: Scintillator
51
+ smargon: Smargon
52
+
53
+
54
+ class UnexpectedSampleError(BeamlineCheckFailureError): ...
55
+
56
+
57
+ class CryoStreamError(BeamlineCheckFailureError): ...
58
+
59
+
60
+ def move_to_udc_default_state(devices: UDCDefaultDevices):
61
+ """Moves beamline to known positions prior to UDC start"""
62
+ yield from _verify_correct_cryostream_selected(devices.cryostream_gantry)
63
+
64
+ cryostream_temp = yield from bps.rd(devices.cryostream.temp)
65
+ cryostream_pressure = yield from bps.rd(devices.cryostream.back_pressure)
66
+ if cryostream_temp > CONST.HARDWARE.MAX_CRYO_TEMP_K:
67
+ raise CryoStreamError("Cryostream temperature is too high, not starting UDC")
68
+ if cryostream_pressure > CONST.HARDWARE.MAX_CRYO_PRESSURE_BAR:
69
+ raise CryoStreamError("Cryostream back pressure is too high, not starting UDC")
70
+
71
+ yield from _verify_no_sample_present(devices.robot)
72
+
73
+ # Close fast shutter before opening hutch shutter
74
+ yield from bps.abs_set(devices.sample_shutter, ZebraShutterState.CLOSE, wait=True)
75
+
76
+ commissioning_mode_enabled = yield from bps.rd(devices.baton.commissioning)
77
+
78
+ if commissioning_mode_enabled:
79
+ LOGGER.warning("Not opening hutch shutter - commissioning mode is enabled.")
80
+ else:
81
+ yield from bps.abs_set(
82
+ devices.hutch_shutter, ShutterDemand.OPEN, group=_GROUP_PRE_BEAMSTOP_CHECK
83
+ )
84
+
85
+ yield from bps.abs_set(devices.scintillator.selected_pos, ScinInOut.OUT, wait=True)
86
+
87
+ yield from bps.abs_set(
88
+ devices.fluorescence_det_motion.pos,
89
+ FlouInOut.OUT,
90
+ group=_GROUP_PRE_BEAMSTOP_CHECK,
91
+ )
92
+
93
+ yield from bps.abs_set(
94
+ devices.collimation_table.inboard_y,
95
+ 0,
96
+ group=_GROUP_PRE_BEAMSTOP_CHECK,
97
+ )
98
+ yield from bps.abs_set(
99
+ devices.collimation_table.outboard_y, 0, group=_GROUP_PRE_BEAMSTOP_CHECK
100
+ )
101
+ yield from bps.abs_set(
102
+ devices.collimation_table.upstream_y, 0, group=_GROUP_PRE_BEAMSTOP_CHECK
103
+ )
104
+ yield from bps.abs_set(
105
+ devices.collimation_table.upstream_x, 0, group=_GROUP_PRE_BEAMSTOP_CHECK
106
+ )
107
+ yield from bps.abs_set(
108
+ devices.collimation_table.downstream_x, 0, group=_GROUP_PRE_BEAMSTOP_CHECK
109
+ )
110
+
111
+ # Wait for all of the above to complete
112
+ yield from bps.wait(group=_GROUP_PRE_BEAMSTOP_CHECK, timeout=0.1)
113
+
114
+ feature_flags: HyperionFeatureSettings = (
115
+ get_hyperion_config_client().get_feature_flags()
116
+ )
117
+ if feature_flags.BEAMSTOP_DIODE_CHECK:
118
+ beamline_parameters = get_beamline_parameters()
119
+ config_client = get_hyperion_config_client()
120
+ features_settings: HyperionFeatureSettings = config_client.get_feature_flags()
121
+ detector_min_z = features_settings.DETECTOR_DISTANCE_LIMIT_MIN_MM
122
+ detector_max_z = features_settings.DETECTOR_DISTANCE_LIMIT_MAX_MM
123
+ yield from move_beamstop_in_and_verify_using_diode(
124
+ devices, beamline_parameters, detector_min_z, detector_max_z
125
+ )
126
+ else:
127
+ yield from bps.abs_set(
128
+ devices.beamstop.selected_pos, BeamstopPositions.DATA_COLLECTION, wait=True
129
+ )
130
+
131
+ yield from bps.abs_set(
132
+ devices.aperture_scatterguard.selected_aperture,
133
+ ApertureValue.SMALL,
134
+ group=_GROUP_POST_BEAMSTOP_CHECK,
135
+ )
136
+
137
+ yield from bps.abs_set(
138
+ devices.cryojet.coarse, CryoInOut.IN, group=_GROUP_POST_BEAMSTOP_CHECK
139
+ )
140
+ yield from bps.abs_set(
141
+ devices.cryojet.fine, CryoInOut.IN, group=_GROUP_POST_BEAMSTOP_CHECK
142
+ )
143
+
144
+ yield from bps.wait(_GROUP_POST_BEAMSTOP_CHECK)
145
+
146
+
147
+ def _verify_correct_cryostream_selected(
148
+ cryostream_gantry: CryoStreamGantry,
149
+ ) -> MsgGenerator:
150
+ cryostream_selection = yield from bps.rd(cryostream_gantry.cryostream_selector)
151
+ cryostream_selected = yield from bps.rd(cryostream_gantry.cryostream_selected)
152
+ if cryostream_selection != CryoStreamSelection.CRYOJET or cryostream_selected != 1:
153
+ raise CryoStreamError(
154
+ f"Cryostream is not selected for use, control PV selection = {cryostream_selection}, "
155
+ f"current status {cryostream_selected}"
156
+ )
157
+
158
+
159
+ def _verify_no_sample_present(robot: BartRobot):
160
+ pin_mounted = yield from bps.rd(robot.gonio_pin_sensor)
161
+
162
+ if pin_mounted != PinMounted.NO_PIN_MOUNTED:
163
+ # Cannot unload this sample because we do not know the correct visit for it
164
+ raise UnexpectedSampleError(
165
+ "An unexpected sample was found, please unload the sample manually."
166
+ )
@@ -220,7 +220,7 @@ def _get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
220
220
  def _get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
221
221
  try:
222
222
  first_collection: dict = parameters["collection"][0]
223
- wavelength = first_collection.get("wavelength")
223
+ wavelength: float | None = first_collection.get("wavelength")
224
224
  assert isinstance(wavelength, float)
225
225
  demand_energy_ev = convert_angstrom_to_ev(wavelength)
226
226
  return {"demand_energy_ev": demand_energy_ev}
@@ -2,6 +2,8 @@ import logging
2
2
  from collections.abc import Callable, Sequence
3
3
  from threading import Thread
4
4
  from time import sleep # noqa
5
+ from urllib import request
6
+ from urllib.error import URLError
5
7
 
6
8
  from bluesky.callbacks import CallbackBase
7
9
  from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
@@ -51,14 +53,17 @@ from mx_bluesky.hyperion.external_interaction.callbacks.snapshot_callback import
51
53
  BeamDrawingCallback,
52
54
  )
53
55
  from mx_bluesky.hyperion.parameters.cli import parse_callback_dev_mode_arg
54
- from mx_bluesky.hyperion.parameters.constants import CONST
56
+ from mx_bluesky.hyperion.parameters.constants import CONST, HyperionConstants
55
57
  from mx_bluesky.hyperion.parameters.gridscan import (
56
58
  GridCommonWithHyperionDetectorParams,
57
59
  HyperionSpecifiedThreeDGridScan,
58
60
  )
59
61
 
62
+ PING_TIMEOUT_S = 1
63
+
60
64
  LIVENESS_POLL_SECONDS = 1
61
65
  ERROR_LOG_BUFFER_LINES = 5000
66
+ HYPERION_PING_INTERVAL_S = 19
62
67
 
63
68
 
64
69
  def create_gridscan_callbacks() -> tuple[
@@ -128,21 +133,6 @@ def setup_logging(dev_mode: bool):
128
133
  log_debug("nexgen logger added to nexus logger")
129
134
 
130
135
 
131
- def setup_threads():
132
- proxy = Proxy(*CONST.CALLBACK_0MQ_PROXY_PORTS)
133
- dispatcher = RemoteDispatcher(f"localhost:{CONST.CALLBACK_0MQ_PROXY_PORTS[1]}")
134
- log_debug("Created proxy and dispatcher objects")
135
-
136
- def start_proxy():
137
- proxy.start()
138
-
139
- def start_dispatcher(callbacks: list[Callable]):
140
- [dispatcher.subscribe(cb) for cb in callbacks]
141
- dispatcher.start()
142
-
143
- return proxy, dispatcher, start_proxy, start_dispatcher
144
-
145
-
146
136
  def log_info(msg, *args, **kwargs):
147
137
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(msg, *args, **kwargs)
148
138
  NEXUS_LOGGER.info(msg, *args, **kwargs)
@@ -175,20 +165,57 @@ class HyperionCallbackRunner:
175
165
  set_alerting_service(LoggingAlertService(CONST.GRAYLOG_STREAM_ID))
176
166
 
177
167
  self.callbacks = setup_callbacks()
178
- self.proxy, self.dispatcher, start_proxy, start_dispatcher = setup_threads()
179
- log_info("Created 0MQ proxy and local RemoteDispatcher.")
180
168
 
181
- self.proxy_thread = Thread(target=start_proxy, daemon=True)
169
+ self.proxy = Proxy(*CONST.CALLBACK_0MQ_PROXY_PORTS)
170
+ self.proxy_thread = Thread(
171
+ target=self.proxy.start, daemon=True, name="0MQ Proxy"
172
+ )
173
+
174
+ self.dispatcher = RemoteDispatcher(
175
+ f"localhost:{CONST.CALLBACK_0MQ_PROXY_PORTS[1]}"
176
+ )
177
+
178
+ def start_dispatcher(callbacks: list[Callable]):
179
+ for cb in callbacks:
180
+ self.dispatcher.subscribe(cb)
181
+ self.dispatcher.start()
182
+
182
183
  self.dispatcher_thread = Thread(
183
- target=start_dispatcher, args=[self.callbacks], daemon=True
184
+ target=start_dispatcher,
185
+ args=[self.callbacks],
186
+ daemon=True,
187
+ name="0MQ Dispatcher",
184
188
  )
185
189
 
190
+ self.watchdog_thread = Thread(target=run_watchdog, daemon=True, name="Watchdog")
191
+ log_info("Created 0MQ proxy and local RemoteDispatcher.")
192
+
186
193
  def start(self):
187
194
  log_info(f"Launching threads, with callbacks: {self.callbacks}")
188
195
  self.proxy_thread.start()
189
196
  self.dispatcher_thread.start()
197
+ self.watchdog_thread.start()
190
198
  log_info("Proxy and dispatcher thread launched.")
191
- wait_for_threads_forever([self.proxy_thread, self.dispatcher_thread])
199
+ wait_for_threads_forever(
200
+ [self.proxy_thread, self.dispatcher_thread, self.watchdog_thread]
201
+ )
202
+
203
+
204
+ def run_watchdog():
205
+ log_info("Hyperion watchdog keepalive running")
206
+ while True:
207
+ try:
208
+ with request.urlopen(
209
+ f"http://localhost:{HyperionConstants.HYPERION_PORT}/callbackPing",
210
+ timeout=PING_TIMEOUT_S,
211
+ ) as response:
212
+ if response.status != 200:
213
+ log_debug(
214
+ f"Unable to ping Hyperion liveness endpoint, status {response.status}"
215
+ )
216
+ except URLError as e:
217
+ log_debug("Unable to ping Hyperion liveness endpoint", exc_info=e)
218
+ sleep(HYPERION_PING_INTERVAL_S)
192
219
 
193
220
 
194
221
  def main(dev_mode=False) -> None:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable, Sequence
4
- from typing import TYPE_CHECKING, Any, cast
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from dodal.devices.zocalo import ZocaloStartInfo
7
7
 
@@ -92,7 +92,7 @@ class RotationISPyBCallback(BaseISPyBCallback):
92
92
  ISPYB_ZOCALO_CALLBACK_LOGGER.info("Beginning ispyb deposition")
93
93
  data_collection_group_info = populate_data_collection_group(self.params)
94
94
  data_collection_info = populate_data_collection_info_for_rotation(
95
- cast(SingleRotationScan, self.params)
95
+ self.params
96
96
  )
97
97
  data_collection_info = populate_remaining_data_collection_info(
98
98
  self.params.comment,
@@ -6,6 +6,7 @@ from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
6
6
 
7
7
  def populate_data_collection_info_for_rotation(params: SingleRotationScan):
8
8
  info = DataCollectionInfo(
9
+ chi_start=params.chi_start_deg,
9
10
  omega_start=params.omega_start_deg,
10
11
  data_collection_number=params.detector_params.run_number, # type:ignore # the validator always makes this int
11
12
  n_images=params.num_images,