Skip to content

Commit

Permalink
Overhaul sim detector tests to use ad standard det factory, improve f…
Browse files Browse the repository at this point in the history
…actory with mock put callback
  • Loading branch information
jwlodek committed Sep 19, 2024
1 parent 1ae69f5 commit b7acf91
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 129 deletions.
32 changes: 0 additions & 32 deletions tests/epics/adsimdetector/test_adsim_controller.py

This file was deleted.

186 changes: 92 additions & 94 deletions tests/epics/adsimdetector/test_sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,40 @@
from collections import defaultdict
from pathlib import Path
from typing import cast
from unittest.mock import patch

import bluesky.plan_stubs as bps
import bluesky.preprocessors as bpp
import pytest
from bluesky import RunEngine
from bluesky.utils import new_uid
from bluesky.run_engine import RunEngine

import ophyd_async.plan_stubs as ops
from ophyd_async.core import (
AsyncStatus,
DetectorTrigger,
DeviceCollector,
StandardDetector,
StaticFilenameProvider,
StaticPathProvider,
TriggerInfo,
assert_emitted,
callback_on_mock_put,
set_mock_value,
)
from ophyd_async.epics import adcore, adsimdetector


async def make_detector(prefix: str, name: str, tmp_path: Path):
fp = StaticFilenameProvider(f"test-{new_uid()}")
dp = StaticPathProvider(fp, tmp_path)

async with DeviceCollector(mock=True):
det = adsimdetector.SimDetector(prefix, dp, name=name)
det._config_sigs = [det.drv.acquire_time, det.drv.acquire]
@pytest.fixture
def test_adsimdetector(ad_standard_det_factory):
return ad_standard_det_factory(adsimdetector.SimDetector)

def _set_full_file_name(val, *args, **kwargs):
set_mock_value(det.hdf.full_file_name, str(tmp_path / val))

callback_on_mock_put(det.hdf.file_name, _set_full_file_name)
@pytest.fixture
def two_test_adsimdetectors(ad_standard_det_factory):
deta = ad_standard_det_factory(adsimdetector.SimDetector)
detb = ad_standard_det_factory(adsimdetector.SimDetector, number=2)

return det
return deta, detb


def count_sim(dets: list[StandardDetector], times: int = 1):
def count_sim(dets: list[adsimdetector.SimDetector], times: int = 1):
"""Test plan to do the equivalent of bp.count for a sim detector."""

yield from bps.stage_all(*dets)
Expand Down Expand Up @@ -79,38 +73,8 @@ def count_sim(dets: list[StandardDetector], times: int = 1):
yield from bps.unstage_all(*dets)


@pytest.fixture
async def single_detector(RE: RunEngine, tmp_path: Path) -> StandardDetector:
detector = await make_detector(prefix="TEST:", name="test", tmp_path=tmp_path)

set_mock_value(detector._controller.driver.array_size_x, 10)
set_mock_value(detector._controller.driver.array_size_y, 20)
return detector


@pytest.fixture
async def two_detectors(tmp_path: Path):
deta = await make_detector(prefix="PREFIX1:", name="testa", tmp_path=tmp_path)
detb = await make_detector(prefix="PREFIX2:", name="testb", tmp_path=tmp_path)

# Simulate backend IOCs being in slightly different states
for i, det in enumerate((deta, detb)):
# accessing the hidden objects just for neat typing
controller = det._controller
writer = det._writer

set_mock_value(controller.driver.acquire_time, 0.8 + i)
set_mock_value(controller.driver.image_mode, adcore.ImageMode.continuous)
set_mock_value(writer.hdf.num_capture, 1000)
set_mock_value(writer.hdf.num_captured, 0)
set_mock_value(writer.hdf.file_path_exists, True)
set_mock_value(controller.driver.array_size_x, 1024 + i)
set_mock_value(controller.driver.array_size_y, 768 + i)
yield deta, detb


