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

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

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

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