From 341da3ceb13a35a42f9a0a3c603a473f1100bb8b Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 26 Jan 2026 14:47:19 +0100 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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', From d3e166a5145cf6453441d4e6bbf2a06c7e1b920a Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 17 Feb 2026 18:10:26 +0100 Subject: [PATCH 13/23] re-enable plot control widgets on Experiment and Analysis tabs --- .../Sidebar/Advanced/Groups/PlotControl.qml | 47 +++++++++++++++- .../Gui/PlotControlRefLines.qml | 55 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 EasyReflectometryApp/Gui/PlotControlRefLines.qml diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml index 34de6956..ccf27b4d 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/Sidebar/Advanced/Groups/PlotControl.qml @@ -3,16 +3,59 @@ // © 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 as Gui +import Gui.Globals as Globals EaElements.GroupBox { title: qsTr("Plot control") collapsed: true - Gui.PlotControlRefLines {} + 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.logarithmicQAxis + text: qsTr("Logarithmic q-axis") + ToolTip.text: qsTr("Checking this box will make the q-axis logarithmic") + onToggled: { + Globals.Variables.logarithmicQAxis = checked + } + } + + 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/PlotControlRefLines.qml b/EasyReflectometryApp/Gui/PlotControlRefLines.qml new file mode 100644 index 00000000..e1ee3eeb --- /dev/null +++ b/EasyReflectometryApp/Gui/PlotControlRefLines.qml @@ -0,0 +1,55 @@ +// 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 + +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.logarithmicQAxis + text: qsTr("Logarithmic q-axis") + ToolTip.text: qsTr("Checking this box will make the q-axis logarithmic") + onToggled: { + Globals.Variables.logarithmicQAxis = checked + } + } + + 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() + } + } +} From 250ff4ae4d96aa11ffa96c0925a177eb5ac9913c Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 23 Feb 2026 14:07:39 +0100 Subject: [PATCH 14/23] properly reset axes in the sample charts on any param change --- EasyReflectometryApp/Backends/Py/py_backend.py | 1 + .../Gui/Pages/Sample/MainContent/SldView.qml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index 9d3d1e0a..b6778b22 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -200,6 +200,7 @@ def _relay_sample_page_sample_changed(self): self._analysis._clearCacheAndEmitParametersChanged() self._status.statusChanged.emit() self._summary.summaryChanged.emit() + self._plotting_1d.samplePageResetAxes.emit() def _relay_experiment_page_experiment_changed(self): self._analysis.experimentsChanged.emit() diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml index 9c034fb1..f6801a4a 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml @@ -320,6 +320,15 @@ Rectangle { function onSamplePageResetAxes() { sldResetAxesTimer.start() } + function onPlotModeChanged() { + refreshAllCharts() + // Delay resetAxes to allow axis range properties to update first + sldResetAxesTimer.start() + } + function onChartAxesResetRequested() { + // Reset axes when model is loaded (e.g., from ORSO file) + sldResetAxesTimer.start() + } } Timer { From dc5c6d723897016ce74aeacecf383145f08560e6 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 23 Feb 2026 14:14:36 +0100 Subject: [PATCH 15/23] reset axes for analysis SLD/Refl plots on parameter change. --- .../Backends/Py/py_backend.py | 1 + .../Analysis/MainContent/AnalysisView.qml | 3 +++ .../Analysis/MainContent/CombinedView.qml | 8 +++++++- .../Pages/Analysis/MainContent/SldView.qml | 20 +++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index b6778b22..964d57e8 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -213,6 +213,7 @@ def _relay_analysis_page(self): self._status.statusChanged.emit() self._experiment.experimentChanged.emit() self._summary.summaryChanged.emit() + self._plotting_1d.samplePageResetAxes.emit() def _refresh_plots(self): self._plotting_1d.sampleChartRangesChanged.emit() diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index 66130a66..29de419a 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -107,6 +107,9 @@ Rectangle { // Reset axes when model is loaded (e.g., from ORSO file) analysisResetAxesTimer.start() } + function onSamplePageResetAxes() { + analysisResetAxesTimer.start() + } } Timer { diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 0ad16c6b..ec4350de 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -83,13 +83,19 @@ Rectangle { // Reset axes when model is loaded (e.g., from ORSO file) combinedAnalysisResetAxesTimer.start() } + function onSamplePageResetAxes() { + combinedAnalysisResetAxesTimer.start() + } } Timer { id: combinedAnalysisResetAxesTimer interval: 50 repeat: false - onTriggered: analysisChartView.resetAxes() + onTriggered: { + analysisChartView.resetAxes() + sldChartView.resetAxes() + } } // Background reference line series diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml index d55ca44a..3143f2f9 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml @@ -164,6 +164,26 @@ Rectangle { chartView.calcSerie) Globals.BackendWrapper.plottingRefreshSLD() } + + Connections { + target: Globals.BackendWrapper + function onSamplePageResetAxes() { + analysisSldResetAxesTimer.start() + } + function onPlotModeChanged() { + analysisSldResetAxesTimer.start() + } + function onChartAxesResetRequested() { + analysisSldResetAxesTimer.start() + } + } + + Timer { + id: analysisSldResetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() + } } // Logic From 6a653f1efe998ceb63740b45232fdb2f9d7b8579 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 23 Feb 2026 15:12:24 +0100 Subject: [PATCH 16/23] update the status line with correct values --- EasyReflectometryApp/Backends/Py/logic/fitting.py | 9 +++++++++ EasyReflectometryApp/Backends/Py/logic/minimizers.py | 6 ++++++ EasyReflectometryApp/Backends/Py/logic/status.py | 9 +++++++++ EasyReflectometryApp/Backends/Py/py_backend.py | 3 +++ 4 files changed, 27 insertions(+) diff --git a/EasyReflectometryApp/Backends/Py/logic/fitting.py b/EasyReflectometryApp/Backends/Py/logic/fitting.py index 1e71955f..7c400c62 100644 --- a/EasyReflectometryApp/Backends/Py/logic/fitting.py +++ b/EasyReflectometryApp/Backends/Py/logic/fitting.py @@ -129,6 +129,15 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: models = [experiments[idx].model for idx in experiment_indices] multi_fitter = MultiFitter(*models) + # Apply the user-selected minimizer to the new fitter + selected_minimizer = minimizers_logic.selected_minimizer_enum() + if selected_minimizer is not None: + multi_fitter.easy_science_multi_fitter.switch_minimizer(selected_minimizer) + if minimizers_logic.tolerance is not None: + multi_fitter.easy_science_multi_fitter.tolerance = minimizers_logic.tolerance + if minimizers_logic.max_iterations is not None: + multi_fitter.easy_science_multi_fitter.max_evaluations = minimizers_logic.max_iterations + # Prepare data arrays for all experiments x_data = [experiments[idx].x for idx in experiment_indices] y_data = [experiments[idx].y for idx in experiment_indices] diff --git a/EasyReflectometryApp/Backends/Py/logic/minimizers.py b/EasyReflectometryApp/Backends/Py/logic/minimizers.py index b6352b56..ef262cce 100644 --- a/EasyReflectometryApp/Backends/Py/logic/minimizers.py +++ b/EasyReflectometryApp/Backends/Py/logic/minimizers.py @@ -26,6 +26,12 @@ def minimizers_available(self) -> list[str]: def minimizer_current_index(self) -> int: return self._minimizer_current_index + def selected_minimizer_enum(self): + """Return the AvailableMinimizers enum for the currently selected minimizer.""" + if 0 <= self._minimizer_current_index < len(self._list_available_minimizers): + return self._list_available_minimizers[self._minimizer_current_index] + return None + def set_minimizer_current_index(self, new_value: int) -> bool: if new_value != self._minimizer_current_index: self._minimizer_current_index = new_value diff --git a/EasyReflectometryApp/Backends/Py/logic/status.py b/EasyReflectometryApp/Backends/Py/logic/status.py index 87ac11ce..89fc2fb7 100644 --- a/EasyReflectometryApp/Backends/Py/logic/status.py +++ b/EasyReflectometryApp/Backends/Py/logic/status.py @@ -4,6 +4,10 @@ class Status: def __init__(self, project_lib: ProjectLib): self._project_lib = project_lib + self._minimizers_logic = None + + def set_minimizers_logic(self, minimizers_logic): + self._minimizers_logic = minimizers_logic @property def project(self): @@ -11,6 +15,11 @@ def project(self): @property def minimizer(self): + if self._minimizers_logic is not None: + available = self._minimizers_logic.minimizers_available() + idx = self._minimizers_logic.minimizer_current_index() + if 0 <= idx < len(available): + return available[idx] return self._project_lib.minimizer.name @property diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index 964d57e8..51fb6de0 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -38,6 +38,9 @@ def __init__(self, parent=None): self._logger = LoggerLevelHandler(self) + # Wire cross-cutting references before connecting signals + self._status._status_logic.set_minimizers_logic(self._analysis._minimizers_logic) + # Must be last to ensure all backend parts are created self._connect_backend_parts() From aa9128e76af518f680b526e81195747e1acac581 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 24 Feb 2026 11:13:33 +0100 Subject: [PATCH 17/23] sample profile on Analysis should include resolution --- EasyReflectometryApp/Backends/Py/analysis.py | 2 +- .../Backends/Py/logic/fitting.py | 3 ++- .../Backends/Py/plotting_1d.py | 24 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index 3f9dacad..ebfa9ed6 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -11,9 +11,9 @@ from .logic.calculators import Calculators as CalculatorsLogic from .logic.experiments import Experiments as ExperimentLogic from .logic.fitting import Fitting as FittingLogic +from .logic.helpers import get_original_name 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 diff --git a/EasyReflectometryApp/Backends/Py/logic/fitting.py b/EasyReflectometryApp/Backends/Py/logic/fitting.py index 7c400c62..a01073c0 100644 --- a/EasyReflectometryApp/Backends/Py/logic/fitting.py +++ b/EasyReflectometryApp/Backends/Py/logic/fitting.py @@ -155,7 +155,8 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: self._show_results_dialog = True return None, None, None, None, None - weights = [1.0 / experiments[idx].ye for idx in experiment_indices] + # ye contains variances (sigma²); weights = 1/sigma = 1/sqrt(variance) + weights = [1.0 / np.sqrt(experiments[idx].ye) for idx in experiment_indices] # Method is optional in fit() - pass None to use minimizer's default method = None diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index 46de877f..a1ed1e62 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -33,6 +33,7 @@ def __init__(self, project_lib: ProjectLib, parent=None): self._proxy = parent self._currentLib1d = 'QtCharts' self._sample_data = {} + self._model_data = {} self._sld_data = {} # Plot mode state @@ -62,6 +63,7 @@ def __init__(self, project_lib: ProjectLib, parent=None): def reset_data(self): self._sample_data = {} + self._model_data = {} self._sld_data = {} console.debug(IO.formatMsg('sub', 'Sample and SLD data cleared')) @@ -221,6 +223,22 @@ def sample_data(self) -> DataSet1D: self._sample_data[idx] = data return data + @property + def model_data(self) -> DataSet1D: + idx = self._project_lib.current_model_index + if idx in self._model_data and self._model_data[idx] is not None: + return self._model_data[idx] + try: + data = self._project_lib.model_data_for_model_at_index(idx) + except IndexError: + data = DataSet1D( + name='Model Data empty', + x=np.empty(0), + y=np.empty(0), + ) + self._model_data[idx] = data + return data + @property def sld_data(self) -> DataSet1D: idx = self._project_lib.current_model_index @@ -543,7 +561,7 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: q_filtered = q_values[mask] # Get calculated model data at the same q points using the correct model index - calc_data = self._project_lib.sample_data_for_model_at_index(model_index, q_filtered) + calc_data = self._project_lib.model_data_for_model_at_index(model_index, q_filtered) points = [] exp_points = list(exp_data.data_points()) @@ -573,6 +591,7 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: def refreshSamplePage(self): # Clear cached data so it gets recalculated self._sample_data = {} + self._model_data = {} self._sld_data = {} # Emit signals to update ranges and trigger QML refresh self.sampleChartRangesChanged.emit() @@ -583,6 +602,7 @@ def refreshExperimentPage(self): self.drawMeasuredOnExperimentChart() def refreshAnalysisPage(self): + self._model_data = {} self.drawCalculatedAndMeasuredOnAnalysisChart() def refreshExperimentRanges(self): @@ -713,7 +733,7 @@ def qtchartsReplaceCalculatedAndMeasuredOnAnalysisChartAndRedraw(self): 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(): + for point in self.model_data.data_points(): q = point[0] r_calc = self._apply_rq4(q, point[1]) series_calculated.append(q, np.log10(r_calc)) From 4c38807ed5d17f209f43b47116fa88e489636934 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 24 Feb 2026 14:19:12 +0100 Subject: [PATCH 18/23] refactor SldView so Sample and Analysis get the same display --- .../Analysis/MainContent/CombinedView.qml | 92 +---- .../Pages/Analysis/MainContent/SldView.qml | 195 +-------- .../Pages/Sample/MainContent/CombinedView.qml | 260 +----------- .../Gui/Pages/Sample/MainContent/SldView.qml | 387 +---------------- EasyReflectometryApp/Gui/SldChart.qml | 388 ++++++++++++++++++ EasyReflectometryApp/Gui/qmldir | 1 + 6 files changed, 429 insertions(+), 894 deletions(-) create mode 100644 EasyReflectometryApp/Gui/SldChart.qml diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index ec4350de..431e856b 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -12,6 +12,7 @@ import EasyApp.Gui.Globals as EaGlobals import EasyApp.Gui.Elements as EaElements import EasyApp.Gui.Charts as EaCharts +import Gui as Gui import Gui.Globals as Globals @@ -94,7 +95,7 @@ Rectangle { repeat: false onTriggered: { analysisChartView.resetAxes() - sldChartView.resetAxes() + sldChart.chartView.resetAxes() } } @@ -312,7 +313,7 @@ Rectangle { ToolTip.text: qsTr("Enable pan") onClicked: { analysisChartView.allowZoom = !analysisChartView.allowZoom - sldChartView.allowZoom = analysisChartView.allowZoom + sldChart.chartView.allowZoom = analysisChartView.allowZoom } } @@ -326,7 +327,7 @@ Rectangle { ToolTip.text: qsTr("Enable box zoom") onClicked: { analysisChartView.allowZoom = !analysisChartView.allowZoom - sldChartView.allowZoom = analysisChartView.allowZoom + sldChart.chartView.allowZoom = analysisChartView.allowZoom } } @@ -339,7 +340,7 @@ Rectangle { ToolTip.text: qsTr("Reset axes") onClicked: { analysisChartView.resetAxes() - sldChartView.resetAxes() + sldChart.chartView.resetAxes() } } } @@ -455,87 +456,18 @@ Rectangle { } // SLD Chart (1/3 height) - Rectangle { - id: sldContainer + Gui.SldChart { + id: sldChart + SplitView.fillHeight: true SplitView.preferredHeight: parent.height * 0.33 SplitView.minimumHeight: 80 - color: EaStyle.Colors.chartBackground - - EaCharts.QtCharts1dMeasVsCalc { - id: sldChartView - - anchors.fill: parent - anchors.topMargin: EaStyle.Sizes.toolButtonHeight - EaStyle.Sizes.fontPixelSize - 1 - - useOpenGL: EaGlobals.Vars.useOpenGL - - property double xRange: Globals.BackendWrapper.plottingSldMaxX - Globals.BackendWrapper.plottingSldMinX - axisX.title: "z (Å)" - axisX.min: Globals.BackendWrapper.plottingSldMinX - xRange * 0.01 - axisX.max: Globals.BackendWrapper.plottingSldMaxX + xRange * 0.01 - axisX.minAfterReset: Globals.BackendWrapper.plottingSldMinX - xRange * 0.01 - axisX.maxAfterReset: Globals.BackendWrapper.plottingSldMaxX + xRange * 0.01 - - property double yRange: Globals.BackendWrapper.plottingSldMaxY - Globals.BackendWrapper.plottingSldMinY - axisY.title: "SLD (10⁻⁶Å⁻²)" - axisY.min: Globals.BackendWrapper.plottingSldMinY - yRange * 0.01 - axisY.max: Globals.BackendWrapper.plottingSldMaxY + yRange * 0.01 - axisY.minAfterReset: Globals.BackendWrapper.plottingSldMinY - yRange * 0.01 - axisY.maxAfterReset: Globals.BackendWrapper.plottingSldMaxY + yRange * 0.01 - - calcSerie.onHovered: (point, state) => showMainTooltip(sldChartView, sldDataToolTip, point, state) - calcSerie.color: { - const models = Globals.BackendWrapper.sampleModels - const idx = Globals.BackendWrapper.sampleCurrentModelIndex - if (models && idx >= 0 && idx < models.length) { - return models[idx].color - } - - return undefined - } + showLegend: Globals.Variables.showLegendOnAnalysisPage + onShowLegendChanged: Globals.Variables.showLegendOnAnalysisPage = showLegend - // Legend - Rectangle { - visible: Globals.Variables.showLegendOnAnalysisPage - - x: sldChartView.plotArea.x + sldChartView.plotArea.width - width - EaStyle.Sizes.fontPixelSize - y: sldChartView.plotArea.y + EaStyle.Sizes.fontPixelSize - width: childrenRect.width - height: childrenRect.height - - color: EaStyle.Colors.mainContentBackgroundHalfTransparent - border.color: EaStyle.Colors.chartGridLine - - Column { - leftPadding: EaStyle.Sizes.fontPixelSize - rightPadding: EaStyle.Sizes.fontPixelSize - topPadding: EaStyle.Sizes.fontPixelSize * 0.5 - bottomPadding: EaStyle.Sizes.fontPixelSize * 0.5 - - EaElements.Label { - text: '━ SLD' - color: sldChartView.calcSerie.color - } - } - } - - EaElements.ToolTip { - id: sldDataToolTip - - arrowLength: 0 - textFormat: Text.RichText - } - - // Data is set in python backend (plotting_1d.py) - Component.onCompleted: { - Globals.References.pages.analysis.mainContent.sldView = sldChartView - Globals.BackendWrapper.plottingSetQtChartsSerieRef('analysisPage', - 'sldSerie', - sldChartView.calcSerie) - Globals.BackendWrapper.plottingRefreshSLD() - } + Component.onCompleted: { + Globals.References.pages.analysis.mainContent.sldView = sldChart.chartView } } } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml index 3143f2f9..31399d53 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/SldView.qml @@ -3,201 +3,20 @@ // © 2025 Contributors to the EasyReflectometry project import QtQuick -import QtQuick.Controls -import QtCharts - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Charts as EaCharts +import Gui as Gui import Gui.Globals as Globals -Rectangle { - id: container - - color: EaStyle.Colors.chartBackground - - EaCharts.QtCharts1dMeasVsCalc { - id: chartView - - anchors.topMargin: EaStyle.Sizes.toolButtonHeight - EaStyle.Sizes.fontPixelSize - 1 - - useOpenGL: EaGlobals.Vars.useOpenGL - - property double xRange: Globals.BackendWrapper.plottingSldMaxX - Globals.BackendWrapper.plottingSldMinX - axisX.title: "z (Å)" - axisX.min: Globals.BackendWrapper.plottingSldMinX - xRange * 0.01 - axisX.max: Globals.BackendWrapper.plottingSldMaxX + xRange * 0.01 - axisX.minAfterReset: Globals.BackendWrapper.plottingSldMinX - xRange * 0.01 - axisX.maxAfterReset: Globals.BackendWrapper.plottingSldMaxX + xRange * 0.01 - - property double yRange: Globals.BackendWrapper.plottingSldMaxY - Globals.BackendWrapper.plottingSldMinY - axisY.title: "SLD (10⁻⁶Å⁻²)" - axisY.min: Globals.BackendWrapper.plottingSldMinY - yRange * 0.01 - axisY.max: Globals.BackendWrapper.plottingSldMaxY + yRange * 0.01 - axisY.minAfterReset: Globals.BackendWrapper.plottingSldMinY - yRange * 0.01 - axisY.maxAfterReset: Globals.BackendWrapper.plottingSldMaxY + yRange * 0.01 - - calcSerie.onHovered: (point, state) => showMainTooltip(chartView, point, state) - calcSerie.color: { - const models = Globals.BackendWrapper.sampleModels - const idx = Globals.BackendWrapper.sampleCurrentModelIndex - - if (models && idx >= 0 && idx < models.length) { - return models[idx].color - } - - return undefined - } - - - // Tool buttons - Row { - id: toolButtons - - x: chartView.plotArea.x + chartView.plotArea.width - width - y: chartView.plotArea.y - height - EaStyle.Sizes.fontPixelSize - - spacing: 0.25 * EaStyle.Sizes.fontPixelSize - - EaElements.TabButton { - checked: Globals.Variables.showLegendOnSamplePage - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "align-left" - ToolTip.text: Globals.Variables.showLegendOnSamplePage ? - qsTr("Hide legend") : - qsTr("Show legend") - onClicked: Globals.Variables.showLegendOnSamplePage = checked - } - - EaElements.TabButton { - checked: chartView.allowHover - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "comment-alt" - ToolTip.text: qsTr("Show coordinates tooltip on hover") - onClicked: chartView.allowHover = !chartView.allowHover - } +Gui.SldChart { + id: sldChart - Item { height: 1; width: 0.5 * EaStyle.Sizes.fontPixelSize } // spacer - - EaElements.TabButton { - checked: !chartView.allowZoom - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "arrows-alt" - ToolTip.text: qsTr("Enable pan") - onClicked: chartView.allowZoom = !chartView.allowZoom - } - - EaElements.TabButton { - checked: chartView.allowZoom - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "expand" - ToolTip.text: qsTr("Enable box zoom") - onClicked: chartView.allowZoom = !chartView.allowZoom - } - - EaElements.TabButton { - checkable: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "backspace" - ToolTip.text: qsTr("Reset axes") - onClicked: chartView.resetAxes() - } - - } - // Tool buttons - - // Legend - Rectangle { - visible: Globals.Variables.showLegendOnExperimentPage - - x: chartView.plotArea.x + chartView.plotArea.width - width - EaStyle.Sizes.fontPixelSize - y: chartView.plotArea.y + EaStyle.Sizes.fontPixelSize - width: childrenRect.width - height: childrenRect.height - - color: EaStyle.Colors.mainContentBackgroundHalfTransparent - border.color: EaStyle.Colors.chartGridLine - - Column { - leftPadding: EaStyle.Sizes.fontPixelSize - rightPadding: EaStyle.Sizes.fontPixelSize - topPadding: EaStyle.Sizes.fontPixelSize * 0.5 - bottomPadding: EaStyle.Sizes.fontPixelSize * 0.5 - - EaElements.Label { - text: '━ SLD' - color: chartView.calcSerie.color - } - } - } - // Legend - - EaElements.ToolTip { - id: dataToolTip - - arrowLength: 0 - textFormat: Text.RichText - } - - // Data is set in python backend (plotting_1d.py) - Component.onCompleted: { - Globals.References.pages.analysis.mainContent.sldView = chartView - Globals.BackendWrapper.plottingSetQtChartsSerieRef('analysisPage', - 'sldSerie', - chartView.calcSerie) - Globals.BackendWrapper.plottingRefreshSLD() - } - - Connections { - target: Globals.BackendWrapper - function onSamplePageResetAxes() { - analysisSldResetAxesTimer.start() - } - function onPlotModeChanged() { - analysisSldResetAxesTimer.start() - } - function onChartAxesResetRequested() { - analysisSldResetAxesTimer.start() - } - } - - Timer { - id: analysisSldResetAxesTimer - interval: 50 - repeat: false - onTriggered: chartView.resetAxes() - } - } + showLegend: Globals.Variables.showLegendOnAnalysisPage - // Logic + onShowLegendChanged: Globals.Variables.showLegendOnAnalysisPage = showLegend - function showMainTooltip(chart, point, state) { - if (!chartView.allowHover) { - return - } - const pos = chart.mapToPosition(Qt.point(point.x, point.y)) - dataToolTip.x = pos.x - dataToolTip.y = pos.y - dataToolTip.text = `

