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

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

More Qt5 related fixes.

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