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

Last change on this file since c71b20a was c71b20a, checked in by ibressler, 6 years ago

FittingWidget?: showing also new plots on recalc conditionally

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