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

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

Update for unit tests and minor functionality quirks

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