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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since f53bc04 was f53bc04, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

When populating the polydispersity table, don't forget about orientation parameters. SASVIEW-1013

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