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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since ef3494b3 was ef3494b3, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 18 months ago

[CHERRY-PICK FROM 0804c9f57] simplify finding S(Q) name

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