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

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

C&S fitting now runs and updates fit tabs - SASVIEW-860

  • Property mode set to 100644
File size: 24.0 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(self.onFitComplete)
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        # get the elapsed time
299        elapsed = result[1]
300
301        # result list
302        results = result[0][0]
303
304        # Find out all tabs to fit
305        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
306
307        # update all involved tabs
308        for i, tab in enumerate(tabs_to_fit):
309            tab_object = ObjectLibrary.getObject(tab)
310            if tab_object is None:
311                # No such tab. removed while job was running
312                return
313            # Make sure result and target objects are the same (same model moniker)
314            if tab_object.kernel_module.name == results[i].model.name:
315                tab_object.fitComplete(([[results[i]]], elapsed))
316
317    def onBatchFitComplete(self, result):
318        """
319        Respond to the successful batch fit complete signal
320        """
321        pass
322
323    def onFitFailed(self, reason):
324        """
325        """
326        print("FIT FAILED: ", reason)
327        pass
328 
329    def isTabImportable(self, tab):
330        """
331        Determines if the tab can be imported and included in the widget
332        """
333        if not self.currentType in tab: return False
334        object = ObjectLibrary.getObject(tab)
335        if not isinstance(object, FittingWidget): return False
336        if object.data is None: return False
337        return True
338
339    def showModelContextMenu(self, position):
340        """
341        Show context specific menu in the tab table widget.
342        """
343        menu = QtWidgets.QMenu()
344        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
345        num_rows = len(rows)
346        if num_rows <= 0:
347            return
348        # Select for fitting
349        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
350        to_string = "to its current value" if num_rows==1 else "to their current values"
351
352        self.actionSelect = QtWidgets.QAction(self)
353        self.actionSelect.setObjectName("actionSelect")
354        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
355        # Unselect from fitting
356        self.actionDeselect = QtWidgets.QAction(self)
357        self.actionDeselect.setObjectName("actionDeselect")
358        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
359
360        self.actionRemoveConstraint = QtWidgets.QAction(self)
361        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
362        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
363
364        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
365        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
366        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
367
368        menu.addAction(self.actionSelect)
369        menu.addAction(self.actionDeselect)
370        menu.addSeparator()
371
372        #menu.addAction(self.actionRemoveConstraint)
373        if num_rows >= 2:
374            menu.addAction(self.actionMutualMultiConstrain)
375
376        # Define the callbacks
377        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
378        #self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
379        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
380        self.actionSelect.triggered.connect(self.selectModels)
381        self.actionDeselect.triggered.connect(self.deselectModels)
382        try:
383            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
384        except AttributeError as ex:
385            logging.error("Error generating context menu: %s" % ex)
386        return
387
388    def showConstrContextMenu(self, position):
389        """
390        Show context specific menu in the tab table widget.
391        """
392        menu = QtWidgets.QMenu()
393        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
394        num_rows = len(rows)
395        if num_rows <= 0:
396            return
397        # Select for fitting
398        param_string = "constraint " if num_rows==1 else "constraints "
399        to_string = "to its current value" if num_rows==1 else "to their current values"
400
401        self.actionSelect = QtWidgets.QAction(self)
402        self.actionSelect.setObjectName("actionSelect")
403        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
404        # Unselect from fitting
405        self.actionDeselect = QtWidgets.QAction(self)
406        self.actionDeselect.setObjectName("actionDeselect")
407        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
408
409        self.actionRemoveConstraint = QtWidgets.QAction(self)
410        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
411        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
412
413        menu.addAction(self.actionSelect)
414        menu.addAction(self.actionDeselect)
415        menu.addSeparator()
416        menu.addAction(self.actionRemoveConstraint)
417
418        # Define the callbacks
419        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
420        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
421        #self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
422        self.actionSelect.triggered.connect(self.selectConstraints)
423        self.actionDeselect.triggered.connect(self.deselectConstraints)
424        try:
425            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
426        except AttributeError as ex:
427            logging.error("Error generating context menu: %s" % ex)
428        return
429
430    def selectConstraints(self):
431        """
432        Selected constraints are chosen for fitting
433        """
434        status = QtCore.Qt.Checked
435        self.setRowSelection(self.tblConstraints, status)
436
437    def deselectConstraints(self):
438        """
439        Selected constraints are removed for fitting
440        """
441        status = QtCore.Qt.Unchecked
442        self.setRowSelection(self.tblConstraints, status)
443
444    def selectModels(self):
445        """
446        Selected models are chosen for fitting
447        """
448        status = QtCore.Qt.Checked
449        self.setRowSelection(self.tblTabList, status)
450
451    def deselectModels(self):
452        """
453        Selected models are removed for fitting
454        """
455        status = QtCore.Qt.Unchecked
456        self.setRowSelection(self.tblTabList, status)
457
458    def selectedParameters(self, widget):
459        """ Returns list of selected (highlighted) parameters """
460        return [s.row() for s in widget.selectionModel().selectedRows()]
461
462    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
463        """
464        Selected models are chosen for fitting
465        """
466        # Convert to proper indices and set requested enablement
467        for row in self.selectedParameters(widget):
468            widget.item(row, 0).setCheckState(status)
469
470    def deleteConstraint(self):#, row):
471        """
472        Delete all selected constraints.
473        """
474        # Removing rows from the table we're iterating over,
475        # so prepare a list of data first
476        constraints_to_delete = []
477        for row in self.selectedParameters(self.tblConstraints):
478            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
479        for constraint in constraints_to_delete:
480            moniker = constraint[:constraint.index(':')]
481            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
482            tab = self.available_tabs[moniker]
483            tab.deleteConstraintOnParameter(param)
484        # Constraints removed - refresh the table widget
485        self.initializeFitList()
486
487    def uneditableItem(self, data=""):
488        """
489        Returns an uneditable Table Widget Item
490        """
491        item = QtWidgets.QTableWidgetItem(data)
492        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
493        return item
494
495    def updateFitLine(self, tab):
496        """
497        Update a single line of the table widget with tab info
498        """
499        fit_page = ObjectLibrary.getObject(tab)
500        model = fit_page.kernel_module
501        if model is None:
502            return
503        tab_name = tab
504        model_name = model.id
505        moniker = model.name
506        model_data = fit_page.data
507        model_filename = model_data.filename
508        self.available_tabs[moniker] = fit_page
509
510        # Update the model table widget
511        pos = self.tblTabList.rowCount()
512        self.tblTabList.insertRow(pos)
513        item = self.uneditableItem(tab_name)
514        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
515        if tab_name in self.tabs_for_fitting:
516            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
517            item.setCheckState(state)
518        else:
519            item.setCheckState(QtCore.Qt.Checked)
520            self.tabs_for_fitting[tab_name] = True
521
522        self.tblTabList.setItem(pos, 0, item)
523        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
524        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
525        # Moniker is editable, so no option change
526        item = QtWidgets.QTableWidgetItem(moniker)
527        # Disable signals so we don't get infinite call recursion
528        self.tblTabList.blockSignals(True)
529        self.tblTabList.setItem(pos, 3, item)
530        self.tblTabList.blockSignals(False)
531
532        # Check if any constraints present in tab
533        constraint_names = fit_page.getConstraintsForModel()
534        constraints = fit_page.getConstraintObjectsForModel()
535        if not constraints: 
536            return
537        self.tblConstraints.setEnabled(True)
538        for constraint, constraint_name in zip(constraints, constraint_names):
539            # Create the text for widget item
540            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
541            pos = self.tblConstraints.rowCount()
542            self.available_constraints[pos] = constraint
543
544            # Show the text in the constraint table
545            item = self.uneditableItem(label)
546            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
547            item.setCheckState(QtCore.Qt.Checked)
548            self.tblConstraints.insertRow(pos)
549            self.tblConstraints.setItem(pos, 0, item)
550
551    def initializeFitList(self):
552        """
553        Fill the list of model/data sets for fitting/constraining
554        """
555        # look at the object library to find all fit tabs
556        # Show the content of the current "model"
557        objects = ObjectLibrary.listObjects()
558
559        # Tab dict
560        # moniker -> (kernel_module, data)
561        self.available_tabs = {}
562        # Constraint dict
563        # moniker -> [constraints]
564        self.available_constraints = {}
565
566        # Reset the table widgets
567        self.tblTabList.setRowCount(0)
568        self.tblConstraints.setRowCount(0)
569
570        # Fit disabled
571        self.cmdFit.setEnabled(False)
572
573        if not objects:
574            return
575
576        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
577        for tab in tabs:
578            self.updateFitLine(tab)
579            self.updateSignalsFromTab(tab)
580            # We have at least 1 fit page, allow fitting
581            self.cmdFit.setEnabled(True)
582
583    def validateMoniker(self, new_moniker=None):
584        """
585        Check new_moniker for correctness.
586        It must be non-empty.
587        It must not be the same as other monikers.
588        """
589        if not new_moniker:
590            return False
591
592        for existing_moniker in self.available_tabs:
593            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
594                return False
595
596        return True
597
598    def getObjectByName(self, name):
599        for object_name in ObjectLibrary.listObjects():
600            object = ObjectLibrary.getObject(object_name)
601            if isinstance(object, FittingWidget):
602                try:
603                    if object.kernel_module.name == name:
604                        return object
605                except AttributeError:
606                    # Disregard atribute errors - empty fit widgets
607                    continue
608        return None
609
610    def showMultiConstraint(self):
611        """
612        Invoke the complex constraint editor
613        """
614        selected_rows = self.selectedParameters(self.tblTabList)
615        assert(len(selected_rows)==2)
616
617        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
618        # Create and display the widget for param1 and param2
619        cc_widget = ComplexConstraint(self, tabs=tab_list)
620        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
621            return
622
623        constraint = Constraint()
624        model1, param1, operator, constraint_text = cc_widget.constraint()
625
626        constraint.func = constraint_text
627        constraint.param = param1
628        # Find the right tab
629        constrained_tab = self.getObjectByName(model1)
630        if constrained_tab is None:
631            return
632
633        # Find the constrained parameter row
634        constrained_row = constrained_tab.getRowFromName(param1)
635
636        # Update the tab
637        constrained_tab.addConstraintToRow(constraint, constrained_row)
638        pass
Note: See TracBrowser for help on using the repository browser.