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

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

fix crash when fitting does not converge; just abort on failure

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