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

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

Fixed inter-branch contamination

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