From 341da3ceb13a35a42f9a0a3c603a473f1100bb8b Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 26 Jan 2026 14:47:19 +0100 Subject: [PATCH 01/12] plot control in Experiment and Analysis --- .../Backends/Mock/Plotting.qml | 42 ++++ .../Backends/Py/plotting_1d.py | 189 ++++++++++++++++-- .../Gui/Globals/BackendWrapper.qml | 25 +++ .../Analysis/MainContent/AnalysisView.qml | 18 ++ .../Analysis/MainContent/CombinedView.qml | 18 ++ .../Sidebar/Advanced/Groups/PlotControl.qml | 50 +++++ .../Analysis/Sidebar/Advanced/Layout.qml | 2 + .../Gui/Pages/Experiment/Layout.qml | 8 +- .../Experiment/MainContent/ExperimentView.qml | 18 ++ .../Sidebar/Advanced/Groups/PlotControl.qml | 50 +++++ .../Experiment/Sidebar/Advanced/Layout.qml | 17 ++ .../Pages/Sample/MainContent/CombinedView.qml | 12 ++ .../Pages/Sample/MainContent/SampleView.qml | 12 ++ .../Sidebar/Advanced/Groups/PlotControl.qml | 10 + 14 files changed, 450 insertions(+), 21 deletions(-) create mode 100644 EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml create mode 100644 EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml create mode 100644 EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml diff --git a/EasyReflectometryApp/Backends/Mock/Plotting.qml b/EasyReflectometryApp/Backends/Mock/Plotting.qml index 537eb221..ea36965f 100644 --- a/EasyReflectometryApp/Backends/Mock/Plotting.qml +++ b/EasyReflectometryApp/Backends/Mock/Plotting.qml @@ -23,6 +23,20 @@ QtObject { property int modelCount: 1 + // Plot mode properties + property bool plotRQ4: false + property string yMainAxisTitle: 'R(q)' + property bool xAxisLog: false + property string xAxisType: 'linear' + property bool sldXDataReversed: false + property bool scaleShown: false + property bool bkgShown: false + + // Signals for plot mode changes + signal plotModeChanged() + signal axisTypeChanged() + signal sldAxisReversedChanged() + signal referenceLineVisibilityChanged() signal samplePageDataChanged() function setQtChartsSerieRef(value1, value2, value3) { @@ -52,4 +66,32 @@ QtObject { return '#0000FF' } + // Plot mode toggle functions + function togglePlotRQ4() { + plotRQ4 = !plotRQ4 + yMainAxisTitle = plotRQ4 ? 'R(q)×q⁴' : 'R(q)' + plotModeChanged() + } + + function toggleXAxisType() { + xAxisLog = !xAxisLog + xAxisType = xAxisLog ? 'log' : 'linear' + axisTypeChanged() + } + + function reverseSldXData() { + sldXDataReversed = !sldXDataReversed + sldAxisReversedChanged() + } + + function flipScaleShown() { + scaleShown = !scaleShown + referenceLineVisibilityChanged() + } + + function flipBkgShown() { + bkgShown = !bkgShown + referenceLineVisibilityChanged() + } + } diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index e06fdf6a..72ef0204 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -20,6 +20,12 @@ class Plotting1d(QObject): experimentDataChanged = Signal() samplePageDataChanged = Signal() # Signal for QML to refresh sample page charts + # New signals for plot mode properties + plotModeChanged = Signal() + axisTypeChanged = Signal() + sldAxisReversedChanged = Signal() + referenceLineVisibilityChanged = Signal() + def __init__(self, project_lib: ProjectLib, parent=None): super().__init__(parent) self._project_lib = project_lib @@ -27,6 +33,13 @@ def __init__(self, project_lib: ProjectLib, parent=None): self._currentLib1d = 'QtCharts' self._sample_data = {} self._sld_data = {} + + # Plot mode state + self._plot_rq4 = False + self._x_axis_log = False + self._sld_x_reversed = False + self._scale_shown = False + self._bkg_shown = False self._chartRefs = { 'QtCharts': { 'samplePage': { @@ -51,6 +64,80 @@ def reset_data(self): self._sld_data = {} console.debug(IO.formatMsg('sub', 'Sample and SLD data cleared')) + # R(q)×q⁴ mode + @Property(bool, notify=plotModeChanged) + def plotRQ4(self) -> bool: + """Return whether R(q)×q⁴ mode is enabled.""" + return self._plot_rq4 + + @Slot() + def togglePlotRQ4(self) -> None: + """Toggle R(q)×q⁴ plotting mode.""" + self._plot_rq4 = not self._plot_rq4 + self.plotModeChanged.emit() + # Refresh all charts with new mode + self.sampleChartRangesChanged.emit() + self.experimentChartRangesChanged.emit() + self.samplePageDataChanged.emit() + + @Property(str, notify=plotModeChanged) + def yMainAxisTitle(self) -> str: + """Return Y-axis title based on current plot mode.""" + return 'R(q)×q⁴' if self._plot_rq4 else 'R(q)' + + # X-axis type (log/linear) + @Property(bool, notify=axisTypeChanged) + def xAxisLog(self) -> bool: + """Return whether X-axis is logarithmic.""" + return self._x_axis_log + + @Slot() + def toggleXAxisType(self) -> None: + """Toggle between linear and logarithmic X-axis.""" + self._x_axis_log = not self._x_axis_log + self.axisTypeChanged.emit() + + @Property(str, notify=axisTypeChanged) + def xAxisType(self) -> str: + """Return X-axis type as string for QML.""" + return 'log' if self._x_axis_log else 'linear' + + # SLD X-axis reversal + @Property(bool, notify=sldAxisReversedChanged) + def sldXDataReversed(self) -> bool: + """Return whether SLD X-axis is reversed.""" + return self._sld_x_reversed + + @Slot() + def reverseSldXData(self) -> None: + """Toggle SLD X-axis reversal.""" + self._sld_x_reversed = not self._sld_x_reversed + self.sldAxisReversedChanged.emit() + self.sldChartRangesChanged.emit() + + # Reference line visibility + @Property(bool, notify=referenceLineVisibilityChanged) + def scaleShown(self) -> bool: + """Return whether scale reference line is shown.""" + return self._scale_shown + + @Slot() + def flipScaleShown(self) -> None: + """Toggle scale line visibility.""" + self._scale_shown = not self._scale_shown + self.referenceLineVisibilityChanged.emit() + + @Property(bool, notify=referenceLineVisibilityChanged) + def bkgShown(self) -> bool: + """Return whether background reference line is shown.""" + return self._bkg_shown + + @Slot() + def flipBkgShown(self) -> None: + """Toggle background line visibility.""" + self._bkg_shown = not self._bkg_shown + self.referenceLineVisibilityChanged.emit() + @property def sample_data(self) -> DataSet1D: idx = self._project_lib.current_model_index @@ -154,8 +241,13 @@ def _get_all_models_sample_range(self): min_x = min(min_x, data.x.min()) max_x = max(max_x, data.x.max()) if data.y.size > 0: - valid_y = data.y[data.y > 0] + valid_mask = data.y > 0 + valid_y = data.y[valid_mask] + valid_x = data.x[valid_mask] if valid_y.size > 0: + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + valid_y = valid_y * (valid_x**4) min_y = min(min_y, np.log10(valid_y.min())) max_y = max(max_y, np.log10(valid_y.max())) except (IndexError, ValueError): @@ -233,13 +325,25 @@ def experimentMinX(self): @Property(float, notify=experimentChartRangesChanged) def experimentMaxY(self): data = self.experiment_data - return np.log10(data.y.max()) if data.y.size > 0 else 1.0 + if data.y.size == 0: + return 1.0 + y_values = data.y + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + y_values = y_values * (data.x**4) + return np.log10(y_values.max()) @Property(float, notify=experimentChartRangesChanged) def experimentMinY(self): data = self.experiment_data valid_y = data.y[data.y > 0] if data.y.size > 0 else np.array([1e-10]) - return np.log10(valid_y.min()) if valid_y.size > 0 else -10.0 + if valid_y.size == 0: + return -10.0 + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + valid_x = data.x[data.y > 0] if data.y.size > 0 else np.array([1.0]) + valid_y = valid_y * (valid_x**4) + return np.log10(valid_y.min()) @Property('QVariant', notify=chartRefsChanged) def chartRefs(self): @@ -284,7 +388,13 @@ def getSampleDataPointsForModel(self, model_index: int) -> list: data = self._project_lib.sample_data_for_model_at_index(model_index) points = [] for point in data.data_points(): - points.append({'x': float(point[0]), 'y': float(np.log10(point[1])) if point[1] > 0 else -10.0}) + x_val = float(point[0]) + y_val = float(point[1]) + # Apply R×q⁴ transformation if enabled + if self._plot_rq4 and y_val > 0: + y_val = y_val * (x_val**4) + y_log = float(np.log10(y_val)) if y_val > 0 else -10.0 + points.append({'x': x_val, 'y': y_log}) return points except Exception as e: console.debug(f'Error getting sample data points for model {model_index}: {e}') @@ -324,12 +434,25 @@ def getExperimentDataPoints(self, experiment_index: int) -> list: points = [] for point in data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: + q = point[0] + r = point[1] + error_var = point[2] + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + q4 = q**4 + r_val = r * q4 + error_upper = (r + np.sqrt(error_var)) * q4 + error_lower = max((r - np.sqrt(error_var)) * q4, 1e-10) + else: + r_val = r + error_upper = r + np.sqrt(error_var) + error_lower = max(r - np.sqrt(error_var), 1e-10) points.append( { - 'x': float(point[0]), - 'y': float(np.log10(point[1])), - 'errorUpper': float(np.log10(point[1] + np.sqrt(point[2]))), - 'errorLower': float(np.log10(max(point[1] - np.sqrt(point[2]), 1e-10))), # Avoid log(0) + 'x': float(q), + 'y': float(np.log10(r_val)), + 'errorUpper': float(np.log10(error_upper)), + 'errorLower': float(np.log10(error_lower)), } ) return points @@ -373,11 +496,18 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: calc_idx = 0 for point in exp_points: if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: - calc_y_val = calc_y[calc_idx] if calc_idx < len(calc_y) else point[1] + q = point[0] + r_meas = point[1] + calc_y_val = calc_y[calc_idx] if calc_idx < len(calc_y) else r_meas + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + q4 = q**4 + r_meas = r_meas * q4 + calc_y_val = calc_y_val * q4 points.append( { - 'x': float(point[0]), - 'measured': float(np.log10(point[1])), + 'x': float(q), + 'measured': float(np.log10(r_meas)), 'calculated': float(np.log10(calc_y_val)), } ) @@ -446,6 +576,7 @@ def qtchartsReplaceCalculatedOnSldChartAndRedraw(self): nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Sld curve', f'{nr_points} points', 'on analysis page', 'replaced')) + @Slot() def drawMeasuredOnExperimentChart(self): if PLOT_BACKEND == 'QtCharts': if self.is_multi_experiment_mode: @@ -463,9 +594,22 @@ def qtchartsReplaceMeasuredOnExperimentChartAndRedraw(self): nr_points = 0 for point in self.experiment_data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: - series_measured.append(point[0], np.log10(point[1])) - series_error_upper.append(point[0], np.log10(point[1] + np.sqrt(point[2]))) - series_error_lower.append(point[0], np.log10(max(point[1] - np.sqrt(point[2]), 1e-10))) + q = point[0] + r = point[1] + error_var = point[2] + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + q4 = q**4 + r_val = r * q4 + error_upper = (r + np.sqrt(error_var)) * q4 + error_lower = max((r - np.sqrt(error_var)) * q4, 1e-10) + else: + r_val = r + error_upper = r + np.sqrt(error_var) + error_lower = max(r - np.sqrt(error_var), 1e-10) + series_measured.append(q, np.log10(r_val)) + series_error_upper.append(q, np.log10(error_upper)) + series_error_lower.append(q, np.log10(error_lower)) nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Measured curve', f'{nr_points} points', 'on experiment page', 'replaced')) @@ -486,6 +630,7 @@ def qtchartsReplaceMultiExperimentChartAndRedraw(self): # This method is called to trigger the refresh, actual drawing is handled by QML self.experimentDataChanged.emit() + @Slot() def drawCalculatedAndMeasuredOnAnalysisChart(self): if PLOT_BACKEND == 'QtCharts': if self.is_multi_experiment_mode: @@ -515,11 +660,21 @@ def qtchartsReplaceCalculatedAndMeasuredOnAnalysisChartAndRedraw(self): nr_points = 0 for point in self.experiment_data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: - series_measured.append(point[0], np.log10(point[1])) + q = point[0] + r_meas = point[1] + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + r_meas = r_meas * (q**4) + series_measured.append(q, np.log10(r_meas)) nr_points = nr_points + 1 - console.debug(IO.formatMsg('sub', 'Measurede curve', f'{nr_points} points', 'on analysis page', 'replaced')) + console.debug(IO.formatMsg('sub', 'Measured curve', f'{nr_points} points', 'on analysis page', 'replaced')) for point in self.sample_data.data_points(): - series_calculated.append(point[0], np.log10(point[1])) + q = point[0] + r_calc = point[1] + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + r_calc = r_calc * (q**4) + series_calculated.append(q, np.log10(r_calc)) nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Calculated curve', f'{nr_points} points', 'on analysis page', 'replaced')) diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index 15e23f18..ea468040 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -328,9 +328,29 @@ QtObject { readonly property var plottingAnalysisMaxY: activeBackend.plotting.sampleMaxY readonly property var calcSerieColor: activeBackend.plotting.calcSerieColor + // Plot mode properties + readonly property bool plottingPlotRQ4: activeBackend.plotting.plotRQ4 + readonly property string plottingYAxisTitle: activeBackend.plotting.yMainAxisTitle + readonly property bool plottingXAxisLog: activeBackend.plotting.xAxisLog + readonly property string plottingXAxisType: activeBackend.plotting.xAxisType + readonly property bool plottingSldXReversed: activeBackend.plotting.sldXDataReversed + + // Reference line visibility + readonly property bool plottingScaleShown: activeBackend.plotting.scaleShown + readonly property bool plottingBkgShown: activeBackend.plotting.bkgShown + + // Plot mode toggle functions + function plottingTogglePlotRQ4() { activeBackend.plotting.togglePlotRQ4() } + function plottingToggleXAxisType() { activeBackend.plotting.toggleXAxisType() } + function plottingReverseSldXData() { activeBackend.plotting.reverseSldXData() } + function plottingFlipScaleShown() { activeBackend.plotting.flipScaleShown() } + function plottingFlipBkgShown() { activeBackend.plotting.flipBkgShown() } + function plottingSetQtChartsSerieRef(value1, value2, value3) { activeBackend.plotting.setQtChartsSerieRef(value1, value2, value3) } function plottingRefreshSample() { activeBackend.plotting.drawCalculatedOnSampleChart() } function plottingRefreshSLD() { activeBackend.plotting.drawCalculatedOnSldChart() } + function plottingRefreshExperiment() { activeBackend.plotting.drawMeasuredOnExperimentChart() } + function plottingRefreshAnalysis() { activeBackend.plotting.drawCalculatedAndMeasuredOnAnalysisChart() } // Multi-model sample page plotting support readonly property int plottingModelCount: activeBackend.plotting.modelCount @@ -358,12 +378,17 @@ QtObject { // Signal for sample page data changes - forward from backend signal samplePageDataChanged() + // Signal for plot mode changes - forward from backend + signal plotModeChanged() // Connect to backend signal (called from Component.onCompleted in QML items) function connectSamplePageDataChanged() { if (activeBackend && activeBackend.plotting && activeBackend.plotting.samplePageDataChanged) { activeBackend.plotting.samplePageDataChanged.connect(samplePageDataChanged) } + if (activeBackend && activeBackend.plotting && activeBackend.plotting.plotModeChanged) { + activeBackend.plotting.plotModeChanged.connect(plotModeChanged) + } } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index 0baf81cd..e7c53da5 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -55,6 +55,24 @@ Rectangle { chartView.updateMultiExperimentSeries() } } + + // Watch for plot mode changes (R(q)×q⁴ toggle) + Connections { + target: Globals.BackendWrapper + function onPlotModeChanged() { + console.debug("AnalysisView: Plot mode changed, refreshing chart") + Globals.BackendWrapper.plottingRefreshAnalysis() + // Delay resetAxes to allow axis range properties to update first + analysisResetAxesTimer.start() + } + } + + Timer { + id: analysisResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() + } property double xRange: Globals.BackendWrapper.plottingAnalysisMaxX - Globals.BackendWrapper.plottingAnalysisMinX axisX.title: "q (Å⁻¹)" diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 305fe01c..6bc9357b 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -69,6 +69,24 @@ Rectangle { } } + // Watch for plot mode changes (R(q)×q⁴ toggle) + Connections { + target: Globals.BackendWrapper + function onPlotModeChanged() { + console.debug("CombinedView Analysis: Plot mode changed, refreshing chart") + Globals.BackendWrapper.plottingRefreshAnalysis() + // Delay resetAxes to allow axis range properties to update first + combinedAnalysisResetAxesTimer.start() + } + } + + Timer { + id: combinedAnalysisResetAxesTimer + interval: 50 + repeat: false + onTriggered: analysisChartView.resetAxes() + } + // Multi-experiment series management function updateMultiExperimentSeries() { // Always get the latest value from backend diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml new file mode 100644 index 00000000..0b2e0ce2 --- /dev/null +++ b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick +import QtQuick.Controls + +import EasyApp.Gui.Style as EaStyle +import EasyApp.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupBox { + title: qsTr("Plot control") + collapsed: true + + Column { + spacing: EaStyle.Sizes.fontPixelSize * 0.5 + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingPlotRQ4 + text: qsTr("Plot R(q)×q⁴") + ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") + onToggled: { + Globals.BackendWrapper.plottingTogglePlotRQ4() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingScaleShown + text: qsTr("Show scale line") + ToolTip.text: qsTr("Checking this box will show the scale reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipScaleShown() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingBkgShown + text: qsTr("Show background line") + ToolTip.text: qsTr("Checking this box will show the background reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipBkgShown() + } + } + } +} diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml index 2571f4d9..deeee520 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml @@ -17,4 +17,6 @@ EaComponents.SideBarColumn { Groups.Calculator {} Groups.Minimizer {} + + Groups.PlotControl {} } diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml index f56ede8a..7c6f1c87 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Layout.qml @@ -22,13 +22,13 @@ EaComponents.ContentPage { sideBar: EaComponents.SideBar { tabs: [ - EaElements.TabButton { text: qsTr('Basic controls') } -// EaElements.TabButton { text: qsTr('Advanced controls') } + EaElements.TabButton { text: qsTr('Basic controls') }, + EaElements.TabButton { text: qsTr('Advanced controls') } ] items: [ - Loader { source: 'Sidebar/Basic/Layout.qml' } - // Loader { source: 'Sidebar/Advanced/Layout.qml' } + Loader { source: 'Sidebar/Basic/Layout.qml' }, + Loader { source: 'Sidebar/Advanced/Layout.qml' } ] continueButton.text: qsTr('Continue') diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index 905dc7c9..03c069c3 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -145,6 +145,24 @@ Rectangle { chartView.updateMultiExperimentSeries() } } + + // Watch for plot mode changes (R(q)×q⁴ toggle) + Connections { + target: Globals.BackendWrapper + function onPlotModeChanged() { + console.debug("ExperimentView: Plot mode changed, refreshing chart") + Globals.BackendWrapper.plottingRefreshExperiment() + // Delay resetAxes to allow axis range properties to update first + experimentResetAxesTimer.start() + } + } + + Timer { + id: experimentResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() + } property double xRange: Globals.BackendWrapper.plottingExperimentMaxX - Globals.BackendWrapper.plottingExperimentMinX axisX.title: "q (Å⁻¹)" diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml new file mode 100644 index 00000000..0b2e0ce2 --- /dev/null +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick +import QtQuick.Controls + +import EasyApp.Gui.Style as EaStyle +import EasyApp.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupBox { + title: qsTr("Plot control") + collapsed: true + + Column { + spacing: EaStyle.Sizes.fontPixelSize * 0.5 + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingPlotRQ4 + text: qsTr("Plot R(q)×q⁴") + ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") + onToggled: { + Globals.BackendWrapper.plottingTogglePlotRQ4() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingScaleShown + text: qsTr("Show scale line") + ToolTip.text: qsTr("Checking this box will show the scale reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipScaleShown() + } + } + + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingBkgShown + text: qsTr("Show background line") + ToolTip.text: qsTr("Checking this box will show the background reference line on the plot") + onToggled: { + Globals.BackendWrapper.plottingFlipBkgShown() + } + } + } +} diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml new file mode 100644 index 00000000..600ca675 --- /dev/null +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Layout.qml @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick + +import EasyApp.Gui.Elements as EaElements +import EasyApp.Gui.Components as EaComponents + +import "./Groups" as Groups + + +EaComponents.SideBarColumn { + + Groups.PlotControl {} + +} diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index fb2f6d4e..e05c715c 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -591,6 +591,18 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onPlotModeChanged() { + refreshAllCharts() + // Delay resetAxes to allow axis range properties to update first + sampleCombinedResetAxesTimer.start() + } + } + + Timer { + id: sampleCombinedResetAxesTimer + interval: 50 + repeat: false + onTriggered: sampleChartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml index 01645bfa..70bb4fff 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml @@ -347,6 +347,18 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onPlotModeChanged() { + refreshAllCharts() + // Delay resetAxes to allow axis range properties to update first + sampleResetAxesTimer.start() + } + } + + Timer { + id: sampleResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml index f7d77177..b448193c 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Advanced/Groups/PlotControl.qml @@ -17,6 +17,16 @@ EaElements.GroupBox { Column { spacing: EaStyle.Sizes.fontPixelSize * 0.5 + EaElements.CheckBox { + topPadding: 0 + checked: Globals.BackendWrapper.plottingPlotRQ4 + text: qsTr("Plot R(q)×q⁴") + ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") + onToggled: { + Globals.BackendWrapper.plottingTogglePlotRQ4() + } + } + EaElements.CheckBox { topPadding: 0 checked: Globals.Variables.reverseSldZAxis From 0cedef69856bb2db7a8ffabfb066f29f0c1085e1 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 26 Jan 2026 17:04:05 +0100 Subject: [PATCH 02/12] added scale/bg lines to exp and analysis plots --- .../Backends/Mock/Plotting.qml | 19 +++++ .../Backends/Py/plotting_1d.py | 69 +++++++++++++++++++ .../Gui/Globals/BackendWrapper.qml | 16 +++++ .../Analysis/MainContent/AnalysisView.qml | 58 ++++++++++++++++ .../Analysis/MainContent/CombinedView.qml | 55 +++++++++++++++ .../Experiment/MainContent/ExperimentView.qml | 58 ++++++++++++++++ 6 files changed, 275 insertions(+) diff --git a/EasyReflectometryApp/Backends/Mock/Plotting.qml b/EasyReflectometryApp/Backends/Mock/Plotting.qml index ea36965f..d973b572 100644 --- a/EasyReflectometryApp/Backends/Mock/Plotting.qml +++ b/EasyReflectometryApp/Backends/Mock/Plotting.qml @@ -94,4 +94,23 @@ QtObject { referenceLineVisibilityChanged() } + // Reference line data accessors (mock implementation) + function getBackgroundData() { + if (!bkgShown) return [] + // Return mock horizontal line at background level + return [ + { 'x': 0.01, 'y': -7.0 }, + { 'x': 0.30, 'y': -7.0 } + ] + } + + function getScaleData() { + if (!scaleShown) return [] + // Return mock horizontal line at scale level (log10(1.0) = 0) + return [ + { 'x': 0.01, 'y': 0.0 }, + { 'x': 0.30, 'y': 0.0 } + ] + } + } diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index 72ef0204..d7b60588 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -138,6 +138,75 @@ def flipBkgShown(self) -> None: self._bkg_shown = not self._bkg_shown self.referenceLineVisibilityChanged.emit() + @Slot(result='QVariantList') + def getBackgroundData(self) -> list: + """Return background reference line data for plotting. + + Returns a horizontal line at the model's background value. + """ + if not self._bkg_shown: + return [] + try: + model = self._project_lib.models[self._project_lib.current_model_index] + exp_data = self._project_lib.experimental_data_for_model_at_index(self._project_lib.current_experiment_index) + if exp_data.x is None or len(exp_data.x) == 0: + return [] + x = exp_data.x + bkg_value = model.background.value + # For log scale plotting, convert background value + bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + # For background, we need to transform: bkg * q^4 + return [ + {'x': float(x[0]), 'y': float(np.log10(bkg_value * x[0] ** 4)) if bkg_value * x[0] ** 4 > 0 else -10.0}, + {'x': float(x[-1]), 'y': float(np.log10(bkg_value * x[-1] ** 4)) if bkg_value * x[-1] ** 4 > 0 else -10.0}, + ] + else: + return [{'x': float(x[0]), 'y': bkg_log}, {'x': float(x[-1]), 'y': bkg_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting background data: {e}') + return [] + + @Slot(result='QVariantList') + def getScaleData(self) -> list: + """Return scale reference line data for plotting. + + Returns a horizontal line at the model's scale value. + Note: Scale is a multiplicative factor, typically close to 1.0. + For reflectometry plots, the scale line at y=scale (log10) shows + where R=scale, i.e., where the reflectivity equals the scale factor. + """ + if not self._scale_shown: + return [] + try: + model = self._project_lib.models[self._project_lib.current_model_index] + exp_data = self._project_lib.experimental_data_for_model_at_index(self._project_lib.current_experiment_index) + if exp_data.x is None or len(exp_data.x) == 0: + return [] + x = exp_data.x + scale_value = model.scale.value + # For log scale plotting, convert scale value + scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 + # Apply R×q⁴ transformation if enabled + if self._plot_rq4: + # For scale, we need to transform: scale * q^4 + return [ + { + 'x': float(x[0]), + 'y': float(np.log10(scale_value * x[0] ** 4)) if scale_value * x[0] ** 4 > 0 else -10.0, + }, + { + 'x': float(x[-1]), + 'y': float(np.log10(scale_value * x[-1] ** 4)) if scale_value * x[-1] ** 4 > 0 else -10.0, + }, + ] + else: + return [{'x': float(x[0]), 'y': scale_log}, {'x': float(x[-1]), 'y': scale_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting scale data: {e}') + return [] + @property def sample_data(self) -> DataSet1D: idx = self._project_lib.current_model_index diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index ea468040..189e6573 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -346,6 +346,22 @@ QtObject { function plottingFlipScaleShown() { activeBackend.plotting.flipScaleShown() } function plottingFlipBkgShown() { activeBackend.plotting.flipBkgShown() } + // Reference line data accessors + function plottingGetBackgroundData() { + try { + return activeBackend.plotting.getBackgroundData() + } catch (e) { + return [] + } + } + function plottingGetScaleData() { + try { + return activeBackend.plotting.getScaleData() + } catch (e) { + return [] + } + } + function plottingSetQtChartsSerieRef(value1, value2, value3) { activeBackend.plotting.setQtChartsSerieRef(value1, value2, value3) } function plottingRefreshSample() { activeBackend.plotting.drawCalculatedOnSampleChart() } function plottingRefreshSLD() { activeBackend.plotting.drawCalculatedOnSldChart() } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index e7c53da5..cb11a5ff 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -31,6 +31,58 @@ Rectangle { useOpenGL: EaGlobals.Vars.useOpenGL + // Background reference line series + LineSeries { + id: backgroundRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#888888" + width: 1 + style: Qt.DashLine + visible: Globals.BackendWrapper.plottingBkgShown + } + + // Scale reference line series + LineSeries { + id: scaleRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#666666" + width: 1 + style: Qt.DotLine + visible: Globals.BackendWrapper.plottingScaleShown + } + + // Update reference lines when visibility changes + Connections { + target: Globals.BackendWrapper.activeBackend.plotting + function onReferenceLineVisibilityChanged() { + chartView.updateReferenceLines() + } + } + + function updateReferenceLines() { + // Update background line + backgroundRefLine.clear() + if (Globals.BackendWrapper.plottingBkgShown) { + var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() + for (var i = 0; i < bkgData.length; i++) { + backgroundRefLine.append(bkgData[i].x, bkgData[i].y) + } + } + + // Update scale line + scaleRefLine.clear() + if (Globals.BackendWrapper.plottingScaleShown) { + var scaleData = Globals.BackendWrapper.plottingGetScaleData() + for (var j = 0; j < scaleData.length; j++) { + scaleRefLine.append(scaleData[j].x, scaleData[j].y) + } + } + } + // Multi-experiment support property var multiExperimentSeries: [] property bool isMultiExperimentMode: { @@ -382,6 +434,9 @@ Rectangle { // Initialize multi-experiment support updateMultiExperimentSeries() + + // Initialize reference lines + updateReferenceLines() } // Update series when chart becomes visible @@ -389,6 +444,9 @@ Rectangle { if (visible && isMultiExperimentMode) { updateMultiExperimentSeries() } + if (visible) { + updateReferenceLines() + } } } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 6bc9357b..3c93b205 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -87,6 +87,58 @@ Rectangle { onTriggered: analysisChartView.resetAxes() } + // Background reference line series + LineSeries { + id: backgroundRefLine + axisX: analysisChartView.axisX + axisY: analysisChartView.axisY + useOpenGL: analysisChartView.useOpenGL + color: "#888888" + width: 1 + style: Qt.DashLine + visible: Globals.BackendWrapper.plottingBkgShown + } + + // Scale reference line series + LineSeries { + id: scaleRefLine + axisX: analysisChartView.axisX + axisY: analysisChartView.axisY + useOpenGL: analysisChartView.useOpenGL + color: "#666666" + width: 1 + style: Qt.DotLine + visible: Globals.BackendWrapper.plottingScaleShown + } + + // Update reference lines when visibility changes + Connections { + target: Globals.BackendWrapper.activeBackend.plotting + function onReferenceLineVisibilityChanged() { + analysisChartView.updateReferenceLines() + } + } + + function updateReferenceLines() { + // Update background line + backgroundRefLine.clear() + if (Globals.BackendWrapper.plottingBkgShown) { + var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() + for (var i = 0; i < bkgData.length; i++) { + backgroundRefLine.append(bkgData[i].x, bkgData[i].y) + } + } + + // Update scale line + scaleRefLine.clear() + if (Globals.BackendWrapper.plottingScaleShown) { + var scaleData = Globals.BackendWrapper.plottingGetScaleData() + for (var j = 0; j < scaleData.length; j++) { + scaleRefLine.append(scaleData[j].x, scaleData[j].y) + } + } + } + // Multi-experiment series management function updateMultiExperimentSeries() { // Always get the latest value from backend @@ -399,6 +451,9 @@ Rectangle { // Initialize multi-experiment support updateMultiExperimentSeries() + + // Initialize reference lines + updateReferenceLines() } } } diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index 03c069c3..dc0dc9a7 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -32,6 +32,58 @@ Rectangle { useOpenGL: EaGlobals.Vars.useOpenGL + // Background reference line series + LineSeries { + id: backgroundRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#888888" + width: 1 + style: Qt.DashLine + visible: Globals.BackendWrapper.plottingBkgShown + } + + // Scale reference line series + LineSeries { + id: scaleRefLine + axisX: chartView.axisX + axisY: chartView.axisY + useOpenGL: chartView.useOpenGL + color: "#666666" + width: 1 + style: Qt.DotLine + visible: Globals.BackendWrapper.plottingScaleShown + } + + // Update reference lines when visibility changes + Connections { + target: Globals.BackendWrapper.activeBackend.plotting + function onReferenceLineVisibilityChanged() { + chartView.updateReferenceLines() + } + } + + function updateReferenceLines() { + // Update background line + backgroundRefLine.clear() + if (Globals.BackendWrapper.plottingBkgShown) { + var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() + for (var i = 0; i < bkgData.length; i++) { + backgroundRefLine.append(bkgData[i].x, bkgData[i].y) + } + } + + // Update scale line + scaleRefLine.clear() + if (Globals.BackendWrapper.plottingScaleShown) { + var scaleData = Globals.BackendWrapper.plottingGetScaleData() + for (var j = 0; j < scaleData.length; j++) { + scaleRefLine.append(scaleData[j].x, scaleData[j].y) + } + } + } + // Multi-experiment support property var multiExperimentSeries: [] property bool isMultiExperimentMode: { @@ -509,6 +561,9 @@ Rectangle { // Initialize multi-experiment support // console.log("ExperimentView initialized - checking multi-experiment mode...") updateMultiExperimentSeries() + + // Initialize reference lines + updateReferenceLines() } // Update series when chart becomes visible @@ -516,6 +571,9 @@ Rectangle { if (visible && isMultiExperimentMode) { updateMultiExperimentSeries() } + if (visible) { + updateReferenceLines() + } } } From a8c88c6985c4e8e95be491041c823ab8566ad5eb Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 26 Jan 2026 17:24:00 +0100 Subject: [PATCH 03/12] Y axis label fix --- .../Gui/Pages/Analysis/MainContent/AnalysisView.qml | 2 +- .../Gui/Pages/Analysis/MainContent/CombinedView.qml | 2 +- .../Gui/Pages/Experiment/MainContent/ExperimentView.qml | 2 +- .../Gui/Pages/Sample/MainContent/CombinedView.qml | 2 +- .../Gui/Pages/Sample/MainContent/SampleView.qml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index cb11a5ff..c453d75d 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -134,7 +134,7 @@ Rectangle { axisX.maxAfterReset: Globals.BackendWrapper.plottingAnalysisMaxX + xRange * 0.01 property double yRange: Globals.BackendWrapper.plottingAnalysisMaxY - Globals.BackendWrapper.plottingAnalysisMinY - axisY.title: "Log10 R(q)" + axisY.title: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle axisY.min: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 axisY.max: Globals.BackendWrapper.plottingAnalysisMaxY + yRange * 0.01 axisY.minAfterReset: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 3c93b205..44dc438b 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -253,7 +253,7 @@ Rectangle { axisX.maxAfterReset: Globals.BackendWrapper.plottingAnalysisMaxX + xRange * 0.01 property double yRange: Globals.BackendWrapper.plottingAnalysisMaxY - Globals.BackendWrapper.plottingAnalysisMinY - axisY.title: "Log10 R(q)" + axisY.title: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle axisY.min: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 axisY.max: Globals.BackendWrapper.plottingAnalysisMaxY + yRange * 0.01 axisY.minAfterReset: Globals.BackendWrapper.plottingAnalysisMinY - yRange * 0.01 diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index dc0dc9a7..a2a5f9ac 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -224,7 +224,7 @@ Rectangle { axisX.maxAfterReset: Globals.BackendWrapper.plottingExperimentMaxX + xRange * 0.01 property double yRange: Globals.BackendWrapper.plottingExperimentMaxY - Globals.BackendWrapper.plottingExperimentMinY - axisY.title: "Log10 R(q)" + axisY.title: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle axisY.min: Globals.BackendWrapper.plottingExperimentMinY - yRange * 0.01 axisY.max: Globals.BackendWrapper.plottingExperimentMaxY + yRange * 0.01 axisY.minAfterReset: Globals.BackendWrapper.plottingExperimentMinY - yRange * 0.01 diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index e05c715c..257d6b6f 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -101,7 +101,7 @@ Rectangle { ValueAxis { id: sampleAxisY - titleText: "Log10 R(q)" + titleText: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle // min/max set imperatively to avoid binding reset during zoom property double minAfterReset: Globals.BackendWrapper.plottingSampleMinY - sampleChartView.yRange * 0.01 property double maxAfterReset: Globals.BackendWrapper.plottingSampleMaxY + sampleChartView.yRange * 0.01 diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml index 70bb4fff..8bed9517 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml @@ -87,7 +87,7 @@ Rectangle { ValueAxis { id: axisY - titleText: "Log10 R(q)" + titleText: "Log10 " + Globals.BackendWrapper.plottingYAxisTitle // min/max set imperatively to avoid binding reset during zoom property double minAfterReset: Globals.BackendWrapper.plottingSampleMinY - chartView.yRange * 0.01 property double maxAfterReset: Globals.BackendWrapper.plottingSampleMaxY + chartView.yRange * 0.01 From b981817d2296999b8e43f03e0dee52001c0eecbc Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Tue, 27 Jan 2026 15:57:58 +0100 Subject: [PATCH 04/12] fixed profile lines. --- .../Backends/Mock/Plotting.qml | 19 +++++ .../Backends/Py/plotting_1d.py | 80 +++++++++++++------ .../Gui/Globals/BackendWrapper.qml | 16 ++++ .../Analysis/MainContent/AnalysisView.qml | 8 +- .../Analysis/MainContent/CombinedView.qml | 8 +- 5 files changed, 99 insertions(+), 32 deletions(-) diff --git a/EasyReflectometryApp/Backends/Mock/Plotting.qml b/EasyReflectometryApp/Backends/Mock/Plotting.qml index d973b572..ff856611 100644 --- a/EasyReflectometryApp/Backends/Mock/Plotting.qml +++ b/EasyReflectometryApp/Backends/Mock/Plotting.qml @@ -113,4 +113,23 @@ QtObject { ] } + // Analysis-specific reference line data accessors (use sample/calculated x-range) + function getBackgroundDataForAnalysis() { + if (!bkgShown) return [] + // Return mock horizontal line at background level using sample x-range + return [ + { 'x': sampleMinX, 'y': -7.0 }, + { 'x': sampleMaxX, 'y': -7.0 } + ] + } + + function getScaleDataForAnalysis() { + if (!scaleShown) return [] + // Return mock horizontal line at scale level using sample x-range + return [ + { 'x': sampleMinX, 'y': 0.0 }, + { 'x': sampleMaxX, 'y': 0.0 } + ] + } + } diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index d7b60588..aae4c810 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -143,6 +143,7 @@ def getBackgroundData(self) -> list: """Return background reference line data for plotting. Returns a horizontal line at the model's background value. + Note: Reference lines are always horizontal, even in R×q⁴ mode. """ if not self._bkg_shown: return [] @@ -155,15 +156,8 @@ def getBackgroundData(self) -> list: bkg_value = model.background.value # For log scale plotting, convert background value bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - # For background, we need to transform: bkg * q^4 - return [ - {'x': float(x[0]), 'y': float(np.log10(bkg_value * x[0] ** 4)) if bkg_value * x[0] ** 4 > 0 else -10.0}, - {'x': float(x[-1]), 'y': float(np.log10(bkg_value * x[-1] ** 4)) if bkg_value * x[-1] ** 4 > 0 else -10.0}, - ] - else: - return [{'x': float(x[0]), 'y': bkg_log}, {'x': float(x[-1]), 'y': bkg_log}] + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x[0]), 'y': bkg_log}, {'x': float(x[-1]), 'y': bkg_log}] except (IndexError, AttributeError, TypeError) as e: console.debug(f'Error getting background data: {e}') return [] @@ -176,6 +170,7 @@ def getScaleData(self) -> list: Note: Scale is a multiplicative factor, typically close to 1.0. For reflectometry plots, the scale line at y=scale (log10) shows where R=scale, i.e., where the reflectivity equals the scale factor. + Reference lines are always horizontal, even in R×q⁴ mode. """ if not self._scale_shown: return [] @@ -188,25 +183,62 @@ def getScaleData(self) -> list: scale_value = model.scale.value # For log scale plotting, convert scale value scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - # For scale, we need to transform: scale * q^4 - return [ - { - 'x': float(x[0]), - 'y': float(np.log10(scale_value * x[0] ** 4)) if scale_value * x[0] ** 4 > 0 else -10.0, - }, - { - 'x': float(x[-1]), - 'y': float(np.log10(scale_value * x[-1] ** 4)) if scale_value * x[-1] ** 4 > 0 else -10.0, - }, - ] - else: - return [{'x': float(x[0]), 'y': scale_log}, {'x': float(x[-1]), 'y': scale_log}] + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x[0]), 'y': scale_log}, {'x': float(x[-1]), 'y': scale_log}] except (IndexError, AttributeError, TypeError) as e: console.debug(f'Error getting scale data: {e}') return [] + @Slot(result='QVariantList') + def getBackgroundDataForAnalysis(self) -> list: + """Return background reference line data for the Analysis chart. + + Uses the analysis/sample x-range (calculated model data range) instead of + experimental data range to ensure the line spans the full chart width. + Reference lines are always horizontal, even in R×q⁴ mode. + """ + if not self._bkg_shown: + return [] + try: + model = self._project_lib.models[self._project_lib.current_model_index] + # Use sample/analysis x-range instead of experimental data + x_min, x_max = self._get_all_models_sample_range()[0:2] + if x_min == float('inf') or x_max == float('-inf'): + return [] + bkg_value = model.background.value + # For log scale plotting, convert background value + bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x_min), 'y': bkg_log}, {'x': float(x_max), 'y': bkg_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting background data for analysis: {e}') + return [] + + @Slot(result='QVariantList') + def getScaleDataForAnalysis(self) -> list: + """Return scale reference line data for the Analysis chart. + + Uses the analysis/sample x-range (calculated model data range) instead of + experimental data range to ensure the line spans the full chart width. + Reference lines are always horizontal, even in R×q⁴ mode. + """ + if not self._scale_shown: + return [] + try: + model = self._project_lib.models[self._project_lib.current_model_index] + # Use sample/analysis x-range instead of experimental data + x_min, x_max = self._get_all_models_sample_range()[0:2] + if x_min == float('inf') or x_max == float('-inf'): + return [] + scale_value = model.scale.value + # For log scale plotting, convert scale value + scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 + # Reference lines are always horizontal (no R×q⁴ transformation) + return [{'x': float(x_min), 'y': scale_log}, {'x': float(x_max), 'y': scale_log}] + except (IndexError, AttributeError, TypeError) as e: + console.debug(f'Error getting scale data for analysis: {e}') + return [] + @property def sample_data(self) -> DataSet1D: idx = self._project_lib.current_model_index diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index 189e6573..0da30766 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -362,6 +362,22 @@ QtObject { } } + // Analysis-specific reference line data accessors (use sample/calculated x-range) + function plottingGetBackgroundDataForAnalysis() { + try { + return activeBackend.plotting.getBackgroundDataForAnalysis() + } catch (e) { + return [] + } + } + function plottingGetScaleDataForAnalysis() { + try { + return activeBackend.plotting.getScaleDataForAnalysis() + } catch (e) { + return [] + } + } + function plottingSetQtChartsSerieRef(value1, value2, value3) { activeBackend.plotting.setQtChartsSerieRef(value1, value2, value3) } function plottingRefreshSample() { activeBackend.plotting.drawCalculatedOnSampleChart() } function plottingRefreshSLD() { activeBackend.plotting.drawCalculatedOnSldChart() } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index c453d75d..212ff3cf 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -64,19 +64,19 @@ Rectangle { } function updateReferenceLines() { - // Update background line + // Update background line (use analysis-specific method for correct x-range) backgroundRefLine.clear() if (Globals.BackendWrapper.plottingBkgShown) { - var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() + var bkgData = Globals.BackendWrapper.plottingGetBackgroundDataForAnalysis() for (var i = 0; i < bkgData.length; i++) { backgroundRefLine.append(bkgData[i].x, bkgData[i].y) } } - // Update scale line + // Update scale line (use analysis-specific method for correct x-range) scaleRefLine.clear() if (Globals.BackendWrapper.plottingScaleShown) { - var scaleData = Globals.BackendWrapper.plottingGetScaleData() + var scaleData = Globals.BackendWrapper.plottingGetScaleDataForAnalysis() for (var j = 0; j < scaleData.length; j++) { scaleRefLine.append(scaleData[j].x, scaleData[j].y) } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 44dc438b..52e5940d 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -120,19 +120,19 @@ Rectangle { } function updateReferenceLines() { - // Update background line + // Update background line (use analysis-specific method for correct x-range) backgroundRefLine.clear() if (Globals.BackendWrapper.plottingBkgShown) { - var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() + var bkgData = Globals.BackendWrapper.plottingGetBackgroundDataForAnalysis() for (var i = 0; i < bkgData.length; i++) { backgroundRefLine.append(bkgData[i].x, bkgData[i].y) } } - // Update scale line + // Update scale line (use analysis-specific method for correct x-range) scaleRefLine.clear() if (Globals.BackendWrapper.plottingScaleShown) { - var scaleData = Globals.BackendWrapper.plottingGetScaleData() + var scaleData = Globals.BackendWrapper.plottingGetScaleDataForAnalysis() for (var j = 0; j < scaleData.length; j++) { scaleRefLine.append(scaleData[j].x, scaleData[j].y) } From abd0201fd371ad906f5d0f134147cd0c6b709841 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Tue, 27 Jan 2026 21:54:56 +0100 Subject: [PATCH 05/12] code review fixes --- .../Backends/Py/plotting_1d.py | 38 ++++++++++++++----- .../Analysis/MainContent/AnalysisView.qml | 6 ++- .../Analysis/MainContent/CombinedView.qml | 6 ++- .../Experiment/MainContent/ExperimentView.qml | 6 ++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index aae4c810..84d6852b 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -148,8 +148,11 @@ def getBackgroundData(self) -> list: if not self._bkg_shown: return [] try: - model = self._project_lib.models[self._project_lib.current_model_index] - exp_data = self._project_lib.experimental_data_for_model_at_index(self._project_lib.current_experiment_index) + # Capture indices atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + exp_idx = self._project_lib.current_experiment_index + model = self._project_lib.models[model_idx] + exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) if exp_data.x is None or len(exp_data.x) == 0: return [] x = exp_data.x @@ -175,8 +178,11 @@ def getScaleData(self) -> list: if not self._scale_shown: return [] try: - model = self._project_lib.models[self._project_lib.current_model_index] - exp_data = self._project_lib.experimental_data_for_model_at_index(self._project_lib.current_experiment_index) + # Capture indices atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + exp_idx = self._project_lib.current_experiment_index + model = self._project_lib.models[model_idx] + exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) if exp_data.x is None or len(exp_data.x) == 0: return [] x = exp_data.x @@ -200,7 +206,9 @@ def getBackgroundDataForAnalysis(self) -> list: if not self._bkg_shown: return [] try: - model = self._project_lib.models[self._project_lib.current_model_index] + # Capture index atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + model = self._project_lib.models[model_idx] # Use sample/analysis x-range instead of experimental data x_min, x_max = self._get_all_models_sample_range()[0:2] if x_min == float('inf') or x_max == float('-inf'): @@ -225,7 +233,9 @@ def getScaleDataForAnalysis(self) -> list: if not self._scale_shown: return [] try: - model = self._project_lib.models[self._project_lib.current_model_index] + # Capture index atomically to prevent race conditions + model_idx = self._project_lib.current_model_index + model = self._project_lib.models[model_idx] # Use sample/analysis x-range instead of experimental data x_min, x_max = self._get_all_models_sample_range()[0:2] if x_min == float('inf') or x_max == float('-inf'): @@ -444,6 +454,10 @@ def experimentMinY(self): if self._plot_rq4: valid_x = data.x[data.y > 0] if data.y.size > 0 else np.array([1.0]) valid_y = valid_y * (valid_x**4) + # Filter again after transformation to avoid log of zero/negative + valid_y = valid_y[valid_y > 0] + if valid_y.size == 0: + return -10.0 return np.log10(valid_y.min()) @Property('QVariant', notify=chartRefsChanged) @@ -539,15 +553,17 @@ def getExperimentDataPoints(self, experiment_index: int) -> list: r = point[1] error_var = point[2] # Apply R×q⁴ transformation if enabled + # Clamp error_lower before transformation to ensure positive values + error_lower_linear = max(r - np.sqrt(error_var), 1e-20) if self._plot_rq4: q4 = q**4 r_val = r * q4 error_upper = (r + np.sqrt(error_var)) * q4 - error_lower = max((r - np.sqrt(error_var)) * q4, 1e-10) + error_lower = error_lower_linear * q4 else: r_val = r error_upper = r + np.sqrt(error_var) - error_lower = max(r - np.sqrt(error_var), 1e-10) + error_lower = error_lower_linear points.append( { 'x': float(q), @@ -699,15 +715,17 @@ def qtchartsReplaceMeasuredOnExperimentChartAndRedraw(self): r = point[1] error_var = point[2] # Apply R×q⁴ transformation if enabled + # Clamp error_lower before transformation to ensure positive values + error_lower_linear = max(r - np.sqrt(error_var), 1e-20) if self._plot_rq4: q4 = q**4 r_val = r * q4 error_upper = (r + np.sqrt(error_var)) * q4 - error_lower = max((r - np.sqrt(error_var)) * q4, 1e-10) + error_lower = error_lower_linear * q4 else: r_val = r error_upper = r + np.sqrt(error_var) - error_lower = max(r - np.sqrt(error_var), 1e-10) + error_lower = error_lower_linear series_measured.append(q, np.log10(r_val)) series_error_upper.append(q, np.log10(error_upper)) series_error_lower.append(q, np.log10(error_lower)) diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index 212ff3cf..9fef7a8f 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -57,7 +57,8 @@ Rectangle { // Update reference lines when visibility changes Connections { - target: Globals.BackendWrapper.activeBackend.plotting + target: Globals.BackendWrapper.activeBackend?.plotting ?? null + enabled: target !== null function onReferenceLineVisibilityChanged() { chartView.updateReferenceLines() } @@ -101,7 +102,8 @@ Rectangle { // Watch for changes in multi-experiment selection Connections { - target: Globals.BackendWrapper.activeBackend + target: Globals.BackendWrapper.activeBackend ?? null + enabled: target !== null function onMultiExperimentSelectionChanged() { console.log("Analysis: Multi-experiment selection changed - updating series") chartView.updateMultiExperimentSeries() diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 52e5940d..6f19845b 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -63,7 +63,8 @@ Rectangle { // Watch for changes in multi-experiment selection Connections { - target: Globals.BackendWrapper.activeBackend + target: Globals.BackendWrapper.activeBackend ?? null + enabled: target !== null function onMultiExperimentSelectionChanged() { analysisChartView.updateMultiExperimentSeries() } @@ -113,7 +114,8 @@ Rectangle { // Update reference lines when visibility changes Connections { - target: Globals.BackendWrapper.activeBackend.plotting + target: Globals.BackendWrapper.activeBackend?.plotting ?? null + enabled: target !== null function onReferenceLineVisibilityChanged() { analysisChartView.updateReferenceLines() } diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index a2a5f9ac..04277a8e 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -58,7 +58,8 @@ Rectangle { // Update reference lines when visibility changes Connections { - target: Globals.BackendWrapper.activeBackend.plotting + target: Globals.BackendWrapper.activeBackend?.plotting ?? null + enabled: target !== null function onReferenceLineVisibilityChanged() { chartView.updateReferenceLines() } @@ -189,7 +190,8 @@ Rectangle { // Watch for changes in multi-experiment selection Connections { - target: Globals.BackendWrapper.activeBackend + target: Globals.BackendWrapper.activeBackend ?? null + enabled: target !== null function onMultiExperimentSelectionChanged() { // Update series when selection changes // The function will handle showing/hiding appropriate series From 2b6c4d6d284c23cdb7181a90bd08d1d1394d108e Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 5 Feb 2026 19:08:15 +0100 Subject: [PATCH 06/12] proper orso sample conversion into layers --- EasyReflectometryApp/Backends/Py/logic/project.py | 8 ++++++++ EasyReflectometryApp/Backends/Py/plotting_1d.py | 1 + EasyReflectometryApp/Backends/Py/project.py | 9 +++++++++ EasyReflectometryApp/Backends/Py/py_backend.py | 3 +++ EasyReflectometryApp/Gui/Globals/BackendWrapper.qml | 5 +++++ .../Gui/Pages/Analysis/MainContent/AnalysisView.qml | 4 ++++ .../Gui/Pages/Analysis/MainContent/CombinedView.qml | 4 ++++ .../Pages/Experiment/MainContent/ExperimentView.qml | 4 ++++ .../Gui/Pages/Sample/MainContent/CombinedView.qml | 9 ++++++++- .../Gui/Pages/Sample/MainContent/SampleView.qml | 4 ++++ .../Gui/Pages/Sample/MainContent/SldView.qml | 11 +++++++++++ .../Sidebar/Basic/Groups/Assemblies/MultiLayer.qml | 2 ++ .../Basic/Groups/Assemblies/SurfactantLayer.qml | 7 ++++++- 13 files changed, 69 insertions(+), 2 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/logic/project.py b/EasyReflectometryApp/Backends/Py/logic/project.py index adf8976c..2ec5d133 100644 --- a/EasyReflectometryApp/Backends/Py/logic/project.py +++ b/EasyReflectometryApp/Backends/Py/logic/project.py @@ -122,6 +122,14 @@ def add_sample_from_orso(self, sample) -> None: new_model_index = len(self._project_lib.models) - 1 self._update_enablement_of_fixed_layers_for_model(new_model_index) + def is_only_default_model(self) -> bool: + """Check if there is only one model and it is the default model.""" + return len(self._project_lib.models) == 1 and self._project_lib.is_default_model(0) + + def remove_model_at_index(self, index: int) -> None: + """Remove the model at the given index.""" + self._project_lib.remove_model_at_index(index) + def reset(self) -> None: self._project_lib.reset() self._project_lib.default_model() diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index d7b60588..5e5415aa 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -19,6 +19,7 @@ class Plotting1d(QObject): experimentChartRangesChanged = Signal() experimentDataChanged = Signal() samplePageDataChanged = Signal() # Signal for QML to refresh sample page charts + chartAxesResetRequested = Signal() # Signal to request QML to reset chart axes # New signals for plot mode properties plotModeChanged = Signal() diff --git a/EasyReflectometryApp/Backends/Py/project.py b/EasyReflectometryApp/Backends/Py/project.py index 5217c0df..3e4ad0b3 100644 --- a/EasyReflectometryApp/Backends/Py/project.py +++ b/EasyReflectometryApp/Backends/Py/project.py @@ -110,7 +110,16 @@ def sampleLoad(self, url: str) -> None: orso_data = orso.load_orso(generalizePath(url)) # Load the sample model sample = load_orso_model(orso_data) + + # Check if we should replace the default model or append + should_replace_default = self._logic.is_only_default_model() + # Add the sample as a new model in the project self._logic.add_sample_from_orso(sample) + + # If we replaced the default model, remove it (it's now at index 0) + if should_replace_default: + self._logic.remove_model_at_index(0) + # notify listeners self.externalProjectLoaded.emit() diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index ce924fe8..41a681af 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -179,6 +179,7 @@ def _relay_project_page_created(self): def _relay_project_page_project_changed(self): self._sample.materialsTableChanged.emit() self._sample.modelsTableChanged.emit() + self._sample.modelsIndexChanged.emit() self._sample.assembliesTableChanged.emit() self._sample._clearCacheAndEmitLayersChanged() self._experiment.experimentChanged.emit() @@ -188,6 +189,8 @@ def _relay_project_page_project_changed(self): self._summary.summaryChanged.emit() self._plotting_1d.reset_data() self._refresh_plots() + # Request QML to reset axes to fit new data + self._plotting_1d.chartAxesResetRequested.emit() def _relay_sample_page_sample_changed(self): self._plotting_1d.reset_data() diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index 189e6573..d18f078e 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -396,6 +396,8 @@ QtObject { signal samplePageDataChanged() // Signal for plot mode changes - forward from backend signal plotModeChanged() + // Signal to request QML to reset chart axes (e.g., after model load) + signal chartAxesResetRequested() // Connect to backend signal (called from Component.onCompleted in QML items) function connectSamplePageDataChanged() { @@ -405,6 +407,9 @@ QtObject { if (activeBackend && activeBackend.plotting && activeBackend.plotting.plotModeChanged) { activeBackend.plotting.plotModeChanged.connect(plotModeChanged) } + if (activeBackend && activeBackend.plotting && activeBackend.plotting.chartAxesResetRequested) { + activeBackend.plotting.chartAxesResetRequested.connect(chartAxesResetRequested) + } } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index c453d75d..4aa387ee 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -117,6 +117,10 @@ Rectangle { // Delay resetAxes to allow axis range properties to update first analysisResetAxesTimer.start() } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + analysisResetAxesTimer.start() + } } Timer { diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 44dc438b..03e2ea7f 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -78,6 +78,10 @@ Rectangle { // Delay resetAxes to allow axis range properties to update first combinedAnalysisResetAxesTimer.start() } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + combinedAnalysisResetAxesTimer.start() + } } Timer { diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index a2a5f9ac..5cd9d546 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -207,6 +207,10 @@ Rectangle { // Delay resetAxes to allow axis range properties to update first experimentResetAxesTimer.start() } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + experimentResetAxesTimer.start() + } } Timer { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index 257d6b6f..cd435517 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -596,13 +596,20 @@ Rectangle { // Delay resetAxes to allow axis range properties to update first sampleCombinedResetAxesTimer.start() } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + sampleCombinedResetAxesTimer.start() + } } Timer { id: sampleCombinedResetAxesTimer interval: 50 repeat: false - onTriggered: sampleChartView.resetAxes() + onTriggered: { + sampleChartView.resetAxes() + sldChartView.resetAxes() + } } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml index 8bed9517..88a8c975 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml @@ -352,6 +352,10 @@ Rectangle { // Delay resetAxes to allow axis range properties to update first sampleResetAxesTimer.start() } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + sampleResetAxesTimer.start() + } } Timer { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml index e27d98f5..6656eaf3 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml @@ -317,6 +317,17 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + sldResetAxesTimer.start() + } + } + + Timer { + id: sldResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml index 213ab528..8697204f 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/MultiLayer.qml @@ -78,6 +78,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].thickness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].thickness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].thickness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerThickness(text) } @@ -85,6 +86,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].roughness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].roughness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].roughness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerRoughness(text) } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml index dd33ea8d..231e02f1 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/Assemblies/SurfactantLayer.qml @@ -60,6 +60,7 @@ EaElements.GroupColumn { EaComponents.TableViewTextInput { horizontalAlignment: Text.AlignHCenter text: Globals.BackendWrapper.sampleLayers[index].formula + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerFormula(text) } @@ -67,6 +68,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].thickness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].thickness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].thickness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerThickness(text) } @@ -74,12 +76,14 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].roughness_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].roughness)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].roughness).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerRoughness(text) } EaComponents.TableViewTextInput { horizontalAlignment: Text.AlignHCenter text: (isNaN(Globals.BackendWrapper.sampleLayers[index].solvation)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].solvation).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerSolvation(text) } @@ -87,6 +91,7 @@ EaElements.GroupColumn { horizontalAlignment: Text.AlignHCenter enabled: Globals.BackendWrapper.sampleLayers[index].apm_enabled === "True" text: (isNaN(Globals.BackendWrapper.sampleLayers[index].apm)) ? '--' : Number(Globals.BackendWrapper.sampleLayers[index].apm).toFixed(2) + onActiveFocusChanged: if (activeFocus && Globals.BackendWrapper.sampleCurrentLayerIndex !== index) Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) onEditingFinished: Globals.BackendWrapper.sampleSetCurrentLayerAPM(text) } @@ -108,7 +113,7 @@ EaElements.GroupColumn { } } mouseArea.onPressed: { - if (Globals.BackendWrapper.samplCurrentLayerIndex !== index) { + if (Globals.BackendWrapper.sampleCurrentLayerIndex !== index) { Globals.BackendWrapper.sampleSetCurrentLayerIndex(index) } } From 1c112b2b09945d6c801b385ed1da1bc3f5560b75 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Sun, 8 Feb 2026 12:34:14 +0100 Subject: [PATCH 07/12] added checkbox for loaded orso overwrite. fixed axes reset on file load --- EasyReflectometryApp/Backends/Mock/Plotting.qml | 1 + EasyReflectometryApp/Backends/Py/logic/project.py | 5 +++++ EasyReflectometryApp/Backends/Py/plotting_1d.py | 1 + EasyReflectometryApp/Backends/Py/project.py | 12 ++++++++---- EasyReflectometryApp/Backends/Py/py_backend.py | 1 + EasyReflectometryApp/Gui/Globals/BackendWrapper.qml | 7 ++++++- .../Gui/Pages/Sample/MainContent/CombinedView.qml | 11 +++++++++++ .../Gui/Pages/Sample/MainContent/SampleView.qml | 3 +++ .../Gui/Pages/Sample/MainContent/SldView.qml | 10 ++++++++++ .../Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml | 9 ++++++++- 10 files changed, 54 insertions(+), 6 deletions(-) diff --git a/EasyReflectometryApp/Backends/Mock/Plotting.qml b/EasyReflectometryApp/Backends/Mock/Plotting.qml index ff856611..57e49717 100644 --- a/EasyReflectometryApp/Backends/Mock/Plotting.qml +++ b/EasyReflectometryApp/Backends/Mock/Plotting.qml @@ -38,6 +38,7 @@ QtObject { signal sldAxisReversedChanged() signal referenceLineVisibilityChanged() signal samplePageDataChanged() + signal samplePageResetAxes() function setQtChartsSerieRef(value1, value2, value3) { console.debug(`setQtChartsSerieRef ${value1}, ${value2}, ${value3}`) diff --git a/EasyReflectometryApp/Backends/Py/logic/project.py b/EasyReflectometryApp/Backends/Py/logic/project.py index adf8976c..47ee5be7 100644 --- a/EasyReflectometryApp/Backends/Py/logic/project.py +++ b/EasyReflectometryApp/Backends/Py/logic/project.py @@ -122,6 +122,11 @@ def add_sample_from_orso(self, sample) -> None: new_model_index = len(self._project_lib.models) - 1 self._update_enablement_of_fixed_layers_for_model(new_model_index) + def replace_models_from_orso(self, sample) -> None: + """Replace all existing models with a single model built from the loaded sample.""" + self._project_lib.replace_models_from_orso(sample) + self._update_enablement_of_fixed_layers_for_model(0) + def reset(self) -> None: self._project_lib.reset() self._project_lib.default_model() diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index 84d6852b..f376564a 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -19,6 +19,7 @@ class Plotting1d(QObject): experimentChartRangesChanged = Signal() experimentDataChanged = Signal() samplePageDataChanged = Signal() # Signal for QML to refresh sample page charts + samplePageResetAxes = Signal() # Signal for QML to reset chart axes after data load # New signals for plot mode properties plotModeChanged = Signal() diff --git a/EasyReflectometryApp/Backends/Py/project.py b/EasyReflectometryApp/Backends/Py/project.py index 5217c0df..be758dd7 100644 --- a/EasyReflectometryApp/Backends/Py/project.py +++ b/EasyReflectometryApp/Backends/Py/project.py @@ -104,13 +104,17 @@ def reset(self) -> None: self.externalNameChanged.emit() self.externalProjectReset.emit() - @Slot(str) - def sampleLoad(self, url: str) -> None: + @Slot(str, bool) + def sampleLoad(self, url: str, append: bool = True) -> None: # Load ORSO file content orso_data = orso.load_orso(generalizePath(url)) # Load the sample model sample = load_orso_model(orso_data) - # Add the sample as a new model in the project - self._logic.add_sample_from_orso(sample) + if append: + # Add the sample as a new model in the project + self._logic.add_sample_from_orso(sample) + else: + # Replace all existing models with the loaded sample + self._logic.replace_models_from_orso(sample) # notify listeners self.externalProjectLoaded.emit() diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index ce924fe8..3a0a616d 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -188,6 +188,7 @@ def _relay_project_page_project_changed(self): self._summary.summaryChanged.emit() self._plotting_1d.reset_data() self._refresh_plots() + self._plotting_1d.samplePageResetAxes.emit() def _relay_sample_page_sample_changed(self): self._plotting_1d.reset_data() diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index 0da30766..2fa06e68 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -78,7 +78,7 @@ QtObject { function projectReset() { activeBackend.project.reset() } function projectSave() { activeBackend.project.save() } function projectLoad(value) { activeBackend.project.load(value) } - function sampleFileLoad(value) { activeBackend.project.sampleLoad(value) } + function sampleFileLoad(value, append) { activeBackend.project.sampleLoad(value, append) } /////////////// @@ -410,6 +410,8 @@ QtObject { // Signal for sample page data changes - forward from backend signal samplePageDataChanged() + // Signal for resetting chart axes after data load + signal samplePageResetAxes() // Signal for plot mode changes - forward from backend signal plotModeChanged() @@ -418,6 +420,9 @@ QtObject { if (activeBackend && activeBackend.plotting && activeBackend.plotting.samplePageDataChanged) { activeBackend.plotting.samplePageDataChanged.connect(samplePageDataChanged) } + if (activeBackend && activeBackend.plotting && activeBackend.plotting.samplePageResetAxes) { + activeBackend.plotting.samplePageResetAxes.connect(samplePageResetAxes) + } if (activeBackend && activeBackend.plotting && activeBackend.plotting.plotModeChanged) { activeBackend.plotting.plotModeChanged.connect(plotModeChanged) } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index 257d6b6f..07301a5a 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -591,6 +591,10 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onSamplePageResetAxes() { + sampleCombinedResetAxesTimer.start() + sldCombinedResetAxesTimer.start() + } function onPlotModeChanged() { refreshAllCharts() // Delay resetAxes to allow axis range properties to update first @@ -605,6 +609,13 @@ Rectangle { onTriggered: sampleChartView.resetAxes() } + Timer { + id: sldCombinedResetAxesTimer + interval: 50 + repeat: false + onTriggered: sldChartView.resetAxes() + } + Component.onCompleted: { Qt.callLater(recreateAllSeries) } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml index 8bed9517..b5cbed77 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml @@ -347,6 +347,9 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onSamplePageResetAxes() { + sampleResetAxesTimer.start() + } function onPlotModeChanged() { refreshAllCharts() // Delay resetAxes to allow axis range properties to update first diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml index e27d98f5..9c034fb1 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml @@ -317,6 +317,16 @@ Rectangle { function onSamplePageDataChanged() { refreshAllCharts() } + function onSamplePageResetAxes() { + sldResetAxesTimer.start() + } + } + + Timer { + id: sldResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() } Component.onCompleted: { diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml index 14973e97..b7ee6ec0 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml @@ -13,6 +13,13 @@ EaElements.GroupBox { collapsed: false EaElements.GroupColumn { + EaElements.CheckBox { + id: appendCheckBox + text: qsTr("Append to existing models") + checked: true + width: EaStyle.Sizes.sideBarContentWidth + } + EaElements.SideBarButton { width: EaStyle.Sizes.sideBarContentWidth fontIcon: "folder-open" @@ -24,7 +31,7 @@ EaElements.GroupBox { id: fileDialog title: qsTr("Select a sample file") nameFilters: [ "ORT files (*.ort)", "ORSO files (*.orso)", "All files (*.*)" ] - onAccepted: Globals.BackendWrapper.sampleFileLoad(selectedFiles[0]) + onAccepted: Globals.BackendWrapper.sampleFileLoad(selectedFiles[0], appendCheckBox.checked) } } } \ No newline at end of file From 77824a6e7a01c21f8054e351c58e7d04b1616e60 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Wed, 11 Feb 2026 19:22:45 +0100 Subject: [PATCH 08/12] show ORSO name when available --- EasyReflectometryApp/Backends/Py/analysis.py | 3 ++- EasyReflectometryApp/Backends/Py/logic/models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index b14d351d..0d3049bb 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -292,7 +292,8 @@ def modelNamesForExperiment(self) -> list: mapped_models = [] experiments = self._experiments_logic._project_lib._experiments for ind in experiments: - mapped_models.append(experiments[ind].model.name) + name = experiments[ind].model.user_data.get('original_name', experiments[ind].model.name) + mapped_models.append(name) return mapped_models @Property('QVariantList', notify=experimentsChanged) diff --git a/EasyReflectometryApp/Backends/Py/logic/models.py b/EasyReflectometryApp/Backends/Py/logic/models.py index cf2777d2..29865707 100644 --- a/EasyReflectometryApp/Backends/Py/logic/models.py +++ b/EasyReflectometryApp/Backends/Py/logic/models.py @@ -128,7 +128,7 @@ def _from_models_collection_to_list_of_dicts(models_collection: ModelCollection) for model in models_collection: models_list.append( { - 'label': model.name, + 'label': model.user_data.get('original_name', model.name), # Use original name if available 'color': str(model.color), } ) From fcabcabe8941e891f1259168581b1ffa0f1d927e Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 12 Feb 2026 13:50:05 +0100 Subject: [PATCH 09/12] model prefix based on its name --- EasyReflectometryApp/Backends/Py/logic/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EasyReflectometryApp/Backends/Py/logic/parameters.py b/EasyReflectometryApp/Backends/Py/logic/parameters.py index bc36f539..2eef21ad 100644 --- a/EasyReflectometryApp/Backends/Py/logic/parameters.py +++ b/EasyReflectometryApp/Backends/Py/logic/parameters.py @@ -226,7 +226,7 @@ def _is_layer_parameter(param: Parameter) -> bool: # Process parameters for each model for model_idx, model in enumerate(models): model_unique_name = model.unique_name - model_prefix = f'M{model_idx + 1}' + model_prefix = model.user_data.get('original_name', model.name) for parameter in parameters: # Skip parameters not in this model's path From 8ee83bddf4f519e4bf9821a15e9ad5a7b291ec63 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Thu, 12 Feb 2026 21:38:32 +0100 Subject: [PATCH 10/12] use orso name when available --- EasyReflectometryApp/Backends/Py/logic/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/EasyReflectometryApp/Backends/Py/logic/models.py b/EasyReflectometryApp/Backends/Py/logic/models.py index 29865707..0dee4dea 100644 --- a/EasyReflectometryApp/Backends/Py/logic/models.py +++ b/EasyReflectometryApp/Backends/Py/logic/models.py @@ -21,7 +21,10 @@ def index(self, new_value: Union[int, str]) -> None: @property def name_at_current_index(self) -> str: - return self._models[self.index].name + if self._models[self.index].user_data.get('original_name'): + return self._models[self.index].user_data['original_name'] + else: + return self._models[self.index].name @property def scaling_at_current_index(self) -> float: From 5f2639d7458c45c5055577c3ce011b9b0c69b3e0 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Fri, 13 Feb 2026 15:14:03 +0100 Subject: [PATCH 11/12] enhance safety of ORSO sample file load --- EasyReflectometryApp/Backends/Py/project.py | 13 +++++++++- .../Gui/Globals/BackendWrapper.qml | 10 ++++++++ .../Sidebar/Basic/Groups/LoadSample.qml | 24 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/EasyReflectometryApp/Backends/Py/project.py b/EasyReflectometryApp/Backends/Py/project.py index be758dd7..4fba7149 100644 --- a/EasyReflectometryApp/Backends/Py/project.py +++ b/EasyReflectometryApp/Backends/Py/project.py @@ -1,3 +1,5 @@ +import warnings + from EasyApp.Logic.Utils.Utils import generalizePath from easyreflectometry import Project as ProjectLib from easyreflectometry.orso_utils import load_orso_model @@ -20,6 +22,7 @@ class Project(QObject): externalNameChanged = Signal() externalProjectLoaded = Signal() externalProjectReset = Signal() + sampleLoadWarning = Signal(str) def __init__(self, project_lib: ProjectLib, parent=None): super().__init__(parent) @@ -109,7 +112,15 @@ def sampleLoad(self, url: str, append: bool = True) -> None: # Load ORSO file content orso_data = orso.load_orso(generalizePath(url)) # Load the sample model - sample = load_orso_model(orso_data) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter('always') + sample = load_orso_model(orso_data) + if sample is None: + warning_msg = 'The ORSO file does not contain a valid sample model definition. No sample was loaded.' + for w in caught_warnings: + warning_msg = str(w.message) + self.sampleLoadWarning.emit(warning_msg) + return if append: # Add the sample as a new model in the project self._logic.add_sample_from_orso(sample) diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index c0e5d5c7..b8479c89 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -80,6 +80,16 @@ QtObject { function projectLoad(value) { activeBackend.project.load(value) } function sampleFileLoad(value, append) { activeBackend.project.sampleLoad(value, append) } + // Sample load warning signal - forwarded from backend + signal sampleLoadWarning(string message) + + property var _sampleLoadWarningConnection: { + if (activeBackend && activeBackend.project && activeBackend.project.sampleLoadWarning) { + activeBackend.project.sampleLoadWarning.connect(sampleLoadWarning) + } + return null + } + /////////////// // Sample page diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml index b7ee6ec0..ec9cba86 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml @@ -34,4 +34,28 @@ EaElements.GroupBox { onAccepted: Globals.BackendWrapper.sampleFileLoad(selectedFiles[0], appendCheckBox.checked) } } + + // Warning dialog for sample load issues + EaElements.Dialog { + id: sampleLoadWarningDialog + title: qsTr("Sample Load Warning") + standardButtons: Dialog.Ok + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + property string warningMessage: "" + + EaElements.Label { + text: sampleLoadWarningDialog.warningMessage + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, EaStyle.Sizes.sideBarContentWidth * 1.5) + } + } + + Connections { + target: Globals.BackendWrapper + function onSampleLoadWarning(message) { + sampleLoadWarningDialog.warningMessage = message + sampleLoadWarningDialog.open() + } + } } \ No newline at end of file From 471f5e1dff7d829f0b1ce49761599227071ecff6 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Sat, 14 Feb 2026 16:28:07 +0100 Subject: [PATCH 12/12] changes after CR --- EasyReflectometryApp/Backends/Py/analysis.py | 3 +- .../Backends/Py/logic/helpers.py | 11 + .../Backends/Py/logic/models.py | 9 +- .../Backends/Py/logic/parameters.py | 4 +- .../Backends/Py/plotting_1d.py | 209 ++++++------------ .../Backends/Py/py_backend.py | 6 +- .../Gui/Globals/BackendWrapper.qml | 19 ++ .../Analysis/MainContent/AnalysisView.qml | 18 +- .../Analysis/MainContent/CombinedView.qml | 18 +- .../Sidebar/Advanced/Groups/PlotControl.qml | 38 +--- .../Experiment/MainContent/ExperimentView.qml | 18 +- .../Sidebar/Advanced/Groups/PlotControl.qml | 38 +--- EasyReflectometryApp/Gui/qmldir | 1 + pyproject.toml | 2 +- 14 files changed, 120 insertions(+), 274 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index 0d3049bb..3f9dacad 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -13,6 +13,7 @@ from .logic.fitting import Fitting as FittingLogic from .logic.minimizers import Minimizers as MinimizersLogic from .logic.parameters import Parameters as ParametersLogic +from .logic.helpers import get_original_name from .workers import FitterWorker @@ -292,7 +293,7 @@ def modelNamesForExperiment(self) -> list: mapped_models = [] experiments = self._experiments_logic._project_lib._experiments for ind in experiments: - name = experiments[ind].model.user_data.get('original_name', experiments[ind].model.name) + name = get_original_name(experiments[ind].model) mapped_models.append(name) return mapped_models diff --git a/EasyReflectometryApp/Backends/Py/logic/helpers.py b/EasyReflectometryApp/Backends/Py/logic/helpers.py index bb1a90d0..70465a5b 100644 --- a/EasyReflectometryApp/Backends/Py/logic/helpers.py +++ b/EasyReflectometryApp/Backends/Py/logic/helpers.py @@ -16,3 +16,14 @@ def formatMsg(type, *args): msg = ' ▌ '.join(msgs) msg = f'{mark} {msg}' return msg + + +def get_original_name(obj) -> str: + """Get original name from user_data, with defensive fallback to obj.name. + + Safely handles cases where user_data is None or not a dict. + """ + user_data = getattr(obj, 'user_data', None) + if isinstance(user_data, dict): + return user_data.get('original_name', obj.name) + return obj.name diff --git a/EasyReflectometryApp/Backends/Py/logic/models.py b/EasyReflectometryApp/Backends/Py/logic/models.py index 0dee4dea..37bc1e81 100644 --- a/EasyReflectometryApp/Backends/Py/logic/models.py +++ b/EasyReflectometryApp/Backends/Py/logic/models.py @@ -5,6 +5,8 @@ from easyreflectometry.model import ModelCollection from easyreflectometry.model.resolution_functions import PercentageFwhm +from .helpers import get_original_name + class Models: def __init__(self, project_lib: ProjectLib): @@ -21,10 +23,7 @@ def index(self, new_value: Union[int, str]) -> None: @property def name_at_current_index(self) -> str: - if self._models[self.index].user_data.get('original_name'): - return self._models[self.index].user_data['original_name'] - else: - return self._models[self.index].name + return get_original_name(self._models[self.index]) @property def scaling_at_current_index(self) -> float: @@ -131,7 +130,7 @@ def _from_models_collection_to_list_of_dicts(models_collection: ModelCollection) for model in models_collection: models_list.append( { - 'label': model.user_data.get('original_name', model.name), # Use original name if available + 'label': get_original_name(model), 'color': str(model.color), } ) diff --git a/EasyReflectometryApp/Backends/Py/logic/parameters.py b/EasyReflectometryApp/Backends/Py/logic/parameters.py index 2eef21ad..5fb05a53 100644 --- a/EasyReflectometryApp/Backends/Py/logic/parameters.py +++ b/EasyReflectometryApp/Backends/Py/logic/parameters.py @@ -9,6 +9,8 @@ from easyscience import global_object from easyscience.variable import Parameter +from .helpers import get_original_name + RESERVED_ALIAS_NAMES = {'np', 'numpy', 'math', 'pi', 'e'} @@ -226,7 +228,7 @@ def _is_layer_parameter(param: Parameter) -> bool: # Process parameters for each model for model_idx, model in enumerate(models): model_unique_name = model.unique_name - model_prefix = model.user_data.get('original_name', model.name) + model_prefix = get_original_name(model) for parameter in parameters: # Skip parameters not in this model's path diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index f376564a..46de877f 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -65,6 +65,15 @@ def reset_data(self): self._sld_data = {} console.debug(IO.formatMsg('sub', 'Sample and SLD data cleared')) + def _apply_rq4(self, x, y): + """Apply R(q)×q⁴ transformation if enabled. + + Works with both numpy arrays and scalar values. + """ + if self._plot_rq4: + return y * (x**4) + return y + # R(q)×q⁴ mode @Property(bool, notify=plotModeChanged) def plotRQ4(self) -> bool: @@ -139,116 +148,62 @@ def flipBkgShown(self) -> None: self._bkg_shown = not self._bkg_shown self.referenceLineVisibilityChanged.emit() - @Slot(result='QVariantList') - def getBackgroundData(self) -> list: - """Return background reference line data for plotting. + def _get_reference_line_data(self, param_attr: str, default_log: float, use_analysis_range: bool) -> list: + """Build a horizontal reference line for the given model parameter. - Returns a horizontal line at the model's background value. - Note: Reference lines are always horizontal, even in R×q⁴ mode. + :param param_attr: Model attribute name ('background' or 'scale') + :param default_log: Default log10 value if parameter <= 0 + :param use_analysis_range: If True, use sample/analysis x-range; if False, use experimental data x-range """ - if not self._bkg_shown: - return [] try: - # Capture indices atomically to prevent race conditions model_idx = self._project_lib.current_model_index - exp_idx = self._project_lib.current_experiment_index model = self._project_lib.models[model_idx] - exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) - if exp_data.x is None or len(exp_data.x) == 0: - return [] - x = exp_data.x - bkg_value = model.background.value - # For log scale plotting, convert background value - bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 - # Reference lines are always horizontal (no R×q⁴ transformation) - return [{'x': float(x[0]), 'y': bkg_log}, {'x': float(x[-1]), 'y': bkg_log}] + + if use_analysis_range: + x_min, x_max = self._get_all_models_sample_range()[0:2] + if x_min == float('inf') or x_max == float('-inf'): + return [] + else: + exp_idx = self._project_lib.current_experiment_index + exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) + if exp_data.x is None or len(exp_data.x) == 0: + return [] + x_min, x_max = float(exp_data.x[0]), float(exp_data.x[-1]) + + param_value = getattr(model, param_attr).value + y_log = float(np.log10(param_value)) if param_value > 0 else default_log + return [{'x': float(x_min), 'y': y_log}, {'x': float(x_max), 'y': y_log}] except (IndexError, AttributeError, TypeError) as e: - console.debug(f'Error getting background data: {e}') + console.debug(f'Error getting {param_attr} reference line data: {e}') return [] @Slot(result='QVariantList') - def getScaleData(self) -> list: - """Return scale reference line data for plotting. + def getBackgroundData(self) -> list: + """Return background reference line data for the Experiment chart.""" + if not self._bkg_shown: + return [] + return self._get_reference_line_data('background', -10.0, use_analysis_range=False) - Returns a horizontal line at the model's scale value. - Note: Scale is a multiplicative factor, typically close to 1.0. - For reflectometry plots, the scale line at y=scale (log10) shows - where R=scale, i.e., where the reflectivity equals the scale factor. - Reference lines are always horizontal, even in R×q⁴ mode. - """ + @Slot(result='QVariantList') + def getScaleData(self) -> list: + """Return scale reference line data for the Experiment chart.""" if not self._scale_shown: return [] - try: - # Capture indices atomically to prevent race conditions - model_idx = self._project_lib.current_model_index - exp_idx = self._project_lib.current_experiment_index - model = self._project_lib.models[model_idx] - exp_data = self._project_lib.experimental_data_for_model_at_index(exp_idx) - if exp_data.x is None or len(exp_data.x) == 0: - return [] - x = exp_data.x - scale_value = model.scale.value - # For log scale plotting, convert scale value - scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 - # Reference lines are always horizontal (no R×q⁴ transformation) - return [{'x': float(x[0]), 'y': scale_log}, {'x': float(x[-1]), 'y': scale_log}] - except (IndexError, AttributeError, TypeError) as e: - console.debug(f'Error getting scale data: {e}') - return [] + return self._get_reference_line_data('scale', 0.0, use_analysis_range=False) @Slot(result='QVariantList') def getBackgroundDataForAnalysis(self) -> list: - """Return background reference line data for the Analysis chart. - - Uses the analysis/sample x-range (calculated model data range) instead of - experimental data range to ensure the line spans the full chart width. - Reference lines are always horizontal, even in R×q⁴ mode. - """ + """Return background reference line data for the Analysis chart (sample x-range).""" if not self._bkg_shown: return [] - try: - # Capture index atomically to prevent race conditions - model_idx = self._project_lib.current_model_index - model = self._project_lib.models[model_idx] - # Use sample/analysis x-range instead of experimental data - x_min, x_max = self._get_all_models_sample_range()[0:2] - if x_min == float('inf') or x_max == float('-inf'): - return [] - bkg_value = model.background.value - # For log scale plotting, convert background value - bkg_log = float(np.log10(bkg_value)) if bkg_value > 0 else -10.0 - # Reference lines are always horizontal (no R×q⁴ transformation) - return [{'x': float(x_min), 'y': bkg_log}, {'x': float(x_max), 'y': bkg_log}] - except (IndexError, AttributeError, TypeError) as e: - console.debug(f'Error getting background data for analysis: {e}') - return [] + return self._get_reference_line_data('background', -10.0, use_analysis_range=True) @Slot(result='QVariantList') def getScaleDataForAnalysis(self) -> list: - """Return scale reference line data for the Analysis chart. - - Uses the analysis/sample x-range (calculated model data range) instead of - experimental data range to ensure the line spans the full chart width. - Reference lines are always horizontal, even in R×q⁴ mode. - """ + """Return scale reference line data for the Analysis chart (sample x-range).""" if not self._scale_shown: return [] - try: - # Capture index atomically to prevent race conditions - model_idx = self._project_lib.current_model_index - model = self._project_lib.models[model_idx] - # Use sample/analysis x-range instead of experimental data - x_min, x_max = self._get_all_models_sample_range()[0:2] - if x_min == float('inf') or x_max == float('-inf'): - return [] - scale_value = model.scale.value - # For log scale plotting, convert scale value - scale_log = float(np.log10(scale_value)) if scale_value > 0 else 0.0 - # Reference lines are always horizontal (no R×q⁴ transformation) - return [{'x': float(x_min), 'y': scale_log}, {'x': float(x_max), 'y': scale_log}] - except (IndexError, AttributeError, TypeError) as e: - console.debug(f'Error getting scale data for analysis: {e}') - return [] + return self._get_reference_line_data('scale', 0.0, use_analysis_range=True) @property def sample_data(self) -> DataSet1D: @@ -355,11 +310,8 @@ def _get_all_models_sample_range(self): if data.y.size > 0: valid_mask = data.y > 0 valid_y = data.y[valid_mask] - valid_x = data.x[valid_mask] if valid_y.size > 0: - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - valid_y = valid_y * (valid_x**4) + valid_y = self._apply_rq4(data.x[valid_mask], valid_y) min_y = min(min_y, np.log10(valid_y.min())) max_y = max(max_y, np.log10(valid_y.max())) except (IndexError, ValueError): @@ -439,10 +391,7 @@ def experimentMaxY(self): data = self.experiment_data if data.y.size == 0: return 1.0 - y_values = data.y - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - y_values = y_values * (data.x**4) + y_values = self._apply_rq4(data.x, data.y) return np.log10(y_values.max()) @Property(float, notify=experimentChartRangesChanged) @@ -451,14 +400,12 @@ def experimentMinY(self): valid_y = data.y[data.y > 0] if data.y.size > 0 else np.array([1e-10]) if valid_y.size == 0: return -10.0 - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - valid_x = data.x[data.y > 0] if data.y.size > 0 else np.array([1.0]) - valid_y = valid_y * (valid_x**4) - # Filter again after transformation to avoid log of zero/negative - valid_y = valid_y[valid_y > 0] - if valid_y.size == 0: - return -10.0 + valid_x = data.x[data.y > 0] if data.y.size > 0 else np.array([1.0]) + valid_y = self._apply_rq4(valid_x, valid_y) + # Filter again after transformation to avoid log of zero/negative + valid_y = valid_y[valid_y > 0] + if valid_y.size == 0: + return -10.0 return np.log10(valid_y.min()) @Property('QVariant', notify=chartRefsChanged) @@ -506,9 +453,8 @@ def getSampleDataPointsForModel(self, model_index: int) -> list: for point in data.data_points(): x_val = float(point[0]) y_val = float(point[1]) - # Apply R×q⁴ transformation if enabled - if self._plot_rq4 and y_val > 0: - y_val = y_val * (x_val**4) + if y_val > 0: + y_val = self._apply_rq4(x_val, y_val) y_log = float(np.log10(y_val)) if y_val > 0 else -10.0 points.append({'x': x_val, 'y': y_log}) return points @@ -553,18 +499,10 @@ def getExperimentDataPoints(self, experiment_index: int) -> list: q = point[0] r = point[1] error_var = point[2] - # Apply R×q⁴ transformation if enabled - # Clamp error_lower before transformation to ensure positive values - error_lower_linear = max(r - np.sqrt(error_var), 1e-20) - if self._plot_rq4: - q4 = q**4 - r_val = r * q4 - error_upper = (r + np.sqrt(error_var)) * q4 - error_lower = error_lower_linear * q4 - else: - r_val = r - error_upper = r + np.sqrt(error_var) - error_lower = error_lower_linear + error_lower_linear = max(r - np.sqrt(error_var), 1e-10) + r_val = self._apply_rq4(q, r) + error_upper = self._apply_rq4(q, r + np.sqrt(error_var)) + error_lower = self._apply_rq4(q, error_lower_linear) points.append( { 'x': float(q), @@ -617,11 +555,8 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: q = point[0] r_meas = point[1] calc_y_val = calc_y[calc_idx] if calc_idx < len(calc_y) else r_meas - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - q4 = q**4 - r_meas = r_meas * q4 - calc_y_val = calc_y_val * q4 + r_meas = self._apply_rq4(q, r_meas) + calc_y_val = self._apply_rq4(q, calc_y_val) points.append( { 'x': float(q), @@ -715,18 +650,10 @@ def qtchartsReplaceMeasuredOnExperimentChartAndRedraw(self): q = point[0] r = point[1] error_var = point[2] - # Apply R×q⁴ transformation if enabled - # Clamp error_lower before transformation to ensure positive values - error_lower_linear = max(r - np.sqrt(error_var), 1e-20) - if self._plot_rq4: - q4 = q**4 - r_val = r * q4 - error_upper = (r + np.sqrt(error_var)) * q4 - error_lower = error_lower_linear * q4 - else: - r_val = r - error_upper = r + np.sqrt(error_var) - error_lower = error_lower_linear + error_lower_linear = max(r - np.sqrt(error_var), 1e-10) + r_val = self._apply_rq4(q, r) + error_upper = self._apply_rq4(q, r + np.sqrt(error_var)) + error_lower = self._apply_rq4(q, error_lower_linear) series_measured.append(q, np.log10(r_val)) series_error_upper.append(q, np.log10(error_upper)) series_error_lower.append(q, np.log10(error_lower)) @@ -781,20 +708,14 @@ def qtchartsReplaceCalculatedAndMeasuredOnAnalysisChartAndRedraw(self): for point in self.experiment_data.data_points(): if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: q = point[0] - r_meas = point[1] - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - r_meas = r_meas * (q**4) + r_meas = self._apply_rq4(q, point[1]) series_measured.append(q, np.log10(r_meas)) nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Measured curve', f'{nr_points} points', 'on analysis page', 'replaced')) for point in self.sample_data.data_points(): q = point[0] - r_calc = point[1] - # Apply R×q⁴ transformation if enabled - if self._plot_rq4: - r_calc = r_calc * (q**4) + r_calc = self._apply_rq4(q, point[1]) series_calculated.append(q, np.log10(r_calc)) nr_points = nr_points + 1 console.debug(IO.formatMsg('sub', 'Calculated curve', f'{nr_points} points', 'on analysis page', 'replaced')) diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index efd5da50..9d3d1e0a 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -177,11 +177,15 @@ def _relay_project_page_created(self): self._summary.summaryChanged.emit() def _relay_project_page_project_changed(self): + # Clear layers cache first so that subsequent signal handlers + # (e.g. ComboBox onModelChanged / onCurrentAssemblyNameChanged in + # MultiLayer.qml) read up-to-date layer data. + self._sample._clearCacheAndEmitLayersChanged() self._sample.materialsTableChanged.emit() self._sample.modelsTableChanged.emit() self._sample.modelsIndexChanged.emit() self._sample.assembliesTableChanged.emit() - self._sample._clearCacheAndEmitLayersChanged() + self._sample.assembliesIndexChanged.emit() self._experiment.experimentChanged.emit() self._analysis.experimentsChanged.emit() self._analysis._clearCacheAndEmitParametersChanged() diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index c0e5d5c7..5d038f6c 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -378,6 +378,25 @@ QtObject { } } + // Helper to update background/scale reference line series on any chart view. + // useAnalysisRange: true for Analysis charts (sample x-range), false for Experiment charts. + function updateRefLines(bkgSeries, scaleSeries, useAnalysisRange) { + bkgSeries.clear() + if (plottingBkgShown) { + var bkgData = useAnalysisRange ? plottingGetBackgroundDataForAnalysis() : plottingGetBackgroundData() + for (var i = 0; i < bkgData.length; i++) { + bkgSeries.append(bkgData[i].x, bkgData[i].y) + } + } + scaleSeries.clear() + if (plottingScaleShown) { + var scaleData = useAnalysisRange ? plottingGetScaleDataForAnalysis() : plottingGetScaleData() + for (var j = 0; j < scaleData.length; j++) { + scaleSeries.append(scaleData[j].x, scaleData[j].y) + } + } + } + function plottingSetQtChartsSerieRef(value1, value2, value3) { activeBackend.plotting.setQtChartsSerieRef(value1, value2, value3) } function plottingRefreshSample() { activeBackend.plotting.drawCalculatedOnSampleChart() } function plottingRefreshSLD() { activeBackend.plotting.drawCalculatedOnSldChart() } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index 13a686dd..66130a66 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -65,23 +65,7 @@ Rectangle { } function updateReferenceLines() { - // Update background line (use analysis-specific method for correct x-range) - backgroundRefLine.clear() - if (Globals.BackendWrapper.plottingBkgShown) { - var bkgData = Globals.BackendWrapper.plottingGetBackgroundDataForAnalysis() - for (var i = 0; i < bkgData.length; i++) { - backgroundRefLine.append(bkgData[i].x, bkgData[i].y) - } - } - - // Update scale line (use analysis-specific method for correct x-range) - scaleRefLine.clear() - if (Globals.BackendWrapper.plottingScaleShown) { - var scaleData = Globals.BackendWrapper.plottingGetScaleDataForAnalysis() - for (var j = 0; j < scaleData.length; j++) { - scaleRefLine.append(scaleData[j].x, scaleData[j].y) - } - } + Globals.BackendWrapper.updateRefLines(backgroundRefLine, scaleRefLine, true) } // Multi-experiment support diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 580d8368..0ad16c6b 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -126,23 +126,7 @@ Rectangle { } function updateReferenceLines() { - // Update background line (use analysis-specific method for correct x-range) - backgroundRefLine.clear() - if (Globals.BackendWrapper.plottingBkgShown) { - var bkgData = Globals.BackendWrapper.plottingGetBackgroundDataForAnalysis() - for (var i = 0; i < bkgData.length; i++) { - backgroundRefLine.append(bkgData[i].x, bkgData[i].y) - } - } - - // Update scale line (use analysis-specific method for correct x-range) - scaleRefLine.clear() - if (Globals.BackendWrapper.plottingScaleShown) { - var scaleData = Globals.BackendWrapper.plottingGetScaleDataForAnalysis() - for (var j = 0; j < scaleData.length; j++) { - scaleRefLine.append(scaleData[j].x, scaleData[j].y) - } - } + Globals.BackendWrapper.updateRefLines(backgroundRefLine, scaleRefLine, true) } // Multi-experiment series management diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml index 0b2e0ce2..34de6956 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Advanced/Groups/PlotControl.qml @@ -3,48 +3,16 @@ // © 2025 Contributors to the EasyReflectometry project import QtQuick -import QtQuick.Controls import EasyApp.Gui.Style as EaStyle import EasyApp.Gui.Elements as EaElements -import Gui.Globals as Globals +import Gui as Gui EaElements.GroupBox { title: qsTr("Plot control") collapsed: true - Column { - spacing: EaStyle.Sizes.fontPixelSize * 0.5 - - EaElements.CheckBox { - topPadding: 0 - checked: Globals.BackendWrapper.plottingPlotRQ4 - text: qsTr("Plot R(q)×q⁴") - ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") - onToggled: { - Globals.BackendWrapper.plottingTogglePlotRQ4() - } - } - - EaElements.CheckBox { - topPadding: 0 - checked: Globals.BackendWrapper.plottingScaleShown - text: qsTr("Show scale line") - ToolTip.text: qsTr("Checking this box will show the scale reference line on the plot") - onToggled: { - Globals.BackendWrapper.plottingFlipScaleShown() - } - } - - EaElements.CheckBox { - topPadding: 0 - checked: Globals.BackendWrapper.plottingBkgShown - text: qsTr("Show background line") - ToolTip.text: qsTr("Checking this box will show the background reference line on the plot") - onToggled: { - Globals.BackendWrapper.plottingFlipBkgShown() - } - } - } + Gui.PlotControlRefLines {} } + diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index 3bd40092..18513ac7 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -66,23 +66,7 @@ Rectangle { } function updateReferenceLines() { - // Update background line - backgroundRefLine.clear() - if (Globals.BackendWrapper.plottingBkgShown) { - var bkgData = Globals.BackendWrapper.plottingGetBackgroundData() - for (var i = 0; i < bkgData.length; i++) { - backgroundRefLine.append(bkgData[i].x, bkgData[i].y) - } - } - - // Update scale line - scaleRefLine.clear() - if (Globals.BackendWrapper.plottingScaleShown) { - var scaleData = Globals.BackendWrapper.plottingGetScaleData() - for (var j = 0; j < scaleData.length; j++) { - scaleRefLine.append(scaleData[j].x, scaleData[j].y) - } - } + Globals.BackendWrapper.updateRefLines(backgroundRefLine, scaleRefLine, false) } // Multi-experiment support diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml index 0b2e0ce2..34de6956 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml @@ -3,48 +3,16 @@ // © 2025 Contributors to the EasyReflectometry project import QtQuick -import QtQuick.Controls import EasyApp.Gui.Style as EaStyle import EasyApp.Gui.Elements as EaElements -import Gui.Globals as Globals +import Gui as Gui EaElements.GroupBox { title: qsTr("Plot control") collapsed: true - Column { - spacing: EaStyle.Sizes.fontPixelSize * 0.5 - - EaElements.CheckBox { - topPadding: 0 - checked: Globals.BackendWrapper.plottingPlotRQ4 - text: qsTr("Plot R(q)×q⁴") - ToolTip.text: qsTr("Checking this box will plot R(q) multiplied by q⁴") - onToggled: { - Globals.BackendWrapper.plottingTogglePlotRQ4() - } - } - - EaElements.CheckBox { - topPadding: 0 - checked: Globals.BackendWrapper.plottingScaleShown - text: qsTr("Show scale line") - ToolTip.text: qsTr("Checking this box will show the scale reference line on the plot") - onToggled: { - Globals.BackendWrapper.plottingFlipScaleShown() - } - } - - EaElements.CheckBox { - topPadding: 0 - checked: Globals.BackendWrapper.plottingBkgShown - text: qsTr("Show background line") - ToolTip.text: qsTr("Checking this box will show the background reference line on the plot") - onToggled: { - Globals.BackendWrapper.plottingFlipBkgShown() - } - } - } + Gui.PlotControlRefLines {} } + diff --git a/EasyReflectometryApp/Gui/qmldir b/EasyReflectometryApp/Gui/qmldir index d3151d58..9145745f 100644 --- a/EasyReflectometryApp/Gui/qmldir +++ b/EasyReflectometryApp/Gui/qmldir @@ -1,4 +1,5 @@ module Gui ApplicationWindow ApplicationWindow.qml +PlotControlRefLines PlotControlRefLines.qml StatusBar StatusBar.qml diff --git a/pyproject.toml b/pyproject.toml index 58c688b2..22977614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ requires-python = '>=3.11' dependencies = [ 'EasyApp @ git+https://github.com/EasyScience/EasyApp.git@develop', - 'easyreflectometry @ git+https://github.com/EasyScience/EasyReflectometryLib.git@append_sample', + 'easyreflectometry @ git+https://github.com/EasyScience/EasyReflectometryLib.git@develop', 'asteval', 'PySide6', 'toml',