Add TransverseIsotropicVEPFlowModel for fault mechanics - #114
Conversation
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
There was a problem hiding this comment.
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
TransverseIsotropicVEPFlowModelcombining 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.
| import math | ||
| offset = (-1 + math.sqrt(1 + delta**2)) / 2 | ||
| g = 1 + (f - 1 + sympy.sqrt((f - 1)**2 + delta**2)) / 2 - offset |
There was a problem hiding this comment.
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.
| 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 |
| else: | ||
| eta_1_eff = sympy.Min(eta_1_eff, vp_eff) | ||
|
|
||
| return inner_self.shear_viscosity_0 |
There was a problem hiding this comment.
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.
| return inner_self.shear_viscosity_0 | |
| return eta_1_eff |
| 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) |
There was a problem hiding this comment.
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)).
| 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) |
|
|
||
| # 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)]) |
There was a problem hiding this comment.
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.
| cm.Parameters.director = sympy.Matrix([np.sin(theta), np.cos(theta)]) | |
| cm.Parameters.director = sympy.Matrix([-np.sin(theta), np.cos(theta)]) |
|
|
||
| ## 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: |
There was a problem hiding this comment.
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.
| 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: |
| | 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`. |
There was a problem hiding this comment.
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.
| 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`. |
- 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
|
This is the fault-model / anisotropy but now with yielding included. |
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
Summary
TransverseIsotropicVEPFlowModel: combines anisotropic viscosity (Muhlhaus-Moresi TI tensor) with viscoelastic stress history and plastic yield resolved on the fault planeKey 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
test_0610)TransverseIsotropicVEPFlowModelinstantiates with correct parametersUnderworld development team with AI support from Claude Code