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

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

Thread stopping for supported optimizers SASVIEW-505

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