From a3b83d5346f4e484be34451642d980f7826ef7fd Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 4 Apr 2024 14:20:57 +0100 Subject: [PATCH 1/7] 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 | 55 ++++++++++-- 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 | 22 ++++- tests/panda/test_trigger.py | 18 +++- tests/panda/test_writer.py | 25 +++++- tests/test_flyer_with_panda.py | 14 +++- 16 files changed, 360 insertions(+), 158 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 881d4f6b24..d512dca67a 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/pvi.py @@ -1,5 +1,6 @@ import re from dataclasses import dataclass +from inspect import isclass from typing import ( Any, Callable, @@ -59,10 +60,11 @@ def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]: def _strip_union(field: Union[Union[T], T]) -> T: 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]]: @@ -92,10 +94,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 @@ -223,7 +230,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={} @@ -291,3 +301,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 dc58fce101..2abdf70eff 100644 --- a/src/ophyd_async/panda/writers/hdf_writer.py +++ b/src/ophyd_async/panda/writers/hdf_writer.py @@ -17,7 +17,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 @@ -96,7 +96,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 1259f199ab..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,39 +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) - await super().connect(sim) +@pytest.fixture +async def sim_panda(panda_t): + async with DeviceCollector(sim=True): + sim_panda = panda_t("PANDAQSRV:", "sim_panda") + 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), @@ -113,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 ( @@ -123,8 +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 @@ -132,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) @@ -154,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() @@ -166,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 c0b67a40f7..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, 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 ffb50dff4e..5ee0478a78 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, @@ -24,9 +26,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 d0cfab8221..c0a7eae8c9 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 From a9651c4af960f270d0cb2c77da199ae7cb6e342f Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 16 Apr 2024 10:29:05 +0100 Subject: [PATCH 2/7] fixed mutable default on `StaticDirectoryProvider` --- src/ophyd_async/core/_providers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ophyd_async/core/_providers.py b/src/ophyd_async/core/_providers.py index 9805f8bc55..295635ed17 100644 --- a/src/ophyd_async/core/_providers.py +++ b/src/ophyd_async/core/_providers.py @@ -39,8 +39,10 @@ def __init__( directory_path: Union[str, Path], filename_prefix: str = "", filename_suffix: str = "", - resource_dir: Path = Path("."), + resource_dir: Optional[Path] = None ) -> None: + if resource_dir is None: + resource_dir = Path("."), if isinstance(directory_path, str): directory_path = Path(directory_path) self._directory_info = DirectoryInfo( From 1885281d256977a534cf2f9156159afb5a43e6ad Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 16 Apr 2024 15:37:02 +0100 Subject: [PATCH 3/7] cleaned up fixture in test_writer --- tests/panda/test_writer.py | 60 +++++++++++++++----------------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index 5ee0478a78..2d69e58f78 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -25,10 +25,14 @@ from ophyd_async.panda.writers.panda_hdf_file import _HDFFile + @pytest.fixture async def panda_t(): + class CaptureBlock(Device): + test_capture: SignalR class Panda(CommonPandaBlocks): - data: DataBlock + block_a: CaptureBlock + block_b: CaptureBlock def __init__(self, prefix: str, name: str = ""): self._prefix = prefix @@ -45,30 +49,14 @@ async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): @pytest.fixture async def sim_panda(panda_t): async with DeviceCollector(sim=True): - 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] - backend=SimSignalBackend(str) - ) - sim_panda.block2.test_capture = SignalRW( # type: ignore[attr-defined] - backend=SimSignalBackend(str) - ) - - await asyncio.gather( - sim_panda.block1.connect(sim=True), # type: ignore[attr-defined] - sim_panda.block2.connect(sim=True), # type: ignore[attr-defined] - sim_panda.connect(sim=True), - ) + sim_panda = panda_t("SIM_PANDA", name="sim_panda") set_sim_value( - sim_panda.block1.test_capture, - Capture.MinMaxMean, # type: ignore[attr-defined] + sim_panda.block_a.test_capture, Capture.MinMaxMean # type: ignore[attr-defined] ) set_sim_value( - sim_panda.block2.test_capture, - Capture.No, # type: ignore[attr-defined] + sim_panda.block_b.test_capture, Capture.No # type: ignore[attr-defined] ) return sim_panda @@ -76,10 +64,10 @@ async def sim_panda(panda_t): @pytest.fixture async def sim_writer(tmp_path, sim_panda) -> PandaHDFWriter: - dir_prov = StaticDirectoryProvider(str(tmp_path), "", "/data.h5") + dir_prov = StaticDirectoryProvider(directory_path=str(tmp_path), filename_prefix="", filename_suffix="/data.h5") async with DeviceCollector(sim=True): writer = PandaHDFWriter( - "TEST-PANDA", dir_prov, lambda: "test-panda", panda_device=sim_panda + prefix="TEST-PANDA", directory_provider=dir_prov, name_provider=lambda: "test-panda", panda_device=sim_panda ) return writer @@ -97,8 +85,8 @@ async def test_get_capture_signals_gets_all_signals(sim_panda): ) capture_signals = get_capture_signals(sim_panda) expected_signals = [ - "block1.test_capture", - "block2.test_capture", + "block_a.test_capture", + "block_b.test_capture", "test_seq.seq1_capture", "test_seq.seq2_capture", ] @@ -108,18 +96,18 @@ async def test_get_capture_signals_gets_all_signals(sim_panda): async def test_get_signals_marked_for_capture(sim_panda): capture_signals = { - "block1.test_capture": sim_panda.block1.test_capture, - "block2.test_capture": sim_panda.block2.test_capture, + "block_a.test_capture": sim_panda.block_a.test_capture, + "block_b.test_capture": sim_panda.block_b.test_capture, } signals_marked_for_capture = await get_signals_marked_for_capture(capture_signals) assert len(signals_marked_for_capture) == 1 - assert signals_marked_for_capture["block1.test"].capture_type == Capture.MinMaxMean + assert signals_marked_for_capture["block_a.test"].capture_type == Capture.MinMaxMean async def test_open_returns_correct_descriptors(sim_writer: PandaHDFWriter): assert hasattr(sim_writer.panda_device, "data") - cap1 = sim_writer.panda_device.block1.test_capture # type: ignore[attr-defined] - cap2 = sim_writer.panda_device.block2.test_capture # type: ignore[attr-defined] + cap1 = sim_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] + cap2 = sim_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] set_sim_value(cap1, Capture.MinMaxMean) set_sim_value(cap2, Capture.Value) description = await sim_writer.open() # to make capturing status not time out @@ -131,10 +119,10 @@ async def test_open_returns_correct_descriptors(sim_writer: PandaHDFWriter): assert "source" in entry assert entry.get("external") == "STREAM:" expected_datakeys = [ - "test-panda-block1-test-Min", - "test-panda-block1-test-Max", - "test-panda-block1-test-Mean", - "test-panda-block2-test-Value", + "test-panda-block_a-test-Min", + "test-panda-block_a-test-Max", + "test-panda-block_a-test-Mean", + "test-panda-block_b-test-Value", ] for key in expected_datakeys: assert key in description @@ -178,8 +166,8 @@ async def test_wait_for_index(sim_writer: PandaHDFWriter): async def test_collect_stream_docs(sim_writer: PandaHDFWriter): # Give the sim writer datasets - cap1 = sim_writer.panda_device.block1.test_capture # type: ignore[attr-defined] - cap2 = sim_writer.panda_device.block2.test_capture # type: ignore[attr-defined] + cap1 = sim_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] + cap2 = sim_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] set_sim_value(cap1, Capture.MinMaxMean) set_sim_value(cap2, Capture.Value) await sim_writer.open() @@ -188,7 +176,7 @@ async def test_collect_stream_docs(sim_writer: PandaHDFWriter): assert type(sim_writer._file) is _HDFFile assert sim_writer._file._last_emitted == 1 resource_doc = sim_writer._file._bundles[0].stream_resource_doc - assert resource_doc["data_key"] == "test-panda-block1-test-Min" + assert resource_doc["data_key"] == "test-panda-block_a-test-Min" assert "sim_panda/data.h5" in resource_doc["resource_path"] From 606e1f4fdedd8b7ac66d2586c5957543a4ebf3d9 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 17 Apr 2024 11:29:53 +0100 Subject: [PATCH 4/7] made test for the new `HDFPanda` --- src/ophyd_async/core/_providers.py | 4 +- src/ophyd_async/panda/common_panda.py | 3 +- src/ophyd_async/panda/writers/hdf_writer.py | 8 +- tests/panda/test_hdf_panda.py | 230 +++++++++++++++++++- tests/panda/test_writer.py | 14 +- 5 files changed, 239 insertions(+), 20 deletions(-) diff --git a/src/ophyd_async/core/_providers.py b/src/ophyd_async/core/_providers.py index 295635ed17..091ea387de 100644 --- a/src/ophyd_async/core/_providers.py +++ b/src/ophyd_async/core/_providers.py @@ -39,10 +39,10 @@ def __init__( directory_path: Union[str, Path], filename_prefix: str = "", filename_suffix: str = "", - resource_dir: Optional[Path] = None + resource_dir: Optional[Path] = None, ) -> None: if resource_dir is None: - resource_dir = Path("."), + resource_dir = Path(".") if isinstance(directory_path, str): directory_path = Path(directory_path) self._directory_info = DirectoryInfo( diff --git a/src/ophyd_async/panda/common_panda.py b/src/ophyd_async/panda/common_panda.py index 0115604c99..94f939cb64 100644 --- a/src/ophyd_async/panda/common_panda.py +++ b/src/ophyd_async/panda/common_panda.py @@ -7,6 +7,7 @@ class DataBlock(Device): + # In future we may decide to make hdf_* optional hdf_directory: SignalRW[str] hdf_file_name: SignalRW[str] num_capture: SignalRW[int] @@ -45,6 +46,4 @@ 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/writers/hdf_writer.py b/src/ophyd_async/panda/writers/hdf_writer.py index 2abdf70eff..4df5dd03de 100644 --- a/src/ophyd_async/panda/writers/hdf_writer.py +++ b/src/ophyd_async/panda/writers/hdf_writer.py @@ -142,11 +142,11 @@ async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]: for attribute_path, capture_signal in to_capture.items(): split_path = attribute_path.split(".") signal_name = split_path[-1] + # Get block names from numbered blocks, eg INENC[1] block_name = ( - split_path[-2] - if not split_path[-2].isnumeric() - # Get block names from numbered blocks, eg INENC[1] - else f"{split_path[-3]}{split_path[-2]}" + f"{split_path[-3]}{split_path[-2]}" + if split_path[-2].isnumeric() + else split_path[-2] ) for suffix in str(capture_signal.capture_type).split(" "): diff --git a/tests/panda/test_hdf_panda.py b/tests/panda/test_hdf_panda.py index a14c1121dd..9d631e30dc 100644 --- a/tests/panda/test_hdf_panda.py +++ b/tests/panda/test_hdf_panda.py @@ -1,19 +1,235 @@ +import asyncio +from typing import Dict, Optional + import pytest +from bluesky import plan_stubs as bps +from bluesky.run_engine import RunEngine + +from ophyd_async.core import StaticDirectoryProvider, set_sim_value +from ophyd_async.core.async_status import AsyncStatus +from ophyd_async.core.detector import DetectorControl, DetectorTrigger +from ophyd_async.core.device import Device +from ophyd_async.core.flyer import HardwareTriggeredFlyable +from ophyd_async.core.signal import SignalR, wait_for_value +from ophyd_async.core.sim_signal_backend import SimSignalBackend +from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.panda import HDFPanda, PcapBlock +from ophyd_async.panda.trigger import StaticSeqTableTriggerLogic +from ophyd_async.panda.writers.hdf_writer import Capture +from ophyd_async.planstubs.prepare_trigger_and_dets import ( + prepare_static_seq_table_flyer_and_detectors_with_same_trigger, +) + -from ophyd_async.core import DeviceCollector, StaticDirectoryProvider -from ophyd_async.panda import HDFPanda +class MockPandaPcapController(DetectorControl): + def __init__(self, pcap: PcapBlock) -> None: + self.pcap = pcap + + def get_deadtime(self, exposure: float) -> float: + return 0.000000008 + + async def arm( + self, + num: int, + trigger: DetectorTrigger = DetectorTrigger.constant_gate, + exposure: Optional[float] = None, + timeout=DEFAULT_TIMEOUT, + ) -> AsyncStatus: + assert trigger in ( + DetectorTrigger.constant_gate, + trigger == DetectorTrigger.variable_gate, + ), ( + f"Receieved trigger {trigger}. Only constant_gate and " + "variable_gate triggering is supported on the PandA" + ) + await self.pcap.arm.set(True, wait=True, timeout=timeout) + await wait_for_value(self.pcap.active, True, timeout=timeout) + await asyncio.sleep(0.2) + await self.pcap.arm.set(False, wait=False, timeout=timeout) + return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) + + async def disarm(self, timeout=DEFAULT_TIMEOUT) -> AsyncStatus: + await self.pcap.arm.set(False, wait=True, timeout=timeout) + await wait_for_value(self.pcap.active, False, timeout=timeout) + await asyncio.sleep(0.2) + set_sim_value(self.pcap.active, True) + return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) @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" - ) + class CaptureBlock(Device): + test_capture: SignalR + + directory_provider = StaticDirectoryProvider(str(tmp_path), filename_prefix="test") + sim_hdf_panda = HDFPanda( + "HDFPANDA:", directory_provider=directory_provider, name="panda" + ) + sim_hdf_panda._controller = MockPandaPcapController(sim_hdf_panda.pcap) + block_a = CaptureBlock(name="block_a") + block_b = CaptureBlock(name="block_b") + block_a.test_capture = SignalR(backend=SimSignalBackend(Capture, source="block_a")) + block_b.test_capture = SignalR(backend=SimSignalBackend(Capture, source="block_b")) + + setattr(sim_hdf_panda, "block_a", block_a) + setattr(sim_hdf_panda, "block_b", block_b) + await sim_hdf_panda.connect(sim=True) + set_sim_value(block_a.test_capture, Capture.Min) + set_sim_value(block_b.test_capture, Capture.Diff) + 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 + + +async def test_hdf_panda_hardware_triggered_flyable( + RE: RunEngine, + sim_hdf_panda, +): + names = [] + docs = [] + + def append_and_print(name, doc): + names.append(name) + docs.append(doc) + + RE.subscribe(append_and_print) + + shutter_time = 0.004 + exposure = 1 + + trigger_logic = StaticSeqTableTriggerLogic(sim_hdf_panda.seq[1]) + flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") + + def flying_plan(): + yield from bps.stage_all(sim_hdf_panda, flyer) + + yield from prepare_static_seq_table_flyer_and_detectors_with_same_trigger( + flyer, + [sim_hdf_panda], + num=1, + width=exposure, + deadtime=sim_hdf_panda.controller.get_deadtime(1), + shutter_time=shutter_time, + ) + # sim_hdf_panda.controller.disarm.assert_called_once # type: ignore + + yield from bps.open_run() + yield from bps.declare_stream(sim_hdf_panda, name="main_stream", collect=True) + + set_sim_value(flyer.trigger_logic.seq.active, 1) + + yield from bps.kickoff(flyer, wait=True) + yield from bps.kickoff(sim_hdf_panda) + + yield from bps.complete(flyer, wait=False, group="complete") + yield from bps.complete(sim_hdf_panda, wait=False, group="complete") + + # Manually incremenet the index as if a frame was taken + set_sim_value( + sim_hdf_panda.data.num_captured, + sim_hdf_panda.data.num_captured._backend._value + 1, + ) + + set_sim_value(flyer.trigger_logic.seq.active, 0) + + done = False + while not done: + try: + yield from bps.wait(group="complete", timeout=0.5) + except TimeoutError: + pass + else: + done = True + yield from bps.collect( + sim_hdf_panda, + return_payload=False, + name="main_stream", + ) + yield from bps.wait(group="complete") + yield from bps.close_run() + + yield from bps.unstage_all(flyer, sim_hdf_panda) + # assert sim_hdf_panda.controller.disarm.called # type: ignore + + # fly scan + RE(flying_plan()) + + assert names == [ + "start", + "descriptor", + "stream_resource", + "stream_resource", + "stream_datum", + "stream_datum", + "stop", + ] + named_docs = dict( + zip( + [ + "start", + "descriptor", + "stream_resource_a", + "stream_resource_b", + "stream_datum_a", + "stream_datum_b", + "stop", + ], + docs, + ) + ) + + # test descriptor + data_key_names: Dict[str, str] = named_docs["descriptor"]["object_keys"]["panda"] + assert data_key_names == [ + "panda-block_a-test-Min", + "panda-block_b-test-Diff", + ] + for data_key_name in data_key_names: + assert ( + named_docs["descriptor"]["data_keys"][data_key_name]["source"] + == "sim://hdf_directory" + ) + + # test stream resources + for block_letter, data_key_name in zip(("a", "b"), data_key_names): + assert ( + named_docs[f"stream_resource_{block_letter}"]["data_key"] == data_key_name + ) + assert ( + named_docs[f"stream_resource_{block_letter}"]["spec"] + == "AD_HDF5_SWMR_SLICE" + ) + assert ( + named_docs[f"stream_resource_{block_letter}"]["run_start"] + == named_docs["start"]["uid"] + ) + assert named_docs[f"stream_resource_{block_letter}"]["resource_kwargs"] == { + "block": f"block_{block_letter}", + "multiplier": 1, + "name": data_key_name, + "path": f"BLOCK_{block_letter.upper()}-TEST-{data_key_name.split('-')[-1]}", + "timestamps": "/entry/instrument/NDAttributes/NDArrayTimeStamp", + } + + # test stream datum + for block_letter in ("a", "b"): + assert ( + named_docs[f"stream_datum_{block_letter}"]["descriptor"] + == named_docs["descriptor"]["uid"] + ) + assert named_docs[f"stream_datum_{block_letter}"]["seq_nums"] == { + "start": 1, + "stop": 2, + } + assert named_docs[f"stream_datum_{block_letter}"]["indices"] == { + "start": 0, + "stop": 1, + } + assert ( + named_docs[f"stream_datum_{block_letter}"]["stream_resource"] + == named_docs[f"stream_resource_{block_letter}"]["uid"] + ) diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index 2d69e58f78..99bf9d9b03 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -13,8 +13,7 @@ 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.common_panda import CommonPandaBlocks, DataBlock +from ophyd_async.panda.common_panda import CommonPandaBlocks from ophyd_async.panda.writers import PandaHDFWriter from ophyd_async.panda.writers.hdf_writer import ( Capture, @@ -25,11 +24,11 @@ from ophyd_async.panda.writers.panda_hdf_file import _HDFFile - @pytest.fixture async def panda_t(): class CaptureBlock(Device): test_capture: SignalR + class Panda(CommonPandaBlocks): block_a: CaptureBlock block_b: CaptureBlock @@ -64,10 +63,15 @@ async def sim_panda(panda_t): @pytest.fixture async def sim_writer(tmp_path, sim_panda) -> PandaHDFWriter: - dir_prov = StaticDirectoryProvider(directory_path=str(tmp_path), filename_prefix="", filename_suffix="/data.h5") + dir_prov = StaticDirectoryProvider( + directory_path=str(tmp_path), filename_prefix="", filename_suffix="/data.h5" + ) async with DeviceCollector(sim=True): writer = PandaHDFWriter( - prefix="TEST-PANDA", directory_provider=dir_prov, name_provider=lambda: "test-panda", panda_device=sim_panda + prefix="TEST-PANDA", + directory_provider=dir_prov, + name_provider=lambda: "test-panda", + panda_device=sim_panda, ) return writer From 878b6cb67a147d9cbd3e7ec092c7732b659a0b97 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 17 Apr 2024 14:36:21 +0100 Subject: [PATCH 5/7] gave panda .py files underscores --- src/ophyd_async/panda/__init__.py | 10 +++++----- .../panda/{common_panda.py => _common_panda.py} | 2 +- src/ophyd_async/panda/{hdf_panda.py => _hdf_panda.py} | 10 +++++----- .../{panda_controller.py => _panda_controller.py} | 0 src/ophyd_async/panda/{table.py => _table.py} | 0 src/ophyd_async/panda/{trigger.py => _trigger.py} | 0 src/ophyd_async/panda/{utils.py => _utils.py} | 0 src/ophyd_async/panda/writers/__init__.py | 2 +- .../panda/writers/{hdf_writer.py => _hdf_writer.py} | 2 +- .../writers/{panda_hdf_file.py => _panda_hdf_file.py} | 0 src/ophyd_async/planstubs/prepare_trigger_and_dets.py | 4 ++-- tests/panda/test_hdf_panda.py | 4 ++-- tests/panda/test_panda_controller.py | 4 ++-- tests/panda/test_panda_utils.py | 4 ++-- tests/panda/test_table.py | 2 +- tests/panda/test_trigger.py | 2 +- tests/panda/test_writer.py | 8 ++++---- tests/test_flyer_with_panda.py | 2 +- 18 files changed, 28 insertions(+), 28 deletions(-) rename src/ophyd_async/panda/{common_panda.py => _common_panda.py} (95%) rename src/ophyd_async/panda/{hdf_panda.py => _hdf_panda.py} (83%) rename src/ophyd_async/panda/{panda_controller.py => _panda_controller.py} (100%) rename src/ophyd_async/panda/{table.py => _table.py} (100%) rename src/ophyd_async/panda/{trigger.py => _trigger.py} (100%) rename src/ophyd_async/panda/{utils.py => _utils.py} (100%) rename src/ophyd_async/panda/writers/{hdf_writer.py => _hdf_writer.py} (99%) rename src/ophyd_async/panda/writers/{panda_hdf_file.py => _panda_hdf_file.py} (100%) diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/panda/__init__.py index e60e60e8b2..585b1f9839 100644 --- a/src/ophyd_async/panda/__init__.py +++ b/src/ophyd_async/panda/__init__.py @@ -1,4 +1,4 @@ -from .common_panda import ( +from ._common_panda import ( CommonPandaBlocks, DataBlock, PcapBlock, @@ -6,16 +6,16 @@ SeqBlock, TimeUnits, ) -from .hdf_panda import HDFPanda -from .panda_controller import PandaPcapController -from .table import ( +from ._hdf_panda import HDFPanda +from ._panda_controller import PandaPcapController +from ._table import ( SeqTable, SeqTableRow, SeqTrigger, seq_table_from_arrays, seq_table_from_rows, ) -from .utils import phase_sorter +from ._utils import phase_sorter __all__ = [ "CommonPandaBlocks", diff --git a/src/ophyd_async/panda/common_panda.py b/src/ophyd_async/panda/_common_panda.py similarity index 95% rename from src/ophyd_async/panda/common_panda.py rename to src/ophyd_async/panda/_common_panda.py index 94f939cb64..9c8501f52f 100644 --- a/src/ophyd_async/panda/common_panda.py +++ b/src/ophyd_async/panda/_common_panda.py @@ -3,7 +3,7 @@ from enum import Enum from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW -from ophyd_async.panda.table import SeqTable +from ophyd_async.panda._table import SeqTable class DataBlock(Device): diff --git a/src/ophyd_async/panda/hdf_panda.py b/src/ophyd_async/panda/_hdf_panda.py similarity index 83% rename from src/ophyd_async/panda/hdf_panda.py rename to src/ophyd_async/panda/_hdf_panda.py index 83fec41807..6a64924b75 100644 --- a/src/ophyd_async/panda/hdf_panda.py +++ b/src/ophyd_async/panda/_hdf_panda.py @@ -10,9 +10,10 @@ ) 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 + +from ._common_panda import CommonPandaBlocks +from ._panda_controller import PandaPcapController +from .writers._hdf_writer import PandaHDFWriter class HDFPanda(CommonPandaBlocks, StandardDetector): @@ -24,9 +25,8 @@ def __init__( name: str = "", ): self._prefix = prefix - self.set_name(name) - pre_initialize_blocks(self, included_optional_fields=("data",)) + pre_initialize_blocks(self) controller = PandaPcapController(pcap=self.pcap) writer = PandaHDFWriter( prefix=prefix, diff --git a/src/ophyd_async/panda/panda_controller.py b/src/ophyd_async/panda/_panda_controller.py similarity index 100% rename from src/ophyd_async/panda/panda_controller.py rename to src/ophyd_async/panda/_panda_controller.py diff --git a/src/ophyd_async/panda/table.py b/src/ophyd_async/panda/_table.py similarity index 100% rename from src/ophyd_async/panda/table.py rename to src/ophyd_async/panda/_table.py diff --git a/src/ophyd_async/panda/trigger.py b/src/ophyd_async/panda/_trigger.py similarity index 100% rename from src/ophyd_async/panda/trigger.py rename to src/ophyd_async/panda/_trigger.py diff --git a/src/ophyd_async/panda/utils.py b/src/ophyd_async/panda/_utils.py similarity index 100% rename from src/ophyd_async/panda/utils.py rename to src/ophyd_async/panda/_utils.py diff --git a/src/ophyd_async/panda/writers/__init__.py b/src/ophyd_async/panda/writers/__init__.py index a623a16a16..7cc7974ea7 100644 --- a/src/ophyd_async/panda/writers/__init__.py +++ b/src/ophyd_async/panda/writers/__init__.py @@ -1,3 +1,3 @@ -from .hdf_writer import PandaHDFWriter +from ._hdf_writer import PandaHDFWriter __all__ = ["PandaHDFWriter"] diff --git a/src/ophyd_async/panda/writers/hdf_writer.py b/src/ophyd_async/panda/writers/_hdf_writer.py similarity index 99% rename from src/ophyd_async/panda/writers/hdf_writer.py rename to src/ophyd_async/panda/writers/_hdf_writer.py index 4df5dd03de..07abdb5bf0 100644 --- a/src/ophyd_async/panda/writers/hdf_writer.py +++ b/src/ophyd_async/panda/writers/_hdf_writer.py @@ -19,7 +19,7 @@ from ophyd_async.core.signal import observe_value from ophyd_async.panda import CommonPandaBlocks -from .panda_hdf_file import _HDFDataset, _HDFFile +from ._panda_hdf_file import _HDFDataset, _HDFFile class Capture(str, Enum): diff --git a/src/ophyd_async/panda/writers/panda_hdf_file.py b/src/ophyd_async/panda/writers/_panda_hdf_file.py similarity index 100% rename from src/ophyd_async/panda/writers/panda_hdf_file.py rename to src/ophyd_async/panda/writers/_panda_hdf_file.py diff --git a/src/ophyd_async/planstubs/prepare_trigger_and_dets.py b/src/ophyd_async/planstubs/prepare_trigger_and_dets.py index 08e481b74e..541c61faac 100644 --- a/src/ophyd_async/planstubs/prepare_trigger_and_dets.py +++ b/src/ophyd_async/planstubs/prepare_trigger_and_dets.py @@ -5,8 +5,8 @@ from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo from ophyd_async.core.flyer import HardwareTriggeredFlyable from ophyd_async.core.utils import in_micros -from ophyd_async.panda.table import SeqTable, SeqTableRow, seq_table_from_rows -from ophyd_async.panda.trigger import SeqTableInfo +from ophyd_async.panda._table import SeqTable, SeqTableRow, seq_table_from_rows +from ophyd_async.panda._trigger import SeqTableInfo def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( diff --git a/tests/panda/test_hdf_panda.py b/tests/panda/test_hdf_panda.py index 9d631e30dc..d880e6b487 100644 --- a/tests/panda/test_hdf_panda.py +++ b/tests/panda/test_hdf_panda.py @@ -14,8 +14,8 @@ from ophyd_async.core.sim_signal_backend import SimSignalBackend from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.panda import HDFPanda, PcapBlock -from ophyd_async.panda.trigger import StaticSeqTableTriggerLogic -from ophyd_async.panda.writers.hdf_writer import Capture +from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic +from ophyd_async.panda.writers._hdf_writer import Capture from ophyd_async.planstubs.prepare_trigger_and_dets import ( prepare_static_seq_table_flyer_and_detectors_with_same_trigger, ) diff --git a/tests/panda/test_panda_controller.py b/tests/panda/test_panda_controller.py index 4f06f0107c..875a4158b6 100644 --- a/tests/panda/test_panda_controller.py +++ b/tests/panda/test_panda_controller.py @@ -33,7 +33,7 @@ class PcapBlock(Device): pass # Not filled pandaController = PandaPcapController(pcap=PcapBlock()) - with patch("ophyd_async.panda.panda_controller.wait_for_value", return_value=None): + 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) @@ -41,7 +41,7 @@ class PcapBlock(Device): async def test_panda_controller_arm_disarm(sim_panda): pandaController = PandaPcapController(sim_panda.pcap) - with patch("ophyd_async.panda.panda_controller.wait_for_value", return_value=None): + 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() diff --git a/tests/panda/test_panda_utils.py b/tests/panda/test_panda_utils.py index 98c3cc4a89..acb4b375fc 100644 --- a/tests/panda/test_panda_utils.py +++ b/tests/panda/test_panda_utils.py @@ -9,8 +9,8 @@ from ophyd_async.epics.pvi import fill_pvi_entries from ophyd_async.epics.signal import epics_signal_rw from ophyd_async.panda import CommonPandaBlocks, TimeUnits -from ophyd_async.panda.common_panda import DataBlock -from ophyd_async.panda.utils import phase_sorter +from ophyd_async.panda._common_panda import DataBlock +from ophyd_async.panda._utils import phase_sorter @pytest.fixture diff --git a/tests/panda/test_table.py b/tests/panda/test_table.py index 0c6f347aa9..735fa32886 100644 --- a/tests/panda/test_table.py +++ b/tests/panda/test_table.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from ophyd_async.panda.table import seq_table_from_arrays +from ophyd_async.panda._table import seq_table_from_arrays def test_from_arrays_inconsistent_lengths(): diff --git a/tests/panda/test_trigger.py b/tests/panda/test_trigger.py index 5a9c1f9b72..ac23aeba7c 100644 --- a/tests/panda/test_trigger.py +++ b/tests/panda/test_trigger.py @@ -3,7 +3,7 @@ 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 +from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic @pytest.fixture diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index 99bf9d9b03..d90706a2d2 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -13,15 +13,15 @@ set_sim_value, ) from ophyd_async.epics.pvi import fill_pvi_entries, pre_initialize_blocks -from ophyd_async.panda.common_panda import CommonPandaBlocks +from ophyd_async.panda._common_panda import CommonPandaBlocks from ophyd_async.panda.writers import PandaHDFWriter -from ophyd_async.panda.writers.hdf_writer import ( +from ophyd_async.panda.writers._hdf_writer import ( Capture, CaptureSignalWrapper, get_capture_signals, get_signals_marked_for_capture, ) -from ophyd_async.panda.writers.panda_hdf_file import _HDFFile +from ophyd_async.panda.writers._panda_hdf_file import _HDFFile @pytest.fixture @@ -194,7 +194,7 @@ async def get_numeric_signal(_): } with patch( - "ophyd_async.panda.writers.hdf_writer.get_signals_marked_for_capture", + "ophyd_async.panda.writers._hdf_writer.get_signals_marked_for_capture", get_numeric_signal, ): assert "test-panda-block-1-Capture.Value" in await sim_writer.open() diff --git a/tests/test_flyer_with_panda.py b/tests/test_flyer_with_panda.py index c0a7eae8c9..521f3f1bbc 100644 --- a/tests/test_flyer_with_panda.py +++ b/tests/test_flyer_with_panda.py @@ -21,7 +21,7 @@ from ophyd_async.core.signal import observe_value, set_sim_value 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.panda._trigger import StaticSeqTableTriggerLogic from ophyd_async.planstubs import ( prepare_static_seq_table_flyer_and_detectors_with_same_trigger, ) From 2fcdb3b033df70b9413464fd541d69823f98e118 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 17 Apr 2024 14:41:17 +0100 Subject: [PATCH 6/7] renamed pre_initialized blocks --- src/ophyd_async/epics/pvi/__init__.py | 4 ++-- src/ophyd_async/epics/pvi/pvi.py | 24 ++++++++---------------- src/ophyd_async/panda/_hdf_panda.py | 5 ++--- tests/epics/test_pvi.py | 14 ++++++++------ tests/panda/test_panda_connect.py | 4 ++-- tests/panda/test_writer.py | 8 ++++---- 6 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/ophyd_async/epics/pvi/__init__.py b/src/ophyd_async/epics/pvi/__init__.py index 522b96ae0a..ad638b740c 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, pre_initialize_blocks +from .pvi import PVIEntry, create_children_from_annotations, fill_pvi_entries -__all__ = ["PVIEntry", "fill_pvi_entries", "pre_initialize_blocks"] +__all__ = ["PVIEntry", "fill_pvi_entries", "create_children_from_annotations"] diff --git a/src/ophyd_async/epics/pvi/pvi.py b/src/ophyd_async/epics/pvi/pvi.py index d512dca67a..8379936baf 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/pvi.py @@ -95,14 +95,10 @@ def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]): continue assert entry.sub_entries 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 sub_name not in entry.sub_entries and not is_optional: + 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 @@ -303,19 +299,15 @@ async def fill_pvi_entries( device.set_name(device.name) -def pre_initialize_blocks( - device: Device, included_optional_fields: Optional[Tuple[str, ...]] = None +def create_children_from_annotations( + device: Device, included_optional_fields: Tuple[str, ...] = () ): """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 - ): + if is_optional and name not in included_optional_fields: continue is_device_vector, device_type = _strip_device_vector(device_type) if ( @@ -327,4 +319,4 @@ def pre_initialize_blocks( sub_device = device_type() setattr(device, name, sub_device) - pre_initialize_blocks(sub_device) + create_children_from_annotations(sub_device) diff --git a/src/ophyd_async/panda/_hdf_panda.py b/src/ophyd_async/panda/_hdf_panda.py index 6a64924b75..cc59898a0e 100644 --- a/src/ophyd_async/panda/_hdf_panda.py +++ b/src/ophyd_async/panda/_hdf_panda.py @@ -8,8 +8,7 @@ SignalR, StandardDetector, ) -from ophyd_async.epics.pvi import fill_pvi_entries, pre_initialize_blocks - +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries from ._common_panda import CommonPandaBlocks from ._panda_controller import PandaPcapController @@ -26,7 +25,7 @@ def __init__( ): self._prefix = prefix - pre_initialize_blocks(self) + create_children_from_annotations(self) controller = PandaPcapController(pcap=self.pcap) writer = PandaHDFWriter( prefix=prefix, diff --git a/tests/epics/test_pvi.py b/tests/epics/test_pvi.py index 0993716f8d..65574f14dc 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, pre_initialize_blocks +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries class Block1(Device): @@ -97,14 +97,14 @@ async def test_fill_pvi_entries_sim_mode(pvi_test_device_t): @pytest.fixture -def pvi_test_device_pre_initialize_blocks_t(): +def pvi_test_device_create_children_from_annotations_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) + create_children_from_annotations(self) async def connect( self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT @@ -116,14 +116,16 @@ async def connect( 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:") +async def test_device_create_children_from_annotations( + pvi_test_device_create_children_from_annotations_t, +): + device = pvi_test_device_create_children_from_annotations_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, + # The create_children_from_annotations has only made blocks, # not signals or device vectors assert isinstance(block_2_device, Block2) assert isinstance(block_1_device, Block1) diff --git a/tests/panda/test_panda_connect.py b/tests/panda/test_panda_connect.py index 67706ec8cb..7dcd9c5b51 100644 --- a/tests/panda/test_panda_connect.py +++ b/tests/panda/test_panda_connect.py @@ -9,7 +9,7 @@ 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.epics.pvi.pvi import pre_initialize_blocks +from ophyd_async.epics.pvi.pvi import create_children_from_annotations from ophyd_async.panda import PcapBlock, PulseBlock, SeqBlock, SeqTable, SeqTrigger @@ -47,7 +47,7 @@ class CommonPandaBlocksNoData(Device): class Panda(CommonPandaBlocksNoData): def __init__(self, prefix: str, name: str = ""): self._prefix = prefix - pre_initialize_blocks(self) + create_children_from_annotations(self) super().__init__(name) async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index d90706a2d2..d708f1b98b 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -12,12 +12,12 @@ StaticDirectoryProvider, set_sim_value, ) -from ophyd_async.epics.pvi import fill_pvi_entries, pre_initialize_blocks -from ophyd_async.panda._common_panda import CommonPandaBlocks -from ophyd_async.panda.writers import PandaHDFWriter +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries +from ophyd_async.panda import CommonPandaBlocks from ophyd_async.panda.writers._hdf_writer import ( Capture, CaptureSignalWrapper, + PandaHDFWriter, get_capture_signals, get_signals_marked_for_capture, ) @@ -35,7 +35,7 @@ class Panda(CommonPandaBlocks): def __init__(self, prefix: str, name: str = ""): self._prefix = prefix - pre_initialize_blocks(self) + create_children_from_annotations(self) super().__init__(name) async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): From 5c48c40853758f75c5e303f7b8c1e84e341130c5 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 17 Apr 2024 15:17:38 +0100 Subject: [PATCH 7/7] cleaned up after rebase --- src/ophyd_async/epics/pvi/pvi.py | 22 ++--- src/ophyd_async/panda/__init__.py | 2 +- .../{_common_panda.py => _common_blocks.py} | 0 src/ophyd_async/panda/_hdf_panda.py | 2 +- tests/panda/test_hdf_panda.py | 85 +++++++------------ tests/panda/test_panda_utils.py | 2 +- tests/panda/test_writer.py | 6 +- 7 files changed, 46 insertions(+), 73 deletions(-) rename src/ophyd_async/panda/{_common_panda.py => _common_blocks.py} (100%) diff --git a/src/ophyd_async/epics/pvi/pvi.py b/src/ophyd_async/epics/pvi/pvi.py index 8379936baf..e68cf3b132 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/pvi.py @@ -57,14 +57,14 @@ def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]: return tp, None -def _strip_union(field: Union[Union[T], T]) -> T: +def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]: if get_origin(field) is Union: args = get_args(field) is_optional = type(None) in args - for field in args: - if field is not type(None): - break - return field, is_optional + for arg in args: + if arg is not type(None): + return arg, is_optional + return field, False def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]: @@ -131,7 +131,7 @@ def _parse_type( ): if common_device_type: # pre-defined type - device_cls = _strip_union(common_device_type) + device_cls, _ = _strip_union(common_device_type) is_device_vector, device_cls = _strip_device_vector(device_cls) device_cls, device_args = _split_subscript(device_cls) assert issubclass(device_cls, Device) @@ -165,7 +165,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): ) for device_name, device_cls in sub_devices: - device_cls = _strip_union(device_cls) + device_cls, _ = _strip_union(device_cls) is_device_vector, device_cls = _strip_device_vector(device_cls) device_cls, device_args = _split_subscript(device_cls) assert issubclass(device_cls, Device) @@ -190,8 +190,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): if is_signal: sub_device = device_cls(SimSignalBackend(signal_dtype)) else: - sub_device = device_cls() - + sub_device = getattr(device, device_name, device_cls()) _sim_common_blocks(sub_device, stripped_type=device_cls) setattr(device, device_name, sub_device) @@ -226,10 +225,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT): if is_signal: device = _pvi_mapping[frozenset(pva_entries.keys())](signal_dtype, *pvs) else: - if hasattr(entry.device, sub_name): - device = getattr(entry.device, sub_name) - else: - device = device_type() + device = getattr(entry.device, sub_name, device_type()) sub_entry = PVIEntry( device=device, common_device_type=device_type, sub_entries={} diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/panda/__init__.py index 585b1f9839..f2170263af 100644 --- a/src/ophyd_async/panda/__init__.py +++ b/src/ophyd_async/panda/__init__.py @@ -1,4 +1,4 @@ -from ._common_panda import ( +from ._common_blocks import ( CommonPandaBlocks, DataBlock, PcapBlock, diff --git a/src/ophyd_async/panda/_common_panda.py b/src/ophyd_async/panda/_common_blocks.py similarity index 100% rename from src/ophyd_async/panda/_common_panda.py rename to src/ophyd_async/panda/_common_blocks.py diff --git a/src/ophyd_async/panda/_hdf_panda.py b/src/ophyd_async/panda/_hdf_panda.py index cc59898a0e..75c483e031 100644 --- a/src/ophyd_async/panda/_hdf_panda.py +++ b/src/ophyd_async/panda/_hdf_panda.py @@ -10,7 +10,7 @@ ) from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries -from ._common_panda import CommonPandaBlocks +from ._common_blocks import CommonPandaBlocks from ._panda_controller import PandaPcapController from .writers._hdf_writer import PandaHDFWriter diff --git a/tests/panda/test_hdf_panda.py b/tests/panda/test_hdf_panda.py index d880e6b487..2c376fb12f 100644 --- a/tests/panda/test_hdf_panda.py +++ b/tests/panda/test_hdf_panda.py @@ -21,6 +21,11 @@ ) +def assert_emitted(docs: Dict[str, list], **numbers: int): + assert list(docs) == list(numbers) + assert {name: len(d) for name, d in docs.items()} == numbers + + class MockPandaPcapController(DetectorControl): def __init__(self, pcap: PcapBlock) -> None: self.pcap = pcap @@ -68,8 +73,8 @@ class CaptureBlock(Device): sim_hdf_panda._controller = MockPandaPcapController(sim_hdf_panda.pcap) block_a = CaptureBlock(name="block_a") block_b = CaptureBlock(name="block_b") - block_a.test_capture = SignalR(backend=SimSignalBackend(Capture, source="block_a")) - block_b.test_capture = SignalR(backend=SimSignalBackend(Capture, source="block_b")) + block_a.test_capture = SignalR(backend=SimSignalBackend(Capture)) + block_b.test_capture = SignalR(backend=SimSignalBackend(Capture)) setattr(sim_hdf_panda, "block_a", block_a) setattr(sim_hdf_panda, "block_b", block_b) @@ -89,12 +94,12 @@ async def test_hdf_panda_hardware_triggered_flyable( RE: RunEngine, sim_hdf_panda, ): - names = [] - docs = [] + docs = {} def append_and_print(name, doc): - names.append(name) - docs.append(doc) + if name not in docs: + docs[name] = [] + docs[name] += [doc] RE.subscribe(append_and_print) @@ -158,56 +163,30 @@ def flying_plan(): # fly scan RE(flying_plan()) - assert names == [ - "start", - "descriptor", - "stream_resource", - "stream_resource", - "stream_datum", - "stream_datum", - "stop", - ] - named_docs = dict( - zip( - [ - "start", - "descriptor", - "stream_resource_a", - "stream_resource_b", - "stream_datum_a", - "stream_datum_b", - "stop", - ], - docs, - ) + assert_emitted( + docs, start=1, descriptor=1, stream_resource=2, stream_datum=2, stop=1 ) # test descriptor - data_key_names: Dict[str, str] = named_docs["descriptor"]["object_keys"]["panda"] + data_key_names: Dict[str, str] = docs["descriptor"][0]["object_keys"]["panda"] assert data_key_names == [ "panda-block_a-test-Min", "panda-block_b-test-Diff", ] for data_key_name in data_key_names: assert ( - named_docs["descriptor"]["data_keys"][data_key_name]["source"] - == "sim://hdf_directory" + docs["descriptor"][0]["data_keys"][data_key_name]["source"] + == "soft://panda-data-hdf_directory" ) # test stream resources - for block_letter, data_key_name in zip(("a", "b"), data_key_names): - assert ( - named_docs[f"stream_resource_{block_letter}"]["data_key"] == data_key_name - ) - assert ( - named_docs[f"stream_resource_{block_letter}"]["spec"] - == "AD_HDF5_SWMR_SLICE" - ) - assert ( - named_docs[f"stream_resource_{block_letter}"]["run_start"] - == named_docs["start"]["uid"] - ) - assert named_docs[f"stream_resource_{block_letter}"]["resource_kwargs"] == { + for block_letter, stream_resource, data_key_name in zip( + ("a", "b"), docs["stream_resource"], data_key_names + ): + assert stream_resource["data_key"] == data_key_name + assert stream_resource["spec"] == "AD_HDF5_SWMR_SLICE" + assert stream_resource["run_start"] == docs["start"][0]["uid"] + assert stream_resource["resource_kwargs"] == { "block": f"block_{block_letter}", "multiplier": 1, "name": data_key_name, @@ -216,20 +195,16 @@ def flying_plan(): } # test stream datum - for block_letter in ("a", "b"): - assert ( - named_docs[f"stream_datum_{block_letter}"]["descriptor"] - == named_docs["descriptor"]["uid"] - ) - assert named_docs[f"stream_datum_{block_letter}"]["seq_nums"] == { + for stream_datum in docs["stream_datum"]: + assert stream_datum["descriptor"] == docs["descriptor"][0]["uid"] + assert stream_datum["seq_nums"] == { "start": 1, "stop": 2, } - assert named_docs[f"stream_datum_{block_letter}"]["indices"] == { + assert stream_datum["indices"] == { "start": 0, "stop": 1, } - assert ( - named_docs[f"stream_datum_{block_letter}"]["stream_resource"] - == named_docs[f"stream_resource_{block_letter}"]["uid"] - ) + assert stream_datum["stream_resource"] in [ + sd["uid"].split("/")[0] for sd in docs["stream_datum"] + ] diff --git a/tests/panda/test_panda_utils.py b/tests/panda/test_panda_utils.py index acb4b375fc..35e53a07b3 100644 --- a/tests/panda/test_panda_utils.py +++ b/tests/panda/test_panda_utils.py @@ -9,7 +9,7 @@ from ophyd_async.epics.pvi import fill_pvi_entries from ophyd_async.epics.signal import epics_signal_rw from ophyd_async.panda import CommonPandaBlocks, TimeUnits -from ophyd_async.panda._common_panda import DataBlock +from ophyd_async.panda._common_blocks import DataBlock from ophyd_async.panda._utils import phase_sorter diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index d708f1b98b..29687a3b74 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -51,11 +51,13 @@ async def sim_panda(panda_t): sim_panda = panda_t("SIM_PANDA", name="sim_panda") set_sim_value( - sim_panda.block_a.test_capture, Capture.MinMaxMean # type: ignore[attr-defined] + sim_panda.block_a.test_capture, + Capture.MinMaxMean, # type: ignore[attr-defined] ) set_sim_value( - sim_panda.block_b.test_capture, Capture.No # type: ignore[attr-defined] + sim_panda.block_b.test_capture, + Capture.No, # type: ignore[attr-defined] ) return sim_panda