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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 6280464 was 6280464, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

More Qt5 related fixes.

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