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

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

Towards working C&S fits - SASVIEW-846

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