wip: digital twin
CI for debye_bec / test (pull_request) Failing after 1m3s
CI for debye_bec / test (push) Failing after 1m5s

This commit is contained in:
x01da
2026-05-04 12:46:50 +02:00
parent b14f2c0fe3
commit 16bd819a9f
2 changed files with 71 additions and 228 deletions
@@ -11,7 +11,7 @@ from qtpy.QtWidgets import (
)
# pylint: disable=E0611
from qtpy.QtCore import Qt, QTimer
from qtpy.QtGui import QColor, QFont
from qtpy.QtGui import QColor, QFont, QBrush
import pyqtgraph as pg
from xrt.backends.raycing.physconsts import CHeVcm, AVOGADRO
@@ -133,9 +133,9 @@ class DigitalTwin(BECWidget, QWidget):
dE_over_E = fwhm_rad / np.tan(theta_B)
dE = dE_over_E * E
logger.info(f"DCM FWHM : {r2-r1:.2f} µrad")
logger.info(f"ΔE/E : {dE_over_E:.2e}")
logger.info(f"ΔE : {dE:.3f} eV at {E} eV")
# logger.info(f"DCM FWHM : {r2-r1:.2f} µrad")
# logger.info(f"ΔE/E : {dE_over_E:.2e}")
# logger.info(f"ΔE : {dE:.3f} eV at {E} eV")
self.input.mo1_eres.setValue(dE)
@@ -146,13 +146,17 @@ class DigitalTwin(BECWidget, QWidget):
np.sin(-self.input.cm_pitch.value() * 1e-3)
)[0:2]
self.input.cm_refl.setValue(100 * abs(rs)**2)
self.input.cm_refl.setLabel(f"Refl. at {self.input.energy.value():.0f} eV")
self.input.cm_refl.setLabel(f"Reflectivity at \n{self.input.energy.value():.0f} eV")
rs, rp = bl.cm.material[index].get_amplitude(
2 * self.input.energy.value(),
np.sin(-self.input.cm_pitch.value() * 1e-3)
)[0:2]
self.input.cm_refl_harm.setValue(100 * abs(rs)**2)
self.input.cm_refl_harm.setLabel(f"Refl. at {2 * self.input.energy.value():.0f} eV")
self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3 * self.input.energy.value():.0f} eV")
harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value())
self.input.cm_fm_harm_suppr.setValue(harm_suppr)
self.input.cm_fm_harm_suppr.setLabel(f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV")
def calc_fm_reflectivity(self, *args):
if self.input.fm_stripe.currentText() in ('Rh (toroid)', 'Pt (toroid)'):
@@ -170,7 +174,17 @@ class DigitalTwin(BECWidget, QWidget):
np.sin(-self.input.fm_pitch.value() * 1e-3)
)[0:2]
self.input.fm_refl.setValue(100 * abs(rs)**2)
self.input.fm_refl.setLabel(f"Refl. at {self.input.energy.value():.0f} eV")
self.input.fm_refl.setLabel(f"Reflectivity at \n{self.input.energy.value():.0f} eV")
rs, rp = material[index].get_amplitude(
2 * self.input.energy.value(),
np.sin(-self.input.fm_pitch.value() * 1e-3)
)[0:2]
self.input.fm_refl_harm.setValue(100 * abs(rs)**2)
self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3 * self.input.energy.value():.0f} eV")
harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value())
self.input.cm_fm_harm_suppr.setValue(harm_suppr)
self.input.cm_fm_harm_suppr.setLabel(f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV")
def get_assistant_config(self):
config = { # Config in SI units!
@@ -387,8 +401,8 @@ class InputPanel(QWidget):
self.cm_stripe = ComboBox('Stripe', ['Si', 'Rh', 'Pt'])
self.cm_pitch_critical = NumberIndicator('Critical Pitch', 'mrad', decimals=3)
self.cm_pitch = InputNumberField('Pitch [mrad]', init=-2.391, decimals=3, single_step=0.01, ll=-4.6, hl=-1.2)
self.cm_refl = NumberIndicator('Refl. at x eV', '%', decimals=0)
self.cm_refl_harm = NumberIndicator('Refl. at x eV', '%', decimals=0)
self.cm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0)
self.cm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0)
self.cm_ass_group = Group(
'Collimating Mirror',
[
@@ -419,7 +433,8 @@ class InputPanel(QWidget):
self.fm_stripe = ComboBox('Stripe', ['Rh (toroid)', 'Rh (flat)', 'Pt (toroid)', 'Pt (flat)'])
self.fm_pitch_ideal = NumberIndicator('Ideal Pitch', 'mrad', decimals=3)
self.fm_pitch = InputNumberField('Pitch [mrad]', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2)
self.fm_refl = NumberIndicator('Refl. at x eV', '%', decimals=0)
self.fm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0)
self.fm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0)
self.fm_ass_group = Group(
'Focusing Mirror',
[
@@ -427,10 +442,12 @@ class InputPanel(QWidget):
self.fm_pitch_ideal,
self.fm_pitch,
self.fm_refl,
self.fm_refl_harm,
]
)
# Sample
self.cm_fm_harm_suppr = NumberIndicator('Total Suppression Factor at x eV', '', decimals=0)
self.smpl = InputNumberField('Sample Position [mm]', init=23511, decimals=0, single_step=100, ll=23000, hl=30000)
# Assemble complete assitant group
@@ -442,6 +459,7 @@ class InputPanel(QWidget):
self.cm_ass_group,
self.mo1_ass_group,
self.fm_ass_group,
self.cm_fm_harm_suppr,
self.smpl,
]
)
@@ -458,8 +476,8 @@ class PositionsPanel(QWidget):
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
# FE Slits
self.sldi_gapx = NumberIndicator('GAPX', 'mm', decimals=3)
self.sldi_gapy = NumberIndicator('GAPY', 'mm', decimals=3)
self.sldi_gapx = NumberIndicator('GAPX', 'mm', decimals=2)
self.sldi_gapy = NumberIndicator('GAPY', 'mm', decimals=2)
self.sldi_pos_group = Group(
'FE Slits',
[
@@ -469,8 +487,8 @@ class PositionsPanel(QWidget):
)
# Collimating mirror
self.cm_trx = NumberIndicator('TRX', 'mm', decimals=1)
self.cm_try = NumberIndicator('TRY', 'mm', decimals=3)
self.cm_trx = NumberIndicator('TRX', 'mm', decimals=2)
self.cm_try = NumberIndicator('TRY', 'mm', decimals=2)
self.cm_bnd = NumberIndicator('BENDER', 'km', decimals=2)
self.cm_rotx = NumberIndicator('PITCH', 'mrad', decimals=3)
self.cm_pos_group = Group(
@@ -485,8 +503,8 @@ class PositionsPanel(QWidget):
# Monochromator
self.mo1_bragg_angle = NumberIndicator('Bragg Angle', 'deg', decimals=3)
self.mo1_trx = NumberIndicator('TRX', 'mm', decimals=1)
self.mo1_try = NumberIndicator('TRY', 'mm', decimals=3)
self.mo1_trx = NumberIndicator('TRX', 'mm', decimals=2)
self.mo1_try = NumberIndicator('TRY', 'mm', decimals=2)
self.mo1_pos_group = Group(
'Monochromator',
[
@@ -497,7 +515,7 @@ class PositionsPanel(QWidget):
)
# OP Slits 1
self.sl1_centery = NumberIndicator('CENTERY', 'mm', decimals=1)
self.sl1_centery = NumberIndicator('CENTERY', 'mm', decimals=2)
self.sl1_pos_group = Group(
'OP Slits 1',
[
@@ -506,7 +524,7 @@ class PositionsPanel(QWidget):
)
# OP Beam Monitor 1
self.bm1_try = NumberIndicator('TRY', 'mm', decimals=1)
self.bm1_try = NumberIndicator('TRY', 'mm', decimals=2)
self.bm1_pos_group = Group(
'OP Beam Monitor 1',
[
@@ -515,8 +533,8 @@ class PositionsPanel(QWidget):
)
# Focusing Mirror
self.fm_trx = NumberIndicator('TRX', 'mm', decimals=1)
self.fm_try = NumberIndicator('TRY', 'mm', decimals=3)
self.fm_trx = NumberIndicator('TRX', 'mm', decimals=2)
self.fm_try = NumberIndicator('TRY', 'mm', decimals=2)
self.fm_bnd = NumberIndicator('BENDER', 'km', decimals=2)
self.fm_rotx = NumberIndicator('PITCH', 'mrad', decimals=3)
self.fm_pos_group = Group(
@@ -530,7 +548,7 @@ class PositionsPanel(QWidget):
)
# OP Slits 2
self.sl2_centery = NumberIndicator('CENTERY', 'mm', decimals=1)
self.sl2_centery = NumberIndicator('CENTERY', 'mm', decimals=2)
self.sl2_pos_group = Group(
'OP Slits 2',
[
@@ -539,7 +557,7 @@ class PositionsPanel(QWidget):
)
# OP Beam Monitor 2
self.bm2_try = NumberIndicator('TRY', 'mm', decimals=1)
self.bm2_try = NumberIndicator('TRY', 'mm', decimals=2)
self.bm2_pos_group = Group(
'OP Beam Monitor 2',
[
@@ -548,7 +566,7 @@ class PositionsPanel(QWidget):
)
# Optical Table
self.ot_try = NumberIndicator('TRY', 'mm', decimals=0)
self.ot_try = NumberIndicator('TRY', 'mm', decimals=2)
self.ot_rotx = NumberIndicator('ROTX', 'mrad', decimals=3)
self.ot_es1_trz = NumberIndicator('ES1 TRZ', 'mm', decimals=0)
self.ot_pos_group = Group(
@@ -585,7 +603,6 @@ class SurfacePlots(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QHBoxLayout(self)
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
self.surfaces = {
'assistant': {
@@ -614,10 +631,11 @@ class SurfacePlots(QWidget):
if theme == "light":
self.color_impenetrable = (30, 30, 30)
self.colors = [(79, 163, 224), (240, 128, 60)]
self.text_color = (255, 255, 255)
else: # dark theme
self.color_impenetrable = (220, 220, 220)
self.colors = [(26, 111, 173), (212, 83, 10)]
self.text_color = (0, 0, 0)
# Create plot widgets
for name, widget in self.plots.items():
@@ -634,8 +652,6 @@ class SurfacePlots(QWidget):
plot_widget.setLabel('left', 'Z [mm]')
plot_widget.setLabel('bottom', 'X [mm]')
plot_widget.setMouseEnabled(x=False, y=False)
# plot_widget.setXRange(0, 25000, padding=0.1)
# plot_widget.setYRange(-20, 120, padding=0.1)
plot_widget.setMenuEnabled(False)
plot_widget.hideButtons()
@@ -645,25 +661,26 @@ class SurfacePlots(QWidget):
# Create surfaces
for idx, scene in enumerate(self.surfaces):
for name, device in self.surfaces[scene].items():
brush = pg.mkBrush(color=self.colors[idx] + (150,))
if scene in 'assistant':
brush = QBrush(QColor(*self.colors[idx], 255), Qt.DiagCrossPattern)
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1, style=Qt.DashLine)
z_value = 2
else:
brush = QBrush(QColor(*self.colors[idx], 255))
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1)
z_value = 1
widget = self.plots[name]
self.plots[name][scene] = widget['widget'].plot(
[],
[],
pen=None,
pen=pen,
name=scene,
brush=brush,
fillLevel=0,
)
self.plots[name][scene].setZValue(1)
# self._layout.addStretch()
logger.info(f'Created surfaces: {self.surfaces}')
logger.info(f'Created plots: {self.plots}')
self.plots[name][scene].setZValue(z_value)
self.plot_walls()
# self.update_curves()
def plot_walls(self):
@@ -678,10 +695,10 @@ class SurfacePlots(QWidget):
rect.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
widget.addItem(rect)
text = pg.TextItem(sf, color='w', anchor=(0.5, 0.5)) # TODO: CHange color according to theme
text = pg.TextItem(sf, color=self.text_color, anchor=(0.5, 0.5)) # TODO: CHange color according to theme
widget.addItem(text)
text.setPos((hx+lx)/2, (hy+ly)/2)
text.setZValue(2)
text.setZValue(10)
def plot_mono_surface(widget, xtal, xtalWidth, xtalOffsetX, xtalLength):
for sf, w, offx, len in zip(xtal, xtalWidth, xtalOffsetX, xtalLength):
@@ -694,10 +711,10 @@ class SurfacePlots(QWidget):
rect.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
widget.addItem(rect)
text = pg.TextItem(sf, color='w', anchor=(0.5, 0.5)) # TODO: CHange color according to theme
text = pg.TextItem(sf, color=self.text_color, anchor=(0.5, 0.5)) # TODO: CHange color according to theme
widget.addItem(text)
text.setPos(offx, 0)
text.setZValue(2)
text.setZValue(10)
for name, plot in self.plots.items():
if name in 'cm':
@@ -714,95 +731,6 @@ class SurfacePlots(QWidget):
for name, plot in self.plots.items():
plot['widget'].disableAutoRange()
def impenetrable_color(self):
app = QApplication.instance()
theme = app.theme.theme # type: ignore
if theme == "light":
return (30, 30, 30)
else:
return (220, 220, 220)
def golden_angle_color(
self,
colormap: str,
num: int,
format="QColor",
theme_offset=0.2,
theme=None,
) -> list:
"""
Extract num colors from the specified colormap following golden angle distribution and return them in the specified format.
Args:
colormap (str): Name of the colormap.
num (int): Number of requested colors.
format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
theme_offset (float): Has to be between 0-1. Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
Returns:
list: List of colors in the specified format.
Raises:
ValueError: If theme_offset is not between 0 and 1.
"""
cmap = pg.colormap.get(colormap)
phi = (1 + np.sqrt(5)) / 2 # Golden ratio
golden_angle_conjugate = 1 - (1 / phi) # Approximately 0.38196601125
min_pos, max_pos = self.set_theme_offset(theme, theme_offset)
# Generate positions within the acceptable range
positions = np.mod(np.arange(num) * golden_angle_conjugate, 1)
positions = min_pos + positions * (max_pos - min_pos)
# Sample colors from the colormap at the calculated positions
colors = cmap.map(positions, mode="float") # type: ignore
color_list = []
for color in colors: # type: ignore
if format.upper() == "HEX":
color_list.append(QColor.fromRgbF(*color).name())
elif format.upper() == "RGB":
color_list.append(tuple((np.array(color) * 255).astype(int)))
elif format.upper() == "QCOLOR":
color_list.append(QColor.fromRgbF(*color))
else:
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
return color_list
def set_theme_offset(self, theme = None, offset=0.2) -> tuple:
"""
Set the theme offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
Args:
theme(str): The theme to be applied.
offset(float): Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
Returns:
tuple: Tuple of min_pos and max_pos.
Raises:
ValueError: If theme_offset is not between 0 and 1.
"""
if offset < 0 or offset > 1:
raise ValueError("theme_offset must be between 0 and 1")
if theme is None:
app = QApplication.instance()
if hasattr(app, "theme"):
theme = app.theme.theme # type: ignore
if theme == "light":
min_pos = 0.0
max_pos = 1 - offset
else:
min_pos = 0.0 + offset
max_pos = 1.0
return min_pos, max_pos
def update_surfaces(self, scene, data):
self.surfaces[scene] = data
for name, device in self.surfaces[scene].items():
@@ -810,9 +738,6 @@ class SurfacePlots(QWidget):
x = np.array(device['x'] + [device['x'][0]]) if len(device['x']) != 0 else np.array([])
y = np.array(device['y'] + [device['y'][0]]) if len(device['y']) != 0 else np.array([])
plot.setData(x=x, y=y)
# fill = pg.FillBetweenItem(curve, widget.plot(device['x'], np.zeros(len(device['x'])), pen=None), brush=pg.mkBrush('b'))
# widget.addItem(fill)
logger.info(self.surfaces)
class SideviewPlot(QWidget):
"""Plot widget with two curves and legend."""
@@ -907,95 +832,6 @@ class SideviewPlot(QWidget):
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
self.plot_widget.addItem(rect)
def impenetrable_color(self):
app = QApplication.instance()
theme = app.theme.theme # type: ignore
if theme == "light":
return (30, 30, 30)
else:
return (220, 220, 220)
def golden_angle_color(
self,
colormap: str,
num: int,
format="QColor",
theme_offset=0.2,
theme=None,
) -> list:
"""
Extract num colors from the specified colormap following golden angle distribution and return them in the specified format.
Args:
colormap (str): Name of the colormap.
num (int): Number of requested colors.
format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
theme_offset (float): Has to be between 0-1. Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
Returns:
list: List of colors in the specified format.
Raises:
ValueError: If theme_offset is not between 0 and 1.
"""
cmap = pg.colormap.get(colormap)
phi = (1 + np.sqrt(5)) / 2 # Golden ratio
golden_angle_conjugate = 1 - (1 / phi) # Approximately 0.38196601125
min_pos, max_pos = self.set_theme_offset(theme, theme_offset)
# Generate positions within the acceptable range
positions = np.mod(np.arange(num) * golden_angle_conjugate, 1)
positions = min_pos + positions * (max_pos - min_pos)
# Sample colors from the colormap at the calculated positions
colors = cmap.map(positions, mode="float") # type: ignore
color_list = []
for color in colors: # type: ignore
if format.upper() == "HEX":
color_list.append(QColor.fromRgbF(*color).name())
elif format.upper() == "RGB":
color_list.append(tuple((np.array(color) * 255).astype(int)))
elif format.upper() == "QCOLOR":
color_list.append(QColor.fromRgbF(*color))
else:
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
return color_list
def set_theme_offset(self, theme = None, offset=0.2) -> tuple:
"""
Set the theme offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
Args:
theme(str): The theme to be applied.
offset(float): Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
Returns:
tuple: Tuple of min_pos and max_pos.
Raises:
ValueError: If theme_offset is not between 0 and 1.
"""
if offset < 0 or offset > 1:
raise ValueError("theme_offset must be between 0 and 1")
if theme is None:
app = QApplication.instance()
if hasattr(app, "theme"):
theme = app.theme.theme # type: ignore
if theme == "light":
min_pos = 0.0
max_pos = 1 - offset
else:
min_pos = 0.0 + offset
max_pos = 1.0
return min_pos, max_pos
def update_curves(self):
for idx, element in enumerate(self.data):
self.curves[idx].setData(
@@ -1004,8 +840,6 @@ class SideviewPlot(QWidget):
)
# --------------------------------------------------------- Standalone run ---
if __name__ == "__main__":
from qtpy.QtWidgets import QApplication
from bec_widgets.utils.bec_dispatcher import BECDispatcher
+14 -5
View File
@@ -6,6 +6,7 @@ from qtpy.QtWidgets import (
QPushButton, QGroupBox, QComboBox, QApplication, QDoubleSpinBox
)
from qtpy.QtGui import QFont
from qtpy.QtCore import Qt
class Group(QGroupBox):
def __init__(self, label, widgets):
@@ -47,9 +48,12 @@ class NumberIndicator(QWidget):
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.label = QLabel(label)
self.label.setFixedWidth(150)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QLabel('-')
self.val.setAlignment(Qt.AlignTop) # type: ignore
# self.val.setFixedWidth(140)
layout.addWidget(self.val)
self.unit = unit
@@ -84,7 +88,9 @@ class InputTextField(QWidget):
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.label = QLabel(label)
self.label.setFixedWidth(150)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QLineEdit()
self.val.setPlaceholderText('0')
@@ -113,7 +119,9 @@ class InputNumberField(QWidget):
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.label = QLabel(label)
self.label.setFixedWidth(150)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.val = QDoubleSpinBox()
self.val.setRange(ll, hl)
@@ -204,10 +212,11 @@ class ComboBox(QWidget):
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)
self.label = QLabel(label)
self.label.setFixedWidth(150)
self.label.setFixedWidth(140)
self.label.setContentsMargins(0, 0, 10, 0)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.value = QComboBox()
# self.value.setFixedWidth(140)
for entry in enums:
self.value.addItem(entry)
layout.addWidget(self.value)