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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9562a6b was 9562a6b, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 18 months ago

Reset multiplicity to allow generation of correct product model.
SASVIEW-1197

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