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

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 5e66738 was da9a0722, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

unit test updates

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