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

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

Chain fitting for constraint batch fitting

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