diff --git a/.vscode/hyperion-mx-bluesky.code-workspace b/.vscode/hyperion-mx-bluesky.code-workspace new file mode 100644 index 000000000..3b46937de --- /dev/null +++ b/.vscode/hyperion-mx-bluesky.code-workspace @@ -0,0 +1,23 @@ +{ + "folders": [ + { + "path": ".." + }, + { + "path": "../../dodal" + }, + { + "path": "../../mx-bluesky" + } + ], + "settings": { + "python.languageServer": "Pylance", + "terminal.integrated.gpuAcceleration": "off", + "esbonio.sphinx.confDir": "", + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none", + "python.analysis.enablePytestExtra": false + } +} \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 4c140471c..58dfd5351 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,6 @@ install_requires = pyzmq scanspec scipy - semver # # These dependencies may be issued as pre-release versions and should have a pin constraint # as by default pip-install will not upgrade to a pre-release. @@ -41,6 +40,7 @@ install_requires = ophyd-async >= 0.3a5 bluesky >= 1.13.0a4 blueapi >= 0.4.3-rc1 + mx-bluesky @ git+https://github.com/DiamondLightSource/mx-bluesky.git@0492e92f75eefa8885be59dde3eff323c1ea81c2 dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git [options.entry_points] diff --git a/src/hyperion/experiment_plans/oav_snapshot_plan.py b/src/hyperion/experiment_plans/oav_snapshot_plan.py index e92033c55..9228627be 100644 --- a/src/hyperion/experiment_plans/oav_snapshot_plan.py +++ b/src/hyperion/experiment_plans/oav_snapshot_plan.py @@ -8,9 +8,9 @@ from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.smargon import Smargon +from mx_bluesky.parameters import WithSnapshot from hyperion.device_setup_plans.setup_oav import setup_general_oav_params -from hyperion.parameters.components import WithSnapshot from hyperion.parameters.constants import DocDescriptorNames OAV_SNAPSHOT_SETUP_GROUP = "oav_snapshot_setup" diff --git a/src/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py b/src/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py index eeaa0372d..cdfcb10cc 100644 --- a/src/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +++ b/src/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py @@ -3,6 +3,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Callable, Optional +from mx_bluesky.parameters import IspybExperimentType + from hyperion.external_interaction.callbacks.common.ispyb_mapping import ( populate_data_collection_group, populate_remaining_data_collection_info, @@ -23,7 +25,6 @@ StoreInIspyb, ) from hyperion.log import ISPYB_LOGGER, set_dcgid_tag -from hyperion.parameters.components import IspybExperimentType from hyperion.parameters.constants import CONST from hyperion.parameters.rotation import RotationScan diff --git a/src/hyperion/parameters/components.py b/src/hyperion/parameters/components.py index e5b2d9503..3863b61dc 100644 --- a/src/hyperion/parameters/components.py +++ b/src/hyperion/parameters/components.py @@ -3,19 +3,15 @@ import datetime import json from abc import abstractmethod -from enum import StrEnum from pathlib import Path -from typing import Sequence, SupportsInt, TypeVar +from typing import TypeVar -from dodal.devices.aperturescatterguard import AperturePositionGDANames from dodal.devices.detector import ( - DetectorParams, TriggerMode, ) +from mx_bluesky.parameters import DiffractionExperiment, ParameterVersion, WithSample from numpy.typing import NDArray -from pydantic import BaseModel, Extra, Field, root_validator, validator -from scanspec.core import AxesPoints -from semver import Version +from pydantic import BaseModel, Extra, Field, validator from hyperion.external_interaction.config_server import FeatureFlags from hyperion.external_interaction.ispyb.ispyb_dataclass import ( @@ -26,85 +22,9 @@ T = TypeVar("T") -class ParameterVersion(Version): - @classmethod - def _parse(cls, version): - if isinstance(version, cls): - return version - return cls.parse(version) - - @classmethod - def __get_validators__(cls): - """Return a list of validator methods for pydantic models.""" - yield cls._parse - - @classmethod - def __modify_schema__(cls, field_schema): - """Inject/mutate the pydantic field schema in-place.""" - field_schema.update(examples=["1.0.2", "2.15.3-alpha", "21.3.15-beta+12345"]) - - PARAMETER_VERSION = ParameterVersion.parse("5.0.0") -class RotationAxis(StrEnum): - OMEGA = "omega" - PHI = "phi" - CHI = "chi" - KAPPA = "kappa" - - -class XyzAxis(StrEnum): - X = "sam_x" - Y = "sam_y" - Z = "sam_z" - - -class IspybExperimentType(StrEnum): - # Enum values from ispyb column data type - SAD = "SAD" # at or slightly above the peak - SAD_INVERSE_BEAM = "SAD - Inverse Beam" - OSC = "OSC" # "native" (in the absence of a heavy atom) - COLLECT_MULTIWEDGE = ( - "Collect - Multiwedge" # "poorly determined" ~ EDNA complex strategy??? - ) - MAD = "MAD" - HELICAL = "Helical" - MULTI_POSITIONAL = "Multi-positional" - MESH = "Mesh" - BURN = "Burn" - MAD_INVERSE_BEAM = "MAD - Inverse Beam" - CHARACTERIZATION = "Characterization" - DEHYDRATION = "Dehydration" - TOMO = "tomo" - EXPERIMENT = "experiment" - EM = "EM" - PDF = "PDF" - PDF_BRAGG = "PDF+Bragg" - BRAGG = "Bragg" - SINGLE_PARTICLE = "single particle" - SERIAL_FIXED = "Serial Fixed" - SERIAL_JET = "Serial Jet" - STANDARD = "Standard" # Routine structure determination experiment - TIME_RESOLVED = "Time Resolved" # Investigate the change of a system over time - DLS_ANVIL_HP = "Diamond Anvil High Pressure" # HP sample environment pressure cell - CUSTOM = "Custom" # Special or non-standard data collection - XRF_MAP = "XRF map" - ENERGY_SCAN = "Energy scan" - XRF_SPECTRUM = "XRF spectrum" - XRF_MAP_XAS = "XRF map xas" - MESH_3D = "Mesh3D" - SCREENING = "Screening" - STILL = "Still" - SSX_CHIP = "SSX-Chip" - SSX_JET = "SSX-Jet" - - # Aliases for historic hyperion experiment type mapping - ROTATION = "SAD" - GRIDSCAN_2D = "mesh" - GRIDSCAN_3D = "Mesh3D" - - class HyperionParameters(BaseModel): class Config: arbitrary_types_allowed = True @@ -140,48 +60,18 @@ def from_json(cls, input: str | None, *, allow_extras: bool = False): return params -class WithSnapshot(BaseModel): - snapshot_directory: Path - snapshot_omegas_deg: list[float] | None - - @property - def take_snapshots(self) -> bool: - return bool(self.snapshot_omegas_deg) - - -class DiffractionExperiment(HyperionParameters, WithSnapshot): +class HyperionDiffractionExperiment(DiffractionExperiment, HyperionParameters): """For all experiments which use beam""" - visit: str = Field(min_length=1) - file_name: str = Field(pattern=r"[\w]{2}[\d]+-[\d]+") - exposure_time_s: float = Field(gt=0) - comment: str = Field(default="") - beamline: str = Field(default=CONST.I03.BEAMLINE, pattern=r"BL\d{2}[BIJS]") + beamline: str = Field(default=CONST.I03.BEAMLINE, regex=r"BL\d{2}[BIJS]") insertion_prefix: str = Field( - default=CONST.I03.INSERTION_PREFIX, pattern=r"SR\d{2}[BIJS]" + default=CONST.I03.INSERTION_PREFIX, regex=r"SR\d{2}[BIJS]" ) det_dist_to_beam_converter_path: str = Field( default=CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH ) zocalo_environment: str = Field(default=CONST.ZOCALO_ENV) trigger_mode: TriggerMode = Field(default=TriggerMode.FREE_RUN) - detector_distance_mm: float | None = Field(default=None, gt=0) - demand_energy_ev: float | None = Field(default=None, gt=0) - run_number: int | None = Field(default=None, ge=0) - selected_aperture: AperturePositionGDANames | None = Field(default=None) - transmission_frac: float = Field(default=0.1) - ispyb_experiment_type: IspybExperimentType - storage_directory: str - - @root_validator(pre=True) - def validate_snapshot_directory(cls, values): - snapshot_dir = values.get( - "snapshot_directory", Path(values["storage_directory"], "snapshots") - ) - values["snapshot_directory"] = ( - snapshot_dir if isinstance(snapshot_dir, Path) else Path(snapshot_dir) - ) - return values @property def visit_directory(self) -> Path: @@ -189,79 +79,13 @@ def visit_directory(self) -> Path: Path(CONST.I03.BASE_DATA_DIR) / str(datetime.date.today().year) / self.visit ) - @property - def num_images(self) -> int: - return 0 - - @property - @abstractmethod - def detector_params(self) -> DetectorParams: ... - @property @abstractmethod def ispyb_params(self) -> IspybParams: # Soon to remove ... -class WithScan(BaseModel): - """For experiments where the scan is known""" - - @property - @abstractmethod - def scan_points(self) -> AxesPoints: ... - - @property - @abstractmethod - def num_images(self) -> int: ... - - -class SplitScan(BaseModel): - @property - @abstractmethod - def scan_indices(self) -> Sequence[SupportsInt]: - """Should return the first index of each scan (i.e. for each nexus file)""" - ... - - -class WithSample(BaseModel): - sample_id: int - sample_puck: int | None = None - sample_pin: int | None = None - - -class DiffractionExperimentWithSample(DiffractionExperiment, WithSample): ... - - -class WithOavCentring(BaseModel): - oav_centring_file: str = Field(default=CONST.I03.OAV_CENTRING_FILE) - - -class OptionalXyzStarts(BaseModel): - x_start_um: float | None = None - y_start_um: float | None = None - z_start_um: float | None = None - - -class XyzStarts(BaseModel): - x_start_um: float - y_start_um: float - z_start_um: float - - def _start_for_axis(self, axis: XyzAxis) -> float: - match axis: - case XyzAxis.X: - return self.x_start_um - case XyzAxis.Y: - return self.y_start_um - case XyzAxis.Z: - return self.z_start_um - - -class OptionalGonioAngleStarts(BaseModel): - omega_start_deg: float | None = None - phi_start_deg: float | None = None - chi_start_deg: float | None = None - kappa_start_deg: float | None = None +class DiffractionExperimentWithSample(HyperionDiffractionExperiment, WithSample): ... class TemporaryIspybExtras(BaseModel): diff --git a/src/hyperion/parameters/gridscan.py b/src/hyperion/parameters/gridscan.py index 778b01c90..5b2c97ba0 100644 --- a/src/hyperion/parameters/gridscan.py +++ b/src/hyperion/parameters/gridscan.py @@ -11,15 +11,7 @@ PandAGridScanParams, ZebraGridScanParams, ) -from pydantic import Field, PrivateAttr -from scanspec.core import Path as ScanPath -from scanspec.specs import Line, Static - -from hyperion.external_interaction.ispyb.ispyb_dataclass import ( - GridscanIspybParams, -) -from hyperion.parameters.components import ( - DiffractionExperimentWithSample, +from mx_bluesky.parameters import ( IspybExperimentType, OptionalGonioAngleStarts, SplitScan, @@ -27,12 +19,21 @@ WithScan, XyzStarts, ) +from pydantic import Field, PrivateAttr +from scanspec.core import Path as ScanPath +from scanspec.specs import Line, Static + +from hyperion.external_interaction.ispyb.ispyb_dataclass import ( + GridscanIspybParams, +) +from hyperion.parameters.components import DiffractionExperimentWithSample from hyperion.parameters.constants import CONST, I03Constants class GridCommon( DiffractionExperimentWithSample, OptionalGonioAngleStarts, WithOavCentring ): + oav_centring_file: str = Field(default=CONST.I03.OAV_CENTRING_FILE) grid_width_um: float = Field(default=CONST.PARAM.GRIDSCAN.WIDTH_UM) exposure_time_s: float = Field(default=CONST.PARAM.GRIDSCAN.EXPOSURE_TIME_S) use_roi_mode: bool = Field(default=CONST.PARAM.GRIDSCAN.USE_ROI) diff --git a/src/hyperion/parameters/rotation.py b/src/hyperion/parameters/rotation.py index ce59b2988..01d8c6eae 100644 --- a/src/hyperion/parameters/rotation.py +++ b/src/hyperion/parameters/rotation.py @@ -13,6 +13,14 @@ from dodal.devices.zebra import ( RotationDirection, ) +from mx_bluesky.parameters import ( + IspybExperimentType, + OptionalGonioAngleStarts, + OptionalXyzStarts, + RotationAxis, + SplitScan, + WithScan, +) from pydantic import Field, root_validator from scanspec.core import AxesPoints from scanspec.core import Path as ScanPath @@ -21,13 +29,7 @@ from hyperion.external_interaction.ispyb.ispyb_dataclass import RotationIspybParams from hyperion.parameters.components import ( DiffractionExperimentWithSample, - IspybExperimentType, - OptionalGonioAngleStarts, - OptionalXyzStarts, - RotationAxis, - SplitScan, TemporaryIspybExtras, - WithScan, ) from hyperion.parameters.constants import CONST, I03Constants diff --git a/tests/system_tests/external_interaction/test_ispyb_dev_connection.py b/tests/system_tests/external_interaction/test_ispyb_dev_connection.py index 97ab98451..79a2ae33c 100644 --- a/tests/system_tests/external_interaction/test_ispyb_dev_connection.py +++ b/tests/system_tests/external_interaction/test_ispyb_dev_connection.py @@ -12,6 +12,7 @@ from bluesky.run_engine import RunEngine from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.synchrotron import SynchrotronMode +from mx_bluesky.parameters import IspybExperimentType from ophyd.sim import NullStatus from ophyd_async.core import AsyncStatus, set_mock_value @@ -48,7 +49,6 @@ IspybIds, StoreInIspyb, ) -from hyperion.parameters.components import IspybExperimentType from hyperion.parameters.constants import CONST from hyperion.parameters.gridscan import GridScanWithEdgeDetect, ThreeDGridScan from hyperion.parameters.rotation import RotationScan @@ -446,9 +446,7 @@ def composite_for_rotation_scan(fake_create_rotation_devices: RotationScanCompos fake_create_rotation_devices.dcm.energy_in_kev.user_readback, energy_ev / 1000, # pyright: ignore ) - set_mock_value( - fake_create_rotation_devices.undulator.current_gap, 1.12 - ) # pyright: ignore + set_mock_value(fake_create_rotation_devices.undulator.current_gap, 1.12) # pyright: ignore set_mock_value( fake_create_rotation_devices.synchrotron.synchrotron_mode, SynchrotronMode.USER, diff --git a/tests/unit_tests/experiment_plans/test_oav_snapshot_plan.py b/tests/unit_tests/experiment_plans/test_oav_snapshot_plan.py index f01287444..dd0f22ac9 100644 --- a/tests/unit_tests/experiment_plans/test_oav_snapshot_plan.py +++ b/tests/unit_tests/experiment_plans/test_oav_snapshot_plan.py @@ -10,13 +10,13 @@ from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.oav.utils import ColorMode from dodal.devices.smargon import Smargon +from mx_bluesky.parameters import WithSnapshot from hyperion.experiment_plans.oav_snapshot_plan import ( OAV_SNAPSHOT_SETUP_SHOT, OavSnapshotComposite, oav_snapshot_plan, ) -from hyperion.parameters.components import WithSnapshot from hyperion.parameters.constants import DocDescriptorNames from ...conftest import raw_params_from_file diff --git a/tests/unit_tests/external_interaction/callbacks/test_rotation_callbacks.py b/tests/unit_tests/external_interaction/callbacks/test_rotation_callbacks.py index f8efd8967..d3f3b057b 100644 --- a/tests/unit_tests/external_interaction/callbacks/test_rotation_callbacks.py +++ b/tests/unit_tests/external_interaction/callbacks/test_rotation_callbacks.py @@ -11,6 +11,7 @@ from dodal.devices.eiger import EigerDetector from dodal.devices.flux import Flux from event_model import RunStart +from mx_bluesky.parameters import IspybExperimentType from ophyd.sim import make_fake_device from ophyd_async.core import DeviceCollector, set_mock_value @@ -38,7 +39,6 @@ IspybIds, StoreInIspyb, ) -from hyperion.parameters.components import IspybExperimentType from hyperion.parameters.constants import CONST from hyperion.parameters.rotation import RotationScan from hyperion.utils.aperturescatterguard import ( diff --git a/tests/unit_tests/parameters/test_parameter_model.py b/tests/unit_tests/parameters/test_parameter_model.py index b5a60869a..a4ea14c5e 100644 --- a/tests/unit_tests/parameters/test_parameter_model.py +++ b/tests/unit_tests/parameters/test_parameter_model.py @@ -22,7 +22,7 @@ def minimal_3d_gridscan_params(): "y_start_um": 0.777, "z_start_um": 0.05, "parameter_model_version": "5.0.0", - "visit": "cm12345", + "visit": "cm12345-12", "file_name": "test_file_name", "y2_start_um": 2, "z2_start_um": 2, @@ -53,7 +53,7 @@ def test_serialise_deserialise(minimal_3d_gridscan_params): serialised = json.loads(test_params.json()) deserialised = ThreeDGridScan(**serialised) assert deserialised.demand_energy_ev is None - assert deserialised.visit == "cm12345" + assert deserialised.visit == "cm12345-12" assert deserialised.x_start_um == 0.123 @@ -76,7 +76,7 @@ def test_robot_load_then_centre_params(): params = { "parameter_model_version": "5.0.0", "sample_id": 123456, - "visit": "cm12345", + "visit": "cm12345-12", "file_name": "file_name", "storage_directory": "/tmp/dls/i03/data/2024/cm31105-4/xraycentring/123456/", }