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

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

Added constraints to the fitter

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