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

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 a95c44b was a95c44b, checked in by wojciech, 7 years ago

Added descriptive tooltips to column headers

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