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

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 fd7ef36 was fd7ef36, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

delete intermediate theory-only plots after model evaluation, before adding current ones

this applies only to beta approximation, whereby plots such as beta(Q)
and S_eff(Q) may be removed between calculations. however, since it does
not affect behaviour otherwise, I am pushing to ESS_GUI to ensure no
later conflicts occur

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