source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 305114c

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

Added a warning label to simple and complex constraint widgets,
shown whenever polydisperse parameters are constrained SASVIEW-1042

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