Reading a constitutive Parameters value and assigning it straight back is not a no-op — it nests a UWexpression inside itself, and the next .value access recurses until the stack dies.
cm = stokes.constitutive_model
saved = cm.Parameters.yield_stress # read
...
cm.Parameters.yield_stress = saved # write back -> RecursionError later
Traceback (abridged):
constitutive_models.py:_apply_floor return uw.maths.smooth_max(value, floor, rounding)
maths/functions.py:smooth_max return (a + b + sympy.sqrt((a - b) ** 2 + epsilon**2)) / 2
function/expressions.py:__add__ result_value = self.value + other.value
function/expressions.py:947 value if hasattr(self._sym, 'value'):
[Previous line repeated 977 more times]
RecursionError: maximum recursion depth exceeded
UWexpression.value (expressions.py:947) walks self._sym looking for a nested .value, and self-assignment makes _sym reference the expression that owns it, so the walk never terminates.
Why it bites in practice. Save/restore around a temporary parameter change is the natural way to probe a model without disturbing it — e.g. removing a regularisation to measure a residual against the unregularised law, then putting it back. That pattern is silently a trap. The workaround is to re-derive the value rather than restore the object, which is only possible if you know how it was built.
Two things worth considering:
- the setter could unwrap an incoming
UWexpression that is already this parameter's own expression (or, more generally, refuse to nest an expression inside itself);
UWexpression.value could carry a cycle guard so the failure is a clear error rather than a stack overflow 977 frames deep.
Related: the same class of self-referential blow-up was seen with sympy.Max(<UWexpression>, x) recursing to stack death during the Drucker–Prager consistent-Jacobian work, which is why uw.maths.smooth_max exists.
Found while measuring true hard-Min residuals during viscoplastic continuation work (PETSc 3.25, development).
Reading a constitutive
Parametersvalue and assigning it straight back is not a no-op — it nests aUWexpressioninside itself, and the next.valueaccess recurses until the stack dies.Traceback (abridged):
UWexpression.value(expressions.py:947) walksself._symlooking for a nested.value, and self-assignment makes_symreference the expression that owns it, so the walk never terminates.Why it bites in practice. Save/restore around a temporary parameter change is the natural way to probe a model without disturbing it — e.g. removing a regularisation to measure a residual against the unregularised law, then putting it back. That pattern is silently a trap. The workaround is to re-derive the value rather than restore the object, which is only possible if you know how it was built.
Two things worth considering:
UWexpressionthat is already this parameter's own expression (or, more generally, refuse to nest an expression inside itself);UWexpression.valuecould carry a cycle guard so the failure is a clear error rather than a stack overflow 977 frames deep.Related: the same class of self-referential blow-up was seen with
sympy.Max(<UWexpression>, x)recursing to stack death during the Drucker–Prager consistent-Jacobian work, which is whyuw.maths.smooth_maxexists.Found while measuring true hard-Min residuals during viscoplastic continuation work (PETSc 3.25,
development).