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

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

More Qt5 related fixes

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