logic for gain setting and readback for bpm amplifiers
This commit is contained in:
443
csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py
Normal file
443
csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py
Normal file
@@ -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())
|
||||
Reference in New Issue
Block a user