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

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

Fixed logic in 2D batch fitting. SASVIEW-1168

  • Property mode set to 100644
File size: 136.4 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            return
2496        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2497
2498        # assure the current index is set properly for batch
2499        if len(self._logic) > 1:
2500            for i, logic in enumerate(self._logic):
2501                if logic.data.name in fitted_data.name:
2502                    self.data_index = i
2503
2504        residuals = self.calculateResiduals(fitted_data)
2505        self.model_data = fitted_data
2506        new_plots = [fitted_data]
2507        if residuals is not None:
2508            new_plots.append(residuals)
2509
2510        if self.data_is_loaded:
2511            # delete any plots associated with the data that were not updated
2512            # (e.g. to remove beta(Q), S_eff(Q))
2513            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2514            pass
2515        else:
2516            # delete theory items for the model, in order to get rid of any
2517            # redundant items, e.g. beta(Q), S_eff(Q)
2518            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2519
2520        # Create plots for parameters with enabled polydispersity
2521        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2522            data_id = fitted_data.id.split()
2523            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2524            data_name = fitted_data.name.split()
2525            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2526            self.createNewIndex(plot)
2527            new_plots.append(plot)
2528
2529        # Create plots for intermediate product data
2530        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2531        for plot in plots:
2532            plot.symbol = "Line"
2533            self.createNewIndex(plot)
2534            new_plots.append(plot)
2535
2536        for plot in new_plots:
2537            self.communicate.plotUpdateSignal.emit([plot])
2538
2539    def complete2D(self, return_data):
2540        """
2541        Plot the current 2D data
2542        """
2543        # Bring the GUI to normal state
2544        self.enableInteractiveElements()
2545
2546        if return_data is None:
2547            return
2548
2549        fitted_data = self.logic.new2DPlot(return_data)
2550        # assure the current index is set properly for batch
2551        if len(self._logic) > 1:
2552            for i, logic in enumerate(self._logic):
2553                if logic.data.name in fitted_data.name:
2554                    self.data_index = i
2555
2556        residuals = self.calculateResiduals(fitted_data)
2557        self.model_data = fitted_data
2558        new_plots = [fitted_data]
2559        if residuals is not None:
2560            new_plots.append(residuals)
2561
2562        # Update/generate plots
2563        for plot in new_plots:
2564            self.communicate.plotUpdateSignal.emit([plot])
2565
2566    def calculateResiduals(self, fitted_data):
2567        """
2568        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2569        """
2570        # Create a new index for holding data
2571        fitted_data.symbol = "Line"
2572
2573        # Modify fitted_data with weighting
2574        weighted_data = self.addWeightingToData(fitted_data)
2575
2576        self.createNewIndex(weighted_data)
2577
2578        # Calculate difference between return_data and logic.data
2579        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2580        # Update the control
2581        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2582        self.lblChi2Value.setText(chi2_repr)
2583
2584        # Plot residuals if actual data
2585        if not self.data_is_loaded:
2586            return
2587
2588        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2589        if residuals_plot is None:
2590            return
2591        residuals_plot.id = "Residual " + residuals_plot.id
2592        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2593        self.createNewIndex(residuals_plot)
2594        return residuals_plot
2595
2596    def onCategoriesChanged(self):
2597            """
2598            Reload the category/model comboboxes
2599            """
2600            # Store the current combo indices
2601            current_cat = self.cbCategory.currentText()
2602            current_model = self.cbModel.currentText()
2603
2604            # reread the category file and repopulate the combo
2605            self.cbCategory.blockSignals(True)
2606            self.cbCategory.clear()
2607            self.readCategoryInfo()
2608            self.initializeCategoryCombo()
2609
2610            # Scroll back to the original index in Categories
2611            new_index = self.cbCategory.findText(current_cat)
2612            if new_index != -1:
2613                self.cbCategory.setCurrentIndex(new_index)
2614            self.cbCategory.blockSignals(False)
2615            # ...and in the Models
2616            self.cbModel.blockSignals(True)
2617            new_index = self.cbModel.findText(current_model)
2618            if new_index != -1:
2619                self.cbModel.setCurrentIndex(new_index)
2620            self.cbModel.blockSignals(False)
2621
2622            return
2623
2624    def calcException(self, etype, value, tb):
2625        """
2626        Thread threw an exception.
2627        """
2628        # Bring the GUI to normal state
2629        self.enableInteractiveElements()
2630        # TODO: remimplement thread cancellation
2631        logger.error("".join(traceback.format_exception(etype, value, tb)))
2632
2633    def setTableProperties(self, table):
2634        """
2635        Setting table properties
2636        """
2637        # Table properties
2638        table.verticalHeader().setVisible(False)
2639        table.setAlternatingRowColors(True)
2640        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2641        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2642        table.resizeColumnsToContents()
2643
2644        # Header
2645        header = table.horizontalHeader()
2646        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2647        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2648
2649        # Qt5: the following 2 lines crash - figure out why!
2650        # Resize column 0 and 7 to content
2651        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2652        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2653
2654    def setPolyModel(self):
2655        """
2656        Set polydispersity values
2657        """
2658        if not self.model_parameters:
2659            return
2660        self._poly_model.clear()
2661
2662        parameters = self.model_parameters.form_volume_parameters
2663        if self.is2D:
2664            parameters += self.model_parameters.orientation_parameters
2665
2666        [self.setPolyModelParameters(i, param) for i, param in \
2667            enumerate(parameters) if param.polydisperse]
2668
2669        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2670
2671    def setPolyModelParameters(self, i, param):
2672        """
2673        Standard of multishell poly parameter driver
2674        """
2675        param_name = param.name
2676        # see it the parameter is multishell
2677        if '[' in param.name:
2678            # Skip empty shells
2679            if self.current_shell_displayed == 0:
2680                return
2681            else:
2682                # Create as many entries as current shells
2683                for ishell in range(1, self.current_shell_displayed+1):
2684                    # Remove [n] and add the shell numeral
2685                    name = param_name[0:param_name.index('[')] + str(ishell)
2686                    self.addNameToPolyModel(i, name)
2687        else:
2688            # Just create a simple param entry
2689            self.addNameToPolyModel(i, param_name)
2690
2691    def addNameToPolyModel(self, i, param_name):
2692        """
2693        Creates a checked row in the poly model with param_name
2694        """
2695        # Polydisp. values from the sasmodel
2696        width = self.kernel_module.getParam(param_name + '.width')
2697        npts = self.kernel_module.getParam(param_name + '.npts')
2698        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2699        _, min, max = self.kernel_module.details[param_name]
2700
2701        # Update local param dict
2702        self.poly_params[param_name + '.width'] = width
2703        self.poly_params[param_name + '.npts'] = npts
2704        self.poly_params[param_name + '.nsigmas'] = nsigs
2705
2706        # Construct a row with polydisp. related variable.
2707        # This will get added to the polydisp. model
2708        # Note: last argument needs extra space padding for decent display of the control
2709        checked_list = ["Distribution of " + param_name, str(width),
2710                        str(min), str(max),
2711                        str(npts), str(nsigs), "gaussian      ",'']
2712        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2713
2714        # All possible polydisp. functions as strings in combobox
2715        func = QtWidgets.QComboBox()
2716        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2717        # Set the default index
2718        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2719        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2720        self.lstPoly.setIndexWidget(ind, func)
2721        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2722
2723    def onPolyFilenameChange(self, row_index):
2724        """
2725        Respond to filename_updated signal from the delegate
2726        """
2727        # For the given row, invoke the "array" combo handler
2728        array_caption = 'array'
2729
2730        # Get the combo box reference
2731        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2732        widget = self.lstPoly.indexWidget(ind)
2733
2734        # Update the combo box so it displays "array"
2735        widget.blockSignals(True)
2736        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2737        widget.blockSignals(False)
2738
2739        # Invoke the file reader
2740        self.onPolyComboIndexChange(array_caption, row_index)
2741
2742    def onPolyComboIndexChange(self, combo_string, row_index):
2743        """
2744        Modify polydisp. defaults on function choice
2745        """
2746        # Get npts/nsigs for current selection
2747        param = self.model_parameters.form_volume_parameters[row_index]
2748        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2749        combo_box = self.lstPoly.indexWidget(file_index)
2750
2751        def updateFunctionCaption(row):
2752            # Utility function for update of polydispersity function name in the main model
2753            if not self.isCheckable(row):
2754                return
2755            self._model_model.blockSignals(True)
2756            param_name = str(self._model_model.item(row, 0).text())
2757            self._model_model.blockSignals(False)
2758            if param_name !=  param.name:
2759                return
2760            # Modify the param value
2761            self._model_model.blockSignals(True)
2762            if self.has_error_column:
2763                # err column changes the indexing
2764                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2765            else:
2766                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2767            self._model_model.blockSignals(False)
2768
2769        if combo_string == 'array':
2770            try:
2771                self.loadPolydispArray(row_index)
2772                # Update main model for display
2773                self.iterateOverModel(updateFunctionCaption)
2774                # disable the row
2775                lo = self.lstPoly.itemDelegate().poly_pd
2776                hi = self.lstPoly.itemDelegate().poly_function
2777                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2778                return
2779            except IOError:
2780                combo_box.setCurrentIndex(self.orig_poly_index)
2781                # Pass for cancel/bad read
2782                pass
2783
2784        # Enable the row in case it was disabled by Array
2785        self._poly_model.blockSignals(True)
2786        max_range = self.lstPoly.itemDelegate().poly_filename
2787        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2788        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2789        self._poly_model.setData(file_index, "")
2790        self._poly_model.blockSignals(False)
2791
2792        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2793        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2794
2795        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2796        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2797
2798        self._poly_model.setData(npts_index, npts)
2799        self._poly_model.setData(nsigs_index, nsigs)
2800
2801        self.iterateOverModel(updateFunctionCaption)
2802        self.orig_poly_index = combo_box.currentIndex()
2803
2804    def loadPolydispArray(self, row_index):
2805        """
2806        Show the load file dialog and loads requested data into state
2807        """
2808        datafile = QtWidgets.QFileDialog.getOpenFileName(
2809            self, "Choose a weight file", "", "All files (*.*)", None,
2810            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2811
2812        if not datafile:
2813            logger.info("No weight data chosen.")
2814            raise IOError
2815
2816        values = []
2817        weights = []
2818        def appendData(data_tuple):
2819            """
2820            Fish out floats from a tuple of strings
2821            """
2822            try:
2823                values.append(float(data_tuple[0]))
2824                weights.append(float(data_tuple[1]))
2825            except (ValueError, IndexError):
2826                # just pass through if line with bad data
2827                return
2828
2829        with open(datafile, 'r') as column_file:
2830            column_data = [line.rstrip().split() for line in column_file.readlines()]
2831            [appendData(line) for line in column_data]
2832
2833        # If everything went well - update the sasmodel values
2834        self.disp_model = POLYDISPERSITY_MODELS['array']()
2835        self.disp_model.set_weights(np.array(values), np.array(weights))
2836        # + update the cell with filename
2837        fname = os.path.basename(str(datafile))
2838        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2839        self._poly_model.setData(fname_index, fname)
2840
2841    def onColumnWidthUpdate(self, index, old_size, new_size):
2842        """
2843        Simple state update of the current column widths in the  param list
2844        """
2845        self.lstParamHeaderSizes[index] = new_size
2846
2847    def setMagneticModel(self):
2848        """
2849        Set magnetism values on model
2850        """
2851        if not self.model_parameters:
2852            return
2853        self._magnet_model.clear()
2854        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2855            self.model_parameters.call_parameters if param.type == 'magnetic']
2856        FittingUtilities.addHeadersToModel(self._magnet_model)
2857
2858    def shellNamesList(self):
2859        """
2860        Returns list of names of all multi-shell parameters
2861        E.g. for sld[n], radius[n], n=1..3 it will return
2862        [sld1, sld2, sld3, radius1, radius2, radius3]
2863        """
2864        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2865        top_index = self.kernel_module.multiplicity_info.number
2866        shell_names = []
2867        for i in range(1, top_index+1):
2868            for name in multi_names:
2869                shell_names.append(name+str(i))
2870        return shell_names
2871
2872    def addCheckedMagneticListToModel(self, param, model):
2873        """
2874        Wrapper for model update with a subset of magnetic parameters
2875        """
2876        if param.name[param.name.index(':')+1:] in self.shell_names:
2877            # check if two-digit shell number
2878            try:
2879                shell_index = int(param.name[-2:])
2880            except ValueError:
2881                shell_index = int(param.name[-1:])
2882
2883            if shell_index > self.current_shell_displayed:
2884                return
2885
2886        checked_list = [param.name,
2887                        str(param.default),
2888                        str(param.limits[0]),
2889                        str(param.limits[1]),
2890                        param.units]
2891
2892        self.magnet_params[param.name] = param.default
2893
2894        FittingUtilities.addCheckedListToModel(model, checked_list)
2895
2896    def enableStructureFactorControl(self, structure_factor):
2897        """
2898        Add structure factors to the list of parameters
2899        """
2900        if self.kernel_module.is_form_factor or structure_factor == 'None':
2901            self.enableStructureCombo()
2902        else:
2903            self.disableStructureCombo()
2904
2905    def addExtraShells(self):
2906        """
2907        Add a combobox for multiple shell display
2908        """
2909        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2910
2911        if param_length == 0:
2912            return
2913
2914        # cell 1: variable name
2915        item1 = QtGui.QStandardItem(param_name)
2916
2917        func = QtWidgets.QComboBox()
2918
2919        # cell 2: combobox
2920        item2 = QtGui.QStandardItem()
2921
2922        # cell 3: min value
2923        item3 = QtGui.QStandardItem()
2924
2925        # cell 4: max value
2926        item4 = QtGui.QStandardItem()
2927
2928        # cell 4: SLD button
2929        item5 = QtGui.QStandardItem()
2930        button = QtWidgets.QPushButton()
2931        button.setText("Show SLD Profile")
2932
2933        self._model_model.appendRow([item1, item2, item3, item4, item5])
2934
2935        # Beautify the row:  span columns 2-4
2936        shell_row = self._model_model.rowCount()
2937        shell_index = self._model_model.index(shell_row-1, 1)
2938        button_index = self._model_model.index(shell_row-1, 4)
2939
2940        self.lstParams.setIndexWidget(shell_index, func)
2941        self.lstParams.setIndexWidget(button_index, button)
2942        self._n_shells_row = shell_row - 1
2943
2944        # Get the default number of shells for the model
2945        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
2946        shell_par = None
2947        for par in kernel_pars:
2948            if par.name == param_name:
2949                shell_par = par
2950                break
2951        if not shell_par:
2952            logger.error("Could not find %s in kernel parameters.", param_name)
2953        default_shell_count = shell_par.default
2954        shell_min = 0
2955        shell_max = 0
2956        try:
2957            shell_min = int(shell_par.limits[0])
2958            shell_max = int(shell_par.limits[1])
2959        except IndexError as ex:
2960            # no info about limits
2961            pass
2962        item3.setText(str(shell_min))
2963        item4.setText(str(shell_max))
2964
2965        # Respond to index change
2966        func.currentTextChanged.connect(self.modifyShellsInList)
2967
2968        # Respond to button press
2969        button.clicked.connect(self.onShowSLDProfile)
2970
2971        # Available range of shells displayed in the combobox
2972        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
2973
2974        # Add default number of shells to the model
2975        func.setCurrentText(str(default_shell_count))
2976
2977    def modifyShellsInList(self, text):
2978        """
2979        Add/remove additional multishell parameters
2980        """
2981        # Find row location of the combobox
2982        first_row = self._n_shells_row + 1
2983        remove_rows = self._num_shell_params
2984        try:
2985            index = int(text)
2986        except ValueError:
2987            # bad text on the control!
2988            index = 0
2989            logger.error("Multiplicity incorrect! Setting to 0")
2990        self.kernel_module.multiplicity = index
2991        if remove_rows > 1:
2992            self._model_model.removeRows(first_row, remove_rows)
2993
2994        new_rows = FittingUtilities.addShellsToModel(
2995                self.model_parameters,
2996                self._model_model,
2997                index,
2998                first_row,
2999                self.lstParams)
3000
3001        self._num_shell_params = len(new_rows)
3002        self.current_shell_displayed = index
3003
3004        # Param values for existing shells were reset to default; force all changes into kernel module
3005        for row in new_rows:
3006            par = row[0].text()
3007            val = GuiUtils.toDouble(row[1].text())
3008            self.kernel_module.setParam(par, val)
3009
3010        # Change 'n' in the parameter model; also causes recalculation
3011        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3012
3013        # Update relevant models
3014        self.setPolyModel()
3015        self.setMagneticModel()
3016
3017    def onShowSLDProfile(self):
3018        """
3019        Show a quick plot of SLD profile
3020        """
3021        # get profile data
3022        x, y = self.kernel_module.getProfile()
3023        y *= 1.0e6
3024        profile_data = Data1D(x=x, y=y)
3025        profile_data.name = "SLD"
3026        profile_data.scale = 'linear'
3027        profile_data.symbol = 'Line'
3028        profile_data.hide_error = True
3029        profile_data._xaxis = "R(\AA)"
3030        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3031
3032        plotter = PlotterWidget(self, quickplot=True)
3033        plotter.data = profile_data
3034        plotter.showLegend = True
3035        plotter.plot(hide_error=True, marker='-')
3036
3037        self.plot_widget = QtWidgets.QWidget()
3038        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3039        layout = QtWidgets.QVBoxLayout()
3040        layout.addWidget(plotter)
3041        self.plot_widget.setLayout(layout)
3042        self.plot_widget.show()
3043
3044    def setInteractiveElements(self, enabled=True):
3045        """
3046        Switch interactive GUI elements on/off
3047        """
3048        assert isinstance(enabled, bool)
3049
3050        self.lstParams.setEnabled(enabled)
3051        self.lstPoly.setEnabled(enabled)
3052        self.lstMagnetic.setEnabled(enabled)
3053
3054        self.cbCategory.setEnabled(enabled)
3055        self.cbModel.setEnabled(enabled)
3056        self.cbStructureFactor.setEnabled(enabled)
3057
3058        self.chkPolydispersity.setEnabled(enabled)
3059        self.chkMagnetism.setEnabled(enabled)
3060        self.chk2DView.setEnabled(enabled)
3061
3062    def enableInteractiveElements(self):
3063        """
3064        Set buttion caption on fitting/calculate finish
3065        Enable the param table(s)
3066        """
3067        # Notify the user that fitting is available
3068        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3069        self.cmdFit.setText("Fit")
3070        self.fit_started = False
3071        self.setInteractiveElements(True)
3072
3073    def disableInteractiveElements(self):
3074        """
3075        Set buttion caption on fitting/calculate start
3076        Disable the param table(s)
3077        """
3078        # Notify the user that fitting is being run
3079        # Allow for stopping the job
3080        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3081        self.cmdFit.setText('Stop fit')
3082        self.setInteractiveElements(False)
3083
3084    def readFitPage(self, fp):
3085        """
3086        Read in state from a fitpage object and update GUI
3087        """
3088        assert isinstance(fp, FitPage)
3089        # Main tab info
3090        self.logic.data.filename = fp.filename
3091        self.data_is_loaded = fp.data_is_loaded
3092        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3093        self.chkMagnetism.setCheckState(fp.is_magnetic)
3094        self.chk2DView.setCheckState(fp.is2D)
3095
3096        # Update the comboboxes
3097        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3098        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3099        if fp.current_factor:
3100            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3101
3102        self.chi2 = fp.chi2
3103
3104        # Options tab
3105        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3106        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3107        self.npts = fp.fit_options[fp.NPTS]
3108        self.log_points = fp.fit_options[fp.LOG_POINTS]
3109        self.weighting = fp.fit_options[fp.WEIGHTING]
3110
3111        # Models
3112        self._model_model = fp.model_model
3113        self._poly_model = fp.poly_model
3114        self._magnet_model = fp.magnetism_model
3115
3116        # Resolution tab
3117        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3118        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3119        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3120        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3121        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3122
3123        # TODO: add polidyspersity and magnetism
3124
3125    def saveToFitPage(self, fp):
3126        """
3127        Write current state to the given fitpage
3128        """
3129        assert isinstance(fp, FitPage)
3130
3131        # Main tab info
3132        fp.filename = self.logic.data.filename
3133        fp.data_is_loaded = self.data_is_loaded
3134        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3135        fp.is_magnetic = self.chkMagnetism.isChecked()
3136        fp.is2D = self.chk2DView.isChecked()
3137        fp.data = self.data
3138
3139        # Use current models - they contain all the required parameters
3140        fp.model_model = self._model_model
3141        fp.poly_model = self._poly_model
3142        fp.magnetism_model = self._magnet_model
3143
3144        if self.cbCategory.currentIndex() != 0:
3145            fp.current_category = str(self.cbCategory.currentText())
3146            fp.current_model = str(self.cbModel.currentText())
3147
3148        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3149            fp.current_factor = str(self.cbStructureFactor.currentText())
3150        else:
3151            fp.current_factor = ''
3152
3153        fp.chi2 = self.chi2
3154        fp.main_params_to_fit = self.main_params_to_fit
3155        fp.poly_params_to_fit = self.poly_params_to_fit
3156        fp.magnet_params_to_fit = self.magnet_params_to_fit
3157        fp.kernel_module = self.kernel_module
3158
3159        # Algorithm options
3160        # fp.algorithm = self.parent.fit_options.selected_id
3161
3162        # Options tab
3163        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3164        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3165        fp.fit_options[fp.NPTS] = self.npts
3166        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3167        fp.fit_options[fp.LOG_POINTS] = self.log_points
3168        fp.fit_options[fp.WEIGHTING] = self.weighting
3169
3170        # Resolution tab
3171        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3172        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3173        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3174        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3175        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3176
3177        # TODO: add polidyspersity and magnetism
3178
3179    def updateUndo(self):
3180        """
3181        Create a new state page and add it to the stack
3182        """
3183        if self.undo_supported:
3184            self.pushFitPage(self.currentState())
3185
3186    def currentState(self):
3187        """
3188        Return fit page with current state
3189        """
3190        new_page = FitPage()
3191        self.saveToFitPage(new_page)
3192
3193        return new_page
3194
3195    def pushFitPage(self, new_page):
3196        """
3197        Add a new fit page object with current state
3198        """
3199        self.page_stack.append(new_page)
3200
3201    def popFitPage(self):
3202        """
3203        Remove top fit page from stack
3204        """
3205        if self.page_stack:
3206            self.page_stack.pop()
3207
3208    def getReport(self):
3209        """
3210        Create and return HTML report with parameters and charts
3211        """
3212        index = None
3213        if self.all_data:
3214            index = self.all_data[self.data_index]
3215        else:
3216            index = self.theory_item
3217        report_logic = ReportPageLogic(self,
3218                                       kernel_module=self.kernel_module,
3219                                       data=self.data,
3220                                       index=index,
3221                                       model=self._model_model)
3222
3223        return report_logic.reportList()
3224
3225    def savePageState(self):
3226        """
3227        Create and serialize local PageState
3228        """
3229        from sas.sascalc.fit.pagestate import Reader
3230        model = self.kernel_module
3231
3232        # Old style PageState object
3233        state = PageState(model=model, data=self.data)
3234
3235        # Add parameter data to the state
3236        self.getCurrentFitState(state)
3237
3238        # Create the filewriter, aptly named 'Reader'
3239        state_reader = Reader(self.loadPageStateCallback)
3240        filepath = self.saveAsAnalysisFile()
3241        if filepath is None or filepath == "":
3242            return
3243        state_reader.write(filename=filepath, fitstate=state)
3244        pass
3245
3246    def saveAsAnalysisFile(self):
3247        """
3248        Show the save as... dialog and return the chosen filepath
3249        """
3250        default_name = "FitPage"+str(self.tab_id)+".fitv"
3251
3252        wildcard = "fitv files (*.fitv)"
3253        kwargs = {
3254            'caption'   : 'Save As',
3255            'directory' : default_name,
3256            'filter'    : wildcard,
3257            'parent'    : None,
3258        }
3259        # Query user for filename.
3260        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3261        filename = filename_tuple[0]
3262        return filename
3263
3264    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3265        """
3266        This is a callback method called from the CANSAS reader.
3267        We need the instance of this reader only for writing out a file,
3268        so there's nothing here.
3269        Until Load Analysis is implemented, that is.
3270        """
3271        pass
3272
3273    def loadPageState(self, pagestate=None):
3274        """
3275        Load the PageState object and update the current widget
3276        """
3277        pass
3278
3279    def getCurrentFitState(self, state=None):
3280        """
3281        Store current state for fit_page
3282        """
3283        # save model option
3284        #if self.model is not None:
3285        #    self.disp_list = self.getDispParamList()
3286        #    state.disp_list = copy.deepcopy(self.disp_list)
3287        #    #state.model = self.model.clone()
3288
3289        # Comboboxes
3290        state.categorycombobox = self.cbCategory.currentText()
3291        state.formfactorcombobox = self.cbModel.currentText()
3292        if self.cbStructureFactor.isEnabled():
3293            state.structurecombobox = self.cbStructureFactor.currentText()
3294        state.tcChi = self.chi2
3295
3296        state.enable2D = self.is2D
3297
3298        #state.weights = copy.deepcopy(self.weights)
3299        # save data
3300        state.data = copy.deepcopy(self.data)
3301
3302        # save plotting range
3303        state.qmin = self.q_range_min
3304        state.qmax = self.q_range_max
3305        state.npts = self.npts
3306
3307        #    self.state.enable_disp = self.enable_disp.GetValue()
3308        #    self.state.disable_disp = self.disable_disp.GetValue()
3309
3310        #    self.state.enable_smearer = \
3311        #                        copy.deepcopy(self.enable_smearer.GetValue())
3312        #    self.state.disable_smearer = \
3313        #                        copy.deepcopy(self.disable_smearer.GetValue())
3314
3315        #self.state.pinhole_smearer = \
3316        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3317        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3318        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3319        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3320        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3321        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3322
3323        p = self.model_parameters
3324        # save checkbutton state and txtcrtl values
3325        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3326        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3327
3328        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3329        #self._copy_parameters_state(self.parameters, self.state.parameters)
3330        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3331        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3332
3333    def onParameterCopy(self, format=None):
3334        """
3335        Copy current parameters into the clipboard
3336        """
3337        # run a loop over all parameters and pull out
3338        # first - regular params
3339        param_list = []
3340
3341        param_list.append(['model_name', str(self.cbModel.currentText())])
3342        def gatherParams(row):
3343            """
3344            Create list of main parameters based on _model_model
3345            """
3346            param_name = str(self._model_model.item(row, 0).text())
3347            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3348            param_value = str(self._model_model.item(row, 1).text())
3349            param_error = None
3350            param_min = None
3351            param_max = None
3352            column_offset = 0
3353            if self.has_error_column:
3354                param_error = str(self._model_model.item(row, 2).text())
3355                column_offset = 1
3356
3357            try:
3358                param_min = str(self._model_model.item(row, 2+column_offset).text())
3359                param_max = str(self._model_model.item(row, 3+column_offset).text())
3360            except:
3361                pass
3362
3363            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3364
3365        def gatherPolyParams(row):
3366            """
3367            Create list of polydisperse parameters based on _poly_model
3368            """
3369            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3370            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3371            param_value = str(self._poly_model.item(row, 1).text())
3372            param_error = None
3373            column_offset = 0
3374            if self.has_poly_error_column:
3375                param_error = str(self._poly_model.item(row, 2).text())
3376                column_offset = 1
3377            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3378            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3379            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3380            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3381            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3382            # width
3383            name = param_name+".width"
3384            param_list.append([name, param_checked, param_value, param_error,
3385                                param_npts, param_nsigs, param_min, param_max, param_fun])
3386
3387        def gatherMagnetParams(row):
3388            """
3389            Create list of magnetic parameters based on _magnet_model
3390            """
3391            param_name = str(self._magnet_model.item(row, 0).text())
3392            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3393            param_value = str(self._magnet_model.item(row, 1).text())
3394            param_error = None
3395            column_offset = 0
3396            if self.has_magnet_error_column:
3397                param_error = str(self._magnet_model.item(row, 2).text())
3398                column_offset = 1
3399            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3400            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3401            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3402
3403        self.iterateOverModel(gatherParams)
3404        if self.chkPolydispersity.isChecked():
3405            self.iterateOverPolyModel(gatherPolyParams)
3406        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
3407            self.iterateOverMagnetModel(gatherMagnetParams)
3408
3409        if format=="":
3410            formatted_output = FittingUtilities.formatParameters(param_list)
3411        elif format == "Excel":
3412            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3413        elif format == "Latex":
3414            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3415        else:
3416            raise AttributeError("Bad format specifier.")
3417
3418        # Dump formatted_output to the clipboard
3419        cb = QtWidgets.QApplication.clipboard()
3420        cb.setText(formatted_output)
3421
3422    def onParameterPaste(self):
3423        """
3424        Use the clipboard to update fit state
3425        """
3426        # Check if the clipboard contains right stuff
3427        cb = QtWidgets.QApplication.clipboard()
3428        cb_text = cb.text()
3429
3430        context = {}
3431        # put the text into dictionary
3432        lines = cb_text.split(':')
3433        if lines[0] != 'sasview_parameter_values':
3434            return False
3435
3436        model = lines[1].split(',')
3437
3438        if model[0] != 'model_name':
3439            return False
3440
3441        context['model_name'] = [model[1]]
3442        for line in lines[2:-1]:
3443            if len(line) != 0:
3444                item = line.split(',')
3445                check = item[1]
3446                name = item[0]
3447                value = item[2]
3448                # Transfer the text to content[dictionary]
3449                context[name] = [check, value]
3450
3451                # limits
3452                try:
3453                    limit_lo = item[3]
3454                    context[name].append(limit_lo)
3455                    limit_hi = item[4]
3456                    context[name].append(limit_hi)
3457                except:
3458                    pass
3459
3460                # Polydisp
3461                if len(item) > 5:
3462                    value = item[5]
3463                    context[name].append(value)
3464                    try:
3465                        value = item[6]
3466                        context[name].append(value)
3467                        value = item[7]
3468                        context[name].append(value)
3469                    except IndexError:
3470                        pass
3471
3472        if str(self.cbModel.currentText()) != str(context['model_name'][0]):
3473            msg = QtWidgets.QMessageBox()
3474            msg.setIcon(QtWidgets.QMessageBox.Information)
3475            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3476                         Not all parameters saved may paste correctly.")
3477            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3478            result = msg.exec_()
3479            if result == QtWidgets.QMessageBox.Ok:
3480                pass
3481            else:
3482                return
3483
3484        self.updateFullModel(context)
3485        self.updateFullPolyModel(context)
3486
3487    def updateFullModel(self, param_dict):
3488        """
3489        Update the model with new parameters
3490        """
3491        assert isinstance(param_dict, dict)
3492        if not dict:
3493            return
3494
3495        def updateFittedValues(row):
3496            # Utility function for main model update
3497            # internal so can use closure for param_dict
3498            param_name = str(self._model_model.item(row, 0).text())
3499            if param_name not in list(param_dict.keys()):
3500                return
3501            # checkbox state
3502            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3503            self._model_model.item(row, 0).setCheckState(param_checked)
3504
3505            # modify the param value
3506            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3507            self._model_model.item(row, 1).setText(param_repr)
3508
3509            # Potentially the error column
3510            ioffset = 0
3511            if len(param_dict[param_name])>4 and self.has_error_column:
3512                # error values are not editable - no need to update
3513                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3514                #self._model_model.item(row, 2).setText(error_repr)
3515                ioffset = 1
3516            # min/max
3517            try:
3518                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3519                self._model_model.item(row, 2+ioffset).setText(param_repr)
3520                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3521                self._model_model.item(row, 3+ioffset).setText(param_repr)
3522            except:
3523                pass
3524
3525            self.setFocus()
3526
3527
3528
3529        # block signals temporarily, so we don't end up
3530        # updating charts with every single model change on the end of fitting
3531        self._model_model.blockSignals(True)
3532        self.iterateOverModel(updateFittedValues)
3533        self._model_model.blockSignals(False)
3534
3535
3536    def updateFullPolyModel(self, param_dict):
3537        """
3538        Update the polydispersity model with new parameters, create the errors column
3539        """
3540        assert isinstance(param_dict, dict)
3541        if not dict:
3542            return
3543
3544        def updateFittedValues(row):
3545            # Utility function for main model update
3546            # internal so can use closure for param_dict
3547            if row >= self._poly_model.rowCount():
3548                return
3549            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3550            if param_name not in list(param_dict.keys()):
3551                return
3552            # checkbox state
3553            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3554            self._poly_model.item(row,0).setCheckState(param_checked)
3555
3556            # modify the param value
3557            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3558            self._poly_model.item(row, 1).setText(param_repr)
3559
3560            # Potentially the error column
3561            ioffset = 0
3562            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3563                ioffset = 1
3564            # min
3565            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3566            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3567            # max
3568            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3569            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3570            # Npts
3571            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3572            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3573            # Nsigs
3574            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3575            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3576
3577            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3578            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3579            self.setFocus()
3580
3581        # block signals temporarily, so we don't end up
3582        # updating charts with every single model change on the end of fitting
3583        self._poly_model.blockSignals(True)
3584        self.iterateOverPolyModel(updateFittedValues)
3585        self._poly_model.blockSignals(False)
3586
3587
3588
Note: See TracBrowser for help on using the repository browser.