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

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

Disable the Calculate/Plot? button while the calculation is running.
SASVIEW-1153

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