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

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

Fixing unit tests + removal of unnecessary files

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