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

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 906e0c7 was 906e0c7, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

misc fixes to polydispersity table: updated values enter main model correctly, including upon fit

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