diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index aa89577..cea6fac 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -30,11 +30,12 @@ if TYPE_CHECKING: # pragma: no cover logger = bec_logger.logger -#: Channels per statistic group. Group sub-signals are emitted as -#: 'nidaq__' (e.g. 'nidaq_mean_ai0') in BOTH trigger modes. -NIDAQ_MEAN_CHANNELS = [f"ai{i}" for i in range(8)] + [f"ci{i}" for i in range(18)] -NIDAQ_MAX_CHANNELS = [f"di{i}" for i in range(5)] -NIDAQ_STD_DEV_CHANNELS = [f"ai{i}" for i in range(8)] + [f"ci{i}" for i in range(18)] +#: FINAL naming convention (channel-first, identical to the backend stream): +#: nidaq_ai0_mean, nidaq_ai0_std_dev, nidaq_di0_max, nidaq_energy, ... +#: ai/ci channels are two-signal DynamicSignal groups (mean/std_dev); every +#: other channel is a single-signal group whose emitted name IS the group name. +NIDAQ_AI_CI_CHANNELS = [f"ai{i}" for i in range(8)] + [f"ci{i}" for i in range(18)] +NIDAQ_DI_CHANNELS = [f"di{i}_max" for i in range(5)] NIDAQ_STREAM_CHANNELS = [ "energy", "enc", @@ -47,34 +48,28 @@ NIDAQ_STREAM_CHANNELS = [ "xrd_timestamp", "xrd_angle", "xrd_energy", - "xrd_ai0_mean", - "xrd_ai0_std_dev", + "xrd_ai0", ] -#: 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"}, -} - -#: Backend channel name -> (group, group channel) for the unified bridge. -#: Covers both backend naming variants: with RLE the per-oscillation statistics -#: carry _mean/_std_dev/_max suffixes, without RLE the names are bare. +#: Backend channel name -> (group component, put key) for the unified bridge. +#: Two-signal groups (ai/ci) take the statistic as key; single-signal groups +#: require the FULL emitted name as key (bec_signals single-channel rename). +#: Bare ai/ci names (backend no-RLE mode) map onto the mean channel. NIDAQ_BRIDGE_ROUTING: dict[str, tuple[str, str]] = ( - {f"ai{i}_mean": ("mean", f"ai{i}") for i in range(8)} - | {f"ai{i}": ("mean", f"ai{i}") for i in range(8)} - | {f"ai{i}_std_dev": ("std_dev", f"ai{i}") for i in range(8)} - | {f"ci{i}_mean": ("mean", f"ci{i}") for i in range(18)} - | {f"ci{i}": ("mean", f"ci{i}") for i in range(18)} - | {f"ci{i}_std_dev": ("std_dev", f"ci{i}") for i in range(18)} - | {f"di{i}_max": ("max", f"di{i}") for i in range(5)} - | {f"di{i}": ("max", f"di{i}") for i in range(5)} - | {channel: ("stream", channel) for channel in NIDAQ_STREAM_CHANNELS} - | {"xrd_ai0": ("stream", "xrd_ai0_mean")} + {f"{ch}_mean": (ch, "mean") for ch in NIDAQ_AI_CI_CHANNELS} + | {f"{ch}_std_dev": (ch, "std_dev") for ch in NIDAQ_AI_CI_CHANNELS} + | {ch: (ch, "mean") for ch in NIDAQ_AI_CI_CHANNELS} + | {f"di{i}_max": (f"di{i}_max", f"nidaq_di{i}_max") for i in range(5)} + | {f"di{i}": (f"di{i}_max", f"nidaq_di{i}_max") for i in range(5)} + | {ch: (ch, f"nidaq_{ch}") for ch in NIDAQ_STREAM_CHANNELS} +) + +#: Software-mode sources: (group component, put key) -> EPICS scalar attribute. +#: std_dev and the derived stream channels have no EPICS counterpart. +NIDAQ_SW_FORWARD_SOURCES: dict[tuple[str, str], str] = ( + {(ch, "mean"): f"{ch}_epics" for ch in NIDAQ_AI_CI_CHANNELS} + | {(f"di{i}_max", f"nidaq_di{i}_max"): f"di{i}_epics" for i in range(5)} + | {("energy", "nidaq_energy"): "energy_epics", ("enc", "nidaq_enc"): "enc_epics"} ) @@ -86,39 +81,39 @@ class NidaqControl(Device): """Nidaq control class with all PVs""" # fmt: off - ai0 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.omitted, doc="EPICS analog input 0", auto_monitor=True) - ai1 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.omitted, doc="EPICS analog input 1", auto_monitor=True) - ai2 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.omitted, doc="EPICS analog input 2", auto_monitor=True) - ai3 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.omitted, doc="EPICS analog input 3", auto_monitor=True) - ai4 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.omitted, doc="EPICS analog input 4", auto_monitor=True) - ai5 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.omitted, doc="EPICS analog input 5", auto_monitor=True) - ai6 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.omitted, doc="EPICS analog input 6", auto_monitor=True) - ai7 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.omitted, doc="EPICS analog input 7", auto_monitor=True) + ai0_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.omitted, doc="EPICS analog input 0", auto_monitor=True) + ai1_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.omitted, doc="EPICS analog input 1", auto_monitor=True) + ai2_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.omitted, doc="EPICS analog input 2", auto_monitor=True) + ai3_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.omitted, doc="EPICS analog input 3", auto_monitor=True) + ai4_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.omitted, doc="EPICS analog input 4", auto_monitor=True) + ai5_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.omitted, doc="EPICS analog input 5", auto_monitor=True) + ai6_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.omitted, doc="EPICS analog input 6", auto_monitor=True) + ai7_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.omitted, doc="EPICS analog input 7", auto_monitor=True) - ci0 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True) - ci1 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True) - ci2 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True) - ci3 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True) - ci4 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True) - ci5 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True) - ci6 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True) - ci7 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True) - ci8 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.omitted, doc="EPICS counter input 8", auto_monitor=True) - ci9 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.omitted, doc="EPICS counter input 9", auto_monitor=True) - ci10 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True) - ci11 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True) - ci12 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True) - ci13 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True) - ci14 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True) - ci15 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True) - ci16 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True) - ci17 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True) + ci0_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True) + ci1_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True) + ci2_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True) + ci3_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True) + ci4_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True) + ci5_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True) + ci6_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True) + ci7_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True) + ci8_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.omitted, doc="EPICS counter input 8", auto_monitor=True) + ci9_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.omitted, doc="EPICS counter input 9", auto_monitor=True) + ci10_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True) + ci11_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True) + ci12_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True) + ci13_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True) + ci14_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True) + ci15_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True) + ci16_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True) + ci17_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True) - di0 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.omitted, doc="EPICS digital input 0", auto_monitor=True) - di1 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.omitted, doc="EPICS digital input 1", auto_monitor=True) - di2 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.omitted, doc="EPICS digital input 2", auto_monitor=True) - di3 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.omitted, doc="EPICS digital input 3", auto_monitor=True) - di4 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.omitted, doc="EPICS digital input 4", auto_monitor=True) + di0_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.omitted, doc="EPICS digital input 0", auto_monitor=True) + di1_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.omitted, doc="EPICS digital input 1", auto_monitor=True) + di2_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.omitted, doc="EPICS digital input 2", auto_monitor=True) + di3_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.omitted, doc="EPICS digital input 3", auto_monitor=True) + di4_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.omitted, doc="EPICS digital input 4", auto_monitor=True) enc_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENC", kind=Kind.omitted, doc="EPICS Encoder reading", auto_monitor=True) @@ -188,12 +183,50 @@ class Nidaq(PSIDeviceBase, NidaqControl): progress_signal = Cpt(ProgressSignal, name="progress_signal") # fmt: off - ### Statistic groups: emitted as nidaq__ in both trigger modes ### - #TODO decide on how to group signals, statistic groupd, analog/counter/digital etc. - mean = Cpt(DynamicSignal, name="mean", ndim=1, max_size=1000, signals=NIDAQ_MEAN_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Per-oscillation means (HW) / synchronous readbacks (SW)") - max = Cpt(DynamicSignal, name="max", ndim=1, max_size=1000, signals=NIDAQ_MAX_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Per-oscillation maxima (HW) / synchronous readbacks (SW)") - std_dev = Cpt(DynamicSignal, name="std_dev", ndim=1, max_size=1000, signals=NIDAQ_STD_DEV_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Per-oscillation standard deviations (HW only)") - stream = Cpt(DynamicSignal, name="stream", ndim=1, max_size=1000, signals=NIDAQ_STREAM_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Energy, encoder, derived and XRD channels") + ### Per-channel async groups: emitted names are channel-first (nidaq_ai0_mean) ### + ai0 = Cpt(DynamicSignal, name="ai0", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai1 = Cpt(DynamicSignal, name="ai1", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai2 = Cpt(DynamicSignal, name="ai2", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai3 = Cpt(DynamicSignal, name="ai3", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai4 = Cpt(DynamicSignal, name="ai4", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai5 = Cpt(DynamicSignal, name="ai5", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai6 = Cpt(DynamicSignal, name="ai6", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ai7 = Cpt(DynamicSignal, name="ai7", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci0 = Cpt(DynamicSignal, name="ci0", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci1 = Cpt(DynamicSignal, name="ci1", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci2 = Cpt(DynamicSignal, name="ci2", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci3 = Cpt(DynamicSignal, name="ci3", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci4 = Cpt(DynamicSignal, name="ci4", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci5 = Cpt(DynamicSignal, name="ci5", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci6 = Cpt(DynamicSignal, name="ci6", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci7 = Cpt(DynamicSignal, name="ci7", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci8 = Cpt(DynamicSignal, name="ci8", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci9 = Cpt(DynamicSignal, name="ci9", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci10 = Cpt(DynamicSignal, name="ci10", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci11 = Cpt(DynamicSignal, name="ci11", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci12 = Cpt(DynamicSignal, name="ci12", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci13 = Cpt(DynamicSignal, name="ci13", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci14 = Cpt(DynamicSignal, name="ci14", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci15 = Cpt(DynamicSignal, name="ci15", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci16 = Cpt(DynamicSignal, name="ci16", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + ci17 = Cpt(DynamicSignal, name="ci17", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]}) + di0_max = Cpt(DynamicSignal, name="di0_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]}) + di1_max = Cpt(DynamicSignal, name="di1_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]}) + di2_max = Cpt(DynamicSignal, name="di2_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]}) + di3_max = Cpt(DynamicSignal, name="di3_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]}) + di4_max = Cpt(DynamicSignal, name="di4_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]}) + energy = Cpt(DynamicSignal, name="energy", ndim=1, max_size=1000, signals=["energy"], async_update={"type": "add", "max_shape": [None]}) + enc = Cpt(DynamicSignal, name="enc", ndim=1, max_size=1000, signals=["enc"], async_update={"type": "add", "max_shape": [None]}) + smpl_abs = Cpt(DynamicSignal, name="smpl_abs", ndim=1, max_size=1000, signals=["smpl_abs"], async_update={"type": "add", "max_shape": [None]}) + smpl_fluo = Cpt(DynamicSignal, name="smpl_fluo", ndim=1, max_size=1000, signals=["smpl_fluo"], async_update={"type": "add", "max_shape": [None]}) + ref_abs = Cpt(DynamicSignal, name="ref_abs", ndim=1, max_size=1000, signals=["ref_abs"], async_update={"type": "add", "max_shape": [None]}) + cisum = Cpt(DynamicSignal, name="cisum", ndim=1, max_size=1000, signals=["cisum"], async_update={"type": "add", "max_shape": [None]}) + rle = Cpt(DynamicSignal, name="rle", ndim=1, max_size=1000, signals=["rle"], async_update={"type": "add", "max_shape": [None]}) + xas_timestamp = Cpt(DynamicSignal, name="xas_timestamp", ndim=1, max_size=1000, signals=["xas_timestamp"], async_update={"type": "add", "max_shape": [None]}) + xrd_timestamp = Cpt(DynamicSignal, name="xrd_timestamp", ndim=1, max_size=1000, signals=["xrd_timestamp"], async_update={"type": "add", "max_shape": [None]}) + xrd_angle = Cpt(DynamicSignal, name="xrd_angle", ndim=1, max_size=1000, signals=["xrd_angle"], async_update={"type": "add", "max_shape": [None]}) + xrd_energy = Cpt(DynamicSignal, name="xrd_energy", ndim=1, max_size=1000, signals=["xrd_energy"], async_update={"type": "add", "max_shape": [None]}) + xrd_ai0 = Cpt(DynamicSignal, name="xrd_ai0", ndim=1, max_size=1000, signals=["xrd_ai0"], async_update={"type": "add", "max_shape": [None]}) # fmt: on USER_ACCESS = ["set_config"] @@ -279,15 +312,13 @@ class Nidaq(PSIDeviceBase, NidaqControl): ): return now = time.time() - for group, sources in NIDAQ_SW_FORWARD_SOURCES.items(): - batch = { - 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 - # monitored readouts (cf. MCSCardCSAXS acquisition_group logic). - self._guarded_group_put(group, batch, acquisition_group="monitored") + buckets: dict[str, dict] = defaultdict(dict) + for (group, key), attr in NIDAQ_SW_FORWARD_SOURCES.items(): + buckets[group][key] = {"value": getattr(self, attr).get(), "timestamp": now} + for group, batch in buckets.items(): + # Annotate the alignment: these rows are in sync with the + # monitored readouts (cf. MCSCardCSAXS acquisition_group logic). + self._guarded_group_put(group, batch, acquisition_group="monitored") def _guarded_group_put( self, @@ -391,7 +422,7 @@ class Nidaq(PSIDeviceBase, NidaqControl): logger.info(f"Device {self.name}: unified bridge stopped") def _forward_backend_message(self, msg) -> None: - """Route one backend DeviceMessage into the statistic groups.""" + """Route one backend DeviceMessage into the per-channel groups.""" device_prefix = f"{self.name}_" buckets: dict[str, dict] = defaultdict(dict) now = time.time() @@ -405,8 +436,8 @@ class Nidaq(PSIDeviceBase, NidaqControl): f"Device {self.name}: skipping unknown backend channel '{signal_name}'" ) continue - group, group_channel = route - buckets[group][group_channel] = { + group, key = route + buckets[group][key] = { "value": data["value"], "timestamp": data.get("timestamp") or now, } diff --git a/debye_bec/file_writer/debye_nexus_structure.py b/debye_bec/file_writer/debye_nexus_structure.py index 20c32db..9ccea4f 100644 --- a/debye_bec/file_writer/debye_nexus_structure.py +++ b/debye_bec/file_writer/debye_nexus_structure.py @@ -236,10 +236,10 @@ class DebyeNexusStructure(DefaultFormat): self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value") ) - # The statistic groups are written by the async writer under the device itself, - # independent of the readout priority the device is configured with. Compression - # (RLE) no longer changes the name: per-oscillation means and raw readbacks both - # arrive in the 'mean' group. + # The per-channel async groups are written by the async writer under the device + # itself, using the FINAL channel-first names (identical to the backend stream). + # Compression (RLE) no longer changes the name: the bridge maps the bare no-RLE + # channel names onto the mean signals. nidaq_data = "/entry/collection/devices/nidaq" measurement_mode = entry.create_group(name="mode") @@ -303,9 +303,7 @@ class DebyeNexusStructure(DefaultFormat): energy.attrs["NX_class"] = "NXdata" energy.attrs["units"] = "eV" - main_data.create_soft_link( - name="energy", target=f"{nidaq_data}/nidaq_stream_energy/value" - ) + main_data.create_soft_link(name="energy", target=f"{nidaq_data}/nidaq_energy/value") ################## ## i0 @@ -316,7 +314,7 @@ class DebyeNexusStructure(DefaultFormat): i0.attrs["NX_class"] = "NXdata" i0.attrs["units"] = "V" - main_data.create_soft_link(name="i0", target=f"{nidaq_data}/nidaq_mean_ai0/value") + main_data.create_soft_link(name="i0", target=f"{nidaq_data}/nidaq_ai0_mean/value") ################## ## i1 @@ -327,7 +325,7 @@ class DebyeNexusStructure(DefaultFormat): i1.attrs["NX_class"] = "NXdata" i1.attrs["units"] = "V" - main_data.create_soft_link(name="i1", target=f"{nidaq_data}/nidaq_mean_ai2/value") + main_data.create_soft_link(name="i1", target=f"{nidaq_data}/nidaq_ai2_mean/value") ################## ## i2 @@ -338,7 +336,7 @@ class DebyeNexusStructure(DefaultFormat): i2.attrs["NX_class"] = "NXdata" i2.attrs["units"] = "V" - main_data.create_soft_link(name="i2", target=f"{nidaq_data}/nidaq_mean_ai4/value") + main_data.create_soft_link(name="i2", target=f"{nidaq_data}/nidaq_ai4_mean/value") ################## ## ci sum @@ -350,7 +348,7 @@ class DebyeNexusStructure(DefaultFormat): ci_sum.attrs["units"] = "counts" main_data.create_soft_link( - name="Fluorescence_Sum", target=f"{nidaq_data}/nidaq_stream_cisum/value" + name="Fluorescence_Sum", target=f"{nidaq_data}/nidaq_cisum/value" ) ################## @@ -362,7 +360,7 @@ class DebyeNexusStructure(DefaultFormat): mu_sample.attrs["NX_class"] = "NXdata" main_data.create_soft_link( - name="mu_sample", target=f"{nidaq_data}/nidaq_stream_smpl_abs/value" + name="mu_sample", target=f"{nidaq_data}/nidaq_smpl_abs/value" ) ################## @@ -374,7 +372,7 @@ class DebyeNexusStructure(DefaultFormat): mu_sample.attrs["NX_class"] = "NXdata" main_data.create_soft_link( - name="fluo_sample", target=f"{nidaq_data}/nidaq_stream_smpl_fluo/value" + name="fluo_sample", target=f"{nidaq_data}/nidaq_smpl_fluo/value" ) ################## @@ -386,5 +384,5 @@ class DebyeNexusStructure(DefaultFormat): mu_reference.attrs["NX_class"] = "NXdata" main_data.create_soft_link( - name="mu_reference", target=f"{nidaq_data}/nidaq_stream_ref_abs/value" + name="mu_reference", target=f"{nidaq_data}/nidaq_ref_abs/value" ) diff --git a/tests/tests_devices/test_nidaq.py b/tests/tests_devices/test_nidaq.py index fc04549..03f3ee2 100644 --- a/tests/tests_devices/test_nidaq.py +++ b/tests/tests_devices/test_nidaq.py @@ -184,119 +184,100 @@ def test_on_complete(mock_nidaq, scan_info_mock): assert status.done is True -########## DynamicSignal statistic groups ########## +########## per-channel DynamicSignal groups (channel-first naming) ########## -from ophyd import Kind +from types import SimpleNamespace + +from ophyd import Kind, Staged from ophyd_devices.utils.bec_signals import DynamicSignal -def test_statistic_groups_and_omitted_epics(mock_nidaq): - """Groups own the unified namespace; EPICS scalars are silent data sources. - - DynamicSignal is deliberate (per the NIDAQ maintainers, the active channel - set cannot be predicted upfront): channel names are validated, batch - completeness is not. - """ +def test_channel_groups_and_omitted_epics(mock_nidaq): + """Per-channel groups own the FINAL channel-first namespace; the EPICS + scalars are silent (omitted) data sources for the software forwarding.""" dev = mock_nidaq - assert isinstance(dev.mean, DynamicSignal) - assert dev.mean.strict_signal_validation is False - assert dev.mean.name == "nidaq_mean" - assert dev.max.name == "nidaq_max" - assert dev.std_dev.name == "nidaq_std_dev" - assert dev.stream.name == "nidaq_stream" - # EPICS scalars demoted: no longer in the readout namespace - assert dev.ai0.kind == Kind.omitted + assert isinstance(dev.ai0, DynamicSignal) + assert dev.ai0.name == "nidaq_ai0" + assert [name for name, _ in dev.di0_max.signals] == ["nidaq_di0_max"] + assert [name for name, _ in dev.energy.signals] == ["nidaq_energy"] + assert dev.ai0_epics.kind == Kind.omitted assert dev.enc_epics.kind == Kind.omitted assert "nidaq_ai0" not in dev.read() -########## software-mode forwarding into the statistic groups ########## - -from types import SimpleNamespace - -from ophyd import Staged - - 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.""" + """Software mode: ALL EPICS scalars forwarded under the channel-first names.""" dev = mock_nidaq - 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.ai0_epics._read_pv.mock_data = 5.0 + dev.ai1_epics._read_pv.mock_data = 99.0 + dev.ci3_epics._read_pv.mock_data = 42 + dev.di2_epics._read_pv.mock_data = 1 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 with mock.patch.object(dev.scan_info, "msg", SimpleNamespace(status="open")): - # not staged -> no forwarding dev.read() - assert dev.mean.get() is None + assert dev.ai0.get() is None # not staged dev._staged = Staged.yes - # backend-driven scan -> no forwarding (the hardware stream owns the groups) dev.read() - assert dev.mean.get() is None + assert dev.ai0.get() is None # backend-driven scan -> hardware path owns the groups - # 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 out.metadata["acquisition_group"] == "monitored" - 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 + assert dev.ai0.get().signals["nidaq_ai0_mean"]["value"] == 5.0 + assert dev.ai1.get().signals["nidaq_ai1_mean"]["value"] == 99.0 + assert dev.ci3.get().signals["nidaq_ci3_mean"]["value"] == 42 + assert dev.ai0.get().metadata["acquisition_group"] == "monitored" + assert dev.di2_max.get().signals["nidaq_di2_max"]["value"] == 1 + assert dev.energy.get().signals["nidaq_energy"]["value"] == 8000.0 + assert dev.enc.get().signals["nidaq_enc"]["value"] == 12.5 # no software source for standard deviations - assert dev.std_dev.get() is None - - # scan not open -> no forwarding - dev.ai0._read_pv.mock_data = 7.0 - with mock.patch.object(dev.scan_info, "msg", SimpleNamespace(status="closed")): - dev.read() - assert dev.mean.get().signals["nidaq_mean_ai0"]["value"] == 5.0 + assert "nidaq_ai0_std_dev" not in dev.ai0.get().signals dev._staged = Staged.no -def test_guarded_put_reports_unknown_names_once(mock_nidaq, scan_info_mock): - """An undeclared channel name is dropped and reported once per group per scan.""" +def test_bridge_routes_backend_names_to_channel_groups(mock_nidaq): + """Backend messages (both RLE and bare no-RLE names) land under the final names.""" + from bec_lib import messages + dev = mock_nidaq - dev._guarded_group_put("mean", {"bogus": {"value": [1.0], "timestamp": 1.0}}) - assert dev.mean.get() is None - assert "mean" in dev._group_put_errors - # staging for the next scan resets the reporting + msg = messages.DeviceMessage( + signals={ + "nidaq_ai0_mean": {"value": [1.0, 2.0]}, + "nidaq_ai0_std_dev": {"value": [0.1, 0.2]}, + "ci3": {"value": [7.0]}, # bare no-RLE name -> mean + "di2_max": {"value": [1.0]}, + "energy": {"value": [8000.0]}, + "xrd_ai0": {"value": [3.0]}, + "not_a_channel": {"value": [9.0]}, + }, + metadata={"async_update": {"type": "add_slice", "index": 4, "max_shape": [None, 57]}}, + ) + dev._forward_backend_message(msg) + assert dev.ai0.get().signals["nidaq_ai0_mean"]["value"] == [1.0, 2.0] + assert dev.ai0.get().signals["nidaq_ai0_std_dev"]["value"] == [0.1, 0.2] + assert dev.ai0.get().metadata["async_update"]["type"] == "add_slice" + assert dev.ci3.get().signals["nidaq_ci3_mean"]["value"] == [7.0] + assert dev.di2_max.get().signals["nidaq_di2_max"]["value"] == [1.0] + assert dev.energy.get().signals["nidaq_energy"]["value"] == [8000.0] + assert dev.xrd_ai0.get().signals["nidaq_xrd_ai0"]["value"] == [3.0] + assert "not_a_channel" in dev._unknown_bridge_channels + + +def test_guarded_put_reports_unknown_names_once(mock_nidaq, scan_info_mock): + dev = mock_nidaq + dev._guarded_group_put("ai0", {"bogus": {"value": [1.0], "timestamp": 1.0}}) + assert dev.ai0.get() is None + assert "ai0" in dev._group_put_errors scan_info_mock.scan_name = "line_scan" dev.scan_info.msg.info.update(scan_info_mock.model_dump()) dev.on_stage() # returns early: not a backend-driven scan assert dev._group_put_errors == set() -def test_group_put_notifies_subscribers(mock_nidaq): - """The device server publishes to the per-signal device_async_signal endpoint - from BECMessageSignal subscriptions; a put must notify subscribers.""" - dev = mock_nidaq - received = [] - dev.mean.subscribe(lambda *a, **kw: received.append(kw.get("value")), run=False) - dev._guarded_group_put("mean", {"ai0": {"value": 5.0, "timestamp": 1.0}}) - assert len(received) == 1 - assert received[0].signals["nidaq_mean_ai0"]["value"] == 5.0 - - def test_device_manager_is_an_explicit_init_parameter(): """The device server injects device_manager only if it is in the signature; without it the unified bridge has no connector and silently never starts."""