diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py new file mode 100644 index 0000000..8796869 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py @@ -0,0 +1,443 @@ +""" +csaxs_dlpca200.py +================= +BEC control script for FEMTO DLPCA-200 Variable Gain Low Noise Current Amplifiers +connected to Galil RIO digital outputs. + +DLPCA-200 Remote Control (datasheet page 4) +------------------------------------------- +Sub-D pin → function: + Pin 10 → gain LSB (digital out channel, index 0 in bit-tuple) + Pin 11 → gain MID (digital out channel, index 1 in bit-tuple) + Pin 12 → gain MSB (digital out channel, index 2 in bit-tuple) + Pin 13 → coupling LOW = AC, HIGH = DC + Pin 14 → speed mode HIGH = low noise (Pin14=1), LOW = high speed (Pin14=0) + +Gain truth table (MSB, MID, LSB): + 0,0,0 → low-noise: 1e3 high-speed: 1e5 + 0,0,1 → low-noise: 1e4 high-speed: 1e6 + 0,1,0 → low-noise: 1e5 high-speed: 1e7 + 0,1,1 → low-noise: 1e6 high-speed: 1e8 + 1,0,0 → low-noise: 1e7 high-speed: 1e9 + 1,0,1 → low-noise: 1e8 high-speed: 1e10 + 1,1,0 → low-noise: 1e9 high-speed: 1e11 + +Strategy: prefer low-noise mode (1e3–1e9). For 1e10 and 1e11, +automatically fall back to high-speed mode. + +Device wiring example (galilrioesxbox): + bpm4: Pin10→ch0, Pin11→ch1, Pin12→ch2, Pin13→ch3, Pin14→ch4 + bim: Pin10→ch6, Pin11→ch7, Pin12→ch8, Pin13→ch9, Pin14→ch10 + +Usage examples +-------------- + csaxs_amp = cSAXSDLPCA200(client) + + csaxs_amp.set_gain("bpm4", 1e7) # low-noise if possible + csaxs_amp.set_gain("bim", 1e10) # auto high-speed + csaxs_amp.set_coupling("bpm4", "DC") + csaxs_amp.set_coupling("bim", "AC") + csaxs_amp.info("bpm4") # print current settings + csaxs_amp.info_all() # print all configured amplifiers +""" + +import builtins + +from bec_lib import bec_logger + +logger = bec_logger.logger + +bec = builtins.__dict__.get("bec") +dev = builtins.__dict__.get("dev") + + +# --------------------------------------------------------------------------- +# Amplifier registry +# --------------------------------------------------------------------------- +# Each entry describes one DLPCA-200 amplifier connected to a Galil RIO. +# +# Keys inside "channels": +# gain_lsb → digital output channel number wired to DLPCA-200 Pin 10 +# gain_mid → digital output channel number wired to DLPCA-200 Pin 11 +# gain_msb → digital output channel number wired to DLPCA-200 Pin 12 +# coupling → digital output channel number wired to DLPCA-200 Pin 13 +# speed_mode → digital output channel number wired to DLPCA-200 Pin 14 +# +# To add a new amplifier, simply extend this dict. +# --------------------------------------------------------------------------- +DLPCA200_AMPLIFIER_CONFIG: dict[str, dict] = { + "bpm4": { + "rio_device": "galilrioesxbox", + "description": "Beam Position Monitor 4 current amplifier", + "channels": { + "gain_lsb": 0, # Pin 10 → Galil ch0 + "gain_mid": 1, # Pin 11 → Galil ch1 + "gain_msb": 2, # Pin 12 → Galil ch2 + "coupling": 3, # Pin 13 → Galil ch3 + "speed_mode": 4, # Pin 14 → Galil ch4 + }, + }, + "bim": { + "rio_device": "galilrioesxbox", + "description": "Beam Intensity Monitor current amplifier", + "channels": { + "gain_lsb": 6, # Pin 10 → Galil ch6 + "gain_mid": 7, # Pin 11 → Galil ch7 + "gain_msb": 8, # Pin 12 → Galil ch8 + "coupling": 9, # Pin 13 → Galil ch9 + "speed_mode": 10, # Pin 14 → Galil ch10 + }, + }, +} + +# --------------------------------------------------------------------------- +# DLPCA-200 gain encoding tables +# --------------------------------------------------------------------------- +# (msb, mid, lsb) → gain in V/A +_GAIN_BITS_LOW_NOISE: dict[tuple, int] = { + (0, 0, 0): int(1e3), + (0, 0, 1): int(1e4), + (0, 1, 0): int(1e5), + (0, 1, 1): int(1e6), + (1, 0, 0): int(1e7), + (1, 0, 1): int(1e8), + (1, 1, 0): int(1e9), +} + +_GAIN_BITS_HIGH_SPEED: dict[tuple, int] = { + (0, 0, 0): int(1e5), + (0, 0, 1): int(1e6), + (0, 1, 0): int(1e7), + (0, 1, 1): int(1e8), + (1, 0, 0): int(1e9), + (1, 0, 1): int(1e10), + (1, 1, 0): int(1e11), +} + +# Inverse maps: gain → (msb, mid, lsb, low_noise_flag) +# low_noise_flag: True = Pin14 HIGH, False = Pin14 LOW +_GAIN_TO_BITS: dict[int, tuple] = {} +for _bits, _gain in _GAIN_BITS_LOW_NOISE.items(): + _GAIN_TO_BITS[_gain] = (*_bits, True) +for _bits, _gain in _GAIN_BITS_HIGH_SPEED.items(): + if _gain not in _GAIN_TO_BITS: # low-noise takes priority + _GAIN_TO_BITS[_gain] = (*_bits, False) + +VALID_GAINS = sorted(_GAIN_TO_BITS.keys()) + + +class cSAXSDLPCA200Error(Exception): + pass + + +class cSAXSDLPCA200: + """ + Control class for FEMTO DLPCA-200 current amplifiers connected via Galil RIO + digital outputs in a BEC environment. + + Supports: + - Forward control: set_gain(), set_coupling() + - Readback reporting: info(), info_all(), read_settings() + - Robust error handling and logging following cSAXS conventions. + """ + + TAG = "[DLPCA200]" + + def __init__(self, client, config: dict | None = None) -> None: + """ + Parameters + ---------- + client : BEC client object (passed through for future use) + config : optional override for DLPCA200_AMPLIFIER_CONFIG. + Falls back to the module-level dict if not provided. + """ + self.client = client + self._config: dict[str, dict] = config if config is not None else DLPCA200_AMPLIFIER_CONFIG + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _require_dev(self) -> None: + if dev is None: + raise cSAXSDLPCA200Error( + f"{self.TAG} BEC 'dev' namespace is not available in this session." + ) + + def _get_cfg(self, amp_name: str) -> dict: + """Return config dict for a named amplifier, raising on unknown names.""" + if amp_name not in self._config: + known = ", ".join(sorted(self._config.keys())) + raise cSAXSDLPCA200Error( + f"{self.TAG} Unknown amplifier '{amp_name}'. Known: [{known}]" + ) + return self._config[amp_name] + + def _get_rio(self, amp_name: str): + """Return the live RIO device object for a given amplifier.""" + self._require_dev() + cfg = self._get_cfg(amp_name) + rio_name = cfg["rio_device"] + try: + rio = getattr(dev, rio_name) + except AttributeError: + raise cSAXSDLPCA200Error( + f"{self.TAG} RIO device '{rio_name}' not found in BEC 'dev'." + ) + return rio + + def _dout_get(self, rio, ch: int) -> int: + """Read one digital output channel (returns 0 or 1).""" + attr = getattr(rio.digital_out, f"ch{ch}") + val = attr.get() + return int(val) + + def _dout_set(self, rio, ch: int, value: bool) -> None: + """Write one digital output channel (True=HIGH=1, False=LOW=0).""" + attr = getattr(rio.digital_out, f"ch{ch}") + attr.set(value) + + def _read_gain_bits(self, amp_name: str) -> tuple[int, int, int, int]: + """ + Read current gain bit-state from hardware. + + Returns + ------- + (msb, mid, lsb, speed_mode) + speed_mode: 1 = low-noise (Pin14=HIGH), 0 = high-speed (Pin14=LOW) + """ + rio = self._get_rio(amp_name) + ch = self._get_cfg(amp_name)["channels"] + msb = self._dout_get(rio, ch["gain_msb"]) + mid = self._dout_get(rio, ch["gain_mid"]) + lsb = self._dout_get(rio, ch["gain_lsb"]) + speed_mode = self._dout_get(rio, ch["speed_mode"]) + return msb, mid, lsb, speed_mode + + def _decode_gain(self, msb: int, mid: int, lsb: int, speed_mode: int) -> int | None: + """ + Decode hardware bit-state into gain value (V/A). + + speed_mode=1 → low-noise table, speed_mode=0 → high-speed table. + Returns None if the bit combination is not in the table. + """ + bits = (msb, mid, lsb) + if speed_mode: + return _GAIN_BITS_LOW_NOISE.get(bits) + else: + return _GAIN_BITS_HIGH_SPEED.get(bits) + + # ------------------------------------------------------------------ + # Public API – control + # ------------------------------------------------------------------ + + def set_gain(self, amp_name: str, gain: float, force_high_speed: bool = False) -> None: + """ + Set the transimpedance gain of a DLPCA-200 amplifier. + + The method automatically selects low-noise mode (Pin14=HIGH) whenever + the requested gain is achievable in low-noise mode (1e3 – 1e9 V/A). + For gains of 1e10 and 1e11 V/A, high-speed mode is used automatically. + + Parameters + ---------- + amp_name : str + Amplifier name as defined in DLPCA200_AMPLIFIER_CONFIG (e.g. "bpm4"). + gain : float or int + Target gain in V/A. Must be one of: + 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11. + force_high_speed : bool, optional + If True, force high-speed (low-noise=False) mode even for gains + below 1e10. Default: False (prefer low-noise). + + Examples + -------- + csaxs_amp.set_gain("bpm4", 1e7) # low-noise mode (automatic) + csaxs_amp.set_gain("bim", 1e10) # high-speed mode (automatic) + csaxs_amp.set_gain("bpm4", 1e7, force_high_speed=True) # override to high-speed + """ + gain_int = int(gain) + if gain_int not in _GAIN_TO_BITS: + valid_str = ", ".join(f"1e{int(round(__import__('math').log10(g)))}" for g in VALID_GAINS) + raise cSAXSDLPCA200Error( + f"{self.TAG} Invalid gain {gain:.2e} V/A for '{amp_name}'. " + f"Valid values: {valid_str}" + ) + + msb, mid, lsb, low_noise_preferred = _GAIN_TO_BITS[gain_int] + + # Apply force_high_speed override + if force_high_speed and low_noise_preferred: + # Check if this gain is achievable in high-speed mode + hs_entry = next( + (bits for bits, g in _GAIN_BITS_HIGH_SPEED.items() if g == gain_int), None + ) + if hs_entry is None: + raise cSAXSDLPCA200Error( + f"{self.TAG} Gain {gain:.2e} V/A is not achievable in high-speed mode " + f"for '{amp_name}'." + ) + msb, mid, lsb = hs_entry + low_noise_preferred = False + + use_low_noise = low_noise_preferred and not force_high_speed + + try: + rio = self._get_rio(amp_name) + ch = self._get_cfg(amp_name)["channels"] + + self._dout_set(rio, ch["gain_msb"], bool(msb)) + self._dout_set(rio, ch["gain_mid"], bool(mid)) + self._dout_set(rio, ch["gain_lsb"], bool(lsb)) + self._dout_set(rio, ch["speed_mode"], use_low_noise) # True=low-noise + + mode_str = "low-noise" if use_low_noise else "high-speed" + logger.info( + f"{self.TAG} [{amp_name}] gain set to {gain_int:.2e} V/A " + f"({mode_str} mode, bits MSB={msb} MID={mid} LSB={lsb})" + ) + print( + f"{amp_name}: gain → {gain_int:.2e} V/A [{mode_str}] " + f"(bits: MSB={msb} MID={mid} LSB={lsb})" + ) + + except cSAXSDLPCA200Error: + raise + except Exception as exc: + raise cSAXSDLPCA200Error( + f"{self.TAG} Failed to set gain on '{amp_name}': {exc}" + ) from exc + + def set_coupling(self, amp_name: str, coupling: str) -> None: + """ + Set AC or DC coupling on a DLPCA-200 amplifier. + + Parameters + ---------- + amp_name : str + Amplifier name (e.g. "bpm4", "bim"). + coupling : str + "AC" or "DC" (case-insensitive). + DC → Pin13 HIGH, AC → Pin13 LOW. + + Examples + -------- + csaxs_amp.set_coupling("bpm4", "DC") + csaxs_amp.set_coupling("bim", "AC") + """ + coupling_upper = coupling.strip().upper() + if coupling_upper not in ("AC", "DC"): + raise cSAXSDLPCA200Error( + f"{self.TAG} Invalid coupling '{coupling}' for '{amp_name}'. " + f"Use 'AC' or 'DC'." + ) + + pin13_high = coupling_upper == "DC" + + try: + rio = self._get_rio(amp_name) + ch = self._get_cfg(amp_name)["channels"] + self._dout_set(rio, ch["coupling"], pin13_high) + + logger.info(f"{self.TAG} [{amp_name}] coupling set to {coupling_upper}") + print(f"{amp_name}: coupling → {coupling_upper}") + + except cSAXSDLPCA200Error: + raise + except Exception as exc: + raise cSAXSDLPCA200Error( + f"{self.TAG} Failed to set coupling on '{amp_name}': {exc}" + ) from exc + + # ------------------------------------------------------------------ + # Public API – readback / reporting + # ------------------------------------------------------------------ + + def read_settings(self, amp_name: str) -> dict: + """ + Read back the current settings from hardware digital outputs. + + Returns + ------- + dict with keys: + "amp_name" : str + "gain" : int or None – gain in V/A (None if unknown bit pattern) + "mode" : str – "low-noise" or "high-speed" + "coupling" : str – "AC" or "DC" + "bits" : dict – raw bit values {msb, mid, lsb, speed_mode, coupling} + """ + rio = self._get_rio(amp_name) + ch = self._get_cfg(amp_name)["channels"] + + msb = self._dout_get(rio, ch["gain_msb"]) + mid = self._dout_get(rio, ch["gain_mid"]) + lsb = self._dout_get(rio, ch["gain_lsb"]) + speed_mode = self._dout_get(rio, ch["speed_mode"]) + coupling_bit = self._dout_get(rio, ch["coupling"]) + + gain = self._decode_gain(msb, mid, lsb, speed_mode) + mode = "low-noise" if speed_mode else "high-speed" + coupling = "DC" if coupling_bit else "AC" + + return { + "amp_name": amp_name, + "gain": gain, + "mode": mode, + "coupling": coupling, + "bits": { + "msb": msb, + "mid": mid, + "lsb": lsb, + "speed_mode": speed_mode, + "coupling": coupling_bit, + }, + } + + def info(self, amp_name: str) -> None: + """ + Print a plain summary of the current settings for one amplifier. + + Example output + -------------- + Amplifier : bpm4 + Description : Beam Position Monitor 4 current amplifier + RIO device : galilrioesxbox + Gain : 1.00e+07 V/A + Mode : low-noise + Coupling : DC + Raw bits : MSB=1 MID=0 LSB=0 speed=1 coup=1 + """ + cfg = self._get_cfg(amp_name) + + try: + s = self.read_settings(amp_name) + except Exception as exc: + print(f"{self.TAG} [{amp_name}] Could not read settings: {exc}") + return + + gain_str = ( + f"{s['gain']:.2e} V/A" if s["gain"] is not None else "UNKNOWN (invalid bit pattern)" + ) + bits = s["bits"] + + print(f" {'Amplifier':<12}: {amp_name}") + print(f" {'Description':<12}: {cfg.get('description', '')}") + print(f" {'RIO device':<12}: {cfg['rio_device']}") + print(f" {'Gain':<12}: {gain_str}") + print(f" {'Mode':<12}: {s['mode']}") + print(f" {'Coupling':<12}: {s['coupling']}") + print(f" {'Raw bits':<12}: MSB={bits['msb']} MID={bits['mid']} LSB={bits['lsb']} speed={bits['speed_mode']} coup={bits['coupling']}") + + def info_all(self) -> None: + """ + Print a plain summary for ALL configured amplifiers. + """ + print("\nDLPCA-200 Amplifier Status Report") + print("-" * 40) + for amp_name in sorted(self._config.keys()): + self.info(amp_name) + print() + + def list_amplifiers(self) -> list[str]: + """Return sorted list of configured amplifier names.""" + return sorted(self._config.keys()) \ No newline at end of file