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

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

Fixes to the Invariant perspective

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