From 496a22ed000923897ecc4a5b50ba4977d733b954 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 21 Jul 2025 11:03:33 +0100 Subject: [PATCH 01/21] temp storing changes to work on another ticket --- .../devices/dae/_period_settings.py | 8 ++ .../plans/dae_table_wrapper.py | 41 +++++++ .../plans/num_periods_wrapper.py | 41 +++++++ .../plans/test_time_channel_wrapper.py | 42 +++++++ .../plans/time_channels_wrapper.py | 41 +++++++ src/ibex_bluesky_core/plans/wrapper_test.py | 96 ++++++++++++++++ tests/devices/test_dae.py | 103 +++++++++--------- 7 files changed, 320 insertions(+), 52 deletions(-) create mode 100644 src/ibex_bluesky_core/plans/dae_table_wrapper.py create mode 100644 src/ibex_bluesky_core/plans/num_periods_wrapper.py create mode 100644 src/ibex_bluesky_core/plans/test_time_channel_wrapper.py create mode 100644 src/ibex_bluesky_core/plans/time_channels_wrapper.py create mode 100644 src/ibex_bluesky_core/plans/wrapper_test.py diff --git a/src/ibex_bluesky_core/devices/dae/_period_settings.py b/src/ibex_bluesky_core/devices/dae/_period_settings.py index b9c8e520..b4a1d811 100644 --- a/src/ibex_bluesky_core/devices/dae/_period_settings.py +++ b/src/ibex_bluesky_core/devices/dae/_period_settings.py @@ -53,6 +53,14 @@ class SinglePeriodSettings: label: str | None = None +class PeriodSettingType(Enum): + """Periods type option for a single row.""" + + UNUSED = 0 + DAQ = 1 + DWEll = 2 + + @dataclass(kw_only=True) class DaePeriodSettingsData: """Dataclass for the hardware period settings.""" diff --git a/src/ibex_bluesky_core/plans/dae_table_wrapper.py b/src/ibex_bluesky_core/plans/dae_table_wrapper.py new file mode 100644 index 00000000..4f29c17b --- /dev/null +++ b/src/ibex_bluesky_core/plans/dae_table_wrapper.py @@ -0,0 +1,41 @@ +"""Wrap a plan with temporary modification to DAE Settings.""" + +import copy +from collections.abc import Generator + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from bluesky.utils import Msg +from ophyd_async.plan_stubs import ensure_connected + +from ibex_bluesky_core.devices.dae import Dae + + +def time_channels_wrapper( + plan: Generator[Msg, None, None], + dae: "Dae", + **modified_dae: int | str | None) -> Generator[Msg, None, None]: + """Wrap a plan with temporary modification to DAE Settings.""" + yield from ensure_connected(dae) + + original_dae_setting = None + + def _inner() -> Generator[Msg, None, None]: + nonlocal original_dae_setting + original_dae_setting = yield from bps.rd(dae.dae_settings) + + new_dea_tables = copy.deepcopy(original_dae_setting) + + for key, value in modified_dae.items(): + if hasattr(new_dea_tables, key): + setattr(new_dea_tables, key, value) + + yield from bps.mv(dae.dae_settings, original_dae_setting) + yield from plan + + def _onexit() -> Generator[Msg, None, None]: + nonlocal original_dae_setting + if original_dae_setting is not None: + yield from bps.mv(dae.dae_settings, original_dae_setting) + + return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/num_periods_wrapper.py b/src/ibex_bluesky_core/plans/num_periods_wrapper.py new file mode 100644 index 00000000..689f29f1 --- /dev/null +++ b/src/ibex_bluesky_core/plans/num_periods_wrapper.py @@ -0,0 +1,41 @@ +"""Wrap a plan with temporary modification to Periods Settings.""" + +import copy +from collections.abc import Generator + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from bluesky.utils import Msg +from ophyd_async.plan_stubs import ensure_connected + +from ibex_bluesky_core.devices.dae import Dae + + +def num_periods_wrapper( + plan: Generator[Msg, None, None], + dae: "Dae", + **modified_periods: int | str | None) -> Generator[Msg, None, None]: + """Wrap a plan with temporary modification to Periods Settings.""" + yield from ensure_connected(dae) + + original_num_periods = None + + def _inner() -> Generator[Msg, None, None]: + nonlocal original_num_periods + original_num_periods = yield from bps.rd(dae.period_settings) + + new_num_periods = copy.deepcopy(original_num_periods) + + for key, value in modified_periods.items(): + if hasattr(new_num_periods, key): + setattr(new_num_periods, key, value) + + yield from bps.mv(dae.period_settings, new_num_periods) + yield from plan + + def _onexit() -> Generator[Msg, None, None]: + nonlocal original_num_periods + if original_num_periods is not None: + yield from bps.mv(dae.period_settings, original_num_periods) + + return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/test_time_channel_wrapper.py b/src/ibex_bluesky_core/plans/test_time_channel_wrapper.py new file mode 100644 index 00000000..3115c418 --- /dev/null +++ b/src/ibex_bluesky_core/plans/test_time_channel_wrapper.py @@ -0,0 +1,42 @@ +from ibex_bluesky_core.devices.dae import Dae, TimeRegimeMode +from bluesky.plans import scan +from ibex_bluesky_core.plans.time_channels_wrapper import tcb_wrapper +from ibex_bluesky_core.devices.simpledae import ( + GoodFramesNormalizer, + GoodFramesWaiter, + RunPerPointController, + SimpleDae, +) +from ibex_bluesky_core.utils import get_pv_prefix +from ibex_bluesky_core.devices.block import block_rw_rbv + +def test_tcb_wrapper_runs(): + + prefix = get_pv_prefix() + + controller = RunPerPointController(save_run=True) + waiter = GoodFramesWaiter(500) + reducer = GoodFramesNormalizer( + prefix=prefix, + detector_spectra=[i for i in range(1, 100)], + ) + + dae = SimpleDae( + prefix=prefix, + controller=controller, + waiter=waiter, + reducer=reducer, + ) + wrapped_plan = tcb_wrapper( + scan, + dae, + from_=9, + to=87, + steps=2, + mode=TimeRegimeMode.DT + ) + + list(wrapped_plan) # Forces execution + + # If no exception occurs, test passes + assert True diff --git a/src/ibex_bluesky_core/plans/time_channels_wrapper.py b/src/ibex_bluesky_core/plans/time_channels_wrapper.py new file mode 100644 index 00000000..4254d866 --- /dev/null +++ b/src/ibex_bluesky_core/plans/time_channels_wrapper.py @@ -0,0 +1,41 @@ +"""Wrap a plan with temporary modification to Time Channel Settings.""" + +import copy +from collections.abc import Generator + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from bluesky.utils import Msg +from ophyd_async.plan_stubs import ensure_connected + +from ibex_bluesky_core.devices.dae import Dae + + +def tcb_wrapper( + plan: Generator[Msg, None, None], + dae: "Dae", + **modified_dae: int | str | None) -> Generator[Msg, None, None]: + """Wrap a plan with temporary modification to DAE Settings.""" + yield from ensure_connected(dae) + + original_time_channels = None + + def _inner() -> Generator[Msg, None, None]: + nonlocal original_time_channels + original_time_channels = yield from bps.rd(dae.tcb_settings) + + new_time_channels = copy.deepcopy(original_time_channels) + + for key, value in modified_dae.items(): + if hasattr(new_time_channels, key): + setattr(new_time_channels, key, value) + + yield from bps.mv(dae.tcb_settings, new_time_channels) + yield from plan + + def _onexit() -> Generator[Msg, None, None]: + nonlocal original_time_channels + if original_time_channels is not None: + yield from bps.mv(dae.tcb_settings, original_time_channels) + + return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/wrapper_test.py b/src/ibex_bluesky_core/plans/wrapper_test.py new file mode 100644 index 00000000..780f9cb1 --- /dev/null +++ b/src/ibex_bluesky_core/plans/wrapper_test.py @@ -0,0 +1,96 @@ +from unittest.mock import Mock + + +# Mock the Msg class since we might not have bluesky installed +class Msg: + def __init__(self, command, obj=None, *args, **kwargs): + self.command = command + self.obj = obj + self.args = args + self.kwargs = kwargs + +# Remove the function definition since we're importing it +# from time_channels_wrapper import time_channels_wrapper + + +def test_wrapper_runs_without_error(): + """Simple test to check if the wrapper function executes without crashing.""" + + # Create a simple mock plan + def dummy_plan(): + yield Msg('checkpoint') + + # Create a mock DAE with the required attributes + mock_dae = Mock() + mock_dae.tcb_settings = Mock() + mock_dae.tcb_settings.time_channel_1 = 100 + mock_dae.tcb_settings.time_channel_2 = 200 + + # Create mock modules and functions + import sys + from types import ModuleType + + # Create mock modules + mock_bps = ModuleType('bluesky.plan_stubs') + mock_ophyd = ModuleType('ophyd_async.plan_stubs') + mock_bpp = ModuleType('bluesky.preprocessors') + + def mock_ensure_connected(device): + yield Msg('null') + + def mock_rd(device): + yield Msg('null') + return mock_dae.tcb_settings + + def mock_mv(device, value): + yield Msg('null') + + def mock_finalize_wrapper(plan, cleanup): + try: + yield from plan + finally: + yield from cleanup() + + # Add functions to mock modules + mock_bps.rd = mock_rd + mock_bps.mv = mock_mv + mock_ophyd.ensure_connected = mock_ensure_connected + mock_bpp.finalize_wrapper = mock_finalize_wrapper + + # Add to sys.modules so imports work + sys.modules['bluesky.plan_stubs'] = mock_bps + sys.modules['ophyd_async.plan_stubs'] = mock_ophyd + sys.modules['bluesky.preprocessors'] = mock_bpp + + try: + + # Import your function from the file + from time_channels_wrapper import tcb_wrapper + + # Run the wrapper - if it completes without exception, it works! + wrapped_plan = tcb_wrapper( + dummy_plan(), + mock_dae, + time_channel_1=500 + ) + + # Execute the generator to completion + list(wrapped_plan) + + print("✅ Test passed - wrapper runs without error!") + + except ImportError as e: + print(f"❌ Import error: {e}") + print("💡 You need to:") + print(" 1. Replace 'your_module' with the actual file/module name") + print(" 2. Or copy your time_channels_wrapper function into this test file") + + finally: + # Clean up sys.modules + for module in ['bluesky.plan_stubs', 'ophyd_async.plan_stubs', 'bluesky.preprocessors']: + if module in sys.modules: + del sys.modules[module] + + +if __name__ == "__main__": + test_wrapper_runs_without_error() diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index d468741d..3ae66f50 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -10,6 +10,7 @@ import scipp.testing from bluesky.run_engine import RunEngine from ophyd_async.testing import get_mock_put, set_mock_value +from ibex_bluesky_core.plans.num_periods_wrapper import num_periods_wrapper from ibex_bluesky_core.devices import compress_and_hex, dehex_and_decompress from ibex_bluesky_core.devices.dae import ( @@ -296,20 +297,19 @@ async def test_dae_settings_get_parsed_correctly(): xml = await daesettings._raw_dae_settings.get_value() assert ET.canonicalize(xml) == ET.canonicalize(xml_filled_in) - -async def test_period_settings_get_parsed_correctly(): +async def test_num_periods_wrapper_modifies_and_restores_settings(RE: RunEngine, dae: Dae): expected_setup_source = PeriodSource.FILE expected_period_type = PeriodType.SOFTWARE expected_periods_file = "C:\\someperiodfile.txt" - expected_soft_periods_num = 42 - expected_hardware_period_sequences = 52 - expected_output_delay = 123 - expected_type_1 = 0 - expected_frames_1 = 1 - expected_output_1 = 2 - expected_type_2 = 0 - expected_frames_2 = 1 - expected_output_2 = 2 + expected_soft_periods_num = 10 + expected_hardware_period_sequences = 5 + expected_output_delay = 100 + expected_type_1 = 1 + expected_frames_1 = 2 + expected_output_1 = 3 + expected_type_2 = 2 + expected_frames_2 = 3 + expected_output_2 = 4 expected_type_3 = 1 expected_frames_3 = 2 expected_output_3 = 3 @@ -328,7 +328,7 @@ async def test_period_settings_get_parsed_correctly(): expected_type_8 = 2 expected_frames_8 = 2 expected_output_8 = 2 - + periods_settings = [ SinglePeriodSettings( type=expected_type_1, frames=expected_frames_1, output=expected_output_1 @@ -356,7 +356,7 @@ async def test_period_settings_get_parsed_correctly(): ), ] - data = DaePeriodSettingsData( + initial_settings = DaePeriodSettingsData( periods_soft_num=expected_soft_periods_num, periods_type=expected_period_type, periods_src=expected_setup_source, @@ -365,47 +365,46 @@ async def test_period_settings_get_parsed_correctly(): periods_delay=expected_output_delay, periods_settings=periods_settings, ) - xml_filled_in = period_settings_template.format( - period_src=expected_setup_source.value, - period_type=expected_period_type.value, - period_file=expected_periods_file, - num_soft_periods=expected_soft_periods_num, - period_seq=expected_hardware_period_sequences, - period_delay=expected_output_delay, - type_1=expected_type_1, - frames_1=expected_frames_1, - output_1=expected_output_1, - type_2=expected_type_2, - frames_2=expected_frames_2, - output_2=expected_output_2, - type_3=expected_type_3, - frames_3=expected_frames_3, - output_3=expected_output_3, - type_4=expected_type_4, - frames_4=expected_frames_4, - output_4=expected_output_4, - type_5=expected_type_5, - frames_5=expected_frames_5, - output_5=expected_output_5, - type_6=expected_type_6, - frames_6=expected_frames_6, - output_6=expected_output_6, - type_7=expected_type_7, - frames_7=expected_frames_7, - output_7=expected_output_7, - type_8=expected_type_8, - frames_8=expected_frames_8, - output_8=expected_output_8, + + # Connect the period_settings sub-device and set up the initial data + await dae.period_settings._raw_period_settings.connect(mock=True) + await dae.period_settings._raw_period_settings.set(initial_period_settings) + await dae.period_settings.set(initial_settings) + + # Read the original settings + original_settings: DaePeriodSettingsData = RE(bps.rd(dae.period_settings)).plan_result + assert original_settings.periods_soft_num == 10 + assert original_settings.periods_file == "C:\\someperiodfile.txt" + assert original_settings.periods_seq == 5 + + def inner_plan(): + current_settings: DaePeriodSettingsData = yield from bps.rd(dae.period_settings) + assert current_settings.periods_soft_num == 20 + assert current_settings.periods_file == "C:\\modified_file.txt" + assert current_settings.periods_seq == 5 + return current_settings + + # Run the wrapper test + result = RE( + num_periods_wrapper( + inner_plan(), + dae.period_settings, + periods_soft_num=20, + periods_file="C:\\modified_file.txt", + ) ) - periodsettings = DaePeriodSettings(MOCK_PREFIX) - await periodsettings._raw_period_settings.connect(mock=True) - await periodsettings._raw_period_settings.set(initial_period_settings) - await periodsettings.set(data) - location = await periodsettings.locate() - assert location == {"setpoint": data, "readback": data} - xml = await periodsettings._raw_period_settings.get_value() - assert ET.canonicalize(xml) == ET.canonicalize(xml_filled_in) + # Check the modified settings + modified_settings = result.plan_result + assert modified_settings.periods_soft_num == 20 + assert modified_settings.periods_file == "C:\\modified_file.txt" + assert modified_settings.periods_seq == 5 + + # Check that settings are restored after the wrapper + final_settings: DaePeriodSettingsData = RE(bps.rd(dae.period_settings)).plan_result + assert final_settings.periods_soft_num == 10 + assert final_settings.periods_file == "C:\\someperiodfile.txt" + assert final_settings.periods_seq == 5 async def test_tcb_settings_get_parsed_correctly(): expected_tcb_file = "C:\\tcb.dat" From 1d2636e2074ec4c819448527a2c68188bc2e13f0 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 11:10:55 +0100 Subject: [PATCH 02/21] three wrappers, tests and documentation --- doc/plan_stubs/plan_wrappers.md | 39 ++++ src/ibex_bluesky_core/plan_stubs/__init__.py | 197 +++++++++++++++++ .../plan_stubs/dae_table_wrapper.py | 42 ++++ .../plan_stubs/num_periods_wrapper.py | 43 ++++ .../plan_stubs/time_channels_wrapper.py | 42 ++++ src/ibex_bluesky_core/plans/__init__.py | 2 + tests/devices/test_dae.py | 199 +++++++++++++----- 7 files changed, 512 insertions(+), 52 deletions(-) create mode 100644 doc/plan_stubs/plan_wrappers.md create mode 100644 src/ibex_bluesky_core/plan_stubs/__init__.py create mode 100644 src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py create mode 100644 src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py create mode 100644 src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py diff --git a/doc/plan_stubs/plan_wrappers.md b/doc/plan_stubs/plan_wrappers.md new file mode 100644 index 00000000..e76db698 --- /dev/null +++ b/doc/plan_stubs/plan_wrappers.md @@ -0,0 +1,39 @@ +# Plan Wrappers + +Plan wrappers that temporarily modify DAE (Data Acquisition Electronics) settings during a plan, automatically restoring the original values afterwards. This ensures that your experiments don't permanently change instrument configuration. + +## Available Wrappers + +['dae_table_wrapper](ibex_bluesky_core.plan_stubs.dae_table_wrapper) +['num_periods_wrapper](ibex_bluesky_core.plan_stubs.num_periods_wrapper) +['time_channels_wrapper](ibex_bluesky_core.plan_stubs.time_channels_wrapper) + +## Usage + +To use these wrappers, the plan written by the user must be wrapped by the function within the RunEngine: + +``` python + +from bluesky import RunEngine +from ibex_bluesky_core.plan_stubs import with_num_periods +from ibex_bluesky_core.devices.simpledae import SimpleDae + +dae = SimpleDae() # Give your DAE options here +RE = RunEngine() + +modified_settings = 1 + +def example_plan(): + yield from bps.mv(dae.number_of_periods, modified_settings) + +RE( + with_num_periods( + example_plan(), + dae=dae, + ) + ) + +``` + +the plan with the modified DAE settings, restoring the original settings afterwards. + diff --git a/src/ibex_bluesky_core/plan_stubs/__init__.py b/src/ibex_bluesky_core/plan_stubs/__init__.py new file mode 100644 index 00000000..d811f8fa --- /dev/null +++ b/src/ibex_bluesky_core/plan_stubs/__init__.py @@ -0,0 +1,197 @@ +"""Core plan stubs.""" + +import logging +from collections.abc import Callable, Generator +from typing import Any, ParamSpec, TypeVar, cast + +from bluesky import plan_stubs as bps +from bluesky.plan_stubs import trigger_and_read +from bluesky.preprocessors import finalize_wrapper +from bluesky.protocols import Readable +from bluesky.utils import Msg +from ophyd_async.epics.motor import Motor, UseSetMode + +from ibex_bluesky_core.devices.reflectometry import ReflParameter +from ibex_bluesky_core.utils import NamedReadableAndMovable + +logger = logging.getLogger(__name__) + +P = ParamSpec("P") +T = TypeVar("T") + + +CALL_SYNC_MSG_KEY = "ibex_bluesky_core_call_sync" +CALL_QT_AWARE_MSG_KEY = "ibex_bluesky_core_call_qt_aware" + + +__all__ = [ + "call_qt_aware", + "call_sync", + "polling_plan", + "prompt_user_for_choice", + "redefine_motor", + "redefine_refl_parameter", +] + + +def call_sync(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Generator[Msg, None, T]: + """Call a synchronous user function in a plan, and returns the result of that call. + + Attempts to guard against the most common pitfalls of naive implementations, for example: + + - Blocking the whole event loop + - Breaking keyboard interrupt handling + + It does not necessarily guard against all possible cases, and as such it is *recommended* to + use native bluesky functionality wherever possible in preference to this plan stub. This should + be seen as an escape-hatch. + + The wrapped function will be run in a new thread. + + Args: + func: A callable to run. + *args: Arbitrary arguments to be passed to the wrapped function + **kwargs: Arbitrary keyword arguments to be passed to the wrapped function + + Returns: + The return value of the wrapped function + + """ + return cast(T, (yield Msg(CALL_SYNC_MSG_KEY, func, *args, **kwargs))) + + +def call_qt_aware( + func: Callable[P, T], *args: P.args, **kwargs: P.kwargs +) -> Generator[Msg, None, T]: + """Call a matplotlib function in a Qt-aware context, from within a plan. + + If matplotlib is using a Qt backend then UI operations are run on the Qt thread via Qt signals. + + Only matplotlib functions may be run using this plan stub. + + Args: + func: A matplotlib function reference. + *args: Arbitrary arguments, passed through to matplotlib.pyplot.subplots + **kwargs: Arbitrary keyword arguments, passed through to matplotlib.pyplot.subplots + + Raises: + ValueError: if the passed function is not a matplotlib function. + + Returns: + The return value of the wrapped function + + """ + # Limit potential for misuse - constrain to just running matplotlib functions. + if not getattr(func, "__module__", "").startswith("matplotlib"): + raise ValueError("Only matplotlib functions should be passed to call_qt_aware") + + return cast(T, (yield Msg(CALL_QT_AWARE_MSG_KEY, func, *args, **kwargs))) + + +def redefine_motor( + motor: Motor, position: float, *, sleep: float = 1 +) -> Generator[Msg, None, None]: + """Redefines the current positions of a motor. + + Note: + This does not move the motor, it just redefines its position to be the given value. + + Args: + motor: The motor to set a position on. + position: The position to set. + sleep: An amount of time to sleep, in seconds, after redefining. Defaults to 1 second. + This avoids race conditions where a motor is redefined and then immediately moved. + + """ + logger.info("Redefining motor %s to %s", motor.name, position) + + def make_motor_usable() -> Generator[Msg, None, None]: + yield from bps.abs_set(motor.set_use_switch, UseSetMode.USE) + yield from bps.sleep(sleep) + + def inner() -> Generator[Msg, None, None]: + yield from bps.abs_set(motor.set_use_switch, UseSetMode.SET) + yield from bps.abs_set(motor.user_setpoint, position) + + return (yield from finalize_wrapper(inner(), make_motor_usable())) + + +def redefine_refl_parameter( + parameter: ReflParameter, position: float +) -> Generator[Msg, None, None]: + """Redefines the current positions of a reflectometry parameter. + + Note: + This does not move the parameter, it just redefines its position to be the given value. + + Args: + parameter: The reflectometry parameter to set a position on. + position: The position to set. + + """ + if parameter.redefine is None: + raise ValueError(f"Parameter {parameter.name} cannot be redefined.") + logger.info("Redefining refl parameter %s to %s", parameter.name, position) + yield from bps.mv(parameter.redefine, position) + + +def prompt_user_for_choice(*, prompt: str, choices: list[str]) -> Generator[Msg, None, str]: + """Prompt the user to choose between a limited set of options. + + Args: + prompt: The user prompt string. + choices: A list of allowable choices. + + Returns: + One of the entries in the choices list. + + """ + choice = yield from call_sync(input, prompt) + while choice not in choices: + choice = yield from call_sync(input, prompt) + + return choice + + +def polling_plan( + motor: NamedReadableAndMovable, readable: Readable[Any], destination: float +) -> Generator[Msg, None, None]: + """Move to a destination but drop updates from readable if motor position has not changed. + + Note - this does not start a run, this should be done with a run_decorator or similar in an + outer plan which calls this plan. + + Args: + motor: the motor to move. + readable: the readable to read updates from, but drop if motor has not moved. + destination: the destination position. + + Returns: + None + + If we just used bp.scan() with a readable that updates more frequently than a motor can + register that it has moved, we would have lots of updates with the same motor position, + which may not be helpful. + + """ + yield from bps.checkpoint() + yield from bps.create() + reading = yield from bps.read(motor) + yield from bps.read(readable) + yield from bps.save() + + # start the ramp + status = yield from bps.abs_set(motor, destination, wait=False) + while not status.done: + yield from bps.create() + new_reading = yield from bps.read(motor) + yield from bps.read(readable) + + if new_reading[motor.name]["value"] == reading[motor.name]["value"]: + yield from bps.drop() + else: + reading = new_reading + yield from bps.save() + + # take a 'post' data point + yield from trigger_and_read([motor, readable]) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py new file mode 100644 index 00000000..56bd60c8 --- /dev/null +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -0,0 +1,42 @@ +"""Wrap a plan with temporary modification to DAE Settings.""" + +import copy +from collections.abc import Generator + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from bluesky.utils import Msg +from ophyd_async.plan_stubs import ensure_connected + +from ibex_bluesky_core.devices.dae import Dae + + +def with_dae_tables( + plan: Generator[Msg, None, None], + dae: "Dae") -> Generator[Msg, None, None]: + """Wrap a plan with temporary modification to DAE Settings. + + Args: + plan: The plan to wrap. + dae: The Dae instance. + + Returns: + A generator which runs the plan with the modified DAE settings, restoring the original + settings afterwards. + """ + yield from ensure_connected(dae) + + original_dae_setting = None + + def _inner() -> Generator[Msg, None, None]: + nonlocal original_dae_setting + original_dae_setting = yield from bps.rd(dae.dae_settings) + + yield from plan + + def _onexit() -> Generator[Msg, None, None]: + nonlocal original_dae_setting + if original_dae_setting is not None: + yield from bps.mv(dae.dae_settings, original_dae_setting) + + return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py new file mode 100644 index 00000000..efd53426 --- /dev/null +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -0,0 +1,43 @@ +"""Wrap a plan with temporary modification to Periods Settings.""" + +import copy +from collections.abc import Generator + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from bluesky.utils import Msg +from ophyd_async.plan_stubs import ensure_connected + +from ibex_bluesky_core.devices.dae import DaePeriodSettings + + +def with_num_periods( + plan: Generator[Msg, None, None], + dae: DaePeriodSettings) -> Generator[Msg, None, None]: + """Wrap a plan with temporary modification to Periods Settings. + + Args: + plan: The plan to wrap. + dae: The Dae instance. + + Returns: + A generator which runs the plan with the modified DAE settings, restoring the original + settings afterwards. + """ + + yield from ensure_connected(dae) + + original_num_periods = None + + def _inner() -> Generator[Msg, None, None]: + nonlocal original_num_periods + original_num_periods = yield from bps.rd(dae.number_of_periods) + + yield from plan + + def _onexit() -> Generator[Msg, None, None]: + nonlocal original_num_periods + if original_num_periods is not None: + yield from bps.mv(dae.number_of_periods, original_num_periods) + + return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py new file mode 100644 index 00000000..d1141111 --- /dev/null +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -0,0 +1,42 @@ +"""Wrap a plan with temporary modification to Time Channel Settings.""" + +import copy +from collections.abc import Generator + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from bluesky.utils import Msg +from ophyd_async.plan_stubs import ensure_connected + +from ibex_bluesky_core.devices.dae import DaeTCBSettings + + +def with_time_channels( + plan: Generator[Msg, None, None], + dae: DaeTCBSettings) -> Generator[Msg, None, None]: + """Wrap a plan with temporary modification to Time Channel Settings. + + Args: + plan: The plan to wrap. + dae: The Dae instance. + + Returns: + A generator which runs the plan with the modified DAE settings, restoring the original + settings afterwards. + """ + yield from ensure_connected(dae) + + original_time_channels = None + + def _inner() -> Generator[Msg, None, None]: + nonlocal original_time_channels + original_time_channels = yield from bps.rd(dae.tcb_settings) + + yield from plan + + def _onexit() -> Generator[Msg, None, None]: + nonlocal original_time_channels + if original_time_channels is not None: + yield from bps.mv(dae.tcb_settings, original_time_channels) + + return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/__init__.py b/src/ibex_bluesky_core/plans/__init__.py index 86afbc54..a900ec04 100644 --- a/src/ibex_bluesky_core/plans/__init__.py +++ b/src/ibex_bluesky_core/plans/__init__.py @@ -16,6 +16,7 @@ from ibex_bluesky_core.devices.simpledae import monitor_normalising_dae from ibex_bluesky_core.fitting import FitMethod from ibex_bluesky_core.plan_stubs import call_qt_aware, polling_plan +from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods from ibex_bluesky_core.utils import NamedReadableAndMovable, centred_pixel if TYPE_CHECKING: @@ -26,6 +27,7 @@ "adaptive_scan", "motor_adaptive_scan", "motor_scan", + "with_num_periods", "polling_plan", "scan", ] diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 3ae66f50..3bddd78c 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1,6 +1,6 @@ # pyright: reportMissingParameterType=false from enum import Enum -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from xml.etree import ElementTree as ET import bluesky.plan_stubs as bps @@ -10,7 +10,6 @@ import scipp.testing from bluesky.run_engine import RunEngine from ophyd_async.testing import get_mock_put, set_mock_value -from ibex_bluesky_core.plans.num_periods_wrapper import num_periods_wrapper from ibex_bluesky_core.devices import compress_and_hex, dehex_and_decompress from ibex_bluesky_core.devices.dae import ( @@ -40,6 +39,9 @@ ) from ibex_bluesky_core.devices.dae._period_settings import _convert_period_settings_to_xml from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml +from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods +from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables +from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels from tests.conftest import MOCK_PREFIX from tests.devices.dae_testing_data import ( dae_settings_template, @@ -297,19 +299,20 @@ async def test_dae_settings_get_parsed_correctly(): xml = await daesettings._raw_dae_settings.get_value() assert ET.canonicalize(xml) == ET.canonicalize(xml_filled_in) -async def test_num_periods_wrapper_modifies_and_restores_settings(RE: RunEngine, dae: Dae): + +async def test_period_settings_get_parsed_correctly(): expected_setup_source = PeriodSource.FILE expected_period_type = PeriodType.SOFTWARE expected_periods_file = "C:\\someperiodfile.txt" - expected_soft_periods_num = 10 - expected_hardware_period_sequences = 5 - expected_output_delay = 100 - expected_type_1 = 1 - expected_frames_1 = 2 - expected_output_1 = 3 - expected_type_2 = 2 - expected_frames_2 = 3 - expected_output_2 = 4 + expected_soft_periods_num = 42 + expected_hardware_period_sequences = 52 + expected_output_delay = 123 + expected_type_1 = 0 + expected_frames_1 = 1 + expected_output_1 = 2 + expected_type_2 = 0 + expected_frames_2 = 1 + expected_output_2 = 2 expected_type_3 = 1 expected_frames_3 = 2 expected_output_3 = 3 @@ -328,7 +331,7 @@ async def test_num_periods_wrapper_modifies_and_restores_settings(RE: RunEngine, expected_type_8 = 2 expected_frames_8 = 2 expected_output_8 = 2 - + periods_settings = [ SinglePeriodSettings( type=expected_type_1, frames=expected_frames_1, output=expected_output_1 @@ -356,7 +359,7 @@ async def test_num_periods_wrapper_modifies_and_restores_settings(RE: RunEngine, ), ] - initial_settings = DaePeriodSettingsData( + data = DaePeriodSettingsData( periods_soft_num=expected_soft_periods_num, periods_type=expected_period_type, periods_src=expected_setup_source, @@ -365,46 +368,47 @@ async def test_num_periods_wrapper_modifies_and_restores_settings(RE: RunEngine, periods_delay=expected_output_delay, periods_settings=periods_settings, ) - - # Connect the period_settings sub-device and set up the initial data - await dae.period_settings._raw_period_settings.connect(mock=True) - await dae.period_settings._raw_period_settings.set(initial_period_settings) - await dae.period_settings.set(initial_settings) - - # Read the original settings - original_settings: DaePeriodSettingsData = RE(bps.rd(dae.period_settings)).plan_result - assert original_settings.periods_soft_num == 10 - assert original_settings.periods_file == "C:\\someperiodfile.txt" - assert original_settings.periods_seq == 5 - - def inner_plan(): - current_settings: DaePeriodSettingsData = yield from bps.rd(dae.period_settings) - assert current_settings.periods_soft_num == 20 - assert current_settings.periods_file == "C:\\modified_file.txt" - assert current_settings.periods_seq == 5 - return current_settings - - # Run the wrapper test - result = RE( - num_periods_wrapper( - inner_plan(), - dae.period_settings, - periods_soft_num=20, - periods_file="C:\\modified_file.txt", - ) + xml_filled_in = period_settings_template.format( + period_src=expected_setup_source.value, + period_type=expected_period_type.value, + period_file=expected_periods_file, + num_soft_periods=expected_soft_periods_num, + period_seq=expected_hardware_period_sequences, + period_delay=expected_output_delay, + type_1=expected_type_1, + frames_1=expected_frames_1, + output_1=expected_output_1, + type_2=expected_type_2, + frames_2=expected_frames_2, + output_2=expected_output_2, + type_3=expected_type_3, + frames_3=expected_frames_3, + output_3=expected_output_3, + type_4=expected_type_4, + frames_4=expected_frames_4, + output_4=expected_output_4, + type_5=expected_type_5, + frames_5=expected_frames_5, + output_5=expected_output_5, + type_6=expected_type_6, + frames_6=expected_frames_6, + output_6=expected_output_6, + type_7=expected_type_7, + frames_7=expected_frames_7, + output_7=expected_output_7, + type_8=expected_type_8, + frames_8=expected_frames_8, + output_8=expected_output_8, ) + periodsettings = DaePeriodSettings(MOCK_PREFIX) + await periodsettings._raw_period_settings.connect(mock=True) + await periodsettings._raw_period_settings.set(initial_period_settings) + await periodsettings.set(data) + location = await periodsettings.locate() + assert location == {"setpoint": data, "readback": data} + xml = await periodsettings._raw_period_settings.get_value() + assert ET.canonicalize(xml) == ET.canonicalize(xml_filled_in) - # Check the modified settings - modified_settings = result.plan_result - assert modified_settings.periods_soft_num == 20 - assert modified_settings.periods_file == "C:\\modified_file.txt" - assert modified_settings.periods_seq == 5 - - # Check that settings are restored after the wrapper - final_settings: DaePeriodSettingsData = RE(bps.rd(dae.period_settings)).plan_result - assert final_settings.periods_soft_num == 10 - assert final_settings.periods_file == "C:\\someperiodfile.txt" - assert final_settings.periods_seq == 5 async def test_tcb_settings_get_parsed_correctly(): expected_tcb_file = "C:\\tcb.dat" @@ -1022,3 +1026,94 @@ async def test_if_tof_edges_has_no_units_then_read_spec_dataarray_gives_error( def test_dae_repr(): assert repr(Dae(prefix="foo", name="bar")) == "Dae(name=bar, prefix=foo)" + + +async def test_num_periods_wrapper(dae: Dae, RE: RunEngine): + original_settings = 4 + modified_settings = 7 + + await dae.number_of_periods.set(original_settings) + + def _dummy_plan_which_sets_periods(dae): + yield from bps.mv(dae.number_of_periods, modified_settings) + + current = yield from bps.rd(dae.number_of_periods) + assert current == 7 + + with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): + RE( + with_num_periods( + _dummy_plan_which_sets_periods(dae), + dae=dae, # type: ignore + ) + ) + + result = await dae.number_of_periods.read() + assert result['DAE-number_of_periods-signal']['value'] == original_settings + + +def test_time_channels_wrapper(dae: Dae, RE: RunEngine): + original_settings = compress_and_hex(initial_tcb_settings).decode() + modified_settings = DaeTCBSettingsData(time_unit=TCBTimeUnit.NANOSECONDS) + + expected_time_units = TCBTimeUnit.MICROSECONDS + + set_mock_value(dae.tcb_settings._raw_tcb_settings, original_settings) + + before: DaeTCBSettingsData = RE(bps.rd(dae.tcb_settings)).plan_result # type: ignore + + def _dummy_plan_which_sets_time_units(dae): + yield from bps.mv(dae.tcb_settings, modified_settings) + + current = yield from bps.rd(dae.tcb_settings) + assert current.time_unit == modified_settings.time_unit + + with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): + RE( + with_time_channels( + _dummy_plan_which_sets_time_units(dae), + dae=dae, # type: ignore + ) + ) + + after: DaeTCBSettingsData = RE(bps.rd(dae.tcb_settings)).plan_result # type: ignore + assert after == before + assert after.time_unit == expected_time_units + + +def test_dae_tables_wrapper(dae: Dae, RE: RunEngine): + original_settings = initial_dae_settings + modified_settings = DaeSettingsData(wiring_filepath="C:\\somefile.dat", + spectra_filepath="C:\\anotherfile.dat", + detector_filepath="C:\\anotherfile123.dat") + + expected_wiring = "NIMROD84modules+9monitors+LAB5Oct2012Wiring.dat" + expected_spectra = "NIMROD84modules+9monitors+LAB5Oct2012Spectra.dat" + expected_detector = "NIMROD84modules+9monitors+LAB5Oct2012Detector.dat" + + set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) + + before: DaeSettingsData = RE(bps.rd(dae.dae_settings)).plan_result # type: ignore + + def _dummy_plan_which_sets_wiring_sepectra_detector(dae): + yield from bps.mv(dae.dae_settings, modified_settings) + + current = yield from bps.rd(dae.dae_settings) + assert current.wiring_filepath == modified_settings.wiring_filepath + assert current.spectra_filepath == modified_settings.spectra_filepath + assert current.detector_filepath == modified_settings.detector_filepath + + with patch("ibex_bluesky_core.plans.dae_table_wrapper.ensure_connected"): + RE( + with_dae_tables( + _dummy_plan_which_sets_wiring_sepectra_detector(dae), + dae=dae, # type: ignore + ) + ) + + after: DaeSettingsData = RE(bps.rd(dae.dae_settings)).plan_result # type: ignore + + assert after == before + assert after.wiring_filepath.endswith(expected_wiring) + assert after.spectra_filepath.endswith(expected_spectra) + assert after.detector_filepath.endswith(expected_detector) From f10dedba18b6ffeef7ca5b132ee995fc8e2ee2b1 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 11:26:48 +0100 Subject: [PATCH 03/21] fixed incorrect path within dae_table test --- tests/devices/test_dae.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 3bddd78c..faeaaaac 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1081,7 +1081,7 @@ def _dummy_plan_which_sets_time_units(dae): assert after.time_unit == expected_time_units -def test_dae_tables_wrapper(dae: Dae, RE: RunEngine): +def test_dae_table_wrapper(dae: Dae, RE: RunEngine): original_settings = initial_dae_settings modified_settings = DaeSettingsData(wiring_filepath="C:\\somefile.dat", spectra_filepath="C:\\anotherfile.dat", @@ -1103,7 +1103,7 @@ def _dummy_plan_which_sets_wiring_sepectra_detector(dae): assert current.spectra_filepath == modified_settings.spectra_filepath assert current.detector_filepath == modified_settings.detector_filepath - with patch("ibex_bluesky_core.plans.dae_table_wrapper.ensure_connected"): + with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): RE( with_dae_tables( _dummy_plan_which_sets_wiring_sepectra_detector(dae), From ffd43aaa6e97775121784d3b0baa0649d81ebbef Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 11:30:43 +0100 Subject: [PATCH 04/21] ruff checks --- src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py | 8 ++++---- src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py | 7 +++---- src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py | 6 +++--- src/ibex_bluesky_core/plans/__init__.py | 2 +- tests/devices/test_dae.py | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 56bd60c8..68011240 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -1,6 +1,5 @@ """Wrap a plan with temporary modification to DAE Settings.""" -import copy from collections.abc import Generator import bluesky.plan_stubs as bps @@ -15,14 +14,15 @@ def with_dae_tables( plan: Generator[Msg, None, None], dae: "Dae") -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. - + Args: plan: The plan to wrap. dae: The Dae instance. - + Returns: A generator which runs the plan with the modified DAE settings, restoring the original settings afterwards. + """ yield from ensure_connected(dae) @@ -31,7 +31,7 @@ def with_dae_tables( def _inner() -> Generator[Msg, None, None]: nonlocal original_dae_setting original_dae_setting = yield from bps.rd(dae.dae_settings) - + yield from plan def _onexit() -> Generator[Msg, None, None]: diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index efd53426..ded97389 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -1,6 +1,5 @@ """Wrap a plan with temporary modification to Periods Settings.""" -import copy from collections.abc import Generator import bluesky.plan_stubs as bps @@ -15,16 +14,16 @@ def with_num_periods( plan: Generator[Msg, None, None], dae: DaePeriodSettings) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. - + Args: plan: The plan to wrap. dae: The Dae instance. - + Returns: A generator which runs the plan with the modified DAE settings, restoring the original settings afterwards. - """ + """ yield from ensure_connected(dae) original_num_periods = None diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index d1141111..fef788d6 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -1,6 +1,5 @@ """Wrap a plan with temporary modification to Time Channel Settings.""" -import copy from collections.abc import Generator import bluesky.plan_stubs as bps @@ -15,14 +14,15 @@ def with_time_channels( plan: Generator[Msg, None, None], dae: DaeTCBSettings) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. - + Args: plan: The plan to wrap. dae: The Dae instance. - + Returns: A generator which runs the plan with the modified DAE settings, restoring the original settings afterwards. + """ yield from ensure_connected(dae) diff --git a/src/ibex_bluesky_core/plans/__init__.py b/src/ibex_bluesky_core/plans/__init__.py index a900ec04..da769558 100644 --- a/src/ibex_bluesky_core/plans/__init__.py +++ b/src/ibex_bluesky_core/plans/__init__.py @@ -27,9 +27,9 @@ "adaptive_scan", "motor_adaptive_scan", "motor_scan", - "with_num_periods", "polling_plan", "scan", + "with_num_periods", ] diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index faeaaaac..4b4407e8 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -39,8 +39,8 @@ ) from ibex_bluesky_core.devices.dae._period_settings import _convert_period_settings_to_xml from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml -from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables +from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels from tests.conftest import MOCK_PREFIX from tests.devices.dae_testing_data import ( From 67b5670ce13de521197f094bf9c1ac9a4ae845ca Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 12:22:01 +0100 Subject: [PATCH 05/21] deleted files --- src/ibex_bluesky_core/plan_stubs.py | 197 ------------------ .../plans/dae_table_wrapper.py | 41 ---- .../plans/num_periods_wrapper.py | 41 ---- .../plans/test_time_channel_wrapper.py | 42 ---- .../plans/time_channels_wrapper.py | 41 ---- src/ibex_bluesky_core/plans/wrapper_test.py | 96 --------- 6 files changed, 458 deletions(-) delete mode 100644 src/ibex_bluesky_core/plan_stubs.py delete mode 100644 src/ibex_bluesky_core/plans/dae_table_wrapper.py delete mode 100644 src/ibex_bluesky_core/plans/num_periods_wrapper.py delete mode 100644 src/ibex_bluesky_core/plans/test_time_channel_wrapper.py delete mode 100644 src/ibex_bluesky_core/plans/time_channels_wrapper.py delete mode 100644 src/ibex_bluesky_core/plans/wrapper_test.py diff --git a/src/ibex_bluesky_core/plan_stubs.py b/src/ibex_bluesky_core/plan_stubs.py deleted file mode 100644 index d811f8fa..00000000 --- a/src/ibex_bluesky_core/plan_stubs.py +++ /dev/null @@ -1,197 +0,0 @@ -"""Core plan stubs.""" - -import logging -from collections.abc import Callable, Generator -from typing import Any, ParamSpec, TypeVar, cast - -from bluesky import plan_stubs as bps -from bluesky.plan_stubs import trigger_and_read -from bluesky.preprocessors import finalize_wrapper -from bluesky.protocols import Readable -from bluesky.utils import Msg -from ophyd_async.epics.motor import Motor, UseSetMode - -from ibex_bluesky_core.devices.reflectometry import ReflParameter -from ibex_bluesky_core.utils import NamedReadableAndMovable - -logger = logging.getLogger(__name__) - -P = ParamSpec("P") -T = TypeVar("T") - - -CALL_SYNC_MSG_KEY = "ibex_bluesky_core_call_sync" -CALL_QT_AWARE_MSG_KEY = "ibex_bluesky_core_call_qt_aware" - - -__all__ = [ - "call_qt_aware", - "call_sync", - "polling_plan", - "prompt_user_for_choice", - "redefine_motor", - "redefine_refl_parameter", -] - - -def call_sync(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Generator[Msg, None, T]: - """Call a synchronous user function in a plan, and returns the result of that call. - - Attempts to guard against the most common pitfalls of naive implementations, for example: - - - Blocking the whole event loop - - Breaking keyboard interrupt handling - - It does not necessarily guard against all possible cases, and as such it is *recommended* to - use native bluesky functionality wherever possible in preference to this plan stub. This should - be seen as an escape-hatch. - - The wrapped function will be run in a new thread. - - Args: - func: A callable to run. - *args: Arbitrary arguments to be passed to the wrapped function - **kwargs: Arbitrary keyword arguments to be passed to the wrapped function - - Returns: - The return value of the wrapped function - - """ - return cast(T, (yield Msg(CALL_SYNC_MSG_KEY, func, *args, **kwargs))) - - -def call_qt_aware( - func: Callable[P, T], *args: P.args, **kwargs: P.kwargs -) -> Generator[Msg, None, T]: - """Call a matplotlib function in a Qt-aware context, from within a plan. - - If matplotlib is using a Qt backend then UI operations are run on the Qt thread via Qt signals. - - Only matplotlib functions may be run using this plan stub. - - Args: - func: A matplotlib function reference. - *args: Arbitrary arguments, passed through to matplotlib.pyplot.subplots - **kwargs: Arbitrary keyword arguments, passed through to matplotlib.pyplot.subplots - - Raises: - ValueError: if the passed function is not a matplotlib function. - - Returns: - The return value of the wrapped function - - """ - # Limit potential for misuse - constrain to just running matplotlib functions. - if not getattr(func, "__module__", "").startswith("matplotlib"): - raise ValueError("Only matplotlib functions should be passed to call_qt_aware") - - return cast(T, (yield Msg(CALL_QT_AWARE_MSG_KEY, func, *args, **kwargs))) - - -def redefine_motor( - motor: Motor, position: float, *, sleep: float = 1 -) -> Generator[Msg, None, None]: - """Redefines the current positions of a motor. - - Note: - This does not move the motor, it just redefines its position to be the given value. - - Args: - motor: The motor to set a position on. - position: The position to set. - sleep: An amount of time to sleep, in seconds, after redefining. Defaults to 1 second. - This avoids race conditions where a motor is redefined and then immediately moved. - - """ - logger.info("Redefining motor %s to %s", motor.name, position) - - def make_motor_usable() -> Generator[Msg, None, None]: - yield from bps.abs_set(motor.set_use_switch, UseSetMode.USE) - yield from bps.sleep(sleep) - - def inner() -> Generator[Msg, None, None]: - yield from bps.abs_set(motor.set_use_switch, UseSetMode.SET) - yield from bps.abs_set(motor.user_setpoint, position) - - return (yield from finalize_wrapper(inner(), make_motor_usable())) - - -def redefine_refl_parameter( - parameter: ReflParameter, position: float -) -> Generator[Msg, None, None]: - """Redefines the current positions of a reflectometry parameter. - - Note: - This does not move the parameter, it just redefines its position to be the given value. - - Args: - parameter: The reflectometry parameter to set a position on. - position: The position to set. - - """ - if parameter.redefine is None: - raise ValueError(f"Parameter {parameter.name} cannot be redefined.") - logger.info("Redefining refl parameter %s to %s", parameter.name, position) - yield from bps.mv(parameter.redefine, position) - - -def prompt_user_for_choice(*, prompt: str, choices: list[str]) -> Generator[Msg, None, str]: - """Prompt the user to choose between a limited set of options. - - Args: - prompt: The user prompt string. - choices: A list of allowable choices. - - Returns: - One of the entries in the choices list. - - """ - choice = yield from call_sync(input, prompt) - while choice not in choices: - choice = yield from call_sync(input, prompt) - - return choice - - -def polling_plan( - motor: NamedReadableAndMovable, readable: Readable[Any], destination: float -) -> Generator[Msg, None, None]: - """Move to a destination but drop updates from readable if motor position has not changed. - - Note - this does not start a run, this should be done with a run_decorator or similar in an - outer plan which calls this plan. - - Args: - motor: the motor to move. - readable: the readable to read updates from, but drop if motor has not moved. - destination: the destination position. - - Returns: - None - - If we just used bp.scan() with a readable that updates more frequently than a motor can - register that it has moved, we would have lots of updates with the same motor position, - which may not be helpful. - - """ - yield from bps.checkpoint() - yield from bps.create() - reading = yield from bps.read(motor) - yield from bps.read(readable) - yield from bps.save() - - # start the ramp - status = yield from bps.abs_set(motor, destination, wait=False) - while not status.done: - yield from bps.create() - new_reading = yield from bps.read(motor) - yield from bps.read(readable) - - if new_reading[motor.name]["value"] == reading[motor.name]["value"]: - yield from bps.drop() - else: - reading = new_reading - yield from bps.save() - - # take a 'post' data point - yield from trigger_and_read([motor, readable]) diff --git a/src/ibex_bluesky_core/plans/dae_table_wrapper.py b/src/ibex_bluesky_core/plans/dae_table_wrapper.py deleted file mode 100644 index 4f29c17b..00000000 --- a/src/ibex_bluesky_core/plans/dae_table_wrapper.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Wrap a plan with temporary modification to DAE Settings.""" - -import copy -from collections.abc import Generator - -import bluesky.plan_stubs as bps -import bluesky.preprocessors as bpp -from bluesky.utils import Msg -from ophyd_async.plan_stubs import ensure_connected - -from ibex_bluesky_core.devices.dae import Dae - - -def time_channels_wrapper( - plan: Generator[Msg, None, None], - dae: "Dae", - **modified_dae: int | str | None) -> Generator[Msg, None, None]: - """Wrap a plan with temporary modification to DAE Settings.""" - yield from ensure_connected(dae) - - original_dae_setting = None - - def _inner() -> Generator[Msg, None, None]: - nonlocal original_dae_setting - original_dae_setting = yield from bps.rd(dae.dae_settings) - - new_dea_tables = copy.deepcopy(original_dae_setting) - - for key, value in modified_dae.items(): - if hasattr(new_dea_tables, key): - setattr(new_dea_tables, key, value) - - yield from bps.mv(dae.dae_settings, original_dae_setting) - yield from plan - - def _onexit() -> Generator[Msg, None, None]: - nonlocal original_dae_setting - if original_dae_setting is not None: - yield from bps.mv(dae.dae_settings, original_dae_setting) - - return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/num_periods_wrapper.py b/src/ibex_bluesky_core/plans/num_periods_wrapper.py deleted file mode 100644 index 689f29f1..00000000 --- a/src/ibex_bluesky_core/plans/num_periods_wrapper.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Wrap a plan with temporary modification to Periods Settings.""" - -import copy -from collections.abc import Generator - -import bluesky.plan_stubs as bps -import bluesky.preprocessors as bpp -from bluesky.utils import Msg -from ophyd_async.plan_stubs import ensure_connected - -from ibex_bluesky_core.devices.dae import Dae - - -def num_periods_wrapper( - plan: Generator[Msg, None, None], - dae: "Dae", - **modified_periods: int | str | None) -> Generator[Msg, None, None]: - """Wrap a plan with temporary modification to Periods Settings.""" - yield from ensure_connected(dae) - - original_num_periods = None - - def _inner() -> Generator[Msg, None, None]: - nonlocal original_num_periods - original_num_periods = yield from bps.rd(dae.period_settings) - - new_num_periods = copy.deepcopy(original_num_periods) - - for key, value in modified_periods.items(): - if hasattr(new_num_periods, key): - setattr(new_num_periods, key, value) - - yield from bps.mv(dae.period_settings, new_num_periods) - yield from plan - - def _onexit() -> Generator[Msg, None, None]: - nonlocal original_num_periods - if original_num_periods is not None: - yield from bps.mv(dae.period_settings, original_num_periods) - - return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/test_time_channel_wrapper.py b/src/ibex_bluesky_core/plans/test_time_channel_wrapper.py deleted file mode 100644 index 3115c418..00000000 --- a/src/ibex_bluesky_core/plans/test_time_channel_wrapper.py +++ /dev/null @@ -1,42 +0,0 @@ -from ibex_bluesky_core.devices.dae import Dae, TimeRegimeMode -from bluesky.plans import scan -from ibex_bluesky_core.plans.time_channels_wrapper import tcb_wrapper -from ibex_bluesky_core.devices.simpledae import ( - GoodFramesNormalizer, - GoodFramesWaiter, - RunPerPointController, - SimpleDae, -) -from ibex_bluesky_core.utils import get_pv_prefix -from ibex_bluesky_core.devices.block import block_rw_rbv - -def test_tcb_wrapper_runs(): - - prefix = get_pv_prefix() - - controller = RunPerPointController(save_run=True) - waiter = GoodFramesWaiter(500) - reducer = GoodFramesNormalizer( - prefix=prefix, - detector_spectra=[i for i in range(1, 100)], - ) - - dae = SimpleDae( - prefix=prefix, - controller=controller, - waiter=waiter, - reducer=reducer, - ) - wrapped_plan = tcb_wrapper( - scan, - dae, - from_=9, - to=87, - steps=2, - mode=TimeRegimeMode.DT - ) - - list(wrapped_plan) # Forces execution - - # If no exception occurs, test passes - assert True diff --git a/src/ibex_bluesky_core/plans/time_channels_wrapper.py b/src/ibex_bluesky_core/plans/time_channels_wrapper.py deleted file mode 100644 index 4254d866..00000000 --- a/src/ibex_bluesky_core/plans/time_channels_wrapper.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Wrap a plan with temporary modification to Time Channel Settings.""" - -import copy -from collections.abc import Generator - -import bluesky.plan_stubs as bps -import bluesky.preprocessors as bpp -from bluesky.utils import Msg -from ophyd_async.plan_stubs import ensure_connected - -from ibex_bluesky_core.devices.dae import Dae - - -def tcb_wrapper( - plan: Generator[Msg, None, None], - dae: "Dae", - **modified_dae: int | str | None) -> Generator[Msg, None, None]: - """Wrap a plan with temporary modification to DAE Settings.""" - yield from ensure_connected(dae) - - original_time_channels = None - - def _inner() -> Generator[Msg, None, None]: - nonlocal original_time_channels - original_time_channels = yield from bps.rd(dae.tcb_settings) - - new_time_channels = copy.deepcopy(original_time_channels) - - for key, value in modified_dae.items(): - if hasattr(new_time_channels, key): - setattr(new_time_channels, key, value) - - yield from bps.mv(dae.tcb_settings, new_time_channels) - yield from plan - - def _onexit() -> Generator[Msg, None, None]: - nonlocal original_time_channels - if original_time_channels is not None: - yield from bps.mv(dae.tcb_settings, original_time_channels) - - return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plans/wrapper_test.py b/src/ibex_bluesky_core/plans/wrapper_test.py deleted file mode 100644 index 780f9cb1..00000000 --- a/src/ibex_bluesky_core/plans/wrapper_test.py +++ /dev/null @@ -1,96 +0,0 @@ -from unittest.mock import Mock - - -# Mock the Msg class since we might not have bluesky installed -class Msg: - def __init__(self, command, obj=None, *args, **kwargs): - self.command = command - self.obj = obj - self.args = args - self.kwargs = kwargs - -# Remove the function definition since we're importing it -# from time_channels_wrapper import time_channels_wrapper - - -def test_wrapper_runs_without_error(): - """Simple test to check if the wrapper function executes without crashing.""" - - # Create a simple mock plan - def dummy_plan(): - yield Msg('checkpoint') - - # Create a mock DAE with the required attributes - mock_dae = Mock() - mock_dae.tcb_settings = Mock() - mock_dae.tcb_settings.time_channel_1 = 100 - mock_dae.tcb_settings.time_channel_2 = 200 - - # Create mock modules and functions - import sys - from types import ModuleType - - # Create mock modules - mock_bps = ModuleType('bluesky.plan_stubs') - mock_ophyd = ModuleType('ophyd_async.plan_stubs') - mock_bpp = ModuleType('bluesky.preprocessors') - - def mock_ensure_connected(device): - yield Msg('null') - - def mock_rd(device): - yield Msg('null') - return mock_dae.tcb_settings - - def mock_mv(device, value): - yield Msg('null') - - def mock_finalize_wrapper(plan, cleanup): - try: - yield from plan - finally: - yield from cleanup() - - # Add functions to mock modules - mock_bps.rd = mock_rd - mock_bps.mv = mock_mv - mock_ophyd.ensure_connected = mock_ensure_connected - mock_bpp.finalize_wrapper = mock_finalize_wrapper - - # Add to sys.modules so imports work - sys.modules['bluesky.plan_stubs'] = mock_bps - sys.modules['ophyd_async.plan_stubs'] = mock_ophyd - sys.modules['bluesky.preprocessors'] = mock_bpp - - try: - - # Import your function from the file - from time_channels_wrapper import tcb_wrapper - - # Run the wrapper - if it completes without exception, it works! - wrapped_plan = tcb_wrapper( - dummy_plan(), - mock_dae, - time_channel_1=500 - ) - - # Execute the generator to completion - list(wrapped_plan) - - print("✅ Test passed - wrapper runs without error!") - - except ImportError as e: - print(f"❌ Import error: {e}") - print("💡 You need to:") - print(" 1. Replace 'your_module' with the actual file/module name") - print(" 2. Or copy your time_channels_wrapper function into this test file") - - finally: - # Clean up sys.modules - for module in ['bluesky.plan_stubs', 'ophyd_async.plan_stubs', 'bluesky.preprocessors']: - if module in sys.modules: - del sys.modules[module] - - -if __name__ == "__main__": - test_wrapper_runs_without_error() From 3ff0cf6622b8bf25a0130f60aae1ce8b07237e9d Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 13:38:02 +0100 Subject: [PATCH 06/21] ruff format changes --- .../plan_stubs/dae_table_wrapper.py | 4 +--- .../plan_stubs/num_periods_wrapper.py | 4 ++-- .../plan_stubs/time_channels_wrapper.py | 4 ++-- tests/devices/test_dae.py | 14 ++++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 68011240..53a41288 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -10,9 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae -def with_dae_tables( - plan: Generator[Msg, None, None], - dae: "Dae") -> Generator[Msg, None, None]: +def with_dae_tables(plan: Generator[Msg, None, None], dae: "Dae") -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. Args: diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index ded97389..d82f3b46 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -11,8 +11,8 @@ def with_num_periods( - plan: Generator[Msg, None, None], - dae: DaePeriodSettings) -> Generator[Msg, None, None]: + plan: Generator[Msg, None, None], dae: DaePeriodSettings +) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. Args: diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index fef788d6..08d488a1 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -11,8 +11,8 @@ def with_time_channels( - plan: Generator[Msg, None, None], - dae: DaeTCBSettings) -> Generator[Msg, None, None]: + plan: Generator[Msg, None, None], dae: DaeTCBSettings +) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. Args: diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 4b4407e8..9b9eb7c0 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1045,11 +1045,11 @@ def _dummy_plan_which_sets_periods(dae): with_num_periods( _dummy_plan_which_sets_periods(dae), dae=dae, # type: ignore - ) ) + ) result = await dae.number_of_periods.read() - assert result['DAE-number_of_periods-signal']['value'] == original_settings + assert result["DAE-number_of_periods-signal"]["value"] == original_settings def test_time_channels_wrapper(dae: Dae, RE: RunEngine): @@ -1083,9 +1083,11 @@ def _dummy_plan_which_sets_time_units(dae): def test_dae_table_wrapper(dae: Dae, RE: RunEngine): original_settings = initial_dae_settings - modified_settings = DaeSettingsData(wiring_filepath="C:\\somefile.dat", - spectra_filepath="C:\\anotherfile.dat", - detector_filepath="C:\\anotherfile123.dat") + modified_settings = DaeSettingsData( + wiring_filepath="C:\\somefile.dat", + spectra_filepath="C:\\anotherfile.dat", + detector_filepath="C:\\anotherfile123.dat", + ) expected_wiring = "NIMROD84modules+9monitors+LAB5Oct2012Wiring.dat" expected_spectra = "NIMROD84modules+9monitors+LAB5Oct2012Spectra.dat" @@ -1108,8 +1110,8 @@ def _dummy_plan_which_sets_wiring_sepectra_detector(dae): with_dae_tables( _dummy_plan_which_sets_wiring_sepectra_detector(dae), dae=dae, # type: ignore - ) ) + ) after: DaeSettingsData = RE(bps.rd(dae.dae_settings)).plan_result # type: ignore From 42a49cd30f69c5c185799e14293c15e55bb6d4ad Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 14:20:32 +0100 Subject: [PATCH 07/21] updated dae parameters in wrapper --- src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py | 4 ++-- src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py | 4 ++-- src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 53a41288..fb0a2a3f 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae -def with_dae_tables(plan: Generator[Msg, None, None], dae: "Dae") -> Generator[Msg, None, None]: +def with_dae_tables(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. Args: @@ -28,7 +28,7 @@ def with_dae_tables(plan: Generator[Msg, None, None], dae: "Dae") -> Generator[M def _inner() -> Generator[Msg, None, None]: nonlocal original_dae_setting - original_dae_setting = yield from bps.rd(dae.dae_settings) + original_dae_setting = yield from bps.rd(dae.dae_settings) yield from plan diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index d82f3b46..030327c4 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -7,11 +7,11 @@ from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected -from ibex_bluesky_core.devices.dae import DaePeriodSettings +from ibex_bluesky_core.devices.dae import Dae def with_num_periods( - plan: Generator[Msg, None, None], dae: DaePeriodSettings + plan: Generator[Msg, None, None], dae: Dae ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index 08d488a1..ccf51d39 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -7,11 +7,11 @@ from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected -from ibex_bluesky_core.devices.dae import DaeTCBSettings +from ibex_bluesky_core.devices.dae import Dae def with_time_channels( - plan: Generator[Msg, None, None], dae: DaeTCBSettings + plan: Generator[Msg, None, None], dae: Dae ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. @@ -30,7 +30,7 @@ def with_time_channels( def _inner() -> Generator[Msg, None, None]: nonlocal original_time_channels - original_time_channels = yield from bps.rd(dae.tcb_settings) + original_time_channels = yield from bps.rd(dae.tcb_settings) #type: ignore yield from plan From d8dabb37e53a7396c134f1f78613cc163daf01ef Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 14:22:40 +0100 Subject: [PATCH 08/21] ruff check and format --- src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py | 2 +- src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py | 4 +--- src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index fb0a2a3f..1fc8d892 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -28,7 +28,7 @@ def with_dae_tables(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg def _inner() -> Generator[Msg, None, None]: nonlocal original_dae_setting - original_dae_setting = yield from bps.rd(dae.dae_settings) + original_dae_setting = yield from bps.rd(dae.dae_settings) yield from plan diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index 030327c4..f23463d2 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -10,9 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae -def with_num_periods( - plan: Generator[Msg, None, None], dae: Dae -) -> Generator[Msg, None, None]: +def with_num_periods(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. Args: diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index ccf51d39..a8414645 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -10,9 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae -def with_time_channels( - plan: Generator[Msg, None, None], dae: Dae -) -> Generator[Msg, None, None]: +def with_time_channels(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. Args: @@ -30,7 +28,7 @@ def with_time_channels( def _inner() -> Generator[Msg, None, None]: nonlocal original_time_channels - original_time_channels = yield from bps.rd(dae.tcb_settings) #type: ignore + original_time_channels = yield from bps.rd(dae.tcb_settings) # type: ignore yield from plan From fd772c1bfdaf2f0f6c627dff416e855af656a33e Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 14:26:33 +0100 Subject: [PATCH 09/21] ruff --fix --- tests/devices/test_dae.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 9b9eb7c0..60a2c1dc 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1116,6 +1116,6 @@ def _dummy_plan_which_sets_wiring_sepectra_detector(dae): after: DaeSettingsData = RE(bps.rd(dae.dae_settings)).plan_result # type: ignore assert after == before - assert after.wiring_filepath.endswith(expected_wiring) - assert after.spectra_filepath.endswith(expected_spectra) - assert after.detector_filepath.endswith(expected_detector) + assert after.wiring_filepath.endswith(expected_wiring) # type: ignore + assert after.spectra_filepath.endswith(expected_spectra) # type: ignore + assert after.detector_filepath.endswith(expected_detector) # type: ignore From 36805b1e7894fc723bdca6862520f9bf883e0954 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Mon, 13 Oct 2025 15:33:13 +0100 Subject: [PATCH 10/21] removed if statement --- src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py | 3 +-- src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py | 3 +-- src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 1fc8d892..853e0ab5 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -34,7 +34,6 @@ def _inner() -> Generator[Msg, None, None]: def _onexit() -> Generator[Msg, None, None]: nonlocal original_dae_setting - if original_dae_setting is not None: - yield from bps.mv(dae.dae_settings, original_dae_setting) + yield from bps.mv(dae.dae_settings, original_dae_setting) return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index f23463d2..34bcec7f 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -34,7 +34,6 @@ def _inner() -> Generator[Msg, None, None]: def _onexit() -> Generator[Msg, None, None]: nonlocal original_num_periods - if original_num_periods is not None: - yield from bps.mv(dae.number_of_periods, original_num_periods) + yield from bps.mv(dae.number_of_periods, original_num_periods) return (yield from bpp.finalize_wrapper(_inner(), _onexit())) diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index a8414645..9752988d 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -34,7 +34,6 @@ def _inner() -> Generator[Msg, None, None]: def _onexit() -> Generator[Msg, None, None]: nonlocal original_time_channels - if original_time_channels is not None: - yield from bps.mv(dae.tcb_settings, original_time_channels) + yield from bps.mv(dae.tcb_settings, original_time_channels) return (yield from bpp.finalize_wrapper(_inner(), _onexit())) From 915149793a15ac24d6a0336f747702415a935576 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Tue, 28 Oct 2025 11:59:19 +0000 Subject: [PATCH 11/21] updates that aren't working --- .../devices/dae/_period_settings.py | 4 +- .../devices/dae/_tcb_settings.py | 4 +- .../plan_stubs/dae_table_wrapper.py | 14 +++---- .../plan_stubs/num_periods_wrapper.py | 19 +++++---- .../plan_stubs/time_channels_wrapper.py | 17 ++++---- tests/devices/test_dae.py | 42 ++++++++++++------- 6 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/ibex_bluesky_core/devices/dae/_period_settings.py b/src/ibex_bluesky_core/devices/dae/_period_settings.py index b4a1d811..d4dadd57 100644 --- a/src/ibex_bluesky_core/devices/dae/_period_settings.py +++ b/src/ibex_bluesky_core/devices/dae/_period_settings.py @@ -7,7 +7,7 @@ from xml.etree.ElementTree import tostring from bluesky.protocols import Locatable, Location, Movable -from ophyd_async.core import AsyncStatus, Device, SignalRW +from ophyd_async.core import AsyncStatus, StandardReadable, SignalRW from ibex_bluesky_core.devices import ( isis_epics_signal_rw, @@ -116,7 +116,7 @@ def _convert_period_settings_to_xml(current_xml: str, value: DaePeriodSettingsDa return tostring(root, encoding="unicode") -class DaePeriodSettings(Device, Locatable[DaePeriodSettingsData], Movable[DaePeriodSettingsData]): +class DaePeriodSettings(StandardReadable, Locatable[DaePeriodSettingsData], Movable[DaePeriodSettingsData]): """Subdevice for the DAE hardware period settings.""" def __init__(self, dae_prefix: str, name: str = "") -> None: diff --git a/src/ibex_bluesky_core/devices/dae/_tcb_settings.py b/src/ibex_bluesky_core/devices/dae/_tcb_settings.py index db04f192..77e23436 100644 --- a/src/ibex_bluesky_core/devices/dae/_tcb_settings.py +++ b/src/ibex_bluesky_core/devices/dae/_tcb_settings.py @@ -7,7 +7,7 @@ from xml.etree.ElementTree import tostring from bluesky.protocols import Locatable, Location, Movable -from ophyd_async.core import AsyncStatus, Device, SignalRW +from ophyd_async.core import AsyncStatus, SignalRW, StandardReadable from ibex_bluesky_core.devices import ( compress_and_hex, @@ -120,7 +120,7 @@ def _convert_tcb_settings_to_xml(current_xml: str, settings: DaeTCBSettingsData) return tostring(root, encoding="unicode") -class DaeTCBSettings(Device, Locatable[DaeTCBSettingsData], Movable[DaeTCBSettingsData]): +class DaeTCBSettings(StandardReadable, Locatable[DaeTCBSettingsData], Movable[DaeTCBSettingsData]): """Subdevice for the DAE time channel settings.""" def __init__(self, dae_prefix: str, name: str = "") -> None: diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 853e0ab5..ea660811 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -7,10 +7,12 @@ from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected -from ibex_bluesky_core.devices.dae import Dae +from ibex_bluesky_core.devices.dae import Dae, DaeSettingsData -def with_dae_tables(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg, None, None]: +def with_dae_tables(plan: Generator[Msg, None, None], + dae: Dae, + new_settings: DaeSettingsData) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. Args: @@ -30,10 +32,8 @@ def _inner() -> Generator[Msg, None, None]: nonlocal original_dae_setting original_dae_setting = yield from bps.rd(dae.dae_settings) - yield from plan + yield from bps.mv(dae.dae_settings, new_settings) - def _onexit() -> Generator[Msg, None, None]: - nonlocal original_dae_setting - yield from bps.mv(dae.dae_settings, original_dae_setting) + yield from plan - return (yield from bpp.finalize_wrapper(_inner(), _onexit())) + return (yield from bpp.finalize_wrapper(_inner(), bps.mv(dae.dae_settings, original_dae_setting))) diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index 34bcec7f..d22f4809 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -1,16 +1,19 @@ """Wrap a plan with temporary modification to Periods Settings.""" +import copy from collections.abc import Generator import bluesky.plan_stubs as bps import bluesky.preprocessors as bpp from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected +from ibex_bluesky_core.devices.dae import DaePeriodSettings, Dae -from ibex_bluesky_core.devices.dae import Dae - -def with_num_periods(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg, None, None]: +def with_num_periods( + plan: Generator[Msg, None, None], + dae: Dae, + number_of_periods: int) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. Args: @@ -22,18 +25,16 @@ def with_num_periods(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Ms settings afterwards. """ - yield from ensure_connected(dae) original_num_periods = None def _inner() -> Generator[Msg, None, None]: + yield from ensure_connected(dae) nonlocal original_num_periods original_num_periods = yield from bps.rd(dae.number_of_periods) - yield from plan + yield from bps.mv(dae.number_of_periods, number_of_periods) - def _onexit() -> Generator[Msg, None, None]: - nonlocal original_num_periods - yield from bps.mv(dae.number_of_periods, original_num_periods) + yield from plan - return (yield from bpp.finalize_wrapper(_inner(), _onexit())) + return (yield from bpp.finalize_wrapper(_inner(), bps.mv(dae.number_of_periods, original_num_periods))) diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index 9752988d..2cc0a06e 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -7,10 +7,13 @@ from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected -from ibex_bluesky_core.devices.dae import Dae +from ibex_bluesky_core.devices.dae import Dae, DaeTCBSettingsData -def with_time_channels(plan: Generator[Msg, None, None], dae: Dae) -> Generator[Msg, None, None]: + +def with_time_channels(plan: Generator[Msg, None, None], + dae: Dae, + new_tcb_settings: DaeTCBSettingsData) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. Args: @@ -28,12 +31,10 @@ def with_time_channels(plan: Generator[Msg, None, None], dae: Dae) -> Generator[ def _inner() -> Generator[Msg, None, None]: nonlocal original_time_channels - original_time_channels = yield from bps.rd(dae.tcb_settings) # type: ignore + original_time_channels = yield from bps.rd(dae.tcb_settings) - yield from plan + yield from bps.mv(dae.tcb_settings, new_tcb_settings) - def _onexit() -> Generator[Msg, None, None]: - nonlocal original_time_channels - yield from bps.mv(dae.tcb_settings, original_time_channels) + yield from plan - return (yield from bpp.finalize_wrapper(_inner(), _onexit())) + return (yield from bpp.finalize_wrapper(_inner(), bps.mv(dae.tcb_settings, original_time_channels))) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 60a2c1dc..45ae6c29 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1028,28 +1028,39 @@ def test_dae_repr(): assert repr(Dae(prefix="foo", name="bar")) == "Dae(name=bar, prefix=foo)" -async def test_num_periods_wrapper(dae: Dae, RE: RunEngine): +def test_num_periods_wrapper(dae: Dae, RE: RunEngine): original_settings = 4 - modified_settings = 7 - await dae.number_of_periods.set(original_settings) + # await dae.number_of_periods.set(original_settings) + set_mock_value(dae.number_of_periods.signal, original_settings) - def _dummy_plan_which_sets_periods(dae): - yield from bps.mv(dae.number_of_periods, modified_settings) + # t = await dae.number_of_periods.signal.get_value() - current = yield from bps.rd(dae.number_of_periods) - assert current == 7 + # def _dummy_plan_which_sets_periods(dae): + # current = yield from bps.rd(dae.number_of_periods) + # print("Current number of periods:", current) - with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): - RE( - with_num_periods( - _dummy_plan_which_sets_periods(dae), - dae=dae, # type: ignore - ) + # assert current == 80 + + #with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): + # This is needed for the DaeCheckingSignal + # set_mock_value(dae.number_of_periods.signal, 80) + + RE( + with_num_periods( + bps.null(), + dae=dae, # type: ignore + number_of_periods = 80, ) + ) + - result = await dae.number_of_periods.read() - assert result["DAE-number_of_periods-signal"]["value"] == original_settings + mock_set = get_mock_put(dae.number_of_periods.signal) + + mock_set.assert_called_with(80) + mock_set.assert_called_with(original_settings) + + def test_time_channels_wrapper(dae: Dae, RE: RunEngine): @@ -1081,6 +1092,7 @@ def _dummy_plan_which_sets_time_units(dae): assert after.time_unit == expected_time_units + def test_dae_table_wrapper(dae: Dae, RE: RunEngine): original_settings = initial_dae_settings modified_settings = DaeSettingsData( From 7e03513252b0d5eaf85eaf5d7d3dcc8941940e99 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Tue, 28 Oct 2025 13:03:08 +0000 Subject: [PATCH 12/21] fix num_periods test --- .../plan_stubs/dae_table_wrapper.py | 12 ++++--- .../plan_stubs/num_periods_wrapper.py | 15 ++++---- .../plan_stubs/time_channels_wrapper.py | 14 +++++--- tests/devices/test_dae.py | 36 ++++++------------- 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index ea660811..3eb4b0c6 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -10,14 +10,15 @@ from ibex_bluesky_core.devices.dae import Dae, DaeSettingsData -def with_dae_tables(plan: Generator[Msg, None, None], - dae: Dae, - new_settings: DaeSettingsData) -> Generator[Msg, None, None]: +def with_dae_tables( + plan: Generator[Msg, None, None], dae: Dae, new_settings: DaeSettingsData +) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. Args: plan: The plan to wrap. dae: The Dae instance. + new_settings: The new DAE Settings to apply temporarily. Returns: A generator which runs the plan with the modified DAE settings, restoring the original @@ -36,4 +37,7 @@ def _inner() -> Generator[Msg, None, None]: yield from plan - return (yield from bpp.finalize_wrapper(_inner(), bps.mv(dae.dae_settings, original_dae_setting))) + def _cleanup() -> Generator[Msg, None, None]: + yield from bps.mv(dae.dae_settings, original_dae_setting) + + return (yield from bpp.finalize_wrapper(_inner(), _cleanup())) diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index d22f4809..cd52df94 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -11,18 +11,18 @@ def with_num_periods( - plan: Generator[Msg, None, None], - dae: Dae, - number_of_periods: int) -> Generator[Msg, None, None]: + plan: Generator[Msg, None, None], dae: Dae, number_of_periods: int +) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. Args: plan: The plan to wrap. dae: The Dae instance. + number_of_periods: The number of periods to set to temporarily. Returns: - A generator which runs the plan with the modified DAE settings, restoring the original - settings afterwards. + A generator which runs the plan with the modified number of periods, restoring the original + number of periods afterwards. """ @@ -37,4 +37,7 @@ def _inner() -> Generator[Msg, None, None]: yield from plan - return (yield from bpp.finalize_wrapper(_inner(), bps.mv(dae.number_of_periods, original_num_periods))) + def _cleanup() -> Generator[Msg, None, None]: + yield from bps.mv(dae.number_of_periods, original_num_periods) + + return (yield from bpp.finalize_wrapper(_inner(), _cleanup())) diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index 2cc0a06e..39b2a63e 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -11,17 +11,18 @@ from ibex_bluesky_core.devices.dae import Dae, DaeTCBSettingsData -def with_time_channels(plan: Generator[Msg, None, None], - dae: Dae, - new_tcb_settings: DaeTCBSettingsData) -> Generator[Msg, None, None]: +def with_time_channels( + plan: Generator[Msg, None, None], dae: Dae, new_tcb_settings: DaeTCBSettingsData +) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. Args: plan: The plan to wrap. dae: The Dae instance. + new_tcb_settings: The time channel settings to apply temporarily. Returns: - A generator which runs the plan with the modified DAE settings, restoring the original + A generator which runs the plan with the modified TCB settings, restoring the original settings afterwards. """ @@ -37,4 +38,7 @@ def _inner() -> Generator[Msg, None, None]: yield from plan - return (yield from bpp.finalize_wrapper(_inner(), bps.mv(dae.tcb_settings, original_time_channels))) + def _cleanup() -> Generator[Msg, None, None]: + yield from bps.mv(dae.tcb_settings, original_time_channels) + + return (yield from bpp.finalize_wrapper(_inner(), _cleanup())) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 45ae6c29..c39637be 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1031,36 +1031,21 @@ def test_dae_repr(): def test_num_periods_wrapper(dae: Dae, RE: RunEngine): original_settings = 4 - # await dae.number_of_periods.set(original_settings) set_mock_value(dae.number_of_periods.signal, original_settings) - # t = await dae.number_of_periods.signal.get_value() - - # def _dummy_plan_which_sets_periods(dae): - # current = yield from bps.rd(dae.number_of_periods) - # print("Current number of periods:", current) - - # assert current == 80 - - #with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): - # This is needed for the DaeCheckingSignal - # set_mock_value(dae.number_of_periods.signal, 80) - - RE( - with_num_periods( - bps.null(), - dae=dae, # type: ignore - number_of_periods = 80, + with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): + RE( + with_num_periods( + bps.null(), + dae=dae, + number_of_periods=80, + ) ) - ) + mock_set_calls = get_mock_put(dae.number_of_periods.signal).call_args_list - mock_set = get_mock_put(dae.number_of_periods.signal) - - mock_set.assert_called_with(80) - mock_set.assert_called_with(original_settings) - - + assert mock_set_calls[0].args == 80 + assert mock_set_calls[1].args == original_settings def test_time_channels_wrapper(dae: Dae, RE: RunEngine): @@ -1092,7 +1077,6 @@ def _dummy_plan_which_sets_time_units(dae): assert after.time_unit == expected_time_units - def test_dae_table_wrapper(dae: Dae, RE: RunEngine): original_settings = initial_dae_settings modified_settings = DaeSettingsData( From 0865761d0399b3bd81013a707a82810a7b4c2cc3 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 11:43:28 +0000 Subject: [PATCH 13/21] updated tests for wrappers --- tests/devices/test_dae.py | 74 ++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index c39637be..a71af73c 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1085,33 +1085,67 @@ def test_dae_table_wrapper(dae: Dae, RE: RunEngine): detector_filepath="C:\\anotherfile123.dat", ) - expected_wiring = "NIMROD84modules+9monitors+LAB5Oct2012Wiring.dat" - expected_spectra = "NIMROD84modules+9monitors+LAB5Oct2012Spectra.dat" - expected_detector = "NIMROD84modules+9monitors+LAB5Oct2012Detector.dat" - - set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) - - before: DaeSettingsData = RE(bps.rd(dae.dae_settings)).plan_result # type: ignore + original_settings = dae_settings_template.format( + wiring_table="C:\\originalfile.dat", + detector_table="C:\\anotheroriginalfile.dat", + spectra_table="C:\\originalspectrafile.dat", + mon_spec=1, + from_=1, + to=1, + timing_src=1, + smp_veto=1, + ts2_veto=1, + hz50_veto=1, + veto_0=1, + veto_1=1, + veto_2=1, + veto_3=1, + fermi_veto=1, + fc_delay=1, + fc_width=1, + muon_ms_mode=1, + muon_cherenkov_pulse=1, + veto_0_name="test", + veto_1_name="test1", + ) - def _dummy_plan_which_sets_wiring_sepectra_detector(dae): - yield from bps.mv(dae.dae_settings, modified_settings) + modified_settings_xml = dae_settings_template.format( + wiring_table=modified_settings.wiring_filepath, + detector_table=modified_settings.detector_filepath, + spectra_table=modified_settings.spectra_filepath, + mon_spec=1, + from_=1, + to=1, + timing_src=1, + smp_veto=1, + ts2_veto=1, + hz50_veto=1, + veto_0=1, + veto_1=1, + veto_2=1, + veto_3=1, + fermi_veto=1, + fc_delay=1, + fc_width=1, + muon_ms_mode=1, + muon_cherenkov_pulse=1, + veto_0_name="test", + veto_1_name="test1", + ) - current = yield from bps.rd(dae.dae_settings) - assert current.wiring_filepath == modified_settings.wiring_filepath - assert current.spectra_filepath == modified_settings.spectra_filepath - assert current.detector_filepath == modified_settings.detector_filepath + set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): RE( with_dae_tables( - _dummy_plan_which_sets_wiring_sepectra_detector(dae), - dae=dae, # type: ignore + bps.null(), + dae=dae, + new_settings=modified_settings ) ) - after: DaeSettingsData = RE(bps.rd(dae.dae_settings)).plan_result # type: ignore + mock_set_calls = get_mock_put(dae.dae_settings._raw_dae_settings).call_args_list + + assert ET.canonicalize(modified_settings_xml) == ET.canonicalize(mock_set_calls[0].args[0]) + assert ET.canonicalize(original_settings) == ET.canonicalize(mock_set_calls[1].args[0]) - assert after == before - assert after.wiring_filepath.endswith(expected_wiring) # type: ignore - assert after.spectra_filepath.endswith(expected_spectra) # type: ignore - assert after.detector_filepath.endswith(expected_detector) # type: ignore From dba1ce28613eca59e33800c6683714e0705f3203 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 11:45:17 +0000 Subject: [PATCH 14/21] updated wrapper tests --- tests/devices/test_dae.py | 287 +++++++++++++++++++++++++++++++++++--- 1 file changed, 271 insertions(+), 16 deletions(-) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index a71af73c..207b772a 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -38,7 +38,7 @@ _set_value_in_dae_xml, ) from ibex_bluesky_core.devices.dae._period_settings import _convert_period_settings_to_xml -from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml +from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml, _convert_xml_to_tcb_settings from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels @@ -1048,37 +1048,292 @@ def test_num_periods_wrapper(dae: Dae, RE: RunEngine): assert mock_set_calls[1].args == original_settings -def test_time_channels_wrapper(dae: Dae, RE: RunEngine): - original_settings = compress_and_hex(initial_tcb_settings).decode() +async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): + expected_tcb_file = "C:\\tcb.dat" + expected_calc_method = TCBCalculationMethod.SPECIFY_PARAMETERS + expected_time_unit = TCBTimeUnit.MICROSECONDS + + modified_settings = DaeTCBSettingsData(time_unit=TCBTimeUnit.NANOSECONDS) - expected_time_units = TCBTimeUnit.MICROSECONDS - set_mock_value(dae.tcb_settings._raw_tcb_settings, original_settings) + original_tcb_settings = tcb_settings_template.format( + tcb_file=expected_tcb_file, + time_units=expected_time_unit.value, + calc_method=expected_calc_method.value, + tr1_mode_1=1, + tr1_from_1=1, + tr1_to_1=1, + tr1_steps_1=1, + tr1_mode_2=1, + tr1_from_2=1, + tr1_to_2=1, + tr1_steps_2=1, + tr1_mode_3=1, + tr1_from_3=1, + tr1_to_3=1, + tr1_steps_3=1, + tr1_mode_4=1, + tr1_from_4=1, + tr1_to_4=1, + tr1_steps_4=1, + tr1_mode_5=1, + tr1_from_5=1, + tr1_to_5=1, + tr1_steps_5=1, + tr2_mode_1=1, + tr2_from_1=1, + tr2_to_1=1, + tr2_steps_1=1, + tr2_mode_2=1, + tr2_from_2=1, + tr2_to_2=1, + tr2_steps_2=1, + tr2_mode_3=1, + tr2_from_3=1, + tr2_to_3=1, + tr2_steps_3=1, + tr2_mode_4=1, + tr2_from_4=1, + tr2_to_4=1, + tr2_steps_4=1, + tr2_mode_5=1, + tr2_from_5=1, + tr2_to_5=1, + tr2_steps_5=1, + tr3_mode_1=1, + tr3_from_1=1, + tr3_to_1=1, + tr3_steps_1=1, + tr3_mode_2=1, + tr3_from_2=1, + tr3_to_2=1, + tr3_steps_2=1, + tr3_mode_3=1, + tr3_from_3=1, + tr3_to_3=1, + tr3_steps_3=1, + tr3_mode_4=1, + tr3_from_4=1, + tr3_to_4=1, + tr3_steps_4=1, + tr3_mode_5=1, + tr3_from_5=1, + tr3_to_5=1, + tr3_steps_5=1, + tr4_mode_1=1, + tr4_from_1=1, + tr4_to_1=1, + tr4_steps_1=1, + tr4_mode_2=1, + tr4_from_2=1, + tr4_to_2=1, + tr4_steps_2=1, + tr4_mode_3=1, + tr4_from_3=1, + tr4_to_3=1, + tr4_steps_3=1, + tr4_mode_4=1, + tr4_from_4=1, + tr4_to_4=1, + tr4_steps_4=1, + tr4_mode_5=1, + tr4_from_5=1, + tr4_to_5=1, + tr4_steps_5=1, + tr5_mode_1=1, + tr5_from_1=1, + tr5_to_1=1, + tr5_steps_1=1, + tr5_mode_2=1, + tr5_from_2=1, + tr5_to_2=1, + tr5_steps_2=1, + tr5_mode_3=1, + tr5_from_3=1, + tr5_to_3=1, + tr5_steps_3=1, + tr5_mode_4=1, + tr5_from_4=1, + tr5_to_4=1, + tr5_steps_4=1, + tr5_mode_5=1, + tr5_from_5=1, + tr5_to_5=1, + tr5_steps_5=1, + tr6_mode_1=1, + tr6_from_1=1, + tr6_to_1=1, + tr6_steps_1=1, + tr6_mode_2=1, + tr6_from_2=1, + tr6_to_2=1, + tr6_steps_2=1, + tr6_mode_3=1, + tr6_from_3=1, + tr6_to_3=1, + tr6_steps_3=1, + tr6_mode_4=1, + tr6_from_4=1, + tr6_to_4=1, + tr6_steps_4=1, + tr6_mode_5=1, + tr6_from_5=1, + tr6_to_5=1, + tr6_steps_5=1, + ) - before: DaeTCBSettingsData = RE(bps.rd(dae.tcb_settings)).plan_result # type: ignore - def _dummy_plan_which_sets_time_units(dae): - yield from bps.mv(dae.tcb_settings, modified_settings) + modified_raw_tcb_settings = tcb_settings_template.format( + tcb_file=expected_tcb_file, + time_units=TCBTimeUnit.NANOSECONDS.value, + calc_method=expected_calc_method.value, + tr1_mode_1=1, + tr1_from_1=1, + tr1_to_1=1, + tr1_steps_1=1, + tr1_mode_2=1, + tr1_from_2=1, + tr1_to_2=1, + tr1_steps_2=1, + tr1_mode_3=1, + tr1_from_3=1, + tr1_to_3=1, + tr1_steps_3=1, + tr1_mode_4=1, + tr1_from_4=1, + tr1_to_4=1, + tr1_steps_4=1, + tr1_mode_5=1, + tr1_from_5=1, + tr1_to_5=1, + tr1_steps_5=1, + tr2_mode_1=1, + tr2_from_1=1, + tr2_to_1=1, + tr2_steps_1=1, + tr2_mode_2=1, + tr2_from_2=1, + tr2_to_2=1, + tr2_steps_2=1, + tr2_mode_3=1, + tr2_from_3=1, + tr2_to_3=1, + tr2_steps_3=1, + tr2_mode_4=1, + tr2_from_4=1, + tr2_to_4=1, + tr2_steps_4=1, + tr2_mode_5=1, + tr2_from_5=1, + tr2_to_5=1, + tr2_steps_5=1, + tr3_mode_1=1, + tr3_from_1=1, + tr3_to_1=1, + tr3_steps_1=1, + tr3_mode_2=1, + tr3_from_2=1, + tr3_to_2=1, + tr3_steps_2=1, + tr3_mode_3=1, + tr3_from_3=1, + tr3_to_3=1, + tr3_steps_3=1, + tr3_mode_4=1, + tr3_from_4=1, + tr3_to_4=1, + tr3_steps_4=1, + tr3_mode_5=1, + tr3_from_5=1, + tr3_to_5=1, + tr3_steps_5=1, + tr4_mode_1=1, + tr4_from_1=1, + tr4_to_1=1, + tr4_steps_1=1, + tr4_mode_2=1, + tr4_from_2=1, + tr4_to_2=1, + tr4_steps_2=1, + tr4_mode_3=1, + tr4_from_3=1, + tr4_to_3=1, + tr4_steps_3=1, + tr4_mode_4=1, + tr4_from_4=1, + tr4_to_4=1, + tr4_steps_4=1, + tr4_mode_5=1, + tr4_from_5=1, + tr4_to_5=1, + tr4_steps_5=1, + tr5_mode_1=1, + tr5_from_1=1, + tr5_to_1=1, + tr5_steps_1=1, + tr5_mode_2=1, + tr5_from_2=1, + tr5_to_2=1, + tr5_steps_2=1, + tr5_mode_3=1, + tr5_from_3=1, + tr5_to_3=1, + tr5_steps_3=1, + tr5_mode_4=1, + tr5_from_4=1, + tr5_to_4=1, + tr5_steps_4=1, + tr5_mode_5=1, + tr5_from_5=1, + tr5_to_5=1, + tr5_steps_5=1, + tr6_mode_1=1, + tr6_from_1=1, + tr6_to_1=1, + tr6_steps_1=1, + tr6_mode_2=1, + tr6_from_2=1, + tr6_to_2=1, + tr6_steps_2=1, + tr6_mode_3=1, + tr6_from_3=1, + tr6_to_3=1, + tr6_steps_3=1, + tr6_mode_4=1, + tr6_from_4=1, + tr6_to_4=1, + tr6_steps_4=1, + tr6_mode_5=1, + tr6_from_5=1, + tr6_to_5=1, + tr6_steps_5=1, + ) - current = yield from bps.rd(dae.tcb_settings) - assert current.time_unit == modified_settings.time_unit + set_mock_value(dae.tcb_settings._raw_tcb_settings, compress_and_hex(original_tcb_settings).decode()) with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): RE( with_time_channels( - _dummy_plan_which_sets_time_units(dae), - dae=dae, # type: ignore + bps.null(), + dae=dae, + new_tcb_settings=modified_settings ) ) - after: DaeTCBSettingsData = RE(bps.rd(dae.tcb_settings)).plan_result # type: ignore - assert after == before - assert after.time_unit == expected_time_units + mock_set_calls = get_mock_put(dae.tcb_settings._raw_tcb_settings).call_args_list + + # Note for these two assertions that you can't compare XML directly as order isn't guaranteed, + # so convert to the dataclass instead. + + # assert that modified settings are set + assert _convert_xml_to_tcb_settings(modified_raw_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[0].args[0]).decode()) + + # assert that the original settings are restored + assert _convert_xml_to_tcb_settings(original_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[1].args[0]).decode()) def test_dae_table_wrapper(dae: Dae, RE: RunEngine): - original_settings = initial_dae_settings modified_settings = DaeSettingsData( wiring_filepath="C:\\somefile.dat", spectra_filepath="C:\\anotherfile.dat", From 23afffff08551324fb3f70788f0d3bb0cfb86d1d Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 11:55:51 +0000 Subject: [PATCH 15/21] refactoring of tests --- tests/conftest.py | 8 + tests/devices/test_dae.py | 392 +------------------------------------- tests/test_plan_stubs.py | 387 ++++++++++++++++++++++++++++++++++++- 3 files changed, 397 insertions(+), 390 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4dd4a0b9..d7b6d5b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import pytest from bluesky.run_engine import RunEngine +from ibex_bluesky_core.devices.dae import Dae from ibex_bluesky_core.devices.simpledae import Controller, Reducer, SimpleDae, Waiter from ibex_bluesky_core.run_engine import get_run_engine @@ -36,3 +37,10 @@ async def simpledae() -> SimpleDae: ) await dae.connect(mock=True) return dae + + +@pytest.fixture +async def dae() -> Dae: + dae = Dae("UNITTEST:MOCK:") + await dae.connect(mock=True) + return dae diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 207b772a..67da24b8 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -1,6 +1,6 @@ # pyright: reportMissingParameterType=false from enum import Enum -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from xml.etree import ElementTree as ET import bluesky.plan_stubs as bps @@ -38,11 +38,8 @@ _set_value_in_dae_xml, ) from ibex_bluesky_core.devices.dae._period_settings import _convert_period_settings_to_xml -from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml, _convert_xml_to_tcb_settings -from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables -from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods -from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels -from tests.conftest import MOCK_PREFIX +from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml +from tests.conftest import MOCK_PREFIX, dae from tests.devices.dae_testing_data import ( dae_settings_template, initial_dae_settings, @@ -53,13 +50,6 @@ ) -@pytest.fixture -async def dae() -> Dae: - dae = Dae("UNITTEST:MOCK:") - await dae.connect(mock=True) - return dae - - @pytest.fixture async def spectrum() -> DaeSpectra: spectrum = DaeSpectra(dae_prefix="UNITTEST:MOCK:", spectra=1, period=1) @@ -1028,379 +1018,3 @@ def test_dae_repr(): assert repr(Dae(prefix="foo", name="bar")) == "Dae(name=bar, prefix=foo)" -def test_num_periods_wrapper(dae: Dae, RE: RunEngine): - original_settings = 4 - - set_mock_value(dae.number_of_periods.signal, original_settings) - - with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): - RE( - with_num_periods( - bps.null(), - dae=dae, - number_of_periods=80, - ) - ) - - mock_set_calls = get_mock_put(dae.number_of_periods.signal).call_args_list - - assert mock_set_calls[0].args == 80 - assert mock_set_calls[1].args == original_settings - - -async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): - expected_tcb_file = "C:\\tcb.dat" - expected_calc_method = TCBCalculationMethod.SPECIFY_PARAMETERS - expected_time_unit = TCBTimeUnit.MICROSECONDS - - - modified_settings = DaeTCBSettingsData(time_unit=TCBTimeUnit.NANOSECONDS) - - - original_tcb_settings = tcb_settings_template.format( - tcb_file=expected_tcb_file, - time_units=expected_time_unit.value, - calc_method=expected_calc_method.value, - tr1_mode_1=1, - tr1_from_1=1, - tr1_to_1=1, - tr1_steps_1=1, - tr1_mode_2=1, - tr1_from_2=1, - tr1_to_2=1, - tr1_steps_2=1, - tr1_mode_3=1, - tr1_from_3=1, - tr1_to_3=1, - tr1_steps_3=1, - tr1_mode_4=1, - tr1_from_4=1, - tr1_to_4=1, - tr1_steps_4=1, - tr1_mode_5=1, - tr1_from_5=1, - tr1_to_5=1, - tr1_steps_5=1, - tr2_mode_1=1, - tr2_from_1=1, - tr2_to_1=1, - tr2_steps_1=1, - tr2_mode_2=1, - tr2_from_2=1, - tr2_to_2=1, - tr2_steps_2=1, - tr2_mode_3=1, - tr2_from_3=1, - tr2_to_3=1, - tr2_steps_3=1, - tr2_mode_4=1, - tr2_from_4=1, - tr2_to_4=1, - tr2_steps_4=1, - tr2_mode_5=1, - tr2_from_5=1, - tr2_to_5=1, - tr2_steps_5=1, - tr3_mode_1=1, - tr3_from_1=1, - tr3_to_1=1, - tr3_steps_1=1, - tr3_mode_2=1, - tr3_from_2=1, - tr3_to_2=1, - tr3_steps_2=1, - tr3_mode_3=1, - tr3_from_3=1, - tr3_to_3=1, - tr3_steps_3=1, - tr3_mode_4=1, - tr3_from_4=1, - tr3_to_4=1, - tr3_steps_4=1, - tr3_mode_5=1, - tr3_from_5=1, - tr3_to_5=1, - tr3_steps_5=1, - tr4_mode_1=1, - tr4_from_1=1, - tr4_to_1=1, - tr4_steps_1=1, - tr4_mode_2=1, - tr4_from_2=1, - tr4_to_2=1, - tr4_steps_2=1, - tr4_mode_3=1, - tr4_from_3=1, - tr4_to_3=1, - tr4_steps_3=1, - tr4_mode_4=1, - tr4_from_4=1, - tr4_to_4=1, - tr4_steps_4=1, - tr4_mode_5=1, - tr4_from_5=1, - tr4_to_5=1, - tr4_steps_5=1, - tr5_mode_1=1, - tr5_from_1=1, - tr5_to_1=1, - tr5_steps_1=1, - tr5_mode_2=1, - tr5_from_2=1, - tr5_to_2=1, - tr5_steps_2=1, - tr5_mode_3=1, - tr5_from_3=1, - tr5_to_3=1, - tr5_steps_3=1, - tr5_mode_4=1, - tr5_from_4=1, - tr5_to_4=1, - tr5_steps_4=1, - tr5_mode_5=1, - tr5_from_5=1, - tr5_to_5=1, - tr5_steps_5=1, - tr6_mode_1=1, - tr6_from_1=1, - tr6_to_1=1, - tr6_steps_1=1, - tr6_mode_2=1, - tr6_from_2=1, - tr6_to_2=1, - tr6_steps_2=1, - tr6_mode_3=1, - tr6_from_3=1, - tr6_to_3=1, - tr6_steps_3=1, - tr6_mode_4=1, - tr6_from_4=1, - tr6_to_4=1, - tr6_steps_4=1, - tr6_mode_5=1, - tr6_from_5=1, - tr6_to_5=1, - tr6_steps_5=1, - ) - - - modified_raw_tcb_settings = tcb_settings_template.format( - tcb_file=expected_tcb_file, - time_units=TCBTimeUnit.NANOSECONDS.value, - calc_method=expected_calc_method.value, - tr1_mode_1=1, - tr1_from_1=1, - tr1_to_1=1, - tr1_steps_1=1, - tr1_mode_2=1, - tr1_from_2=1, - tr1_to_2=1, - tr1_steps_2=1, - tr1_mode_3=1, - tr1_from_3=1, - tr1_to_3=1, - tr1_steps_3=1, - tr1_mode_4=1, - tr1_from_4=1, - tr1_to_4=1, - tr1_steps_4=1, - tr1_mode_5=1, - tr1_from_5=1, - tr1_to_5=1, - tr1_steps_5=1, - tr2_mode_1=1, - tr2_from_1=1, - tr2_to_1=1, - tr2_steps_1=1, - tr2_mode_2=1, - tr2_from_2=1, - tr2_to_2=1, - tr2_steps_2=1, - tr2_mode_3=1, - tr2_from_3=1, - tr2_to_3=1, - tr2_steps_3=1, - tr2_mode_4=1, - tr2_from_4=1, - tr2_to_4=1, - tr2_steps_4=1, - tr2_mode_5=1, - tr2_from_5=1, - tr2_to_5=1, - tr2_steps_5=1, - tr3_mode_1=1, - tr3_from_1=1, - tr3_to_1=1, - tr3_steps_1=1, - tr3_mode_2=1, - tr3_from_2=1, - tr3_to_2=1, - tr3_steps_2=1, - tr3_mode_3=1, - tr3_from_3=1, - tr3_to_3=1, - tr3_steps_3=1, - tr3_mode_4=1, - tr3_from_4=1, - tr3_to_4=1, - tr3_steps_4=1, - tr3_mode_5=1, - tr3_from_5=1, - tr3_to_5=1, - tr3_steps_5=1, - tr4_mode_1=1, - tr4_from_1=1, - tr4_to_1=1, - tr4_steps_1=1, - tr4_mode_2=1, - tr4_from_2=1, - tr4_to_2=1, - tr4_steps_2=1, - tr4_mode_3=1, - tr4_from_3=1, - tr4_to_3=1, - tr4_steps_3=1, - tr4_mode_4=1, - tr4_from_4=1, - tr4_to_4=1, - tr4_steps_4=1, - tr4_mode_5=1, - tr4_from_5=1, - tr4_to_5=1, - tr4_steps_5=1, - tr5_mode_1=1, - tr5_from_1=1, - tr5_to_1=1, - tr5_steps_1=1, - tr5_mode_2=1, - tr5_from_2=1, - tr5_to_2=1, - tr5_steps_2=1, - tr5_mode_3=1, - tr5_from_3=1, - tr5_to_3=1, - tr5_steps_3=1, - tr5_mode_4=1, - tr5_from_4=1, - tr5_to_4=1, - tr5_steps_4=1, - tr5_mode_5=1, - tr5_from_5=1, - tr5_to_5=1, - tr5_steps_5=1, - tr6_mode_1=1, - tr6_from_1=1, - tr6_to_1=1, - tr6_steps_1=1, - tr6_mode_2=1, - tr6_from_2=1, - tr6_to_2=1, - tr6_steps_2=1, - tr6_mode_3=1, - tr6_from_3=1, - tr6_to_3=1, - tr6_steps_3=1, - tr6_mode_4=1, - tr6_from_4=1, - tr6_to_4=1, - tr6_steps_4=1, - tr6_mode_5=1, - tr6_from_5=1, - tr6_to_5=1, - tr6_steps_5=1, - ) - - set_mock_value(dae.tcb_settings._raw_tcb_settings, compress_and_hex(original_tcb_settings).decode()) - - with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): - RE( - with_time_channels( - bps.null(), - dae=dae, - new_tcb_settings=modified_settings - ) - ) - - mock_set_calls = get_mock_put(dae.tcb_settings._raw_tcb_settings).call_args_list - - # Note for these two assertions that you can't compare XML directly as order isn't guaranteed, - # so convert to the dataclass instead. - - # assert that modified settings are set - assert _convert_xml_to_tcb_settings(modified_raw_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[0].args[0]).decode()) - - # assert that the original settings are restored - assert _convert_xml_to_tcb_settings(original_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[1].args[0]).decode()) - - -def test_dae_table_wrapper(dae: Dae, RE: RunEngine): - modified_settings = DaeSettingsData( - wiring_filepath="C:\\somefile.dat", - spectra_filepath="C:\\anotherfile.dat", - detector_filepath="C:\\anotherfile123.dat", - ) - - original_settings = dae_settings_template.format( - wiring_table="C:\\originalfile.dat", - detector_table="C:\\anotheroriginalfile.dat", - spectra_table="C:\\originalspectrafile.dat", - mon_spec=1, - from_=1, - to=1, - timing_src=1, - smp_veto=1, - ts2_veto=1, - hz50_veto=1, - veto_0=1, - veto_1=1, - veto_2=1, - veto_3=1, - fermi_veto=1, - fc_delay=1, - fc_width=1, - muon_ms_mode=1, - muon_cherenkov_pulse=1, - veto_0_name="test", - veto_1_name="test1", - ) - - modified_settings_xml = dae_settings_template.format( - wiring_table=modified_settings.wiring_filepath, - detector_table=modified_settings.detector_filepath, - spectra_table=modified_settings.spectra_filepath, - mon_spec=1, - from_=1, - to=1, - timing_src=1, - smp_veto=1, - ts2_veto=1, - hz50_veto=1, - veto_0=1, - veto_1=1, - veto_2=1, - veto_3=1, - fermi_veto=1, - fc_delay=1, - fc_width=1, - muon_ms_mode=1, - muon_cherenkov_pulse=1, - veto_0_name="test", - veto_1_name="test1", - ) - - set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) - - with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): - RE( - with_dae_tables( - bps.null(), - dae=dae, - new_settings=modified_settings - ) - ) - - mock_set_calls = get_mock_put(dae.dae_settings._raw_dae_settings).call_args_list - - assert ET.canonicalize(modified_settings_xml) == ET.canonicalize(mock_set_calls[0].args[0]) - assert ET.canonicalize(original_settings) == ET.canonicalize(mock_set_calls[1].args[0]) - diff --git a/tests/test_plan_stubs.py b/tests/test_plan_stubs.py index d45cbafd..2daf9034 100644 --- a/tests/test_plan_stubs.py +++ b/tests/test_plan_stubs.py @@ -2,16 +2,20 @@ import time from asyncio import CancelledError from unittest.mock import MagicMock, call, patch +from xml.etree import ElementTree as ET import matplotlib.pyplot as plt import pytest +from bluesky import RunEngine, plan_stubs as bps from bluesky.utils import Msg from ophyd_async.epics.motor import UseSetMode from ophyd_async.plan_stubs import ensure_connected from ophyd_async.testing import get_mock_put, set_mock_value -from ibex_bluesky_core.devices import NoYesChoice +from ibex_bluesky_core.devices import NoYesChoice, compress_and_hex, dehex_and_decompress from ibex_bluesky_core.devices.block import BlockMot +from ibex_bluesky_core.devices.dae import Dae, TCBCalculationMethod, TCBTimeUnit, DaeTCBSettingsData, DaeSettingsData +from ibex_bluesky_core.devices.dae._tcb_settings import _convert_xml_to_tcb_settings from ibex_bluesky_core.devices.reflectometry import ReflParameter from ibex_bluesky_core.plan_stubs import ( CALL_QT_AWARE_MSG_KEY, @@ -21,7 +25,11 @@ redefine_motor, redefine_refl_parameter, ) +from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables +from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods +from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels from ibex_bluesky_core.run_engine._msg_handlers import call_sync_handler +from tests.devices.dae_testing_data import tcb_settings_template, dae_settings_template def test_call_sync_returns_result(RE): @@ -180,3 +188,380 @@ def test_get_user_input(RE): result = RE(prompt_user_for_choice(prompt="choice?", choices=["bar", "baz"])) assert result.plan_result == "bar" + + +def test_num_periods_wrapper(dae: Dae, RE: RunEngine): + original_settings = 4 + + set_mock_value(dae.number_of_periods.signal, original_settings) + + with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): + RE( + with_num_periods( + bps.null(), + dae=dae, + number_of_periods=80, + ) + ) + + mock_set_calls = get_mock_put(dae.number_of_periods.signal).call_args_list + + assert mock_set_calls[0].args[0] == 80 + assert mock_set_calls[1].args[0] == original_settings + + +async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): + expected_tcb_file = "C:\\tcb.dat" + expected_calc_method = TCBCalculationMethod.SPECIFY_PARAMETERS + expected_time_unit = TCBTimeUnit.MICROSECONDS + + + modified_settings = DaeTCBSettingsData(time_unit=TCBTimeUnit.NANOSECONDS) + + + original_tcb_settings = tcb_settings_template.format( + tcb_file=expected_tcb_file, + time_units=expected_time_unit.value, + calc_method=expected_calc_method.value, + tr1_mode_1=1, + tr1_from_1=1, + tr1_to_1=1, + tr1_steps_1=1, + tr1_mode_2=1, + tr1_from_2=1, + tr1_to_2=1, + tr1_steps_2=1, + tr1_mode_3=1, + tr1_from_3=1, + tr1_to_3=1, + tr1_steps_3=1, + tr1_mode_4=1, + tr1_from_4=1, + tr1_to_4=1, + tr1_steps_4=1, + tr1_mode_5=1, + tr1_from_5=1, + tr1_to_5=1, + tr1_steps_5=1, + tr2_mode_1=1, + tr2_from_1=1, + tr2_to_1=1, + tr2_steps_1=1, + tr2_mode_2=1, + tr2_from_2=1, + tr2_to_2=1, + tr2_steps_2=1, + tr2_mode_3=1, + tr2_from_3=1, + tr2_to_3=1, + tr2_steps_3=1, + tr2_mode_4=1, + tr2_from_4=1, + tr2_to_4=1, + tr2_steps_4=1, + tr2_mode_5=1, + tr2_from_5=1, + tr2_to_5=1, + tr2_steps_5=1, + tr3_mode_1=1, + tr3_from_1=1, + tr3_to_1=1, + tr3_steps_1=1, + tr3_mode_2=1, + tr3_from_2=1, + tr3_to_2=1, + tr3_steps_2=1, + tr3_mode_3=1, + tr3_from_3=1, + tr3_to_3=1, + tr3_steps_3=1, + tr3_mode_4=1, + tr3_from_4=1, + tr3_to_4=1, + tr3_steps_4=1, + tr3_mode_5=1, + tr3_from_5=1, + tr3_to_5=1, + tr3_steps_5=1, + tr4_mode_1=1, + tr4_from_1=1, + tr4_to_1=1, + tr4_steps_1=1, + tr4_mode_2=1, + tr4_from_2=1, + tr4_to_2=1, + tr4_steps_2=1, + tr4_mode_3=1, + tr4_from_3=1, + tr4_to_3=1, + tr4_steps_3=1, + tr4_mode_4=1, + tr4_from_4=1, + tr4_to_4=1, + tr4_steps_4=1, + tr4_mode_5=1, + tr4_from_5=1, + tr4_to_5=1, + tr4_steps_5=1, + tr5_mode_1=1, + tr5_from_1=1, + tr5_to_1=1, + tr5_steps_1=1, + tr5_mode_2=1, + tr5_from_2=1, + tr5_to_2=1, + tr5_steps_2=1, + tr5_mode_3=1, + tr5_from_3=1, + tr5_to_3=1, + tr5_steps_3=1, + tr5_mode_4=1, + tr5_from_4=1, + tr5_to_4=1, + tr5_steps_4=1, + tr5_mode_5=1, + tr5_from_5=1, + tr5_to_5=1, + tr5_steps_5=1, + tr6_mode_1=1, + tr6_from_1=1, + tr6_to_1=1, + tr6_steps_1=1, + tr6_mode_2=1, + tr6_from_2=1, + tr6_to_2=1, + tr6_steps_2=1, + tr6_mode_3=1, + tr6_from_3=1, + tr6_to_3=1, + tr6_steps_3=1, + tr6_mode_4=1, + tr6_from_4=1, + tr6_to_4=1, + tr6_steps_4=1, + tr6_mode_5=1, + tr6_from_5=1, + tr6_to_5=1, + tr6_steps_5=1, + ) + + + modified_raw_tcb_settings = tcb_settings_template.format( + tcb_file=expected_tcb_file, + time_units=TCBTimeUnit.NANOSECONDS.value, + calc_method=expected_calc_method.value, + tr1_mode_1=1, + tr1_from_1=1, + tr1_to_1=1, + tr1_steps_1=1, + tr1_mode_2=1, + tr1_from_2=1, + tr1_to_2=1, + tr1_steps_2=1, + tr1_mode_3=1, + tr1_from_3=1, + tr1_to_3=1, + tr1_steps_3=1, + tr1_mode_4=1, + tr1_from_4=1, + tr1_to_4=1, + tr1_steps_4=1, + tr1_mode_5=1, + tr1_from_5=1, + tr1_to_5=1, + tr1_steps_5=1, + tr2_mode_1=1, + tr2_from_1=1, + tr2_to_1=1, + tr2_steps_1=1, + tr2_mode_2=1, + tr2_from_2=1, + tr2_to_2=1, + tr2_steps_2=1, + tr2_mode_3=1, + tr2_from_3=1, + tr2_to_3=1, + tr2_steps_3=1, + tr2_mode_4=1, + tr2_from_4=1, + tr2_to_4=1, + tr2_steps_4=1, + tr2_mode_5=1, + tr2_from_5=1, + tr2_to_5=1, + tr2_steps_5=1, + tr3_mode_1=1, + tr3_from_1=1, + tr3_to_1=1, + tr3_steps_1=1, + tr3_mode_2=1, + tr3_from_2=1, + tr3_to_2=1, + tr3_steps_2=1, + tr3_mode_3=1, + tr3_from_3=1, + tr3_to_3=1, + tr3_steps_3=1, + tr3_mode_4=1, + tr3_from_4=1, + tr3_to_4=1, + tr3_steps_4=1, + tr3_mode_5=1, + tr3_from_5=1, + tr3_to_5=1, + tr3_steps_5=1, + tr4_mode_1=1, + tr4_from_1=1, + tr4_to_1=1, + tr4_steps_1=1, + tr4_mode_2=1, + tr4_from_2=1, + tr4_to_2=1, + tr4_steps_2=1, + tr4_mode_3=1, + tr4_from_3=1, + tr4_to_3=1, + tr4_steps_3=1, + tr4_mode_4=1, + tr4_from_4=1, + tr4_to_4=1, + tr4_steps_4=1, + tr4_mode_5=1, + tr4_from_5=1, + tr4_to_5=1, + tr4_steps_5=1, + tr5_mode_1=1, + tr5_from_1=1, + tr5_to_1=1, + tr5_steps_1=1, + tr5_mode_2=1, + tr5_from_2=1, + tr5_to_2=1, + tr5_steps_2=1, + tr5_mode_3=1, + tr5_from_3=1, + tr5_to_3=1, + tr5_steps_3=1, + tr5_mode_4=1, + tr5_from_4=1, + tr5_to_4=1, + tr5_steps_4=1, + tr5_mode_5=1, + tr5_from_5=1, + tr5_to_5=1, + tr5_steps_5=1, + tr6_mode_1=1, + tr6_from_1=1, + tr6_to_1=1, + tr6_steps_1=1, + tr6_mode_2=1, + tr6_from_2=1, + tr6_to_2=1, + tr6_steps_2=1, + tr6_mode_3=1, + tr6_from_3=1, + tr6_to_3=1, + tr6_steps_3=1, + tr6_mode_4=1, + tr6_from_4=1, + tr6_to_4=1, + tr6_steps_4=1, + tr6_mode_5=1, + tr6_from_5=1, + tr6_to_5=1, + tr6_steps_5=1, + ) + + set_mock_value(dae.tcb_settings._raw_tcb_settings, compress_and_hex(original_tcb_settings).decode()) + + with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): + RE( + with_time_channels( + bps.null(), + dae=dae, + new_tcb_settings=modified_settings + ) + ) + + mock_set_calls = get_mock_put(dae.tcb_settings._raw_tcb_settings).call_args_list + + # Note for these two assertions that you can't compare XML directly as order isn't guaranteed, + # so convert to the dataclass instead. + + # assert that modified settings are set + assert _convert_xml_to_tcb_settings(modified_raw_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[0].args[0]).decode()) + + # assert that the original settings are restored + assert _convert_xml_to_tcb_settings(original_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[1].args[0]).decode()) + + +def test_dae_table_wrapper(dae: Dae, RE: RunEngine): + modified_settings = DaeSettingsData( + wiring_filepath="C:\\somefile.dat", + spectra_filepath="C:\\anotherfile.dat", + detector_filepath="C:\\anotherfile123.dat", + ) + + original_settings = dae_settings_template.format( + wiring_table="C:\\originalfile.dat", + detector_table="C:\\anotheroriginalfile.dat", + spectra_table="C:\\originalspectrafile.dat", + mon_spec=1, + from_=1, + to=1, + timing_src=1, + smp_veto=1, + ts2_veto=1, + hz50_veto=1, + veto_0=1, + veto_1=1, + veto_2=1, + veto_3=1, + fermi_veto=1, + fc_delay=1, + fc_width=1, + muon_ms_mode=1, + muon_cherenkov_pulse=1, + veto_0_name="test", + veto_1_name="test1", + ) + + modified_settings_xml = dae_settings_template.format( + wiring_table=modified_settings.wiring_filepath, + detector_table=modified_settings.detector_filepath, + spectra_table=modified_settings.spectra_filepath, + mon_spec=1, + from_=1, + to=1, + timing_src=1, + smp_veto=1, + ts2_veto=1, + hz50_veto=1, + veto_0=1, + veto_1=1, + veto_2=1, + veto_3=1, + fermi_veto=1, + fc_delay=1, + fc_width=1, + muon_ms_mode=1, + muon_cherenkov_pulse=1, + veto_0_name="test", + veto_1_name="test1", + ) + + set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) + + with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): + RE( + with_dae_tables( + bps.null(), + dae=dae, + new_settings=modified_settings + ) + ) + + mock_set_calls = get_mock_put(dae.dae_settings._raw_dae_settings).call_args_list + + assert ET.canonicalize(modified_settings_xml) == ET.canonicalize(mock_set_calls[0].args[0]) + assert ET.canonicalize(original_settings) == ET.canonicalize(mock_set_calls[1].args[0]) From 4432835a6ed6c4f65a4a0ef00fda52c3712977f8 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 15:27:29 +0000 Subject: [PATCH 16/21] ruff checks --- .../devices/dae/_period_settings.py | 6 ++- .../plan_stubs/num_periods_wrapper.py | 5 +- .../plan_stubs/time_channels_wrapper.py | 1 - tests/devices/test_dae.py | 4 +- tests/test_plan_stubs.py | 46 +++++++++---------- 5 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/ibex_bluesky_core/devices/dae/_period_settings.py b/src/ibex_bluesky_core/devices/dae/_period_settings.py index d4dadd57..373a7922 100644 --- a/src/ibex_bluesky_core/devices/dae/_period_settings.py +++ b/src/ibex_bluesky_core/devices/dae/_period_settings.py @@ -7,7 +7,7 @@ from xml.etree.ElementTree import tostring from bluesky.protocols import Locatable, Location, Movable -from ophyd_async.core import AsyncStatus, StandardReadable, SignalRW +from ophyd_async.core import AsyncStatus, SignalRW, StandardReadable from ibex_bluesky_core.devices import ( isis_epics_signal_rw, @@ -116,7 +116,9 @@ def _convert_period_settings_to_xml(current_xml: str, value: DaePeriodSettingsDa return tostring(root, encoding="unicode") -class DaePeriodSettings(StandardReadable, Locatable[DaePeriodSettingsData], Movable[DaePeriodSettingsData]): +class DaePeriodSettings( + StandardReadable, Locatable[DaePeriodSettingsData], Movable[DaePeriodSettingsData] +): """Subdevice for the DAE hardware period settings.""" def __init__(self, dae_prefix: str, name: str = "") -> None: diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index cd52df94..217ec1ad 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -1,13 +1,13 @@ """Wrap a plan with temporary modification to Periods Settings.""" -import copy from collections.abc import Generator import bluesky.plan_stubs as bps import bluesky.preprocessors as bpp from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected -from ibex_bluesky_core.devices.dae import DaePeriodSettings, Dae + +from ibex_bluesky_core.devices.dae import Dae def with_num_periods( @@ -25,7 +25,6 @@ def with_num_periods( number of periods afterwards. """ - original_num_periods = None def _inner() -> Generator[Msg, None, None]: diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index 39b2a63e..ff152d47 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -7,7 +7,6 @@ from bluesky.utils import Msg from ophyd_async.plan_stubs import ensure_connected - from ibex_bluesky_core.devices.dae import Dae, DaeTCBSettingsData diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 67da24b8..84cad7e1 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -39,7 +39,7 @@ ) from ibex_bluesky_core.devices.dae._period_settings import _convert_period_settings_to_xml from ibex_bluesky_core.devices.dae._tcb_settings import _convert_tcb_settings_to_xml -from tests.conftest import MOCK_PREFIX, dae +from tests.conftest import MOCK_PREFIX from tests.devices.dae_testing_data import ( dae_settings_template, initial_dae_settings, @@ -1016,5 +1016,3 @@ async def test_if_tof_edges_has_no_units_then_read_spec_dataarray_gives_error( def test_dae_repr(): assert repr(Dae(prefix="foo", name="bar")) == "Dae(name=bar, prefix=foo)" - - diff --git a/tests/test_plan_stubs.py b/tests/test_plan_stubs.py index 2daf9034..188f6fde 100644 --- a/tests/test_plan_stubs.py +++ b/tests/test_plan_stubs.py @@ -6,7 +6,8 @@ import matplotlib.pyplot as plt import pytest -from bluesky import RunEngine, plan_stubs as bps +from bluesky import RunEngine +from bluesky import plan_stubs as bps from bluesky.utils import Msg from ophyd_async.epics.motor import UseSetMode from ophyd_async.plan_stubs import ensure_connected @@ -14,7 +15,13 @@ from ibex_bluesky_core.devices import NoYesChoice, compress_and_hex, dehex_and_decompress from ibex_bluesky_core.devices.block import BlockMot -from ibex_bluesky_core.devices.dae import Dae, TCBCalculationMethod, TCBTimeUnit, DaeTCBSettingsData, DaeSettingsData +from ibex_bluesky_core.devices.dae import ( + Dae, + DaeSettingsData, + DaeTCBSettingsData, + TCBCalculationMethod, + TCBTimeUnit, +) from ibex_bluesky_core.devices.dae._tcb_settings import _convert_xml_to_tcb_settings from ibex_bluesky_core.devices.reflectometry import ReflParameter from ibex_bluesky_core.plan_stubs import ( @@ -29,7 +36,7 @@ from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels from ibex_bluesky_core.run_engine._msg_handlers import call_sync_handler -from tests.devices.dae_testing_data import tcb_settings_template, dae_settings_template +from tests.devices.dae_testing_data import dae_settings_template, tcb_settings_template def test_call_sync_returns_result(RE): @@ -210,15 +217,13 @@ def test_num_periods_wrapper(dae: Dae, RE: RunEngine): assert mock_set_calls[1].args[0] == original_settings -async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): +def test_time_channels_wrapper(dae: Dae, RE: RunEngine): expected_tcb_file = "C:\\tcb.dat" expected_calc_method = TCBCalculationMethod.SPECIFY_PARAMETERS expected_time_unit = TCBTimeUnit.MICROSECONDS - modified_settings = DaeTCBSettingsData(time_unit=TCBTimeUnit.NANOSECONDS) - original_tcb_settings = tcb_settings_template.format( tcb_file=expected_tcb_file, time_units=expected_time_unit.value, @@ -345,7 +350,6 @@ async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): tr6_steps_5=1, ) - modified_raw_tcb_settings = tcb_settings_template.format( tcb_file=expected_tcb_file, time_units=TCBTimeUnit.NANOSECONDS.value, @@ -472,16 +476,12 @@ async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): tr6_steps_5=1, ) - set_mock_value(dae.tcb_settings._raw_tcb_settings, compress_and_hex(original_tcb_settings).decode()) + set_mock_value( + dae.tcb_settings._raw_tcb_settings, compress_and_hex(original_tcb_settings).decode() + ) with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): - RE( - with_time_channels( - bps.null(), - dae=dae, - new_tcb_settings=modified_settings - ) - ) + RE(with_time_channels(bps.null(), dae=dae, new_tcb_settings=modified_settings)) mock_set_calls = get_mock_put(dae.tcb_settings._raw_tcb_settings).call_args_list @@ -489,10 +489,14 @@ async def test_time_channels_wrapper(dae: Dae, RE: RunEngine): # so convert to the dataclass instead. # assert that modified settings are set - assert _convert_xml_to_tcb_settings(modified_raw_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[0].args[0]).decode()) + assert _convert_xml_to_tcb_settings(modified_raw_tcb_settings) == _convert_xml_to_tcb_settings( + dehex_and_decompress(mock_set_calls[0].args[0]).decode() + ) # assert that the original settings are restored - assert _convert_xml_to_tcb_settings(original_tcb_settings) == _convert_xml_to_tcb_settings(dehex_and_decompress(mock_set_calls[1].args[0]).decode()) + assert _convert_xml_to_tcb_settings(original_tcb_settings) == _convert_xml_to_tcb_settings( + dehex_and_decompress(mock_set_calls[1].args[0]).decode() + ) def test_dae_table_wrapper(dae: Dae, RE: RunEngine): @@ -553,13 +557,7 @@ def test_dae_table_wrapper(dae: Dae, RE: RunEngine): set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): - RE( - with_dae_tables( - bps.null(), - dae=dae, - new_settings=modified_settings - ) - ) + RE(with_dae_tables(bps.null(), dae=dae, new_settings=modified_settings)) mock_set_calls = get_mock_put(dae.dae_settings._raw_dae_settings).call_args_list From 161818ed432686dca77cc8861f0b9942551a934e Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 16:33:02 +0000 Subject: [PATCH 17/21] ruff checks --- src/ibex_bluesky_core/plan_stubs/__init__.py | 7 +++++++ src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py | 2 +- .../plan_stubs/num_periods_wrapper.py | 2 +- .../plan_stubs/time_channels_wrapper.py | 2 +- tests/test_plan_stubs.py | 10 ++++------ 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/__init__.py b/src/ibex_bluesky_core/plan_stubs/__init__.py index d811f8fa..73b1077d 100644 --- a/src/ibex_bluesky_core/plan_stubs/__init__.py +++ b/src/ibex_bluesky_core/plan_stubs/__init__.py @@ -14,6 +14,10 @@ from ibex_bluesky_core.devices.reflectometry import ReflParameter from ibex_bluesky_core.utils import NamedReadableAndMovable +from ibex_bluesky_core.plan_stubs.dae_table_wrapper import _with_dae_tables +from ibex_bluesky_core.plan_stubs.num_periods_wrapper import _with_num_periods +from ibex_bluesky_core.plan_stubs.time_channels_wrapper import _with_time_channels + logger = logging.getLogger(__name__) P = ParamSpec("P") @@ -31,6 +35,9 @@ "prompt_user_for_choice", "redefine_motor", "redefine_refl_parameter", + "_with_dae_tables", + "_with_num_periods", + "_with_time_channels", ] diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 3eb4b0c6..bc6dec70 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae, DaeSettingsData -def with_dae_tables( +def _with_dae_tables( plan: Generator[Msg, None, None], dae: Dae, new_settings: DaeSettingsData ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index 217ec1ad..41169adc 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae -def with_num_periods( +def _with_num_periods( plan: Generator[Msg, None, None], dae: Dae, number_of_periods: int ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index ff152d47..9da13014 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae, DaeTCBSettingsData -def with_time_channels( +def _with_time_channels( plan: Generator[Msg, None, None], dae: Dae, new_tcb_settings: DaeTCBSettingsData ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. diff --git a/tests/test_plan_stubs.py b/tests/test_plan_stubs.py index 188f6fde..80760117 100644 --- a/tests/test_plan_stubs.py +++ b/tests/test_plan_stubs.py @@ -32,9 +32,7 @@ redefine_motor, redefine_refl_parameter, ) -from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables -from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods -from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels +from ibex_bluesky_core.plan_stubs import _with_dae_tables, _with_num_periods, _with_time_channels from ibex_bluesky_core.run_engine._msg_handlers import call_sync_handler from tests.devices.dae_testing_data import dae_settings_template, tcb_settings_template @@ -204,7 +202,7 @@ def test_num_periods_wrapper(dae: Dae, RE: RunEngine): with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): RE( - with_num_periods( + _with_num_periods( bps.null(), dae=dae, number_of_periods=80, @@ -481,7 +479,7 @@ def test_time_channels_wrapper(dae: Dae, RE: RunEngine): ) with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): - RE(with_time_channels(bps.null(), dae=dae, new_tcb_settings=modified_settings)) + RE(_with_time_channels(bps.null(), dae=dae, new_tcb_settings=modified_settings)) mock_set_calls = get_mock_put(dae.tcb_settings._raw_tcb_settings).call_args_list @@ -557,7 +555,7 @@ def test_dae_table_wrapper(dae: Dae, RE: RunEngine): set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): - RE(with_dae_tables(bps.null(), dae=dae, new_settings=modified_settings)) + RE(_with_dae_tables(bps.null(), dae=dae, new_settings=modified_settings)) mock_set_calls = get_mock_put(dae.dae_settings._raw_dae_settings).call_args_list From c31c38864439980fc556345a743712da1c43d14b Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 16:33:02 +0000 Subject: [PATCH 18/21] exposed wrapper files to __all__ --- src/ibex_bluesky_core/plan_stubs/__init__.py | 7 +++++++ src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py | 2 +- .../plan_stubs/num_periods_wrapper.py | 2 +- .../plan_stubs/time_channels_wrapper.py | 2 +- tests/test_plan_stubs.py | 10 ++++------ 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/__init__.py b/src/ibex_bluesky_core/plan_stubs/__init__.py index d811f8fa..73b1077d 100644 --- a/src/ibex_bluesky_core/plan_stubs/__init__.py +++ b/src/ibex_bluesky_core/plan_stubs/__init__.py @@ -14,6 +14,10 @@ from ibex_bluesky_core.devices.reflectometry import ReflParameter from ibex_bluesky_core.utils import NamedReadableAndMovable +from ibex_bluesky_core.plan_stubs.dae_table_wrapper import _with_dae_tables +from ibex_bluesky_core.plan_stubs.num_periods_wrapper import _with_num_periods +from ibex_bluesky_core.plan_stubs.time_channels_wrapper import _with_time_channels + logger = logging.getLogger(__name__) P = ParamSpec("P") @@ -31,6 +35,9 @@ "prompt_user_for_choice", "redefine_motor", "redefine_refl_parameter", + "_with_dae_tables", + "_with_num_periods", + "_with_time_channels", ] diff --git a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py index 3eb4b0c6..bc6dec70 100644 --- a/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/dae_table_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae, DaeSettingsData -def with_dae_tables( +def _with_dae_tables( plan: Generator[Msg, None, None], dae: Dae, new_settings: DaeSettingsData ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to DAE Settings. diff --git a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py index 217ec1ad..41169adc 100644 --- a/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/num_periods_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae -def with_num_periods( +def _with_num_periods( plan: Generator[Msg, None, None], dae: Dae, number_of_periods: int ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Periods Settings. diff --git a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py index ff152d47..9da13014 100644 --- a/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py +++ b/src/ibex_bluesky_core/plan_stubs/time_channels_wrapper.py @@ -10,7 +10,7 @@ from ibex_bluesky_core.devices.dae import Dae, DaeTCBSettingsData -def with_time_channels( +def _with_time_channels( plan: Generator[Msg, None, None], dae: Dae, new_tcb_settings: DaeTCBSettingsData ) -> Generator[Msg, None, None]: """Wrap a plan with temporary modification to Time Channel Settings. diff --git a/tests/test_plan_stubs.py b/tests/test_plan_stubs.py index 188f6fde..80760117 100644 --- a/tests/test_plan_stubs.py +++ b/tests/test_plan_stubs.py @@ -32,9 +32,7 @@ redefine_motor, redefine_refl_parameter, ) -from ibex_bluesky_core.plan_stubs.dae_table_wrapper import with_dae_tables -from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods -from ibex_bluesky_core.plan_stubs.time_channels_wrapper import with_time_channels +from ibex_bluesky_core.plan_stubs import _with_dae_tables, _with_num_periods, _with_time_channels from ibex_bluesky_core.run_engine._msg_handlers import call_sync_handler from tests.devices.dae_testing_data import dae_settings_template, tcb_settings_template @@ -204,7 +202,7 @@ def test_num_periods_wrapper(dae: Dae, RE: RunEngine): with patch("ibex_bluesky_core.plan_stubs.num_periods_wrapper.ensure_connected"): RE( - with_num_periods( + _with_num_periods( bps.null(), dae=dae, number_of_periods=80, @@ -481,7 +479,7 @@ def test_time_channels_wrapper(dae: Dae, RE: RunEngine): ) with patch("ibex_bluesky_core.plan_stubs.time_channels_wrapper.ensure_connected"): - RE(with_time_channels(bps.null(), dae=dae, new_tcb_settings=modified_settings)) + RE(_with_time_channels(bps.null(), dae=dae, new_tcb_settings=modified_settings)) mock_set_calls = get_mock_put(dae.tcb_settings._raw_tcb_settings).call_args_list @@ -557,7 +555,7 @@ def test_dae_table_wrapper(dae: Dae, RE: RunEngine): set_mock_value(dae.dae_settings._raw_dae_settings, original_settings) with patch("ibex_bluesky_core.plan_stubs.dae_table_wrapper.ensure_connected"): - RE(with_dae_tables(bps.null(), dae=dae, new_settings=modified_settings)) + RE(_with_dae_tables(bps.null(), dae=dae, new_settings=modified_settings)) mock_set_calls = get_mock_put(dae.dae_settings._raw_dae_settings).call_args_list From d8fb08b96d82639c644f578a0df2c8b4a6c830bc Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 16:37:41 +0000 Subject: [PATCH 19/21] ruff fixes --- src/ibex_bluesky_core/plan_stubs/__init__.py | 9 ++++----- tests/test_plan_stubs.py | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/__init__.py b/src/ibex_bluesky_core/plan_stubs/__init__.py index 73b1077d..625ced96 100644 --- a/src/ibex_bluesky_core/plan_stubs/__init__.py +++ b/src/ibex_bluesky_core/plan_stubs/__init__.py @@ -12,11 +12,10 @@ from ophyd_async.epics.motor import Motor, UseSetMode from ibex_bluesky_core.devices.reflectometry import ReflParameter -from ibex_bluesky_core.utils import NamedReadableAndMovable - from ibex_bluesky_core.plan_stubs.dae_table_wrapper import _with_dae_tables from ibex_bluesky_core.plan_stubs.num_periods_wrapper import _with_num_periods from ibex_bluesky_core.plan_stubs.time_channels_wrapper import _with_time_channels +from ibex_bluesky_core.utils import NamedReadableAndMovable logger = logging.getLogger(__name__) @@ -29,15 +28,15 @@ __all__ = [ + "_with_dae_tables", + "_with_num_periods", + "_with_time_channels", "call_qt_aware", "call_sync", "polling_plan", "prompt_user_for_choice", "redefine_motor", "redefine_refl_parameter", - "_with_dae_tables", - "_with_num_periods", - "_with_time_channels", ] diff --git a/tests/test_plan_stubs.py b/tests/test_plan_stubs.py index 80760117..ba78dc21 100644 --- a/tests/test_plan_stubs.py +++ b/tests/test_plan_stubs.py @@ -26,13 +26,15 @@ from ibex_bluesky_core.devices.reflectometry import ReflParameter from ibex_bluesky_core.plan_stubs import ( CALL_QT_AWARE_MSG_KEY, + _with_dae_tables, + _with_num_periods, + _with_time_channels, call_qt_aware, call_sync, prompt_user_for_choice, redefine_motor, redefine_refl_parameter, ) -from ibex_bluesky_core.plan_stubs import _with_dae_tables, _with_num_periods, _with_time_channels from ibex_bluesky_core.run_engine._msg_handlers import call_sync_handler from tests.devices.dae_testing_data import dae_settings_template, tcb_settings_template From be7125da2dcea716eccab46af4505f6bf34c5d43 Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Wed, 29 Oct 2025 16:47:01 +0000 Subject: [PATCH 20/21] ruff fixes (plan stubs) --- src/ibex_bluesky_core/plan_stubs/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/ibex_bluesky_core/plan_stubs/__init__.py b/src/ibex_bluesky_core/plan_stubs/__init__.py index 4308f8f4..625ced96 100644 --- a/src/ibex_bluesky_core/plan_stubs/__init__.py +++ b/src/ibex_bluesky_core/plan_stubs/__init__.py @@ -17,10 +17,6 @@ from ibex_bluesky_core.plan_stubs.time_channels_wrapper import _with_time_channels from ibex_bluesky_core.utils import NamedReadableAndMovable -from ibex_bluesky_core.plan_stubs.dae_table_wrapper import _with_dae_tables -from ibex_bluesky_core.plan_stubs.num_periods_wrapper import _with_num_periods -from ibex_bluesky_core.plan_stubs.time_channels_wrapper import _with_time_channels - logger = logging.getLogger(__name__) P = ParamSpec("P") @@ -41,9 +37,6 @@ "prompt_user_for_choice", "redefine_motor", "redefine_refl_parameter", - "_with_dae_tables", - "_with_num_periods", - "_with_time_channels", ] From 04cb045612e52e7b61bd9427a72a4fa3931856bf Mon Sep 17 00:00:00 2001 From: Daniel Maclaren Date: Thu, 30 Oct 2025 13:52:27 +0000 Subject: [PATCH 21/21] corrected the rest of the comments --- doc/plan_stubs/plan_wrappers.md | 64 +++++++++++++++---- .../devices/dae/_period_settings.py | 8 --- src/ibex_bluesky_core/plans/__init__.py | 2 - 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/doc/plan_stubs/plan_wrappers.md b/doc/plan_stubs/plan_wrappers.md index e76db698..184522fc 100644 --- a/doc/plan_stubs/plan_wrappers.md +++ b/doc/plan_stubs/plan_wrappers.md @@ -1,12 +1,56 @@ # Plan Wrappers -Plan wrappers that temporarily modify DAE (Data Acquisition Electronics) settings during a plan, automatically restoring the original values afterwards. This ensures that your experiments don't permanently change instrument configuration. +Plan wrappers that temporarily modify [DAE (Data Acquisition Electronics)](https://isiscomputinggroup.github.io/ibex_bluesky_core/devices/dae.html#dae-data-acquisition-electronics) settings during a plan, automatically restoring the original values afterwards. This ensures that your experiments don't permanently change instrument configuration. ## Available Wrappers -['dae_table_wrapper](ibex_bluesky_core.plan_stubs.dae_table_wrapper) -['num_periods_wrapper](ibex_bluesky_core.plan_stubs.num_periods_wrapper) -['time_channels_wrapper](ibex_bluesky_core.plan_stubs.time_channels_wrapper) +### DAE Table + +:py:obj:`dae_table_wrapper ` + +```python +RE( + _with_dae_tables( + bps.null(), + dae=dae, + new_settings=modified_settings + ) + ) +``` + +Where `modified_settings` is a dataset in the form :py:obj:`DaeSettingsData < ibex_bluesky_core.devices.dae.DaeSettingsData>` + +A function that wraps a plan to temporarily modify the DAE table. + +### Num Periods +:py:obj:`num_periods_wrapper ` + +```python +RE( + _with_num_periods( + bps.null(), + dae=dae, + number_of_periods=1000 + ) + ) +``` +A function that wraps a plan to temporarily modify the number of periods. + +### Time Channels +:py:obj:`time_channels_wrapper `: + +```python +RE( + _with_time_channels( + bps.null(), + dae=dae, + new_settings=modified_settings + ) + ) +``` +Where `modified_settings` is a dataset in the form :py:obj:`DaeTCBSettingsData < ibex_bluesky_core.devices.dae.DaeTCBSettingsData>` + +A function that wraps a plan to temporarily modify the time channels boundaries. ## Usage @@ -15,21 +59,17 @@ To use these wrappers, the plan written by the user must be wrapped by the funct ``` python from bluesky import RunEngine -from ibex_bluesky_core.plan_stubs import with_num_periods +from ibex_bluesky_core.plan_stubs import _with_num_periods from ibex_bluesky_core.devices.simpledae import SimpleDae dae = SimpleDae() # Give your DAE options here RE = RunEngine() -modified_settings = 1 - -def example_plan(): - yield from bps.mv(dae.number_of_periods, modified_settings) - RE( - with_num_periods( - example_plan(), + _with_num_periods( + bps.null(), # Default plan to run dae=dae, + number_of_periods=1000 # Temporary number of periods to run ) ) diff --git a/src/ibex_bluesky_core/devices/dae/_period_settings.py b/src/ibex_bluesky_core/devices/dae/_period_settings.py index 373a7922..3854d8c1 100644 --- a/src/ibex_bluesky_core/devices/dae/_period_settings.py +++ b/src/ibex_bluesky_core/devices/dae/_period_settings.py @@ -53,14 +53,6 @@ class SinglePeriodSettings: label: str | None = None -class PeriodSettingType(Enum): - """Periods type option for a single row.""" - - UNUSED = 0 - DAQ = 1 - DWEll = 2 - - @dataclass(kw_only=True) class DaePeriodSettingsData: """Dataclass for the hardware period settings.""" diff --git a/src/ibex_bluesky_core/plans/__init__.py b/src/ibex_bluesky_core/plans/__init__.py index da769558..86afbc54 100644 --- a/src/ibex_bluesky_core/plans/__init__.py +++ b/src/ibex_bluesky_core/plans/__init__.py @@ -16,7 +16,6 @@ from ibex_bluesky_core.devices.simpledae import monitor_normalising_dae from ibex_bluesky_core.fitting import FitMethod from ibex_bluesky_core.plan_stubs import call_qt_aware, polling_plan -from ibex_bluesky_core.plan_stubs.num_periods_wrapper import with_num_periods from ibex_bluesky_core.utils import NamedReadableAndMovable, centred_pixel if TYPE_CHECKING: @@ -29,7 +28,6 @@ "motor_scan", "polling_plan", "scan", - "with_num_periods", ]