Fix GenericFluxModel flux storage — preserve symbolic field dependencies - #455
Conversation
GenericFluxModel.Parameters.flux setter (and __init__) wrapped each flux component in a UWexpression via validate_parameters(), creating intermediate symbols (q_0, q_1) that the JIT treated as independent constants, losing the dependency on the unknown field variable and producing a singular matrix. Fix: store raw sympy expressions directly, bypassing the UWexpression wrapper. Includes test test_1060_generic_flux_model_update.py verifying both the _force_setup and _solver-link propagation paths. Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
This PR fixes GenericFluxModel.Parameters.flux storage so flux components are preserved as raw SymPy expressions (instead of being wrapped into UWexpression placeholders), preventing loss of symbolic dependencies on the unknown field during JIT assembly (which previously could yield singular systems).
Changes:
- Updated
GenericFluxModel._Parameters.__init__andfluxsetter to store raw SymPy vectors directly (novalidate_parameters()wrapping intoq_isymbols). - Added a new tier_a / level_1 regression test module covering flux updates across solves and ensuring flux is stored as raw SymPy.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/underworld3/constitutive_models.py |
Stores GenericFluxModel flux as raw SymPy to preserve field-variable dependencies for JIT assembly. |
tests/test_1060_generic_flux_model_update.py |
New regression tests validating flux update propagation and raw-SymPy storage. |
Comments suppressed due to low confidence (2)
tests/test_1060_generic_flux_model_update.py:86
- This test asserts internal/private solver state (
_solver_is_setup,_needs_function_rewire). Those flags are implementation details and may change without breaking the external behaviour this test is trying to validate (that changingParameters.fluxtriggers a rewire and the nextsolve()uses the new expression). It’s safer to validate behaviour via the second solve + ratio checks only.
assert not solver.constitutive_model._solver_is_setup
assert solver._needs_function_rewire
tests/test_1060_generic_flux_model_update.py:116
- The raw-sympy check relies on
"q_" not in str(flux), which is brittle (string representations can change, and legitimate symbols could containq_). Prefer a structural check that elements are notUWexpressioninstances.
# Check it's a plain sympy Matrix, not a UWexpression
assert isinstance(flux, sympy.Matrix), f"Expected sympy.Matrix, got {type(flux)}"
# The elements should be raw sympy, not UWexpressions
# UWexpressions show as "q_0 = ..." in str representation
flux_str = str(flux)
assert "q_" not in flux_str, (
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| solver = Poisson(mesh, u_Field=u) | ||
| solver.petsc_options["ksp_type"] = "preonly" | ||
| solver.petsc_options["pc_type"] = "lu" | ||
| solver.petsc_options["pc_factor_mat_solver_type"] = "mumps" | ||
| solver.constitutive_model = uw.constitutive_models.GenericFluxModel(solver.Unknowns) | ||
| solver.constitutive_model.Parameters._solver = solver | ||
| solver.constitutive_model.Parameters.flux = flux_expr |
Adversarial review — live probes on the PR head and its exact parent commitVerdict: the fix is correct and merge-worthy; the stated mechanism is wrong in a way that matters for the record. No change to the fix itself is needed — a wording correction and one test cleanup follow in a response commit, then this merges. The true mechanism (probed, decisive)Reproduced pre-fix on the PR's exact parent (
So this is not "the JIT treated the components as independent constants" — the manifest was empty and the residual was never corrupted (PR #416's reveal machinery handled the wrappers correctly, which is why the Newton-mode run converges with the identical residual). The defect is in the default Picard tangent: This distinction matters: the current docstring/comment wording implicitly indicts the #416 constants machinery, which is exonerated, and would misdirect the next person who hits a singular matrix. Findings
The trap that remains (scoped precisely)The general hazard is narrower than "wrapped field-dependent parameters break Jacobians": coefficient-level wrapping under the Picard default is intentional frozen-coefficient semantics and stays nonsingular. The dangerous pattern is exactly one — a wrapped Parameter that swallows the entire ∇u dependence of a flux term — and GenericFluxModel was the only in-tree instance. Future models (or user-built constitutive models) can re-create it; a warn-if-wrapped-sym-contains-the-unknown's-gradient guard is conceivable but out of scope here. Adversarial review per project policy — Underworld development team with AI support from Claude Code |
… tangent, not the JIT From the adversarial review on the PR thread: - The setter docstring and __init__ comment claimed the JIT treated the wrapped flux components as independent constants. Probed false: the constants manifest was empty, the residual was never corrupted, and the identical pre-fix code solves under consistent_jacobian=True. The true mechanism is that the DEFAULT (Picard) tangent differentiates the flux without unwrapping (frozen-coefficient semantics) — an opaque wrapper holding the ENTIRE flux has zero derivative w.r.t. the unknown and its gradient, so G0-G3 vanish and the operator is structurally singular. Wording corrected so the record does not indict the JIT constants machinery (#416, exonerated). - Documented the side effect: with nothing left to freeze, GenericFluxModel's default tangent is effectively full Newton. - Test hygiene: removed the hand-wiring of Parameters._solver (the constitutive_model setter does it — hand-wiring masks a regression in that path) and the MUMPS solver pin (house policy avoids MUMPS; plain LU is used by the review probes and passes identically). Fix itself unchanged; all review probes pass (see thread). Underworld development team with AI support from Claude Code
…d the MUMPS pin The constitutive_model setter wires Parameters._solver — hand-wiring masked a regression in that path and weakened test_flux_update_via_solver_link. Plain LU replaces the MUMPS pin (house policy; review probes used LU throughout). Underworld development team with AI support from Claude Code
Summary
GenericFluxModel.Parameters.fluxsetter (and__init__) wrapped each flux component in a UWexpression viavalidate_parameters(), creating intermediate symbols (q_0,q_1) that the JIT treated as independent constants. This lost the dependency on the unknown field variable and produced a singular matrix.Fix
Store raw sympy expressions directly in the flux setter, bypassing the UWexpression wrapper. The
__init__now also sets a rawsympy.zerosvector instead of validated components.Tests
New test file
tests/test_1060_generic_flux_model_update.pywith three tier_a, level_1 tests:_force_setup=Trueworks (solution scales correctly with k)_solverback-link triggers automatic rewire (no_force_setupneeded)q_0,q_1artifacts)Verification
All three tests pass with the fix; before the fix they would produce singular matrices.