mx-bluesky 1.4.6__py3-none-any.whl → 1.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/aithre_lasershaping/__init__.py +13 -0
  3. mx_bluesky/beamlines/aithre_lasershaping/check_goniometer_performance.py +29 -0
  4. mx_bluesky/beamlines/aithre_lasershaping/goniometer_controls.py +18 -0
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +31 -25
  6. mx_bluesky/beamlines/i04/thawing_plan.py +10 -1
  7. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +12 -12
  8. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +30 -29
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +10 -11
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +8 -10
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +10 -3
  12. mx_bluesky/beamlines/i24/serial/log.py +1 -0
  13. mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
  14. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +16 -16
  15. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +47 -48
  16. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +1 -1
  17. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +9 -7
  18. mx_bluesky/beamlines/i24/serial/write_nexus.py +3 -2
  19. mx_bluesky/common/device_setup_plans/xbpm_feedback.py +45 -0
  20. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +2 -4
  21. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  22. mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +18 -15
  23. mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/sample_handling_callback.py +16 -4
  24. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +41 -5
  25. mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +2 -1
  26. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -0
  27. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +14 -1
  28. mx_bluesky/common/external_interaction/nexus/nexus_utils.py +1 -1
  29. mx_bluesky/common/parameters/constants.py +2 -0
  30. mx_bluesky/common/parameters/gridscan.py +1 -1
  31. mx_bluesky/common/plans/write_sample_status.py +46 -0
  32. mx_bluesky/common/preprocessors/__init__.py +0 -0
  33. mx_bluesky/common/preprocessors/preprocessors.py +105 -0
  34. mx_bluesky/common/protocols/__init__.py +0 -0
  35. mx_bluesky/common/protocols/protocols.py +10 -0
  36. mx_bluesky/hyperion/__main__.py +3 -9
  37. mx_bluesky/hyperion/baton_handler.py +84 -0
  38. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -1
  39. mx_bluesky/hyperion/experiment_plans/__init__.py +0 -4
  40. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +10 -25
  41. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +0 -7
  42. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +11 -10
  43. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +5 -1
  44. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +2 -2
  45. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +5 -3
  46. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +0 -26
  47. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +23 -18
  48. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +25 -6
  49. mx_bluesky/hyperion/external_interaction/agamemnon.py +148 -10
  50. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +12 -6
  51. mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +107 -0
  52. mx_bluesky/hyperion/parameters/gridscan.py +2 -2
  53. mx_bluesky/hyperion/parameters/rotation.py +1 -1
  54. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/METADATA +7 -7
  55. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/RECORD +60 -51
  56. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/WHEEL +1 -1
  57. mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py +0 -22
  58. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +0 -103
  59. /mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  60. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/entry_points.txt +0 -0
  61. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info/licenses}/LICENSE +0 -0
  62. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,9 @@ from dodal.devices.zocalo.zocalo_results import (
24
24
  XrcResult,
25
25
  get_full_processing_results,
26
26
  )
27
+ from dodal.plans.preprocessors.verify_undulator_gap import (
28
+ verify_undulator_gap_before_run_decorator,
29
+ )
27
30
 
28
31
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
29
32
  ispyb_activation_wrapper,
@@ -34,6 +37,9 @@ from mx_bluesky.common.plans.read_hardware import (
34
37
  standard_read_hardware_during_collection,
35
38
  standard_read_hardware_pre_collection,
36
39
  )
40
+ from mx_bluesky.common.preprocessors.preprocessors import (
41
+ transmission_and_xbpm_feedback_for_collection_decorator,
42
+ )
37
43
  from mx_bluesky.common.utils.context import device_composite_from_context
38
44
  from mx_bluesky.common.utils.exceptions import (
39
45
  CrystalNotFoundException,
@@ -52,9 +58,6 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
52
58
  setup_zebra_for_panda_flyscan,
53
59
  tidy_up_zebra_after_gridscan,
54
60
  )
