Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling of constraints for polydisperse parameters (fix #1588) #2348

Merged
merged 37 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2a2fabe
Initial commit. Not yet functional.
rozyczko Jul 19, 2019
c88153e
Use Qt.UserRole to store actual polydisp param name.
rozyczko Jul 19, 2019
bd200d2
More changes and fixes for mutual constraints, deleting and editing.
rozyczko Jul 31, 2019
40f827a
Show status bar info about the polydisp. constraint.
rozyczko Aug 1, 2019
b8843ef
Merge branch 'ESS_GUI' into ESS_GUI_constrain_poly
rozyczko Aug 7, 2019
7b9a446
Generalized constraint removal for certain cases, fixed complex
rozyczko Aug 8, 2019
4bc1fdd
Remove potential empty tabs on project load
rozyczko Aug 14, 2019
6f1c433
Minor stability fixes
rozyczko Sep 23, 2019
e0fd41b
Merge remote-tracking branch 'origin/main' into ESS_GUI_constrain_poly
rozyczko Feb 4, 2021
17c2ed7
Added proper filling of the first parameter list.
rozyczko Aug 20, 2021
2ffef79
don't update non-existing GUI elements
rozyczko Aug 30, 2021
754fc29
polydisperse parameters now appear as options for m2
Caddy-Jones Sep 7, 2021
f2cd591
Model encoded as a dictionary
Caddy-Jones Oct 7, 2021
1b67e06
Streamlined how model_key is determined, and fixed an issue when cons…
Caddy-Jones Oct 7, 2021
3c5e9f9
Fix TypeError when constraining two polidysperse parameters
Caddy-Jones Oct 29, 2021
aadaf70
Fixed the issue with adding multiple constraints
Caddy-Jones Oct 29, 2021
8cbe68f
Check for model is None
lucas-wilkins Jun 16, 2022
470f4d9
comment on a comment
lucas-wilkins Jun 16, 2022
24e4f51
Merge branch 'main' into ESS_GUI_constrain_poly
lucas-wilkins Jun 17, 2022
43a0c83
Merge branch 'main' into ESS_GUI_constrain_poly
gonzalezma Oct 26, 2022
0444df7
debugging
gonzalezma Oct 27, 2022
ad5b96d
Merge branch 'main' into ESS_GUI_constrain_poly
gonzalezma Oct 28, 2022
0cb37a9
Merge branch 'main' into ESS_GUI_constrain_poly
gonzalezma Oct 28, 2022
d391c2c
Pushing after 1st successful test and before starting cleaning
gonzalezma Oct 29, 2022
76eeeea
Fix failure arising when a second polydispersity constraint is added
gonzalezma Oct 29, 2022
97f1115
Make poly constraints removable, remove unnecessary name checks, and …
gonzalezma Oct 30, 2022
e419cf7
Make All button to work also for PD constraints
gonzalezma Oct 31, 2022
c6e3181
Merge branch 'main' into ESS_GUI_constrain_poly
gonzalezma Oct 31, 2022
72ae012
Try to improve handling of uncertainties when parameters are constrai…
gonzalezma Nov 7, 2022
73da363
Get back to previous version of BumpsFit, but fixing evaluation order…
gonzalezma Nov 8, 2022
2c1fc37
Fix tests for chained dependencies
Nov 8, 2022
d1c1ffb
Remove unnecessary code
gonzalezma Nov 9, 2022
5c8012d
Show all parameters, including constrained ones, in RHS of the Comple…
gonzalezma Nov 10, 2022
1d8fea2
More code simplification
gonzalezma Nov 14, 2022
6278f5b
Remove unneeded try/except block
gonzalezma Nov 15, 2022
c6732b9
Suggested code simplification
gonzalezma Nov 17, 2022
51c83de
Add warning for user if parameter error cannot be computed
gonzalezma Nov 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/sas/qtgui/Perspectives/Fitting/ComplexConstraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ def setupWidgets(self):

self.setupParamWidgets()


self.setupMenu()

def setupMenu(self):
Expand All @@ -110,10 +109,10 @@ def setupParamWidgets(self):
# Populate the left combobox parameter arbitrarily with the parameters
# from the first tab if `All` option is selected
if self.cbModel1.currentText() == "All":
items1 = self.tabs[1].main_params_to_fit
items1 = self.tabs[1].main_params_to_fit + self.tabs[1].poly_params_to_fit
else:
tab_index1 = self.cbModel1.currentIndex()
items1 = self.tabs[tab_index1].main_params_to_fit
items1 = self.tabs[tab_index1].main_params_to_fit + self.tabs[tab_index1].poly_params_to_fit
self.cbParam1.addItems(items1)
# Show the previously selected parameter if available
if previous_param1 in items1:
Expand All @@ -122,10 +121,13 @@ def setupParamWidgets(self):

# Store previously select parameter
previous_param2 = self.cbParam2.currentText()
# M2 has to be non-constrained
self.cbParam2.clear()
tab_index2 = self.cbModel2.currentIndex()
items2 = [param for param in self.params[tab_index2] if not self.tabs[tab_index2].paramHasConstraint(param)]
items2 = [param for param in self.params[tab_index2]]
# The following can be used if it is judged preferable that constrained
# parameters are not used in the definition of a new constraint
#items2 = [param for param in self.params[tab_index2] if not self.tabs[tab_index2].paramHasConstraint(param)]

self.cbParam2.addItems(items2)
# Show the previously selected parameter if available
if previous_param2 in items2:
Expand Down Expand Up @@ -210,9 +212,9 @@ def validateFormula(self):
"""
Add visual cues when formula is incorrect
"""
# temporarily disable validation
# temporarily disable validation, as not yet fully operational
return
#

formula_is_valid = self.validateConstraint(self.txtConstraint.text())
if not formula_is_valid:
self.cmdOK.setEnabled(False)
Expand Down Expand Up @@ -340,7 +342,9 @@ def applyAcrossTabs(self, tabs, param, expr):
"""
for tab in tabs:
if hasattr(tab, "kernel_module"):
if param in tab.kernel_module.params:
if (param in tab.kernel_module.params or
param in tab.poly_params or
param in tab.magnet_params):
value_ex = tab.kernel_module.name + "." +param
constraint = Constraint(param=param,
value=param,
Expand Down
11 changes: 10 additions & 1 deletion src/sas/qtgui/Perspectives/Fitting/Constraint.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Constraint(object):
"""
Internal representation of a single parameter constraint
Currently just a data structure, might get expaned with more functionality,
Currently just a data structure, might get expanded with more functionality,
hence made into a class.
"""
def __init__(self, parent=None, param=None, value=0.0,
Expand All @@ -14,6 +14,7 @@ def __init__(self, parent=None, param=None, value=0.0,
self._min = min
self._max = max
self._operator = operator
self._model = None
self.validate = True
self.active = True

Expand Down Expand Up @@ -81,3 +82,11 @@ def operator(self):
def operator(self, val):
self._operator = val

@property
def model(self):
# model this constraint originates from
return self._model

@model.setter
def model(self, val):
self._model = val
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need a property to access an attribute as an attribute. Just rename _model to model.

The reason you might want to convert an attribute to a property is if you want filter the value on get/set. For example,

@property
def angle_deg(self):
    return np.degrees(self.angle)
@angle_deg.setter
def angle_deg(self, val):
    self.angle = np.radians(val)

My convention is to only use properties if the amount of work is small, otherwise use a method. Imagine that you are accessing the property value in a loop. If caching the value before the loop provides a significant speedup then you should not be using a property.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes from the original design by @rozyczko, so I let him decide on this (my coding expertise is not enough to judge between the merits of each approach!).

75 changes: 49 additions & 26 deletions src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def initializeWidgets(self):
# Single Fit is the default, so disable chainfit
self.chkChain.setVisible(False)

# disabled constraint
# disabled constraint
labels = ['Constraint']
self.tblConstraints.setColumnCount(len(labels))
self.tblConstraints.setHorizontalHeaderLabels(labels)
Expand Down Expand Up @@ -400,6 +400,10 @@ def onHelp(self):
help_location = tree_location + helpfile

# OMG, really? Crawling up the object hierarchy...
#
# It's the top level that needs to do the show help.
# Perhaps better to address directly, but it does need to
# be that object. I don't like that the type is hidden. :LW
self.parent.parent.showHelp(help_location)

def onTabCellEdit(self, row, column):
Expand Down Expand Up @@ -500,16 +504,16 @@ def onTabCellEdit(self, row, column):

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.
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.
In case of an error during checking, a warning message box is shown
and the constraint is cancelled by reloading the view.
View is also reloaded when the user is finished for consistency.
"""
item = self.tblConstraints.item(row, column)
# extract information from the constraint object
# Extract information from the constraint object
constraint = self.available_constraints[row]
model = constraint.value_ex[:constraint.value_ex.index(".")]
param = constraint.param
Expand All @@ -529,6 +533,7 @@ def onConstraintChange(self, row, column):
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)
Expand All @@ -546,7 +551,7 @@ def onConstraintChange(self, row, column):
# 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
# Check that the symbol is known so we don't 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
Expand All @@ -566,6 +571,7 @@ def onConstraintChange(self, row, column):
return
new_function = rhs
new_tab = self.available_tabs[new_model]
model_key = tab.getModelKeyFromName(param)
# Make sure we are dealing with fit tabs
assert isinstance(tab, FittingWidget)
assert isinstance(new_tab, FittingWidget)
Expand All @@ -574,8 +580,9 @@ def onConstraintChange(self, row, column):
# Apply the new constraint
constraint = Constraint(param=new_param, func=new_function,
value_ex=new_model + "." + new_param)
model_key = tab.getModelKeyFromName(new_param)
new_tab.addConstraintToRow(constraint=constraint,
row=tab.getRowFromName(new_param))
row=tab.getRowFromName(new_param), model_key=model_key)
# 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
Expand All @@ -594,9 +601,9 @@ def onConstraintChange(self, row, column):
font.setItalic(True)
brush = QtGui.QBrush(QtGui.QColor('blue'))
tab.modifyViewOnRow(tab.getRowFromName(new_param), font=font,
brush=brush)
brush=brush, model_key=model_key)
else:
tab.modifyViewOnRow(tab.getRowFromName(new_param))
tab.modifyViewOnRow(tab.getRowFromName(new_param), model_key=model_key)
# reload the view so the user gets a consistent feedback on the
# constraints
self.initializeFitList()
Expand Down Expand Up @@ -891,7 +898,8 @@ def deleteConstraint(self):#, row):
moniker = constraint[:constraint.index(':')]
param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
tab = self.available_tabs[moniker]
tab.deleteConstraintOnParameter(param)
model_key = tab.getModelKeyFromName(param)
tab.deleteConstraintOnParameter(param, model_key=model_key)

