Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion src/test_rig_bluesky/plans.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import math
from dataclasses import dataclass
from typing import Any

from bluesky import plan_stubs as bps
from bluesky.plans import count
from bluesky.protocols import Movable
from bluesky.utils import MsgGenerator
from dodal.common import inject
from dodal.devices.motors import XYZStage
from dodal.plan_stubs.data_session import attach_data_session_metadata_decorator
from dodal.plans import spec_scan
from ophyd_async.core import TriggerInfo
from ophyd_async.epics.adaravis import AravisDetector
from ophyd_async.epics.adcore import NDAttributePv, NDAttributePvDbrType
from ophyd_async.epics.adcore._core_io import NDROIStatNIO
from ophyd_async.plan_stubs import setup_ndattributes
from scanspec.specs import Line, Spec

imaging_detector = inject("imaging_detector")
spectroscopy_detector = inject("spectroscopy_detector")
sample_stage = inject("sample_stage")


@dataclass
class ROI:
channel: int
name: str
start_x: int
start_y: int
size: int


@attach_data_session_metadata_decorator()
def snapshot(
imaging_detector: AravisDetector = imaging_detector,
Expand All @@ -30,14 +44,60 @@ def snapshot(
def spectroscopy(
spectroscopy_detector: AravisDetector = spectroscopy_detector,
sample_stage: XYZStage = sample_stage,
spec: Spec | None = None,
spec: Spec[Movable] | None = None,
exposure_time: float = 0.1,
metadata: dict[str, Any] | None = None,
) -> MsgGenerator[None]:
"""Do a spectroscopy scan."""
yield from bps.prepare(
spectroscopy_detector, TriggerInfo(livetime=exposure_time), wait=True
)
# TODO: This would be nicer if NDArrayBaseIO had a PortName signal
yield from bps.abs_set(
spectroscopy_detector.fileio.nd_array_port, "D2.roistat", wait=True
)

rois = [
ROI(2, "Green", 880, 605, 150),
ROI(3, "Blue", 1665, 600, 150),
ROI(1, "Red", 95, 610, 150),
]

params: list[NDAttributePv] = []
for roi in rois:
roistatn = spectroscopy_detector.roistat.channels[roi.channel] # type: ignore
assert isinstance(roistatn, NDROIStatNIO)

yield from bps.mv(
*(roistatn.name_, roi.name),
*(roistatn.min_x, roi.start_x),
*(roistatn.min_y, roi.start_y),
*(roistatn.size_x, roi.size),
*(roistatn.size_y, roi.size),
*(roistatn.use, True),
wait=True,
)

# TODO: We can't include all the channels as it makes the xml longer than 256
# params.append(
params = [
NDAttributePv(
name=f"{roi.name}Total",
signal=roistatn.total,
dbrtype=NDAttributePvDbrType.DBR_LONG,
description=f"Sum of {roi.name} channel",
)
]
# )

yield from setup_ndattributes(spectroscopy_detector.roistat, params) # type: ignore

for motor in [sample_stage.x, sample_stage.y]:
yield from bps.mv(
*(motor.acceleration_time, 0.001),
*(motor.velocity, 100),
wait=True,
)

spec = spec or Line(sample_stage.x, 0, 5, 5)

Expand Down
31 changes: 31 additions & 0 deletions tests/system_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import socket
import textwrap
from collections.abc import Generator
from pathlib import Path

Expand All @@ -12,6 +14,35 @@

PROJECT_ROOT = Path(__file__).parent.parent.parent

BEAMLINE_HOSTS = [
"b01-1-ws001.diamond.ac.uk",
"b01-1-control.diamond.ac.uk",
"bl01c-ea-serv-01.diamond.ac.uk",
"bl01c-di-serv-01.diamond.ac.uk",
]


def pytest_configure(config: pytest.Config):
config.addinivalue_line(
"markers", "control_system: test requires direct access to the control system"
)


def pytest_runtest_setup(item: pytest.Item):
if next(item.iter_markers(name="control_system"), None) is not None:
if not on_controllable_machine():
pytest.skip(
reason=textwrap.dedent(f"""
This test needs direct access to the control system and can only
be run from one of the following machines: {BEAMLINE_HOSTS}
""")
)


def on_controllable_machine() -> bool:
hostname = socket.gethostname()
return hostname in BEAMLINE_HOSTS


@pytest.fixture
def instrument() -> str:
Expand Down
20 changes: 19 additions & 1 deletion tests/system_tests/test_plans_system.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import pytest
from blueapi.service.model import TaskRequest
from bluesky import RunEngine
from dodal.beamlines.b01_1 import sample_stage, spectroscopy_detector
from scanspec.specs import Line

from test_rig_bluesky.plans import spectroscopy
from test_rig_bluesky.testing import BlueskyPlanRunner


Expand Down Expand Up @@ -34,12 +39,25 @@ def test_spectroscopy(
def test_demo_spectroscopy(
bluesky_plan_runner: BlueskyPlanRunner, latest_commissioning_instrument_session: str
):
_sample_stage = sample_stage()
scan_spec = Line(_sample_stage.x, 2, 5, 30) * Line(_sample_stage.y, 0, 5, 50)
events = bluesky_plan_runner.run(
TaskRequest(
name="demo_spectroscopy",
params={},
params={"spec": scan_spec.serialize()},
instrument_session=latest_commissioning_instrument_session,
),
timeout=10,
)
assert events["FINISHED"][0]["scanDimensions"] == [5, 5]


@pytest.mark.control_system
def test_spectroscopy_re():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could: My only objection to putting tests like this back in is that I can't run them from my laptop over eduroam/vpn, which is very convenient. However if you think they add more value than take I won't block over this.

Copy link
Contributor Author

@GDYendell GDYendell Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this for two reasons:

  • Blueapi was not working - maybe that is less likely to be the case in future?
  • Passing the scanspec into the TaskRequest didn't seem to work. Do you have an example
    of that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can have a look at passing the scanspec into the request, for this we could use something like pytest-requires and then you could have @pytest.mark.requires("beamline network") or similar

Copy link
Contributor

@callumforrester callumforrester Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the reason the spec didn't work is because there is no type bound in the plan signature:

def spectroscopy(
    spectroscopy_detector: AravisDetector = spectroscopy_detector,
    sample_stage: XYZStage = sample_stage,
-   spec: Spec | None = None,
+   spec: Spec[Movable] | None = None,
    exposure_time: float = 0.1,
    metadata: dict[str, Any] | None = None,
) -> MsgGenerator[None]:

Specifying a bluesky type tells blueapi to resolve it as a special case (device or devices). I also made a PR on top of this PR that just skips tests like this if you're not on a beamline machine, up to you. https://github.com/DiamondLightSource/test-rig-bluesky/pull/17/files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pulled in the PR.

So if I add Spec[Movable] I can do something like this?

    _sample_stage = sample_stage()
    scan_spec = Line(_sample_stage.x, 2, 5, 30) * Line(_sample_stage.y, 0, 5, 50)
    events = bluesky_plan_runner.run(
        TaskRequest(
            name="demo_spectroscopy",
            params={"spec": scan_spec.serialize()},
            instrument_session=latest_commissioning_instrument_session,
        ),
        timeout=10,
    )

I can't test currently because rabbit mq is not running.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe that should work

RE = RunEngine()
_spectroscopy_detector = spectroscopy_detector(connect_immediately=True)
_sample_stage = sample_stage(connect_immediately=True)

scan_spec = Line(_sample_stage.x, 2, 5, 30) * Line(_sample_stage.y, 0, 5, 50)

RE(spectroscopy(_spectroscopy_detector, _sample_stage, scan_spec))
8 changes: 4 additions & 4 deletions tests/unit_tests/test_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ async def test_spectroscopy(RE: RunEngine):
docs,
start=1,
descriptor=1,
stream_resource=1,
stream_datum=30,
stream_resource=2,
stream_datum=2 * 30,
event=30,
stop=1,
)
Expand Down Expand Up @@ -114,8 +114,8 @@ async def test_spectroscopy_defaults(RE: RunEngine):
docs,
start=1,
descriptor=1,
stream_resource=1,
stream_datum=5,
stream_resource=2,
stream_datum=2 * 5,
event=5,
stop=1,
)
Expand Down