Skip to content

Add TransverseIsotropicVEPFlowModel for fault mechanics - #114

Merged
lmoresi merged 9 commits into
developmentfrom
feature/ti-vep-faults
Apr 13, 2026
Merged

Add TransverseIsotropicVEPFlowModel for fault mechanics#114
lmoresi merged 9 commits into
developmentfrom
feature/ti-vep-faults

Conversation

@lmoresi

@lmoresi lmoresi commented Apr 12, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds TransverseIsotropicVEPFlowModel: combines anisotropic viscosity (Muhlhaus-Moresi TI tensor) with viscoelastic stress history and plastic yield resolved on the fault plane
  • Fixes the softmin yield approximation: corrected offset formula so g(0)=1, default delta reduced from 0.5 to 0.1, removed sympy.simplify from TI-VEP tensor construction
  • Technical documentation with mathematical formulation and benchmark figure
  • Example script: 2D shear box with angled embedded fault

Key Features

The TI-VEP model uses a Pythagoras formulation for resolved fault-plane shear that works in both 2D and 3D without constructing explicit tangent vectors. Yield is applied only to fault-plane shear (eta_1), while the bulk response (eta_0) remains pure viscoelastic. The corrected softmin (delta=0.1) reaches within 1-2% of the analytical yield cap across all tested configurations.

Cherry-picked from feature/vep-order2 (commits 77ba637..a42d286) which was developed but never PR'd.

Test plan

  • Constitutive tensor regression tests pass (test_0610)
  • Smoke test: TransverseIsotropicVEPFlowModel instantiates with correct parameters
  • 0-degree benchmark: resolved shear caps at tau_y (ratio > 0.99)
  • 15-degree benchmark: resolved shear caps within 1-2% of tau_y
  • Existing VEP tests unaffected by softmin/delta changes

Underworld development team with AI support from Claude Code

lmoresi added 7 commits April 10, 2026 15:28
New constitutive model combining:
- Rank-4 anisotropic viscosity tensor (η₀ bulk, η₁ fault-plane)
- VE stress history with BDF + bdf_blend
- Yield limiting on fault-plane shear (not full invariant)
- All three yield modes: smooth (default), softmin, min, harmonic

Inherits from TransverseIsotropicFlowModel and adds the VEP
machinery from ViscoElasticPlasticFlowModel (BDF coefficients,
stress history, corrected harmonic yield).

Initial implementation — needs testing with fault-flow workflow.

Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
Both TransverseIsotropicFlowModel and TransverseIsotropicVEPFlowModel
now default the director to [0,...,0,1] (dimension-dependent) instead
of scalar 1, which caused TypeError in tensor construction.

Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
The stress() method used raw η_ve for VE history terms instead of the
yield-limited η₁_eff. This prevented yield from capping stress — the
isotropic VEP uses yield-limited viscosity for both tensor and history
terms. Now the TI-VEP matches the isotropic VEP step-by-step.

Also fix is_elastic, is_viscoplastic, ve_effective_viscosity, and
_plastic_effective_viscosity to use .sym identity checks (is sympy.oo)
instead of object-level comparisons that always returned True/False
regardless of the actual parameter value.

Validated: TI-VEP matches isotropic VEP to 4 d.p. across 15+ steps
with smooth yield mode. Min yield mode diverges (58/80 steps) — smooth
is the correct default.

Underworld development team with AI support from Claude Code
Three coupled fixes for the TransverseIsotropicVEPFlowModel:

1. Yield criterion uses resolved fault-plane shear strain rate
   γ̇ = t·ε̇_eff·n instead of global invariant ε̇_II. This ensures
   yield activates when fault-plane shear exceeds τ_y regardless
   of fault orientation (validated at 15 degrees).

2. Tensor base uses VE effective η₀_ve = η₀·μ·dt/(c₀·η₀+μ·dt)
   instead of raw η₀. This ensures Δ = η₀_ve - η₁_eff = 0 when
   η₁ = η₀ and yield is inactive (no spurious anisotropy).

3. Stress formula simplified to σ = C(η₀_ve, η₁_eff) : ε̇_eff.
   The tensor naturally separates normal (VE, η₀_ve) and shear
   (VEP, η₁_eff) components on the fault plane. No separate
   scalar history terms needed.

