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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 44deced was 44deced, checked in by ibressler, 6 years ago

plotPolydispersities() does not need POLYDISPERSITY_MODELS anymore

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