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

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

Initial setup for batch fitting. SASVIEW-615

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