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

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 457d961 was 457d961, checked in by Celine Durniak <celine.durniak@…>, 7 years ago

Corrected bugs in display of new GUI (angstrom, size of line edit)

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