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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since a486918 was a486918, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Added missed q range display formatting

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