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 24258c6..1ae45a1 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -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 diff --git a/debye_bec/bec_widgets/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/qt_widgets.py index 8a9d1df..d9f3d6b 100644 --- a/debye_bec/bec_widgets/widgets/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/qt_widgets.py @@ -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)