From acc5e320cf40eeaa2445ac52b9bf476949515f6c Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 6 May 2026 12:58:45 +0200 Subject: [PATCH] wip: digital twin --- .../widgets/digital_twin/calc_surfaces.py | 5 + .../widgets/digital_twin/digital_twin.py | 185 +++++++++++++----- 2 files changed, 139 insertions(+), 51 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py index f5d408a..bdaf703 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calc_surfaces.py @@ -24,6 +24,9 @@ def calc_surfaces(cfg): index = bl.cm.surface.index(cfg['cm_stripe']) cen = (bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 + if cfg['cm_trx'] is not None: + cen = cfg['cm_trx'] + out['cm']['x'] = [cen-w1/2, cen-w2/2, cen+w2/2, cen+w1/2] out['cm']['y'] = [-l/2, l/2, l/2, -l/2] @@ -70,6 +73,8 @@ def calc_surfaces(cfg): index = surface.index(stripe) off = (bl.fm.limOptXFlat[0][index] + bl.fm.limOptXFlat[1][index]) / 2 r = bl.fm.r[index] + if cfg['fm_trx'] is not None: + off = cfg['fm_trx'] widthBeam = 2 * bl.fm.center[1] * np.tan(cfg['h_acc']) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py index b3a9846..3146a34 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -148,9 +148,8 @@ class DigitalTwin(BECWidget, QWidget): def calc_reality(self): config = self.get_reality_config() beam = calc_sideview(config) - self.sideview_plot.data['reality']['x'] = beam['Z'] - self.sideview_plot.data['reality']['y'] = beam['Y'] - self.sideview_plot.update_curves() + data = {'x': beam['Z'], 'y': beam['Y']} + self.sideview_plot.update_curves('reality', data) surfaces = calc_surfaces(config) self.surface_plots.update_surfaces(scene='reality', data=surfaces) @@ -233,11 +232,13 @@ class DigitalTwin(BECWidget, QWidget): 'v_acc' : self.input.sldi_vacc.value() * 1e-3, 'cm_pitch' : -self.input.cm_pitch.value() * 1e-3, 'cm_stripe' : self.input.cm_stripe.currentText(), + 'cm_trx' : None, 'mo1_mode' : self.input.mo1_mode.currentText(), 'mo1_xtal' : self.input.mo1_xtal.currentText(), 'mo1_bragg' : self.bragg_angle, 'fm_pitch' : -self.input.fm_pitch.value() * 1e-3, 'fm_stripe' : self.input.fm_stripe.currentText(), + 'fm_trx' : None, 'fm_gain_height' : 1, 'smpl' : self.input.smpl.value(), } @@ -277,11 +278,13 @@ class DigitalTwin(BECWidget, QWidget): 'v_acc' : v_acc, 'cm_pitch' : cm_pitch, 'cm_stripe' : cm_stripe, + 'cm_trx' : cm_trx, 'mo1_mode' : mo1_mode, 'mo1_xtal' : mo1_bragg['mo1_bragg_crystal_current_xtal_string']['value'], 'mo1_bragg' : mo1_bragg['mo1_bragg_angle']['value']/180*np.pi, 'fm_pitch' : fm_pitch_real, 'fm_stripe' : fm_stripe, + 'fm_trx' : fm_trx, 'fm_gain_height' : 1, 'smpl' : self.dev.ot_es1_trz.read()['ot_es1_trz']['value'], } @@ -291,9 +294,8 @@ class DigitalTwin(BECWidget, QWidget): @SafeSlot() def calc_assistant_sideview(self): beam = calc_sideview(self.get_assistant_config()) - self.sideview_plot.data['assistant']['x'] = beam['Z'] - self.sideview_plot.data['assistant']['y'] = beam['Y'] - self.sideview_plot.update_curves() + data = {'x': beam['Z'], 'y': beam['Y']} + self.sideview_plot.update_curves('assistant', data) @SafeSlot() def calc_assistant_surfaces(self): @@ -453,8 +455,8 @@ class InputPanel(QWidget): # Focusing Mirror self.fm_stripe = ComboBox('fm_stripe', 'Stripe', ['Rh (toroid)', 'Rh (flat)', 'Pt (toroid)', 'Pt (flat)']) - self.fm_pitch_ideal = NumberIndicator('Ideal Pitch', 'mrad', decimals=3) - self.fm_pitch = InputNumberField('fm_pitch', 'Pitch [mrad]', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2) + self.fm_pitch_ideal = NumberIndicator('Ideal Incidence Angle', 'mrad', decimals=3) + self.fm_pitch = InputNumberField('fm_pitch', 'Incidence Angle [mrad]', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2) 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( @@ -656,11 +658,11 @@ class MoverPanel(QWidget): self._layout .addWidget(self.mover_group) self._layout .addStretch() -class SurfacePlots(QWidget): +class SurfacePlots(BECWidget, QWidget): """Plot widget with two curves and legend.""" - def __init__(self, parent=None): - super().__init__(parent) + def __init__(self, parent=None, *arg, **kwargs): + super().__init__(parent=parent, theme_update=True, *arg, **kwargs) self._layout = QHBoxLayout(self) self.surfaces = { @@ -685,16 +687,9 @@ class SurfacePlots(QWidget): 'fm': {}, } - app = QApplication.instance() - theme = app.theme.theme # type: ignore - 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) + self.color_impenetrable = (0, 0, 0) + self.colors = [(255, 255, 0), (255, 0, 255)] + self.text_color = (255, 255, 255) # Create plot widgets for name, widget in self.plots.items(): @@ -739,8 +734,57 @@ class SurfacePlots(QWidget): ) self.plots[name][scene].setZValue(z_value) + self.walls = [] + self.texts = [] + self.plot_walls() + self.apply_theme() + + def apply_theme(self, theme=None): + + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + for _, plot in self.plots.items(): + # Background + plot['widget'].setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = plot['widget'].getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + 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) + + for idx, scene in enumerate(self.surfaces): + for name, device in self.surfaces[scene].items(): + 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) + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1) + self.plots[name][scene].setPen(pen) + self.plots[name][scene].setBrush(brush) + + for wall in self.walls: + wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + wall.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 + + for text in self.texts: + text.setColor(self.text_color) + def plot_walls(self): def plot_mirror_stripe(widget, surface, limOptX, limOptY): @@ -758,6 +802,8 @@ class SurfacePlots(QWidget): widget.addItem(text) text.setPos((hx+lx)/2, (hy+ly)/2) text.setZValue(10) + self.walls.append(rect) + self.texts.append(text) def plot_mono_surface(widget, xtal, xtalWidth, xtalOffsetX, xtalLength): for sf, w, offx, len in zip(xtal, xtalWidth, xtalOffsetX, xtalLength): @@ -774,6 +820,8 @@ class SurfacePlots(QWidget): widget.addItem(text) text.setPos(offx, 0) text.setZValue(10) + self.walls.append(rect) + self.texts.append(text) for name, plot in self.plots.items(): if name in 'cm': @@ -798,11 +846,11 @@ class SurfacePlots(QWidget): y = np.array(device['y'] + [device['y'][0]]) if len(device['y']) != 0 else np.array([]) plot.setData(x=x, y=y) -class SideviewPlot(QWidget): +class SideviewPlot(BECWidget, QWidget): """Plot widget with two curves and legend.""" - def __init__(self, parent=None): - super().__init__(parent) + def __init__(self, parent=None, *arg, **kwargs): + super().__init__(parent=parent, theme_update=True, *arg, **kwargs) self._layout = QVBoxLayout(self) # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore @@ -810,36 +858,29 @@ class SideviewPlot(QWidget): self.plot_widget.getAxis('bottom').enableAutoSIPrefix(False) self.plot_widget.addLegend() - app = QApplication.instance() - theme = app.theme.theme # type: ignore - if theme == "light": - self.color_impenetrable = (30, 30, 30) - self.colors = [(26, 111, 173), (212, 83, 10)] - else: # dark theme - self.color_impenetrable = (220, 220, 220) - self.colors = [(79, 163, 224), (240, 128, 60)] - - self.curves = [] + self.color_impenetrable = (0, 0, 0) + self.colors = [(255, 255, 0), (255, 0, 255)] self.data = { 'assistant': {'x': [0, 1000, 2000], 'y': [0, 20, 30]}, 'reality': {'x': [0, 1000, 2000], 'y': [0, 15, 50]}, } + + self.plots = {} + self.pipes = [] self.walls = [] - for idx, name in enumerate(self.data.keys()): - if name in "assistant": + for idx, scene in enumerate(self.data.keys()): + if scene in "assistant": pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.DashLine) else: pen = pg.mkPen(color=self.colors[idx], width=2) - self.curves.append( - self.plot_widget.plot( - [], - [], - pen=pen, - name=name, - ) + self.plots[scene] = self.plot_widget.plot( + [], + [], + pen=pen, + name=scene, ) self.plot_group = Group( @@ -862,7 +903,50 @@ class SideviewPlot(QWidget): self.plot_vacuum_pipes() self.plot_walls() - self.update_curves() + + self.apply_theme() + + def apply_theme(self, theme=None): + + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + # Background + self.plot_widget.setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = self.plot_widget.getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + 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) + + for idx, scene in enumerate(self.data): + 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) + else: + brush = QBrush(QColor(*self.colors[idx], 255)) + pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1) + self.plots[scene].setPen(pen) + self.plots[scene].setBrush(brush) + + for wall in self.walls: + wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) + wall.setBrush(pg.QtGui.QBrush(pg.QtGui.QColor(*self.color_impenetrable))) # pylint: disable=E1101 + + for pipe in self.pipes: + pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=2)) def plot_vacuum_pipes(self): for i, _ in enumerate(bl.vacuum_pipes.center): @@ -890,13 +974,12 @@ class SideviewPlot(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)) self.plot_widget.addItem(rect) + self.walls.append(rect) - def update_curves(self): - for idx, element in enumerate(self.data): - self.curves[idx].setData( - x=np.array(self.data[element]['x']), - y=np.array(self.data[element]['y']), - ) + def update_curves(self, scene, data): + self.data[scene] = data + plot = self.plots[scene] + plot.setData(x=self.data[scene]['x'], y=self.data[scene]['y']) if __name__ == "__main__": @@ -911,4 +994,4 @@ if __name__ == "__main__": # win.resize(1000, 800) win.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_())