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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9d130f3 was 9d130f3, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Replace method names for C&S fit calls. SASVIEW-1159

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