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

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

Converted unit tests

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