mx-bluesky 1.2.0__py3-none-any.whl → 1.4.1__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 (105) 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/redis_to_murko_forwarder.py +178 -0
  6. mx_bluesky/beamlines/i04/thawing_plan.py +49 -11
  7. mx_bluesky/beamlines/i24/serial/__init__.py +3 -0
  8. mx_bluesky/beamlines/i24/serial/dcid.py +143 -171
  9. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +1 -1
  10. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +121 -110
  11. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +3 -6
  12. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  13. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +164 -169
  14. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +149 -225
  15. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +7 -216
  16. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +18 -17
  17. mx_bluesky/beamlines/i24/serial/log.py +58 -49
  18. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +4 -0
  19. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -1
  20. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +42 -15
  21. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +3 -3
  22. mx_bluesky/beamlines/i24/serial/run_extruder.sh +30 -5
  23. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +30 -5
  24. mx_bluesky/beamlines/i24/serial/run_serial.py +24 -8
  25. mx_bluesky/beamlines/i24/serial/setup_beamline/ca.py +0 -2
  26. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  27. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +104 -82
  28. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +9 -20
  29. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +26 -28
  30. mx_bluesky/beamlines/i24/serial/write_nexus.py +74 -72
  31. mx_bluesky/common/__init__.py +0 -0
  32. mx_bluesky/common/device_setup_plans/read_hardware_for_setup.py +14 -0
  33. mx_bluesky/common/external_interaction/config_server.py +46 -0
  34. mx_bluesky/common/parameters/components.py +258 -0
  35. mx_bluesky/common/parameters/constants.py +143 -0
  36. mx_bluesky/common/parameters/gridscan.py +94 -0
  37. mx_bluesky/common/parameters/robot_load.py +16 -0
  38. mx_bluesky/common/plans/__init__.py +1 -0
  39. mx_bluesky/common/plans/do_fgs.py +121 -0
  40. mx_bluesky/common/utils/log.py +118 -0
  41. mx_bluesky/{hyperion → common/utils}/tracing.py +2 -2
  42. mx_bluesky/hyperion/__main__.py +13 -10
  43. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +47 -52
  44. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +6 -12
  45. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +6 -12
  46. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -6
  47. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +49 -18
  48. mx_bluesky/hyperion/device_setup_plans/smargon.py +9 -9
  49. mx_bluesky/hyperion/device_setup_plans/utils.py +2 -2
  50. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +4 -4
  51. mx_bluesky/hyperion/exceptions.py +13 -1
  52. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  53. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  54. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  55. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  56. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +147 -169
  57. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +48 -22
  58. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +75 -9
  59. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +21 -20
  60. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +9 -6
  61. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  62. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +40 -21
  63. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +22 -22
  64. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +43 -39
  65. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +69 -18
  66. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +17 -7
  67. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +13 -13
  68. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  69. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -2
  70. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  71. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  72. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  73. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +30 -25
  74. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +29 -12
  75. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +1 -1
  76. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +19 -11
  77. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +7 -4
  78. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  79. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  80. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  81. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +38 -27
  82. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  83. mx_bluesky/hyperion/external_interaction/config_server.py +11 -28
  84. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  85. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  86. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +1 -1
  87. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +2 -2
  88. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +1 -1
  89. mx_bluesky/hyperion/log.py +0 -84
  90. mx_bluesky/hyperion/parameters/components.py +4 -251
  91. mx_bluesky/hyperion/parameters/constants.py +22 -119
  92. mx_bluesky/hyperion/parameters/gridscan.py +35 -74
  93. mx_bluesky/hyperion/parameters/load_centre_collect.py +16 -11
  94. mx_bluesky/hyperion/parameters/rotation.py +23 -10
  95. mx_bluesky/hyperion/utils/utils.py +17 -0
  96. mx_bluesky/hyperion/utils/validation.py +5 -6
  97. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/METADATA +36 -33
  98. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/RECORD +102 -89
  99. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/WHEEL +1 -1
  100. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -161
  101. mx_bluesky/example.py +0 -19
  102. mx_bluesky/hyperion/parameters/robot_load.py +0 -16
  103. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/LICENSE +0 -0
  104. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/entry_points.txt +0 -0
  105. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,6 @@
