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

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

plotPolydispersities(): use precalc. weights from SasviewModel?

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