Skip to content

ETD Maxwell integrator: a viscosity that varies in space keeps its elastic response - #851

Open
lmoresi wants to merge 5 commits into
developmentfrom
bugfix/etd-variable-maxwell-time
Open

lmoresi wants to merge 5 commits into
developmentfrom
bugfix/etd-variable-maxwell-time

Conversation

@lmoresi

@lmoresi lmoresi commented Oct 18, 2026

Copy link
Copy Markdown
Member

The exponential (ETD) Maxwell integrator formed alpha = exp(-dt/tau) and phi from float(viscosity)/float(modulus). A viscosity written as an expression of position or of a field (a damage band, a temperature-dependent law) has no float; the Maxwell time fell to None, which the coefficient update reads as the viscous limit (alpha = phi = 0). The model silently lost its elastic response wherever the viscosity was not a plain number (measured on a box fault model: first-step end velocity 0.007, the viscous value, against 0.061).

What changes:

  • _etd_maxwell_time(eta, mu), used by all three callers (ViscoElasticPlasticFlowModel, TransverseIsotropicVEPFlowModel, TransverseIsotropicVEPSplitFlowModel): a number when both parameters are numbers, otherwise the ratio of the parameters' own expression atoms, so the kernels evaluate it pointwise and the units system scales it.
  • ddt._update_exp_values(..., dt_symbol=...): the symbolic coefficients take the step as the dt_elastic atom, so a new step reaches the kernels as a constant (no recompile, no stale coefficient); phi is -expm1(-x)/x; the Maxwell time and the modulus are floored with uw.maths.functions.vanishing, so a vanishing modulus is the elastic limit as on the numeric path.
  • tests/test_1053_ve_etd_variable_maxwell_time.py (level_2, tier_b): a numerically uniform expression (1 + 1e-12 x) must reproduce the constant to 1e-10 at order 1 and 2, across a changing step, and for the transversely isotropic model; a weak band is more compliant than the uniform block and far from the viscous limit of the same banded model. All five fail on development; the step-change test also fails when the step is frozen into the kernel as a number. 5 passed serial and on mpirun -n 2.

The fix first appeared on feature/tidal-shell (9e97670); it is ported here so it does not wait on that branch, and extended after review.

Internal adversarial review, two rounds (findings and evidence in the comment below). Left open, all predating this branch: a dt_elastic that cannot be read as a number still reads as the viscous limit; ETD's Maxwell time uses the raw shear_viscosity_0, not the yield-modified viscosity; ETD-2 under a step traction gives a velocity that alternates in sign.

Checklist (UW3 Style Charter — docs/developer/UW3_STYLE_CHARTER.md)

  • 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 (maybe_ / try_ / do_) 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 — no with ....access(...), no mesh.data (S7)
  • Parallel safety considered — np2/np4 checked where swarm/mesh/solver dispatch is touched (S11)
  • No pixi.toml / pixi.lock or other dependency changes riding in an unrelated PR
  • AI-assisted work carries the attribution line below

Underworld development team with AI support from Claude Code

…astic response

The exponential coefficients alpha = exp(-dt/tau) and phi were formed from
float(viscosity)/float(modulus). A viscosity written as an expression of
position (a damage band, a temperature-dependent law) raised there and the
Maxwell time fell silently to None, which the coefficient update reads as the
viscous limit: the model lost its elastic response wherever the viscosity was
not a plain number (measured on the box fault model: a first-step end
velocity of 0.007, the viscous value, against 0.061 with a constant).

The coefficients are now formed symbolically when the Maxwell time is an
expression, in the shared helper and at both constitutive-model callers; a
constant viscosity takes the numeric path as before.

Test: tests/test_1093 — a uniform-valued expression must match the constant
it equals, and a weak band must not make the block viscous. Both shown
failing before the fix.

Underworld development team with AI support from Claude Code
…egulariser

The symbolic path floored tau with a literal 1e-300; the Charter's
regulariser is uw.maths.functions.vanishing. The comment records where
phi = (1 - alpha)/x loses digits (x below ~1e-12) and that order 1 does not
use phi.

Underworld development team with AI support from Claude Code
…oured

Review of the ported fix (four findings):

- The symbolic coefficients took the step as a number, frozen into the
  compiled kernel: a change of step left the kernels on an earlier step's
  alpha (Stokes 1-8 %, Navier-Stokes 77 % on a step change). The step now
  enters as the dt_elastic atom, so a new value reaches the kernels as a
  constant.
- The Maxwell time is formed from the parameters' own atoms, so the units
  system scales it: a dimensional modulus with a varying viscosity raised
  SympifyError (alpha now evaluates to exp(-0.125) in a units model).
