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

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

Complex constraint widget updating the main constraint widget and fitting tabs

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