diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml
index 4178af026a..c4ef451108 100644
--- a/.github/workflows/installers.yml
+++ b/.github/workflows/installers.yml
@@ -72,7 +72,8 @@ jobs:
python -m pip install numpy scipy==1.7.3 docutils "pytest<6" sphinx unittest-xml-reporting
python -m pip install tinycc h5py sphinx pyparsing html5lib reportlab==3.6.6 pybind11 appdirs
python -m pip install six numba mako ipython qtconsole xhtml2pdf unittest-xml-reporting pylint
- python -m pip install qt5reactor periodictable uncertainties debugpy
+ python -m pip install qt5reactor periodictable uncertainties debugpy dominate
+
- name: Install PyQt (Windows + Linux)
if: ${{ matrix.os != 'macos-latest' }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 54f1aa6768..3cd117db22 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -74,7 +74,9 @@ jobs:
python -m pip install numpy scipy==1.7.3 docutils "pytest<6" sphinx unittest-xml-reporting
python -m pip install tinycc h5py sphinx pyparsing html5lib reportlab==3.6.6 pybind11 appdirs
python -m pip install six numba mako ipython qtconsole xhtml2pdf unittest-xml-reporting pylint
- python -m pip install qt5reactor periodictable uncertainties debugpy
+ python -m pip install qt5reactor periodictable uncertainties debugpy dominate importlib_resources
+ python -m pip install html2text
+
- name: Install PyQt (Windows + Linux)
if: ${{ matrix.os != 'macos-latest' }}
diff --git a/.gitignore b/.gitignore
index 9ab7a67578..8990c97167 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,9 @@ default_categories.json
**/logs
tests.log
+# Test reports
+/src/sas/qtgui/Utilities/Reports/report_test*
+
# Installer files
/sasview-install
/installers/build
diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt
index 43dbb92a8d..033afa2d1b 100644
--- a/build_tools/requirements.txt
+++ b/build_tools/requirements.txt
@@ -29,3 +29,7 @@ matplotlib==3.4.3; sys_platform == 'darwin'
lxml
pytools
cffi
+dominate
+html5lib
+importlib-resources
+importlib_resources
\ No newline at end of file
diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py
index 632c55d941..9aae37e7e7 100644
--- a/src/sas/qtgui/MainWindow/DataExplorer.py
+++ b/src/sas/qtgui/MainWindow/DataExplorer.py
@@ -183,8 +183,8 @@ def enableGraphCombo(self, combo_text):
"""
Enables/disables "Assign Plot" elements
"""
- self.cbgraph.setEnabled(len(PlotHelper.currentPlots()) > 0)
- self.cmdAppend.setEnabled(len(PlotHelper.currentPlots()) > 0)
+ self.cbgraph.setEnabled(len(PlotHelper.currentPlotIds()) > 0)
+ self.cmdAppend.setEnabled(len(PlotHelper.currentPlotIds()) > 0)
def initPerspectives(self):
"""
@@ -956,7 +956,7 @@ def updateGraphCount(self, graphs):
deleted graphs
"""
graph2, delete = graphs
- graph_list = PlotHelper.currentPlots()
+ graph_list = PlotHelper.currentPlotIds()
self.updateGraphCombo(graph_list)
if not self.active_plots:
@@ -983,11 +983,21 @@ def updatePerspectiveCombo(self, index):
"""
Notify the gui manager about the new perspective chosen.
"""
+
+ # Notify via communicator
self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
- self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
- # Deactivate and uncheck the swap data option if the current perspective does not allow it
- self.chkSwap.setEnabled(self.parent.perspective().allowSwap())
- if not self.parent.perspective().allowSwap():
+
+ # Set checkboxes
+ current_perspective = self.parent.perspective()
+
+ allow_batch = False if current_perspective is None else current_perspective.allowBatch()
+ allow_swap = False if current_perspective is None else current_perspective.allowSwap()
+
+ self.chkBatch.setEnabled(allow_batch)
+ self.chkSwap.setEnabled(allow_swap)
+
+ # Using this conditional prevents the checkbox for going into the "neither checked nor unchecked" state
+ if not allow_swap:
self.chkSwap.setCheckState(False)
def itemFromDisplayName(self, name):
@@ -1213,7 +1223,7 @@ def appendPlot(self):
# old plot data
plot_id = str(self.cbgraph.currentText())
try:
- assert plot_id in PlotHelper.currentPlots(), "No such plot: %s" % (plot_id)
+ assert plot_id in PlotHelper.currentPlotIds(), "No such plot: %s" % (plot_id)
except:
return
@@ -1828,7 +1838,7 @@ def closeAllPlots(self):
Close all currently displayed plots
"""
- for plot_id in PlotHelper.currentPlots():
+ for plot_id in PlotHelper.currentPlotIds():
try:
plotter = PlotHelper.plotById(plot_id)
plotter.close()
@@ -1841,7 +1851,7 @@ def minimizeAllPlots(self):
"""
Minimize all currently displayed plots
"""
- for plot_id in PlotHelper.currentPlots():
+ for plot_id in PlotHelper.currentPlotIds():
plotter = PlotHelper.plotById(plot_id)
plotter.showMinimized()
@@ -1853,7 +1863,7 @@ def closePlotsForItem(self, item):
# {} -> 'Graph1' : HashableStandardItem()
current_plot_items = {}
- for plot_name in PlotHelper.currentPlots():
+ for plot_name in PlotHelper.currentPlotIds():
current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
# item and its hashable children
@@ -1883,15 +1893,17 @@ def closePlotsForItem(self, item):
pass # debugger anchor
- def onAnalysisUpdate(self, new_perspective=""):
+ def onAnalysisUpdate(self, new_perspective_name: str):
"""
Update the perspective combo index based on passed string
"""
- assert new_perspective in Perspectives.PERSPECTIVES.keys()
+ assert new_perspective_name in Perspectives.PERSPECTIVES.keys()
+
self.cbFitting.blockSignals(True)
- self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
+ index = self.cbFitting.findText(new_perspective_name)
+ self.cbFitting.setCurrentIndex(index)
self.cbFitting.blockSignals(False)
- pass
+
def loadComplete(self, output):
"""
diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py
index dd93bd494d..5afadc8296 100644
--- a/src/sas/qtgui/MainWindow/GuiManager.py
+++ b/src/sas/qtgui/MainWindow/GuiManager.py
@@ -1,14 +1,15 @@
import sys
import os
-import subprocess
import logging
import json
import webbrowser
import traceback
+from typing import Optional
+
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
-from PyQt5.QtCore import Qt, QLocale, QUrl
+from PyQt5.QtCore import Qt, QLocale
import matplotlib as mpl
mpl.use("Qt5Agg")
@@ -18,7 +19,7 @@
from twisted.internet import reactor
# General SAS imports
-from sas import get_local_config, get_custom_config
+from sas import get_custom_config
from sas.qtgui.Utilities.ConnectionProxy import ConnectionProxy
from sas.qtgui.Utilities.SasviewLogger import setup_qt_logging
@@ -31,7 +32,7 @@
from sas.qtgui.Utilities.GridPanel import BatchOutputPanel
from sas.qtgui.Utilities.ResultPanel import ResultPanel
-from sas.qtgui.Utilities.ReportDialog import ReportDialog
+from sas.qtgui.Utilities.Reports.ReportDialog import ReportDialog
from sas.qtgui.MainWindow.Acknowledgements import Acknowledgements
from sas.qtgui.MainWindow.AboutBox import AboutBox
from sas.qtgui.MainWindow.WelcomePanel import WelcomePanel
@@ -48,12 +49,19 @@
from sas.qtgui.Calculators.ResolutionCalculatorPanel import ResolutionCalculatorPanel
from sas.qtgui.Calculators.DataOperationUtilityPanel import DataOperationUtilityPanel
+
import sas.qtgui.Plotting.PlotHelper as PlotHelper
# Perspectives
import sas.qtgui.Perspectives as Perspectives
+from sas.qtgui.Perspectives.perspective import Perspective
+
from sas.qtgui.Perspectives.Fitting.FittingPerspective import FittingWindow
-from sas.qtgui.MainWindow.DataExplorer import DataExplorerWindow, DEFAULT_PERSPECTIVE
+from sas.qtgui.Perspectives.Corfunc.CorfuncPerspective import CorfuncWindow
+from sas.qtgui.Perspectives.Invariant.InvariantPerspective import InvariantWindow
+from sas.qtgui.Perspectives.Inversion.InversionPerspective import InversionWindow
+
+from sas.qtgui.MainWindow.DataExplorer import DataExplorerWindow
from sas.qtgui.Utilities.AddMultEditor import AddMultEditor
from sas.qtgui.Utilities.ImageViewer import ImageViewer
@@ -62,7 +70,7 @@
logger = logging.getLogger(__name__)
-class GuiManager(object):
+class GuiManager:
"""
Main SasView window functionality
"""
@@ -93,7 +101,7 @@ def __init__(self, parent=None):
self.addTriggers()
# Currently displayed perspective
- self._current_perspective = None
+ self._current_perspective: Optional[Perspective] = None
self.loadedPerspectives = {}
# Populate the main window with stuff
@@ -259,7 +267,7 @@ def plotSelectedSlot(self, plot_name):
Set focus on the selected plot
"""
# loop over all visible plots and find the requested plot
- for plot in PlotHelper.currentPlots():
+ for plot in PlotHelper.currentPlotIds():
# take last plot
if PlotHelper.plotById(plot).data[-1].name == plot_name:
# set focus on the plot
@@ -341,32 +349,82 @@ def workspace(self):
"""
return self._workspace.workspace
- def perspectiveChanged(self, perspective_name):
+ def perspectiveChanged(self, new_perspective_name: str):
"""
Respond to change of the perspective signal
"""
- # Remove the previous perspective from the window
- self.clearPerspectiveMenubarOptions(self._current_perspective)
- if self._current_perspective:
+
+ assert new_perspective_name in self.loadedPerspectives # supplied name should always be in loaded perspectives
+
+ # Uncheck all menu items
+ for menuItem in self._workspace.menuAnalysis.actions():
+ menuItem.setChecked(False)
+
+ if self._current_perspective is not None:
+
+ # Remove the fitting menu for now, will be replaced later if we move back to a perspective that supports it
+ # I do not like that this requires the menu action to exist to be correct
+ if self._current_perspective.supports_fitting_menu:
+ self._workspace.menubar.removeAction(self._workspace.menuFitting.menuAction())
+
# Remove perspective and store in Perspective dictionary
- self.loadedPerspectives[
- self._current_perspective.name] = self._current_perspective
+ self.loadedPerspectives[self._current_perspective.name] = self._current_perspective
+
self._workspace.workspace.removeSubWindow(self._current_perspective)
self._workspace.workspace.removeSubWindow(self.subwindow)
- # Get new perspective
- self._current_perspective = self.loadedPerspectives[str(perspective_name)]
- self.setupPerspectiveMenubarOptions(self._current_perspective)
+ # Get new perspective - note that _current_perspective is of type Optional[Perspective],
+ # but new_perspective is of type Perspective, thus call to Perspective members are safe
+ new_perspective = self.loadedPerspectives[new_perspective_name]
- self.subwindow = self._workspace.workspace.addSubWindow(
- self._current_perspective)
+ self._workspace.actionReport.setEnabled(new_perspective.supports_reports)
+ self._workspace.actionOpen_Analysis.setEnabled(False)
+ self._workspace.actionSave_Analysis.setEnabled(False)
+
+ if new_perspective.isSerializable():
+ self._workspace.actionOpen_Analysis.setEnabled(True)
+ self._workspace.actionSave_Analysis.setEnabled(True)
+
+ if new_perspective.supports_fitting_menu:
+ # Put the fitting menu back in
+ # This is a bit involved but it is needed to preserve the menu ordering
+ self._workspace.menubar.removeAction(self._workspace.menuWindow.menuAction())
+ self._workspace.menubar.removeAction(self._workspace.menuHelp.menuAction())
+
+ self._workspace.menubar.addAction(self._workspace.menuFitting.menuAction())
+
+ self._workspace.menubar.addAction(self._workspace.menuWindow.menuAction())
+ self._workspace.menubar.addAction(self._workspace.menuHelp.menuAction())
+
+ #
+ # Selection on perspective choice menu
+ #
+ if isinstance(new_perspective, FittingWindow):
+ self.checkAnalysisOption(self._workspace.actionFitting)
+
+ elif isinstance(new_perspective, InvariantWindow):
+ self.checkAnalysisOption(self._workspace.actionInvariant)
+
+ elif isinstance(new_perspective, InversionWindow):
+ self.checkAnalysisOption(self._workspace.actionInversion)
+
+ elif isinstance(new_perspective, CorfuncWindow):
+ self.checkAnalysisOption(self._workspace.actionCorfunc)
+
+
+ #
+ # Set up the window
+ #
+ self.subwindow = self._workspace.workspace.addSubWindow(new_perspective)
# Resize to the workspace height
workspace_height = self._workspace.workspace.sizeHint().height()
- perspective_size = self._current_perspective.sizeHint()
+ perspective_size = new_perspective.sizeHint()
perspective_width = perspective_size.width()
- self._current_perspective.resize(perspective_width, workspace_height-10)
+ new_perspective.resize(perspective_width, workspace_height-10)
+ # Set the current perspective to new one and show
+ self._current_perspective = new_perspective
self._current_perspective.show()
def updatePerspective(self, data):
@@ -773,16 +831,16 @@ def actionReport(self):
"""
Show the Fit Report dialog.
"""
- report_list = None
- if getattr(self._current_perspective, "currentTab"):
- try:
- report_list = self._current_perspective.currentTab.getReport()
- except Exception as ex:
- logging.error("Report generation failed with: " + str(ex))
+ if self._current_perspective is not None:
+ report_data = self._current_perspective.getReport()
+
+ if report_data is None:
+ logging.info("Report data is empty, dialog not shown")
+ else:
+ self.report_dialog = ReportDialog(report_data=report_data, parent=self._parent)
+ self.report_dialog.show()
+
- if report_list is not None:
- self.report_dialog = ReportDialog(parent=self, report_list=report_list)
- self.report_dialog.show()
def actionReset(self):
"""
@@ -1237,44 +1295,6 @@ def checkAnalysisOption(self, analysisMenuOption):
self.uncheckAllMenuItems(self._workspace.menuAnalysis)
analysisMenuOption.setChecked(True)
- def clearPerspectiveMenubarOptions(self, perspective):
- """
- When closing a perspective, clears the menu bar
- """
- for menuItem in self._workspace.menuAnalysis.actions():
- menuItem.setChecked(False)
-
- if isinstance(self._current_perspective, Perspectives.PERSPECTIVES["Fitting"]):
- self._workspace.menubar.removeAction(self._workspace.menuFitting.menuAction())
-
- def setupPerspectiveMenubarOptions(self, perspective):
- """
- When setting a perspective, sets up the menu bar
- """
- self._workspace.actionReport.setEnabled(False)
- self._workspace.actionOpen_Analysis.setEnabled(False)
- self._workspace.actionSave_Analysis.setEnabled(False)
- if hasattr(perspective, 'isSerializable') and perspective.isSerializable():
- self._workspace.actionOpen_Analysis.setEnabled(True)
- self._workspace.actionSave_Analysis.setEnabled(True)
-
- if isinstance(perspective, Perspectives.PERSPECTIVES["Fitting"]):
- self.checkAnalysisOption(self._workspace.actionFitting)
- # Put the fitting menu back in
- # This is a bit involved but it is needed to preserve the menu ordering
- self._workspace.menubar.removeAction(self._workspace.menuWindow.menuAction())
- self._workspace.menubar.removeAction(self._workspace.menuHelp.menuAction())
- self._workspace.menubar.addAction(self._workspace.menuFitting.menuAction())
- self._workspace.menubar.addAction(self._workspace.menuWindow.menuAction())
- self._workspace.menubar.addAction(self._workspace.menuHelp.menuAction())
- self._workspace.actionReport.setEnabled(True)
-
- elif isinstance(perspective, Perspectives.PERSPECTIVES["Invariant"]):
- self.checkAnalysisOption(self._workspace.actionInvariant)
- elif isinstance(perspective, Perspectives.PERSPECTIVES["Inversion"]):
- self.checkAnalysisOption(self._workspace.actionInversion)
- elif isinstance(perspective, Perspectives.PERSPECTIVES["Corfunc"]):
- self.checkAnalysisOption(self._workspace.actionCorfunc)
def saveCustomConfig(self):
"""
diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py
index 8303a75d0d..1545e1086b 100644
--- a/src/sas/qtgui/MainWindow/MainWindow.py
+++ b/src/sas/qtgui/MainWindow/MainWindow.py
@@ -32,7 +32,7 @@ def __init__(self, screen_resolution, parent=None):
# the two scrollbars will help managing the workspace.
self.workspace.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.workspace.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
- self.screen_width = screen_resolution.width()
+ self.screen_width = screen_resolution.width()
self.screen_height = screen_resolution.height()
self.setCentralWidget(self.workspace)
@@ -47,6 +47,7 @@ def __init__(self, screen_resolution, parent=None):
except Exception as ex:
import logging
logging.error("Application failed with: "+str(ex))
+ raise ex
def closeEvent(self, event):
if self.guiManager.quitApplication():
@@ -120,6 +121,3 @@ def run_sasview():
# No need to .exec_ - the reactor takes care of it.
reactor.run()
-
-if __name__ == "__main__":
- run_sasview()
diff --git a/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py b/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py
index c35f84a140..e4843a0585 100644
--- a/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py
+++ b/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py
@@ -604,7 +604,7 @@ def testNewPlot1D(self, test_patch):
QApplication.processEvents()
# The plot was registered
- self.assertEqual(len(PlotHelper.currentPlots()), 1)
+ self.assertEqual(len(PlotHelper.currentPlotIds()), 1)
self.assertTrue(self.form.cbgraph.isEnabled())
self.assertTrue(self.form.cmdAppend.isEnabled())
@@ -682,14 +682,14 @@ def testAppendPlot(self, test_patch):
QApplication.processEvents()
# See that we have two plots
- self.assertEqual(len(PlotHelper.currentPlots()), 2)
+ self.assertEqual(len(PlotHelper.currentPlotIds()), 2)
# Add data to plot #1
self.form.cbgraph.setCurrentIndex(1)
self.form.appendPlot()
# See that we still have two plots
- self.assertEqual(len(PlotHelper.currentPlots()), 2)
+ self.assertEqual(len(PlotHelper.currentPlotIds()), 2)
def testUpdateGraphCombo(self):
"""
@@ -1111,7 +1111,7 @@ def testClosePlotsForItem(self):
QApplication.processEvents()
# The plot was registered
- self.assertEqual(len(PlotHelper.currentPlots()), 1)
+ self.assertEqual(len(PlotHelper.currentPlotIds()), 1)
self.assertEqual(len(self.form.plot_widgets), 1)
# could have leftovers from previous tests
#self.assertEqual(list(self.form.plot_widgets.keys()), ['Graph3'])
@@ -1124,7 +1124,7 @@ def testClosePlotsForItem(self):
self.form.closePlotsForItem(model_item)
# See that no plot remained
- self.assertEqual(len(PlotHelper.currentPlots()), 0)
+ self.assertEqual(len(PlotHelper.currentPlotIds()), 0)
self.assertEqual(len(self.form.plot_widgets), 0)
def testPlotsFromMultipleData1D(self):
diff --git a/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py b/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py
index 5bcfaaf854..215d1837fe 100644
--- a/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py
+++ b/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py
@@ -4,6 +4,7 @@
# pylint: disable=E1101
# global
+from PyQt5.QtGui import QStandardItem
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg \
as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT
@@ -11,22 +12,26 @@
from numpy.linalg.linalg import LinAlgError
import numpy as np
+from typing import Optional, List, Tuple
+
from PyQt5 import QtCore
from PyQt5 import QtGui, QtWidgets
-from PyQt5.QtWidgets import QMessageBox
# sas-global
# pylint: disable=import-error, no-name-in-module
+
import sas.qtgui.Utilities.GuiUtils as GuiUtils
+from sas.qtgui.Utilities.Reports.reportdata import ReportData
+from sas.qtgui.Utilities.Reports import ReportBase
+
from sas.sascalc.corfunc.corfunc_calculator import CorfuncCalculator
# pylint: enable=import-error, no-name-in-module
# local
from .UI.CorfuncPanel import Ui_CorfuncDialog
+from .corefuncutil import WIDGETS
from .saveextrapolated import SaveExtrapolatedPopup
-from .corefuncutil import WIDGETS as W
-
-
+from ..perspective import Perspective
class MyMplCanvas(FigureCanvas):
"""Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
@@ -38,7 +43,7 @@ def __init__(self, model, width=5, height=4, dpi=100):
FigureCanvas.__init__(self, self.fig)
- self.data = None
+ self.data: Optional[Tuple[np.ndarray, np.ndarray, np.ndarray]] = None
self.extrap = None
self.dragging = None
self.draggable = False
@@ -66,9 +71,9 @@ def on_mouse_down(self, event):
if self.on_legend(event.x, event.y):
return
- qmin = float(self.model.item(W.W_QMIN).text())
- qmax1 = float(self.model.item(W.W_QMAX).text())
- qmax2 = float(self.model.item(W.W_QCUTOFF).text())
+ qmin = float(self.model.item(WIDGETS.W_QMIN).text())
+ qmax1 = float(self.model.item(WIDGETS.W_QMAX).text())
+ qmax2 = float(self.model.item(WIDGETS.W_QCUTOFF).text())
q = event.xdata
@@ -89,11 +94,11 @@ def on_mouse_up(self, event):
return
if self.dragging == "qmin":
- item = W.W_QMIN
+ item = WIDGETS.W_QMIN
elif self.dragging == "qmax1":
- item = W.W_QMAX
+ item = WIDGETS.W_QMAX
else:
- item = W.W_QCUTOFF
+ item = WIDGETS.W_QCUTOFF
self.model.setItem(item, QtGui.QStandardItem(str(GuiUtils.formatNumber(event.xdata))))
@@ -108,11 +113,11 @@ def on_motion(self, event):
return
if self.dragging == "qmin":
- item = W.W_QMIN
+ item = WIDGETS.W_QMIN
elif self.dragging == "qmax1":
- item = W.W_QMAX
+ item = WIDGETS.W_QMAX
else:
- item = W.W_QCUTOFF
+ item = WIDGETS.W_QCUTOFF
self.model.setItem(item, QtGui.QStandardItem(str(GuiUtils.formatNumber(event.xdata))))
@@ -124,7 +129,7 @@ def draw_q_space(self):
as the bounds set by self.qmin, self.qmax1, and self.qmax2.
It will also plot the extrpolation in self.extrap, if it exists."""
- self.draggable = True;
+ self.draggable = True
self.fig.clf()
@@ -136,9 +141,9 @@ def draw_q_space(self):
self.axes.set_title("Scattering data")
self.fig.tight_layout()
- qmin = float(self.model.item(W.W_QMIN).text())
- qmax1 = float(self.model.item(W.W_QMAX).text())
- qmax2 = float(self.model.item(W.W_QCUTOFF).text())
+ qmin = float(self.model.item(WIDGETS.W_QMIN).text())
+ qmax1 = float(self.model.item(WIDGETS.W_QMAX).text())
+ qmax2 = float(self.model.item(WIDGETS.W_QCUTOFF).text())
if self.data:
# self.axes.plot(self.data.x, self.data.y, label="Experimental Data")
@@ -189,10 +194,16 @@ def draw_real_space(self):
self.draw()
-class CorfuncWindow(QtWidgets.QDialog, Ui_CorfuncDialog):
+class CorfuncWindow(QtWidgets.QDialog, Ui_CorfuncDialog, Perspective):
"""Displays the correlation function analysis of sas data."""
- name = "Corfunc" # For displaying in the combo box
- ext = " crf" # File extension used for saving analysis files
+
+ name = "Corfunc"
+ ext = "crf"
+
+ @property
+ def title(self):
+ """ Window title """
+ return "Corfunc Perspective"
trigger = QtCore.pyqtSignal(tuple)
@@ -201,7 +212,7 @@ def __init__(self, parent=None):
super(CorfuncWindow, self).__init__()
self.setupUi(self)
- self.setWindowTitle("Corfunc Perspective")
+ self.setWindowTitle(self.title)
self.parent = parent
self.mapper = None
@@ -211,19 +222,19 @@ def __init__(self, parent=None):
self.communicate.dataDeletedSignal.connect(self.removeData)
self._calculator = CorfuncCalculator()
self._allow_close = False
- self._model_item = None
+ self._model_item: Optional[QStandardItem] = None
self.data = None
self.has_data = False
self.txtLowerQMin.setText("0.0")
self.txtLowerQMin.setEnabled(False)
self.extrapolation_curve = None
- self._canvas = MyMplCanvas(self.model)
- self.plotLayout.insertWidget(0, self._canvas)
- self.plotLayout.insertWidget(1, NavigationToolbar2QT(self._canvas, self))
- self._realplot = MyMplCanvas(self.model)
- self.plotLayout.insertWidget(2, self._realplot)
- self.plotLayout.insertWidget(3, NavigationToolbar2QT(self._realplot, self))
+ self._q_space_plot = MyMplCanvas(self.model)
+ self.plotLayout.insertWidget(0, self._q_space_plot)
+ self.plotLayout.insertWidget(1, NavigationToolbar2QT(self._q_space_plot, self))
+ self._real_space_plot = MyMplCanvas(self.model) # TODO: This is not a good name, or structure either
+ self.plotLayout.insertWidget(2, self._real_space_plot)
+ self.plotLayout.insertWidget(3, NavigationToolbar2QT(self._real_space_plot, self))
self.gridLayout_4.setColumnStretch(0, 1)
self.gridLayout_4.setColumnStretch(1, 2)
@@ -269,44 +280,44 @@ def setup_model(self):
"""Populate the model with default data."""
# filename
item = QtGui.QStandardItem(self._path)
- self.model.setItem(W.W_FILENAME, item)
+ self.model.setItem(WIDGETS.W_FILENAME, item)
- self.model.setItem(W.W_QMIN,
+ self.model.setItem(WIDGETS.W_QMIN,
QtGui.QStandardItem("0.01"))
- self.model.setItem(W.W_QMAX,
+ self.model.setItem(WIDGETS.W_QMAX,
QtGui.QStandardItem("0.20"))
- self.model.setItem(W.W_QCUTOFF,
+ self.model.setItem(WIDGETS.W_QCUTOFF,
QtGui.QStandardItem("0.22"))
- self.model.setItem(W.W_BACKGROUND,
+ self.model.setItem(WIDGETS.W_BACKGROUND,
QtGui.QStandardItem("0"))
#self.model.setItem(W.W_TRANSFORM,
# QtGui.QStandardItem("Fourier"))
- self.model.setItem(W.W_GUINIERA,
+ self.model.setItem(WIDGETS.W_GUINIERA,
QtGui.QStandardItem("0.0"))
- self.model.setItem(W.W_GUINIERB,
+ self.model.setItem(WIDGETS.W_GUINIERB,
QtGui.QStandardItem("0.0"))
- self.model.setItem(W.W_PORODK,
+ self.model.setItem(WIDGETS.W_PORODK,
QtGui.QStandardItem("0.0"))
- self.model.setItem(W.W_PORODSIGMA,
+ self.model.setItem(WIDGETS.W_PORODSIGMA,
QtGui.QStandardItem("0.0"))
- self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(str(0)))
- self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(str(0)))
- self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(str(0)))
- self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(str(0)))
- self.model.setItem(W.W_POLY, QtGui.QStandardItem(str(0)))
- self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(str(0)))
+ self.model.setItem(WIDGETS.W_CORETHICK, QtGui.QStandardItem(str(0)))
+ self.model.setItem(WIDGETS.W_INTTHICK, QtGui.QStandardItem(str(0)))
+ self.model.setItem(WIDGETS.W_HARDBLOCK, QtGui.QStandardItem(str(0)))
+ self.model.setItem(WIDGETS.W_CRYSTAL, QtGui.QStandardItem(str(0)))
+ self.model.setItem(WIDGETS.W_POLY, QtGui.QStandardItem(str(0)))
+ self.model.setItem(WIDGETS.W_PERIOD, QtGui.QStandardItem(str(0)))
def removeData(self, data_list=None):
"""Remove the existing data reference from the Invariant Persepective"""
if not data_list or self._model_item not in data_list:
return
# Clear data plots
- self._canvas.data = None
- self._canvas.extrap = None
- self._canvas.draw_q_space()
- self._realplot.data = None
- self._realplot.extrap = None
- self._realplot.draw_real_space()
+ self._q_space_plot.data = None
+ self._q_space_plot.extrap = None
+ self._q_space_plot.draw_q_space()
+ self._real_space_plot.data = None
+ self._real_space_plot.extrap = None
+ self._real_space_plot.draw_real_space()
# Clear calculator, model, and data path
self._calculator = CorfuncCalculator()
self._model_item = None
@@ -320,15 +331,15 @@ def model_changed(self, _):
if not self.mapper:
return
self.mapper.toFirst()
- self._canvas.draw_q_space()
+ self._q_space_plot.draw_q_space()
def _update_calculator(self):
- self._calculator.lowerq = float(self.model.item(W.W_QMIN).text())
- qmax1 = float(self.model.item(W.W_QMAX).text())
- qmax2 = float(self.model.item(W.W_QCUTOFF).text())
+ self._calculator.lowerq = float(self.model.item(WIDGETS.W_QMIN).text())
+ qmax1 = float(self.model.item(WIDGETS.W_QMAX).text())
+ qmax2 = float(self.model.item(WIDGETS.W_QCUTOFF).text())
self._calculator.upperq = (qmax1, qmax2)
self._calculator.background = \
- float(self.model.item(W.W_BACKGROUND).text())
+ float(self.model.item(WIDGETS.W_BACKGROUND).text())
def extrapolate(self):
"""Extend the experiemntal data with guinier and porod curves."""
@@ -342,23 +353,23 @@ def extrapolate(self):
"cutoff Q, or increasing the lower Q."
QtWidgets.QMessageBox.warning(self, "Calculation Error",
message)
- self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem(""))
- self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem(""))
- self.model.setItem(W.W_PORODK, QtGui.QStandardItem(""))
- self.model.setItem(W.W_PORODSIGMA, QtGui.QStandardItem(""))
- self._canvas.extrap = None
+ self.model.setItem(WIDGETS.W_GUINIERA, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_GUINIERB, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_PORODK, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_PORODSIGMA, QtGui.QStandardItem(""))
+ self._q_space_plot.extrap = None
self.model_changed(None)
return
finally:
self.model.itemChanged.connect(self.model_changed)
- self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem("{:.3g}".format(params['A'])))
- self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem("{:.3g}".format(params['B'])))
- self.model.setItem(W.W_PORODK, QtGui.QStandardItem("{:.3g}".format(params['K'])))
- self.model.setItem(W.W_PORODSIGMA,
- QtGui.QStandardItem("{:.4g}".format(params['sigma'])))
+ self.model.setItem(WIDGETS.W_GUINIERA, QtGui.QStandardItem("{:.3g}".format(params['A'])))
+ self.model.setItem(WIDGETS.W_GUINIERB, QtGui.QStandardItem("{:.3g}".format(params['B'])))
+ self.model.setItem(WIDGETS.W_PORODK, QtGui.QStandardItem("{:.3g}".format(params['K'])))
+ self.model.setItem(WIDGETS.W_PORODSIGMA,
+ QtGui.QStandardItem("{:.4g}".format(params['sigma'])))
- self._canvas.extrap = extrapolation
+ self._q_space_plot.extrap = extrapolation
self.model_changed(None)
self.cmdTransform.setEnabled(True)
self.cmdSaveExtrapolation.setEnabled(True)
@@ -370,8 +381,8 @@ def transform(self):
method = "fourier"
- extrap = self._canvas.extrap
- background = float(self.model.item(W.W_BACKGROUND).text())
+ extrap = self._q_space_plot.extrap
+ background = float(self.model.item(WIDGETS.W_BACKGROUND).text())
def updatefn(msg):
"""Report progress of transformation."""
@@ -387,26 +398,26 @@ def completefn(transforms):
def finish_transform(self, transforms):
- self._realplot.data = transforms
+ self._real_space_plot.data = transforms
self.update_real_space_plot(transforms)
- self._realplot.draw_real_space()
+ self._real_space_plot.draw_real_space()
self.cmdExtract.setEnabled(True)
self.cmdSave.setEnabled(True)
def extract(self):
- transforms = self._realplot.data
+ transforms = self._real_space_plot.data
params = self._calculator.extract_parameters(transforms[0])
self.model.itemChanged.disconnect(self.model_changed)
- self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem("{:.3g}".format(params['d0'])))
- self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem("{:.3g}".format(params['dtr'])))
- self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem("{:.3g}".format(params['Lc'])))
- self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem("{:.3g}".format(params['fill'])))
- self.model.setItem(W.W_POLY, QtGui.QStandardItem("{:.3g}".format(params['A'])))
- self.model.setItem(W.W_PERIOD, QtGui.QStandardItem("{:.3g}".format(params['max'])))
+ self.model.setItem(WIDGETS.W_CORETHICK, QtGui.QStandardItem("{:.3g}".format(params['d0'])))
+ self.model.setItem(WIDGETS.W_INTTHICK, QtGui.QStandardItem("{:.3g}".format(params['dtr'])))
+ self.model.setItem(WIDGETS.W_HARDBLOCK, QtGui.QStandardItem("{:.3g}".format(params['Lc'])))
+ self.model.setItem(WIDGETS.W_CRYSTAL, QtGui.QStandardItem("{:.3g}".format(params['fill'])))
+ self.model.setItem(WIDGETS.W_POLY, QtGui.QStandardItem("{:.3g}".format(params['A'])))
+ self.model.setItem(WIDGETS.W_PERIOD, QtGui.QStandardItem("{:.3g}".format(params['max'])))
self.model.itemChanged.connect(self.model_changed)
self.model_changed(None)
@@ -440,25 +451,25 @@ def setup_mapper(self):
self.mapper.setOrientation(QtCore.Qt.Vertical)
self.mapper.setModel(self.model)
- self.mapper.addMapping(self.txtLowerQMax, W.W_QMIN)
- self.mapper.addMapping(self.txtUpperQMin, W.W_QMAX)
- self.mapper.addMapping(self.txtUpperQMax, W.W_QCUTOFF)
- self.mapper.addMapping(self.txtBackground, W.W_BACKGROUND)
+ self.mapper.addMapping(self.txtLowerQMax, WIDGETS.W_QMIN)
+ self.mapper.addMapping(self.txtUpperQMin, WIDGETS.W_QMAX)
+ self.mapper.addMapping(self.txtUpperQMax, WIDGETS.W_QCUTOFF)
+ self.mapper.addMapping(self.txtBackground, WIDGETS.W_BACKGROUND)
#self.mapper.addMapping(self.transformCombo, W.W_TRANSFORM)
- self.mapper.addMapping(self.txtGuinierA, W.W_GUINIERA)
- self.mapper.addMapping(self.txtGuinierB, W.W_GUINIERB)
- self.mapper.addMapping(self.txtPorodK, W.W_PORODK)
- self.mapper.addMapping(self.txtPorodSigma, W.W_PORODSIGMA)
+ self.mapper.addMapping(self.txtGuinierA, WIDGETS.W_GUINIERA)
+ self.mapper.addMapping(self.txtGuinierB, WIDGETS.W_GUINIERB)
+ self.mapper.addMapping(self.txtPorodK, WIDGETS.W_PORODK)
+ self.mapper.addMapping(self.txtPorodSigma, WIDGETS.W_PORODSIGMA)
- self.mapper.addMapping(self.txtAvgCoreThick, W.W_CORETHICK)
- self.mapper.addMapping(self.txtAvgIntThick, W.W_INTTHICK)
- self.mapper.addMapping(self.txtAvgHardBlock, W.W_HARDBLOCK)
- self.mapper.addMapping(self.txtPolydisp, W.W_POLY)
- self.mapper.addMapping(self.txtLongPeriod, W.W_PERIOD)
- self.mapper.addMapping(self.txtLocalCrystal, W.W_CRYSTAL)
+ self.mapper.addMapping(self.txtAvgCoreThick, WIDGETS.W_CORETHICK)
+ self.mapper.addMapping(self.txtAvgIntThick, WIDGETS.W_INTTHICK)
+ self.mapper.addMapping(self.txtAvgHardBlock, WIDGETS.W_HARDBLOCK)
+ self.mapper.addMapping(self.txtPolydisp, WIDGETS.W_POLY)
+ self.mapper.addMapping(self.txtLongPeriod, WIDGETS.W_PERIOD)
+ self.mapper.addMapping(self.txtLocalCrystal, WIDGETS.W_CRYSTAL)
- self.mapper.addMapping(self.txtFilename, W.W_FILENAME)
+ self.mapper.addMapping(self.txtFilename, WIDGETS.W_FILENAME)
self.mapper.toFirst()
@@ -468,7 +479,7 @@ def calculate_background(self):
try:
background = self._calculator.compute_background()
temp = QtGui.QStandardItem("{:.4g}".format(background))
- self.model.setItem(W.W_BACKGROUND, temp)
+ self.model.setItem(WIDGETS.W_BACKGROUND, temp)
except (LinAlgError, ValueError):
message = "These is not enough data in the fitting range. "\
"Try decreasing the upper Q or increasing the cutoff Q"
@@ -499,18 +510,12 @@ def allowSwap():
"""
return False
- def setData(self, data_item, is_batch=False):
+ def setData(self, data_item: List[QStandardItem], is_batch=False):
"""
Obtain a QStandardItem object and dissect it to get Data1D/2D
Pass it over to the calculator
"""
- if not isinstance(data_item, list):
- msg = "Incorrect type passed to the Corfunc Perpsective"
- raise AttributeError(msg)
- if not isinstance(data_item[0], QtGui.QStandardItem):
- msg = "Incorrect type passed to the Corfunc Perspective"
- raise AttributeError(msg)
if self.has_data:
msg = "Data is already loaded into the Corfunc perspective. Sending a new data set "
@@ -530,26 +535,26 @@ def setData(self, data_item, is_batch=False):
self.cmdExtrapolate.setEnabled(True)
self.model.itemChanged.disconnect(self.model_changed)
- self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem(""))
- self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem(""))
- self.model.setItem(W.W_PORODK, QtGui.QStandardItem(""))
- self.model.setItem(W.W_PORODSIGMA, QtGui.QStandardItem(""))
- self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(""))
- self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(""))
- self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(""))
- self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(""))
- self.model.setItem(W.W_POLY, QtGui.QStandardItem(""))
- self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_GUINIERA, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_GUINIERB, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_PORODK, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_PORODSIGMA, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_CORETHICK, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_INTTHICK, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_HARDBLOCK, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_CRYSTAL, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_POLY, QtGui.QStandardItem(""))
+ self.model.setItem(WIDGETS.W_PERIOD, QtGui.QStandardItem(""))
self.model.itemChanged.connect(self.model_changed)
- self._canvas.data = data
- self._canvas.extrap = None
+ self._q_space_plot.data = data
+ self._q_space_plot.extrap = None
self.model_changed(None)
self.cmdTransform.setEnabled(False)
self._path = data.name
- self.model.setItem(W.W_FILENAME, QtGui.QStandardItem(self._path))
- self._realplot.data = None
- self._realplot.draw_real_space()
+ self.model.setItem(WIDGETS.W_FILENAME, QtGui.QStandardItem(self._path))
+ self._real_space_plot.data = None
+ self._real_space_plot.draw_real_space()
self.has_data = True
def setClosable(self, value=True):
@@ -576,13 +581,6 @@ def closeEvent(self, event):
# Maybe we should just minimize
self.setWindowState(QtCore.Qt.WindowMinimized)
- def title(self):
- """
- Window title function used by certain error messages.
- Check DataExplorer.py, line 355
- """
- return "Corfunc Perspective"
-
def on_save(self):
"""
Save corfunc state into a file
@@ -596,7 +594,7 @@ def on_save(self):
if "." not in f_name:
f_name += ".crf"
- data1, data3, data_idf = self._realplot.data
+ data1, data3, data_idf = self._real_space_plot.data
with open(f_name, "w") as outfile:
outfile.write("X 1D 3D IDF\n")
@@ -681,34 +679,34 @@ def updateFromParameters(self, params):
raise TypeError(f"{msg}: {c_name} received")
# Assign values to 'Invariant' tab inputs - use defaults if not found
self.model.setItem(
- W.W_GUINIERA, QtGui.QStandardItem(params.get('guinier_a', '0.0')))
+ WIDGETS.W_GUINIERA, QtGui.QStandardItem(params.get('guinier_a', '0.0')))
self.model.setItem(
- W.W_GUINIERB, QtGui.QStandardItem(params.get('guinier_b', '0.0')))
+ WIDGETS.W_GUINIERB, QtGui.QStandardItem(params.get('guinier_b', '0.0')))
self.model.setItem(
- W.W_PORODK, QtGui.QStandardItem(params.get('porod_k', '0.0')))
- self.model.setItem(W.W_PORODSIGMA, QtGui.QStandardItem(
+ WIDGETS.W_PORODK, QtGui.QStandardItem(params.get('porod_k', '0.0')))
+ self.model.setItem(WIDGETS.W_PORODSIGMA, QtGui.QStandardItem(
params.get('porod_sigma', '0.0')))
- self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(
+ self.model.setItem(WIDGETS.W_CORETHICK, QtGui.QStandardItem(
params.get('avg_core_thick', '0')))
- self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(
+ self.model.setItem(WIDGETS.W_INTTHICK, QtGui.QStandardItem(
params.get('avg_inter_thick', '0')))
- self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(
+ self.model.setItem(WIDGETS.W_HARDBLOCK, QtGui.QStandardItem(
params.get('avg_hard_block_thick', '0')))
- self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(
+ self.model.setItem(WIDGETS.W_CRYSTAL, QtGui.QStandardItem(
params.get('local_crystalinity', '0')))
self.model.setItem(
- W.W_POLY, QtGui.QStandardItem(params.get('polydispersity', '0')))
+ WIDGETS.W_POLY, QtGui.QStandardItem(params.get('polydispersity', '0')))
self.model.setItem(
- W.W_PERIOD, QtGui.QStandardItem(params.get('long_period', '0')))
+ WIDGETS.W_PERIOD, QtGui.QStandardItem(params.get('long_period', '0')))
self.model.setItem(
- W.W_FILENAME, QtGui.QStandardItem(params.get('data_name', '')))
+ WIDGETS.W_FILENAME, QtGui.QStandardItem(params.get('data_name', '')))
self.model.setItem(
- W.W_QMIN, QtGui.QStandardItem(params.get('lower_q_max', '0.01')))
+ WIDGETS.W_QMIN, QtGui.QStandardItem(params.get('lower_q_max', '0.01')))
self.model.setItem(
- W.W_QMAX, QtGui.QStandardItem(params.get('upper_q_min', '0.20')))
+ WIDGETS.W_QMAX, QtGui.QStandardItem(params.get('upper_q_min', '0.20')))
self.model.setItem(
- W.W_QCUTOFF, QtGui.QStandardItem(params.get('upper_q_max', '0.22')))
- self.model.setItem(W.W_BACKGROUND, QtGui.QStandardItem(
+ WIDGETS.W_QCUTOFF, QtGui.QStandardItem(params.get('upper_q_max', '0.22')))
+ self.model.setItem(WIDGETS.W_BACKGROUND, QtGui.QStandardItem(
params.get('background', '0')))
self.cmdCalculateBg.setEnabled(params.get('background', '0') != '0')
self.cmdSave.setEnabled(params.get('guinier_a', '0.0') != '0.0')
@@ -719,3 +717,46 @@ def updateFromParameters(self, params):
self.extrapolate()
if params.get('long_period', '0') != '0':
self.transform()
+
+ def get_figures(self):
+ """
+ Get plots for the report
+ """
+
+ return [self._real_space_plot.fig]
+
+ @property
+ def real_space_figure(self):
+ return self._real_space_plot.fig
+
+ @property
+ def q_space_figure(self):
+ return self._q_space_plot.fig
+
+ @property
+ def supports_reports(self) -> bool:
+ return True
+
+ def getReport(self) -> Optional[ReportData]:
+ if not self.has_data:
+ return None
+
+ report = ReportBase("Correlation Function")
+ report.add_data_details(self.data)
+
+ # Format keys
+ parameters = self.getState()
+ fancy_parameters = {}
+
+ for key in parameters:
+ nice_key = " ".join([s.capitalize() for s in key.split("_")])
+ if parameters[key].strip() == '':
+ fancy_parameters[nice_key] = '-'
+ else:
+ fancy_parameters[nice_key] = parameters[key]
+
+ report.add_table_dict(fancy_parameters, ("Parameter", "Value"))
+ report.add_plot(self.q_space_figure)
+ report.add_plot(self.real_space_figure)
+
+ return report.report_data
\ No newline at end of file
diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingLogic.py b/src/sas/qtgui/Perspectives/Fitting/FittingLogic.py
index d80b4ab95d..90f30c1e16 100644
--- a/src/sas/qtgui/Perspectives/Fitting/FittingLogic.py
+++ b/src/sas/qtgui/Perspectives/Fitting/FittingLogic.py
@@ -7,7 +7,7 @@
from sas.sascalc.dataloader.data_info import Source
-class FittingLogic(object):
+class FittingLogic:
"""
All the data-related logic. This class deals exclusively with Data1D/2D
No QStandardModelIndex here.
diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py b/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
index 880fdd80ca..1e86f1fe9d 100644
--- a/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
+++ b/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
@@ -1,8 +1,8 @@
-from distutils.command.config import config
-
import numpy
import copy
+from typing import Optional
+
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
@@ -10,7 +10,6 @@
from bumps import options
from bumps import fitters
-import sas.qtgui.Utilities.LocalConfig as LocalConfig
import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
import sas.qtgui.Utilities.GuiUtils as GuiUtils
from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
@@ -19,19 +18,28 @@
from sas.qtgui.Perspectives.Fitting.ConstraintWidget import ConstraintWidget
from sas.qtgui.Perspectives.Fitting.FittingOptions import FittingOptions
from sas.qtgui.Perspectives.Fitting.GPUOptions import GPUOptions
+from sas.qtgui.Perspectives.perspective import Perspective
+
+from sas.qtgui.Utilities.Reports.reportdata import ReportData
-class FittingWindow(QtWidgets.QTabWidget):
+class FittingWindow(QtWidgets.QTabWidget, Perspective):
"""
"""
tabsModifiedSignal = QtCore.pyqtSignal()
fittingStartedSignal = QtCore.pyqtSignal(list)
fittingStoppedSignal = QtCore.pyqtSignal(list)
- name = "Fitting" # For displaying in the combo box in DataExplorer
- ext = "fitv" # Extension used for saving analyses
+ name = "Fitting"
+ ext = "fitv"
+
+ @property
+ def title(self):
+ """ Window title"""
+ return "Fitting Perspective"
+
def __init__(self, parent=None, data=None):
- super(FittingWindow, self).__init__()
+ super().__init__()
self.parent = parent
self._data = data
@@ -94,6 +102,7 @@ def __init__(self, parent=None, data=None):
self.plusButton.setToolTip("Add a new Fit Page")
self.plusButton.clicked.connect(lambda: self.addFit(None))
+
def updateWindowTitle(self):
"""
Update the window title with the current optimizer name
@@ -498,12 +507,20 @@ def getCurrentStateAsXml(self):
return state
@property
- def currentTab(self):
+ def currentTab(self): # TODO: More pythonic name
"""
Returns the tab widget currently shown
"""
return self.currentWidget()
+ @property
+ def currentFittingWidget(self) -> Optional[FittingWidget]:
+ current_tab = self.currentTab
+ if isinstance(current_tab, FittingWidget):
+ return current_tab
+ else:
+ return None
+
def getFitTabs(self):
"""
Returns the list of fitting tabs
@@ -555,3 +572,17 @@ def getTabByName(self, name):
if tab.modelName() == name:
return tab
return None
+
+ @property
+ def supports_reports(self) -> bool:
+ return True
+
+ def getReport(self) -> Optional[ReportData]:
+ """ Get the report from the current tab"""
+ fitting_widget = self.currentFittingWidget
+ return None if fitting_widget is None else fitting_widget.getReport()
+
+ @property
+ def supports_fitting_menu(self) -> bool:
+ return True
+
diff --git a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py
index ab97dbbff1..73a7ceb859 100644
--- a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py
+++ b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py
@@ -3,21 +3,26 @@
import datetime
import re
import sys
-import tempfile
+
+from typing import List
import logging
from io import BytesIO
import urllib.parse
+import html2text
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from bumps import options
import sas.qtgui.Utilities.GuiUtils as GuiUtils
+from sas.qtgui.Plotting.PlotterBase import PlotterBase
+from sas.qtgui.Utilities.Reports.reportdata import ReportData
+
from sas.sasview import __version__ as SASVIEW_VERSION
from sasmodels import __version__ as SASMODELS_VERSION
-
-class ReportPageLogic(object):
+# TODO: Integrate with other reports
+class ReportPageLogic:
"""
Logic for the Report Page functionality. Refactored from FittingWidget.
"""
@@ -29,22 +34,18 @@ def __init__(self, parent=None, kernel_module=None, data=None, index=None, param
self._index = index
self.params = params
- @staticmethod
- def cleanhtml(raw_html):
- """Remove html tags from a document"""
- cleanr = re.compile('<.*?>')
- cleantext = re.sub(cleanr, '', raw_html)
- return cleantext
- def reportList(self):
+ def reportList(self) -> ReportData: # TODO: Rename to reference report object
"""
Return the HTML version of the full report
"""
if self.kernel_module is None:
- report_txt = "No model defined"
- report_html = HEADER % report_txt
- images = []
- return [report_html, report_txt, images]
+
+ text = "No model defined"
+
+ return ReportData(
+ html=HEADER % text,
+ text=text)
# Get plot image from plotpanel
images = self.getImages()
@@ -59,9 +60,10 @@ def reportList(self):
report_html = report_header + report_parameters + imagesHTML
- report_txt = self.cleanhtml(report_html)
+ report_txt = html2text.html2text(GuiUtils.replaceHTMLwithASCII(report_html))
- report_list = [report_html, report_txt, images]
+ # report_list = ReportData(html=report_html, text=report_txt, images=images)
+ report_list = ReportData(html=report_html, text=report_txt)
return report_list
@@ -98,7 +100,7 @@ def reportHeader(self):
return report
- def buildPlotsForReport(self, images):
+ def buildPlotsForReport(self, images): # TODO: Unify with other report image to html conversion
""" Convert Matplotlib figure 'fig' into a tag for HTML use using base64 encoding. """
html = FEET_1 % self.data.name
@@ -161,19 +163,19 @@ def reportParams(self):
return report
- def getImages(self):
+ def getImages(self) -> List[PlotterBase]:
"""
Create MPL figures for the current fit
"""
graphs = []
modelname = self.kernel_module.name
if not modelname or self._index is None:
- return None
+ return []
plot_ids = [plot.id for plot in GuiUtils.plotsFromModel(modelname, self._index)]
# Active plots
import sas.qtgui.Plotting.PlotHelper as PlotHelper
- shown_plot_names = PlotHelper.currentPlots()
+ shown_plot_names = PlotHelper.currentPlotIds()
# current_plots = list of graph names of currently shown plots
# which are related to this dataset
@@ -188,6 +190,7 @@ def getImages(self):
# Simple html report template
+# TODO Remove microsoft based stuff - probably implicit in the refactoring to come
HEADER = "\n"
HEADER += "