feat(auto_gain): Implement for X10DA
CI for debye_bec / test (push) Successful in 1m30s
CI for debye_bec / test (pull_request) Successful in 1m41s

This commit is contained in:
hitz_s committed 2026-10-22 08:30:25 +02:00
1 parent 7144e55a7b
commit 0215d6ef6c
10 files changed
+151 -90

No files matched your search

@@ -12,21 +12,23 @@ from ...devices.eh_shutter import STATUS as EH_PH_STATUS
from ...devices.ionization_chambers.ionization_chamber_enums import AmplifierEnable
from ...devices.nidaq.nidaq_enums import EpicsMode, NidaqState
from ...devices.op_shutter import STATUS as OP_PH_STATUS
from .beamline import BeamlineId, get_beamline_id
logger = bec_logger.logger
EMIN = -100
EMAX = 200
EMIN = -200 # Minimum energy will be edge energy - EMIN
EMAX = 1000 # Maximum energy will be edge energy + EMAX
MIN_RING_CURRENT = -1 # Minimum ring current to use auto-gain
MIN_RING_CURRENT = 1 # Minimum ring current to use auto-gain
NOMINAL_RING_CURRENT = 400 # Nominal ring current of SLS2
TIMEOUT_PUT_PV = 5 # Timeout to set a PV
MONO_VELOCITY = 20 # Move velocity in deg/s
MONO_VELOCITY = 20 # Move velocity in deg/s (only X01DA)
TIMEOUT_MONO_MOVE = 30 # Timeout to finish a movement on the mono
AVAILABLE_GAINS = [1e6, 1e7, 5e7, 1e8, 1e9] # ascending order
AVAILABLE_GAINS_X01DA = [1e6, 1e7, 5e7, 1e8, 1e9] # ascending order
AVAILABLE_GAINS_X10DA = [1e6, 1e7, 1e8, 1e9, 1e10] # ascending order
MIN_SIGNAL = 0.05 # Minimum signal to count as valid signal
FULL_SCALE_V = 10.0 # NIDAQ AI full-scale range
SAFETY_MARGIN = 0.9 # keep max signal under 90% of full scale
@@ -64,17 +66,22 @@ class AutoGainCore:
raise AutoGainError("Did not get bec")
self.dev = dev
self.bec = bec
self.beamline = get_beamline_id()
if self.beamline == BeamlineId.X01DA:
self.available_gains = AVAILABLE_GAINS_X01DA
else:
self.available_gains = AVAILABLE_GAINS_X10DA
self.linked_channels = None
self.prohibited_gains = {}
def available_gains(self, amplifier):
def allowed_gains(self, amplifier):
"""Returns a list of gains which are allowed for the amplifier"""
if amplifier in self.prohibited_gains:
gains = AVAILABLE_GAINS.copy()
gains = self.available_gains.copy()
for prohibited_gain in self.prohibited_gains[amplifier]:
gains.remove(prohibited_gain)
return gains
return AVAILABLE_GAINS
return self.available_gains
def start(
self,
@@ -96,7 +103,9 @@ class AutoGainCore:
element(str): Element which defines the energy at which the gain will be set, e.g. 'Cu'
edge(str): Corresponding edge, e.g. 'L1'
amplifier(list[str]): Amplifiers where auto-gain should be applied to
Defaults to all amplifiers -> ['ic0', 'ic1', 'ic2', 'pips']
Defaults to all amplifiers ->
X01DA = ['ic0', 'ic1', 'ic2', 'pips']
X10DA = ['ic0', 'ic1', 'ic2']
link_ic0_ic1(bool): Apply same gain to ic0 and ic1. Defaults to True
comp_ring_current(bool): Respects the current ring current and calculates the gain(s)
for a nominal ring current of 400 mA. Defaults to True
@@ -127,7 +136,10 @@ class AutoGainCore:
self.prohibited_gains = prohibited_gains
if amplifier is None:
amplifier = ["ic0", "ic1", "ic2", "pips"]
if self.beamline == BeamlineId.X01DA:
amplifier = ["ic0", "ic1", "ic2", "pips"]
else:
amplifier = ["ic0", "ic1", "ic2"]
# Make sure NIDAQ is in standby mode
if self.dev.nidaq.state.get() != NidaqState.STANDBY:
@@ -155,15 +167,17 @@ class AutoGainCore:
emin = energy + EMIN
emax = energy + EMAX
# Check range of mono
low_limit = self.dev.mo1_bragg.low_lim.get()
high_limit = self.dev.mo1_bragg.high_lim.get()
if emin < low_limit or emax > high_limit:
raise ValueError(
f"Chosen element/edge {element}/{edge} with edge energy of {energy}"
+ " is outside of accessible range of monochromator "
+ f"{low_limit:.1f} eV - {high_limit:.1f} eV"
)
# Check range of mono (only X01DA)
if self.beamline == BeamlineId.X01DA:
low_limit = self.dev.mo1_bragg.low_lim.get()
high_limit = self.dev.mo1_bragg.high_lim.get()
if emin < low_limit or emax > high_limit:
raise ValueError(
f"Chosen element/edge {element}/{edge} with edge energy of {energy}"
+ " is outside of accessible range of monochromator "
+ f"{low_limit:.1f} eV - {high_limit:.1f} eV"
)
# X10DA energy axis (goniometer) does not have lo/hi limits
# Map amplifier names to their NIDAQ channels
channel_map = {
@@ -174,30 +188,38 @@ class AutoGainCore:
}
active_channels = {name: ch for name, ch in channel_map.items() if name in amplifier}
# Check if amplifieres are switched on
for name, ch in active_channels.items():
if ch["self.dev"].amp.cOnOff.get() != AmplifierEnable.ON:
raise AutoGainError(f"Amplifier of self.device {name} is not enabled")
# Tests only for X01DA:
if self.beamline == BeamlineId.X01DA:
# Check if amplifieres are switched on (only X01DA)
for name, ch in active_channels.items():
if ch["self.dev"].amp.cOnOff.get() != AmplifierEnable.ON:
raise AutoGainError(f"Amplifier of self.device {name} is not enabled")
# Check high voltage on ionization chambers
for name, ch in active_channels.items():
if name != "pips":
if ch["self.dev"].hv_en.ena.get() != 1:
raise AutoGainError(f"High voltage of ionization chamber {name} is not enabled")
if ch["self.dev"].hv.hv_v.get() < 1000:
raise AutoGainError(f"HV voltage of ionization chamber {name} is < 1000")
if ch["self.dev"].hv.grid_v.get() < 1000:
raise AutoGainError(f"Grid voltage of ionization chamber {name} is < 1000")
# Check high voltage on ionization chambers (only X01DA)
for name, ch in active_channels.items():
if name != "pips":
if ch["self.dev"].hv_en.ena.get() != 1:
raise AutoGainError(
f"High voltage of ionization chamber {name} is not enabled"
)
if ch["self.dev"].hv.hv_v.get() < 1000:
raise AutoGainError(f"HV voltage of ionization chamber {name} is < 1000")
if ch["self.dev"].hv.grid_v.get() < 1000:
raise AutoGainError(f"Grid voltage of ionization chamber {name} is < 1000")
# Check gas filling of ionization chambers
for name, ch in active_channels.items():
if name != "pips":
if ch["self.dev"].gmes.status.get() != 1:
raise AutoGainError(f"Gas filling of ionization chamber {name} is not OK")
# Check gas filling of ionization chambers (only X01DA)
for name, ch in active_channels.items():
if name != "pips":
if ch["self.dev"].gmes.status.get() != 1:
raise AutoGainError(f"Gas filling of ionization chamber {name} is not OK")
# Get initial monochromator position and velocity
init_pos = self.dev.mo1_bragg.position
init_vel = self.dev.mo1_bragg.velocity.get()
if self.beamline == BeamlineId.X01DA:
init_pos = self.dev.mo1_bragg.position
init_vel = self.dev.mo1_bragg.velocity.get()
else:
init_pos = self.dev.mo1_rotx_energy.get()
init_vel = None
# Get initial gains
for name, ch in active_channels.items():
@@ -209,9 +231,12 @@ class AutoGainCore:
try:
logger.info(f"Move mono to start of {emin} eV")
self.dev.mo1_bragg.velocity.put(MONO_VELOCITY)
wait_for_signal(self.dev.mo1_bragg.velocity, MONO_VELOCITY, timeout=TIMEOUT_PUT_PV)
status = self.dev.mo1_bragg.move(emin)
if self.beamline == BeamlineId.X01DA:
self.dev.mo1_bragg.velocity.put(MONO_VELOCITY)
wait_for_signal(self.dev.mo1_bragg.velocity, MONO_VELOCITY, timeout=TIMEOUT_PUT_PV)
status = self.dev.mo1_bragg.move(emin)
else:
status = self.dev.mo1_rotx_energy.move(emin)
status.wait(TIMEOUT_MONO_MOVE)
# Set NIDAQ to max mode
@@ -220,7 +245,7 @@ class AutoGainCore:
# Set gains to lowest gain
for name, ch in active_channels.items():
lowest_gain = self.available_gains(name)[0]
lowest_gain = self.allowed_gains(name)[0]
ch["self.dev"].set_gain(lowest_gain)
ch["gain"] = lowest_gain
@@ -245,8 +270,10 @@ class AutoGainCore:
)
# Scan range, recording the peak NIDAQ signal per channel
self.dev.mo1_bragg.move(emax).wait(timeout=TIMEOUT_MONO_MOVE)
status.wait(TIMEOUT_MONO_MOVE)
if self.beamline == BeamlineId.X01DA:
self.dev.mo1_bragg.move(emax).wait(timeout=TIMEOUT_MONO_MOVE)
else:
self.dev.mo1_rotx_energy.move(emax).wait(timeout=TIMEOUT_MONO_MOVE)
for name, ch in active_channels.items():
data[name] = max(data[name], ch["signal"].get())
@@ -260,7 +287,10 @@ class AutoGainCore:
ring_current = (ring_current_1 + ring_current_2) / 2
# Move back to first monochromator position
status = self.dev.mo1_bragg.move(emin)
if self.beamline == BeamlineId.X01DA:
status = self.dev.mo1_bragg.move(emin)
else:
status = self.dev.mo1_rotx_energy.move(emin)
# Choose gain per channel based on the max signal recorded during the scan
remeasure = False
@@ -297,14 +327,14 @@ class AutoGainCore:
if raw_signal_min < MIN_SIGNAL:
logger.info(f"Raw signal for {label} is below {MIN_SIGNAL}")
if gain_ref == self.available_gains(name)[-1]:
if gain_ref == self.allowed_gains(name)[-1]:
logger.warning(
f"Amplifier of {label} at highest gain {gain_ref} and still not"
+ f" measured signal above {MIN_SIGNAL}"
)
else:
next_gain = self.available_gains(name)[
bisect_right(self.available_gains(name), gain_ref * 100) - 1
next_gain = self.allowed_gains(name)[
bisect_right(self.allowed_gains(name), gain_ref * 100) - 1
]
for gname in group_names:
active_channels[gname]["self.dev"].set_gain(next_gain)
@@ -316,10 +346,10 @@ class AutoGainCore:
gain = max(
(
g
for g in self.available_gains(name)
for g in self.allowed_gains(name)
if raw_signal_max / gain_ref * g <= FULL_SCALE_V * SAFETY_MARGIN
),
default=min(self.available_gains(name)),
default=min(self.allowed_gains(name)),
)
for gname in group_names:
active_channels[gname]["self.dev"].set_gain(gain)
@@ -345,10 +375,14 @@ class AutoGainCore:
wait_for_signal(self.dev.nidaq.epics_mode, EpicsMode.MEAN, timeout=TIMEOUT_PUT_PV)
# Wait for mono to move to initial position and reset velocity
status = self.dev.mo1_bragg.move(init_pos)
status.wait(TIMEOUT_MONO_MOVE)
self.dev.mo1_bragg.velocity.put(init_vel)
wait_for_signal(self.dev.mo1_bragg.velocity, init_vel, timeout=TIMEOUT_PUT_PV)
if self.beamline == BeamlineId.X01DA:
status = self.dev.mo1_bragg.move(init_pos)
status.wait(TIMEOUT_MONO_MOVE)
self.dev.mo1_bragg.velocity.put(init_vel)
wait_for_signal(self.dev.mo1_bragg.velocity, init_vel, timeout=TIMEOUT_PUT_PV)
else:
status = self.dev.mo1_rotx_energy.move(init_pos)
status.wait(TIMEOUT_MONO_MOVE)
return results
@@ -1,12 +1,20 @@
import socket
from enum import Enum
from bec_lib import bec_logger
from .types import BeamlineId
logger = bec_logger.logger
class BeamlineId(str, Enum):
"""
Identifier for supported beamlines.
"""
X01DA = "x01da"
X10DA = "x10da"
def get_beamline_id() -> BeamlineId:
"""
Based on the bec servers hostname, tries to extract the beamline
@@ -38,14 +46,3 @@ def get_beamline_id() -> BeamlineId:
raise ValueError(f"Wrong selection {bl}")
else:
raise ValueError("Cannot open digital twin without a beamline")
def get_parameters():
beamline = get_beamline_id()
if beamline == "x01da":
from . import x01da_parameters as parameters
elif beamline == "x10da":
from . import x10da_parameters as parameters
else:
raise ValueError(f"Unknown beamline: {beamline}")
return parameters
@@ -1,3 +1,3 @@
from .beamline import get_parameters
from .digital_twin_core import get_parameters
parameters = get_parameters()
@@ -10,8 +10,8 @@ from bec_lib import bec_logger
from scipy.interpolate import UnivariateSpline
from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm
from ..beamline import get_beamline_id
from . import parameters as bl
from .beamline import get_beamline_id
from .motion_worker import MotionOutcome, MotionWorker
from .types import BeamlineId, ConfigDict, DataDict, SurfaceDict
@@ -26,6 +26,17 @@ OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml")
logger = bec_logger.logger
def get_parameters():
beamline = get_beamline_id()
if beamline == "x01da":
from . import x01da_parameters as parameters
elif beamline == "x10da":
from . import x10da_parameters as parameters
else:
raise ValueError(f"Unknown beamline: {beamline}")
return parameters
class _ExclusiveGroupManager:
def __init__(self):
self._cond = threading.Condition()
@@ -8,7 +8,7 @@ from bec_lib import bec_logger
# pylint: disable=E0402
from ....devices.absorber import STATUS as ABS_STATUS
from .types import BeamlineId
from ..beamline import BeamlineId
logger = bec_logger.logger
@@ -5,17 +5,17 @@ Auto Gain: Custom BEC widget to automatically set gains.
import sys
from bec_lib import bec_logger
from bec_lib.logger import bec_logger
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme, get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
from qtpy.QtCore import QObject, QThread, Signal
# pylint: disable=E0611
from qtpy.QtCore import QObject, QThread, Signal
from qtpy.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
from ....bec_ipython_client.plugins.auto_gain_core import AutoGainCore
from ....bec_ipython_client.plugins.beamline import BeamlineId, get_beamline_id
from ..edge_selector import EdgeSelector
from .qt_widgets import CheckBox, Group, MatrixSelector, MyButton, TextIndicator
@@ -67,6 +67,8 @@ class AutoGain(BECWidget, QWidget):
super().__init__(parent=parent, *arg, **kwargs)
self.get_bec_shortcuts()
self.beamline = get_beamline_id()
central = QWidget()
self.root_layout = QVBoxLayout(central)
@@ -85,16 +87,22 @@ class AutoGain(BECWidget, QWidget):
self.ic1_check = CheckBox("ic1", "IC1")
self.ic2_check = CheckBox("ic2", "IC2")
self.pips_check = CheckBox("pips", "PIPS")
self.apply_group = Group(
"Apply auto gain on", [self.ic0_check, self.ic1_check, self.ic2_check, self.pips_check]
)
if self.beamline == BeamlineId.X01DA:
self.apply_group = Group(
"Apply auto gain on",
[self.ic0_check, self.ic1_check, self.ic2_check, self.pips_check],
)
else: # PIPS of X10DA has fixed gain amplifier
self.apply_group = Group(
"Apply auto gain on", [self.ic0_check, self.ic1_check, self.ic2_check]
)
self.root_layout.addWidget(self.apply_group)
self.link_ic0_ic1 = CheckBox("link_ic0_ic1", "Link IC0/IC1 gains")
self.comp_ring_current = CheckBox("comp_ring_current", "Calculate for 400 mA ring current")
self.prohibited_gains = MatrixSelector("Prohibited gains")
self.prohibited_gains = MatrixSelector("Prohibited gains", beamline=self.beamline)
self.expert_group = Group(
"Expert settings", [self.link_ic0_ic1, self.comp_ring_current, self.prohibited_gains]
@@ -14,6 +14,8 @@ from qtpy.QtWidgets import (
QWidget,
)
from ....bec_ipython_client.plugins.beamline import BeamlineId
LABEL_WIDTH = 118
ROW_MARGINS = (4, 0, 4, 0)
ROW_SPACING = 6
@@ -83,11 +85,21 @@ class MatrixSelector(QWidget):
{'ic0': [1000000.0], 'ic1': [1000000.0, 50000000.0]}
"""
ROWS = ["IC0", "IC1", "IC2", "PIPS"]
COLS = ["1e6", "1e7", "5e7", "1e8", "1e9"]
ROWS_X01DA = ["IC0", "IC1", "IC2", "PIPS"]
COLS_X01DA = ["1e6", "1e7", "5e7", "1e8", "1e9"]
def __init__(self, label: str = "", parent=None):
ROWS_X10DA = ["IC0", "IC1", "IC2"]
COLS_X10DA = ["1e6", "1e7", "1e8", "1e9", "1e10"]
def __init__(self, label: str, beamline: BeamlineId, parent=None):
super().__init__(parent)
if beamline == BeamlineId.X01DA:
self.cols = self.COLS_X01DA
self.rows = self.ROWS_X01DA
else:
self.cols = self.COLS_X10DA
self.rows = self.ROWS_X10DA
layout = QHBoxLayout(self)
layout.setContentsMargins(*ROW_MARGINS)
layout.setSpacing(ROW_SPACING)
@@ -99,14 +111,14 @@ class MatrixSelector(QWidget):
grid_layout = QGridLayout()
# Column headers
for c, col in enumerate(self.COLS, start=1):
for c, col in enumerate(self.cols, start=1):
grid_layout.addWidget(QLabel(col), 0, c, alignment=Qt.AlignmentFlag.AlignCenter)
# Row headers + checkboxes
self.checks = {}
for r, row in enumerate(self.ROWS, start=1):
for r, row in enumerate(self.rows, start=1):
grid_layout.addWidget(QLabel(row), r, 0)
for c, col in enumerate(self.COLS, start=1):
for c, col in enumerate(self.cols, start=1):
cb = QCheckBox()
grid_layout.addWidget(cb, r, c, alignment=Qt.AlignmentFlag.AlignCenter)
self.checks[(row, col)] = cb
@@ -117,8 +129,8 @@ class MatrixSelector(QWidget):
def selection(self):
"""Return e.g. {"ic0": [1e6], "ic1": [1e6, 5e7]}; rows with no selection are omitted."""
result = {}
for row in self.ROWS:
values = [float(col) for col in self.COLS if self.checks[(row, col)].isChecked()]
for row in self.rows:
values = [float(col) for col in self.cols if self.checks[(row, col)].isChecked()]
if values:
result[row.lower()] = values
return result
@@ -131,9 +143,9 @@ class MatrixSelector(QWidget):
"""
# Normalize keys to lowercase and values to floats for comparison
wanted = {key.lower(): {float(v) for v in values} for key, values in selection.items()}
for row in self.ROWS:
for row in self.rows:
row_values = wanted.get(row.lower(), set())
for col in self.COLS:
for col in self.cols:
cb = self.checks[(row, col)]
cb.setChecked(float(col) in row_values)
@@ -7,7 +7,7 @@ from typing import Union
# pylint: disable=E0611
from qtpy.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId
from .....bec_ipython_client.plugins.beamline import BeamlineId
from ..widgets.qt_widgets import (
Button,
ComboBox,
@@ -7,7 +7,7 @@ from typing import Literal
# pylint: disable=E0611
from qtpy.QtWidgets import QVBoxLayout, QWidget
from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId
from .....bec_ipython_client.plugins.beamline import BeamlineId
from ..widgets.move_widget import AbsorberWidget, MoveWidget
from ..widgets.qt_widgets import Button, Group, QLabel
@@ -18,7 +18,6 @@ from .....bec_ipython_client.plugins.digital_twin_core.motion_worker import (
MotionOutcome,
MotionWorker,
)
from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId
logger = bec_logger.logger