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

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

shell parameters appear in P(Q) section correctly

  • 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            if self.isCheckable(row_i):
1726                func(row_i)
1727
1728    def updateMagnetModelFromList(self, param_dict):
1729        """
1730        Update the magnetic model with new parameters, create the errors column
1731        """
1732        assert isinstance(param_dict, dict)
1733        if not dict:
1734            return
1735        if self._magnet_model.rowCount() == 0:
1736            return
1737
1738        def updateFittedValues(row):
1739            # Utility function for main model update
1740            # internal so can use closure for param_dict
1741            if self._magnet_model.item(row, 0) is None:
1742                return
1743            param_name = str(self._magnet_model.item(row, 0).text())
1744            if param_name not in list(param_dict.keys()):
1745                return
1746            # modify the param value
1747            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1748            self._magnet_model.item(row, 1).setText(param_repr)
1749            if self.has_magnet_error_column:
1750                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1751                self._magnet_model.item(row, 2).setText(error_repr)
1752
1753        def createErrorColumn(row):
1754            # Utility function for error column update
1755            item = QtGui.QStandardItem()
1756            def createItem(param_name):
1757                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1758                item.setText(error_repr)
1759            def curr_param():
1760                return str(self._magnet_model.item(row, 0).text())
1761
1762            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1763
1764            error_column.append(item)
1765
1766        # block signals temporarily, so we don't end up
1767        # updating charts with every single model change on the end of fitting
1768        self._magnet_model.blockSignals(True)
1769        self.iterateOverMagnetModel(updateFittedValues)
1770        self._magnet_model.blockSignals(False)
1771
1772        if self.has_magnet_error_column:
1773            return
1774
1775        self.lstMagnetic.itemDelegate().addErrorColumn()
1776        error_column = []
1777        self.iterateOverMagnetModel(createErrorColumn)
1778
1779        # switch off reponse to model change
1780        self._magnet_model.blockSignals(True)
1781        self._magnet_model.insertColumn(2, error_column)
1782        self._magnet_model.blockSignals(False)
1783        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1784
1785        self.has_magnet_error_column = True
1786
1787    def onPlot(self):
1788        """
1789        Plot the current set of data
1790        """
1791        # Regardless of previous state, this should now be `plot show` functionality only
1792        self.cmdPlot.setText("Show Plot")
1793        # Force data recalculation so existing charts are updated
1794        self.recalculatePlotData()
1795        self.showPlot()
1796
1797    def onSmearingOptionsUpdate(self):
1798        """
1799        React to changes in the smearing widget
1800        """
1801        self.calculateQGridForModel()
1802
1803    def recalculatePlotData(self):
1804        """
1805        Generate a new dataset for model
1806        """
1807        if not self.data_is_loaded:
1808            self.createDefaultDataset()
1809        self.calculateQGridForModel()
1810
1811    def showPlot(self):
1812        """
1813        Show the current plot in MPL
1814        """
1815        # Show the chart if ready
1816        data_to_show = self.data if self.data_is_loaded else self.model_data
1817        if data_to_show is not None:
1818            self.communicate.plotRequestedSignal.emit([data_to_show])
1819
1820    def onOptionsUpdate(self):
1821        """
1822        Update local option values and replot
1823        """
1824        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1825            self.options_widget.state()
1826        # set Q range labels on the main tab
1827        self.lblMinRangeDef.setText(str(self.q_range_min))
1828        self.lblMaxRangeDef.setText(str(self.q_range_max))
1829        self.recalculatePlotData()
1830
1831    def setDefaultStructureCombo(self):
1832        """
1833        Fill in the structure factors combo box with defaults
1834        """
1835        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1836        factors = [factor[0] for factor in structure_factor_list]
1837        factors.insert(0, STRUCTURE_DEFAULT)
1838        self.cbStructureFactor.clear()
1839        self.cbStructureFactor.addItems(sorted(factors))
1840
1841    def createDefaultDataset(self):
1842        """
1843        Generate default Dataset 1D/2D for the given model
1844        """
1845        # Create default datasets if no data passed
1846        if self.is2D:
1847            qmax = self.q_range_max/np.sqrt(2)
1848            qstep = self.npts
1849            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1850            return
1851        elif self.log_points:
1852            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1853            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1854            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1855        else:
1856            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1857                                   num=self.npts, endpoint=True)
1858        self.logic.createDefault1dData(interval, self.tab_id)
1859
1860    def readCategoryInfo(self):
1861        """
1862        Reads the categories in from file
1863        """
1864        self.master_category_dict = defaultdict(list)
1865        self.by_model_dict = defaultdict(list)
1866        self.model_enabled_dict = defaultdict(bool)
1867
1868        categorization_file = CategoryInstaller.get_user_file()
1869        if not os.path.isfile(categorization_file):
1870            categorization_file = CategoryInstaller.get_default_file()
1871        with open(categorization_file, 'rb') as cat_file:
1872            self.master_category_dict = json.load(cat_file)
1873            self.regenerateModelDict()
1874
1875        # Load the model dict
1876        models = load_standard_models()
1877        for model in models:
1878            self.models[model.name] = model
1879
1880        self.readCustomCategoryInfo()
1881
1882    def readCustomCategoryInfo(self):
1883        """
1884        Reads the custom model category
1885        """
1886        #Looking for plugins
1887        self.plugins = list(self.custom_models.values())
1888        plugin_list = []
1889        for name, plug in self.custom_models.items():
1890            self.models[name] = plug
1891            plugin_list.append([name, True])
1892        self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1893
1894    def regenerateModelDict(self):
1895        """
1896        Regenerates self.by_model_dict which has each model name as the
1897        key and the list of categories belonging to that model
1898        along with the enabled mapping
1899        """
1900        self.by_model_dict = defaultdict(list)
1901        for category in self.master_category_dict:
1902            for (model, enabled) in self.master_category_dict[category]:
1903                self.by_model_dict[model].append(category)
1904                self.model_enabled_dict[model] = enabled
1905
1906    def addBackgroundToModel(self, model):
1907        """
1908        Adds background parameter with default values to the model
1909        """
1910        assert isinstance(model, QtGui.QStandardItemModel)
1911        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1912        FittingUtilities.addCheckedListToModel(model, checked_list)
1913        last_row = model.rowCount()-1
1914        model.item(last_row, 0).setEditable(False)
1915        model.item(last_row, 4).setEditable(False)
1916
1917    def addScaleToModel(self, model):
1918        """
1919        Adds scale parameter with default values to the model
1920        """
1921        assert isinstance(model, QtGui.QStandardItemModel)
1922        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1923        FittingUtilities.addCheckedListToModel(model, checked_list)
1924        last_row = model.rowCount()-1
1925        model.item(last_row, 0).setEditable(False)
1926        model.item(last_row, 4).setEditable(False)
1927
1928    def addWeightingToData(self, data):
1929        """
1930        Adds weighting contribution to fitting data
1931        """
1932        # Send original data for weighting
1933        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1934        if self.is2D:
1935            data.err_data = weight
1936        else:
1937            data.dy = weight
1938        pass
1939
1940    def updateQRange(self):
1941        """
1942        Updates Q Range display
1943        """
1944        if self.data_is_loaded:
1945            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1946        # set Q range labels on the main tab
1947        self.lblMinRangeDef.setText(str(self.q_range_min))
1948        self.lblMaxRangeDef.setText(str(self.q_range_max))
1949        # set Q range labels on the options tab
1950        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1951
1952    def SASModelToQModel(self, model_name, structure_factor=None):
1953        """
1954        Setting model parameters into table based on selected category
1955        """
1956        # Crete/overwrite model items
1957        self._model_model.clear()
1958        self._poly_model.clear()
1959        self._magnet_model.clear()
1960
1961        if model_name is None:
1962            if structure_factor not in (None, "None"):
1963                # S(Q) on its own, treat the same as a form factor
1964                self.kernel_module = None
1965                self.fromStructureFactorToQModel(structure_factor)
1966            else:
1967                # No models selected
1968                return
1969        else:
1970            self.fromModelToQModel(model_name)
1971            self.addExtraShells()
1972
1973            if structure_factor not in (None, "None"):
1974                # add S(Q)
1975                self.fromStructureFactorToQModel(structure_factor)
1976            else:
1977                # enable selection of S(Q)
1978                self.enableStructureFactorControl(structure_factor)
1979
1980            # Add polydispersity to the model
1981            self.setPolyModel()
1982            # Add magnetic parameters to the model
1983            self.setMagneticModel()
1984
1985        # Adjust the table cells width
1986        self.lstParams.resizeColumnToContents(0)
1987        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1988
1989        # Now we claim the model has been loaded
1990        self.model_is_loaded = True
1991        # Change the model name to a monicker
1992        self.kernel_module.name = self.modelName()
1993        # Update the smearing tab
1994        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
1995
1996        # (Re)-create headers
1997        FittingUtilities.addHeadersToModel(self._model_model)
1998        self.lstParams.header().setFont(self.boldFont)
1999
2000        # Update Q Ranges
2001        self.updateQRange()
2002
2003    def fromModelToQModel(self, model_name):
2004        """
2005        Setting model parameters into QStandardItemModel based on selected _model_
2006        """
2007        name = model_name
2008        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2009            # custom kernel load requires full path
2010            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2011        try:
2012            kernel_module = generate.load_kernel_module(name)
2013        except ModuleNotFoundError:
2014            # maybe it's a recategorised custom model?
2015            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2016            # If this rises, it's a valid problem.
2017            kernel_module = generate.load_kernel_module(name)
2018
2019        if hasattr(kernel_module, 'parameters'):
2020            # built-in and custom models
2021            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2022
2023        elif hasattr(kernel_module, 'model_info'):
2024            # for sum/multiply models
2025            self.model_parameters = kernel_module.model_info.parameters
2026
2027        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2028            # this probably won't work if there's no model_info, but just in case
2029            self.model_parameters = kernel_module.Model._model_info.parameters
2030        else:
2031            # no parameters - default to blank table
2032            msg = "No parameters found in model '{}'.".format(model_name)
2033            logger.warning(msg)
2034            self.model_parameters = modelinfo.ParameterTable([])
2035
2036        # Instantiate the current sasmodel
2037        self.kernel_module = self.models[model_name]()
2038
2039        # Explicitly add scale and background with default values
2040        temp_undo_state = self.undo_supported
2041        self.undo_supported = False
2042        self.addScaleToModel(self._model_model)
2043        self.addBackgroundToModel(self._model_model)
2044        self.undo_supported = temp_undo_state
2045
2046        self.shell_names = self.shellNamesList()
2047
2048        # Get new rows for QModel
2049        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
2050
2051        # Add heading row
2052        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2053
2054        # Update QModel
2055        for row in new_rows:
2056            self._model_model.appendRow(row)
2057
2058    def fromStructureFactorToQModel(self, structure_factor):
2059        """
2060        Setting model parameters into QStandardItemModel based on selected _structure factor_
2061        """
2062        s_kernel = self.models[structure_factor]()
2063        p_kernel = self.kernel_module
2064
2065        if p_kernel is None:
2066            # Not a product model, just S(Q)
2067            self.kernel_module = s_kernel
2068            params = modelinfo.ParameterTable(self.kernel_module._model_info.parameters.kernel_parameters)
2069            new_rows = FittingUtilities.addSimpleParametersToModel(params, self.is2D)
2070        else:
2071            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2072            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2073
2074            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2075            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2076            all_param_names = [param.name for param in all_params]
2077
2078            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2079            # conflicting names with P(Q) params will cause a rename; we also lose radius_effective (for now...)
2080
2081            # TODO: merge rest of beta approx implementation in
2082            # This is to ensure compatibility when we merge beta approx support in...!
2083
2084            # radius_effective is always s_params[0]
2085
2086            # if radius_effective_mode is in all_params, then all_params contains radius_effective and we want to
2087            # keep it in the model
2088
2089            # if radius_effective_mode is NOT in all_params, then radius_effective should NOT be kept, because the user
2090            # cannot specify it themselves; but, make sure we only remove it if it's actually there in the first place
2091            # (sasmodels master removes it already)
2092            if "radius_effective_mode" in all_param_names:
2093                # Show all parameters
2094                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2095                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2096            else:
2097                # Ensure radius_effective is not displayed
2098                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2099                if "radius_effective" in all_param_names:
2100                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2101                else:
2102                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2103
2104            # Get new rows for QModel
2105            # Any renamed parameters are stored as data in the relevant item, for later handling
2106            new_rows = FittingUtilities.addSimpleParametersToModel(s_params, self.is2D, s_params_orig)
2107
2108            # TODO: merge rest of beta approx implementation in
2109            # These parameters are not part of P(Q) nor S(Q), but are added only to the product model (e.g. specifying
2110            # structure factor calculation mode)
2111            # product_params = all_params[p_pars_len+s_pars_len:]
2112
2113        # Add heading row
2114        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2115
2116        # Update QModel
2117        for row in new_rows:
2118            self._model_model.appendRow(row)
2119            # disable fitting of parameters not listed in self.kernel_module (probably radius_effective)
2120            # if row[0].text() not in self.kernel_module.params.keys():
2121            #     row_num = self._model_model.rowCount() - 1
2122            #     FittingUtilities.markParameterDisabled(self._model_model, row_num)
2123
2124    def haveParamsToFit(self):
2125        """
2126        Finds out if there are any parameters ready to be fitted
2127        """
2128        return (self.main_params_to_fit!=[]
2129                or self.poly_params_to_fit!=[]
2130                or self.magnet_params_to_fit != []) and \
2131                self.logic.data_is_loaded
2132
2133    def onMainParamsChange(self, item):
2134        """
2135        Callback method for updating the sasmodel parameters with the GUI values
2136        """
2137        model_column = item.column()
2138
2139        if model_column == 0:
2140            self.checkboxSelected(item)
2141            self.cmdFit.setEnabled(self.haveParamsToFit())
2142            # Update state stack
2143            self.updateUndo()
2144            return
2145
2146        model_row = item.row()
2147        name_index = self._model_model.index(model_row, 0)
2148        name_item = self._model_model.itemFromIndex(name_index)
2149
2150        # Extract changed value.
2151        try:
2152            value = GuiUtils.toDouble(item.text())
2153        except TypeError:
2154            # Unparsable field
2155            return
2156
2157        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2158        if name_item.data(QtCore.Qt.UserRole):
2159            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2160        else:
2161            parameter_name = str(self._model_model.data(name_index))
2162
2163        # Update the parameter value - note: this supports +/-inf as well
2164        self.kernel_module.params[parameter_name] = value
2165
2166        # Update the parameter value - note: this supports +/-inf as well
2167        param_column = self.lstParams.itemDelegate().param_value
2168        min_column = self.lstParams.itemDelegate().param_min
2169        max_column = self.lstParams.itemDelegate().param_max
2170        if model_column == param_column:
2171            self.kernel_module.setParam(parameter_name, value)
2172        elif model_column == min_column:
2173            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2174            self.kernel_module.details[parameter_name][1] = value
2175        elif model_column == max_column:
2176            self.kernel_module.details[parameter_name][2] = value
2177        else:
2178            # don't update the chart
2179            return
2180
2181        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2182        # TODO: multishell params in self.kernel_module.details[??] = value
2183
2184        # Force the chart update when actual parameters changed
2185        if model_column == 1:
2186            self.recalculatePlotData()
2187
2188        # Update state stack
2189        self.updateUndo()
2190
2191    def isCheckable(self, row):
2192        return self._model_model.item(row, 0).isCheckable()
2193
2194    def checkboxSelected(self, item):
2195        # Assure we're dealing with checkboxes
2196        if not item.isCheckable():
2197            return
2198        status = item.checkState()
2199
2200        # If multiple rows selected - toggle all of them, filtering uncheckable
2201        # Switch off signaling from the model to avoid recursion
2202        self._model_model.blockSignals(True)
2203        # Convert to proper indices and set requested enablement
2204        self.setParameterSelection(status)
2205        self._model_model.blockSignals(False)
2206
2207        # update the list of parameters to fit
2208        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2209
2210    def checkedListFromModel(self, model):
2211        """
2212        Returns list of checked parameters for given model
2213        """
2214        def isChecked(row):
2215            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2216
2217        return [str(model.item(row_index, 0).text())
2218                for row_index in range(model.rowCount())
2219                if isChecked(row_index)]
2220
2221    def createNewIndex(self, fitted_data):
2222        """
2223        Create a model or theory index with passed Data1D/Data2D
2224        """
2225        if self.data_is_loaded:
2226            if not fitted_data.name:
2227                name = self.nameForFittedData(self.data.filename)
2228                fitted_data.title = name
2229                fitted_data.name = name
2230                fitted_data.filename = name
2231                fitted_data.symbol = "Line"
2232            self.updateModelIndex(fitted_data)
2233        else:
2234            if not fitted_data.name:
2235                name = self.nameForFittedData(self.kernel_module.id)
2236            else:
2237                name = fitted_data.name
2238            fitted_data.title = name
2239            fitted_data.filename = name
2240            fitted_data.symbol = "Line"
2241            self.createTheoryIndex(fitted_data)
2242
2243    def updateModelIndex(self, fitted_data):
2244        """
2245        Update a QStandardModelIndex containing model data
2246        """
2247        name = self.nameFromData(fitted_data)
2248        # Make this a line if no other defined
2249        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2250            fitted_data.symbol = 'Line'
2251        # Notify the GUI manager so it can update the main model in DataExplorer
2252        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2253
2254    def createTheoryIndex(self, fitted_data):
2255        """
2256        Create a QStandardModelIndex containing model data
2257        """
2258        name = self.nameFromData(fitted_data)
2259        # Notify the GUI manager so it can create the theory model in DataExplorer
2260        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2261        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
2262
2263    def nameFromData(self, fitted_data):
2264        """
2265        Return name for the dataset. Terribly impure function.
2266        """
2267        if fitted_data.name is None:
2268            name = self.nameForFittedData(self.logic.data.filename)
2269            fitted_data.title = name
2270            fitted_data.name = name
2271            fitted_data.filename = name
2272        else:
2273            name = fitted_data.name
2274        return name
2275
2276    def methodCalculateForData(self):
2277        '''return the method for data calculation'''
2278        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2279
2280    def methodCompleteForData(self):
2281        '''return the method for result parsin on calc complete '''
2282        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2283
2284    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2285        """
2286        Wrapper for Calc1D/2D calls
2287        """
2288        if data is None:
2289            data = self.data
2290        if model is None:
2291            model = self.kernel_module
2292        if completefn is None:
2293            completefn = self.methodCompleteForData()
2294        smearer = self.smearing_widget.smearer()
2295        # Awful API to a backend method.
2296        calc_thread = self.methodCalculateForData()(data=data,
2297                                               model=model,
2298                                               page_id=0,
2299                                               qmin=self.q_range_min,
2300                                               qmax=self.q_range_max,
2301                                               smearer=smearer,
2302                                               state=None,
2303                                               weight=None,
2304                                               fid=None,
2305                                               toggle_mode_on=False,
2306                                               completefn=completefn,
2307                                               update_chisqr=True,
2308                                               exception_handler=self.calcException,
2309                                               source=None)
2310        if use_threads:
2311            if LocalConfig.USING_TWISTED:
2312                # start the thread with twisted
2313                thread = threads.deferToThread(calc_thread.compute)
2314                thread.addCallback(completefn)
2315                thread.addErrback(self.calculateDataFailed)
2316            else:
2317                # Use the old python threads + Queue
2318                calc_thread.queue()
2319                calc_thread.ready(2.5)
2320        else:
2321            results = calc_thread.compute()
2322            completefn(results)
2323
2324    def calculateQGridForModel(self):
2325        """
2326        Prepare the fitting data object, based on current ModelModel
2327        """
2328        if self.kernel_module is None:
2329            return
2330        self.calculateQGridForModelExt()
2331
2332    def calculateDataFailed(self, reason):
2333        """
2334        Thread returned error
2335        """
2336        print("Calculate Data failed with ", reason)
2337
2338    def completed1D(self, return_data):
2339        self.Calc1DFinishedSignal.emit(return_data)
2340
2341    def completed2D(self, return_data):
2342        self.Calc2DFinishedSignal.emit(return_data)
2343
2344    def complete1D(self, return_data):
2345        """
2346        Plot the current 1D data
2347        """
2348        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2349        residuals = self.calculateResiduals(fitted_data)
2350        self.model_data = fitted_data
2351
2352        new_plots = [fitted_data, residuals]
2353
2354        # Create plots for intermediate product data
2355        pq_data, sq_data = self.logic.new1DProductPlots(return_data, self.tab_id)
2356        if pq_data is not None:
2357            pq_data.symbol = "Line"
2358            self.createNewIndex(pq_data)
2359            # self.communicate.plotUpdateSignal.emit([pq_data])
2360            new_plots.append(pq_data)
2361        if sq_data is not None:
2362            sq_data.symbol = "Line"
2363            self.createNewIndex(sq_data)
2364            # self.communicate.plotUpdateSignal.emit([sq_data])
2365            new_plots.append(sq_data)
2366
2367        if self.data_is_loaded:
2368            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2369
2370        # TODO: merge rest of beta approx implementation in
2371        # TODO: refactor
2372        # deal with constrained radius_effective
2373        # for row in range(self._model_model.rowCount()):
2374        #     if self._model_model.item(row, 0).text() == "radius_effective_mode":
2375        #         if GuiUtils.toDouble(self._model_model.item(row, 1).text()) == 0:
2376        #             return
2377        # radius_effective = intermediate_ER()
2378        # if radius_effective:
2379        #     for row in range(self._model_model.rowCount()):
2380        #         if self._model_model.item(row, 0).text() == "radius_effective":
2381        #             self._model_model.item(row, 1).setText(str(radius_effective))
2382        #             break
2383
2384        for plot in new_plots:
2385            if hasattr(plot, "id") and "esidual" in plot.id:
2386                # TODO: fix updates to residuals plot
2387                pass
2388            elif plot is not None:
2389                self.communicate.plotUpdateSignal.emit([plot])
2390
2391    def complete2D(self, return_data):
2392        """
2393        Plot the current 2D data
2394        """
2395        fitted_data = self.logic.new2DPlot(return_data)
2396        self.calculateResiduals(fitted_data)
2397        self.model_data = fitted_data
2398
2399    def calculateResiduals(self, fitted_data):
2400        """
2401        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2402        """
2403        # Create a new index for holding data
2404        fitted_data.symbol = "Line"
2405
2406        # Modify fitted_data with weighting
2407        self.addWeightingToData(fitted_data)
2408
2409        self.createNewIndex(fitted_data)
2410        # Calculate difference between return_data and logic.data
2411        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
2412        # Update the control
2413        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2414        self.lblChi2Value.setText(chi2_repr)
2415
2416        # self.communicate.plotUpdateSignal.emit([fitted_data])
2417
2418        # Plot residuals if actual data
2419        if not self.data_is_loaded:
2420            return
2421
2422        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
2423        residuals_plot.id = "Residual " + residuals_plot.id
2424        self.createNewIndex(residuals_plot)
2425        return residuals_plot
2426
2427    def onCategoriesChanged(self):
2428            """
2429            Reload the category/model comboboxes
2430            """
2431            # Store the current combo indices
2432            current_cat = self.cbCategory.currentText()
2433            current_model = self.cbModel.currentText()
2434
2435            # reread the category file and repopulate the combo
2436            self.cbCategory.blockSignals(True)
2437            self.cbCategory.clear()
2438            self.readCategoryInfo()
2439            self.initializeCategoryCombo()
2440
2441            # Scroll back to the original index in Categories
2442            new_index = self.cbCategory.findText(current_cat)
2443            if new_index != -1:
2444                self.cbCategory.setCurrentIndex(new_index)
2445            self.cbCategory.blockSignals(False)
2446            # ...and in the Models
2447            self.cbModel.blockSignals(True)
2448            new_index = self.cbModel.findText(current_model)
2449            if new_index != -1:
2450                self.cbModel.setCurrentIndex(new_index)
2451            self.cbModel.blockSignals(False)
2452
2453            return
2454
2455    def calcException(self, etype, value, tb):
2456        """
2457        Thread threw an exception.
2458        """
2459        # TODO: remimplement thread cancellation
2460        logging.error("".join(traceback.format_exception(etype, value, tb)))
2461
2462    def setTableProperties(self, table):
2463        """
2464        Setting table properties
2465        """
2466        # Table properties
2467        table.verticalHeader().setVisible(False)
2468        table.setAlternatingRowColors(True)
2469        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2470        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2471        table.resizeColumnsToContents()
2472
2473        # Header
2474        header = table.horizontalHeader()
2475        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2476        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2477
2478        # Qt5: the following 2 lines crash - figure out why!
2479        # Resize column 0 and 7 to content
2480        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2481        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2482
2483    def setPolyModel(self):
2484        """
2485        Set polydispersity values
2486        """
2487        if not self.model_parameters:
2488            return
2489        self._poly_model.clear()
2490
2491        [self.setPolyModelParameters(i, param) for i, param in \
2492            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
2493        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2494
2495    def setPolyModelParameters(self, i, param):
2496        """
2497        Standard of multishell poly parameter driver
2498        """
2499        param_name = param.name
2500        # see it the parameter is multishell
2501        if '[' in param.name:
2502            # Skip empty shells
2503            if self.current_shell_displayed == 0:
2504                return
2505            else:
2506                # Create as many entries as current shells
2507                for ishell in range(1, self.current_shell_displayed+1):
2508                    # Remove [n] and add the shell numeral
2509                    name = param_name[0:param_name.index('[')] + str(ishell)
2510                    self.addNameToPolyModel(i, name)
2511        else:
2512            # Just create a simple param entry
2513            self.addNameToPolyModel(i, param_name)
2514
2515    def addNameToPolyModel(self, i, param_name):
2516        """
2517        Creates a checked row in the poly model with param_name
2518        """
2519        # Polydisp. values from the sasmodel
2520        width = self.kernel_module.getParam(param_name + '.width')
2521        npts = self.kernel_module.getParam(param_name + '.npts')
2522        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2523        _, min, max = self.kernel_module.details[param_name]
2524
2525        # Construct a row with polydisp. related variable.
2526        # This will get added to the polydisp. model
2527        # Note: last argument needs extra space padding for decent display of the control
2528        checked_list = ["Distribution of " + param_name, str(width),
2529                        str(min), str(max),
2530                        str(npts), str(nsigs), "gaussian      ",'']
2531        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2532
2533        # All possible polydisp. functions as strings in combobox
2534        func = QtWidgets.QComboBox()
2535        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2536        # Set the default index
2537        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2538        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2539        self.lstPoly.setIndexWidget(ind, func)
2540        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2541
2542    def onPolyFilenameChange(self, row_index):
2543        """
2544        Respond to filename_updated signal from the delegate
2545        """
2546        # For the given row, invoke the "array" combo handler
2547        array_caption = 'array'
2548
2549        # Get the combo box reference
2550        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2551        widget = self.lstPoly.indexWidget(ind)
2552
2553        # Update the combo box so it displays "array"
2554        widget.blockSignals(True)
2555        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2556        widget.blockSignals(False)
2557
2558        # Invoke the file reader
2559        self.onPolyComboIndexChange(array_caption, row_index)
2560
2561    def onPolyComboIndexChange(self, combo_string, row_index):
2562        """
2563        Modify polydisp. defaults on function choice
2564        """
2565        # Get npts/nsigs for current selection
2566        param = self.model_parameters.form_volume_parameters[row_index]
2567        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2568        combo_box = self.lstPoly.indexWidget(file_index)
2569
2570        def updateFunctionCaption(row):
2571            # Utility function for update of polydispersity function name in the main model
2572            if not self.isCheckable(row):
2573                return
2574            param_name = str(self._model_model.item(row, 0).text())
2575            if param_name !=  param.name:
2576                return
2577            # Modify the param value
2578            if self.has_error_column:
2579                # err column changes the indexing
2580                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2581            else:
2582                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2583
2584        if combo_string == 'array':
2585            try:
2586                self.loadPolydispArray(row_index)
2587                # Update main model for display
2588                self.iterateOverModel(updateFunctionCaption)
2589                # disable the row
2590                lo = self.lstPoly.itemDelegate().poly_pd
2591                hi = self.lstPoly.itemDelegate().poly_function
2592                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2593                return
2594            except IOError:
2595                combo_box.setCurrentIndex(self.orig_poly_index)
2596                # Pass for cancel/bad read
2597                pass
2598
2599        # Enable the row in case it was disabled by Array
2600        self._poly_model.blockSignals(True)
2601        max_range = self.lstPoly.itemDelegate().poly_filename
2602        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2603        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2604        self._poly_model.setData(file_index, "")
2605        self._poly_model.blockSignals(False)
2606
2607        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2608        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2609
2610        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2611        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2612
2613        self._poly_model.setData(npts_index, npts)
2614        self._poly_model.setData(nsigs_index, nsigs)
2615
2616        self.iterateOverModel(updateFunctionCaption)
2617        self.orig_poly_index = combo_box.currentIndex()
2618
2619    def loadPolydispArray(self, row_index):
2620        """
2621        Show the load file dialog and loads requested data into state
2622        """
2623        datafile = QtWidgets.QFileDialog.getOpenFileName(
2624            self, "Choose a weight file", "", "All files (*.*)", None,
2625            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2626
2627        if not datafile:
2628            logging.info("No weight data chosen.")
2629            raise IOError
2630
2631        values = []
2632        weights = []
2633        def appendData(data_tuple):
2634            """
2635            Fish out floats from a tuple of strings
2636            """
2637            try:
2638                values.append(float(data_tuple[0]))
2639                weights.append(float(data_tuple[1]))
2640            except (ValueError, IndexError):
2641                # just pass through if line with bad data
2642                return
2643
2644        with open(datafile, 'r') as column_file:
2645            column_data = [line.rstrip().split() for line in column_file.readlines()]
2646            [appendData(line) for line in column_data]
2647
2648        # If everything went well - update the sasmodel values
2649        self.disp_model = POLYDISPERSITY_MODELS['array']()
2650        self.disp_model.set_weights(np.array(values), np.array(weights))
2651        # + update the cell with filename
2652        fname = os.path.basename(str(datafile))
2653        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2654        self._poly_model.setData(fname_index, fname)
2655
2656    def setMagneticModel(self):
2657        """
2658        Set magnetism values on model
2659        """
2660        if not self.model_parameters:
2661            return
2662        self._magnet_model.clear()
2663        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2664            self.model_parameters.call_parameters if param.type == 'magnetic']
2665        FittingUtilities.addHeadersToModel(self._magnet_model)
2666
2667    def shellNamesList(self):
2668        """
2669        Returns list of names of all multi-shell parameters
2670        E.g. for sld[n], radius[n], n=1..3 it will return
2671        [sld1, sld2, sld3, radius1, radius2, radius3]
2672        """
2673        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2674        top_index = self.kernel_module.multiplicity_info.number
2675        shell_names = []
2676        for i in range(1, top_index+1):
2677            for name in multi_names:
2678                shell_names.append(name+str(i))
2679        return shell_names
2680
2681    def addCheckedMagneticListToModel(self, param, model):
2682        """
2683        Wrapper for model update with a subset of magnetic parameters
2684        """
2685        if param.name[param.name.index(':')+1:] in self.shell_names:
2686            # check if two-digit shell number
2687            try:
2688                shell_index = int(param.name[-2:])
2689            except ValueError:
2690                shell_index = int(param.name[-1:])
2691
2692            if shell_index > self.current_shell_displayed:
2693                return
2694
2695        checked_list = [param.name,
2696                        str(param.default),
2697                        str(param.limits[0]),
2698                        str(param.limits[1]),
2699                        param.units]
2700
2701        FittingUtilities.addCheckedListToModel(model, checked_list)
2702
2703    def enableStructureFactorControl(self, structure_factor):
2704        """
2705        Add structure factors to the list of parameters
2706        """
2707        if self.kernel_module.is_form_factor or structure_factor == 'None':
2708            self.enableStructureCombo()
2709        else:
2710            self.disableStructureCombo()
2711
2712    def addExtraShells(self):
2713        """
2714        Add a combobox for multiple shell display
2715        """
2716        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2717
2718        if param_length == 0:
2719            return
2720
2721        # cell 1: variable name
2722        item1 = QtGui.QStandardItem(param_name)
2723
2724        func = QtWidgets.QComboBox()
2725        # Available range of shells displayed in the combobox
2726        func.addItems([str(i) for i in range(param_length+1)])
2727
2728        # Respond to index change
2729        func.currentIndexChanged.connect(self.modifyShellsInList)
2730
2731        # cell 2: combobox
2732        item2 = QtGui.QStandardItem()
2733        self._model_model.appendRow([item1, item2])
2734
2735        # Beautify the row:  span columns 2-4
2736        shell_row = self._model_model.rowCount()
2737        shell_index = self._model_model.index(shell_row-1, 1)
2738
2739        self.lstParams.setIndexWidget(shell_index, func)
2740        self._n_shells_row = shell_row - 1
2741
2742        # Set the index to the state-kept value
2743        func.setCurrentIndex(self.current_shell_displayed
2744                             if self.current_shell_displayed < func.count() else 0)
2745
2746    def modifyShellsInList(self, index):
2747        """
2748        Add/remove additional multishell parameters
2749        """
2750        # Find row location of the combobox
2751        first_row = self._n_shells_row + 1
2752        remove_rows = self._num_shell_params
2753
2754        if remove_rows > 1:
2755            self._model_model.removeRows(first_row, remove_rows)
2756
2757        new_rows = FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index, first_row)
2758        self._num_shell_params = len(new_rows)
2759
2760        self.current_shell_displayed = index
2761
2762        # Update relevant models
2763        self.setPolyModel()
2764        self.setMagneticModel()
2765
2766    def setFittingStarted(self):
2767        """
2768        Set buttion caption on fitting start
2769        """
2770        # Notify the user that fitting is being run
2771        # Allow for stopping the job
2772        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
2773        self.cmdFit.setText('Stop fit')
2774
2775    def setFittingStopped(self):
2776        """
2777        Set button caption on fitting stop
2778        """
2779        # Notify the user that fitting is available
2780        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
2781        self.cmdFit.setText("Fit")
2782        self.fit_started = False
2783
2784    def readFitPage(self, fp):
2785        """
2786        Read in state from a fitpage object and update GUI
2787        """
2788        assert isinstance(fp, FitPage)
2789        # Main tab info
2790        self.logic.data.filename = fp.filename
2791        self.data_is_loaded = fp.data_is_loaded
2792        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2793        self.chkMagnetism.setCheckState(fp.is_magnetic)
2794        self.chk2DView.setCheckState(fp.is2D)
2795
2796        # Update the comboboxes
2797        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2798        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2799        if fp.current_factor:
2800            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2801
2802        self.chi2 = fp.chi2
2803
2804        # Options tab
2805        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2806        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2807        self.npts = fp.fit_options[fp.NPTS]
2808        self.log_points = fp.fit_options[fp.LOG_POINTS]
2809        self.weighting = fp.fit_options[fp.WEIGHTING]
2810
2811        # Models
2812        self._model_model = fp.model_model
2813        self._poly_model = fp.poly_model
2814        self._magnet_model = fp.magnetism_model
2815
2816        # Resolution tab
2817        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2818        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2819        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2820        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2821        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2822
2823        # TODO: add polidyspersity and magnetism
2824
2825    def saveToFitPage(self, fp):
2826        """
2827        Write current state to the given fitpage
2828        """
2829        assert isinstance(fp, FitPage)
2830
2831        # Main tab info
2832        fp.filename = self.logic.data.filename
2833        fp.data_is_loaded = self.data_is_loaded
2834        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2835        fp.is_magnetic = self.chkMagnetism.isChecked()
2836        fp.is2D = self.chk2DView.isChecked()
2837        fp.data = self.data
2838
2839        # Use current models - they contain all the required parameters
2840        fp.model_model = self._model_model
2841        fp.poly_model = self._poly_model
2842        fp.magnetism_model = self._magnet_model
2843
2844        if self.cbCategory.currentIndex() != 0:
2845            fp.current_category = str(self.cbCategory.currentText())
2846            fp.current_model = str(self.cbModel.currentText())
2847
2848        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2849            fp.current_factor = str(self.cbStructureFactor.currentText())
2850        else:
2851            fp.current_factor = ''
2852
2853        fp.chi2 = self.chi2
2854        fp.main_params_to_fit = self.main_params_to_fit
2855        fp.poly_params_to_fit = self.poly_params_to_fit
2856        fp.magnet_params_to_fit = self.magnet_params_to_fit
2857        fp.kernel_module = self.kernel_module
2858
2859        # Algorithm options
2860        # fp.algorithm = self.parent.fit_options.selected_id
2861
2862        # Options tab
2863        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2864        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2865        fp.fit_options[fp.NPTS] = self.npts
2866        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2867        fp.fit_options[fp.LOG_POINTS] = self.log_points
2868        fp.fit_options[fp.WEIGHTING] = self.weighting
2869
2870        # Resolution tab
2871        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2872        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2873        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2874        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2875        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2876
2877        # TODO: add polidyspersity and magnetism
2878
2879
2880    def updateUndo(self):
2881        """
2882        Create a new state page and add it to the stack
2883        """
2884        if self.undo_supported:
2885            self.pushFitPage(self.currentState())
2886
2887    def currentState(self):
2888        """
2889        Return fit page with current state
2890        """
2891        new_page = FitPage()
2892        self.saveToFitPage(new_page)
2893
2894        return new_page
2895
2896    def pushFitPage(self, new_page):
2897        """
2898        Add a new fit page object with current state
2899        """
2900        self.page_stack.append(new_page)
2901
2902    def popFitPage(self):
2903        """
2904        Remove top fit page from stack
2905        """
2906        if self.page_stack:
2907            self.page_stack.pop()
2908
2909    def getReport(self):
2910        """
2911        Create and return HTML report with parameters and charts
2912        """
2913        index = None
2914        if self.all_data:
2915            index = self.all_data[self.data_index]
2916        report_logic = ReportPageLogic(self,
2917                                       kernel_module=self.kernel_module,
2918                                       data=self.data,
2919                                       index=index,
2920                                       model=self._model_model)
2921
2922        return report_logic.reportList()
2923
2924    def savePageState(self):
2925        """
2926        Create and serialize local PageState
2927        """
2928        from sas.sascalc.fit.pagestate import Reader
2929        model = self.kernel_module
2930
2931        # Old style PageState object
2932        state = PageState(model=model, data=self.data)
2933
2934        # Add parameter data to the state
2935        self.getCurrentFitState(state)
2936
2937        # Create the filewriter, aptly named 'Reader'
2938        state_reader = Reader(self.loadPageStateCallback)
2939        filepath = self.saveAsAnalysisFile()
2940        if filepath is None or filepath == "":
2941            return
2942        state_reader.write(filename=filepath, fitstate=state)
2943        pass
2944
2945    def saveAsAnalysisFile(self):
2946        """
2947        Show the save as... dialog and return the chosen filepath
2948        """
2949        default_name = "FitPage"+str(self.tab_id)+".fitv"
2950
2951        wildcard = "fitv files (*.fitv)"
2952        kwargs = {
2953            'caption'   : 'Save As',
2954            'directory' : default_name,
2955            'filter'    : wildcard,
2956            'parent'    : None,
2957        }
2958        # Query user for filename.
2959        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
2960        filename = filename_tuple[0]
2961        return filename
2962
2963    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
2964        """
2965        This is a callback method called from the CANSAS reader.
2966        We need the instance of this reader only for writing out a file,
2967        so there's nothing here.
2968        Until Load Analysis is implemented, that is.
2969        """
2970        pass
2971
2972    def loadPageState(self, pagestate=None):
2973        """
2974        Load the PageState object and update the current widget
2975        """
2976        pass
2977
2978    def getCurrentFitState(self, state=None):
2979        """
2980        Store current state for fit_page
2981        """
2982        # save model option
2983        #if self.model is not None:
2984        #    self.disp_list = self.getDispParamList()
2985        #    state.disp_list = copy.deepcopy(self.disp_list)
2986        #    #state.model = self.model.clone()
2987
2988        # Comboboxes
2989        state.categorycombobox = self.cbCategory.currentText()
2990        state.formfactorcombobox = self.cbModel.currentText()
2991        if self.cbStructureFactor.isEnabled():
2992            state.structurecombobox = self.cbStructureFactor.currentText()
2993        state.tcChi = self.chi2
2994
2995        state.enable2D = self.is2D
2996
2997        #state.weights = copy.deepcopy(self.weights)
2998        # save data
2999        state.data = copy.deepcopy(self.data)
3000
3001        # save plotting range
3002        state.qmin = self.q_range_min
3003        state.qmax = self.q_range_max
3004        state.npts = self.npts
3005
3006        #    self.state.enable_disp = self.enable_disp.GetValue()
3007        #    self.state.disable_disp = self.disable_disp.GetValue()
3008
3009        #    self.state.enable_smearer = \
3010        #                        copy.deepcopy(self.enable_smearer.GetValue())
3011        #    self.state.disable_smearer = \
3012        #                        copy.deepcopy(self.disable_smearer.GetValue())
3013
3014        #self.state.pinhole_smearer = \
3015        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3016        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3017        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3018        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3019        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3020        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3021
3022        p = self.model_parameters
3023        # save checkbutton state and txtcrtl values
3024        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3025        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3026
3027        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3028        #self._copy_parameters_state(self.parameters, self.state.parameters)
3029        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3030        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3031
3032    def onParameterCopy(self, format=None):
3033        """
3034        Copy current parameters into the clipboard
3035        """
3036        # run a loop over all parameters and pull out
3037        # first - regular params
3038        param_list = []
3039        def gatherParams(row):
3040            """
3041            Create list of main parameters based on _model_model
3042            """
3043            param_name = str(self._model_model.item(row, 0).text())
3044            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3045            param_value = str(self._model_model.item(row, 1).text())
3046            param_error = None
3047            column_offset = 0
3048            if self.has_error_column:
3049                param_error = str(self._model_model.item(row, 2).text())
3050                column_offset = 1
3051            param_min = str(self._model_model.item(row, 2+column_offset).text())
3052            param_max = str(self._model_model.item(row, 3+column_offset).text())
3053            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3054
3055        def gatherPolyParams(row):
3056            """
3057            Create list of polydisperse parameters based on _poly_model
3058            """
3059            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3060            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3061            param_value = str(self._poly_model.item(row, 1).text())
3062            param_error = None
3063            column_offset = 0
3064            if self.has_poly_error_column:
3065                param_error = str(self._poly_model.item(row, 2).text())
3066                column_offset = 1
3067            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3068            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3069            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3070            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3071            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3072            # width
3073            name = param_name+".width"
3074            param_list.append([name, param_checked, param_value, param_error,
3075                                param_npts, param_nsigs, param_min, param_max, param_fun])
3076
3077        def gatherMagnetParams(row):
3078            """
3079            Create list of magnetic parameters based on _magnet_model
3080            """
3081            param_name = str(self._magnet_model.item(row, 0).text())
3082            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3083            param_value = str(self._magnet_model.item(row, 1).text())
3084            param_error = None
3085            column_offset = 0
3086            if self.has_magnet_error_column:
3087                param_error = str(self._magnet_model.item(row, 2).text())
3088                column_offset = 1
3089            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3090            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3091            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3092
3093        self.iterateOverModel(gatherParams)
3094        if self.chkPolydispersity.isChecked():
3095            self.iterateOverPolyModel(gatherPolyParams)
3096        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
3097            self.iterateOverMagnetModel(gatherMagnetParams)
3098
3099        if format=="":
3100            formatted_output = FittingUtilities.formatParameters(param_list)
3101        elif format == "Excel":
3102            formatted_output = FittingUtilities.formatParametersExcel(param_list)
3103        elif format == "Latex":
3104            formatted_output = FittingUtilities.formatParametersLatex(param_list)
3105        else:
3106            raise AttributeError("Bad format specifier.")
3107
3108        # Dump formatted_output to the clipboard
3109        cb = QtWidgets.QApplication.clipboard()
3110        cb.setText(formatted_output)
3111
3112    def onParameterPaste(self):
3113        """
3114        Use the clipboard to update fit state
3115        """
3116        # Check if the clipboard contains right stuff
3117        cb = QtWidgets.QApplication.clipboard()
3118        cb_text = cb.text()
3119
3120        context = {}
3121        # put the text into dictionary
3122        lines = cb_text.split(':')
3123        if lines[0] != 'sasview_parameter_values':
3124            return False
3125        for line in lines[1:-1]:
3126            if len(line) != 0:
3127                item = line.split(',')
3128                check = item[1]
3129                name = item[0]
3130                value = item[2]
3131                # Transfer the text to content[dictionary]
3132                context[name] = [check, value]
3133
3134                # limits
3135                limit_lo = item[3]
3136                context[name].append(limit_lo)
3137                limit_hi = item[4]
3138                context[name].append(limit_hi)
3139
3140                # Polydisp
3141                if len(item) > 5:
3142                    value = item[5]
3143                    context[name].append(value)
3144                    try:
3145                        value = item[6]
3146                        context[name].append(value)
3147                        value = item[7]
3148                        context[name].append(value)
3149                    except IndexError:
3150                        pass
3151
3152        self.updateFullModel(context)
3153        self.updateFullPolyModel(context)
3154
3155    def updateFullModel(self, param_dict):
3156        """
3157        Update the model with new parameters
3158        """
3159        assert isinstance(param_dict, dict)
3160        if not dict:
3161            return
3162
3163        def updateFittedValues(row):
3164            # Utility function for main model update
3165            # internal so can use closure for param_dict
3166            param_name = str(self._model_model.item(row, 0).text())
3167            if param_name not in list(param_dict.keys()):
3168                return
3169            # checkbox state
3170            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3171            self._model_model.item(row, 0).setCheckState(param_checked)
3172
3173            # modify the param value
3174            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3175            self._model_model.item(row, 1).setText(param_repr)
3176
3177            # Potentially the error column
3178            ioffset = 0
3179            if len(param_dict[param_name])>4 and self.has_error_column:
3180                # error values are not editable - no need to update
3181                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3182                #self._model_model.item(row, 2).setText(error_repr)
3183                ioffset = 1
3184            # min/max
3185            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3186            self._model_model.item(row, 2+ioffset).setText(param_repr)
3187            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3188            self._model_model.item(row, 3+ioffset).setText(param_repr)
3189
3190        # block signals temporarily, so we don't end up
3191        # updating charts with every single model change on the end of fitting
3192        self._model_model.blockSignals(True)
3193        self.iterateOverModel(updateFittedValues)
3194        self._model_model.blockSignals(False)
3195
3196    def updateFullPolyModel(self, param_dict):
3197        """
3198        Update the polydispersity model with new parameters, create the errors column
3199        """
3200        assert isinstance(param_dict, dict)
3201        if not dict:
3202            return
3203
3204        def updateFittedValues(row):
3205            # Utility function for main model update
3206            # internal so can use closure for param_dict
3207            if row >= self._poly_model.rowCount():
3208                return
3209            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3210            if param_name not in list(param_dict.keys()):
3211                return
3212            # checkbox state
3213            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3214            self._poly_model.item(row,0).setCheckState(param_checked)
3215
3216            # modify the param value
3217            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3218            self._poly_model.item(row, 1).setText(param_repr)
3219
3220            # Potentially the error column
3221            ioffset = 0
3222            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3223                ioffset = 1
3224            # min
3225            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3226            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3227            # max
3228            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3229            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3230            # Npts
3231            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3232            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3233            # Nsigs
3234            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3235            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3236
3237            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3238            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3239
3240        # block signals temporarily, so we don't end up
3241        # updating charts with every single model change on the end of fitting
3242        self._poly_model.blockSignals(True)
3243        self.iterateOverPolyModel(updateFittedValues)
3244        self._poly_model.blockSignals(False)
3245
3246
Note: See TracBrowser for help on using the repository browser.