diff --git a/EasyReflectometryApp/Backends/Py/analysis.py b/EasyReflectometryApp/Backends/Py/analysis.py index 14217d4d..7a738433 100644 --- a/EasyReflectometryApp/Backends/Py/analysis.py +++ b/EasyReflectometryApp/Backends/Py/analysis.py @@ -214,6 +214,7 @@ def _on_fit_progress(self, payload: dict) -> None: def _on_fit_finished(self, results: list) -> None: """Handle successful completion of threaded fit.""" self._fitting_logic.on_fit_finished(results) + self._project_lib._last_fit_results = self._fitting_logic.last_fit_results self._fitter_thread = None self.fittingChanged.emit() self._clearCacheAndEmitParametersChanged() diff --git a/EasyReflectometryApp/Backends/Py/logic/fitting.py b/EasyReflectometryApp/Backends/Py/logic/fitting.py index 919e92ff..7ff09b79 100644 --- a/EasyReflectometryApp/Backends/Py/logic/fitting.py +++ b/EasyReflectometryApp/Backends/Py/logic/fitting.py @@ -294,6 +294,10 @@ def on_fit_finished(self, results: FitResults | List[FitResults]) -> None: self._result = single_result self._results = [single_result] if single_result is not None else [] + @property + def last_fit_results(self): + return self._results if self._results else None + @property def fit_n_pars(self) -> int: """Return the global number of refined parameters for the fit.""" @@ -309,7 +313,7 @@ def fit_chi2(self) -> float: if self._results: try: if len(self._results) == 1: - return float(self._results[0].reduced_chi) + return float(self._results[0].reduced_chi2) total_chi2 = float(sum(result.chi2 for result in self._results)) total_points = sum(len(result.x) for result in self._results) n_params = self._results[0].n_pars @@ -322,7 +326,7 @@ def fit_chi2(self) -> float: if self._result is None: return 0.0 try: - return float(self._result.reduced_chi) + return float(self._result.reduced_chi2) except (ValueError, TypeError): return 0.0 diff --git a/EasyReflectometryApp/Gui/Pages/Summary/MainContent/Summary.qml b/EasyReflectometryApp/Gui/Pages/Summary/MainContent/Summary.qml index 43df99d9..ccc17229 100644 --- a/EasyReflectometryApp/Gui/Pages/Summary/MainContent/Summary.qml +++ b/EasyReflectometryApp/Gui/Pages/Summary/MainContent/Summary.qml @@ -53,4 +53,27 @@ Rectangle { } // Flickable + // Tooltip for truncated experiment datablock names. + // Python wraps long names in . + // TextEdit.hoveredLink fires when the mouse enters/leaves such a link; + // we decode the full name and show it in a native ToolTip near the cursor. + HoverHandler { + id: hoverHandler + } + + ToolTip { + id: nameToolTip + + readonly property string scheme: "nametooltip:" + + visible: textArea.hoveredLink.startsWith(scheme) + text: visible ? decodeURIComponent(textArea.hoveredLink.substring(scheme.length)) : "" + + x: hoverHandler.point.position.x + 12 + y: hoverHandler.point.position.y + 12 + + delay: 300 + timeout: 8000 + } + } diff --git a/pyproject.toml b/pyproject.toml index 9d9448f4..c3749c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ requires-python = '>=3.12' dependencies = [ 'easyapplication', - 'easyreflectometry', + 'easyreflectometry @ git+https://github.com/easyscience/reflectometry-lib.git@develop', 'asteval', 'PySide6', 'toml', diff --git a/tests/factories.py b/tests/factories.py index 147f33bf..38d5fe3d 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -292,12 +292,12 @@ def make_dependent_on(self, dependency_expression, dependency_map): class FakeFitResult: - def __init__(self, success=True, chi2=1.0, n_pars=1, x=None, reduced_chi=0.5, minimizer_engine='stub'): + def __init__(self, success=True, chi2=1.0, n_pars=1, x=None, reduced_chi2=0.5, minimizer_engine='stub'): self.success = success self.chi2 = chi2 self.n_pars = n_pars self.x = [] if x is None else x - self.reduced_chi = reduced_chi + self.reduced_chi2 = reduced_chi2 self.minimizer_engine = minimizer_engine diff --git a/tests/test_logic_fitting.py b/tests/test_logic_fitting.py index ec1a137f..a1153ab3 100644 --- a/tests/test_logic_fitting.py +++ b/tests/test_logic_fitting.py @@ -89,8 +89,8 @@ def test_on_fit_finished_and_fit_properties_cover_multi_and_single_results(monke logic.prepare_for_threaded_fit() logic.on_fit_finished([ - make_fit_result(success=True, chi2=4.0, n_pars=2, x=[1, 2, 3], reduced_chi=1.1), - make_fit_result(success=True, chi2=6.0, n_pars=2, x=[1, 2, 3, 4], reduced_chi=1.2), + make_fit_result(success=True, chi2=4.0, n_pars=2, x=[1, 2, 3], reduced_chi2=1.1), + make_fit_result(success=True, chi2=6.0, n_pars=2, x=[1, 2, 3, 4], reduced_chi2=1.2), ]) assert logic.fit_finished is True @@ -98,12 +98,29 @@ def test_on_fit_finished_and_fit_properties_cover_multi_and_single_results(monke assert logic.fit_n_pars == 2 assert logic.fit_chi2 == 2.0 - logic.on_fit_finished(make_fit_result(success=False, chi2=9.0, n_pars=1, x=[1, 2], reduced_chi=4.5)) + logic.on_fit_finished(make_fit_result(success=False, chi2=9.0, n_pars=1, x=[1, 2], reduced_chi2=4.5)) assert logic.fit_success is False assert logic.fit_n_pars == 1 assert logic.fit_chi2 == 4.5 +def test_last_fit_results_reflects_stored_results(): + project = make_project() + logic = fitting_module.Fitting(project) + + assert logic.last_fit_results is None + + multi_results = [ + make_fit_result(success=True, chi2=2.0, n_pars=1, x=[1, 2], reduced_chi2=1.0), + make_fit_result(success=True, chi2=3.0, n_pars=1, x=[1, 2, 3], reduced_chi2=1.5), + ] + logic.on_fit_finished(multi_results) + assert logic.last_fit_results is multi_results + + logic.on_fit_finished(make_fit_result(success=True, chi2=1.0, n_pars=1, x=[1], reduced_chi2=1.0)) + assert len(logic.last_fit_results) == 1 + + def test_fit_n_pars_uses_global_free_parameter_count_for_multi_experiment_results(monkeypatch): project = make_project() logic = fitting_module.Fitting(project) @@ -111,8 +128,8 @@ def test_fit_n_pars_uses_global_free_parameter_count_for_multi_experiment_result logic.prepare_for_threaded_fit() logic.on_fit_finished([ - make_fit_result(success=True, chi2=4.0, n_pars=3, x=[1, 2, 3], reduced_chi=1.1), - make_fit_result(success=True, chi2=6.0, n_pars=3, x=[1, 2, 3, 4], reduced_chi=1.2), + make_fit_result(success=True, chi2=4.0, n_pars=3, x=[1, 2, 3], reduced_chi2=1.1), + make_fit_result(success=True, chi2=6.0, n_pars=3, x=[1, 2, 3, 4], reduced_chi2=1.2), ]) assert logic.fit_n_pars == 3 @@ -149,7 +166,7 @@ def test_fit_progress_state_resets_on_finish_failure_and_stop(): logic.prepare_for_threaded_fit() logic.on_fit_progress({'iteration': 3, 'chi2': 8.0, 'parameter_values': {'beta': 1.0}}) - logic.on_fit_finished(make_fit_result(success=True, chi2=8.0, n_pars=1, x=[1, 2], reduced_chi=4.0)) + logic.on_fit_finished(make_fit_result(success=True, chi2=8.0, n_pars=1, x=[1, 2], reduced_chi2=4.0)) assert logic.fit_iteration == 0 assert logic.fit_progress_message == '' @@ -191,7 +208,7 @@ def test_fit_failure_and_cancellation_state_transitions(): def test_start_stop_handles_success_and_fiterror(): project = make_project(models=[object()]) - project.fitter = SimpleNamespace(fit_single_data_set_1d=lambda exp_data: make_fit_result(success=True, chi2=1.7, reduced_chi=1.7)) + project.fitter = SimpleNamespace(fit_single_data_set_1d=lambda exp_data: make_fit_result(success=True, chi2=1.7, reduced_chi2=1.7)) logic = fitting_module.Fitting(project) logic.start_stop()