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

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 2d47985 was 2d47985, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed poly/magnet param update on model change for specific cases. SASVIEW-1056

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