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

Last change on this file since ff8cb73 was 5d1440e1, checked in by Tim Snow <tim.snow@…>, 7 years ago

Bugfix - Ticket SVCC-74

Structure factors can now be calculated by using the product routines in sasmodels

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