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

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

subclass to QStandardItemModel

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