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

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

Replaced 'smart' plot generation with explicit plot requests on "Show Plot". SASVIEW-1018

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