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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since c5e0d84 was 9a7c81c, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed smearing for 1 and 2D

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