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

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

Added SLD profile view. SASVIEW-1050

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