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

ESS_GUIESS_GUI_InvariantESS_GUI_batch_fittingESS_GUI_ordering
Last change on this file since d2007a8 was d2007a8, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 2 months ago

Fitpage state copy/paste reworked. SASVIEW-1053 and SASVIEW-1196

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