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

Last change on this file since 6b43c58 was 02f1d12, checked in by Tim Snow <tim.snow@…>, 7 years ago

Magnetism fixes

Made the magnetism parameters editable along with setting the correct parameters for these to be pushed to the calculations.

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