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

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

allow for only P(Q) or S(Q) to be present in intermediate results; improve comments

  • Property mode set to 100644
File size: 117.4 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5import copy
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10import webbrowser
11
12from PyQt5 import QtCore
13from PyQt5 import QtGui
14from PyQt5 import QtWidgets
15
16from sasmodels import generate
17from sasmodels import modelinfo
18from sasmodels.sasview_model import load_standard_models
19from sasmodels.sasview_model import MultiplicationModel
20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
23from sas.sascalc.fit.pagestate import PageState
24
25import sas.qtgui.Utilities.GuiUtils as GuiUtils
26import sas.qtgui.Utilities.LocalConfig as LocalConfig
27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
30
31from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
32from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
33from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
34
35from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
36from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
37from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
38from sas.qtgui.Perspectives.Fitting import FittingUtilities
39from sas.qtgui.Perspectives.Fitting import ModelUtilities
40from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
41from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
42from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
43from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
44from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
45from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
46from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
47from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
48from sas.qtgui.Perspectives.Fitting.ReportPageLogic import ReportPageLogic
49
50
51
52TAB_MAGNETISM = 4
53TAB_POLY = 3
54CATEGORY_DEFAULT = "Choose category..."
55CATEGORY_STRUCTURE = "Structure Factor"
56CATEGORY_CUSTOM = "Plugin Models"
57STRUCTURE_DEFAULT = "None"
58
59DEFAULT_POLYDISP_FUNCTION = 'gaussian'
60
61
62logger = logging.getLogger(__name__)
63
64
65class ToolTippedItemModel(QtGui.QStandardItemModel):
66    """
67    Subclass from QStandardItemModel to allow displaying tooltips in
68    QTableView model.
69    """
70    def __init__(self, parent=None):
71        QtGui.QStandardItemModel.__init__(self, parent)
72
73    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
74        """
75        Displays tooltip for each column's header
76        :param section:
77        :param orientation:
78        :param role:
79        :return:
80        """
81        if role == QtCore.Qt.ToolTipRole:
82            if orientation == QtCore.Qt.Horizontal:
83                return str(self.header_tooltips[section])
84
85        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
86
87class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
88    """
89    Main widget for selecting form and structure factor models
90    """
91    constraintAddedSignal = QtCore.pyqtSignal(list)
92    newModelSignal = QtCore.pyqtSignal()
93    fittingFinishedSignal = QtCore.pyqtSignal(tuple)
94    batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
95    Calc1DFinishedSignal = QtCore.pyqtSignal(tuple)
96    Calc2DFinishedSignal = QtCore.pyqtSignal(tuple)
97
98    def __init__(self, parent=None, data=None, tab_id=1):
99
100        super(FittingWidget, self).__init__()
101
102        # Necessary globals
103        self.parent = parent
104
105        # Which tab is this widget displayed in?
106        self.tab_id = tab_id
107
108        # Globals
109        self.initializeGlobals()
110
111        # data index for the batch set
112        self.data_index = 0
113        # Main Data[12]D holders
114        # Logics.data contains a single Data1D/Data2D object
115        self._logic = [FittingLogic()]
116
117        # Main GUI setup up
118        self.setupUi(self)
119        self.setWindowTitle("Fitting")
120
121        # Set up tabs widgets
122        self.initializeWidgets()
123
124        # Set up models and views
125        self.initializeModels()
126
127        # Defaults for the structure factors
128        self.setDefaultStructureCombo()
129
130        # Make structure factor and model CBs disabled
131        self.disableModelCombo()
132        self.disableStructureCombo()
133
134        # Generate the category list for display
135        self.initializeCategoryCombo()
136
137        # Initial control state
138        self.initializeControls()
139
140        QtWidgets.QApplication.processEvents()
141
142        # Connect signals to controls
143        self.initializeSignals()
144
145        if data is not None:
146            self.data = data
147
148        # New font to display angstrom symbol
149        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
150        self.label_17.setStyleSheet(new_font)
151        self.label_19.setStyleSheet(new_font)
152
153    @property
154    def logic(self):
155        # make sure the logic contains at least one element
156        assert self._logic
157        # logic connected to the currently shown data
158        return self._logic[self.data_index]
159
160    @property
161    def data(self):
162        return self.logic.data
163
164    @data.setter
165    def data(self, value):
166        """ data setter """
167        # Value is either a list of indices for batch fitting or a simple index
168        # for standard fitting. Assure we have a list, regardless.
169        if isinstance(value, list):
170            self.is_batch_fitting = True
171        else:
172            value = [value]
173
174        assert isinstance(value[0], QtGui.QStandardItem)
175
176        # Keep reference to all datasets for batch
177        self.all_data = value
178
179        # Create logics with data items
180        # Logics.data contains only a single Data1D/Data2D object
181        if len(value) == 1:
182            # single data logic is already defined, update data on it
183            self._logic[0].data = GuiUtils.dataFromItem(value[0])
184        else:
185            # batch datasets
186            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            if not fitted_data.name:
2116                name = self.nameForFittedData(self.kernel_module.id)
2117            else:
2118                name = fitted_data.name
2119            fitted_data.title = name
2120            fitted_data.filename = name
2121            fitted_data.symbol = "Line"
2122            self.createTheoryIndex(fitted_data)
2123
2124    def updateModelIndex(self, fitted_data):
2125        """
2126        Update a QStandardModelIndex containing model data
2127        """
2128        name = self.nameFromData(fitted_data)
2129        # Make this a line if no other defined
2130        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2131            fitted_data.symbol = 'Line'
2132        # Notify the GUI manager so it can update the main model in DataExplorer
2133        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2134
2135    def createTheoryIndex(self, fitted_data):
2136        """
2137        Create a QStandardModelIndex containing model data
2138        """
2139        name = self.nameFromData(fitted_data)
2140        # Notify the GUI manager so it can create the theory model in DataExplorer
2141        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2142        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
2143
2144    def nameFromData(self, fitted_data):
2145        """
2146        Return name for the dataset. Terribly impure function.
2147        """
2148        if fitted_data.name is None:
2149            name = self.nameForFittedData(self.logic.data.filename)
2150            fitted_data.title = name
2151            fitted_data.name = name
2152            fitted_data.filename = name
2153        else:
2154            name = fitted_data.name
2155        return name
2156
2157    def methodCalculateForData(self):
2158        '''return the method for data calculation'''
2159        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2160
2161    def methodCompleteForData(self):
2162        '''return the method for result parsin on calc complete '''
2163        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2164
2165    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2166        """
2167        Wrapper for Calc1D/2D calls
2168        """
2169        if data is None:
2170            data = self.data
2171        if model is None:
2172            model = self.kernel_module
2173        if completefn is None:
2174            completefn = self.methodCompleteForData()
2175        smearer = self.smearing_widget.smearer()
2176        # Awful API to a backend method.
2177        calc_thread = self.methodCalculateForData()(data=data,
2178                                               model=model,
2179                                               page_id=0,
2180                                               qmin=self.q_range_min,
2181                                               qmax=self.q_range_max,
2182                                               smearer=smearer,
2183                                               state=None,
2184                                               weight=None,
2185                                               fid=None,
2186                                               toggle_mode_on=False,
2187                                               completefn=completefn,
2188                                               update_chisqr=True,
2189                                               exception_handler=self.calcException,
2190                                               source=None)
2191        if use_threads:
2192            if LocalConfig.USING_TWISTED:
2193                # start the thread with twisted
2194                thread = threads.deferToThread(calc_thread.compute)
2195                thread.addCallback(completefn)
2196                thread.addErrback(self.calculateDataFailed)
2197            else:
2198                # Use the old python threads + Queue
2199                calc_thread.queue()
2200                calc_thread.ready(2.5)
2201        else:
2202            results = calc_thread.compute()
2203            completefn(results)
2204
2205    def calculateQGridForModel(self):
2206        """
2207        Prepare the fitting data object, based on current ModelModel
2208        """
2209        if self.kernel_module is None:
2210            return
2211        self.calculateQGridForModelExt()
2212
2213    def calculateDataFailed(self, reason):
2214        """
2215        Thread returned error
2216        """
2217        print("Calculate Data failed with ", reason)
2218
2219    def completed1D(self, return_data):
2220        self.Calc1DFinishedSignal.emit(return_data)
2221
2222    def completed2D(self, return_data):
2223        self.Calc2DFinishedSignal.emit(return_data)
2224
2225    def complete1D(self, return_data):
2226        """
2227        Plot the current 1D data
2228        """
2229        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2230        self.calculateResiduals(fitted_data)
2231        self.model_data = fitted_data
2232
2233        # Create plots for intermediate product data
2234        pq_data, sq_data = self.logic.new1DProductPlots(return_data, self.tab_id)
2235        if pq_data is not None:
2236            pq_data.symbol = "Line"
2237            self.createNewIndex(pq_data)
2238        if sq_data is not None:
2239            sq_data.symbol = "Line"
2240            self.createNewIndex(sq_data)
2241
2242    def complete2D(self, return_data):
2243        """
2244        Plot the current 2D data
2245        """
2246        fitted_data = self.logic.new2DPlot(return_data)
2247        self.calculateResiduals(fitted_data)
2248        self.model_data = fitted_data
2249
2250    def calculateResiduals(self, fitted_data):
2251        """
2252        Calculate and print Chi2 and display chart of residuals
2253        """
2254        # Create a new index for holding data
2255        fitted_data.symbol = "Line"
2256
2257        # Modify fitted_data with weighting
2258        self.addWeightingToData(fitted_data)
2259
2260        self.createNewIndex(fitted_data)
2261        # Calculate difference between return_data and logic.data
2262        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
2263        # Update the control
2264        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2265        self.lblChi2Value.setText(chi2_repr)
2266
2267        self.communicate.plotUpdateSignal.emit([fitted_data])
2268
2269        # Plot residuals if actual data
2270        if not self.data_is_loaded:
2271            return
2272
2273        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
2274        residuals_plot.id = "Residual " + residuals_plot.id
2275        self.createNewIndex(residuals_plot)
2276
2277    def onCategoriesChanged(self):
2278            """
2279            Reload the category/model comboboxes
2280            """
2281            # Store the current combo indices
2282            current_cat = self.cbCategory.currentText()
2283            current_model = self.cbModel.currentText()
2284
2285            # reread the category file and repopulate the combo
2286            self.cbCategory.blockSignals(True)
2287            self.cbCategory.clear()
2288            self.readCategoryInfo()
2289            self.initializeCategoryCombo()
2290
2291            # Scroll back to the original index in Categories
2292            new_index = self.cbCategory.findText(current_cat)
2293            if new_index != -1:
2294                self.cbCategory.setCurrentIndex(new_index)
2295            self.cbCategory.blockSignals(False)
2296            # ...and in the Models
2297            self.cbModel.blockSignals(True)
2298            new_index = self.cbModel.findText(current_model)
2299            if new_index != -1:
2300                self.cbModel.setCurrentIndex(new_index)
2301            self.cbModel.blockSignals(False)
2302
2303            return
2304
2305    def calcException(self, etype, value, tb):
2306        """
2307        Thread threw an exception.
2308        """
2309        # TODO: remimplement thread cancellation
2310        logging.error("".join(traceback.format_exception(etype, value, tb)))
2311
2312    def setTableProperties(self, table):
2313        """
2314        Setting table properties
2315        """
2316        # Table properties
2317        table.verticalHeader().setVisible(False)
2318        table.setAlternatingRowColors(True)
2319        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2320        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2321        table.resizeColumnsToContents()
2322
2323        # Header
2324        header = table.horizontalHeader()
2325        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2326        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2327
2328        # Qt5: the following 2 lines crash - figure out why!
2329        # Resize column 0 and 7 to content
2330        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2331        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2332
2333    def setPolyModel(self):
2334        """
2335        Set polydispersity values
2336        """
2337        if not self.model_parameters:
2338            return
2339        self._poly_model.clear()
2340
2341        [self.setPolyModelParameters(i, param) for i, param in \
2342            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
2343        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2344
2345    def setPolyModelParameters(self, i, param):
2346        """
2347        Standard of multishell poly parameter driver
2348        """
2349        param_name = param.name
2350        # see it the parameter is multishell
2351        if '[' in param.name:
2352            # Skip empty shells
2353            if self.current_shell_displayed == 0:
2354                return
2355            else:
2356                # Create as many entries as current shells
2357                for ishell in range(1, self.current_shell_displayed+1):
2358                    # Remove [n] and add the shell numeral
2359                    name = param_name[0:param_name.index('[')] + str(ishell)
2360                    self.addNameToPolyModel(i, name)
2361        else:
2362            # Just create a simple param entry
2363            self.addNameToPolyModel(i, param_name)
2364
2365    def addNameToPolyModel(self, i, param_name):
2366        """
2367        Creates a checked row in the poly model with param_name
2368        """
2369        # Polydisp. values from the sasmodel
2370        width = self.kernel_module.getParam(param_name + '.width')
2371        npts = self.kernel_module.getParam(param_name + '.npts')
2372        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2373        _, min, max = self.kernel_module.details[param_name]
2374
2375        # Construct a row with polydisp. related variable.
2376        # This will get added to the polydisp. model
2377        # Note: last argument needs extra space padding for decent display of the control
2378        checked_list = ["Distribution of " + param_name, str(width),
2379                        str(min), str(max),
2380                        str(npts), str(nsigs), "gaussian      ",'']
2381        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2382
2383        # All possible polydisp. functions as strings in combobox
2384        func = QtWidgets.QComboBox()
2385        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2386        # Set the default index
2387        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2388        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2389        self.lstPoly.setIndexWidget(ind, func)
2390        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2391
2392    def onPolyFilenameChange(self, row_index):
2393        """
2394        Respond to filename_updated signal from the delegate
2395        """
2396        # For the given row, invoke the "array" combo handler
2397        array_caption = 'array'
2398
2399        # Get the combo box reference
2400        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2401        widget = self.lstPoly.indexWidget(ind)
2402
2403        # Update the combo box so it displays "array"
2404        widget.blockSignals(True)
2405        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2406        widget.blockSignals(False)
2407
2408        # Invoke the file reader
2409        self.onPolyComboIndexChange(array_caption, row_index)
2410
2411    def onPolyComboIndexChange(self, combo_string, row_index):
2412        """
2413        Modify polydisp. defaults on function choice
2414        """
2415        # Get npts/nsigs for current selection
2416        param = self.model_parameters.form_volume_parameters[row_index]
2417        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2418        combo_box = self.lstPoly.indexWidget(file_index)
2419
2420        def updateFunctionCaption(row):
2421            # Utility function for update of polydispersity function name in the main model
2422            param_name = str(self._model_model.item(row, 0).text())
2423            if param_name !=  param.name:
2424                return
2425            # Modify the param value
2426            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2427
2428        if combo_string == 'array':
2429            try:
2430                self.loadPolydispArray(row_index)
2431                # Update main model for display
2432                self.iterateOverModel(updateFunctionCaption)
2433                # disable the row
2434                lo = self.lstPoly.itemDelegate().poly_pd
2435                hi = self.lstPoly.itemDelegate().poly_function
2436                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2437                return
2438            except IOError:
2439                combo_box.setCurrentIndex(self.orig_poly_index)
2440                # Pass for cancel/bad read
2441                pass
2442
2443        # Enable the row in case it was disabled by Array
2444        self._poly_model.blockSignals(True)
2445        max_range = self.lstPoly.itemDelegate().poly_filename
2446        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2447        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2448        self._poly_model.setData(file_index, "")
2449        self._poly_model.blockSignals(False)
2450
2451        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2452        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2453
2454        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2455        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2456
2457        self._poly_model.setData(npts_index, npts)
2458        self._poly_model.setData(nsigs_index, nsigs)
2459
2460        self.iterateOverModel(updateFunctionCaption)
2461        self.orig_poly_index = combo_box.currentIndex()
2462
2463    def loadPolydispArray(self, row_index):
2464        """
2465        Show the load file dialog and loads requested data into state
2466        """
2467        datafile = QtWidgets.QFileDialog.getOpenFileName(
2468            self, "Choose a weight file", "", "All files (*.*)", None,
2469            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2470
2471        if not datafile:
2472            logging.info("No weight data chosen.")
2473            raise IOError
2474
2475        values = []
2476        weights = []
2477        def appendData(data_tuple):
2478            """
2479            Fish out floats from a tuple of strings
2480            """
2481            try:
2482                values.append(float(data_tuple[0]))
2483                weights.append(float(data_tuple[1]))
2484            except (ValueError, IndexError):
2485                # just pass through if line with bad data
2486                return
2487
2488        with open(datafile, 'r') as column_file:
2489            column_data = [line.rstrip().split() for line in column_file.readlines()]
2490            [appendData(line) for line in column_data]
2491
2492        # If everything went well - update the sasmodel values
2493        self.disp_model = POLYDISPERSITY_MODELS['array']()
2494        self.disp_model.set_weights(np.array(values), np.array(weights))
2495        # + update the cell with filename
2496        fname = os.path.basename(str(datafile))
2497        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2498        self._poly_model.setData(fname_index, fname)
2499
2500    def setMagneticModel(self):
2501        """
2502        Set magnetism values on model
2503        """
2504        if not self.model_parameters:
2505            return
2506        self._magnet_model.clear()
2507        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2508            self.model_parameters.call_parameters if param.type == 'magnetic']
2509        FittingUtilities.addHeadersToModel(self._magnet_model)
2510
2511    def shellNamesList(self):
2512        """
2513        Returns list of names of all multi-shell parameters
2514        E.g. for sld[n], radius[n], n=1..3 it will return
2515        [sld1, sld2, sld3, radius1, radius2, radius3]
2516        """
2517        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2518        top_index = self.kernel_module.multiplicity_info.number
2519        shell_names = []
2520        for i in range(1, top_index+1):
2521            for name in multi_names:
2522                shell_names.append(name+str(i))
2523        return shell_names
2524
2525    def addCheckedMagneticListToModel(self, param, model):
2526        """
2527        Wrapper for model update with a subset of magnetic parameters
2528        """
2529        if param.name[param.name.index(':')+1:] in self.shell_names:
2530            # check if two-digit shell number
2531            try:
2532                shell_index = int(param.name[-2:])
2533            except ValueError:
2534                shell_index = int(param.name[-1:])
2535
2536            if shell_index > self.current_shell_displayed:
2537                return
2538
2539        checked_list = [param.name,
2540                        str(param.default),
2541                        str(param.limits[0]),
2542                        str(param.limits[1]),
2543                        param.units]
2544
2545        FittingUtilities.addCheckedListToModel(model, checked_list)
2546
2547    def enableStructureFactorControl(self, structure_factor):
2548        """
2549        Add structure factors to the list of parameters
2550        """
2551        if self.kernel_module.is_form_factor or structure_factor == 'None':
2552            self.enableStructureCombo()
2553        else:
2554            self.disableStructureCombo()
2555
2556    def addExtraShells(self):
2557        """
2558        Add a combobox for multiple shell display
2559        """
2560        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2561
2562        if param_length == 0:
2563            return
2564
2565        # cell 1: variable name
2566        item1 = QtGui.QStandardItem(param_name)
2567
2568        func = QtWidgets.QComboBox()
2569        # Available range of shells displayed in the combobox
2570        func.addItems([str(i) for i in range(param_length+1)])
2571
2572        # Respond to index change
2573        func.currentIndexChanged.connect(self.modifyShellsInList)
2574
2575        # cell 2: combobox
2576        item2 = QtGui.QStandardItem()
2577        self._model_model.appendRow([item1, item2])
2578
2579        # Beautify the row:  span columns 2-4
2580        shell_row = self._model_model.rowCount()
2581        shell_index = self._model_model.index(shell_row-1, 1)
2582
2583        self.lstParams.setIndexWidget(shell_index, func)
2584        self._last_model_row = self._model_model.rowCount()
2585
2586        # Set the index to the state-kept value
2587        func.setCurrentIndex(self.current_shell_displayed
2588                             if self.current_shell_displayed < func.count() else 0)
2589
2590    def modifyShellsInList(self, index):
2591        """
2592        Add/remove additional multishell parameters
2593        """
2594        # Find row location of the combobox
2595        last_row = self._last_model_row
2596        remove_rows = self._model_model.rowCount() - last_row
2597
2598        if remove_rows > 1:
2599            self._model_model.removeRows(last_row, remove_rows)
2600
2601        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
2602        self.current_shell_displayed = index
2603
2604        # Update relevant models
2605        self.setPolyModel()
2606        self.setMagneticModel()
2607
2608    def setFittingStarted(self):
2609        """
2610        Set buttion caption on fitting start
2611        """
2612        # Notify the user that fitting is being run
2613        # Allow for stopping the job
2614        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
2615        self.cmdFit.setText('Stop fit')
2616
2617    def setFittingStopped(self):
2618        """
2619        Set button caption on fitting stop
2620        """
2621        # Notify the user that fitting is available
2622        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
2623        self.cmdFit.setText("Fit")
2624        self.fit_started = False
2625
2626    def readFitPage(self, fp):
2627        """
2628        Read in state from a fitpage object and update GUI
2629        """
2630        assert isinstance(fp, FitPage)
2631        # Main tab info
2632        self.logic.data.filename = fp.filename
2633        self.data_is_loaded = fp.data_is_loaded
2634        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2635        self.chkMagnetism.setCheckState(fp.is_magnetic)
2636        self.chk2DView.setCheckState(fp.is2D)
2637
2638        # Update the comboboxes
2639        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2640        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2641        if fp.current_factor:
2642            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2643
2644        self.chi2 = fp.chi2
2645
2646        # Options tab
2647        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2648        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2649        self.npts = fp.fit_options[fp.NPTS]
2650        self.log_points = fp.fit_options[fp.LOG_POINTS]
2651        self.weighting = fp.fit_options[fp.WEIGHTING]
2652
2653        # Models
2654        self._model_model = fp.model_model
2655        self._poly_model = fp.poly_model
2656        self._magnet_model = fp.magnetism_model
2657
2658        # Resolution tab
2659        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2660        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2661        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2662        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2663        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2664
2665        # TODO: add polidyspersity and magnetism
2666
2667    def saveToFitPage(self, fp):
2668        """
2669        Write current state to the given fitpage
2670        """
2671        assert isinstance(fp, FitPage)
2672
2673        # Main tab info
2674        fp.filename = self.logic.data.filename
2675        fp.data_is_loaded = self.data_is_loaded
2676        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2677        fp.is_magnetic = self.chkMagnetism.isChecked()
2678        fp.is2D = self.chk2DView.isChecked()
2679        fp.data = self.data
2680
2681        # Use current models - they contain all the required parameters
2682        fp.model_model = self._model_model
2683        fp.poly_model = self._poly_model
2684        fp.magnetism_model = self._magnet_model
2685
2686        if self.cbCategory.currentIndex() != 0:
2687            fp.current_category = str(self.cbCategory.currentText())
2688            fp.current_model = str(self.cbModel.currentText())
2689
2690        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2691            fp.current_factor = str(self.cbStructureFactor.currentText())
2692        else:
2693            fp.current_factor = ''
2694
2695        fp.chi2 = self.chi2
2696        fp.parameters_to_fit = self.parameters_to_fit
2697        fp.kernel_module = self.kernel_module
2698
2699        # Algorithm options
2700        # fp.algorithm = self.parent.fit_options.selected_id
2701
2702        # Options tab
2703        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2704        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2705        fp.fit_options[fp.NPTS] = self.npts
2706        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2707        fp.fit_options[fp.LOG_POINTS] = self.log_points
2708        fp.fit_options[fp.WEIGHTING] = self.weighting
2709
2710        # Resolution tab
2711        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2712        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2713        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2714        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2715        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2716
2717        # TODO: add polidyspersity and magnetism
2718
2719
2720    def updateUndo(self):
2721        """
2722        Create a new state page and add it to the stack
2723        """
2724        if self.undo_supported:
2725            self.pushFitPage(self.currentState())
2726
2727    def currentState(self):
2728        """
2729        Return fit page with current state
2730        """
2731        new_page = FitPage()
2732        self.saveToFitPage(new_page)
2733
2734        return new_page
2735
2736    def pushFitPage(self, new_page):
2737        """
2738        Add a new fit page object with current state
2739        """
2740        self.page_stack.append(new_page)
2741
2742    def popFitPage(self):
2743        """
2744        Remove top fit page from stack
2745        """
2746        if self.page_stack:
2747            self.page_stack.pop()
2748
2749    def getReport(self):
2750        """
2751        Create and return HTML report with parameters and charts
2752        """
2753        index = None
2754        if self.all_data:
2755            index = self.all_data[self.data_index]
2756        report_logic = ReportPageLogic(self,
2757                                       kernel_module=self.kernel_module,
2758                                       data=self.data,
2759                                       index=index,
2760                                       model=self._model_model)
2761
2762        return report_logic.reportList()
2763
2764    def savePageState(self):
2765        """
2766        Create and serialize local PageState
2767        """
2768        from sas.sascalc.fit.pagestate import Reader
2769        model = self.kernel_module
2770
2771        # Old style PageState object
2772        state = PageState(model=model, data=self.data)
2773
2774        # Add parameter data to the state
2775        self.getCurrentFitState(state)
2776
2777        # Create the filewriter, aptly named 'Reader'
2778        state_reader = Reader(self.loadPageStateCallback)
2779        filepath = self.saveAsAnalysisFile()
2780        if filepath is None:
2781            return
2782        state_reader.write(filename=filepath, fitstate=state)
2783        pass
2784
2785    def saveAsAnalysisFile(self):
2786        """
2787        Show the save as... dialog and return the chosen filepath
2788        """
2789        default_name = "FitPage"+str(self.tab_id)+".fitv"
2790
2791        wildcard = "fitv files (*.fitv)"
2792        kwargs = {
2793            'caption'   : 'Save As',
2794            'directory' : default_name,
2795            'filter'    : wildcard,
2796            'parent'    : None,
2797        }
2798        # Query user for filename.
2799        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
2800        filename = filename_tuple[0]
2801        return filename
2802
2803    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
2804        """
2805        This is a callback method called from the CANSAS reader.
2806        We need the instance of this reader only for writing out a file,
2807        so there's nothing here.
2808        Until Load Analysis is implemented, that is.
2809        """
2810        pass
2811
2812    def loadPageState(self, pagestate=None):
2813        """
2814        Load the PageState object and update the current widget
2815        """
2816        pass
2817
2818    def getCurrentFitState(self, state=None):
2819        """
2820        Store current state for fit_page
2821        """
2822        # save model option
2823        #if self.model is not None:
2824        #    self.disp_list = self.getDispParamList()
2825        #    state.disp_list = copy.deepcopy(self.disp_list)
2826        #    #state.model = self.model.clone()
2827
2828        # Comboboxes
2829        state.categorycombobox = self.cbCategory.currentText()
2830        state.formfactorcombobox = self.cbModel.currentText()
2831        if self.cbStructureFactor.isEnabled():
2832            state.structureCombobox = self.cbStructureFactor.currentText()
2833        state.tcChi = self.chi2
2834
2835        state.enable2D = self.is2D
2836
2837        #state.weights = copy.deepcopy(self.weights)
2838        # save data
2839        state.data = copy.deepcopy(self.data)
2840
2841        # save plotting range
2842        state.qmin = self.q_range_min
2843        state.qmax = self.q_range_max
2844        state.npts = self.npts
2845
2846        #    self.state.enable_disp = self.enable_disp.GetValue()
2847        #    self.state.disable_disp = self.disable_disp.GetValue()
2848
2849        #    self.state.enable_smearer = \
2850        #                        copy.deepcopy(self.enable_smearer.GetValue())
2851        #    self.state.disable_smearer = \
2852        #                        copy.deepcopy(self.disable_smearer.GetValue())
2853
2854        #self.state.pinhole_smearer = \
2855        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
2856        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
2857        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
2858        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
2859        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
2860        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
2861
2862        p = self.model_parameters
2863        # save checkbutton state and txtcrtl values
2864        state.parameters = FittingUtilities.getStandardParam()
2865        state.orientation_params_disp = FittingUtilities.getOrientationParam()
2866
2867        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
2868        #self._copy_parameters_state(self.parameters, self.state.parameters)
2869        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
2870        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
2871
2872    def onParameterCopy(self, format=None):
2873        """
2874        Copy current parameters into the clipboard
2875        """
2876        # run a loop over all parameters and pull out
2877        # first - regular params
2878        param_list = []
2879        def gatherParams(row):
2880            """
2881            Create list of main parameters based on _model_model
2882            """
2883            param_name = str(self._model_model.item(row, 0).text())
2884            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2885            param_value = str(self._model_model.item(row, 1).text())
2886            param_error = None
2887            column_offset = 0
2888            if self.has_error_column:
2889                param_error = str(self._model_model.item(row, 2).text())
2890                column_offset = 1
2891            param_min = str(self._model_model.item(row, 2+column_offset).text())
2892            param_max = str(self._model_model.item(row, 3+column_offset).text())
2893            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
2894
2895        def gatherPolyParams(row):
2896            """
2897            Create list of polydisperse parameters based on _poly_model
2898            """
2899            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
2900            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2901            param_value = str(self._poly_model.item(row, 1).text())
2902            param_error = None
2903            column_offset = 0
2904            if self.has_poly_error_column:
2905                param_error = str(self._poly_model.item(row, 2).text())
2906                column_offset = 1
2907            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
2908            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
2909            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
2910            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
2911            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
2912            # width
2913            name = param_name+".width"
2914            param_list.append([name, param_checked, param_value, param_error,
2915                                param_npts, param_nsigs, param_min, param_max, param_fun])
2916
2917        def gatherMagnetParams(row):
2918            """
2919            Create list of magnetic parameters based on _magnet_model
2920            """
2921            param_name = str(self._magnet_model.item(row, 0).text())
2922            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2923            param_value = str(self._magnet_model.item(row, 1).text())
2924            param_error = None
2925            column_offset = 0
2926            if self.has_magnet_error_column:
2927                param_error = str(self._magnet_model.item(row, 2).text())
2928                column_offset = 1
2929            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
2930            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
2931            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
2932
2933        self.iterateOverModel(gatherParams)
2934        if self.chkPolydispersity.isChecked():
2935            self.iterateOverPolyModel(gatherPolyParams)
2936        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
2937            self.iterateOverMagnetModel(gatherMagnetParams)
2938
2939        if format=="":
2940            formatted_output = FittingUtilities.formatParameters(param_list)
2941        elif format == "Excel":
2942            formatted_output = FittingUtilities.formatParametersExcel(param_list)
2943        elif format == "Latex":
2944            formatted_output = FittingUtilities.formatParametersLatex(param_list)
2945        else:
2946            raise AttributeError("Bad format specifier.")
2947
2948        # Dump formatted_output to the clipboard
2949        cb = QtWidgets.QApplication.clipboard()
2950        cb.setText(formatted_output)
2951
2952    def onParameterPaste(self):
2953        """
2954        Use the clipboard to update fit state
2955        """
2956        # Check if the clipboard contains right stuff
2957        cb = QtWidgets.QApplication.clipboard()
2958        cb_text = cb.text()
2959
2960        context = {}
2961        # put the text into dictionary
2962        lines = cb_text.split(':')
2963        if lines[0] != 'sasview_parameter_values':
2964            return False
2965        for line in lines[1:-1]:
2966            if len(line) != 0:
2967                item = line.split(',')
2968                check = item[1]
2969                name = item[0]
2970                value = item[2]
2971                # Transfer the text to content[dictionary]
2972                context[name] = [check, value]
2973
2974                # limits
2975                limit_lo = item[3]
2976                context[name].append(limit_lo)
2977                limit_hi = item[4]
2978                context[name].append(limit_hi)
2979
2980                # Polydisp
2981                if len(item) > 5:
2982                    value = item[5]
2983                    context[name].append(value)
2984                    try:
2985                        value = item[6]
2986                        context[name].append(value)
2987                        value = item[7]
2988                        context[name].append(value)
2989                    except IndexError:
2990                        pass
2991
2992        self.updateFullModel(context)
2993        self.updateFullPolyModel(context)
2994
2995    def updateFullModel(self, param_dict):
2996        """
2997        Update the model with new parameters
2998        """
2999        assert isinstance(param_dict, dict)
3000        if not dict:
3001            return
3002
3003        def updateFittedValues(row):
3004            # Utility function for main model update
3005            # internal so can use closure for param_dict
3006            param_name = str(self._model_model.item(row, 0).text())
3007            if param_name not in list(param_dict.keys()):
3008                return
3009            # checkbox state
3010            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3011            self._model_model.item(row, 0).setCheckState(param_checked)
3012
3013            # modify the param value
3014            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3015            self._model_model.item(row, 1).setText(param_repr)
3016
3017            # Potentially the error column
3018            ioffset = 0
3019            if len(param_dict[param_name])>4 and self.has_error_column:
3020                # error values are not editable - no need to update
3021                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3022                #self._model_model.item(row, 2).setText(error_repr)
3023                ioffset = 1
3024            # min/max
3025            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3026            self._model_model.item(row, 2+ioffset).setText(param_repr)
3027            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3028            self._model_model.item(row, 3+ioffset).setText(param_repr)
3029
3030        # block signals temporarily, so we don't end up
3031        # updating charts with every single model change on the end of fitting
3032        self._model_model.blockSignals(True)
3033        self.iterateOverModel(updateFittedValues)
3034        self._model_model.blockSignals(False)
3035
3036    def updateFullPolyModel(self, param_dict):
3037        """
3038        Update the polydispersity model with new parameters, create the errors column
3039        """
3040        assert isinstance(param_dict, dict)
3041        if not dict:
3042            return
3043
3044        def updateFittedValues(row):
3045            # Utility function for main model update
3046            # internal so can use closure for param_dict
3047            if row >= self._poly_model.rowCount():
3048                return
3049            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3050            if param_name not in list(param_dict.keys()):
3051                return
3052            # checkbox state
3053            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3054            self._poly_model.item(row,0).setCheckState(param_checked)
3055
3056            # modify the param value
3057            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3058            self._poly_model.item(row, 1).setText(param_repr)
3059
3060            # Potentially the error column
3061            ioffset = 0
3062            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3063                ioffset = 1
3064            # min
3065            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3066            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3067            # max
3068            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3069            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3070            # Npts
3071            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3072            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3073            # Nsigs
3074            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3075            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3076
3077            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3078            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3079
3080        # block signals temporarily, so we don't end up
3081        # updating charts with every single model change on the end of fitting
3082        self._poly_model.blockSignals(True)
3083        self.iterateOverPolyModel(updateFittedValues)
3084        self._poly_model.blockSignals(False)
3085
3086
Note: See TracBrowser for help on using the repository browser.