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

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

Fixed failing unit tests

  • 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        property_index = self._magnet_model.headerData(1, model_column).toInt()[0]-1 # Value, min, max, etc.
647
648        # Update the parameter value - note: this supports +/-inf as well
649        self.kernel_module.params[parameter_name] = value
650
651        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
652        self.kernel_module.details[parameter_name][property_index] = value
653
654        # Force the chart update when actual parameters changed
655        if model_column == 1:
656            self.recalculatePlotData()
657
658        # Update state stack
659        self.updateUndo()
660
661    def onHelp(self):
662        """
663        Show the "Fitting" section of help
664        """
665        tree_location = GuiUtils.HELP_DIRECTORY_LOCATION + "/user/sasgui/perspectives/fitting/"
666
667        # Actual file will depend on the current tab
668        tab_id = self.tabFitting.currentIndex()
669        helpfile = "fitting.html"
670        if tab_id == 0:
671            helpfile = "fitting_help.html"
672        elif tab_id == 1:
673            helpfile = "residuals_help.html"
674        elif tab_id == 2:
675            helpfile = "sm_help.html"
676        elif tab_id == 3:
677            helpfile = "pd_help.html"
678        elif tab_id == 4:
679            helpfile = "mag_help.html"
680        help_location = tree_location + helpfile
681        self.helpView.load(QtCore.QUrl(help_location))
682        self.helpView.show()
683
684    def onDisplayMagneticAngles(self):
685        """
686        Display a simple image showing direction of magnetic angles
687        """
688        self.magneticAnglesWidget.show()
689
690    def onFit(self):
691        """
692        Perform fitting on the current data
693        """
694
695        # Data going in
696        data = self.logic.data
697        model = self.kernel_module
698        qmin = self.q_range_min
699        qmax = self.q_range_max
700        params_to_fit = self.parameters_to_fit
701
702        # Potential weights added directly to data
703        self.addWeightingToData(data)
704
705        # Potential smearing added
706        # Remember that smearing_min/max can be None ->
707        # deal with it until Python gets discriminated unions
708        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
709
710        # These should be updating somehow?
711        fit_id = 0
712        constraints = []
713        smearer = None
714        page_id = [210]
715        handler = None
716        batch_inputs = {}
717        batch_outputs = {}
718        list_page_id = [page_id]
719        #---------------------------------
720        if USING_TWISTED:
721            handler = None
722            updater = None
723        else:
724            handler = ConsoleUpdate(parent=self.parent,
725                                    manager=self,
726                                    improvement_delta=0.1)
727            updater = handler.update_fit
728
729        # Parameterize the fitter
730        fitters = []
731        for fit_index in self.all_data:
732            fitter = Fit()
733            data = GuiUtils.dataFromItem(fit_index)
734            fitter.set_model(model, fit_id, params_to_fit, data=data,
735                             constraints=constraints)
736            qmin, qmax, _ = self.logic.computeRangeFromData(data)
737            fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
738                            qmax=qmax)
739            fitter.select_problem_for_fit(id=fit_id, value=1)
740            fitter.fitter_id = page_id
741            fit_id += 1
742            fitters.append(fitter)
743
744        # Create the fitting thread, based on the fitter
745        completefn = self.batchFitComplete if self.is_batch_fitting else self.fitComplete
746
747        calc_fit = FitThread(handler=handler,
748                                fn=fitters,
749                                batch_inputs=batch_inputs,
750                                batch_outputs=batch_outputs,
751                                page_id=list_page_id,
752                                updatefn=updater,
753                                completefn=completefn)
754
755        if USING_TWISTED:
756            # start the trhrhread with twisted
757            calc_thread = threads.deferToThread(calc_fit.compute)
758            calc_thread.addCallback(self.fitComplete)
759            calc_thread.addErrback(self.fitFailed)
760        else:
761            # Use the old python threads + Queue
762            calc_fit.queue()
763            calc_fit.ready(2.5)
764
765
766        #disable the Fit button
767        self.cmdFit.setText('Running...')
768        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
769        self.cmdFit.setEnabled(False)
770
771    def updateFit(self):
772        """
773        """
774        print "UPDATE FIT"
775        pass
776
777    def fitFailed(self, reason):
778        """
779        """
780        print "FIT FAILED: ", reason
781        pass
782
783    def batchFitComplete(self, result):
784        """
785        Receive and display batch fitting results
786        """
787        #re-enable the Fit button
788        self.cmdFit.setText("Fit")
789        self.cmdFit.setEnabled(True)
790
791        print ("BATCH FITTING FINISHED")
792        # Add the Qt version of wx.aui.AuiNotebook and populate it
793        pass
794
795    def fitComplete(self, result):
796        """
797        Receive and display fitting results
798        "result" is a tuple of actual result list and the fit time in seconds
799        """
800        #re-enable the Fit button
801        self.cmdFit.setText("Fit")
802        self.cmdFit.setEnabled(True)
803
804        assert result is not None
805
806        res_list = result[0][0]
807        res = res_list[0]
808        if res.fitness is None or \
809            not np.isfinite(res.fitness) or \
810            np.any(res.pvec is None) or \
811            not np.all(np.isfinite(res.pvec)):
812            msg = "Fitting did not converge!!!"
813            self.communicate.statusBarUpdateSignal.emit(msg)
814            logging.error(msg)
815            return
816
817        elapsed = result[1]
818        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
819        self.communicate.statusBarUpdateSignal.emit(msg)
820
821        self.chi2 = res.fitness
822        param_list = res.param_list # ['radius', 'radius.width']
823        param_values = res.pvec     # array([ 0.36221662,  0.0146783 ])
824        param_stderr = res.stderr   # array([ 1.71293015,  1.71294233])
825        params_and_errors = zip(param_values, param_stderr)
826        param_dict = dict(izip(param_list, params_and_errors))
827
828        # Dictionary of fitted parameter: value, error
829        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
830        self.updateModelFromList(param_dict)
831
832        self.updatePolyModelFromList(param_dict)
833
834        self.updateMagnetModelFromList(param_dict)
835
836        # update charts
837        self.onPlot()
838
839        # Read only value - we can get away by just printing it here
840        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
841        self.lblChi2Value.setText(chi2_repr)
842
843    def iterateOverModel(self, func):
844        """
845        Take func and throw it inside the model row loop
846        """
847        for row_i in xrange(self._model_model.rowCount()):
848            func(row_i)
849
850    def updateModelFromList(self, param_dict):
851        """
852        Update the model with new parameters, create the errors column
853        """
854        assert isinstance(param_dict, dict)
855        if not dict:
856            return
857
858        def updateFittedValues(row):
859            # Utility function for main model update
860            # internal so can use closure for param_dict
861            param_name = str(self._model_model.item(row, 0).text())
862            if param_name not in param_dict.keys():
863                return
864            # modify the param value
865            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
866            self._model_model.item(row, 1).setText(param_repr)
867            if self.has_error_column:
868                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
869                self._model_model.item(row, 2).setText(error_repr)
870
871        def updatePolyValues(row):
872            # Utility function for updateof polydispersity part of the main model
873            param_name = str(self._model_model.item(row, 0).text())+'.width'
874            if param_name not in param_dict.keys():
875                return
876            # modify the param value
877            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
878            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
879
880        def createErrorColumn(row):
881            # Utility function for error column update
882            item = QtGui.QStandardItem()
883            def createItem(param_name):
884                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
885                item.setText(error_repr)
886            def curr_param():
887                return str(self._model_model.item(row, 0).text())
888
889            [createItem(param_name) for param_name in param_dict.keys() if curr_param() == param_name]
890
891            error_column.append(item)
892
893        # block signals temporarily, so we don't end up
894        # updating charts with every single model change on the end of fitting
895        self._model_model.blockSignals(True)
896        self.iterateOverModel(updateFittedValues)
897        self.iterateOverModel(updatePolyValues)
898        self._model_model.blockSignals(False)
899
900        if self.has_error_column:
901            return
902
903        error_column = []
904        self.lstParams.itemDelegate().addErrorColumn()
905        self.iterateOverModel(createErrorColumn)
906
907        # switch off reponse to model change
908        self._model_model.blockSignals(True)
909        self._model_model.insertColumn(2, error_column)
910        self._model_model.blockSignals(False)
911        FittingUtilities.addErrorHeadersToModel(self._model_model)
912        # Adjust the table cells width.
913        # TODO: find a way to dynamically adjust column width while resized expanding
914        self.lstParams.resizeColumnToContents(0)
915        self.lstParams.resizeColumnToContents(4)
916        self.lstParams.resizeColumnToContents(5)
917        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
918
919        self.has_error_column = True
920
921    def updatePolyModelFromList(self, param_dict):
922        """
923        Update the polydispersity model with new parameters, create the errors column
924        """
925        assert isinstance(param_dict, dict)
926        if not dict:
927            return
928
929        def iterateOverPolyModel(func):
930            """
931            Take func and throw it inside the poly model row loop
932            """
933            for row_i in xrange(self._poly_model.rowCount()):
934                func(row_i)
935
936        def updateFittedValues(row_i):
937            # Utility function for main model update
938            # internal so can use closure for param_dict
939            if row_i >= self._poly_model.rowCount():
940                return
941            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
942            if param_name not in param_dict.keys():
943                return
944            # modify the param value
945            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
946            self._poly_model.item(row_i, 1).setText(param_repr)
947            if self.has_poly_error_column:
948                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
949                self._poly_model.item(row_i, 2).setText(error_repr)
950
951
952        def createErrorColumn(row_i):
953            # Utility function for error column update
954            if row_i >= self._poly_model.rowCount():
955                return
956            item = QtGui.QStandardItem()
957
958            def createItem(param_name):
959                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
960                item.setText(error_repr)
961
962            def poly_param():
963                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
964
965            [createItem(param_name) for param_name in param_dict.keys() if poly_param() == param_name]
966
967            error_column.append(item)
968
969        # block signals temporarily, so we don't end up
970        # updating charts with every single model change on the end of fitting
971        self._poly_model.blockSignals(True)
972        iterateOverPolyModel(updateFittedValues)
973        self._poly_model.blockSignals(False)
974
975        if self.has_poly_error_column:
976            return
977
978        self.lstPoly.itemDelegate().addErrorColumn()
979        error_column = []
980        iterateOverPolyModel(createErrorColumn)
981
982        # switch off reponse to model change
983        self._poly_model.blockSignals(True)
984        self._poly_model.insertColumn(2, error_column)
985        self._poly_model.blockSignals(False)
986        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
987
988        self.has_poly_error_column = True
989
990    def updateMagnetModelFromList(self, param_dict):
991        """
992        Update the magnetic model with new parameters, create the errors column
993        """
994        assert isinstance(param_dict, dict)
995        if not dict:
996            return
997
998        def iterateOverMagnetModel(func):
999            """
1000            Take func and throw it inside the magnet model row loop
1001            """
1002            for row_i in xrange(self._model_model.rowCount()):
1003                func(row_i)
1004
1005        def updateFittedValues(row):
1006            # Utility function for main model update
1007            # internal so can use closure for param_dict
1008            param_name = str(self._magnet_model.item(row, 0).text())
1009            if param_name not in param_dict.keys():
1010                return
1011            # modify the param value
1012            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1013            self._magnet_model.item(row, 1).setText(param_repr)
1014            if self.has_magnet_error_column:
1015                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1016                self._magnet_model.item(row, 2).setText(error_repr)
1017
1018        def createErrorColumn(row):
1019            # Utility function for error column update
1020            item = QtGui.QStandardItem()
1021            def createItem(param_name):
1022                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1023                item.setText(error_repr)
1024            def curr_param():
1025                return str(self._magnet_model.item(row, 0).text())
1026
1027            [createItem(param_name) for param_name in param_dict.keys() if curr_param() == param_name]
1028
1029            error_column.append(item)
1030
1031        # block signals temporarily, so we don't end up
1032        # updating charts with every single model change on the end of fitting
1033        self._magnet_model.blockSignals(True)
1034        iterateOverMagnetModel(updateFittedValues)
1035        self._magnet_model.blockSignals(False)
1036
1037        if self.has_magnet_error_column:
1038            return
1039
1040        self.lstMagnetic.itemDelegate().addErrorColumn()
1041        error_column = []
1042        iterateOverMagnetModel(createErrorColumn)
1043
1044        # switch off reponse to model change
1045        self._magnet_model.blockSignals(True)
1046        self._magnet_model.insertColumn(2, error_column)
1047        self._magnet_model.blockSignals(False)
1048        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1049
1050        self.has_magnet_error_column = True
1051
1052    def onPlot(self):
1053        """
1054        Plot the current set of data
1055        """
1056        # Regardless of previous state, this should now be `plot show` functionality only
1057        self.cmdPlot.setText("Show Plot")
1058        # Force data recalculation so existing charts are updated
1059        self.recalculatePlotData()
1060        self.showPlot()
1061
1062    def recalculatePlotData(self):
1063        """
1064        Generate a new dataset for model
1065        """
1066        if not self.data_is_loaded:
1067            self.createDefaultDataset()
1068        self.calculateQGridForModel()
1069
1070    def showPlot(self):
1071        """
1072        Show the current plot in MPL
1073        """
1074        # Show the chart if ready
1075        data_to_show = self.data if self.data_is_loaded else self.model_data
1076        if data_to_show is not None:
1077            self.communicate.plotRequestedSignal.emit([data_to_show])
1078
1079    def onOptionsUpdate(self):
1080        """
1081        Update local option values and replot
1082        """
1083        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1084            self.options_widget.state()
1085        # set Q range labels on the main tab
1086        self.lblMinRangeDef.setText(str(self.q_range_min))
1087        self.lblMaxRangeDef.setText(str(self.q_range_max))
1088        self.recalculatePlotData()
1089
1090    def setDefaultStructureCombo(self):
1091        """
1092        Fill in the structure factors combo box with defaults
1093        """
1094        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1095        factors = [factor[0] for factor in structure_factor_list]
1096        factors.insert(0, STRUCTURE_DEFAULT)
1097        self.cbStructureFactor.clear()
1098        self.cbStructureFactor.addItems(sorted(factors))
1099
1100    def createDefaultDataset(self):
1101        """
1102        Generate default Dataset 1D/2D for the given model
1103        """
1104        # Create default datasets if no data passed
1105        if self.is2D:
1106            qmax = self.q_range_max/np.sqrt(2)
1107            qstep = self.npts
1108            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1109            return
1110        elif self.log_points:
1111            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1112            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1113            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1114        else:
1115            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1116                                   num=self.npts, endpoint=True)
1117        self.logic.createDefault1dData(interval, self.tab_id)
1118
1119    def readCategoryInfo(self):
1120        """
1121        Reads the categories in from file
1122        """
1123        self.master_category_dict = defaultdict(list)
1124        self.by_model_dict = defaultdict(list)
1125        self.model_enabled_dict = defaultdict(bool)
1126
1127        categorization_file = CategoryInstaller.get_user_file()
1128        if not os.path.isfile(categorization_file):
1129            categorization_file = CategoryInstaller.get_default_file()
1130        with open(categorization_file, 'rb') as cat_file:
1131            self.master_category_dict = json.load(cat_file)
1132            self.regenerateModelDict()
1133
1134        # Load the model dict
1135        models = load_standard_models()
1136        for model in models:
1137            self.models[model.name] = model
1138
1139    def regenerateModelDict(self):
1140        """
1141        Regenerates self.by_model_dict which has each model name as the
1142        key and the list of categories belonging to that model
1143        along with the enabled mapping
1144        """
1145        self.by_model_dict = defaultdict(list)
1146        for category in self.master_category_dict:
1147            for (model, enabled) in self.master_category_dict[category]:
1148                self.by_model_dict[model].append(category)
1149                self.model_enabled_dict[model] = enabled
1150
1151    def addBackgroundToModel(self, model):
1152        """
1153        Adds background parameter with default values to the model
1154        """
1155        assert isinstance(model, QtGui.QStandardItemModel)
1156        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1157        FittingUtilities.addCheckedListToModel(model, checked_list)
1158        last_row = model.rowCount()-1
1159        model.item(last_row, 0).setEditable(False)
1160        model.item(last_row, 4).setEditable(False)
1161
1162    def addScaleToModel(self, model):
1163        """
1164        Adds scale parameter with default values to the model
1165        """
1166        assert isinstance(model, QtGui.QStandardItemModel)
1167        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1168        FittingUtilities.addCheckedListToModel(model, checked_list)
1169        last_row = model.rowCount()-1
1170        model.item(last_row, 0).setEditable(False)
1171        model.item(last_row, 4).setEditable(False)
1172
1173    def addWeightingToData(self, data):
1174        """
1175        Adds weighting contribution to fitting data
1176        """
1177        # Send original data for weighting
1178        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1179        update_module = data.err_data if self.is2D else data.dy
1180        # Overwrite relevant values in data
1181        update_module = weight
1182
1183    def updateQRange(self):
1184        """
1185        Updates Q Range display
1186        """
1187        if self.data_is_loaded:
1188            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1189        # set Q range labels on the main tab
1190        self.lblMinRangeDef.setText(str(self.q_range_min))
1191        self.lblMaxRangeDef.setText(str(self.q_range_max))
1192        # set Q range labels on the options tab
1193        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1194
1195    def SASModelToQModel(self, model_name, structure_factor=None):
1196        """
1197        Setting model parameters into table based on selected category
1198        """
1199        # Crete/overwrite model items
1200        self._model_model.clear()
1201
1202        # First, add parameters from the main model
1203        if model_name is not None:
1204            self.fromModelToQModel(model_name)
1205
1206        # Then, add structure factor derived parameters
1207        if structure_factor is not None and structure_factor != "None":
1208            if model_name is None:
1209                # Instantiate the current sasmodel for SF-only models
1210                self.kernel_module = self.models[structure_factor]()
1211            self.fromStructureFactorToQModel(structure_factor)
1212        else:
1213            # Allow the SF combobox visibility for the given sasmodel
1214            self.enableStructureFactorControl(structure_factor)
1215
1216        # Then, add multishells
1217        if model_name is not None:
1218            # Multishell models need additional treatment
1219            self.addExtraShells()
1220
1221        # Add polydispersity to the model
1222        self.setPolyModel()
1223        # Add magnetic parameters to the model
1224        self.setMagneticModel()
1225
1226        # Adjust the table cells width
1227        self.lstParams.resizeColumnToContents(0)
1228        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1229
1230        # Now we claim the model has been loaded
1231        self.model_is_loaded = True
1232
1233        # (Re)-create headers
1234        FittingUtilities.addHeadersToModel(self._model_model)
1235        self.lstParams.header().setFont(self.boldFont)
1236
1237        # Update Q Ranges
1238        self.updateQRange()
1239
1240    def fromModelToQModel(self, model_name):
1241        """
1242        Setting model parameters into QStandardItemModel based on selected _model_
1243        """
1244        kernel_module = generate.load_kernel_module(model_name)
1245        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1246
1247        # Instantiate the current sasmodel
1248        self.kernel_module = self.models[model_name]()
1249
1250        # Explicitly add scale and background with default values
1251        temp_undo_state = self.undo_supported
1252        self.undo_supported = False
1253        self.addScaleToModel(self._model_model)
1254        self.addBackgroundToModel(self._model_model)
1255        self.undo_supported = temp_undo_state
1256
1257        self.shell_names = self.shellNamesList()
1258
1259        # Update the QModel
1260        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1261
1262        for row in new_rows:
1263            self._model_model.appendRow(row)
1264        # Update the counter used for multishell display
1265        self._last_model_row = self._model_model.rowCount()
1266
1267    def fromStructureFactorToQModel(self, structure_factor):
1268        """
1269        Setting model parameters into QStandardItemModel based on selected _structure factor_
1270        """
1271        structure_module = generate.load_kernel_module(structure_factor)
1272        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1273        structure_kernel = self.models[structure_factor]()
1274
1275        self.kernel_module._model_info = product.make_product_info(self.kernel_module._model_info, structure_kernel._model_info)
1276
1277        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1278        for row in new_rows:
1279            self._model_model.appendRow(row)
1280        # Update the counter used for multishell display
1281        self._last_model_row = self._model_model.rowCount()
1282
1283    def onMainParamsChange(self, item):
1284        """
1285        Callback method for updating the sasmodel parameters with the GUI values
1286        """
1287        model_column = item.column()
1288
1289        if model_column == 0:
1290            self.checkboxSelected(item)
1291            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1292            # Update state stack
1293            self.updateUndo()
1294            return
1295
1296        model_row = item.row()
1297        name_index = self._model_model.index(model_row, 0)
1298
1299        # Extract changed value.
1300        try:
1301            value = float(item.text())
1302        except ValueError:
1303            # Unparsable field
1304            return
1305        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
1306
1307        # Update the parameter value - note: this supports +/-inf as well
1308        self.kernel_module.params[parameter_name] = value
1309
1310        # Update the parameter value - note: this supports +/-inf as well
1311        param_column = self.lstParams.itemDelegate().param_value
1312        min_column = self.lstParams.itemDelegate().param_min
1313        max_column = self.lstParams.itemDelegate().param_max
1314        if model_column == param_column:
1315            self.kernel_module.setParam(parameter_name, value)
1316        elif model_column == min_column:
1317            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1318            self.kernel_module.details[parameter_name][1] = value
1319        elif model_column == max_column:
1320            self.kernel_module.details[parameter_name][2] = value
1321        else:
1322            # don't update the chart
1323            return
1324
1325        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1326        # TODO: multishell params in self.kernel_module.details[??] = value
1327
1328        # Force the chart update when actual parameters changed
1329        if model_column == 1:
1330            self.recalculatePlotData()
1331
1332        # Update state stack
1333        self.updateUndo()
1334
1335    def checkboxSelected(self, item):
1336        # Assure we're dealing with checkboxes
1337        if not item.isCheckable():
1338            return
1339        status = item.checkState()
1340
1341        def isCheckable(row):
1342            return self._model_model.item(row, 0).isCheckable()
1343
1344        # If multiple rows selected - toggle all of them, filtering uncheckable
1345        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1346
1347        # Switch off signaling from the model to avoid recursion
1348        self._model_model.blockSignals(True)
1349        # Convert to proper indices and set requested enablement
1350        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1351        self._model_model.blockSignals(False)
1352
1353        # update the list of parameters to fit
1354        main_params = self.checkedListFromModel(self._model_model)
1355        poly_params = self.checkedListFromModel(self._poly_model)
1356        magnet_params = self.checkedListFromModel(self._magnet_model)
1357
1358        # Retrieve poly params names
1359        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1360
1361        self.parameters_to_fit = main_params + poly_params + magnet_params
1362
1363    def checkedListFromModel(self, model):
1364        """
1365        Returns list of checked parameters for given model
1366        """
1367        def isChecked(row):
1368            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1369
1370        return [str(model.item(row_index, 0).text())
1371                for row_index in xrange(model.rowCount())
1372                if isChecked(row_index)]
1373
1374    def nameForFittedData(self, name):
1375        """
1376        Generate name for the current fit
1377        """
1378        if self.is2D:
1379            name += "2d"
1380        name = "M%i [%s]" % (self.tab_id, name)
1381        return name
1382
1383    def createNewIndex(self, fitted_data):
1384        """
1385        Create a model or theory index with passed Data1D/Data2D
1386        """
1387        if self.data_is_loaded:
1388            if not fitted_data.name:
1389                name = self.nameForFittedData(self.data.filename)
1390                fitted_data.title = name
1391                fitted_data.name = name
1392                fitted_data.filename = name
1393                fitted_data.symbol = "Line"
1394            self.updateModelIndex(fitted_data)
1395        else:
1396            name = self.nameForFittedData(self.kernel_module.name)
1397            fitted_data.title = name
1398            fitted_data.name = name
1399            fitted_data.filename = name
1400            fitted_data.symbol = "Line"
1401            self.createTheoryIndex(fitted_data)
1402
1403    def updateModelIndex(self, fitted_data):
1404        """
1405        Update a QStandardModelIndex containing model data
1406        """
1407        name = self.nameFromData(fitted_data)
1408        # Make this a line if no other defined
1409        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1410            fitted_data.symbol = 'Line'
1411        # Notify the GUI manager so it can update the main model in DataExplorer
1412        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
1413
1414    def createTheoryIndex(self, fitted_data):
1415        """
1416        Create a QStandardModelIndex containing model data
1417        """
1418        name = self.nameFromData(fitted_data)
1419        # Notify the GUI manager so it can create the theory model in DataExplorer
1420        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
1421        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1422
1423    def nameFromData(self, fitted_data):
1424        """
1425        Return name for the dataset. Terribly impure function.
1426        """
1427        if fitted_data.name is None:
1428            name = self.nameForFittedData(self.logic.data.filename)
1429            fitted_data.title = name
1430            fitted_data.name = name
1431            fitted_data.filename = name
1432        else:
1433            name = fitted_data.name
1434        return name
1435
1436    def methodCalculateForData(self):
1437        '''return the method for data calculation'''
1438        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1439
1440    def methodCompleteForData(self):
1441        '''return the method for result parsin on calc complete '''
1442        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1443
1444    def calculateQGridForModel(self):
1445        """
1446        Prepare the fitting data object, based on current ModelModel
1447        """
1448        if self.kernel_module is None:
1449            return
1450        # Awful API to a backend method.
1451        method = self.methodCalculateForData()(data=self.data,
1452                                               model=self.kernel_module,
1453                                               page_id=0,
1454                                               qmin=self.q_range_min,
1455                                               qmax=self.q_range_max,
1456                                               smearer=None,
1457                                               state=None,
1458                                               weight=None,
1459                                               fid=None,
1460                                               toggle_mode_on=False,
1461                                               completefn=None,
1462                                               update_chisqr=True,
1463                                               exception_handler=self.calcException,
1464                                               source=None)
1465
1466        calc_thread = threads.deferToThread(method.compute)
1467        calc_thread.addCallback(self.methodCompleteForData())
1468        calc_thread.addErrback(self.calculateDataFailed)
1469
1470    def calculateDataFailed(self, reason):
1471        """
1472        Thread returned error
1473        """
1474        print "Calculate Data failed with ", reason
1475
1476    def complete1D(self, return_data):
1477        """
1478        Plot the current 1D data
1479        """
1480        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1481        self.calculateResiduals(fitted_data)
1482        self.model_data = fitted_data
1483
1484    def complete2D(self, return_data):
1485        """
1486        Plot the current 2D data
1487        """
1488        fitted_data = self.logic.new2DPlot(return_data)
1489        self.calculateResiduals(fitted_data)
1490        self.model_data = fitted_data
1491
1492    def calculateResiduals(self, fitted_data):
1493        """
1494        Calculate and print Chi2 and display chart of residuals
1495        """
1496        # Create a new index for holding data
1497        fitted_data.symbol = "Line"
1498
1499        # Modify fitted_data with weighting
1500        self.addWeightingToData(fitted_data)
1501
1502        self.createNewIndex(fitted_data)
1503        # Calculate difference between return_data and logic.data
1504        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1505        # Update the control
1506        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1507        self.lblChi2Value.setText(chi2_repr)
1508
1509        self.communicate.plotUpdateSignal.emit([fitted_data])
1510
1511        # Plot residuals if actual data
1512        if not self.data_is_loaded:
1513            return
1514
1515        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1516        residuals_plot.id = "Residual " + residuals_plot.id
1517        self.createNewIndex(residuals_plot)
1518        self.communicate.plotUpdateSignal.emit([residuals_plot])
1519
1520    def calcException(self, etype, value, tb):
1521        """
1522        Thread threw an exception.
1523        """
1524        # TODO: remimplement thread cancellation
1525        logging.error("".join(traceback.format_exception(etype, value, tb)))
1526
1527    def setTableProperties(self, table):
1528        """
1529        Setting table properties
1530        """
1531        # Table properties
1532        table.verticalHeader().setVisible(False)
1533        table.setAlternatingRowColors(True)
1534        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1535        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1536        table.resizeColumnsToContents()
1537
1538        # Header
1539        header = table.horizontalHeader()
1540        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1541
1542        header.ResizeMode(QtGui.QHeaderView.Interactive)
1543        # Resize column 0 and 7 to content
1544        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1545        header.setResizeMode(7, QtGui.QHeaderView.ResizeToContents)
1546
1547    def setPolyModel(self):
1548        """
1549        Set polydispersity values
1550        """
1551        if not self.model_parameters:
1552            return
1553        self._poly_model.clear()
1554
1555        [self.setPolyModelParameters(i, param) for i, param in \
1556            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1557        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1558
1559    def setPolyModelParameters(self, i, param):
1560        """
1561        Standard of multishell poly parameter driver
1562        """
1563        param_name = param.name
1564        # see it the parameter is multishell
1565        if '[' in param.name:
1566            # Skip empty shells
1567            if self.current_shell_displayed == 0:
1568                return
1569            else:
1570                # Create as many entries as current shells
1571                for ishell in xrange(1, self.current_shell_displayed+1):
1572                    # Remove [n] and add the shell numeral
1573                    name = param_name[0:param_name.index('[')] + str(ishell)
1574                    self.addNameToPolyModel(i, name)
1575        else:
1576            # Just create a simple param entry
1577            self.addNameToPolyModel(i, param_name)
1578
1579    def addNameToPolyModel(self, i, param_name):
1580        """
1581        Creates a checked row in the poly model with param_name
1582        """
1583        # Polydisp. values from the sasmodel
1584        width = self.kernel_module.getParam(param_name + '.width')
1585        npts = self.kernel_module.getParam(param_name + '.npts')
1586        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1587        _, min, max = self.kernel_module.details[param_name]
1588
1589        # Construct a row with polydisp. related variable.
1590        # This will get added to the polydisp. model
1591        # Note: last argument needs extra space padding for decent display of the control
1592        checked_list = ["Distribution of " + param_name, str(width),
1593                        str(min), str(max),
1594                        str(npts), str(nsigs), "gaussian      ",'']
1595        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1596
1597        # All possible polydisp. functions as strings in combobox
1598        func = QtGui.QComboBox()
1599        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.iterkeys()])
1600        # Set the default index
1601        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1602        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1603        self.lstPoly.setIndexWidget(ind, func)
1604        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1605
1606    def onPolyFilenameChange(self, row_index):
1607        """
1608        Respond to filename_updated signal from the delegate
1609        """
1610        # For the given row, invoke the "array" combo handler
1611        array_caption = 'array'
1612
1613        # Get the combo box reference
1614        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1615        widget = self.lstPoly.indexWidget(ind)
1616
1617        # Update the combo box so it displays "array"
1618        widget.blockSignals(True)
1619        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1620        widget.blockSignals(False)
1621
1622        # Invoke the file reader
1623        self.onPolyComboIndexChange(array_caption, row_index)
1624
1625    def onPolyComboIndexChange(self, combo_string, row_index):
1626        """
1627        Modify polydisp. defaults on function choice
1628        """
1629        # Get npts/nsigs for current selection
1630        param = self.model_parameters.form_volume_parameters[row_index]
1631        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1632        combo_box = self.lstPoly.indexWidget(file_index)
1633
1634        def updateFunctionCaption(row):
1635            # Utility function for update of polydispersity function name in the main model
1636            param_name = str(self._model_model.item(row, 0).text())
1637            if param_name !=  param.name:
1638                return
1639            # Modify the param value
1640            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1641
1642        if combo_string == 'array':
1643            try:
1644                self.loadPolydispArray(row_index)
1645                # Update main model for display
1646                self.iterateOverModel(updateFunctionCaption)
1647                # disable the row
1648                lo = self.lstPoly.itemDelegate().poly_pd
1649                hi = self.lstPoly.itemDelegate().poly_function
1650                [self._poly_model.item(row_index, i).setEnabled(False) for i in xrange(lo, hi)]
1651                return
1652            except IOError:
1653                combo_box.setCurrentIndex(self.orig_poly_index)
1654                # Pass for cancel/bad read
1655                pass
1656
1657        # Enable the row in case it was disabled by Array
1658        self._poly_model.blockSignals(True)
1659        max_range = self.lstPoly.itemDelegate().poly_filename
1660        [self._poly_model.item(row_index, i).setEnabled(True) for i in xrange(7)]
1661        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1662        self._poly_model.setData(file_index, QtCore.QVariant(""))
1663        self._poly_model.blockSignals(False)
1664
1665        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1666        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1667
1668        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1669        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1670
1671        self._poly_model.setData(npts_index, QtCore.QVariant(npts))
1672        self._poly_model.setData(nsigs_index, QtCore.QVariant(nsigs))
1673
1674        self.iterateOverModel(updateFunctionCaption)
1675        self.orig_poly_index = combo_box.currentIndex()
1676
1677    def loadPolydispArray(self, row_index):
1678        """
1679        Show the load file dialog and loads requested data into state
1680        """
1681        datafile = QtGui.QFileDialog.getOpenFileName(
1682            self, "Choose a weight file", "", "All files (*.*)",
1683            None, QtGui.QFileDialog.DontUseNativeDialog)
1684
1685        if datafile is None or str(datafile)=='':
1686            logging.info("No weight data chosen.")
1687            raise IOError
1688
1689        values = []
1690        weights = []
1691        def appendData(data_tuple):
1692            """
1693            Fish out floats from a tuple of strings
1694            """
1695            try:
1696                values.append(float(data_tuple[0]))
1697                weights.append(float(data_tuple[1]))
1698            except (ValueError, IndexError):
1699                # just pass through if line with bad data
1700                return
1701
1702        with open(datafile, 'r') as column_file:
1703            column_data = [line.rstrip().split() for line in column_file.readlines()]
1704            [appendData(line) for line in column_data]
1705
1706        # If everything went well - update the sasmodel values
1707        self.disp_model = POLYDISPERSITY_MODELS['array']()
1708        self.disp_model.set_weights(np.array(values), np.array(weights))
1709        # + update the cell with filename
1710        fname = os.path.basename(str(datafile))
1711        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1712        self._poly_model.setData(fname_index, QtCore.QVariant(fname))
1713
1714    def setMagneticModel(self):
1715        """
1716        Set magnetism values on model
1717        """
1718        if not self.model_parameters:
1719            return
1720        self._magnet_model.clear()
1721        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1722            self.model_parameters.call_parameters if param.type == 'magnetic']
1723        FittingUtilities.addHeadersToModel(self._magnet_model)
1724
1725    def shellNamesList(self):
1726        """
1727        Returns list of names of all multi-shell parameters
1728        E.g. for sld[n], radius[n], n=1..3 it will return
1729        [sld1, sld2, sld3, radius1, radius2, radius3]
1730        """
1731        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1732        top_index = self.kernel_module.multiplicity_info.number
1733        shell_names = []
1734        for i in xrange(1, top_index+1):
1735            for name in multi_names:
1736                shell_names.append(name+str(i))
1737        return shell_names
1738
1739    def addCheckedMagneticListToModel(self, param, model):
1740        """
1741        Wrapper for model update with a subset of magnetic parameters
1742        """
1743        if param.name[param.name.index(':')+1:] in self.shell_names:
1744            # check if two-digit shell number
1745            try:
1746                shell_index = int(param.name[-2:])
1747            except ValueError:
1748                shell_index = int(param.name[-1:])
1749
1750            if shell_index > self.current_shell_displayed:
1751                return
1752
1753        checked_list = [param.name,
1754                        str(param.default),
1755                        str(param.limits[0]),
1756                        str(param.limits[1]),
1757                        param.units]
1758
1759        FittingUtilities.addCheckedListToModel(model, checked_list)
1760
1761    def enableStructureFactorControl(self, structure_factor):
1762        """
1763        Add structure factors to the list of parameters
1764        """
1765        if self.kernel_module.is_form_factor or structure_factor == 'None':
1766            self.enableStructureCombo()
1767        else:
1768            self.disableStructureCombo()
1769
1770    def addExtraShells(self):
1771        """
1772        Add a combobox for multiple shell display
1773        """
1774        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1775
1776        if param_length == 0:
1777            return
1778
1779        # cell 1: variable name
1780        item1 = QtGui.QStandardItem(param_name)
1781
1782        func = QtGui.QComboBox()
1783        # Available range of shells displayed in the combobox
1784        func.addItems([str(i) for i in xrange(param_length+1)])
1785
1786        # Respond to index change
1787        func.currentIndexChanged.connect(self.modifyShellsInList)
1788
1789        # cell 2: combobox
1790        item2 = QtGui.QStandardItem()
1791        self._model_model.appendRow([item1, item2])
1792
1793        # Beautify the row:  span columns 2-4
1794        shell_row = self._model_model.rowCount()
1795        shell_index = self._model_model.index(shell_row-1, 1)
1796
1797        self.lstParams.setIndexWidget(shell_index, func)
1798        self._last_model_row = self._model_model.rowCount()
1799
1800        # Set the index to the state-kept value
1801        func.setCurrentIndex(self.current_shell_displayed
1802                             if self.current_shell_displayed < func.count() else 0)
1803
1804    def modifyShellsInList(self, index):
1805        """
1806        Add/remove additional multishell parameters
1807        """
1808        # Find row location of the combobox
1809        last_row = self._last_model_row
1810        remove_rows = self._model_model.rowCount() - last_row
1811
1812        if remove_rows > 1:
1813            self._model_model.removeRows(last_row, remove_rows)
1814
1815        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1816        self.current_shell_displayed = index
1817
1818        # Update relevant models
1819        self.setPolyModel()
1820        self.setMagneticModel()
1821
1822    def readFitPage(self, fp):
1823        """
1824        Read in state from a fitpage object and update GUI
1825        """
1826        assert isinstance(fp, FitPage)
1827        # Main tab info
1828        self.logic.data.filename = fp.filename
1829        self.data_is_loaded = fp.data_is_loaded
1830        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1831        self.chkMagnetism.setCheckState(fp.is_magnetic)
1832        self.chk2DView.setCheckState(fp.is2D)
1833
1834        # Update the comboboxes
1835        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1836        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1837        if fp.current_factor:
1838            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1839
1840        self.chi2 = fp.chi2
1841
1842        # Options tab
1843        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1844        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1845        self.npts = fp.fit_options[fp.NPTS]
1846        self.log_points = fp.fit_options[fp.LOG_POINTS]
1847        self.weighting = fp.fit_options[fp.WEIGHTING]
1848
1849        # Models
1850        self._model_model = fp.model_model
1851        self._poly_model = fp.poly_model
1852        self._magnet_model = fp.magnetism_model
1853
1854        # Resolution tab
1855        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1856        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1857        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1858        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1859        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1860
1861        # TODO: add polidyspersity and magnetism
1862
1863    def saveToFitPage(self, fp):
1864        """
1865        Write current state to the given fitpage
1866        """
1867        assert isinstance(fp, FitPage)
1868
1869        # Main tab info
1870        fp.filename = self.logic.data.filename
1871        fp.data_is_loaded = self.data_is_loaded
1872        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1873        fp.is_magnetic = self.chkMagnetism.isChecked()
1874        fp.is2D = self.chk2DView.isChecked()
1875        fp.data = self.data
1876
1877        # Use current models - they contain all the required parameters
1878        fp.model_model = self._model_model
1879        fp.poly_model = self._poly_model
1880        fp.magnetism_model = self._magnet_model
1881
1882        if self.cbCategory.currentIndex() != 0:
1883            fp.current_category = str(self.cbCategory.currentText())
1884            fp.current_model = str(self.cbModel.currentText())
1885
1886        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1887            fp.current_factor = str(self.cbStructureFactor.currentText())
1888        else:
1889            fp.current_factor = ''
1890
1891        fp.chi2 = self.chi2
1892        fp.parameters_to_fit = self.parameters_to_fit
1893        fp.kernel_module = self.kernel_module
1894
1895        # Algorithm options
1896        # fp.algorithm = self.parent.fit_options.selected_id
1897
1898        # Options tab
1899        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1900        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1901        fp.fit_options[fp.NPTS] = self.npts
1902        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1903        fp.fit_options[fp.LOG_POINTS] = self.log_points
1904        fp.fit_options[fp.WEIGHTING] = self.weighting
1905
1906        # Resolution tab
1907        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1908        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1909        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1910        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1911        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1912
1913        # TODO: add polidyspersity and magnetism
1914
1915
1916    def updateUndo(self):
1917        """
1918        Create a new state page and add it to the stack
1919        """
1920        if self.undo_supported:
1921            self.pushFitPage(self.currentState())
1922
1923    def currentState(self):
1924        """
1925        Return fit page with current state
1926        """
1927        new_page = FitPage()
1928        self.saveToFitPage(new_page)
1929
1930        return new_page
1931
1932    def pushFitPage(self, new_page):
1933        """
1934        Add a new fit page object with current state
1935        """
1936        self.page_stack.append(new_page)
1937
1938    def popFitPage(self):
1939        """
1940        Remove top fit page from stack
1941        """
1942        if self.page_stack:
1943            self.page_stack.pop()
1944
Note: See TracBrowser for help on using the repository browser.