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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 87dfca4 was 87dfca4, checked in by Piotr Rozyczko <rozyczko@…>, 4 years ago

Fixed a bug where weighting options would not correctly update on data load

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