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

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

Initial, in-progress version. Not really working atm. SASVIEW-787

  • 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):
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 = float(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 = float(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).toPyObject())
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 = float(item.text())
637        except ValueError:
638            # Unparsable field
639            return
640
641        property_index = self._magnet_model.headerData(1, model_column).toInt()[0]-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 = float(item.text())
1298        except ValueError:
1299            # Unparsable field
1300            return
1301        parameter_name = str(self._model_model.data(name_index)) #.toPyObject()) # sld, background etc.
1302
1303        # Update the parameter value - note: this supports +/-inf as well
1304        self.kernel_module.params[parameter_name] = value
1305
1306        # Update the parameter value - note: this supports +/-inf as well
1307        param_column = self.lstParams.itemDelegate().param_value
1308        min_column = self.lstParams.itemDelegate().param_min
1309        max_column = self.lstParams.itemDelegate().param_max
1310        if model_column == param_column:
1311            self.kernel_module.setParam(parameter_name, value)
1312        elif model_column == min_column:
1313            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1314            self.kernel_module.details[parameter_name][1] = value
1315        elif model_column == max_column:
1316            self.kernel_module.details[parameter_name][2] = value
1317        else:
1318            # don't update the chart
1319            return
1320
1321        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1322        # TODO: multishell params in self.kernel_module.details[??] = value
1323
1324        # Force the chart update when actual parameters changed
1325        if model_column == 1:
1326            self.recalculatePlotData()
1327
1328        # Update state stack
1329        self.updateUndo()
1330
1331    def checkboxSelected(self, item):
1332        # Assure we're dealing with checkboxes
1333        if not item.isCheckable():
1334            return
1335        status = item.checkState()
1336
1337        def isCheckable(row):
1338            return self._model_model.item(row, 0).isCheckable()
1339
1340        # If multiple rows selected - toggle all of them, filtering uncheckable
1341        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1342
1343        # Switch off signaling from the model to avoid recursion
1344        self._model_model.blockSignals(True)
1345        # Convert to proper indices and set requested enablement
1346        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1347        self._model_model.blockSignals(False)
1348
1349        # update the list of parameters to fit
1350        main_params = self.checkedListFromModel(self._model_model)
1351        poly_params = self.checkedListFromModel(self._poly_model)
1352        magnet_params = self.checkedListFromModel(self._magnet_model)
1353
1354        # Retrieve poly params names
1355        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1356
1357        self.parameters_to_fit = main_params + poly_params + magnet_params
1358
1359    def checkedListFromModel(self, model):
1360        """
1361        Returns list of checked parameters for given model
1362        """
1363        def isChecked(row):
1364            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1365
1366        return [str(model.item(row_index, 0).text())
1367                for row_index in range(model.rowCount())
1368                if isChecked(row_index)]
1369
1370    def nameForFittedData(self, name):
1371        """
1372        Generate name for the current fit
1373        """
1374        if self.is2D:
1375            name += "2d"
1376        name = "M%i [%s]" % (self.tab_id, name)
1377        return name
1378
1379    def createNewIndex(self, fitted_data):
1380        """
1381        Create a model or theory index with passed Data1D/Data2D
1382        """
1383        if self.data_is_loaded:
1384            if not fitted_data.name:
1385                name = self.nameForFittedData(self.data.filename)
1386                fitted_data.title = name
1387                fitted_data.name = name
1388                fitted_data.filename = name
1389                fitted_data.symbol = "Line"
1390            self.updateModelIndex(fitted_data)
1391        else:
1392            name = self.nameForFittedData(self.kernel_module.name)
1393            fitted_data.title = name
1394            fitted_data.name = name
1395            fitted_data.filename = name
1396            fitted_data.symbol = "Line"
1397            self.createTheoryIndex(fitted_data)
1398
1399    def updateModelIndex(self, fitted_data):
1400        """
1401        Update a QStandardModelIndex containing model data
1402        """
1403        name = self.nameFromData(fitted_data)
1404        # Make this a line if no other defined
1405        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1406            fitted_data.symbol = 'Line'
1407        # Notify the GUI manager so it can update the main model in DataExplorer
1408        GuiUtils.updateModelItemWithPlot(self._index, fitted_data, name)
1409
1410    def createTheoryIndex(self, fitted_data):
1411        """
1412        Create a QStandardModelIndex containing model data
1413        """
1414        name = self.nameFromData(fitted_data)
1415        # Notify the GUI manager so it can create the theory model in DataExplorer
1416        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
1417        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1418
1419    def nameFromData(self, fitted_data):
1420        """
1421        Return name for the dataset. Terribly impure function.
1422        """
1423        if fitted_data.name is None:
1424            name = self.nameForFittedData(self.logic.data.filename)
1425            fitted_data.title = name
1426            fitted_data.name = name
1427            fitted_data.filename = name
1428        else:
1429            name = fitted_data.name
1430        return name
1431
1432    def methodCalculateForData(self):
1433        '''return the method for data calculation'''
1434        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1435
1436    def methodCompleteForData(self):
1437        '''return the method for result parsin on calc complete '''
1438        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1439
1440    def calculateQGridForModel(self):
1441        """
1442        Prepare the fitting data object, based on current ModelModel
1443        """
1444        if self.kernel_module is None:
1445            return
1446        # Awful API to a backend method.
1447        method = self.methodCalculateForData()(data=self.data,
1448                                               model=self.kernel_module,
1449                                               page_id=0,
1450                                               qmin=self.q_range_min,
1451                                               qmax=self.q_range_max,
1452                                               smearer=None,
1453                                               state=None,
1454                                               weight=None,
1455                                               fid=None,
1456                                               toggle_mode_on=False,
1457                                               completefn=None,
1458                                               update_chisqr=True,
1459                                               exception_handler=self.calcException,
1460                                               source=None)
1461
1462        calc_thread = threads.deferToThread(method.compute)
1463        calc_thread.addCallback(self.methodCompleteForData())
1464        calc_thread.addErrback(self.calculateDataFailed)
1465
1466    def calculateDataFailed(self, reason):
1467        """
1468        Thread returned error
1469        """
1470        print("Calculate Data failed with ", reason)
1471
1472    def complete1D(self, return_data):
1473        """
1474        Plot the current 1D data
1475        """
1476        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1477        self.calculateResiduals(fitted_data)
1478        self.model_data = fitted_data
1479
1480    def complete2D(self, return_data):
1481        """
1482        Plot the current 2D data
1483        """
1484        fitted_data = self.logic.new2DPlot(return_data)
1485        self.calculateResiduals(fitted_data)
1486        self.model_data = fitted_data
1487
1488    def calculateResiduals(self, fitted_data):
1489        """
1490        Calculate and print Chi2 and display chart of residuals
1491        """
1492        # Create a new index for holding data
1493        fitted_data.symbol = "Line"
1494
1495        # Modify fitted_data with weighting
1496        self.addWeightingToData(fitted_data)
1497
1498        self.createNewIndex(fitted_data)
1499        # Calculate difference between return_data and logic.data
1500        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1501        # Update the control
1502        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1503        self.lblChi2Value.setText(chi2_repr)
1504
1505        self.communicate.plotUpdateSignal.emit([fitted_data])
1506
1507        # Plot residuals if actual data
1508        if not self.data_is_loaded:
1509            return
1510
1511        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1512        residuals_plot.id = "Residual " + residuals_plot.id
1513        self.createNewIndex(residuals_plot)
1514        self.communicate.plotUpdateSignal.emit([residuals_plot])
1515
1516    def calcException(self, etype, value, tb):
1517        """
1518        Thread threw an exception.
1519        """
1520        # TODO: remimplement thread cancellation
1521        logging.error("".join(traceback.format_exception(etype, value, tb)))
1522
1523    def setTableProperties(self, table):
1524        """
1525        Setting table properties
1526        """
1527        # Table properties
1528        table.verticalHeader().setVisible(False)
1529        table.setAlternatingRowColors(True)
1530        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1531        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
1532        table.resizeColumnsToContents()
1533
1534        # Header
1535        header = table.horizontalHeader()
1536        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
1537        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
1538
1539        # Qt5: the following 2 lines crash - figure out why!
1540        # Resize column 0 and 7 to content
1541        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
1542        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
1543
1544    def setPolyModel(self):
1545        """
1546        Set polydispersity values
1547        """
1548        if not self.model_parameters:
1549            return
1550        self._poly_model.clear()
1551
1552        [self.setPolyModelParameters(i, param) for i, param in \
1553            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1554        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1555
1556    def setPolyModelParameters(self, i, param):
1557        """
1558        Standard of multishell poly parameter driver
1559        """
1560        param_name = param.name
1561        # see it the parameter is multishell
1562        if '[' in param.name:
1563            # Skip empty shells
1564            if self.current_shell_displayed == 0:
1565                return
1566            else:
1567                # Create as many entries as current shells
1568                for ishell in range(1, self.current_shell_displayed+1):
1569                    # Remove [n] and add the shell numeral
1570                    name = param_name[0:param_name.index('[')] + str(ishell)
1571                    self.addNameToPolyModel(i, name)
1572        else:
1573            # Just create a simple param entry
1574            self.addNameToPolyModel(i, param_name)
1575
1576    def addNameToPolyModel(self, i, param_name):
1577        """
1578        Creates a checked row in the poly model with param_name
1579        """
1580        # Polydisp. values from the sasmodel
1581        width = self.kernel_module.getParam(param_name + '.width')
1582        npts = self.kernel_module.getParam(param_name + '.npts')
1583        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1584        _, min, max = self.kernel_module.details[param_name]
1585
1586        # Construct a row with polydisp. related variable.
1587        # This will get added to the polydisp. model
1588        # Note: last argument needs extra space padding for decent display of the control
1589        checked_list = ["Distribution of " + param_name, str(width),
1590                        str(min), str(max),
1591                        str(npts), str(nsigs), "gaussian      ",'']
1592        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1593
1594        # All possible polydisp. functions as strings in combobox
1595        func = QtWidgets.QComboBox()
1596        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
1597        # Set the default index
1598        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1599        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1600        self.lstPoly.setIndexWidget(ind, func)
1601        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1602
1603    def onPolyFilenameChange(self, row_index):
1604        """
1605        Respond to filename_updated signal from the delegate
1606        """
1607        # For the given row, invoke the "array" combo handler
1608        array_caption = 'array'
1609
1610        # Get the combo box reference
1611        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1612        widget = self.lstPoly.indexWidget(ind)
1613
1614        # Update the combo box so it displays "array"
1615        widget.blockSignals(True)
1616        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1617        widget.blockSignals(False)
1618
1619        # Invoke the file reader
1620        self.onPolyComboIndexChange(array_caption, row_index)
1621
1622    def onPolyComboIndexChange(self, combo_string, row_index):
1623        """
1624        Modify polydisp. defaults on function choice
1625        """
1626        # Get npts/nsigs for current selection
1627        param = self.model_parameters.form_volume_parameters[row_index]
1628        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1629        combo_box = self.lstPoly.indexWidget(file_index)
1630
1631        def updateFunctionCaption(row):
1632            # Utility function for update of polydispersity function name in the main model
1633            param_name = str(self._model_model.item(row, 0).text())
1634            if param_name !=  param.name:
1635                return
1636            # Modify the param value
1637            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1638
1639        if combo_string == 'array':
1640            try:
1641                self.loadPolydispArray(row_index)
1642                # Update main model for display
1643                self.iterateOverModel(updateFunctionCaption)
1644                # disable the row
1645                lo = self.lstPoly.itemDelegate().poly_pd
1646                hi = self.lstPoly.itemDelegate().poly_function
1647                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
1648                return
1649            except IOError:
1650                combo_box.setCurrentIndex(self.orig_poly_index)
1651                # Pass for cancel/bad read
1652                pass
1653
1654        # Enable the row in case it was disabled by Array
1655        self._poly_model.blockSignals(True)
1656        max_range = self.lstPoly.itemDelegate().poly_filename
1657        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
1658        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1659        self._poly_model.setData(file_index, "")
1660        self._poly_model.blockSignals(False)
1661
1662        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1663        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1664
1665        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1666        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1667
1668        self._poly_model.setData(npts_index, npts)
1669        self._poly_model.setData(nsigs_index, nsigs)
1670
1671        self.iterateOverModel(updateFunctionCaption)
1672        self.orig_poly_index = combo_box.currentIndex()
1673
1674    def loadPolydispArray(self, row_index):
1675        """
1676        Show the load file dialog and loads requested data into state
1677        """
1678        datafile = QtWidgets.QFileDialog.getOpenFileName(
1679            self, "Choose a weight file", "", "All files (*.*)", None,
1680            QtWidgets.QFileDialog.DontUseNativeDialog)
1681
1682        if datafile is None or str(datafile)=='':
1683            logging.info("No weight data chosen.")
1684            raise IOError
1685
1686        values = []
1687        weights = []
1688        def appendData(data_tuple):
1689            """
1690            Fish out floats from a tuple of strings
1691            """
1692            try:
1693                values.append(float(data_tuple[0]))
1694                weights.append(float(data_tuple[1]))
1695            except (ValueError, IndexError):
1696                # just pass through if line with bad data
1697                return
1698
1699        with open(datafile, 'r') as column_file:
1700            column_data = [line.rstrip().split() for line in column_file.readlines()]
1701            [appendData(line) for line in column_data]
1702
1703        # If everything went well - update the sasmodel values
1704        self.disp_model = POLYDISPERSITY_MODELS['array']()
1705        self.disp_model.set_weights(np.array(values), np.array(weights))
1706        # + update the cell with filename
1707        fname = os.path.basename(str(datafile))
1708        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1709        self._poly_model.setData(fname_index, fname)
1710
1711    def setMagneticModel(self):
1712        """
1713        Set magnetism values on model
1714        """
1715        if not self.model_parameters:
1716            return
1717        self._magnet_model.clear()
1718        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1719            self.model_parameters.call_parameters if param.type == 'magnetic']
1720        FittingUtilities.addHeadersToModel(self._magnet_model)
1721
1722    def shellNamesList(self):
1723        """
1724        Returns list of names of all multi-shell parameters
1725        E.g. for sld[n], radius[n], n=1..3 it will return
1726        [sld1, sld2, sld3, radius1, radius2, radius3]
1727        """
1728        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1729        top_index = self.kernel_module.multiplicity_info.number
1730        shell_names = []
1731        for i in range(1, top_index+1):
1732            for name in multi_names:
1733                shell_names.append(name+str(i))
1734        return shell_names
1735
1736    def addCheckedMagneticListToModel(self, param, model):
1737        """
1738        Wrapper for model update with a subset of magnetic parameters
1739        """
1740        if param.name[param.name.index(':')+1:] in self.shell_names:
1741            # check if two-digit shell number
1742            try:
1743                shell_index = int(param.name[-2:])
1744            except ValueError:
1745                shell_index = int(param.name[-1:])
1746
1747            if shell_index > self.current_shell_displayed:
1748                return
1749
1750        checked_list = [param.name,
1751                        str(param.default),
1752                        str(param.limits[0]),
1753                        str(param.limits[1]),
1754                        param.units]
1755
1756        FittingUtilities.addCheckedListToModel(model, checked_list)
1757
1758    def enableStructureFactorControl(self, structure_factor):
1759        """
1760        Add structure factors to the list of parameters
1761        """
1762        if self.kernel_module.is_form_factor or structure_factor == 'None':
1763            self.enableStructureCombo()
1764        else:
1765            self.disableStructureCombo()
1766
1767    def addExtraShells(self):
1768        """
1769        Add a combobox for multiple shell display
1770        """
1771        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1772
1773        if param_length == 0:
1774            return
1775
1776        # cell 1: variable name
1777        item1 = QtGui.QStandardItem(param_name)
1778
1779        func = QtWidgets.QComboBox()
1780        # Available range of shells displayed in the combobox
1781        func.addItems([str(i) for i in range(param_length+1)])
1782
1783        # Respond to index change
1784        func.currentIndexChanged.connect(self.modifyShellsInList)
1785
1786        # cell 2: combobox
1787        item2 = QtGui.QStandardItem()
1788        self._model_model.appendRow([item1, item2])
1789
1790        # Beautify the row:  span columns 2-4
1791        shell_row = self._model_model.rowCount()
1792        shell_index = self._model_model.index(shell_row-1, 1)
1793
1794        self.lstParams.setIndexWidget(shell_index, func)
1795        self._last_model_row = self._model_model.rowCount()
1796
1797        # Set the index to the state-kept value
1798        func.setCurrentIndex(self.current_shell_displayed
1799                             if self.current_shell_displayed < func.count() else 0)
1800
1801    def modifyShellsInList(self, index):
1802        """
1803        Add/remove additional multishell parameters
1804        """
1805        # Find row location of the combobox
1806        last_row = self._last_model_row
1807        remove_rows = self._model_model.rowCount() - last_row
1808
1809        if remove_rows > 1:
1810            self._model_model.removeRows(last_row, remove_rows)
1811
1812        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1813        self.current_shell_displayed = index
1814
1815        # Update relevant models
1816        self.setPolyModel()
1817        self.setMagneticModel()
1818
1819    def readFitPage(self, fp):
1820        """
1821        Read in state from a fitpage object and update GUI
1822        """
1823        assert isinstance(fp, FitPage)
1824        # Main tab info
1825        self.logic.data.filename = fp.filename
1826        self.data_is_loaded = fp.data_is_loaded
1827        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1828        self.chkMagnetism.setCheckState(fp.is_magnetic)
1829        self.chk2DView.setCheckState(fp.is2D)
1830
1831        # Update the comboboxes
1832        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1833        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1834        if fp.current_factor:
1835            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1836
1837        self.chi2 = fp.chi2
1838
1839        # Options tab
1840        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1841        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1842        self.npts = fp.fit_options[fp.NPTS]
1843        self.log_points = fp.fit_options[fp.LOG_POINTS]
1844        self.weighting = fp.fit_options[fp.WEIGHTING]
1845
1846        # Models
1847        self._model_model = fp.model_model
1848        self._poly_model = fp.poly_model
1849        self._magnet_model = fp.magnetism_model
1850
1851        # Resolution tab
1852        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1853        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1854        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1855        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1856        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1857
1858        # TODO: add polidyspersity and magnetism
1859
1860    def saveToFitPage(self, fp):
1861        """
1862        Write current state to the given fitpage
1863        """
1864        assert isinstance(fp, FitPage)
1865
1866        # Main tab info
1867        fp.filename = self.logic.data.filename
1868        fp.data_is_loaded = self.data_is_loaded
1869        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1870        fp.is_magnetic = self.chkMagnetism.isChecked()
1871        fp.is2D = self.chk2DView.isChecked()
1872        fp.data = self.data
1873
1874        # Use current models - they contain all the required parameters
1875        fp.model_model = self._model_model
1876        fp.poly_model = self._poly_model
1877        fp.magnetism_model = self._magnet_model
1878
1879        if self.cbCategory.currentIndex() != 0:
1880            fp.current_category = str(self.cbCategory.currentText())
1881            fp.current_model = str(self.cbModel.currentText())
1882
1883        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1884            fp.current_factor = str(self.cbStructureFactor.currentText())
1885        else:
1886            fp.current_factor = ''
1887
1888        fp.chi2 = self.chi2
1889        fp.parameters_to_fit = self.parameters_to_fit
1890        fp.kernel_module = self.kernel_module
1891
1892        # Algorithm options
1893        # fp.algorithm = self.parent.fit_options.selected_id
1894
1895        # Options tab
1896        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1897        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1898        fp.fit_options[fp.NPTS] = self.npts
1899        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1900        fp.fit_options[fp.LOG_POINTS] = self.log_points
1901        fp.fit_options[fp.WEIGHTING] = self.weighting
1902
1903        # Resolution tab
1904        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1905        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1906        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1907        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1908        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1909
1910        # TODO: add polidyspersity and magnetism
1911
1912
1913    def updateUndo(self):
1914        """
1915        Create a new state page and add it to the stack
1916        """
1917        if self.undo_supported:
1918            self.pushFitPage(self.currentState())
1919
1920    def currentState(self):
1921        """
1922        Return fit page with current state
1923        """
1924        new_page = FitPage()
1925        self.saveToFitPage(new_page)
1926
1927        return new_page
1928
1929    def pushFitPage(self, new_page):
1930        """
1931        Add a new fit page object with current state
1932        """
1933        self.page_stack.append(new_page)
1934
1935    def popFitPage(self):
1936        """
1937        Remove top fit page from stack
1938        """
1939        if self.page_stack:
1940            self.page_stack.pop()
1941
Note: See TracBrowser for help on using the repository browser.