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

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 b259485 was b259485, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 13 months ago

typo

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