Validated: 15 degree rotated fault, η₁=η₀=1, τ_y=0.15.
Resolved fault shear caps at τ_y while normal stress grows as
pure VE. Stable with 1 SNES iteration (smooth yield mode).

Underworld development team with AI support from Claude Code
Replace the 2D-specific tangent vector (t = [n₁, -n₀]) with the
dimension-independent Pythagoras approach:
  T = ε̇_eff · n         (traction on fault)
  ε̇_n = T · n            (normal component)
  |γ̇| = √(|T|² - ε̇_n²)  (in-plane shear magnitude)

This works in any dimension without constructing an explicit tangent
vector (which is not unique in 3D). Verified identical results to
the 2D formulation on the 15° rotated fault test.

Underworld development team with AI support from Claude Code
Three fixes to the softmin yield approximation:

1. Corrected softmin formula so g(0) = 1 exactly. The old formula
   g = (1+f)/2 + sqrt((f-1)^2 + d^2)/2 gives g(0) = 1.06 for d=0.5,
   causing spurious yield correction below onset. New formula subtracts
   the constant offset: g = 1 + softplus(f-1) - softplus(-1).

2. Changed default yield_softness from 0.5 to 0.1. The old value was
   too soft for cases where tau_y is a significant fraction of the
   viscous stress (f_ss < 2), causing 15-50% undershoot of the yield
   cap. With delta=0.1, all tested cases reach within 1-2% of tau_y.

3. Removed sympy.simplify() from TI-VEP _build_c_tensor and
   _build_c_tensor_ve. These caused hangs with sympy.Min expressions
   and are unnecessary (the Mandel conversion is correct without
   simplification, consistent with the fix already applied elsewhere).

Validated: 0-degree and 15-degree fault benchmarks with tau_y = 0.15
and 0.30 all reach within 1-2% of analytical yield cap.

Underworld development team with AI support from Claude Code
Technical document (docs/advanced/vep-transverse-isotropy-faults.md):
- Mathematical formulation from isotropic VEP through TI tensor to
  combined TI-VEP with resolved fault-plane yield criterion
- Pythagoras formulation for dimension-independent resolved shear
- Softmin yield approximation with corrected offset formula
- Guidance on choosing the sharpness parameter delta
- Benchmark results at 0 and 15 degree fault angles

Benchmark figure (docs/advanced/figures/ti_vep_benchmark_final.png):
- 0-degree and 15-degree fault, tau_y = 0.15 and 0.30
- Analytical VE curves with yield cap overlay
- Shows resolved shear capping at tau_y while global stress grows

Example (Ex_TI_VEP_Angled_Fault.py):
- 2D shear box with 15-degree embedded fault
- TransverseIsotropicVEPFlowModel with softmin yield
- Time-stepping with stress monitoring and matplotlib output

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings April 12, 2026 16:25

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.

Pull request overview

Adds a new transverse-isotropic viscoelastic-plastic (TI-VEP) constitutive model intended for fault mechanics workflows, and updates the existing VEP softmin yield regularisation plus related documentation/examples.

Changes:

  • Introduces TransverseIsotropicVEPFlowModel combining TI viscosity tensor, VE stress history (BDF), and fault-plane resolved plastic yielding.
  • Updates the "softmin" yield approximation (offset correction) and reduces the default softness parameter from 0.5 → 0.1.
  • Adds an advanced technical note and a runnable example demonstrating an angled-fault shear box setup.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 6 comments.

File Description
src/underworld3/constitutive_models.py Adjusts VEP softmin yield formulation/defaults; fixes TI director default; adds new TI-VEP constitutive model implementation.
docs/examples/solid_mechanics/intermediate/Ex_TI_VEP_Angled_Fault.py New example script demonstrating TI-VEP in a 2D shear box with an angled embedded fault and stress monitoring.
docs/advanced/vep-transverse-isotropy-faults.md New technical documentation describing formulation, softmin regularisation, and benchmark context for TI-VEP.
docs/advanced/index.md Adds the new advanced TI-VEP documentation page to the docs index.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/underworld3/constitutive_models.py Outdated
Comment on lines +1493 to +1495
import math
offset = (-1 + math.sqrt(1 + delta**2)) / 2
g = 1 + (f - 1 + sympy.sqrt((f - 1)**2 + delta**2)) / 2 - offset

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

