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

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 97df8a9 was 97df8a9, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

More padding fixes for uneven rows SASVIEW-889

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