55
- from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import (
56
- transmission_and_xbpm_feedback_for_collection_decorator,
57
- )
58
61
  from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import (
59
62
  change_aperture_then_move_to_xtal,
60
63
  )
@@ -102,13 +105,6 @@ def flyscan_xray_centre_no_move(
102
105
  }
103
106
  )
104
107
  @bpp.finalize_decorator(lambda: feature_controlled.tidy_plan(composite))
105
- @transmission_and_xbpm_feedback_for_collection_decorator(
106
- composite.undulator,
107
- composite.xbpm_feedback,
108
- composite.attenuator,
109
- composite.dcm,
110
- parameters.transmission_frac,
111
- )
112
108
  def run_gridscan_and_fetch_and_tidy(
113
109
  fgs_composite: HyperionFlyScanXRayCentreComposite,
114
110
  params: HyperionSpecifiedThreeDGridScan,
@@ -140,6 +136,11 @@ def flyscan_xray_centre(
140
136
  """
141
137
  xrc_event_handler = XRayCentreEventHandler()
142
138
 
139
+ @transmission_and_xbpm_feedback_for_collection_decorator(
140
+ composite,
141
+ parameters.transmission_frac,
142
+ )
143
+ @verify_undulator_gap_before_run_decorator(composite)
143
144
  @bpp.subs_decorator(xrc_event_handler)
144
145
  def flyscan_and_fetch_results() -> MsgGenerator:
145
146
  yield from ispyb_activation_wrapper(
@@ -9,6 +9,7 @@ from bluesky.utils import MsgGenerator
9
9
  from dodal.devices.oav.oav_parameters import OAVParameters
10
10
 
11
11
  import mx_bluesky.common.xrc_result as flyscan_result
12
+ from mx_bluesky.common.parameters.components import WithSnapshot
12
13
  from mx_bluesky.common.utils.context import device_composite_from_context
13
14
  from mx_bluesky.common.utils.log import LOGGER
14
15
  from mx_bluesky.common.xrc_result import XRayCentreEventHandler
@@ -54,7 +55,10 @@ def load_centre_collect_full(
54
55
  @run_decorator(
55
56
  md={
56
57
  "metadata": {"sample_id": parameters.sample_id},
57
- "activate_callbacks": ["SampleHandlingCallback"],
58
+ "activate_callbacks": ["BeamDrawingCallback", "SampleHandlingCallback"],
59
+ "with_snapshot": parameters.multi_rotation_scan.model_dump_json(
60
+ include=WithSnapshot.model_fields.keys() # type: ignore
61
+ ),
58
62
  }
59
63
  )
60
64
  def plan_with_callback_subs():
@@ -67,7 +67,7 @@ def _take_oav_snapshot(composite: OavSnapshotComposite, omega: float):
67
67
  composite.smargon.omega, omega, group=OAV_SNAPSHOT_SETUP_SHOT
68
68
  )
69
69
  time_now = datetime.now()
70
- filename = f"{time_now.strftime('%H%M%S')}_oav_snapshot_{omega:.0f}"
70
+ filename = f"{time_now.strftime('%H%M%S%f')[:8]}_oav_snapshot_{omega:.0f}"
71
71
  yield from bps.abs_set(
72
72
  composite.oav.snapshot.filename,
73
73
  filename,
@@ -76,5 +76,5 @@ def _take_oav_snapshot(composite: OavSnapshotComposite, omega: float):
76
76
  yield from bps.wait(group=OAV_SNAPSHOT_SETUP_SHOT)
77
77
  yield from bps.trigger(composite.oav.snapshot, wait=True)
78
78
  yield from bps.create(DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED)
79
- yield from bps.read(composite.oav.snapshot)
79
+ yield from bps.read(composite.oav)
80
80
  yield from bps.save()
@@ -9,6 +9,7 @@ from dodal.devices.oav.oav_detector import OAV
9
9
  from dodal.devices.oav.oav_parameters import OAV_CONFIG_JSON, OAVParameters
10
10
  from dodal.devices.oav.pin_image_recognition import PinTipDetection, Tip
11
11
  from dodal.devices.oav.utils import (
12
+ PinNotFoundException,
12
13
  Pixel,
13
14
  get_move_required_so_that_beam_is_at_pixel,
14
15
  wait_for_tip_to_be_found,
@@ -16,7 +17,7 @@ from dodal.devices.oav.utils import (
16
17
  from dodal.devices.smargon import Smargon
17
18
 
18
19
  from mx_bluesky.common.utils.context import device_composite_from_context
19
- from mx_bluesky.common.utils.exceptions import SampleException
20
+ from mx_bluesky.common.utils.exceptions import SampleException, catch_exception_and_warn
20
21
  from mx_bluesky.common.utils.log import LOGGER
21
22
  from mx_bluesky.hyperion.device_setup_plans.setup_oav import pre_centring_setup_oav
22
23
  from mx_bluesky.hyperion.device_setup_plans.smargon import (
@@ -159,6 +160,7 @@ def pin_tip_centre_plan(
159
160
  # need to wait for the OAV image to update
160
161
  # See #673 for improvements
161
162
  yield from bps.sleep(0.3)
162
-
163
- tip = yield from wait_for_tip_to_be_found(pin_tip_detect)
163
+ tip = yield from catch_exception_and_warn(
164
+ PinNotFoundException, wait_for_tip_to_be_found, pin_tip_detect
165
+ )
164
166
  yield from offset_and_move(tip)
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from math import isclose
4
4
  from typing import cast
5
5
 
6
- import bluesky.preprocessors as bpp
7
6
  import pydantic
8
7
  from blueapi.core import BlueskyContext
9
8
  from bluesky import plan_stubs as bps
@@ -37,14 +36,10 @@ from dodal.log import LOGGER
37
36
  from ophyd_async.fastcs.panda import HDFPanda
38
37
 
39
38
  from mx_bluesky.common.parameters.constants import OavConstants
40
- from mx_bluesky.common.xrc_result import XRayCentreEventHandler
41
39
  from mx_bluesky.hyperion.device_setup_plans.utils import (
42
40
  fill_in_energy_if_not_supplied,
43
41
  start_preparing_data_collection_then_do_plan,
44
42
  )
45
- from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import (
46
- change_aperture_then_move_to_xtal,
47
- )
48
43
  from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import (
49
44
  GridDetectThenXRayCentreComposite,
50
45
  )
@@ -133,27 +128,6 @@ def _robot_load_then_flyscan_plan(
133
128
  yield from _flyscan_plan_from_robot_load_params(composite, params, oav_config_file)
134
129
 
135
130
 
136
- def robot_load_then_centre(
137
- composite: RobotLoadThenCentreComposite,
138
- parameters: RobotLoadThenCentre,
139
- ) -> MsgGenerator:
140
- """Perform pin-tip detection followed by a flyscan to determine centres of interest.
141
- Performs a robot load if necessary. Centre on the best diffracting centre.
142
- """
143
-
144
- xray_centre_event_handler = XRayCentreEventHandler()
145
-
146
- yield from bpp.subs_wrapper(
147
- robot_load_then_xray_centre(composite, parameters), xray_centre_event_handler
148
- )
149
- flyscan_results = xray_centre_event_handler.xray_centre_results
150
- if flyscan_results is not None:
151
- yield from change_aperture_then_move_to_xtal(
152
- flyscan_results[0], composite.smargon, composite.aperture_scatterguard
153
- )
154
- # else no chi change, no need to recentre.
155
-
156
-
157
131
  def robot_load_then_xray_centre(
158
132
  composite: RobotLoadThenCentreComposite,
159
133
  parameters: RobotLoadThenCentre,
@@ -26,12 +26,19 @@ from dodal.devices.xbpm_feedback import XBPMFeedback
26
26
  from dodal.devices.zebra.zebra import RotationDirection, Zebra
27
27
  from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
28
28
  from dodal.plan_stubs.check_topup import check_topup_and_wait_if_necessary
29
+ from dodal.plans.preprocessors.verify_undulator_gap import (
30
+ verify_undulator_gap_before_run_decorator,
31
+ )
29
32
 
33
+ from mx_bluesky.common.parameters.components import WithSnapshot
30
34
  from mx_bluesky.common.plans.read_hardware import (
31
35
  read_hardware_for_zocalo,
32
36
  standard_read_hardware_during_collection,
33
37
  standard_read_hardware_pre_collection,
34
38
  )
39
+ from mx_bluesky.common.preprocessors.preprocessors import (
40
+ transmission_and_xbpm_feedback_for_collection_decorator,
41
+ )
35
42
  from mx_bluesky.common.utils.context import device_composite_from_context
36
43
  from mx_bluesky.common.utils.log import LOGGER
37
44
  from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import (
@@ -48,9 +55,6 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
48
55
  from mx_bluesky.hyperion.device_setup_plans.utils import (
49
56
  start_preparing_data_collection_then_do_plan,
50
57
  )
51
- from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import (
52
- transmission_and_xbpm_feedback_for_collection_decorator,
53
- )
54
58
  from mx_bluesky.hyperion.experiment_plans.oav_snapshot_plan import (
55
59
  OavSnapshotComposite,
56
60
  oav_snapshot_plan,
@@ -369,24 +373,26 @@ def rotation_scan(
369
373
  if not oav_params:
370
374
  oav_params = OAVParameters(context="xrayCentring")
371
375
 
376
+ @transmission_and_xbpm_feedback_for_collection_decorator(
377
+ composite,
378
+ parameters.transmission_frac,
379
+ )
380
+ @verify_undulator_gap_before_run_decorator(composite)
372
381
  @bpp.set_run_key_decorator("rotation_scan")
373
382
  @bpp.run_decorator( # attach experiment metadata to the start document
374
383
  md={
375
384
  "subplan_name": CONST.PLAN.ROTATION_OUTER,
376
385
  "mx_bluesky_parameters": parameters.model_dump_json(),
386
+ "with_snapshot": parameters.model_dump_json(
387
+ include=WithSnapshot.model_fields.keys() # type: ignore
388
+ ),
377
389
  "activate_callbacks": [
390
+ "BeamDrawingCallback",
378
391
  "RotationISPyBCallback",
379
392
  "RotationNexusFileCallback",
380
393
  ],
381
394
  }
382
395
  )
383
- @transmission_and_xbpm_feedback_for_collection_decorator(
384
- composite.undulator,
385
- composite.xbpm_feedback,
386
- composite.attenuator,
387
- composite.dcm,
388
- parameters.transmission_frac,
389
- )
390
396
  def rotation_scan_plan_with_stage_and_cleanup(
391
397
  params: RotationScan,
392
398
  ):
@@ -422,6 +428,10 @@ def multi_rotation_scan(
422
428
  eiger: EigerDetector = composite.eiger
423
429
  eiger.set_detector_parameters(parameters.detector_params)
424
430
 
431
+ @transmission_and_xbpm_feedback_for_collection_decorator(
432
+ composite,
433
+ parameters.transmission_frac,
434
+ )
425
435
  @bpp.set_run_key_decorator("multi_rotation_scan")
426
436
  @bpp.run_decorator(
427
437
  md={
@@ -434,17 +444,11 @@ def multi_rotation_scan(
434
444
  ],
435
445
  }
436
446
  )
437
- @transmission_and_xbpm_feedback_for_collection_decorator(
438
- composite.undulator,
439
- composite.xbpm_feedback,
440
- composite.attenuator,
441
- composite.dcm,
442
- parameters.transmission_frac,
443
- )
444
447
  @bpp.finalize_decorator(lambda: _cleanup_plan(composite))
445
448
  def _multi_rotation_scan():
446
449
  for single_scan in parameters.single_rotation_scans:
447
450
 
451
+ @verify_undulator_gap_before_run_decorator(composite)
448
452
  @bpp.set_run_key_decorator("rotation_scan")
449
453
  @bpp.run_decorator( # attach experiment metadata to the start document
450
454
  md={
@@ -459,6 +463,8 @@ def multi_rotation_scan(
459
463
 
460
464
  yield from rotation_scan_core(single_scan)
461
465
 
466
+ yield from bps.unstage(eiger)
467
+
462
468
  LOGGER.info("setting up and staging eiger...")
463
469
  yield from start_preparing_data_collection_then_do_plan(
464
470
  composite.beamstop,
@@ -468,4 +474,3 @@ def multi_rotation_scan(
468
474
  _multi_rotation_scan(),
469
475
  group=CONST.WAIT.ROTATION_READY_FOR_DC,
470
476
  )
471
- yield from bps.unstage(eiger)
@@ -5,18 +5,21 @@
5
5
  * reenable feedback
6
6
  """
7
7
 
8
+ import bluesky.preprocessors as bpp
8
9
  import pydantic
9
10
  from bluesky import plan_stubs as bps
10
11
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
11
12
  from dodal.devices.dcm import DCM
12
13
  from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
14
+ from dodal.devices.undulator import Undulator
13
15
  from dodal.devices.undulator_dcm import UndulatorDCM
14
16
  from dodal.devices.xbpm_feedback import XBPMFeedback
15
17
 
16
- from mx_bluesky.hyperion.device_setup_plans import dcm_pitch_roll_mirror_adjuster
17
- from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import (
18
+ from mx_bluesky.common.parameters.constants import PlanNameConstants
19
+ from mx_bluesky.common.preprocessors.preprocessors import (
18
20
  transmission_and_xbpm_feedback_for_collection_wrapper,
19
21
  )
22
+ from mx_bluesky.hyperion.device_setup_plans import dcm_pitch_roll_mirror_adjuster
20
23
 
21
24
  DESIRED_TRANSMISSION_FRACTION = 0.1
22
25
 
@@ -33,6 +36,17 @@ class SetEnergyComposite:
33
36
  attenuator: BinaryFilterAttenuator
34
37
 
35
38
 
39
+ # Remove composite after https://github.com/DiamondLightSource/dodal/issues/1092
40
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
41
+ class XBPMWrapperComposite:
42
+ undulator: Undulator
43
+ xbpm_feedback: XBPMFeedback
44
+ attenuator: BinaryFilterAttenuator
45
+ dcm: DCM
46
+
47
+
48
+ @bpp.set_run_key_decorator(PlanNameConstants.SET_ENERGY)
49
+ @bpp.run_decorator()
36
50
  def _set_energy_plan(
37
51
  energy_kev,
38
52
  composite: SetEnergyComposite,
@@ -51,12 +65,17 @@ def set_energy_plan(
51
65
  energy_ev: float | None,
52
66
  composite: SetEnergyComposite,
53
67
  ):
68
+ # Remove conversion after https://github.com/DiamondLightSource/dodal/issues/1092
69
+ composite_for_wrapper = XBPMWrapperComposite(
70
+ composite.undulator_dcm.undulator_ref._obj, # noqa: SLF001
71
+ composite.xbpm_feedback,
72
+ composite.attenuator,
73
+ composite.dcm,
74
+ )
75
+
54
76
  if energy_ev:
55
77
  yield from transmission_and_xbpm_feedback_for_collection_wrapper(
56
78
  _set_energy_plan(energy_ev / 1000, composite),
57
- composite.undulator_dcm.undulator_ref(),
58
- composite.xbpm_feedback,
59
- composite.attenuator,
60
- composite.dcm,
79
+ composite_for_wrapper,
61
80
  DESIRED_TRANSMISSION_FRACTION,
62
81
  )
@@ -1,21 +1,52 @@
1
1
  import dataclasses
2
2
  import json
3
3
  import re
4
- from typing import TypeVar
4
+ from os import path
5
+ from typing import Any, TypeVar
5
6
 
6
7
  import requests
8
+ from deepdiff.diff import DeepDiff
7
9
  from dodal.utils import get_beamline_name
8
-
9
- from mx_bluesky.common.parameters.components import WithVisit
10
- from mx_bluesky.common.parameters.constants import GridscanParamConstants
10
+ from jsonschema import ValidationError
11
+ from pydantic_extra_types.semantic_version import SemanticVersion
12
+
13
+ from mx_bluesky.common.parameters.components import (
14
+ PARAMETER_VERSION,
15
+ MxBlueskyParameters,
16
+ TopNByMaxCountSelection,
17
+ WithCentreSelection,
18
+ WithOptionalEnergyChange,
19
+ WithSample,
20
+ WithVisit,
21
+ )
22
+ from mx_bluesky.common.parameters.constants import (
23
+ GridscanParamConstants,
24
+ )
11
25
  from mx_bluesky.common.utils.log import LOGGER
26
+ from mx_bluesky.common.utils.utils import convert_angstrom_to_eV
27
+ from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
12
28
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
29
+ from mx_bluesky.hyperion.parameters.robot_load import RobotLoadThenCentre
13
30
 
14
31
  T = TypeVar("T", bound=WithVisit)
15
32
  AGAMEMNON_URL = "http://agamemnon.diamond.ac.uk/"
16
33
  MULTIPIN_PREFIX = "multipin"
17
34
  MULTIPIN_FORMAT_DESC = "Expected multipin format is multipin_{number_of_wells}x{well_size}+{distance_between_tip_and_first_well}"
18
35
  MULTIPIN_REGEX = rf"^{MULTIPIN_PREFIX}_(\d+)x(\d+(?:\.\d+)?)\+(\d+(?:\.\d+)?)$"
36
+ MX_GENERAL_ROOT_REGEX = r"^/dls/(?P<beamline>[^/]+)/data/[^/]*/(?P<visit>[^/]+)(?:/|$)"
37
+
38
+
39
+ class AgamemnonLoadCentreCollect(
40
+ MxBlueskyParameters,
41
+ WithVisit,
42
+ WithSample,
43
+ WithCentreSelection,
44
+ WithHyperionUDCFeatures,
45
+ WithOptionalEnergyChange,
46
+ ):
47
+ """Experiment parameters to compare against GDA populated LoadCentreCollect."""
48
+
49
+ robot_load_then_centre: RobotLoadThenCentre
19
50
 
20
51
 
21
52
  @dataclasses.dataclass
@@ -60,7 +91,7 @@ def _get_parameters_from_url(url: str) -> dict:
60
91
  raise KeyError(f"Unexpected json from agamemnon: {response_json}") from e
61
92
 
62
93
 
63
- def _get_pin_type_from_agamemnon_parameters(parameters: dict) -> PinType:
94
+ def get_pin_type_from_agamemnon_parameters(parameters: dict) -> PinType:
64
95
  loop_type_name: str | None = parameters["sample"]["loopType"]
65
96
  if loop_type_name:
66
97
  regex_search = re.search(MULTIPIN_REGEX, loop_type_name)
@@ -81,15 +112,119 @@ def get_next_instruction(beamline: str) -> dict:
81
112
  return _get_parameters_from_url(AGAMEMNON_URL + f"getnextcollect/{beamline}")
82
113
 
83
114
 
84
- def get_pin_type_from_agamemnon(beamline: str) -> PinType:
85
- params = get_next_instruction(beamline)
86
- return _get_pin_type_from_agamemnon_parameters(params)
115
+ def get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
116
+ try:
117
+ prefix = parameters["prefix"]
118
+ collection = parameters["collection"]
119
+ # Assuming distance is identical for multiple collections. Remove after https://github.com/DiamondLightSource/mx-bluesky/issues/773
120
+ detector_distance = collection[0]["distance"]
121
+ except KeyError as e:
122
+ raise KeyError("Unexpected json from agamemnon") from e
123
+
124
+ match = re.match(MX_GENERAL_ROOT_REGEX, prefix) if prefix else None
125
+
126
+ if match:
127
+ return (match.group("visit"), detector_distance)
128
+
129
+ raise ValueError(
130
+ f"Agamemnon prefix '{prefix}' does not match MX-General root structure"
131
+ )
132
+
133
+
134
+ def get_withsample_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
135
+ assert parameters.get("sample"), "instruction does not have a sample"
136
+ return {
137
+ "sample_id": parameters["sample"]["id"],
138
+ "sample_puck": parameters["sample"]["container"],
139
+ "sample_pin": parameters["sample"]["position"],
140
+ }
141
+
142
+
143
+ def get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
144
+ try:
145
+ first_collection: dict = parameters["collection"][0]
146
+ wavelength = first_collection.get("wavelength")
147
+ assert isinstance(wavelength, float)
148
+ demand_energy_ev = convert_angstrom_to_eV(wavelength)
149
+ return {"demand_energy_ev": demand_energy_ev}
150
+ except (KeyError, IndexError, AttributeError, TypeError):
151
+ return {"demand_energy_ev": None}
152
+
153
+
154
+ def get_param_version() -> SemanticVersion:
155
+ return SemanticVersion.validate_from_str(str(PARAMETER_VERSION))
156
+
157
+
158
+ def create_robot_load_then_centre_params_from_agamemnon(
159
+ parameters: dict,
160
+ ) -> RobotLoadThenCentre:
161
+ visit, detector_distance = get_withvisit_parameters_from_agamemnon(parameters)
162
+ with_sample_params = get_withsample_parameters_from_agamemnon(parameters)
163
+ with_energy_params = get_withenergy_parameters_from_agamemnon(parameters)
164
+ visit_directory, file_name = path.split(parameters["prefix"])
165
+ return RobotLoadThenCentre(
166
+ parameter_model_version=get_param_version(),
167
+ storage_directory=visit_directory + "/xraycentring",
168
+ visit=visit,
169
+ detector_distance_mm=detector_distance,
170
+ snapshot_directory=visit_directory + "/snapshots",
171
+ file_name=file_name,
172
+ **with_energy_params,
173
+ **with_sample_params,
174
+ )
175
+
176
+
177
+ def populate_parameters_from_agamemnon(agamemnon_params):
178
+ visit, detector_distance = get_withvisit_parameters_from_agamemnon(agamemnon_params)
179
+ with_sample_params = get_withsample_parameters_from_agamemnon(agamemnon_params)
180
+ with_energy_params = get_withenergy_parameters_from_agamemnon(agamemnon_params)
181
+ pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
182
+ robot_load_params = create_robot_load_then_centre_params_from_agamemnon(
183
+ agamemnon_params
184
+ )
185
+ return AgamemnonLoadCentreCollect(
186
+ parameter_model_version=SemanticVersion.validate_from_str(
187
+ str(PARAMETER_VERSION)
188
+ ),
189
+ visit=visit,
190
+ detector_distance_mm=detector_distance,
191
+ select_centres=TopNByMaxCountSelection(n=pin_type.expected_number_of_crystals),
192
+ robot_load_then_centre=robot_load_params,
193
+ **with_sample_params,
194
+ **with_energy_params,
195
+ )
196
+
197
+
198
+ def create_parameters_from_agamemnon() -> AgamemnonLoadCentreCollect:
199
+ beamline_name = get_beamline_name("i03")
200
+ agamemnon_params = get_next_instruction(beamline_name)
201
+
202
+ return populate_parameters_from_agamemnon(agamemnon_params)
203
+
204
+
205
+ def compare_params(load_centre_collect_params):
206
+ try:
207
+ parameters = create_parameters_from_agamemnon()
208
+
209
+ # Log differences against GDA populated parameters
210
+ differences = DeepDiff(
211
+ parameters, load_centre_collect_params, math_epsilon=1e-5
212
+ )
213
+ if differences:
214
+ LOGGER.info(
215
+ f"Different parameters found when directly reading from Hyperion: {differences}"
216
+ )
217
+ except (ValueError, KeyError) as e:
218
+ LOGGER.warning(f"Failed to compare parameters: {e}")
219
+ except Exception as e:
220
+ LOGGER.warning(f"Unexpected error occurred. Failed to compare parameters: {e}")
87
221
 
88
222
 
89
223
  def update_params_from_agamemnon(parameters: T) -> T:
90
224
  try:
91
225
  beamline_name = get_beamline_name("i03")
92
- pin_type = get_pin_type_from_agamemnon(beamline_name)
226
+ agamemnon_params = get_next_instruction(beamline_name)
227
+ pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
93
228
  if isinstance(parameters, LoadCentreCollect):
94
229
  parameters.robot_load_then_centre.tip_offset_um = pin_type.full_width / 2
95
230
  parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
@@ -99,6 +234,9 @@ def update_params_from_agamemnon(parameters: T) -> T:
99
234
  # Before we do https://github.com/DiamondLightSource/mx-bluesky/issues/226
100
235
  # this will give no snapshots but that's preferable
101
236
  parameters.multi_rotation_scan.snapshot_omegas_deg = []
237
+ except (ValueError, ValidationError) as e:
238
+ LOGGER.warning(f"Failed to update parameters: {e}")
102
239
  except Exception as e:
103
- LOGGER.warning(f"Failed to get pin type from agamemnon, using single pin {e}")
240
+ LOGGER.warning(f"Unexpected error occurred. Failed to update parameters: {e}")
241
+
104
242
  return parameters
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  from collections.abc import Callable, Sequence
3
3
  from threading import Thread
4
- from time import sleep
5
4
 
5
+ import bluesky.plan_stubs as bps
6
6
  from bluesky.callbacks import CallbackBase
7
7
  from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
8
8
  from dodal.log import LOGGER as dodal_logger
@@ -14,6 +14,9 @@ from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callbac
14
14
  from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import (
15
15
  ZocaloCallback,
16
16
  )
17
+ from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import (
18
+ SampleHandlingCallback,
19
+ )
17
20
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
18
21
  GridscanISPyBCallback,
19
22
  )
@@ -35,8 +38,8 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback
35
38
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
36
39
  RotationNexusFileCallback,
37
40
  )
38
- from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import (
39
- SampleHandlingCallback,
41
+ from mx_bluesky.hyperion.external_interaction.callbacks.snapshot_callback import (
42
+ BeamDrawingCallback,
40
43
  )
41
44
  from mx_bluesky.hyperion.parameters.cli import parse_callback_dev_mode_arg
42
45
  from mx_bluesky.hyperion.parameters.constants import CONST
@@ -67,15 +70,18 @@ def create_rotation_callbacks() -> tuple[
67
70
  return (
68
71
  RotationNexusFileCallback(),
69
72
  RotationISPyBCallback(
70
- emit=ZocaloCallback(CONST.PLAN.ROTATION_MAIN, CONST.ZOCALO_ENV)
73
+ emit=ZocaloCallback(CONST.PLAN.ROTATION_MULTI, CONST.ZOCALO_ENV)
71
74
  ),
72
75
  )
73
76
 
74
77
 
75
78
  def setup_callbacks() -> list[CallbackBase]:
79
+ rot_nexus_cb, rot_ispyb_cb = create_rotation_callbacks()
80
+ snapshot_cb = BeamDrawingCallback(emit=rot_ispyb_cb)
76
81
  return [
77
82
  *create_gridscan_callbacks(),
78
- *create_rotation_callbacks(),
83
+ rot_nexus_cb,
84
+ snapshot_cb,
79
85
  LogUidTaggingCallback(),
80
86
  RobotLoadISPyBCallback(),
81
87
  SampleHandlingCallback(),
@@ -134,7 +140,7 @@ def wait_for_threads_forever(threads: Sequence[Thread]):
134
140
  try:
135
141
  log_debug("Trying to wait forever on callback and dispatcher threads")
136
142
  while all(alive):
137
- sleep(LIVENESS_POLL_SECONDS)
143
+ yield from bps.sleep(LIVENESS_POLL_SECONDS)
138
144
  alive = [t.is_alive() for t in threads]
139
145
  except KeyboardInterrupt:
140
146
  log_info("Main thread received interrupt - exiting.")