From ed470e2e64da7cc5ddae80315dc219861391da3e Mon Sep 17 00:00:00 2001 From: gkalua Date: Thu, 15 Oct 2026 14:47:56 +0000 Subject: [PATCH 1/9] updates plan to determine scan type based on enum choice --- src/test_rig_bluesky/plans.py | 9 ++ src/test_rig_bluesky/spectroscopy_plans.py | 112 +++++++++------------ tests/system_tests/test_plans_system.py | 52 ++++++++++ 3 files changed, 107 insertions(+), 66 deletions(-) diff --git a/src/test_rig_bluesky/plans.py b/src/test_rig_bluesky/plans.py index 46d8522..1202e0d 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 @@ -39,6 +40,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..382a10a 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,53 @@ def spectroscopy( pandabrick: HDFPanda = pandabrick, spec: Spec[Movable] | None = None, exposure_time: float = 0.1, - fly: bool = True, + total_number_of_scan_points: int = 25, + grid_size: float = 5.0, + grid_origin_x: float = 0.0, + grid_origin_y: float = 0.0, metadata: dict[str, Any] | None = None, + *, + scan_type: ScanType, ) -> MsgGenerator[None]: """Do a spectroscopy scan.""" + + if scan_type == ScanType.FLY: + xsteps = int(round(math.sqrt(total_number_of_scan_points))) + velocity = grid_size / (xsteps * exposure_time) + + 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." + ) + + LOGGER.info( + "Performing a fly scan. Estimated velocity: %.2f mm/s.", + velocity, + ) + else: + LOGGER.info("Performing a step scan.") + + if spec is None: + xsteps = ysteps = int(round(math.sqrt(max(total_number_of_scan_points, 1)))) + xmin = grid_origin_x + xmax = grid_origin_x + grid_size + ymin = grid_origin_y + ymax = grid_origin_y + grid_size + + # Move to the start point + yield from bps.mv( + *(sample_stage.x, xmin), *(sample_stage.y, ymin), group="initial_move" + ) + + spec = Line(sample_stage.y, ymin, ymax, ysteps) * ~Line( + sample_stage.x, + xmin, + xmax, + xsteps, + ) + yield from load_settings( device=spectroscopy_detector, design_name="spectroscopy_detector_baseline", @@ -125,15 +168,12 @@ def spectroscopy( ], ) - spec = spec or Line(sample_stage.x, 0, 5, 5) # type: ignore - # write json-serializable form of spec into metadata if metadata is None: metadata = {} metadata["spec"] = serialize_spec(spec) - if fly: - LOGGER.info("Performing a fly scan.") + if scan_type == ScanType.FLY: yield from fly_scan( spec, spectroscopy_detector, @@ -145,68 +185,8 @@ def spectroscopy( metadata=metadata, ) else: - LOGGER.info("Performing a step scan.") yield from spec_scan( {spectroscopy_detector, sample_stage}, spec, # type: ignore metadata=metadata, ) - - -def demo_spectroscopy( - spectroscopy_detector: AravisDetector = spectroscopy_detector, - sample_stage: XYZStage = sample_stage, - pmac: PmacIO = pmac, - pandabrick: HDFPanda = pandabrick, - total_number_of_scan_points: int = 25, - grid_size: float = 5.0, - grid_origin_x: float = 0.0, - grid_origin_y: float = 0.0, - exposure_time: float = 0.1, - metadata: dict[str, Any] | None = None, -) -> 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. - """ - xsteps = ysteps = int(round(math.sqrt(max(total_number_of_scan_points, 1)))) - xmin = grid_origin_x - xmax = grid_origin_x + grid_size - 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." - ) - - # Move to the start point - # pmac should handle premove - yield from bps.mv( - *(sample_stage.x, xmin), *(sample_stage.y, ymin), group="initial_move" - ) - - grid = Line(sample_stage.y, ymin, ymax, ysteps) * ~Line( # type: ignore - sample_stage.x, # type: ignore - xmin, - xmax, - xsteps, - ) - yield from spectroscopy( - spectroscopy_detector=spectroscopy_detector, - sample_stage=sample_stage, - pmac=pmac, - pandabrick=pandabrick, - spec=grid, - exposure_time=exposure_time, - fly=fly, - metadata=metadata, - ) diff --git a/tests/system_tests/test_plans_system.py b/tests/system_tests/test_plans_system.py index 3593fa4..a0d8511 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 @@ -138,3 +139,54 @@ def test_spectroscopy_re(): spec=scan_spec, ) ) + + +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, 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, + ) + ) From fd8e6f2fade970efcad6d01e21e57a8c46d8192e Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 09:40:05 +0000 Subject: [PATCH 2/9] change velocity check from an error to a warning. Use explicit elif scan_type = STEP --- src/test_rig_bluesky/spectroscopy_plans.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/test_rig_bluesky/spectroscopy_plans.py b/src/test_rig_bluesky/spectroscopy_plans.py index 382a10a..24b0291 100644 --- a/src/test_rig_bluesky/spectroscopy_plans.py +++ b/src/test_rig_bluesky/spectroscopy_plans.py @@ -60,18 +60,22 @@ def spectroscopy( xsteps = int(round(math.sqrt(total_number_of_scan_points))) velocity = grid_size / (xsteps * exposure_time) + # This estimate assumes the default square grid, so only warn: a spec + # passed in directly may move at a different speed, and ophyd-async + # will reject the scan if the stage really cannot go that fast. 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." + LOGGER.warning( + "Estimated velocity for a square grid fly scan is %.2f mm/s, " + "exceeding the maximum stage velocity of %.2f mm/s.", + velocity, + MAX_STAGE_VELOCITY, ) LOGGER.info( - "Performing a fly scan. Estimated velocity: %.2f mm/s.", + "Performing a fly scan. Estimated velocity for a square grid: %.2f mm/s.", velocity, ) - else: + elif scan_type == ScanType.STEP: LOGGER.info("Performing a step scan.") if spec is None: From 8f553161aad80f47fbdd39c9f00a02fb497ecbce Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 09:42:28 +0000 Subject: [PATCH 3/9] update tomography plans with scan type enum --- src/test_rig_bluesky/tomography_plans.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test_rig_bluesky/tomography_plans.py b/src/test_rig_bluesky/tomography_plans.py index 2368a20..ba41e81 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__) @@ -143,9 +143,10 @@ def tomography( 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) @@ -169,7 +170,7 @@ def tomography( metadata[LIGHT_SOURCE_KEY] = light_source.value def scan() -> MsgGenerator[None]: - if fly: + if scan_type == ScanType.FLY: LOGGER.info("Performing a tomography fly scan.") yield from fly_scan( spec, @@ -183,7 +184,7 @@ def scan() -> MsgGenerator[None]: plan_name="tomography_fly_scan", metadata=metadata, ) - else: + elif scan_type == ScanType.STEP: LOGGER.info("Performing a tomography step scan.") yield from spec_scan( {tomography_detector, tomography_stage}, From 79c53e616e0aaa670e9714bc999adb87c3cbcc79 Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 10:03:11 +0000 Subject: [PATCH 4/9] revert back to two plans --- src/test_rig_bluesky/spectroscopy_plans.py | 113 ++++++++++++--------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/src/test_rig_bluesky/spectroscopy_plans.py b/src/test_rig_bluesky/spectroscopy_plans.py index 24b0291..43d7cc2 100644 --- a/src/test_rig_bluesky/spectroscopy_plans.py +++ b/src/test_rig_bluesky/spectroscopy_plans.py @@ -46,57 +46,12 @@ def spectroscopy( pandabrick: HDFPanda = pandabrick, spec: Spec[Movable] | None = None, exposure_time: float = 0.1, - total_number_of_scan_points: int = 25, - grid_size: float = 5.0, - grid_origin_x: float = 0.0, - grid_origin_y: float = 0.0, metadata: dict[str, Any] | None = None, *, scan_type: ScanType, ) -> MsgGenerator[None]: """Do a spectroscopy scan.""" - if scan_type == ScanType.FLY: - xsteps = int(round(math.sqrt(total_number_of_scan_points))) - velocity = grid_size / (xsteps * exposure_time) - - # This estimate assumes the default square grid, so only warn: a spec - # passed in directly may move at a different speed, and ophyd-async - # will reject the scan if the stage really cannot go that fast. - if velocity > MAX_STAGE_VELOCITY: - LOGGER.warning( - "Estimated velocity for a square grid fly scan is %.2f mm/s, " - "exceeding the maximum stage velocity of %.2f mm/s.", - velocity, - MAX_STAGE_VELOCITY, - ) - - LOGGER.info( - "Performing a fly scan. Estimated velocity for a square grid: %.2f mm/s.", - velocity, - ) - elif scan_type == ScanType.STEP: - LOGGER.info("Performing a step scan.") - - if spec is None: - xsteps = ysteps = int(round(math.sqrt(max(total_number_of_scan_points, 1)))) - xmin = grid_origin_x - xmax = grid_origin_x + grid_size - ymin = grid_origin_y - ymax = grid_origin_y + grid_size - - # Move to the start point - yield from bps.mv( - *(sample_stage.x, xmin), *(sample_stage.y, ymin), group="initial_move" - ) - - spec = Line(sample_stage.y, ymin, ymax, ysteps) * ~Line( - sample_stage.x, - xmin, - xmax, - xsteps, - ) - yield from load_settings( device=spectroscopy_detector, design_name="spectroscopy_detector_baseline", @@ -172,12 +127,15 @@ def spectroscopy( ], ) + spec = spec or Line(sample_stage.x, 0, 5, 5) # type: ignore + # write json-serializable form of spec into metadata if metadata is None: metadata = {} metadata["spec"] = serialize_spec(spec) if scan_type == ScanType.FLY: + LOGGER.info("Performing a fly scan.") yield from fly_scan( spec, spectroscopy_detector, @@ -188,9 +146,72 @@ 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}, spec, # type: ignore metadata=metadata, ) + + +def demo_spectroscopy( + spectroscopy_detector: AravisDetector = spectroscopy_detector, + sample_stage: XYZStage = sample_stage, + pmac: PmacIO = pmac, + pandabrick: HDFPanda = pandabrick, + total_number_of_scan_points: int = 25, + grid_size: float = 5.0, + grid_origin_x: float = 0.0, + 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. + 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 + xmax = grid_origin_x + grid_size + ymin = grid_origin_y + ymax = grid_origin_y + grid_size + + if scan_type == ScanType.FLY: + velocity = grid_size / (xsteps * exposure_time) + LOGGER.info("Estimated fly scan velocity is %.2f mm/s.", velocity) + + # Only warn, as ophyd-async will reject the scan if the stage really + # cannot go that fast. + if velocity > MAX_STAGE_VELOCITY: + LOGGER.warning( + "Estimated fly scan velocity of %.2f mm/s exceeds the maximum " + "stage velocity of %.2f mm/s.", + velocity, + MAX_STAGE_VELOCITY, + ) + + # Move to the start point + # pmac should handle premove + yield from bps.mv( + *(sample_stage.x, xmin), *(sample_stage.y, ymin), group="initial_move" + ) + + grid = Line(sample_stage.y, ymin, ymax, ysteps) * ~Line( # type: ignore + sample_stage.x, # type: ignore + xmin, + xmax, + xsteps, + ) + yield from spectroscopy( + spectroscopy_detector=spectroscopy_detector, + sample_stage=sample_stage, + pmac=pmac, + pandabrick=pandabrick, + spec=grid, + exposure_time=exposure_time, + metadata=metadata, + scan_type=scan_type, + ) From 17be3250b7db659ddaecbf6c7fb68a39f44dac0e Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 10:03:42 +0000 Subject: [PATCH 5/9] copy two plan setup to create the spec for tomography --- src/test_rig_bluesky/tomography_plans.py | 97 +++++++++++++++--------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/src/test_rig_bluesky/tomography_plans.py b/src/test_rig_bluesky/tomography_plans.py index ba41e81..31c5b05 100644 --- a/src/test_rig_bluesky/tomography_plans.py +++ b/src/test_rig_bluesky/tomography_plans.py @@ -139,9 +139,6 @@ 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, spec: Spec[Movable] | None = None, metadata: dict[str, Any] | None = None, @@ -157,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 = {} @@ -169,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 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, - ) - - 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. @@ -209,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, + ) From 9a2b43f9758e87b23598b746390e0e67006420ac Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 10:04:10 +0000 Subject: [PATCH 6/9] fix tests --- tests/system_tests/test_plans_system.py | 8 +++-- tests/unit_tests/test_plans.py | 42 ++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/tests/system_tests/test_plans_system.py b/tests/system_tests/test_plans_system.py index a0d8511..d4dde39 100644 --- a/tests/system_tests/test_plans_system.py +++ b/tests/system_tests/test_plans_system.py @@ -34,6 +34,7 @@ def test_spectroscopy( events = bluesky_plan_runner.run( TaskRequest( name="spectroscopy", + params={"scan_type": ScanType.FLY.value}, instrument_session=latest_commissioning_instrument_session, ), timeout=20, @@ -50,7 +51,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, @@ -64,6 +65,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, @@ -78,6 +80,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, @@ -137,6 +140,7 @@ def test_spectroscopy_re(): spectroscopy_detector=spectroscopy_detector, sample_stage=sample_stage, spec=scan_spec, + scan_type=ScanType.FLY, ) ) @@ -170,7 +174,7 @@ def test_spectroscopy_fly_scan( timeout=60, ) - assert events["FINISHED"][0]["scanDimensions"] == [5, 5] + assert events["FINISHED"][0]["scanDimensions"] == [5] @pytest.mark.control_system diff --git a/tests/unit_tests/test_plans.py b/tests/unit_tests/test_plans.py index 58ad495..a9973e1 100644 --- a/tests/unit_tests/test_plans.py +++ b/tests/unit_tests/test_plans.py @@ -1,4 +1,5 @@ import asyncio +import logging import unittest.mock from collections import defaultdict from unittest.mock import ANY, AsyncMock, Mock, patch @@ -15,6 +16,7 @@ from scanspec.specs import Line from test_rig_bluesky.plans import ( + ScanType, load_settings, save_settings, snapshot, @@ -151,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, ) ) @@ -182,7 +184,7 @@ async def test_spectroscopy_defaults( spectroscopy_detector=spectroscopy_detector, sample_stage=sample_stage, pandabrick=pandabrick, - fly=False, + scan_type=ScanType.STEP, ) ) @@ -213,7 +215,7 @@ def test_spectroscopy_datasets( spectroscopy_detector=spectroscopy_detector, sample_stage=sample_stage, pandabrick=pandabrick, - fly=False, + scan_type=ScanType.STEP, ) ) @@ -238,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 @@ -258,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 @@ -271,3 +274,34 @@ 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 + + +@pytest.mark.parametrize( + "grid_size, expect_warning", + [(4.0, False), (10.0, True)], +) +def test_demo_spectroscopy_warns_if_velocity_too_high( + caplog: pytest.LogCaptureFixture, + grid_size: float, + expect_warning: bool, +): + with unittest.mock.patch("test_rig_bluesky.spectroscopy_plans.spectroscopy"): + plan = demo_spectroscopy( + spectroscopy_detector=unittest.mock.MagicMock(), + sample_stage=unittest.mock.MagicMock(), + grid_size=grid_size, + exposure_time=0.1, + total_number_of_scan_points=25, + scan_type=ScanType.FLY, + ) + with caplog.at_level(logging.WARNING): + # Consume the generator to check the warning does not stop the plan + for _ in plan: + pass + + warned = any( + "exceeds the maximum stage velocity" in record.message + for record in caplog.records + ) + assert warned == expect_warning From fa2d0221f6b25020360298a5675cc28b310b4aff Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 10:16:50 +0000 Subject: [PATCH 7/9] remove redundant test --- tests/unit_tests/test_plans.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/unit_tests/test_plans.py b/tests/unit_tests/test_plans.py index a9973e1..aa4e00a 100644 --- a/tests/unit_tests/test_plans.py +++ b/tests/unit_tests/test_plans.py @@ -1,5 +1,4 @@ import asyncio -import logging import unittest.mock from collections import defaultdict from unittest.mock import ANY, AsyncMock, Mock, patch @@ -275,33 +274,3 @@ def test_demo_spectroscopy(): fake_stage.x, 0.0, 5.0, 5 ) assert called_kwargs["scan_type"] == ScanType.FLY - - -@pytest.mark.parametrize( - "grid_size, expect_warning", - [(4.0, False), (10.0, True)], -) -def test_demo_spectroscopy_warns_if_velocity_too_high( - caplog: pytest.LogCaptureFixture, - grid_size: float, - expect_warning: bool, -): - with unittest.mock.patch("test_rig_bluesky.spectroscopy_plans.spectroscopy"): - plan = demo_spectroscopy( - spectroscopy_detector=unittest.mock.MagicMock(), - sample_stage=unittest.mock.MagicMock(), - grid_size=grid_size, - exposure_time=0.1, - total_number_of_scan_points=25, - scan_type=ScanType.FLY, - ) - with caplog.at_level(logging.WARNING): - # Consume the generator to check the warning does not stop the plan - for _ in plan: - pass - - warned = any( - "exceeds the maximum stage velocity" in record.message - for record in caplog.records - ) - assert warned == expect_warning From c19625755285b17d8547d982f0f1ab88ac930d07 Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 10:25:09 +0000 Subject: [PATCH 8/9] raise error on too fast again --- src/test_rig_bluesky/spectroscopy_plans.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/test_rig_bluesky/spectroscopy_plans.py b/src/test_rig_bluesky/spectroscopy_plans.py index 43d7cc2..bbf168e 100644 --- a/src/test_rig_bluesky/spectroscopy_plans.py +++ b/src/test_rig_bluesky/spectroscopy_plans.py @@ -183,14 +183,11 @@ def demo_spectroscopy( velocity = grid_size / (xsteps * exposure_time) LOGGER.info("Estimated fly scan velocity is %.2f mm/s.", velocity) - # Only warn, as ophyd-async will reject the scan if the stage really - # cannot go that fast. if velocity > MAX_STAGE_VELOCITY: - LOGGER.warning( - "Estimated fly scan velocity of %.2f mm/s exceeds the maximum " - "stage velocity of %.2f mm/s.", - 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 From b39182fc5e14746c6c79616518dd09584c2cc2d4 Mon Sep 17 00:00:00 2001 From: Tom Kane Date: Mon, 19 Oct 2026 10:30:33 +0000 Subject: [PATCH 9/9] remove redundant tests --- tests/system_tests/test_plans_system.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/system_tests/test_plans_system.py b/tests/system_tests/test_plans_system.py index d4dde39..745a9b7 100644 --- a/tests/system_tests/test_plans_system.py +++ b/tests/system_tests/test_plans_system.py @@ -28,20 +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", - params={"scan_type": ScanType.FLY.value}, - 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 ):