math.sqrt() is used to compute the softmin offset. This will raise a TypeError if yield_softness is ever set to a SymPy expression (or other non-float numeric type), and it also makes the formula inconsistent with the rest of the SymPy-based expression construction. Prefer sympy.sqrt(1 + delta**2) (and SymPy rationals) so the expression remains fully symbolic and robust.

Suggested change
import math
offset = (-1 + math.sqrt(1 + delta**2)) / 2
g = 1 + (f - 1 + sympy.sqrt((f - 1)**2 + delta**2)) / 2 - offset
one = sympy.Integer(1)
two = sympy.Integer(2)
offset = (-one + sympy.sqrt(one + delta**2)) / two
g = one + (f - one + sympy.sqrt((f - one)**2 + delta**2)) / two - offset

Copilot uses AI. Check for mistakes.
else:
eta_1_eff = sympy.Min(eta_1_eff, vp_eff)

return inner_self.shear_viscosity_0

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

TransverseIsotropicVEPFlowModel.viscosity computes eta_1_eff (including VE + yield limiting) but then returns shear_viscosity_0 unconditionally. This makes the property misleading/incorrect and breaks downstream uses like plastic_fraction (and any solver/diagnostic code expecting viscosity to reflect the yielded fault-plane response). The return value should be the effective fault-plane viscosity (eta_1_eff) or the method should be renamed/rewritten to match what it returns.

Suggested change
return inner_self.shear_viscosity_0
return eta_1_eff

Copilot uses AI. Check for mistakes.
Comment thread src/underworld3/constitutive_models.py Outdated
Comment on lines +2861 to +2866
def stress_projection(self):
"""VE stress without plastic correction (for history storage)."""
edot = self.grad_u
# Use the full anisotropic tensor but without yield
self._build_c_tensor_ve()
return self._q(edot)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

stress_projection() calls _build_c_tensor_ve() but then returns self._q(edot). _q() always uses self.c (backed by self._c), so this projection will not use the VE-only tensor stored in self._c_ve. As written, _build_c_tensor_ve() has no effect on the returned value. If you need a VE-only projection, either temporarily swap self._c/self._is_setup, or add a helper that contracts a provided tensor (e.g. _q_with_tensor(edot, self._c_ve)).

Suggested change
def stress_projection(self):
"""VE stress without plastic correction (for history storage)."""
edot = self.grad_u
# Use the full anisotropic tensor but without yield
self._build_c_tensor_ve()
return self._q(edot)
def _q_with_tensor(self, edot, c_tensor):
"""Stress from a supplied rank-4 constitutive tensor."""
d = self.dim
edot_mandel = uw.maths.tensor.rank2_to_mandel(edot, d)
c_mandel = uw.maths.tensor.rank4_to_mandel(c_tensor, d)
q_mandel = c_mandel * edot_mandel
return uw.maths.tensor.mandel_to_rank2(q_mandel, d)
def stress_projection(self):
"""VE stress without plastic correction (for history storage)."""
edot = self.grad_u
# Use the full anisotropic tensor but without yield
self._build_c_tensor_ve()
return self._q_with_tensor(edot, self._c_ve)

Copilot uses AI. Check for mistakes.

# Director from fault normal (e.g., fault at 15 degrees from horizontal)
theta = np.radians(15)
cm.Parameters.director = sympy.Matrix([np.sin(theta), np.cos(theta)])

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

The example sets the director (fault normal) as Matrix([sin(theta), cos(theta)]), but this vector is not perpendicular to a fault at angle theta from horizontal (its dot product with [cos(theta), sin(theta)] is nonzero). Use a true normal such as [-sin(theta), cos(theta)] (sign arbitrary) to match the stated geometry.

Suggested change
cm.Parameters.director = sympy.Matrix([np.sin(theta), np.cos(theta)])
cm.Parameters.director = sympy.Matrix([-np.sin(theta), np.cos(theta)])

