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

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

Attempt to pass parameters

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