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

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

Towards working C&S fits - SASVIEW-846

  • Property mode set to 100644
File size: 22.4 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.initializeFitList)
96        self.parent.tabsModifiedSignal.connect(self.onModifiedTabs)
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 onFit(self):
122        """
123        Perform the constrained/simultaneous fit
124        """
125        # Find out all tabs to fit
126        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
127
128        # Single fitter for the simultaneous run
129        sim_fitter = Fit()
130        sim_fitter.fitter_id = self.page_id
131
132        # prepare fitting problems for each tab
133        #
134        page_ids = []
135        fitter_id = 0
136        sim_fitter=[sim_fitter]
137        for tab in tabs_to_fit:
138            tab_object = ObjectLibrary.getObject(tab)
139            sim_fitter, fitter_id = tab_object.prepareFitters(fitter=sim_fitter[0], fit_id=fitter_id)
140            page_ids.append([tab_object.page_id])
141
142        # Create the fitting thread, based on the fitter
143        completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
144
145        #if USING_TWISTED:
146        handler = None
147        updater = None
148        #else:
149        #    handler = ConsoleUpdate(parent=self.parent,
150        #                            manager=self,
151        #                            improvement_delta=0.1)
152        #    updater = handler.update_fit
153
154        batch_inputs = {}
155        batch_outputs = {}
156
157        # new fit thread object
158        calc_fit = FitThread(handler=handler,
159                             fn=sim_fitter,
160                             batch_inputs=batch_inputs,
161                             batch_outputs=batch_outputs,
162                             page_id=page_ids,
163                             updatefn=updater,
164                             completefn=completefn)
165
166        #if USING_TWISTED:
167        # start the trhrhread with twisted
168        calc_thread = threads.deferToThread(calc_fit.compute)
169        calc_thread.addCallback(self.onFitComplete)
170        calc_thread.addErrback(self.onFitFailed)
171        #else:
172        #    # Use the old python threads + Queue
173        #    calc_fit.queue()
174        #    calc_fit.ready(2.5)
175
176
177        #disable the Fit button
178        self.cmdFit.setText('Running...')
179        self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
180        self.cmdFit.setEnabled(False)
181
182    def onHelp(self):
183        """
184        Display the help page
185        """
186        pass
187
188    def onTabCellEdit(self, row, column):
189        """
190        Respond to check/uncheck and to modify the model moniker actions
191        """
192        item = self.tblTabList.item(row, column)
193        if column == 0:
194            # Update the tabs for fitting list
195            tab_name = item.text()
196            self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
197            # Enable fitting only when there are models to fit
198            self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
199
200        if column not in self.editable_tab_columns:
201            return
202        new_moniker = item.data(0)
203
204        # The new name should be validated on the fly, with QValidator
205        # but let's just assure it post-factum
206        is_good_moniker = self.validateMoniker(new_moniker)
207        if not is_good_moniker:
208            self.tblTabList.blockSignals(True)
209            item.setBackground(QtCore.Qt.red)
210            self.tblTabList.blockSignals(False)
211            self.cmdFit.setEnabled(False)
212            return
213        self.tblTabList.blockSignals(True)
214        item.setBackground(QtCore.Qt.white)
215        self.tblTabList.blockSignals(False)
216        self.cmdFit.setEnabled(True)
217        if not self.current_cell:
218            return
219        # Remember the value
220        if self.current_cell not in self.available_tabs:
221            return
222        temp_tab = self.available_tabs[self.current_cell]
223        # Remove the key from the dictionaries
224        self.available_tabs.pop(self.current_cell, None)
225        # Change the model name
226        model = temp_tab.kernel_module
227        model.name = new_moniker
228        # Replace constraint name
229        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
230        # Replace constraint name in the remaining tabs
231        for tab in self.available_tabs.values():
232            tab.replaceConstraintName(self.current_cell, new_moniker)
233        # Reinitialize the display
234        self.initializeFitList()
235
236    def onConstraintChange(self, row, column):
237        """
238        Modify the constraint's "active" instance variable.
239        """
240        item = self.tblConstraints.item(row, column)
241        if column == 0:
242            # Update the tabs for fitting list
243            constraint = self.available_constraints[row]
244            constraint.active = (item.checkState() == QtCore.Qt.Checked)
245
246    def onTabCellEntered(self, row, column):
247        """
248        Remember the original tab list cell data.
249        Needed for reverting back on bad validation
250        """
251        if column != 3:
252            return
253        self.current_cell = self.tblTabList.item(row, column).data(0)
254
255    def onModifiedTabs(self):
256        """
257        Respond to tabs being deleted by deleting involved constraints
258
259        This should probably be done in FittingWidget as it is the owner of
260        all the fitting data, but I want to keep the FW oblivious about
261        dependence on other FW tabs, so enforcing the constraint deletion here.
262        """
263        # Get the list of all constraints from querying the table
264        #constraints = getConstraintsForModel()
265
266        # Get the current list of tabs
267        #tabs = ObjectLibrary.listObjects()
268
269        # Check if any of the constraint dependencies got deleted
270        # Check the list of constraints
271        self.initializeFitList()
272        pass
273
274    def onFitComplete(self, result):
275        """
276        Respond to the successful fit complete signal
277        """
278        pass
279
280    def onBatchFitComplete(self, result):
281        """
282        Respond to the successful batch fit complete signal
283        """
284        pass
285
286    def onFitFailed(self, reason):
287        """
288        """
289        print("FIT FAILED: ", reason)
290        pass
291 
292    def isTabImportable(self, tab):
293        """
294        Determines if the tab can be imported and included in the widget
295        """
296        if not self.currentType in tab: return False
297        object = ObjectLibrary.getObject(tab)
298        if not isinstance(object, FittingWidget): return False
299        if object.data is None: return False
300        return True
301
302    def showModelContextMenu(self, position):
303        """
304        Show context specific menu in the tab table widget.
305        """
306        menu = QtWidgets.QMenu()
307        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
308        num_rows = len(rows)
309        if num_rows <= 0:
310            return
311        # Select for fitting
312        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
313        to_string = "to its current value" if num_rows==1 else "to their current values"
314
315        self.actionSelect = QtWidgets.QAction(self)
316        self.actionSelect.setObjectName("actionSelect")
317        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
318        # Unselect from fitting
319        self.actionDeselect = QtWidgets.QAction(self)
320        self.actionDeselect.setObjectName("actionDeselect")
321        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
322
323        self.actionRemoveConstraint = QtWidgets.QAction(self)
324        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
325        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
326
327        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
328        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
329        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
330
331        menu.addAction(self.actionSelect)
332        menu.addAction(self.actionDeselect)
333        menu.addSeparator()
334
335        #menu.addAction(self.actionRemoveConstraint)
336        if num_rows >= 2:
337            menu.addAction(self.actionMutualMultiConstrain)
338
339        # Define the callbacks
340        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
341        #self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
342        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
343        self.actionSelect.triggered.connect(self.selectModels)
344        self.actionDeselect.triggered.connect(self.deselectModels)
345        try:
346            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
347        except AttributeError as ex:
348            logging.error("Error generating context menu: %s" % ex)
349        return
350
351    def showConstrContextMenu(self, position):
352        """
353        Show context specific menu in the tab table widget.
354        """
355        menu = QtWidgets.QMenu()
356        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
357        num_rows = len(rows)
358        if num_rows <= 0:
359            return
360        # Select for fitting
361        param_string = "constraint " if num_rows==1 else "constraints "
362        to_string = "to its current value" if num_rows==1 else "to their current values"
363
364        self.actionSelect = QtWidgets.QAction(self)
365        self.actionSelect.setObjectName("actionSelect")
366        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
367        # Unselect from fitting
368        self.actionDeselect = QtWidgets.QAction(self)
369        self.actionDeselect.setObjectName("actionDeselect")
370        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
371
372        self.actionRemoveConstraint = QtWidgets.QAction(self)
373        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
374        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
375
376        menu.addAction(self.actionSelect)
377        menu.addAction(self.actionDeselect)
378        menu.addSeparator()
379        menu.addAction(self.actionRemoveConstraint)
380
381        # Define the callbacks
382        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
383        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
384        #self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
385        self.actionSelect.triggered.connect(self.selectConstraints)
386        self.actionDeselect.triggered.connect(self.deselectConstraints)
387        try:
388            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
389        except AttributeError as ex:
390            logging.error("Error generating context menu: %s" % ex)
391        return
392
393    def selectConstraints(self):
394        """
395        Selected constraints are chosen for fitting
396        """
397        status = QtCore.Qt.Checked
398        self.setRowSelection(self.tblConstraints, status)
399
400    def deselectConstraints(self):
401        """
402        Selected constraints are removed for fitting
403        """
404        status = QtCore.Qt.Unchecked
405        self.setRowSelection(self.tblConstraints, status)
406
407    def selectModels(self):
408        """
409        Selected models are chosen for fitting
410        """
411        status = QtCore.Qt.Checked
412        self.setRowSelection(self.tblTabList, status)
413
414    def deselectModels(self):
415        """
416        Selected models are removed for fitting
417        """
418        status = QtCore.Qt.Unchecked
419        self.setRowSelection(self.tblTabList, status)
420
421    def selectedParameters(self, widget):
422        """ Returns list of selected (highlighted) parameters """
423        return [s.row() for s in widget.selectionModel().selectedRows()]
424
425    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
426        """
427        Selected models are chosen for fitting
428        """
429        # Convert to proper indices and set requested enablement
430        for row in self.selectedParameters(widget):
431            widget.item(row, 0).setCheckState(status)
432
433    def deleteConstraint(self):#, row):
434        """
435        Delete all selected constraints.
436        """
437        # Removing rows from the table we're iterating over,
438        # so prepare a list of data first
439        constraints_to_delete = []
440        for row in self.selectedParameters(self.tblConstraints):
441            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
442        for constraint in constraints_to_delete:
443            moniker = constraint[:constraint.index(':')]
444            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
445            tab = self.available_tabs[moniker]
446            tab.deleteConstraintOnParameter(param)
447        # Constraints removed - refresh the table widget
448        self.initializeFitList()
449
450    def uneditableItem(self, data=""):
451        """
452        Returns an uneditable Table Widget Item
453        """
454        item = QtWidgets.QTableWidgetItem(data)
455        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
456        return item
457
458    def updateFitLine(self, tab):
459        """
460        Update a single line of the table widget with tab info
461        """
462        fit_page = ObjectLibrary.getObject(tab)
463        model = fit_page.kernel_module
464        if model is None:
465            return
466        tab_name = tab
467        model_name = model.id
468        moniker = model.name
469        model_data = fit_page.data
470        model_filename = model_data.filename
471        self.available_tabs[moniker] = fit_page
472
473        # Update the model table widget
474        pos = self.tblTabList.rowCount()
475        self.tblTabList.insertRow(pos)
476        item = self.uneditableItem(tab_name)
477        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
478        if tab_name in self.tabs_for_fitting:
479            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
480            item.setCheckState(state)
481        else:
482            item.setCheckState(QtCore.Qt.Checked)
483            self.tabs_for_fitting[tab_name] = True
484
485        self.tblTabList.setItem(pos, 0, item)
486        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
487        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
488        # Moniker is editable, so no option change
489        item = QtWidgets.QTableWidgetItem(moniker)
490        # Disable signals so we don't get infinite call recursion
491        self.tblTabList.blockSignals(True)
492        self.tblTabList.setItem(pos, 3, item)
493        self.tblTabList.blockSignals(False)
494
495        # Check if any constraints present in tab
496        constraint_names = fit_page.getConstraintsForModel()
497        constraints = fit_page.getConstraintObjectsForModel()
498        if not constraints: 
499            return
500        self.tblConstraints.setEnabled(True)
501        for constraint, constraint_name in zip(constraints, constraint_names):
502            # Create the text for widget item
503            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
504            pos = self.tblConstraints.rowCount()
505            self.available_constraints[pos] = constraint
506
507            # Show the text in the constraint table
508            item = self.uneditableItem(label)
509            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
510            item.setCheckState(QtCore.Qt.Checked)
511            self.tblConstraints.insertRow(pos)
512            self.tblConstraints.setItem(pos, 0, item)
513
514    def initializeFitList(self):
515        """
516        Fill the list of model/data sets for fitting/constraining
517        """
518        # look at the object library to find all fit tabs
519        # Show the content of the current "model"
520        objects = ObjectLibrary.listObjects()
521
522        # Tab dict
523        # moniker -> (kernel_module, data)
524        self.available_tabs = {}
525        # Constraint dict
526        # moniker -> [constraints]
527        self.available_constraints = {}
528
529        # Reset the table widgets
530        self.tblTabList.setRowCount(0)
531        self.tblConstraints.setRowCount(0)
532
533        # Fit disabled
534        self.cmdFit.setEnabled(False)
535
536        if not objects:
537            return
538
539        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
540        for tab in tabs:
541            self.updateFitLine(tab)
542            self.updateSignalsFromTab(tab)
543            # We have at least 1 fit page, allow fitting
544            self.cmdFit.setEnabled(True)
545
546    def validateMoniker(self, new_moniker=None):
547        """
548        Check new_moniker for correctness.
549        It must be non-empty.
550        It must not be the same as other monikers.
551        """
552        if not new_moniker:
553            return False
554
555        for existing_moniker in self.available_tabs:
556            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
557                return False
558
559        return True
560
561    def getObjectByName(self, name):
562        for object_name in ObjectLibrary.listObjects():
563            object = ObjectLibrary.getObject(object_name)
564            if isinstance(object, FittingWidget):
565                try:
566                    if object.kernel_module.name == name:
567                        return object
568                except AttributeError:
569                    # Disregard atribute errors - empty fit widgets
570                    continue
571        return None
572
573    def showMultiConstraint(self):
574        """
575        Invoke the complex constraint editor
576        """
577        selected_rows = self.selectedParameters(self.tblTabList)
578        assert(len(selected_rows)==2)
579
580        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
581        # Create and display the widget for param1 and param2
582        cc_widget = ComplexConstraint(self, tabs=tab_list)
583        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
584            return
585
586        constraint = Constraint()
587        model1, param1, operator, constraint_text = cc_widget.constraint()
588
589        constraint.func = constraint_text
590        constraint.param = param1
591        # Find the right tab
592        constrained_tab = self.getObjectByName(model1)
593        if constrained_tab is None:
594            return
595
596        # Find the constrained parameter row
597        constrained_row = constrained_tab.getRowFromName(param1)
598
599        # Update the tab
600        constrained_tab.addConstraintToRow(constraint, constrained_row)
601        pass
Note: See TracBrowser for help on using the repository browser.