source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 557fc498

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 557fc498 was 557fc498, checked in by Piotr Rozyczko <rozyczko@…>, 19 months ago

Added GUI elements disablement/enablement on fit and calculate SASVIEW-1064

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