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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 7fb68060 was 7fb68060, checked in by piotr, 5 years ago

Minor fixes to the combobox enablement and multiplicity choice. SASVIEW-1153

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