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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since d1e4689 was d1e4689, checked in by Piotr Rozyczko <piotrrozyczko@…>, 6 years ago

Added status bar display for single value constraints SASVIEW-1043
Fixed an issue with theories not showing up properly

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