Source code for sas.qtgui.Perspectives.Fitting.ConstraintWidget
import logging
import copy
import re
from twisted.internet import threads
import sas.qtgui.Utilities.GuiUtils as GuiUtils
import sas.qtgui.Utilities.LocalConfig as LocalConfig
from PyQt5 import QtGui, QtCore, QtWidgets
from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
from sas.qtgui.Perspectives.Fitting import FittingUtilities
from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
[docs]class DnDTableWidget(QtWidgets.QTableWidget):
[docs] def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.viewport().setAcceptDrops(True)
self.setDragDropOverwriteMode(False)
self.setDropIndicatorShown(True)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self._is_dragged = False
[docs] def dragEnterEvent(self, event):
"""
Called automatically on a drag in the TableWidget
"""
self._is_dragged = True
event.accept()
[docs] def dragLeaveEvent(self, event):
"""
Called automatically on a drag stop
"""
self._is_dragged = False
event.accept()
[docs] def dropEvent(self, event: QtGui.QDropEvent):
if not event.isAccepted() and event.source() == self:
drop_row = self.drop_on(event)
rows = sorted(set(item.row() for item in self.selectedItems()))
rows_to_move = [[QtWidgets.QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount())]
for row_index in rows]
for row_index in reversed(rows):
self.removeRow(row_index)
if row_index < drop_row:
drop_row -= 1
for row_index, data in enumerate(rows_to_move):
row_index += drop_row
self.insertRow(row_index)
for column_index, column_data in enumerate(data):
self.setItem(row_index, column_index, column_data)
event.accept()
for row_index in range(len(rows_to_move)):
self.item(drop_row + row_index, 0).setSelected(True)
self.item(drop_row + row_index, 1).setSelected(True)
super().dropEvent(event)
# Reset the drag flag. Must be done after the drop even got accepted!
self._is_dragged = False
[docs] def drop_on(self, event):
index = self.indexAt(event.pos())
if not index.isValid():
return self.rowCount()
return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
[docs] def is_below(self, pos, index):
rect = self.visualRect(index)
margin = 2
if pos.y() - rect.top() < margin:
return False
elif rect.bottom() - pos.y() < margin:
return True
return rect.contains(pos, True) and not \
(int(self.model().flags(index)) & QtCore.Qt.ItemIsDropEnabled) and \
pos.y() >= rect.center().y()
[docs]class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
"""
Constraints Dialog to select the desired parameter/model constraints.
"""
fitCompleteSignal = QtCore.pyqtSignal(tuple)
batchCompleteSignal = QtCore.pyqtSignal(tuple)
fitFailedSignal = QtCore.pyqtSignal(tuple)
[docs] def __init__(self, parent=None):
super(ConstraintWidget, self).__init__()
self.parent = parent
self.setupUi(self)
self.currentType = "FitPage"
# Page id for fitting
# To keep with previous SasView values, use 300 as the start offset
self.page_id = 301
self.tab_id = self.page_id
# fitpage order in the widget
self._row_order = []
# Set the table widget into layout
self.tblTabList = DnDTableWidget(self)
self.tblLayout.addWidget(self.tblTabList)
# Are we chain fitting?
self.is_chain_fitting = False
# Is the fit job running?
self.is_running = False
self.calc_fit = None
# Remember previous content of modified cell
self.current_cell = ""
# Tabs used in simultaneous fitting
# tab_name : True/False
self.tabs_for_fitting = {}
# Flag that warns ComplexConstraint widget if the constraint has been
# accepted
self.constraint_accepted = True
# Set up the widgets
self.initializeWidgets()
# Set up signals/slots
self.initializeSignals()
# Create the list of tabs
self.initializeFitList()
[docs] def initializeWidgets(self):
"""
Set up various widget states
"""
# disable special cases until properly defined
self.label.setVisible(False)
self.cbCases.setVisible(False)
labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
# tab widget - headers
self.editable_tab_columns = [labels.index('Mnemonic')]
self.tblTabList.setColumnCount(len(labels))
self.tblTabList.setHorizontalHeaderLabels(labels)
self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
# Single Fit is the default, so disable chainfit
self.chkChain.setVisible(False)
# disabled constraint
labels = ['Constraint']
self.tblConstraints.setColumnCount(len(labels))
self.tblConstraints.setHorizontalHeaderLabels(labels)
self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.tblConstraints.setEnabled(False)
header = self.tblConstraints.horizontalHeaderItem(0)
header.setToolTip("Double click a row below to edit the constraint.")
self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
[docs] def initializeSignals(self):
"""
Set up signals/slots for this widget
"""
# simple widgets
self.btnSingle.toggled.connect(self.onFitTypeChange)
self.btnBatch.toggled.connect(self.onFitTypeChange)
self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
self.cmdFit.clicked.connect(self.onFit)
self.cmdHelp.clicked.connect(self.onHelp)
self.cmdAdd.clicked.connect(self.showMultiConstraint)
self.chkChain.toggled.connect(self.onChainFit)
# QTableWidgets
self.tblTabList.cellChanged.connect(self.onTabCellEdit)
self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
self.tblConstraints.cellChanged.connect(self.onConstraintChange)
# Internal signals
self.fitCompleteSignal.connect(self.fitComplete)
self.batchCompleteSignal.connect(self.batchComplete)
self.fitFailedSignal.connect(self.fitFailed)
# External signals
self.parent.tabsModifiedSignal.connect(self.initializeFitList)
[docs] def updateSignalsFromTab(self, tab=None):
"""
Intercept update signals from fitting tabs
"""
if tab is None:
return
tab_object = ObjectLibrary.getObject(tab)
# Disconnect all local slots, if connected
if tab_object.receivers(tab_object.newModelSignal) > 0:
tab_object.newModelSignal.disconnect()
if tab_object.receivers(tab_object.constraintAddedSignal) > 0:
tab_object.constraintAddedSignal.disconnect()
# Reconnect tab signals to local slots
tab_object.constraintAddedSignal.connect(self.initializeFitList)
tab_object.newModelSignal.connect(self.initializeFitList)
[docs] def onFitTypeChange(self, checked):
"""
Respond to the fit type change
single fit/batch fit
"""
source = self.sender().objectName()
self.currentType = "BatchPage" if source == "btnBatch" else "FitPage"
self.chkChain.setVisible(source=="btnBatch")
self.initializeFitList()
[docs] def onSpecialCaseChange(self, index):
"""
Respond to the combobox change for special case constraint sets
"""
pass
[docs] def getTabsForFit(self):
"""
Returns list of tab names selected for fitting
"""
return [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
[docs] def onChainFit(self, is_checked):
"""
Respond to selecting the Chain Fit checkbox
"""
self.is_chain_fitting = is_checked
[docs] def onFit(self):
"""
Perform the constrained/simultaneous fit
"""
# Stop if we're running
if self.is_running:
self.is_running = False
#re-enable the Fit button
self.cmdFit.setStyleSheet('QPushButton {color: black;}')
self.cmdFit.setText("Fit")
# stop the fitpages
self.calc_fit.stop()
return
# Find out all tabs to fit
tabs_to_fit = self.getTabsForFit()
# Single fitter for the simultaneous run
fitter = Fit()
fitter.fitter_id = self.page_id
# prepare fitting problems for each tab
#
page_ids = []
fitter_id = 0
sim_fitter_list=[fitter]
# Prepare the fitter object
try:
for tab in tabs_to_fit:
if not self.isTabImportable(tab): continue
tab_object = ObjectLibrary.getObject(tab)
if tab_object is None:
# No such tab!
return
sim_fitter_list, fitter_id = \
tab_object.prepareFitters(fitter=sim_fitter_list[0], fit_id=fitter_id)
page_ids.append([tab_object.page_id])
except ValueError:
# No parameters selected in one of the tabs
no_params_msg = "Fitting cannot be performed.\n" +\
"Not all tabs chosen for fitting have parameters selected for fitting."
QtWidgets.QMessageBox.warning(self,
'Warning',
no_params_msg,
QtWidgets.QMessageBox.Ok)
return
# Create the fitting thread, based on the fitter
completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
if LocalConfig.USING_TWISTED:
handler = None
updater = None
else:
handler = ConsoleUpdate(parent=self.parent,
manager=self,
improvement_delta=0.1)
updater = handler.update_fit
batch_inputs = {}
batch_outputs = {}
# Notify the parent about fitting started
self.parent.fittingStartedSignal.emit(tabs_to_fit)
# new fit thread object
self.calc_fit = FitThread(handler=handler,
fn=sim_fitter_list,
batch_inputs=batch_inputs,
batch_outputs=batch_outputs,
page_id=page_ids,
updatefn=updater,
completefn=completefn,
reset_flag=self.is_chain_fitting)
if LocalConfig.USING_TWISTED:
# start the trhrhread with twisted
self.calc_fit = threads.deferToThread(self.calc_fit.compute)
self.calc_fit.addCallback(completefn)
self.calc_fit.addErrback(self.onFitFailed)
else:
# Use the old python threads + Queue
self.calc_fit.queue()
self.calc_fit.ready(2.5)
# modify the Fit button
self.cmdFit.setStyleSheet('QPushButton {color: red;}')
self.cmdFit.setText('Stop fit')
self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
self.is_running = True
[docs] def onHelp(self):
"""
Show the "Fitting" section of help
"""
tree_location = "/user/qtgui/Perspectives/Fitting/"
helpfile = "fitting_help.html#simultaneous-fit-mode"
help_location = tree_location + helpfile
# OMG, really? Crawling up the object hierarchy...
self.parent.parent.showHelp(help_location)
[docs] def onTabCellEdit(self, row, column):
"""
Respond to check/uncheck and to modify the model moniker actions
"""
# If this "Edit" is just a response from moving rows around,
# update the tab order and leave
if self.tblTabList.isDragged():
self._row_order = []
for i in range(self.tblTabList.rowCount()):
self._row_order.append(self.tblTabList.item(i,0).data(0))
return
item = self.tblTabList.item(row, column)
if column == 0:
# Update the tabs for fitting list
tab_name = item.text()
self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
# Enable fitting only when there are models to fit
self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
if column not in self.editable_tab_columns:
return
new_moniker = item.data(0)
# The new name should be validated on the fly, with QValidator
# but let's just assure it post-factum
is_good_moniker = self.validateMoniker(new_moniker)
if not is_good_moniker:
self.tblTabList.blockSignals(True)
item.setBackground(QtCore.Qt.red)
self.tblTabList.blockSignals(False)
self.cmdFit.setEnabled(False)
if new_moniker == "":
msg = "Please use a non-empty name."
else:
msg = "Please use a unique name."
self.parent.communicate.statusBarUpdateSignal.emit(msg)
item.setToolTip(msg)
return
self.tblTabList.blockSignals(True)
item.setBackground(QtCore.Qt.white)
self.tblTabList.blockSignals(False)
self.cmdFit.setEnabled(True)
item.setToolTip("")
msg = "Fitpage name changed to {}.".format(new_moniker)
self.parent.communicate.statusBarUpdateSignal.emit(msg)
if not self.current_cell:
return
# Remember the value
if self.current_cell not in self.available_tabs:
return
temp_tab = self.available_tabs[self.current_cell]
# Remove the key from the dictionaries
self.available_tabs.pop(self.current_cell, None)
# Change the model name
model = temp_tab.kernel_module
model.name = new_moniker
# Replace constraint name
temp_tab.replaceConstraintName(self.current_cell, new_moniker)
# Replace constraint name in the remaining tabs
for tab in self.available_tabs.values():
tab.replaceConstraintName(self.current_cell, new_moniker)
# Reinitialize the display
self.initializeFitList()
[docs] def onConstraintChange(self, row, column):
"""
Modify the constraint when the user edits the constraint list. If the
user changes the constrained parameter, the constraint is erased and a
new one is created.
Checking is performed on the constrained entered by the user, showing
message box warning him the constraint is not valid and cancelling
his changes by reloading the view. View is reloaded
when the user is finished for consistency.
"""
item = self.tblConstraints.item(row, column)
# extract information from the constraint object
constraint = self.available_constraints[row]
model = constraint.value_ex[:constraint.value_ex.index(".")]
param = constraint.param
function = constraint.func
tab = self.available_tabs[model]
# Extract information from the text in the table
constraint_text = item.data(0)
# Basic sanity check of the string
# First check if we have an acceptable sign
if "=" not in constraint_text:
msg = ("Incorrect operator in constraint definition. Please use = "
"sign to define constraints.")
QtWidgets.QMessageBox.critical(
self,
"Inconsistent constraint",
msg,
QtWidgets.QMessageBox.Ok)
self.initializeFitList()
return
# Then check if the parameter is correctly defined with colons
# separating model and parameter name
lhs, rhs = re.split(" *= *", item.data(0).strip(), 1)
if ":" not in lhs:
msg = ("Incorrect constrained parameter definition. Please use "
"colons to separate model and parameter on the rhs of the "
"definition, e.g. M1:scale")
QtWidgets.QMessageBox.critical(
self,
"Inconsistent constraint",
msg,
QtWidgets.QMessageBox.Ok)
self.initializeFitList()
return
# We can parse the string
new_param = lhs.split(":", 1)[1].strip()
new_model = lhs.split(":", 1)[0].strip()
# Check that the symbol is known so we dont get an unknown tab
# All the conditional statements could be grouped in one or
# alternatively we could check with expression.py, but we would still
# need to do some checks to parse the string
symbol_dict = (self.parent.parent.perspective().
getSymbolDictForConstraints())
qualified_par = f"{new_model}.{new_param}"
if qualified_par not in symbol_dict:
msg = (f"Unknown parameter {qualified_par} used in constraint."
" Please use a single known parameter in the rhs of the "
"constraint definition, e.g. M1:scale = M1.radius + 2")
QtWidgets.QMessageBox.critical(
self,
"Inconsistent constraint",
msg,
QtWidgets.QMessageBox.Ok)
self.initializeFitList()
return
new_function = rhs
new_tab = self.available_tabs[new_model]
# Make sure we are dealing with fit tabs
assert isinstance(tab, FittingWidget)
assert isinstance(new_tab, FittingWidget)
# Now check if the user has redefined the constraint and reapply it
if new_function != function or new_model != model or new_param != param:
# Apply the new constraint
constraint = Constraint(param=new_param, func=new_function,
value_ex=new_model + "." + new_param)
new_tab.addConstraintToRow(constraint=constraint,
row=tab.getRowFromName(new_param))
# If the constraint is valid and we are changing model or
# parameter, delete the old constraint
if (self.constraint_accepted and new_model != model or
new_param != param):
tab.deleteConstraintOnParameter(param)
# Reload the view
self.initializeFitList()
return
# activate/deactivate constraint if the user has changed the checkbox
# state
constraint.active = (item.checkState() == QtCore.Qt.Checked)
# Update the fitting widget whenever a constraint is
# activated/deactivated
if item.checkState() == QtCore.Qt.Checked:
font = QtGui.QFont()
font.setItalic(True)
brush = QtGui.QBrush(QtGui.QColor('blue'))
tab.modifyViewOnRow(tab.getRowFromName(new_param), font=font,
brush=brush)
else:
tab.modifyViewOnRow(tab.getRowFromName(new_param))
# reload the view so the user gets a consistent feedback on the
# constraints
self.initializeFitList()
[docs] def onTabCellEntered(self, row, column):
"""
Remember the original tab list cell data.
Needed for reverting back on bad validation
"""
if column != 3:
return
self.current_cell = self.tblTabList.item(row, column).data(0)
[docs] def onFitComplete(self, result):
"""
Send the fit complete signal to main thread
"""
# Complete signal only accepts a tuple, so send an empty tuple
# when fit throws an error.
if result is None:
result = ()
self.fitCompleteSignal.emit(result)
[docs] def fitComplete(self, result):
"""
Respond to the successful fit complete signal
"""
#re-enable the Fit button
self.cmdFit.setStyleSheet('QPushButton {color: black;}')
self.cmdFit.setText("Fit")
self.is_running = False
# Notify the parent about completed fitting
self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
# Assure the fitting succeeded
if result is None or not result:
msg = "Fitting failed."
self.parent.communicate.statusBarUpdateSignal.emit(msg)
return
# Get the results list
results = result[0][0]
if isinstance(result[0], str):
msg = ("Fitting failed with the following message: " +
result[0])
self.parent.communicate.statusBarUpdateSignal.emit(msg)
return
if not results[0].success:
if isinstance(results[0].mesg[0], str):
msg = ("Fitting failed with the following message: " +
results[0].mesg[0])
else:
msg = ("Fitting failed. Please ensure correctness of " +
"chosen constraints.")
self.parent.communicate.statusBarUpdateSignal.emit(msg)
return
# get the elapsed time
elapsed = result[1]
# Find out all tabs to fit
tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
# update all involved tabs
for i, tab in enumerate(tabs_to_fit):
tab_object = ObjectLibrary.getObject(tab)
if tab_object is None:
# No such tab. removed while job was running
return
# Make sure result and target objects are the same (same model moniker)
if tab_object.kernel_module.name == results[i].model.name:
tab_object.fitComplete(([[results[i]]], elapsed))
msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
self.parent.communicate.statusBarUpdateSignal.emit(msg)
[docs] def onBatchFitComplete(self, result):
"""
Send the fit complete signal to main thread
"""
self.batchCompleteSignal.emit(result)
[docs] def batchComplete(self, result):
"""
Respond to the successful batch fit complete signal
"""
#re-enable the Fit button
self.cmdFit.setStyleSheet('QPushButton {color: black;}')
self.cmdFit.setText("Fit")
self.is_running = False
# Notify the parent about completed fitting
self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
if result is None:
msg = "Fitting failed."
self.parent.communicate.statusBarUpdateSignal.emit(msg)
return
# get the elapsed time
elapsed = result[1]
# Get the results list
results = result[0][0]
# Warn the user if fitting has been unsuccessful
if not results[0].success:
if isinstance(results[0].mesg[0], str):
msg = ("Fitting failed with the following message: " +
results[0].mesg[0])
else:
msg = ("Fitting failed. Please ensure correctness of " +
"chosen constraints.")
self.parent.communicate.statusBarUpdateSignal.emit(msg)
return
# Show the grid panel
page_name = "ConstSimulPage"
results = copy.deepcopy(result[0])
results.append(page_name)
self.parent.communicate.sendDataToGridSignal.emit(results)
msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
self.parent.communicate.statusBarUpdateSignal.emit(msg)
[docs] def onFitFailed(self, reason):
"""
Send the fit failed signal to main thread
"""
self.fitFailedSignal.emit(reason)
[docs] def fitFailed(self, reason):
"""
Respond to fitting failure.
"""
#re-enable the Fit button
self.cmdFit.setStyleSheet('QPushButton {color: black;}')
self.cmdFit.setText("Fit")
self.is_running = False
# Notify the parent about completed fitting
self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
msg = "Fitting failed: %s s.\n" % reason
self.parent.communicate.statusBarUpdateSignal.emit(msg)
[docs] def isTabImportable(self, tab):
"""
Determines if the tab can be imported and included in the widget
"""
if not isinstance(tab, str): return False
if not self.currentType in tab: return False
object = ObjectLibrary.getObject(tab)
if not isinstance(object, FittingWidget): return False
if not object.data_is_loaded : return False
return True
[docs] def showModelContextMenu(self, position):
"""
Show context specific menu in the tab table widget.
"""
menu = QtWidgets.QMenu()
rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
num_rows = len(rows)
if num_rows <= 0:
return
# Select for fitting
param_string = "Fit Page " if num_rows==1 else "Fit Pages "
self.actionSelect = QtWidgets.QAction(self)
self.actionSelect.setObjectName("actionSelect")
self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
# Unselect from fitting
self.actionDeselect = QtWidgets.QAction(self)
self.actionDeselect.setObjectName("actionDeselect")
self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
self.actionRemoveConstraint = QtWidgets.QAction(self)
self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
self.actionMutualMultiConstrain = QtWidgets.QAction(self)
self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
menu.addAction(self.actionSelect)
menu.addAction(self.actionDeselect)
menu.addSeparator()
if num_rows >= 2:
menu.addAction(self.actionMutualMultiConstrain)
# Define the callbacks
self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
self.actionSelect.triggered.connect(self.selectModels)
self.actionDeselect.triggered.connect(self.deselectModels)
try:
menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
except AttributeError as ex:
logging.error("Error generating context menu: %s" % ex)
return
[docs] def showConstrContextMenu(self, position):
"""
Show context specific menu in the tab table widget.
"""
menu = QtWidgets.QMenu()
rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
num_rows = len(rows)
if num_rows <= 0:
return
# Select for fitting
param_string = "constraint " if num_rows==1 else "constraints "
self.actionSelect = QtWidgets.QAction(self)
self.actionSelect.setObjectName("actionSelect")
self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
# Unselect from fitting
self.actionDeselect = QtWidgets.QAction(self)
self.actionDeselect.setObjectName("actionDeselect")
self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
self.actionRemoveConstraint = QtWidgets.QAction(self)
self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
menu.addAction(self.actionSelect)
menu.addAction(self.actionDeselect)
menu.addSeparator()
menu.addAction(self.actionRemoveConstraint)
# Define the callbacks
self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
self.actionSelect.triggered.connect(self.selectConstraints)
self.actionDeselect.triggered.connect(self.deselectConstraints)
try:
menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
except AttributeError as ex:
logging.error("Error generating context menu: %s" % ex)
return
[docs] def selectConstraints(self):
"""
Selected constraints are chosen for fitting
"""
status = QtCore.Qt.Checked
self.setRowSelection(self.tblConstraints, status)
[docs] def deselectConstraints(self):
"""
Selected constraints are removed for fitting
"""
status = QtCore.Qt.Unchecked
self.setRowSelection(self.tblConstraints, status)
[docs] def selectModels(self):
"""
Selected models are chosen for fitting
"""
status = QtCore.Qt.Checked
self.setRowSelection(self.tblTabList, status)
[docs] def deselectModels(self):
"""
Selected models are removed for fitting
"""
status = QtCore.Qt.Unchecked
self.setRowSelection(self.tblTabList, status)
[docs] def selectedParameters(self, widget):
""" Returns list of selected (highlighted) parameters """
return [s.row() for s in widget.selectionModel().selectedRows()]
[docs] def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
"""
Selected models are chosen for fitting
"""
# Convert to proper indices and set requested enablement
for row in self.selectedParameters(widget):
widget.item(row, 0).setCheckState(status)
[docs] def deleteConstraint(self):#, row):
"""
Delete all selected constraints.
"""
# Removing rows from the table we're iterating over,
# so prepare a list of data first
constraints_to_delete = []
for row in self.selectedParameters(self.tblConstraints):
constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
for constraint in constraints_to_delete:
moniker = constraint[:constraint.index(':')]
param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
tab = self.available_tabs[moniker]
tab.deleteConstraintOnParameter(param)
# Constraints removed - refresh the table widget
self.initializeFitList()
[docs] def uneditableItem(self, data=""):
"""
Returns an uneditable Table Widget Item
"""
item = QtWidgets.QTableWidgetItem(data)
item.setFlags( QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled )
return item
[docs] def updateFitLine(self, tab):
"""
Update a single line of the table widget with tab info
"""
fit_page = ObjectLibrary.getObject(tab)
model = fit_page.kernel_module
if model is None:
return
tab_name = tab
model_name = model.id
moniker = model.name
model_data = fit_page.data
model_filename = model_data.filename
self.available_tabs[moniker] = fit_page
# Update the model table widget
pos = self.tblTabList.rowCount()
self.tblTabList.insertRow(pos)
item = self.uneditableItem(tab_name)
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
if tab_name in self.tabs_for_fitting:
state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
item.setCheckState(state)
else:
item.setCheckState(QtCore.Qt.Checked)
self.tabs_for_fitting[tab_name] = True
# Disable signals so we don't get infinite call recursion
self.tblTabList.blockSignals(True)
self.tblTabList.setItem(pos, 0, item)
self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
# Moniker is editable, so no option change
item = QtWidgets.QTableWidgetItem(moniker)
self.tblTabList.setItem(pos, 3, item)
self.tblTabList.blockSignals(False)
# Check if any constraints present in tab
active_constraint_names = fit_page.getComplexConstraintsForModel()
constraint_names = fit_page.getFullConstraintNameListForModel()
constraints = fit_page.getConstraintObjectsForModel()
if not constraints:
return
self.tblConstraints.setEnabled(True)
self.tblConstraints.blockSignals(True)
for constraint, constraint_name in zip(constraints, constraint_names):
# Ignore constraints that have no *func* attribute defined
if constraint.func is None:
continue
# Create the text for widget item
label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
pos = self.tblConstraints.rowCount()
self.available_constraints[pos] = constraint
# Show the text in the constraint table
item = QtWidgets.QTableWidgetItem(label)
item.setFlags(QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsUserCheckable
| QtCore.Qt.ItemIsEditable)
# Why was this set to non-interactive??
if constraint_name in active_constraint_names:
item.setCheckState(QtCore.Qt.Checked)
else:
item.setCheckState(QtCore.Qt.Unchecked)
self.tblConstraints.insertRow(pos)
self.tblConstraints.setItem(pos, 0, item)
self.tblConstraints.blockSignals(False)
[docs] def initializeFitList(self):
"""
Fill the list of model/data sets for fitting/constraining
"""
# look at the object library to find all fit tabs
# Show the content of the current "model"
objects = ObjectLibrary.listObjects()
# Tab dict
# moniker -> (kernel_module, data)
self.available_tabs = {}
# Constraint dict
# moniker -> [constraints]
self.available_constraints = {}
# Reset the table widgets
self.tblTabList.setRowCount(0)
self.tblConstraints.setRowCount(0)
# Fit disabled
self.cmdFit.setEnabled(False)
if not objects:
return
tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
if len(tabs) < 1:
self.cmdAdd.setEnabled(False)
else:
self.cmdAdd.setEnabled(True)
if not self._row_order:
# Initialize tab order list
self._row_order = tabs
else:
tabs = self.orderedSublist(self._row_order, tabs)
self._row_order = tabs
for tab in tabs:
self.updateFitLine(tab)
self.updateSignalsFromTab(tab)
# We have at least 1 fit page, allow fitting
self.cmdFit.setEnabled(True)
[docs] def orderedSublist(self, order_list, target_list):
"""
Orders the target_list such that any elements
present in order_list show up first and in the order
from order_list.
"""
tmp_list = []
# 1. get the non-matching elements
nonmatching = list(set(target_list) - set(order_list))
# 2: start with matching tabs, in the correct order
for elem in order_list:
if elem in target_list:
tmp_list.append(elem)
# 3. add the remaning tabs in any order
ordered_list = tmp_list + nonmatching
return ordered_list
[docs] def validateMoniker(self, new_moniker=None):
"""
Check new_moniker for correctness.
It must be non-empty.
It must not be the same as other monikers.
"""
if not new_moniker:
return False
for existing_moniker in self.available_tabs:
if existing_moniker == new_moniker and existing_moniker != self.current_cell:
return False
return True
[docs] def getObjectByName(self, name):
"""
Given name of the fit, returns associated fit object
"""
for object_name in ObjectLibrary.listObjects():
object = ObjectLibrary.getObject(object_name)
if isinstance(object, FittingWidget):
try:
if object.kernel_module.name == name:
return object
except AttributeError:
# Disregard atribute errors - empty fit widgets
continue
return None
[docs] def onAcceptConstraint(self, con_tuple):
"""
Receive constraint tuple from the ComplexConstraint dialog and adds
constraint.
Checks the constraints for errors and displays a warning message
interrupting flow if any are found
"""
#"M1, M2, M3" etc
model_name, constraint = con_tuple
constrained_tab = self.getObjectByName(model_name)
if constrained_tab is None:
return
# Find the constrained parameter row
constrained_row = constrained_tab.getRowFromName(constraint.param)
# Update the tab
constrained_tab.addConstraintToRow(constraint, constrained_row)
if not self.constraint_accepted:
return
# Select this parameter for adjusting/fitting
constrained_tab.changeCheckboxStatus(constrained_row, True)
[docs] def showMultiConstraint(self):
"""
Invoke the complex constraint editor
"""
selected_rows = self.selectedParameters(self.tblTabList)
tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in range(self.tblTabList.rowCount())]
# Create and display the widget for param1 and param2
cc_widget = ComplexConstraint(self, tabs=tab_list)
cc_widget.constraintReadySignal.connect(self.onAcceptConstraint)
if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
return
[docs] def getFitPage(self):
"""
Retrieves the state of this page
"""
param_list = []
param_list.append(['is_constraint', 'True'])
param_list.append(['data_id', "cs_tab"+str(self.page_id)])
param_list.append(['current_type', self.currentType])
param_list.append(['is_chain_fitting', str(self.is_chain_fitting)])
param_list.append(['special_case', self.cbCases.currentText()])
return param_list
[docs] def getFitModel(self):
"""
Retrieves current model
"""
model_list = []
checked_models = {}
for row in range(self.tblTabList.rowCount()):
model_name = self.tblTabList.item(row,1).data(0)
active = self.tblTabList.item(row,0).checkState()# == QtCore.Qt.Checked
checked_models[model_name] = str(active)
checked_constraints = {}
for row in range(self.tblConstraints.rowCount()):
model_name = self.tblConstraints.item(row,0).data(0)
active = self.tblConstraints.item(row,0).checkState()# == QtCore.Qt.Checked
checked_constraints[model_name] = str(active)
model_list.append(['checked_models', checked_models])
model_list.append(['checked_constraints', checked_constraints])
return model_list
[docs] def createPageForParameters(self, parameters=None):
"""
Update the page with passed parameter values
"""
# checked models
if not 'checked_models' in parameters:
return
models = parameters['checked_models'][0]
for model, check_state in models.items():
for row in range(self.tblTabList.rowCount()):
model_name = self.tblTabList.item(row,1).data(0)
if model_name != model:
continue
# check/uncheck item
self.tblTabList.item(row,0).setCheckState(int(check_state))
if not 'checked_constraints' in parameters:
return
# checked constraints
models = parameters['checked_constraints'][0]
for model, check_state in models.items():
for row in range(self.tblConstraints.rowCount()):
model_name = self.tblConstraints.item(row,0).data(0)
if model_name != model:
continue
# check/uncheck item
self.tblConstraints.item(row,0).setCheckState(int(check_state))
# fit/batch radio
isBatch = parameters['current_type'][0] == 'BatchPage'
if isBatch:
self.btnBatch.toggle()
# chain
is_chain = parameters['is_chain_fitting'][0] == 'True'
if isBatch:
self.chkChain.setChecked(is_chain)
[docs] def getReport(self):
"""
Wrapper for non-existent functionality.
Tell the user to use the reporting tool
on separate fit pages.
"""
msg = "Please use Report Results directly on fit pages"
msg += " involved in the Constrained and Simultaneous fitting process."
msgbox = QtWidgets.QMessageBox(self)
msgbox.setIcon(QtWidgets.QMessageBox.Warning)
msgbox.setText(msg)
msgbox.setWindowTitle("Fit Report")
_ = msgbox.exec_()
return
[docs] def uncheckConstraint(self, name):
"""
Unchecks the constraint in tblConstraint with *name* slave
parameter and deactivates the constraint.
"""
for row in range(self.tblConstraints.rowCount()):
constraint = self.tblConstraints.item(row, 0).data(0)
# slave parameter has model name and parameter separated
# by colon e.g `M1:scale` so no need to parse the constraint
# string.
if name in constraint:
self.tblConstraints.item(row, 0).setCheckState(0)
# deactivate the constraint
tab = self.parent.getTabByName(name[:name.index(":")])
row = tab.getRowFromName(name[name.index(":") + 1:])
tab.getConstraintForRow(row).active = False