source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 6052c02

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

added S(Q) tab with a preliminary (example) param. tree, incl. attempt to use comboboxes for some properties

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