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

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

Show help pages in default browser. Fixed some help links and modified unit tests. SASVIEW-800

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