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

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

Fixed a crash when the file open dialog would lose reference to the underlying listview cell on cell losing focus SASVIEW-625

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