Skip to content

Viscoelastic stress past the conformation limit: a positive-definite convected step and a log-conformation history - #795

Merged
lmoresi merged 3 commits into
developmentfrom
feature/log-conformation
Oct 8, 2026
Merged

lmoresi merged 3 commits into
developmentfrom
feature/log-conformation

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 26, 2026

Copy link
Copy Markdown
Member

Stacked on #789. Lets the viscoelastic Stokes solve reach the high-Weissenberg regime of the benchmark flows: with it the creeping UCM cross-slot shows its purely elastic pitchfork, where before every run from De 0.5 stalled or hung.

What lost the conformation, measured on the cross-slot at De 0.8:

  • The step. The upper-convected BDF-1 step is F c* Fᵀ (F = I + dt L) less its second-order term dt² L c* Lᵀ, so it goes indefinite once dt|L| is of order one. A smaller timestep did not help: at a corner |L| grows with resolution.
  • The store. Interpolation, projection and per-cell fits undershoot near a corner singularity, and the forward store's linear fit extrapolates to the cell edges; the stress read back was indefinite although every stored value was positive.

Two options on ViscoElasticPlasticFlowModel (upper-convected, first order):

  • convected_step="deformation" keeps the dropped term: positive-definite for any step, still first order. The exponential step completes the relaxation target's stretch to a product the same way.
  • stress_history="log_conformation" (2-D) stores ψ = log c and reads σ* = G(exp ψ* − I); it implies the deformation step. The flavours are untouched: each transports its stored tensor by pure advection, and the step stays on c, where relaxation is linear, so neither integrator changes.

The model owns the encoding. The default NavierStokes decodes its history flux; SNES_NavierStokes and the multi-material model refuse the log store. stokes.tau projects the decoded stress; the health check reports where the logarithm's floor acted.

Result, cross-slot, full resolution, forward history, seeded: growth rate of the asymmetry −0.77, −0.24, +0.06, +0.27 at De 0.5, 0.65, 0.72, 0.8 (De = λU/H, H the half-width), onset near 0.71, against 0.62 expected from Poole et al. (full-width 0.31). The conformation stays above 0.33 everywhere.

Tests (test_1065, and log cases in test_1064): the one-step extension closed form for both integrators and both representations, plus the linear step's indefinite −0.818; the shear start-up across four flavours and both integrators against the discrete recurrence in σ_xy and N1; the units model. The stress-history, swarm and advection files pass (316); the style gate is clean.

Limits: first order; the log history 2-D only; not for stresses that are not conformations (geodynamic VEP with no objective rate).

Underworld development team with AI support from Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo

…convected step and a log-conformation history

Two things lose the conformation c = sigma/G + I at a re-entrant corner or a
stagnation point at high Weissenberg number, measured on the cross-slot:

- The step. The upper-convected BDF-1 step is F c* F^T (F = I + dt L) less its
  second-order term dt^2 L c* L^T, so it goes indefinite once dt |L| ~ 1.
  convected_step="deformation" keeps the term (source += dt(L s* L^T + G L L^T)):
  positive-definite for any step, still first order. The exponential step
  completes the relaxation target's stretch to a product the same way, written
  in x = dt/lam rather than the integrator's alpha, which is clamped to one in
  the elastic limit. max_elastic_timestep reports no limit for this step.
- The store. Interpolation, projection and per-cell fits undershoot near a
  corner singularity, and a fit extrapolates to the cell edges.
  stress_history="log_conformation" stores psi = log c; the model reads
  sigma* = G (exp psi* - I), a conformation whatever was done to psi. It implies
  the deformation step. Every flavour transports its stored tensor by pure
  advection, so none changes; the step stays on c, where relaxation is linear.

The model owns the encoding (encode_history; the decode is its carried-stress
read); the solver stores the model's record (one helper for the flux and the
commit), builds a log store dimensionless, and hands the history the encoding
for the inflow datum. The default NavierStokes decodes its history flux;
SNES_NavierStokes, which reads the history as a flux, and the multi-material
model refuse the log store. stokes.tau projects the decoded stress. The health
check reports where the logarithm's floor (1e-12, round-off only) acted.

