From 73c47ce6e602e019ceb0141badcad412c1dbaa18 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 30 Sep 2026 17:33:48 -0400 Subject: [PATCH 1/2] Port QEPro to ophyd-async and extract plans Assisted-by: oh-my-pi:gpt-5.6-terra --- pyproject.toml | 1 - src/xpd_tools/detectors/__init__.py | 4 + src/xpd_tools/detectors/qepro.py | 236 ++++++++++++++++++++++++++++ src/xpd_tools/qepro_plans.py | 60 +++++++ tests/detectors/test_qepro.py | 196 +++++++++++++++++++++++ tests/test_qepro_plans.py | 93 +++++++++++ uv.lock | 19 +-- 7 files changed, 591 insertions(+), 18 deletions(-) create mode 100644 src/xpd_tools/detectors/qepro.py create mode 100644 src/xpd_tools/qepro_plans.py create mode 100644 tests/detectors/test_qepro.py create mode 100644 tests/test_qepro_plans.py diff --git a/pyproject.toml b/pyproject.toml index b769261..b1537e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ dev = [ "pickleshare", "pipdeptree", "pre-commit", - "pyepics>=3.4.2", "pyright", "pytest", "pytest-asyncio", diff --git a/src/xpd_tools/detectors/__init__.py b/src/xpd_tools/detectors/__init__.py index 1bf8ff4..fa03c55 100644 --- a/src/xpd_tools/detectors/__init__.py +++ b/src/xpd_tools/detectors/__init__.py @@ -14,6 +14,8 @@ Pilatus4ROIMode, Pilatus4TriggerMode, ) +from .qepro import QEPro, QEProTEC + __all__ = [ "PandAConfiguration", @@ -26,4 +28,6 @@ "Pilatus4TriggerMode", "Pilatus4HDF5Format", "Pilatus4DriverIO", + "QEPro", + "QEProTEC", ] diff --git a/src/xpd_tools/detectors/qepro.py b/src/xpd_tools/detectors/qepro.py new file mode 100644 index 0000000..7308ae0 --- /dev/null +++ b/src/xpd_tools/detectors/qepro.py @@ -0,0 +1,236 @@ +"""Ophyd-async interface for QEPro spectrometers.""" + +import asyncio +from typing import Annotated as A + +import numpy as np +from ophyd_async.core import ( + AsyncStatus, + DeviceConnector, + DeviceMock, + SignalR, + SignalRW, + StandardReadable, + callback_on_mock_put, + default_mock_class, + observe_value, + set_mock_value, +) +from ophyd_async.core import StandardReadableFormat as Format +from ophyd_async.epics.core import EpicsDevice, PvSuffix + + +class QEProTEC(StandardReadable, EpicsDevice): + """Thermoelectric cooler settings for a QEPro spectrometer.""" + + tec: A[SignalRW[bool], PvSuffix.rbv("TEC")] + tec_temp: A[SignalRW[float], PvSuffix.rbv("TEC_TEMP"), Format.CONFIG_SIGNAL] + curr_tec_temp: A[SignalR[float], PvSuffix("CURR_TEC_TEMP_RBV")] + + def __init__( + self, + prefix: str = "", + *, + tolerance: float = 1.0, + name: str = "", + connector: DeviceConnector | None = None, + ) -> None: + self.tolerance = tolerance + super().__init__(prefix, name=name, connector=connector) + + @AsyncStatus.wrap + async def set(self, value: float) -> None: + """Set the cooler temperature and wait for an in-tolerance update.""" + updates = observe_value(self.curr_tec_temp) + try: + await anext(updates) + await self.tec_temp.set(value) + await self.tec.set(True) + async for current in updates: + if abs(current - value) < self.tolerance: + return + finally: + await updates.aclose() + + +class QEProMock(DeviceMock["QEPro"]): + """Deterministic QEPro behaviour for mock-mode tests and startup.""" + + async def connect(self, device: "QEPro") -> None: + """Initialize predictable spectra and complete mocked acquisitions.""" + x_axis = np.linspace(200.0, 1000.0, 8) + sample = np.arange(8, dtype=float) + + set_mock_value(device.serial, "SIM-QEPRO") + set_mock_value(device.model, "QEPro") + set_mock_value(device.status, 0) + set_mock_value(device.status_msg, "Idle") + set_mock_value(device.device_connected, True) + set_mock_value(device.check_status, False) + set_mock_value(device.features, 63) + set_mock_value(device.strobe, False) + set_mock_value(device.electric_dark_correction, False) + set_mock_value(device.non_linearity_correction, False) + set_mock_value(device.shutter, False) + set_mock_value(device.tec_device.tec, False) + set_mock_value(device.tec_device.tec_temp, 20.0) + set_mock_value(device.tec_device.curr_tec_temp, 20.0) + set_mock_value(device.light_source, False) + set_mock_value(device.light_source_intensity, 0.0) + set_mock_value(device.light_source_count, 0) + set_mock_value(device.num_spectra, 1) + set_mock_value(device.spectra_collected, 0) + set_mock_value(device.int_min_time, 1.0) + set_mock_value(device.int_max_time, 10_000.0) + set_mock_value(device.integration_time, 100.0) + set_mock_value(device.buff_min_capacity, 1) + set_mock_value(device.buff_max_capacity, 3) + set_mock_value(device.buff_capacity, 3) + set_mock_value(device.buff_element_count, 0) + set_mock_value(device.output, sample) + set_mock_value(device.sample, sample) + set_mock_value(device.dark, np.zeros_like(sample)) + set_mock_value(device.reference, np.full_like(sample, 100.0)) + set_mock_value(device.formatted_spectrum_len, len(sample)) + set_mock_value(device.x_axis, x_axis) + set_mock_value(device.x_axis_format, "Wavelength") + set_mock_value(device.dark_available, True) + set_mock_value(device.ref_available, True) + set_mock_value(device.acquire, False) + set_mock_value(device.collect_mode, "Average") + set_mock_value(device.spectrum_type, "Absorbtion") + set_mock_value(device.correction, "Reference") + set_mock_value(device.trigger_mode, "Normal") + + async def _on_tec_enabled(enabled: bool) -> None: + if enabled: + set_mock_value( + device.tec_device.curr_tec_temp, + await device.tec_device.tec_temp.get_value(), + ) + + def _on_acquire_write(collect: bool) -> None: + if collect: + asyncio.get_running_loop().call_soon( + set_mock_value, device.acquire, False + ) + + callback_on_mock_put(device.tec_device.tec, _on_tec_enabled) + callback_on_mock_put(device.acquire, _on_acquire_write) + + +@default_mock_class(QEProMock) +class QEPro(StandardReadable, EpicsDevice): + """Ophyd-async device interface for a QEPro spectrometer.""" + + # Device information + serial: A[SignalRW[str], PvSuffix("SERIAL")] + model: A[SignalRW[str], PvSuffix("MODEL")] + + # Device status + status: A[SignalRW[int], PvSuffix("STATUS")] + status_msg: A[SignalRW[str], PvSuffix("STATUS_MSG")] + device_connected: A[SignalR[bool], PvSuffix("CONNECTED_RBV")] + check_status: A[SignalRW[bool], PvSuffix("CHECK_STATUS")] + features: A[SignalR[int], PvSuffix("FEATURES_RBV")] + + # Togglable features + strobe: A[SignalRW[bool], PvSuffix.rbv("STROBE")] + electric_dark_correction: A[SignalRW[bool], PvSuffix.rbv("EDC")] + non_linearity_correction: A[SignalRW[bool], PvSuffix.rbv("NLC")] + shutter: A[SignalRW[bool], PvSuffix.rbv("SHUTTER")] + + # Thermal electric cooler + tec_device: A[QEProTEC, PvSuffix(""), Format.CHILD] + + # Light source feature signals + light_source: A[SignalRW[bool], PvSuffix.rbv("LIGHT_SOURCE")] + light_source_intensity: A[SignalRW[float], PvSuffix.rbv("LIGHT_SOURCE_INTENSITY")] + light_source_count: A[SignalR[int], PvSuffix("LIGHT_SOURCE_COUNT_RBV")] + + # Spectra averaging settings and current-scan count + num_spectra: A[SignalRW[int], PvSuffix.rbv("NUM_SPECTRA"), Format.UNCACHED_SIGNAL] + spectra_collected: A[SignalR[int], PvSuffix("SPECTRA_COLLECTED_RBV")] + + # Integration time settings in milliseconds + int_min_time: A[SignalR[float], PvSuffix("INT_MIN_TIME_RBV")] + int_max_time: A[SignalR[float], PvSuffix("INT_MAX_TIME_RBV")] + integration_time: A[ + SignalRW[float], PvSuffix.rbv("INTEGRATION_TIME"), Format.UNCACHED_SIGNAL + ] + + # Internal buffer settings + buff_min_capacity: A[SignalR[int], PvSuffix("BUFF_MIN_CAPACITY_RBV")] + buff_max_capacity: A[SignalR[int], PvSuffix("BUFF_MAX_CAPACITY_RBV")] + buff_capacity: A[ + SignalRW[int], PvSuffix.rbv("BUFF_CAPACITY"), Format.UNCACHED_SIGNAL + ] + buff_element_count: A[SignalR[int], PvSuffix("BUFF_ELEMENT_COUNT_RBV")] + + # Formatted spectra + output: A[SignalRW[np.ndarray], PvSuffix("OUTPUT"), Format.UNCACHED_SIGNAL] + sample: A[SignalRW[np.ndarray], PvSuffix("SAMPLE"), Format.UNCACHED_SIGNAL] + dark: A[SignalRW[np.ndarray], PvSuffix("DARK"), Format.UNCACHED_SIGNAL] + reference: A[SignalRW[np.ndarray], PvSuffix("REFERENCE"), Format.UNCACHED_SIGNAL] + formatted_spectrum_len: A[SignalR[int], PvSuffix("FORMATTED_SPECTRUM_LEN_RBV")] + + # X-axis settings + x_axis: A[SignalRW[np.ndarray], PvSuffix("X_AXIS"), Format.UNCACHED_SIGNAL] + x_axis_format: A[SignalRW[str], PvSuffix.rbv("X_AXIS_FORMAT")] + + # Dark/reference availability + dark_available: A[SignalR[bool], PvSuffix("DARK_AVAILABLE_RBV")] + ref_available: A[SignalR[bool], PvSuffix("REF_AVAILABLE_RBV")] + + # Collection settings and trigger + acquire: A[SignalRW[bool], PvSuffix.rbv("COLLECT")] + collect_mode: A[SignalRW[str], PvSuffix.rbv("COLLECT_MODE")] + spectrum_type: A[ + SignalRW[str], PvSuffix.rbv("SPECTRUM_TYPE"), Format.UNCACHED_SIGNAL + ] + correction: A[SignalRW[str], PvSuffix.rbv("CORRECTION")] + trigger_mode: A[SignalRW[str], PvSuffix.rbv("TRIGGER_MODE")] + + def __init__(self, prefix: str = "", *, name: str = "") -> None: + super().__init__(prefix, name=name) + self.set_name(name, child_name_separator="_") + + async def has_nlc_feature(self) -> int: + """Return the non-linearity correction feature bit.""" + return await self.features.get_value() & 32 + + async def has_lightsource_feature(self) -> int: + """Return the light source feature bit.""" + return await self.features.get_value() & 16 + + async def has_edc_feature(self) -> int: + """Return the electric dark correction feature bit.""" + return await self.features.get_value() & 8 + + async def has_buffer_feature(self) -> int: + """Return the internal buffer feature bit.""" + return await self.features.get_value() & 4 + + async def has_tec_feature(self) -> int: + """Return the thermoelectric cooler feature bit.""" + return await self.features.get_value() & 2 + + async def has_irrad_feature(self) -> int: + """Return the irradiance calibration feature bit.""" + return await self.features.get_value() & 1 + + @AsyncStatus.wrap + async def trigger(self) -> None: + """Collect one spectrum and wait for its acquire cycle to finish.""" + updates = observe_value(self.acquire) + try: + await anext(updates) + await self.acquire.set(True) + collecting = False + async for acquired in updates: + if acquired: + collecting = True + elif collecting: + return + finally: + await updates.aclose() diff --git a/src/xpd_tools/qepro_plans.py b/src/xpd_tools/qepro_plans.py new file mode 100644 index 0000000..156777b --- /dev/null +++ b/src/xpd_tools/qepro_plans.py @@ -0,0 +1,60 @@ +"""Hardware-independent Bluesky plans for QEPro spectrometers.""" + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp + +from xpd_tools.detectors import QEPro + + +def configure_qepro( + qepro: QEPro, + *, + integration_time: float = 100, + num_spectra_to_average: int = 10, + buffer: int = 3, + spectrum_type: str = "Absorbtion", + correction_type: str = "Reference", + electric_dark_correction: bool = True, +): + """Apply the standard QEPro collection configuration.""" + yield from bps.abs_set(qepro.integration_time, integration_time, wait=True) + yield from bps.abs_set(qepro.num_spectra, num_spectra_to_average, wait=True) + yield from bps.abs_set(qepro.buff_capacity, buffer, wait=True) + yield from bps.abs_set( + qepro.collect_mode, + "Average" if num_spectra_to_average > 1 else "Single", + wait=True, + ) + if electric_dark_correction: + yield from bps.abs_set(qepro.electric_dark_correction, True, wait=True) + yield from bps.abs_set(qepro.correction, correction_type, wait=True) + yield from bps.abs_set(qepro.spectrum_type, spectrum_type, wait=True) + + +def _capture_qepro_frame(qepro: QEPro, spectrum_type: str, settle_time: float): + yield from bps.abs_set(qepro.spectrum_type, spectrum_type, wait=True) + yield from bps.trigger(qepro, wait=True) + yield from bps.sleep(settle_time) + + +def _take_qepro_frame(qepro: QEPro, spectrum_type: str, settle_time: float): + previous_spectrum_type = yield from bps.rd(qepro.spectrum_type) + yield from bpp.finalize_wrapper( + _capture_qepro_frame(qepro, spectrum_type, settle_time), + bps.abs_set(qepro.spectrum_type, previous_spectrum_type, wait=True), + ) + + +def take_qepro_dark_frame(qepro: QEPro, *, settle_time: float = 1.0): + """Capture a dark frame and restore the prior spectrum type.""" + yield from _take_qepro_frame(qepro, "Dark", settle_time) + + +def take_qepro_reference_frame(qepro: QEPro, *, settle_time: float = 1.0): + """Capture a reference frame and restore the prior spectrum type.""" + yield from _take_qepro_frame(qepro, "Reference", settle_time) + + +def set_qepro_temperature(qepro: QEPro, temperature: float): + """Set QEPro's thermoelectric cooler and wait for convergence.""" + yield from bps.abs_set(qepro.tec_device, temperature, wait=True) diff --git a/tests/detectors/test_qepro.py b/tests/detectors/test_qepro.py new file mode 100644 index 0000000..e8abcac --- /dev/null +++ b/tests/detectors/test_qepro.py @@ -0,0 +1,196 @@ +import asyncio +from typing import Any + +import numpy as np +import pytest +from ophyd_async.core import callback_on_mock_put, init_devices, set_mock_value + +from xpd_tools.detectors import QEPro + +FEATURE_MASKS = [ + ("has_nlc_feature", 32), + ("has_lightsource_feature", 16), + ("has_edc_feature", 8), + ("has_buffer_feature", 4), + ("has_tec_feature", 2), + ("has_irrad_feature", 1), +] + +PV_SUFFIXES = { + "serial": "SERIAL", + "model": "MODEL", + "status": "STATUS", + "status_msg": "STATUS_MSG", + "device_connected": "CONNECTED_RBV", + "check_status": "CHECK_STATUS", + "features": "FEATURES_RBV", + "strobe": "STROBE_RBV", + "electric_dark_correction": "EDC_RBV", + "non_linearity_correction": "NLC_RBV", + "shutter": "SHUTTER_RBV", + "tec_device.tec": "TEC_RBV", + "tec_device.tec_temp": "TEC_TEMP_RBV", + "tec_device.curr_tec_temp": "CURR_TEC_TEMP_RBV", + "light_source": "LIGHT_SOURCE_RBV", + "light_source_intensity": "LIGHT_SOURCE_INTENSITY_RBV", + "light_source_count": "LIGHT_SOURCE_COUNT_RBV", + "num_spectra": "NUM_SPECTRA_RBV", + "spectra_collected": "SPECTRA_COLLECTED_RBV", + "int_min_time": "INT_MIN_TIME_RBV", + "int_max_time": "INT_MAX_TIME_RBV", + "integration_time": "INTEGRATION_TIME_RBV", + "buff_min_capacity": "BUFF_MIN_CAPACITY_RBV", + "buff_max_capacity": "BUFF_MAX_CAPACITY_RBV", + "buff_capacity": "BUFF_CAPACITY_RBV", + "buff_element_count": "BUFF_ELEMENT_COUNT_RBV", + "output": "OUTPUT", + "sample": "SAMPLE", + "dark": "DARK", + "reference": "REFERENCE", + "formatted_spectrum_len": "FORMATTED_SPECTRUM_LEN_RBV", + "x_axis": "X_AXIS", + "x_axis_format": "X_AXIS_FORMAT_RBV", + "dark_available": "DARK_AVAILABLE_RBV", + "ref_available": "REF_AVAILABLE_RBV", + "acquire": "COLLECT_RBV", + "collect_mode": "COLLECT_MODE_RBV", + "spectrum_type": "SPECTRUM_TYPE_RBV", + "correction": "CORRECTION_RBV", + "trigger_mode": "TRIGGER_MODE_RBV", +} + + +@pytest.fixture +async def qepro() -> QEPro: + async with init_devices(mock=True): + device = QEPro("SIM:", name="QEPro") + return device + + +def _source(device: QEPro, path: str) -> str: + child: Any = device + for attr in path.split("."): + child = getattr(child, attr) + return child.source + + +async def _next_loop_turn() -> None: + marker = asyncio.Event() + asyncio.get_running_loop().call_soon(marker.set) + await marker.wait() + + +def test_qepro_pv_contract() -> None: + qepro = QEPro("SIM:", name="QEPro") + + assert {path: _source(qepro, path) for path in PV_SUFFIXES} == { + path: f"ca://SIM:{suffix}" for path, suffix in PV_SUFFIXES.items() + } + + +@pytest.mark.parametrize(("feature_name", "mask"), FEATURE_MASKS) +async def test_qepro_feature_masks(qepro: QEPro, feature_name: str, mask: int) -> None: + set_mock_value(qepro.features, mask) + + assert await getattr(qepro, feature_name)() == mask + for other_feature_name, _ in FEATURE_MASKS: + if other_feature_name != feature_name: + assert await getattr(qepro, other_feature_name)() == 0 + + +async def test_qepro_tec_set_completes_only_within_tolerance(qepro: QEPro) -> None: + tec_enabled = asyncio.Event() + + def hold_temperature(enabled: bool) -> None: + if enabled: + tec_enabled.set() + + callback_on_mock_put(qepro.tec_device.tec, hold_temperature) + status = qepro.tec_device.set(20.0) + + await asyncio.wait_for(tec_enabled.wait(), timeout=1) + await _next_loop_turn() + assert await qepro.tec_device.tec_temp.get_value() == 20.0 + assert not status.done + + set_mock_value(qepro.tec_device.curr_tec_temp, 21.0) + await _next_loop_turn() + assert not status.done + + set_mock_value(qepro.tec_device.curr_tec_temp, 20.5) + await status + assert status.success + + +async def test_qepro_tec_mock_completes_temperature_change(qepro: QEPro) -> None: + status = qepro.tec_device.set(25.0) + + await status + + assert await qepro.tec_device.curr_tec_temp.get_value() == 25.0 + + +async def test_qepro_trigger_completes_on_collect_falling_edge(qepro: QEPro) -> None: + collection_started = asyncio.Event() + + def hold_collection(collect: bool) -> None: + if collect: + collection_started.set() + + callback_on_mock_put(qepro.acquire, hold_collection) + status = qepro.trigger() + + await asyncio.wait_for(collection_started.wait(), timeout=1) + await _next_loop_turn() + assert await qepro.acquire.get_value() is True + assert not status.done + + set_mock_value(qepro.acquire, False) + await status + assert status.success + + +async def test_qepro_mock_completes_collection_cycle(qepro: QEPro) -> None: + status = qepro.trigger() + + await status + + assert await qepro.acquire.get_value() is False + + +async def test_qepro_reads_fresh_spectrum_after_trigger(qepro: QEPro) -> None: + expected_output = np.array([101.0, 202.0]) + + def complete_collection(collect: bool) -> None: + if collect: + set_mock_value(qepro.output, expected_output) + asyncio.get_running_loop().call_soon(set_mock_value, qepro.acquire, False) + + callback_on_mock_put(qepro.acquire, complete_collection) + + await qepro.trigger() + + reading = await qepro.read() + np.testing.assert_array_equal(reading["QEPro_output"]["value"], expected_output) + + +async def test_qepro_read_fields_include_export_data(qepro: QEPro) -> None: + reading = await qepro.read() + + expected_fields = { + "QEPro_x_axis", + "QEPro_output", + "QEPro_sample", + "QEPro_dark", + "QEPro_reference", + "QEPro_spectrum_type", + "QEPro_integration_time", + "QEPro_num_spectra", + "QEPro_buff_capacity", + } + + assert set(reading) == expected_fields + assert reading["QEPro_spectrum_type"]["value"] == "Absorbtion" + + configuration = await qepro.read_configuration() + assert set(configuration) == {"QEPro_tec_device_tec_temp"} diff --git a/tests/test_qepro_plans.py b/tests/test_qepro_plans.py new file mode 100644 index 0000000..0f720c8 --- /dev/null +++ b/tests/test_qepro_plans.py @@ -0,0 +1,93 @@ +"""Behavioral tests for reusable QEPro plan primitives.""" + +from collections.abc import Callable, Generator +from typing import Any, cast + +import bluesky.plan_stubs as bps +import pytest +from bluesky import Msg +from bluesky.run_engine import RunEngine +from ophyd_async.core import callback_on_mock_put, init_devices + +from xpd_tools.detectors import QEPro +from xpd_tools.qepro_plans import ( + configure_qepro, + set_qepro_temperature, + take_qepro_dark_frame, + take_qepro_reference_frame, +) + + +@pytest.fixture +async def qepro() -> QEPro: + async with init_devices(mock=True): + device = QEPro("SIM:", name="QEPro") + return device + + +def _read(signal) -> Generator[Msg, None, object]: + return (yield from bps.rd(signal)) + + +def _read_value(RE: RunEngine, signal) -> object: + return cast(Any, RE(_read(signal))).plan_result + + +def test_configure_qepro_sets_collection_contract(RE: RunEngine, qepro: QEPro) -> None: + RE( + configure_qepro( + qepro, + integration_time=25.0, + num_spectra_to_average=1, + buffer=7, + spectrum_type="Corrected Sample", + correction_type="Dark", + electric_dark_correction=True, + ) + ) + + assert _read_value(RE, qepro.integration_time) == 25.0 + assert _read_value(RE, qepro.num_spectra) == 1 + assert _read_value(RE, qepro.buff_capacity) == 7 + assert _read_value(RE, qepro.collect_mode) == "Single" + assert _read_value(RE, qepro.electric_dark_correction) is True + assert _read_value(RE, qepro.correction) == "Dark" + assert _read_value(RE, qepro.spectrum_type) == "Corrected Sample" + + RE(configure_qepro(qepro, num_spectra_to_average=2, electric_dark_correction=False)) + + assert _read_value(RE, qepro.collect_mode) == "Average" + assert _read_value(RE, qepro.electric_dark_correction) is True + + +@pytest.mark.parametrize( + ("frame_plan", "requested_spectrum_type"), + [ + (take_qepro_dark_frame, "Dark"), + (take_qepro_reference_frame, "Reference"), + ], +) +def test_frame_plans_restore_prior_spectrum_type( + RE: RunEngine, + qepro: QEPro, + frame_plan: Callable[..., Generator[Msg, None, None]], + requested_spectrum_type: str, +) -> None: + RE(bps.abs_set(qepro.spectrum_type, "Corrected Sample", wait=True)) + spectrum_type_writes: list[str] = [] + callback_on_mock_put(qepro.spectrum_type, spectrum_type_writes.append) + + RE(frame_plan(qepro, settle_time=0)) + + assert spectrum_type_writes == [requested_spectrum_type, "Corrected Sample"] + assert _read_value(RE, qepro.spectrum_type) == "Corrected Sample" + assert _read_value(RE, qepro.acquire) is False + + +def test_set_qepro_temperature_waits_for_tec_convergence( + RE: RunEngine, qepro: QEPro +) -> None: + RE(set_qepro_temperature(qepro, 23.5)) + + assert _read_value(RE, qepro.tec_device.tec_temp) == 23.5 + assert _read_value(RE, qepro.tec_device.curr_tec_temp) == 23.5 diff --git a/uv.lock b/uv.lock index 02e239f..e4dbfc0 100644 --- a/uv.lock +++ b/uv.lock @@ -3,10 +3,10 @@ revision = 3 requires-python = ">=3.11, <3.14" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] @@ -3076,19 +3076,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/08/28e2194ed1c3c3a3e86e0600e2dcc7f21dcd670bd31f3efab2e3e2cf0cbd/pydata_sphinx_theme-0.20.0-py3-none-any.whl", hash = "sha256:56744483c9d72c783e075de716ab95d486108b69605df7528078090b73f11f69", size = 6201166, upload-time = "2026-07-09T09:09:12.899Z" }, ] -[[package]] -name = "pyepics" -version = "3.5.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/57/29e1e9ef11ba2a3419f489946ae1d8190025a1e4f4e1c1686758bb2b6e0a/pyepics-3.5.10.tar.gz", hash = "sha256:f390cf9be40aba757b9528888114bc14d8db01086d0c93914720b1772460375b", size = 6150481, upload-time = "2026-05-20T18:44:36.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/36/e517fd83bd97d6bfe4105e0c6b322070c5cc36fb56a8fab2866e319f8804/pyepics-3.5.10-py3-none-any.whl", hash = "sha256:c9fac2c54f2b595268e66fc348cf5ec24e63718e7dbd07d73d1b37d4da6bfee1", size = 5332376, upload-time = "2026-05-20T18:44:34.242Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -4707,7 +4694,6 @@ dev = [ { name = "pickleshare" }, { name = "pipdeptree" }, { name = "pre-commit" }, - { name = "pyepics" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -4761,7 +4747,6 @@ dev = [ { name = "pickleshare" }, { name = "pipdeptree" }, { name = "pre-commit" }, - { name = "pyepics", specifier = ">=3.4.2" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, From a045a87d9f7574ca6893868dbca6e8cbf1c24ed7 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 30 Sep 2026 17:43:22 -0400 Subject: [PATCH 2/2] pre-commit --- src/xpd_tools/detectors/__init__.py | 1 - uv.lock | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/xpd_tools/detectors/__init__.py b/src/xpd_tools/detectors/__init__.py index fa03c55..eeb85e4 100644 --- a/src/xpd_tools/detectors/__init__.py +++ b/src/xpd_tools/detectors/__init__.py @@ -16,7 +16,6 @@ ) from .qepro import QEPro, QEProTEC - __all__ = [ "PandAConfiguration", "switch_panda_configuration", diff --git a/uv.lock b/uv.lock index 5169ec4..a6312a2 100644 --- a/uv.lock +++ b/uv.lock @@ -3076,6 +3076,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/08/28e2194ed1c3c3a3e86e0600e2dcc7f21dcd670bd31f3efab2e3e2cf0cbd/pydata_sphinx_theme-0.20.0-py3-none-any.whl", hash = "sha256:56744483c9d72c783e075de716ab95d486108b69605df7528078090b73f11f69", size = 6201166, upload-time = "2026-07-09T09:09:12.899Z" }, ] +[[package]] +name = "pyepics" +version = "3.5.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/57/29e1e9ef11ba2a3419f489946ae1d8190025a1e4f4e1c1686758bb2b6e0a/pyepics-3.5.10.tar.gz", hash = "sha256:f390cf9be40aba757b9528888114bc14d8db01086d0c93914720b1772460375b", size = 6150481, upload-time = "2026-05-20T18:44:36.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/36/e517fd83bd97d6bfe4105e0c6b322070c5cc36fb56a8fab2866e319f8804/pyepics-3.5.10-py3-none-any.whl", hash = "sha256:c9fac2c54f2b595268e66fc348cf5ec24e63718e7dbd07d73d1b37d4da6bfee1", size = 5332376, upload-time = "2026-05-20T18:44:34.242Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -4788,6 +4801,7 @@ dev = [ { name = "pickleshare" }, { name = "pipdeptree" }, { name = "pre-commit" }, + { name = "pyepics" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -4842,6 +4856,7 @@ dev = [ { name = "pickleshare" }, { name = "pipdeptree" }, { name = "pre-commit" }, + { name = "pyepics", specifier = ">=3.4.2" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" },