fix(nidaq): forward all EPICS channels plus energy and encoder in software mode
This commit is contained in:
@@ -48,9 +48,15 @@ NIDAQ_STREAM_CHANNELS = [
|
||||
"xrd_ai0_std_dev",
|
||||
]
|
||||
|
||||
#: Groups fed from the EPICS scalars during software-triggered scans. The
|
||||
#: std_dev and stream groups have no synchronous source and stay hardware-only.
|
||||
NIDAQ_SW_FORWARD_GROUPS = ("mean", "max")
|
||||
#: Group channel -> device attribute of the live EPICS scalar backing it in
|
||||
#: software-triggered scans. The backend's standby acquisition feeds ALL
|
||||
#: channels (plus energy and encoder), so everything with an EPICS counterpart
|
||||
#: is forwarded; std_dev and the derived stream channels are hardware-only.
|
||||
NIDAQ_SW_FORWARD_SOURCES: dict[str, dict[str, str]] = {
|
||||
"mean": {channel: channel for channel in NIDAQ_MEAN_CHANNELS},
|
||||
"max": {channel: channel for channel in NIDAQ_MAX_CHANNELS},
|
||||
"stream": {"energy": "energy_epics", "enc": "enc_epics"},
|
||||
}
|
||||
|
||||
|
||||
class NidaqError(Exception):
|
||||
@@ -215,31 +221,17 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
self._forward_epics_readings()
|
||||
return data
|
||||
|
||||
def _enabled_sw_channels(self) -> dict[str, list[str]]:
|
||||
"""Channels currently enabled via the bitmasks, per software-forwarded group.
|
||||
|
||||
Used as a data-quality filter for the software-mode forwarding only (do
|
||||
not forward junk values of disabled inputs); it is NOT a validation
|
||||
contract for the hardware stream, whose active channel set cannot be
|
||||
predicted upfront.
|
||||
"""
|
||||
ai_mask = int(self.ai_chans.get() or 0)
|
||||
ci_mask = int(self.ci_chans.get() or 0)
|
||||
di_mask = int(self.di_chans.get() or 0)
|
||||
return {
|
||||
"mean": [f"ai{i}" for i in range(8) if ai_mask >> i & 1]
|
||||
+ [f"ci{i}" for i in range(18) if ci_mask >> i & 1],
|
||||
"max": [f"di{i}" for i in range(5) if di_mask >> i & 1],
|
||||
}
|
||||
|
||||
def _forward_epics_readings(self) -> None:
|
||||
"""Forward the current EPICS scalar readings to the statistic groups.
|
||||
|
||||
Software-triggered scans only: during the backend-driven scans
|
||||
(valid_scan_names) the per-oscillation statistics arrive through the
|
||||
hardware path, which owns the groups. Only the groups whose members
|
||||
have a synchronous EPICS counterpart are fed (NIDAQ_SW_FORWARD_GROUPS),
|
||||
restricted to the bitmask-enabled channels.
|
||||
hardware path, which owns the groups. ALL channels are forwarded: the
|
||||
backend's standby acquisition runs every input regardless of the
|
||||
measurement bitmasks (AIChans/CIChans/DIChans describe the last
|
||||
hardware task, not what is live), so every EPICS scalar carries a
|
||||
valid reading. std_dev and the derived stream channels have no EPICS
|
||||
counterpart and stay hardware-only (NIDAQ_SW_FORWARD_SOURCES).
|
||||
"""
|
||||
if self._staged != Staged.yes:
|
||||
return
|
||||
@@ -250,10 +242,10 @@ class Nidaq(PSIDeviceBase, NidaqControl):
|
||||
):
|
||||
return
|
||||
now = time.time()
|
||||
for group, channels in self._enabled_sw_channels().items():
|
||||
for group, sources in NIDAQ_SW_FORWARD_SOURCES.items():
|
||||
batch = {
|
||||
channel: {"value": getattr(self, channel).get(), "timestamp": now}
|
||||
for channel in channels
|
||||
channel: {"value": getattr(self, attr).get(), "timestamp": now}
|
||||
for channel, attr in sources.items()
|
||||
}
|
||||
if batch:
|
||||
# Annotate the alignment: these rows are in sync with the
|
||||
|
||||
@@ -217,31 +217,20 @@ from types import SimpleNamespace
|
||||
from ophyd import Staged
|
||||
|
||||
|
||||
def _enable_channels(dev, ai_mask=0b1, ci_mask=0b1000, di_mask=0b110):
|
||||
"""Enable ai0, ci3 and di1/di2 via the channel bitmasks."""
|
||||
dev.ai_chans._read_pv.mock_data = ai_mask
|
||||
dev.ci_chans._read_pv.mock_data = ci_mask
|
||||
dev.di_chans._read_pv.mock_data = di_mask
|
||||
|
||||
|
||||
def test_enabled_sw_channels_follow_bitmasks(mock_nidaq):
|
||||
def test_read_forwards_all_epics_scalars_when_gates_open(mock_nidaq, scan_info_mock):
|
||||
"""Software-triggered scans: the monitored readout forwards ALL EPICS
|
||||
values into the SAME groups the hardware-triggered stream feeds. The
|
||||
measurement bitmasks are irrelevant: the backend's standby acquisition
|
||||
runs every channel."""
|
||||
dev = mock_nidaq
|
||||
_enable_channels(dev, ai_mask=0b101, ci_mask=0b10, di_mask=0b110)
|
||||
enabled = dev._enabled_sw_channels()
|
||||
assert enabled["mean"] == ["ai0", "ai2", "ci1"]
|
||||
assert enabled["max"] == ["di1", "di2"]
|
||||
|
||||
|
||||
def test_read_forwards_enabled_epics_scalars_when_gates_open(mock_nidaq, scan_info_mock):
|
||||
"""Software-triggered scans: the monitored readout forwards the ENABLED
|
||||
EPICS values into the SAME groups the hardware-triggered stream feeds."""
|
||||
dev = mock_nidaq
|
||||
_enable_channels(dev) # ai0, ci3, di1, di2
|
||||
dev.ai_chans._read_pv.mock_data = 0b1 # stale hardware-scan bitmask, must be ignored
|
||||
dev.ai0._read_pv.mock_data = 5.0
|
||||
dev.ai1._read_pv.mock_data = 99.0 # not in the bitmask, forwarded anyway
|
||||
dev.ci3._read_pv.mock_data = 42
|
||||
dev.di1._read_pv.mock_data = 0
|
||||
dev.di2._read_pv.mock_data = 1
|
||||
dev.ai1._read_pv.mock_data = 99.0 # disabled channel, must not be forwarded
|
||||
dev.energy_epics._read_pv.mock_data = 8000.0
|
||||
dev.enc_epics._read_pv.mock_data = 12.5
|
||||
|
||||
scan_info_mock.scan_name = "xas_simple_scan"
|
||||
dev.scan_parameters = scan_info_mock
|
||||
@@ -255,19 +244,26 @@ def test_read_forwards_enabled_epics_scalars_when_gates_open(mock_nidaq, scan_in
|
||||
dev.read()
|
||||
assert dev.mean.get() is None
|
||||
|
||||
# software-triggered scan -> enabled channels forwarded under the unified names
|
||||
# software-triggered scan -> ALL channels forwarded under the unified names
|
||||
scan_info_mock.scan_name = "line_scan"
|
||||
dev.read()
|
||||
out = dev.mean.get()
|
||||
assert len(out.signals) == 26 # all 8 ai + 18 ci, bitmask ignored
|
||||
assert out.signals["nidaq_mean_ai0"]["value"] == 5.0
|
||||
assert out.signals["nidaq_mean_ai1"]["value"] == 99.0
|
||||
assert out.signals["nidaq_mean_ci3"]["value"] == 42
|
||||
assert "nidaq_mean_ai1" not in out.signals # disabled -> filtered
|
||||
assert out.metadata["acquisition_group"] == "monitored"
|
||||
assert dev.max.get().signals["nidaq_max_di1"]["value"] == 0
|
||||
assert dev.max.get().signals["nidaq_max_di2"]["value"] == 1
|
||||
# hardware-only groups untouched in software mode
|
||||
out_max = dev.max.get()
|
||||
assert len(out_max.signals) == 5
|
||||
assert out_max.signals["nidaq_max_di1"]["value"] == 0
|
||||
assert out_max.signals["nidaq_max_di2"]["value"] == 1
|
||||
# energy and encoder are live standby readings -> stream group
|
||||
out_stream = dev.stream.get()
|
||||
assert set(out_stream.signals) == {"nidaq_stream_energy", "nidaq_stream_enc"}
|
||||
assert out_stream.signals["nidaq_stream_energy"]["value"] == 8000.0
|
||||
assert out_stream.signals["nidaq_stream_enc"]["value"] == 12.5
|
||||
# no software source for standard deviations
|
||||
assert dev.std_dev.get() is None
|
||||
assert dev.stream.get() is None
|
||||
|
||||
# scan not open -> no forwarding
|
||||
dev.ai0._read_pv.mock_data = 7.0
|
||||
|
||||
Reference in New Issue
Block a user