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

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

Fixed naming of datasets for both theory and file data

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