source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ e793f62

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since e793f62 was e4335ae, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

SASVIEW-957 logging changes (#158)

  • rework XStream to continue to write to stdout/stderr alongside redirection; make logging level setting consistent; make log configuration more consistent
  • rm XStream; QT signal in QtHandler? logging handler; only logs in Log Explorer (no stdout/stderr)
  • no need to change handler level
  • use QTextBrowser.append to facilitate auto-scrolling in the Log Explorer
  • modify logger unit test to reflect changes (passes)
  • Property mode set to 100644
File size: 104.3 KB
RevLine 
[60af928]1import json
[cd31251]2import os
[57be490]3import copy
[60af928]4from collections import defaultdict
[b3e8629]5
[d4dac80]6import copy
[5236449]7import logging
8import traceback
[cbcdd2c]9from twisted.internet import threads
[1bc27f1]10import numpy as np
[e90988c]11import webbrowser
[5236449]12
[4992ff2]13from PyQt5 import QtCore
14from PyQt5 import QtGui
15from PyQt5 import QtWidgets
[60af928]16
[5d1440e1]17from sasmodels import product
[60af928]18from sasmodels import generate
19from sasmodels import modelinfo
[5236449]20from sasmodels.sasview_model import load_standard_models
[358b39d]21from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
22
[f182f93]23from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
[57be490]24from sas.sascalc.fit.pagestate import PageState
[5236449]25
[83eb5208]26import sas.qtgui.Utilities.GuiUtils as GuiUtils
[14ec91c5]27import sas.qtgui.Utilities.LocalConfig as LocalConfig
[dc5ef15]28from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
29from sas.qtgui.Plotting.PlotterData import Data1D
30from sas.qtgui.Plotting.PlotterData import Data2D
[5236449]31
[1bc27f1]32from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
[dc5ef15]33from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
[7adc2a8]34from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
35
[dc5ef15]36from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
[4d457df]38from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
39from sas.qtgui.Perspectives.Fitting import FittingUtilities
[3b3b40b]40from sas.qtgui.Perspectives.Fitting import ModelUtilities
[1bc27f1]41from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
42from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
43from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
[ad6b4e2]44from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
[6011788]45from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
[b00414d]46from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
[14ec91c5]47from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
[eae226b]48from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
[57be490]49from sas.qtgui.Perspectives.Fitting.ReportPageLogic import ReportPageLogic
50
[60af928]51
[8222f171]52
[60af928]53TAB_MAGNETISM = 4
54TAB_POLY = 3
[cbcdd2c]55CATEGORY_DEFAULT = "Choose category..."
[4d457df]56CATEGORY_STRUCTURE = "Structure Factor"
[3b3b40b]57CATEGORY_CUSTOM = "Plugin Models"
[351b53e]58STRUCTURE_DEFAULT = "None"
[60af928]59
[358b39d]60DEFAULT_POLYDISP_FUNCTION = 'gaussian'
61
[7adc2a8]62
[13cd397]63class ToolTippedItemModel(QtGui.QStandardItemModel):
[f54ce30]64    """
65    Subclass from QStandardItemModel to allow displaying tooltips in
66    QTableView model.
67    """
[d0dfcb2]68    def __init__(self, parent=None):
[13cd397]69        QtGui.QStandardItemModel.__init__(self,parent)
70
[fbfc488]71    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
[f54ce30]72        """
73        Displays tooltip for each column's header
74        :param section:
75        :param orientation:
76        :param role:
77        :return:
78        """
[13cd397]79        if role == QtCore.Qt.ToolTipRole:
80            if orientation == QtCore.Qt.Horizontal:
[b3e8629]81                return str(self.header_tooltips[section])
[ca7c6bd]82
[a95c44b]83        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
[13cd397]84
[4992ff2]85class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
[60af928]86    """
[f46f6dc]87    Main widget for selecting form and structure factor models
[60af928]88    """
[be8f4b0]89    constraintAddedSignal = QtCore.pyqtSignal(list)
90    newModelSignal = QtCore.pyqtSignal()
[3b3b40b]91    fittingFinishedSignal = QtCore.pyqtSignal(tuple)
92    batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
[d4dac80]93    Calc1DFinishedSignal = QtCore.pyqtSignal(tuple)
94    Calc2DFinishedSignal = QtCore.pyqtSignal(tuple)
[3b3b40b]95
[1bc27f1]96    def __init__(self, parent=None, data=None, tab_id=1):
[60af928]97
98        super(FittingWidget, self).__init__()
99
[86f88d1]100        # Necessary globals
[cbcdd2c]101        self.parent = parent
[2a432e7]102
103        # Which tab is this widget displayed in?
104        self.tab_id = tab_id
105
106        # Globals
107        self.initializeGlobals()
108
[d4dac80]109        # data index for the batch set
110        self.data_index = 0
111        # Main Data[12]D holders
112        # Logics.data contains a single Data1D/Data2D object
113        self._logic = [FittingLogic()]
114
[2a432e7]115        # Main GUI setup up
116        self.setupUi(self)
117        self.setWindowTitle("Fitting")
118
119        # Set up tabs widgets
120        self.initializeWidgets()
121
122        # Set up models and views
123        self.initializeModels()
124
125        # Defaults for the structure factors
126        self.setDefaultStructureCombo()
127
128        # Make structure factor and model CBs disabled
129        self.disableModelCombo()
130        self.disableStructureCombo()
131
132        # Generate the category list for display
133        self.initializeCategoryCombo()
134
135        # Connect signals to controls
136        self.initializeSignals()
137
138        # Initial control state
139        self.initializeControls()
140
[d4dac80]141        if data is not None:
142            self.data = data
[2a432e7]143
[457d961]144        # New font to display angstrom symbol
145        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
146        self.label_17.setStyleSheet(new_font)
147        self.label_19.setStyleSheet(new_font)
148
[d4dac80]149    @property
150    def logic(self):
151        # make sure the logic contains at least one element
152        assert(self._logic)
153        # logic connected to the currently shown data
154        return self._logic[self.data_index]
[2a432e7]155
156    @property
157    def data(self):
158        return self.logic.data
159
160    @data.setter
161    def data(self, value):
162        """ data setter """
[f7d14a1]163        # Value is either a list of indices for batch fitting or a simple index
164        # for standard fitting. Assure we have a list, regardless.
[ee18d33]165        if isinstance(value, list):
166            self.is_batch_fitting = True
167        else:
168            value = [value]
169
170        assert isinstance(value[0], QtGui.QStandardItem)
171
172        # Keep reference to all datasets for batch
173        self.all_data = value
[2a432e7]174
[d4dac80]175        # Create logics with data items
[f7d14a1]176        # Logics.data contains only a single Data1D/Data2D object
[87dfca4]177        if len(value) == 1:
178            # single data logic is already defined, update data on it
179            self._logic[0].data = GuiUtils.dataFromItem(value[0])
180        else:
181            # batch datasets
182            for data_item in value:
183                logic = FittingLogic(data=GuiUtils.dataFromItem(data_item))
184                self._logic.append(logic)
[2a432e7]185
186        # Overwrite data type descriptor
187        self.is2D = True if isinstance(self.logic.data, Data2D) else False
188
[f7d14a1]189        # Let others know we're full of data now
[2a432e7]190        self.data_is_loaded = True
191
192        # Enable/disable UI components
193        self.setEnablementOnDataLoad()
194
195    def initializeGlobals(self):
196        """
197        Initialize global variables used in this class
198        """
[cbcdd2c]199        # SasModel is loaded
[60af928]200        self.model_is_loaded = False
[cbcdd2c]201        # Data[12]D passed and set
[5236449]202        self.data_is_loaded = False
[ee18d33]203        # Batch/single fitting
204        self.is_batch_fitting = False
[7fd20fc]205        self.is_chain_fitting = False
[ded5e77]206        # Is the fit job running?
207        self.fit_started=False
208        # The current fit thread
209        self.calc_fit = None
[cbcdd2c]210        # Current SasModel in view
[5236449]211        self.kernel_module = None
[cbcdd2c]212        # Current SasModel view dimension
[60af928]213        self.is2D = False
[cbcdd2c]214        # Current SasModel is multishell
[86f88d1]215        self.model_has_shells = False
[cbcdd2c]216        # Utility variable to enable unselectable option in category combobox
[86f88d1]217        self._previous_category_index = 0
[cbcdd2c]218        # Utility variable for multishell display
[86f88d1]219        self._last_model_row = 0
[cbcdd2c]220        # Dictionary of {model name: model class} for the current category
[5236449]221        self.models = {}
[f182f93]222        # Parameters to fit
223        self.parameters_to_fit = None
[180bd54]224        # Fit options
225        self.q_range_min = 0.005
226        self.q_range_max = 0.1
227        self.npts = 25
228        self.log_points = False
229        self.weighting = 0
[2add354]230        self.chi2 = None
[6011788]231        # Does the control support UNDO/REDO
232        # temporarily off
[2241130]233        self.undo_supported = False
234        self.page_stack = []
[377ade1]235        self.all_data = []
[3b3b40b]236        # custom plugin models
237        # {model.name:model}
238        self.custom_models = self.customModels()
[8222f171]239        # Polydisp widget table default index for function combobox
240        self.orig_poly_index = 3
[d4dac80]241        # copy of current kernel model
242        self.kernel_module_copy = None
[6011788]243
[116dd4c1]244        # Page id for fitting
245        # To keep with previous SasView values, use 200 as the start offset
246        self.page_id = 200 + self.tab_id
247
[d48cc19]248        # Data for chosen model
249        self.model_data = None
250
[a9b568c]251        # Which shell is being currently displayed?
252        self.current_shell_displayed = 0
[0d13814]253        # List of all shell-unique parameters
254        self.shell_names = []
[b00414d]255
256        # Error column presence in parameter display
[f182f93]257        self.has_error_column = False
[aca8418]258        self.has_poly_error_column = False
[b00414d]259        self.has_magnet_error_column = False
[a9b568c]260
[2a432e7]261        # signal communicator
[cbcdd2c]262        self.communicate = self.parent.communicate
[60af928]263
[2a432e7]264    def initializeWidgets(self):
265        """
266        Initialize widgets for tabs
267        """
[180bd54]268        # Options widget
[4992ff2]269        layout = QtWidgets.QGridLayout()
[180bd54]270        self.options_widget = OptionsWidget(self, self.logic)
[1bc27f1]271        layout.addWidget(self.options_widget)
[180bd54]272        self.tabOptions.setLayout(layout)
273
[e1e3e09]274        # Smearing widget
[4992ff2]275        layout = QtWidgets.QGridLayout()
[e1e3e09]276        self.smearing_widget = SmearingWidget(self)
[1bc27f1]277        layout.addWidget(self.smearing_widget)
[180bd54]278        self.tabResolution.setLayout(layout)
[e1e3e09]279
[b1e36a3]280        # Define bold font for use in various controls
[1bc27f1]281        self.boldFont = QtGui.QFont()
[a0f5c36]282        self.boldFont.setBold(True)
283
284        # Set data label
[b1e36a3]285        self.label.setFont(self.boldFont)
286        self.label.setText("No data loaded")
287        self.lblFilename.setText("")
288
[6ff2eb3]289        # Magnetic angles explained in one picture
[4992ff2]290        self.magneticAnglesWidget = QtWidgets.QWidget()
291        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
[6ff2eb3]292        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.bmp')
293        labl.setPixmap(pixmap)
294        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
295
[2a432e7]296    def initializeModels(self):
297        """
298        Set up models and views
299        """
[86f88d1]300        # Set the main models
[cd31251]301        # We can't use a single model here, due to restrictions on flattening
302        # the model tree with subclassed QAbstractProxyModel...
[13cd397]303        self._model_model = ToolTippedItemModel()
304        self._poly_model = ToolTippedItemModel()
305        self._magnet_model = ToolTippedItemModel()
[60af928]306
307        # Param model displayed in param list
308        self.lstParams.setModel(self._model_model)
[5236449]309        self.readCategoryInfo()
[4992ff2]310
[60af928]311        self.model_parameters = None
[ad6b4e2]312
313        # Delegates for custom editing and display
314        self.lstParams.setItemDelegate(ModelViewDelegate(self))
315
[86f88d1]316        self.lstParams.setAlternatingRowColors(True)
[61a92d4]317        stylesheet = """
[457d961]318
319            QTreeView {
320                paint-alternating-row-colors-for-empty-area:0;
321            }
322
[2a432e7]323            QTreeView::item:hover {
324                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
325                border: 1px solid #bfcde4;
[61a92d4]326            }
[2a432e7]327
328            QTreeView::item:selected {
329                border: 1px solid #567dbc;
330            }
331
332            QTreeView::item:selected:active{
333                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
334            }
335
336            QTreeView::item:selected:!active {
337                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
338            }
339           """
[61a92d4]340        self.lstParams.setStyleSheet(stylesheet)
[672b8ab]341        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
[7fd20fc]342        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
[457d961]343        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
[60af928]344        # Poly model displayed in poly list
[811bec1]345        self.lstPoly.setModel(self._poly_model)
[60af928]346        self.setPolyModel()
347        self.setTableProperties(self.lstPoly)
[6011788]348        # Delegates for custom editing and display
[aca8418]349        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
350        # Polydispersity function combo response
351        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
[e43fc91]352        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
[60af928]353
354        # Magnetism model displayed in magnetism list
355        self.lstMagnetic.setModel(self._magnet_model)
356        self.setMagneticModel()
357        self.setTableProperties(self.lstMagnetic)
[b00414d]358        # Delegates for custom editing and display
359        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
[60af928]360
[2a432e7]361    def initializeCategoryCombo(self):
362        """
363        Model category combo setup
364        """
[60af928]365        category_list = sorted(self.master_category_dict.keys())
[86f88d1]366        self.cbCategory.addItem(CATEGORY_DEFAULT)
[60af928]367        self.cbCategory.addItems(category_list)
[4d457df]368        self.cbCategory.addItem(CATEGORY_STRUCTURE)
[6f7f652]369        self.cbCategory.setCurrentIndex(0)
[60af928]370
[e1e3e09]371    def setEnablementOnDataLoad(self):
372        """
373        Enable/disable various UI elements based on data loaded
374        """
[cbcdd2c]375        # Tag along functionality
[b1e36a3]376        self.label.setText("Data loaded from: ")
[a0f5c36]377        self.lblFilename.setText(self.logic.data.filename)
[5236449]378        self.updateQRange()
[e1e3e09]379        # Switch off Data2D control
380        self.chk2DView.setEnabled(False)
381        self.chk2DView.setVisible(False)
[6ff2eb3]382        self.chkMagnetism.setEnabled(self.is2D)
[e20870bc]383        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked())
[ee18d33]384        # Combo box or label for file name"
385        if self.is_batch_fitting:
386            self.lblFilename.setVisible(False)
387            for dataitem in self.all_data:
388                filename = GuiUtils.dataFromItem(dataitem).filename
389                self.cbFileNames.addItem(filename)
390            self.cbFileNames.setVisible(True)
[7fd20fc]391            self.chkChainFit.setEnabled(True)
392            self.chkChainFit.setVisible(True)
[38eb433]393            # This panel is not designed to view individual fits, so disable plotting
394            self.cmdPlot.setVisible(False)
[180bd54]395        # Similarly on other tabs
396        self.options_widget.setEnablementOnDataLoad()
[f7d14a1]397        self.onSelectModel()
[e1e3e09]398        # Smearing tab
[9a7c81c]399        self.smearing_widget.updateData(self.data)
[60af928]400
[f46f6dc]401    def acceptsData(self):
402        """ Tells the caller this widget can accept new dataset """
[5236449]403        return not self.data_is_loaded
[f46f6dc]404
[6f7f652]405    def disableModelCombo(self):
[cbcdd2c]406        """ Disable the combobox """
[6f7f652]407        self.cbModel.setEnabled(False)
[b1e36a3]408        self.lblModel.setEnabled(False)
[6f7f652]409
410    def enableModelCombo(self):
[cbcdd2c]411        """ Enable the combobox """
[6f7f652]412        self.cbModel.setEnabled(True)
[b1e36a3]413        self.lblModel.setEnabled(True)
[6f7f652]414
415    def disableStructureCombo(self):
[cbcdd2c]416        """ Disable the combobox """
[6f7f652]417        self.cbStructureFactor.setEnabled(False)
[b1e36a3]418        self.lblStructure.setEnabled(False)
[6f7f652]419
420    def enableStructureCombo(self):
[cbcdd2c]421        """ Enable the combobox """
[6f7f652]422        self.cbStructureFactor.setEnabled(True)
[b1e36a3]423        self.lblStructure.setEnabled(True)
[6f7f652]424
[0268aed]425    def togglePoly(self, isChecked):
[454670d]426        """ Enable/disable the polydispersity tab """
[0268aed]427        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
428
429    def toggleMagnetism(self, isChecked):
[454670d]430        """ Enable/disable the magnetism tab """
[0268aed]431        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
432
[7fd20fc]433    def toggleChainFit(self, isChecked):
434        """ Enable/disable chain fitting """
435        self.is_chain_fitting = isChecked
436
[0268aed]437    def toggle2D(self, isChecked):
[454670d]438        """ Enable/disable the controls dependent on 1D/2D data instance """
[0268aed]439        self.chkMagnetism.setEnabled(isChecked)
440        self.is2D = isChecked
[1970780]441        # Reload the current model
[e1e3e09]442        if self.kernel_module:
443            self.onSelectModel()
[5236449]444
[8b480d27]445    @classmethod
446    def customModels(cls):
[3b3b40b]447        """ Reads in file names in the custom plugin directory """
448        return ModelUtilities._find_models()
449
[86f88d1]450    def initializeControls(self):
451        """
452        Set initial control enablement
453        """
[ee18d33]454        self.cbFileNames.setVisible(False)
[86f88d1]455        self.cmdFit.setEnabled(False)
[d48cc19]456        self.cmdPlot.setEnabled(False)
[180bd54]457        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
[86f88d1]458        self.chkPolydispersity.setEnabled(True)
459        self.chkPolydispersity.setCheckState(False)
460        self.chk2DView.setEnabled(True)
461        self.chk2DView.setCheckState(False)
462        self.chkMagnetism.setEnabled(False)
463        self.chkMagnetism.setCheckState(False)
[7fd20fc]464        self.chkChainFit.setEnabled(False)
465        self.chkChainFit.setVisible(False)
[cbcdd2c]466        # Tabs
[86f88d1]467        self.tabFitting.setTabEnabled(TAB_POLY, False)
468        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
469        self.lblChi2Value.setText("---")
[e1e3e09]470        # Smearing tab
[9a7c81c]471        self.smearing_widget.updateData(self.data)
[180bd54]472        # Line edits in the option tab
473        self.updateQRange()
[86f88d1]474
475    def initializeSignals(self):
476        """
477        Connect GUI element signals
478        """
[cbcdd2c]479        # Comboboxes
[cd31251]480        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
481        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
482        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
[ee18d33]483        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
[cbcdd2c]484        # Checkboxes
[86f88d1]485        self.chk2DView.toggled.connect(self.toggle2D)
486        self.chkPolydispersity.toggled.connect(self.togglePoly)
487        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
[7fd20fc]488        self.chkChainFit.toggled.connect(self.toggleChainFit)
[cbcdd2c]489        # Buttons
[5236449]490        self.cmdFit.clicked.connect(self.onFit)
[cbcdd2c]491        self.cmdPlot.clicked.connect(self.onPlot)
[2add354]492        self.cmdHelp.clicked.connect(self.onHelp)
[6ff2eb3]493        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
[cbcdd2c]494
495        # Respond to change in parameters from the UI
[b00414d]496        self._model_model.itemChanged.connect(self.onMainParamsChange)
[eae226b]497        #self.constraintAddedSignal.connect(self.modifyViewOnConstraint)
[cd31251]498        self._poly_model.itemChanged.connect(self.onPolyModelChange)
[b00414d]499        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
[3b3b40b]500        self.lstParams.selectionModel().selectionChanged.connect(self.onSelectionChanged)
501
502        # Local signals
503        self.batchFittingFinishedSignal.connect(self.batchFitComplete)
504        self.fittingFinishedSignal.connect(self.fitComplete)
[d4dac80]505        self.Calc1DFinishedSignal.connect(self.complete1D)
506        self.Calc2DFinishedSignal.connect(self.complete2D)
[86f88d1]507
[180bd54]508        # Signals from separate tabs asking for replot
509        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
[e20870bc]510        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
[180bd54]511
[3b3b40b]512        # Signals from other widgets
513        self.communicate.customModelDirectoryChanged.connect(self.onCustomModelChange)
[57be490]514        self.communicate.saveAnalysisSignal.connect(self.savePageState)
[9a7c81c]515        self.smearing_widget.smearingChangedSignal.connect(self.onSmearingOptionsUpdate)
[57be490]516        #self.communicate.saveReportSignal.connect(self.saveReport)
[3b3b40b]517
[d3c0b95]518    def modelName(self):
519        """
520        Returns model name, by default M<tab#>, e.g. M1, M2
521        """
522        return "M%i" % self.tab_id
523
524    def nameForFittedData(self, name):
525        """
526        Generate name for the current fit
527        """
528        if self.is2D:
529            name += "2d"
530        name = "%s [%s]" % (self.modelName(), name)
531        return name
[7fd20fc]532
[d3c0b95]533    def showModelContextMenu(self, position):
534        """
535        Show context specific menu in the parameter table.
536        When clicked on parameter(s): fitting/constraints options
537        When clicked on white space: model description
538        """
[eae226b]539        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows()]
[7fd20fc]540        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
541        try:
542            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
543        except AttributeError as ex:
544            logging.error("Error generating context menu: %s" % ex)
545        return
546
547    def modelContextMenu(self, rows):
[eae226b]548        """
[d3c0b95]549        Create context menu for the parameter selection
[eae226b]550        """
[7fd20fc]551        menu = QtWidgets.QMenu()
[eae226b]552        num_rows = len(rows)
[63319b0]553        if num_rows < 1:
554            return menu
[7fd20fc]555        # Select for fitting
[eae226b]556        param_string = "parameter " if num_rows==1 else "parameters "
557        to_string = "to its current value" if num_rows==1 else "to their current values"
[d3c0b95]558        has_constraints = any([self.rowHasConstraint(i) for i in rows])
[eae226b]559
[7fd20fc]560        self.actionSelect = QtWidgets.QAction(self)
561        self.actionSelect.setObjectName("actionSelect")
[eae226b]562        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
[7fd20fc]563        # Unselect from fitting
564        self.actionDeselect = QtWidgets.QAction(self)
565        self.actionDeselect.setObjectName("actionDeselect")
[eae226b]566        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
[7fd20fc]567
568        self.actionConstrain = QtWidgets.QAction(self)
569        self.actionConstrain.setObjectName("actionConstrain")
[eae226b]570        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain "+param_string + to_string))
571
572        self.actionRemoveConstraint = QtWidgets.QAction(self)
573        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
574        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove constraint"))
[7fd20fc]575
576        self.actionMultiConstrain = QtWidgets.QAction(self)
577        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
578        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
579
580        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
581        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
582        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
583
584        menu.addAction(self.actionSelect)
585        menu.addAction(self.actionDeselect)
586        menu.addSeparator()
587
[d3c0b95]588        if has_constraints:
[eae226b]589            menu.addAction(self.actionRemoveConstraint)
[d3c0b95]590            #if num_rows == 1:
591            #    menu.addAction(self.actionEditConstraint)
[eae226b]592        else:
[7fd20fc]593            menu.addAction(self.actionConstrain)
[d3c0b95]594            if num_rows == 2:
595                menu.addAction(self.actionMutualMultiConstrain)
[7fd20fc]596
597        # Define the callbacks
[0595bb7]598        self.actionConstrain.triggered.connect(self.addSimpleConstraint)
[eae226b]599        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
[0595bb7]600        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
[7fd20fc]601        self.actionSelect.triggered.connect(self.selectParameters)
602        self.actionDeselect.triggered.connect(self.deselectParameters)
603        return menu
604
[0595bb7]605    def showMultiConstraint(self):
[7fd20fc]606        """
607        Show the constraint widget and receive the expression
608        """
[0595bb7]609        selected_rows = self.lstParams.selectionModel().selectedRows()
[6e58f2f]610        # There have to be only two rows selected. The caller takes care of that
611        # but let's check the correctness.
[676f137]612        assert(len(selected_rows)==2)
[0595bb7]613
614        params_list = [s.data() for s in selected_rows]
[eae226b]615        # Create and display the widget for param1 and param2
[7fd20fc]616        mc_widget = MultiConstraint(self, params=params_list)
[eae226b]617        if mc_widget.exec_() != QtWidgets.QDialog.Accepted:
618            return
619
[0595bb7]620        constraint = Constraint()
621        c_text = mc_widget.txtConstraint.text()
622
[eae226b]623        # widget.params[0] is the parameter we're constraining
624        constraint.param = mc_widget.params[0]
[3b3b40b]625        # parameter should have the model name preamble
[06234fc]626        model_name = self.kernel_module.name
[3b3b40b]627        # param_used is the parameter we're using in constraining function
628        param_used = mc_widget.params[1]
629        # Replace param_used with model_name.param_used
630        updated_param_used = model_name + "." + param_used
631        new_func = c_text.replace(param_used, updated_param_used)
632        constraint.func = new_func
[d3c0b95]633        # Which row is the constrained parameter in?
[2d466e4]634        row = self.getRowFromName(constraint.param)
[eae226b]635
[6e58f2f]636        # Create a new item and add the Constraint object as a child
637        self.addConstraintToRow(constraint=constraint, row=row)
[eae226b]638
[2d466e4]639    def getRowFromName(self, name):
[eae226b]640        """
[d3c0b95]641        Given parameter name get the row number in self._model_model
[eae226b]642        """
643        for row in range(self._model_model.rowCount()):
644            row_name = self._model_model.item(row).text()
645            if row_name == name:
646                return row
647        return None
648
[2d466e4]649    def getParamNames(self):
650        """
651        Return list of all parameters for the current model
652        """
653        return [self._model_model.item(row).text() for row in range(self._model_model.rowCount())]
654
[eae226b]655    def modifyViewOnRow(self, row, font=None, brush=None):
656        """
657        Chage how the given row of the main model is shown
658        """
659        fields_enabled = False
660        if font is None:
661            font = QtGui.QFont()
662            fields_enabled = True
663        if brush is None:
664            brush = QtGui.QBrush()
665            fields_enabled = True
[0595bb7]666        self._model_model.blockSignals(True)
667        # Modify font and foreground of affected rows
668        for column in range(0, self._model_model.columnCount()):
669            self._model_model.item(row, column).setForeground(brush)
670            self._model_model.item(row, column).setFont(font)
[eae226b]671            self._model_model.item(row, column).setEditable(fields_enabled)
[0595bb7]672        self._model_model.blockSignals(False)
673
[c5a2722f]674    def addConstraintToRow(self, constraint=None, row=0):
675        """
676        Adds the constraint object to requested row
677        """
678        # Create a new item and add the Constraint object as a child
679        assert(isinstance(constraint, Constraint))
680        assert(0<=row<=self._model_model.rowCount())
681
682        item = QtGui.QStandardItem()
683        item.setData(constraint)
684        self._model_model.item(row, 1).setChild(0, item)
685        # Set min/max to the value constrained
686        self.constraintAddedSignal.emit([row])
687        # Show visual hints for the constraint
688        font = QtGui.QFont()
689        font.setItalic(True)
690        brush = QtGui.QBrush(QtGui.QColor('blue'))
691        self.modifyViewOnRow(row, font=font, brush=brush)
692        self.communicate.statusBarUpdateSignal.emit('Constraint added')
693
[0595bb7]694    def addSimpleConstraint(self):
[7fd20fc]695        """
696        Adds a constraint on a single parameter.
[2add354]697        """
[d3c0b95]698        min_col = self.lstParams.itemDelegate().param_min
699        max_col = self.lstParams.itemDelegate().param_max
[0595bb7]700        for row in self.selectedParameters():
701            param = self._model_model.item(row, 0).text()
702            value = self._model_model.item(row, 1).text()
[235d766]703            min_t = self._model_model.item(row, min_col).text()
704            max_t = self._model_model.item(row, max_col).text()
[eae226b]705            # Create a Constraint object
[235d766]706            constraint = Constraint(param=param, value=value, min=min_t, max=max_t)
[eae226b]707            # Create a new item and add the Constraint object as a child
[0595bb7]708            item = QtGui.QStandardItem()
709            item.setData(constraint)
710            self._model_model.item(row, 1).setChild(0, item)
[235d766]711            # Assumed correctness from the validator
712            value = float(value)
713            # BUMPS calculates log(max-min) without any checks, so let's assign minor range
714            min_v = value - (value/10000.0)
715            max_v = value + (value/10000.0)
[eae226b]716            # Set min/max to the value constrained
[235d766]717            self._model_model.item(row, min_col).setText(str(min_v))
718            self._model_model.item(row, max_col).setText(str(max_v))
[be8f4b0]719            self.constraintAddedSignal.emit([row])
[eae226b]720            # Show visual hints for the constraint
721            font = QtGui.QFont()
722            font.setItalic(True)
723            brush = QtGui.QBrush(QtGui.QColor('blue'))
724            self.modifyViewOnRow(row, font=font, brush=brush)
[7fd20fc]725        self.communicate.statusBarUpdateSignal.emit('Constraint added')
726
[eae226b]727    def deleteConstraint(self):
728        """
729        Delete constraints from selected parameters.
730        """
[3b3b40b]731        params =  [s.data() for s in self.lstParams.selectionModel().selectedRows()
732                   if self.isCheckable(s.row())]
733        for param in params:
734            self.deleteConstraintOnParameter(param=param)
[be8f4b0]735
736    def deleteConstraintOnParameter(self, param=None):
737        """
738        Delete the constraint on model parameter 'param'
739        """
[d3c0b95]740        min_col = self.lstParams.itemDelegate().param_min
741        max_col = self.lstParams.itemDelegate().param_max
[be8f4b0]742        for row in range(self._model_model.rowCount()):
[3b3b40b]743            if not self.rowHasConstraint(row):
744                continue
[eae226b]745            # Get the Constraint object from of the model item
746            item = self._model_model.item(row, 1)
[3b3b40b]747            constraint = self.getConstraintForRow(row)
[d3c0b95]748            if constraint is None:
749                continue
750            if not isinstance(constraint, Constraint):
751                continue
[be8f4b0]752            if param and constraint.param != param:
753                continue
754            # Now we got the right row. Delete the constraint and clean up
[eae226b]755            # Retrieve old values and put them on the model
756            if constraint.min is not None:
[d3c0b95]757                self._model_model.item(row, min_col).setText(constraint.min)
[eae226b]758            if constraint.max is not None:
[d3c0b95]759                self._model_model.item(row, max_col).setText(constraint.max)
[eae226b]760            # Remove constraint item
761            item.removeRow(0)
[be8f4b0]762            self.constraintAddedSignal.emit([row])
[eae226b]763            self.modifyViewOnRow(row)
[be8f4b0]764
[eae226b]765        self.communicate.statusBarUpdateSignal.emit('Constraint removed')
[be8f4b0]766
[d3c0b95]767    def getConstraintForRow(self, row):
768        """
769        For the given row, return its constraint, if any
770        """
771        try:
772            item = self._model_model.item(row, 1)
773            return item.child(0).data()
774        except AttributeError:
775            # return none when no constraints
776            return None
777
[eae226b]778    def rowHasConstraint(self, row):
779        """
780        Finds out if row of the main model has a constraint child
781        """
782        item = self._model_model.item(row,1)
[be8f4b0]783        if item.hasChildren():
784            c = item.child(0).data()
[235d766]785            if isinstance(c, Constraint):
[be8f4b0]786                return True
787        return False
[116dd4c1]788
789    def rowHasActiveConstraint(self, row):
790        """
791        Finds out if row of the main model has an active constraint child
792        """
793        item = self._model_model.item(row,1)
794        if item.hasChildren():
795            c = item.child(0).data()
[235d766]796            if isinstance(c, Constraint) and c.active:
797                return True
798        return False
799
800    def rowHasActiveComplexConstraint(self, row):
801        """
802        Finds out if row of the main model has an active, nontrivial constraint child
803        """
804        item = self._model_model.item(row,1)
805        if item.hasChildren():
806            c = item.child(0).data()
[116dd4c1]807            if isinstance(c, Constraint) and c.func and c.active:
808                return True
809        return False
[eae226b]810
[7fd20fc]811    def selectParameters(self):
812        """
[d3c0b95]813        Selected parameter is chosen for fitting
[7fd20fc]814        """
815        status = QtCore.Qt.Checked
816        self.setParameterSelection(status)
817
818    def deselectParameters(self):
819        """
820        Selected parameters are removed for fitting
821        """
822        status = QtCore.Qt.Unchecked
823        self.setParameterSelection(status)
824
825    def selectedParameters(self):
826        """ Returns list of selected (highlighted) parameters """
[d3c0b95]827        return [s.row() for s in self.lstParams.selectionModel().selectedRows()
828                if self.isCheckable(s.row())]
[7fd20fc]829
830    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
831        """
832        Selected parameters are chosen for fitting
833        """
834        # Convert to proper indices and set requested enablement
835        for row in self.selectedParameters():
836            self._model_model.item(row, 0).setCheckState(status)
[d3c0b95]837
838    def getConstraintsForModel(self):
839        """
840        Return a list of tuples. Each tuple contains constraints mapped as
841        ('constrained parameter', 'function to constrain')
842        e.g. [('sld','5*sld_solvent')]
843        """
844        param_number = self._model_model.rowCount()
845        params = [(self._model_model.item(s, 0).text(),
[c5a2722f]846                    self._model_model.item(s, 1).child(0).data().func)
[116dd4c1]847                    for s in range(param_number) if self.rowHasActiveConstraint(s)]
[d3c0b95]848        return params
[7fd20fc]849
[235d766]850    def getComplexConstraintsForModel(self):
851        """
852        Return a list of tuples. Each tuple contains constraints mapped as
853        ('constrained parameter', 'function to constrain')
[06234fc]854        e.g. [('sld','5*M2.sld_solvent')].
[235d766]855        Only for constraints with defined VALUE
856        """
857        param_number = self._model_model.rowCount()
858        params = [(self._model_model.item(s, 0).text(),
859                    self._model_model.item(s, 1).child(0).data().func)
860                    for s in range(param_number) if self.rowHasActiveComplexConstraint(s)]
861        return params
862
[ba01ad1]863    def getConstraintObjectsForModel(self):
864        """
865        Returns Constraint objects present on the whole model
866        """
867        param_number = self._model_model.rowCount()
868        constraints = [self._model_model.item(s, 1).child(0).data()
869                       for s in range(param_number) if self.rowHasConstraint(s)]
870
871        return constraints
872
[3b3b40b]873    def getConstraintsForFitting(self):
874        """
875        Return a list of constraints in format ready for use in fiting
876        """
877        # Get constraints
878        constraints = self.getComplexConstraintsForModel()
879        # See if there are any constraints across models
880        multi_constraints = [cons for cons in constraints if self.isConstraintMultimodel(cons[1])]
881
882        if multi_constraints:
883            # Let users choose what to do
884            msg = "The current fit contains constraints relying on other fit pages.\n"
885            msg += "Parameters with those constraints are:\n" +\
886                '\n'.join([cons[0] for cons in multi_constraints])
887            msg += "\n\nWould you like to remove these constraints or cancel fitting?"
888            msgbox = QtWidgets.QMessageBox(self)
889            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
890            msgbox.setText(msg)
891            msgbox.setWindowTitle("Existing Constraints")
892            # custom buttons
893            button_remove = QtWidgets.QPushButton("Remove")
894            msgbox.addButton(button_remove, QtWidgets.QMessageBox.YesRole)
895            button_cancel = QtWidgets.QPushButton("Cancel")
896            msgbox.addButton(button_cancel, QtWidgets.QMessageBox.RejectRole)
897            retval = msgbox.exec_()
898            if retval == QtWidgets.QMessageBox.RejectRole:
899                # cancel fit
900                raise ValueError("Fitting cancelled")
901            else:
902                # remove constraint
903                for cons in multi_constraints:
904                    self.deleteConstraintOnParameter(param=cons[0])
905                # re-read the constraints
906                constraints = self.getComplexConstraintsForModel()
907
908        return constraints
909
[7fd20fc]910    def showModelDescription(self):
911        """
912        Creates a window with model description, when right clicked in the treeview
[2add354]913        """
914        msg = 'Model description:\n'
915        if self.kernel_module is not None:
916            if str(self.kernel_module.description).rstrip().lstrip() == '':
917                msg += "Sorry, no information is available for this model."
918            else:
919                msg += self.kernel_module.description + '\n'
920        else:
921            msg += "You must select a model to get information on this"
922
[4992ff2]923        menu = QtWidgets.QMenu()
924        label = QtWidgets.QLabel(msg)
[d6b8a1d]925        action = QtWidgets.QWidgetAction(self)
[672b8ab]926        action.setDefaultWidget(label)
927        menu.addAction(action)
[7fd20fc]928        return menu
[2add354]929
[0268aed]930    def onSelectModel(self):
[cbcdd2c]931        """
[0268aed]932        Respond to select Model from list event
[cbcdd2c]933        """
[d6b8a1d]934        model = self.cbModel.currentText()
[0268aed]935
[b3e8629]936        # empty combobox forced to be read
937        if not model:
938            return
[0268aed]939        # Reset structure factor
940        self.cbStructureFactor.setCurrentIndex(0)
941
[f182f93]942        # Reset parameters to fit
943        self.parameters_to_fit = None
[d7ff531]944        self.has_error_column = False
[aca8418]945        self.has_poly_error_column = False
[f182f93]946
[fd1ae6d1]947        self.respondToModelStructure(model=model, structure_factor=None)
948
[ee18d33]949    def onSelectBatchFilename(self, data_index):
950        """
951        Update the logic based on the selected file in batch fitting
952        """
[d4dac80]953        self.data_index = data_index
[ee18d33]954        self.updateQRange()
955
[fd1ae6d1]956    def onSelectStructureFactor(self):
957        """
958        Select Structure Factor from list
959        """
960        model = str(self.cbModel.currentText())
961        category = str(self.cbCategory.currentText())
962        structure = str(self.cbStructureFactor.currentText())
963        if category == CATEGORY_STRUCTURE:
964            model = None
965        self.respondToModelStructure(model=model, structure_factor=structure)
966
[3b3b40b]967    def onCustomModelChange(self):
968        """
969        Reload the custom model combobox
970        """
971        self.custom_models = self.customModels()
972        self.readCustomCategoryInfo()
973        # See if we need to update the combo in-place
974        if self.cbCategory.currentText() != CATEGORY_CUSTOM: return
975
976        current_text = self.cbModel.currentText()
977        self.cbModel.blockSignals(True)
978        self.cbModel.clear()
979        self.cbModel.blockSignals(False)
980        self.enableModelCombo()
981        self.disableStructureCombo()
982        # Retrieve the list of models
983        model_list = self.master_category_dict[CATEGORY_CUSTOM]
984        # Populate the models combobox
985        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
986        new_index = self.cbModel.findText(current_text)
987        if new_index != -1:
988            self.cbModel.setCurrentIndex(self.cbModel.findText(current_text))
989
990    def onSelectionChanged(self):
991        """
992        React to parameter selection
993        """
994        rows = self.lstParams.selectionModel().selectedRows()
995        # Clean previous messages
996        self.communicate.statusBarUpdateSignal.emit("")
997        if len(rows) == 1:
998            # Show constraint, if present
999            row = rows[0].row()
1000            if self.rowHasConstraint(row):
1001                func = self.getConstraintForRow(row).func
1002                if func is not None:
1003                    self.communicate.statusBarUpdateSignal.emit("Active constrain: "+func)
1004
[47d7d2d]1005    def replaceConstraintName(self, old_name, new_name=""):
1006        """
1007        Replace names of models in defined constraints
1008        """
1009        param_number = self._model_model.rowCount()
1010        # loop over parameters
1011        for row in range(param_number):
1012            if self.rowHasConstraint(row):
1013                func = self._model_model.item(row, 1).child(0).data().func
1014                if old_name in func:
1015                    new_func = func.replace(old_name, new_name)
1016                    self._model_model.item(row, 1).child(0).data().func = new_func
1017
[3b3b40b]1018    def isConstraintMultimodel(self, constraint):
1019        """
1020        Check if the constraint function text contains current model name
1021        """
1022        current_model_name = self.kernel_module.name
1023        if current_model_name in constraint:
1024            return False
1025        else:
1026            return True
1027
[9c0ce68]1028    def updateData(self):
1029        """
1030        Helper function for recalculation of data used in plotting
1031        """
1032        # Update the chart
[0268aed]1033        if self.data_is_loaded:
[d48cc19]1034            self.cmdPlot.setText("Show Plot")
[0268aed]1035            self.calculateQGridForModel()
1036        else:
[d48cc19]1037            self.cmdPlot.setText("Calculate")
[0268aed]1038            # Create default datasets if no data passed
1039            self.createDefaultDataset()
1040
[9c0ce68]1041    def respondToModelStructure(self, model=None, structure_factor=None):
1042        # Set enablement on calculate/plot
1043        self.cmdPlot.setEnabled(True)
1044
1045        # kernel parameters -> model_model
1046        self.SASModelToQModel(model, structure_factor)
1047
1048        # Update plot
1049        self.updateData()
1050
[6011788]1051        # Update state stack
[00b3b40]1052        self.updateUndo()
[2add354]1053
[be8f4b0]1054        # Let others know
1055        self.newModelSignal.emit()
1056
[cd31251]1057    def onSelectCategory(self):
[60af928]1058        """
1059        Select Category from list
1060        """
[d6b8a1d]1061        category = self.cbCategory.currentText()
[86f88d1]1062        # Check if the user chose "Choose category entry"
[4d457df]1063        if category == CATEGORY_DEFAULT:
[86f88d1]1064            # if the previous category was not the default, keep it.
1065            # Otherwise, just return
1066            if self._previous_category_index != 0:
[351b53e]1067                # We need to block signals, or else state changes on perceived unchanged conditions
1068                self.cbCategory.blockSignals(True)
[86f88d1]1069                self.cbCategory.setCurrentIndex(self._previous_category_index)
[351b53e]1070                self.cbCategory.blockSignals(False)
[86f88d1]1071            return
1072
[4d457df]1073        if category == CATEGORY_STRUCTURE:
[6f7f652]1074            self.disableModelCombo()
1075            self.enableStructureCombo()
[29eb947]1076            self._model_model.clear()
[6f7f652]1077            return
[8b480d27]1078
[cbcdd2c]1079        # Safely clear and enable the model combo
[6f7f652]1080        self.cbModel.blockSignals(True)
1081        self.cbModel.clear()
1082        self.cbModel.blockSignals(False)
1083        self.enableModelCombo()
1084        self.disableStructureCombo()
1085
[86f88d1]1086        self._previous_category_index = self.cbCategory.currentIndex()
[cbcdd2c]1087        # Retrieve the list of models
[4d457df]1088        model_list = self.master_category_dict[category]
[cbcdd2c]1089        # Populate the models combobox
[b1e36a3]1090        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
[4d457df]1091
[0268aed]1092    def onPolyModelChange(self, item):
1093        """
1094        Callback method for updating the main model and sasmodel
1095        parameters with the GUI values in the polydispersity view
1096        """
1097        model_column = item.column()
1098        model_row = item.row()
1099        name_index = self._poly_model.index(model_row, 0)
[7fb471d]1100        parameter_name = str(name_index.data()).lower() # "distribution of sld" etc.
[c1e380e]1101        if "distribution of" in parameter_name:
[358b39d]1102            # just the last word
1103            parameter_name = parameter_name.rsplit()[-1]
[c1e380e]1104
[06b0138]1105        # Extract changed value.
[8eaa101]1106        if model_column == self.lstPoly.itemDelegate().poly_parameter:
[00b3b40]1107            # Is the parameter checked for fitting?
[0268aed]1108            value = item.checkState()
[1643d8ed]1109            parameter_name = parameter_name + '.width'
[c1e380e]1110            if value == QtCore.Qt.Checked:
1111                self.parameters_to_fit.append(parameter_name)
1112            else:
1113                if parameter_name in self.parameters_to_fit:
1114                    self.parameters_to_fit.remove(parameter_name)
[b00414d]1115            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
[c1e380e]1116            return
[8eaa101]1117        elif model_column in [self.lstPoly.itemDelegate().poly_min, self.lstPoly.itemDelegate().poly_max]:
[aca8418]1118            try:
[fbfc488]1119                value = GuiUtils.toDouble(item.text())
[0261bc1]1120            except TypeError:
[aca8418]1121                # Can't be converted properly, bring back the old value and exit
1122                return
1123
1124            current_details = self.kernel_module.details[parameter_name]
1125            current_details[model_column-1] = value
[8eaa101]1126        elif model_column == self.lstPoly.itemDelegate().poly_function:
[919d47c]1127            # name of the function - just pass
1128            return
[0268aed]1129        else:
1130            try:
[fbfc488]1131                value = GuiUtils.toDouble(item.text())
[0261bc1]1132            except TypeError:
[0268aed]1133                # Can't be converted properly, bring back the old value and exit
1134                return
1135
[aca8418]1136            # Update the sasmodel
1137            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
[919d47c]1138            self.kernel_module.setParam(parameter_name + '.' + \
[8eaa101]1139                                        self.lstPoly.itemDelegate().columnDict()[model_column], value)
[0268aed]1140
[9c0ce68]1141            # Update plot
1142            self.updateData()
1143
[b00414d]1144    def onMagnetModelChange(self, item):
1145        """
1146        Callback method for updating the sasmodel magnetic parameters with the GUI values
1147        """
1148        model_column = item.column()
1149        model_row = item.row()
1150        name_index = self._magnet_model.index(model_row, 0)
[fbfc488]1151        parameter_name = str(self._magnet_model.data(name_index))
[b00414d]1152
1153        if model_column == 0:
1154            value = item.checkState()
1155            if value == QtCore.Qt.Checked:
1156                self.parameters_to_fit.append(parameter_name)
1157            else:
1158                if parameter_name in self.parameters_to_fit:
1159                    self.parameters_to_fit.remove(parameter_name)
1160            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1161            # Update state stack
1162            self.updateUndo()
1163            return
1164
1165        # Extract changed value.
1166        try:
[fbfc488]1167            value = GuiUtils.toDouble(item.text())
[0261bc1]1168        except TypeError:
[b00414d]1169            # Unparsable field
1170            return
1171
[fbfc488]1172        property_index = self._magnet_model.headerData(1, model_column)-1 # Value, min, max, etc.
[b00414d]1173
1174        # Update the parameter value - note: this supports +/-inf as well
1175        self.kernel_module.params[parameter_name] = value
1176
1177        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1178        self.kernel_module.details[parameter_name][property_index] = value
1179
1180        # Force the chart update when actual parameters changed
1181        if model_column == 1:
1182            self.recalculatePlotData()
1183
1184        # Update state stack
1185        self.updateUndo()
1186
[2add354]1187    def onHelp(self):
1188        """
1189        Show the "Fitting" section of help
1190        """
[aed0532]1191        tree_location = "/user/qtgui/Perspectives/Fitting/"
[70080a0]1192
1193        # Actual file will depend on the current tab
1194        tab_id = self.tabFitting.currentIndex()
1195        helpfile = "fitting.html"
1196        if tab_id == 0:
1197            helpfile = "fitting_help.html"
1198        elif tab_id == 1:
1199            helpfile = "residuals_help.html"
1200        elif tab_id == 2:
[e90988c]1201            helpfile = "resolution.html"
[70080a0]1202        elif tab_id == 3:
[e90988c]1203            helpfile = "pd/polydispersity.html"
[70080a0]1204        elif tab_id == 4:
[e90988c]1205            helpfile = "magnetism/magnetism.html"
[70080a0]1206        help_location = tree_location + helpfile
[d6b8a1d]1207
[e90988c]1208        self.showHelp(help_location)
1209
1210    def showHelp(self, url):
1211        """
1212        Calls parent's method for opening an HTML page
1213        """
1214        self.parent.showHelp(url)
[2add354]1215
[6ff2eb3]1216    def onDisplayMagneticAngles(self):
1217        """
1218        Display a simple image showing direction of magnetic angles
1219        """
1220        self.magneticAnglesWidget.show()
1221
[0268aed]1222    def onFit(self):
1223        """
1224        Perform fitting on the current data
1225        """
[ded5e77]1226        if self.fit_started:
1227            self.stopFit()
1228            return
1229
[116dd4c1]1230        # initialize fitter constants
[f182f93]1231        fit_id = 0
1232        handler = None
1233        batch_inputs = {}
1234        batch_outputs = {}
1235        #---------------------------------
[14ec91c5]1236        if LocalConfig.USING_TWISTED:
[7adc2a8]1237            handler = None
1238            updater = None
1239        else:
1240            handler = ConsoleUpdate(parent=self.parent,
1241                                    manager=self,
1242                                    improvement_delta=0.1)
1243            updater = handler.update_fit
[f182f93]1244
[116dd4c1]1245        # Prepare the fitter object
[c6343a5]1246        try:
1247            fitters, _ = self.prepareFitters()
1248        except ValueError as ex:
1249            # This should not happen! GUI explicitly forbids this situation
[3b3b40b]1250            self.communicate.statusBarUpdateSignal.emit(str(ex))
[c6343a5]1251            return
[f182f93]1252
[d4dac80]1253        # keep local copy of kernel parameters, as they will change during the update
1254        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1255
[f182f93]1256        # Create the fitting thread, based on the fitter
[3b3b40b]1257        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
[ee18d33]1258
[ded5e77]1259        self.calc_fit = FitThread(handler=handler,
[116dd4c1]1260                            fn=fitters,
1261                            batch_inputs=batch_inputs,
1262                            batch_outputs=batch_outputs,
1263                            page_id=[[self.page_id]],
1264                            updatefn=updater,
[91ad45c]1265                            completefn=completefn,
1266                            reset_flag=self.is_chain_fitting)
[7adc2a8]1267
[14ec91c5]1268        if LocalConfig.USING_TWISTED:
[7adc2a8]1269            # start the trhrhread with twisted
[ded5e77]1270            calc_thread = threads.deferToThread(self.calc_fit.compute)
[14ec91c5]1271            calc_thread.addCallback(completefn)
[7adc2a8]1272            calc_thread.addErrback(self.fitFailed)
1273        else:
1274            # Use the old python threads + Queue
[ded5e77]1275            self.calc_fit.queue()
1276            self.calc_fit.ready(2.5)
[f182f93]1277
[d7ff531]1278        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
[ded5e77]1279        self.fit_started = True
[14ec91c5]1280        # Disable some elements
1281        self.setFittingStarted()
[0268aed]1282
[ded5e77]1283    def stopFit(self):
1284        """
1285        Attempt to stop the fitting thread
1286        """
1287        if self.calc_fit is None or not self.calc_fit.isrunning():
1288            return
1289        self.calc_fit.stop()
1290        #self.fit_started=False
1291        #re-enable the Fit button
1292        self.setFittingStopped()
1293
1294        msg = "Fitting cancelled."
1295        self.communicate.statusBarUpdateSignal.emit(msg)
1296
[f182f93]1297    def updateFit(self):
1298        """
1299        """
[b3e8629]1300        print("UPDATE FIT")
[0268aed]1301        pass
1302
[02ddfb4]1303    def fitFailed(self, reason):
1304        """
1305        """
[ded5e77]1306        self.setFittingStopped()
1307        msg = "Fitting failed with: "+ str(reason)
1308        self.communicate.statusBarUpdateSignal.emit(msg)
[02ddfb4]1309
[3b3b40b]1310    def batchFittingCompleted(self, result):
1311        """
1312        Send the finish message from calculate threads to main thread
1313        """
1314        self.batchFittingFinishedSignal.emit(result)
1315
[ee18d33]1316    def batchFitComplete(self, result):
1317        """
1318        Receive and display batch fitting results
1319        """
1320        #re-enable the Fit button
[14ec91c5]1321        self.setFittingStopped()
[d4dac80]1322
1323        if result is None:
1324            msg = "Fitting failed."
1325            self.communicate.statusBarUpdateSignal.emit(msg)
1326            return
1327
[3b3b40b]1328        # Show the grid panel
[d4dac80]1329        self.communicate.sendDataToGridSignal.emit(result[0])
1330
1331        elapsed = result[1]
1332        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1333        self.communicate.statusBarUpdateSignal.emit(msg)
1334
1335        # Run over the list of results and update the items
1336        for res_index, res_list in enumerate(result[0]):
1337            # results
1338            res = res_list[0]
1339            param_dict = self.paramDictFromResults(res)
1340
1341            # create local kernel_module
1342            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1343            # pull out current data
1344            data = self._logic[res_index].data
1345
1346            # Switch indexes
1347            self.onSelectBatchFilename(res_index)
1348
1349            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1350            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1351
1352        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1353        if self.kernel_module is not None:
1354            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1355
1356    def paramDictFromResults(self, results):
1357        """
1358        Given the fit results structure, pull out optimized parameters and return them as nicely
1359        formatted dict
1360        """
1361        if results.fitness is None or \
1362            not np.isfinite(results.fitness) or \
1363            np.any(results.pvec is None) or \
1364            not np.all(np.isfinite(results.pvec)):
1365            msg = "Fitting did not converge!"
1366            self.communicate.statusBarUpdateSignal.emit(msg)
1367            msg += results.mesg
1368            logging.error(msg)
1369            return
1370
1371        param_list = results.param_list # ['radius', 'radius.width']
1372        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1373        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1374        params_and_errors = list(zip(param_values, param_stderr))
1375        param_dict = dict(zip(param_list, params_and_errors))
1376
1377        return param_dict
[3b3b40b]1378
1379    def fittingCompleted(self, result):
1380        """
1381        Send the finish message from calculate threads to main thread
1382        """
1383        self.fittingFinishedSignal.emit(result)
[ee18d33]1384
[f182f93]1385    def fitComplete(self, result):
1386        """
1387        Receive and display fitting results
1388        "result" is a tuple of actual result list and the fit time in seconds
1389        """
1390        #re-enable the Fit button
[14ec91c5]1391        self.setFittingStopped()
[d7ff531]1392
[c3eb858]1393        if result is None:
[3b3b40b]1394            msg = "Fitting failed."
[06234fc]1395            self.communicate.statusBarUpdateSignal.emit(msg)
1396            return
[d7ff531]1397
[ee18d33]1398        res_list = result[0][0]
[f182f93]1399        res = res_list[0]
[d4dac80]1400        self.chi2 = res.fitness
1401        param_dict = self.paramDictFromResults(res)
[f182f93]1402
1403        elapsed = result[1]
[ded5e77]1404        if self.calc_fit._interrupting:
1405            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1406            logging.warning("\n"+msg+"\n")
1407        else:
1408            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
[f182f93]1409        self.communicate.statusBarUpdateSignal.emit(msg)
1410
1411        # Dictionary of fitted parameter: value, error
1412        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1413        self.updateModelFromList(param_dict)
1414
[aca8418]1415        self.updatePolyModelFromList(param_dict)
1416
[b00414d]1417        self.updateMagnetModelFromList(param_dict)
1418
[d7ff531]1419        # update charts
1420        self.onPlot()
1421
[f182f93]1422        # Read only value - we can get away by just printing it here
[2add354]1423        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]1424        self.lblChi2Value.setText(chi2_repr)
1425
[116dd4c1]1426    def prepareFitters(self, fitter=None, fit_id=0):
1427        """
1428        Prepare the Fitter object for use in fitting
1429        """
1430        # fitter = None -> single/batch fitting
1431        # fitter = Fit() -> simultaneous fitting
1432
1433        # Data going in
1434        data = self.logic.data
1435        model = self.kernel_module
1436        qmin = self.q_range_min
1437        qmax = self.q_range_max
1438        params_to_fit = self.parameters_to_fit
[c6343a5]1439        if (not params_to_fit):
1440            raise ValueError('Fitting requires at least one parameter to optimize.')
[116dd4c1]1441
1442        # Potential smearing added
1443        # Remember that smearing_min/max can be None ->
1444        # deal with it until Python gets discriminated unions
[9a7c81c]1445        self.addWeightingToData(data)
[116dd4c1]1446
[8b480d27]1447        # Get the constraints.
1448        constraints = self.getComplexConstraintsForModel()
1449        if fitter is None:
1450            # For single fits - check for inter-model constraints
1451            constraints = self.getConstraintsForFitting()
[3b3b40b]1452
[9a7c81c]1453        smearer = self.smearing_widget.smearer()
[116dd4c1]1454        handler = None
1455        batch_inputs = {}
1456        batch_outputs = {}
1457
1458        fitters = []
1459        for fit_index in self.all_data:
1460            fitter_single = Fit() if fitter is None else fitter
1461            data = GuiUtils.dataFromItem(fit_index)
[9a7c81c]1462            # Potential weights added directly to data
1463            self.addWeightingToData(data)
[116dd4c1]1464            try:
1465                fitter_single.set_model(model, fit_id, params_to_fit, data=data,
1466                             constraints=constraints)
1467            except ValueError as ex:
[3b3b40b]1468                raise ValueError("Setting model parameters failed with: %s" % ex)
[116dd4c1]1469
1470            qmin, qmax, _ = self.logic.computeRangeFromData(data)
1471            fitter_single.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
1472                            qmax=qmax)
1473            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1474            if fitter is None:
1475                # Assign id to the new fitter only
1476                fitter_single.fitter_id = [self.page_id]
1477            fit_id += 1
1478            fitters.append(fitter_single)
1479
1480        return fitters, fit_id
1481
[f182f93]1482    def iterateOverModel(self, func):
1483        """
1484        Take func and throw it inside the model row loop
1485        """
[b3e8629]1486        for row_i in range(self._model_model.rowCount()):
[f182f93]1487            func(row_i)
1488
1489    def updateModelFromList(self, param_dict):
1490        """
1491        Update the model with new parameters, create the errors column
1492        """
1493        assert isinstance(param_dict, dict)
1494        if not dict:
1495            return
1496
[919d47c]1497        def updateFittedValues(row):
[f182f93]1498            # Utility function for main model update
[d7ff531]1499            # internal so can use closure for param_dict
[919d47c]1500            param_name = str(self._model_model.item(row, 0).text())
[b3e8629]1501            if param_name not in list(param_dict.keys()):
[f182f93]1502                return
1503            # modify the param value
[454670d]1504            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
[919d47c]1505            self._model_model.item(row, 1).setText(param_repr)
[f182f93]1506            if self.has_error_column:
[454670d]1507                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
[919d47c]1508                self._model_model.item(row, 2).setText(error_repr)
[f182f93]1509
[919d47c]1510        def updatePolyValues(row):
1511            # Utility function for updateof polydispersity part of the main model
1512            param_name = str(self._model_model.item(row, 0).text())+'.width'
[b3e8629]1513            if param_name not in list(param_dict.keys()):
[919d47c]1514                return
1515            # modify the param value
1516            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1517            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1518
1519        def createErrorColumn(row):
[f182f93]1520            # Utility function for error column update
1521            item = QtGui.QStandardItem()
[919d47c]1522            def createItem(param_name):
[f182f93]1523                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1524                item.setText(error_repr)
[919d47c]1525            def curr_param():
1526                return str(self._model_model.item(row, 0).text())
1527
[b3e8629]1528            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
[919d47c]1529
[f182f93]1530            error_column.append(item)
1531
[2da5759]1532        # block signals temporarily, so we don't end up
1533        # updating charts with every single model change on the end of fitting
1534        self._model_model.blockSignals(True)
[d7ff531]1535        self.iterateOverModel(updateFittedValues)
[919d47c]1536        self.iterateOverModel(updatePolyValues)
[2da5759]1537        self._model_model.blockSignals(False)
[f182f93]1538
1539        if self.has_error_column:
1540            return
1541
1542        error_column = []
[8f2548c]1543        self.lstParams.itemDelegate().addErrorColumn()
[d7ff531]1544        self.iterateOverModel(createErrorColumn)
[f182f93]1545
[d7ff531]1546        # switch off reponse to model change
[f182f93]1547        self._model_model.insertColumn(2, error_column)
1548        FittingUtilities.addErrorHeadersToModel(self._model_model)
[d7ff531]1549        # Adjust the table cells width.
1550        # TODO: find a way to dynamically adjust column width while resized expanding
1551        self.lstParams.resizeColumnToContents(0)
1552        self.lstParams.resizeColumnToContents(4)
1553        self.lstParams.resizeColumnToContents(5)
[4992ff2]1554        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
[d7ff531]1555
1556        self.has_error_column = True
[f182f93]1557
[aca8418]1558    def updatePolyModelFromList(self, param_dict):
1559        """
1560        Update the polydispersity model with new parameters, create the errors column
1561        """
1562        assert isinstance(param_dict, dict)
1563        if not dict:
1564            return
1565
[b00414d]1566        def iterateOverPolyModel(func):
1567            """
1568            Take func and throw it inside the poly model row loop
1569            """
[b3e8629]1570            for row_i in range(self._poly_model.rowCount()):
[b00414d]1571                func(row_i)
1572
[aca8418]1573        def updateFittedValues(row_i):
1574            # Utility function for main model update
1575            # internal so can use closure for param_dict
1576            if row_i >= self._poly_model.rowCount():
1577                return
1578            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
[b3e8629]1579            if param_name not in list(param_dict.keys()):
[aca8418]1580                return
1581            # modify the param value
1582            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1583            self._poly_model.item(row_i, 1).setText(param_repr)
1584            if self.has_poly_error_column:
1585                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1586                self._poly_model.item(row_i, 2).setText(error_repr)
1587
[919d47c]1588
[aca8418]1589        def createErrorColumn(row_i):
1590            # Utility function for error column update
1591            if row_i >= self._poly_model.rowCount():
1592                return
1593            item = QtGui.QStandardItem()
[919d47c]1594
1595            def createItem(param_name):
[aca8418]1596                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1597                item.setText(error_repr)
[919d47c]1598
1599            def poly_param():
1600                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1601
[b3e8629]1602            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
[919d47c]1603
[aca8418]1604            error_column.append(item)
1605
1606        # block signals temporarily, so we don't end up
1607        # updating charts with every single model change on the end of fitting
1608        self._poly_model.blockSignals(True)
[b00414d]1609        iterateOverPolyModel(updateFittedValues)
[aca8418]1610        self._poly_model.blockSignals(False)
1611
1612        if self.has_poly_error_column:
1613            return
1614
[8eaa101]1615        self.lstPoly.itemDelegate().addErrorColumn()
[aca8418]1616        error_column = []
[b00414d]1617        iterateOverPolyModel(createErrorColumn)
[aca8418]1618
1619        # switch off reponse to model change
1620        self._poly_model.blockSignals(True)
1621        self._poly_model.insertColumn(2, error_column)
1622        self._poly_model.blockSignals(False)
1623        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1624
1625        self.has_poly_error_column = True
1626
[b00414d]1627    def updateMagnetModelFromList(self, param_dict):
1628        """
1629        Update the magnetic model with new parameters, create the errors column
1630        """
1631        assert isinstance(param_dict, dict)
1632        if not dict:
1633            return
[3b3b40b]1634        if self._magnet_model.rowCount() == 0:
[cee5c78]1635            return
[b00414d]1636
1637        def iterateOverMagnetModel(func):
1638            """
1639            Take func and throw it inside the magnet model row loop
1640            """
[b3e8629]1641            for row_i in range(self._model_model.rowCount()):
[b00414d]1642                func(row_i)
1643
1644        def updateFittedValues(row):
1645            # Utility function for main model update
1646            # internal so can use closure for param_dict
[cee5c78]1647            if self._magnet_model.item(row, 0) is None:
1648                return
[b00414d]1649            param_name = str(self._magnet_model.item(row, 0).text())
[b3e8629]1650            if param_name not in list(param_dict.keys()):
[b00414d]1651                return
1652            # modify the param value
1653            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1654            self._magnet_model.item(row, 1).setText(param_repr)
1655            if self.has_magnet_error_column:
1656                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1657                self._magnet_model.item(row, 2).setText(error_repr)
1658
1659        def createErrorColumn(row):
1660            # Utility function for error column update
1661            item = QtGui.QStandardItem()
1662            def createItem(param_name):
1663                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1664                item.setText(error_repr)
1665            def curr_param():
1666                return str(self._magnet_model.item(row, 0).text())
1667
[b3e8629]1668            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
[b00414d]1669
1670            error_column.append(item)
1671
1672        # block signals temporarily, so we don't end up
1673        # updating charts with every single model change on the end of fitting
1674        self._magnet_model.blockSignals(True)
1675        iterateOverMagnetModel(updateFittedValues)
1676        self._magnet_model.blockSignals(False)
1677
1678        if self.has_magnet_error_column:
1679            return
1680
1681        self.lstMagnetic.itemDelegate().addErrorColumn()
1682        error_column = []
1683        iterateOverMagnetModel(createErrorColumn)
1684
1685        # switch off reponse to model change
1686        self._magnet_model.blockSignals(True)
1687        self._magnet_model.insertColumn(2, error_column)
1688        self._magnet_model.blockSignals(False)
1689        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1690
1691        self.has_magnet_error_column = True
1692
[0268aed]1693    def onPlot(self):
1694        """
1695        Plot the current set of data
1696        """
[d48cc19]1697        # Regardless of previous state, this should now be `plot show` functionality only
1698        self.cmdPlot.setText("Show Plot")
[88e1f57]1699        # Force data recalculation so existing charts are updated
1700        self.recalculatePlotData()
[d48cc19]1701        self.showPlot()
1702
[9a7c81c]1703    def onSmearingOptionsUpdate(self):
1704        """
1705        React to changes in the smearing widget
1706        """
1707        self.calculateQGridForModel()
1708
[d48cc19]1709    def recalculatePlotData(self):
1710        """
1711        Generate a new dataset for model
1712        """
[180bd54]1713        if not self.data_is_loaded:
[0268aed]1714            self.createDefaultDataset()
1715        self.calculateQGridForModel()
1716
[d48cc19]1717    def showPlot(self):
1718        """
1719        Show the current plot in MPL
1720        """
1721        # Show the chart if ready
1722        data_to_show = self.data if self.data_is_loaded else self.model_data
1723        if data_to_show is not None:
1724            self.communicate.plotRequestedSignal.emit([data_to_show])
1725
[180bd54]1726    def onOptionsUpdate(self):
[0268aed]1727        """
[180bd54]1728        Update local option values and replot
[0268aed]1729        """
[180bd54]1730        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1731            self.options_widget.state()
[61a92d4]1732        # set Q range labels on the main tab
1733        self.lblMinRangeDef.setText(str(self.q_range_min))
1734        self.lblMaxRangeDef.setText(str(self.q_range_max))
[d48cc19]1735        self.recalculatePlotData()
[6c8fb2c]1736
[0268aed]1737    def setDefaultStructureCombo(self):
1738        """
1739        Fill in the structure factors combo box with defaults
1740        """
1741        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1742        factors = [factor[0] for factor in structure_factor_list]
1743        factors.insert(0, STRUCTURE_DEFAULT)
1744        self.cbStructureFactor.clear()
1745        self.cbStructureFactor.addItems(sorted(factors))
1746
[4d457df]1747    def createDefaultDataset(self):
1748        """
1749        Generate default Dataset 1D/2D for the given model
1750        """
1751        # Create default datasets if no data passed
1752        if self.is2D:
[180bd54]1753            qmax = self.q_range_max/np.sqrt(2)
[4d457df]1754            qstep = self.npts
1755            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
[180bd54]1756            return
1757        elif self.log_points:
1758            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
[1bc27f1]1759            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
[180bd54]1760            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
[4d457df]1761        else:
[180bd54]1762            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
[1bc27f1]1763                                   num=self.npts, endpoint=True)
[180bd54]1764        self.logic.createDefault1dData(interval, self.tab_id)
[60af928]1765
[5236449]1766    def readCategoryInfo(self):
[60af928]1767        """
1768        Reads the categories in from file
1769        """
1770        self.master_category_dict = defaultdict(list)
1771        self.by_model_dict = defaultdict(list)
1772        self.model_enabled_dict = defaultdict(bool)
1773
[cbcdd2c]1774        categorization_file = CategoryInstaller.get_user_file()
1775        if not os.path.isfile(categorization_file):
1776            categorization_file = CategoryInstaller.get_default_file()
1777        with open(categorization_file, 'rb') as cat_file:
[60af928]1778            self.master_category_dict = json.load(cat_file)
[5236449]1779            self.regenerateModelDict()
[60af928]1780
[5236449]1781        # Load the model dict
1782        models = load_standard_models()
1783        for model in models:
1784            self.models[model.name] = model
1785
[3b3b40b]1786        self.readCustomCategoryInfo()
1787
1788    def readCustomCategoryInfo(self):
1789        """
1790        Reads the custom model category
1791        """
1792        #Looking for plugins
1793        self.plugins = list(self.custom_models.values())
1794        plugin_list = []
1795        for name, plug in self.custom_models.items():
1796            self.models[name] = plug
1797            plugin_list.append([name, True])
1798        self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1799
[5236449]1800    def regenerateModelDict(self):
[60af928]1801        """
[cbcdd2c]1802        Regenerates self.by_model_dict which has each model name as the
[60af928]1803        key and the list of categories belonging to that model
1804        along with the enabled mapping
1805        """
1806        self.by_model_dict = defaultdict(list)
1807        for category in self.master_category_dict:
1808            for (model, enabled) in self.master_category_dict[category]:
1809                self.by_model_dict[model].append(category)
1810                self.model_enabled_dict[model] = enabled
1811
[86f88d1]1812    def addBackgroundToModel(self, model):
1813        """
1814        Adds background parameter with default values to the model
1815        """
[cbcdd2c]1816        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]1817        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
[4d457df]1818        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]1819        last_row = model.rowCount()-1
1820        model.item(last_row, 0).setEditable(False)
1821        model.item(last_row, 4).setEditable(False)
[86f88d1]1822
1823    def addScaleToModel(self, model):
1824        """
1825        Adds scale parameter with default values to the model
1826        """
[cbcdd2c]1827        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]1828        checked_list = ['scale', '1.0', '0.0', 'inf', '']
[4d457df]1829        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]1830        last_row = model.rowCount()-1
1831        model.item(last_row, 0).setEditable(False)
1832        model.item(last_row, 4).setEditable(False)
[86f88d1]1833
[9d266d2]1834    def addWeightingToData(self, data):
1835        """
1836        Adds weighting contribution to fitting data
[1bc27f1]1837        """
[e1e3e09]1838        # Send original data for weighting
[dc5ef15]1839        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
[9a7c81c]1840        if self.is2D:
1841            data.err_data = weight
1842        else:
1843            data.dy = weight
1844        pass
[9d266d2]1845
[0268aed]1846    def updateQRange(self):
1847        """
1848        Updates Q Range display
1849        """
1850        if self.data_is_loaded:
1851            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1852        # set Q range labels on the main tab
1853        self.lblMinRangeDef.setText(str(self.q_range_min))
1854        self.lblMaxRangeDef.setText(str(self.q_range_max))
1855        # set Q range labels on the options tab
[180bd54]1856        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[0268aed]1857
[4d457df]1858    def SASModelToQModel(self, model_name, structure_factor=None):
[60af928]1859        """
[cbcdd2c]1860        Setting model parameters into table based on selected category
[60af928]1861        """
1862        # Crete/overwrite model items
1863        self._model_model.clear()
[5236449]1864
[fd1ae6d1]1865        # First, add parameters from the main model
1866        if model_name is not None:
1867            self.fromModelToQModel(model_name)
[5236449]1868
[fd1ae6d1]1869        # Then, add structure factor derived parameters
[cd31251]1870        if structure_factor is not None and structure_factor != "None":
[fd1ae6d1]1871            if model_name is None:
1872                # Instantiate the current sasmodel for SF-only models
1873                self.kernel_module = self.models[structure_factor]()
1874            self.fromStructureFactorToQModel(structure_factor)
[cd31251]1875        else:
[fd1ae6d1]1876            # Allow the SF combobox visibility for the given sasmodel
1877            self.enableStructureFactorControl(structure_factor)
[cd31251]1878
[fd1ae6d1]1879        # Then, add multishells
1880        if model_name is not None:
1881            # Multishell models need additional treatment
1882            self.addExtraShells()
[86f88d1]1883
[5236449]1884        # Add polydispersity to the model
[86f88d1]1885        self.setPolyModel()
[5236449]1886        # Add magnetic parameters to the model
[86f88d1]1887        self.setMagneticModel()
[5236449]1888
[a9b568c]1889        # Adjust the table cells width
1890        self.lstParams.resizeColumnToContents(0)
[4992ff2]1891        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
[a9b568c]1892
[5236449]1893        # Now we claim the model has been loaded
[86f88d1]1894        self.model_is_loaded = True
[be8f4b0]1895        # Change the model name to a monicker
1896        self.kernel_module.name = self.modelName()
[9a7c81c]1897        # Update the smearing tab
1898        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
[86f88d1]1899
[fd1ae6d1]1900        # (Re)-create headers
1901        FittingUtilities.addHeadersToModel(self._model_model)
[6964d44]1902        self.lstParams.header().setFont(self.boldFont)
[fd1ae6d1]1903
[5236449]1904        # Update Q Ranges
1905        self.updateQRange()
1906
[fd1ae6d1]1907    def fromModelToQModel(self, model_name):
1908        """
1909        Setting model parameters into QStandardItemModel based on selected _model_
1910        """
[3b3b40b]1911        name = model_name
1912        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
1913            # custom kernel load requires full path
1914            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
1915        kernel_module = generate.load_kernel_module(name)
[fd1ae6d1]1916        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1917
1918        # Instantiate the current sasmodel
1919        self.kernel_module = self.models[model_name]()
1920
1921        # Explicitly add scale and background with default values
[6964d44]1922        temp_undo_state = self.undo_supported
1923        self.undo_supported = False
[fd1ae6d1]1924        self.addScaleToModel(self._model_model)
1925        self.addBackgroundToModel(self._model_model)
[6964d44]1926        self.undo_supported = temp_undo_state
[fd1ae6d1]1927
[0d13814]1928        self.shell_names = self.shellNamesList()
1929
[fd1ae6d1]1930        # Update the QModel
[aca8418]1931        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1932
[fd1ae6d1]1933        for row in new_rows:
1934            self._model_model.appendRow(row)
1935        # Update the counter used for multishell display
1936        self._last_model_row = self._model_model.rowCount()
1937
1938    def fromStructureFactorToQModel(self, structure_factor):
1939        """
1940        Setting model parameters into QStandardItemModel based on selected _structure factor_
1941        """
1942        structure_module = generate.load_kernel_module(structure_factor)
1943        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
[5d1440e1]1944        structure_kernel = self.models[structure_factor]()
1945
1946        self.kernel_module._model_info = product.make_product_info(self.kernel_module._model_info, structure_kernel._model_info)
[fd1ae6d1]1947
1948        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1949        for row in new_rows:
1950            self._model_model.appendRow(row)
1951        # Update the counter used for multishell display
1952        self._last_model_row = self._model_model.rowCount()
1953
[b00414d]1954    def onMainParamsChange(self, item):
[cd31251]1955        """
1956        Callback method for updating the sasmodel parameters with the GUI values
1957        """
[cbcdd2c]1958        model_column = item.column()
[cd31251]1959
1960        if model_column == 0:
[f182f93]1961            self.checkboxSelected(item)
[2add354]1962            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
[6964d44]1963            # Update state stack
1964            self.updateUndo()
[cd31251]1965            return
1966
[f182f93]1967        model_row = item.row()
1968        name_index = self._model_model.index(model_row, 0)
1969
[b00414d]1970        # Extract changed value.
[2add354]1971        try:
[fbfc488]1972            value = GuiUtils.toDouble(item.text())
[0261bc1]1973        except TypeError:
[2add354]1974            # Unparsable field
1975            return
[fbfc488]1976
1977        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
[cbcdd2c]1978
[00b3b40]1979        # Update the parameter value - note: this supports +/-inf as well
[cbcdd2c]1980        self.kernel_module.params[parameter_name] = value
1981
[8a32a6ff]1982        # Update the parameter value - note: this supports +/-inf as well
[8f2548c]1983        param_column = self.lstParams.itemDelegate().param_value
1984        min_column = self.lstParams.itemDelegate().param_min
1985        max_column = self.lstParams.itemDelegate().param_max
1986        if model_column == param_column:
[8a32a6ff]1987            self.kernel_module.setParam(parameter_name, value)
[8f2548c]1988        elif model_column == min_column:
[8a32a6ff]1989            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
[8f2548c]1990            self.kernel_module.details[parameter_name][1] = value
1991        elif model_column == max_column:
1992            self.kernel_module.details[parameter_name][2] = value
1993        else:
1994            # don't update the chart
1995            return
[00b3b40]1996
1997        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1998        # TODO: multishell params in self.kernel_module.details[??] = value
[cbcdd2c]1999
[d7ff531]2000        # Force the chart update when actual parameters changed
2001        if model_column == 1:
[d48cc19]2002            self.recalculatePlotData()
[7d077d1]2003
[2241130]2004        # Update state stack
[00b3b40]2005        self.updateUndo()
[2241130]2006
[7fd20fc]2007    def isCheckable(self, row):
2008        return self._model_model.item(row, 0).isCheckable()
2009
[f182f93]2010    def checkboxSelected(self, item):
2011        # Assure we're dealing with checkboxes
2012        if not item.isCheckable():
2013            return
2014        status = item.checkState()
2015
2016        # If multiple rows selected - toggle all of them, filtering uncheckable
2017        # Switch off signaling from the model to avoid recursion
2018        self._model_model.blockSignals(True)
2019        # Convert to proper indices and set requested enablement
[7fd20fc]2020        self.setParameterSelection(status)
2021        #[self._model_model.item(row, 0).setCheckState(status) for row in self.selectedParameters()]
[f182f93]2022        self._model_model.blockSignals(False)
2023
2024        # update the list of parameters to fit
[c1e380e]2025        main_params = self.checkedListFromModel(self._model_model)
2026        poly_params = self.checkedListFromModel(self._poly_model)
[b00414d]2027        magnet_params = self.checkedListFromModel(self._magnet_model)
2028
[c1e380e]2029        # Retrieve poly params names
[358b39d]2030        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
[c1e380e]2031
[b00414d]2032        self.parameters_to_fit = main_params + poly_params + magnet_params
[c1e380e]2033
2034    def checkedListFromModel(self, model):
2035        """
2036        Returns list of checked parameters for given model
2037        """
2038        def isChecked(row):
2039            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2040
2041        return [str(model.item(row_index, 0).text())
[b3e8629]2042                for row_index in range(model.rowCount())
[c1e380e]2043                if isChecked(row_index)]
[f182f93]2044
[6fd4e36]2045    def createNewIndex(self, fitted_data):
2046        """
2047        Create a model or theory index with passed Data1D/Data2D
2048        """
2049        if self.data_is_loaded:
[0268aed]2050            if not fitted_data.name:
2051                name = self.nameForFittedData(self.data.filename)
2052                fitted_data.title = name
2053                fitted_data.name = name
2054                fitted_data.filename = name
[7d077d1]2055                fitted_data.symbol = "Line"
[6fd4e36]2056            self.updateModelIndex(fitted_data)
2057        else:
[d6e38661]2058            name = self.nameForFittedData(self.kernel_module.id)
[0268aed]2059            fitted_data.title = name
2060            fitted_data.name = name
2061            fitted_data.filename = name
2062            fitted_data.symbol = "Line"
[6fd4e36]2063            self.createTheoryIndex(fitted_data)
2064
2065    def updateModelIndex(self, fitted_data):
2066        """
2067        Update a QStandardModelIndex containing model data
2068        """
[00b3b40]2069        name = self.nameFromData(fitted_data)
[0268aed]2070        # Make this a line if no other defined
[7d077d1]2071        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
[0268aed]2072            fitted_data.symbol = 'Line'
[6fd4e36]2073        # Notify the GUI manager so it can update the main model in DataExplorer
[d4dac80]2074        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
[6fd4e36]2075
2076    def createTheoryIndex(self, fitted_data):
2077        """
2078        Create a QStandardModelIndex containing model data
2079        """
[00b3b40]2080        name = self.nameFromData(fitted_data)
2081        # Notify the GUI manager so it can create the theory model in DataExplorer
[b3e8629]2082        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
[00b3b40]2083        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
2084
2085    def nameFromData(self, fitted_data):
2086        """
2087        Return name for the dataset. Terribly impure function.
2088        """
[0268aed]2089        if fitted_data.name is None:
[00b3b40]2090            name = self.nameForFittedData(self.logic.data.filename)
[0268aed]2091            fitted_data.title = name
2092            fitted_data.name = name
2093            fitted_data.filename = name
2094        else:
2095            name = fitted_data.name
[00b3b40]2096        return name
[5236449]2097
[4d457df]2098    def methodCalculateForData(self):
2099        '''return the method for data calculation'''
2100        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2101
2102    def methodCompleteForData(self):
2103        '''return the method for result parsin on calc complete '''
[d4dac80]2104        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
[4d457df]2105
[d4dac80]2106    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
[86f88d1]2107        """
[d4dac80]2108        Wrapper for Calc1D/2D calls
[86f88d1]2109        """
[d4dac80]2110        if data is None:
2111            data = self.data
2112        if model is None:
2113            model = self.kernel_module
2114        if completefn is None:
2115            completefn = self.methodCompleteForData()
[9a7c81c]2116        smearer = self.smearing_widget.smearer()
[4d457df]2117        # Awful API to a backend method.
[d4dac80]2118        calc_thread = self.methodCalculateForData()(data=data,
2119                                               model=model,
[1bc27f1]2120                                               page_id=0,
2121                                               qmin=self.q_range_min,
2122                                               qmax=self.q_range_max,
[9a7c81c]2123                                               smearer=smearer,
[1bc27f1]2124                                               state=None,
2125                                               weight=None,
2126                                               fid=None,
2127                                               toggle_mode_on=False,
[d4dac80]2128                                               completefn=completefn,
[1bc27f1]2129                                               update_chisqr=True,
2130                                               exception_handler=self.calcException,
2131                                               source=None)
[d4dac80]2132        if use_threads:
2133            if LocalConfig.USING_TWISTED:
2134                # start the thread with twisted
2135                thread = threads.deferToThread(calc_thread.compute)
2136                thread.addCallback(completefn)
2137                thread.addErrback(self.calculateDataFailed)
2138            else:
2139                # Use the old python threads + Queue
2140                calc_thread.queue()
2141                calc_thread.ready(2.5)
2142        else:
2143            results = calc_thread.compute()
2144            completefn(results)
[4d457df]2145
[d4dac80]2146    def calculateQGridForModel(self):
2147        """
2148        Prepare the fitting data object, based on current ModelModel
2149        """
2150        if self.kernel_module is None:
2151            return
2152        self.calculateQGridForModelExt()
[6964d44]2153
[aca8418]2154    def calculateDataFailed(self, reason):
[6964d44]2155        """
[c1e380e]2156        Thread returned error
[6964d44]2157        """
[b3e8629]2158        print("Calculate Data failed with ", reason)
[5236449]2159
[d4dac80]2160    def completed1D(self, return_data):
2161        self.Calc1DFinishedSignal.emit(return_data)
2162
2163    def completed2D(self, return_data):
2164        self.Calc2DFinishedSignal.emit(return_data)
2165
[cbcdd2c]2166    def complete1D(self, return_data):
[5236449]2167        """
[4d457df]2168        Plot the current 1D data
2169        """
[d48cc19]2170        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2171        self.calculateResiduals(fitted_data)
2172        self.model_data = fitted_data
[cbcdd2c]2173
2174    def complete2D(self, return_data):
2175        """
[4d457df]2176        Plot the current 2D data
2177        """
[6fd4e36]2178        fitted_data = self.logic.new2DPlot(return_data)
2179        self.calculateResiduals(fitted_data)
[d48cc19]2180        self.model_data = fitted_data
[6fd4e36]2181
2182    def calculateResiduals(self, fitted_data):
2183        """
2184        Calculate and print Chi2 and display chart of residuals
2185        """
2186        # Create a new index for holding data
[7d077d1]2187        fitted_data.symbol = "Line"
[6964d44]2188
2189        # Modify fitted_data with weighting
2190        self.addWeightingToData(fitted_data)
2191
[6fd4e36]2192        self.createNewIndex(fitted_data)
2193        # Calculate difference between return_data and logic.data
[2add354]2194        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
[6fd4e36]2195        # Update the control
[2add354]2196        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]2197        self.lblChi2Value.setText(chi2_repr)
[cbcdd2c]2198
[d48cc19]2199        self.communicate.plotUpdateSignal.emit([fitted_data])
2200
[0268aed]2201        # Plot residuals if actual data
[aca8418]2202        if not self.data_is_loaded:
2203            return
2204
2205        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
2206        residuals_plot.id = "Residual " + residuals_plot.id
2207        self.createNewIndex(residuals_plot)
[5236449]2208
2209    def calcException(self, etype, value, tb):
2210        """
[c1e380e]2211        Thread threw an exception.
[5236449]2212        """
[c1e380e]2213        # TODO: remimplement thread cancellation
[5236449]2214        logging.error("".join(traceback.format_exception(etype, value, tb)))
[60af928]2215
2216    def setTableProperties(self, table):
2217        """
2218        Setting table properties
2219        """
2220        # Table properties
2221        table.verticalHeader().setVisible(False)
2222        table.setAlternatingRowColors(True)
[4992ff2]2223        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2224        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
[f46f6dc]2225        table.resizeColumnsToContents()
2226
[60af928]2227        # Header
2228        header = table.horizontalHeader()
[4992ff2]2229        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2230        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
[f46f6dc]2231
[4992ff2]2232        # Qt5: the following 2 lines crash - figure out why!
[e43fc91]2233        # Resize column 0 and 7 to content
[4992ff2]2234        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2235        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
[60af928]2236
2237    def setPolyModel(self):
2238        """
2239        Set polydispersity values
2240        """
[86f88d1]2241        if not self.model_parameters:
2242            return
2243        self._poly_model.clear()
2244
[e43fc91]2245        [self.setPolyModelParameters(i, param) for i, param in \
[aca8418]2246            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
[4d457df]2247        FittingUtilities.addPolyHeadersToModel(self._poly_model)
[60af928]2248
[e43fc91]2249    def setPolyModelParameters(self, i, param):
[aca8418]2250        """
[0d13814]2251        Standard of multishell poly parameter driver
[aca8418]2252        """
[0d13814]2253        param_name = param.name
2254        # see it the parameter is multishell
[06b0138]2255        if '[' in param.name:
[0d13814]2256            # Skip empty shells
2257            if self.current_shell_displayed == 0:
2258                return
2259            else:
2260                # Create as many entries as current shells
[b3e8629]2261                for ishell in range(1, self.current_shell_displayed+1):
[0d13814]2262                    # Remove [n] and add the shell numeral
2263                    name = param_name[0:param_name.index('[')] + str(ishell)
[e43fc91]2264                    self.addNameToPolyModel(i, name)
[0d13814]2265        else:
2266            # Just create a simple param entry
[e43fc91]2267            self.addNameToPolyModel(i, param_name)
[0d13814]2268
[e43fc91]2269    def addNameToPolyModel(self, i, param_name):
[0d13814]2270        """
2271        Creates a checked row in the poly model with param_name
2272        """
[144ec831]2273        # Polydisp. values from the sasmodel
[0d13814]2274        width = self.kernel_module.getParam(param_name + '.width')
2275        npts = self.kernel_module.getParam(param_name + '.npts')
2276        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2277        _, min, max = self.kernel_module.details[param_name]
[144ec831]2278
2279        # Construct a row with polydisp. related variable.
2280        # This will get added to the polydisp. model
2281        # Note: last argument needs extra space padding for decent display of the control
[0d13814]2282        checked_list = ["Distribution of " + param_name, str(width),
2283                        str(min), str(max),
[e43fc91]2284                        str(npts), str(nsigs), "gaussian      ",'']
[aca8418]2285        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2286
2287        # All possible polydisp. functions as strings in combobox
[4992ff2]2288        func = QtWidgets.QComboBox()
[b3e8629]2289        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
[e43fc91]2290        # Set the default index
[aca8418]2291        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
[e43fc91]2292        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2293        self.lstPoly.setIndexWidget(ind, func)
2294        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2295
2296    def onPolyFilenameChange(self, row_index):
2297        """
2298        Respond to filename_updated signal from the delegate
2299        """
2300        # For the given row, invoke the "array" combo handler
2301        array_caption = 'array'
[8222f171]2302
[e43fc91]2303        # Get the combo box reference
2304        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2305        widget = self.lstPoly.indexWidget(ind)
[8222f171]2306
[e43fc91]2307        # Update the combo box so it displays "array"
2308        widget.blockSignals(True)
2309        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2310        widget.blockSignals(False)
[aca8418]2311
[8222f171]2312        # Invoke the file reader
2313        self.onPolyComboIndexChange(array_caption, row_index)
2314
[aca8418]2315    def onPolyComboIndexChange(self, combo_string, row_index):
2316        """
2317        Modify polydisp. defaults on function choice
2318        """
[144ec831]2319        # Get npts/nsigs for current selection
[aca8418]2320        param = self.model_parameters.form_volume_parameters[row_index]
[e43fc91]2321        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2322        combo_box = self.lstPoly.indexWidget(file_index)
[aca8418]2323
[919d47c]2324        def updateFunctionCaption(row):
2325            # Utility function for update of polydispersity function name in the main model
[1643d8ed]2326            param_name = str(self._model_model.item(row, 0).text())
[919d47c]2327            if param_name !=  param.name:
2328                return
[144ec831]2329            # Modify the param value
[919d47c]2330            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2331
[aca8418]2332        if combo_string == 'array':
2333            try:
[e43fc91]2334                self.loadPolydispArray(row_index)
[919d47c]2335                # Update main model for display
2336                self.iterateOverModel(updateFunctionCaption)
[e43fc91]2337                # disable the row
2338                lo = self.lstPoly.itemDelegate().poly_pd
2339                hi = self.lstPoly.itemDelegate().poly_function
[b3e8629]2340                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
[aca8418]2341                return
[e43fc91]2342            except IOError:
[8222f171]2343                combo_box.setCurrentIndex(self.orig_poly_index)
[e43fc91]2344                # Pass for cancel/bad read
2345                pass
[aca8418]2346
2347        # Enable the row in case it was disabled by Array
[919d47c]2348        self._poly_model.blockSignals(True)
[e43fc91]2349        max_range = self.lstPoly.itemDelegate().poly_filename
[b3e8629]2350        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
[e43fc91]2351        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]2352        self._poly_model.setData(file_index, "")
[919d47c]2353        self._poly_model.blockSignals(False)
[aca8418]2354
[8eaa101]2355        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2356        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
[aca8418]2357
2358        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2359        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2360
[b3e8629]2361        self._poly_model.setData(npts_index, npts)
2362        self._poly_model.setData(nsigs_index, nsigs)
[aca8418]2363
[919d47c]2364        self.iterateOverModel(updateFunctionCaption)
[8222f171]2365        self.orig_poly_index = combo_box.currentIndex()
[919d47c]2366
[e43fc91]2367    def loadPolydispArray(self, row_index):
[aca8418]2368        """
2369        Show the load file dialog and loads requested data into state
2370        """
[4992ff2]2371        datafile = QtWidgets.QFileDialog.getOpenFileName(
2372            self, "Choose a weight file", "", "All files (*.*)", None,
[fbfc488]2373            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[72f4834]2374
[fbfc488]2375        if not datafile:
[aca8418]2376            logging.info("No weight data chosen.")
[1643d8ed]2377            raise IOError
[72f4834]2378
[aca8418]2379        values = []
2380        weights = []
[919d47c]2381        def appendData(data_tuple):
2382            """
2383            Fish out floats from a tuple of strings
2384            """
2385            try:
2386                values.append(float(data_tuple[0]))
2387                weights.append(float(data_tuple[1]))
2388            except (ValueError, IndexError):
2389                # just pass through if line with bad data
2390                return
2391
[aca8418]2392        with open(datafile, 'r') as column_file:
2393            column_data = [line.rstrip().split() for line in column_file.readlines()]
[919d47c]2394            [appendData(line) for line in column_data]
[aca8418]2395
[1643d8ed]2396        # If everything went well - update the sasmodel values
[aca8418]2397        self.disp_model = POLYDISPERSITY_MODELS['array']()
2398        self.disp_model.set_weights(np.array(values), np.array(weights))
[e43fc91]2399        # + update the cell with filename
2400        fname = os.path.basename(str(datafile))
2401        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]2402        self._poly_model.setData(fname_index, fname)
[aca8418]2403
[60af928]2404    def setMagneticModel(self):
2405        """
2406        Set magnetism values on model
2407        """
[86f88d1]2408        if not self.model_parameters:
2409            return
2410        self._magnet_model.clear()
[aca8418]2411        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
[06b0138]2412            self.model_parameters.call_parameters if param.type == 'magnetic']
[4d457df]2413        FittingUtilities.addHeadersToModel(self._magnet_model)
[60af928]2414
[0d13814]2415    def shellNamesList(self):
2416        """
2417        Returns list of names of all multi-shell parameters
2418        E.g. for sld[n], radius[n], n=1..3 it will return
2419        [sld1, sld2, sld3, radius1, radius2, radius3]
2420        """
2421        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2422        top_index = self.kernel_module.multiplicity_info.number
2423        shell_names = []
[b3e8629]2424        for i in range(1, top_index+1):
[0d13814]2425            for name in multi_names:
2426                shell_names.append(name+str(i))
2427        return shell_names
2428
[aca8418]2429    def addCheckedMagneticListToModel(self, param, model):
2430        """
2431        Wrapper for model update with a subset of magnetic parameters
2432        """
[0d13814]2433        if param.name[param.name.index(':')+1:] in self.shell_names:
2434            # check if two-digit shell number
2435            try:
2436                shell_index = int(param.name[-2:])
2437            except ValueError:
2438                shell_index = int(param.name[-1:])
2439
2440            if shell_index > self.current_shell_displayed:
2441                return
2442
[aca8418]2443        checked_list = [param.name,
2444                        str(param.default),
2445                        str(param.limits[0]),
2446                        str(param.limits[1]),
2447                        param.units]
2448
2449        FittingUtilities.addCheckedListToModel(model, checked_list)
2450
[fd1ae6d1]2451    def enableStructureFactorControl(self, structure_factor):
[cd31251]2452        """
2453        Add structure factors to the list of parameters
2454        """
[fd1ae6d1]2455        if self.kernel_module.is_form_factor or structure_factor == 'None':
[cd31251]2456            self.enableStructureCombo()
2457        else:
2458            self.disableStructureCombo()
2459
[60af928]2460    def addExtraShells(self):
2461        """
[f46f6dc]2462        Add a combobox for multiple shell display
[60af928]2463        """
[4d457df]2464        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
[f46f6dc]2465
2466        if param_length == 0:
2467            return
2468
[6f7f652]2469        # cell 1: variable name
[f46f6dc]2470        item1 = QtGui.QStandardItem(param_name)
2471
[4992ff2]2472        func = QtWidgets.QComboBox()
[b1e36a3]2473        # Available range of shells displayed in the combobox
[b3e8629]2474        func.addItems([str(i) for i in range(param_length+1)])
[a9b568c]2475
[b1e36a3]2476        # Respond to index change
[86f88d1]2477        func.currentIndexChanged.connect(self.modifyShellsInList)
[60af928]2478
[6f7f652]2479        # cell 2: combobox
[f46f6dc]2480        item2 = QtGui.QStandardItem()
2481        self._model_model.appendRow([item1, item2])
[60af928]2482
[6f7f652]2483        # Beautify the row:  span columns 2-4
[60af928]2484        shell_row = self._model_model.rowCount()
[f46f6dc]2485        shell_index = self._model_model.index(shell_row-1, 1)
[86f88d1]2486
[4d457df]2487        self.lstParams.setIndexWidget(shell_index, func)
[86f88d1]2488        self._last_model_row = self._model_model.rowCount()
2489
[a9b568c]2490        # Set the index to the state-kept value
2491        func.setCurrentIndex(self.current_shell_displayed
2492                             if self.current_shell_displayed < func.count() else 0)
2493
[86f88d1]2494    def modifyShellsInList(self, index):
2495        """
2496        Add/remove additional multishell parameters
2497        """
2498        # Find row location of the combobox
2499        last_row = self._last_model_row
2500        remove_rows = self._model_model.rowCount() - last_row
2501
2502        if remove_rows > 1:
2503            self._model_model.removeRows(last_row, remove_rows)
2504
[4d457df]2505        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
[a9b568c]2506        self.current_shell_displayed = index
[60af928]2507
[0d13814]2508        # Update relevant models
2509        self.setPolyModel()
2510        self.setMagneticModel()
2511
[14ec91c5]2512    def setFittingStarted(self):
2513        """
[ded5e77]2514        Set buttion caption on fitting start
[14ec91c5]2515        """
[ded5e77]2516        # Notify the user that fitting is being run
2517        # Allow for stopping the job
2518        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
2519        self.cmdFit.setText('Stop fit')
[14ec91c5]2520
2521    def setFittingStopped(self):
2522        """
[ded5e77]2523        Set button caption on fitting stop
[14ec91c5]2524        """
[ded5e77]2525        # Notify the user that fitting is available
2526        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[14ec91c5]2527        self.cmdFit.setText("Fit")
[ded5e77]2528        self.fit_started = False
[14ec91c5]2529
[672b8ab]2530    def readFitPage(self, fp):
2531        """
2532        Read in state from a fitpage object and update GUI
2533        """
2534        assert isinstance(fp, FitPage)
2535        # Main tab info
2536        self.logic.data.filename = fp.filename
2537        self.data_is_loaded = fp.data_is_loaded
2538        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2539        self.chkMagnetism.setCheckState(fp.is_magnetic)
2540        self.chk2DView.setCheckState(fp.is2D)
2541
2542        # Update the comboboxes
2543        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2544        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2545        if fp.current_factor:
2546            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2547
2548        self.chi2 = fp.chi2
2549
2550        # Options tab
2551        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2552        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2553        self.npts = fp.fit_options[fp.NPTS]
2554        self.log_points = fp.fit_options[fp.LOG_POINTS]
2555        self.weighting = fp.fit_options[fp.WEIGHTING]
2556
2557        # Models
[d60da0c]2558        self._model_model = fp.model_model
2559        self._poly_model = fp.poly_model
2560        self._magnet_model = fp.magnetism_model
[672b8ab]2561
2562        # Resolution tab
2563        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2564        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2565        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2566        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2567        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2568
2569        # TODO: add polidyspersity and magnetism
2570
2571    def saveToFitPage(self, fp):
2572        """
2573        Write current state to the given fitpage
2574        """
2575        assert isinstance(fp, FitPage)
2576
2577        # Main tab info
2578        fp.filename = self.logic.data.filename
2579        fp.data_is_loaded = self.data_is_loaded
2580        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2581        fp.is_magnetic = self.chkMagnetism.isChecked()
2582        fp.is2D = self.chk2DView.isChecked()
2583        fp.data = self.data
2584
2585        # Use current models - they contain all the required parameters
2586        fp.model_model = self._model_model
2587        fp.poly_model = self._poly_model
2588        fp.magnetism_model = self._magnet_model
2589
2590        if self.cbCategory.currentIndex() != 0:
2591            fp.current_category = str(self.cbCategory.currentText())
2592            fp.current_model = str(self.cbModel.currentText())
2593
2594        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2595            fp.current_factor = str(self.cbStructureFactor.currentText())
2596        else:
2597            fp.current_factor = ''
2598
2599        fp.chi2 = self.chi2
2600        fp.parameters_to_fit = self.parameters_to_fit
[6964d44]2601        fp.kernel_module = self.kernel_module
[672b8ab]2602
[6ff2eb3]2603        # Algorithm options
2604        # fp.algorithm = self.parent.fit_options.selected_id
2605
[672b8ab]2606        # Options tab
2607        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2608        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2609        fp.fit_options[fp.NPTS] = self.npts
2610        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2611        fp.fit_options[fp.LOG_POINTS] = self.log_points
2612        fp.fit_options[fp.WEIGHTING] = self.weighting
2613
2614        # Resolution tab
2615        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2616        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2617        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2618        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2619        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2620
2621        # TODO: add polidyspersity and magnetism
2622
[00b3b40]2623
2624    def updateUndo(self):
2625        """
2626        Create a new state page and add it to the stack
2627        """
2628        if self.undo_supported:
2629            self.pushFitPage(self.currentState())
2630
[672b8ab]2631    def currentState(self):
2632        """
2633        Return fit page with current state
2634        """
2635        new_page = FitPage()
2636        self.saveToFitPage(new_page)
2637
2638        return new_page
2639
2640    def pushFitPage(self, new_page):
2641        """
2642        Add a new fit page object with current state
2643        """
[6011788]2644        self.page_stack.append(new_page)
[672b8ab]2645
2646    def popFitPage(self):
2647        """
2648        Remove top fit page from stack
2649        """
[6011788]2650        if self.page_stack:
2651            self.page_stack.pop()
[672b8ab]2652
[57be490]2653    def getReport(self):
2654        """
2655        Create and return HTML report with parameters and charts
2656        """
2657        index = None
2658        if self.all_data:
2659            index = self.all_data[self.data_index]
2660        report_logic = ReportPageLogic(self,
2661                                       kernel_module=self.kernel_module,
2662                                       data=self.data,
2663                                       index=index,
2664                                       model=self._model_model)
2665
2666        return report_logic.reportList()
2667
2668    def savePageState(self):
2669        """
2670        Create and serialize local PageState
2671        """
2672        from sas.sascalc.fit.pagestate import Reader
2673        model = self.kernel_module
2674
2675        # Old style PageState object
2676        state = PageState(model=model, data=self.data)
2677
2678        # Add parameter data to the state
2679        self.getCurrentFitState(state)
2680
2681        # Create the filewriter, aptly named 'Reader'
2682        state_reader = Reader(self.loadPageStateCallback)
2683        filepath = self.saveAsAnalysisFile()
2684        if filepath is None:
2685            return
2686        state_reader.write(filename=filepath, fitstate=state)
2687        pass
2688
2689    def saveAsAnalysisFile(self):
2690        """
2691        Show the save as... dialog and return the chosen filepath
2692        """
2693        default_name = "FitPage"+str(self.tab_id)+".fitv"
2694
2695        wildcard = "fitv files (*.fitv)"
2696        kwargs = {
2697            'caption'   : 'Save As',
2698            'directory' : default_name,
2699            'filter'    : wildcard,
2700            'parent'    : None,
2701        }
2702        # Query user for filename.
2703        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
2704        filename = filename_tuple[0]
2705        return filename
2706
2707    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
2708        """
2709        This is a callback method called from the CANSAS reader.
2710        We need the instance of this reader only for writing out a file,
2711        so there's nothing here.
2712        Until Load Analysis is implemented, that is.
2713        """
2714        pass
2715
2716    def loadPageState(self, pagestate=None):
2717        """
2718        Load the PageState object and update the current widget
2719        """
2720        pass
2721
2722    def getCurrentFitState(self, state=None):
2723        """
2724        Store current state for fit_page
2725        """
2726        # save model option
2727        #if self.model is not None:
2728        #    self.disp_list = self.getDispParamList()
2729        #    state.disp_list = copy.deepcopy(self.disp_list)
2730        #    #state.model = self.model.clone()
2731
2732        # Comboboxes
2733        state.categorycombobox = self.cbCategory.currentText()
2734        state.formfactorcombobox = self.cbModel.currentText()
2735        if self.cbStructureFactor.isEnabled():
2736            state.structureCombobox = self.cbStructureFactor.currentText()
2737        state.tcChi = self.chi2
2738
2739        state.enable2D = self.is2D
2740
2741        #state.weights = copy.deepcopy(self.weights)
2742        # save data
2743        state.data = copy.deepcopy(self.data)
2744
2745        # save plotting range
2746        state.qmin = self.q_range_min
2747        state.qmax = self.q_range_max
2748        state.npts = self.npts
2749
2750        #    self.state.enable_disp = self.enable_disp.GetValue()
2751        #    self.state.disable_disp = self.disable_disp.GetValue()
2752
2753        #    self.state.enable_smearer = \
2754        #                        copy.deepcopy(self.enable_smearer.GetValue())
2755        #    self.state.disable_smearer = \
2756        #                        copy.deepcopy(self.disable_smearer.GetValue())
2757
2758        #self.state.pinhole_smearer = \
2759        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
2760        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
2761        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
2762        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
2763        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
2764        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
2765
2766        p = self.model_parameters
2767        # save checkbutton state and txtcrtl values
2768        state.parameters = FittingUtilities.getStandardParam()
2769        state.orientation_params_disp = FittingUtilities.getOrientationParam()
2770
2771        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
2772        #self._copy_parameters_state(self.parameters, self.state.parameters)
2773        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
2774        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
2775
2776
Note: See TracBrowser for help on using the repository browser.