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

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

SASVIEW-644: mapping between datasets and fit tabs

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