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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 0177eb6 was a8b3e2c, checked in by ibressler, 6 years ago

SasviewModel?.get_weights(): simplified by using ParameterTable?.getitem

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