From b992e4a44ac98fafad7c0b1ae1295089dc070e06 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 10 Mar 2021 11:41:59 +0100 Subject: [PATCH 1/2] Multithreaded fits implementation from DAS-287 --- .../Gui/Components/AnalysisFitables.qml | 1 + .../Gui/Components/ResultsDialog.qml | 40 +++++++++++++++++ easyDiffractionApp/Gui/Components/qmldir | 3 ++ easyDiffractionApp/Gui/Logic/PyQmlProxy.js | 1 + .../Gui/Pages/Analysis/SideBarBasic.qml | 40 +++++++---------- .../Gui/Pages/Experiment/SideBarBasic.qml | 1 + .../Gui/Pages/Sample/SideBarBasic.qml | 1 + easyDiffractionApp/Logic/Fitter.py | 28 ++++++++++++ easyDiffractionApp/Logic/PyQmlProxy.py | 44 ++++++++++++++++--- 9 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 easyDiffractionApp/Gui/Components/ResultsDialog.qml create mode 100644 easyDiffractionApp/Logic/Fitter.py diff --git a/easyDiffractionApp/Gui/Components/AnalysisFitables.qml b/easyDiffractionApp/Gui/Components/AnalysisFitables.qml index 049c268d..210fc552 100644 --- a/easyDiffractionApp/Gui/Components/AnalysisFitables.qml +++ b/easyDiffractionApp/Gui/Components/AnalysisFitables.qml @@ -14,6 +14,7 @@ EaComponents.TableView { maxRowCountShow: 8 defaultInfoText: qsTr("No Parameters Found") + enabled: ExGlobals.Constants.proxy.isFitFinished // Table model diff --git a/easyDiffractionApp/Gui/Components/ResultsDialog.qml b/easyDiffractionApp/Gui/Components/ResultsDialog.qml new file mode 100644 index 00000000..c1a4289a --- /dev/null +++ b/easyDiffractionApp/Gui/Components/ResultsDialog.qml @@ -0,0 +1,40 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.XmlListModel 2.14 + +import easyAppGui.Globals 1.0 as EaGlobals +import easyAppGui.Style 1.0 as EaStyle +import easyAppGui.Elements 1.0 as EaElements +import easyAppGui.Components 1.0 as EaComponents + +import Gui.Globals 1.0 as ExGlobals +import Gui.Components 1.0 as ExComponents + +// Info dialog (after refinement) +EaElements.Dialog { + id: refinementResultsDialog + parent: Overlay.overlay + + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + + // modal: true + standardButtons: Dialog.Ok + + title: qsTr("Refinement Results") + + Column { + EaElements.Label { text: gotResults() && ExGlobals.Constants.proxy.isFitFinished ? `Success: ${ExGlobals.Constants.proxy.fitResults.success}` : "" } + EaElements.Label { text: gotResults() && ExGlobals.Constants.proxy.isFitFinished ? `Num. refined parameters: ${ExGlobals.Constants.proxy.fitResults.nvarys}` : "Fitting in progress..." } + EaElements.Label { text: gotResults() && ExGlobals.Constants.proxy.isFitFinished ? `Goodness-of-fit (reduced \u03c7\u00b2): ${ExGlobals.Constants.proxy.fitResults.redchi2.toFixed(2)}` : "" } + } + + function gotResults(){ + if ((ExGlobals.Constants.proxy.fitResults != null) && (ExGlobals.Constants.proxy.fitResults.success != null)){ + return true + } + return false + } + +} + diff --git a/easyDiffractionApp/Gui/Components/qmldir b/easyDiffractionApp/Gui/Components/qmldir index aedc0fd9..0d6e5479 100644 --- a/easyDiffractionApp/Gui/Components/qmldir +++ b/easyDiffractionApp/Gui/Components/qmldir @@ -2,6 +2,9 @@ module Components ApplicationWindow 1.0 ApplicationWindow.qml +# Separate popup windows +ResultsDialog 1.0 ResultsDialog.qml + # Main content components SampleStructure3dVtk 1.0 SampleStructure3dVtk.qml diff --git a/easyDiffractionApp/Gui/Logic/PyQmlProxy.js b/easyDiffractionApp/Gui/Logic/PyQmlProxy.js index c1388fe6..953dba84 100644 --- a/easyDiffractionApp/Gui/Logic/PyQmlProxy.js +++ b/easyDiffractionApp/Gui/Logic/PyQmlProxy.js @@ -62,6 +62,7 @@ class PyQmlProxy { get experimentLoaded() { return true } get experimentSkipped() { return false } + get isFitFinished() { return true } get showMeasuredSeries() { return true } get showDifferenceChart() { return true } diff --git a/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml b/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml index 2673bb65..6340d7e6 100644 --- a/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml +++ b/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml @@ -179,43 +179,37 @@ EaComponents.SideBarColumn { // Start fitting button EaElements.SideBarButton { + id: fitButton enabled: ExGlobals.Constants.proxy.experimentLoaded - fontIcon: "play-circle" - text: qsTr("Start fitting") - + fontIcon: ExGlobals.Constants.proxy.isFitFinished ? "play-circle" : "pause-circle" + text: ExGlobals.Constants.proxy.isFitFinished ? qsTr("Start fitting") : qsTr("Stop fitting") wide: true - onClicked: { - //print("Start fitting button clicked") ExGlobals.Constants.proxy.fit() - refinementResultsDialog.open() + } + Component.onCompleted: { + if (gotResults() && ExGlobals.Constants.proxy.isFitFinished) { + refinementResultsDialog.open() + } } } } - // Info dialog (after refinement) - - EaElements.Dialog { + ExComponents.ResultsDialog { id: refinementResultsDialog - - parent: Overlay.overlay - + enabled: gotResults() && ExGlobals.Constants.proxy.isFitFinished + visible: gotResults() && ExGlobals.Constants.proxy.isFitFinished x: (parent.width - width) * 0.5 y: (parent.height - height) * 0.5 - - modal: true - standardButtons: Dialog.Ok - - title: qsTr("Refinement Results") - - Column { - EaElements.Label { text: typeof ExGlobals.Constants.proxy.fitResults !== 'undefined' ? `Success: ${ExGlobals.Constants.proxy.fitResults.success}` : "" } - EaElements.Label { text: typeof ExGlobals.Constants.proxy.fitResults !== 'undefined' ? `Num. refined parameters: ${ExGlobals.Constants.proxy.fitResults.nvarys}` : "" } - EaElements.Label { text: typeof ExGlobals.Constants.proxy.fitResults !== 'undefined' && typeof ExGlobals.Constants.proxy.fitResults.redchi2 !== 'undefined' ? `Goodness-of-fit (reduced \u03c7\u00b2): ${ExGlobals.Constants.proxy.fitResults.redchi2.toFixed(2)}` : "" } - } } // Logic + function gotResults(){ + if ((ExGlobals.Constants.proxy.fitResults != null) && (ExGlobals.Constants.proxy.fitResults.success != null)){ + return true + } + return false + } function formatFilterText(group_icon, icon, text) { if (icon === "") diff --git a/easyDiffractionApp/Gui/Pages/Experiment/SideBarBasic.qml b/easyDiffractionApp/Gui/Pages/Experiment/SideBarBasic.qml index 56ff1c65..dfe3e609 100644 --- a/easyDiffractionApp/Gui/Pages/Experiment/SideBarBasic.qml +++ b/easyDiffractionApp/Gui/Pages/Experiment/SideBarBasic.qml @@ -16,6 +16,7 @@ EaComponents.SideBarColumn { EaElements.GroupBox { title: qsTr("Experimental data") collapsible: false + enabled: ExGlobals.Constants.proxy.isFitFinished ExComponents.ExperimentDataExplorer {} diff --git a/easyDiffractionApp/Gui/Pages/Sample/SideBarBasic.qml b/easyDiffractionApp/Gui/Pages/Sample/SideBarBasic.qml index 940a0e69..fb0b9832 100644 --- a/easyDiffractionApp/Gui/Pages/Sample/SideBarBasic.qml +++ b/easyDiffractionApp/Gui/Pages/Sample/SideBarBasic.qml @@ -16,6 +16,7 @@ EaComponents.SideBarColumn { EaElements.GroupBox { title: qsTr("Structural phases") collapsible: false + enabled: ExGlobals.Constants.proxy.isFitFinished ExComponents.SamplePhasesExplorer {} diff --git a/easyDiffractionApp/Logic/Fitter.py b/easyDiffractionApp/Logic/Fitter.py new file mode 100644 index 00000000..63cc5df2 --- /dev/null +++ b/easyDiffractionApp/Logic/Fitter.py @@ -0,0 +1,28 @@ +from PySide2.QtCore import Signal, QThread + + +class Fitter(QThread): + """ + Simple wrapper for calling a function in separate thread + """ + failed = Signal(str) + finished = Signal(dict) + + def __init__(self, obj, method_name, *args, **kwargs): + QThread.__init__(self, None) + self._obj = obj + self.method_name = method_name + self.args = args + self.kwargs = kwargs + + def run(self): + res = {} + if hasattr(self._obj, self.method_name): + func = getattr(self._obj, self.method_name) + try: + res = func(*self.args, **self.kwargs) + except Exception as ex: + self.failed.emit(str(ex)) + return str(ex) + self.finished.emit(res) + return res diff --git a/easyDiffractionApp/Logic/PyQmlProxy.py b/easyDiffractionApp/Logic/PyQmlProxy.py index 024bc476..9ebf9f61 100644 --- a/easyDiffractionApp/Logic/PyQmlProxy.py +++ b/easyDiffractionApp/Logic/PyQmlProxy.py @@ -22,11 +22,10 @@ from easyCore.Symmetry.tools import SpacegroupInfo from easyCore.Fitting.Fitting import Fitter -from easyCore.Fitting.Constraints import ObjConstraint, NumericConstraint from easyCore.Utils.classTools import generatePath from easyDiffractionLib.sample import Sample -from easyDiffractionLib import Phases, Phase, Lattice, Site, Atoms, SpaceGroup +from easyDiffractionLib import Phases, Phase, Lattice, Site, SpaceGroup from easyDiffractionLib.interface import InterfaceFactory from easyDiffractionLib.Elements.Experiments.Experiment import Pars1D from easyDiffractionLib.Elements.Experiments.Pattern import Pattern1D @@ -39,6 +38,7 @@ from easyDiffractionApp.Logic.Proxies.MatplotlibBackend import MatplotlibBridge from easyDiffractionApp.Logic.Proxies.QtChartsBackend import QtChartsBridge from easyDiffractionApp.Logic.Proxies.BokehBackend import BokehBridge +from easyDiffractionApp.Logic.Fitter import Fitter as ThreadedFitter from easyDiffractionApp.Logic.ScreenRecorder import ScreenRecorder @@ -107,7 +107,7 @@ class PyQmlProxy(QObject): # Status info statusInfoChanged = Signal() - + # Misc dummySignal = Signal() @@ -234,6 +234,10 @@ def __init__(self, parent=None): self.currentMinimizerChanged.connect(self.statusInfoChanged) self.currentMinimizerMethodChanged.connect(self.statusInfoChanged) + # Multithreading + self._fitter_thread = None + self._fit_finished = True + # Screen recorder self._screen_recorder = ScreenRecorder() @@ -1265,6 +1269,7 @@ def _onCurrentCalculatorChanged(self): @Slot() def fit(self): + self.isFitFinished = False exp_data = self._data.experiments[0] x = exp_data.x @@ -1272,15 +1277,34 @@ def fit(self): weights = 1 / exp_data.e method = self._current_minimizer_method_name - res = self.fitter.fit(x, y, weights=weights, method=method) + args = (x, y) + kwargs = {"weights": weights, "method": method} + self._fitter_thread = ThreadedFitter(self.fitter, 'fit', *args, **kwargs) + self._fitter_thread.finished.connect(self._setFitResults) + self._fitter_thread.failed.connect(self._setFitResultsFailed) + self._fitter_thread.start() - self._setFitResults(res) - self.fitFinished.emit() + # self._setFitResults(res) + # self.fitFinished.emit() @Property('QVariant', notify=fitResultsChanged) def fitResults(self): return self._fit_results + @Property(bool, notify=fitFinished) + # @Property(bool, constant=True) + def isFitFinished(self): + print('\n\n isfitFinished called') + return self._fit_finished + + @isFitFinished.setter + def isFitFinished(self, fit_finished: bool): + print('\n\n isFitFinishedSetter called') + if self._fit_finished == fit_finished: + return + self._fit_finished = fit_finished + self.fitFinished.emit() + def _defaultFitResults(self): return { "success": None, @@ -1297,6 +1321,14 @@ def _setFitResults(self, res): "redchi2": float(res.reduced_chi) } self.fitResultsChanged.emit() + self.isFitFinished = True + self.fitFinished.emit() + + def _setFitResultsFailed(self, res): + print("FIT FAILED") + print(str(res)) + self.isFitFinished = True + self.fitFinished.emit() def _onFitFinished(self): print("***** _onFitFinished") From 68d7c881ec6a92badc29664959a31c37fcfd35b7 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 10 Mar 2021 13:13:42 +0100 Subject: [PATCH 2/2] Refactor and clean up fitting results dialog --- .../Gui/Components/AnalysisFitables.qml | 3 +- .../Gui/Components/ResultsDialog.qml | 42 ++++++++++--------- easyDiffractionApp/Gui/Components/qmldir | 7 ++-- .../Gui/Pages/Analysis/SideBarBasic.qml | 26 +++--------- easyDiffractionApp/Logic/PyQmlProxy.py | 5 +-- pyproject.toml | 1 - 6 files changed, 36 insertions(+), 48 deletions(-) diff --git a/easyDiffractionApp/Gui/Components/AnalysisFitables.qml b/easyDiffractionApp/Gui/Components/AnalysisFitables.qml index 210fc552..f7e1111a 100644 --- a/easyDiffractionApp/Gui/Components/AnalysisFitables.qml +++ b/easyDiffractionApp/Gui/Components/AnalysisFitables.qml @@ -12,9 +12,10 @@ import Gui.Globals 1.0 as ExGlobals EaComponents.TableView { id: table + enabled: ExGlobals.Constants.proxy.isFitFinished + maxRowCountShow: 8 defaultInfoText: qsTr("No Parameters Found") - enabled: ExGlobals.Constants.proxy.isFitFinished // Table model diff --git a/easyDiffractionApp/Gui/Components/ResultsDialog.qml b/easyDiffractionApp/Gui/Components/ResultsDialog.qml index c1a4289a..4a184bc5 100644 --- a/easyDiffractionApp/Gui/Components/ResultsDialog.qml +++ b/easyDiffractionApp/Gui/Components/ResultsDialog.qml @@ -1,40 +1,44 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 -import QtQuick.XmlListModel 2.14 +import QtQuick 2.13 +import QtQuick.Controls 2.13 import easyAppGui.Globals 1.0 as EaGlobals import easyAppGui.Style 1.0 as EaStyle import easyAppGui.Elements 1.0 as EaElements -import easyAppGui.Components 1.0 as EaComponents import Gui.Globals 1.0 as ExGlobals -import Gui.Components 1.0 as ExComponents -// Info dialog (after refinement) EaElements.Dialog { - id: refinementResultsDialog + property bool gotResults: typeof ExGlobals.Constants.proxy.fitResults.success !== 'undefined' && + ExGlobals.Constants.proxy.isFitFinished + + title: qsTr("Refinement Results") + parent: Overlay.overlay x: (parent.width - width) * 0.5 y: (parent.height - height) * 0.5 - // modal: true + modal: true standardButtons: Dialog.Ok - title: qsTr("Refinement Results") - Column { - EaElements.Label { text: gotResults() && ExGlobals.Constants.proxy.isFitFinished ? `Success: ${ExGlobals.Constants.proxy.fitResults.success}` : "" } - EaElements.Label { text: gotResults() && ExGlobals.Constants.proxy.isFitFinished ? `Num. refined parameters: ${ExGlobals.Constants.proxy.fitResults.nvarys}` : "Fitting in progress..." } - EaElements.Label { text: gotResults() && ExGlobals.Constants.proxy.isFitFinished ? `Goodness-of-fit (reduced \u03c7\u00b2): ${ExGlobals.Constants.proxy.fitResults.redchi2.toFixed(2)}` : "" } - } + EaElements.Label { + text: gotResults + ? `Success: ${ExGlobals.Constants.proxy.fitResults.success}` + : "" + } - function gotResults(){ - if ((ExGlobals.Constants.proxy.fitResults != null) && (ExGlobals.Constants.proxy.fitResults.success != null)){ - return true + EaElements.Label { + text: gotResults + ? `Num. refined parameters: ${ExGlobals.Constants.proxy.fitResults.nvarys}` + : "" } - return false - } + EaElements.Label { + text: gotResults + ? `Goodness-of-fit (reduced \u03c7\u00b2): ${ExGlobals.Constants.proxy.fitResults.redchi2.toFixed(2)}` + : "" + } + } } diff --git a/easyDiffractionApp/Gui/Components/qmldir b/easyDiffractionApp/Gui/Components/qmldir index 0d6e5479..ad68b7ee 100644 --- a/easyDiffractionApp/Gui/Components/qmldir +++ b/easyDiffractionApp/Gui/Components/qmldir @@ -2,9 +2,6 @@ module Components ApplicationWindow 1.0 ApplicationWindow.qml -# Separate popup windows -ResultsDialog 1.0 ResultsDialog.qml - # Main content components SampleStructure3dVtk 1.0 SampleStructure3dVtk.qml @@ -36,3 +33,7 @@ ExperimentBackground 1.0 ExperimentBackground.qml AnalysisFitables 1.0 AnalysisFitables.qml AnalysisConstraints 1.0 AnalysisConstraints.qml + +# Separate components + +ResultsDialog 1.0 ResultsDialog.qml diff --git a/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml b/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml index 6340d7e6..e4868a7c 100644 --- a/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml +++ b/easyDiffractionApp/Gui/Pages/Analysis/SideBarBasic.qml @@ -173,43 +173,27 @@ EaComponents.SideBarColumn { validator: sliderFromLabel.validator maximumLength: sliderFromLabel.maximumLength text: slider.to.toFixed(4) - onEditingFinished: {} } } // Start fitting button EaElements.SideBarButton { id: fitButton + wide: true enabled: ExGlobals.Constants.proxy.experimentLoaded fontIcon: ExGlobals.Constants.proxy.isFitFinished ? "play-circle" : "pause-circle" text: ExGlobals.Constants.proxy.isFitFinished ? qsTr("Start fitting") : qsTr("Stop fitting") - wide: true - onClicked: { - ExGlobals.Constants.proxy.fit() - } - Component.onCompleted: { - if (gotResults() && ExGlobals.Constants.proxy.isFitFinished) { - refinementResultsDialog.open() - } - } + onClicked: ExGlobals.Constants.proxy.fit() } } + // Init results dialog ExComponents.ResultsDialog { - id: refinementResultsDialog - enabled: gotResults() && ExGlobals.Constants.proxy.isFitFinished - visible: gotResults() && ExGlobals.Constants.proxy.isFitFinished - x: (parent.width - width) * 0.5 - y: (parent.height - height) * 0.5 + visible: typeof ExGlobals.Constants.proxy.fitResults.success !== 'undefined' && + ExGlobals.Constants.proxy.isFitFinished } // Logic - function gotResults(){ - if ((ExGlobals.Constants.proxy.fitResults != null) && (ExGlobals.Constants.proxy.fitResults.success != null)){ - return true - } - return false - } function formatFilterText(group_icon, icon, text) { if (icon === "") diff --git a/easyDiffractionApp/Logic/PyQmlProxy.py b/easyDiffractionApp/Logic/PyQmlProxy.py index 9ebf9f61..9f799872 100644 --- a/easyDiffractionApp/Logic/PyQmlProxy.py +++ b/easyDiffractionApp/Logic/PyQmlProxy.py @@ -1292,14 +1292,13 @@ def fitResults(self): return self._fit_results @Property(bool, notify=fitFinished) - # @Property(bool, constant=True) def isFitFinished(self): - print('\n\n isfitFinished called') + print('+ isfitFinished called') return self._fit_finished @isFitFinished.setter def isFitFinished(self, fit_finished: bool): - print('\n\n isFitFinishedSetter called') + print('+ isFitFinished.setter called', fit_finished) if self._fit_finished == fit_finished: return self._fit_finished = fit_finished diff --git a/pyproject.toml b/pyproject.toml index b219e5c0..e939956e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ vtk = '^8.1.2' pyobjc-core = { version = '^7.1', platform = 'darwin' } pyobjc-framework-cocoa = { version = '^7.1', platform = 'darwin' } # easyScience -#easyCore = { git = 'https://github.com/easyScience/easyCore.git', rev = 'xarray' } easyDiffractionLib = { git = 'https://github.com/easyScience/easyDiffractionLib.git', rev = 'develop' } easyAppLogic = { git = 'https://github.com/easyScience/easyAppLogic.git', rev = 'develop' } easyAppGui = { git = 'https://github.com/easyScience/easyAppGui.git', rev = 'develop' }