1
1
  import configparser
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
2
4
 
3
5
  from requests import patch, post
4
6
  from requests.auth import AuthBase
@@ -29,20 +31,44 @@ def _get_base_url_and_token() -> tuple[str, str]:
29
31
  return expeye_config["url"], expeye_config["token"]
30
32
 
31
33
 
34
+ def _send_and_get_response(auth, url, data, send_func) -> dict:
35
+ response = send_func(url, auth=auth, json=data)
36
+ if not response.ok:
37
+ raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
38
+ return response.json()
39
+
40
+
41
+ @dataclass
42
+ class BLSample:
43
+ container_id: int
44
+ bl_sample_id: int
45
+ bl_sample_status: str | None
46
+
47
+
48
+ class BLSampleStatus(StrEnum):
49
+ # The sample has been loaded
50
+ LOADED = "LOADED"
51
+ # Problem with the sample e.g. pin too long/short
52
+ ERROR_SAMPLE = "ERROR - sample"
53
+ # Any other general error
54
+ ERROR_BEAMLINE = "ERROR - beamline"
55
+
56
+
57
+ assert all(
58
+ len(value) <= 20 for value in BLSampleStatus
59
+ ), "Column size limit of 20 for BLSampleStatus"
60
+
61
+
32
62
  class ExpeyeInteraction:
63
+ """Exposes functionality from the Expeye core API"""
64
+
33
65
  CREATE_ROBOT_ACTION = "/proposals/{proposal}/sessions/{visit_number}/robot-actions"
34
66
  UPDATE_ROBOT_ACTION = "/robot-actions/{action_id}"
35
67
 
36
68
  def __init__(self) -> None:
37
69
  url, token = _get_base_url_and_token()
38
- self.base_url = url + "/core"
39
- self.auth = BearerAuth(token)
40
-
41
- def _send_and_get_response(self, url, data, send_func) -> dict:
42
- response = send_func(url, auth=self.auth, json=data)
43
- if not response.ok:
44
- raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
45
- return response.json()
70
+ self._base_url = url
71
+ self._auth = BearerAuth(token)
46
72
 