async def test_two_detectors_fly_different_rate(
two_detectors: list[adsimdetector.SimDetector], RE: RunEngine
two_test_adsimdetectors: list[adsimdetector.SimDetector], RE: RunEngine
):
trigger_info = TriggerInfo(
number=15,
Expand All @@ -131,37 +95,37 @@ def assert_n_stream_datums(
assert seq_nums["start"] == start
assert seq_nums["stop"] == stop

@bpp.stage_decorator(two_detectors)
@bpp.stage_decorator(two_test_adsimdetectors)
@bpp.run_decorator()
def fly_plan():
for det in two_detectors:
for det in two_test_adsimdetectors:
yield from bps.prepare(det, trigger_info, wait=True, group="prepare")
yield from bps.declare_stream(*two_detectors, name="primary")
yield from bps.declare_stream(*two_test_adsimdetectors, name="primary")

for det in two_detectors:
for det in two_test_adsimdetectors:
yield from bps.trigger(det, wait=False, group="trigger_cleanup")

# det[0] captures 5 frames, but we do not emit a StreamDatum as det[1] has not
set_mock_value(two_detectors[0].hdf.num_captured, 5)
set_mock_value(two_test_adsimdetectors[0].hdf.num_captured, 5)

yield from bps.collect(*two_detectors)
yield from bps.collect(*two_test_adsimdetectors)
assert_n_stream_datums(0)

# det[0] captures 10 frames, but we do not emit a StreamDatum as det[1] has not
set_mock_value(two_detectors[0].hdf.num_captured, 10)
yield from bps.collect(*two_detectors)
set_mock_value(two_test_adsimdetectors[0].hdf.num_captured, 10)
yield from bps.collect(*two_test_adsimdetectors)
assert_n_stream_datums(0)

# det[1] has caught up to first 7 frames, emit streamDatum for seq_num {1,7}
set_mock_value(two_detectors[1].hdf.num_captured, 7)
yield from bps.collect(*two_detectors)
set_mock_value(two_test_adsimdetectors[1].hdf.num_captured, 7)
yield from bps.collect(*two_test_adsimdetectors)
assert_n_stream_datums(2, 1, 8)

for det in two_detectors:
for det in two_test_adsimdetectors:
set_mock_value(det.hdf.num_captured, 15)

# emits stream datum for seq_num {8, 15}
yield from bps.collect(*two_detectors)
yield from bps.collect(*two_test_adsimdetectors)
assert_n_stream_datums(4, 8, 16)

# Trigger has complete as all expected frames written
Expand All @@ -174,7 +138,7 @@ def fly_plan():


async def test_two_detectors_step(
two_detectors: list[StandardDetector],
two_test_adsimdetectors: list[adsimdetector.SimDetector],
RE: RunEngine,
):
names = []
Expand All @@ -183,20 +147,22 @@ async def test_two_detectors_step(
RE.subscribe(lambda _, doc: docs.append(doc))
[
set_mock_value(cast(adcore.ADHDFWriter, det._writer).hdf.file_path_exists, True)
for det in two_detectors
for det in two_test_adsimdetectors
]

controller_a = cast(adsimdetector.SimController, two_detectors[0].controller)
writer_a = cast(adcore.ADHDFWriter, two_detectors[0].writer)
writer_b = cast(adcore.ADHDFWriter, two_detectors[1].writer)
info_a = writer_a._path_provider(device_name=writer_a.hdf.name)
info_b = writer_b._path_provider(device_name=writer_b.hdf.name)
controller_a = cast(
adsimdetector.SimController, two_test_adsimdetectors[0].controller
)
writer_a = cast(adcore.ADHDFWriter, two_test_adsimdetectors[0].writer)
writer_b = cast(adcore.ADHDFWriter, two_test_adsimdetectors[1].writer)
info_a = writer_a._path_provider(device_name=writer_a._name_provider())
info_b = writer_b._path_provider(device_name=writer_b._name_provider())
file_name_a = None
file_name_b = None

def plan():
nonlocal file_name_a, file_name_b
yield from count_sim(two_detectors, times=1)
yield from count_sim(two_test_adsimdetectors, times=1)

drv = controller_a.driver
assert False is (yield from bps.rd(drv.acquire))
Expand Down Expand Up @@ -229,38 +195,51 @@ def plan():
]

_, descriptor, sra, sda, srb, sdb, event, _ = docs
assert descriptor["configuration"]["testa"]["data"]["testa-drv-acquire_time"] == 0.8
assert descriptor["configuration"]["testb"]["data"]["testb-drv-acquire_time"] == 1.8
assert descriptor["data_keys"]["testa"]["shape"] == (768, 1024)
assert descriptor["data_keys"]["testb"]["shape"] == (769, 1025)
assert descriptor["configuration"]["test_adsim1"]["data"][
"test_adsim1-drv-acquire_time"
] == pytest.approx(0.8)
assert descriptor["configuration"]["test_adsim2"]["data"][
"test_adsim2-drv-acquire_time"
] == pytest.approx(1.8)
assert descriptor["data_keys"]["test_adsim1"]["shape"] == (10, 10)
assert descriptor["data_keys"]["test_adsim2"]["shape"] == (11, 11)
assert sda["stream_resource"] == sra["uid"]
assert sdb["stream_resource"] == srb["uid"]
assert srb["uri"] == "file://localhost" + str(info_b.directory_path / file_name_b)
assert sra["uri"] == "file://localhost" + str(info_a.directory_path / file_name_a)
assert (
srb["uri"]
== "file://localhost" + str(info_b.directory_path / info_b.filename) + ".h5"
)
assert (
sra["uri"]
== "file://localhost" + str(info_a.directory_path / info_a.filename) + ".h5"
)

assert event["data"] == {}


async def test_detector_writes_to_file(
RE: RunEngine, single_detector: StandardDetector, tmp_path: Path
RE: RunEngine, test_adsimdetector: adsimdetector.SimDetector, tmp_path: Path
):
names = []
docs = []
RE.subscribe(lambda name, _: names.append(name))
RE.subscribe(lambda _, doc: docs.append(doc))
set_mock_value(
cast(adcore.ADHDFWriter, single_detector._writer).hdf.file_path_exists, True
cast(adcore.ADHDFWriter, test_adsimdetector._writer).hdf.file_path_exists, True
)

RE(count_sim([single_detector], times=3))
RE(count_sim([test_adsimdetector], times=3))

assert await cast(
adcore.ADHDFWriter, single_detector.writer
adcore.ADHDFWriter, test_adsimdetector.writer
).hdf.file_path.get_value() == str(tmp_path)

descriptor_index = names.index("descriptor")

assert docs[descriptor_index].get("data_keys").get("test").get("shape") == (20, 10)
assert docs[descriptor_index].get("data_keys").get("test_adsim1").get("shape") == (
10,
10,
)
assert names == [
"start",
"descriptor",
Expand All @@ -275,39 +254,41 @@ async def test_detector_writes_to_file(
]


async def test_read_and_describe_detector(single_detector: StandardDetector):
describe = await single_detector.describe_configuration()
read = await single_detector.read_configuration()
async def test_read_and_describe_detector(
test_adsimdetector: adsimdetector.SimDetector,
):
describe = await test_adsimdetector.describe_configuration()
read = await test_adsimdetector.read_configuration()
assert describe == {
"test-drv-acquire_time": {
"source": "mock+ca://TEST:cam1:AcquireTime_RBV",
"test_adsim1-drv-acquire_time": {
"source": "mock+ca://SIM1:cam1:AcquireTime_RBV",
"dtype": "number",
"dtype_numpy": "<f8",
"shape": [],
},
"test-drv-acquire": {
"source": "mock+ca://TEST:cam1:Acquire_RBV",
"dtype": "boolean",
"dtype_numpy": "|b1",
"test_adsim1-drv-acquire_period": {
"source": "mock+ca://SIM1:cam1:AcquirePeriod_RBV",
"dtype": "number",
"dtype_numpy": "<f8",
"shape": [],
},
}
assert read == {
"test-drv-acquire_time": {
"value": 0.0,
"test_adsim1-drv-acquire_time": {
"value": 0.8,
"timestamp": pytest.approx(time.monotonic(), rel=1e-2),
"alarm_severity": 0,
},
"test-drv-acquire": {
"value": False,
"test_adsim1-drv-acquire_period": {
"value": 1.0,
"timestamp": pytest.approx(time.monotonic(), rel=1e-2),
"alarm_severity": 0,
},
}


async def test_read_returns_nothing(single_detector: StandardDetector):
assert await single_detector.read() == {}
async def test_read_returns_nothing(test_adsimdetector: adsimdetector.SimDetector):
assert await test_adsimdetector.read() == {}


async def test_trigger_logic():
Expand Down Expand Up @@ -373,3 +354,20 @@ def my_plan():

# Need to unstage to properly kill tasks
RE(bps.unstage(det, wait=True))


async def test_ad_sim_controller(test_adsimdetector: adsimdetector.SimDetector):
ad = test_adsimdetector._controller
with patch("ophyd_async.core._signal.wait_for_value", return_value=None):
await ad.prepare(TriggerInfo(number=1, trigger=DetectorTrigger.internal))
await ad.arm()
await ad.wait_for_idle()

driver = ad.driver
assert await driver.num_images.get_value() == 1
assert await driver.image_mode.get_value() == adcore.ImageMode.multiple
assert await driver.acquire.get_value() is True

await ad.disarm()

assert await driver.acquire.get_value() is False
Loading

0 comments on commit b7acf91

Please sign in to comment.