diff --git a/src/test_rig_bluesky/plans.py b/src/test_rig_bluesky/plans.py index edad977..d1ac5c4 100644 --- a/src/test_rig_bluesky/plans.py +++ b/src/test_rig_bluesky/plans.py @@ -1,5 +1,6 @@ import logging import math +from enum import StrEnum from pathlib import Path from typing import Any @@ -38,6 +39,14 @@ LOGGER = logging.getLogger(__name__) + +class ScanType(StrEnum): + """The type of scan to perform.""" + + FLY = "fly" + STEP = "step" + + pmac = inject("pmac") pandabrick = inject("pandabrick") diff --git a/src/test_rig_bluesky/spectroscopy_plans.py b/src/test_rig_bluesky/spectroscopy_plans.py index 3176140..bbf168e 100644 --- a/src/test_rig_bluesky/spectroscopy_plans.py +++ b/src/test_rig_bluesky/spectroscopy_plans.py @@ -21,7 +21,7 @@ from ophyd_async.fastcs.panda import HDFPanda from scanspec.specs import Line, Spec -from .plans import fly_scan, load_settings, pandabrick, pmac, serialize_spec +from .plans import ScanType, fly_scan, load_settings, pandabrick, pmac, serialize_spec LOGGER = logging.getLogger(__name__) @@ -46,10 +46,12 @@ def spectroscopy( pandabrick: HDFPanda = pandabrick, spec: Spec[Movable] | None = None, exposure_time: float = 0.1, - fly: bool = True, metadata: dict[str, Any] | None = None, + *, + scan_type: ScanType, ) -> MsgGenerator[None]: """Do a spectroscopy scan.""" + yield from load_settings( device=spectroscopy_detector, design_name="spectroscopy_detector_baseline", @@ -132,7 +134,7 @@ def spectroscopy( metadata = {} metadata["spec"] = serialize_spec(spec) - if fly: + if scan_type == ScanType.FLY: LOGGER.info("Performing a fly scan.") yield from fly_scan( spec, @@ -144,7 +146,7 @@ def spectroscopy( panda_whitelist=PANDA_WHITELIST, metadata=metadata, ) - else: + elif scan_type == ScanType.STEP: LOGGER.info("Performing a step scan.") yield from spec_scan( {spectroscopy_detector, sample_stage}, @@ -164,10 +166,12 @@ def demo_spectroscopy( grid_origin_y: float = 0.0, exposure_time: float = 0.1, metadata: dict[str, Any] | None = None, + *, + scan_type: ScanType, ) -> MsgGenerator[None]: """Spectroscopy plan intended for use in Visr demonstrations to visitors. - The time taken to scan is approximately linear in total_numbers_of_grid_points. - All other parameters can be left at their defaults. + Scans a square grid. The time taken to scan is approximately linear in + total_number_of_scan_points. All other parameters can be left at their defaults. """ xsteps = ysteps = int(round(math.sqrt(max(total_number_of_scan_points, 1)))) xmin = grid_origin_x @@ -175,18 +179,16 @@ def demo_spectroscopy( ymin = grid_origin_y ymax = grid_origin_y + grid_size - velomax = grid_size / (xsteps * exposure_time) - if velomax <= MAX_STAGE_VELOCITY * 0.98: - fly = True - LOGGER.info("test-rig-bluesky") - LOGGER.info( - f"Estimated velocity is {velomax:.2f} mm/sec, performing a fly scan." - ) - else: - fly = False - LOGGER.info( - f"Estimated velocity is {velomax:.2f} mm/sec, performing a step scan." - ) + if scan_type == ScanType.FLY: + velocity = grid_size / (xsteps * exposure_time) + LOGGER.info("Estimated fly scan velocity is %.2f mm/s.", velocity) + + if velocity > MAX_STAGE_VELOCITY: + raise ValueError( + f"Requested fly scan requires {velocity:.2f} mm/s, " + f"exceeding the maximum stage velocity of " + f"{MAX_STAGE_VELOCITY:.2f} mm/s." + ) # Move to the start point # pmac should handle premove @@ -207,6 +209,6 @@ def demo_spectroscopy( pandabrick=pandabrick, spec=grid, exposure_time=exposure_time, - fly=fly, metadata=metadata, + scan_type=scan_type, ) diff --git a/src/test_rig_bluesky/tomography_plans.py b/src/test_rig_bluesky/tomography_plans.py index 2368a20..31c5b05 100644 --- a/src/test_rig_bluesky/tomography_plans.py +++ b/src/test_rig_bluesky/tomography_plans.py @@ -19,7 +19,7 @@ from pydantic import Field from scanspec.specs import Line, Spec -from .plans import fly_scan, load_settings, pandabrick, pmac, serialize_spec +from .plans import ScanType, fly_scan, load_settings, pandabrick, pmac, serialize_spec LOGGER = logging.getLogger(__name__) @@ -139,13 +139,11 @@ def tomography( tomography_stage: Motor = tomography_stage, pmac: PmacIO = pmac, pandabrick: HDFPanda = pandabrick, - number_of_projections: NumProjections = 360, - angular_range: float = 360.0, - start_angle: float = 0.0, exposure_time: float = 0.1, - fly: bool = True, spec: Spec[Movable] | None = None, metadata: dict[str, Any] | None = None, + *, + scan_type: ScanType, ) -> MsgGenerator[None]: """Do a tomography scan.""" yield from _setup_detector(tomography_detector, exposure_time) @@ -156,11 +154,8 @@ def tomography( whitelist_pvs=TOMOGRAPHY_STAGE_WHITELIST, ) - caller_supplied_spec = spec is not None - if spec is None: - angular_step = angular_range / number_of_projections - last_angle = start_angle + angular_range - angular_step - spec = Line(tomography_stage, start_angle, last_angle, number_of_projections) # type: ignore + # One projection per degree over a full rotation + spec = spec or Line(tomography_stage, 0, 359, 360) # type: ignore if metadata is None: metadata = {} @@ -168,34 +163,50 @@ def tomography( # Tells the workflow which calibration images belong with these projections. metadata[LIGHT_SOURCE_KEY] = light_source.value - def scan() -> MsgGenerator[None]: - if fly: - LOGGER.info("Performing a tomography fly scan.") - yield from fly_scan( - spec, - tomography_detector, - pmac=pmac, - pandabrick=pandabrick, - exposure_time=exposure_time, - detector_deadtime=MAKO_DETECTOR_DEADTIME, - detector_trigger=TOMOGRAPHY_DETECTOR_TRIGGER, - panda_whitelist=PANDA_WHITELIST, - plan_name="tomography_fly_scan", - metadata=metadata, - ) - else: - LOGGER.info("Performing a tomography step scan.") - yield from spec_scan( - {tomography_detector, tomography_stage}, - spec, # type: ignore - metadata=metadata, - ) - - if caller_supplied_spec: - # The caller owns the geometry, so we do not know where it starts or - # what it would mean to unwind to. - yield from scan() - return + if scan_type == ScanType.FLY: + LOGGER.info("Performing a tomography fly scan.") + yield from fly_scan( + spec, + tomography_detector, + pmac=pmac, + pandabrick=pandabrick, + exposure_time=exposure_time, + detector_deadtime=MAKO_DETECTOR_DEADTIME, + detector_trigger=TOMOGRAPHY_DETECTOR_TRIGGER, + panda_whitelist=PANDA_WHITELIST, + plan_name="tomography_fly_scan", + metadata=metadata, + ) + elif scan_type == ScanType.STEP: + LOGGER.info("Performing a tomography step scan.") + yield from spec_scan( + {tomography_detector, tomography_stage}, + spec, # type: ignore + metadata=metadata, + ) + + +def demo_tomography( + light_source: LightSource, + tomography_detector: AravisDetector = tomography_detector, + tomography_stage: Motor = tomography_stage, + pmac: PmacIO = pmac, + pandabrick: HDFPanda = pandabrick, + number_of_projections: NumProjections = 360, + angular_range: float = 360.0, + start_angle: float = 0.0, + exposure_time: float = 0.1, + metadata: dict[str, Any] | None = None, + *, + scan_type: ScanType, +) -> MsgGenerator[None]: + """Tomography plan intended for use in Visr demonstrations to visitors. + Takes number_of_projections evenly spaced over angular_range from start_angle, + then winds the stage back to start_angle. + """ + angular_step = angular_range / number_of_projections + last_angle = start_angle + angular_range - angular_step + spec = Line(tomography_stage, start_angle, last_angle, number_of_projections) # type: ignore # PMAC handles the run-up, but move to the nominal start first so a failure # here is reported before the trajectory is built. @@ -208,4 +219,17 @@ def wind_back() -> MsgGenerator[None]: # beginning again and reset the angle to 0? yield from bps.mv(tomography_stage, start_angle, group="wind_back") - yield from bpp.finalize_wrapper(scan(), wind_back) + yield from bpp.finalize_wrapper( + tomography( + light_source, + tomography_detector=tomography_detector, + tomography_stage=tomography_stage, + pmac=pmac, + pandabrick=pandabrick, + exposure_time=exposure_time, + spec=spec, + metadata=metadata, + scan_type=scan_type, + ), + wind_back, + ) diff --git a/tests/system_tests/test_plans_system.py b/tests/system_tests/test_plans_system.py index 3593fa4..745a9b7 100644 --- a/tests/system_tests/test_plans_system.py +++ b/tests/system_tests/test_plans_system.py @@ -4,6 +4,7 @@ from bluesky import RunEngine from scanspec.specs import Line +from test_rig_bluesky.plans import ScanType from test_rig_bluesky.spectroscopy_plans import spectroscopy from test_rig_bluesky.testing import BlueskyPlanRunner @@ -27,19 +28,6 @@ def test_snapshot( assert events["FINISHED"][0]["scanDimensions"] == [1] -def test_spectroscopy( - bluesky_plan_runner: BlueskyPlanRunner, latest_commissioning_instrument_session: str -): - events = bluesky_plan_runner.run( - TaskRequest( - name="spectroscopy", - instrument_session=latest_commissioning_instrument_session, - ), - timeout=20, - ) - assert events["FINISHED"][0]["scanDimensions"] == [5] - - def test_spectroscopy_with_custom_trajectory( bluesky_plan_runner: BlueskyPlanRunner, latest_commissioning_instrument_session: str ): @@ -49,7 +37,7 @@ def test_spectroscopy_with_custom_trajectory( events = bluesky_plan_runner.run( TaskRequest( name="spectroscopy", - params={"spec": scan_spec.serialize()}, + params={"spec": scan_spec.serialize(), "scan_type": ScanType.FLY.value}, instrument_session=latest_commissioning_instrument_session, ), timeout=60, @@ -63,6 +51,7 @@ def test_demo_spectroscopy( events = bluesky_plan_runner.run( TaskRequest( name="demo_spectroscopy", + params={"scan_type": ScanType.STEP.value}, instrument_session=latest_commissioning_instrument_session, ), timeout=1000, @@ -77,6 +66,7 @@ def test_demo_spectroscopy_with_custom_trajectory( TaskRequest( name="demo_spectroscopy", params={ + "scan_type": ScanType.STEP.value, "exposure_time": 0.05, "grid_size": 2.5, "grid_origin_y": 0.5, @@ -136,5 +126,57 @@ def test_spectroscopy_re(): spectroscopy_detector=spectroscopy_detector, sample_stage=sample_stage, spec=scan_spec, + scan_type=ScanType.FLY, + ) + ) + + +def test_spectroscopy_step_scan( + bluesky_plan_runner, + latest_commissioning_instrument_session, +): + events = bluesky_plan_runner.run( + TaskRequest( + name="spectroscopy", + params={"scan_type": ScanType.STEP.value}, + instrument_session=latest_commissioning_instrument_session, + ), + timeout=20, + ) + + assert events["FINISHED"][0]["scanDimensions"] == [5] + + +def test_spectroscopy_fly_scan( + bluesky_plan_runner, + latest_commissioning_instrument_session, +): + events = bluesky_plan_runner.run( + TaskRequest( + name="spectroscopy", + params={"scan_type": ScanType.FLY.value}, + instrument_session=latest_commissioning_instrument_session, + ), + timeout=60, + ) + + assert events["FINISHED"][0]["scanDimensions"] == [5] + + +@pytest.mark.control_system +def test_spectroscopy_re_run_engine(): + run_engine = RunEngine() + + spectroscopy_detector = b01_1.spectroscopy_detector.build(connect_immediately=True) + sample_stage = b01_1.sample_stage.build(connect_immediately=True) + + scan_spec = Line(sample_stage.y, 0, 5, 50) * Line(sample_stage.x, 2, 5, 30) + + run_engine( + spectroscopy( + spectroscopy_detector=spectroscopy_detector, + sample_stage=sample_stage, + spec=scan_spec, + scan_type=ScanType.STEP, ) ) diff --git a/tests/unit_tests/test_plans.py b/tests/unit_tests/test_plans.py index 24b1893..1418735 100644 --- a/tests/unit_tests/test_plans.py +++ b/tests/unit_tests/test_plans.py @@ -15,6 +15,7 @@ from scanspec.specs import Line, Spec from test_rig_bluesky.plans import ( + ScanType, load_settings, save_settings, serialize_spec, @@ -152,7 +153,7 @@ async def test_spectroscopy( pandabrick=pandabrick, spec=Line(sample_stage.y, 4.2, 6, 3) * Line(sample_stage.x, 0, 5, 10), exposure_time=0.2, - fly=False, + scan_type=ScanType.STEP, ) ) @@ -183,7 +184,7 @@ async def test_spectroscopy_defaults( spectroscopy_detector=spectroscopy_detector, sample_stage=sample_stage, pandabrick=pandabrick, - fly=False, + scan_type=ScanType.STEP, ) ) @@ -214,7 +215,7 @@ def test_spectroscopy_datasets( spectroscopy_detector=spectroscopy_detector, sample_stage=sample_stage, pandabrick=pandabrick, - fly=False, + scan_type=ScanType.STEP, ) ) @@ -239,7 +240,7 @@ async def test_spectroscopy_sets_exposure_time_and_acquire_period( sample_stage=sample_stage, pandabrick=pandabrick, exposure_time=1.0, - fly=False, + scan_type=ScanType.STEP, ) ) assert await spectroscopy_detector.driver.acquire_time.get_value() == 1.0 @@ -259,6 +260,7 @@ def test_demo_spectroscopy(): spectroscopy_detector=fake_detector, sample_stage=fake_stage, total_number_of_scan_points=25, + scan_type=ScanType.FLY, ) # Consume the generator so that the spectroscopy call is made @@ -272,6 +274,7 @@ def test_demo_spectroscopy(): assert called_kwargs["spec"] == Line(fake_stage.y, 0.0, 5.0, 5) * ~Line( fake_stage.x, 0.0, 5.0, 5 ) + assert called_kwargs["scan_type"] == ScanType.FLY def test_serialize_spec_names_device_axes(sample_stage: XYZStage):