- One helper, _etd_maxwell_time, for all three callers; the experimental
  TransverseIsotropicVEPSplitFlowModel still had the silent viscous
  fallback.
- The test moves to test_1053 (test_109* is outside every CI batch) and
  covers order 1 and 2, a changing step (fails when the step is frozen as
  a number), the transversely isotropic model, and a weak band against
  the uniform block and the band's own viscous limit; all at 1e-10
  against the constant. All five fail on a build without the fix.

Underworld development team with AI support from Claude Code
Second review round (no blockers): at order 2 phi = (1 - e^-x)/x lost its
digits as x -> 0 and was 0/0 where the modulus vanishes. phi is now
-expm1(-x)/x (printed as C expm1), and the symbolic Maxwell time floors the
modulus with the declared regulariser, so mu -> 0 is the elastic limit as on
the numeric path. Test docstring states intent; a redundant dt_elastic
assignment goes.

Underworld development team with AI support from Claude Code
Copilot AI balanced review requested due to automatic review settings October 18, 2026 01:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lmoresi

lmoresi commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Internal adversarial review — two rounds, against builds with and without the fix.

Round 1 (on the first port):

  • Blocking, fixed: the symbolic coefficients took the step as a number frozen into the kernel; a change of step left alpha on an earlier step (Stokes ETD-1 1–2 %, ETD-2 4–8 %, NS 77 %). Now the dt_elastic atom — step-change cases agree with the constant to ≤ 1.6e-12 (Stokes/NS, ETD-1/2, nodal/Eulerian/integration-point histories), including the exact viscous value at dt = 1e6.
  • Blocking, fixed: the test was outside every CI glob (test_109*); now test_1053, check_test_coverage.py OK (355 files).
  • Fixed: dimensional modulus + varying viscosity raised SympifyError; alpha now evaluates to exp(−0.125) in a units model (0.8824969), quantity vs quantity×(1+1e-12x) ≤ 1.8e-12.
  • Fixed: the experimental TransverseIsotropicVEPSplitFlowModel kept the silent fallback; one helper for the three callers (Charter §5).
  • Fixed: test gaps (order 2, step change, TI) and loose tolerances.

Round 2: no blockers. Nit fixed: phi = 0/0 where mu = 0 and lost digits as x → 0 — now -expm1(-x)/x with the modulus floored by vanishing.

Attacks that failed: constant ↔ expression switches mid-run (identical to the constant run both ways); a mesh-variable viscosity changed between solves (exact viscous scaling); recompiles (none on a step change: 0.5–1.0 s vs 0.4–0.9 s per solve); TI-VEP order 1/2 (≤ 1.2e-13); mpirun -n 2 (5 passed); all five tests fail on development.

Not changed here (predates the branch): an unreadable dt_elastic still reads as the viscous limit; ETD's tau uses the raw shear_viscosity_0; ETD-2 under a step traction alternates in sign.

No open issue covers this bug.


Generated by Claude Code

With units, float(par.sym) is the magnitude in the parameter's own units:
1e21 Pa s / 3.2e7 Pa is a Maxwell time in seconds, against a step of 100 kyr,
so alpha = 1 - 3e-12 and the model was purely elastic. The helpers now read
the non-dimensional value the kernels see (dt/tau = 0.1 in the new test, for
the isotropic and transversely isotropic models). Pre-existing: the base read
the same magnitudes.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Post-review commit 189aa88: ETD coefficients from the scaled parameters

Found while reviewing the stacked multi-material branch. When η, μ and dt carry units, float(par.sym) is the magnitude in each parameter's own units: 1e21 Pa s / 3.2e7 Pa is a Maxwell time in seconds, against a step of 100 kyr. α came out as 1 − 3e-12, so the model was purely elastic. The base read the same magnitudes, so this predates the PR; the symbolic branch, through the JIT constants, was already scaled.

_etd_maxwell_time and _etd_timestep now read the non-dimensional value (unwrap_expression(..., mode="nondimensional")) through one helper, _nondimensional_float. No float(….sym) reads of a parameter remain in constitutive_models.py.

New test: test_dimensional_parameters_give_the_nondimensional_maxwell_time, for the isotropic and TI models, expects α = e^-0.1. It failed before the fix (α = 0.9999999999968) and passes after; test_1053 passes in full (7 tests). The BDF path was already correct with units (test_1064, and the reviewer's probe matched the plain-number run to 2.6e-14).


Generated by Claude Code

lmoresi added a commit that referenced this pull request Oct 18, 2026
- 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

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.

2 participants