Copilot uses AI. Check for mistakes.

## Smooth Yield Approximations

The `"softmin"` yield mode (default) uses a smooth approximation to $\min(\eta_{\text{ve}}, \eta_{\text{pl}})$ to avoid the non-differentiable kink that causes problems for the SNES solver. The approximation is:

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

This section states that the "softmin" yield mode is the default, but the implementation defaults to yield_mode = "smooth" (both for ViscoElasticPlasticFlowModel and the new TI-VEP model). Please update the documentation to avoid users assuming they are getting softmin behaviour unless they explicitly set it.

Suggested change
The `"softmin"` yield mode (default) uses a smooth approximation to $\min(\eta_{\text{ve}}, \eta_{\text{pl}})$ to avoid the non-differentiable kink that causes problems for the SNES solver. The approximation is:
The `"softmin"` yield mode uses a smooth approximation to $\min(\eta_{\text{ve}}, \eta_{\text{pl}})$ to avoid the non-differentiable kink that causes problems for the SNES solver. It is **not** the implementation default: both `ViscoElasticPlasticFlowModel` and `TransverseIsotropicVEPFlowModel` default to `yield_mode = "smooth"`, so users should set `"softmin"` explicitly if they want this behaviour. The approximation is:

Copilot uses AI. Check for mistakes.
| 0.1 | ~99% of $\tau_y$ | ~100% | low |
| 0.01 | ~100% | ~100% | moderate |

The default $\delta = 0.1$ is accurate for all cases where the viscous stress exceeds the yield stress by at least 50% ($f_{ss} > 1.5$). For problems where SNES convergence is difficult at yield onset, increase $\delta$ toward 0.3--0.5 as a relaxation parameter. Set it via `cm._yield_softness = 0.1`.

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

The docs recommend setting softness via the private attribute cm._yield_softness. There is a public yield_softness property/setter; the docs should use cm.yield_softness = ... to avoid relying on internal state.

Suggested change
The default $\delta = 0.1$ is accurate for all cases where the viscous stress exceeds the yield stress by at least 50% ($f_{ss} > 1.5$). For problems where SNES convergence is difficult at yield onset, increase $\delta$ toward 0.3--0.5 as a relaxation parameter. Set it via `cm._yield_softness = 0.1`.
The default $\delta = 0.1$ is accurate for all cases where the viscous stress exceeds the yield stress by at least 50% ($f_{ss} > 1.5$). For problems where SNES convergence is difficult at yield onset, increase $\delta$ toward 0.3--0.5 as a relaxation parameter. Set it via `cm.yield_softness = 0.1`.

Copilot uses AI. Check for mistakes.
lmoresi added 2 commits April 12, 2026 10:34
- Fix stress_projection() to use _c_ve tensor directly instead of
  calling _q() which always uses _c (the yield-limited tensor).
  This ensures stress history stores VE-only stress as intended.
- Fix director sign in doc example: [-sin(theta), cos(theta)]
- Clarify that softmin is not the default yield mode (smooth is)
- Use public cm.yield_softness API instead of cm._yield_softness
- Add comment explaining deliberate math.sqrt for float offset

Underworld development team with AI support from Claude Code
Change default from "smooth" to "softmin" in both
ViscoElasticPlasticFlowModel and TransverseIsotropicVEPFlowModel.
The corrected softmin with delta=0.1 is more accurate than smooth
across the full range of yield ratios.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Apr 13, 2026

Copy link
Copy Markdown
Member Author

This is the fault-model / anisotropy but now with yielding included.

@lmoresi
lmoresi merged commit eed2a36 into development Apr 13, 2026
1 check passed
@lmoresi
lmoresi deleted the feature/ti-vep-faults branch April 14, 2026 15:27
lmoresi added a commit that referenced this pull request Apr 21, 2026
Brings in TI-VEP (#114), BdIntegral fix, Nitsche BCs, Gamma_N normalisation,
sympy.simplify removal, nullspace support, and JIT cache refactor.

Conflict in constitutive_models.py resolved: removed stale simplify comment
(simplify call already removed by development).

Underworld development team with AI support from Claude Code
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