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

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

More Qt5 related fixes

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