wip
CI for debye_bec / test (pull_request) Successful in 54s
CI for debye_bec / test (push) Successful in 56s

This commit is contained in:
x01da
2026-09-15 08:54:34 +02:00
parent 318979a5d5
commit 3fb4895ce4
5 changed files with 193 additions and 145 deletions
+147 -126
View File
@@ -6,7 +6,6 @@ from bisect import bisect_right
import xraydb
from bec_lib import bec_logger
from ophyd_devices import CompareStatus
from ...devices.absorber import STATUS as ABS_STATUS
from ...devices.eh_shutter import STATUS as EH_PH_STATUS
@@ -19,7 +18,7 @@ logger = bec_logger.logger
EMIN = -100
EMAX = 200
MIN_RING_CURRENT = 5 # 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
@@ -34,6 +33,22 @@ FULL_SCALE_V = 10.0 # NIDAQ AI full-scale range
SAFETY_MARGIN = 0.9 # keep max signal under 90% of full scale
def wait_for_signal(signal, value, timeout: float) -> None:
"""Function to wait for a signal to reach a certain value.
Args:
signal: Signal to read from
value: Target value the signal should reach
timeout(float): Timeout in seconds before a TimeoutError is raised
"""
start = time.time()
while time.time() - start < timeout:
if signal.get() == value:
return
raise TimeoutError(f"Signal {signal} did not reach value {value} within {timeout} seconds.")
class AutoGainError(Exception):
"""AutoGain specific error"""
@@ -155,145 +170,151 @@ class AutoGain:
if ch["self.dev"].gmes.status.get() != 1:
raise AutoGainError(f"Gas filling of ionization chamber {name} is not OK")
logger.info("All checks done, start preparing for measurement")
# Get initial monochromator position and velocity
init_pos = self.dev.mo1_bragg.position
init_vel = self.dev.mo1_bragg.velocity
init_vel = self.dev.mo1_bragg.velocity.get()
logger.info(f"Move mono to start of {emin} eV")
self.dev.mo1_bragg.velocity.put(MONO_VELOCITY)
# TODO, subscribe to the signal, not the property!
# status = CompareStatus(self.dev.mo1_bragg.velocity, MONO_VELOCITY)
# status.wait(TIMEOUT_PUT_PV)
status = self.dev.mo1_bragg.move(emin)
status.wait(TIMEOUT_MONO_MOVE)
# Set NIDAQ to max mode
self.dev.nidaq.epics_mode.put(EpicsMode.MAX)
# status = CompareStatus(self.dev.nidaq.epics_mode, EpicsMode.MAX)
# status.wait(timeout=TIMEOUT_PUT_PV)
# Set gains to lowest gain
# Get initial gains
for name, ch in active_channels.items():
lowest_gain = AVAILABLE_GAINS[0]
logger.info(f"Setting gain to {lowest_gain:.0e}".replace("+", ""))
ch["self.dev"].set_gain(f"{lowest_gain:.0e}".replace("+", ""))
ch["gain"] = lowest_gain
ch["init_gain"] = ch["self.dev"].amp.cGain_ENUM.get()
remeasure = True
logger.info(f"Start measurement from {emin} eV to {emax} eV")
while remeasure:
# Create temporary storage for max signal per channel
data = {name: 0 for name in active_channels}
logger.info("All checks done, start preparing for measurement")
# Measure current ring current
ring_current_1 = self._get_ring_current()
logger.info(f"Ring current right before measurement: {ring_current_1} mA")
if ring_current_1 == 0:
raise AutoGainError("Ring current dropped to 0 mA right before measurement")
# 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)
for name, ch in active_channels.items():
data[name] = max(data[name], ch["signal"].get())
# Rest max values of NIDAQ signals
self.dev.nidaq.epics_max_reset.put(True)
time.sleep(0.1)
# Measure current ring current again
ring_current_2 = self._get_ring_current()
logger.info(f"Ring current right after measurement: {ring_current_2} mA")
if ring_current_2 == 0:
raise AutoGainError("Ring current dropped to 0 mA during measurement")
ring_current = (ring_current_1 + ring_current_2) / 2
# Move back to first monochromator position
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)
# Choose gain per channel based on the max signal recorded during the scan
remeasure = False
processed = set()
for name, ch in active_channels.items():
if name in processed:
continue
# ic0/ic1 must always end up with the same gain -> treat whichever of
# them are present (one or both) as a single group for this decision
if name in LINKED_CHANNELS:
group_names = [n for n in LINKED_CHANNELS if n in active_channels]
else:
group_names = [name]
group_signals = []
for gname in group_names:
gsig = data[gname]
logger.info(f"Raw signal for device {gname} is {gsig} V")
if comp_ring_current:
gsig = gsig * NOMINAL_RING_CURRENT / ring_current
logger.info(f"Compensate for ring current, new raw signal is {gsig} V")
group_signals.append(gsig)
# worst case: the weakest signal drives "need more gain",
# the strongest signal drives "don't saturate"
raw_signal_min = min(group_signals)
raw_signal_max = max(group_signals)
gain_ref = ch[
"gain"
] # linked channels always share gain, so any member's value works
label = "/".join(group_names)
if raw_signal_min < MIN_SIGNAL:
logger.info(f"Raw signal for {label} is below {MIN_SIGNAL}")
if gain_ref == AVAILABLE_GAINS[-1]:
logger.warning(
f"Amplifier of {label} at highest gain {gain_ref} and still not"
+ f" measured signal above {MIN_SIGNAL}"
)
else:
next_gain = AVAILABLE_GAINS[
bisect_right(AVAILABLE_GAINS, gain_ref * 100) - 1
]
for gname in group_names:
active_channels[gname]["self.dev"].set_gain(
f"{next_gain:.0e}".replace("+", "")
)
active_channels[gname]["gain"] = next_gain
logger.info(f"Setting gain of {label} to {next_gain:.0e} and remeasure")
remeasure = True
else:
gain = max(
(
g
for g in AVAILABLE_GAINS
if raw_signal_max / gain_ref * g <= FULL_SCALE_V * SAFETY_MARGIN
),
default=min(AVAILABLE_GAINS),
)
for gname in group_names:
active_channels[gname]["self.dev"].set_gain(gain)
active_channels[gname]["gain"] = gain
logger.info(f"Calculated final gain for {label} of {gain:.0e}")
processed.update(group_names)
# Wait for mono to return to start position
status.wait(TIMEOUT_MONO_MOVE)
# Set NIDAQ to max mode
self.dev.nidaq.epics_mode.put(EpicsMode.MAX)
wait_for_signal(self.dev.nidaq.epics_mode, EpicsMode.MAX, timeout=TIMEOUT_PUT_PV)
# Set gains to lowest gain
for name, ch in active_channels.items():
lowest_gain = AVAILABLE_GAINS[0]
ch["self.dev"].set_gain(lowest_gain)
ch["gain"] = lowest_gain
remeasure = True
logger.info(f"Start measurement from {emin} eV to {emax} eV")
while remeasure:
# Rest max values of NIDAQ signals
self.dev.nidaq.epics_max_reset.put(True)
time.sleep(0.2)
# Create temporary storage for max signal per channel
data = {name: 0 for name in active_channels}
# Measure current ring current
ring_current_1 = self._get_ring_current()
logger.info(f"Ring current right before measurement: {ring_current_1} mA")
if ring_current_1 < MIN_RING_CURRENT:
raise AutoGainError(
f"Ring current dropped below {MIN_RING_CURRENT} mA right before measurement"
)
# 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)
for name, ch in active_channels.items():
data[name] = max(data[name], ch["signal"].get())
# Measure current ring current again
ring_current_2 = self._get_ring_current()
logger.info(f"Ring current right after measurement: {ring_current_2} mA")
if ring_current_2 < MIN_RING_CURRENT:
raise AutoGainError(
f"Ring current dropped below {MIN_RING_CURRENT} mA during measurement"
)
ring_current = (ring_current_1 + ring_current_2) / 2
# Move back to first monochromator position
status = self.dev.mo1_bragg.move(emin)
# Choose gain per channel based on the max signal recorded during the scan
remeasure = False
processed = set()
for name, ch in active_channels.items():
if name in processed:
continue
# ic0/ic1 must always end up with the same gain -> treat whichever of
# them are present (one or both) as a single group for this decision
if name in LINKED_CHANNELS:
group_names = [n for n in LINKED_CHANNELS if n in active_channels]
else:
group_names = [name]
group_signals = []
for gname in group_names:
gsig = data[gname]
logger.info(f"Raw signal for device {gname} is {gsig} V")
if comp_ring_current:
gsig = gsig * NOMINAL_RING_CURRENT / ring_current
logger.info(f"Compensate for ring current, new raw signal is {gsig} V")
group_signals.append(gsig)
# worst case: the weakest signal drives "need more gain",
# the strongest signal drives "don't saturate"
raw_signal_min = min(group_signals)
raw_signal_max = max(group_signals)
gain_ref = ch[
"gain"
] # linked channels always share gain, so any member's value works
label = "/".join(group_names)
if raw_signal_min < MIN_SIGNAL:
logger.info(f"Raw signal for {label} is below {MIN_SIGNAL}")
if gain_ref == AVAILABLE_GAINS[-1]:
logger.warning(
f"Amplifier of {label} at highest gain {gain_ref} and still not"
+ f" measured signal above {MIN_SIGNAL}"
)
else:
next_gain = AVAILABLE_GAINS[
bisect_right(AVAILABLE_GAINS, gain_ref * 100) - 1
]
for gname in group_names:
active_channels[gname]["self.dev"].set_gain(next_gain)
active_channels[gname]["gain"] = next_gain
logger.info(f"Setting gain of {label} to {next_gain:.0e} and remeasure")
remeasure = True
else:
gain = max(
(
g
for g in AVAILABLE_GAINS
if raw_signal_max / gain_ref * g <= FULL_SCALE_V * SAFETY_MARGIN
),
default=min(AVAILABLE_GAINS),
)
for gname in group_names:
active_channels[gname]["self.dev"].set_gain(gain)
active_channels[gname]["gain"] = gain
logger.info(f"Calculated final gain for {label} of {gain:.0e}")
processed.update(group_names)
# Wait for mono to return to start position
status.wait(TIMEOUT_MONO_MOVE)
except Exception as e:
logger.error("Error during measurement, reset gains to their initial values")
for name, ch in active_channels.items():
ch["self.dev"].amp.cGain_ENUM.put(ch["init_gain"])
raise AutoGainError(f"During execution of auto-gain, an exception occured: {e}") from e
# Reset NIDAQ to mean mode
self.dev.nidaq.epics_mode.put(EpicsMode.MEAN)
# status = CompareStatus(self.dev.nidaq.epics_mode, EpicsMode.MEAN)
# status.wait(timeout=TIMEOUT_PUT_PV)
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) # TODO: Fix, not using property!
# status = CompareStatus(self.dev.mo1_bragg.velocity, init_vel)
# status.wait(timeout=TIMEOUT_PUT_PV)
self.dev.mo1_bragg.velocity.put(init_vel)
wait_for_signal(self.dev.mo1_bragg.velocity, init_vel, timeout=TIMEOUT_PUT_PV)
def _get_ring_current(self) -> float:
ring_current = 0
@@ -488,9 +488,7 @@ class Scheduler(BECWidget, QWidget):
)
elif re.match(r"^dev\..*\.move", item.command):
if item.form_state["relative"]:
text = (
f"Move {item.form_state['device_name']} by {item.form_state['value']}"
)
text = f"Move {item.form_state['device_name']} by {item.form_state['value']:+g}"
else:
text = (
f"Move {item.form_state['device_name']} to {item.form_state['value']}"
@@ -502,7 +500,17 @@ class Scheduler(BECWidget, QWidget):
+ f"{item.form_state['gas2']} to {item.form_state['pressure']} bar"
)
elif item.command.startswith("dev.reffoilchanger"):
text = f"Insert {item.form_state['ref']} reference foil"
text = f"Insert {item.form_state['foil']} reference foil"
elif item.command.startswith("digital_twin.move_with_config({"):
config = item.form_state["config"]
if config["mo1_mode"] == "Monochromatic":
text = f"Align Beamline for {config['energy']} eV"
else:
text = "Align Beamline for pinkbeam"
text += f" (CM: {config['cm_stripe']} at {config['cm_pitch']*1e3} mrad"
if config["mo1_mode"] == "Monochromatic":
text += f", MO1: {config['mo1_xtal']}"
text += f", FM: {config['fm_stripe']} at {config['fm_rotx']*1e3} mrad)"
if text == "":
text = f"{item.command}"
if item.error:
@@ -14,6 +14,7 @@ from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from typeguard import typechecked
from debye_bec.devices.ionization_chambers.ionization_chamber_enums import (
GAIN_MAP,
AmplifierEnable,
AmplifierFilter,
AmplifierGain,
@@ -120,13 +121,17 @@ class IonizationChamber0(PSIDeviceBase):
self.timeout_for_pvwait = 2.5
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
def set_gain(self, gain: float | str) -> None:
def set_gain(self, gain: float) -> None:
"""Configure the gain setting of the specified channel
Args:
gain (Literal['1e6', '1e7', '5e7', '1e8', '1e9']) : Desired gain
gain(float): Amplifier gain to set. Accepts either a float/int or a string,
e.g. 1e6 or "1e6". Must be one of: 1e6, 1e7, 5e7, 1e8, 1e9.
Raises:
ValueError: If `gain` cannot be converted to a float, or does not
match one of the supported gain values.
"""
# TODO: Fix docstring
if self.amp.cOnOff.get() == AmplifierEnable.OFF:
status = CompareStatus(self.amp.cOnOff, AmplifierEnable.ON)
@@ -134,17 +139,21 @@ class IonizationChamber0(PSIDeviceBase):
self.amp.cOnOff.put(AmplifierEnable.ON)
status.wait(self.timeout_for_pvwait)
match gain:
case "1e6" | 1e6:
self.amp.cGain_ENUM.put(AmplifierGain.G1E6)
case "1e7" | 1e7:
self.amp.cGain_ENUM.put(AmplifierGain.G1E7)
case "5e7" | 5e7:
self.amp.cGain_ENUM.put(AmplifierGain.G5E7)
case "1e8" | 1e8:
self.amp.cGain_ENUM.put(AmplifierGain.G1E8)
case "1e9" | 1e9:
self.amp.cGain_ENUM.put(AmplifierGain.G1E9)
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.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["1us", "3us", "10us", "30us", "100us", "300us", "1ms", "3ms"]
@@ -19,6 +19,15 @@ class AmplifierGain(int, enum.Enum):
G1E9 = 4
GAIN_MAP = {
1e6: AmplifierGain.G1E6,
1e7: AmplifierGain.G1E7,
5e7: AmplifierGain.G5E7,
1e8: AmplifierGain.G1E8,
1e9: AmplifierGain.G1E9,
}
class AmplifierFilter(int, enum.Enum):
"""Enum class for the filter of the channel"""
+2 -1
View File
@@ -59,8 +59,9 @@ class EncoderFactors(int, enum.Enum):
X2 = 5
X4 = 6
class EpicsMode(int, enum.Enum):
"""Mode when sending through EPICS"""
MEAN = 0
MAX = 0
MAX = 1