source: sasview/src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py @ bbcf9f0

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_sync_sascalc
Last change on this file since bbcf9f0 was bbcf9f0, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago
  1. Disconnect signals before reconnecting them. SASVIEW-1238
  2. Update Smearing label on the main fitting tab
  • Property mode set to 100644
File size: 35.3 KB
Line 
1import logging
2import copy
3
4from twisted.internet import threads
5
6import sas.qtgui.Utilities.GuiUtils as GuiUtils
7import sas.qtgui.Utilities.LocalConfig as LocalConfig
8
9from PyQt5 import QtGui, QtCore, QtWidgets
10
11from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
12
13import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
14from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
15from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
16from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
17from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
18from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
19from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
20
21class DnDTableWidget(QtWidgets.QTableWidget):
22    def __init__(self, *args, **kwargs):
23        super().__init__(*args, **kwargs)
24
25        self.setDragEnabled(True)
26        self.setAcceptDrops(True)
27        self.viewport().setAcceptDrops(True)
28        self.setDragDropOverwriteMode(False)
29        self.setDropIndicatorShown(True)
30
31        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
32        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
33        self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
34
35        self._is_dragged = False
36
37    def isDragged(self):
38        """
39        Return the drag status
40        """
41        return self._is_dragged
42
43    def dragEnterEvent(self, event):
44        """
45        Called automatically on a drag in the TableWidget
46        """
47        self._is_dragged = True
48        event.accept()
49
50    def dragLeaveEvent(self, event):
51        """
52        Called automatically on a drag stop
53        """
54        self._is_dragged = False
55        event.accept()
56
57    def dropEvent(self, event: QtGui.QDropEvent):
58        if not event.isAccepted() and event.source() == self:
59            drop_row = self.drop_on(event)
60            rows = sorted(set(item.row() for item in self.selectedItems()))
61            rows_to_move = [[QtWidgets.QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount())]
62                            for row_index in rows]
63            for row_index in reversed(rows):
64                self.removeRow(row_index)
65                if row_index < drop_row:
66                    drop_row -= 1
67
68            for row_index, data in enumerate(rows_to_move):
69                row_index += drop_row
70                self.insertRow(row_index)
71                for column_index, column_data in enumerate(data):
72                    self.setItem(row_index, column_index, column_data)
73            event.accept()
74            for row_index in range(len(rows_to_move)):
75                self.item(drop_row + row_index, 0).setSelected(True)
76                self.item(drop_row + row_index, 1).setSelected(True)
77        super().dropEvent(event)
78        # Reset the drag flag. Must be done after the drop even got accepted!
79        self._is_dragged = False
80
81    def drop_on(self, event):
82        index = self.indexAt(event.pos())
83        if not index.isValid():
84            return self.rowCount()
85
86        return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
87
88    def is_below(self, pos, index):
89        rect = self.visualRect(index)
90        margin = 2
91        if pos.y() - rect.top() < margin:
92            return False
93        elif rect.bottom() - pos.y() < margin:
94            return True
95
96        return rect.contains(pos, True) and not \
97            (int(self.model().flags(index)) & QtCore.Qt.ItemIsDropEnabled) and \
98            pos.y() >= rect.center().y()
99
100
101class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
102    """
103    Constraints Dialog to select the desired parameter/model constraints.
104    """
105    fitCompleteSignal = QtCore.pyqtSignal(tuple)
106    batchCompleteSignal = QtCore.pyqtSignal(tuple)
107    fitFailedSignal = QtCore.pyqtSignal(tuple)
108
109    def __init__(self, parent=None):
110        super(ConstraintWidget, self).__init__()
111
112        self.parent = parent
113        self.setupUi(self)
114        self.currentType = "FitPage"
115        # Page id for fitting
116        # To keep with previous SasView values, use 300 as the start offset
117        self.page_id = 301
118        self.tab_id = self.page_id
119        # fitpage order in the widget
120        self._row_order = []
121
122        # Set the table widget into layout
123        self.tblTabList = DnDTableWidget(self)
124        self.tblLayout.addWidget(self.tblTabList)
125
126        # Are we chain fitting?
127        self.is_chain_fitting = False
128
129        # Remember previous content of modified cell
130        self.current_cell = ""
131
132        # Tabs used in simultaneous fitting
133        # tab_name : True/False
134        self.tabs_for_fitting = {}
135
136        # Set up the widgets
137        self.initializeWidgets()
138
139        # Set up signals/slots
140        self.initializeSignals()
141
142        # Create the list of tabs
143        self.initializeFitList()
144
145    def acceptsData(self):
146        """ Tells the caller this widget doesn't accept data """
147        return False
148
149    def initializeWidgets(self):
150        """
151        Set up various widget states
152        """
153        # disable special cases until properly defined
154        self.label.setVisible(False)
155        self.cbCases.setVisible(False)
156
157        labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
158        # tab widget - headers
159        self.editable_tab_columns = [labels.index('Mnemonic')]
160        self.tblTabList.setColumnCount(len(labels))
161        self.tblTabList.setHorizontalHeaderLabels(labels)
162        self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
163
164        self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
165        self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
166
167        # Single Fit is the default, so disable chainfit
168        self.chkChain.setVisible(False)
169
170        # disabled constraint
171        labels = ['Constraint']
172        self.tblConstraints.setColumnCount(len(labels))
173        self.tblConstraints.setHorizontalHeaderLabels(labels)
174        self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
175        self.tblConstraints.setEnabled(False)
176        header = self.tblConstraints.horizontalHeaderItem(0)
177        header.setToolTip("Double click a row below to edit the constraint.")
178
179        self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
180        self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
181
182    def initializeSignals(self):
183        """
184        Set up signals/slots for this widget
185        """
186        # simple widgets
187        self.btnSingle.toggled.connect(self.onFitTypeChange)
188        self.btnBatch.toggled.connect(self.onFitTypeChange)
189        self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
190        self.cmdFit.clicked.connect(self.onFit)
191        self.cmdHelp.clicked.connect(self.onHelp)
192        self.cmdAdd.clicked.connect(self.showMultiConstraint)
193        self.chkChain.toggled.connect(self.onChainFit)
194
195        # QTableWidgets
196        self.tblTabList.cellChanged.connect(self.onTabCellEdit)
197        self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
198        self.tblConstraints.cellChanged.connect(self.onConstraintChange)
199
200        # Internal signals
201        self.fitCompleteSignal.connect(self.fitComplete)
202        self.batchCompleteSignal.connect(self.batchComplete)
203        self.fitFailedSignal.connect(self.fitFailed)
204
205        # External signals
206        self.parent.tabsModifiedSignal.connect(self.initializeFitList)
207
208    def updateSignalsFromTab(self, tab=None):
209        """
210        Intercept update signals from fitting tabs
211        """
212        if tab is None:
213            return
214        tab_object = ObjectLibrary.getObject(tab)
215
216        # Disconnect all local slots, if connected
217        if tab_object.receivers(tab_object.newModelSignal) > 0:
218            tab_object.newModelSignal.disconnect()
219        if tab_object.receivers(tab_object.constraintAddedSignal) > 0:
220            tab_object.constraintAddedSignal.disconnect()
221
222        # Reconnect tab signals to local slots
223        tab_object.constraintAddedSignal.connect(self.initializeFitList)
224        tab_object.newModelSignal.connect(self.initializeFitList)
225
226    def onFitTypeChange(self, checked):
227        """
228        Respond to the fit type change
229        single fit/batch fit
230        """
231        source = self.sender().objectName()
232        self.currentType = "BatchPage" if source == "btnBatch" else "FitPage"
233        self.chkChain.setVisible(source=="btnBatch")
234        self.initializeFitList()
235
236    def onSpecialCaseChange(self, index):
237        """
238        Respond to the combobox change for special case constraint sets
239        """
240        pass
241
242    def getTabsForFit(self):
243        """
244        Returns list of tab names selected for fitting
245        """
246        return [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
247
248    def onChainFit(self, is_checked):
249        """
250        Respond to selecting the Chain Fit checkbox
251        """
252        self.is_chain_fitting = is_checked
253
254    def onFit(self):
255        """
256        Perform the constrained/simultaneous fit
257        """
258        # Find out all tabs to fit
259        tabs_to_fit = self.getTabsForFit()
260
261        # Single fitter for the simultaneous run
262        fitter = Fit()
263        fitter.fitter_id = self.page_id
264
265        # prepare fitting problems for each tab
266        #
267        page_ids = []
268        fitter_id = 0
269        sim_fitter_list=[fitter]
270        # Prepare the fitter object
271        try:
272            for tab in tabs_to_fit:
273                if not self.isTabImportable(tab): continue
274                tab_object = ObjectLibrary.getObject(tab)
275                if tab_object is None:
276                    # No such tab!
277                    return
278                sim_fitter_list, fitter_id = \
279                    tab_object.prepareFitters(fitter=sim_fitter_list[0], fit_id=fitter_id)
280                page_ids.append([tab_object.page_id])
281        except ValueError:
282            # No parameters selected in one of the tabs
283            no_params_msg = "Fitting cannot be performed.\n" +\
284                            "Not all tabs chosen for fitting have parameters selected for fitting."
285            QtWidgets.QMessageBox.warning(self,
286                                          'Warning',
287                                           no_params_msg,
288                                           QtWidgets.QMessageBox.Ok)
289
290            return
291
292        # Create the fitting thread, based on the fitter
293        completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
294
295        if LocalConfig.USING_TWISTED:
296            handler = None
297            updater = None
298        else:
299            handler = ConsoleUpdate(parent=self.parent,
300                                    manager=self,
301                                    improvement_delta=0.1)
302            updater = handler.update_fit
303
304        batch_inputs = {}
305        batch_outputs = {}
306
307        # Notify the parent about fitting started
308        self.parent.fittingStartedSignal.emit(tabs_to_fit)
309
310        # new fit thread object
311        calc_fit = FitThread(handler=handler,
312                             fn=sim_fitter_list,
313                             batch_inputs=batch_inputs,
314                             batch_outputs=batch_outputs,
315                             page_id=page_ids,
316                             updatefn=updater,
317                             completefn=completefn,
318                             reset_flag=self.is_chain_fitting)
319
320        if LocalConfig.USING_TWISTED:
321            # start the trhrhread with twisted
322            calc_thread = threads.deferToThread(calc_fit.compute)
323            calc_thread.addCallback(completefn)
324            calc_thread.addErrback(self.onFitFailed)
325        else:
326            # Use the old python threads + Queue
327            calc_fit.queue()
328            calc_fit.ready(2.5)
329
330
331        #disable the Fit button
332        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
333        self.cmdFit.setText('Running...')
334        self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
335        self.cmdFit.setEnabled(False)
336
337    def onHelp(self):
338        """
339        Show the "Fitting" section of help
340        """
341        tree_location = "/user/qtgui/Perspectives/Fitting/"
342
343        helpfile = "fitting_help.html#simultaneous-fit-mode"
344        help_location = tree_location + helpfile
345
346        # OMG, really? Crawling up the object hierarchy...
347        self.parent.parent.showHelp(help_location)
348
349    def onTabCellEdit(self, row, column):
350        """
351        Respond to check/uncheck and to modify the model moniker actions
352        """
353        # If this "Edit" is just a response from moving rows around,
354        # update the tab order and leave
355        if self.tblTabList.isDragged():
356            self._row_order = []
357            for i in range(self.tblTabList.rowCount()):
358                self._row_order.append(self.tblTabList.item(i,0).data(0))
359            return
360
361        item = self.tblTabList.item(row, column)
362        if column == 0:
363            # Update the tabs for fitting list
364            tab_name = item.text()
365            self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
366            # Enable fitting only when there are models to fit
367            self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
368
369        if column not in self.editable_tab_columns:
370            return
371        new_moniker = item.data(0)
372
373        # The new name should be validated on the fly, with QValidator
374        # but let's just assure it post-factum
375        is_good_moniker = self.validateMoniker(new_moniker)
376        if not is_good_moniker:
377            self.tblTabList.blockSignals(True)
378            item.setBackground(QtCore.Qt.red)
379            self.tblTabList.blockSignals(False)
380            self.cmdFit.setEnabled(False)
381            if new_moniker == "":
382                msg = "Please use a non-empty name."
383            else:
384                msg = "Please use a unique name."
385            self.parent.communicate.statusBarUpdateSignal.emit(msg)
386            item.setToolTip(msg)
387            return
388        self.tblTabList.blockSignals(True)
389        item.setBackground(QtCore.Qt.white)
390        self.tblTabList.blockSignals(False)
391        self.cmdFit.setEnabled(True)
392        item.setToolTip("")
393        msg = "Fitpage name changed to {}.".format(new_moniker)
394        self.parent.communicate.statusBarUpdateSignal.emit(msg)
395
396        if not self.current_cell:
397            return
398        # Remember the value
399        if self.current_cell not in self.available_tabs:
400            return
401        temp_tab = self.available_tabs[self.current_cell]
402        # Remove the key from the dictionaries
403        self.available_tabs.pop(self.current_cell, None)
404        # Change the model name
405        model = temp_tab.kernel_module
406        model.name = new_moniker
407        # Replace constraint name
408        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
409        # Replace constraint name in the remaining tabs
410        for tab in self.available_tabs.values():
411            tab.replaceConstraintName(self.current_cell, new_moniker)
412        # Reinitialize the display
413        self.initializeFitList()
414
415    def onConstraintChange(self, row, column):
416        """
417        Modify the constraint's "active" instance variable.
418        """
419        item = self.tblConstraints.item(row, column)
420        if column != 0: return
421        # Update the tabs for fitting list
422        constraint = self.available_constraints[row]
423        constraint.active = (item.checkState() == QtCore.Qt.Checked)
424        # Update the constraint formula
425        constraint = self.available_constraints[row]
426        function = item.text()
427        # remove anything left of '=' to get the constraint
428        function = function[function.index('=')+1:]
429        # No check on function here - trust the user (R)
430        if function != constraint.func:
431            # This becomes rather difficult to validate now.
432            # Turn off validation for Edit Constraint
433            constraint.func = function
434            constraint.validate = False
435
436    def onTabCellEntered(self, row, column):
437        """
438        Remember the original tab list cell data.
439        Needed for reverting back on bad validation
440        """
441        if column != 3:
442            return
443        self.current_cell = self.tblTabList.item(row, column).data(0)
444
445    def onFitComplete(self, result):
446        """
447        Send the fit complete signal to main thread
448        """
449        self.fitCompleteSignal.emit(result)
450
451    def fitComplete(self, result):
452        """
453        Respond to the successful fit complete signal
454        """
455        #re-enable the Fit button
456        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
457        self.cmdFit.setText("Fit")
458        self.cmdFit.setEnabled(True)
459
460        # Notify the parent about completed fitting
461        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
462
463        # Assure the fitting succeeded
464        if result is None or not result:
465            msg = "Fitting failed. Please ensure correctness of chosen constraints."
466            self.parent.communicate.statusBarUpdateSignal.emit(msg)
467            return
468
469        # get the elapsed time
470        elapsed = result[1]
471
472        # result list
473        results = result[0][0]
474
475        # Find out all tabs to fit
476        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
477
478        # update all involved tabs
479        for i, tab in enumerate(tabs_to_fit):
480            tab_object = ObjectLibrary.getObject(tab)
481            if tab_object is None:
482                # No such tab. removed while job was running
483                return
484            # Make sure result and target objects are the same (same model moniker)
485            if tab_object.kernel_module.name == results[i].model.name:
486                tab_object.fitComplete(([[results[i]]], elapsed))
487
488        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
489        self.parent.communicate.statusBarUpdateSignal.emit(msg)
490
491    def onBatchFitComplete(self, result):
492        """
493        Send the fit complete signal to main thread
494        """
495        self.batchCompleteSignal.emit(result)
496
497    def batchComplete(self, result):
498        """
499        Respond to the successful batch fit complete signal
500        """
501        #re-enable the Fit button
502        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
503        self.cmdFit.setText("Fit")
504        self.cmdFit.setEnabled(True)
505
506        # Notify the parent about completed fitting
507        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
508
509        # get the elapsed time
510        elapsed = result[1]
511
512        if result is None:
513            msg = "Fitting failed."
514            self.parent.communicate.statusBarUpdateSignal.emit(msg)
515            return
516
517        # Show the grid panel
518        page_name = "ConstSimulPage"
519        results = copy.deepcopy(result[0])
520        results.append(page_name)
521        self.parent.communicate.sendDataToGridSignal.emit(results)
522
523        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
524        self.parent.communicate.statusBarUpdateSignal.emit(msg)
525
526    def onFitFailed(self, reason):
527        """
528        Send the fit failed signal to main thread
529        """
530        self.fitFailedSignal.emit(result)
531
532    def fitFailed(self, reason):
533        """
534        Respond to fitting failure.
535        """
536        #re-enable the Fit button
537        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
538        self.cmdFit.setText("Fit")
539        self.cmdFit.setEnabled(True)
540
541        # Notify the parent about completed fitting
542        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
543
544        msg = "Fitting failed: %s s.\n" % reason
545        self.parent.communicate.statusBarUpdateSignal.emit(msg)
546
547    def isTabImportable(self, tab):
548        """
549        Determines if the tab can be imported and included in the widget
550        """
551        if not isinstance(tab, str): return False
552        if not self.currentType in tab: return False
553        object = ObjectLibrary.getObject(tab)
554        if not isinstance(object, FittingWidget): return False
555        if not object.data_is_loaded : return False
556        return True
557
558    def showModelContextMenu(self, position):
559        """
560        Show context specific menu in the tab table widget.
561        """
562        menu = QtWidgets.QMenu()
563        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
564        num_rows = len(rows)
565        if num_rows <= 0:
566            return
567        # Select for fitting
568        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
569
570        self.actionSelect = QtWidgets.QAction(self)
571        self.actionSelect.setObjectName("actionSelect")
572        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
573        # Unselect from fitting
574        self.actionDeselect = QtWidgets.QAction(self)
575        self.actionDeselect.setObjectName("actionDeselect")
576        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
577
578        self.actionRemoveConstraint = QtWidgets.QAction(self)
579        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
580        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
581
582        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
583        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
584        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
585
586        menu.addAction(self.actionSelect)
587        menu.addAction(self.actionDeselect)
588        menu.addSeparator()
589
590        if num_rows >= 2:
591            menu.addAction(self.actionMutualMultiConstrain)
592
593        # Define the callbacks
594        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
595        self.actionSelect.triggered.connect(self.selectModels)
596        self.actionDeselect.triggered.connect(self.deselectModels)
597        try:
598            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
599        except AttributeError as ex:
600            logging.error("Error generating context menu: %s" % ex)
601        return
602
603    def showConstrContextMenu(self, position):
604        """
605        Show context specific menu in the tab table widget.
606        """
607        menu = QtWidgets.QMenu()
608        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
609        num_rows = len(rows)
610        if num_rows <= 0:
611            return
612        # Select for fitting
613        param_string = "constraint " if num_rows==1 else "constraints "
614
615        self.actionSelect = QtWidgets.QAction(self)
616        self.actionSelect.setObjectName("actionSelect")
617        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
618        # Unselect from fitting
619        self.actionDeselect = QtWidgets.QAction(self)
620        self.actionDeselect.setObjectName("actionDeselect")
621        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
622
623        self.actionRemoveConstraint = QtWidgets.QAction(self)
624        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
625        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
626
627        menu.addAction(self.actionSelect)
628        menu.addAction(self.actionDeselect)
629        menu.addSeparator()
630        menu.addAction(self.actionRemoveConstraint)
631
632        # Define the callbacks
633        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
634        self.actionSelect.triggered.connect(self.selectConstraints)
635        self.actionDeselect.triggered.connect(self.deselectConstraints)
636        try:
637            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
638        except AttributeError as ex:
639            logging.error("Error generating context menu: %s" % ex)
640        return
641
642    def selectConstraints(self):
643        """
644        Selected constraints are chosen for fitting
645        """
646        status = QtCore.Qt.Checked
647        self.setRowSelection(self.tblConstraints, status)
648
649    def deselectConstraints(self):
650        """
651        Selected constraints are removed for fitting
652        """
653        status = QtCore.Qt.Unchecked
654        self.setRowSelection(self.tblConstraints, status)
655
656    def selectModels(self):
657        """
658        Selected models are chosen for fitting
659        """
660        status = QtCore.Qt.Checked
661        self.setRowSelection(self.tblTabList, status)
662
663    def deselectModels(self):
664        """
665        Selected models are removed for fitting
666        """
667        status = QtCore.Qt.Unchecked
668        self.setRowSelection(self.tblTabList, status)
669
670    def selectedParameters(self, widget):
671        """ Returns list of selected (highlighted) parameters """
672        return [s.row() for s in widget.selectionModel().selectedRows()]
673
674    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
675        """
676        Selected models are chosen for fitting
677        """
678        # Convert to proper indices and set requested enablement
679        for row in self.selectedParameters(widget):
680            widget.item(row, 0).setCheckState(status)
681
682    def deleteConstraint(self):#, row):
683        """
684        Delete all selected constraints.
685        """
686        # Removing rows from the table we're iterating over,
687        # so prepare a list of data first
688        constraints_to_delete = []
689        for row in self.selectedParameters(self.tblConstraints):
690            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
691        for constraint in constraints_to_delete:
692            moniker = constraint[:constraint.index(':')]
693            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
694            tab = self.available_tabs[moniker]
695            tab.deleteConstraintOnParameter(param)
696        # Constraints removed - refresh the table widget
697        self.initializeFitList()
698
699    def uneditableItem(self, data=""):
700        """
701        Returns an uneditable Table Widget Item
702        """
703        item = QtWidgets.QTableWidgetItem(data)
704        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
705        return item
706
707    def updateFitLine(self, tab):
708        """
709        Update a single line of the table widget with tab info
710        """
711        fit_page = ObjectLibrary.getObject(tab)
712        model = fit_page.kernel_module
713        if model is None:
714            return
715        tab_name = tab
716        model_name = model.id
717        moniker = model.name
718        model_data = fit_page.data
719        model_filename = model_data.filename
720        self.available_tabs[moniker] = fit_page
721
722        # Update the model table widget
723        pos = self.tblTabList.rowCount()
724        self.tblTabList.insertRow(pos)
725        item = self.uneditableItem(tab_name)
726        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
727        if tab_name in self.tabs_for_fitting:
728            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
729            item.setCheckState(state)
730        else:
731            item.setCheckState(QtCore.Qt.Checked)
732            self.tabs_for_fitting[tab_name] = True
733
734        # Disable signals so we don't get infinite call recursion
735        self.tblTabList.blockSignals(True)
736        self.tblTabList.setItem(pos, 0, item)
737        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
738        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
739        # Moniker is editable, so no option change
740        item = QtWidgets.QTableWidgetItem(moniker)
741        self.tblTabList.setItem(pos, 3, item)
742        self.tblTabList.blockSignals(False)
743
744        # Check if any constraints present in tab
745        constraint_names = fit_page.getComplexConstraintsForModel()
746        constraints = fit_page.getConstraintObjectsForModel()
747        if not constraints: 
748            return
749        self.tblConstraints.setEnabled(True)
750        self.tblConstraints.blockSignals(True)
751        for constraint, constraint_name in zip(constraints, constraint_names):
752            # Create the text for widget item
753            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
754            pos = self.tblConstraints.rowCount()
755            self.available_constraints[pos] = constraint
756
757            # Show the text in the constraint table
758            item = self.uneditableItem(label)
759            item = QtWidgets.QTableWidgetItem(label)
760            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
761            item.setCheckState(QtCore.Qt.Checked)
762            self.tblConstraints.insertRow(pos)
763            self.tblConstraints.setItem(pos, 0, item)
764        self.tblConstraints.blockSignals(False)
765
766    def initializeFitList(self):
767        """
768        Fill the list of model/data sets for fitting/constraining
769        """
770        # look at the object library to find all fit tabs
771        # Show the content of the current "model"
772        objects = ObjectLibrary.listObjects()
773
774        # Tab dict
775        # moniker -> (kernel_module, data)
776        self.available_tabs = {}
777        # Constraint dict
778        # moniker -> [constraints]
779        self.available_constraints = {}
780
781        # Reset the table widgets
782        self.tblTabList.setRowCount(0)
783        self.tblConstraints.setRowCount(0)
784
785        # Fit disabled
786        self.cmdFit.setEnabled(False)
787
788        if not objects:
789            return
790
791        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
792        if not self._row_order:
793            # Initialize tab order list
794            self._row_order = tabs
795        else:
796            tabs = self.orderedSublist(self._row_order, tabs)
797            self._row_order = tabs
798
799        for tab in tabs:
800            self.updateFitLine(tab)
801            self.updateSignalsFromTab(tab)
802            # We have at least 1 fit page, allow fitting
803            self.cmdFit.setEnabled(True)
804
805    def orderedSublist(self, order_list, target_list):
806        """
807        Orders the target_list such that any elements
808        present in order_list show up first and in the order
809        from order_list.
810        """
811        tmp_list = []
812        # 1. get the non-matching elements
813        nonmatching = list(set(target_list) - set(order_list))
814        # 2: start with matching tabs, in the correct order
815        for elem in order_list:
816            if elem in target_list:
817                tmp_list.append(elem)
818        # 3. add the remaning tabs in any order
819        ordered_list = tmp_list + nonmatching
820        return ordered_list
821
822    def validateMoniker(self, new_moniker=None):
823        """
824        Check new_moniker for correctness.
825        It must be non-empty.
826        It must not be the same as other monikers.
827        """
828        if not new_moniker:
829            return False
830
831        for existing_moniker in self.available_tabs:
832            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
833                return False
834
835        return True
836
837    def getObjectByName(self, name):
838        """
839        Given name of the fit, returns associated fit object
840        """
841        for object_name in ObjectLibrary.listObjects():
842            object = ObjectLibrary.getObject(object_name)
843            if isinstance(object, FittingWidget):
844                try:
845                    if object.kernel_module.name == name:
846                        return object
847                except AttributeError:
848                    # Disregard atribute errors - empty fit widgets
849                    continue
850        return None
851
852    def onAcceptConstraint(self, con_tuple):
853        """
854        Receive constraint tuple from the ComplexConstraint dialog and adds contraint
855        """
856        #"M1, M2, M3" etc
857        model_name, constraint = con_tuple
858        constrained_tab = self.getObjectByName(model_name)
859        if constrained_tab is None:
860            return
861
862        # Find the constrained parameter row
863        constrained_row = constrained_tab.getRowFromName(constraint.param)
864
865        # Update the tab
866        constrained_tab.addConstraintToRow(constraint, constrained_row)
867
868        # Select this parameter for adjusting/fitting
869        constrained_tab.selectCheckbox(constrained_row)
870
871
872    def showMultiConstraint(self):
873        """
874        Invoke the complex constraint editor
875        """
876        selected_rows = self.selectedParameters(self.tblTabList)
877
878        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in range(self.tblTabList.rowCount())]
879        # Create and display the widget for param1 and param2
880        cc_widget = ComplexConstraint(self, tabs=tab_list)
881        cc_widget.constraintReadySignal.connect(self.onAcceptConstraint)
882
883        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
884            return
885
886    def getFitPage(self):
887        """
888        Retrieves the state of this page
889        """
890        param_list = []
891
892        param_list.append(['is_constraint', 'True'])
893        param_list.append(['data_id', "cs_tab"+str(self.page_id)])
894        param_list.append(['current_type', self.currentType])
895        param_list.append(['is_chain_fitting', str(self.is_chain_fitting)])
896        param_list.append(['special_case', self.cbCases.currentText()])
897
898        return param_list
899
900    def getFitModel(self):
901        """
902        Retrieves current model
903        """
904        model_list = []
905
906        checked_models = {}
907        for row in range(self.tblTabList.rowCount()):
908            model_name = self.tblTabList.item(row,1).data(0)
909            active = self.tblTabList.item(row,0).checkState()# == QtCore.Qt.Checked
910            checked_models[model_name] = str(active)
911
912        checked_constraints = {}
913        for row in range(self.tblConstraints.rowCount()):
914            model_name = self.tblConstraints.item(row,0).data(0)
915            active = self.tblConstraints.item(row,0).checkState()# == QtCore.Qt.Checked
916            checked_constraints[model_name] = str(active)
917
918        model_list.append(['checked_models', checked_models])
919        model_list.append(['checked_constraints', checked_constraints])
920        return model_list
921
922    def createPageForParameters(self, parameters=None):
923        """
924        Update the page with passed parameter values
925        """
926        # checked models
927        if not 'checked_models' in parameters:
928            return
929        models = parameters['checked_models'][0]
930        for model, check_state in models.items():
931            for row in range(self.tblTabList.rowCount()):
932                model_name = self.tblTabList.item(row,1).data(0)
933                if model_name != model:
934                    continue
935                # check/uncheck item
936                self.tblTabList.item(row,0).setCheckState(int(check_state))
937
938        if not 'checked_constraints' in parameters:
939            return
940        # checked constraints
941        models = parameters['checked_constraints'][0]
942        for model, check_state in models.items():
943            for row in range(self.tblConstraints.rowCount()):
944                model_name = self.tblConstraints.item(row,0).data(0)
945                if model_name != model:
946                    continue
947                # check/uncheck item
948                self.tblConstraints.item(row,0).setCheckState(int(check_state))
949
950        # fit/batch radio
951        isBatch = parameters['current_type'][0] == 'BatchPage'
952        if isBatch:
953            self.btnBatch.toggle()
954
955        # chain
956        is_chain = parameters['is_chain_fitting'][0] == 'True'
957        if isBatch:
958            self.chkChain.setChecked(is_chain)
Note: See TracBrowser for help on using the repository browser.