47
73
  def start_load(
48
74
  self,
@@ -66,7 +92,7 @@ class ExpeyeInteraction:
66
92
  Returns:
67
93
  RobotActionID: The id of the robot load action that is created
68
94
  """
69
- url = self.base_url + self.CREATE_ROBOT_ACTION.format(
95
+ url = self._base_url + self.CREATE_ROBOT_ACTION.format(
70
96
  proposal=proposal_reference, visit_number=visit_number
71
97
  )
72
98
 
@@ -77,7 +103,7 @@ class ExpeyeInteraction:
77
103
  "containerLocation": container_location,
78
104
  "dewarLocation": dewar_location,
79
105
  }
80
- response = self._send_and_get_response(url, data, post)
106
+ response = _send_and_get_response(self._auth, url, data, post)
81
107
  return response["robotActionId"]
82
108
 
83
109
  def update_barcode_and_snapshots(
@@ -95,14 +121,14 @@ class ExpeyeInteraction:
95
121
  snapshot_before_path (str): Path to the snapshot before robot load
96
122
  snapshot_after_path (str): Path to the snapshot after robot load
97
123
  """
98
- url = self.base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
124
+ url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
99
125
 
100
126
  data = {
101
127
  "sampleBarcode": barcode,
102
128
  "xtalSnapshotBefore": snapshot_before_path,
103
129
  "xtalSnapshotAfter": snapshot_after_path,
104
130
  }
105
- self._send_and_get_response(url, data, patch)
131
+ _send_and_get_response(self._auth, url, data, patch)
106
132
 
107
133
  def end_load(self, action_id: RobotActionID, status: str, reason: str):
108
134
  """Finish an existing robot action, providing final information about how it went
@@ -113,13 +139,37 @@ class ExpeyeInteraction:
113
139
  otherwise error
114
140
  reason (str): If the status is in error than the reason for that error
115
141
  """
116
- url = self.base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
142
+ url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
117
143
 
118
144
  run_status = "SUCCESS" if status == "success" else "ERROR"
119
145
 
120
146
  data = {
121
147
  "endTimestamp": get_current_time_string(),
122
148
  "status": run_status,
123
- "message": reason,
149
+ "message": reason[:255] if reason else "",
124
150
  }
125
- self._send_and_get_response(url, data, patch)
151
+ _send_and_get_response(self._auth, url, data, patch)
152
+
153
+ def update_sample_status(
154
+ self, bl_sample_id: int, bl_sample_status: BLSampleStatus
155
+ ) -> BLSample:
156
+ """Update the blSampleStatus of a sample.
157
+ Args:
158
+ bl_sample_id: The sample ID
159
+ bl_sample_status: The sample status
160
+ status_message: An optional message
161
+ Returns:
162
+ The updated sample
163
+ """
164
+ data = {"blSampleStatus": (str(bl_sample_status))}
165
+ response = _send_and_get_response(
166
+ self._auth, self._base_url + f"/samples/{bl_sample_id}", data, patch
167
+ )
168
+ return self._sample_from_json(response)
169
+
170
+ def _sample_from_json(self, response) -> BLSample:
171
+ return BLSample(
172
+ bl_sample_id=response["blSampleId"],
173
+ bl_sample_status=response["blSampleStatus"],
174
+ container_id=response["containerId"],
175
+ )
@@ -11,6 +11,7 @@ from ispyb.sp.mxacquisition import MXAcquisition
11
11
  from ispyb.strictordereddict import StrictOrderedDict
12
12
  from pydantic import BaseModel
13
13
 
14
+ from mx_bluesky.common.utils.tracing import TRACER
14
15
  from mx_bluesky.hyperion.external_interaction.ispyb.data_model import (
15
16
  DataCollectionGridInfo,
16
17
  DataCollectionGroupInfo,
@@ -22,7 +23,6 @@ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import (
22
23
  get_session_id_from_visit,
23
24
  )
24
25
  from mx_bluesky.hyperion.log import ISPYB_LOGGER
25
- from mx_bluesky.hyperion.tracing import TRACER
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  pass
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
- from datetime import datetime, timedelta
4
+ from datetime import UTC, datetime, timedelta
5
5
 
6
6
  import numpy as np
7
7
  from dodal.devices.detector import DetectorParams
@@ -93,7 +93,7 @@ def create_goniometer_axes(
93
93
 
94
94
  def get_start_and_predicted_end_time(time_expected: float) -> tuple[str, str]:
95
95
  time_format = r"%Y-%m-%dT%H:%M:%SZ"
96
- start = datetime.utcfromtimestamp(time.time())
96
+ start = datetime.fromtimestamp(time.time(), tz=UTC)
97
97
  end_est = start + timedelta(seconds=time_expected)
98
98
  return start.strftime(time_format), end_est.strftime(time_format)
99
99
 
@@ -15,12 +15,12 @@ from nexgen.nxs_write.nxmx_writer import NXmxFileWriter
15
15
  from numpy.typing import DTypeLike
16
16
  from scanspec.core import AxesPoints
17
17
 
18
+ from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
18
19
  from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import (
19
20
  create_detector_parameters,
20
21
  create_goniometer_axes,
21
22
  get_start_and_predicted_end_time,
22
23
  )
23
- from mx_bluesky.hyperion.parameters.components import DiffractionExperimentWithSample
24
24
 
25
25
 
26
26
  class NexusWriter:
@@ -1,23 +1,10 @@
1
1
  import logging
2
- from logging.handlers import TimedRotatingFileHandler
3
- from os import environ
4
- from pathlib import Path
5
2
 
6
- from dodal.log import (
7
- ERROR_LOG_BUFFER_LINES,
8
- CircularMemoryHandler,
9
- DodalLogHandlers,
10
- integrate_bluesky_and_ophyd_logging,
11
- set_up_all_logging_handlers,
12
- )
13
3
  from dodal.log import LOGGER as dodal_logger
14
4
 
15
- from mx_bluesky.hyperion.parameters.constants import CONST
16
-
17
5
  LOGGER = logging.getLogger("Hyperion")
18
6
  LOGGER.setLevel("DEBUG")
19
7
  LOGGER.parent = dodal_logger
20
- __logger_handlers: DodalLogHandlers | None = None
21
8
 
22
9
  ISPYB_LOGGER = logging.getLogger("Hyperion ISPyB and Zocalo callbacks")
23
10
  ISPYB_LOGGER.setLevel(logging.DEBUG)
@@ -26,74 +13,3 @@ NEXUS_LOGGER = logging.getLogger("Hyperion NeXus callbacks")
26
13
  NEXUS_LOGGER.setLevel(logging.DEBUG)
27
14
 
28
15
  ALL_LOGGERS = [LOGGER, ISPYB_LOGGER, NEXUS_LOGGER]
29
-
30
-
31
- class ExperimentMetadataTagFilter(logging.Filter):
32
- dc_group_id: str | None = None
33
- run_uid: str | None = None
34
-
35
- def filter(self, record):
36
- if self.dc_group_id:
37
- record.dc_group_id = self.dc_group_id
38
- if self.run_uid:
39
- record.run_uid = self.run_uid
40
- return True
41
-
42
-
43
- tag_filter = ExperimentMetadataTagFilter()
44
-
45
-
46
- def set_dcgid_tag(dcgid):
47
- """Set the datacollection group id as a tag on all subsequent log messages.
48
- Setting to None will remove the tag."""
49
- tag_filter.dc_group_id = dcgid
50
-
51
-
52
- def set_uid_tag(uid):
53
- tag_filter.run_uid = uid
54
-
55
-
56
- def do_default_logging_setup(dev_mode=False):
57
- handlers = set_up_all_logging_handlers(
58
- dodal_logger,
59
- _get_logging_dir(),
60
- "hyperion.log",
61
- dev_mode,
62
- ERROR_LOG_BUFFER_LINES,
63
- CONST.GRAYLOG_PORT,
64
- )
65
- integrate_bluesky_and_ophyd_logging(dodal_logger)
66
- handlers["graylog_handler"].addFilter(tag_filter)
67
-
68
- global __logger_handlers
69
- __logger_handlers = handlers
70
-
71
-
72
- def _get_debug_handler() -> CircularMemoryHandler:
73
- assert (
74
- __logger_handlers is not None
75
- ), "You can only use this after running the default logging setup"
76
- return __logger_handlers["debug_memory_handler"]
77
-
78
-
79
- def flush_debug_handler() -> str:
80
- """Writes the contents of the circular debug log buffer to disk and returns the written filename"""
81
- handler = _get_debug_handler()
82
- assert isinstance(
83
- handler.target, TimedRotatingFileHandler
84
- ), "Circular memory handler doesn't have an appropriate fileHandler target"
85
- handler.flush()
86
- return handler.target.baseFilename
87
-
88
-
89
- def _get_logging_dir() -> Path:
90
- """Get the path to write the hyperion log files to.
91
-
92
- If the HYPERION_LOG_DIR environment variable exists then logs will be put in here.
93
- If no environment variable is found it will default it to the ./tmp/dev directory.
94
-
95
- Returns:
96
- logging_path (Path): Path to the log file for the file handler to write to.
97
- """
98
- logging_path = Path(environ.get("HYPERION_LOG_DIR") or "./tmp/dev/")
99
- return logging_path
@@ -1,254 +1,7 @@
1
- from __future__ import annotations
1
+ from pydantic import BaseModel, Field
2
2
 
3
- import json
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 SupportsInt, TypeVar
3
+ from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags
9
4
 
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_serializer,
20
- field_validator,
21
- model_validator,
22
- )
23
- from scanspec.core import AxesPoints
24
- from semver import Version
25
5
 
26
- from mx_bluesky.hyperion.external_interaction.config_server import FeatureFlags
27
- from mx_bluesky.hyperion.parameters.constants import CONST
28
-
29
- T = TypeVar("T")
30
-
31
-
32
- class ParameterVersion(Version):
33
- @classmethod
34
- def _parse(cls, version):
35
- if isinstance(version, cls):
36
- return version
37
- return cls.parse(version)
38
-
39
-
40
- PARAMETER_VERSION = ParameterVersion.parse("5.1.0")
41
-
42
-
43
- class RotationAxis(StrEnum):
44
- OMEGA = "omega"
45
- PHI = "phi"
46
- CHI = "chi"
47
- KAPPA = "kappa"
48
-
49
-
50
- class XyzAxis(StrEnum):
51
- X = "sam_x"
52
- Y = "sam_y"
53
- Z = "sam_z"
54
-
55
-
56
- class IspybExperimentType(StrEnum):
57
- # Enum values from ispyb column data type
58
- SAD = "SAD" # at or slightly above the peak
59
- SAD_INVERSE_BEAM = "SAD - Inverse Beam"
60
- OSC = "OSC" # "native" (in the absence of a heavy atom)
61
- COLLECT_MULTIWEDGE = (
62
- "Collect - Multiwedge" # "poorly determined" ~ EDNA complex strategy???
63
- )
64
- MAD = "MAD"
65
- HELICAL = "Helical"
66
- MULTI_POSITIONAL = "Multi-positional"
67
- MESH = "Mesh"
68
- BURN = "Burn"
69
- MAD_INVERSE_BEAM = "MAD - Inverse Beam"
70
- CHARACTERIZATION = "Characterization"
71
- DEHYDRATION = "Dehydration"
72
- TOMO = "tomo"
73
- EXPERIMENT = "experiment"
74
- EM = "EM"
75
- PDF = "PDF"
76
- PDF_BRAGG = "PDF+Bragg"
77
- BRAGG = "Bragg"
78
- SINGLE_PARTICLE = "single particle"
79
- SERIAL_FIXED = "Serial Fixed"
80
- SERIAL_JET = "Serial Jet"
81
- STANDARD = "Standard" # Routine structure determination experiment
82
- TIME_RESOLVED = "Time Resolved" # Investigate the change of a system over time
83
- DLS_ANVIL_HP = "Diamond Anvil High Pressure" # HP sample environment pressure cell
84
- CUSTOM = "Custom" # Special or non-standard data collection
85
- XRF_MAP = "XRF map"
86
- ENERGY_SCAN = "Energy scan"
87
- XRF_SPECTRUM = "XRF spectrum"
88
- XRF_MAP_XAS = "XRF map xas"
89
- MESH_3D = "Mesh3D"
90
- SCREENING = "Screening"
91
- STILL = "Still"
92
- SSX_CHIP = "SSX-Chip"
93
- SSX_JET = "SSX-Jet"
94
-
95
- # Aliases for historic hyperion experiment type mapping
96
- ROTATION = "SAD"
97
- GRIDSCAN_2D = "mesh"
98
- GRIDSCAN_3D = "Mesh3D"
99
-
100
-
101
- class HyperionParameters(BaseModel):
102
- model_config = ConfigDict(
103
- arbitrary_types_allowed=True,
104
- extra="allow",
105
- )
106
-
107
- def __hash__(self) -> int:
108
- return self.json().__hash__()
109
-
110
- features: FeatureFlags = Field(default=FeatureFlags())
111
- parameter_model_version: ParameterVersion
112
-
113
- @field_serializer("parameter_model_version")
114
- def serialize_parameter_version(self, version: ParameterVersion):
115
- return str(version)
116
-
117
- @field_validator("parameter_model_version", mode="before")
118
- @classmethod
119
- def _validate_version(cls, version_str: str):
120
- version = ParameterVersion.parse(version_str)
121
- assert (
122
- version >= ParameterVersion(major=PARAMETER_VERSION.major)
123
- ), f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}"
124
- assert (
125
- version <= ParameterVersion(major=PARAMETER_VERSION.major + 1)
126
- ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
127
- return version
128
-
129
- @classmethod
130
- def from_json(cls, input: str | None):
131
- assert input is not None
132
- return cls(**json.loads(input))
133
-
134
-
135
- class WithSnapshot(BaseModel):
136
- snapshot_directory: Path
137
- snapshot_omegas_deg: list[float] | None = None
138
-
139
- @property
140
- def take_snapshots(self) -> bool:
141
- return bool(self.snapshot_omegas_deg)
142
-
143
-
144
- class WithOptionalEnergyChange(BaseModel):
145
- demand_energy_ev: float | None = Field(default=None, gt=0)
146
-
147
-
148
- class WithVisit(BaseModel):
149
- visit: str = Field(min_length=1)
150
- zocalo_environment: str = Field(default=CONST.ZOCALO_ENV)
151
- beamline: str = Field(default=CONST.I03.BEAMLINE, pattern=r"BL\d{2}[BIJS]")
152
- det_dist_to_beam_converter_path: str = Field(
153
- default=CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH
154
- )
155
- insertion_prefix: str = Field(
156
- default=CONST.I03.INSERTION_PREFIX, pattern=r"SR\d{2}[BIJS]"
157
- )
158
- detector_distance_mm: float | None = Field(default=None, gt=0)
159
-
160
-
161
- class DiffractionExperiment(
162
- HyperionParameters, WithSnapshot, WithOptionalEnergyChange, WithVisit
163
- ):
164
- """For all experiments which use beam"""
165
-
166
- file_name: str
167
- exposure_time_s: float = Field(gt=0)
168
- comment: str = Field(default="")
169
- trigger_mode: TriggerMode = Field(default=TriggerMode.FREE_RUN)
170
- run_number: int | None = Field(default=None, ge=0)
171
- selected_aperture: ApertureValue | None = Field(default=None)
172
- transmission_frac: float = Field(default=0.1)
173
- ispyb_experiment_type: IspybExperimentType
174
- storage_directory: str
175
-
176
- @model_validator(mode="before")
177
- @classmethod
178
- def validate_snapshot_directory(cls, values):
179
- snapshot_dir = values.get(
180
- "snapshot_directory", Path(values["storage_directory"], "snapshots")
181
- )
182
- values["snapshot_directory"] = (
183
- snapshot_dir if isinstance(snapshot_dir, Path) else Path(snapshot_dir)
184
- )
185
- return values
186
-
187
- @property
188
- def num_images(self) -> int:
189
- return 0
190
-
191
- @property
192
- @abstractmethod
193
- def detector_params(self) -> DetectorParams: ...
194
-
195
-
196
- class WithScan(BaseModel):
197
- """For experiments where the scan is known"""
198
-
199
- @property
200
- @abstractmethod
201
- def scan_points(self) -> AxesPoints: ...
202
-
203
- @property
204
- @abstractmethod
205
- def num_images(self) -> int: ...
206
-
207
-
208
- class SplitScan(BaseModel):
209
- @property
210
- @abstractmethod
211
- def scan_indices(self) -> Sequence[SupportsInt]:
212
- """Should return the first index of each scan (i.e. for each nexus file)"""
213
- ...
214
-
215
-
216
- class WithSample(BaseModel):
217
- sample_id: int
218
- sample_puck: int | None = None
219
- sample_pin: int | None = None
220
-
221
-
222
- class DiffractionExperimentWithSample(DiffractionExperiment, WithSample): ...
223
-
224
-
225
- class WithOavCentring(BaseModel):
226
- oav_centring_file: str = Field(default=CONST.I03.OAV_CENTRING_FILE)
227
-
228
-
229
- class OptionalXyzStarts(BaseModel):
230
- x_start_um: float | None = None
231
- y_start_um: float | None = None
232
- z_start_um: float | None = None
233
-
234
-
235
- class XyzStarts(BaseModel):
236
- x_start_um: float
237
- y_start_um: float
238
- z_start_um: float
239
-
240
- def _start_for_axis(self, axis: XyzAxis) -> float:
241
- match axis:
242
- case XyzAxis.X:
243
- return self.x_start_um
244
- case XyzAxis.Y:
245
- return self.y_start_um
246
- case XyzAxis.Z:
247
- return self.z_start_um
248
-
249
-
250
- class OptionalGonioAngleStarts(BaseModel):
251
- omega_start_deg: float | None = None
252
- phi_start_deg: float | None = None
253
- chi_start_deg: float | None = None
254
- kappa_start_deg: float | None = None
6
+ class WithHyperionFeatures(BaseModel):
7
+ features: HyperionFeatureFlags = Field(default=HyperionFeatureFlags())