Skip to content
9 changes: 9 additions & 0 deletions src/test_rig_bluesky/plans.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import math
from enum import StrEnum
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -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")

Expand Down
40 changes: 21 additions & 19 deletions src/test_rig_bluesky/spectroscopy_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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,
Comment on lines +50 to +51
) -> MsgGenerator[None]:
"""Do a spectroscopy scan."""

yield from load_settings(
device=spectroscopy_detector,
design_name="spectroscopy_detector_baseline",
Expand Down Expand Up @@ -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,
Expand All @@ -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},
Expand All @@ -164,29 +166,29 @@ 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
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."
)
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
Expand All @@ -207,6 +209,6 @@ def demo_spectroscopy(
pandabrick=pandabrick,
spec=grid,
exposure_time=exposure_time,
fly=fly,
metadata=metadata,
scan_type=scan_type,
)
102 changes: 63 additions & 39 deletions src/test_rig_bluesky/tomography_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)
Expand All @@ -156,46 +154,59 @@ 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 = {}
metadata["spec"] = serialize_spec(spec)
# 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.
Expand All @@ -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,
)
70 changes: 56 additions & 14 deletions tests/system_tests/test_plans_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
):
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
)
)
Loading
Loading