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

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 e0b2b2c was e0b2b2c, checked in by wojciech, 6 years ago

Disabling magnetism upon 1d data loading

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