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

Last change on this file since 5dba493 was 5dba493, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

rework XStream to continue to write to stdout/stderr alongside redirection; make logging level setting consistent; make log configuration more consistent

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