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

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

More Qt5 related fixes.

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