x: ${point.x.toFixed(3)}y: ${point.y.toFixed(3)}

` - dataToolTip.parent = chart - dataToolTip.visible = state + Component.onCompleted: { + Globals.References.pages.analysis.mainContent.sldView = sldChart.chartView } } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index c86713f8..41104387 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -12,6 +12,7 @@ import EasyApp.Gui.Globals as EaGlobals import EasyApp.Gui.Elements as EaElements import EasyApp.Gui.Charts as EaCharts +import Gui as Gui import Gui.Globals as Globals @@ -25,7 +26,6 @@ Rectangle { // Store dynamically created series property var sampleSeries: [] - property var sldSeries: [] SplitView { anchors.fill: parent @@ -180,7 +180,7 @@ Rectangle { ToolTip.text: qsTr("Enable pan") onClicked: { sampleChartView.allowZoom = !checked - sldChartView.allowZoom = !checked + sldChart.chartView.allowZoom = !checked } } @@ -194,7 +194,7 @@ Rectangle { ToolTip.text: qsTr("Enable box zoom") onClicked: { sampleChartView.allowZoom = checked - sldChartView.allowZoom = checked + sldChart.chartView.allowZoom = checked } } @@ -207,7 +207,7 @@ Rectangle { ToolTip.text: qsTr("Reset axes") onClicked: { sampleChartView.resetAxes() - sldChartView.resetAxes() + sldChart.chartView.resetAxes() } } } @@ -360,222 +360,19 @@ Rectangle { } // SLD Chart (1/3 height) - Rectangle { - id: sldContainer + Gui.SldChart { + id: sldChart + SplitView.fillHeight: true SplitView.preferredHeight: parent.height * 0.33 SplitView.minimumHeight: 80 - color: EaStyle.Colors.chartBackground - - ChartView { - id: sldChartView - - anchors.fill: parent - anchors.topMargin: EaStyle.Sizes.toolButtonHeight - EaStyle.Sizes.fontPixelSize - 1 - anchors.margins: -12 - - antialiasing: true - legend.visible: false - backgroundRoundness: 0 - backgroundColor: EaStyle.Colors.chartBackground - plotAreaColor: EaStyle.Colors.chartPlotAreaBackground - - property bool allowZoom: true - property bool allowHover: true - - property double xRange: Globals.BackendWrapper.plottingSldMaxX - Globals.BackendWrapper.plottingSldMinX - - // Reverse axis logic - property bool reverseZAxis: Globals.Variables.reverseSldZAxis - - ValueAxis { - id: sldAxisX - titleText: "z (Å)" - // min/max set imperatively to avoid binding reset during zoom - property double minAfterReset: Globals.BackendWrapper.plottingSldMinX - sldChartView.xRange * 0.01 - property double maxAfterReset: Globals.BackendWrapper.plottingSldMaxX + sldChartView.xRange * 0.01 - reverse: sldChartView.reverseZAxis - color: EaStyle.Colors.chartAxis - gridLineColor: EaStyle.Colors.chartGridLine - minorGridLineColor: EaStyle.Colors.chartMinorGridLine - labelsColor: EaStyle.Colors.chartLabels - titleBrush: EaStyle.Colors.chartLabels - Component.onCompleted: { - min = minAfterReset - max = maxAfterReset - } - } - - property double yRange: Globals.BackendWrapper.plottingSldMaxY - Globals.BackendWrapper.plottingSldMinY - - ValueAxis { - id: sldAxisY - titleText: "SLD (10⁻⁶Å⁻²)" - // min/max set imperatively to avoid binding reset during zoom - property double minAfterReset: Globals.BackendWrapper.plottingSldMinY - sldChartView.yRange * 0.01 - property double maxAfterReset: Globals.BackendWrapper.plottingSldMaxY + sldChartView.yRange * 0.01 - color: EaStyle.Colors.chartAxis - gridLineColor: EaStyle.Colors.chartGridLine - minorGridLineColor: EaStyle.Colors.chartMinorGridLine - labelsColor: EaStyle.Colors.chartLabels - titleBrush: EaStyle.Colors.chartLabels - Component.onCompleted: { - min = minAfterReset - max = maxAfterReset - } - } - - function resetAxes() { - sldAxisX.min = sldAxisX.minAfterReset - sldAxisX.max = sldAxisX.maxAfterReset - sldAxisY.min = sldAxisY.minAfterReset - sldAxisY.max = sldAxisY.maxAfterReset - } - - // Legend showing all models - Rectangle { - visible: Globals.Variables.showLegendOnSamplePage - - x: sldChartView.plotArea.x + sldChartView.plotArea.width - width - EaStyle.Sizes.fontPixelSize - y: sldChartView.plotArea.y + EaStyle.Sizes.fontPixelSize - width: childrenRect.width - height: childrenRect.height - - color: EaStyle.Colors.mainContentBackgroundHalfTransparent - border.color: EaStyle.Colors.chartGridLine - - Column { - leftPadding: EaStyle.Sizes.fontPixelSize - rightPadding: EaStyle.Sizes.fontPixelSize - topPadding: EaStyle.Sizes.fontPixelSize * 0.5 - bottomPadding: EaStyle.Sizes.fontPixelSize * 0.5 - Repeater { - model: container.modelCount - EaElements.Label { - text: '━ SLD ' + Globals.BackendWrapper.sampleModels[index].label - color: Globals.BackendWrapper.sampleModels[index].color - } - } - } - } + showLegend: Globals.Variables.showLegendOnSamplePage + reverseZAxis: Globals.Variables.reverseSldZAxis + onShowLegendChanged: Globals.Variables.showLegendOnSamplePage = showLegend - EaElements.ToolTip { - id: sldDataToolTip - - arrowLength: 0 - textFormat: Text.RichText - } - - // Zoom rectangle - Rectangle { - id: sldRecZoom - - property int xScaleZoom: 0 - property int yScaleZoom: 0 - - visible: false - transform: Scale { - origin.x: 0 - origin.y: 0 - xScale: sldRecZoom.xScaleZoom - yScale: sldRecZoom.yScaleZoom - } - border.color: EaStyle.Colors.appBorder - border.width: 1 - opacity: 0.9 - color: "transparent" - - Rectangle { - anchors.fill: parent - opacity: 0.5 - color: sldRecZoom.border.color - } - } - - // Zoom with left mouse button - MouseArea { - id: sldZoomMouseArea - - enabled: sldChartView.allowZoom - anchors.fill: sldChartView - acceptedButtons: Qt.LeftButton - onPressed: { - sldRecZoom.x = mouseX - sldRecZoom.y = mouseY - sldRecZoom.visible = true - } - onMouseXChanged: { - if (mouseX > sldRecZoom.x) { - sldRecZoom.xScaleZoom = 1 - sldRecZoom.width = Math.min(mouseX, sldChartView.width) - sldRecZoom.x - } else { - sldRecZoom.xScaleZoom = -1 - sldRecZoom.width = sldRecZoom.x - Math.max(mouseX, 0) - } - } - onMouseYChanged: { - if (mouseY > sldRecZoom.y) { - sldRecZoom.yScaleZoom = 1 - sldRecZoom.height = Math.min(mouseY, sldChartView.height) - sldRecZoom.y - } else { - sldRecZoom.yScaleZoom = -1 - sldRecZoom.height = sldRecZoom.y - Math.max(mouseY, 0) - } - } - onReleased: { - const x = Math.min(sldRecZoom.x, mouseX) - sldChartView.anchors.leftMargin - const y = Math.min(sldRecZoom.y, mouseY) - sldChartView.anchors.topMargin - const width = sldRecZoom.width - const height = sldRecZoom.height - sldChartView.zoomIn(Qt.rect(x, y, width, height)) - sldRecZoom.visible = false - } - } - - // Pan with left mouse button - MouseArea { - property real pressedX - property real pressedY - property int threshold: 1 - - enabled: !sldZoomMouseArea.enabled - anchors.fill: sldChartView - acceptedButtons: Qt.LeftButton - onPressed: { - pressedX = mouseX - pressedY = mouseY - } - onMouseXChanged: Qt.callLater(update) - onMouseYChanged: Qt.callLater(update) - - function update() { - const dx = mouseX - pressedX - const dy = mouseY - pressedY - pressedX = mouseX - pressedY = mouseY - - if (dx > threshold) - sldChartView.scrollLeft(dx) - else if (dx < -threshold) - sldChartView.scrollRight(-dx) - if (dy > threshold) - sldChartView.scrollUp(dy) - else if (dy < -threshold) - sldChartView.scrollDown(-dy) - } - } - - // Reset axes with right mouse button - MouseArea { - anchors.fill: sldChartView - acceptedButtons: Qt.RightButton - onClicked: sldChartView.resetAxes() - } - - Component.onCompleted: { - Globals.References.pages.sample.mainContent.sldView = sldChartView - } + Component.onCompleted: { + Globals.References.pages.sample.mainContent.sldView = sldChart.chartView } } } @@ -612,7 +409,7 @@ Rectangle { repeat: false onTriggered: { sampleChartView.resetAxes() - sldChartView.resetAxes() + sldChart.chartView.resetAxes() } } @@ -620,7 +417,7 @@ Rectangle { id: sldCombinedResetAxesTimer interval: 50 repeat: false - onTriggered: sldChartView.resetAxes() + onTriggered: sldChart.chartView.resetAxes() } Component.onCompleted: { @@ -637,14 +434,6 @@ Rectangle { } sampleSeries = [] - // Remove old SLD series - for (let j = 0; j < sldSeries.length; j++) { - if (sldSeries[j]) { - sldChartView.removeSeries(sldSeries[j]) - } - } - sldSeries = [] - // Determine which x-axis to use for sample chart based on log setting const sampleXAxisToUse = sampleChartView.useLogQAxis ? sampleAxisXLog : sampleAxisX @@ -659,15 +448,6 @@ Rectangle { // Connect hovered signal for tooltip sampleLine.hovered.connect((point, state) => showMainTooltip(sampleChartView, sampleDataToolTip, point, state)) sampleSeries.push(sampleLine) - - // Create SLD series - const sldLine = sldChartView.createSeries(ChartView.SeriesTypeLine, "SLD " + models[k].label, sldAxisX, sldAxisY) - sldLine.color = models[k].color - sldLine.width = 2 - sldLine.useOpenGL = EaGlobals.Vars.useOpenGL - // Connect hovered signal for tooltip - sldLine.hovered.connect((point, state) => showMainTooltip(sldChartView, sldDataToolTip, point, state)) - sldSeries.push(sldLine) } // Populate data @@ -689,18 +469,6 @@ Rectangle { } } } - - // Refresh SLD series - for (let j = 0; j < sldSeries.length && j < models.length; j++) { - const series = sldSeries[j] - if (series) { - series.clear() - const points = Globals.BackendWrapper.plottingGetSldDataPointsForModel(j) - for (let p = 0; p < points.length; p++) { - series.append(points[p].x, points[p].y) - } - } - } } // Logic diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml index f6801a4a..d8e0fe08 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SldView.qml @@ -3,394 +3,21 @@ // © 2025 Contributors to the EasyReflectometry project import QtQuick -import QtQuick.Controls -import QtCharts - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Charts as EaCharts +import Gui as Gui import Gui.Globals as Globals -Rectangle { - id: container - - color: EaStyle.Colors.chartBackground - - // Track model count changes to refresh charts - property int modelCount: Globals.BackendWrapper.sampleModels.length - - // Store dynamically created series - property var sldSeries: [] - - ChartView { - id: chartView - - anchors.fill: parent - anchors.topMargin: EaStyle.Sizes.toolButtonHeight - EaStyle.Sizes.fontPixelSize - 1 - anchors.margins: -12 - - antialiasing: true - legend.visible: false - backgroundRoundness: 0 - backgroundColor: EaStyle.Colors.chartBackground - plotAreaColor: EaStyle.Colors.chartPlotAreaBackground - - property bool allowZoom: true - property bool allowHover: true - - property double xRange: Globals.BackendWrapper.plottingSldMaxX - Globals.BackendWrapper.plottingSldMinX - - // Reverse axis logic - property bool reverseZAxis: Globals.Variables.reverseSldZAxis - - ValueAxis { - id: axisX - titleText: "z (Å)" - // min/max set imperatively to avoid binding reset during zoom - property double minAfterReset: Globals.BackendWrapper.plottingSldMinX - chartView.xRange * 0.01 - property double maxAfterReset: Globals.BackendWrapper.plottingSldMaxX + chartView.xRange * 0.01 - reverse: chartView.reverseZAxis - color: EaStyle.Colors.chartAxis - gridLineColor: EaStyle.Colors.chartGridLine - minorGridLineColor: EaStyle.Colors.chartMinorGridLine - labelsColor: EaStyle.Colors.chartLabels - titleBrush: EaStyle.Colors.chartLabels - Component.onCompleted: { - min = minAfterReset - max = maxAfterReset - } - } - - property double yRange: Globals.BackendWrapper.plottingSldMaxY - Globals.BackendWrapper.plottingSldMinY - - ValueAxis { - id: axisY - titleText: "SLD (10⁻⁶ Å⁻²)" - // min/max set imperatively to avoid binding reset during zoom - property double minAfterReset: Globals.BackendWrapper.plottingSldMinY - chartView.yRange * 0.01 - property double maxAfterReset: Globals.BackendWrapper.plottingSldMaxY + chartView.yRange * 0.01 - color: EaStyle.Colors.chartAxis - gridLineColor: EaStyle.Colors.chartGridLine - minorGridLineColor: EaStyle.Colors.chartMinorGridLine - labelsColor: EaStyle.Colors.chartLabels - titleBrush: EaStyle.Colors.chartLabels - Component.onCompleted: { - min = minAfterReset - max = maxAfterReset - } - } - - function resetAxes() { - axisX.min = axisX.minAfterReset - axisX.max = axisX.maxAfterReset - axisY.min = axisY.minAfterReset - axisY.max = axisY.maxAfterReset - } - - // Tool buttons - Row { - id: toolButtons - z: 1 // Keep buttons above MouseAreas - - x: chartView.plotArea.x + chartView.plotArea.width - width - y: chartView.plotArea.y - height - EaStyle.Sizes.fontPixelSize - - spacing: 0.25 * EaStyle.Sizes.fontPixelSize - - EaElements.TabButton { - checked: Globals.Variables.showLegendOnSamplePage - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "align-left" - ToolTip.text: Globals.Variables.showLegendOnSamplePage ? - qsTr("Hide legend") : - qsTr("Show legend") - onClicked: Globals.Variables.showLegendOnSamplePage = checked - } - - EaElements.TabButton { - checked: chartView.allowHover - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "comment-alt" - ToolTip.text: qsTr("Show coordinates tooltip on hover") - onClicked: chartView.allowHover = checked - } - - Item { height: 1; width: 0.5 * EaStyle.Sizes.fontPixelSize } // spacer - - EaElements.TabButton { - checked: !chartView.allowZoom - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "arrows-alt" - ToolTip.text: qsTr("Enable pan") - onClicked: chartView.allowZoom = !checked - } - - EaElements.TabButton { - checked: chartView.allowZoom - autoExclusive: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "expand" - ToolTip.text: qsTr("Enable box zoom") - onClicked: chartView.allowZoom = checked - } - - EaElements.TabButton { - checkable: false - height: EaStyle.Sizes.toolButtonHeight - width: EaStyle.Sizes.toolButtonHeight - borderColor: EaStyle.Colors.chartAxis - fontIcon: "backspace" - ToolTip.text: qsTr("Reset axes") - onClicked: chartView.resetAxes() - } - - } - - // Legend showing all models - Rectangle { - visible: Globals.Variables.showLegendOnSamplePage - - x: chartView.plotArea.x + chartView.plotArea.width - width - EaStyle.Sizes.fontPixelSize - y: chartView.plotArea.y + EaStyle.Sizes.fontPixelSize - width: childrenRect.width - height: childrenRect.height - - color: EaStyle.Colors.mainContentBackgroundHalfTransparent - border.color: EaStyle.Colors.chartGridLine - - Column { - leftPadding: EaStyle.Sizes.fontPixelSize - rightPadding: EaStyle.Sizes.fontPixelSize - topPadding: EaStyle.Sizes.fontPixelSize * 0.5 - bottomPadding: EaStyle.Sizes.fontPixelSize * 0.5 +Gui.SldChart { + id: sldChart - Repeater { - model: container.modelCount - EaElements.Label { - text: '━ SLD ' + Globals.BackendWrapper.sampleModels[index].label - color: Globals.BackendWrapper.sampleModels[index].color - } - } - } - } + showLegend: Globals.Variables.showLegendOnSamplePage + reverseZAxis: Globals.Variables.reverseSldZAxis - EaElements.ToolTip { - id: dataToolTip - - arrowLength: 0 - textFormat: Text.RichText - } - - // Zoom rectangle - Rectangle { - id: recZoom - - property int xScaleZoom: 0 - property int yScaleZoom: 0 - - visible: false - transform: Scale { - origin.x: 0 - origin.y: 0 - xScale: recZoom.xScaleZoom - yScale: recZoom.yScaleZoom - } - border.color: EaStyle.Colors.appBorder - border.width: 1 - opacity: 0.9 - color: "transparent" - - Rectangle { - anchors.fill: parent - opacity: 0.5 - color: recZoom.border.color - } - } - - // Zoom with left mouse button - MouseArea { - id: zoomMouseArea - - enabled: chartView.allowZoom - anchors.fill: chartView - acceptedButtons: Qt.LeftButton - onPressed: { - recZoom.x = mouseX - recZoom.y = mouseY - recZoom.visible = true - } - onMouseXChanged: { - if (mouseX > recZoom.x) { - recZoom.xScaleZoom = 1 - recZoom.width = Math.min(mouseX, chartView.width) - recZoom.x - } else { - recZoom.xScaleZoom = -1 - recZoom.width = recZoom.x - Math.max(mouseX, 0) - } - } - onMouseYChanged: { - if (mouseY > recZoom.y) { - recZoom.yScaleZoom = 1 - recZoom.height = Math.min(mouseY, chartView.height) - recZoom.y - } else { - recZoom.yScaleZoom = -1 - recZoom.height = recZoom.y - Math.max(mouseY, 0) - } - } - onReleased: { - const x = Math.min(recZoom.x, mouseX) - chartView.anchors.leftMargin - const y = Math.min(recZoom.y, mouseY) - chartView.anchors.topMargin - const width = recZoom.width - const height = recZoom.height - chartView.zoomIn(Qt.rect(x, y, width, height)) - recZoom.visible = false - } - } - - // Pan with left mouse button - MouseArea { - property real pressedX - property real pressedY - property int threshold: 1 - - enabled: !zoomMouseArea.enabled - anchors.fill: chartView - acceptedButtons: Qt.LeftButton - onPressed: { - pressedX = mouseX - pressedY = mouseY - } - onMouseXChanged: Qt.callLater(update) - onMouseYChanged: Qt.callLater(update) - - function update() { - const dx = mouseX - pressedX - const dy = mouseY - pressedY - pressedX = mouseX - pressedY = mouseY - - if (dx > threshold) - chartView.scrollLeft(dx) - else if (dx < -threshold) - chartView.scrollRight(-dx) - if (dy > threshold) - chartView.scrollUp(dy) - else if (dy < -threshold) - chartView.scrollDown(-dy) - } - } - - // Reset axes with right mouse button - MouseArea { - anchors.fill: chartView - acceptedButtons: Qt.RightButton - onClicked: chartView.resetAxes() - } - - Component.onCompleted: { - Globals.References.pages.sample.mainContent.sldView = chartView - } - } - - // Create series dynamically when model count changes - onModelCountChanged: { - Qt.callLater(recreateAllSeries) - } - - // Refresh all chart series when data changes - Connections { - target: Globals.BackendWrapper - function onSamplePageDataChanged() { - refreshAllCharts() - } - function onSamplePageResetAxes() { - sldResetAxesTimer.start() - } - function onPlotModeChanged() { - refreshAllCharts() - // Delay resetAxes to allow axis range properties to update first - sldResetAxesTimer.start() - } - 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() - } + onShowLegendChanged: Globals.Variables.showLegendOnSamplePage = showLegend Component.onCompleted: { - Qt.callLater(recreateAllSeries) - } - - function recreateAllSeries() { - // Remove old series - for (let i = 0; i < sldSeries.length; i++) { - if (sldSeries[i]) { - chartView.removeSeries(sldSeries[i]) - } - } - sldSeries = [] - - // Create new series for each model - const models = Globals.BackendWrapper.sampleModels - for (let k = 0; k < models.length; k++) { - const line = chartView.createSeries(ChartView.SeriesTypeLine, models[k].label, axisX, axisY) - line.color = models[k].color - line.width = 2 - line.useOpenGL = EaGlobals.Vars.useOpenGL - // Connect hovered signal for tooltip - line.hovered.connect((point, state) => showMainTooltip(chartView, point, state)) - sldSeries.push(line) - } - - refreshAllCharts() - } - - function refreshAllCharts() { - const models = Globals.BackendWrapper.sampleModels - for (let i = 0; i < sldSeries.length && i < models.length; i++) { - const series = sldSeries[i] - if (series) { - series.clear() - const points = Globals.BackendWrapper.plottingGetSldDataPointsForModel(i) - for (let p = 0; p < points.length; p++) { - series.append(points[p].x, points[p].y) - } - } - } - } - - // Logic - function showMainTooltip(chart, point, state) { - if (!chartView.allowHover) { - return - } - const pos = chart.mapToPosition(Qt.point(point.x, point.y)) - dataToolTip.x = pos.x - dataToolTip.y = pos.y - dataToolTip.text = `

