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

Last change on this file since 7be7136 was 5d1440e1, checked in by Tim Snow <tim.snow@…>, 7 years ago

Bugfix - Ticket SVCC-74

Structure factors can now be calculated by using the product routines in sasmodels

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