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

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

fix crash in iterateOverMagnetModel

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