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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 235d766 was 235d766, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Simple vs. complex constraints behaviour fixed.

  • Property mode set to 100644
File size: 24.7 KB
Line 
1import os
2import sys
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 ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
22    """
23    Constraints Dialog to select the desired parameter/model constraints.
24    """
25
26    def __init__(self, parent=None):
27        super(ConstraintWidget, self).__init__()
28        self.parent = parent
29        self.setupUi(self)
30        self.currentType = "FitPage"
31        # Page id for fitting
32        # To keep with previous SasView values, use 300 as the start offset
33        self.page_id = 301
34
35        # Remember previous content of modified cell
36        self.current_cell = ""
37
38        # Tabs used in simultaneous fitting
39        # tab_name : True/False
40        self.tabs_for_fitting = {}
41
42        # Set up the widgets
43        self.initializeWidgets()
44
45        # Set up signals/slots
46        self.initializeSignals()
47
48        # Create the list of tabs
49        self.initializeFitList()
50
51    def acceptsData(self):
52        """ Tells the caller this widget doesn't accept data """
53        return False
54
55    def initializeWidgets(self):
56        """
57        Set up various widget states
58        """
59        labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
60        # tab widget - headers
61        self.editable_tab_columns = [labels.index('Mnemonic')]
62        self.tblTabList.setColumnCount(len(labels))
63        self.tblTabList.setHorizontalHeaderLabels(labels)
64        self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
65
66        self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
67        self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
68
69        # disabled constraint
70        labels = ['Constraint']
71        self.tblConstraints.setColumnCount(len(labels))
72        self.tblConstraints.setHorizontalHeaderLabels(labels)
73        self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
74        self.tblConstraints.setEnabled(False)
75
76        self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
77        self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
78
79    def initializeSignals(self):
80        """
81        Set up signals/slots for this widget
82        """
83        # simple widgets
84        self.btnSingle.toggled.connect(self.onFitTypeChange)
85        self.btnBatch.toggled.connect(self.onFitTypeChange)
86        self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
87        self.cmdFit.clicked.connect(self.onFit)
88        self.cmdHelp.clicked.connect(self.onHelp)
89
90        # QTableWidgets
91        self.tblTabList.cellChanged.connect(self.onTabCellEdit)
92        self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
93        self.tblConstraints.cellChanged.connect(self.onConstraintChange)
94
95        # External signals
96        self.parent.tabsModifiedSignal.connect(self.initializeFitList)
97
98    def updateSignalsFromTab(self, tab=None):
99        """
100        Intercept update signals from fitting tabs
101        """
102        if tab is not None:
103            ObjectLibrary.getObject(tab).constraintAddedSignal.connect(self.initializeFitList)
104            ObjectLibrary.getObject(tab).newModelSignal.connect(self.initializeFitList)
105
106    def onFitTypeChange(self, checked):
107        """
108        Respond to the fit type change
109        single fit/batch fit
110        """
111        source = self.sender().objectName()
112        self.currentType = "BatchPage" if source == "btnBatch" else "FitPage"
113        self.initializeFitList()
114
115    def onSpecialCaseChange(self, index):
116        """
117        Respond to the combobox change for special case constraint sets
118        """
119        pass
120
121    def getTabsForFit(self):
122        """
123        Returns list of tab names selected for fitting
124        """
125        return [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
126
127    def onFit(self):
128        """
129        Perform the constrained/simultaneous fit
130        """
131        # Find out all tabs to fit
132        tabs_to_fit = self.getTabsForFit()
133
134        # Single fitter for the simultaneous run
135        sim_fitter = Fit()
136        sim_fitter.fitter_id = self.page_id
137
138        # Notify the parent about fitting started
139        self.parent.fittingStartedSignal.emit(tabs_to_fit)
140
141        # prepare fitting problems for each tab
142        #
143        page_ids = []
144        fitter_id = 0
145        sim_fitter=[sim_fitter]
146        # Prepare the fitter object
147        try:
148            for tab in tabs_to_fit:
149                tab_object = ObjectLibrary.getObject(tab)
150                if tab_object is None:
151                    # No such tab!
152                    return
153                sim_fitter, fitter_id = tab_object.prepareFitters(fitter=sim_fitter[0], fit_id=fitter_id)
154                page_ids.append([tab_object.page_id])
155        except ValueError as ex:
156            # No parameters selected in one of the tabs
157            no_params_msg = "Fitting can not be performed.\n" +\
158                            "Not all tabs chosen for fitting have parameters selected for fitting."
159            reply = QtWidgets.QMessageBox.question(self,
160                                               'Warning',
161                                               no_params_msg,
162                                               QtWidgets.QMessageBox.Ok)
163
164            return
165
166        # Create the fitting thread, based on the fitter
167        completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
168
169        if LocalConfig.USING_TWISTED:
170            handler = None
171            updater = None
172        else:
173            handler = ConsoleUpdate(parent=self.parent,
174                                    manager=self,
175                                    improvement_delta=0.1)
176            updater = handler.update_fit
177
178        batch_inputs = {}
179        batch_outputs = {}
180
181        # new fit thread object
182        calc_fit = FitThread(handler=handler,
183                             fn=sim_fitter,
184                             batch_inputs=batch_inputs,
185                             batch_outputs=batch_outputs,
186                             page_id=page_ids,
187                             updatefn=updater,
188                             completefn=completefn)
189
190        if LocalConfig.USING_TWISTED:
191            # start the trhrhread with twisted
192            calc_thread = threads.deferToThread(calc_fit.compute)
193            calc_thread.addCallback(completefn)
194            calc_thread.addErrback(self.onFitFailed)
195        else:
196            # Use the old python threads + Queue
197            calc_fit.queue()
198            calc_fit.ready(2.5)
199
200
201        #disable the Fit button
202        self.cmdFit.setText('Running...')
203        self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
204        self.cmdFit.setEnabled(False)
205
206    def onHelp(self):
207        """
208        Show the "Fitting" section of help
209        """
210        tree_location = "/user/sasgui/perspectives/fitting/"
211
212        helpfile = "fitting_help.html#simultaneous-fit-mode"
213        help_location = tree_location + helpfile
214
215        # OMG, really? Crawling up the object hierarchy...
216        self.parent.parent.showHelp(help_location)
217
218    def onTabCellEdit(self, row, column):
219        """
220        Respond to check/uncheck and to modify the model moniker actions
221        """
222        item = self.tblTabList.item(row, column)
223        if column == 0:
224            # Update the tabs for fitting list
225            tab_name = item.text()
226            self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
227            # Enable fitting only when there are models to fit
228            self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
229
230        if column not in self.editable_tab_columns:
231            return
232        new_moniker = item.data(0)
233
234        # The new name should be validated on the fly, with QValidator
235        # but let's just assure it post-factum
236        is_good_moniker = self.validateMoniker(new_moniker)
237        if not is_good_moniker:
238            self.tblTabList.blockSignals(True)
239            item.setBackground(QtCore.Qt.red)
240            self.tblTabList.blockSignals(False)
241            self.cmdFit.setEnabled(False)
242            return
243        self.tblTabList.blockSignals(True)
244        item.setBackground(QtCore.Qt.white)
245        self.tblTabList.blockSignals(False)
246        self.cmdFit.setEnabled(True)
247        if not self.current_cell:
248            return
249        # Remember the value
250        if self.current_cell not in self.available_tabs:
251            return
252        temp_tab = self.available_tabs[self.current_cell]
253        # Remove the key from the dictionaries
254        self.available_tabs.pop(self.current_cell, None)
255        # Change the model name
256        model = temp_tab.kernel_module
257        model.name = new_moniker
258        # Replace constraint name
259        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
260        # Replace constraint name in the remaining tabs
261        for tab in self.available_tabs.values():
262            tab.replaceConstraintName(self.current_cell, new_moniker)
263        # Reinitialize the display
264        self.initializeFitList()
265
266    def onConstraintChange(self, row, column):
267        """
268        Modify the constraint's "active" instance variable.
269        """
270        item = self.tblConstraints.item(row, column)
271        if column == 0:
272            # Update the tabs for fitting list
273            constraint = self.available_constraints[row]
274            constraint.active = (item.checkState() == QtCore.Qt.Checked)
275
276    def onTabCellEntered(self, row, column):
277        """
278        Remember the original tab list cell data.
279        Needed for reverting back on bad validation
280        """
281        if column != 3:
282            return
283        self.current_cell = self.tblTabList.item(row, column).data(0)
284
285    def onFitComplete(self, result):
286        """
287        Respond to the successful fit complete signal
288        """
289        #re-enable the Fit button
290        self.cmdFit.setText("Fit")
291        self.cmdFit.setEnabled(True)
292
293        # Notify the parent about completed fitting
294        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
295
296        # Assure the fitting succeeded
297        if result is None or not result:
298            msg = "Fitting failed. Please ensure correctness of chosen constraints."
299            self.parent.communicate.statusBarUpdateSignal.emit(msg)
300            return
301
302        # get the elapsed time
303        elapsed = result[1]
304
305        # result list
306        results = result[0][0]
307
308        # Find out all tabs to fit
309        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
310
311        # update all involved tabs
312        for i, tab in enumerate(tabs_to_fit):
313            tab_object = ObjectLibrary.getObject(tab)
314            if tab_object is None:
315                # No such tab. removed while job was running
316                return
317            # Make sure result and target objects are the same (same model moniker)
318            if tab_object.kernel_module.name == results[i].model.name:
319                tab_object.fitComplete(([[results[i]]], elapsed))
320
321        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
322        self.parent.communicate.statusBarUpdateSignal.emit(msg)
323
324    def onBatchFitComplete(self, result):
325        """
326        Respond to the successful batch fit complete signal
327        """
328        #re-enable the Fit button
329        self.cmdFit.setText("Fit")
330        self.cmdFit.setEnabled(True)
331
332        # Notify the parent about completed fitting
333        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
334
335        # get the elapsed time
336        elapsed = result[1]
337
338        # ADD THE BATCH FIT VIEW HERE
339        #
340
341        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
342        self.parent.communicate.statusBarUpdateSignal.emit(msg)
343
344        pass
345
346    def onFitFailed(self, reason):
347        """
348        Respond to fitting failure.
349        """
350        #re-enable the Fit button
351        self.cmdFit.setText("Fit")
352        self.cmdFit.setEnabled(True)
353
354        # Notify the parent about completed fitting
355        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
356
357        msg = "Fitting failed: %s s.\n" % reason
358        self.parent.communicate.statusBarUpdateSignal.emit(msg)
359        pass
360 
361    def isTabImportable(self, tab):
362        """
363        Determines if the tab can be imported and included in the widget
364        """
365        if not isinstance(tab, str): return False
366        if not self.currentType in tab: return False
367        object = ObjectLibrary.getObject(tab)
368        if not isinstance(object, FittingWidget): return False
369        if object.data is None: return False
370        return True
371
372    def showModelContextMenu(self, position):
373        """
374        Show context specific menu in the tab table widget.
375        """
376        menu = QtWidgets.QMenu()
377        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
378        num_rows = len(rows)
379        if num_rows <= 0:
380            return
381        # Select for fitting
382        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
383        to_string = "to its current value" if num_rows==1 else "to their current values"
384
385        self.actionSelect = QtWidgets.QAction(self)
386        self.actionSelect.setObjectName("actionSelect")
387        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
388        # Unselect from fitting
389        self.actionDeselect = QtWidgets.QAction(self)
390        self.actionDeselect.setObjectName("actionDeselect")
391        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
392
393        self.actionRemoveConstraint = QtWidgets.QAction(self)
394        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
395        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
396
397        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
398        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
399        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
400
401        menu.addAction(self.actionSelect)
402        menu.addAction(self.actionDeselect)
403        menu.addSeparator()
404
405        if num_rows >= 2:
406            menu.addAction(self.actionMutualMultiConstrain)
407
408        # Define the callbacks
409        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
410        self.actionSelect.triggered.connect(self.selectModels)
411        self.actionDeselect.triggered.connect(self.deselectModels)
412        try:
413            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
414        except AttributeError as ex:
415            logging.error("Error generating context menu: %s" % ex)
416        return
417
418    def showConstrContextMenu(self, position):
419        """
420        Show context specific menu in the tab table widget.
421        """
422        menu = QtWidgets.QMenu()
423        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
424        num_rows = len(rows)
425        if num_rows <= 0:
426            return
427        # Select for fitting
428        param_string = "constraint " if num_rows==1 else "constraints "
429        to_string = "to its current value" if num_rows==1 else "to their current values"
430
431        self.actionSelect = QtWidgets.QAction(self)
432        self.actionSelect.setObjectName("actionSelect")
433        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
434        # Unselect from fitting
435        self.actionDeselect = QtWidgets.QAction(self)
436        self.actionDeselect.setObjectName("actionDeselect")
437        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
438
439        self.actionRemoveConstraint = QtWidgets.QAction(self)
440        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
441        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
442
443        menu.addAction(self.actionSelect)
444        menu.addAction(self.actionDeselect)
445        menu.addSeparator()
446        menu.addAction(self.actionRemoveConstraint)
447
448        # Define the callbacks
449        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
450        self.actionSelect.triggered.connect(self.selectConstraints)
451        self.actionDeselect.triggered.connect(self.deselectConstraints)
452        try:
453            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
454        except AttributeError as ex:
455            logging.error("Error generating context menu: %s" % ex)
456        return
457
458    def selectConstraints(self):
459        """
460        Selected constraints are chosen for fitting
461        """
462        status = QtCore.Qt.Checked
463        self.setRowSelection(self.tblConstraints, status)
464
465    def deselectConstraints(self):
466        """
467        Selected constraints are removed for fitting
468        """
469        status = QtCore.Qt.Unchecked
470        self.setRowSelection(self.tblConstraints, status)
471
472    def selectModels(self):
473        """
474        Selected models are chosen for fitting
475        """
476        status = QtCore.Qt.Checked
477        self.setRowSelection(self.tblTabList, status)
478
479    def deselectModels(self):
480        """
481        Selected models are removed for fitting
482        """
483        status = QtCore.Qt.Unchecked
484        self.setRowSelection(self.tblTabList, status)
485
486    def selectedParameters(self, widget):
487        """ Returns list of selected (highlighted) parameters """
488        return [s.row() for s in widget.selectionModel().selectedRows()]
489
490    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
491        """
492        Selected models are chosen for fitting
493        """
494        # Convert to proper indices and set requested enablement
495        for row in self.selectedParameters(widget):
496            widget.item(row, 0).setCheckState(status)
497
498    def deleteConstraint(self):#, row):
499        """
500        Delete all selected constraints.
501        """
502        # Removing rows from the table we're iterating over,
503        # so prepare a list of data first
504        constraints_to_delete = []
505        for row in self.selectedParameters(self.tblConstraints):
506            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
507        for constraint in constraints_to_delete:
508            moniker = constraint[:constraint.index(':')]
509            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
510            tab = self.available_tabs[moniker]
511            tab.deleteConstraintOnParameter(param)
512        # Constraints removed - refresh the table widget
513        self.initializeFitList()
514
515    def uneditableItem(self, data=""):
516        """
517        Returns an uneditable Table Widget Item
518        """
519        item = QtWidgets.QTableWidgetItem(data)
520        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
521        return item
522
523    def updateFitLine(self, tab):
524        """
525        Update a single line of the table widget with tab info
526        """
527        fit_page = ObjectLibrary.getObject(tab)
528        model = fit_page.kernel_module
529        if model is None:
530            return
531        tab_name = tab
532        model_name = model.id
533        moniker = model.name
534        model_data = fit_page.data
535        model_filename = model_data.filename
536        self.available_tabs[moniker] = fit_page
537
538        # Update the model table widget
539        pos = self.tblTabList.rowCount()
540        self.tblTabList.insertRow(pos)
541        item = self.uneditableItem(tab_name)
542        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
543        if tab_name in self.tabs_for_fitting:
544            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
545            item.setCheckState(state)
546        else:
547            item.setCheckState(QtCore.Qt.Checked)
548            self.tabs_for_fitting[tab_name] = True
549
550        self.tblTabList.setItem(pos, 0, item)
551        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
552        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
553        # Moniker is editable, so no option change
554        item = QtWidgets.QTableWidgetItem(moniker)
555        # Disable signals so we don't get infinite call recursion
556        self.tblTabList.blockSignals(True)
557        self.tblTabList.setItem(pos, 3, item)
558        self.tblTabList.blockSignals(False)
559
560        # Check if any constraints present in tab
561        constraint_names = fit_page.getComplexConstraintsForModel()
562        constraints = fit_page.getConstraintObjectsForModel()
563        if not constraints: 
564            return
565        self.tblConstraints.setEnabled(True)
566        for constraint, constraint_name in zip(constraints, constraint_names):
567            # Create the text for widget item
568            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
569            pos = self.tblConstraints.rowCount()
570            self.available_constraints[pos] = constraint
571
572            # Show the text in the constraint table
573            item = self.uneditableItem(label)
574            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
575            item.setCheckState(QtCore.Qt.Checked)
576            self.tblConstraints.insertRow(pos)
577            self.tblConstraints.setItem(pos, 0, item)
578
579    def initializeFitList(self):
580        """
581        Fill the list of model/data sets for fitting/constraining
582        """
583        # look at the object library to find all fit tabs
584        # Show the content of the current "model"
585        objects = ObjectLibrary.listObjects()
586
587        # Tab dict
588        # moniker -> (kernel_module, data)
589        self.available_tabs = {}
590        # Constraint dict
591        # moniker -> [constraints]
592        self.available_constraints = {}
593
594        # Reset the table widgets
595        self.tblTabList.setRowCount(0)
596        self.tblConstraints.setRowCount(0)
597
598        # Fit disabled
599        self.cmdFit.setEnabled(False)
600
601        if not objects:
602            return
603
604        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
605        for tab in tabs:
606            self.updateFitLine(tab)
607            self.updateSignalsFromTab(tab)
608            # We have at least 1 fit page, allow fitting
609            self.cmdFit.setEnabled(True)
610
611    def validateMoniker(self, new_moniker=None):
612        """
613        Check new_moniker for correctness.
614        It must be non-empty.
615        It must not be the same as other monikers.
616        """
617        if not new_moniker:
618            return False
619
620        for existing_moniker in self.available_tabs:
621            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
622                return False
623
624        return True
625
626    def getObjectByName(self, name):
627        for object_name in ObjectLibrary.listObjects():
628            object = ObjectLibrary.getObject(object_name)
629            if isinstance(object, FittingWidget):
630                try:
631                    if object.kernel_module.name == name:
632                        return object
633                except AttributeError:
634                    # Disregard atribute errors - empty fit widgets
635                    continue
636        return None
637
638    def showMultiConstraint(self):
639        """
640        Invoke the complex constraint editor
641        """
642        selected_rows = self.selectedParameters(self.tblTabList)
643        assert(len(selected_rows)==2)
644
645        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
646        # Create and display the widget for param1 and param2
647        cc_widget = ComplexConstraint(self, tabs=tab_list)
648        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
649            return
650
651        constraint = Constraint()
652        model1, param1, operator, constraint_text = cc_widget.constraint()
653
654        constraint.func = constraint_text
655        constraint.param = param1
656        # Find the right tab
657        constrained_tab = self.getObjectByName(model1)
658        if constrained_tab is None:
659            return
660
661        # Find the constrained parameter row
662        constrained_row = constrained_tab.getRowFromName(param1)
663
664        # Update the tab
665        constrained_tab.addConstraintToRow(constraint, constrained_row)
Note: See TracBrowser for help on using the repository browser.