source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 085e3c9d

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

improve S(Q) tab tree functionality

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