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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since e11106e was e11106e, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

fix error column not being reset after structure factor is re-selected

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