Skip to content

Viscoelastic constituents in the multi-material model - #854

Merged
lmoresi merged 12 commits into
bugfix/etd-variable-maxwell-timefrom
feature/multimaterial-ve
Oct 23, 2026
Merged

lmoresi merged 12 commits into
bugfix/etd-variable-maxwell-timefrom
feature/multimaterial-ve

Conversation

@lmoresi

@lmoresi lmoresi commented Oct 18, 2026

Copy link
Copy Markdown
Member

Viscoelastic constituents in MultiMaterialConstitutiveModel, so a fault material (TI-VEP, or a weak VEP) can sit beside an isotropic VEP bulk under a level set or an index variable. This is item A of the VE/plastic gap closure. Stacked on #851; review and merge #851 first, then this PR retargets to development.

What was broken

A composite with any viscoelastic constituent ran without error and gave wrong results:

  • No stress history. The composite did not forward requires_stress_history, order, the history kwargs, dt_elastic, the pre- and post-solve hooks or history_flux. The run was silently viscous or produced NaN.
  • One ETD α for every material. α and φ lived on the shared stress history, so the last constituent to update them set them for all.
  • TI-VEP given its parameters before a solver. The tensor was built and cached at dt = ∞ and gave NaN, even for a single material.
  • VEP BDF at dt = ∞. The effective viscosity was 0·∞.

What changed

  • ETD coefficients belong to the model (_ExponentialCoefficients mixin). Each constituent relaxes on its own Maxwell time. The history's own coefficients are removed; nothing reads them, and snapshots never carried them.
  • An infinite elastic timestep is the viscous limit in the VEP and TI-VEP effective viscosity and tensor. The TI dt setter resets the cached tensor.
  • The composite forwards the stress-history interface:
    • order is the highest order of any constituent. Each constituent keeps its own order, and a history of lower order than a constituent needs is refused.
    • The solver now checks the order before assigning the model, so a refused composite is never left on the solver.
    • It also forwards effective_order, the history kwargs (contradictory requests raise), dt_elastic, the hooks, and history_flux.
  • Docs: a section in constitutive-models.md; EXPONENTIAL_VE_INTEGRATOR.md.

A single-material run is bit-identical to the base.

Tests

tests/test_1054_multimaterial_viscoelastic.py (23 tests) uses pure shear with prescribed walls, against closed forms for each material:

  • Identical constituents equal the single material to 5.6e-12.
  • 4:1 Maxwell times; BDF-1 beside ETD-1; BDF-2 beside BDF-1; yield in one constituent; isotropic beside TI.
  • A constituent change reaches the solver.
  • Reassignment keeps each constituent's order.
  • A refused composite is not assigned.
  • Dimensional constituents take their scaled Maxwell times.
  • Viscous beside VEP (characterisation, tier_b).
  • The two single-material regressions above.

The errors elsewhere are the projected history smearing the stress jump at the interface, 2e-4 to 8e-3. The tolerances are about three times those values, and the measurements are in the module docstring. On the base, 15 of the first 18 tests fail.

