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

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

SASVIEW-625: code review fixes. Corrected handling for cancelling the file open dialog, updated test cases.

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