Tests (test_1065): the one-step extension closed form for both integrators and
both representations, and the linear step's indefinite -0.818; the shear
start-up across four flavours and both integrators against the discrete
recurrence in sigma_xy and N1 (every deformation term lives in sigma_xx);
units-model cases in test_1064.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
@lmoresi

lmoresi commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

Adversarial review before the push (two reviewers: the units change and this branch, briefed for over-complexity as well as defects). Applied here:

  • One option, not a matrix. The log history implies the deformation step and refuses the linear one; the linear step could hand it an indefinite conformation, which the floor then hid.
  • One owner of the encoding. The model's encode_history and its carried-stress read; one solver helper stores the record for both the flux and the commit (the fallback chain was written three ways); the history gets the encoding once, for the inflow datum, read at one place. Removed: the private maps pushed into the history, the _inflow_value property and its __dict__ access, three of the five encode/decode methods.
  • Readers that took ψ as a stress. The default NavierStokes flux (decodes now), stokes.tau (projects the decoded stress, not the next step's predictor), the multi-material model (refuses).
  • The exponential term at the elastic limit. Written in x = dt/λ; the integrator's α is clamped to one there and the old form divided by zero.
  • The floor. 1e-12 (round-off only after a positive-definite step), not 1e-3, which clipped legitimate conformations such as 1/(1+2Wi) in extension; the health check now reports where it acted, since a decoded conformation is positive by construction.
  • max_elastic_timestep reports no limit for the deformation step; the order guard sits where the order is set.
  • Tests. The shear box checked only σ_xy, where every new term vanishes; it now follows the discrete recurrence in N1 too, and the exponential deformation term has its closed form.

Recorded: set_initial_history takes stored values (documented), while the inflow datum is given as a stress; a checkpoint does not record which representation it holds.

lmoresi added a commit that referenced this pull request Sep 27, 2026
…Stokes velocity history offers every semi-Lagrangian scheme

global_evaluate applied monotone="clamp" on the rank that asked for a point,
bounding it by that rank's nearest nodes; for a departure point evaluated on
another rank those are the wrong neighbourhood. The bound now runs on the
evaluating rank (first pass and containment round), on non-dimensional values
like the nodal data it compares with. Serial results are unchanged. #682's
level-set reproduction (LeVeque swirl, 128^2 quad, P2, SLCN with clamp):
volume at np 1/2/4/8 now 0.0706771442/415/438/412 (np 8 was 1.6% off).

NavierStokesSLCN(velocity_transport=...) chooses the velocity history among
the four semi-Lagrangian schemes, through one _value_history helper shared
with AdvDiffusionSLCN(transport=...). Forward schemes need order=1; the
forward integration-point fit is linear and refuses a P2 velocity.

tests: test_1103 (lid-driven cavity, Re 100, three schemes against recorded
values and each other); test_1066 adds the cavity (equal to serial at np
3/4/6). tests/parallel at np 4: 168 passed, 1 xfailed, 1 failed
(test_1063_constrained_freeslip_parallel[ti], fails identically on #795's
05d5248). Serial set: 393 passed.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
@lmoresi

lmoresi commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Why this is red, and what it needs

The two CI failures are not this PR's. They are byte-identical across #785, #789, #795 and #800:

tests/test_1060_nitsche_freeslip.py::test_nitsche_normal_velocity_zero
    assert np.float64(0.0001008010278702676) < 0.0001
tests/test_1070_free_surface_plume.py::test_freesurface_strong_constraint_beats_penalty
    AssertionError: strong constraint not better: 1.73e-02 vs penalty 3.25e-02

Both tests were already fixed on development, for exactly these numbers, and neither name exists there any more. development's CI is green at 1391e4aa. The replacements name the failures in their own docstrings:

  • test_1060 → test_nitsche_constrains_the_wall_normal_velocity, a relative bound of 0.1: "the previous version of this test asserted an ABSOLUTE 1e-4, which scaled with the buoyancy forcing rather than with anything about Nitsche, and sat a fraction of a percent from failing for unrelated reasons."
  • test_1070 → test_freesurface_strong_constraint_tracks_the_prescribed_rate, an absolute bound of 5e-2 against the datum: "It replaces a comparison against the penalty path (errors["strong"] < 0.5 * errors["penalty"]) … measured 2026-09-14: 1.06e-2 on macOS, 1.73e-2 on the Linux CI runner, where the annulus triangulates differently."

So this branch is stale, not broken — 58 commits behind development.

The merge is not clean, and the conflicts are the same on all four

git merge origin/development conflicts in exactly two files on every one of the four branches (#800 adds a third, tests/test_0066_integration_point_slcn.py):

scripts/test.sh — one hunk, take THIS branch's side. development narrowed the glob to tests/test_1100*py; this side has tests/test_110*py. test_1101_advdiff_swarm_rotating_gaussian.py matches none of development's globs (test_1100*, test_1110*, test_1120*), so the narrow side would leave a test file dark — #721 recurring. scripts/check_test_coverage.py verifies the resolution.

src/underworld3/systems/ddt.py — five hunks, and four of them are mechanical. Four have a zero-line development side: they are this branch's own additions (applies_inflow_value, commits_flux_in_post_solve, the inflow_value setter — the #745/#783 inflow work), flagged only because surrounding context moved. None of those three names exists on development. Take this branch's side.

The fifth, at the top of _DDtBase, is a real design conflict and needs the author: this branch adds update_exp_coefficients / _exp_alpha / _exp_phi to the base class; development has since put update_exp_coefficients in three per-flavour classes (ddt.py:1299, 1734, 3612) with _exp_alpha/_exp_phi at :3623, 3628, and adds _note_history_shift in that same place. Keeping both gives a base-class definition under three overrides. development's per-flavour layout is the later design, but which one this branch's ETD work is written against is the author's call, not a merge-tool decision — and this is the DDt core, where CLAUDE.md asks for benchmarking rather than a judgement.

Order matters

All four touch ddt.py in the same region, so they conflict with each other as well as with development. They want merging one at a time, smallest first (#789, #785, #795, then #800), with a rebuild and the transport tests between each — the base moves for the rest after every merge.

Found in the backlog sweep (#819). No commits pushed to this branch.

Same two files and five hunks as the other stale transport branches.

scripts/test.sh keeps this branch's broader `tests/test_110*py`; development
narrowed it to `test_1100*py`, leaving the other test_110* files matching no
glob — #721 recurring. scripts/check_test_coverage.py verifies it.

Four of the five ddt.py hunks have an empty development side: this branch's own
additions, flagged because surrounding context moved. The fifth takes both —
the base-class ETD coefficients (#739) and development's `_note_history_shift`
— and duplicates nothing, verified on the merged tree.

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Oct 6, 2026
This branch is the base of a four-PR stack (#785 -> #789 -> #795 -> #800) and
was 71 commits behind development, which is why all four showed the same two
CI failures: tests development had already renamed
(test_1060_nitsche_freeslip's absolute 1e-4 bound and
test_1070_free_surface_plume's strong-vs-penalty ratio). The refresh belongs
here, at the root, so each PR's diff against its base stays its own work rather
than growing development's history.

Two conflicts, both this branch's own additions. scripts/test.sh keeps the
broader `tests/test_110*py` glob -- development narrowed it to `test_1100*py`,
which matches none of the other test_110* files here (#721 recurring);
scripts/check_test_coverage.py verifies it. Four of the five ddt.py hunks have
an empty development side, and the fifth takes both: this branch's base-class
`update_exp_coefficients` / `_exp_alpha` / `_exp_phi` (#739, which replaced
three per-flavour copies dating from b6b0e7a in April) and development's
`_note_history_shift`. Verified on the merged tree: one definition of each.

Underworld development team with AI support from Claude Code
@lmoresi
lmoresi changed the base branch from bugfix/stress-history-units to development October 8, 2026 01:45
@lmoresi
lmoresi merged commit 8f1af5a into development Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant