From b22337f5ef9d60bae4d0f16057b89802235b2310 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 4 Apr 2024 14:20:57 +0100 Subject: [PATCH] made a new class for the PandA hdf writer and adjusted `PandAController` Also made it possible to pre-intialise blocks so that the controller can have the same pcap device as the post init panda. --- src/ophyd_async/epics/pvi/__init__.py | 4 +- src/ophyd_async/epics/pvi/pvi.py | 63 +++++++++++--- src/ophyd_async/panda/__init__.py | 9 +- src/ophyd_async/panda/common_panda.py | 50 +++++++++++ src/ophyd_async/panda/hdf_panda.py | 49 +++++++++++ src/ophyd_async/panda/panda.py | 74 ----------------- src/ophyd_async/panda/panda_controller.py | 10 +-- src/ophyd_async/panda/writers/hdf_writer.py | 4 +- tests/epics/test_pvi.py | 46 +++++++++- tests/panda/test_hdf_panda.py | 19 +++++ .../{test_panda.py => test_panda_connect.py} | 83 ++++++++++--------- tests/panda/test_panda_controller.py | 36 ++++++-- tests/panda/test_panda_utils.py | 23 +++-- tests/panda/test_trigger.py | 18 +++- tests/panda/test_writer.py | 25 +++++- tests/test_flyer_with_panda.py | 14 +++- 16 files changed, 363 insertions(+), 164 deletions(-) create mode 100644 src/ophyd_async/panda/common_panda.py create mode 100644 src/ophyd_async/panda/hdf_panda.py delete mode 100644 src/ophyd_async/panda/panda.py create mode 100644 tests/panda/test_hdf_panda.py rename tests/panda/{test_panda.py => test_panda_connect.py} (69%) diff --git a/src/ophyd_async/epics/pvi/__init__.py b/src/ophyd_async/epics/pvi/__init__.py index 307c3b35ef..522b96ae0a 100644 --- a/src/ophyd_async/epics/pvi/__init__.py +++ b/src/ophyd_async/epics/pvi/__init__.py @@ -1,3 +1,3 @@ -from .pvi import PVIEntry, fill_pvi_entries +from .pvi import PVIEntry, fill_pvi_entries, pre_initialize_blocks -__all__ = ["PVIEntry", "fill_pvi_entries"] +__all__ = ["PVIEntry", "fill_pvi_entries", "pre_initialize_blocks"] diff --git a/src/ophyd_async/epics/pvi/pvi.py b/src/ophyd_async/epics/pvi/pvi.py index dc211250db..9a30c61753 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/pvi.py @@ -44,13 +44,15 @@ def _strip_number_from_string(string: str) -> Tuple[str, Optional[int]]: return name, number -def _strip_union(field: Union[Union[T], T]) -> T: +def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]: + is_optional = False if get_origin(field) is Union: args = get_args(field) - for arg in args: - if arg is not type(None): - return arg - return field + is_optional = type(None) in args + for field in args: + if field is not type(None): + break + return field, is_optional def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]: @@ -80,10 +82,15 @@ def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]): if sub_name in ("_name", "parent"): continue assert entry.sub_entries - if sub_name not in entry.sub_entries and get_origin(sub_device) is not Optional: - raise RuntimeError( - f"sub device `{sub_name}:{type(sub_device)}` was not provided by pvi" - ) + device_t, is_optional = _strip_union(sub_device) + if sub_name not in entry.sub_entries: + if is_optional: + continue + else: + raise RuntimeError( + f"sub device `{sub_name}:{type(sub_device)}` " + "was not provided by pvi" + ) if isinstance(entry.sub_entries[sub_name], dict): for sub_sub_entry in entry.sub_entries[sub_name].values(): # type: ignore _verify_common_blocks(sub_sub_entry, sub_device) # type: ignore @@ -115,7 +122,7 @@ def _parse_type( ): if common_device_type: # pre-defined type - device_type = _strip_union(common_device_type) + device_type, _ = _strip_union(common_device_type) is_device_vector, device_type = _strip_device_vector(device_type) if ((origin := get_origin(device_type)) and issubclass(origin, Signal)) or ( @@ -152,7 +159,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): continue # we'll take the first type in the union which isn't NoneType - sub_device_t = _strip_union(sub_device_t) + sub_device_t, _ = _strip_union(sub_device_t) is_device_vector, sub_device_t = _strip_device_vector(sub_device_t) is_signal = ( (origin := get_origin(sub_device_t)) and issubclass(origin, Signal) @@ -185,7 +192,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): signal_type = args[0] if (args := get_args(sub_device_t)) else None sub_device = sub_device_t(SimSignalBackend(signal_type, sub_name)) else: - sub_device = sub_device_t() + sub_device = getattr(device, sub_name, sub_device_t()) if not is_signal: if is_device_vector: @@ -226,7 +233,10 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT): if is_signal: device = _pvi_mapping[frozenset(pva_entries.keys())](signal_dtype, *pvs) else: - device = device_type() + if hasattr(entry.device, sub_name): + device = getattr(entry.device, sub_name) + else: + device = device_type() sub_entry = PVIEntry( device=device, common_device_type=device_type, sub_entries={} @@ -296,3 +306,30 @@ async def fill_pvi_entries( # We call set name now the parent field has been set in all of the # introspect-initialized devices. This will recursively set the names. device.set_name(device.name) + + +def pre_initialize_blocks( + device: Device, included_optional_fields: Optional[Tuple[str, ...]] = None +): + """For intializing blocks at __init__ of ``device``.""" + for name, device_type in get_type_hints(type(device)).items(): + if name in ("_name", "parent"): + continue + device_type, is_optional = _strip_union(device_type) + if ( + is_optional + and included_optional_fields + and name not in included_optional_fields + ): + continue + is_device_vector, device_type = _strip_device_vector(device_type) + if ( + is_device_vector + or ((origin := get_origin(device_type)) and issubclass(origin, Signal)) + or (isclass(device_type) and issubclass(device_type, Signal)) + ): + continue + + sub_device = device_type() + setattr(device, name, sub_device) + pre_initialize_blocks(sub_device) diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/panda/__init__.py index 9c572f52c0..e60e60e8b2 100644 --- a/src/ophyd_async/panda/__init__.py +++ b/src/ophyd_async/panda/__init__.py @@ -1,12 +1,12 @@ -from .panda import ( - CommonPandABlocks, +from .common_panda import ( + CommonPandaBlocks, DataBlock, - PandA, PcapBlock, PulseBlock, SeqBlock, TimeUnits, ) +from .hdf_panda import HDFPanda from .panda_controller import PandaPcapController from .table import ( SeqTable, @@ -18,7 +18,8 @@ from .utils import phase_sorter __all__ = [ - "PandA", + "CommonPandaBlocks", + "HDFPanda", "PcapBlock", "PulseBlock", "seq_table_from_arrays", diff --git a/src/ophyd_async/panda/common_panda.py b/src/ophyd_async/panda/common_panda.py new file mode 100644 index 0000000000..0115604c99 --- /dev/null +++ b/src/ophyd_async/panda/common_panda.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from enum import Enum + +from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW +from ophyd_async.panda.table import SeqTable + + +class DataBlock(Device): + hdf_directory: SignalRW[str] + hdf_file_name: SignalRW[str] + num_capture: SignalRW[int] + num_captured: SignalR[int] + capture: SignalRW[bool] + flush_period: SignalRW[float] + + +class PulseBlock(Device): + delay: SignalRW[float] + width: SignalRW[float] + + +class TimeUnits(str, Enum): + min = "min" + s = "s" + ms = "ms" + us = "us" + + +class SeqBlock(Device): + table: SignalRW[SeqTable] + active: SignalRW[bool] + repeats: SignalRW[int] + prescale: SignalRW[float] + prescale_units: SignalRW[TimeUnits] + enable: SignalRW[str] + + +class PcapBlock(Device): + active: SignalR[bool] + arm: SignalRW[bool] + + +class CommonPandaBlocks(Device): + pulse: DeviceVector[PulseBlock] + seq: DeviceVector[SeqBlock] + pcap: PcapBlock + + # In future we may decide not to have a datablock + data: DataBlock diff --git a/src/ophyd_async/panda/hdf_panda.py b/src/ophyd_async/panda/hdf_panda.py new file mode 100644 index 0000000000..83fec41807 --- /dev/null +++ b/src/ophyd_async/panda/hdf_panda.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import Sequence + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + DirectoryProvider, + SignalR, + StandardDetector, +) +from ophyd_async.epics.pvi import fill_pvi_entries, pre_initialize_blocks + +from .common_panda import CommonPandaBlocks +from .panda_controller import PandaPcapController +from .writers.hdf_writer import PandaHDFWriter + + +class HDFPanda(CommonPandaBlocks, StandardDetector): + def __init__( + self, + prefix: str, + directory_provider: DirectoryProvider, + config_sigs: Sequence[SignalR] = (), + name: str = "", + ): + self._prefix = prefix + self.set_name(name) + + pre_initialize_blocks(self, included_optional_fields=("data",)) + controller = PandaPcapController(pcap=self.pcap) + writer = PandaHDFWriter( + prefix=prefix, + directory_provider=directory_provider, + name_provider=lambda: name, + panda_device=self, + ) + super().__init__( + controller=controller, + writer=writer, + config_sigs=config_sigs, + name=name, + writer_timeout=DEFAULT_TIMEOUT, + ) + + async def connect( + self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + ) -> None: + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim=sim, timeout=timeout) diff --git a/src/ophyd_async/panda/panda.py b/src/ophyd_async/panda/panda.py deleted file mode 100644 index d579877228..0000000000 --- a/src/ophyd_async/panda/panda.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from enum import Enum - -from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceVector, SignalR, SignalRW -from ophyd_async.epics.pvi import fill_pvi_entries -from ophyd_async.panda.table import SeqTable - - -class DataBlock(Device): - hdf_directory: SignalRW[str] - hdf_file_name: SignalRW[str] - num_capture: SignalRW[int] - num_captured: SignalR[int] - capture: SignalRW[bool] - flush_period: SignalRW[float] - - -class PulseBlock(Device): - delay: SignalRW[float] - width: SignalRW[float] - - -class TimeUnits(str, Enum): - min = "min" - s = "s" - ms = "ms" - us = "us" - - -class SeqBlock(Device): - table: SignalRW[SeqTable] - active: SignalRW[bool] - repeats: SignalRW[int] - prescale: SignalRW[float] - prescale_units: SignalRW[TimeUnits] - enable: SignalRW[str] - - -class PcapBlock(Device): - active: SignalR[bool] - arm: SignalRW[bool] - - -class CommonPandABlocks(Device): - pulse: DeviceVector[PulseBlock] - seq: DeviceVector[SeqBlock] - pcap: PcapBlock - - -class PandA(CommonPandABlocks): - data: DataBlock - - def __init__(self, prefix: str, name: str = "") -> None: - self._prefix = prefix - # Remove this assert once PandA IOC supports different prefixes - assert prefix.endswith(":"), f"PandA prefix '{prefix}' must end in ':'" - super().__init__(name) - - async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT - ) -> None: - """Initialises all blocks and connects them. - - First, checks for pvi information. If it exists, make all blocks from this. - Then, checks that all required blocks in the PandA have been made. - - If there's no pvi information, that's because we're in sim mode. In that case, - makes all required blocks. - """ - - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - - await super().connect(sim) diff --git a/src/ophyd_async/panda/panda_controller.py b/src/ophyd_async/panda/panda_controller.py index 2c1100bfff..6000909576 100644 --- a/src/ophyd_async/panda/panda_controller.py +++ b/src/ophyd_async/panda/panda_controller.py @@ -7,15 +7,11 @@ DetectorTrigger, wait_for_value, ) - -from .panda import PcapBlock +from ophyd_async.panda import PcapBlock class PandaPcapController(DetectorControl): - def __init__( - self, - pcap: PcapBlock, - ) -> None: + def __init__(self, pcap: PcapBlock) -> None: self.pcap = pcap def get_deadtime(self, exposure: float) -> float: @@ -35,7 +31,7 @@ async def arm( await wait_for_value(self.pcap.active, True, timeout=1) return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) - async def disarm(self): + async def disarm(self) -> AsyncStatus: await asyncio.gather(self.pcap.arm.set(False)) await wait_for_value(self.pcap.active, False, timeout=1) return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) diff --git a/src/ophyd_async/panda/writers/hdf_writer.py b/src/ophyd_async/panda/writers/hdf_writer.py index 69034f7de7..8c02ea188e 100644 --- a/src/ophyd_async/panda/writers/hdf_writer.py +++ b/src/ophyd_async/panda/writers/hdf_writer.py @@ -16,7 +16,7 @@ wait_for_value, ) from ophyd_async.core.signal import observe_value -from ophyd_async.panda.panda import PandA +from ophyd_async.panda import CommonPandaBlocks from .panda_hdf_file import _HDFDataset, _HDFFile @@ -91,7 +91,7 @@ def __init__( prefix: str, directory_provider: DirectoryProvider, name_provider: NameProvider, - panda_device: PandA, + panda_device: CommonPandaBlocks, ) -> None: self.panda_device = panda_device self._prefix = prefix diff --git a/tests/epics/test_pvi.py b/tests/epics/test_pvi.py index 6f29441254..0993716f8d 100644 --- a/tests/epics/test_pvi.py +++ b/tests/epics/test_pvi.py @@ -10,7 +10,7 @@ SignalRW, SignalX, ) -from ophyd_async.epics.pvi import fill_pvi_entries +from ophyd_async.epics.pvi import fill_pvi_entries, pre_initialize_blocks class Block1(Device): @@ -94,3 +94,47 @@ async def test_fill_pvi_entries_sim_mode(pvi_test_device_t): # top level signals are typed assert test_device.signal_rw._backend.datatype is int + + +@pytest.fixture +def pvi_test_device_pre_initialize_blocks_t(): + """A fixture since pytest discourages init in test case classes""" + + class TestDevice(Block3, Device): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + pre_initialize_blocks(self) + + async def connect( + self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + ) -> None: + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + + await super().connect(sim) + + yield TestDevice + + +async def test_device_pre_initialize_blocks(pvi_test_device_pre_initialize_blocks_t): + device = pvi_test_device_pre_initialize_blocks_t("PREFIX:") + + block_2_device = device.device + block_1_device = device.device.device + top_block_1_device = device.signal_device + + # The pre_initialize_blocks has only made blocks, + # not signals or device vectors + assert isinstance(block_2_device, Block2) + assert isinstance(block_1_device, Block1) + assert isinstance(top_block_1_device, Block1) + assert not hasattr(device, "signal_x") + assert not hasattr(device, "signal_rw") + assert not hasattr(top_block_1_device, "signal_rw") + + await device.connect(sim=True) + + # The memory addresses have not changed + assert device.device is block_2_device + assert device.device.device is block_1_device + assert device.signal_device is top_block_1_device diff --git a/tests/panda/test_hdf_panda.py b/tests/panda/test_hdf_panda.py new file mode 100644 index 0000000000..a14c1121dd --- /dev/null +++ b/tests/panda/test_hdf_panda.py @@ -0,0 +1,19 @@ +import pytest + +from ophyd_async.core import DeviceCollector, StaticDirectoryProvider +from ophyd_async.panda import HDFPanda + + +@pytest.fixture +async def sim_hdf_panda(tmp_path): + directory_provider = StaticDirectoryProvider(str(tmp_path), "test") + async with DeviceCollector(sim=True): + sim_hdf_panda = HDFPanda( + "HDFPANDA:", directory_provider=directory_provider, name="panda" + ) + yield sim_hdf_panda + + +async def test_hdf_panda_passes_blocks_to_controller(sim_hdf_panda: HDFPanda): + assert hasattr(sim_hdf_panda.controller, "pcap") + assert sim_hdf_panda.controller.pcap is sim_hdf_panda.pcap diff --git a/tests/panda/test_panda.py b/tests/panda/test_panda_connect.py similarity index 69% rename from tests/panda/test_panda.py rename to tests/panda/test_panda_connect.py index ba9e753e63..67706ec8cb 100644 --- a/tests/panda/test_panda.py +++ b/tests/panda/test_panda_connect.py @@ -1,4 +1,4 @@ -"""Test file specifying how we want to eventually interact with the panda...""" +"""Used to test setting up signals for a PandA""" import copy from typing import Dict @@ -6,18 +6,11 @@ import numpy as np import pytest -from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceCollector +from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceCollector, DeviceVector from ophyd_async.core.utils import NotConnected from ophyd_async.epics.pvi import PVIEntry, fill_pvi_entries -from ophyd_async.panda import ( - CommonPandABlocks, - PandA, - PcapBlock, - PulseBlock, - SeqBlock, - SeqTable, - SeqTrigger, -) +from ophyd_async.epics.pvi.pvi import pre_initialize_blocks +from ophyd_async.panda import PcapBlock, PulseBlock, SeqBlock, SeqTable, SeqTrigger class DummyDict: @@ -45,40 +38,45 @@ def get(self, pv: str, timeout: float = 0.0): @pytest.fixture -async def sim_panda(): - async with DeviceCollector(sim=True): - sim_panda = PandA("PANDAQSRV:", "sim_panda") +async def panda_t(): + class CommonPandaBlocksNoData(Device): + pcap: PcapBlock + pulse: DeviceVector[PulseBlock] + seq: DeviceVector[SeqBlock] - assert sim_panda.name == "sim_panda" - yield sim_panda + class Panda(CommonPandaBlocksNoData): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + pre_initialize_blocks(self) + super().__init__(name) + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) -class PandANoDataBlock(CommonPandABlocks): - def __init__(self, prefix: str, name: str = "") -> None: - self._prefix = prefix - assert prefix.endswith(":"), f"PandA prefix '{prefix}' must end in ':'" - super().__init__(name) + yield Panda - async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT - ) -> None: - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) +@pytest.fixture +async def sim_panda(panda_t): + async with DeviceCollector(sim=True): + sim_panda = panda_t("PANDAQSRV:", "sim_panda") - await super().connect(sim) + assert sim_panda.name == "sim_panda" + yield sim_panda -def test_panda_names_correct(sim_panda: PandA): +def test_panda_names_correct(sim_panda): assert sim_panda.seq[1].name == "sim_panda-seq-1" assert sim_panda.pulse[1].name == "sim_panda-pulse-1" -def test_panda_name_set(): - panda = PandA(":", "panda") +def test_panda_name_set(panda_t): + panda = panda_t(":", "panda") assert panda.name == "panda" -async def test_panda_children_connected(sim_panda: PandA): +async def test_panda_children_connected(sim_panda): # try to set and retrieve from simulated values... table = SeqTable( repeats=np.array([1, 1, 1, 32]).astype(np.uint16), @@ -114,8 +112,8 @@ async def test_panda_children_connected(sim_panda: PandA): assert readback_seq == table -async def test_panda_with_missing_blocks(panda_pva): - panda = PandA("PANDAQSRVI:") +async def test_panda_with_missing_blocks(panda_pva, panda_t): + panda = panda_t("PANDAQSRVI:") with pytest.raises(RuntimeError) as exc: await panda.connect() assert ( @@ -124,9 +122,8 @@ async def test_panda_with_missing_blocks(panda_pva): ) -async def test_panda_with_extra_blocks_and_signals(panda_pva): - - panda = PandANoDataBlock("PANDAQSRV:") +async def test_panda_with_extra_blocks_and_signals(panda_pva, panda_t): + panda = panda_t("PANDAQSRV:") await panda.connect() assert panda.extra # type: ignore assert panda.extra[1] # type: ignore @@ -134,10 +131,14 @@ async def test_panda_with_extra_blocks_and_signals(panda_pva): assert panda.pcap.newsignal # type: ignore -async def test_panda_gets_types_from_common_class(panda_pva): - panda = PandANoDataBlock("PANDAQSRV:") +async def test_panda_gets_types_from_common_class(panda_pva, panda_t): + panda = panda_t("PANDAQSRV:") + pcap = panda.pcap await panda.connect() + # The pre-initialized blocks are now filled + assert pcap is panda.pcap + # sub devices have the correct types assert isinstance(panda.pcap, PcapBlock) assert isinstance(panda.seq[1], SeqBlock) @@ -156,8 +157,8 @@ async def test_panda_gets_types_from_common_class(panda_pva): assert panda.pcap.newsignal._backend.datatype is None -async def test_panda_block_missing_signals(panda_pva): - panda = PandA("PANDAQSRVIB:") +async def test_panda_block_missing_signals(panda_pva, panda_t): + panda = panda_t("PANDAQSRVIB:") with pytest.raises(Exception) as exc: await panda.connect() @@ -168,8 +169,8 @@ async def test_panda_block_missing_signals(panda_pva): ) -async def test_panda_unable_to_connect_to_pvi(): - panda = PandA("NON-EXISTENT:") +async def test_panda_unable_to_connect_to_pvi(panda_t): + panda = panda_t("NON-EXISTENT:") with pytest.raises(NotConnected) as exc: await panda.connect(timeout=0.01) diff --git a/tests/panda/test_panda_controller.py b/tests/panda/test_panda_controller.py index d58c7e12ea..4f06f0107c 100644 --- a/tests/panda/test_panda_controller.py +++ b/tests/panda/test_panda_controller.py @@ -4,25 +4,49 @@ import pytest -from ophyd_async.core import DetectorTrigger, DeviceCollector -from ophyd_async.panda import PandA, PandaPcapController +from ophyd_async.core import DEFAULT_TIMEOUT, DetectorTrigger, Device, DeviceCollector +from ophyd_async.epics.pvi import fill_pvi_entries +from ophyd_async.epics.signal import epics_signal_rw +from ophyd_async.panda import CommonPandaBlocks, PandaPcapController @pytest.fixture async def sim_panda(): + class Panda(CommonPandaBlocks): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDACONTROLLER:", "sim_panda") + sim_panda = Panda("PANDACONTROLLER:", "sim_panda") + sim_panda.phase_1_signal_units = epics_signal_rw(int, "") + assert sim_panda.name == "sim_panda" yield sim_panda +async def test_panda_controller_not_filled_blocks(): + class PcapBlock(Device): + pass # Not filled + + pandaController = PandaPcapController(pcap=PcapBlock()) + with patch("ophyd_async.panda.panda_controller.wait_for_value", return_value=None): + with pytest.raises(AttributeError) as exc: + await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) + assert ("'PcapBlock' object has no attribute 'arm'") in str(exc.value) + + async def test_panda_controller_arm_disarm(sim_panda): - pandaController = PandaPcapController(pcap=sim_panda.pcap) + pandaController = PandaPcapController(sim_panda.pcap) with patch("ophyd_async.panda.panda_controller.wait_for_value", return_value=None): await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) await pandaController.disarm() -async def test_panda_controller_wrong_trigger(sim_panda): - pandaController = PandaPcapController(pcap=sim_panda.pcap) +async def test_panda_controller_wrong_trigger(): + pandaController = PandaPcapController(None) with pytest.raises(AssertionError): await pandaController.arm(num=1, trigger=DetectorTrigger.internal) diff --git a/tests/panda/test_panda_utils.py b/tests/panda/test_panda_utils.py index c636b62e01..98c3cc4a89 100644 --- a/tests/panda/test_panda_utils.py +++ b/tests/panda/test_panda_utils.py @@ -5,15 +5,29 @@ from ophyd_async.core import save_device from ophyd_async.core.device import DeviceCollector +from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.epics.pvi import fill_pvi_entries from ophyd_async.epics.signal import epics_signal_rw -from ophyd_async.panda import PandA +from ophyd_async.panda import CommonPandaBlocks, TimeUnits +from ophyd_async.panda.common_panda import DataBlock from ophyd_async.panda.utils import phase_sorter @pytest.fixture async def sim_panda(): + class Panda(CommonPandaBlocks): + data: DataBlock + + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDA:") + sim_panda = Panda("PANDA") sim_panda.phase_1_signal_units = epics_signal_rw(int, "") assert sim_panda.name == "sim_panda" yield sim_panda @@ -27,8 +41,8 @@ async def test_save_panda(mock_save_to_yaml, sim_panda, RE: RunEngine): [ { "phase_1_signal_units": 0, - "seq.1.prescale_units": "min", - "seq.2.prescale_units": "min", + "seq.1.prescale_units": TimeUnits("min"), + "seq.2.prescale_units": TimeUnits("min"), }, { "data.capture": False, @@ -37,7 +51,6 @@ async def test_save_panda(mock_save_to_yaml, sim_panda, RE: RunEngine): "data.hdf_file_name": "", "data.num_capture": 0, "pcap.arm": False, - "pcap.arm": False, "pulse.1.delay": 0.0, "pulse.1.width": 0.0, "pulse.2.delay": 0.0, diff --git a/tests/panda/test_trigger.py b/tests/panda/test_trigger.py index a4c3dc8a78..5a9c1f9b72 100644 --- a/tests/panda/test_trigger.py +++ b/tests/panda/test_trigger.py @@ -1,20 +1,30 @@ import pytest -from ophyd_async.core.device import DeviceCollector -from ophyd_async.panda import PandA +from ophyd_async.core.device import DEFAULT_TIMEOUT, DeviceCollector +from ophyd_async.epics.pvi.pvi import fill_pvi_entries +from ophyd_async.panda import CommonPandaBlocks from ophyd_async.panda.trigger import StaticSeqTableTriggerLogic @pytest.fixture async def panda(): + class Panda(CommonPandaBlocks): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDAQSRV:", "sim_panda") + sim_panda = Panda("PANDAQSRV:", "sim_panda") assert sim_panda.name == "sim_panda" yield sim_panda -def test_trigger_logic_has_given_methods(panda: PandA): +def test_trigger_logic_has_given_methods(panda): trigger_logic = StaticSeqTableTriggerLogic(panda.seq[1]) assert hasattr(trigger_logic, "prepare") assert hasattr(trigger_logic, "kickoff") diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index 18984ec61d..404655872a 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -4,6 +4,7 @@ import pytest from ophyd_async.core import ( + DEFAULT_TIMEOUT, Device, DeviceCollector, SignalR, @@ -11,8 +12,9 @@ StaticDirectoryProvider, set_sim_value, ) +from ophyd_async.epics.pvi import fill_pvi_entries, pre_initialize_blocks from ophyd_async.epics.signal.signal import SignalRW -from ophyd_async.panda import PandA +from ophyd_async.panda.common_panda import CommonPandaBlocks, DataBlock from ophyd_async.panda.writers import PandaHDFWriter from ophyd_async.panda.writers.hdf_writer import ( Capture, @@ -23,9 +25,26 @@ @pytest.fixture -async def sim_panda() -> PandA: +async def panda_t(): + class Panda(CommonPandaBlocks): + data: DataBlock + + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + pre_initialize_blocks(self) + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + + yield Panda + + +@pytest.fixture +async def sim_panda(panda_t): async with DeviceCollector(sim=True): - sim_panda = PandA("SIM_PANDA:", name="sim_panda") + sim_panda = panda_t("SIM_PANDA:", name="sim_panda") sim_panda.block1 = Device("BLOCK1") # type: ignore[attr-defined] sim_panda.block2 = Device("BLOCK2") # type: ignore[attr-defined] sim_panda.block1.test_capture = SignalRW( # type: ignore[attr-defined] diff --git a/tests/test_flyer_with_panda.py b/tests/test_flyer_with_panda.py index 573177e701..3e70df3467 100644 --- a/tests/test_flyer_with_panda.py +++ b/tests/test_flyer_with_panda.py @@ -19,7 +19,8 @@ from ophyd_async.core.detector import StandardDetector from ophyd_async.core.device import DeviceCollector from ophyd_async.core.signal import observe_value, set_sim_value -from ophyd_async.panda import PandA +from ophyd_async.epics.pvi.pvi import fill_pvi_entries +from ophyd_async.panda import CommonPandaBlocks from ophyd_async.panda.trigger import StaticSeqTableTriggerLogic from ophyd_async.planstubs import ( prepare_static_seq_table_flyer_and_detectors_with_same_trigger, @@ -115,8 +116,17 @@ async def dummy_arm_2(self=None, trigger=None, num=0, exposure=None): @pytest.fixture async def panda(): + class Panda(CommonPandaBlocks): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDAQSRV:", "sim_panda") + sim_panda = Panda("PANDAQSRV:", "sim_panda") assert sim_panda.name == "sim_panda" yield sim_panda