source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 3ae9179

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

intermediate P(Q), S(Q) values are now saved in Data Explorer and are plottable

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