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

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

Converted more syntax not covered by 2to3

  • Property mode set to 100644
File size: 72.1 KB
Line 
1import json
2import os
3from collections import defaultdict
4
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 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        self.onSelectModel()
362        # Smearing tab
363        self.smearing_widget.updateSmearing(self.data)
364
365    def acceptsData(self):
366        """ Tells the caller this widget can accept new dataset """
367        return not self.data_is_loaded
368
369    def disableModelCombo(self):
370        """ Disable the combobox """
371        self.cbModel.setEnabled(False)
372        self.lblModel.setEnabled(False)
373
374    def enableModelCombo(self):
375        """ Enable the combobox """
376        self.cbModel.setEnabled(True)
377        self.lblModel.setEnabled(True)
378
379    def disableStructureCombo(self):
380        """ Disable the combobox """
381        self.cbStructureFactor.setEnabled(False)
382        self.lblStructure.setEnabled(False)
383
384    def enableStructureCombo(self):
385        """ Enable the combobox """
386        self.cbStructureFactor.setEnabled(True)
387        self.lblStructure.setEnabled(True)
388
389    def togglePoly(self, isChecked):
390        """ Enable/disable the polydispersity tab """
391        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
392
393    def toggleMagnetism(self, isChecked):
394        """ Enable/disable the magnetism tab """
395        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
396
397    def toggle2D(self, isChecked):
398        """ Enable/disable the controls dependent on 1D/2D data instance """
399        self.chkMagnetism.setEnabled(isChecked)
400        self.is2D = isChecked
401        # Reload the current model
402        if self.kernel_module:
403            self.onSelectModel()
404
405    def initializeControls(self):
406        """
407        Set initial control enablement
408        """
409        self.cbFileNames.setVisible(False)
410        self.cmdFit.setEnabled(False)
411        self.cmdPlot.setEnabled(False)
412        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
413        self.chkPolydispersity.setEnabled(True)
414        self.chkPolydispersity.setCheckState(False)
415        self.chk2DView.setEnabled(True)
416        self.chk2DView.setCheckState(False)
417        self.chkMagnetism.setEnabled(False)
418        self.chkMagnetism.setCheckState(False)
419        # Tabs
420        self.tabFitting.setTabEnabled(TAB_POLY, False)
421        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
422        self.lblChi2Value.setText("---")
423        # Smearing tab
424        self.smearing_widget.updateSmearing(self.data)
425        # Line edits in the option tab
426        self.updateQRange()
427
428    def initializeSignals(self):
429        """
430        Connect GUI element signals
431        """
432        # Comboboxes
433        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
434        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
435        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
436        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
437        # Checkboxes
438        self.chk2DView.toggled.connect(self.toggle2D)
439        self.chkPolydispersity.toggled.connect(self.togglePoly)
440        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
441        # Buttons
442        self.cmdFit.clicked.connect(self.onFit)
443        self.cmdPlot.clicked.connect(self.onPlot)
444        self.cmdHelp.clicked.connect(self.onHelp)
445        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
446
447        # Respond to change in parameters from the UI
448        self._model_model.itemChanged.connect(self.onMainParamsChange)
449        self._poly_model.itemChanged.connect(self.onPolyModelChange)
450        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
451
452        # Signals from separate tabs asking for replot
453        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
454
455    def showModelDescription(self, position):
456        """
457        Shows a window with model description, when right clicked in the treeview
458        """
459        msg = 'Model description:\n'
460        if self.kernel_module is not None:
461            if str(self.kernel_module.description).rstrip().lstrip() == '':
462                msg += "Sorry, no information is available for this model."
463            else:
464                msg += self.kernel_module.description + '\n'
465        else:
466            msg += "You must select a model to get information on this"
467
468        menu = QtGui.QMenu()
469        label = QtGui.QLabel(msg)
470        action = QtGui.QWidgetAction(self)
471        action.setDefaultWidget(label)
472        menu.addAction(action)
473        menu.exec_(self.lstParams.viewport().mapToGlobal(position))
474
475    def onSelectModel(self):
476        """
477        Respond to select Model from list event
478        """
479        model = str(self.cbModel.currentText())
480
481        # empty combobox forced to be read
482        if not model:
483            return
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(0, 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 = list(zip(param_values, param_stderr))
826        param_dict = dict(zip(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 range(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 list(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 list(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 list(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 range(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 list(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 list(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        if self._model_model.rowCount() == 0:
998            return
999
1000        def iterateOverMagnetModel(func):
1001            """
1002            Take func and throw it inside the magnet model row loop
1003            """
1004            for row_i in range(self._model_model.rowCount()):
1005                func(row_i)
1006
1007        def updateFittedValues(row):
1008            # Utility function for main model update
1009            # internal so can use closure for param_dict
1010            if self._magnet_model.item(row, 0) is None:
1011                return
1012            param_name = str(self._magnet_model.item(row, 0).text())
1013            if param_name not in list(param_dict.keys()):
1014                return
1015            # modify the param value
1016            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1017            self._magnet_model.item(row, 1).setText(param_repr)
1018            if self.has_magnet_error_column:
1019                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1020                self._magnet_model.item(row, 2).setText(error_repr)
1021
1022        def createErrorColumn(row):
1023            # Utility function for error column update
1024            item = QtGui.QStandardItem()
1025            def createItem(param_name):
1026                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1027                item.setText(error_repr)
1028            def curr_param():
1029                return str(self._magnet_model.item(row, 0).text())
1030
1031            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1032
1033            error_column.append(item)
1034
1035        # block signals temporarily, so we don't end up
1036        # updating charts with every single model change on the end of fitting
1037        self._magnet_model.blockSignals(True)
1038        iterateOverMagnetModel(updateFittedValues)
1039        self._magnet_model.blockSignals(False)
1040
1041        if self.has_magnet_error_column:
1042            return
1043
1044        self.lstMagnetic.itemDelegate().addErrorColumn()
1045        error_column = []
1046        iterateOverMagnetModel(createErrorColumn)
1047
1048        # switch off reponse to model change
1049        self._magnet_model.blockSignals(True)
1050        self._magnet_model.insertColumn(2, error_column)
1051        self._magnet_model.blockSignals(False)
1052        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1053
1054        self.has_magnet_error_column = True
1055
1056    def onPlot(self):
1057        """
1058        Plot the current set of data
1059        """
1060        # Regardless of previous state, this should now be `plot show` functionality only
1061        self.cmdPlot.setText("Show Plot")
1062        # Force data recalculation so existing charts are updated
1063        self.recalculatePlotData()
1064        self.showPlot()
1065
1066    def recalculatePlotData(self):
1067        """
1068        Generate a new dataset for model
1069        """
1070        if not self.data_is_loaded:
1071            self.createDefaultDataset()
1072        self.calculateQGridForModel()
1073
1074    def showPlot(self):
1075        """
1076        Show the current plot in MPL
1077        """
1078        # Show the chart if ready
1079        data_to_show = self.data if self.data_is_loaded else self.model_data
1080        if data_to_show is not None:
1081            self.communicate.plotRequestedSignal.emit([data_to_show])
1082
1083    def onOptionsUpdate(self):
1084        """
1085        Update local option values and replot
1086        """
1087        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1088            self.options_widget.state()
1089        # set Q range labels on the main tab
1090        self.lblMinRangeDef.setText(str(self.q_range_min))
1091        self.lblMaxRangeDef.setText(str(self.q_range_max))
1092        self.recalculatePlotData()
1093
1094    def setDefaultStructureCombo(self):
1095        """
1096        Fill in the structure factors combo box with defaults
1097        """
1098        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1099        factors = [factor[0] for factor in structure_factor_list]
1100        factors.insert(0, STRUCTURE_DEFAULT)
1101        self.cbStructureFactor.clear()
1102        self.cbStructureFactor.addItems(sorted(factors))
1103
1104    def createDefaultDataset(self):
1105        """
1106        Generate default Dataset 1D/2D for the given model
1107        """
1108        # Create default datasets if no data passed
1109        if self.is2D:
1110            qmax = self.q_range_max/np.sqrt(2)
1111            qstep = self.npts
1112            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1113            return
1114        elif self.log_points:
1115            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1116            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1117            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1118        else:
1119            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1120                                   num=self.npts, endpoint=True)
1121        self.logic.createDefault1dData(interval, self.tab_id)
1122
1123    def readCategoryInfo(self):
1124        """
1125        Reads the categories in from file
1126        """
1127        self.master_category_dict = defaultdict(list)
1128        self.by_model_dict = defaultdict(list)
1129        self.model_enabled_dict = defaultdict(bool)
1130
1131        categorization_file = CategoryInstaller.get_user_file()
1132        if not os.path.isfile(categorization_file):
1133            categorization_file = CategoryInstaller.get_default_file()
1134        with open(categorization_file, 'rb') as cat_file:
1135            self.master_category_dict = json.load(cat_file)
1136            self.regenerateModelDict()
1137
1138        # Load the model dict
1139        models = load_standard_models()
1140        for model in models:
1141            self.models[model.name] = model
1142
1143    def regenerateModelDict(self):
1144        """
1145        Regenerates self.by_model_dict which has each model name as the
1146        key and the list of categories belonging to that model
1147        along with the enabled mapping
1148        """
1149        self.by_model_dict = defaultdict(list)
1150        for category in self.master_category_dict:
1151            for (model, enabled) in self.master_category_dict[category]:
1152                self.by_model_dict[model].append(category)
1153                self.model_enabled_dict[model] = enabled
1154
1155    def addBackgroundToModel(self, model):
1156        """
1157        Adds background parameter with default values to the model
1158        """
1159        assert isinstance(model, QtGui.QStandardItemModel)
1160        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1161        FittingUtilities.addCheckedListToModel(model, checked_list)
1162        last_row = model.rowCount()-1
1163        model.item(last_row, 0).setEditable(False)
1164        model.item(last_row, 4).setEditable(False)
1165
1166    def addScaleToModel(self, model):
1167        """
1168        Adds scale parameter with default values to the model
1169        """
1170        assert isinstance(model, QtGui.QStandardItemModel)
1171        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1172        FittingUtilities.addCheckedListToModel(model, checked_list)
1173        last_row = model.rowCount()-1
1174        model.item(last_row, 0).setEditable(False)
1175        model.item(last_row, 4).setEditable(False)
1176
1177    def addWeightingToData(self, data):
1178        """
1179        Adds weighting contribution to fitting data
1180        """
1181        # Send original data for weighting
1182        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1183        update_module = data.err_data if self.is2D else data.dy
1184        # Overwrite relevant values in data
1185        update_module = weight
1186
1187    def updateQRange(self):
1188        """
1189        Updates Q Range display
1190        """
1191        if self.data_is_loaded:
1192            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1193        # set Q range labels on the main tab
1194        self.lblMinRangeDef.setText(str(self.q_range_min))
1195        self.lblMaxRangeDef.setText(str(self.q_range_max))
1196        # set Q range labels on the options tab
1197        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1198
1199    def SASModelToQModel(self, model_name, structure_factor=None):
1200        """
1201        Setting model parameters into table based on selected category
1202        """
1203        # Crete/overwrite model items
1204        self._model_model.clear()
1205
1206        # First, add parameters from the main model
1207        if model_name is not None:
1208            self.fromModelToQModel(model_name)
1209
1210        # Then, add structure factor derived parameters
1211        if structure_factor is not None and structure_factor != "None":
1212            if model_name is None:
1213                # Instantiate the current sasmodel for SF-only models
1214                self.kernel_module = self.models[structure_factor]()
1215            self.fromStructureFactorToQModel(structure_factor)
1216        else:
1217            # Allow the SF combobox visibility for the given sasmodel
1218            self.enableStructureFactorControl(structure_factor)
1219
1220        # Then, add multishells
1221        if model_name is not None:
1222            # Multishell models need additional treatment
1223            self.addExtraShells()
1224
1225        # Add polydispersity to the model
1226        self.setPolyModel()
1227        # Add magnetic parameters to the model
1228        self.setMagneticModel()
1229
1230        # Adjust the table cells width
1231        self.lstParams.resizeColumnToContents(0)
1232        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1233
1234        # Now we claim the model has been loaded
1235        self.model_is_loaded = True
1236
1237        # (Re)-create headers
1238        FittingUtilities.addHeadersToModel(self._model_model)
1239        self.lstParams.header().setFont(self.boldFont)
1240
1241        # Update Q Ranges
1242        self.updateQRange()
1243
1244    def fromModelToQModel(self, model_name):
1245        """
1246        Setting model parameters into QStandardItemModel based on selected _model_
1247        """
1248        kernel_module = generate.load_kernel_module(model_name)
1249        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1250
1251        # Instantiate the current sasmodel
1252        self.kernel_module = self.models[model_name]()
1253
1254        # Explicitly add scale and background with default values
1255        temp_undo_state = self.undo_supported
1256        self.undo_supported = False
1257        self.addScaleToModel(self._model_model)
1258        self.addBackgroundToModel(self._model_model)
1259        self.undo_supported = temp_undo_state
1260
1261        self.shell_names = self.shellNamesList()
1262
1263        # Update the QModel
1264        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1265
1266        for row in new_rows:
1267            self._model_model.appendRow(row)
1268        # Update the counter used for multishell display
1269        self._last_model_row = self._model_model.rowCount()
1270
1271    def fromStructureFactorToQModel(self, structure_factor):
1272        """
1273        Setting model parameters into QStandardItemModel based on selected _structure factor_
1274        """
1275        structure_module = generate.load_kernel_module(structure_factor)
1276        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1277        structure_kernel = self.models[structure_factor]()
1278
1279        self.kernel_module._model_info = product.make_product_info(self.kernel_module._model_info, structure_kernel._model_info)
1280
1281        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1282        for row in new_rows:
1283            self._model_model.appendRow(row)
1284        # Update the counter used for multishell display
1285        self._last_model_row = self._model_model.rowCount()
1286
1287    def onMainParamsChange(self, item):
1288        """
1289        Callback method for updating the sasmodel parameters with the GUI values
1290        """
1291        model_column = item.column()
1292
1293        if model_column == 0:
1294            self.checkboxSelected(item)
1295            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1296            # Update state stack
1297            self.updateUndo()
1298            return
1299
1300        model_row = item.row()
1301        name_index = self._model_model.index(model_row, 0)
1302
1303        # Extract changed value.
1304        try:
1305            value = float(item.text())
1306        except ValueError:
1307            # Unparsable field
1308            return
1309        parameter_name = str(self._model_model.data(name_index)) #.toPyObject()) # sld, background etc.
1310
1311        # Update the parameter value - note: this supports +/-inf as well
1312        self.kernel_module.params[parameter_name] = value
1313
1314        # Update the parameter value - note: this supports +/-inf as well
1315        param_column = self.lstParams.itemDelegate().param_value
1316        min_column = self.lstParams.itemDelegate().param_min
1317        max_column = self.lstParams.itemDelegate().param_max
1318        if model_column == param_column:
1319            self.kernel_module.setParam(parameter_name, value)
1320        elif model_column == min_column:
1321            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1322            self.kernel_module.details[parameter_name][1] = value
1323        elif model_column == max_column:
1324            self.kernel_module.details[parameter_name][2] = value
1325        else:
1326            # don't update the chart
1327            return
1328
1329        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1330        # TODO: multishell params in self.kernel_module.details[??] = value
1331
1332        # Force the chart update when actual parameters changed
1333        if model_column == 1:
1334            self.recalculatePlotData()
1335
1336        # Update state stack
1337        self.updateUndo()
1338
1339    def checkboxSelected(self, item):
1340        # Assure we're dealing with checkboxes
1341        if not item.isCheckable():
1342            return
1343        status = item.checkState()
1344
1345        def isCheckable(row):
1346            return self._model_model.item(row, 0).isCheckable()
1347
1348        # If multiple rows selected - toggle all of them, filtering uncheckable
1349        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1350
1351        # Switch off signaling from the model to avoid recursion
1352        self._model_model.blockSignals(True)
1353        # Convert to proper indices and set requested enablement
1354        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1355        self._model_model.blockSignals(False)
1356
1357        # update the list of parameters to fit
1358        main_params = self.checkedListFromModel(self._model_model)
1359        poly_params = self.checkedListFromModel(self._poly_model)
1360        magnet_params = self.checkedListFromModel(self._magnet_model)
1361
1362        # Retrieve poly params names
1363        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1364
1365        self.parameters_to_fit = main_params + poly_params + magnet_params
1366
1367    def checkedListFromModel(self, model):
1368        """
1369        Returns list of checked parameters for given model
1370        """
1371        def isChecked(row):
1372            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1373
1374        return [str(model.item(row_index, 0).text())
1375                for row_index in range(model.rowCount())
1376                if isChecked(row_index)]
1377
1378    def nameForFittedData(self, name):
1379        """
1380        Generate name for the current fit
1381        """
1382        if self.is2D:
1383            name += "2d"
1384        name = "M%i [%s]" % (self.tab_id, name)
1385        return name
1386
1387    def createNewIndex(self, fitted_data):
1388        """
1389        Create a model or theory index with passed Data1D/Data2D
1390        """
1391        if self.data_is_loaded:
1392            if not fitted_data.name:
1393                name = self.nameForFittedData(self.data.filename)
1394                fitted_data.title = name
1395                fitted_data.name = name
1396                fitted_data.filename = name
1397                fitted_data.symbol = "Line"
1398            self.updateModelIndex(fitted_data)
1399        else:
1400            name = self.nameForFittedData(self.kernel_module.name)
1401            fitted_data.title = name
1402            fitted_data.name = name
1403            fitted_data.filename = name
1404            fitted_data.symbol = "Line"
1405            self.createTheoryIndex(fitted_data)
1406
1407    def updateModelIndex(self, fitted_data):
1408        """
1409        Update a QStandardModelIndex containing model data
1410        """
1411        name = self.nameFromData(fitted_data)
1412        # Make this a line if no other defined
1413        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1414            fitted_data.symbol = 'Line'
1415        # Notify the GUI manager so it can update the main model in DataExplorer
1416        GuiUtils.updateModelItemWithPlot(self._index, fitted_data, name)
1417
1418    def createTheoryIndex(self, fitted_data):
1419        """
1420        Create a QStandardModelIndex containing model data
1421        """
1422        name = self.nameFromData(fitted_data)
1423        # Notify the GUI manager so it can create the theory model in DataExplorer
1424        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
1425        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1426
1427    def nameFromData(self, fitted_data):
1428        """
1429        Return name for the dataset. Terribly impure function.
1430        """
1431        if fitted_data.name is None:
1432            name = self.nameForFittedData(self.logic.data.filename)
1433            fitted_data.title = name
1434            fitted_data.name = name
1435            fitted_data.filename = name
1436        else:
1437            name = fitted_data.name
1438        return name
1439
1440    def methodCalculateForData(self):
1441        '''return the method for data calculation'''
1442        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1443
1444    def methodCompleteForData(self):
1445        '''return the method for result parsin on calc complete '''
1446        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1447
1448    def calculateQGridForModel(self):
1449        """
1450        Prepare the fitting data object, based on current ModelModel
1451        """
1452        if self.kernel_module is None:
1453            return
1454        # Awful API to a backend method.
1455        method = self.methodCalculateForData()(data=self.data,
1456                                               model=self.kernel_module,
1457                                               page_id=0,
1458                                               qmin=self.q_range_min,
1459                                               qmax=self.q_range_max,
1460                                               smearer=None,
1461                                               state=None,
1462                                               weight=None,
1463                                               fid=None,
1464                                               toggle_mode_on=False,
1465                                               completefn=None,
1466                                               update_chisqr=True,
1467                                               exception_handler=self.calcException,
1468                                               source=None)
1469
1470        calc_thread = threads.deferToThread(method.compute)
1471        calc_thread.addCallback(self.methodCompleteForData())
1472        calc_thread.addErrback(self.calculateDataFailed)
1473
1474    def calculateDataFailed(self, reason):
1475        """
1476        Thread returned error
1477        """
1478        print("Calculate Data failed with ", reason)
1479
1480    def complete1D(self, return_data):
1481        """
1482        Plot the current 1D data
1483        """
1484        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1485        self.calculateResiduals(fitted_data)
1486        self.model_data = fitted_data
1487
1488    def complete2D(self, return_data):
1489        """
1490        Plot the current 2D data
1491        """
1492        fitted_data = self.logic.new2DPlot(return_data)
1493        self.calculateResiduals(fitted_data)
1494        self.model_data = fitted_data
1495
1496    def calculateResiduals(self, fitted_data):
1497        """
1498        Calculate and print Chi2 and display chart of residuals
1499        """
1500        # Create a new index for holding data
1501        fitted_data.symbol = "Line"
1502
1503        # Modify fitted_data with weighting
1504        self.addWeightingToData(fitted_data)
1505
1506        self.createNewIndex(fitted_data)
1507        # Calculate difference between return_data and logic.data
1508        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1509        # Update the control
1510        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1511        self.lblChi2Value.setText(chi2_repr)
1512
1513        self.communicate.plotUpdateSignal.emit([fitted_data])
1514
1515        # Plot residuals if actual data
1516        if not self.data_is_loaded:
1517            return
1518
1519        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1520        residuals_plot.id = "Residual " + residuals_plot.id
1521        self.createNewIndex(residuals_plot)
1522        self.communicate.plotUpdateSignal.emit([residuals_plot])
1523
1524    def calcException(self, etype, value, tb):
1525        """
1526        Thread threw an exception.
1527        """
1528        # TODO: remimplement thread cancellation
1529        logging.error("".join(traceback.format_exception(etype, value, tb)))
1530
1531    def setTableProperties(self, table):
1532        """
1533        Setting table properties
1534        """
1535        # Table properties
1536        table.verticalHeader().setVisible(False)
1537        table.setAlternatingRowColors(True)
1538        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1539        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1540        table.resizeColumnsToContents()
1541
1542        # Header
1543        header = table.horizontalHeader()
1544        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1545
1546        header.ResizeMode(QtGui.QHeaderView.Interactive)
1547        # Resize column 0 and 7 to content
1548        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1549        header.setResizeMode(7, QtGui.QHeaderView.ResizeToContents)
1550
1551    def setPolyModel(self):
1552        """
1553        Set polydispersity values
1554        """
1555        if not self.model_parameters:
1556            return
1557        self._poly_model.clear()
1558
1559        [self.setPolyModelParameters(i, param) for i, param in \
1560            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1561        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1562
1563    def setPolyModelParameters(self, i, param):
1564        """
1565        Standard of multishell poly parameter driver
1566        """
1567        param_name = param.name
1568        # see it the parameter is multishell
1569        if '[' in param.name:
1570            # Skip empty shells
1571            if self.current_shell_displayed == 0:
1572                return
1573            else:
1574                # Create as many entries as current shells
1575                for ishell in range(1, self.current_shell_displayed+1):
1576                    # Remove [n] and add the shell numeral
1577                    name = param_name[0:param_name.index('[')] + str(ishell)
1578                    self.addNameToPolyModel(i, name)
1579        else:
1580            # Just create a simple param entry
1581            self.addNameToPolyModel(i, param_name)
1582
1583    def addNameToPolyModel(self, i, param_name):
1584        """
1585        Creates a checked row in the poly model with param_name
1586        """
1587        # Polydisp. values from the sasmodel
1588        width = self.kernel_module.getParam(param_name + '.width')
1589        npts = self.kernel_module.getParam(param_name + '.npts')
1590        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1591        _, min, max = self.kernel_module.details[param_name]
1592
1593        # Construct a row with polydisp. related variable.
1594        # This will get added to the polydisp. model
1595        # Note: last argument needs extra space padding for decent display of the control
1596        checked_list = ["Distribution of " + param_name, str(width),
1597                        str(min), str(max),
1598                        str(npts), str(nsigs), "gaussian      ",'']
1599        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1600
1601        # All possible polydisp. functions as strings in combobox
1602        func = QtGui.QComboBox()
1603        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
1604        # Set the default index
1605        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1606        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1607        self.lstPoly.setIndexWidget(ind, func)
1608        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1609
1610    def onPolyFilenameChange(self, row_index):
1611        """
1612        Respond to filename_updated signal from the delegate
1613        """
1614        # For the given row, invoke the "array" combo handler
1615        array_caption = 'array'
1616
1617        # Get the combo box reference
1618        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1619        widget = self.lstPoly.indexWidget(ind)
1620
1621        # Update the combo box so it displays "array"
1622        widget.blockSignals(True)
1623        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1624        widget.blockSignals(False)
1625
1626        # Invoke the file reader
1627        self.onPolyComboIndexChange(array_caption, row_index)
1628
1629    def onPolyComboIndexChange(self, combo_string, row_index):
1630        """
1631        Modify polydisp. defaults on function choice
1632        """
1633        # Get npts/nsigs for current selection
1634        param = self.model_parameters.form_volume_parameters[row_index]
1635        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1636        combo_box = self.lstPoly.indexWidget(file_index)
1637
1638        def updateFunctionCaption(row):
1639            # Utility function for update of polydispersity function name in the main model
1640            param_name = str(self._model_model.item(row, 0).text())
1641            if param_name !=  param.name:
1642                return
1643            # Modify the param value
1644            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1645
1646        if combo_string == 'array':
1647            try:
1648                self.loadPolydispArray(row_index)
1649                # Update main model for display
1650                self.iterateOverModel(updateFunctionCaption)
1651                # disable the row
1652                lo = self.lstPoly.itemDelegate().poly_pd
1653                hi = self.lstPoly.itemDelegate().poly_function
1654                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
1655                return
1656            except IOError:
1657                combo_box.setCurrentIndex(self.orig_poly_index)
1658                # Pass for cancel/bad read
1659                pass
1660
1661        # Enable the row in case it was disabled by Array
1662        self._poly_model.blockSignals(True)
1663        max_range = self.lstPoly.itemDelegate().poly_filename
1664        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
1665        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1666        self._poly_model.setData(file_index, "")
1667        self._poly_model.blockSignals(False)
1668
1669        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1670        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1671
1672        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1673        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1674
1675        self._poly_model.setData(npts_index, npts)
1676        self._poly_model.setData(nsigs_index, nsigs)
1677
1678        self.iterateOverModel(updateFunctionCaption)
1679        self.orig_poly_index = combo_box.currentIndex()
1680
1681    def loadPolydispArray(self, row_index):
1682        """
1683        Show the load file dialog and loads requested data into state
1684        """
1685        datafile = QtGui.QFileDialog.getOpenFileName(
1686            self, "Choose a weight file", "", "All files (*.*)",
1687            QtGui.QFileDialog.DontUseNativeDialog)
1688
1689        if datafile is None or str(datafile)=='':
1690            logging.info("No weight data chosen.")
1691            raise IOError
1692
1693        values = []
1694        weights = []
1695        def appendData(data_tuple):
1696            """
1697            Fish out floats from a tuple of strings
1698            """
1699            try:
1700                values.append(float(data_tuple[0]))
1701                weights.append(float(data_tuple[1]))
1702            except (ValueError, IndexError):
1703                # just pass through if line with bad data
1704                return
1705
1706        with open(datafile, 'r') as column_file:
1707            column_data = [line.rstrip().split() for line in column_file.readlines()]
1708            [appendData(line) for line in column_data]
1709
1710        # If everything went well - update the sasmodel values
1711        self.disp_model = POLYDISPERSITY_MODELS['array']()
1712        self.disp_model.set_weights(np.array(values), np.array(weights))
1713        # + update the cell with filename
1714        fname = os.path.basename(str(datafile))
1715        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1716        self._poly_model.setData(fname_index, fname)
1717
1718    def setMagneticModel(self):
1719        """
1720        Set magnetism values on model
1721        """
1722        if not self.model_parameters:
1723            return
1724        self._magnet_model.clear()
1725        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1726            self.model_parameters.call_parameters if param.type == 'magnetic']
1727        FittingUtilities.addHeadersToModel(self._magnet_model)
1728
1729    def shellNamesList(self):
1730        """
1731        Returns list of names of all multi-shell parameters
1732        E.g. for sld[n], radius[n], n=1..3 it will return
1733        [sld1, sld2, sld3, radius1, radius2, radius3]
1734        """
1735        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1736        top_index = self.kernel_module.multiplicity_info.number
1737        shell_names = []
1738        for i in range(1, top_index+1):
1739            for name in multi_names:
1740                shell_names.append(name+str(i))
1741        return shell_names
1742
1743    def addCheckedMagneticListToModel(self, param, model):
1744        """
1745        Wrapper for model update with a subset of magnetic parameters
1746        """
1747        if param.name[param.name.index(':')+1:] in self.shell_names:
1748            # check if two-digit shell number
1749            try:
1750                shell_index = int(param.name[-2:])
1751            except ValueError:
1752                shell_index = int(param.name[-1:])
1753
1754            if shell_index > self.current_shell_displayed:
1755                return
1756
1757        checked_list = [param.name,
1758                        str(param.default),
1759                        str(param.limits[0]),
1760                        str(param.limits[1]),
1761                        param.units]
1762
1763        FittingUtilities.addCheckedListToModel(model, checked_list)
1764
1765    def enableStructureFactorControl(self, structure_factor):
1766        """
1767        Add structure factors to the list of parameters
1768        """
1769        if self.kernel_module.is_form_factor or structure_factor == 'None':
1770            self.enableStructureCombo()
1771        else:
1772            self.disableStructureCombo()
1773
1774    def addExtraShells(self):
1775        """
1776        Add a combobox for multiple shell display
1777        """
1778        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1779
1780        if param_length == 0:
1781            return
1782
1783        # cell 1: variable name
1784        item1 = QtGui.QStandardItem(param_name)
1785
1786        func = QtGui.QComboBox()
1787        # Available range of shells displayed in the combobox
1788        func.addItems([str(i) for i in range(param_length+1)])
1789
1790        # Respond to index change
1791        func.currentIndexChanged.connect(self.modifyShellsInList)
1792
1793        # cell 2: combobox
1794        item2 = QtGui.QStandardItem()
1795        self._model_model.appendRow([item1, item2])
1796
1797        # Beautify the row:  span columns 2-4
1798        shell_row = self._model_model.rowCount()
1799        shell_index = self._model_model.index(shell_row-1, 1)
1800
1801        self.lstParams.setIndexWidget(shell_index, func)
1802        self._last_model_row = self._model_model.rowCount()
1803
1804        # Set the index to the state-kept value
1805        func.setCurrentIndex(self.current_shell_displayed
1806                             if self.current_shell_displayed < func.count() else 0)
1807
1808    def modifyShellsInList(self, index):
1809        """
1810        Add/remove additional multishell parameters
1811        """
1812        # Find row location of the combobox
1813        last_row = self._last_model_row
1814        remove_rows = self._model_model.rowCount() - last_row
1815
1816        if remove_rows > 1:
1817            self._model_model.removeRows(last_row, remove_rows)
1818
1819        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1820        self.current_shell_displayed = index
1821
1822        # Update relevant models
1823        self.setPolyModel()
1824        self.setMagneticModel()
1825
1826    def readFitPage(self, fp):
1827        """
1828        Read in state from a fitpage object and update GUI
1829        """
1830        assert isinstance(fp, FitPage)
1831        # Main tab info
1832        self.logic.data.filename = fp.filename
1833        self.data_is_loaded = fp.data_is_loaded
1834        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1835        self.chkMagnetism.setCheckState(fp.is_magnetic)
1836        self.chk2DView.setCheckState(fp.is2D)
1837
1838        # Update the comboboxes
1839        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1840        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1841        if fp.current_factor:
1842            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1843
1844        self.chi2 = fp.chi2
1845
1846        # Options tab
1847        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1848        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1849        self.npts = fp.fit_options[fp.NPTS]
1850        self.log_points = fp.fit_options[fp.LOG_POINTS]
1851        self.weighting = fp.fit_options[fp.WEIGHTING]
1852
1853        # Models
1854        self._model_model = fp.model_model
1855        self._poly_model = fp.poly_model
1856        self._magnet_model = fp.magnetism_model
1857
1858        # Resolution tab
1859        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1860        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1861        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1862        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1863        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1864
1865        # TODO: add polidyspersity and magnetism
1866
1867    def saveToFitPage(self, fp):
1868        """
1869        Write current state to the given fitpage
1870        """
1871        assert isinstance(fp, FitPage)
1872
1873        # Main tab info
1874        fp.filename = self.logic.data.filename
1875        fp.data_is_loaded = self.data_is_loaded
1876        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1877        fp.is_magnetic = self.chkMagnetism.isChecked()
1878        fp.is2D = self.chk2DView.isChecked()
1879        fp.data = self.data
1880
1881        # Use current models - they contain all the required parameters
1882        fp.model_model = self._model_model
1883        fp.poly_model = self._poly_model
1884        fp.magnetism_model = self._magnet_model
1885
1886        if self.cbCategory.currentIndex() != 0:
1887            fp.current_category = str(self.cbCategory.currentText())
1888            fp.current_model = str(self.cbModel.currentText())
1889
1890        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1891            fp.current_factor = str(self.cbStructureFactor.currentText())
1892        else:
1893            fp.current_factor = ''
1894
1895        fp.chi2 = self.chi2
1896        fp.parameters_to_fit = self.parameters_to_fit
1897        fp.kernel_module = self.kernel_module
1898
1899        # Algorithm options
1900        # fp.algorithm = self.parent.fit_options.selected_id
1901
1902        # Options tab
1903        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1904        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1905        fp.fit_options[fp.NPTS] = self.npts
1906        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1907        fp.fit_options[fp.LOG_POINTS] = self.log_points
1908        fp.fit_options[fp.WEIGHTING] = self.weighting
1909
1910        # Resolution tab
1911        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1912        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1913        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1914        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1915        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1916
1917        # TODO: add polidyspersity and magnetism
1918
1919
1920    def updateUndo(self):
1921        """
1922        Create a new state page and add it to the stack
1923        """
1924        if self.undo_supported:
1925            self.pushFitPage(self.currentState())
1926
1927    def currentState(self):
1928        """
1929        Return fit page with current state
1930        """
1931        new_page = FitPage()
1932        self.saveToFitPage(new_page)
1933
1934        return new_page
1935
1936    def pushFitPage(self, new_page):
1937        """
1938        Add a new fit page object with current state
1939        """
1940        self.page_stack.append(new_page)
1941
1942    def popFitPage(self):
1943        """
1944        Remove top fit page from stack
1945        """
1946        if self.page_stack:
1947            self.page_stack.pop()
1948
Note: See TracBrowser for help on using the repository browser.