Summary
A constant UWexpression that is collected into solver.constants_manifest can nonetheless have its value baked into the compiled C source, so changing its .sym between solves has no effect — the parameter is not live-rampable even though it is listed as a live constant. A full solver rebuild (is_setup = False + re-set the constitutive parameter) is required to pick up the new value.
This breaks the documented contract that a manifested constant can be updated via .sym and repacked through constants[] without recompilation. It is the mechanism the δ-continuation / homotopy work relies on, and it works for some parameters (δ) but silently fails for others (a rate-strengthening viscosity floor ξ).
Observed behaviour (Stokes, ViscousFlowModel, Drucker–Prager + Perzyna floor)
Viscosity contains two rampable constants defined identically as uw.expression(...):
- δ (soft-min smoothing): appears as
δ² inside sqrt((f-1)² + δ²) where f depends on the strain-rate field. Live-ramps correctly — setting δ.sym and solving picks up the new value (the δ-march converges as expected).
- ξ (Perzyna floor): appears as
ξ·η_bg in η_p = σ_y/(2·ε̇) + ξ·η_bg. Does NOT live-ramp — setting ξ.sym between solves leaves the solution unchanged; only a rebuild propagates the new ξ.
Both appear in constants_manifest (verified by printing [e.name for i,e in solver.constants_manifest] — \xi is present at a valid index). So ξ is listed as a live constant but its value is baked in the generated C — updating constants[] for its slot does not change the residual.
Verification was via full solves (a warm ξ-descent): without a rebuild between ξ steps the converged solution (and a band-thickness diagnostic) is frozen across all ξ; with a rebuild per ξ it varies correctly. (Note: a quick snes_max_it=0 + getFunctionNorm() residual probe is not a reliable test here — it returns a lagged norm — so please validate any fix with a real solve.)
Likely mechanism (hypothesis for the maintainers)
constants_manifest is built by _extract_constants / _collect_constant_atoms on the pre-unwrap callbacks (so ξ is found and listed). The C source is generated from the post-unwrap expression. The nondimensionalisation/unwrap appears to fold pure-constant subexpressions to numbers: ξ·η_bg is a product of two constants and collapses to a literal, so ξ never reaches the substitution that would route it through constants[]. δ survives only because its sole occurrence is inside a field-dependent subexpression ((f-1)² + δ²) that cannot fold. Net: a manifested constant whose every occurrence is inside an otherwise fully-constant subexpression gets baked, and the manifest/C-source disagree.
If correct, reformulating ξ so it always multiplies a field (e.g. the natural Perzyna numerator ξ·η_bg·2·ε̇ before dividing by 2·ε̇) might keep it live — but this is untested (the fold may still occur after sympy simplification), and the deeper fix is to make _extract_constants and the C-source generation agree (either don't fold a subexpression that contains a manifested constant, or substitute manifested constants before the fold).
Impact
- Silent: no error; the parameter simply doesn't update, which can invalidate continuation/homotopy studies that ramp such a parameter in place.
- Same family as the previously-noted "constant in an exponent doesn't ramp" JIT gotcha.
Suggested checks
- Make
constants_manifest and the emitted C source consistent — a listed constant must be read from its constants[] slot, never baked.
- Add a guard/warning when a manifested constant is detected to have been folded out of the C source (manifest lists it but the code never references its slot).
- A minimal repro: two
uw.expression constants in a viscosity, one multiplying a field, one multiplying only other constants; ramp each .sym and confirm via a real solve which propagates.
Reported from the yield-homotopy investigation (Spiegelman notch, harness convergence.py); happy to provide the full harness or help build a minimal repro.
Summary
A constant
UWexpressionthat is collected intosolver.constants_manifestcan nonetheless have its value baked into the compiled C source, so changing its.symbetween solves has no effect — the parameter is not live-rampable even though it is listed as a live constant. A full solver rebuild (is_setup = False+ re-set the constitutive parameter) is required to pick up the new value.This breaks the documented contract that a manifested constant can be updated via
.symand repacked throughconstants[]without recompilation. It is the mechanism the δ-continuation / homotopy work relies on, and it works for some parameters (δ) but silently fails for others (a rate-strengthening viscosity floor ξ).Observed behaviour (Stokes,
ViscousFlowModel, Drucker–Prager + Perzyna floor)Viscosity contains two rampable constants defined identically as
uw.expression(...):δ²insidesqrt((f-1)² + δ²)wherefdepends on the strain-rate field. Live-ramps correctly — settingδ.symand solving picks up the new value (the δ-march converges as expected).ξ·η_bginη_p = σ_y/(2·ε̇) + ξ·η_bg. Does NOT live-ramp — settingξ.symbetween solves leaves the solution unchanged; only a rebuild propagates the new ξ.Both appear in
constants_manifest(verified by printing[e.name for i,e in solver.constants_manifest]—\xiis present at a valid index). So ξ is listed as a live constant but its value is baked in the generated C — updatingconstants[]for its slot does not change the residual.Verification was via full solves (a warm ξ-descent): without a rebuild between ξ steps the converged solution (and a band-thickness diagnostic) is frozen across all ξ; with a rebuild per ξ it varies correctly. (Note: a quick
snes_max_it=0+getFunctionNorm()residual probe is not a reliable test here — it returns a lagged norm — so please validate any fix with a real solve.)Likely mechanism (hypothesis for the maintainers)
constants_manifestis built by_extract_constants/_collect_constant_atomson the pre-unwrap callbacks (so ξ is found and listed). The C source is generated from the post-unwrap expression. The nondimensionalisation/unwrap appears to fold pure-constant subexpressions to numbers:ξ·η_bgis a product of two constants and collapses to a literal, so ξ never reaches the substitution that would route it throughconstants[]. δ survives only because its sole occurrence is inside a field-dependent subexpression ((f-1)² + δ²) that cannot fold. Net: a manifested constant whose every occurrence is inside an otherwise fully-constant subexpression gets baked, and the manifest/C-source disagree.If correct, reformulating ξ so it always multiplies a field (e.g. the natural Perzyna numerator
ξ·η_bg·2·ε̇before dividing by2·ε̇) might keep it live — but this is untested (the fold may still occur after sympy simplification), and the deeper fix is to make_extract_constantsand the C-source generation agree (either don't fold a subexpression that contains a manifested constant, or substitute manifested constants before the fold).Impact
Suggested checks
constants_manifestand the emitted C source consistent — a listed constant must be read from itsconstants[]slot, never baked.uw.expressionconstants in a viscosity, one multiplying a field, one multiplying only other constants; ramp each.symand confirm via a real solve which propagates.Reported from the yield-homotopy investigation (Spiegelman notch, harness
convergence.py); happy to provide the full harness or help build a minimal repro.