Skip to content

Viscoelastic constituents in the multi-material model - #854

Open
lmoresi wants to merge 7 commits into
bugfix/etd-variable-maxwell-timefrom
feature/multimaterial-ve
Open

lmoresi wants to merge 7 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

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

This branch has not been deployed

No deployments
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