From ef0c31c8dc403af45166ffd83b69268e39ec2152 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 8 Dec 2025 17:37:39 +0100 Subject: [PATCH] refactor(mcs-card): adjust mcs card to only have mca channels. --- .../devices/epics/mcs_card/mcs_card_csaxs.py | 470 ++++++++++++------ 1 file changed, 320 insertions(+), 150 deletions(-) diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py index 91149eb..066c311 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -2,16 +2,17 @@ from __future__ import annotations -import enum +import time from threading import RLock -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import numpy as np from bec_lib.logger import bec_logger from ophyd import Component as Cpt -from ophyd import Device, EpicsSignalRO, Kind, Signal -from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus +from ophyd import EpicsSignalRO, Kind, SignalRO +from ophyd_devices import AsyncSignal, CompareStatus, ProgressSignal, TransitionStatus from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from ophyd_devices.utils.bec_signals import AsyncMultiSignal from csaxs_bec.devices.epics.mcs_card.mcs_card import ( ACQUIREMODE, @@ -24,7 +25,6 @@ from csaxs_bec.devices.epics.mcs_card.mcs_card import ( READMODE, MCSCard, ) -from csaxs_bec.devices.epics.xbpms import DiffXYSignal, SumSignal if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import DeviceManagerBase, ScanInfo @@ -32,81 +32,289 @@ if TYPE_CHECKING: # pragma: no cover logger = bec_logger.logger -class READYTOREAD(int, enum.Enum): - - PROCESSING = 0 - DONE = 1 - - -class BPMDevice(Device): - """Class for BPM device of the MCSCard.""" - - current1 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 1") - current2 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 2") - current3 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 3") - current4 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 4") - count_time = Cpt(Signal, kind=Kind.normal, doc="Count time for bpm signal counts") - sum = Cpt(SumSignal, kind="hinted", doc="Sum of all currents") - x = Cpt( - DiffXYSignal, - sum1=["current1", "current2"], - sum2=["current3", "current4"], - doc="X difference signal", - ) - y = Cpt( - DiffXYSignal, - sum1=["current1", "current3"], - sum2=["current2", "current4"], - doc="Y difference signal", - ) - diag = Cpt( - DiffXYSignal, - sum1=["current1", "current4"], - sum2=["current2", "current3"], - doc="Diagonal difference signal", - ) - - -class MCSRaw(Device): - """Class for BPM device of the MCSCard with normalized currents.""" - - mca1 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca1 channel") - mca2 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca2 channel") - mca3 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca3 channel") - mca4 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca4 channel") - mca5 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca5 channel") - - class MCSCardCSAXS(PSIDeviceBase, MCSCard): """ Implementation of the MCSCard SIS3820 for CSAXS, prefix 'X12SA-MCS:'. The basic functionality is inherited from the MCSCard class. """ - ready_to_read = Cpt( - Signal, - kind=Kind.omitted, - doc="Signal that indicates if mcs card is ready to be read from after triggers. 0 not ready, 1 ready", - ) - progress: ProgressSignal = Cpt(ProgressSignal, name="progress") - # Make this an async signal.. - mcs = Cpt( - MCSRaw, - name="mcs", + # All counter from the MCS card. + # mca = Cpt( + # AsyncMultiSignal, + # name="counters", + # signals=[ + # f"mca{i}" for i in range(1, 33) + # ], # This needs to be in sync with counters DynamicDeviceComponent + # ndim=0, + # async_update={"type": "add", "max_shape": [None]}, + # max_size=1000, + # kind=Kind.normal, + # doc="AsyncMultiSignal for MCA card channels 1-32", + # ) + mca1 = Cpt( + AsyncSignal, + name="mca1", kind=Kind.normal, - doc="MCS device with raw current and count time readings", + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 1", ) - bpm = Cpt( - BPMDevice, - name="bpm", + mca2 = Cpt( + AsyncSignal, + name="mca2", kind=Kind.normal, - doc="BPM device for MCSCard with count times and normalized currents", + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 2", ) + mca3 = Cpt( + AsyncSignal, + name="mca3", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 3", + ) + mca4 = Cpt( + AsyncSignal, + name="mca4", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 4", + ) + mca5 = Cpt( + AsyncSignal, + name="mca5", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 5", + ) + mca6 = Cpt( + AsyncSignal, + name="mca6", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 6", + ) + mca7 = Cpt( + AsyncSignal, + name="mca7", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 7", + ) + mca8 = Cpt( + AsyncSignal, + name="mca8", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 8", + ) + mca9 = Cpt( + AsyncSignal, + name="mca9", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 9", + ) + mca10 = Cpt( + AsyncSignal, + name="mca10", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 10", + ) + mca11 = Cpt( + AsyncSignal, + name="mca11", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 11", + ) + mca12 = Cpt( + AsyncSignal, + name="mca12", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 12", + ) + mca13 = Cpt( + AsyncSignal, + name="mca13", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 13", + ) + mca14 = Cpt( + AsyncSignal, + name="mca14", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 14", + ) + mca15 = Cpt( + AsyncSignal, + name="mca15", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 15", + ) + mca16 = Cpt( + AsyncSignal, + name="mca16", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 16", + ) + mca17 = Cpt( + AsyncSignal, + name="mca17", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 17", + ) + mca18 = Cpt( + AsyncSignal, + name="mca18", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 18", + ) + mca19 = Cpt( + AsyncSignal, + name="mca19", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 19", + ) + mca20 = Cpt( + AsyncSignal, + name="mca20", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 20", + ) + mca21 = Cpt( + AsyncSignal, + name="mca21", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 21", + ) + mca22 = Cpt( + AsyncSignal, + name="mca22", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 22", + ) + mca23 = Cpt( + AsyncSignal, + name="mca23", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 23", + ) + mca24 = Cpt( + AsyncSignal, + name="mca24", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 24", + ) + mca25 = Cpt( + AsyncSignal, + name="mca25", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 25", + ) + mca26 = Cpt( + AsyncSignal, + name="mca26", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 26", + ) + mca27 = Cpt( + AsyncSignal, + name="mca27", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 27", + ) + mca28 = Cpt( + AsyncSignal, + name="mca28", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 28", + ) + mca29 = Cpt( + AsyncSignal, + name="mca29", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 29", + ) + mca30 = Cpt( + AsyncSignal, + name="mca30", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 30", + ) + mca31 = Cpt( + AsyncSignal, + name="mca31", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 31", + ) + mca32 = Cpt( + AsyncSignal, + name="mca32", + kind=Kind.normal, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + doc="AsyncSignal for MCA channel 32", + ) + # Progress Signal + progress = Cpt(ProgressSignal, doc="ProgressSignal indicating the progress of the device") def __init__( self, name: str, prefix: str = "", + num_connected_channels: int = 5, scan_info: ScanInfo | None = None, device_manager: DeviceManagerBase | None = None, **kwargs, @@ -118,16 +326,14 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs ) self._mcs_clock = 1e7 # 10MHz clock -> 1e7 Hz - self._pv_timeout = 3 # TODO remove timeout once #129 in ophyd_devices is solved + self._pv_timeout = 2.0 # seconds self._rlock = RLock() # Needed to ensure thread safety for counter updates - self.counter_mapping = { # Any mca counter that should be updated has to be added here - f"{self.counters.name}_mca1": "current1", - f"{self.counters.name}_mca2": "current2", - f"{self.counters.name}_mca3": "current3", - f"{self.counters.name}_mca4": "current4", - f"{self.counters.name}_mca5": "count_time", - } - self.counter_updated = [] + self.num_connected_channels = num_connected_channels + self._received_updates: dict[ + str, dict[Literal["value", "timestamp"], list[int] | float] + ] = {} + self._acquisition_group: str = "monitored" + self._num_total_triggers: int = 0 def on_connected(self): """ @@ -136,14 +342,15 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): # Make sure card is not running self.stop_all.put(1) - # TODO Check channel1_source !! + # Setup the MCS card settings self.channel_advance.set(CHANNELADVANCE.EXTERNAL).wait(timeout=self._pv_timeout) self.channel1_source.set(CHANNEL1SOURCE.EXTERNAL).wait(timeout=self._pv_timeout) self.prescale.set(1).wait(timeout=self._pv_timeout) - # Set the user LED to off self.user_led.set(0).wait(timeout=self._pv_timeout) - # Only channel 1-5 are connected so far, adjust if more are needed - self.mux_output.set(5).wait(timeout=self._pv_timeout) + + # Set mux_output to number of connected channels. Connect channels in increasing order + self.mux_output.set(self.num_connected_channels).wait(timeout=self._pv_timeout) + # Set the input and output modes & polarities self.input_mode.set(INPUTMODE.MODE_3).wait(timeout=self._pv_timeout) self.input_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout) @@ -151,90 +358,60 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self.output_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout) self.count_on_start.set(0).wait(timeout=self._pv_timeout) - # Set appropriate read mode + # Set ReadMode to PASSIVE, card will wait for external trigger to be read self.read_mode.set(READMODE.PASSIVE).wait(timeout=self._pv_timeout) # Set the acquire mode self.acquire_mode.set(ACQUIREMODE.MCS).wait(timeout=self._pv_timeout) # Subscribe the progress signal - # self.current_channel.subscribe(self._progress_update, run=False) + self.current_channel.subscribe(self._progress_update, run=False) # Subscribe to the mca updates - for name in self.counter_mapping.keys(): - sig: EpicsSignalRO = getattr(self.counters, name.split("_")[-1]) - sig.subscribe(self._on_counter_update, run=False) + for sig in self.counters.component_names: + sig_obj: EpicsSignalRO = getattr(self.counters, sig) + sig_obj.subscribe(self._on_counter_update, run=False) def _on_counter_update(self, value, **kwargs) -> None: """ Callback for counter updates of the mca channels (1-32). - The raw data is pushed to the mcs sub-device (MCSRaw). We need to ensure that - the MCSRaw device has all signals defined for which we want to push the values. + Data from the mca channels will be pushed to a list, and then forwarded to + the async multi signal 'raw' for readout after the trigger is complete. - As we may receive multiple readings per point, e.g. if frames_per_trigger > 1, - we also create a mean value for the counter signals. These are then pushed to the bpm device - for plotting and further processing. The signal names are defined and mapped in the - self.counter_mapping dictionary & the bpm sub-device. - - There are multiple mca channels, each giving individual updates. We want to ensure that - each is updated before we signal that we are ready to read. In future, these signals may - become asynchronous, but we first need to ensure that we can properly combine monitored - signals with async signals for plotting. Until then, we will keep this logic. """ with self._rlock: - # Retrieve the signal object which executes this callback signal = kwargs.get("obj", None) - if signal is None: # This should never happen, but just in case - logger.info(f"Called without 'obj' in kwargs: {kwargs}") + if signal is None: + logger.error(f"Called without 'obj' in kwargs: {kwargs}") return - # Get the maped signal name from the mapping dictionary - mapped_signal_name = self.counter_mapping.get(signal.name, None) - # If we did not map the signal name in counter_mapping, but receive an update - # we will skip it. - if mapped_signal_name is None: + attr_name = signal.name + mca_channel = getattr(self, attr_name, None) + if mca_channel is None: + logger.error(f"Could not find matching MCA channel for signal {signal.name}") return - # Push the raw values of the mca channels. The signal name has to be defined - # in the self.mcs sub-device (MCSRaw) to be able to push the values. Otherwise - # we will skip the update. - mca_raw = getattr(self.mcs, signal.name.split("_")[-1], None) - if mca_raw is None: - return - # In case there was more than one value received, i.e. frames_per_trigger > 1, - # we will receive a np.array of values. - if isinstance(value, np.ndarray): - # We push the raw values as a list to the mca_raw signal - # And otherwise compute the mean value for plotting of counter signals - mca_raw.put(value.tolist()) - # compute the count_time in seconds - if mapped_signal_name == "count_time": - value = value / self._mcs_clock - value = float(value.mean()) - else: - # We received a single value, so we can directly push it - mca_raw.put(value) - # compute the count_time in seconds - if mapped_signal_name == "count_time": - value = value / self._mcs_clock + mca_channel: AsyncSignal - # Get the mapped signal from the bpm device and update it - sig = getattr(self.bpm, mapped_signal_name) - sig.put(value) - self.counter_updated.append(signal.name) - # Once all mca channels have been updated, we can signal that we are ready to read - received_all_updates = set(self.counter_updated) == set(self.counter_mapping.keys()) - if received_all_updates: - self.ready_to_read.put(READYTOREAD.DONE) - # The reset of the signal is done in the on_trigger method of ddg1 for the next trigger - self.counter_updated.clear() # Clear the list for the next update cycle + if isinstance(value, np.ndarray): + value = value.tolist() # Convert numpy array to list + else: + value = [value] # Received single value, convert to list + data = { + attr_name: {"value": value, "timestamp": kwargs.get("timestamp") or time.time()} + } + mca_channel.put(data) + # self._received_updates.update(data) + # if len(self._received_updates) == self.num_connected_channels: + # # Send out data on multi async signal + # self.mca.put(self._received_updates, acquisition_group=self._acquisition_group) + # self._received_updates.clear() def _progress_update(self, value, **kwargs) -> None: """Callback for progress updates from ophyd subscription on current_channel.""" - # This logic needs to be further refined as this is currently reporting the progress - # of a single trigger from BEC within a burst scan. - frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) self.progress.put( - value=value, max_value=frames_per_trigger, done=bool(value == frames_per_trigger) + value=value, + max_value=self._num_total_triggers, + done=bool(value == self._num_total_triggers), ) def on_stage(self) -> None: @@ -243,25 +420,21 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): """ self.erase_all.set(1).wait(timeout=self._pv_timeout) triggers = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1) + num_points = self.scan_info.msg.num_points + self._num_total_triggers = triggers * num_points + self._acquisition_group = "monitored" if triggers == 1 else "burst_group" self.preset_real.set(0).wait(timeout=self._pv_timeout) self.num_use_all.set(triggers).wait(timeout=self._pv_timeout) + # Reset data + self._received_updates.clear() def on_unstage(self) -> None: """ Called when the device is unstaged. """ self.stop_all.put(1) - self.ready_to_read.put(READYTOREAD.DONE) - # TODO why 0? self.erase_all.set(0).wait(timeout=self._pv_timeout) - def on_trigger(self) -> None: - status = TransitionStatus( - self.ready_to_read, strict=True, transitions=[READYTOREAD.PROCESSING, READYTOREAD.DONE] - ) - self.cancel_on_stop(status) - return status - def on_pre_scan(self) -> None: """ Called before the scan starts. @@ -279,6 +452,3 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): Called when the scan is stopped. """ self.stop_all.put(1) - self.ready_to_read.put(READYTOREAD.DONE) - # Reset the progress signal - # self.progress.put(0, done=True)