From 49c2ecbf743dc0d1a66f65429767ff5f9ab59605 Mon Sep 17 00:00:00 2001 From: hitz_s Date: Thu, 22 Oct 2026 08:00:41 +0200 Subject: [PATCH] feat(icx): Added ionization chambers in preparation for auto_gain --- .../x10da_experimental_hutch.yaml | 30 ++ .../devices/ionization_chambers/__init__.py | 0 .../ionization_chambers/ionization_chamber.py | 411 ++++++++++++++++++ .../ionization_chamber_enums.py | 101 +++++ 4 files changed, 542 insertions(+) create mode 100644 superxas_bec/devices/ionization_chambers/__init__.py create mode 100644 superxas_bec/devices/ionization_chambers/ionization_chamber.py create mode 100644 superxas_bec/devices/ionization_chambers/ionization_chamber_enums.py diff --git a/superxas_bec/device_configs/x10da_experimental_hutch.yaml b/superxas_bec/device_configs/x10da_experimental_hutch.yaml index 71a085d..24427f0 100644 --- a/superxas_bec/device_configs/x10da_experimental_hutch.yaml +++ b/superxas_bec/device_configs/x10da_experimental_hutch.yaml @@ -70,6 +70,36 @@ es1ic2_try: enabled: true softwareTrigger: false +ic0: + readoutPriority: baseline + description: Ionization chamber 0 + deviceClass: superxas_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0 + deviceConfig: + prefix: "X10DA-" + onFailure: retry + enabled: true + softwareTrigger: false + +ic1: + readoutPriority: baseline + description: Ionization chamber 1 + deviceClass: superxas_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1 + deviceConfig: + prefix: "X10DA-" + onFailure: retry + enabled: true + softwareTrigger: false + +ic2: + readoutPriority: baseline + description: Ionization chamber 2 + deviceClass: superxas_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2 + deviceConfig: + prefix: "X10DA-" + onFailure: retry + enabled: true + softwareTrigger: false + ################################### ## Sample Manipulator ## ################################### diff --git a/superxas_bec/devices/ionization_chambers/__init__.py b/superxas_bec/devices/ionization_chambers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/superxas_bec/devices/ionization_chambers/ionization_chamber.py b/superxas_bec/devices/ionization_chambers/ionization_chamber.py new file mode 100644 index 0000000..2775f01 --- /dev/null +++ b/superxas_bec/devices/ionization_chambers/ionization_chamber.py @@ -0,0 +1,411 @@ +"""Ionization chamber device class""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +from bec_lib.logger import bec_logger +from ophyd import DynamicDeviceComponent as Dcpt +from ophyd import EpicsSignal, EpicsSignalRO +from ophyd_devices import CompareStatus +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase + +from superxas_bec.devices.ionization_chambers.ionization_chamber_enums import ( + GAIN_MAP, + AmplifierBias, + AmplifierError, + AmplifierFilter, + AmplifierFilterAutoFilt, + AmplifierFilterStatus, + AmplifierGain10X, + AmplifierSuppr, + AmplifierZeroCheck, +) + +if TYPE_CHECKING: # pragma: no cover + from bec_lib.devicemanager import ScanInfo + +logger = bec_logger.logger + + +class IonizationChamberError(Exception): + """Exception for the Ionization Chambers""" + + +class IonizationChamber0(PSIDeviceBase): + """Ionization Chamber 0, prefix should be 'X10DA-'.""" + + USER_ACCESS = ["set_gain", "set_filter"] + + num = 1 + amp_signals = { + "cGain_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getGain", + "write_pv": f"ES-KEITH{num}:setGain", + "kind": "config", + "doc": f"Gain of Keithley {num} -> IC{num-1}", + }, + ), + "cGain10X_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getGain10X", + "write_pv": f"ES-KEITH{num}:setGain10X", + "kind": "config", + "doc": f"Gain 10x of Keithley {num} -> IC{num-1}", + }, + ), + "cFilterStatus_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getFilter", + "write_pv": f"ES-KEITH{num}:setFilter", + "kind": "config", + "doc": f"Filter Status of Keithley {num} -> IC{num-1}", + }, + ), + "cFilter_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getRiseTime", + "write_pv": f"ES-KEITH{num}:setRiseTime", + "kind": "config", + "doc": f"Filter of Keithley {num} -> IC{num-1}", + }, + ), + "cFilterAutoFilt_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getAutoFilt", + "write_pv": f"ES-KEITH{num}:setAutoFilt", + "kind": "config", + "doc": f"Filter Status of Keithley {num} -> IC{num-1}", + }, + ), + "cZeroCheck_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getZeroCheck", + "write_pv": f"ES-KEITH{num}:setZeroCheck", + "kind": "config", + "doc": f"ZeroCheck of Keithley {num} -> IC{num-1}", + }, + ), + "cBias_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getBias", + "write_pv": f"ES-KEITH{num}:setBias", + "kind": "config", + "doc": f"Bias of Keithley {num} -> IC{num-1}", + }, + ), + "cSuppr_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getSuppr", + "write_pv": f"ES-KEITH{num}:setSuppr", + "kind": "config", + "doc": f"Suppression of Keithley {num} -> IC{num-1}", + }, + ), + "cError_ENUM": ( + EpicsSignalRO, + { + "read_pv": f"ES-KEITH{num}:getError", + "kind": "config", + "doc": f"Error of Keithley {num} -> IC{num-1}", + }, + ), + } + amp = Dcpt(amp_signals) + + def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): + self.timeout_for_pvwait = 2.5 + super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) + + def set_keithley(self) -> None: + """Make sure Keithley is in a normal operation mode""" + status_list = [] + if self.amp.cZeroCheck_ENUM.get() != AmplifierZeroCheck.OFF: + status_list.append(self.amp.cZeroCheck_ENUM.set(AmplifierZeroCheck.OFF)) + if self.amp.cAutoFilt_Enum.get() != AmplifierFilterAutoFilt.MANUAL: + status_list.append(self.amp.cAutoFilt_Enum.set(AmplifierFilterAutoFilt.MANUAL)) + if self.amp.cBias_ENUM.get() != AmplifierBias.OFF: + status_list.append(self.amp.cBias_ENUM.set(AmplifierBias.OFF)) + if self.amp.cSuppr_ENUM.get() != AmplifierSuppr.OFF: + status_list.append(self.amp.cSuppr_ENUM.set(AmplifierSuppr.OFF)) + for s in status_list: + s.wait(timeout=self.timeout_for_pvwait) + + error = self.amp.cError_ENUM.get() + if error in [ + AmplifierError.EEPROM_ERROR, + AmplifierError.INVALID_COMMAND, + AmplifierError.INVALID_OPTION, + AmplifierError.NOT_IN_REMOTE, + AmplifierError.SELFTEST_FAILED, + AmplifierError.SUPPR_CONFLICT, + AmplifierError.ZEROCHECK_ON, + AmplifierError.ZEROCORR_FAILED, + ]: + raise IonizationChamberError(f"Keithley {self.num} error {AmplifierError(error)}") + + def set_gain(self, gain: float) -> None: + """Configure the gain setting of the specified channel + + Args: + gain(float): Amplifier gain to set. Accepts either a float/int or a string, + e.g. 1e6 or "1e6". Must be one of: 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10. + + Raises: + ValueError: If `gain` cannot be converted to a float, or does not + match one of the supported gain values. + """ + + try: + key = float(gain) + except (TypeError, ValueError) as exc: + raise ValueError(f"Invalid gain: {gain!r}") from exc + + try: + enum_val = GAIN_MAP[key] + except KeyError as exc: + raise ValueError( + f"Unsupported gain: {gain!r}. Must be one of {sorted(GAIN_MAP)}" + ) from exc + + self.set_keithley() + + if self.amp.cGain10X_ENUM.get() != AmplifierGain10X.G1X: + logger.warning( + f"Keithley {self.num} was not in Gain10X == {AmplifierGain10X.G1X}, will try to set it to this value" + ) + self.amp.cGain10X_ENUM.put(AmplifierGain10X.G1X) + status = CompareStatus(self.amp.cGain10X_ENUM, AmplifierGain10X.G1X) + status.wait(self.timeout_for_pvwait) + + self.amp.cGain_ENUM.put(enum_val) + status = CompareStatus(self.amp.cGain_ENUM, enum_val) + status.wait(self.timeout_for_pvwait) + + def set_filter( + self, + value: Literal[ + "10us", "30us", "100us", "300us", "1ms", "3ms", "10ms", "30ms", "100ms", "300ms" + ], + ) -> None: + """Configure the filter setting of the specified channel + + Args: + value (Literal["10us", "30us", "100us", "300us", "1ms", "3ms", "10ms", "30ms", "100ms", "300ms"]) :Desired filter + """ + self.set_keithley() + + if self.amp.cFilterStatus_ENUM.get() != AmplifierFilterStatus.ON: + logger.warning(f"Keithley {self.num} filter was switched off, switch it on now") + self.amp.cFilterStatus_ENUM.put(AmplifierFilterStatus.ON) + status = CompareStatus(self.amp.cFilterStatus_ENUM, AmplifierFilterStatus.ON) + status.wait(self.timeout_for_pvwait) + + match value: + case "10us": + self.amp.cFilter_ENUM.put(AmplifierFilter.F10US) + case "30us": + self.amp.cFilter_ENUM.put(AmplifierFilter.F30US) + case "100us": + self.amp.cFilter_ENUM.put(AmplifierFilter.F100US) + case "300us": + self.amp.cFilter_ENUM.put(AmplifierFilter.F300US) + case "1ms": + self.amp.cFilter_ENUM.put(AmplifierFilter.F1MS) + case "3ms": + self.amp.cFilter_ENUM.put(AmplifierFilter.F3MS) + case "10ms": + self.amp.cFilter_ENUM.put(AmplifierFilter.F10MS) + case "30ms": + self.amp.cFilter_ENUM.put(AmplifierFilter.F30MS) + case "100ms": + self.amp.cFilter_ENUM.put(AmplifierFilter.F100MS) + case "300ms": + self.amp.cFilter_ENUM.put(AmplifierFilter.F300MS) + + +class IonizationChamber1(IonizationChamber0): + """Ionization Chamber 1, prefix should be 'X10DA-'.""" + + num = 2 + amp_signals = { + "cGain_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getGain", + "write_pv": f"ES-KEITH{num}:setGain", + "kind": "config", + "doc": f"Gain of Keithley {num} -> IC{num-1}", + }, + ), + "cGain10X_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getGain10X", + "write_pv": f"ES-KEITH{num}:setGain10X", + "kind": "config", + "doc": f"Gain 10x of Keithley {num} -> IC{num-1}", + }, + ), + "cFilterStatus_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getFilter", + "write_pv": f"ES-KEITH{num}:setFilter", + "kind": "config", + "doc": f"Filter Status of Keithley {num} -> IC{num-1}", + }, + ), + "cFilter_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getRiseTime", + "write_pv": f"ES-KEITH{num}:setRiseTime", + "kind": "config", + "doc": f"Filter of Keithley {num} -> IC{num-1}", + }, + ), + "cFilterAutoFilt_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getAutoFilt", + "write_pv": f"ES-KEITH{num}:setAutoFilt", + "kind": "config", + "doc": f"Filter Status of Keithley {num} -> IC{num-1}", + }, + ), + "cZeroCheck_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getZeroCheck", + "write_pv": f"ES-KEITH{num}:setZeroCheck", + "kind": "config", + "doc": f"ZeroCheck of Keithley {num} -> IC{num-1}", + }, + ), + "cBias_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getBias", + "write_pv": f"ES-KEITH{num}:setBias", + "kind": "config", + "doc": f"Bias of Keithley {num} -> IC{num-1}", + }, + ), + "cSuppr_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getSuppr", + "write_pv": f"ES-KEITH{num}:setSuppr", + "kind": "config", + "doc": f"Suppression of Keithley {num} -> IC{num-1}", + }, + ), + "cError_ENUM": ( + EpicsSignalRO, + { + "read_pv": f"ES-KEITH{num}:getError", + "kind": "config", + "doc": f"Error of Keithley {num} -> IC{num-1}", + }, + ), + } + amp = Dcpt(amp_signals) + + +class IonizationChamber2(IonizationChamber0): + """Ionization Chamber 2, prefix should be 'X10DA-'.""" + + num = 3 + amp_signals = { + "cGain_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getGain", + "write_pv": f"ES-KEITH{num}:setGain", + "kind": "config", + "doc": f"Gain of Keithley {num} -> IC{num-1}", + }, + ), + "cGain10X_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getGain10X", + "write_pv": f"ES-KEITH{num}:setGain10X", + "kind": "config", + "doc": f"Gain 10x of Keithley {num} -> IC{num-1}", + }, + ), + "cFilterStatus_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getFilter", + "write_pv": f"ES-KEITH{num}:setFilter", + "kind": "config", + "doc": f"Filter Status of Keithley {num} -> IC{num-1}", + }, + ), + "cFilter_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getRiseTime", + "write_pv": f"ES-KEITH{num}:setRiseTime", + "kind": "config", + "doc": f"Filter of Keithley {num} -> IC{num-1}", + }, + ), + "cFilterAutoFilt_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getAutoFilt", + "write_pv": f"ES-KEITH{num}:setAutoFilt", + "kind": "config", + "doc": f"Filter Status of Keithley {num} -> IC{num-1}",f"ES-KEITH{num}: + }, + ), + "cZeroCheck_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getZeroCheck", + "write_pv": f"ES-KEITH{num}:setZeroCheck", + "kind": "config", + "doc": f"ZeroCheck of Keithley {num} -> IC{num-1}", + }, + ), + "cBias_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getBias", + "write_pv": f"ES-KEITH{num}:setBias", + "kind": "config", + "doc": f"Bias of Keithley {num} -> IC{num-1}", + }, + ), + "cSuppr_ENUM": ( + EpicsSignal, + { + "read_pv": f"ES-KEITH{num}:getSuppr", + "write_pv": f"ES-KEITH{num}:setSuppr", + "kind": "config", + "doc": f"Suppression of Keithley {num} -> IC{num-1}", + }, + ), + "cError_ENUM": ( + EpicsSignalRO, + { + "read_pv": f"ES-KEITH{num}:getError", + "kind": "config", + "doc": f"Error of Keithley {num} -> IC{num-1}", + }, + ), + } + amp = Dcpt(amp_signals) diff --git a/superxas_bec/devices/ionization_chambers/ionization_chamber_enums.py b/superxas_bec/devices/ionization_chambers/ionization_chamber_enums.py new file mode 100644 index 0000000..4dce9e8 --- /dev/null +++ b/superxas_bec/devices/ionization_chambers/ionization_chamber_enums.py @@ -0,0 +1,101 @@ +import enum + + +class AmplifierGain(int, enum.Enum): + """Enum class for the gain of the channel""" + + G1E3 = 0 + G1E4 = 1 + G1E5 = 2 + G1E6 = 3 + G1E7 = 4 + G1E8 = 5 + G1E9 = 6 + G1E10 = 7 + + +GAIN_MAP = { + 1e3: AmplifierGain.G1E3, + 1e4: AmplifierGain.G1E4, + 1e5: AmplifierGain.G1E5, + 1e6: AmplifierGain.G1E6, + 1e7: AmplifierGain.G1E7, + 1e8: AmplifierGain.G1E8, + 1e9: AmplifierGain.G1E9, + 1e10: AmplifierGain.G1E10, +} + + +class AmplifierGain10X(int, enum.Enum): + """Enum class for the 10X gain selection""" + + G1X = 0 + G10X = 1 + + +class AmplifierFilter(int, enum.Enum): + """Enum class for the filter of the channel""" + + F10US = 0 + F30US = 1 + F100US = 2 + F300US = 3 + F1MS = 4 + F3MS = 5 + F10MS = 6 + F30MS = 7 + F100MS = 8 + F300MS = 9 + + +class AmplifierFilterStatus(int, enum.Enum): + """Enum class for the filter status""" + + OFF = 0 + ON = 1 + + +class AmplifierFilterAutoFilt(int, enum.Enum): + """Enum class for the filter auto filt functionality""" + + MANUAL = 0 + AUTO = 1 + + +class AmplifierZeroCheck(int, enum.Enum): + """Enum class for the zero check""" + + OFF = 0 + ON = 1 + AUTO = 2 + + +class AmplifierBias(int, enum.Enum): + """Enum class for the bias""" + + OFF = 0 + ON = 1 + + +class AmplifierSuppr(int, enum.Enum): + """Enum class for the suppression""" + + OFF = 0 + ON = 1 + + +class AmplifierError(int, enum.Enum): + """Enum class for amplifier errors""" + + OK = 0 + GAIN_CONFLICT = 1 + OVERLOAD = 2 + EEPROM_ERROR = 3 + ZEROCORR_FAILED = 4 + ZEROCHECK_ON = 5 + INPUT_TOO_LARGE = 6 + SUPPR_CONFLICT = 7 + SELFTEST_FAILED = 8 + NOT_IN_REMOTE = 9 + INVALID_OPTION = 10 + INVALID_COMMAND = 11 -- 2.54.0