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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 676a430 was 676a430, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Use only unique parameters in the polydisp. table SASVIEW-1064
Move the 2D color bar a bit to the right so it doesn't overlap with the plot.

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