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

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 57be490 was 57be490, checked in by Piotr Rozyczko <rozyczko@…>, 4 years ago

Merged ESS_GUI_reporting

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