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

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 3b3b40b was 3b3b40b, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Merging feature branches

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