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

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

Fixed a crash when the file open dialog would lose reference to the underlying listview cell on cell losing focus SASVIEW-625

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