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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 060413c was 060413c, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

More CR and merge fixes.

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