x: ${point.x.toFixed(3)}y: ${point.y.toFixed(3)}

` - dataToolTip.parent = chart - dataToolTip.visible = state + Globals.References.pages.sample.mainContent.sldView = sldChart.chartView } } diff --git a/EasyReflectometryApp/Gui/SldChart.qml b/EasyReflectometryApp/Gui/SldChart.qml new file mode 100644 index 00000000..305d3969 --- /dev/null +++ b/EasyReflectometryApp/Gui/SldChart.qml @@ -0,0 +1,388 @@ +// SPDX-FileCopyrightText: 2025 EasyReflectometry contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2025 Contributors to the EasyReflectometry project + +import QtQuick +import QtQuick.Controls +import QtCharts + +import EasyApp.Gui.Style as EaStyle +import EasyApp.Gui.Globals as EaGlobals +import EasyApp.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +Rectangle { + id: root + + color: EaStyle.Colors.chartBackground + + // Whether to show the legend (caller binds to the right page variable) + property bool showLegend: false + + // Whether to show the z-axis reversed + property bool reverseZAxis: false + + // Expose the ChartView so callers can store a reference / call resetAxes + readonly property alias chartView: chartView + + // Track model count changes to refresh charts + property int modelCount: Globals.BackendWrapper.sampleModels.length + + // Store dynamically created series + property var sldSeries: [] + + ChartView { + id: chartView + + anchors.fill: parent + anchors.topMargin: EaStyle.Sizes.toolButtonHeight - EaStyle.Sizes.fontPixelSize - 1 + anchors.margins: -12 + + antialiasing: true + legend.visible: false + backgroundRoundness: 0 + backgroundColor: EaStyle.Colors.chartBackground + plotAreaColor: EaStyle.Colors.chartPlotAreaBackground + + property bool allowZoom: true + property bool allowHover: true + + property double xRange: Globals.BackendWrapper.plottingSldMaxX - Globals.BackendWrapper.plottingSldMinX + + ValueAxis { + id: axisX + titleText: "z (Å)" + property double minAfterReset: Globals.BackendWrapper.plottingSldMinX - chartView.xRange * 0.01 + property double maxAfterReset: Globals.BackendWrapper.plottingSldMaxX + chartView.xRange * 0.01 + reverse: root.reverseZAxis + color: EaStyle.Colors.chartAxis + gridLineColor: EaStyle.Colors.chartGridLine + minorGridLineColor: EaStyle.Colors.chartMinorGridLine + labelsColor: EaStyle.Colors.chartLabels + titleBrush: EaStyle.Colors.chartLabels + Component.onCompleted: { + min = minAfterReset + max = maxAfterReset + } + } + + property double yRange: Globals.BackendWrapper.plottingSldMaxY - Globals.BackendWrapper.plottingSldMinY + + ValueAxis { + id: axisY + titleText: "SLD (10⁻⁶Å⁻²)" + property double minAfterReset: Globals.BackendWrapper.plottingSldMinY - chartView.yRange * 0.01 + property double maxAfterReset: Globals.BackendWrapper.plottingSldMaxY + chartView.yRange * 0.01 + color: EaStyle.Colors.chartAxis + gridLineColor: EaStyle.Colors.chartGridLine + minorGridLineColor: EaStyle.Colors.chartMinorGridLine + labelsColor: EaStyle.Colors.chartLabels + titleBrush: EaStyle.Colors.chartLabels + Component.onCompleted: { + min = minAfterReset + max = maxAfterReset + } + } + + function resetAxes() { + axisX.min = axisX.minAfterReset + axisX.max = axisX.maxAfterReset + axisY.min = axisY.minAfterReset + axisY.max = axisY.maxAfterReset + } + + // Tool buttons + Row { + id: toolButtons + z: 1 + + x: chartView.plotArea.x + chartView.plotArea.width - width + y: chartView.plotArea.y - height - EaStyle.Sizes.fontPixelSize + + spacing: 0.25 * EaStyle.Sizes.fontPixelSize + + EaElements.TabButton { + checked: root.showLegend + autoExclusive: false + height: EaStyle.Sizes.toolButtonHeight + width: EaStyle.Sizes.toolButtonHeight + borderColor: EaStyle.Colors.chartAxis + fontIcon: "align-left" + ToolTip.text: root.showLegend ? + qsTr("Hide legend") : + qsTr("Show legend") + onClicked: root.showLegend = checked + } + + EaElements.TabButton { + checked: chartView.allowHover + autoExclusive: false + height: EaStyle.Sizes.toolButtonHeight + width: EaStyle.Sizes.toolButtonHeight + borderColor: EaStyle.Colors.chartAxis + fontIcon: "comment-alt" + ToolTip.text: qsTr("Show coordinates tooltip on hover") + onClicked: chartView.allowHover = checked + } + + Item { height: 1; width: 0.5 * EaStyle.Sizes.fontPixelSize } // spacer + + EaElements.TabButton { + checked: !chartView.allowZoom + autoExclusive: false + height: EaStyle.Sizes.toolButtonHeight + width: EaStyle.Sizes.toolButtonHeight + borderColor: EaStyle.Colors.chartAxis + fontIcon: "arrows-alt" + ToolTip.text: qsTr("Enable pan") + onClicked: chartView.allowZoom = !checked + } + + EaElements.TabButton { + checked: chartView.allowZoom + autoExclusive: false + height: EaStyle.Sizes.toolButtonHeight + width: EaStyle.Sizes.toolButtonHeight + borderColor: EaStyle.Colors.chartAxis + fontIcon: "expand" + ToolTip.text: qsTr("Enable box zoom") + onClicked: chartView.allowZoom = checked + } + + EaElements.TabButton { + checkable: false + height: EaStyle.Sizes.toolButtonHeight + width: EaStyle.Sizes.toolButtonHeight + borderColor: EaStyle.Colors.chartAxis + fontIcon: "backspace" + ToolTip.text: qsTr("Reset axes") + onClicked: chartView.resetAxes() + } + } + + // Legend showing all models + Rectangle { + visible: root.showLegend + + x: chartView.plotArea.x + chartView.plotArea.width - width - EaStyle.Sizes.fontPixelSize + y: chartView.plotArea.y + EaStyle.Sizes.fontPixelSize + width: childrenRect.width + height: childrenRect.height + + color: EaStyle.Colors.mainContentBackgroundHalfTransparent + border.color: EaStyle.Colors.chartGridLine + + Column { + leftPadding: EaStyle.Sizes.fontPixelSize + rightPadding: EaStyle.Sizes.fontPixelSize + topPadding: EaStyle.Sizes.fontPixelSize * 0.5 + bottomPadding: EaStyle.Sizes.fontPixelSize * 0.5 + + Repeater { + model: root.modelCount + EaElements.Label { + text: '━ SLD ' + Globals.BackendWrapper.sampleModels[index].label + color: Globals.BackendWrapper.sampleModels[index].color + } + } + } + } + + EaElements.ToolTip { + id: dataToolTip + arrowLength: 0 + textFormat: Text.RichText + } + + // Zoom rectangle + Rectangle { + id: recZoom + + property int xScaleZoom: 0 + property int yScaleZoom: 0 + + visible: false + transform: Scale { + origin.x: 0 + origin.y: 0 + xScale: recZoom.xScaleZoom + yScale: recZoom.yScaleZoom + } + border.color: EaStyle.Colors.appBorder + border.width: 1 + opacity: 0.9 + color: "transparent" + + Rectangle { + anchors.fill: parent + opacity: 0.5 + color: recZoom.border.color + } + } + + // Zoom with left mouse button + MouseArea { + id: zoomMouseArea + + enabled: chartView.allowZoom + anchors.fill: chartView + acceptedButtons: Qt.LeftButton + onPressed: { + recZoom.x = mouseX + recZoom.y = mouseY + recZoom.visible = true + } + onMouseXChanged: { + if (mouseX > recZoom.x) { + recZoom.xScaleZoom = 1 + recZoom.width = Math.min(mouseX, chartView.width) - recZoom.x + } else { + recZoom.xScaleZoom = -1 + recZoom.width = recZoom.x - Math.max(mouseX, 0) + } + } + onMouseYChanged: { + if (mouseY > recZoom.y) { + recZoom.yScaleZoom = 1 + recZoom.height = Math.min(mouseY, chartView.height) - recZoom.y + } else { + recZoom.yScaleZoom = -1 + recZoom.height = recZoom.y - Math.max(mouseY, 0) + } + } + onReleased: { + const x = Math.min(recZoom.x, mouseX) - chartView.anchors.leftMargin + const y = Math.min(recZoom.y, mouseY) - chartView.anchors.topMargin + const width = recZoom.width + const height = recZoom.height + chartView.zoomIn(Qt.rect(x, y, width, height)) + recZoom.visible = false + } + } + + // Pan with left mouse button + MouseArea { + property real pressedX + property real pressedY + property int threshold: 1 + + enabled: !zoomMouseArea.enabled + anchors.fill: chartView + acceptedButtons: Qt.LeftButton + onPressed: { + pressedX = mouseX + pressedY = mouseY + } + onMouseXChanged: Qt.callLater(update) + onMouseYChanged: Qt.callLater(update) + + function update() { + const dx = mouseX - pressedX + const dy = mouseY - pressedY + pressedX = mouseX + pressedY = mouseY + + if (dx > threshold) + chartView.scrollLeft(dx) + else if (dx < -threshold) + chartView.scrollRight(-dx) + if (dy > threshold) + chartView.scrollUp(dy) + else if (dy < -threshold) + chartView.scrollDown(-dy) + } + } + + // Reset axes with right mouse button + MouseArea { + anchors.fill: chartView + acceptedButtons: Qt.RightButton + onClicked: chartView.resetAxes() + } + } + + // Create series dynamically when model count changes + onModelCountChanged: { + Qt.callLater(recreateAllSeries) + } + + // Refresh all chart series when data changes + Connections { + target: Globals.BackendWrapper + function onSamplePageDataChanged() { + refreshAllCharts() + } + function onSamplePageResetAxes() { + resetAxesTimer.start() + } + function onPlotModeChanged() { + refreshAllCharts() + resetAxesTimer.start() + } + function onChartAxesResetRequested() { + resetAxesTimer.start() + } + } + + Timer { + id: resetAxesTimer + interval: 50 + repeat: false + onTriggered: chartView.resetAxes() + } + + Component.onCompleted: { + Qt.callLater(recreateAllSeries) + } + + function recreateAllSeries() { + // Remove old series + for (let i = 0; i < sldSeries.length; i++) { + if (sldSeries[i]) { + chartView.removeSeries(sldSeries[i]) + } + } + sldSeries = [] + + // Create new series for each model + const models = Globals.BackendWrapper.sampleModels + for (let k = 0; k < models.length; k++) { + const line = chartView.createSeries(ChartView.SeriesTypeLine, models[k].label, axisX, axisY) + line.color = models[k].color + line.width = 2 + line.useOpenGL = EaGlobals.Vars.useOpenGL + line.hovered.connect((point, state) => showMainTooltip(point, state)) + sldSeries.push(line) + } + + refreshAllCharts() + } + + function refreshAllCharts() { + const models = Globals.BackendWrapper.sampleModels + for (let i = 0; i < sldSeries.length && i < models.length; i++) { + const series = sldSeries[i] + if (series) { + series.clear() + const points = Globals.BackendWrapper.plottingGetSldDataPointsForModel(i) + for (let p = 0; p < points.length; p++) { + series.append(points[p].x, points[p].y) + } + } + } + } + + function showMainTooltip(point, state) { + if (!chartView.allowHover) { + return + } + const pos = chartView.mapToPosition(Qt.point(point.x, point.y)) + dataToolTip.x = pos.x + dataToolTip.y = pos.y + dataToolTip.text = `