# Constraints removed - refresh the table widget
self.initializeFitList()
Expand All @@ -904,14 +912,17 @@ def uneditableItem(self, data=""):
item.setFlags( QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled )
return item

def updateFitLine(self, tab):
def updateFitLine(self, tab, model_key="standard"):
"""
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:
logging.warning("No model selected")
return

tab_name = tab
model_name = model.id
moniker = model.name
Expand Down Expand Up @@ -948,19 +959,29 @@ def updateFitLine(self, tab):
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()
constraint_names = fit_page.getComplexConstraintsForAllModels()
constraints = fit_page.getConstraintObjectsForAllModels()

active_constraint_names = []
constraint_names = []
constraints = []
for model_key in fit_page.model_dict.keys():
active_constraint_names += fit_page.getComplexConstraintsForModel(model_key=model_key)
constraint_names += fit_page.getFullConstraintNameListForModel(model_key=model_key)
constraints += fit_page.getConstraintObjectsForModel(model_key=model_key)

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:
if not constraint_name and len(constraint_name) < 2:
continue
if constraint_name[0] is None or constraint_name[1] is None:
continue
# Create the text for widget item
label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
label = moniker + ":" + constraint_name[0] + " = " + constraint_name[1]
pos = self.tblConstraints.rowCount()
self.available_constraints[pos] = constraint

Expand All @@ -979,7 +1000,7 @@ def updateFitLine(self, tab):
self.tblConstraints.setItem(pos, 0, item)
self.tblConstraints.blockSignals(False)

def initializeFitList(self):
def initializeFitList(self, row=0, model_key="standard"):
"""
Fill the list of model/data sets for fitting/constraining
"""
Expand Down Expand Up @@ -1018,7 +1039,7 @@ def initializeFitList(self):
self._row_order = tabs

for tab in tabs:
self.updateFitLine(tab)
self.updateFitLine(tab, model_key=model_key)
self.updateSignalsFromTab(tab)
# We have at least 1 fit page, allow fitting
self.cmdFit.setEnabled(True)
Expand Down Expand Up @@ -1085,14 +1106,15 @@ def onAcceptConstraint(self, con_tuple):

# Find the constrained parameter row
constrained_row = constrained_tab.getRowFromName(constraint.param)
model_key = constrained_tab.getModelKeyFromName(constraint.param)

# Update the tab
constrained_tab.addConstraintToRow(constraint, constrained_row)
constrained_tab.addConstraintToRow(constraint, constrained_row, model_key=model_key)
if not self.constraint_accepted:
return

# Select this parameter for adjusting/fitting
constrained_tab.changeCheckboxStatus(constrained_row, True)
# constrained_tab.selectCheckbox(constrained_row, model=model)

def showMultiConstraint(self):
"""
Expand Down Expand Up @@ -1212,4 +1234,5 @@ def uncheckConstraint(self, name):
# deactivate the constraint
tab = self.parent.getTabByName(name[:name.index(":")])
row = tab.getRowFromName(name[name.index(":") + 1:])
tab.getConstraintForRow(row).active = False
model_key = tab.getModelKey(constraint)
tab.getConstraintForRow(row, model_key=model_key).active = False
10 changes: 6 additions & 4 deletions src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,11 @@ def updateFromConstraints(self, constraint_dict):
constraint.param = constraint_param[1]
constraint.value_ex = constraint_param[2]
constraint.validate = constraint_param[3]
model_key = tab.getModelKey(constraint)
tab.addConstraintToRow(constraint=constraint,
row=tab.getRowFromName(
constraint_param[1]))
constraint_param[1]),
model_key=model_key)