Run: test_1054 serial and np2 (23/23); the VE and constitutive tier a/b subset (test_005*, 0017, 0019, 0070, 0610, 0857, test_10*): 874 passed. Two failures, both on the base too: test_1079 (#848), and test_0017 after test_1057 (its plugin TensorTransport stays in the family registry, the same order dependence as test_0016).

Review

Two internal adversarial review rounds, both posted below. The second found a units bug in the base PR (#851's ETD read parameter magnitudes, not scaled values), which is now fixed there (189aa88) and merged here.

Checklist (UW3 Style Charter)

  • I have read the Style Charter and followed it over the surrounding code (S2)
  • No drive-by refactors, renames, or "while I was here" cleanups (S9)
  • Bug fixes ship the regression test, written first, with level_* and tier_* markers (S8)
  • No hedging names and no commented-out code (S3, S4)
  • Every exception swallow states its sanctioned failure mode in a comment (S4)
  • New data access uses .array / mesh.X.coords (S7)
  • Parallel safety considered: test_1054 on 2 ranks (S11)
  • No dependency changes
  • AI-assisted work carries the attribution line below

Underworld development team with AI support from Claude Code

lmoresi and others added 9 commits October 13, 2026 11:17
test_1079's `_checkpoint_fields` helper asserted the field's saved vector
is at `uw_checkpoint/<name>`. #756 moved the reload payload under
/restart/petsc, where the same vector sits one group deeper as
`.../dms/<name>/vecs/<name>/<name>`, so the assertion fails on every
current file and development has been red since #708 merged.

Nothing was broken. By the time that line runs, both the write and the
resume phases have already returned 0 and the saved metadata has already
compared equal -- the restart works. The test is marked
`xfail(strict=True, raises=SymbolicDiskContinuationMismatch)`, so only
that one exception counts as the expected failure and an AssertionError
from the layout check surfaces as a genuine failure, which is why a
cosmetic relocation reads as a broken restart.

The helper now accepts either spelling, so a file written by either
version stays readable to the test. This is the same assumption that
regressed #640 in the reader (fixed in #847): the payload is found by
structure, not by one hard-coded path.

Proven on one build: with the single-spelling assertion restored the test
fails at line 34; with this change it reaches its intended strict xfail.
Checkpoint and snapshot suite: 122 passed, 1 xfailed.

How this reached development: #708 added this test and its CI was green,
but that run predates #756 by a day. #708 then merged twelve hours after
#756 without a re-run, so the layout change and the assertion never met
in CI until afterwards.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E87Q7KrpapxeQiLD1RiNXv
docker-image.yaml matched branch pushes only, so v3.2.0 produced no
version-tagged laptop image and :v3.2.0 was added by hand afterwards.
`tags: ['v*']` makes a release tag build like any other push; the semver
entries already in metadata-action then emit 3.2.0 and 3.2.

The `release: published` trigger in both workflows has never fired —
release.yml creates the Release with GITHUB_TOKEN, which raises no
workflow events. The comment in hpc-uw3-image.yml said the opposite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The exponential integrator's alpha = exp(-dt/tau) and phi lived once on the
DFDt, written by each model's pre-solve hook from its own Maxwell time. Two
models sharing one history (the constituents of a multi-material model)
therefore both relaxed on whichever Maxwell time was written last: 60 %
error in the other material.

Each VEP / TI-VEP / TI-VEP-split model now owns its (alpha, phi)
(_ExponentialCoefficients), created like the DDt's and refreshed with
_update_exp_values from its own Maxwell time, keeping the dt_symbol path for
field Maxwell times; the ETD-1 reduction phi = alpha is applied to the
model's own pair. Nothing in the library reads the DDt's coefficients any
more; they are kept for outside callers of update_exp_coefficients
(TODO(DESIGN) at their allocation).

Single-material results are bit-identical (velocity, history, sampled
stress) for VEP BDF-1/2, ETD-1/2, ETD with a field viscosity, TI-VEP BDF
and ETD, and the TI split model.

Underworld development team with AI support from Claude Code
… viscosity

Before the solver sets a step, dt_elastic is infinite. With a finite
modulus, eta mu dt / (c0 eta + mu dt) then evaluates to inf/inf = nan:

- ViscoElasticPlasticFlowModel.ve_effective_viscosity guarded mu = inf but
  not dt = inf; it now returns the viscosity for either.
- TransverseIsotropicVEPFlowModel: the same guard in ve_effective_viscosity
  and in _eta_for_tensor. Its tensor is cached (_is_setup), and a model
  given its parameters before it meets a solver was built at dt = inf and
  kept the nan tensor once the step became finite; the dt_elastic setter
  now resets the model when the step crosses between infinite and finite.

Regression tests are in tests/test_1054_multimaterial_viscoelastic.py (next
commit). Single-material results with the parameters set after assignment
are unchanged bit for bit.

Underworld development team with AI support from Claude Code
…stituents

MultiMaterialConstitutiveModel took none of the stress-history interface
from its constituents: it reported requires_stress_history = False (so the
solver made no history), had no Parameters.dt_elastic (the VE solve
raised), and never called the constituents' history hooks, so a
viscoelastic constituent kept dt_elastic = inf -- nan (BDF) or a purely
viscous response (ETD), silently.

The composite now forwards: requires_stress_history (any constituent);
order (highest) and effective_order (highest: the solver rebuilds when it
changes, so it must follow the ramp of the highest-order constituent);
stress_history_ddt_kwargs (union; contradictory requests raise);
Parameters.dt_elastic (set on every viscoelastic constituent);
_update_history_coefficients / _update_history_post_solve; history_flux
(level-set sum of each constituent's history_flux or flux, sharing one
weighting helper with flux); Parameters._solver and _solver_is_setup, so a
constituent's parameter change rebuilds the solver's functions.

Mixed integrators and orders are allowed: the history stores the composite
stress, which inside each material is its own stress, and each constituent
reads it through its own scheme and coefficients (its own ETD alpha, phi
since the previous commits). The log-conformation refusal moves into
_validate_model_compatibility, which now states what combines.

tests/test_1054_multimaterial_viscoelastic.py: pure shear, two materials,
closed forms away from the interface -- identical constituents (5.6e-12),
two Maxwell times BDF and ETD (5.6e-4, interface smearing of the projected
history), BDF-1 beside ETD-1, BDF-2 beside BDF-1, VEP beside TI-VEP, yield
in one constituent, a constituent's parameter change between steps; plus
the single-material TI-VEP parameters-before-assignment and infinite-step
regressions. 15 of 18 fail on the base build; all pass serial and on two
ranks. Subsystem doc gains the combination rules.

Underworld development team with AI support from Claude Code
Since each viscoelastic model owns its (alpha, phi), nothing in src,
tests, examples or notebooks read the DDt's _exp_coeffs, _exp_alpha,
_exp_phi or update_exp_coefficients. Remove them, and the with_exp switch
of _init_coefficient_expressions; _create_exp_coefficients now names its
owner as the constitutive model. EXPONENTIAL_VE_INTEGRATOR.md no longer
says the coefficients live on the DDt. (docs/docstrings/review_queue.md
still lists them; it is generated by docstring_sweep.py.)

Single-material results stay bit-identical to the base build (VEP BDF-1/2,
ETD-1/2, ETD with a field viscosity, TI-VEP BDF/ETD, TI split).

Underworld development team with AI support from Claude Code
The solver passes its history's order to a model it is given again
(constitutive_model.order = self._order). The composite forwarded that to
every viscoelastic constituent, so reassigning it, or a VE_Stokes(order=2)
or user history, silently turned a BDF-1 constituent into BDF-2 (error
against its BDF-1 closed form 3e-4 -> 9.2e-2). The setter now leaves the
constituents alone and refuses a history of lower order than a constituent
integrates at.

Tests (test_1054):
- reassigning the composite keeps orders (2, 1) and each closed form;
  fails with the previous forwarding setter.
- the constituent-change test now makes a structural change (a yield
  stress given to B between steps); the previous viscosity change was a
  rampable constant that needs no rebuild. Fails with the rebuild
  forwarding disabled (B [0.976, 1.18] against tau_y = 0.5).
- viscous beside VEP, tolerance from the measured 7.9e-3.
- the TI parameters-before test names its three controls.

constitutive-models.md: the smearing error grows with the stress jump
against the local stress; measured cases stated, including three materials
in thirds (4.1e-2).

Underworld development team with AI support from Claude Code
- The solver sets an explicit history order on a model before assigning it,
  so a composite whose constituent needs a higher-order history is refused
  and the solver keeps the model it had (it was left holding the refused one).
- With #851's scaled Maxwell time merged in, each constituent's ETD
  coefficient comes from its own scaled tau when eta, mu and dt carry units
  (test; the units test from #851 reads alpha from the model, where this
  branch keeps it).
- Stale comments about the history's exponential coefficients removed; the
  guide states how a composite and a single model take the solver's order.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Internal adversarial review: two rounds

Each round had independent reviewers working against a build of the base (#851). Probes are kept in the session scratchpad.

Round 1 (at 45b6d58)

# Finding Severity Done
1 The composite's order setter forwarded the solver's order to every constituent, so a BDF-1 constituent beside a BDF-2 one became BDF-2 on reassignment (error 9.2e-2 against the closed form) should-fix The setter keeps each constituent's order and refuses a history of lower order than a constituent needs (83e8b76); reassignment test
2 The constituent-change test could not fail: the change it made did not alter the compiled flux should-fix Rewritten as a structural change. B is [0.4996, 0.5067] against τ_y = 0.5 with forwarding, and [0.976, 1.18] without, under a tolerance of 0.05 τ_y
3 The stress history's own ETD coefficients were dead once each model owned its α and φ should-fix Removed (a8dd765). No reader is left in src, tests, examples or notebooks, and snapshots never carried them

Round 2 (at 83e8b76)

# Finding Severity Done
1 ETD in a units model: α = 1 − 3e-12 where e^-0.1 was expected (the parameter magnitudes were read in their own units). It is in the base #851 and predates both PRs should-fix (base) Fixed on #851 (189aa88) and merged here. Tests in test_1053 (single model, VEP and TI) and test_1054 (each constituent's own scaled τ)
2 A refused composite stayed assigned: VE_Stokes(order=1) given a BDF-2 constituent raised, but solver._constitutive_model was already the composite should-fix The solver sets an explicit history order on a model before assigning it (petsc_generic_snes_solvers.pyx); test_a_refused_composite_is_not_assigned
3 Stale comments in systems/solvers.py about the history's exponential coefficients should-fix Fixed. inventory.json is generated and is left to regenerate
4 A single model and a composite take the solver's order differently (a single model is raised to the history's order; a composite keeps each constituent's own) nit One line in constitutive-models.md

Attacks that failed (round 2)

  • Order paths: Stokes(order=2) with two BDF-1 constituents; reassignment keeps (2,1); a lower-order history is refused with a clear message.
  • BDF-2 step-ratio guard tripped inside a mix: the BDF-2 material tracks a single-material BDF-2 run within 7.6e-4 relative at every step.
  • NavierStokes with a composite (ETD-1 beside BDF-2): eulerian and semi-Lagrangian agree to 3 figures at the sample points.
  • describe() on an ETD composite names each model's own α.
  • Every new test can fail: checked against the pre-fix code, or by the separation shown above.

Interactions


Generated by Claude Code

ci: build a version-tagged container on the release tag
…assertion-1079

Find the checkpoint vector wherever the writer put it (test_1079)
Picks up the test_1079 checkpoint-layout fix (#848), which was the only
failure in this branch's last CI run.

Underworld development team with AI support from Claude Code
@lmoresi
lmoresi merged commit ce1f05b into bugfix/etd-variable-maxwell-time Oct 23, 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