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

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 cb90b65 was cb90b65, checked in by Piotr Rozyczko <rozyczko@…>, 18 months ago

Make sure we get meaningful reports for theories.

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