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

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

More Qt5 related fixes.

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