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

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 10fee37 was 10fee37, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 2 years ago

misc fixes to fitpage save

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