def closeEvent(self, event):
"""
Expand Down Expand Up @@ -535,9 +537,9 @@ def getActiveConstraintList(self):
constraints = []
for tab in self.getFitTabs():
tab_name = tab.modelName()
tab_constraints = tab.getConstraintsForModel()
constraints.extend((tab_name + "." + par, expr)
for par, expr in tab_constraints)
tab_constraints = tab.getConstraintsForAllModels()
constraints.extend((tab_name + "." + par, expr) for par, expr in tab_constraints)

return constraints

def getSymbolDictForConstraints(self):
Expand Down
18 changes: 11 additions & 7 deletions src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,18 @@ def addSimpleParametersToModel(parameters, is2D, parameters_original=None, model
Actually appends to model, if model and view params are not None.
Always returns list of lists of QStandardItems.

parameters_original: list of parameters before any tagging on their IDs, e.g. for product model (so that those are
the display names; see below)
parameters_original: list of parameters before any tagging on their IDs,
e.g. for product model (so that those are the display names; see below)
"""
if is2D:
params = [p for p in parameters.kernel_parameters if p.type != 'magnetic']
else:
params = parameters.iq_parameters

if parameters_original:
# 'parameters_original' contains the parameters as they are to be DISPLAYED, while 'parameters'
# contains the parameters as they were renamed; this is for handling name collisions in product model.
# 'parameters_original' contains the parameters as they are to be DISPLAYED,
# while 'parameters' contains the parameters as they were renamed;
# this is for handling name collisions in product model.
# The 'real name' of the parameter will be stored in the item's user data.
if is2D:
params_orig = [p for p in parameters_original.kernel_parameters if p.type != 'magnetic']
Expand Down Expand Up @@ -710,7 +711,6 @@ def getRelativeError(data, is2d, flag=None):

return weight


def calcWeightIncrease(weights, ratios, flag=False):
""" Calculate the weights to be passed to bumps in order to ensure
that each data set contributes to the total residual with a
Expand Down Expand Up @@ -767,7 +767,6 @@ def calcWeightIncrease(weights, ratios, flag=False):

return weight_increase


def updateKernelWithResults(kernel, results):
"""
Takes model kernel and applies results dict to its parameters,
Expand All @@ -782,7 +781,6 @@ def updateKernelWithResults(kernel, results):

return local_kernel


def getStandardParam(model=None):
"""
Returns a list with standard parameters for the current model
Expand Down Expand Up @@ -965,9 +963,15 @@ def isParamPolydisperse(param_name, kernel_params, is2D=False):
"""
Simple lookup for polydispersity for the given param name
"""
# First, check if this is a polydisperse parameter directly
if '.width' in param_name:
return True

parameters = kernel_params.form_volume_parameters
if is2D:
parameters += kernel_params.orientation_parameters

# Next, check if the parameter is included in para.polydisperse
has_poly = False
for param in parameters:
if param.name==param_name and param.polydisperse:
Expand Down
Loading