wip: digital twin widget
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# This file was automatically generated by generate_cli.py
|
||||
# type: ignore
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# pylint: skip-file
|
||||
|
||||
|
||||
_Widgets = {}
|
||||
|
||||
|
||||
class DigitalTwin(RPCBase):
|
||||
"""A simple BEC widget with:"""
|
||||
|
||||
@rpc_call
|
||||
def set_a(self, value: float):
|
||||
"""
|
||||
Set input A remotely from the BEC CLI.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_b(self, value: float):
|
||||
"""
|
||||
Set input B remotely from the BEC CLI.
|
||||
"""
|
||||
@@ -0,0 +1,221 @@
|
||||
import os
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
|
||||
os.environ["USE_XRT"] = "False"
|
||||
import debye_bec.bec_ipython_client.plugins.digital_twin.x01da_parameters as bl
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
def calculate_positions(cfg):
|
||||
|
||||
pos = {}
|
||||
|
||||
## FE slits
|
||||
trxr = -np.arctan(cfg['h_acc'])*bl.feSlits.center1[1]
|
||||
trxw = (np.arctan(cfg['h_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1]
|
||||
tryb = -np.arctan(cfg['v_acc'])*bl.feSlits.center1[1]
|
||||
tryt = (np.arctan(cfg['v_acc'])*bl.feSlits.center1[1])/bl.feSlits.center1[1]*bl.feSlits.center2[1]
|
||||
|
||||
trxw_proj = trxw/bl.feSlits.center2[1]*bl.feSlits.center1[1]
|
||||
tryt_proj = tryt/bl.feSlits.center2[1]*bl.feSlits.center1[1]
|
||||
|
||||
xcen = (trxr + trxw_proj) / 2
|
||||
ycen = (tryb + tryt_proj) / 2
|
||||
xgap = trxw_proj - trxr
|
||||
ygap = tryt_proj - tryb
|
||||
|
||||
pos['sldi_gapx'] = {'value': xgap}
|
||||
pos['sldi_gapy'] = {'value': ygap}
|
||||
|
||||
## Collimating Mirror
|
||||
obj_dist = bl.cm.center[1] # object distance
|
||||
|
||||
# TRX
|
||||
try:
|
||||
index = bl.cm.surface.index(cfg['cm_stripe'])
|
||||
except:
|
||||
raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!")
|
||||
cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2
|
||||
pos['cm_trx'] = {'value': cm_trx}
|
||||
|
||||
# TRY
|
||||
height = obj_dist * np.tan(cfg['v_acc'])**2 * 1 / np.tan(cfg['cm_pitch'])
|
||||
pos['cm_try'] = {'value': height}
|
||||
|
||||
# Pitch
|
||||
pos['cm_rotx'] = {'value': -cfg["cm_pitch"]*1e3} # invert and convert to mrad (same as EGU of rotx axis)
|
||||
|
||||
# Bending Radius
|
||||
radius = 2. * obj_dist / np.sin(cfg['cm_pitch']) # Elements of modern X-ray Physics, page 108 ff.
|
||||
pos['cm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km
|
||||
|
||||
## Monochromator
|
||||
# Bragg Angle
|
||||
# TODO Should the bragg angle be corrected for the symmetric bragg case?
|
||||
# See raytracing script or here: bragg = np.asin(rm.ch / (2.*cfg['dSpacing']*cfg['energyCCM'])) - aCrystal.get_dtheta_symmetric_Bragg(cfg['energyCCM'])
|
||||
if cfg['mo_mode'] == 'Monochromatic':
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg = ((2 * cfg['cm_pitch']) + cfg['mo_bragg'][1]) / np.pi * 180
|
||||
elif cfg['mo_mode'] == 'Pinkbeam':
|
||||
# Align xtal surfaces parallel to beam
|
||||
bragg = (2 * cfg['cm_pitch']) / np.pi * 180
|
||||
else:
|
||||
raise Exception('Monochromator mode not supported')
|
||||
pos['mo1_bragg_angle'] = {'value': bragg} # Bragg angle in deg
|
||||
|
||||
# TRY, Height
|
||||
l = bl.mo1.xtalGap[0]/np.sin(cfg['mo_bragg'][1])
|
||||
yhor = l*np.cos(2.*(cfg['mo_bragg'][1]+cfg['cm_pitch']))
|
||||
yver = yhor*np.tan(2.*cfg['cm_pitch'])
|
||||
|
||||
if cfg['mo_mode'] == 'Monochromatic':
|
||||
beamOffsetCCM = l*np.sin(2.*(cfg['mo_bragg'][1]+cfg['cm_pitch']))-yver # Resultat ist korrekt!
|
||||
elif cfg['mo_mode'] == 'Pinkbeam':
|
||||
beamOffsetCCM = 0
|
||||
else:
|
||||
raise Exception('Monochromator mode not supported')
|
||||
|
||||
def csc(a):
|
||||
return 1/np.sin(a)
|
||||
|
||||
def cot(a):
|
||||
return 1/np.tan(a)
|
||||
|
||||
# calculate height of center of first crystal surface
|
||||
f = bl.mo1.rotOffset # rotation offset, mm
|
||||
logger.info(f'f = {f}')
|
||||
d = bl.mo1.heightOffset # xtal height offset, mm
|
||||
logger.info(f'd = {d}')
|
||||
c = d*csc(cfg['mo_bragg'][1])-f*cot(cfg['mo_bragg'][1])
|
||||
logger.info(f'c = {c}')
|
||||
|
||||
# Calculate height of center of rotation
|
||||
b = np.sqrt(d**2*csc(cfg['mo_bragg'][1])**2-2*d*f*cot(cfg['mo_bragg'][1])*csc(cfg['mo_bragg'][1])+f**2*cot(cfg['mo_bragg'][1])**2+f**2)
|
||||
logger.info(f'b = {b}')
|
||||
h = np.cos(np.pi/2-np.arctan(f/c)-cfg['mo_bragg'][1]-2*cfg['cm_pitch'])*b
|
||||
logger.info(f'h = {h}')
|
||||
h2 = ((bl.mo1.center[1] - bl.cm.center[1])-np.sqrt(b**2-h**2))*np.tan(2*cfg['cm_pitch'])
|
||||
logger.info(f'mo1 = {bl.mo1.center[1]}')
|
||||
logger.info(f'cm = {bl.cm.center[1]}')
|
||||
logger.info(f'pitch = {cfg["cm_pitch"]}')
|
||||
logger.info(f'h2 = {h2}')
|
||||
#TODO Mono height not exactly the same as in raytracing
|
||||
heightCCM1real = h + h2 # per design, the height should not change if the pitch of the CM is not changed!
|
||||
# heightCCM1real = heightCCM1real - 30 # Zero position of stage is at 1430 mm from ground.
|
||||
if cfg['mo_mode'] == 'Monochromatic':
|
||||
pass
|
||||
elif cfg['mo_mode'] == 'Pinkbeam':
|
||||
heightCCM1real = heightCCM1real - 13 # Move down to let beam pass between both crystal without touching copper cooler
|
||||
else:
|
||||
raise Exception('Monochromator mode not supported')
|
||||
pos['mo1_try'] = {'value': heightCCM1real}
|
||||
|
||||
# TRX, Crystal selection
|
||||
try:
|
||||
xtal = cfg['mo_xtal'].translate(str.maketrans('', '', '()')) # Remove brackets from xtal name to conform with parameters
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
except:
|
||||
raise ValueError(f"Requested xtal {xtal} not found in parameters!")
|
||||
pos['mo1_trx'] = {'value': bl.mo1.xtalOffsetX[index]}
|
||||
|
||||
|
||||
#TODO move to mono, calc for beam Z-movement between crystal surfaces
|
||||
diag = bl.mo1.xtalGap[0] / np.sin(bragg) # Calculations for Mono
|
||||
dz = diag * np.cos(2 * (cfg['cm_pitch'] + bragg))
|
||||
|
||||
## Slits 1
|
||||
d = bl.opSlits1.center[1] - bl.cm.center[1] - dz
|
||||
sl1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM
|
||||
pos['sl1_centery'] = {'value': sl1_beam_height}
|
||||
|
||||
## Beam Monitor 1
|
||||
d = bl.opBM1.center[1] - bl.cm.center[1] - dz
|
||||
logger.info(f'distance: {d}')
|
||||
logger.info(f'cm pitch: {cfg["cm_pitch"]}')
|
||||
logger.info(f'mono offset: {beamOffsetCCM}')
|
||||
bm1_beam_height = d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM
|
||||
pos['bm1_try'] = {'value': bm1_beam_height}
|
||||
|
||||
## Focusing Mirror
|
||||
p = bl.fm.center[1]
|
||||
q = cfg['smpl'] - bl.fm.center[1]
|
||||
f = (p*q)/(p+q) # focal length
|
||||
|
||||
# Bender radius
|
||||
radius = 2 * q / np.sin(cfg['fm_pitch']) # ideal bending radius
|
||||
pos['fm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km
|
||||
|
||||
# Pitch
|
||||
d = bl.fm.center[1] - bl.cm.center[1] - dz
|
||||
fm_pitch = 2 * cfg['cm_pitch'] - cfg['fm_pitch'] # calculate pitch in absolute values (according to horizontal plane)
|
||||
pos['fm_rotx'] = {'value': -fm_pitch * 1e3} # invert and convert to mrad (same as EGU of rotx axis)
|
||||
|
||||
if cfg['fm_stripe'] in ('Rh (toroid)', 'Pt (toroid)'):
|
||||
|
||||
# TRY
|
||||
if cfg['fm_stripe'] in 'Rh (toroid)':
|
||||
r = bl.fm.r[0]
|
||||
h_cyl = bl.fm.hToroid[0]
|
||||
else: # PT toroid
|
||||
r = bl.fm.r[1]
|
||||
h_cyl = bl.fm.hToroid[1]
|
||||
widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc'] * 1e-3)
|
||||
alpha = np.arccos(1 - widthBeam**2 / (2 * r**2))
|
||||
h = r - (r * np.cos(alpha / 2))
|
||||
fm_beam_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height']
|
||||
fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM - h_cyl + h / 2) * cfg['fm_gain_height']
|
||||
pos['fm_try'] = {'value': fm_height}
|
||||
|
||||
# TRX
|
||||
if cfg['fm_stripe'] in 'Rh (toroid)':
|
||||
x_cyl = - bl.fm.xToroid[0]
|
||||
else:
|
||||
x_cyl = - bl.fm.xToroid[1]
|
||||
pos['fm_trx'] = {'value': x_cyl}
|
||||
|
||||
elif cfg['fm_stripe'] in ('Rh (flat)', 'Pt (flat)'):
|
||||
|
||||
# TRY
|
||||
fm_height = (d * np.tan(2 * cfg['cm_pitch']) + beamOffsetCCM) * cfg['fm_gain_height']
|
||||
fm_beam_height = fm_height
|
||||
pos['fm_try'] = {'value': fm_height}
|
||||
|
||||
# TRX
|
||||
if cfg['fm_stripe'] in 'Rh (flat)':
|
||||
x_flat = - bl.fm.xFlat[0]
|
||||
else:
|
||||
x_flat = - bl.fm.xFlat[1]
|
||||
pos['fm_trx'] = {'value': x_flat}
|
||||
|
||||
else:
|
||||
raise Exception('FM Stripe selection not valid')
|
||||
|
||||
## Slits 2
|
||||
d = bl.opSlits2.center[1] - bl.fm.center[1]
|
||||
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
|
||||
pos['sl2_centery'] = {'value': sl2_beam_height}
|
||||
|
||||
## Beam Monitor 2
|
||||
d = bl.opBM2.center[1] - bl.fm.center[1]
|
||||
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
|
||||
pos['bm2_try'] = {'value': bm2_beam_height}
|
||||
|
||||
## Optical Table / Exit Window
|
||||
|
||||
# TRY
|
||||
d = bl.ehWindow.center[1] - bl.fm.center[1]
|
||||
ot_height = fm_beam_height - d * np.tan(-(2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch']))
|
||||
# logger.info(fm_height)
|
||||
# logger.info(d * np.tan((2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch'])))
|
||||
pos['ot_try'] = {'value': ot_height}
|
||||
|
||||
# Pitch
|
||||
ot_pitch = - (2 * cfg['cm_pitch'] - 2 * cfg['fm_pitch'])
|
||||
pos['ot_rotx'] = {'value': ot_pitch * 1e3}
|
||||
|
||||
# TRZ ES1
|
||||
ot_es1_trz = cfg['smpl']
|
||||
pos['ot_es1_trz'] = {'value': ot_es1_trz}
|
||||
|
||||
return pos
|
||||
@@ -0,0 +1,365 @@
|
||||
import sys
|
||||
import datetime
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QDoubleSpinBox, QGroupBox, QApplication, QLineEdit, QLayout
|
||||
)
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import QTimer, Qt
|
||||
from qtpy.QtGui import QColor
|
||||
import pyqtgraph as pg
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
from debye_bec.bec_widgets.widgets.qt_widgets import InputNumberField, ComboBox, Group
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
class DigitalTwin(BECWidget, QWidget):
|
||||
"""
|
||||
A simple BEC widget with:
|
||||
- Two numeric inputs (A, B)
|
||||
- Two computed outputs (Sum, Product)
|
||||
- A live plot that updates every second
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["set_a", "set_b"]
|
||||
PLUGIN = True
|
||||
ICON_NAME = "lightbulb"
|
||||
|
||||
def __init__(self, parent=None, *arg, **kwargs):
|
||||
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self._history = [] # stores (sum, product) over time
|
||||
self._t = 0 # tick counter
|
||||
|
||||
central = QWidget()
|
||||
self.root_layout = QHBoxLayout(central)
|
||||
|
||||
self.plot_widget = PlotWidget(title='Plot title', chart_data = [])
|
||||
self.control_panel = InputPanel()
|
||||
|
||||
self.root_layout.addWidget(self.plot_widget, stretch=3)
|
||||
self.root_layout.addWidget(self.control_panel, stretch=1, alignment=Qt.AlignTop)
|
||||
|
||||
self.setLayout(self.root_layout)
|
||||
self.setWindowTitle("Digital Twin")
|
||||
self.resize(600, 500)
|
||||
|
||||
# self.init_ui()
|
||||
# self._recalculate() # populate outputs on startup
|
||||
|
||||
# Timer: update plot every 1 second
|
||||
# self._timer = QTimer(self)
|
||||
# self._timer.setInterval(1000)
|
||||
# self._timer.timeout.connect(self._tick)
|
||||
# self._timer.start()
|
||||
|
||||
# ------------------------------------------------------------------ UI ---
|
||||
|
||||
# def init_ui(self):
|
||||
|
||||
# self.spin_a = InputNumberField('Acceptance 1')
|
||||
# self.spin_b = InputNumberField('Acceptance 2')
|
||||
# self.input_group = Group(
|
||||
# 'Inputs',
|
||||
# [
|
||||
# self.spin_a,
|
||||
# self.spin_b,
|
||||
# ]
|
||||
# )
|
||||
# self.root_layout.addWidget(self.input_group)
|
||||
# self.root_layout.addStretch()
|
||||
|
||||
|
||||
# root = QVBoxLayout(self)
|
||||
|
||||
# # --- Inputs ---
|
||||
# input_group = QGroupBox("Inputs")
|
||||
# input_layout = QHBoxLayout(input_group)
|
||||
|
||||
# self._spin_a = QLineEdit()
|
||||
# self._spin_a.setPlaceholderText('0')
|
||||
# self._spin_a.setText('0')
|
||||
# # self._spin_a.setRange(-1e6, 1e6)
|
||||
# # self._spin_a.setDecimals(3)
|
||||
# # self._spin_a.setValue(1.0)
|
||||
# # self._spin_a.setSingleStep(0.1)
|
||||
|
||||
# self._spin_b = QLineEdit()
|
||||
# self._spin_b.setPlaceholderText('0')
|
||||
# self._spin_b.setText('0')
|
||||
# # self._spin_b.setRange(-1e6, 1e6)
|
||||
# # self._spin_b.setDecimals(3)
|
||||
# # self._spin_b.setValue(2.0)
|
||||
# # self._spin_b.setSingleStep(0.1)
|
||||
|
||||
# self._spin_c = QLineEdit()
|
||||
# self._spin_c.setPlaceholderText('0')
|
||||
# self._spin_c.setText('10')
|
||||
|
||||
# input_layout.addWidget(QLabel("A:"))
|
||||
# input_layout.addWidget(self._spin_a)
|
||||
# input_layout.addWidget(QLabel("B:"))
|
||||
# input_layout.addWidget(self._spin_b)
|
||||
# input_layout.addWidget(QLabel("C:"))
|
||||
# input_layout.addWidget(self._spin_c)
|
||||
# root.addWidget(input_group)
|
||||
|
||||
# # --- Outputs ---
|
||||
# output_group = QGroupBox("Outputs")
|
||||
# output_layout = QHBoxLayout(output_group)
|
||||
|
||||
# self._label_sum = QLabel("Sum: —")
|
||||
# self._label_product = QLabel("Product: —")
|
||||
# output_layout.addWidget(self._label_sum)
|
||||
# output_layout.addWidget(self._label_product)
|
||||
# root.addWidget(output_group)
|
||||
|
||||
# # --- Plot ---
|
||||
# plot_group = QGroupBox("Live History (updates every 1 s)")
|
||||
# plot_layout = QVBoxLayout(plot_group)
|
||||
|
||||
# self._plot_widget = pg.PlotWidget()
|
||||
# self._plot_widget.setBackground("w")
|
||||
# self._plot_widget.addLegend()
|
||||
# self._plot_widget.setLabel("left", "Value")
|
||||
# self._plot_widget.setLabel("bottom", "Tick")
|
||||
|
||||
# self._curve_sum = self._plot_widget.plot(
|
||||
# pen=pg.mkPen("b", width=2), name="Sum"
|
||||
# )
|
||||
# self._curve_product = self._plot_widget.plot(
|
||||
# pen=pg.mkPen("r", width=2), name="Product"
|
||||
# )
|
||||
# plot_layout.addWidget(self._plot_widget)
|
||||
# plot_group.setLayout(plot_layout)
|
||||
# root.addWidget(plot_group)
|
||||
|
||||
# self.setLayout(root)
|
||||
# self.setWindowTitle("BEC Calculator Widget")
|
||||
# self.resize(600, 500)
|
||||
|
||||
# # Connect inputs → recalculate
|
||||
# self._spin_a.editingFinished .connect(self._recalculate)
|
||||
# self._spin_b.editingFinished .connect(self._recalculate)
|
||||
|
||||
# ---------------------------------------------------------- Logic ---
|
||||
|
||||
# @SafeSlot()
|
||||
# def _recalculate(self):
|
||||
# # logger.info(var)
|
||||
# a = float(self._spin_a.text())
|
||||
# b = float(self._spin_b.text())
|
||||
# s = a + b
|
||||
# p = a * b
|
||||
# self._label_sum.setText(f"Sum: {s:.4f}")
|
||||
# self._label_product.setText(f"Product: {p:.4f}")
|
||||
# self._current_sum = s
|
||||
# self._current_product = p
|
||||
|
||||
# @SafeSlot()
|
||||
# def _tick(self):
|
||||
# """Called every second: record current outputs and refresh plot."""
|
||||
# self._history.append((self._t, self._current_sum, self._current_product))
|
||||
# self._t += 1
|
||||
|
||||
# ticks = [h[0] for h in self._history]
|
||||
# sums = [h[1] for h in self._history]
|
||||
# products = [h[2] for h in self._history]
|
||||
|
||||
# self._curve_sum.setData(ticks, sums)
|
||||
# self._curve_product.setData(ticks, products)
|
||||
|
||||
# # --------------------------------------------------- RPC interface ---
|
||||
|
||||
# def set_a(self, value: float):
|
||||
# """Set input A remotely from the BEC CLI."""
|
||||
# self._spin_a.setValue(value)
|
||||
|
||||
# def set_b(self, value: float):
|
||||
# """Set input B remotely from the BEC CLI."""
|
||||
# self._spin_b.setValue(value)
|
||||
|
||||
|
||||
class InputPanel(QWidget):
|
||||
"""Right-side control panel: input field, indicator, send, recording."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setSizeConstraint(QLayout.SetFixedSize)
|
||||
|
||||
self.energy = InputNumberField('Energy [keV]')
|
||||
|
||||
self.sldi_hacc = InputNumberField('Horizontal [± mrad]')
|
||||
self.sldi_vacc = InputNumberField('Vertical [± mrad]')
|
||||
self.fe_slits_group = Group(
|
||||
'FE Slits Acceptance',
|
||||
[
|
||||
self.sldi_hacc,
|
||||
self.sldi_vacc,
|
||||
]
|
||||
)
|
||||
|
||||
self.assistant_group = Group(
|
||||
'Assistant',
|
||||
[
|
||||
self.energy,
|
||||
self.fe_slits_group,
|
||||
]
|
||||
)
|
||||
|
||||
self._layout .addWidget(self.assistant_group)
|
||||
self._layout .addStretch()
|
||||
|
||||
class PlotWidget(QWidget):
|
||||
"""Plot widget with two curves and legend."""
|
||||
|
||||
def __init__(self, title: str = "Title", chart_data = [], max_points=2000, parent=None):
|
||||
super().__init__(parent)
|
||||
self.chart_data = chart_data
|
||||
self.max_points = max_points
|
||||
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._title = QLabel(f"<h2>{title}</h2>")
|
||||
self._layout.addWidget(self._title)
|
||||
|
||||
self.plot_widget = pg.PlotWidget(axisItems={'bottom': TimeAxis(orientation='bottom')})
|
||||
self.plot_widget.getAxis('bottom').enableAutoSIPrefix(False)
|
||||
self.plot_widget.addLegend()
|
||||
|
||||
self.curves = []
|
||||
colors = self.golden_angle_color(
|
||||
colormap='plasma', num=max(10, len(self.curves) + 1), format="HEX"
|
||||
)
|
||||
|
||||
for idx, element in enumerate(self.chart_data):
|
||||
self.curves.append(
|
||||
self.plot_widget.plot(
|
||||
[],
|
||||
[],
|
||||
pen=pg.mkPen(color=colors[idx], width=2),
|
||||
name=element,
|
||||
)
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.plot_widget)
|
||||
|
||||
self.plot_widget.setLabel('left', 'Temperature [°C]')
|
||||
self.plot_widget.setLabel('bottom', 'Time')
|
||||
|
||||
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")
|
||||
color_list = []
|
||||
|
||||
for color in colors:
|
||||
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
|
||||
|
||||
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, timestamps: list[str], data: list[float]):
|
||||
x = timestamps.copy()
|
||||
y = data.copy()
|
||||
min_len = min([min([len(i) for i in y]), len(x)])
|
||||
x_float = [t.timestamp() for t in x]
|
||||
for idx, element in enumerate(y):
|
||||
self.curves[idx].setData(x=np.array(x_float)[0:min_len], y=np.array(element)[0:min_len])
|
||||
|
||||
class TimeAxis(pg.AxisItem):
|
||||
def tickStrings(self, values, scale, spacing):
|
||||
return [datetime.fromtimestamp(value).strftime("%H:%M:%S") for value in values]
|
||||
|
||||
|
||||
# --------------------------------------------------------- Standalone run ---
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
dispatcher = BECDispatcher(gui_id="digital_twin")
|
||||
win = DigitalTwin()
|
||||
|
||||
win.resize(1000, 800)
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['digital_twin.py']}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.digital_twin import DigitalTwin
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='DigitalTwin' name='digital_twin'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class DigitalTwinPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = DigitalTwin(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(DigitalTwin.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "digital_twin"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "DigitalTwin"
|
||||
|
||||
def toolTip(self):
|
||||
return "DigitalTwin"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from debye_bec.bec_widgets.widgets.digital_twin.digital_twin_plugin import DigitalTwinPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(DigitalTwinPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
X01DA / Debye Beamline Parameters.
|
||||
This file describes the parameter of each component of the Debye beamline
|
||||
to be used for raytracing and geometrical calculations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
from collections import namedtuple
|
||||
|
||||
if os.environ.get("USE_XRT", "True").lower() in ("1", "true", "yes"):
|
||||
import xrt.backends.raycing.materials as rm # type: ignore
|
||||
else:
|
||||
class _DummyClass:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
class _DummyMaterials:
|
||||
Material = _DummyClass
|
||||
CrystalSi = _DummyClass
|
||||
rm = _DummyMaterials()
|
||||
|
||||
# XRT definitions
|
||||
filterBeryl = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
|
||||
stripeSi = rm.Material('Si', rho=2.33) # pyright: ignore[reportArgumentType]
|
||||
stripePt = rm.Material('Pt', rho=21.45) # pyright: ignore[reportArgumentType]
|
||||
stripeRh = rm.Material('Rh', rho=12.41) # pyright: ignore[reportArgumentType]
|
||||
stripeCr = rm.Material('Cr', rho=7.14) # pyright: ignore[reportArgumentType]
|
||||
stripePyrex = rm.Material('Si', rho=2.20) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
|
||||
|
||||
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
|
||||
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
|
||||
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
|
||||
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
|
||||
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
|
||||
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
|
||||
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
|
||||
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
|
||||
|
||||
filterDiamond = rm.Material('C', rho=3.52, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterBe = rm.Material('Be', rho=1.85, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterSi3N4 = rm.Material(['Si', 'N'], quantities=[3, 4], rho=3.44, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterAl = rm.Material('Al', rho=2.69, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material('C', rho=2.266, kind='plate') # pyright: ignore[reportArgumentType]
|
||||
|
||||
# General parameters
|
||||
sourceHeight = 0
|
||||
|
||||
#Synchrotron
|
||||
synchrotron = namedtuple('synchrotron', ['eE', 'eI', 'eEspread',
|
||||
'eEpsilonX', 'eEpsilonZ', 'betaX', 'betaZ'])
|
||||
|
||||
sls1 = synchrotron(
|
||||
eE = 2.4,
|
||||
eI = 0.4,
|
||||
eEspread=0.878e-3,
|
||||
eEpsilonX=5.63,
|
||||
eEpsilonZ=0.007,
|
||||
betaX=0.45,
|
||||
betaZ=14.4,
|
||||
)
|
||||
|
||||
sls2 = synchrotron(
|
||||
eE=2.7,
|
||||
eI=0.4,
|
||||
eEspread=1.147e-3,
|
||||
eEpsilonX=0.156,
|
||||
eEpsilonZ=0.01,
|
||||
betaX=0.18,
|
||||
betaZ=4.6,
|
||||
)
|
||||
|
||||
# Source
|
||||
bendingMagnet = namedtuple('bendingMagnet', ['name', 'center', 'sync', 'B0'])
|
||||
|
||||
sls1_14t = bendingMagnet(
|
||||
name='FE-BM-SLS1-1.4T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls1,
|
||||
B0=1.4,)
|
||||
|
||||
sls2_21t = bendingMagnet(
|
||||
name='FE-BM-SLS2-2.1T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls2,
|
||||
B0=2.1,)
|
||||
|
||||
sls2_35t = bendingMagnet(
|
||||
name='FE-BM-SLS2-3.5T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls2,
|
||||
B0=3.5,)
|
||||
|
||||
sls2_50t = bendingMagnet(
|
||||
name='FE-BM-SLS2-5.0T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls2,
|
||||
B0=5.0,)
|
||||
|
||||
# FE slits
|
||||
fe_slits = namedtuple('slits', ['name', 'center', 'center1', 'center2', 'maxDivH', 'maxDivV'])
|
||||
|
||||
feSlits = fe_slits(
|
||||
name='FE-SLITS',
|
||||
center=(0, 6117, sourceHeight),
|
||||
center1=(0, 5045, sourceHeight),
|
||||
center2=(0, 5289.5, sourceHeight),
|
||||
maxDivH=1.8e-3,
|
||||
maxDivV=0.8e-3,)
|
||||
|
||||
# FE Window
|
||||
filt = namedtuple('filt', ['name', 'center', 'pitch', 'limPhysX', 'limPhysY', 'surface', 'material', 'thickness'])
|
||||
|
||||
feWindow = filt(
|
||||
name='FE-WINDOW',
|
||||
center=(0., 7020, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-6, 6),
|
||||
limPhysY=(-3., 3.),
|
||||
surface='None',
|
||||
material=filterDiamond,
|
||||
thickness=0.1,)
|
||||
feWindow = feWindow._replace(surface=f'CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m')
|
||||
|
||||
# Collimating mirror
|
||||
collimatingMirror = namedtuple('collimatingMirror', ['name',
|
||||
'center', 'surface', 'material', 'limPhysX', 'limPhysY',
|
||||
'limOptX', 'limOptY', 'R', 'pitch', 'jack1', 'jack2', 'jack3',
|
||||
'tx1', 'tx2'])
|
||||
|
||||
cm = collimatingMirror(
|
||||
name='FE-CM',
|
||||
center=[0, 6890, sourceHeight],
|
||||
surface=('Si','Pt','Rh'),
|
||||
material=(stripeSi, stripePt, stripeRh),
|
||||
limPhysX=(-34, 34),
|
||||
limPhysY=(-600, 600),
|
||||
limOptX=((-27, -3.5, 15), (-11, 6.5, 25)),
|
||||
limOptY=((-500, -500, -500), (500, 500, 500)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, -0.0e-3],
|
||||
jack1=[0., 7210., 0.], #Tripod X, Y, Z (global)
|
||||
jack2=[-210., 8310., 0.],
|
||||
jack3=[210., 8310., 0.],
|
||||
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
|
||||
tx2=[0.0, 575],) # X-Stage 2
|
||||
|
||||
apertures = namedtuple('apertures', ['name', 'center', 'opening'])
|
||||
|
||||
fePS = apertures(
|
||||
name='FE-PS',
|
||||
center=[0, 8815, sourceHeight],
|
||||
opening=[-20., 20., -20.+12.5, 20.+12.5]) # left, right, bottom, top
|
||||
|
||||
opWbBsBlock = apertures(
|
||||
name='OP-WB-BS-BLOCK',
|
||||
center=[0., 13860, sourceHeight],
|
||||
opening=[-18., 18., 25, 85.5]) # left, right, bottom, top
|
||||
# opening=[-18., 18., 42, 76], # X10DA
|
||||
|
||||
# Monochromator
|
||||
monochromator = namedtuple('monochromator', ['name', 'center',
|
||||
'xtal', 'material1', 'material2', 'xtalWidth', 'xtalOffsetX',
|
||||
'xtalLength1', 'xtalLength2', 'xtalGap', 'rotOffset',
|
||||
'heightOffset', 'braggLim', 'jack1', 'jack2', 'jack3', 'tx'])
|
||||
|
||||
mo1 = monochromator(
|
||||
name='OP-MO1',
|
||||
center=[0., 11750, sourceHeight],
|
||||
xtal=('Si311','Si111'),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth = (24, 24),
|
||||
xtalOffsetX=(-21.2, 21.2),
|
||||
xtalLength1 = (55, 55),
|
||||
xtalLength2 = (105, 105),
|
||||
xtalGap = (8, 8),
|
||||
rotOffset = 6,
|
||||
heightOffset = 8.5,
|
||||
braggLim = [3.6, 33],
|
||||
jack1=[0., 11350., 0.], #Tripod maybe not available!
|
||||
jack2=[-400., 12350., 0.],
|
||||
jack3=[400., 12350., 0.],
|
||||
tx=0.0,) # X-Stage [x]
|
||||
|
||||
mo2 = monochromator(
|
||||
name='OP-CCM2',
|
||||
center=[0., 13250, sourceHeight],
|
||||
xtal=('Si311','Si111'),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth = (24, 24),
|
||||
xtalOffsetX=(-21, 21),
|
||||
xtalLength1 = (55, 55),
|
||||
xtalLength2 = (105, 105),
|
||||
xtalGap = (8, 8),
|
||||
rotOffset = 6,
|
||||
heightOffset = 8.5,
|
||||
braggLim = [3.6, 33],
|
||||
jack1=[0., 13350., 0.], #Tripod maybe not available!
|
||||
jack2=[-400., 14350., 0.],
|
||||
jack3=[400., 14350., 0.],
|
||||
tx=0.0,) # X-Stage [x]
|
||||
|
||||
# OP Slits
|
||||
op_slits = namedtuple('op_slits', ['name', 'center'])
|
||||
|
||||
opSlits1 = op_slits(
|
||||
name='OP-SLITS 1',
|
||||
center=(0, 14349.6, sourceHeight),
|
||||
)
|
||||
|
||||
opSlits2 = op_slits(
|
||||
name='OP-SLITS 2',
|
||||
center=(0, 18134.8, sourceHeight),
|
||||
)
|
||||
|
||||
# OP Beam Monitors
|
||||
op_bm = namedtuple('op_bm', ['name', 'center'])
|
||||
|
||||
opBM1 = op_bm(
|
||||
name='OP Beam Monitor 1',
|
||||
center=(0, 14599.6, sourceHeight),
|
||||
)
|
||||
|
||||
opBM2 = op_bm(
|
||||
name='OP Beam Monitor 2',
|
||||
center=(0, 18384.8, sourceHeight),
|
||||
)
|
||||
|
||||
# Focusing mirror
|
||||
focusingMirror = namedtuple('focusingMirror', ['name', 'center',
|
||||
'surfaceToroid', 'materialToroid', 'surfaceFlat', 'materialFlat',
|
||||
'limPhysXToroid', 'limPhysYToroid', 'limPhysXFlat', 'limPhysYFlat',
|
||||
'limOptXToroid', 'limOptYToroid', 'limOptXFlat', 'limOptYFlat',
|
||||
'R', 'pitch', 'r', 'xToroid', 'xFlat', 'hToroid', 'jack1', 'jack2', 'jack3',
|
||||
'tx1', 'tx2'])
|
||||
|
||||
fm = focusingMirror(
|
||||
name='OP-FM',
|
||||
center=[0., 15670, sourceHeight], # nominal height 58 mm above ring, SLS1!
|
||||
surfaceToroid=('Rh', 'Pt'),
|
||||
materialToroid=(stripeRh, stripePt),
|
||||
surfaceFlat=('Rh', 'Pt'),
|
||||
materialFlat=(stripeRh, stripePt),
|
||||
limPhysXToroid=(-79., 79.),
|
||||
limPhysYToroid=(-575., 575.),
|
||||
limPhysXFlat=(-79., 79.),
|
||||
limPhysYFlat=(-575., 575.),
|
||||
limOptXToroid=((-38, 66), (-66, 31)),
|
||||
limOptYToroid=((-500., -500.), (500., 500.)),
|
||||
limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)),
|
||||
limOptYFlat=((-500., -500.), (500., 500.)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, 0e-3],
|
||||
r=[35.510, 24.986],
|
||||
xToroid=[-52, 48.5], # offset in local x
|
||||
xFlat = [-20.95, 8.55],
|
||||
hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
|
||||
jack1=[-130., 15535-538., 0.],
|
||||
jack2=[130., 15535+538., 0.],
|
||||
jack3=[0., 15535+538., 0.],
|
||||
tx1=[0., -575.], # X-Stage 1 [x, y]
|
||||
tx2=[0., 575.],) # X-Stage 2 [x, y]
|
||||
|
||||
# EH Window
|
||||
ehWindow = filt(
|
||||
name='EH-WINDOW',
|
||||
center=(0., 19998.3, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-20., 20.),
|
||||
limPhysY=(-4, 4),
|
||||
surface='None',
|
||||
material=filterSi3N4,
|
||||
thickness=0.002,)
|
||||
ehWindow = ehWindow._replace(surface=f'Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m')
|
||||
|
||||
# Sample
|
||||
sample = namedtuple('sample', ['name', 'center'])
|
||||
|
||||
smpl = sample(
|
||||
name='EH-SMPL',
|
||||
center=[0, 23365, sourceHeight],)
|
||||
|
||||
smpl2 = sample(
|
||||
name='EH-SMPL2',
|
||||
center=[0, 27500, sourceHeight],)
|
||||
@@ -0,0 +1,273 @@
|
||||
|
||||
from functools import partial
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QGroupBox, QComboBox, QApplication, QDoubleSpinBox
|
||||
)
|
||||
from qtpy.QtGui import QFont
|
||||
|
||||
class Group(QGroupBox):
|
||||
def __init__(self, label, widgets):
|
||||
super().__init__(label)
|
||||
self.layout = QVBoxLayout(self)
|
||||
for widget in widgets:
|
||||
self.layout.addWidget(widget)
|
||||
|
||||
class Indicator(QWidget):
|
||||
def __init__(self, label, unit=None, highlight=False):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.value = QLabel('-')
|
||||
self.value.setFixedWidth(160)
|
||||
layout.addWidget(self.value)
|
||||
self.unit = unit
|
||||
self.highlight = highlight
|
||||
if highlight:
|
||||
font = QFont()
|
||||
font.setBold(True)
|
||||
font.setPointSize(14)
|
||||
self.label.setFont(font)
|
||||
self.value.setFont(font)
|
||||
|
||||
def set_text(self, text):
|
||||
if self.unit is not None:
|
||||
text = text + ' ' + self.unit
|
||||
self.value.setText(text)
|
||||
|
||||
class InputTextField(QWidget):
|
||||
def __init__(self, topic, label):
|
||||
super().__init__()
|
||||
self.topic = topic
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.value = QLineEdit()
|
||||
self.value.setPlaceholderText('0')
|
||||
self.value.setFixedWidth(160)
|
||||
layout.addWidget(self.value)
|
||||
|
||||
def set_text(self, text):
|
||||
self.value.setText(text)
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return self.value.hasFocus()
|
||||
|
||||
def set_on_return(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.returnPressed.connect(
|
||||
partial(func, self.value, self.topic, lambda: self.value.text())
|
||||
)
|
||||
|
||||
class InputNumberField(QWidget):
|
||||
def __init__(self, label, init=0, decimals=1, single_step=0.1, ll=-1e6, hl=1e6):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.value = QDoubleSpinBox()
|
||||
self.value.setValue(init)
|
||||
self.value.setRange(ll, hl)
|
||||
self.value.setDecimals(decimals)
|
||||
self.value.setSingleStep(single_step)
|
||||
self.value.setFixedWidth(160)
|
||||
layout.addWidget(self.value)
|
||||
|
||||
def set_number(self, number):
|
||||
self.value.setValue(number)
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return self.value.hasFocus()
|
||||
|
||||
def set_on_return(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.editingFinished.connect(
|
||||
partial(func, self.value, lambda: self.value.text())
|
||||
)
|
||||
|
||||
class IPAdressInputField(QWidget):
|
||||
def __init__(self, topic, label):
|
||||
super().__init__()
|
||||
self.topic = topic
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.oct0 = QLineEdit()
|
||||
self.oct0.setPlaceholderText('0')
|
||||
self.oct0.setFixedWidth(30)
|
||||
layout.addWidget(self.oct0)
|
||||
separator1 = QLabel('.')
|
||||
layout.addWidget(separator1)
|
||||
self.oct1 = QLineEdit()
|
||||
self.oct1.setPlaceholderText('0')
|
||||
self.oct1.setFixedWidth(30)
|
||||
layout.addWidget(self.oct1)
|
||||
separator2 = QLabel('.')
|
||||
layout.addWidget(separator2)
|
||||
self.oct2 = QLineEdit()
|
||||
self.oct2.setPlaceholderText('0')
|
||||
self.oct2.setFixedWidth(30)
|
||||
layout.addWidget(self.oct2)
|
||||
separator3 = QLabel('.')
|
||||
layout.addWidget(separator3)
|
||||
self.oct3 = QLineEdit()
|
||||
self.oct3.setPlaceholderText('0')
|
||||
self.oct3.setFixedWidth(30)
|
||||
layout.addWidget(self.oct3)
|
||||
|
||||
self.oct0.editingFinished.connect(partial(self.check_octet, self.oct0))
|
||||
self.oct1.editingFinished.connect(partial(self.check_octet, self.oct1))
|
||||
self.oct2.editingFinished.connect(partial(self.check_octet, self.oct2))
|
||||
self.oct3.editingFinished.connect(partial(self.check_octet, self.oct3))
|
||||
|
||||
def check_octet(self, octet):
|
||||
if octet.text().isnumeric():
|
||||
if int(octet.text()) < 0:
|
||||
octet.setText('0')
|
||||
if int(octet.text()) > 254:
|
||||
octet.setText('254')
|
||||
else:
|
||||
octet.setText('')
|
||||
|
||||
def get_ip(self):
|
||||
return f'{self.oct0.text()}.{self.oct1.text()}.{self.oct2.text()}.{self.oct3.text()}'
|
||||
|
||||
def set_ip(self, ip):
|
||||
octets = ip.split('.')
|
||||
if len(octets) == 4 and all(octet.isnumeric() for octet in octets):
|
||||
if all(int(octet) > 0 and int(octet) < 254 for octet in octets):
|
||||
self.oct0.setText(octets[0])
|
||||
self.oct1.setText(octets[1])
|
||||
self.oct2.setText(octets[2])
|
||||
self.oct3.setText(octets[3])
|
||||
|
||||
class ComboBox(QWidget):
|
||||
def __init__(self, enums, label):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.value = QComboBox()
|
||||
self.value.setFixedWidth(160)
|
||||
for entry in enums:
|
||||
self.value.addItem(entry)
|
||||
layout.addWidget(self.value)
|
||||
|
||||
def set_current_text(self, text):
|
||||
self.value.setCurrentText(text)
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return QApplication.focusWidget() is self.value.view()
|
||||
|
||||
def set_on_change(self, func, reset_plot=False):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.activated.connect(
|
||||
partial(func, self.value, lambda: self.value.currentText(), reset_plot)
|
||||
)
|
||||
|
||||
class LED(QWidget):
|
||||
def __init__(self, states, colors, label):
|
||||
super().__init__()
|
||||
self.states = states
|
||||
self.colors = colors
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.led = QLabel()
|
||||
self.led.setFixedWidth(160)
|
||||
layout.addWidget(self.led)
|
||||
|
||||
def apply_color(self, val):
|
||||
color = self.colors[self.states.index(val)]
|
||||
self.led.setStyleSheet(f"background-color: {color}; border: 1px solid black;")
|
||||
|
||||
class StartStop(QWidget):
|
||||
def __init__(self, label, label_buttons=['Start', 'Stop']):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.start = QPushButton(label_buttons[0])
|
||||
self.start.setStyleSheet("color: black; background-color: green;")
|
||||
self.start.setFixedWidth(80)
|
||||
self.stop = QPushButton(label_buttons[1])
|
||||
self.stop.setStyleSheet("color: black; background-color: firebrick;")
|
||||
self.stop.setFixedWidth(80)
|
||||
layout.addWidget(self.start)
|
||||
layout.addWidget(self.stop)
|
||||
|
||||
def set_on_start(self, func):
|
||||
"""Connect a function to the start button press."""
|
||||
self.start.clicked.connect(func)
|
||||
|
||||
def set_on_stop(self, func):
|
||||
"""Connect a function to the stop button press."""
|
||||
self.stop.clicked.connect(func)
|
||||
|
||||
def enable_start(self):
|
||||
self.start.setEnabled(True)
|
||||
self.start.setStyleSheet("color: black; background-color: green;")
|
||||
|
||||
def enable_stop(self):
|
||||
self.stop.setEnabled(True)
|
||||
self.stop.setStyleSheet("color: black; background-color: firebrick;")
|
||||
|
||||
def disable_start(self):
|
||||
self.start.setEnabled(False)
|
||||
self.start.setStyleSheet("color: black; background-color: grey;")
|
||||
|
||||
def disable_stop(self):
|
||||
self.stop.setEnabled(False)
|
||||
self.stop.setStyleSheet("color: black; background-color: grey;")
|
||||
|
||||
class Button(QWidget):
|
||||
def __init__(self, label, label_button):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(150)
|
||||
layout.addWidget(self.label)
|
||||
self.button = QPushButton(label_button)
|
||||
self.button.setStyleSheet("color: black; background-color: dodgerblue;")
|
||||
self.button.setFixedWidth(160)
|
||||
layout.addWidget(self.button)
|
||||
|
||||
def set_on_press(self, func):
|
||||
"""Connect a function to the button press."""
|
||||
self.button.clicked.connect(func)
|
||||
|
||||
def enable_button(self):
|
||||
self.button.setEnabled(True)
|
||||
self.button.setStyleSheet("color: black; background-color: dodgerblue;")
|
||||
|
||||
def disable_button(self):
|
||||
self.button.setEnabled(False)
|
||||
self.button.setStyleSheet("color: black; background-color: grey;")
|
||||
|
||||
def set_button_text(self, text):
|
||||
self.button.setText(text)
|
||||
Reference in New Issue
Block a user