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

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

Initial changes to make SasView? run with python3

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