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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9e587bc was 62c6dc0, checked in by piotr, 6 years ago

Disable model update on showing read-only values. SASVIEW-1092

  • Property mode set to 100644
File size: 140.9 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5import copy
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 generate
17from sasmodels import modelinfo
18from sasmodels.sasview_model import load_standard_models
19from sasmodels.sasview_model import MultiplicationModel
20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
23from sas.sascalc.fit.pagestate import PageState
24
25import sas.qtgui.Utilities.GuiUtils as GuiUtils
26import sas.qtgui.Utilities.LocalConfig as LocalConfig
27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
30from sas.qtgui.Plotting.Plotter import PlotterWidget
31
32from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
33from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
34from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
35
36from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
38from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
39from sas.qtgui.Perspectives.Fitting import FittingUtilities
40from sas.qtgui.Perspectives.Fitting import ModelUtilities
41from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
42from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
43from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
44from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
45from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
46from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
47from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
48from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
49from sas.qtgui.Perspectives.Fitting.ReportPageLogic import ReportPageLogic
50
51TAB_MAGNETISM = 4
52TAB_POLY = 3
53CATEGORY_DEFAULT = "Choose category..."
54CATEGORY_STRUCTURE = "Structure Factor"
55CATEGORY_CUSTOM = "Plugin Models"
56STRUCTURE_DEFAULT = "None"
57
58DEFAULT_POLYDISP_FUNCTION = 'gaussian'
59
60# CRUFT: remove when new release of sasmodels is available
61# https://github.com/SasView/sasview/pull/181#discussion_r218135162
62from sasmodels.sasview_model import SasviewModel
63if not hasattr(SasviewModel, 'get_weights'):
64    def get_weights(self, name):
65        """
66        Returns the polydispersity distribution for parameter *name* as *value* and *weight* arrays.
67        """
68        # type: (str) -> Tuple(np.ndarray, np.ndarray)
69        _, x, w = self._get_weights(self._model_info.parameters[name])
70        return x, w
71
72    SasviewModel.get_weights = get_weights
73
74logger = logging.getLogger(__name__)
75
76class ToolTippedItemModel(QtGui.QStandardItemModel):
77    """
78    Subclass from QStandardItemModel to allow displaying tooltips in
79    QTableView model.
80    """
81    def __init__(self, parent=None):
82        QtGui.QStandardItemModel.__init__(self, parent)
83
84    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
85        """
86        Displays tooltip for each column's header
87        :param section:
88        :param orientation:
89        :param role:
90        :return:
91        """
92        if role == QtCore.Qt.ToolTipRole:
93            if orientation == QtCore.Qt.Horizontal:
94                return str(self.header_tooltips[section])
95
96        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
97
98class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
99    """
100    Main widget for selecting form and structure factor models
101    """
102    constraintAddedSignal = QtCore.pyqtSignal(list)
103    newModelSignal = QtCore.pyqtSignal()
104    fittingFinishedSignal = QtCore.pyqtSignal(tuple)
105    batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
106    Calc1DFinishedSignal = QtCore.pyqtSignal(dict)
107    Calc2DFinishedSignal = QtCore.pyqtSignal(dict)
108
109    def __init__(self, parent=None, data=None, tab_id=1):
110
111        super(FittingWidget, self).__init__()
112
113        # Necessary globals
114        self.parent = parent
115
116        # Which tab is this widget displayed in?
117        self.tab_id = tab_id
118
119        # Globals
120        self.initializeGlobals()
121
122        # data index for the batch set
123        self.data_index = 0
124        # Main Data[12]D holders
125        # Logics.data contains a single Data1D/Data2D object
126        self._logic = [FittingLogic()]
127
128        # Main GUI setup up
129        self.setupUi(self)
130        self.setWindowTitle("Fitting")
131
132        # Set up tabs widgets
133        self.initializeWidgets()
134
135        # Set up models and views
136        self.initializeModels()
137
138        # Defaults for the structure factors
139        self.setDefaultStructureCombo()
140
141        # Make structure factor and model CBs disabled
142        self.disableModelCombo()
143        self.disableStructureCombo()
144
145        # Generate the category list for display
146        self.initializeCategoryCombo()
147
148        # Initial control state
149        self.initializeControls()
150
151        QtWidgets.QApplication.processEvents()
152
153        # Connect signals to controls
154        self.initializeSignals()
155
156        if data is not None:
157            self.data = data
158
159        # New font to display angstrom symbol
160        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
161        self.label_17.setStyleSheet(new_font)
162        self.label_19.setStyleSheet(new_font)
163
164    @property
165    def logic(self):
166        # make sure the logic contains at least one element
167        assert self._logic
168        # logic connected to the currently shown data
169        return self._logic[self.data_index]
170
171    @property
172    def data(self):
173        return self.logic.data
174
175    @data.setter
176    def data(self, value):
177        """ data setter """
178        # Value is either a list of indices for batch fitting or a simple index
179        # for standard fitting. Assure we have a list, regardless.
180        if isinstance(value, list):
181            self.is_batch_fitting = True
182        else:
183            value = [value]
184
185        assert isinstance(value[0], QtGui.QStandardItem)
186
187        # Keep reference to all datasets for batch
188        self.all_data = value
189
190        # Create logics with data items
191        # Logics.data contains only a single Data1D/Data2D object
192        if len(value) == 1:
193            # single data logic is already defined, update data on it
194            self._logic[0].data = GuiUtils.dataFromItem(value[0])
195        else:
196            # batch datasets
197            self._logic = []
198            for data_item in value:
199                logic = FittingLogic(data=GuiUtils.dataFromItem(data_item))
200                self._logic.append(logic)
201
202        # Overwrite data type descriptor
203
204        self.is2D = True if isinstance(self.logic.data, Data2D) else False
205
206        # Let others know we're full of data now
207        self.data_is_loaded = True
208
209        # Enable/disable UI components
210        self.setEnablementOnDataLoad()
211
212    def initializeGlobals(self):
213        """
214        Initialize global variables used in this class
215        """
216        # SasModel is loaded
217        self.model_is_loaded = False
218        # Data[12]D passed and set
219        self.data_is_loaded = False
220        # Batch/single fitting
221        self.is_batch_fitting = False
222        self.is_chain_fitting = False
223        # Is the fit job running?
224        self.fit_started = False
225        # The current fit thread
226        self.calc_fit = None
227        # Current SasModel in view
228        self.kernel_module = None
229        # Current SasModel view dimension
230        self.is2D = False
231        # Current SasModel is multishell
232        self.model_has_shells = False
233        # Utility variable to enable unselectable option in category combobox
234        self._previous_category_index = 0
235        # Utility variables for multishell display
236        self._n_shells_row = 0
237        self._num_shell_params = 0
238        # Dictionary of {model name: model class} for the current category
239        self.models = {}
240        # Parameters to fit
241        self.main_params_to_fit = []
242        self.poly_params_to_fit = []
243        self.magnet_params_to_fit = []
244
245        # Fit options
246        self.q_range_min = 0.005
247        self.q_range_max = 0.1
248        self.npts = 25
249        self.log_points = False
250        self.weighting = 0
251        self.chi2 = None
252        # Does the control support UNDO/REDO
253        # temporarily off
254        self.undo_supported = False
255        self.page_stack = []
256        self.all_data = []
257        # custom plugin models
258        # {model.name:model}
259        self.custom_models = self.customModels()
260        # Polydisp widget table default index for function combobox
261        self.orig_poly_index = 3
262        # copy of current kernel model
263        self.kernel_module_copy = None
264
265        # dictionaries of current params
266        self.poly_params = {}
267        self.magnet_params = {}
268
269        # Page id for fitting
270        # To keep with previous SasView values, use 200 as the start offset
271        self.page_id = 200 + self.tab_id
272
273        # Data for chosen model
274        self.model_data = None
275
276        # Which shell is being currently displayed?
277        self.current_shell_displayed = 0
278        # List of all shell-unique parameters
279        self.shell_names = []
280
281        # Error column presence in parameter display
282        self.has_error_column = False
283        self.has_poly_error_column = False
284        self.has_magnet_error_column = False
285
286        # If the widget generated theory item, save it
287        self.theory_item = None
288
289        # list column widths
290        self.lstParamHeaderSizes = {}
291
292        # signal communicator
293        self.communicate = self.parent.communicate
294
295    def initializeWidgets(self):
296        """
297        Initialize widgets for tabs
298        """
299        # Options widget
300        layout = QtWidgets.QGridLayout()
301        self.options_widget = OptionsWidget(self, self.logic)
302        layout.addWidget(self.options_widget)
303        self.tabOptions.setLayout(layout)
304
305        # Smearing widget
306        layout = QtWidgets.QGridLayout()
307        self.smearing_widget = SmearingWidget(self)
308        layout.addWidget(self.smearing_widget)
309        self.tabResolution.setLayout(layout)
310
311        # Define bold font for use in various controls
312        self.boldFont = QtGui.QFont()
313        self.boldFont.setBold(True)
314
315        # Set data label
316        self.label.setFont(self.boldFont)
317        self.label.setText("No data loaded")
318        self.lblFilename.setText("")
319
320        # Magnetic angles explained in one picture
321        self.magneticAnglesWidget = QtWidgets.QWidget()
322        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
323        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.png')
324        labl.setPixmap(pixmap)
325        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
326
327    def initializeModels(self):
328        """
329        Set up models and views
330        """
331        # Set the main models
332        # We can't use a single model here, due to restrictions on flattening
333        # the model tree with subclassed QAbstractProxyModel...
334        self._model_model = ToolTippedItemModel()
335        self._poly_model = ToolTippedItemModel()
336        self._magnet_model = ToolTippedItemModel()
337
338        # Param model displayed in param list
339        self.lstParams.setModel(self._model_model)
340        self.readCategoryInfo()
341
342        self.model_parameters = None
343
344        # Delegates for custom editing and display
345        self.lstParams.setItemDelegate(ModelViewDelegate(self))
346
347        self.lstParams.setAlternatingRowColors(True)
348        stylesheet = """
349
350            QTreeView {
351                paint-alternating-row-colors-for-empty-area:0;
352            }
353
354            QTreeView::item {
355                border: 1px;
356                padding: 2px 1px;
357            }
358
359            QTreeView::item:hover {
360                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
361                border: 1px solid #bfcde4;
362            }
363
364            QTreeView::item:selected {
365                border: 1px solid #567dbc;
366            }
367
368            QTreeView::item:selected:active{
369                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
370            }
371
372            QTreeView::item:selected:!active {
373                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
374            }
375           """
376        self.lstParams.setStyleSheet(stylesheet)
377        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
378        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
379        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
380        # Column resize signals
381        self.lstParams.header().sectionResized.connect(self.onColumnWidthUpdate)
382
383        # Poly model displayed in poly list
384        self.lstPoly.setModel(self._poly_model)
385        self.setPolyModel()
386        self.setTableProperties(self.lstPoly)
387        # Delegates for custom editing and display
388        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
389        # Polydispersity function combo response
390        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
391        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
392
393        # Magnetism model displayed in magnetism list
394        self.lstMagnetic.setModel(self._magnet_model)
395        self.setMagneticModel()
396        self.setTableProperties(self.lstMagnetic)
397        # Delegates for custom editing and display
398        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
399
400    def initializeCategoryCombo(self):
401        """
402        Model category combo setup
403        """
404        category_list = sorted(self.master_category_dict.keys())
405        self.cbCategory.addItem(CATEGORY_DEFAULT)
406        self.cbCategory.addItems(category_list)
407        if CATEGORY_STRUCTURE not in category_list:
408            self.cbCategory.addItem(CATEGORY_STRUCTURE)
409        self.cbCategory.setCurrentIndex(0)
410
411    def setEnablementOnDataLoad(self):
412        """
413        Enable/disable various UI elements based on data loaded
414        """
415        # Tag along functionality
416        self.label.setText("Data loaded from: ")
417        if self.logic.data.filename:
418            self.lblFilename.setText(self.logic.data.filename)
419        else:
420            self.lblFilename.setText(self.logic.data.name)
421        self.updateQRange()
422        # Switch off Data2D control
423        self.chk2DView.setEnabled(False)
424        self.chk2DView.setVisible(False)
425        self.chkMagnetism.setEnabled(self.is2D)
426        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked())
427        # Combo box or label for file name"
428        if self.is_batch_fitting:
429            self.lblFilename.setVisible(False)
430            for dataitem in self.all_data:
431                filename = GuiUtils.dataFromItem(dataitem).filename
432                self.cbFileNames.addItem(filename)
433            self.cbFileNames.setVisible(True)
434            self.chkChainFit.setEnabled(True)
435            self.chkChainFit.setVisible(True)
436            # This panel is not designed to view individual fits, so disable plotting
437            self.cmdPlot.setVisible(False)
438        # Similarly on other tabs
439        self.options_widget.setEnablementOnDataLoad()
440        self.onSelectModel()
441        # Smearing tab
442        self.smearing_widget.updateData(self.data)
443
444    def acceptsData(self):
445        """ Tells the caller this widget can accept new dataset """
446        return not self.data_is_loaded
447
448    def disableModelCombo(self):
449        """ Disable the combobox """
450        self.cbModel.setEnabled(False)
451        self.lblModel.setEnabled(False)
452
453    def enableModelCombo(self):
454        """ Enable the combobox """
455        self.cbModel.setEnabled(True)
456        self.lblModel.setEnabled(True)
457
458    def disableStructureCombo(self):
459        """ Disable the combobox """
460        self.cbStructureFactor.setEnabled(False)
461        self.lblStructure.setEnabled(False)
462
463    def enableStructureCombo(self):
464        """ Enable the combobox """
465        self.cbStructureFactor.setEnabled(True)
466        self.lblStructure.setEnabled(True)
467
468    def togglePoly(self, isChecked):
469        """ Enable/disable the polydispersity tab """
470        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
471        # Check if any parameters are ready for fitting
472        self.cmdFit.setEnabled(self.haveParamsToFit())
473
474    def toggleMagnetism(self, isChecked):
475        """ Enable/disable the magnetism tab """
476        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
477        # Check if any parameters are ready for fitting
478        self.cmdFit.setEnabled(self.haveParamsToFit())
479
480    def toggleChainFit(self, isChecked):
481        """ Enable/disable chain fitting """
482        self.is_chain_fitting = isChecked
483
484    def toggle2D(self, isChecked):
485        """ Enable/disable the controls dependent on 1D/2D data instance """
486        self.chkMagnetism.setEnabled(isChecked)
487        self.is2D = isChecked
488        # Reload the current model
489        if self.kernel_module:
490            self.onSelectModel()
491
492    @classmethod
493    def customModels(cls):
494        """ Reads in file names in the custom plugin directory """
495        return ModelUtilities._find_models()
496
497    def initializeControls(self):
498        """
499        Set initial control enablement
500        """
501        self.cbFileNames.setVisible(False)
502        self.cmdFit.setEnabled(False)
503        self.cmdPlot.setEnabled(False)
504        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
505        self.chkPolydispersity.setEnabled(True)
506        self.chkPolydispersity.setCheckState(False)
507        self.chk2DView.setEnabled(True)
508        self.chk2DView.setCheckState(False)
509        self.chkMagnetism.setEnabled(False)
510        self.chkMagnetism.setCheckState(False)
511        self.chkChainFit.setEnabled(False)
512        self.chkChainFit.setVisible(False)
513        # Tabs
514        self.tabFitting.setTabEnabled(TAB_POLY, False)
515        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
516        self.lblChi2Value.setText("---")
517        # Smearing tab
518        self.smearing_widget.updateData(self.data)
519        # Line edits in the option tab
520        self.updateQRange()
521
522    def initializeSignals(self):
523        """
524        Connect GUI element signals
525        """
526        # Comboboxes
527        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
528        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
529        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
530        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
531        # Checkboxes
532        self.chk2DView.toggled.connect(self.toggle2D)
533        self.chkPolydispersity.toggled.connect(self.togglePoly)
534        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
535        self.chkChainFit.toggled.connect(self.toggleChainFit)
536        # Buttons
537        self.cmdFit.clicked.connect(self.onFit)
538        self.cmdPlot.clicked.connect(self.onPlot)
539        self.cmdHelp.clicked.connect(self.onHelp)
540        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
541
542        # Respond to change in parameters from the UI
543        self._model_model.itemChanged.connect(self.onMainParamsChange)
544        #self.constraintAddedSignal.connect(self.modifyViewOnConstraint)
545        self._poly_model.itemChanged.connect(self.onPolyModelChange)
546        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
547        self.lstParams.selectionModel().selectionChanged.connect(self.onSelectionChanged)
548
549        # Local signals
550        self.batchFittingFinishedSignal.connect(self.batchFitComplete)
551        self.fittingFinishedSignal.connect(self.fitComplete)
552        self.Calc1DFinishedSignal.connect(self.complete1D)
553        self.Calc2DFinishedSignal.connect(self.complete2D)
554
555        # Signals from separate tabs asking for replot
556        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
557
558        # Signals from other widgets
559        self.communicate.customModelDirectoryChanged.connect(self.onCustomModelChange)
560        self.communicate.saveAnalysisSignal.connect(self.savePageState)
561        self.smearing_widget.smearingChangedSignal.connect(self.onSmearingOptionsUpdate)
562
563        # Communicator signal
564        self.communicate.updateModelCategoriesSignal.connect(self.onCategoriesChanged)
565
566    def modelName(self):
567        """
568        Returns model name, by default M<tab#>, e.g. M1, M2
569        """
570        return "M%i" % self.tab_id
571
572    def nameForFittedData(self, name):
573        """
574        Generate name for the current fit
575        """
576        if self.is2D:
577            name += "2d"
578        name = "%s [%s]" % (self.modelName(), name)
579        return name
580
581    def showModelContextMenu(self, position):
582        """
583        Show context specific menu in the parameter table.
584        When clicked on parameter(s): fitting/constraints options
585        When clicked on white space: model description
586        """
587        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows()
588                if self.isCheckable(s.row())]
589        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
590        try:
591            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
592        except AttributeError as ex:
593            logger.error("Error generating context menu: %s" % ex)
594        return
595
596    def modelContextMenu(self, rows):
597        """
598        Create context menu for the parameter selection
599        """
600        menu = QtWidgets.QMenu()
601        num_rows = len(rows)
602        if num_rows < 1:
603            return menu
604        # Select for fitting
605        param_string = "parameter " if num_rows == 1 else "parameters "
606        to_string = "to its current value" if num_rows == 1 else "to their current values"
607        has_constraints = any([self.rowHasConstraint(i) for i in rows])
608
609        self.actionSelect = QtWidgets.QAction(self)
610        self.actionSelect.setObjectName("actionSelect")
611        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
612        # Unselect from fitting
613        self.actionDeselect = QtWidgets.QAction(self)
614        self.actionDeselect.setObjectName("actionDeselect")
615        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
616
617        self.actionConstrain = QtWidgets.QAction(self)
618        self.actionConstrain.setObjectName("actionConstrain")
619        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain "+param_string + to_string))
620
621        self.actionRemoveConstraint = QtWidgets.QAction(self)
622        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
623        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove constraint"))
624
625        self.actionMultiConstrain = QtWidgets.QAction(self)
626        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
627        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
628
629        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
630        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
631        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
632
633        menu.addAction(self.actionSelect)
634        menu.addAction(self.actionDeselect)
635        menu.addSeparator()
636
637        if has_constraints:
638            menu.addAction(self.actionRemoveConstraint)
639            #if num_rows == 1:
640            #    menu.addAction(self.actionEditConstraint)
641        else:
642            menu.addAction(self.actionConstrain)
643            if num_rows == 2:
644                menu.addAction(self.actionMutualMultiConstrain)
645
646        # Define the callbacks
647        self.actionConstrain.triggered.connect(self.addSimpleConstraint)
648        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
649        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
650        self.actionSelect.triggered.connect(self.selectParameters)
651        self.actionDeselect.triggered.connect(self.deselectParameters)
652        return menu
653
654    def showMultiConstraint(self):
655        """
656        Show the constraint widget and receive the expression
657        """
658        selected_rows = self.lstParams.selectionModel().selectedRows()
659        # There have to be only two rows selected. The caller takes care of that
660        # but let's check the correctness.
661        assert len(selected_rows) == 2
662
663        params_list = [s.data() for s in selected_rows]
664        # Create and display the widget for param1 and param2
665        mc_widget = MultiConstraint(self, params=params_list)
666        # Check if any of the parameters are polydisperse
667        if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
668            # no parameters are pd - reset the text to not show the warning
669            mc_widget.lblWarning.setText("")
670        if mc_widget.exec_() != QtWidgets.QDialog.Accepted:
671            return
672
673        constraint = Constraint()
674        c_text = mc_widget.txtConstraint.text()
675
676        # widget.params[0] is the parameter we're constraining
677        constraint.param = mc_widget.params[0]
678        # parameter should have the model name preamble
679        model_name = self.kernel_module.name
680        # param_used is the parameter we're using in constraining function
681        param_used = mc_widget.params[1]
682        # Replace param_used with model_name.param_used
683        updated_param_used = model_name + "." + param_used
684        new_func = c_text.replace(param_used, updated_param_used)
685        constraint.func = new_func
686        # Which row is the constrained parameter in?
687        row = self.getRowFromName(constraint.param)
688
689        # Create a new item and add the Constraint object as a child
690        self.addConstraintToRow(constraint=constraint, row=row)
691
692    def getRowFromName(self, name):
693        """
694        Given parameter name get the row number in self._model_model
695        """
696        for row in range(self._model_model.rowCount()):
697            row_name = self._model_model.item(row).text()
698            if row_name == name:
699                return row
700        return None
701
702    def getParamNames(self):
703        """
704        Return list of all parameters for the current model
705        """
706        return [self._model_model.item(row).text()
707                for row in range(self._model_model.rowCount())
708                if self.isCheckable(row)]
709
710    def modifyViewOnRow(self, row, font=None, brush=None):
711        """
712        Chage how the given row of the main model is shown
713        """
714        fields_enabled = False
715        if font is None:
716            font = QtGui.QFont()
717            fields_enabled = True
718        if brush is None:
719            brush = QtGui.QBrush()
720            fields_enabled = True
721        self._model_model.blockSignals(True)
722        # Modify font and foreground of affected rows
723        for column in range(0, self._model_model.columnCount()):
724            self._model_model.item(row, column).setForeground(brush)
725            self._model_model.item(row, column).setFont(font)
726            self._model_model.item(row, column).setEditable(fields_enabled)
727        self._model_model.blockSignals(False)
728
729    def addConstraintToRow(self, constraint=None, row=0):
730        """
731        Adds the constraint object to requested row
732        """
733        # Create a new item and add the Constraint object as a child
734        assert isinstance(constraint, Constraint)
735        assert 0 <= row <= self._model_model.rowCount()
736        assert self.isCheckable(row)
737
738        item = QtGui.QStandardItem()
739        item.setData(constraint)
740        self._model_model.item(row, 1).setChild(0, item)
741        # Set min/max to the value constrained
742        self.constraintAddedSignal.emit([row])
743        # Show visual hints for the constraint
744        font = QtGui.QFont()
745        font.setItalic(True)
746        brush = QtGui.QBrush(QtGui.QColor('blue'))
747        self.modifyViewOnRow(row, font=font, brush=brush)
748        self.communicate.statusBarUpdateSignal.emit('Constraint added')
749
750    def addSimpleConstraint(self):
751        """
752        Adds a constraint on a single parameter.
753        """
754        min_col = self.lstParams.itemDelegate().param_min
755        max_col = self.lstParams.itemDelegate().param_max
756        for row in self.selectedParameters():
757            assert(self.isCheckable(row))
758            param = self._model_model.item(row, 0).text()
759            value = self._model_model.item(row, 1).text()
760            min_t = self._model_model.item(row, min_col).text()
761            max_t = self._model_model.item(row, max_col).text()
762            # Create a Constraint object
763            constraint = Constraint(param=param, value=value, min=min_t, max=max_t)
764            # Create a new item and add the Constraint object as a child
765            item = QtGui.QStandardItem()
766            item.setData(constraint)
767            self._model_model.item(row, 1).setChild(0, item)
768            # Assumed correctness from the validator
769            value = float(value)
770            # BUMPS calculates log(max-min) without any checks, so let's assign minor range
771            min_v = value - (value/10000.0)
772            max_v = value + (value/10000.0)
773            # Set min/max to the value constrained
774            self._model_model.item(row, min_col).setText(str(min_v))
775            self._model_model.item(row, max_col).setText(str(max_v))
776            self.constraintAddedSignal.emit([row])
777            # Show visual hints for the constraint
778            font = QtGui.QFont()
779            font.setItalic(True)
780            brush = QtGui.QBrush(QtGui.QColor('blue'))
781            self.modifyViewOnRow(row, font=font, brush=brush)
782        self.communicate.statusBarUpdateSignal.emit('Constraint added')
783
784    def deleteConstraint(self):
785        """
786        Delete constraints from selected parameters.
787        """
788        params = [s.data() for s in self.lstParams.selectionModel().selectedRows()
789                   if self.isCheckable(s.row())]
790        for param in params:
791            self.deleteConstraintOnParameter(param=param)
792
793    def deleteConstraintOnParameter(self, param=None):
794        """
795        Delete the constraint on model parameter 'param'
796        """
797        min_col = self.lstParams.itemDelegate().param_min
798        max_col = self.lstParams.itemDelegate().param_max
799        for row in range(self._model_model.rowCount()):
800            if not self.isCheckable(row):
801                continue
802            if not self.rowHasConstraint(row):
803                continue
804            # Get the Constraint object from of the model item
805            item = self._model_model.item(row, 1)
806            constraint = self.getConstraintForRow(row)
807            if constraint is None:
808                continue
809            if not isinstance(constraint, Constraint):
810                continue
811            if param and constraint.param != param:
812                continue
813            # Now we got the right row. Delete the constraint and clean up
814            # Retrieve old values and put them on the model
815            if constraint.min is not None:
816                self._model_model.item(row, min_col).setText(constraint.min)
817            if constraint.max is not None:
818                self._model_model.item(row, max_col).setText(constraint.max)
819            # Remove constraint item
820            item.removeRow(0)
821            self.constraintAddedSignal.emit([row])
822            self.modifyViewOnRow(row)
823
824        self.communicate.statusBarUpdateSignal.emit('Constraint removed')
825
826    def getConstraintForRow(self, row):
827        """
828        For the given row, return its constraint, if any (otherwise None)
829        """
830        if not self.isCheckable(row):
831            return None
832        item = self._model_model.item(row, 1)
833        try:
834            return item.child(0).data()
835        except AttributeError:
836            return None
837
838    def rowHasConstraint(self, row):
839        """
840        Finds out if row of the main model has a constraint child
841        """
842        if not self.isCheckable(row):
843            return False
844        item = self._model_model.item(row, 1)
845        if not item.hasChildren():
846            return False
847        c = item.child(0).data()
848        if isinstance(c, Constraint):
849            return True
850        return False
851
852    def rowHasActiveConstraint(self, row):
853        """
854        Finds out if row of the main model has an active constraint child
855        """
856        if not self.isCheckable(row):
857            return False
858        item = self._model_model.item(row, 1)
859        if not item.hasChildren():
860            return False
861        c = item.child(0).data()
862        if isinstance(c, Constraint) and c.active:
863            return True
864        return False
865
866    def rowHasActiveComplexConstraint(self, row):
867        """
868        Finds out if row of the main model has an active, nontrivial constraint child
869        """
870        if not self.isCheckable(row):
871            return False
872        item = self._model_model.item(row, 1)
873        if not item.hasChildren():
874            return False
875        c = item.child(0).data()
876        if isinstance(c, Constraint) and c.func and c.active:
877            return True
878        return False
879
880    def selectParameters(self):
881        """
882        Selected parameter is chosen for fitting
883        """
884        status = QtCore.Qt.Checked
885        self.setParameterSelection(status)
886
887    def deselectParameters(self):
888        """
889        Selected parameters are removed for fitting
890        """
891        status = QtCore.Qt.Unchecked
892        self.setParameterSelection(status)
893
894    def selectedParameters(self):
895        """ Returns list of selected (highlighted) parameters """
896        return [s.row() for s in self.lstParams.selectionModel().selectedRows()
897                if self.isCheckable(s.row())]
898
899    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
900        """
901        Selected parameters are chosen for fitting
902        """
903        # Convert to proper indices and set requested enablement
904        for row in self.selectedParameters():
905            self._model_model.item(row, 0).setCheckState(status)
906
907    def getConstraintsForModel(self):
908        """
909        Return a list of tuples. Each tuple contains constraints mapped as
910        ('constrained parameter', 'function to constrain')
911        e.g. [('sld','5*sld_solvent')]
912        """
913        param_number = self._model_model.rowCount()
914        params = [(self._model_model.item(s, 0).text(),
915                    self._model_model.item(s, 1).child(0).data().func)
916                    for s in range(param_number) if self.rowHasActiveConstraint(s)]
917        return params
918
919    def getComplexConstraintsForModel(self):
920        """
921        Return a list of tuples. Each tuple contains constraints mapped as
922        ('constrained parameter', 'function to constrain')
923        e.g. [('sld','5*M2.sld_solvent')].
924        Only for constraints with defined VALUE
925        """
926        param_number = self._model_model.rowCount()
927        params = [(self._model_model.item(s, 0).text(),
928                    self._model_model.item(s, 1).child(0).data().func)
929                    for s in range(param_number) if self.rowHasActiveComplexConstraint(s)]
930        return params
931
932    def getConstraintObjectsForModel(self):
933        """
934        Returns Constraint objects present on the whole model
935        """
936        param_number = self._model_model.rowCount()
937        constraints = [self._model_model.item(s, 1).child(0).data()
938                       for s in range(param_number) if self.rowHasConstraint(s)]
939
940        return constraints
941
942    def getConstraintsForFitting(self):
943        """
944        Return a list of constraints in format ready for use in fiting
945        """
946        # Get constraints
947        constraints = self.getComplexConstraintsForModel()
948        # See if there are any constraints across models
949        multi_constraints = [cons for cons in constraints if self.isConstraintMultimodel(cons[1])]
950
951        if multi_constraints:
952            # Let users choose what to do
953            msg = "The current fit contains constraints relying on other fit pages.\n"
954            msg += "Parameters with those constraints are:\n" +\
955                '\n'.join([cons[0] for cons in multi_constraints])
956            msg += "\n\nWould you like to remove these constraints or cancel fitting?"
957            msgbox = QtWidgets.QMessageBox(self)
958            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
959            msgbox.setText(msg)
960            msgbox.setWindowTitle("Existing Constraints")
961            # custom buttons
962            button_remove = QtWidgets.QPushButton("Remove")
963            msgbox.addButton(button_remove, QtWidgets.QMessageBox.YesRole)
964            button_cancel = QtWidgets.QPushButton("Cancel")
965            msgbox.addButton(button_cancel, QtWidgets.QMessageBox.RejectRole)
966            retval = msgbox.exec_()
967            if retval == QtWidgets.QMessageBox.RejectRole:
968                # cancel fit
969                raise ValueError("Fitting cancelled")
970            else:
971                # remove constraint
972                for cons in multi_constraints:
973                    self.deleteConstraintOnParameter(param=cons[0])
974                # re-read the constraints
975                constraints = self.getComplexConstraintsForModel()
976
977        return constraints
978
979    def showModelDescription(self):
980        """
981        Creates a window with model description, when right clicked in the treeview
982        """
983        msg = 'Model description:\n'
984        if self.kernel_module is not None:
985            if str(self.kernel_module.description).rstrip().lstrip() == '':
986                msg += "Sorry, no information is available for this model."
987            else:
988                msg += self.kernel_module.description + '\n'
989        else:
990            msg += "You must select a model to get information on this"
991
992        menu = QtWidgets.QMenu()
993        label = QtWidgets.QLabel(msg)
994        action = QtWidgets.QWidgetAction(self)
995        action.setDefaultWidget(label)
996        menu.addAction(action)
997        return menu
998
999    def onSelectModel(self):
1000        """
1001        Respond to select Model from list event
1002        """
1003        model = self.cbModel.currentText()
1004
1005        # Assure the control is active
1006        if not self.cbModel.isEnabled():
1007            return
1008        # Empty combobox forced to be read
1009        if not model:
1010            return
1011
1012        # Reset parameters to fit
1013        self.resetParametersToFit()
1014        self.has_error_column = False
1015        self.has_poly_error_column = False
1016
1017        structure = None
1018        if self.cbStructureFactor.isEnabled():
1019            structure = str(self.cbStructureFactor.currentText())
1020        self.respondToModelStructure(model=model, structure_factor=structure)
1021
1022    def onSelectBatchFilename(self, data_index):
1023        """
1024        Update the logic based on the selected file in batch fitting
1025        """
1026        self.data_index = data_index
1027        self.updateQRange()
1028
1029    def onSelectStructureFactor(self):
1030        """
1031        Select Structure Factor from list
1032        """
1033        model = str(self.cbModel.currentText())
1034        category = str(self.cbCategory.currentText())
1035        structure = str(self.cbStructureFactor.currentText())
1036        if category == CATEGORY_STRUCTURE:
1037            model = None
1038
1039        # Reset parameters to fit
1040        self.resetParametersToFit()
1041        self.has_error_column = False
1042        self.has_poly_error_column = False
1043
1044        self.respondToModelStructure(model=model, structure_factor=structure)
1045
1046    def resetParametersToFit(self):
1047        """
1048        Clears the list of parameters to be fitted
1049        """
1050        self.main_params_to_fit = []
1051        self.poly_params_to_fit = []
1052        self.magnet_params_to_fit = []
1053
1054    def onCustomModelChange(self):
1055        """
1056        Reload the custom model combobox
1057        """
1058        self.custom_models = self.customModels()
1059        self.readCustomCategoryInfo()
1060        self.onCategoriesChanged()
1061
1062        # See if we need to update the combo in-place
1063        if self.cbCategory.currentText() != CATEGORY_CUSTOM: return
1064
1065        current_text = self.cbModel.currentText()
1066        self.cbModel.blockSignals(True)
1067        self.cbModel.clear()
1068        self.cbModel.blockSignals(False)
1069        self.enableModelCombo()
1070        self.disableStructureCombo()
1071        # Retrieve the list of models
1072        model_list = self.master_category_dict[CATEGORY_CUSTOM]
1073        # Populate the models combobox
1074        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1075        new_index = self.cbModel.findText(current_text)
1076        if new_index != -1:
1077            self.cbModel.setCurrentIndex(self.cbModel.findText(current_text))
1078
1079    def onSelectionChanged(self):
1080        """
1081        React to parameter selection
1082        """
1083        rows = self.lstParams.selectionModel().selectedRows()
1084        # Clean previous messages
1085        self.communicate.statusBarUpdateSignal.emit("")
1086        if len(rows) == 1:
1087            # Show constraint, if present
1088            row = rows[0].row()
1089            if not self.rowHasConstraint(row):
1090                return
1091            constr = self.getConstraintForRow(row)
1092            func = self.getConstraintForRow(row).func
1093            if constr.func is not None:
1094                # inter-parameter constraint
1095                update_text = "Active constraint: "+func
1096            elif constr.param == rows[0].data():
1097                # current value constraint
1098                update_text = "Value constrained to: " + str(constr.value)
1099            else:
1100                # ill defined constraint
1101                return
1102            self.communicate.statusBarUpdateSignal.emit(update_text)
1103
1104    def replaceConstraintName(self, old_name, new_name=""):
1105        """
1106        Replace names of models in defined constraints
1107        """
1108        param_number = self._model_model.rowCount()
1109        # loop over parameters
1110        for row in range(param_number):
1111            if self.rowHasConstraint(row):
1112                func = self._model_model.item(row, 1).child(0).data().func
1113                if old_name in func:
1114                    new_func = func.replace(old_name, new_name)
1115                    self._model_model.item(row, 1).child(0).data().func = new_func
1116
1117    def isConstraintMultimodel(self, constraint):
1118        """
1119        Check if the constraint function text contains current model name
1120        """
1121        current_model_name = self.kernel_module.name
1122        if current_model_name in constraint:
1123            return False
1124        else:
1125            return True
1126
1127    def updateData(self):
1128        """
1129        Helper function for recalculation of data used in plotting
1130        """
1131        # Update the chart
1132        if self.data_is_loaded:
1133            self.cmdPlot.setText("Show Plot")
1134            self.calculateQGridForModel()
1135        else:
1136            self.cmdPlot.setText("Calculate")
1137            # Create default datasets if no data passed
1138            self.createDefaultDataset()
1139            self.theory_item = None # ensure theory is recalc. before plot, see showTheoryPlot()
1140
1141    def respondToModelStructure(self, model=None, structure_factor=None):
1142        # Set enablement on calculate/plot
1143        self.cmdPlot.setEnabled(True)
1144
1145        # kernel parameters -> model_model
1146        self.SASModelToQModel(model, structure_factor)
1147
1148        for column, width in self.lstParamHeaderSizes.items():
1149            self.lstParams.setColumnWidth(column, width)
1150
1151        # Update plot
1152        self.updateData()
1153
1154        # Update state stack
1155        self.updateUndo()
1156
1157        # Let others know
1158        self.newModelSignal.emit()
1159
1160    def onSelectCategory(self):
1161        """
1162        Select Category from list
1163        """
1164        category = self.cbCategory.currentText()
1165        # Check if the user chose "Choose category entry"
1166        if category == CATEGORY_DEFAULT:
1167            # if the previous category was not the default, keep it.
1168            # Otherwise, just return
1169            if self._previous_category_index != 0:
1170                # We need to block signals, or else state changes on perceived unchanged conditions
1171                self.cbCategory.blockSignals(True)
1172                self.cbCategory.setCurrentIndex(self._previous_category_index)
1173                self.cbCategory.blockSignals(False)
1174            return
1175
1176        if category == CATEGORY_STRUCTURE:
1177            self.disableModelCombo()
1178            self.enableStructureCombo()
1179            # set the index to 0
1180            self.cbStructureFactor.setCurrentIndex(0)
1181            self.model_parameters = None
1182            self._model_model.clear()
1183            return
1184
1185        # Safely clear and enable the model combo
1186        self.cbModel.blockSignals(True)
1187        self.cbModel.clear()
1188        self.cbModel.blockSignals(False)
1189        self.enableModelCombo()
1190        self.disableStructureCombo()
1191
1192        self._previous_category_index = self.cbCategory.currentIndex()
1193        # Retrieve the list of models
1194        model_list = self.master_category_dict[category]
1195        # Populate the models combobox
1196        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1197
1198    def onPolyModelChange(self, item):
1199        """
1200        Callback method for updating the main model and sasmodel
1201        parameters with the GUI values in the polydispersity view
1202        """
1203        model_column = item.column()
1204        model_row = item.row()
1205        name_index = self._poly_model.index(model_row, 0)
1206        parameter_name = str(name_index.data()) # "distribution of sld" etc.
1207        if "istribution of" in parameter_name:
1208            # just the last word
1209            parameter_name = parameter_name.rsplit()[-1]
1210
1211        delegate = self.lstPoly.itemDelegate()
1212
1213        # Extract changed value.
1214        if model_column == delegate.poly_parameter:
1215            # Is the parameter checked for fitting?
1216            value = item.checkState()
1217            parameter_name = parameter_name + '.width'
1218            if value == QtCore.Qt.Checked:
1219                self.poly_params_to_fit.append(parameter_name)
1220            else:
1221                if parameter_name in self.poly_params_to_fit:
1222                    self.poly_params_to_fit.remove(parameter_name)
1223            self.cmdFit.setEnabled(self.haveParamsToFit())
1224
1225        elif model_column in [delegate.poly_min, delegate.poly_max]:
1226            try:
1227                value = GuiUtils.toDouble(item.text())
1228            except TypeError:
1229                # Can't be converted properly, bring back the old value and exit
1230                return
1231
1232            current_details = self.kernel_module.details[parameter_name]
1233            if self.has_poly_error_column:
1234                # err column changes the indexing
1235                current_details[model_column-2] = value
1236            else:
1237                current_details[model_column-1] = value
1238
1239        elif model_column == delegate.poly_function:
1240            # name of the function - just pass
1241            pass
1242
1243        else:
1244            try:
1245                value = GuiUtils.toDouble(item.text())
1246            except TypeError:
1247                # Can't be converted properly, bring back the old value and exit
1248                return
1249
1250            # Update the sasmodel
1251            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
1252            #self.kernel_module.setParam(parameter_name + '.' + delegate.columnDict()[model_column], value)
1253            key = parameter_name + '.' + delegate.columnDict()[model_column]
1254            self.poly_params[key] = value
1255
1256            # Update plot
1257            self.updateData()
1258
1259        # update in param model
1260        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1261            row = self.getRowFromName(parameter_name)
1262            param_item = self._model_model.item(row).child(0).child(0, model_column)
1263            if param_item is None:
1264                return
1265            self._model_model.blockSignals(True)
1266            param_item.setText(item.text())
1267            self._model_model.blockSignals(False)
1268
1269    def onMagnetModelChange(self, item):
1270        """
1271        Callback method for updating the sasmodel magnetic parameters with the GUI values
1272        """
1273        model_column = item.column()
1274        model_row = item.row()
1275        name_index = self._magnet_model.index(model_row, 0)
1276        parameter_name = str(self._magnet_model.data(name_index))
1277
1278        if model_column == 0:
1279            value = item.checkState()
1280            if value == QtCore.Qt.Checked:
1281                self.magnet_params_to_fit.append(parameter_name)
1282            else:
1283                if parameter_name in self.magnet_params_to_fit:
1284                    self.magnet_params_to_fit.remove(parameter_name)
1285            self.cmdFit.setEnabled(self.haveParamsToFit())
1286            # Update state stack
1287            self.updateUndo()
1288            return
1289
1290        # Extract changed value.
1291        try:
1292            value = GuiUtils.toDouble(item.text())
1293        except TypeError:
1294            # Unparsable field
1295            return
1296        delegate = self.lstMagnetic.itemDelegate()
1297
1298        if model_column > 1:
1299            if model_column == delegate.mag_min:
1300                pos = 1
1301            elif model_column == delegate.mag_max:
1302                pos = 2
1303            elif model_column == delegate.mag_unit:
1304                pos = 0
1305            else:
1306                raise AttributeError("Wrong column in magnetism table.")
1307            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1308            self.kernel_module.details[parameter_name][pos] = value
1309        else:
1310            self.magnet_params[parameter_name] = value
1311            #self.kernel_module.setParam(parameter_name) = value
1312            # Force the chart update when actual parameters changed
1313            self.recalculatePlotData()
1314
1315        # Update state stack
1316        self.updateUndo()
1317
1318    def onHelp(self):
1319        """
1320        Show the "Fitting" section of help
1321        """
1322        tree_location = "/user/qtgui/Perspectives/Fitting/"
1323
1324        # Actual file will depend on the current tab
1325        tab_id = self.tabFitting.currentIndex()
1326        helpfile = "fitting.html"
1327        if tab_id == 0:
1328            helpfile = "fitting_help.html"
1329        elif tab_id == 1:
1330            helpfile = "residuals_help.html"
1331        elif tab_id == 2:
1332            helpfile = "resolution.html"
1333        elif tab_id == 3:
1334            helpfile = "pd/polydispersity.html"
1335        elif tab_id == 4:
1336            helpfile = "magnetism/magnetism.html"
1337        help_location = tree_location + helpfile
1338
1339        self.showHelp(help_location)
1340
1341    def showHelp(self, url):
1342        """
1343        Calls parent's method for opening an HTML page
1344        """
1345        self.parent.showHelp(url)
1346
1347    def onDisplayMagneticAngles(self):
1348        """
1349        Display a simple image showing direction of magnetic angles
1350        """
1351        self.magneticAnglesWidget.show()
1352
1353    def onFit(self):
1354        """
1355        Perform fitting on the current data
1356        """
1357        if self.fit_started:
1358            self.stopFit()
1359            return
1360
1361        # initialize fitter constants
1362        fit_id = 0
1363        handler = None
1364        batch_inputs = {}
1365        batch_outputs = {}
1366        #---------------------------------
1367        if LocalConfig.USING_TWISTED:
1368            handler = None
1369            updater = None
1370        else:
1371            handler = ConsoleUpdate(parent=self.parent,
1372                                    manager=self,
1373                                    improvement_delta=0.1)
1374            updater = handler.update_fit
1375
1376        # Prepare the fitter object
1377        try:
1378            fitters, _ = self.prepareFitters()
1379        except ValueError as ex:
1380            # This should not happen! GUI explicitly forbids this situation
1381            self.communicate.statusBarUpdateSignal.emit(str(ex))
1382            return
1383
1384        # keep local copy of kernel parameters, as they will change during the update
1385        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1386
1387        # Create the fitting thread, based on the fitter
1388        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
1389
1390        self.calc_fit = FitThread(handler=handler,
1391                            fn=fitters,
1392                            batch_inputs=batch_inputs,
1393                            batch_outputs=batch_outputs,
1394                            page_id=[[self.page_id]],
1395                            updatefn=updater,
1396                            completefn=completefn,
1397                            reset_flag=self.is_chain_fitting)
1398
1399        if LocalConfig.USING_TWISTED:
1400            # start the trhrhread with twisted
1401            calc_thread = threads.deferToThread(self.calc_fit.compute)
1402            calc_thread.addCallback(completefn)
1403            calc_thread.addErrback(self.fitFailed)
1404        else:
1405            # Use the old python threads + Queue
1406            self.calc_fit.queue()
1407            self.calc_fit.ready(2.5)
1408
1409        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
1410        self.fit_started = True
1411
1412        # Disable some elements
1413        self.disableInteractiveElements()
1414
1415    def stopFit(self):
1416        """
1417        Attempt to stop the fitting thread
1418        """
1419        if self.calc_fit is None or not self.calc_fit.isrunning():
1420            return
1421        self.calc_fit.stop()
1422        #re-enable the Fit button
1423        self.enableInteractiveElements()
1424
1425        msg = "Fitting cancelled."
1426        self.communicate.statusBarUpdateSignal.emit(msg)
1427
1428    def updateFit(self):
1429        """
1430        """
1431        print("UPDATE FIT")
1432        pass
1433
1434    def fitFailed(self, reason):
1435        """
1436        """
1437        self.enableInteractiveElements()
1438        msg = "Fitting failed with: "+ str(reason)
1439        self.communicate.statusBarUpdateSignal.emit(msg)
1440
1441    def batchFittingCompleted(self, result):
1442        """
1443        Send the finish message from calculate threads to main thread
1444        """
1445        if result is None:
1446            result = tuple()
1447        self.batchFittingFinishedSignal.emit(result)
1448
1449    def batchFitComplete(self, result):
1450        """
1451        Receive and display batch fitting results
1452        """
1453        #re-enable the Fit button
1454        self.enableInteractiveElements()
1455
1456        if len(result) == 0:
1457            msg = "Fitting failed."
1458            self.communicate.statusBarUpdateSignal.emit(msg)
1459            return
1460
1461        # Show the grid panel
1462        self.communicate.sendDataToGridSignal.emit(result[0])
1463
1464        elapsed = result[1]
1465        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1466        self.communicate.statusBarUpdateSignal.emit(msg)
1467
1468        # Run over the list of results and update the items
1469        for res_index, res_list in enumerate(result[0]):
1470            # results
1471            res = res_list[0]
1472            param_dict = self.paramDictFromResults(res)
1473
1474            # create local kernel_module
1475            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1476            # pull out current data
1477            data = self._logic[res_index].data
1478
1479            # Switch indexes
1480            self.onSelectBatchFilename(res_index)
1481
1482            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1483            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1484
1485        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1486        if self.kernel_module is not None:
1487            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1488
1489    def paramDictFromResults(self, results):
1490        """
1491        Given the fit results structure, pull out optimized parameters and return them as nicely
1492        formatted dict
1493        """
1494        if results.fitness is None or \
1495            not np.isfinite(results.fitness) or \
1496            np.any(results.pvec is None) or \
1497            not np.all(np.isfinite(results.pvec)):
1498            msg = "Fitting did not converge!"
1499            self.communicate.statusBarUpdateSignal.emit(msg)
1500            msg += results.mesg
1501            logger.error(msg)
1502            return
1503
1504        param_list = results.param_list # ['radius', 'radius.width']
1505        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1506        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1507        params_and_errors = list(zip(param_values, param_stderr))
1508        param_dict = dict(zip(param_list, params_and_errors))
1509
1510        return param_dict
1511
1512    def fittingCompleted(self, result):
1513        """
1514        Send the finish message from calculate threads to main thread
1515        """
1516        if result is None:
1517            result = tuple()
1518        self.fittingFinishedSignal.emit(result)
1519
1520    def fitComplete(self, result):
1521        """
1522        Receive and display fitting results
1523        "result" is a tuple of actual result list and the fit time in seconds
1524        """
1525        #re-enable the Fit button
1526        self.enableInteractiveElements()
1527
1528        if len(result) == 0:
1529            msg = "Fitting failed."
1530            self.communicate.statusBarUpdateSignal.emit(msg)
1531            return
1532
1533        res_list = result[0][0]
1534        res = res_list[0]
1535        self.chi2 = res.fitness
1536        param_dict = self.paramDictFromResults(res)
1537
1538        if param_dict is None:
1539            return
1540
1541        elapsed = result[1]
1542        if self.calc_fit is not None and self.calc_fit._interrupting:
1543            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1544            logger.warning("\n"+msg+"\n")
1545        else:
1546            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1547        self.communicate.statusBarUpdateSignal.emit(msg)
1548
1549        # Dictionary of fitted parameter: value, error
1550        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1551        self.updateModelFromList(param_dict)
1552
1553        self.updatePolyModelFromList(param_dict)
1554
1555        self.updateMagnetModelFromList(param_dict)
1556
1557        # update charts
1558        self.onPlot()
1559        #self.recalculatePlotData()
1560
1561
1562        # Read only value - we can get away by just printing it here
1563        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1564        self.lblChi2Value.setText(chi2_repr)
1565
1566    def prepareFitters(self, fitter=None, fit_id=0):
1567        """
1568        Prepare the Fitter object for use in fitting
1569        """
1570        # fitter = None -> single/batch fitting
1571        # fitter = Fit() -> simultaneous fitting
1572
1573        # Data going in
1574        data = self.logic.data
1575        model = copy.deepcopy(self.kernel_module)
1576        qmin = self.q_range_min
1577        qmax = self.q_range_max
1578        # add polydisperse/magnet parameters if asked
1579        self.updateKernelModelWithExtraParams(model)
1580
1581        params_to_fit = copy.deepcopy(self.main_params_to_fit)
1582        if self.chkPolydispersity.isChecked():
1583            params_to_fit += self.poly_params_to_fit
1584        if self.chkMagnetism.isChecked():
1585            params_to_fit += self.magnet_params_to_fit
1586        if not params_to_fit:
1587            raise ValueError('Fitting requires at least one parameter to optimize.')
1588
1589        # Get the constraints.
1590        constraints = self.getComplexConstraintsForModel()
1591        if fitter is None:
1592            # For single fits - check for inter-model constraints
1593            constraints = self.getConstraintsForFitting()
1594
1595        smearer = self.smearing_widget.smearer()
1596        handler = None
1597        batch_inputs = {}
1598        batch_outputs = {}
1599
1600        fitters = []
1601        for fit_index in self.all_data:
1602            fitter_single = Fit() if fitter is None else fitter
1603            data = GuiUtils.dataFromItem(fit_index)
1604            # Potential weights added directly to data
1605            weighted_data = self.addWeightingToData(data)
1606            try:
1607                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
1608                             constraints=constraints)
1609            except ValueError as ex:
1610                raise ValueError("Setting model parameters failed with: %s" % ex)
1611
1612            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1613            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1614                            qmax=qmax)
1615            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1616            if fitter is None:
1617                # Assign id to the new fitter only
1618                fitter_single.fitter_id = [self.page_id]
1619            fit_id += 1
1620            fitters.append(fitter_single)
1621
1622        return fitters, fit_id
1623
1624    def iterateOverModel(self, func):
1625        """
1626        Take func and throw it inside the model row loop
1627        """
1628        for row_i in range(self._model_model.rowCount()):
1629            func(row_i)
1630
1631    def updateModelFromList(self, param_dict):
1632        """
1633        Update the model with new parameters, create the errors column
1634        """
1635        assert isinstance(param_dict, dict)
1636        if not dict:
1637            return
1638
1639        def updateFittedValues(row):
1640            # Utility function for main model update
1641            # internal so can use closure for param_dict
1642            param_name = str(self._model_model.item(row, 0).text())
1643            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1644                return
1645            # modify the param value
1646            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1647            self._model_model.item(row, 1).setText(param_repr)
1648            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1649            if self.has_error_column:
1650                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1651                self._model_model.item(row, 2).setText(error_repr)
1652
1653        def updatePolyValues(row):
1654            # Utility function for updateof polydispersity part of the main model
1655            param_name = str(self._model_model.item(row, 0).text())+'.width'
1656            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1657                return
1658            # modify the param value
1659            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1660            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1661            # modify the param error
1662            if self.has_error_column:
1663                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1664                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1665
1666        def createErrorColumn(row):
1667            # Utility function for error column update
1668            item = QtGui.QStandardItem()
1669            def createItem(param_name):
1670                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1671                item.setText(error_repr)
1672            def curr_param():
1673                return str(self._model_model.item(row, 0).text())
1674
1675            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1676
1677            error_column.append(item)
1678
1679        def createPolyErrorColumn(row):
1680            # Utility function for error column update in the polydispersity sub-rows
1681            # NOTE: only creates empty items; updatePolyValues adds the error value
1682            item = self._model_model.item(row, 0)
1683            if not item.hasChildren():
1684                return
1685            poly_item = item.child(0)
1686            if not poly_item.hasChildren():
1687                return
1688            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1689
1690        if not self.has_error_column:
1691            # create top-level error column
1692            error_column = []
1693            self.lstParams.itemDelegate().addErrorColumn()
1694            self.iterateOverModel(createErrorColumn)
1695
1696            self._model_model.insertColumn(2, error_column)
1697
1698            FittingUtilities.addErrorHeadersToModel(self._model_model)
1699
1700            # create error column in polydispersity sub-rows
1701            self.iterateOverModel(createPolyErrorColumn)
1702
1703            self.has_error_column = True
1704
1705        # block signals temporarily, so we don't end up
1706        # updating charts with every single model change on the end of fitting
1707        self._model_model.itemChanged.disconnect()
1708        self.iterateOverModel(updateFittedValues)
1709        self.iterateOverModel(updatePolyValues)
1710        self._model_model.itemChanged.connect(self.onMainParamsChange)
1711
1712    def iterateOverPolyModel(self, func):
1713        """
1714        Take func and throw it inside the poly model row loop
1715        """
1716        for row_i in range(self._poly_model.rowCount()):
1717            func(row_i)
1718
1719    def updatePolyModelFromList(self, param_dict):
1720        """
1721        Update the polydispersity model with new parameters, create the errors column
1722        """
1723        assert isinstance(param_dict, dict)
1724        if not dict:
1725            return
1726
1727        def updateFittedValues(row_i):
1728            # Utility function for main model update
1729            # internal so can use closure for param_dict
1730            if row_i >= self._poly_model.rowCount():
1731                return
1732            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1733            if param_name not in list(param_dict.keys()):
1734                return
1735            # modify the param value
1736            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1737            self._poly_model.item(row_i, 1).setText(param_repr)
1738            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1739            if self.has_poly_error_column:
1740                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1741                self._poly_model.item(row_i, 2).setText(error_repr)
1742
1743        def createErrorColumn(row_i):
1744            # Utility function for error column update
1745            if row_i >= self._poly_model.rowCount():
1746                return
1747            item = QtGui.QStandardItem()
1748
1749            def createItem(param_name):
1750                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1751                item.setText(error_repr)
1752
1753            def poly_param():
1754                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1755
1756            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1757
1758            error_column.append(item)
1759
1760        # block signals temporarily, so we don't end up
1761        # updating charts with every single model change on the end of fitting
1762        self._poly_model.itemChanged.disconnect()
1763        self.iterateOverPolyModel(updateFittedValues)
1764        self._poly_model.itemChanged.connect(self.onPolyModelChange)
1765
1766        if self.has_poly_error_column:
1767            return
1768
1769        self.lstPoly.itemDelegate().addErrorColumn()
1770        error_column = []
1771        self.iterateOverPolyModel(createErrorColumn)
1772
1773        # switch off reponse to model change
1774        self._poly_model.insertColumn(2, error_column)
1775        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1776
1777        self.has_poly_error_column = True
1778
1779    def iterateOverMagnetModel(self, func):
1780        """
1781        Take func and throw it inside the magnet model row loop
1782        """
1783        for row_i in range(self._magnet_model.rowCount()):
1784            func(row_i)
1785
1786    def updateMagnetModelFromList(self, param_dict):
1787        """
1788        Update the magnetic model with new parameters, create the errors column
1789        """
1790        assert isinstance(param_dict, dict)
1791        if not dict:
1792            return
1793        if self._magnet_model.rowCount() == 0:
1794            return
1795
1796        def updateFittedValues(row):
1797            # Utility function for main model update
1798            # internal so can use closure for param_dict
1799            if self._magnet_model.item(row, 0) is None:
1800                return
1801            param_name = str(self._magnet_model.item(row, 0).text())
1802            if param_name not in list(param_dict.keys()):
1803                return
1804            # modify the param value
1805            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1806            self._magnet_model.item(row, 1).setText(param_repr)
1807            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1808            if self.has_magnet_error_column:
1809                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1810                self._magnet_model.item(row, 2).setText(error_repr)
1811
1812        def createErrorColumn(row):
1813            # Utility function for error column update
1814            item = QtGui.QStandardItem()
1815            def createItem(param_name):
1816                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1817                item.setText(error_repr)
1818            def curr_param():
1819                return str(self._magnet_model.item(row, 0).text())
1820
1821            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1822
1823            error_column.append(item)
1824
1825        # block signals temporarily, so we don't end up
1826        # updating charts with every single model change on the end of fitting
1827        self._magnet_model.itemChanged.disconnect()
1828        self.iterateOverMagnetModel(updateFittedValues)
1829        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
1830
1831        if self.has_magnet_error_column:
1832            return
1833
1834        self.lstMagnetic.itemDelegate().addErrorColumn()
1835        error_column = []
1836        self.iterateOverMagnetModel(createErrorColumn)
1837
1838        # switch off reponse to model change
1839        self._magnet_model.insertColumn(2, error_column)
1840        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1841
1842        self.has_magnet_error_column = True
1843
1844    def onPlot(self):
1845        """
1846        Plot the current set of data
1847        """
1848        # Regardless of previous state, this should now be `plot show` functionality only
1849        self.cmdPlot.setText("Show Plot")
1850        # Force data recalculation so existing charts are updated
1851        if not self.data_is_loaded:
1852            self.showTheoryPlot()
1853        else:
1854            self.showPlot()
1855        # This is an important processEvent.
1856        # This allows charts to be properly updated in order
1857        # of plots being applied.
1858        QtWidgets.QApplication.processEvents()
1859        self.recalculatePlotData() # recalc+plot theory again (2nd)
1860
1861    def onSmearingOptionsUpdate(self):
1862        """
1863        React to changes in the smearing widget
1864        """
1865        self.calculateQGridForModel()
1866
1867    def recalculatePlotData(self):
1868        """
1869        Generate a new dataset for model
1870        """
1871        if not self.data_is_loaded:
1872            self.createDefaultDataset()
1873        self.calculateQGridForModel()
1874
1875    def showTheoryPlot(self):
1876        """
1877        Show the current theory plot in MPL
1878        """
1879        # Show the chart if ready
1880        if self.theory_item is None:
1881            self.recalculatePlotData()
1882        elif self.model_data:
1883            self._requestPlots(self.model_data.filename, self.theory_item.model())
1884
1885    def showPlot(self):
1886        """
1887        Show the current plot in MPL
1888        """
1889        # Show the chart if ready
1890        data_to_show = self.data
1891        # Any models for this page
1892        current_index = self.all_data[self.data_index]
1893        item = self._requestPlots(self.data.filename, current_index.model())
1894        if item:
1895            # fit+data has not been shown - show just data
1896            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
1897
1898    def _requestPlots(self, item_name, item_model):
1899        """
1900        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
1901        """
1902        fitpage_name = "" if self.tab_id is None else "M"+str(self.tab_id)
1903        plots = GuiUtils.plotsFromFilename(item_name, item_model)
1904        # Has the fitted data been shown?
1905        data_shown = False
1906        item = None
1907        for item, plot in plots.items():
1908            if fitpage_name in plot.name:
1909                data_shown = True
1910                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
1911        # return the last data item seen, if nothing was plotted; supposed to be just data)
1912        return None if data_shown else item
1913
1914    def onOptionsUpdate(self):
1915        """
1916        Update local option values and replot
1917        """
1918        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1919            self.options_widget.state()
1920        # set Q range labels on the main tab
1921        self.lblMinRangeDef.setText(str(self.q_range_min))
1922        self.lblMaxRangeDef.setText(str(self.q_range_max))
1923        self.recalculatePlotData()
1924
1925    def setDefaultStructureCombo(self):
1926        """
1927        Fill in the structure factors combo box with defaults
1928        """
1929        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1930        factors = [factor[0] for factor in structure_factor_list]
1931        factors.insert(0, STRUCTURE_DEFAULT)
1932        self.cbStructureFactor.clear()
1933        self.cbStructureFactor.addItems(sorted(factors))
1934
1935    def createDefaultDataset(self):
1936        """
1937        Generate default Dataset 1D/2D for the given model
1938        """
1939        # Create default datasets if no data passed
1940        if self.is2D:
1941            qmax = self.q_range_max/np.sqrt(2)
1942            qstep = self.npts
1943            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1944            return
1945        elif self.log_points:
1946            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1947            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1948            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1949        else:
1950            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1951                                   num=self.npts, endpoint=True)
1952        self.logic.createDefault1dData(interval, self.tab_id)
1953
1954    def readCategoryInfo(self):
1955        """
1956        Reads the categories in from file
1957        """
1958        self.master_category_dict = defaultdict(list)
1959        self.by_model_dict = defaultdict(list)
1960        self.model_enabled_dict = defaultdict(bool)
1961
1962        categorization_file = CategoryInstaller.get_user_file()
1963        if not os.path.isfile(categorization_file):
1964            categorization_file = CategoryInstaller.get_default_file()
1965        with open(categorization_file, 'rb') as cat_file:
1966            self.master_category_dict = json.load(cat_file)
1967            self.regenerateModelDict()
1968
1969        # Load the model dict
1970        models = load_standard_models()
1971        for model in models:
1972            self.models[model.name] = model
1973
1974        self.readCustomCategoryInfo()
1975
1976    def readCustomCategoryInfo(self):
1977        """
1978        Reads the custom model category
1979        """
1980        #Looking for plugins
1981        self.plugins = list(self.custom_models.values())
1982        plugin_list = []
1983        for name, plug in self.custom_models.items():
1984            self.models[name] = plug
1985            plugin_list.append([name, True])
1986        if plugin_list:
1987            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1988
1989    def regenerateModelDict(self):
1990        """
1991        Regenerates self.by_model_dict which has each model name as the
1992        key and the list of categories belonging to that model
1993        along with the enabled mapping
1994        """
1995        self.by_model_dict = defaultdict(list)
1996        for category in self.master_category_dict:
1997            for (model, enabled) in self.master_category_dict[category]:
1998                self.by_model_dict[model].append(category)
1999                self.model_enabled_dict[model] = enabled
2000
2001    def addBackgroundToModel(self, model):
2002        """
2003        Adds background parameter with default values to the model
2004        """
2005        assert isinstance(model, QtGui.QStandardItemModel)
2006        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
2007        FittingUtilities.addCheckedListToModel(model, checked_list)
2008        last_row = model.rowCount()-1
2009        model.item(last_row, 0).setEditable(False)
2010        model.item(last_row, 4).setEditable(False)
2011
2012    def addScaleToModel(self, model):
2013        """
2014        Adds scale parameter with default values to the model
2015        """
2016        assert isinstance(model, QtGui.QStandardItemModel)
2017        checked_list = ['scale', '1.0', '0.0', 'inf', '']
2018        FittingUtilities.addCheckedListToModel(model, checked_list)
2019        last_row = model.rowCount()-1
2020        model.item(last_row, 0).setEditable(False)
2021        model.item(last_row, 4).setEditable(False)
2022
2023    def addWeightingToData(self, data):
2024        """
2025        Adds weighting contribution to fitting data
2026        """
2027        new_data = copy.deepcopy(data)
2028        # Send original data for weighting
2029        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2030        if self.is2D:
2031            new_data.err_data = weight
2032        else:
2033            new_data.dy = weight
2034
2035        return new_data
2036
2037    def updateQRange(self):
2038        """
2039        Updates Q Range display
2040        """
2041        if self.data_is_loaded:
2042            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2043        # set Q range labels on the main tab
2044        self.lblMinRangeDef.setText(str(self.q_range_min))
2045        self.lblMaxRangeDef.setText(str(self.q_range_max))
2046        # set Q range labels on the options tab
2047        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
2048
2049    def SASModelToQModel(self, model_name, structure_factor=None):
2050        """
2051        Setting model parameters into table based on selected category
2052        """
2053        # Crete/overwrite model items
2054        self._model_model.clear()
2055        self._poly_model.clear()
2056        self._magnet_model.clear()
2057
2058        if model_name is None:
2059            if structure_factor not in (None, "None"):
2060                # S(Q) on its own, treat the same as a form factor
2061                self.kernel_module = None
2062                self.fromStructureFactorToQModel(structure_factor)
2063            else:
2064                # No models selected
2065                return
2066        else:
2067            self.fromModelToQModel(model_name)
2068            self.addExtraShells()
2069
2070            # Allow the SF combobox visibility for the given sasmodel
2071            self.enableStructureFactorControl(structure_factor)
2072       
2073            # Add S(Q)
2074            if self.cbStructureFactor.isEnabled():
2075                structure_factor = self.cbStructureFactor.currentText()
2076                self.fromStructureFactorToQModel(structure_factor)
2077
2078            # Add polydispersity to the model
2079            self.poly_params = {}
2080            self.setPolyModel()
2081            # Add magnetic parameters to the model
2082            self.magnet_params = {}
2083            self.setMagneticModel()
2084
2085        # Now we claim the model has been loaded
2086        self.model_is_loaded = True
2087        # Change the model name to a monicker
2088        self.kernel_module.name = self.modelName()
2089        # Update the smearing tab
2090        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2091
2092        # (Re)-create headers
2093        FittingUtilities.addHeadersToModel(self._model_model)
2094        self.lstParams.header().setFont(self.boldFont)
2095
2096        # Update Q Ranges
2097        self.updateQRange()
2098
2099    def fromModelToQModel(self, model_name):
2100        """
2101        Setting model parameters into QStandardItemModel based on selected _model_
2102        """
2103        name = model_name
2104        kernel_module = None
2105        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2106            # custom kernel load requires full path
2107            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2108        try:
2109            kernel_module = generate.load_kernel_module(name)
2110        except ModuleNotFoundError as ex:
2111            pass
2112        except FileNotFoundError as ex:
2113            # can happen when name attribute not the same as actual filename
2114            pass
2115
2116        if kernel_module is None:
2117            # mismatch between "name" attribute and actual filename.
2118            curr_model = self.models[model_name]
2119            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2120            try:
2121                kernel_module = generate.load_kernel_module(name)
2122            except ModuleNotFoundError as ex:
2123                logger.error("Can't find the model "+ str(ex))
2124                return
2125
2126        if hasattr(kernel_module, 'parameters'):
2127            # built-in and custom models
2128            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2129
2130        elif hasattr(kernel_module, 'model_info'):
2131            # for sum/multiply models
2132            self.model_parameters = kernel_module.model_info.parameters
2133
2134        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2135            # this probably won't work if there's no model_info, but just in case
2136            self.model_parameters = kernel_module.Model._model_info.parameters
2137        else:
2138            # no parameters - default to blank table
2139            msg = "No parameters found in model '{}'.".format(model_name)
2140            logger.warning(msg)
2141            self.model_parameters = modelinfo.ParameterTable([])
2142
2143        # Instantiate the current sasmodel
2144        self.kernel_module = self.models[model_name]()
2145
2146        # Change the model name to a monicker
2147        self.kernel_module.name = self.modelName()
2148
2149        # Explicitly add scale and background with default values
2150        temp_undo_state = self.undo_supported
2151        self.undo_supported = False
2152        self.addScaleToModel(self._model_model)
2153        self.addBackgroundToModel(self._model_model)
2154        self.undo_supported = temp_undo_state
2155
2156        self.shell_names = self.shellNamesList()
2157
2158        # Add heading row
2159        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2160
2161        # Update the QModel
2162        FittingUtilities.addParametersToModel(
2163                self.model_parameters,
2164                self.kernel_module,
2165                self.is2D,
2166                self._model_model,
2167                self.lstParams)
2168
2169    def fromStructureFactorToQModel(self, structure_factor):
2170        """
2171        Setting model parameters into QStandardItemModel based on selected _structure factor_
2172        """
2173        if structure_factor is None or structure_factor=="None":
2174            return
2175
2176        product_params = None
2177
2178        if self.kernel_module is None:
2179            # Structure factor is the only selected model; build it and show all its params
2180            self.kernel_module = self.models[structure_factor]()
2181            self.kernel_module.name = self.modelName()
2182            s_params = self.kernel_module._model_info.parameters
2183            s_params_orig = s_params
2184        else:
2185            s_kernel = self.models[structure_factor]()
2186            p_kernel = self.kernel_module
2187
2188            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2189            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2190
2191            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2192            # Modify the name to correspond to shown items
2193            self.kernel_module.name = self.modelName()
2194            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2195            all_param_names = [param.name for param in all_params]
2196
2197            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2198            # conflicting names with P(Q) params will cause a rename
2199
2200            if "radius_effective_mode" in all_param_names:
2201                # Show all parameters
2202                # In this case, radius_effective is NOT pruned by sasmodels.product
2203                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2204                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2205                product_params = modelinfo.ParameterTable(
2206                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2207            else:
2208                # Ensure radius_effective is not displayed
2209                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2210                if "radius_effective" in all_param_names:
2211                    # In this case, radius_effective is NOT pruned by sasmodels.product
2212                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2213                    product_params = modelinfo.ParameterTable(
2214                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2215                else:
2216                    # In this case, radius_effective is pruned by sasmodels.product
2217                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2218                    product_params = modelinfo.ParameterTable(
2219                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
2220
2221        # Add heading row
2222        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2223
2224        # Get new rows for QModel
2225        # Any renamed parameters are stored as data in the relevant item, for later handling
2226        FittingUtilities.addSimpleParametersToModel(
2227                parameters=s_params,
2228                is2D=self.is2D,
2229                parameters_original=s_params_orig,
2230                model=self._model_model,
2231                view=self.lstParams)
2232
2233        # Insert product-only params into QModel
2234        if product_params:
2235            prod_rows = FittingUtilities.addSimpleParametersToModel(
2236                    parameters=product_params,
2237                    is2D=self.is2D,
2238                    parameters_original=None,
2239                    model=self._model_model,
2240                    view=self.lstParams,
2241                    row_num=2)
2242
2243            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2244            self._n_shells_row += len(prod_rows)
2245
2246    def haveParamsToFit(self):
2247        """
2248        Finds out if there are any parameters ready to be fitted
2249        """
2250        if not self.logic.data_is_loaded:
2251            return False
2252        if self.main_params_to_fit:
2253            return True
2254        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2255            return True
2256        if self.chkMagnetism.isChecked() and self.magnet_params_to_fit:
2257            return True
2258        return False
2259
2260    def onMainParamsChange(self, item):
2261        """
2262        Callback method for updating the sasmodel parameters with the GUI values
2263        """
2264        model_column = item.column()
2265
2266        if model_column == 0:
2267            self.checkboxSelected(item)
2268            self.cmdFit.setEnabled(self.haveParamsToFit())
2269            # Update state stack
2270            self.updateUndo()
2271            return
2272
2273        model_row = item.row()
2274        name_index = self._model_model.index(model_row, 0)
2275        name_item = self._model_model.itemFromIndex(name_index)
2276
2277        # Extract changed value.
2278        try:
2279            value = GuiUtils.toDouble(item.text())
2280        except TypeError:
2281            # Unparsable field
2282            return
2283
2284        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2285        if name_item.data(QtCore.Qt.UserRole):
2286            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2287        else:
2288            parameter_name = str(self._model_model.data(name_index))
2289
2290        # Update the parameter value - note: this supports +/-inf as well
2291        self.kernel_module.params[parameter_name] = value
2292
2293        # Update the parameter value - note: this supports +/-inf as well
2294        param_column = self.lstParams.itemDelegate().param_value
2295        min_column = self.lstParams.itemDelegate().param_min
2296        max_column = self.lstParams.itemDelegate().param_max
2297        if model_column == param_column:
2298            self.kernel_module.setParam(parameter_name, value)
2299        elif model_column == min_column:
2300            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2301            self.kernel_module.details[parameter_name][1] = value
2302        elif model_column == max_column:
2303            self.kernel_module.details[parameter_name][2] = value
2304        else:
2305            # don't update the chart
2306            return
2307
2308        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2309        # TODO: multishell params in self.kernel_module.details[??] = value
2310
2311        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2312        # necessary
2313        self.processEffectiveRadius()
2314
2315        # Force the chart update when actual parameters changed
2316        if model_column == 1:
2317            self.recalculatePlotData()
2318
2319        # Update state stack
2320        self.updateUndo()
2321
2322    def processEffectiveRadius(self):
2323        """
2324        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2325        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2326        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2327
2328        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2329        displayed for the user; that is not dealt with here, but in complete1D.
2330        """
2331        ER_row = self.getRowFromName("radius_effective")
2332        if ER_row is None:
2333            return
2334
2335        ER_mode_row = self.getRowFromName("radius_effective_mode")
2336        if ER_mode_row is None:
2337            return
2338
2339        try:
2340            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2341        except ValueError:
2342            logging.error("radius_effective_mode was set to an invalid value.")
2343            return
2344
2345        if ER_mode == 0:
2346            # ensure the ER value can be modified by user
2347            self.setParamEditableByRow(ER_row, True)
2348        elif ER_mode > 0:
2349            # ensure the ER value cannot be modified by user
2350            self.setParamEditableByRow(ER_row, False)
2351        else:
2352            logging.error("radius_effective_mode was set to an invalid value.")
2353
2354    def setParamEditableByRow(self, row, editable=True):
2355        """
2356        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2357        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2358        """
2359        item_name = self._model_model.item(row, 0)
2360        item_value = self._model_model.item(row, 1)
2361
2362        item_value.setEditable(editable)
2363
2364        if editable:
2365            # reset font
2366            item_name.setFont(QtGui.QFont())
2367            # reset colour
2368            item_name.setForeground(QtGui.QBrush())
2369            # make checkable
2370            item_name.setCheckable(True)
2371        else:
2372            # change font
2373            font = QtGui.QFont()
2374            font.setItalic(True)
2375            item_name.setFont(font)
2376            # change colour
2377            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2378            # make not checkable (and uncheck)
2379            item_name.setCheckState(QtCore.Qt.Unchecked)
2380            item_name.setCheckable(False)
2381
2382    def isCheckable(self, row):
2383        return self._model_model.item(row, 0).isCheckable()
2384
2385    def checkboxSelected(self, item):
2386        # Assure we're dealing with checkboxes
2387        if not item.isCheckable():
2388            return
2389        status = item.checkState()
2390
2391        # If multiple rows selected - toggle all of them, filtering uncheckable
2392        # Switch off signaling from the model to avoid recursion
2393        self._model_model.blockSignals(True)
2394        # Convert to proper indices and set requested enablement
2395        self.setParameterSelection(status)
2396        self._model_model.blockSignals(False)
2397
2398        # update the list of parameters to fit
2399        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2400
2401    def checkedListFromModel(self, model):
2402        """
2403        Returns list of checked parameters for given model
2404        """
2405        def isChecked(row):
2406            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2407
2408        return [str(model.item(row_index, 0).text())
2409                for row_index in range(model.rowCount())
2410                if isChecked(row_index)]
2411
2412    def createNewIndex(self, fitted_data):
2413        """
2414        Create a model or theory index with passed Data1D/Data2D
2415        """
2416        if self.data_is_loaded:
2417            if not fitted_data.name:
2418                name = self.nameForFittedData(self.data.filename)
2419                fitted_data.title = name
2420                fitted_data.name = name
2421                fitted_data.filename = name
2422                fitted_data.symbol = "Line"
2423            self.updateModelIndex(fitted_data)
2424        else:
2425            if not fitted_data.name:
2426                name = self.nameForFittedData(self.kernel_module.id)
2427            else:
2428                name = fitted_data.name
2429            fitted_data.title = name
2430            fitted_data.filename = name
2431            fitted_data.symbol = "Line"
2432            self.createTheoryIndex(fitted_data)
2433            # Switch to the theory tab for user's glee
2434            self.communicate.changeDataExplorerTabSignal.emit(1)
2435
2436    def updateModelIndex(self, fitted_data):
2437        """
2438        Update a QStandardModelIndex containing model data
2439        """
2440        name = self.nameFromData(fitted_data)
2441        # Make this a line if no other defined
2442        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2443            fitted_data.symbol = 'Line'
2444        # Notify the GUI manager so it can update the main model in DataExplorer
2445        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2446
2447    def createTheoryIndex(self, fitted_data):
2448        """
2449        Create a QStandardModelIndex containing model data
2450        """
2451        name = self.nameFromData(fitted_data)
2452        # Notify the GUI manager so it can create the theory model in DataExplorer
2453        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2454        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2455
2456    def nameFromData(self, fitted_data):
2457        """
2458        Return name for the dataset. Terribly impure function.
2459        """
2460        if fitted_data.name is None:
2461            name = self.nameForFittedData(self.logic.data.filename)
2462            fitted_data.title = name
2463            fitted_data.name = name
2464            fitted_data.filename = name
2465        else:
2466            name = fitted_data.name
2467        return name
2468
2469    def methodCalculateForData(self):
2470        '''return the method for data calculation'''
2471        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2472
2473    def methodCompleteForData(self):
2474        '''return the method for result parsin on calc complete '''
2475        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2476
2477    def updateKernelModelWithExtraParams(self, model=None):
2478        """
2479        Updates kernel model 'model' with extra parameters from
2480        the polydisp and magnetism tab, if the tabs are enabled
2481        """
2482        if model is None: return
2483        if not hasattr(model, 'setParam'): return
2484
2485        # add polydisperse parameters if asked
2486        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2487            for key, value in self.poly_params.items():
2488                model.setParam(key, value)
2489        # add magnetic params if asked
2490        if self.chkMagnetism.isChecked() and self._magnet_model.rowCount() > 0:
2491            for key, value in self.magnet_params.items():
2492                model.setParam(key, value)
2493
2494    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2495        """
2496        Wrapper for Calc1D/2D calls
2497        """
2498        if data is None:
2499            data = self.data
2500        if model is None:
2501            model = copy.deepcopy(self.kernel_module)
2502            self.updateKernelModelWithExtraParams(model)
2503
2504        if completefn is None:
2505            completefn = self.methodCompleteForData()
2506        smearer = self.smearing_widget.smearer()
2507        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2508
2509        # Disable buttons/table
2510        self.disableInteractiveElements()
2511        # Awful API to a backend method.
2512        calc_thread = self.methodCalculateForData()(data=data,
2513                                               model=model,
2514                                               page_id=0,
2515                                               qmin=self.q_range_min,
2516                                               qmax=self.q_range_max,
2517                                               smearer=smearer,
2518                                               state=None,
2519                                               weight=weight,
2520                                               fid=None,
2521                                               toggle_mode_on=False,
2522                                               completefn=completefn,
2523                                               update_chisqr=True,
2524                                               exception_handler=self.calcException,
2525                                               source=None)
2526        if use_threads:
2527            if LocalConfig.USING_TWISTED:
2528                # start the thread with twisted
2529                thread = threads.deferToThread(calc_thread.compute)
2530                thread.addCallback(completefn)
2531                thread.addErrback(self.calculateDataFailed)
2532            else:
2533                # Use the old python threads + Queue
2534                calc_thread.queue()
2535                calc_thread.ready(2.5)
2536        else:
2537            results = calc_thread.compute()
2538            completefn(results)
2539
2540    def calculateQGridForModel(self):
2541        """
2542        Prepare the fitting data object, based on current ModelModel
2543        """
2544        if self.kernel_module is None:
2545            return
2546        self.calculateQGridForModelExt()
2547
2548    def calculateDataFailed(self, reason):
2549        """
2550        Thread returned error
2551        """
2552        # Bring the GUI to normal state
2553        self.enableInteractiveElements()
2554        print("Calculate Data failed with ", reason)
2555
2556    def completed1D(self, return_data):
2557        self.Calc1DFinishedSignal.emit(return_data)
2558
2559    def completed2D(self, return_data):
2560        self.Calc2DFinishedSignal.emit(return_data)
2561
2562    def complete1D(self, return_data):
2563        """
2564        Plot the current 1D data
2565        """
2566        # Bring the GUI to normal state
2567        self.enableInteractiveElements()
2568        if return_data is None:
2569            return
2570        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2571
2572        # assure the current index is set properly for batch
2573        if len(self._logic) > 1:
2574            for i, logic in enumerate(self._logic):
2575                if logic.data.name in fitted_data.name:
2576                    self.data_index = i
2577
2578        residuals = self.calculateResiduals(fitted_data)
2579        self.model_data = fitted_data
2580        new_plots = [fitted_data]
2581        if residuals is not None:
2582            new_plots.append(residuals)
2583
2584        if self.data_is_loaded:
2585            # delete any plots associated with the data that were not updated
2586            # (e.g. to remove beta(Q), S_eff(Q))
2587            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2588            pass
2589        else:
2590            # delete theory items for the model, in order to get rid of any
2591            # redundant items, e.g. beta(Q), S_eff(Q)
2592            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2593
2594        # Create plots for parameters with enabled polydispersity
2595        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2596            data_id = fitted_data.id.split()
2597            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2598            data_name = fitted_data.name.split()
2599            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2600            self.createNewIndex(plot)
2601            new_plots.append(plot)
2602
2603        # Create plots for intermediate product data
2604        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2605        for plot in plots:
2606            plot.symbol = "Line"
2607            self.createNewIndex(plot)
2608            new_plots.append(plot)
2609
2610        for plot in new_plots:
2611            self.communicate.plotUpdateSignal.emit([plot])
2612
2613        # Update radius_effective if relevant
2614        self.updateEffectiveRadius(return_data)
2615
2616    def complete2D(self, return_data):
2617        """
2618        Plot the current 2D data
2619        """
2620        # Bring the GUI to normal state
2621        self.enableInteractiveElements()
2622
2623        if return_data is None:
2624            return
2625
2626        fitted_data = self.logic.new2DPlot(return_data)
2627        # assure the current index is set properly for batch
2628        if len(self._logic) > 1:
2629            for i, logic in enumerate(self._logic):
2630                if logic.data.name in fitted_data.name:
2631                    self.data_index = i
2632
2633        residuals = self.calculateResiduals(fitted_data)
2634        self.model_data = fitted_data
2635        new_plots = [fitted_data]
2636        if residuals is not None:
2637            new_plots.append(residuals)
2638
2639        # Update/generate plots
2640        for plot in new_plots:
2641            self.communicate.plotUpdateSignal.emit([plot])
2642
2643    def updateEffectiveRadius(self, return_data):
2644        """
2645        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2646        calculated value as returned by sasmodels (if the value was returned).
2647        """
2648        ER_mode_row = self.getRowFromName("radius_effective_mode")
2649        if ER_mode_row is None:
2650            return
2651        try:
2652            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2653        except ValueError:
2654            logging.error("radius_effective_mode was set to an invalid value.")
2655            return
2656        if ER_mode < 1:
2657            # does not need updating if it is not being computed
2658            return
2659
2660        ER_row = self.getRowFromName("radius_effective")
2661        if ER_row is None:
2662            return
2663
2664        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2665        ER_value = scalar_results.get("effective_radius") # note name of key
2666        if ER_value is None:
2667            return
2668        # ensure the model does not recompute when updating the value
2669        self._model_model.blockSignals(True)
2670        self._model_model.item(ER_row, 1).setText(str(ER_value))
2671        self._model_model.blockSignals(False)
2672        # ensure the view is updated immediately
2673        self._model_model.layoutChanged.emit()
2674
2675    def calculateResiduals(self, fitted_data):
2676        """
2677        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2678        """
2679        # Create a new index for holding data
2680        fitted_data.symbol = "Line"
2681
2682        # Modify fitted_data with weighting
2683        weighted_data = self.addWeightingToData(fitted_data)
2684
2685        self.createNewIndex(weighted_data)
2686
2687        # Calculate difference between return_data and logic.data
2688        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2689        # Update the control
2690        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2691        self.lblChi2Value.setText(chi2_repr)
2692
2693        # Plot residuals if actual data
2694        if not self.data_is_loaded:
2695            return
2696
2697        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2698        if residuals_plot is None:
2699            return
2700        residuals_plot.id = "Residual " + residuals_plot.id
2701        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2702        self.createNewIndex(residuals_plot)
2703        return residuals_plot
2704
2705    def onCategoriesChanged(self):
2706            """
2707            Reload the category/model comboboxes
2708            """
2709            # Store the current combo indices
2710            current_cat = self.cbCategory.currentText()
2711            current_model = self.cbModel.currentText()
2712
2713            # reread the category file and repopulate the combo
2714            self.cbCategory.blockSignals(True)
2715            self.cbCategory.clear()
2716            self.readCategoryInfo()
2717            self.initializeCategoryCombo()
2718
2719            # Scroll back to the original index in Categories
2720            new_index = self.cbCategory.findText(current_cat)
2721            if new_index != -1:
2722                self.cbCategory.setCurrentIndex(new_index)
2723            self.cbCategory.blockSignals(False)
2724            # ...and in the Models
2725            self.cbModel.blockSignals(True)
2726            new_index = self.cbModel.findText(current_model)
2727            if new_index != -1:
2728                self.cbModel.setCurrentIndex(new_index)
2729            self.cbModel.blockSignals(False)
2730
2731            return
2732
2733    def calcException(self, etype, value, tb):
2734        """
2735        Thread threw an exception.
2736        """
2737        # Bring the GUI to normal state
2738        self.enableInteractiveElements()
2739        # TODO: remimplement thread cancellation
2740        logger.error("".join(traceback.format_exception(etype, value, tb)))
2741
2742    def setTableProperties(self, table):
2743        """
2744        Setting table properties
2745        """
2746        # Table properties
2747        table.verticalHeader().setVisible(False)
2748        table.setAlternatingRowColors(True)
2749        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2750        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2751        table.resizeColumnsToContents()
2752
2753        # Header
2754        header = table.horizontalHeader()
2755        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2756        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2757
2758        # Qt5: the following 2 lines crash - figure out why!
2759        # Resize column 0 and 7 to content
2760        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2761        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2762
2763    def setPolyModel(self):
2764        """
2765        Set polydispersity values
2766        """
2767        if not self.model_parameters:
2768            return
2769        self._poly_model.clear()
2770
2771        parameters = self.model_parameters.form_volume_parameters
2772        if self.is2D:
2773            parameters += self.model_parameters.orientation_parameters
2774
2775        [self.setPolyModelParameters(i, param) for i, param in \
2776            enumerate(parameters) if param.polydisperse]
2777
2778        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2779
2780    def setPolyModelParameters(self, i, param):
2781        """
2782        Standard of multishell poly parameter driver
2783        """
2784        param_name = param.name
2785        # see it the parameter is multishell
2786        if '[' in param.name:
2787            # Skip empty shells
2788            if self.current_shell_displayed == 0:
2789                return
2790            else:
2791                # Create as many entries as current shells
2792                for ishell in range(1, self.current_shell_displayed+1):
2793                    # Remove [n] and add the shell numeral
2794                    name = param_name[0:param_name.index('[')] + str(ishell)
2795                    self.addNameToPolyModel(i, name)
2796        else:
2797            # Just create a simple param entry
2798            self.addNameToPolyModel(i, param_name)
2799
2800    def addNameToPolyModel(self, i, param_name):
2801        """
2802        Creates a checked row in the poly model with param_name
2803        """
2804        # Polydisp. values from the sasmodel
2805        width = self.kernel_module.getParam(param_name + '.width')
2806        npts = self.kernel_module.getParam(param_name + '.npts')
2807        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2808        _, min, max = self.kernel_module.details[param_name]
2809
2810        # Update local param dict
2811        self.poly_params[param_name + '.width'] = width
2812        self.poly_params[param_name + '.npts'] = npts
2813        self.poly_params[param_name + '.nsigmas'] = nsigs
2814
2815        # Construct a row with polydisp. related variable.
2816        # This will get added to the polydisp. model
2817        # Note: last argument needs extra space padding for decent display of the control
2818        checked_list = ["Distribution of " + param_name, str(width),
2819                        str(min), str(max),
2820                        str(npts), str(nsigs), "gaussian      ",'']
2821        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2822
2823        # All possible polydisp. functions as strings in combobox
2824        func = QtWidgets.QComboBox()
2825        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2826        # Set the default index
2827        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2828        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2829        self.lstPoly.setIndexWidget(ind, func)
2830        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2831
2832    def onPolyFilenameChange(self, row_index):
2833        """
2834        Respond to filename_updated signal from the delegate
2835        """
2836        # For the given row, invoke the "array" combo handler
2837        array_caption = 'array'
2838
2839        # Get the combo box reference
2840        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2841        widget = self.lstPoly.indexWidget(ind)
2842
2843        # Update the combo box so it displays "array"
2844        widget.blockSignals(True)
2845        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2846        widget.blockSignals(False)
2847
2848        # Invoke the file reader
2849        self.onPolyComboIndexChange(array_caption, row_index)
2850
2851    def onPolyComboIndexChange(self, combo_string, row_index):
2852        """
2853        Modify polydisp. defaults on function choice
2854        """
2855        # Get npts/nsigs for current selection
2856        param = self.model_parameters.form_volume_parameters[row_index]
2857        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2858        combo_box = self.lstPoly.indexWidget(file_index)
2859
2860        def updateFunctionCaption(row):
2861            # Utility function for update of polydispersity function name in the main model
2862            if not self.isCheckable(row):
2863                return
2864            self._model_model.blockSignals(True)
2865            param_name = str(self._model_model.item(row, 0).text())
2866            self._model_model.blockSignals(False)
2867            if param_name !=  param.name:
2868                return
2869            # Modify the param value
2870            self._model_model.blockSignals(True)
2871            if self.has_error_column:
2872                # err column changes the indexing
2873                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2874            else:
2875                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2876            self._model_model.blockSignals(False)
2877
2878        if combo_string == 'array':
2879            try:
2880                self.loadPolydispArray(row_index)
2881                # Update main model for display
2882                self.iterateOverModel(updateFunctionCaption)
2883                # disable the row
2884                lo = self.lstPoly.itemDelegate().poly_pd
2885                hi = self.lstPoly.itemDelegate().poly_function
2886                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2887                return
2888            except IOError:
2889                combo_box.setCurrentIndex(self.orig_poly_index)
2890                # Pass for cancel/bad read
2891                pass
2892
2893        # Enable the row in case it was disabled by Array
2894        self._poly_model.blockSignals(True)
2895        max_range = self.lstPoly.itemDelegate().poly_filename
2896        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2897        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2898        self._poly_model.setData(file_index, "")
2899        self._poly_model.blockSignals(False)
2900
2901        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2902        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2903
2904        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2905        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2906
2907        self._poly_model.setData(npts_index, npts)
2908        self._poly_model.setData(nsigs_index, nsigs)
2909
2910        self.iterateOverModel(updateFunctionCaption)
2911        self.orig_poly_index = combo_box.currentIndex()
2912
2913    def loadPolydispArray(self, row_index):
2914        """
2915        Show the load file dialog and loads requested data into state
2916        """
2917        datafile = QtWidgets.QFileDialog.getOpenFileName(
2918            self, "Choose a weight file", "", "All files (*.*)", None,
2919            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2920
2921        if not datafile:
2922            logger.info("No weight data chosen.")
2923            raise IOError
2924
2925        values = []
2926        weights = []
2927        def appendData(data_tuple):
2928            """
2929            Fish out floats from a tuple of strings
2930            """
2931            try:
2932                values.append(float(data_tuple[0]))
2933                weights.append(float(data_tuple[1]))
2934            except (ValueError, IndexError):
2935                # just pass through if line with bad data
2936                return
2937
2938        with open(datafile, 'r') as column_file:
2939            column_data = [line.rstrip().split() for line in column_file.readlines()]
2940            [appendData(line) for line in column_data]
2941
2942        # If everything went well - update the sasmodel values
2943        self.disp_model = POLYDISPERSITY_MODELS['array']()
2944        self.disp_model.set_weights(np.array(values), np.array(weights))
2945        # + update the cell with filename
2946        fname = os.path.basename(str(datafile))
2947        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2948        self._poly_model.setData(fname_index, fname)
2949
2950    def onColumnWidthUpdate(self, index, old_size, new_size):
2951        """
2952        Simple state update of the current column widths in the  param list
2953        """
2954        self.lstParamHeaderSizes[index] = new_size
2955
2956    def setMagneticModel(self):
2957        """
2958        Set magnetism values on model
2959        """
2960        if not self.model_parameters:
2961            return
2962        self._magnet_model.clear()
2963        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2964            self.model_parameters.call_parameters if param.type == 'magnetic']
2965        FittingUtilities.addHeadersToModel(self._magnet_model)
2966
2967    def shellNamesList(self):
2968        """
2969        Returns list of names of all multi-shell parameters
2970        E.g. for sld[n], radius[n], n=1..3 it will return
2971        [sld1, sld2, sld3, radius1, radius2, radius3]
2972        """
2973        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2974        top_index = self.kernel_module.multiplicity_info.number
2975        shell_names = []
2976        for i in range(1, top_index+1):
2977            for name in multi_names:
2978                shell_names.append(name+str(i))
2979        return shell_names
2980
2981    def addCheckedMagneticListToModel(self, param, model):
2982        """
2983        Wrapper for model update with a subset of magnetic parameters
2984        """
2985        if param.name[param.name.index(':')+1:] in self.shell_names:
2986            # check if two-digit shell number
2987            try:
2988                shell_index = int(param.name[-2:])
2989            except ValueError:
2990                shell_index = int(param.name[-1:])
2991
2992            if shell_index > self.current_shell_displayed:
2993                return
2994
2995        checked_list = [param.name,
2996                        str(param.default),
2997                        str(param.limits[0]),
2998                        str(param.limits[1]),
2999                        param.units]
3000
3001        self.magnet_params[param.name] = param.default
3002
3003        FittingUtilities.addCheckedListToModel(model, checked_list)
3004
3005    def enableStructureFactorControl(self, structure_factor):
3006        """
3007        Add structure factors to the list of parameters
3008        """
3009        if self.kernel_module.is_form_factor or structure_factor == 'None':
3010            self.enableStructureCombo()
3011        else:
3012            self.disableStructureCombo()
3013
3014    def addExtraShells(self):
3015        """
3016        Add a combobox for multiple shell display
3017        """
3018        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3019
3020        if param_length == 0:
3021            return
3022
3023        # cell 1: variable name
3024        item1 = QtGui.QStandardItem(param_name)
3025
3026        func = QtWidgets.QComboBox()
3027
3028        # cell 2: combobox
3029        item2 = QtGui.QStandardItem()
3030
3031        # cell 3: min value
3032        item3 = QtGui.QStandardItem()
3033
3034        # cell 4: max value
3035        item4 = QtGui.QStandardItem()
3036
3037        # cell 4: SLD button
3038        item5 = QtGui.QStandardItem()
3039        button = QtWidgets.QPushButton()
3040        button.setText("Show SLD Profile")
3041
3042        self._model_model.appendRow([item1, item2, item3, item4, item5])
3043
3044        # Beautify the row:  span columns 2-4
3045        shell_row = self._model_model.rowCount()
3046        shell_index = self._model_model.index(shell_row-1, 1)
3047        button_index = self._model_model.index(shell_row-1, 4)
3048
3049        self.lstParams.setIndexWidget(shell_index, func)
3050        self.lstParams.setIndexWidget(button_index, button)
3051        self._n_shells_row = shell_row - 1
3052
3053        # Get the default number of shells for the model
3054        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3055        shell_par = None
3056        for par in kernel_pars:
3057            if par.name == param_name:
3058                shell_par = par
3059                break
3060        if not shell_par:
3061            logger.error("Could not find %s in kernel parameters.", param_name)
3062        default_shell_count = shell_par.default
3063        shell_min = 0
3064        shell_max = 0
3065        try:
3066            shell_min = int(shell_par.limits[0])
3067            shell_max = int(shell_par.limits[1])
3068        except IndexError as ex:
3069            # no info about limits
3070            pass
3071        # don't update the kernel here - this data is display only
3072        self._model_model.blockSignals(True)
3073        item3.setText(str(shell_min))
3074        item4.setText(str(shell_max))
3075        self._model_model.blockSignals(False)
3076
3077        # Respond to index change
3078        func.currentTextChanged.connect(self.modifyShellsInList)
3079
3080        # Respond to button press
3081        button.clicked.connect(self.onShowSLDProfile)
3082
3083        # Available range of shells displayed in the combobox
3084        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3085
3086        # Add default number of shells to the model
3087        func.setCurrentText(str(default_shell_count))
3088
3089    def modifyShellsInList(self, text):
3090        """
3091        Add/remove additional multishell parameters
3092        """
3093        # Find row location of the combobox
3094        first_row = self._n_shells_row + 1
3095        remove_rows = self._num_shell_params
3096        try:
3097            index = int(text)
3098        except ValueError:
3099            # bad text on the control!
3100            index = 0
3101            logger.error("Multiplicity incorrect! Setting to 0")
3102        self.kernel_module.multiplicity = index
3103        if remove_rows > 1:
3104            self._model_model.removeRows(first_row, remove_rows)
3105
3106        new_rows = FittingUtilities.addShellsToModel(
3107                self.model_parameters,
3108                self._model_model,
3109                index,
3110                first_row,
3111                self.lstParams)
3112
3113        self._num_shell_params = len(new_rows)
3114        self.current_shell_displayed = index
3115
3116        # Param values for existing shells were reset to default; force all changes into kernel module
3117        for row in new_rows:
3118            par = row[0].text()
3119            val = GuiUtils.toDouble(row[1].text())
3120            self.kernel_module.setParam(par, val)
3121
3122        # Change 'n' in the parameter model; also causes recalculation
3123        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3124
3125        # Update relevant models
3126        self.setPolyModel()
3127        self.setMagneticModel()
3128
3129    def onShowSLDProfile(self):
3130        """
3131        Show a quick plot of SLD profile
3132        """
3133        # get profile data
3134        x, y = self.kernel_module.getProfile()
3135        y *= 1.0e6
3136        profile_data = Data1D(x=x, y=y)
3137        profile_data.name = "SLD"
3138        profile_data.scale = 'linear'
3139        profile_data.symbol = 'Line'
3140        profile_data.hide_error = True
3141        profile_data._xaxis = "R(\AA)"
3142        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3143
3144        plotter = PlotterWidget(self, quickplot=True)
3145        plotter.data = profile_data
3146        plotter.showLegend = True
3147        plotter.plot(hide_error=True, marker='-')
3148
3149        self.plot_widget = QtWidgets.QWidget()
3150        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3151        layout = QtWidgets.QVBoxLayout()
3152        layout.addWidget(plotter)
3153        self.plot_widget.setLayout(layout)
3154        self.plot_widget.show()
3155
3156    def setInteractiveElements(self, enabled=True):
3157        """
3158        Switch interactive GUI elements on/off
3159        """
3160        assert isinstance(enabled, bool)
3161
3162        self.lstParams.setEnabled(enabled)
3163        self.lstPoly.setEnabled(enabled)
3164        self.lstMagnetic.setEnabled(enabled)
3165
3166        self.cbCategory.setEnabled(enabled)
3167        self.cbModel.setEnabled(enabled)
3168        self.cbStructureFactor.setEnabled(enabled)
3169
3170        self.chkPolydispersity.setEnabled(enabled)
3171        self.chkMagnetism.setEnabled(enabled)
3172        self.chk2DView.setEnabled(enabled)
3173
3174    def enableInteractiveElements(self):
3175        """
3176        Set buttion caption on fitting/calculate finish
3177        Enable the param table(s)
3178        """
3179        # Notify the user that fitting is available
3180        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3181        self.cmdFit.setText("Fit")
3182        self.fit_started = False
3183        self.setInteractiveElements(True)
3184
3185    def disableInteractiveElements(self):
3186        """
3187        Set buttion caption on fitting/calculate start
3188        Disable the param table(s)
3189        """
3190        # Notify the user that fitting is being run
3191        # Allow for stopping the job
3192        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3193        self.cmdFit.setText('Stop fit')
3194        self.setInteractiveElements(False)
3195
3196    def readFitPage(self, fp):
3197        """
3198        Read in state from a fitpage object and update GUI
3199        """
3200        assert isinstance(fp, FitPage)
3201        # Main tab info
3202        self.logic.data.filename = fp.filename
3203        self.data_is_loaded = fp.data_is_loaded
3204        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3205        self.chkMagnetism.setCheckState(fp.is_magnetic)
3206        self.chk2DView.setCheckState(fp.is2D)
3207
3208        # Update the comboboxes
3209        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3210        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3211        if fp.current_factor:
3212            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3213
3214        self.chi2 = fp.chi2
3215
3216        # Options tab
3217        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3218        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3219        self.npts = fp.fit_options[fp.NPTS]
3220        self.log_points = fp.fit_options[fp.LOG_POINTS]
3221        self.weighting = fp.fit_options[fp.WEIGHTING]
3222
3223        # Models
3224        self._model_model = fp.model_model
3225        self._poly_model = fp.poly_model
3226        self._magnet_model = fp.magnetism_model
3227
3228        # Resolution tab
3229        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3230        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3231        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3232        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3233        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3234
3235        # TODO: add polidyspersity and magnetism
3236
3237    def saveToFitPage(self, fp):
3238        """
3239        Write current state to the given fitpage
3240        """
3241        assert isinstance(fp, FitPage)
3242
3243        # Main tab info
3244        fp.filename = self.logic.data.filename
3245        fp.data_is_loaded = self.data_is_loaded
3246        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3247        fp.is_magnetic = self.chkMagnetism.isChecked()
3248        fp.is2D = self.chk2DView.isChecked()
3249        fp.data = self.data
3250
3251        # Use current models - they contain all the required parameters
3252        fp.model_model = self._model_model
3253        fp.poly_model = self._poly_model
3254        fp.magnetism_model = self._magnet_model
3255
3256        if self.cbCategory.currentIndex() != 0:
3257            fp.current_category = str(self.cbCategory.currentText())
3258            fp.current_model = str(self.cbModel.currentText())
3259
3260        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3261            fp.current_factor = str(self.cbStructureFactor.currentText())
3262        else:
3263            fp.current_factor = ''
3264
3265        fp.chi2 = self.chi2
3266        fp.main_params_to_fit = self.main_params_to_fit
3267        fp.poly_params_to_fit = self.poly_params_to_fit
3268        fp.magnet_params_to_fit = self.magnet_params_to_fit
3269        fp.kernel_module = self.kernel_module
3270
3271        # Algorithm options
3272        # fp.algorithm = self.parent.fit_options.selected_id
3273
3274        # Options tab
3275        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3276        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3277        fp.fit_options[fp.NPTS] = self.npts
3278        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3279        fp.fit_options[fp.LOG_POINTS] = self.log_points
3280        fp.fit_options[fp.WEIGHTING] = self.weighting
3281
3282        # Resolution tab
3283        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3284        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3285        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3286        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3287        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3288
3289        # TODO: add polidyspersity and magnetism
3290
3291    def updateUndo(self):
3292        """
3293        Create a new state page and add it to the stack
3294        """
3295        if self.undo_supported:
3296            self.pushFitPage(self.currentState())
3297
3298    def currentState(self):
3299        """
3300        Return fit page with current state
3301        """
3302        new_page = FitPage()
3303        self.saveToFitPage(new_page)
3304
3305        return new_page
3306
3307    def pushFitPage(self, new_page):
3308        """
3309        Add a new fit page object with current state
3310        """
3311        self.page_stack.append(new_page)
3312
3313    def popFitPage(self):
3314        """
3315        Remove top fit page from stack
3316        """
3317        if self.page_stack:
3318            self.page_stack.pop()
3319
3320    def getReport(self):
3321        """
3322        Create and return HTML report with parameters and charts
3323        """
3324        index = None
3325        if self.all_data:
3326            index = self.all_data[self.data_index]
3327        else:
3328            index = self.theory_item
3329        report_logic = ReportPageLogic(self,
3330                                       kernel_module=self.kernel_module,
3331                                       data=self.data,
3332                                       index=index,
3333                                       model=self._model_model)
3334
3335        return report_logic.reportList()
3336
3337    def savePageState(self):
3338        """
3339        Create and serialize local PageState
3340        """
3341        from sas.sascalc.fit.pagestate import Reader
3342        model = self.kernel_module
3343
3344        # Old style PageState object
3345        state = PageState(model=model, data=self.data)
3346
3347        # Add parameter data to the state
3348        self.getCurrentFitState(state)
3349
3350        # Create the filewriter, aptly named 'Reader'
3351        state_reader = Reader(self.loadPageStateCallback)
3352        filepath = self.saveAsAnalysisFile()
3353        if filepath is None or filepath == "":
3354            return
3355        state_reader.write(filename=filepath, fitstate=state)
3356        pass
3357
3358    def saveAsAnalysisFile(self):
3359        """
3360        Show the save as... dialog and return the chosen filepath
3361        """
3362        default_name = "FitPage"+str(self.tab_id)+".fitv"
3363
3364        wildcard = "fitv files (*.fitv)"
3365        kwargs = {
3366            'caption'   : 'Save As',
3367            'directory' : default_name,
3368            'filter'    : wildcard,
3369            'parent'    : None,
3370        }
3371        # Query user for filename.
3372        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3373        filename = filename_tuple[0]
3374        return filename
3375
3376    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3377        """
3378        This is a callback method called from the CANSAS reader.
3379        We need the instance of this reader only for writing out a file,
3380        so there's nothing here.
3381        Until Load Analysis is implemented, that is.
3382        """
3383        pass
3384
3385    def loadPageState(self, pagestate=None):
3386        """
3387        Load the PageState object and update the current widget
3388        """
3389        pass
3390
3391    def getCurrentFitState(self, state=None):
3392        """
3393        Store current state for fit_page
3394        """
3395        # save model option
3396        #if self.model is not None:
3397        #    self.disp_list = self.getDispParamList()
3398        #    state.disp_list = copy.deepcopy(self.disp_list)
3399        #    #state.model = self.model.clone()
3400
3401        # Comboboxes
3402        state.categorycombobox = self.cbCategory.currentText()
3403        state.formfactorcombobox = self.cbModel.currentText()
3404        if self.cbStructureFactor.isEnabled():
3405            state.structurecombobox = self.cbStructureFactor.currentText()
3406        state.tcChi = self.chi2
3407
3408        state.enable2D = self.is2D
3409
3410        #state.weights = copy.deepcopy(self.weights)
3411        # save data
3412        state.data = copy.deepcopy(self.data)
3413
3414        # save plotting range
3415        state.qmin = self.q_range_min
3416        state.qmax = self.q_range_max
3417        state.npts = self.npts
3418
3419        #    self.state.enable_disp = self.enable_disp.GetValue()
3420        #    self.state.disable_disp = self.disable_disp.GetValue()
3421
3422        #    self.state.enable_smearer = \
3423        #                        copy.deepcopy(self.enable_smearer.GetValue())
3424        #    self.state.disable_smearer = \
3425        #                        copy.deepcopy(self.disable_smearer.GetValue())
3426
3427        #self.state.pinhole_smearer = \
3428        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3429        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3430        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3431        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3432        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3433        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3434
3435        p = self.model_parameters
3436        # save checkbutton state and txtcrtl values
3437        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3438        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3439
3440        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3441        #self._copy_parameters_state(self.parameters, self.state.parameters)
3442        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3443        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3444
3445    def onParameterCopy(self, format=None):
3446        """
3447        Copy current parameters into the clipboard
3448        """
3449        # run a loop over all parameters and pull out
3450        # first - regular params
3451        param_list = []
3452
3453        param_list.append(['model_name', str(self.cbModel.currentText())])
3454        def gatherParams(row):
3455            """
3456            Create list of main parameters based on _model_model
3457            """
3458            param_name = str(self._model_model.item(row, 0).text())
3459            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3460            param_value = str(self._model_model.item(row, 1).text())
3461            param_error = None
3462            param_min = None
3463            param_max = None
3464            column_offset = 0
3465            if self.has_error_column:
3466                param_error = str(self._model_model.item(row, 2).text())
3467                column_offset = 1
3468
3469            try:
3470                param_min = str(self._model_model.item(row, 2+column_offset).text())
3471                param_max = str(self._model_model.item(row, 3+column_offset).text())
3472            except:
3473                pass
3474
3475            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3476
3477        def gatherPolyParams(row):
3478            """
3479            Create list of polydisperse parameters based on _poly_model
3480            """
3481            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3482            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3483            param_value = str(self._poly_model.item(row, 1).text())
3484            param_error = None
3485            column_offset = 0
3486            if self.has_poly_error_column:
3487                param_error = str(self._poly_model.item(row, 2).text())
3488                column_offset = 1
3489            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3490            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3491            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3492            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3493            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3494            # width
3495            name = param_name+".width"
3496            param_list.append([name, param_checked, param_value, param_error,
3497                                param_npts, param_nsigs, param_min, param_max, param_fun])
3498
3499        def gatherMagnetParams(row):
3500            """
3501            Create list of magnetic parameters based on _magnet_model
3502            """
3503            param_name = str(self._magnet_model.item(row, 0).text())
3504            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3505            param_value = str(self._magnet_model.item(row, 1).text())
3506            param_error = None
3507            column_offset = 0
3508            if self.has_magnet_error_column:
3509                param_error = str(self._magnet_model.item(row, 2).text())
3510                column_offset = 1
3511            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3512            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3513            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3514
3515        self.iterateOverModel(gatherParams)
3516        if self.chkPolydispersity.isChecked():
3517            self.iterateOverPolyModel(gatherPolyParams)
3518        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
3519            self.iterateOverMagnetModel(gatherMagnetParams)
3520
3521        if format=="":
3522            formatted_output = FittingUtilities.formatParameters(param_list)
3523        elif format == "Excel":
3524            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3525        elif format == "Latex":
3526            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3527        else:
3528            raise AttributeError("Bad format specifier.")
3529
3530        # Dump formatted_output to the clipboard
3531        cb = QtWidgets.QApplication.clipboard()
3532        cb.setText(formatted_output)
3533
3534    def onParameterPaste(self):
3535        """
3536        Use the clipboard to update fit state
3537        """
3538        # Check if the clipboard contains right stuff
3539        cb = QtWidgets.QApplication.clipboard()
3540        cb_text = cb.text()
3541
3542        context = {}
3543        # put the text into dictionary
3544        lines = cb_text.split(':')
3545        if lines[0] != 'sasview_parameter_values':
3546            return False
3547
3548        model = lines[1].split(',')
3549
3550        if model[0] != 'model_name':
3551            return False
3552
3553        context['model_name'] = [model[1]]
3554        for line in lines[2:-1]:
3555            if len(line) != 0:
3556                item = line.split(',')
3557                check = item[1]
3558                name = item[0]
3559                value = item[2]
3560                # Transfer the text to content[dictionary]
3561                context[name] = [check, value]
3562
3563                # limits
3564                try:
3565                    limit_lo = item[3]
3566                    context[name].append(limit_lo)
3567                    limit_hi = item[4]
3568                    context[name].append(limit_hi)
3569                except:
3570                    pass
3571
3572                # Polydisp
3573                if len(item) > 5:
3574                    value = item[5]
3575                    context[name].append(value)
3576                    try:
3577                        value = item[6]
3578                        context[name].append(value)
3579                        value = item[7]
3580                        context[name].append(value)
3581                    except IndexError:
3582                        pass
3583
3584        if str(self.cbModel.currentText()) != str(context['model_name'][0]):
3585            msg = QtWidgets.QMessageBox()
3586            msg.setIcon(QtWidgets.QMessageBox.Information)
3587            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3588                         Not all parameters saved may paste correctly.")
3589            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3590            result = msg.exec_()
3591            if result == QtWidgets.QMessageBox.Ok:
3592                pass
3593            else:
3594                return
3595
3596        self.updateFullModel(context)
3597        self.updateFullPolyModel(context)
3598
3599    def updateFullModel(self, param_dict):
3600        """
3601        Update the model with new parameters
3602        """
3603        assert isinstance(param_dict, dict)
3604        if not dict:
3605            return
3606
3607        def updateFittedValues(row):
3608            # Utility function for main model update
3609            # internal so can use closure for param_dict
3610            param_name = str(self._model_model.item(row, 0).text())
3611            if param_name not in list(param_dict.keys()):
3612                return
3613            # checkbox state
3614            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3615            self._model_model.item(row, 0).setCheckState(param_checked)
3616
3617            # modify the param value
3618            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3619            self._model_model.item(row, 1).setText(param_repr)
3620
3621            # Potentially the error column
3622            ioffset = 0
3623            if len(param_dict[param_name])>4 and self.has_error_column:
3624                # error values are not editable - no need to update
3625                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3626                #self._model_model.item(row, 2).setText(error_repr)
3627                ioffset = 1
3628            # min/max
3629            try:
3630                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3631                self._model_model.item(row, 2+ioffset).setText(param_repr)
3632                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3633                self._model_model.item(row, 3+ioffset).setText(param_repr)
3634            except:
3635                pass
3636
3637            self.setFocus()
3638
3639
3640
3641        # block signals temporarily, so we don't end up
3642        # updating charts with every single model change on the end of fitting
3643        self._model_model.blockSignals(True)
3644        self.iterateOverModel(updateFittedValues)
3645        self._model_model.blockSignals(False)
3646
3647
3648    def updateFullPolyModel(self, param_dict):
3649        """
3650        Update the polydispersity model with new parameters, create the errors column
3651        """
3652        assert isinstance(param_dict, dict)
3653        if not dict:
3654            return
3655
3656        def updateFittedValues(row):
3657            # Utility function for main model update
3658            # internal so can use closure for param_dict
3659            if row >= self._poly_model.rowCount():
3660                return
3661            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3662            if param_name not in list(param_dict.keys()):
3663                return
3664            # checkbox state
3665            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3666            self._poly_model.item(row,0).setCheckState(param_checked)
3667
3668            # modify the param value
3669            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3670            self._poly_model.item(row, 1).setText(param_repr)
3671
3672            # Potentially the error column
3673            ioffset = 0
3674            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3675                ioffset = 1
3676            # min
3677            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3678            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3679            # max
3680            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3681            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3682            # Npts
3683            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3684            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3685            # Nsigs
3686            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3687            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3688
3689            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3690            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3691            self.setFocus()
3692
3693        # block signals temporarily, so we don't end up
3694        # updating charts with every single model change on the end of fitting
3695        self._poly_model.blockSignals(True)
3696        self.iterateOverPolyModel(updateFittedValues)
3697        self._poly_model.blockSignals(False)
3698
3699
3700
Note: See TracBrowser for help on using the repository browser.