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

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 54492dc was 54492dc, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed issue with batch loading
Added log/log to the quick list SASVIEW-990

  • Property mode set to 100644
File size: 117.7 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        # Extract changed value.
1127        if model_column == self.lstPoly.itemDelegate().poly_parameter:
1128            # Is the parameter checked for fitting?
1129            value = item.checkState()
1130            parameter_name = parameter_name + '.width'
1131            if value == QtCore.Qt.Checked:
1132                self.parameters_to_fit.append(parameter_name)
1133            else:
1134                if parameter_name in self.parameters_to_fit:
1135                    self.parameters_to_fit.remove(parameter_name)
1136            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1137            return
1138        elif model_column in [self.lstPoly.itemDelegate().poly_min, self.lstPoly.itemDelegate().poly_max]:
1139            try:
1140                value = GuiUtils.toDouble(item.text())
1141            except TypeError:
1142                # Can't be converted properly, bring back the old value and exit
1143                return
1144
1145            current_details = self.kernel_module.details[parameter_name]
1146            current_details[model_column-1] = value
1147        elif model_column == self.lstPoly.itemDelegate().poly_function:
1148            # name of the function - just pass
1149            return
1150        else:
1151            try:
1152                value = GuiUtils.toDouble(item.text())
1153            except TypeError:
1154                # Can't be converted properly, bring back the old value and exit
1155                return
1156
1157            # Update the sasmodel
1158            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
1159            self.kernel_module.setParam(parameter_name + '.' + \
1160                                        self.lstPoly.itemDelegate().columnDict()[model_column], value)
1161
1162            # Update plot
1163            self.updateData()
1164
1165    def onMagnetModelChange(self, item):
1166        """
1167        Callback method for updating the sasmodel magnetic parameters with the GUI values
1168        """
1169        model_column = item.column()
1170        model_row = item.row()
1171        name_index = self._magnet_model.index(model_row, 0)
1172        parameter_name = str(self._magnet_model.data(name_index))
1173
1174        if model_column == 0:
1175            value = item.checkState()
1176            if value == QtCore.Qt.Checked:
1177                self.parameters_to_fit.append(parameter_name)
1178            else:
1179                if parameter_name in self.parameters_to_fit:
1180                    self.parameters_to_fit.remove(parameter_name)
1181            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1182            # Update state stack
1183            self.updateUndo()
1184            return
1185
1186        # Extract changed value.
1187        try:
1188            value = GuiUtils.toDouble(item.text())
1189        except TypeError:
1190            # Unparsable field
1191            return
1192
1193        property_index = self._magnet_model.headerData(1, model_column)-1 # Value, min, max, etc.
1194
1195        # Update the parameter value - note: this supports +/-inf as well
1196        self.kernel_module.params[parameter_name] = value
1197
1198        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1199        self.kernel_module.details[parameter_name][property_index] = value
1200
1201        # Force the chart update when actual parameters changed
1202        if model_column == 1:
1203            self.recalculatePlotData()
1204
1205        # Update state stack
1206        self.updateUndo()
1207
1208    def onHelp(self):
1209        """
1210        Show the "Fitting" section of help
1211        """
1212        tree_location = "/user/qtgui/Perspectives/Fitting/"
1213
1214        # Actual file will depend on the current tab
1215        tab_id = self.tabFitting.currentIndex()
1216        helpfile = "fitting.html"
1217        if tab_id == 0:
1218            helpfile = "fitting_help.html"
1219        elif tab_id == 1:
1220            helpfile = "residuals_help.html"
1221        elif tab_id == 2:
1222            helpfile = "resolution.html"
1223        elif tab_id == 3:
1224            helpfile = "pd/polydispersity.html"
1225        elif tab_id == 4:
1226            helpfile = "magnetism/magnetism.html"
1227        help_location = tree_location + helpfile
1228
1229        self.showHelp(help_location)
1230
1231    def showHelp(self, url):
1232        """
1233        Calls parent's method for opening an HTML page
1234        """
1235        self.parent.showHelp(url)
1236
1237    def onDisplayMagneticAngles(self):
1238        """
1239        Display a simple image showing direction of magnetic angles
1240        """
1241        self.magneticAnglesWidget.show()
1242
1243    def onFit(self):
1244        """
1245        Perform fitting on the current data
1246        """
1247        if self.fit_started:
1248            self.stopFit()
1249            return
1250
1251        # initialize fitter constants
1252        fit_id = 0
1253        handler = None
1254        batch_inputs = {}
1255        batch_outputs = {}
1256        #---------------------------------
1257        if LocalConfig.USING_TWISTED:
1258            handler = None
1259            updater = None
1260        else:
1261            handler = ConsoleUpdate(parent=self.parent,
1262                                    manager=self,
1263                                    improvement_delta=0.1)
1264            updater = handler.update_fit
1265
1266        # Prepare the fitter object
1267        try:
1268            fitters, _ = self.prepareFitters()
1269        except ValueError as ex:
1270            # This should not happen! GUI explicitly forbids this situation
1271            self.communicate.statusBarUpdateSignal.emit(str(ex))
1272            return
1273
1274        # keep local copy of kernel parameters, as they will change during the update
1275        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1276
1277        # Create the fitting thread, based on the fitter
1278        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
1279
1280        self.calc_fit = FitThread(handler=handler,
1281                            fn=fitters,
1282                            batch_inputs=batch_inputs,
1283                            batch_outputs=batch_outputs,
1284                            page_id=[[self.page_id]],
1285                            updatefn=updater,
1286                            completefn=completefn,
1287                            reset_flag=self.is_chain_fitting)
1288
1289        if LocalConfig.USING_TWISTED:
1290            # start the trhrhread with twisted
1291            calc_thread = threads.deferToThread(self.calc_fit.compute)
1292            calc_thread.addCallback(completefn)
1293            calc_thread.addErrback(self.fitFailed)
1294        else:
1295            # Use the old python threads + Queue
1296            self.calc_fit.queue()
1297            self.calc_fit.ready(2.5)
1298
1299        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
1300        self.fit_started = True
1301        # Disable some elements
1302        self.setFittingStarted()
1303
1304    def stopFit(self):
1305        """
1306        Attempt to stop the fitting thread
1307        """
1308        if self.calc_fit is None or not self.calc_fit.isrunning():
1309            return
1310        self.calc_fit.stop()
1311        #self.fit_started=False
1312        #re-enable the Fit button
1313        self.setFittingStopped()
1314
1315        msg = "Fitting cancelled."
1316        self.communicate.statusBarUpdateSignal.emit(msg)
1317
1318    def updateFit(self):
1319        """
1320        """
1321        print("UPDATE FIT")
1322        pass
1323
1324    def fitFailed(self, reason):
1325        """
1326        """
1327        self.setFittingStopped()
1328        msg = "Fitting failed with: "+ str(reason)
1329        self.communicate.statusBarUpdateSignal.emit(msg)
1330
1331    def batchFittingCompleted(self, result):
1332        """
1333        Send the finish message from calculate threads to main thread
1334        """
1335        if result is None:
1336            result = tuple()
1337        self.batchFittingFinishedSignal.emit(result)
1338
1339    def batchFitComplete(self, result):
1340        """
1341        Receive and display batch fitting results
1342        """
1343        #re-enable the Fit button
1344        self.setFittingStopped()
1345
1346        if len(result) == 0:
1347            msg = "Fitting failed."
1348            self.communicate.statusBarUpdateSignal.emit(msg)
1349            return
1350
1351        # Show the grid panel
1352        self.communicate.sendDataToGridSignal.emit(result[0])
1353
1354        elapsed = result[1]
1355        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1356        self.communicate.statusBarUpdateSignal.emit(msg)
1357
1358        # Run over the list of results and update the items
1359        for res_index, res_list in enumerate(result[0]):
1360            # results
1361            res = res_list[0]
1362            param_dict = self.paramDictFromResults(res)
1363
1364            # create local kernel_module
1365            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1366            # pull out current data
1367            data = self._logic[res_index].data
1368
1369            # Switch indexes
1370            self.onSelectBatchFilename(res_index)
1371
1372            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1373            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1374
1375        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1376        if self.kernel_module is not None:
1377            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1378
1379    def paramDictFromResults(self, results):
1380        """
1381        Given the fit results structure, pull out optimized parameters and return them as nicely
1382        formatted dict
1383        """
1384        if results.fitness is None or \
1385            not np.isfinite(results.fitness) or \
1386            np.any(results.pvec is None) or \
1387            not np.all(np.isfinite(results.pvec)):
1388            msg = "Fitting did not converge!"
1389            self.communicate.statusBarUpdateSignal.emit(msg)
1390            msg += results.mesg
1391            logging.error(msg)
1392            return
1393
1394        param_list = results.param_list # ['radius', 'radius.width']
1395        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1396        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1397        params_and_errors = list(zip(param_values, param_stderr))
1398        param_dict = dict(zip(param_list, params_and_errors))
1399
1400        return param_dict
1401
1402    def fittingCompleted(self, result):
1403        """
1404        Send the finish message from calculate threads to main thread
1405        """
1406        if result is None:
1407            result = tuple()
1408        self.fittingFinishedSignal.emit(result)
1409
1410    def fitComplete(self, result):
1411        """
1412        Receive and display fitting results
1413        "result" is a tuple of actual result list and the fit time in seconds
1414        """
1415        #re-enable the Fit button
1416        self.setFittingStopped()
1417
1418        if len(result) == 0:
1419            msg = "Fitting failed."
1420            self.communicate.statusBarUpdateSignal.emit(msg)
1421            return
1422
1423        res_list = result[0][0]
1424        res = res_list[0]
1425        self.chi2 = res.fitness
1426        param_dict = self.paramDictFromResults(res)
1427
1428        if param_dict is None:
1429            return
1430
1431        elapsed = result[1]
1432        if self.calc_fit._interrupting:
1433            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1434            logging.warning("\n"+msg+"\n")
1435        else:
1436            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1437        self.communicate.statusBarUpdateSignal.emit(msg)
1438
1439        # Dictionary of fitted parameter: value, error
1440        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1441        self.updateModelFromList(param_dict)
1442
1443        self.updatePolyModelFromList(param_dict)
1444
1445        self.updateMagnetModelFromList(param_dict)
1446
1447        # update charts
1448        self.onPlot()
1449
1450        # Read only value - we can get away by just printing it here
1451        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1452        self.lblChi2Value.setText(chi2_repr)
1453
1454    def prepareFitters(self, fitter=None, fit_id=0):
1455        """
1456        Prepare the Fitter object for use in fitting
1457        """
1458        # fitter = None -> single/batch fitting
1459        # fitter = Fit() -> simultaneous fitting
1460
1461        # Data going in
1462        data = self.logic.data
1463        model = self.kernel_module
1464        qmin = self.q_range_min
1465        qmax = self.q_range_max
1466        params_to_fit = self.parameters_to_fit
1467        if not params_to_fit:
1468            raise ValueError('Fitting requires at least one parameter to optimize.')
1469
1470        # Potential smearing added
1471        # Remember that smearing_min/max can be None ->
1472        # deal with it until Python gets discriminated unions
1473        self.addWeightingToData(data)
1474
1475        # Get the constraints.
1476        constraints = self.getComplexConstraintsForModel()
1477        if fitter is None:
1478            # For single fits - check for inter-model constraints
1479            constraints = self.getConstraintsForFitting()
1480
1481        smearer = self.smearing_widget.smearer()
1482        handler = None
1483        batch_inputs = {}
1484        batch_outputs = {}
1485
1486        fitters = []
1487        for fit_index in self.all_data:
1488            fitter_single = Fit() if fitter is None else fitter
1489            data = GuiUtils.dataFromItem(fit_index)
1490            # Potential weights added directly to data
1491            self.addWeightingToData(data)
1492            try:
1493                fitter_single.set_model(model, fit_id, params_to_fit, data=data,
1494                             constraints=constraints)
1495            except ValueError as ex:
1496                raise ValueError("Setting model parameters failed with: %s" % ex)
1497
1498            qmin, qmax, _ = self.logic.computeRangeFromData(data)
1499            fitter_single.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
1500                            qmax=qmax)
1501            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1502            if fitter is None:
1503                # Assign id to the new fitter only
1504                fitter_single.fitter_id = [self.page_id]
1505            fit_id += 1
1506            fitters.append(fitter_single)
1507
1508        return fitters, fit_id
1509
1510    def iterateOverModel(self, func):
1511        """
1512        Take func and throw it inside the model row loop
1513        """
1514        for row_i in range(self._model_model.rowCount()):
1515            func(row_i)
1516
1517    def updateModelFromList(self, param_dict):
1518        """
1519        Update the model with new parameters, create the errors column
1520        """
1521        assert isinstance(param_dict, dict)
1522        if not dict:
1523            return
1524
1525        def updateFittedValues(row):
1526            # Utility function for main model update
1527            # internal so can use closure for param_dict
1528            param_name = str(self._model_model.item(row, 0).text())
1529            if param_name not in list(param_dict.keys()):
1530                return
1531            # modify the param value
1532            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1533            self._model_model.item(row, 1).setText(param_repr)
1534            if self.has_error_column:
1535                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1536                self._model_model.item(row, 2).setText(error_repr)
1537
1538        def updatePolyValues(row):
1539            # Utility function for updateof polydispersity part of the main model
1540            param_name = str(self._model_model.item(row, 0).text())+'.width'
1541            if param_name not in list(param_dict.keys()):
1542                return
1543            # modify the param value
1544            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1545            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1546
1547        def createErrorColumn(row):
1548            # Utility function for error column update
1549            item = QtGui.QStandardItem()
1550            def createItem(param_name):
1551                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1552                item.setText(error_repr)
1553            def curr_param():
1554                return str(self._model_model.item(row, 0).text())
1555
1556            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1557
1558            error_column.append(item)
1559
1560        # block signals temporarily, so we don't end up
1561        # updating charts with every single model change on the end of fitting
1562        self._model_model.blockSignals(True)
1563        self.iterateOverModel(updateFittedValues)
1564        self.iterateOverModel(updatePolyValues)
1565        self._model_model.blockSignals(False)
1566
1567        if self.has_error_column:
1568            return
1569
1570        error_column = []
1571        self.lstParams.itemDelegate().addErrorColumn()
1572        self.iterateOverModel(createErrorColumn)
1573
1574        # switch off reponse to model change
1575        self._model_model.insertColumn(2, error_column)
1576        FittingUtilities.addErrorHeadersToModel(self._model_model)
1577        # Adjust the table cells width.
1578        # TODO: find a way to dynamically adjust column width while resized expanding
1579        self.lstParams.resizeColumnToContents(0)
1580        self.lstParams.resizeColumnToContents(4)
1581        self.lstParams.resizeColumnToContents(5)
1582        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1583
1584        self.has_error_column = True
1585
1586    def iterateOverPolyModel(self, func):
1587        """
1588        Take func and throw it inside the poly model row loop
1589        """
1590        for row_i in range(self._poly_model.rowCount()):
1591            func(row_i)
1592
1593    def updatePolyModelFromList(self, param_dict):
1594        """
1595        Update the polydispersity model with new parameters, create the errors column
1596        """
1597        assert isinstance(param_dict, dict)
1598        if not dict:
1599            return
1600
1601        def updateFittedValues(row_i):
1602            # Utility function for main model update
1603            # internal so can use closure for param_dict
1604            if row_i >= self._poly_model.rowCount():
1605                return
1606            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1607            if param_name not in list(param_dict.keys()):
1608                return
1609            # modify the param value
1610            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1611            self._poly_model.item(row_i, 1).setText(param_repr)
1612            if self.has_poly_error_column:
1613                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1614                self._poly_model.item(row_i, 2).setText(error_repr)
1615
1616
1617        def createErrorColumn(row_i):
1618            # Utility function for error column update
1619            if row_i >= self._poly_model.rowCount():
1620                return
1621            item = QtGui.QStandardItem()
1622
1623            def createItem(param_name):
1624                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1625                item.setText(error_repr)
1626
1627            def poly_param():
1628                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1629
1630            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1631
1632            error_column.append(item)
1633
1634        # block signals temporarily, so we don't end up
1635        # updating charts with every single model change on the end of fitting
1636        self._poly_model.blockSignals(True)
1637        self.iterateOverPolyModel(updateFittedValues)
1638        self._poly_model.blockSignals(False)
1639
1640        if self.has_poly_error_column:
1641            return
1642
1643        self.lstPoly.itemDelegate().addErrorColumn()
1644        error_column = []
1645        self.iterateOverPolyModel(createErrorColumn)
1646
1647        # switch off reponse to model change
1648        self._poly_model.blockSignals(True)
1649        self._poly_model.insertColumn(2, error_column)
1650        self._poly_model.blockSignals(False)
1651        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1652
1653        self.has_poly_error_column = True
1654
1655    def iterateOverMagnetModel(self, func):
1656        """
1657        Take func and throw it inside the magnet model row loop
1658        """
1659        for row_i in range(self._magnet_model.rowCount()):
1660            func(row_i)
1661
1662    def updateMagnetModelFromList(self, param_dict):
1663        """
1664        Update the magnetic model with new parameters, create the errors column
1665        """
1666        assert isinstance(param_dict, dict)
1667        if not dict:
1668            return
1669        if self._magnet_model.rowCount() == 0:
1670            return
1671
1672        def updateFittedValues(row):
1673            # Utility function for main model update
1674            # internal so can use closure for param_dict
1675            if self._magnet_model.item(row, 0) is None:
1676                return
1677            param_name = str(self._magnet_model.item(row, 0).text())
1678            if param_name not in list(param_dict.keys()):
1679                return
1680            # modify the param value
1681            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1682            self._magnet_model.item(row, 1).setText(param_repr)
1683            if self.has_magnet_error_column:
1684                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1685                self._magnet_model.item(row, 2).setText(error_repr)
1686
1687        def createErrorColumn(row):
1688            # Utility function for error column update
1689            item = QtGui.QStandardItem()
1690            def createItem(param_name):
1691                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1692                item.setText(error_repr)
1693            def curr_param():
1694                return str(self._magnet_model.item(row, 0).text())
1695
1696            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1697
1698            error_column.append(item)
1699
1700        # block signals temporarily, so we don't end up
1701        # updating charts with every single model change on the end of fitting
1702        self._magnet_model.blockSignals(True)
1703        self.iterateOverMagnetModel(updateFittedValues)
1704        self._magnet_model.blockSignals(False)
1705
1706        if self.has_magnet_error_column:
1707            return
1708
1709        self.lstMagnetic.itemDelegate().addErrorColumn()
1710        error_column = []
1711        self.iterateOverMagnetModel(createErrorColumn)
1712
1713        # switch off reponse to model change
1714        self._magnet_model.blockSignals(True)
1715        self._magnet_model.insertColumn(2, error_column)
1716        self._magnet_model.blockSignals(False)
1717        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1718
1719        self.has_magnet_error_column = True
1720
1721    def onPlot(self):
1722        """
1723        Plot the current set of data
1724        """
1725        # Regardless of previous state, this should now be `plot show` functionality only
1726        self.cmdPlot.setText("Show Plot")
1727        # Force data recalculation so existing charts are updated
1728        self.recalculatePlotData()
1729        self.showPlot()
1730
1731    def onSmearingOptionsUpdate(self):
1732        """
1733        React to changes in the smearing widget
1734        """
1735        self.calculateQGridForModel()
1736
1737    def recalculatePlotData(self):
1738        """
1739        Generate a new dataset for model
1740        """
1741        if not self.data_is_loaded:
1742            self.createDefaultDataset()
1743        self.calculateQGridForModel()
1744
1745    def showPlot(self):
1746        """
1747        Show the current plot in MPL
1748        """
1749        # Show the chart if ready
1750        data_to_show = self.data if self.data_is_loaded else self.model_data
1751        if data_to_show is not None:
1752            self.communicate.plotRequestedSignal.emit([data_to_show])
1753
1754    def onOptionsUpdate(self):
1755        """
1756        Update local option values and replot
1757        """
1758        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1759            self.options_widget.state()
1760        # set Q range labels on the main tab
1761        self.lblMinRangeDef.setText(str(self.q_range_min))
1762        self.lblMaxRangeDef.setText(str(self.q_range_max))
1763        self.recalculatePlotData()
1764
1765    def setDefaultStructureCombo(self):
1766        """
1767        Fill in the structure factors combo box with defaults
1768        """
1769        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1770        factors = [factor[0] for factor in structure_factor_list]
1771        factors.insert(0, STRUCTURE_DEFAULT)
1772        self.cbStructureFactor.clear()
1773        self.cbStructureFactor.addItems(sorted(factors))
1774
1775    def createDefaultDataset(self):
1776        """
1777        Generate default Dataset 1D/2D for the given model
1778        """
1779        # Create default datasets if no data passed
1780        if self.is2D:
1781            qmax = self.q_range_max/np.sqrt(2)
1782            qstep = self.npts
1783            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1784            return
1785        elif self.log_points:
1786            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1787            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1788            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1789        else:
1790            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1791                                   num=self.npts, endpoint=True)
1792        self.logic.createDefault1dData(interval, self.tab_id)
1793
1794    def readCategoryInfo(self):
1795        """
1796        Reads the categories in from file
1797        """
1798        self.master_category_dict = defaultdict(list)
1799        self.by_model_dict = defaultdict(list)
1800        self.model_enabled_dict = defaultdict(bool)
1801
1802        categorization_file = CategoryInstaller.get_user_file()
1803        if not os.path.isfile(categorization_file):
1804            categorization_file = CategoryInstaller.get_default_file()
1805        with open(categorization_file, 'rb') as cat_file:
1806            self.master_category_dict = json.load(cat_file)
1807            self.regenerateModelDict()
1808
1809        # Load the model dict
1810        models = load_standard_models()
1811        for model in models:
1812            self.models[model.name] = model
1813
1814        self.readCustomCategoryInfo()
1815
1816    def readCustomCategoryInfo(self):
1817        """
1818        Reads the custom model category
1819        """
1820        #Looking for plugins
1821        self.plugins = list(self.custom_models.values())
1822        plugin_list = []
1823        for name, plug in self.custom_models.items():
1824            self.models[name] = plug
1825            plugin_list.append([name, True])
1826        self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1827
1828    def regenerateModelDict(self):
1829        """
1830        Regenerates self.by_model_dict which has each model name as the
1831        key and the list of categories belonging to that model
1832        along with the enabled mapping
1833        """
1834        self.by_model_dict = defaultdict(list)
1835        for category in self.master_category_dict:
1836            for (model, enabled) in self.master_category_dict[category]:
1837                self.by_model_dict[model].append(category)
1838                self.model_enabled_dict[model] = enabled
1839
1840    def addBackgroundToModel(self, model):
1841        """
1842        Adds background parameter with default values to the model
1843        """
1844        assert isinstance(model, QtGui.QStandardItemModel)
1845        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1846        FittingUtilities.addCheckedListToModel(model, checked_list)
1847        last_row = model.rowCount()-1
1848        model.item(last_row, 0).setEditable(False)
1849        model.item(last_row, 4).setEditable(False)
1850
1851    def addScaleToModel(self, model):
1852        """
1853        Adds scale parameter with default values to the model
1854        """
1855        assert isinstance(model, QtGui.QStandardItemModel)
1856        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1857        FittingUtilities.addCheckedListToModel(model, checked_list)
1858        last_row = model.rowCount()-1
1859        model.item(last_row, 0).setEditable(False)
1860        model.item(last_row, 4).setEditable(False)
1861
1862    def addWeightingToData(self, data):
1863        """
1864        Adds weighting contribution to fitting data
1865        """
1866        # Send original data for weighting
1867        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1868        if self.is2D:
1869            data.err_data = weight
1870        else:
1871            data.dy = weight
1872        pass
1873
1874    def updateQRange(self):
1875        """
1876        Updates Q Range display
1877        """
1878        if self.data_is_loaded:
1879            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1880        # set Q range labels on the main tab
1881        self.lblMinRangeDef.setText(str(self.q_range_min))
1882        self.lblMaxRangeDef.setText(str(self.q_range_max))
1883        # set Q range labels on the options tab
1884        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1885
1886    def SASModelToQModel(self, model_name, structure_factor=None):
1887        """
1888        Setting model parameters into table based on selected category
1889        """
1890        # Crete/overwrite model items
1891        self._model_model.clear()
1892
1893        # First, add parameters from the main model
1894        if model_name is not None:
1895            self.fromModelToQModel(model_name)
1896
1897        # Then, add structure factor derived parameters
1898        if structure_factor is not None and structure_factor != "None":
1899            if model_name is None:
1900                # Instantiate the current sasmodel for SF-only models
1901                self.kernel_module = self.models[structure_factor]()
1902            self.fromStructureFactorToQModel(structure_factor)
1903        else:
1904            # Allow the SF combobox visibility for the given sasmodel
1905            self.enableStructureFactorControl(structure_factor)
1906
1907        # Then, add multishells
1908        if model_name is not None:
1909            # Multishell models need additional treatment
1910            self.addExtraShells()
1911
1912        # Add polydispersity to the model
1913        self.setPolyModel()
1914        # Add magnetic parameters to the model
1915        self.setMagneticModel()
1916
1917        # Adjust the table cells width
1918        self.lstParams.resizeColumnToContents(0)
1919        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1920
1921        # Now we claim the model has been loaded
1922        self.model_is_loaded = True
1923        # Change the model name to a monicker
1924        self.kernel_module.name = self.modelName()
1925        # Update the smearing tab
1926        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
1927
1928        # (Re)-create headers
1929        FittingUtilities.addHeadersToModel(self._model_model)
1930        self.lstParams.header().setFont(self.boldFont)
1931
1932        # Update Q Ranges
1933        self.updateQRange()
1934
1935    def fromModelToQModel(self, model_name):
1936        """
1937        Setting model parameters into QStandardItemModel based on selected _model_
1938        """
1939        name = model_name
1940        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
1941            # custom kernel load requires full path
1942            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
1943        try:
1944            kernel_module = generate.load_kernel_module(name)
1945        except ModuleNotFoundError:
1946            # maybe it's a recategorised custom model?
1947            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
1948            # If this rises, it's a valid problem.
1949            kernel_module = generate.load_kernel_module(name)
1950
1951        if hasattr(kernel_module, 'parameters'):
1952            # built-in and custom models
1953            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1954
1955        elif hasattr(kernel_module, 'model_info'):
1956            # for sum/multiply models
1957            self.model_parameters = kernel_module.model_info.parameters
1958
1959        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
1960            # this probably won't work if there's no model_info, but just in case
1961            self.model_parameters = kernel_module.Model._model_info.parameters
1962        else:
1963            # no parameters - default to blank table
1964            msg = "No parameters found in model '{}'.".format(model_name)
1965            logger.warning(msg)
1966            self.model_parameters = modelinfo.ParameterTable([])
1967
1968        # Instantiate the current sasmodel
1969        self.kernel_module = self.models[model_name]()
1970
1971        # Explicitly add scale and background with default values
1972        temp_undo_state = self.undo_supported
1973        self.undo_supported = False
1974        self.addScaleToModel(self._model_model)
1975        self.addBackgroundToModel(self._model_model)
1976        self.undo_supported = temp_undo_state
1977
1978        self.shell_names = self.shellNamesList()
1979
1980        # Update the QModel
1981        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1982
1983        for row in new_rows:
1984            self._model_model.appendRow(row)
1985        # Update the counter used for multishell display
1986        self._last_model_row = self._model_model.rowCount()
1987
1988    def fromStructureFactorToQModel(self, structure_factor):
1989        """
1990        Setting model parameters into QStandardItemModel based on selected _structure factor_
1991        """
1992        structure_module = generate.load_kernel_module(structure_factor)
1993        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1994
1995        structure_kernel = self.models[structure_factor]()
1996        form_kernel = self.kernel_module
1997
1998        self.kernel_module = MultiplicationModel(form_kernel, structure_kernel)
1999
2000        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
2001        for row in new_rows:
2002            self._model_model.appendRow(row)
2003            # disable fitting of parameters not listed in self.kernel_module (probably radius_effective)
2004            if row[0].text() not in self.kernel_module.params.keys():
2005                row_num = self._model_model.rowCount() - 1
2006                FittingUtilities.markParameterDisabled(self._model_model, row_num)
2007
2008        # Update the counter used for multishell display
2009        self._last_model_row = self._model_model.rowCount()
2010
2011    def onMainParamsChange(self, item):
2012        """
2013        Callback method for updating the sasmodel parameters with the GUI values
2014        """
2015        model_column = item.column()
2016
2017        if model_column == 0:
2018            self.checkboxSelected(item)
2019            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
2020            # Update state stack
2021            self.updateUndo()
2022            return
2023
2024        model_row = item.row()
2025        name_index = self._model_model.index(model_row, 0)
2026
2027        # Extract changed value.
2028        try:
2029            value = GuiUtils.toDouble(item.text())
2030        except TypeError:
2031            # Unparsable field
2032            return
2033
2034        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
2035
2036        # Update the parameter value - note: this supports +/-inf as well
2037        self.kernel_module.params[parameter_name] = value
2038
2039        # Update the parameter value - note: this supports +/-inf as well
2040        param_column = self.lstParams.itemDelegate().param_value
2041        min_column = self.lstParams.itemDelegate().param_min
2042        max_column = self.lstParams.itemDelegate().param_max
2043        if model_column == param_column:
2044            self.kernel_module.setParam(parameter_name, value)
2045        elif model_column == min_column:
2046            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2047            self.kernel_module.details[parameter_name][1] = value
2048        elif model_column == max_column:
2049            self.kernel_module.details[parameter_name][2] = value
2050        else:
2051            # don't update the chart
2052            return
2053
2054        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2055        # TODO: multishell params in self.kernel_module.details[??] = value
2056
2057        # Force the chart update when actual parameters changed
2058        if model_column == 1:
2059            self.recalculatePlotData()
2060
2061        # Update state stack
2062        self.updateUndo()
2063
2064    def isCheckable(self, row):
2065        return self._model_model.item(row, 0).isCheckable()
2066
2067    def checkboxSelected(self, item):
2068        # Assure we're dealing with checkboxes
2069        if not item.isCheckable():
2070            return
2071        status = item.checkState()
2072
2073        # If multiple rows selected - toggle all of them, filtering uncheckable
2074        # Switch off signaling from the model to avoid recursion
2075        self._model_model.blockSignals(True)
2076        # Convert to proper indices and set requested enablement
2077        self.setParameterSelection(status)
2078        #[self._model_model.item(row, 0).setCheckState(status) for row in self.selectedParameters()]
2079        self._model_model.blockSignals(False)
2080
2081        # update the list of parameters to fit
2082        main_params = self.checkedListFromModel(self._model_model)
2083        poly_params = self.checkedListFromModel(self._poly_model)
2084        magnet_params = self.checkedListFromModel(self._magnet_model)
2085
2086        # Retrieve poly params names
2087        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
2088
2089        self.parameters_to_fit = main_params + poly_params + magnet_params
2090
2091    def checkedListFromModel(self, model):
2092        """
2093        Returns list of checked parameters for given model
2094        """
2095        def isChecked(row):
2096            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2097
2098        return [str(model.item(row_index, 0).text())
2099                for row_index in range(model.rowCount())
2100                if isChecked(row_index)]
2101
2102    def createNewIndex(self, fitted_data):
2103        """
2104        Create a model or theory index with passed Data1D/Data2D
2105        """
2106        if self.data_is_loaded:
2107            if not fitted_data.name:
2108                name = self.nameForFittedData(self.data.filename)
2109                fitted_data.title = name
2110                fitted_data.name = name
2111                fitted_data.filename = name
2112                fitted_data.symbol = "Line"
2113            self.updateModelIndex(fitted_data)
2114        else:
2115            if not fitted_data.name:
2116                name = self.nameForFittedData(self.kernel_module.id)
2117            else:
2118                name = fitted_data.name
2119            fitted_data.title = name
2120            fitted_data.filename = name
2121            fitted_data.symbol = "Line"
2122            self.createTheoryIndex(fitted_data)
2123
2124    def updateModelIndex(self, fitted_data):
2125        """
2126        Update a QStandardModelIndex containing model data
2127        """
2128        name = self.nameFromData(fitted_data)
2129        # Make this a line if no other defined
2130        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2131            fitted_data.symbol = 'Line'
2132        # Notify the GUI manager so it can update the main model in DataExplorer
2133        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2134
2135    def createTheoryIndex(self, fitted_data):
2136        """
2137        Create a QStandardModelIndex containing model data
2138        """
2139        name = self.nameFromData(fitted_data)
2140        # Notify the GUI manager so it can create the theory model in DataExplorer
2141        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2142        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
2143
2144    def nameFromData(self, fitted_data):
2145        """
2146        Return name for the dataset. Terribly impure function.
2147        """
2148        if fitted_data.name is None:
2149            name = self.nameForFittedData(self.logic.data.filename)
2150            fitted_data.title = name
2151            fitted_data.name = name
2152            fitted_data.filename = name
2153        else:
2154            name = fitted_data.name
2155        return name
2156
2157    def methodCalculateForData(self):
2158        '''return the method for data calculation'''
2159        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2160
2161    def methodCompleteForData(self):
2162        '''return the method for result parsin on calc complete '''
2163        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2164
2165    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2166        """
2167        Wrapper for Calc1D/2D calls
2168        """
2169        if data is None:
2170            data = self.data
2171        if model is None:
2172            model = self.kernel_module
2173        if completefn is None:
2174            completefn = self.methodCompleteForData()
2175        smearer = self.smearing_widget.smearer()
2176        # Awful API to a backend method.
2177        calc_thread = self.methodCalculateForData()(data=data,
2178                                               model=model,
2179                                               page_id=0,
2180                                               qmin=self.q_range_min,
2181                                               qmax=self.q_range_max,
2182                                               smearer=smearer,
2183                                               state=None,
2184                                               weight=None,
2185                                               fid=None,
2186                                               toggle_mode_on=False,
2187                                               completefn=completefn,
2188                                               update_chisqr=True,
2189                                               exception_handler=self.calcException,
2190                                               source=None)
2191        if use_threads:
2192            if LocalConfig.USING_TWISTED:
2193                # start the thread with twisted
2194                thread = threads.deferToThread(calc_thread.compute)
2195                thread.addCallback(completefn)
2196                thread.addErrback(self.calculateDataFailed)
2197            else:
2198                # Use the old python threads + Queue
2199                calc_thread.queue()
2200                calc_thread.ready(2.5)
2201        else:
2202            results = calc_thread.compute()
2203            completefn(results)
2204
2205    def calculateQGridForModel(self):
2206        """
2207        Prepare the fitting data object, based on current ModelModel
2208        """
2209        if self.kernel_module is None:
2210            return
2211        self.calculateQGridForModelExt()
2212
2213    def calculateDataFailed(self, reason):
2214        """
2215        Thread returned error
2216        """
2217        print("Calculate Data failed with ", reason)
2218
2219    def completed1D(self, return_data):
2220        self.Calc1DFinishedSignal.emit(return_data)
2221
2222    def completed2D(self, return_data):
2223        self.Calc2DFinishedSignal.emit(return_data)
2224
2225    def complete1D(self, return_data):
2226        """
2227        Plot the current 1D data
2228        """
2229        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2230        residuals = self.calculateResiduals(fitted_data)
2231        self.model_data = fitted_data
2232
2233        new_plots = [fitted_data, residuals]
2234
2235        # Create plots for intermediate product data
2236        pq_data, sq_data = self.logic.new1DProductPlots(return_data, self.tab_id)
2237        if pq_data is not None:
2238            pq_data.symbol = "Line"
2239            self.createNewIndex(pq_data)
2240            new_plots.append(pq_data)
2241        if sq_data is not None:
2242            sq_data.symbol = "Line"
2243            self.createNewIndex(sq_data)
2244            new_plots.append(sq_data)
2245
2246        if self.data_is_loaded:
2247            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2248
2249    def complete2D(self, return_data):
2250        """
2251        Plot the current 2D data
2252        """
2253        fitted_data = self.logic.new2DPlot(return_data)
2254        self.calculateResiduals(fitted_data)
2255        self.model_data = fitted_data
2256
2257    def calculateResiduals(self, fitted_data):
2258        """
2259        Calculate and print Chi2 and display chart of residuals
2260        """
2261        # Create a new index for holding data
2262        fitted_data.symbol = "Line"
2263
2264        # Modify fitted_data with weighting
2265        self.addWeightingToData(fitted_data)
2266
2267        self.createNewIndex(fitted_data)
2268        # Calculate difference between return_data and logic.data
2269        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
2270        # Update the control
2271        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2272        self.lblChi2Value.setText(chi2_repr)
2273
2274        self.communicate.plotUpdateSignal.emit([fitted_data])
2275
2276        # Plot residuals if actual data
2277        if not self.data_is_loaded:
2278            return
2279
2280        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
2281        residuals_plot.id = "Residual " + residuals_plot.id
2282        self.createNewIndex(residuals_plot)
2283        return residuals_plot
2284
2285    def onCategoriesChanged(self):
2286            """
2287            Reload the category/model comboboxes
2288            """
2289            # Store the current combo indices
2290            current_cat = self.cbCategory.currentText()
2291            current_model = self.cbModel.currentText()
2292
2293            # reread the category file and repopulate the combo
2294            self.cbCategory.blockSignals(True)
2295            self.cbCategory.clear()
2296            self.readCategoryInfo()
2297            self.initializeCategoryCombo()
2298
2299            # Scroll back to the original index in Categories
2300            new_index = self.cbCategory.findText(current_cat)
2301            if new_index != -1:
2302                self.cbCategory.setCurrentIndex(new_index)
2303            self.cbCategory.blockSignals(False)
2304            # ...and in the Models
2305            self.cbModel.blockSignals(True)
2306            new_index = self.cbModel.findText(current_model)
2307            if new_index != -1:
2308                self.cbModel.setCurrentIndex(new_index)
2309            self.cbModel.blockSignals(False)
2310
2311            return
2312
2313    def calcException(self, etype, value, tb):
2314        """
2315        Thread threw an exception.
2316        """
2317        # TODO: remimplement thread cancellation
2318        logging.error("".join(traceback.format_exception(etype, value, tb)))
2319
2320    def setTableProperties(self, table):
2321        """
2322        Setting table properties
2323        """
2324        # Table properties
2325        table.verticalHeader().setVisible(False)
2326        table.setAlternatingRowColors(True)
2327        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2328        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2329        table.resizeColumnsToContents()
2330
2331        # Header
2332        header = table.horizontalHeader()
2333        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2334        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2335
2336        # Qt5: the following 2 lines crash - figure out why!
2337        # Resize column 0 and 7 to content
2338        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2339        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2340
2341    def setPolyModel(self):
2342        """
2343        Set polydispersity values
2344        """
2345        if not self.model_parameters:
2346            return
2347        self._poly_model.clear()
2348
2349        [self.setPolyModelParameters(i, param) for i, param in \
2350            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
2351        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2352
2353    def setPolyModelParameters(self, i, param):
2354        """
2355        Standard of multishell poly parameter driver
2356        """
2357        param_name = param.name
2358        # see it the parameter is multishell
2359        if '[' in param.name:
2360            # Skip empty shells
2361            if self.current_shell_displayed == 0:
2362                return
2363            else:
2364                # Create as many entries as current shells
2365                for ishell in range(1, self.current_shell_displayed+1):
2366                    # Remove [n] and add the shell numeral
2367                    name = param_name[0:param_name.index('[')] + str(ishell)
2368                    self.addNameToPolyModel(i, name)
2369        else:
2370            # Just create a simple param entry
2371            self.addNameToPolyModel(i, param_name)
2372
2373    def addNameToPolyModel(self, i, param_name):
2374        """
2375        Creates a checked row in the poly model with param_name
2376        """
2377        # Polydisp. values from the sasmodel
2378        width = self.kernel_module.getParam(param_name + '.width')
2379        npts = self.kernel_module.getParam(param_name + '.npts')
2380        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2381        _, min, max = self.kernel_module.details[param_name]
2382
2383        # Construct a row with polydisp. related variable.
2384        # This will get added to the polydisp. model
2385        # Note: last argument needs extra space padding for decent display of the control
2386        checked_list = ["Distribution of " + param_name, str(width),
2387                        str(min), str(max),
2388                        str(npts), str(nsigs), "gaussian      ",'']
2389        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2390
2391        # All possible polydisp. functions as strings in combobox
2392        func = QtWidgets.QComboBox()
2393        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2394        # Set the default index
2395        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2396        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2397        self.lstPoly.setIndexWidget(ind, func)
2398        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2399
2400    def onPolyFilenameChange(self, row_index):
2401        """
2402        Respond to filename_updated signal from the delegate
2403        """
2404        # For the given row, invoke the "array" combo handler
2405        array_caption = 'array'
2406
2407        # Get the combo box reference
2408        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2409        widget = self.lstPoly.indexWidget(ind)
2410
2411        # Update the combo box so it displays "array"
2412        widget.blockSignals(True)
2413        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2414        widget.blockSignals(False)
2415
2416        # Invoke the file reader
2417        self.onPolyComboIndexChange(array_caption, row_index)
2418
2419    def onPolyComboIndexChange(self, combo_string, row_index):
2420        """
2421        Modify polydisp. defaults on function choice
2422        """
2423        # Get npts/nsigs for current selection
2424        param = self.model_parameters.form_volume_parameters[row_index]
2425        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2426        combo_box = self.lstPoly.indexWidget(file_index)
2427
2428        def updateFunctionCaption(row):
2429            # Utility function for update of polydispersity function name in the main model
2430            param_name = str(self._model_model.item(row, 0).text())
2431            if param_name !=  param.name:
2432                return
2433            # Modify the param value
2434            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2435
2436        if combo_string == 'array':
2437            try:
2438                self.loadPolydispArray(row_index)
2439                # Update main model for display
2440                self.iterateOverModel(updateFunctionCaption)
2441                # disable the row
2442                lo = self.lstPoly.itemDelegate().poly_pd
2443                hi = self.lstPoly.itemDelegate().poly_function
2444                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2445                return
2446            except IOError:
2447                combo_box.setCurrentIndex(self.orig_poly_index)
2448                # Pass for cancel/bad read
2449                pass
2450
2451        # Enable the row in case it was disabled by Array
2452        self._poly_model.blockSignals(True)
2453        max_range = self.lstPoly.itemDelegate().poly_filename
2454        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2455        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2456        self._poly_model.setData(file_index, "")
2457        self._poly_model.blockSignals(False)
2458
2459        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2460        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2461
2462        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2463        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2464
2465        self._poly_model.setData(npts_index, npts)
2466        self._poly_model.setData(nsigs_index, nsigs)
2467
2468        self.iterateOverModel(updateFunctionCaption)
2469        self.orig_poly_index = combo_box.currentIndex()
2470
2471    def loadPolydispArray(self, row_index):
2472        """
2473        Show the load file dialog and loads requested data into state
2474        """
2475        datafile = QtWidgets.QFileDialog.getOpenFileName(
2476            self, "Choose a weight file", "", "All files (*.*)", None,
2477            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2478
2479        if not datafile:
2480            logging.info("No weight data chosen.")
2481            raise IOError
2482
2483        values = []
2484        weights = []
2485        def appendData(data_tuple):
2486            """
2487            Fish out floats from a tuple of strings
2488            """
2489            try:
2490                values.append(float(data_tuple[0]))
2491                weights.append(float(data_tuple[1]))
2492            except (ValueError, IndexError):
2493                # just pass through if line with bad data
2494                return
2495
2496        with open(datafile, 'r') as column_file:
2497            column_data = [line.rstrip().split() for line in column_file.readlines()]
2498            [appendData(line) for line in column_data]
2499
2500        # If everything went well - update the sasmodel values
2501        self.disp_model = POLYDISPERSITY_MODELS['array']()
2502        self.disp_model.set_weights(np.array(values), np.array(weights))
2503        # + update the cell with filename
2504        fname = os.path.basename(str(datafile))
2505        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2506        self._poly_model.setData(fname_index, fname)
2507
2508    def setMagneticModel(self):
2509        """
2510        Set magnetism values on model
2511        """
2512        if not self.model_parameters:
2513            return
2514        self._magnet_model.clear()
2515        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2516            self.model_parameters.call_parameters if param.type == 'magnetic']
2517        FittingUtilities.addHeadersToModel(self._magnet_model)
2518
2519    def shellNamesList(self):
2520        """
2521        Returns list of names of all multi-shell parameters
2522        E.g. for sld[n], radius[n], n=1..3 it will return
2523        [sld1, sld2, sld3, radius1, radius2, radius3]
2524        """
2525        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2526        top_index = self.kernel_module.multiplicity_info.number
2527        shell_names = []
2528        for i in range(1, top_index+1):
2529            for name in multi_names:
2530                shell_names.append(name+str(i))
2531        return shell_names
2532
2533    def addCheckedMagneticListToModel(self, param, model):
2534        """
2535        Wrapper for model update with a subset of magnetic parameters
2536        """
2537        if param.name[param.name.index(':')+1:] in self.shell_names:
2538            # check if two-digit shell number
2539            try:
2540                shell_index = int(param.name[-2:])
2541            except ValueError:
2542                shell_index = int(param.name[-1:])
2543
2544            if shell_index > self.current_shell_displayed:
2545                return
2546
2547        checked_list = [param.name,
2548                        str(param.default),
2549                        str(param.limits[0]),
2550                        str(param.limits[1]),
2551                        param.units]
2552
2553        FittingUtilities.addCheckedListToModel(model, checked_list)
2554
2555    def enableStructureFactorControl(self, structure_factor):
2556        """
2557        Add structure factors to the list of parameters
2558        """
2559        if self.kernel_module.is_form_factor or structure_factor == 'None':
2560            self.enableStructureCombo()
2561        else:
2562            self.disableStructureCombo()
2563
2564    def addExtraShells(self):
2565        """
2566        Add a combobox for multiple shell display
2567        """
2568        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2569
2570        if param_length == 0:
2571            return
2572
2573        # cell 1: variable name
2574        item1 = QtGui.QStandardItem(param_name)
2575
2576        func = QtWidgets.QComboBox()
2577        # Available range of shells displayed in the combobox
2578        func.addItems([str(i) for i in range(param_length+1)])
2579
2580        # Respond to index change
2581        func.currentIndexChanged.connect(self.modifyShellsInList)
2582
2583        # cell 2: combobox
2584        item2 = QtGui.QStandardItem()
2585        self._model_model.appendRow([item1, item2])
2586
2587        # Beautify the row:  span columns 2-4
2588        shell_row = self._model_model.rowCount()
2589        shell_index = self._model_model.index(shell_row-1, 1)
2590
2591        self.lstParams.setIndexWidget(shell_index, func)
2592        self._last_model_row = self._model_model.rowCount()
2593
2594        # Set the index to the state-kept value
2595        func.setCurrentIndex(self.current_shell_displayed
2596                             if self.current_shell_displayed < func.count() else 0)
2597
2598    def modifyShellsInList(self, index):
2599        """
2600        Add/remove additional multishell parameters
2601        """
2602        # Find row location of the combobox
2603        last_row = self._last_model_row
2604        remove_rows = self._model_model.rowCount() - last_row
2605
2606        if remove_rows > 1:
2607            self._model_model.removeRows(last_row, remove_rows)
2608
2609        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
2610        self.current_shell_displayed = index
2611
2612        # Update relevant models
2613        self.setPolyModel()
2614        self.setMagneticModel()
2615
2616    def setFittingStarted(self):
2617        """
2618        Set buttion caption on fitting start
2619        """
2620        # Notify the user that fitting is being run
2621        # Allow for stopping the job
2622        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
2623        self.cmdFit.setText('Stop fit')
2624
2625    def setFittingStopped(self):
2626        """
2627        Set button caption on fitting stop
2628        """
2629        # Notify the user that fitting is available
2630        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
2631        self.cmdFit.setText("Fit")
2632        self.fit_started = False
2633
2634    def readFitPage(self, fp):
2635        """
2636        Read in state from a fitpage object and update GUI
2637        """
2638        assert isinstance(fp, FitPage)
2639        # Main tab info
2640        self.logic.data.filename = fp.filename
2641        self.data_is_loaded = fp.data_is_loaded
2642        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2643        self.chkMagnetism.setCheckState(fp.is_magnetic)
2644        self.chk2DView.setCheckState(fp.is2D)
2645
2646        # Update the comboboxes
2647        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2648        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2649        if fp.current_factor:
2650            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2651
2652        self.chi2 = fp.chi2
2653
2654        # Options tab
2655        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2656        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2657        self.npts = fp.fit_options[fp.NPTS]
2658        self.log_points = fp.fit_options[fp.LOG_POINTS]
2659        self.weighting = fp.fit_options[fp.WEIGHTING]
2660
2661        # Models
2662        self._model_model = fp.model_model
2663        self._poly_model = fp.poly_model
2664        self._magnet_model = fp.magnetism_model
2665
2666        # Resolution tab
2667        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2668        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2669        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2670        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2671        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2672
2673        # TODO: add polidyspersity and magnetism
2674
2675    def saveToFitPage(self, fp):
2676        """
2677        Write current state to the given fitpage
2678        """
2679        assert isinstance(fp, FitPage)
2680
2681        # Main tab info
2682        fp.filename = self.logic.data.filename
2683        fp.data_is_loaded = self.data_is_loaded
2684        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2685        fp.is_magnetic = self.chkMagnetism.isChecked()
2686        fp.is2D = self.chk2DView.isChecked()
2687        fp.data = self.data
2688
2689        # Use current models - they contain all the required parameters
2690        fp.model_model = self._model_model
2691        fp.poly_model = self._poly_model
2692        fp.magnetism_model = self._magnet_model
2693
2694        if self.cbCategory.currentIndex() != 0:
2695            fp.current_category = str(self.cbCategory.currentText())
2696            fp.current_model = str(self.cbModel.currentText())
2697
2698        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2699            fp.current_factor = str(self.cbStructureFactor.currentText())
2700        else:
2701            fp.current_factor = ''
2702
2703        fp.chi2 = self.chi2
2704        fp.parameters_to_fit = self.parameters_to_fit
2705        fp.kernel_module = self.kernel_module
2706
2707        # Algorithm options
2708        # fp.algorithm = self.parent.fit_options.selected_id
2709
2710        # Options tab
2711        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2712        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2713        fp.fit_options[fp.NPTS] = self.npts
2714        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2715        fp.fit_options[fp.LOG_POINTS] = self.log_points
2716        fp.fit_options[fp.WEIGHTING] = self.weighting
2717
2718        # Resolution tab
2719        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2720        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2721        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2722        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2723        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2724
2725        # TODO: add polidyspersity and magnetism
2726
2727
2728    def updateUndo(self):
2729        """
2730        Create a new state page and add it to the stack
2731        """
2732        if self.undo_supported:
2733            self.pushFitPage(self.currentState())
2734
2735    def currentState(self):
2736        """
2737        Return fit page with current state
2738        """
2739        new_page = FitPage()
2740        self.saveToFitPage(new_page)
2741
2742        return new_page
2743
2744    def pushFitPage(self, new_page):
2745        """
2746        Add a new fit page object with current state
2747        """
2748        self.page_stack.append(new_page)
2749
2750    def popFitPage(self):
2751        """
2752        Remove top fit page from stack
2753        """
2754        if self.page_stack:
2755            self.page_stack.pop()
2756
2757    def getReport(self):
2758        """
2759        Create and return HTML report with parameters and charts
2760        """
2761        index = None
2762        if self.all_data:
2763            index = self.all_data[self.data_index]
2764        report_logic = ReportPageLogic(self,
2765                                       kernel_module=self.kernel_module,
2766                                       data=self.data,
2767                                       index=index,
2768                                       model=self._model_model)
2769
2770        return report_logic.reportList()
2771
2772    def savePageState(self):
2773        """
2774        Create and serialize local PageState
2775        """
2776        from sas.sascalc.fit.pagestate import Reader
2777        model = self.kernel_module
2778
2779        # Old style PageState object
2780        state = PageState(model=model, data=self.data)
2781
2782        # Add parameter data to the state
2783        self.getCurrentFitState(state)
2784
2785        # Create the filewriter, aptly named 'Reader'
2786        state_reader = Reader(self.loadPageStateCallback)
2787        filepath = self.saveAsAnalysisFile()
2788        if filepath is None or filepath == "":
2789            return
2790        state_reader.write(filename=filepath, fitstate=state)
2791        pass
2792
2793    def saveAsAnalysisFile(self):
2794        """
2795        Show the save as... dialog and return the chosen filepath
2796        """
2797        default_name = "FitPage"+str(self.tab_id)+".fitv"
2798
2799        wildcard = "fitv files (*.fitv)"
2800        kwargs = {
2801            'caption'   : 'Save As',
2802            'directory' : default_name,
2803            'filter'    : wildcard,
2804            'parent'    : None,
2805        }
2806        # Query user for filename.
2807        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
2808        filename = filename_tuple[0]
2809        return filename
2810
2811    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
2812        """
2813        This is a callback method called from the CANSAS reader.
2814        We need the instance of this reader only for writing out a file,
2815        so there's nothing here.
2816        Until Load Analysis is implemented, that is.
2817        """
2818        pass
2819
2820    def loadPageState(self, pagestate=None):
2821        """
2822        Load the PageState object and update the current widget
2823        """
2824        pass
2825
2826    def getCurrentFitState(self, state=None):
2827        """
2828        Store current state for fit_page
2829        """
2830        # save model option
2831        #if self.model is not None:
2832        #    self.disp_list = self.getDispParamList()
2833        #    state.disp_list = copy.deepcopy(self.disp_list)
2834        #    #state.model = self.model.clone()
2835
2836        # Comboboxes
2837        state.categorycombobox = self.cbCategory.currentText()
2838        state.formfactorcombobox = self.cbModel.currentText()
2839        if self.cbStructureFactor.isEnabled():
2840            state.structurecombobox = self.cbStructureFactor.currentText()
2841        state.tcChi = self.chi2
2842
2843        state.enable2D = self.is2D
2844
2845        #state.weights = copy.deepcopy(self.weights)
2846        # save data
2847        state.data = copy.deepcopy(self.data)
2848
2849        # save plotting range
2850        state.qmin = self.q_range_min
2851        state.qmax = self.q_range_max
2852        state.npts = self.npts
2853
2854        #    self.state.enable_disp = self.enable_disp.GetValue()
2855        #    self.state.disable_disp = self.disable_disp.GetValue()
2856
2857        #    self.state.enable_smearer = \
2858        #                        copy.deepcopy(self.enable_smearer.GetValue())
2859        #    self.state.disable_smearer = \
2860        #                        copy.deepcopy(self.disable_smearer.GetValue())
2861
2862        #self.state.pinhole_smearer = \
2863        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
2864        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
2865        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
2866        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
2867        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
2868        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
2869
2870        p = self.model_parameters
2871        # save checkbutton state and txtcrtl values
2872        state.parameters = FittingUtilities.getStandardParam(self._model_model)
2873        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
2874
2875        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
2876        #self._copy_parameters_state(self.parameters, self.state.parameters)
2877        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
2878        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
2879
2880    def onParameterCopy(self, format=None):
2881        """
2882        Copy current parameters into the clipboard
2883        """
2884        # run a loop over all parameters and pull out
2885        # first - regular params
2886        param_list = []
2887        def gatherParams(row):
2888            """
2889            Create list of main parameters based on _model_model
2890            """
2891            param_name = str(self._model_model.item(row, 0).text())
2892            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2893            param_value = str(self._model_model.item(row, 1).text())
2894            param_error = None
2895            column_offset = 0
2896            if self.has_error_column:
2897                param_error = str(self._model_model.item(row, 2).text())
2898                column_offset = 1
2899            param_min = str(self._model_model.item(row, 2+column_offset).text())
2900            param_max = str(self._model_model.item(row, 3+column_offset).text())
2901            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
2902
2903        def gatherPolyParams(row):
2904            """
2905            Create list of polydisperse parameters based on _poly_model
2906            """
2907            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
2908            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2909            param_value = str(self._poly_model.item(row, 1).text())
2910            param_error = None
2911            column_offset = 0
2912            if self.has_poly_error_column:
2913                param_error = str(self._poly_model.item(row, 2).text())
2914                column_offset = 1
2915            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
2916            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
2917            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
2918            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
2919            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
2920            # width
2921            name = param_name+".width"
2922            param_list.append([name, param_checked, param_value, param_error,
2923                                param_npts, param_nsigs, param_min, param_max, param_fun])
2924
2925        def gatherMagnetParams(row):
2926            """
2927            Create list of magnetic parameters based on _magnet_model
2928            """
2929            param_name = str(self._magnet_model.item(row, 0).text())
2930            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2931            param_value = str(self._magnet_model.item(row, 1).text())
2932            param_error = None
2933            column_offset = 0
2934            if self.has_magnet_error_column:
2935                param_error = str(self._magnet_model.item(row, 2).text())
2936                column_offset = 1
2937            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
2938            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
2939            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
2940
2941        self.iterateOverModel(gatherParams)
2942        if self.chkPolydispersity.isChecked():
2943            self.iterateOverPolyModel(gatherPolyParams)
2944        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
2945            self.iterateOverMagnetModel(gatherMagnetParams)
2946
2947        if format=="":
2948            formatted_output = FittingUtilities.formatParameters(param_list)
2949        elif format == "Excel":
2950            formatted_output = FittingUtilities.formatParametersExcel(param_list)
2951        elif format == "Latex":
2952            formatted_output = FittingUtilities.formatParametersLatex(param_list)
2953        else:
2954            raise AttributeError("Bad format specifier.")
2955
2956        # Dump formatted_output to the clipboard
2957        cb = QtWidgets.QApplication.clipboard()
2958        cb.setText(formatted_output)
2959
2960    def onParameterPaste(self):
2961        """
2962        Use the clipboard to update fit state
2963        """
2964        # Check if the clipboard contains right stuff
2965        cb = QtWidgets.QApplication.clipboard()
2966        cb_text = cb.text()
2967
2968        context = {}
2969        # put the text into dictionary
2970        lines = cb_text.split(':')
2971        if lines[0] != 'sasview_parameter_values':
2972            return False
2973        for line in lines[1:-1]:
2974            if len(line) != 0:
2975                item = line.split(',')
2976                check = item[1]
2977                name = item[0]
2978                value = item[2]
2979                # Transfer the text to content[dictionary]
2980                context[name] = [check, value]
2981
2982                # limits
2983                limit_lo = item[3]
2984                context[name].append(limit_lo)
2985                limit_hi = item[4]
2986                context[name].append(limit_hi)
2987
2988                # Polydisp
2989                if len(item) > 5:
2990                    value = item[5]
2991                    context[name].append(value)
2992                    try:
2993                        value = item[6]
2994                        context[name].append(value)
2995                        value = item[7]
2996                        context[name].append(value)
2997                    except IndexError:
2998                        pass
2999
3000        self.updateFullModel(context)
3001        self.updateFullPolyModel(context)
3002
3003    def updateFullModel(self, param_dict):
3004        """
3005        Update the model with new parameters
3006        """
3007        assert isinstance(param_dict, dict)
3008        if not dict:
3009            return
3010
3011        def updateFittedValues(row):
3012            # Utility function for main model update
3013            # internal so can use closure for param_dict
3014            param_name = str(self._model_model.item(row, 0).text())
3015            if param_name not in list(param_dict.keys()):
3016                return
3017            # checkbox state
3018            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3019            self._model_model.item(row, 0).setCheckState(param_checked)
3020
3021            # modify the param value
3022            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3023            self._model_model.item(row, 1).setText(param_repr)
3024
3025            # Potentially the error column
3026            ioffset = 0
3027            if len(param_dict[param_name])>4 and self.has_error_column:
3028                # error values are not editable - no need to update
3029                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3030                #self._model_model.item(row, 2).setText(error_repr)
3031                ioffset = 1
3032            # min/max
3033            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3034            self._model_model.item(row, 2+ioffset).setText(param_repr)
3035            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3036            self._model_model.item(row, 3+ioffset).setText(param_repr)
3037
3038        # block signals temporarily, so we don't end up
3039        # updating charts with every single model change on the end of fitting
3040        self._model_model.blockSignals(True)
3041        self.iterateOverModel(updateFittedValues)
3042        self._model_model.blockSignals(False)
3043
3044    def updateFullPolyModel(self, param_dict):
3045        """
3046        Update the polydispersity model with new parameters, create the errors column
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            if row >= self._poly_model.rowCount():
3056                return
3057            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3058            if param_name not in list(param_dict.keys()):
3059                return
3060            # checkbox state
3061            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3062            self._poly_model.item(row,0).setCheckState(param_checked)
3063
3064            # modify the param value
3065            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3066            self._poly_model.item(row, 1).setText(param_repr)
3067
3068            # Potentially the error column
3069            ioffset = 0
3070            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3071                ioffset = 1
3072            # min
3073            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3074            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3075            # max
3076            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3077            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3078            # Npts
3079            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3080            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3081            # Nsigs
3082            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3083            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3084
3085            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3086            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3087
3088        # block signals temporarily, so we don't end up
3089        # updating charts with every single model change on the end of fitting
3090        self._poly_model.blockSignals(True)
3091        self.iterateOverPolyModel(updateFittedValues)
3092        self._poly_model.blockSignals(False)
3093
3094
Note: See TracBrowser for help on using the repository browser.