x: ${point.x.toFixed(3)}y: ${point.y.toFixed(3)}

` + dataToolTip.parent = chartView + dataToolTip.visible = state + } +} diff --git a/EasyReflectometryApp/Gui/qmldir b/EasyReflectometryApp/Gui/qmldir index 9145745f..fe68abae 100644 --- a/EasyReflectometryApp/Gui/qmldir +++ b/EasyReflectometryApp/Gui/qmldir @@ -2,4 +2,5 @@ module Gui ApplicationWindow ApplicationWindow.qml PlotControlRefLines PlotControlRefLines.qml +SldChart SldChart.qml StatusBar StatusBar.qml From b6e0e6e7294aba14cad1b271ac4cc1853f84c0f0 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 24 Feb 2026 15:39:59 +0100 Subject: [PATCH 19/23] logging for minimizer change --- EasyReflectometryApp/Backends/Py/logic/fitting.py | 8 ++++++++ EasyReflectometryApp/Backends/Py/logic/minimizers.py | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/logic/fitting.py b/EasyReflectometryApp/Backends/Py/logic/fitting.py index a01073c0..ca9446f1 100644 --- a/EasyReflectometryApp/Backends/Py/logic/fitting.py +++ b/EasyReflectometryApp/Backends/Py/logic/fitting.py @@ -133,6 +133,12 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: selected_minimizer = minimizers_logic.selected_minimizer_enum() if selected_minimizer is not None: multi_fitter.easy_science_multi_fitter.switch_minimizer(selected_minimizer) + logger.info( + 'Fitting: applied minimizer %s to MultiFitter (engine: %s, method: %s)', + selected_minimizer.name, + multi_fitter.easy_science_multi_fitter.minimizer.package, + multi_fitter.easy_science_multi_fitter.minimizer._method, + ) if minimizers_logic.tolerance is not None: multi_fitter.easy_science_multi_fitter.tolerance = minimizers_logic.tolerance if minimizers_logic.max_iterations is not None: @@ -186,6 +192,8 @@ def on_fit_finished(self, results: List[FitResults]) -> None: # For multi-experiment fits, store the list; use first for single-result properties self._results = results self._result = results[0] + engine_name = getattr(results[0], 'minimizer_engine', 'unknown') + logger.info('Fit finished: engine=%s, chi2=%s, success=%s', engine_name, self.fit_chi2, results[0].success) else: self._result = results self._results = [results] if results else [] diff --git a/EasyReflectometryApp/Backends/Py/logic/minimizers.py b/EasyReflectometryApp/Backends/Py/logic/minimizers.py index ef262cce..0a190dfb 100644 --- a/EasyReflectometryApp/Backends/Py/logic/minimizers.py +++ b/EasyReflectometryApp/Backends/Py/logic/minimizers.py @@ -35,9 +35,8 @@ def selected_minimizer_enum(self): def set_minimizer_current_index(self, new_value: int) -> bool: if new_value != self._minimizer_current_index: self._minimizer_current_index = new_value - if self._project_lib._fitter is not None: - enum_new_minimizer = self._list_available_minimizers[new_value] - self._project_lib._fitter.switch_minimizer(enum_new_minimizer) + enum_new_minimizer = self._list_available_minimizers[new_value] + self._project_lib.minimizer = enum_new_minimizer return True return False From 6ba05e97f92b2386dd30a9c626d35bb188000fab Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 24 Feb 2026 16:00:50 +0100 Subject: [PATCH 20/23] formatting for the error column --- .../Pages/Analysis/Sidebar/Basic/Groups/Fittables.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Basic/Groups/Fittables.qml b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Basic/Groups/Fittables.qml index 405e5291..10e39a35 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Basic/Groups/Fittables.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/Sidebar/Basic/Groups/Fittables.qml @@ -261,7 +261,7 @@ EaElements.GroupBox { } EaComponents.TableViewLabel { - text: EaLogic.Utils.toDefaultPrecision(Globals.BackendWrapper.analysisFitableParameters[index].error) + text: formatError(Globals.BackendWrapper.analysisFitableParameters[index].error) color: (Globals.BackendWrapper.analysisFitableParameters[index].independent !== undefined ? Globals.BackendWrapper.analysisFitableParameters[index].independent : true) ? EaStyle.Colors.themeForeground : EaStyle.Colors.themeForegroundDisabled @@ -436,6 +436,13 @@ EaElements.GroupBox { fittables.parameterSlider.to = EaLogic.Utils.toDefaultPrecision(to) } + function formatError(value) { + if (value === undefined || value === 0 || isNaN(value)) return '' + var s = Number(value.toPrecision(2)).toString() + if (s.length <= 6) return s + return value.toExponential(1) + } + function allFittablesSelected() { const params = backend.analysisFitableParameters if (!params || !params.length) { From 36bc64b6a2261f1cdb1048eb2d47a0974e8e4ab5 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Fri, 27 Feb 2026 08:23:18 +0100 Subject: [PATCH 21/23] improved constraints check --- EasyReflectometryApp/Backends/Py/analysis.py | 4 ---- .../Backends/Py/logic/parameters.py | 3 ++- EasyReflectometryApp/Backends/Py/sample.py | 20 +++++++++++++++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index ebfa9ed6..afc7f8ad 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -503,14 +503,10 @@ def enabledParameters(self) -> list[dict[str]]: if self._chached_enabled_parameters is not None: return self._chached_enabled_parameters enabled_parameters = [] - # import time - # t0 = time.time() for parameter in self._parameters_logic.parameters: if not parameter['enabled']: continue enabled_parameters.append(parameter) - # t1 = time.time() - # print(f"Enabled parameters computation time: {t1 - t0:.4f} seconds") self._chached_enabled_parameters = enabled_parameters return enabled_parameters diff --git a/EasyReflectometryApp/Backends/Py/logic/parameters.py b/EasyReflectometryApp/Backends/Py/logic/parameters.py index 5fb05a53..1cb57160 100644 --- a/EasyReflectometryApp/Backends/Py/logic/parameters.py +++ b/EasyReflectometryApp/Backends/Py/logic/parameters.py @@ -251,6 +251,7 @@ def _is_layer_parameter(param: Parameter) -> bool: prefixed_display_name = display_name alias = _make_alias(prefixed_display_name or parameter.name) + param_value = float(parameter.value) parameter_list.append( { 'name': prefixed_display_name, @@ -258,7 +259,7 @@ def _is_layer_parameter(param: Parameter) -> bool: 'group': group_name, 'alias': alias, 'unique_name': parameter.unique_name, - 'value': float(parameter.value), + 'value': param_value, 'error': float(parameter.variance), 'max': float(parameter.max), 'min': float(parameter.min), diff --git a/EasyReflectometryApp/Backends/Py/sample.py b/EasyReflectometryApp/Backends/Py/sample.py index 73c7647a..1c73c226 100644 --- a/EasyReflectometryApp/Backends/Py/sample.py +++ b/EasyReflectometryApp/Backends/Py/sample.py @@ -552,13 +552,29 @@ def _sanitize_relation(operator: str) -> str: def _format_numeric(value: float) -> str: return f'{value:.6g}' + def _get_independent_parameter_entries(self) -> list[dict]: + """Return the filtered list of independent+enabled parameter entries. + + This must match the same filtering as dependentParameterNames so that + the QML dropdown index maps to the correct parameter object. + """ + entries = [] + for parameter in self._parameters_logic.parameters: + if not parameter['independent']: + continue + if hasattr(parameter['object'], 'enabled') and not parameter['object'].enabled: + continue + entries.append(parameter) + return entries + def _prepare_constraint_instruction( self, dependent_index: int, relation_operator: str, expression: str, ) -> dict[str, Any]: - if dependent_index < 0 or dependent_index >= len(self._project_lib.parameters): + independent_entries = self._get_independent_parameter_entries() + if dependent_index < 0 or dependent_index >= len(independent_entries): raise ValueError('Select a dependent parameter before defining a constraint.') relation = self._sanitize_relation(relation_operator) @@ -868,7 +884,7 @@ def addConstraint(self, dependent_index: int, relation: str, expression: str): except Exception as error: # noqa: BLE001 return {'success': False, 'message': str(error)} - dependent = self._project_lib.parameters[dependent_index] + dependent = self._get_independent_parameter_entries()[dependent_index]['object'] previous_state = self._capture_parameter_state(dependent) self._ensure_parameter_independent(dependent) From 453ee5d7e44e029855bbef76adbae50e57b1ebaf Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 27 Feb 2026 10:59:09 +0100 Subject: [PATCH 22/23] added chi2 display in the status bar --- EasyReflectometryApp/Gui/StatusBar.qml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/EasyReflectometryApp/Gui/StatusBar.qml b/EasyReflectometryApp/Gui/StatusBar.qml index cf92b436..9afb2c96 100644 --- a/EasyReflectometryApp/Gui/StatusBar.qml +++ b/EasyReflectometryApp/Gui/StatusBar.qml @@ -53,4 +53,11 @@ EaElements.StatusBar { valueText: Globals.BackendWrapper.statusVariables ?? '' ToolTip.text: qsTr('Number of parameters: total, free and fixed') } + EaElements.StatusBarItem { + visible: Globals.BackendWrapper.analysisFitChi2 > 0 + keyIcon: 'chart-line' + keyText: qsTr('Chi²') + valueText: Globals.BackendWrapper.analysisFitChi2.toFixed(2) + ToolTip.text: qsTr('Goodness of fit (chi-squared)') + } } From 5af149720aa7d305adf49052118ee4eb677afe64 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 27 Feb 2026 17:06:43 +0100 Subject: [PATCH 23/23] PR review #2 --- EasyReflectometryApp/Backends/Py/analysis.py | 33 +++++++-- .../Backends/Py/logic/experiments.py | 74 +++++++++++++++---- .../Backends/Py/logic/fitting.py | 41 +++++++--- .../Backends/Py/plotting_1d.py | 16 +++- .../Backends/Py/py_backend.py | 11 +-- .../Gui/Globals/BackendWrapper.qml | 13 ++++ .../Analysis/MainContent/AnalysisView.qml | 2 +- .../Analysis/MainContent/CombinedView.qml | 2 +- .../Experiment/MainContent/ExperimentView.qml | 2 +- .../Pages/Sample/MainContent/CombinedView.qml | 4 +- .../Pages/Sample/MainContent/SampleView.qml | 2 +- .../Sidebar/Basic/Groups/LoadSample.qml | 4 +- EasyReflectometryApp/Gui/SldChart.qml | 2 +- 13 files changed, 156 insertions(+), 50 deletions(-) diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index afc7f8ad..2ad4fd81 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -60,6 +60,25 @@ def _initialize_selected_experiments(self) -> None: else: self._selected_experiment_indices = [] + def _ordered_experiments(self) -> list: + """Return experiments as an ordered list of experiment objects. + + Handles mapping-like storage without assuming contiguous integer keys. + """ + experiments = self._experiments_logic._project_lib._experiments + if not experiments: + return [] + + if hasattr(experiments, 'items'): + items = list(experiments.items()) + try: + items.sort(key=lambda item: item[0]) + except TypeError: + pass + return [experiment for _, experiment in items] + + return list(experiments) + ######################## ## Fitting @Property(str, notify=fittingChanged) @@ -279,7 +298,7 @@ def setExperimentNameAtIndex(self, index: int, new_name: str) -> None: def modelIndexForExperiment(self) -> int: # return the model index for the current experiment models = self._experiments_logic._project_lib._models - experiments = self._experiments_logic._project_lib._experiments + experiments = self._ordered_experiments() index = self.experimentCurrentIndex current_experiment = experiments[index] if 0 <= index < len(experiments) else None if current_experiment is not None: @@ -291,9 +310,9 @@ def modelIndexForExperiment(self) -> int: def modelNamesForExperiment(self) -> list: # return a list of model names for each experiment mapped_models = [] - experiments = self._experiments_logic._project_lib._experiments - for ind in experiments: - name = get_original_name(experiments[ind].model) + experiments = self._ordered_experiments() + for experiment in experiments: + name = get_original_name(experiment.model) mapped_models.append(name) return mapped_models @@ -301,9 +320,9 @@ def modelNamesForExperiment(self) -> list: def modelColorsForExperiment(self) -> list: # return a list of model colors for each experiment mapped_models = [] - experiments = self._experiments_logic._project_lib._experiments - for ind in experiments: - mapped_models.append(experiments[ind].model.color) + experiments = self._ordered_experiments() + for experiment in experiments: + mapped_models.append(experiment.model.color) return mapped_models @Slot(int) diff --git a/EasyReflectometryApp/Backends/Py/logic/experiments.py b/EasyReflectometryApp/Backends/Py/logic/experiments.py index 3a2dbff9..f292d886 100644 --- a/EasyReflectometryApp/Backends/Py/logic/experiments.py +++ b/EasyReflectometryApp/Backends/Py/logic/experiments.py @@ -5,12 +5,41 @@ class Experiments: def __init__(self, project_lib: ProjectLib): self._project_lib = project_lib + def _ordered_experiment_items(self) -> list[tuple[object, object]]: + """Return experiments as ordered ``(key, experiment)`` pairs. + + Supports mapping-like storage without assuming contiguous integer keys. + """ + experiments = self._project_lib._experiments + if not experiments: + return [] + + if hasattr(experiments, 'items'): + items = list(experiments.items()) + try: + items.sort(key=lambda item: item[0]) + except TypeError: + pass + return items + + return list(enumerate(experiments)) + + def _experiment_at_index(self, index: int): + items = self._ordered_experiment_items() + if 0 <= index < len(items): + return items[index][1] + return None + + def _experiment_key_at_index(self, index: int): + items = self._ordered_experiment_items() + if 0 <= index < len(items): + return items[index][0] + return None + def available(self) -> list[str]: experiments_name = [] try: - # get .name from self._project_lib._experiments and append to experiments_name - for ind in self._project_lib._experiments.keys(): - exp = self._project_lib._experiments[ind] + for _, exp in self._ordered_experiment_items(): experiments_name.append(exp.name) except IndexError: pass @@ -26,19 +55,19 @@ def set_current_index(self, new_value: int) -> None: return False def set_experiment_name(self, new_name: str) -> None: - exp = self._project_lib._experiments.get(self._project_lib._current_experiment_index) + exp = self._experiment_at_index(self._project_lib._current_experiment_index) if exp: exp.name = new_name def set_experiment_name_at_index(self, index: int, new_name: str) -> None: - exp = self._project_lib._experiments.get(index) + exp = self._experiment_at_index(index) if exp: exp.name = new_name def model_on_experiment(self, experiment_index: int = -1) -> dict: if experiment_index == -1: experiment_index = self._project_lib._current_experiment_index - exp = self._project_lib._experiments.get(experiment_index) + exp = self._experiment_at_index(experiment_index) if exp: return exp.model return {} @@ -50,7 +79,7 @@ def model_index_on_experiment(self) -> int: return -1 def set_model_on_experiment(self, new_value: int) -> None: - exp = self._project_lib._experiments.get(self._project_lib._current_experiment_index) + exp = self._experiment_at_index(self._project_lib._current_experiment_index) models = self._project_lib._models if exp and models: try: @@ -66,12 +95,27 @@ def remove_experiment(self, index: int) -> None: """ Remove the experiment at the given index. """ - if 0 <= index < len(self.available()): - del self._project_lib._experiments[index] - # readjust the dictionary keys for continuity - temp_experiments = self._project_lib._experiments.copy() - self._project_lib._experiments = {i: exp for i, exp in enumerate(temp_experiments.values())} - if self._project_lib._current_experiment_index >= index: - self._project_lib._current_experiment_index = max(0, self._project_lib._current_experiment_index - 1) - else: + total = len(self.available()) + if not (0 <= index < total): + print(f'Experiment index {index} is out of range.') + return + + experiments = self._project_lib._experiments + exp_key = self._experiment_key_at_index(index) + if exp_key is None: print(f'Experiment index {index} is out of range.') + return + + if hasattr(experiments, 'items'): + del experiments[exp_key] + else: + experiments.pop(index) + + current = self._project_lib._current_experiment_index + new_total = max(0, total - 1) + if new_total == 0: + self._project_lib._current_experiment_index = 0 + elif current > index: + self._project_lib._current_experiment_index = current - 1 + elif current >= new_total: + self._project_lib._current_experiment_index = new_total - 1 diff --git a/EasyReflectometryApp/Backends/Py/logic/fitting.py b/EasyReflectometryApp/Backends/Py/logic/fitting.py index ca9446f1..18d683af 100644 --- a/EasyReflectometryApp/Backends/Py/logic/fitting.py +++ b/EasyReflectometryApp/Backends/Py/logic/fitting.py @@ -106,6 +106,25 @@ def prepare_for_threaded_fit(self) -> None: self._show_results_dialog = False self._fit_error_message = None + def _ordered_experiments(self) -> list: + """Return experiments as an ordered list of experiment objects. + + Handles mapping-like storage without assuming contiguous integer keys. + """ + experiments = self._project_lib._experiments + if not experiments: + return [] + + if hasattr(experiments, 'items'): + items = list(experiments.items()) + try: + items.sort(key=lambda item: item[0]) + except TypeError: + pass + return [experiment for _, experiment in items] + + return list(experiments) + def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: """Prepare data for threaded fitting. @@ -115,7 +134,7 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: try: from easyreflectometry.fitting import MultiFitter - experiments = self._project_lib._experiments + experiments = self._ordered_experiments() if not experiments: self._fit_error_message = 'No experiments to fit' self._running = False @@ -123,10 +142,8 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: self._show_results_dialog = True return None, None, None, None, None - experiment_indices = range(len(experiments)) - # Create MultiFitter with all models - models = [experiments[idx].model for idx in experiment_indices] + models = [experiment.model for experiment in experiments] multi_fitter = MultiFitter(*models) # Apply the user-selected minimizer to the new fitter @@ -145,16 +162,16 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: multi_fitter.easy_science_multi_fitter.max_evaluations = minimizers_logic.max_iterations # Prepare data arrays for all experiments - x_data = [experiments[idx].x for idx in experiment_indices] - y_data = [experiments[idx].y for idx in experiment_indices] + x_data = [experiment.x for experiment in experiments] + y_data = [experiment.y for experiment in experiments] # Validate error values before computing weights to avoid division by zero import numpy as np - for idx in experiment_indices: - ye = experiments[idx].ye + for idx, experiment in enumerate(experiments): + ye = experiment.ye if np.any(ye == 0): - exp_name = experiments[idx].name if hasattr(experiments[idx], 'name') else f'index {idx}' + exp_name = experiment.name if hasattr(experiment, 'name') else f'index {idx}' self._fit_error_message = f'Experiment {exp_name} has zero error values which would cause division by zero' self._running = False self._finished = True @@ -162,7 +179,7 @@ def prepare_threaded_fit(self, minimizers_logic: 'Minimizers') -> tuple: return None, None, None, None, None # ye contains variances (sigma²); weights = 1/sigma = 1/sqrt(variance) - weights = [1.0 / np.sqrt(experiments[idx].ye) for idx in experiment_indices] + weights = [1.0 / np.sqrt(experiment.ye) for experiment in experiments] # Method is optional in fit() - pass None to use minimizer's default method = None @@ -240,12 +257,12 @@ def start_stop(self) -> None: # Handle fit failure - create a failed result self._result = None self._fit_error_message = str(e) - print(f'Fit failed: {e}') + logger.warning('Fit failed: %s', e) except Exception as e: # Handle any other unexpected exceptions self._result = None self._fit_error_message = str(e) - print(f'Unexpected error during fit: {e}') + logger.warning('Unexpected error during fit: %s', e) finally: self._running = False self._finished = True diff --git a/EasyReflectometryApp/Backends/Py/plotting_1d.py b/EasyReflectometryApp/Backends/Py/plotting_1d.py index a1ed1e62..36ee9f88 100644 --- a/EasyReflectometryApp/Backends/Py/plotting_1d.py +++ b/EasyReflectometryApp/Backends/Py/plotting_1d.py @@ -341,9 +341,11 @@ def _get_all_models_sample_range(self): if max_x == float('-inf'): max_x = self.sample_data.x.max() if self.sample_data.x.size > 0 else 1.0 if min_y == float('inf'): - min_y = np.log10(self.sample_data.y.min()) if self.sample_data.y.size > 0 else -10.0 + valid_y = self.sample_data.y[self.sample_data.y > 0] if self.sample_data.y.size > 0 else np.array([]) + min_y = np.log10(valid_y.min()) if valid_y.size > 0 else -10.0 if max_y == float('-inf'): - max_y = np.log10(self.sample_data.y.max()) if self.sample_data.y.size > 0 else 0.0 + valid_y = self.sample_data.y[self.sample_data.y > 0] if self.sample_data.y.size > 0 else np.array([]) + max_y = np.log10(valid_y.max()) if valid_y.size > 0 else 0.0 return (min_x, max_x, min_y, max_y) @@ -544,12 +546,17 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: # Get the model index for this experiment - it may be different from experiment_index # When multiple experiments share the same model model_index = 0 + model_found = False if hasattr(exp_data, 'model') and exp_data.model is not None: # Find the model index in the models collection for idx, model in enumerate(self._project_lib.models): if model is exp_data.model: model_index = idx + model_found = True break + if not model_found: + console.debug(f'Warning: model for experiment {experiment_index} ' + f'not found in models collection, falling back to model 0') else: # Fallback: use experiment_index if it's within model range, else 0 model_index = experiment_index if experiment_index < len(self._project_lib.models) else 0 @@ -567,6 +574,11 @@ def getAnalysisDataPoints(self, experiment_index: int) -> list: exp_points = list(exp_data.data_points()) calc_y = calc_data.y + if len(calc_y) != len(q_filtered): + console.debug(f'Warning: calculated data length ({len(calc_y)}) ' + f'differs from filtered experimental data ({len(q_filtered)}) ' + f'for experiment {experiment_index}') + calc_idx = 0 for point in exp_points: if point[0] < self._project_lib.q_max and self._project_lib.q_min < point[0]: diff --git a/EasyReflectometryApp/Backends/Py/py_backend.py b/EasyReflectometryApp/Backends/Py/py_backend.py index 51fb6de0..57ba5a8b 100644 --- a/EasyReflectometryApp/Backends/Py/py_backend.py +++ b/EasyReflectometryApp/Backends/Py/py_backend.py @@ -1,4 +1,5 @@ from EasyApp.Logic.Logging import LoggerLevelHandler +from EasyApp.Logic.Logging import console from easyreflectometry import Project as ProjectLib from PySide6.QtCore import Property from PySide6.QtCore import QObject @@ -97,18 +98,18 @@ def analysisSelectedExperimentIndices(self) -> list: @Slot('QVariantList') def analysisSetSelectedExperimentIndices(self, indices) -> None: """Set multiple selected experiment indices.""" - print(f'PyBackend.analysisSetSelectedExperimentIndices called with: {indices}') - print(f'Type of indices: {type(indices)}') + console.debug(f'PyBackend.analysisSetSelectedExperimentIndices called with: {indices}') + console.debug(f'Type of indices: {type(indices)}') # Convert QVariantList to Python list if needed python_indices = list(indices) if hasattr(indices, '__iter__') else [] - print(f'Converted to Python list: {python_indices}') + console.debug(f'Converted to Python list: {python_indices}') if hasattr(self._analysis, 'setSelectedExperimentIndices'): self._analysis.setSelectedExperimentIndices(python_indices) - print('Successfully called analysis.setSelectedExperimentIndices') + console.debug('Successfully called analysis.setSelectedExperimentIndices') else: - print('ERROR: analysis.setSelectedExperimentIndices method not found') + console.debug('ERROR: analysis.setSelectedExperimentIndices method not found') # Emit our local signal to notify QML properties self.multiExperimentSelectionChanged.emit() diff --git a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml index 5e946d2c..df602135 100644 --- a/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml +++ b/EasyReflectometryApp/Gui/Globals/BackendWrapper.qml @@ -218,6 +218,7 @@ QtObject { try { return activeBackend.analysisExperimentsSelectedCount || 1 } catch (e) { + console.warn("analysisExperimentsSelectedCount failed:", e) return 1 } } @@ -225,6 +226,7 @@ QtObject { try { return activeBackend.analysisSelectedExperimentIndices || [] } catch (e) { + console.warn("analysisSelectedExperimentIndices failed:", e) return [] } } @@ -361,6 +363,7 @@ QtObject { try { return activeBackend.plotting.getBackgroundData() } catch (e) { + console.warn("plottingGetBackgroundData failed:", e) return [] } } @@ -368,6 +371,7 @@ QtObject { try { return activeBackend.plotting.getScaleData() } catch (e) { + console.warn("plottingGetScaleData failed:", e) return [] } } @@ -377,6 +381,7 @@ QtObject { try { return activeBackend.plotting.getBackgroundDataForAnalysis() } catch (e) { + console.warn("plottingGetBackgroundDataForAnalysis failed:", e) return [] } } @@ -384,6 +389,7 @@ QtObject { try { return activeBackend.plotting.getScaleDataForAnalysis() } catch (e) { + console.warn("plottingGetScaleDataForAnalysis failed:", e) return [] } } @@ -419,6 +425,7 @@ QtObject { try { return activeBackend.plotting.getSampleDataPointsForModel(index) } catch (e) { + console.warn("plottingGetSampleDataPointsForModel failed:", e) return [] } } @@ -426,6 +433,7 @@ QtObject { try { return activeBackend.plotting.getSldDataPointsForModel(index) } catch (e) { + console.warn("plottingGetSldDataPointsForModel failed:", e) return [] } } @@ -433,6 +441,7 @@ QtObject { try { return activeBackend.plotting.getModelColor(index) } catch (e) { + console.warn("plottingGetModelColor failed:", e) return '#000000' } } @@ -471,6 +480,7 @@ QtObject { try { return activeBackend.plottingIsMultiExperimentMode || false } catch (e) { + console.warn("plottingIsMultiExperimentMode failed:", e) return false } } @@ -478,6 +488,7 @@ QtObject { try { return activeBackend.plottingIndividualExperimentDataList || [] } catch (e) { + console.warn("plottingIndividualExperimentDataList failed:", e) return [] } } @@ -485,6 +496,7 @@ QtObject { try { return activeBackend.plottingGetExperimentDataPoints(index) } catch (e) { + console.warn("plottingGetExperimentDataPoints failed:", e) return [] } } @@ -492,6 +504,7 @@ QtObject { try { return activeBackend.plottingGetAnalysisDataPoints(index) } catch (e) { + console.warn("plottingGetAnalysisDataPoints failed:", e) return [] } } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml index 29de419a..fec58f95 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/AnalysisView.qml @@ -114,7 +114,7 @@ Rectangle { Timer { id: analysisResetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: chartView.resetAxes() } diff --git a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml index 431e856b..9eab767e 100644 --- a/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Analysis/MainContent/CombinedView.qml @@ -91,7 +91,7 @@ Rectangle { Timer { id: combinedAnalysisResetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: { analysisChartView.resetAxes() diff --git a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml index 18513ac7..070e809d 100644 --- a/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml +++ b/EasyReflectometryApp/Gui/Pages/Experiment/MainContent/ExperimentView.qml @@ -201,7 +201,7 @@ Rectangle { Timer { id: experimentResetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: chartView.resetAxes() } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml index 41104387..3801d2e6 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/CombinedView.qml @@ -405,7 +405,7 @@ Rectangle { Timer { id: sampleCombinedResetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: { sampleChartView.resetAxes() @@ -415,7 +415,7 @@ Rectangle { Timer { id: sldCombinedResetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: sldChart.chartView.resetAxes() } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml index e492bbd4..38c821cd 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/MainContent/SampleView.qml @@ -363,7 +363,7 @@ Rectangle { Timer { id: sampleResetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: chartView.resetAxes() } diff --git a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml index ec9cba86..04235b1f 100644 --- a/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml +++ b/EasyReflectometryApp/Gui/Pages/Sample/Sidebar/Basic/Groups/LoadSample.qml @@ -1,5 +1,5 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 +import QtQuick +import QtQuick.Controls import QtQuick.Dialogs import EasyApp.Gui.Style as EaStyle diff --git a/EasyReflectometryApp/Gui/SldChart.qml b/EasyReflectometryApp/Gui/SldChart.qml index 305d3969..908b28b3 100644 --- a/EasyReflectometryApp/Gui/SldChart.qml +++ b/EasyReflectometryApp/Gui/SldChart.qml @@ -328,7 +328,7 @@ Rectangle { Timer { id: resetAxesTimer - interval: 50 + interval: 75 repeat: false onTriggered: chartView.resetAxes() }