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

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

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

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