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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since cb39d66 was cb39d66, checked in by GitHub <noreply@…>, 8 months ago

Merge pull request #184 from SasView?/ESS_GUI_iss1052

SASVIEW-1052: Let user choose if radius_effective is computed from form factor, or free/fittable

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