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

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 13cd397 was 13cd397, checked in by wojciech, 7 years ago

subclass to QStandardItemModel

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