Skip to content

Commit ec19c9e

Browse files
committed
Add J2 plasticity analytical tangent
Also remove FPE trapping as it causes more problems due to how FPEs are used in some Julia packages.
1 parent e1fc779 commit ec19c9e

File tree

7 files changed

+287
-41
lines changed

7 files changed

+287
-41
lines changed

src/Norma.jl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ end
2121

2222
function run(sim::Simulation)
2323
start_time = time()
24-
if get(sim.params, "enable FPE", false) == true
25-
enable_fpe_traps()
26-
end
2724
evolve(sim)
2825
elapsed_time = time() - start_time
2926
norma_log(0, :done, "Simulation Complete")

src/constitutive.jl

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,8 @@ function _j2_stress(
567567
end
568568

569569
# Consistent algorithmic tangent AA_{iJkL} = ∂P_{iJ}/∂F_{kL} via forward FD.
570-
function _j2_tangent(
570+
# 9 extra stress evaluations; used for benchmarking against the analytical tangent.
571+
function _j2_tangent_fd(
571572
material::J2Plasticity, F::SMatrix{3,3,Float64,9}, state_old::Vector{Float64},
572573
P0::SMatrix{3,3,Float64,9}
573574
)
@@ -589,9 +590,151 @@ function _j2_tangent(
589590
return SArray{Tuple{3,3,3,3},Float64,4,81}(AA)
590591
end
591592

593+
# ---------------------------------------------------------------------------
594+
# Analytical tangent AA_{iJkL} = ∂P_{iJ}/∂F_{kL}.
595+
#
596+
# P = Fᵉ_new⁻ᵀ M_new Fᵖ_new⁻ᵀ, Fᵉ_new = F Fᵖ_new⁻¹.
597+
#
598+
# Approximation: Fᵖ_new is treated as FROZEN when differentiating P with
599+
# respect to F. The consequence:
600+
# • Elastic step (Fᵖ_new = Fᵖ_old, truly constant): result is EXACT.
601+
# • Plastic step (Fᵖ_new depends on F): the term ∂Fᵖ_new/∂F is omitted,
602+
# introducing an error O(Δεᵖ) relative to the full consistent tangent.
603+
# In practice this error is small (~0.1–1 % of the tangent norm) and
604+
# Newton convergence remains rapid.
605+
#
606+
# ∂P/∂F = geometric + material contributions:
607+
#
608+
# Geometric: –(F⁻ᵀ)_{iL} P_{kJ}
609+
# from ∂Fᵉ_new⁻ᵀ/∂F at fixed Fᵖ_new and M_new.
610+
#
611+
# Material: 2 Σ_{ABCD} (Fᵉ_new⁻ᵀ)_{iA} ℂ_{ABCD} (Fᵖ_new⁻ᵀ)_{BJ}
612+
# (Fᵉ_tr)_{kC} (Fᵖ_old⁻¹)_{DL}
613+
# from ∂M_new/∂Cᵉ_tr · ∂Cᵉ_tr/∂F; the factor of 2 comes
614+
# from symmetry of ℂ in (C,D) and ∂Cᵉ_tr/∂F.
615+
# Note: Cᵉ_tr = Fᵉ_trᵀ Fᵉ_tr uses Fᵖ_OLD (fixed), so this
616+
# chain is exact.
617+
#
618+
# ℂ_{ABCD} = ∂M_new/∂Cᵉ_tr assembled in the spectral basis {nᵢ} of Cᵉ_tr:
619+
#
620+
# Material part: Σᵢⱼ [ĉᵢⱼ/(2cⱼ)] (nᵢ⊗nᵢ)_{AB} (nⱼ⊗nⱼ)_{CD}
621+
# Geometric part: Σᵢ≠ⱼ θᵢⱼ (nᵢ⊗nⱼ)_sym_{AB} (nᵢ⊗nⱼ)_sym_{CD}
622+
#
623+
# cᵢ = eigenvalues of Cᵉ_tr, nᵢ = eigenvectors,
624+
# θᵢⱼ = (mᵢ_new − mⱼ_new)/(cᵢ − cⱼ) [L'Hôpital limit when cᵢ ≈ cⱼ].
625+
#
626+
# Principal-space algorithmic moduli ĉᵢⱼ = ∂mᵢ_new/∂εⱼ_tr:
627+
# Elastic: ĉᵢⱼ = λ + 2μ δᵢⱼ
628+
# Plastic: ĉᵢⱼ = (λ + μβ) + 2μ(1 – 3β/2) δᵢⱼ + (2μβ – 4μ²/(3μ+H)) Nᵢ Nⱼ
629+
# β = 2μ Δεᵖ / σvm_tr, Nᵢ = 1.5 mdev_i_tr / σvm_tr.
630+
# ---------------------------------------------------------------------------
631+
function _j2_tangent_analytical(
632+
material::J2Plasticity,
633+
F::SMatrix{3,3,Float64,9},
634+
state_old::Vector{Float64},
635+
P::SMatrix{3,3,Float64,9},
636+
state_new::Vector{Float64},
637+
)
638+
λ = material.λ
639+
μ = material.μ
640+
H = material.H
641+
642+
# Recover kinematics
643+
Fp_old = reshape(state_old[1:9], 3, 3)
644+
eqps_old = state_old[10]
645+
Fp_new = reshape(state_new[1:9], 3, 3)
646+
Δεᵖ = state_new[10] - eqps_old
647+
648+
Fm = Matrix{Float64}(F)
649+
Fp_old_inv = inv(Fp_old)
650+
Fp_new_inv = inv(Fp_new)
651+
Fe_tr = Fm * Fp_old_inv
652+
Fe_new = Fm * Fp_new_inv
653+
Fe_new_inv_T = inv(Fe_new)'
654+
F_inv_T = inv(Fm)'
655+
656+
# Spectral decomposition of trial Ce
657+
Ce_tr = Symmetric(Fe_tr' * Fe_tr)
658+
eig = eigen(Ce_tr)
659+
c = eig.values # principal squared stretches
660+
Q = eig.vectors # columns are eigenvectors
661+
662+
# Trial principal Hencky strains and Mandel stresses
663+
ε_tr = 0.5 .* log.(c)
664+
tr_ε = sum(ε_tr)
665+
m_tr =* tr_ε + 2μ * ε_tr[i] for i in 1:3]
666+
m_dev_tr = m_tr .- sum(m_tr) / 3
667+
σvm_tr = sqrt(1.5) * norm(m_dev_tr)
668+
669+
# Principal-space algorithmic moduli ĉᵢⱼ and updated principal stresses
670+
f_tr = σvm_tr - (material.σy + H * eqps_old)
671+
if f_tr 0.0
672+
# Elastic step
673+
m_new = m_tr
674+
ĉ =+ 2μ * Float64(i == j) for i in 1:3, j in 1:3]
675+
else
676+
# Plastic step
677+
N_pr = 1.5 .* m_dev_tr ./ σvm_tr # principal values of flow direction
678+
m_new = m_tr .- 2μ * Δεᵖ .* N_pr
679+
β = 2μ * Δεᵖ / σvm_tr
680+
A = λ + μ * β
681+
B = 2μ * (1.0 - 1.5β)
682+
Ccoef = 2μ * β - 4μ^2 / (3μ + H)
683+
ĉ = [A + B * Float64(i == j) + Ccoef * N_pr[i] * N_pr[j] for i in 1:3, j in 1:3]
684+
end
685+
686+
# Assemble ℂ_{ABCD} = ∂M_new/∂Ce_tr in the principal basis
687+
CC = zeros(3, 3, 3, 3)
688+
689+
# Material part: Σᵢⱼ [ĉᵢⱼ/(2cⱼ)] (nᵢ⊗nᵢ)_{AB} (nⱼ⊗nⱼ)_{CD}
690+
for α in 1:3, β_idx in 1:3
691+
coeff = ĉ[α, β_idx] / (2c[β_idx])
692+
= Q[:, α]
693+
= Q[:, β_idx]
694+
for A in 1:3, B in 1:3, C in 1:3, D in 1:3
695+
CC[A, B, C, D] += coeff * nα[A] * nα[B] * nβ[C] * nβ[D]
696+
end
697+
end
698+
699+
# Geometric part: Σᵢ≠ⱼ θᵢⱼ (nᵢ⊗nⱼ)_sym_{AB} (nᵢ⊗nⱼ)_sym_{CD}
700+
tol = 1e-10
701+
for α in 1:3, β_idx in 1:3
702+
α == β_idx && continue
703+
= Q[:, α]
704+
= Q[:, β_idx]
705+
Δc = c[α] - c[β_idx]
706+
if abs(Δc) > tol * (c[α] + c[β_idx])
707+
θ = (m_new[α] - m_new[β_idx]) / Δc
708+
else
709+
# L'Hôpital: lim_{cβ→cα} (mα−mβ)/(cα−cβ) = (ĉ_{αα}−ĉ_{βα})/(2cα)
710+
θ = (ĉ[α, α] - ĉ[β_idx, α]) / (2c[α])
711+
end
712+
for A in 1:3, B in 1:3, C in 1:3, D in 1:3
713+
sym_AB = 0.5 * (nα[A] * nβ[B] + nα[B] * nβ[A])
714+
sym_CD = 0.5 * (nα[C] * nβ[D] + nα[D] * nβ[C])
715+
CC[A, B, C, D] += θ * sym_AB * sym_CD
716+
end
717+
end
718+
719+
# Assemble AA_{iJkL} = –(F⁻ᵀ)_{iL} P_{kJ}
720+
# + 2 Σ_{ABCD} (Fe_new⁻ᵀ)_{iA} CC_{ABCD} (Fp_new⁻ᵀ)_{BJ} (Fe_tr)_{kC} (Fp_old⁻¹)_{DL}
721+
# Factor of 2 arises from symmetry of CC in (C,D) and ∂Ce_tr/∂F.
722+
Fp_new_inv_T = Fp_new_inv'
723+
AA = MArray{Tuple{3,3,3,3},Float64}(undef)
724+
for i in 1:3, J in 1:3, k in 1:3, L in 1:3
725+
val = -F_inv_T[i, L] * P[k, J]
726+
for A in 1:3, B in 1:3, C in 1:3, D in 1:3
727+
val += 2 * Fe_new_inv_T[i, A] * CC[A, B, C, D] * Fp_new_inv_T[B, J] *
728+
Fe_tr[k, C] * Fp_old_inv[D, L]
729+
end
730+
AA[i, J, k, L] = val
731+
end
732+
return SArray{Tuple{3,3,3,3},Float64,4,81}(AA)
733+
end
734+
592735
function constitutive(material::J2Plasticity, F::SMatrix{3,3,Float64,9}, state_old::Vector{Float64})
593736
W, P, state_new = _j2_stress(material, F, state_old)
594-
AA = _j2_tangent(material, F, state_old, P)
737+
AA = _j2_tangent_analytical(material, F, state_old, P, state_new)
595738
return W, P, AA, state_new
596739
end
597740

src/utils.jl

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -149,31 +149,6 @@ function configure_logger()
149149
return nothing
150150
end
151151

152-
# Constants for Linux/glibc FPE trap masks
153-
const FE_INVALID = 1 # 0x01
154-
const FE_DIVBYZERO = 4 # 0x04
155-
const FE_OVERFLOW = 8 # 0x08
156-
157-
"""
158-
enable_fpe_traps()
159-
160-
Attempts to enable floating-point exceptions (FPE traps) on supported platforms.
161-
Catches invalid operations, divide-by-zero, and overflow. Only supported on Linux x86_64.
162-
Warns that parser behavior may break due to this setting.
163-
"""
164-
function enable_fpe_traps()
165-
if Sys.islinux() && Sys.ARCH == :x86_64
166-
norma_log(0, :warning, "Enabling FPE traps can break Julia's parser if done too early")
167-
norma_log(0, :warning, "(e.g., before Meta.parse()).")
168-
mask = FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW
169-
ccall((:feenableexcept, "libm.so.6"), Cuint, (Cuint,), mask)
170-
norma_log(0, :info, "Floating-point exceptions enabled (invalid, div-by-zero, overflow)")
171-
else
172-
norma_log(0, :warning, "FPE trap support not available on this platform: $(Sys.KERNEL) $(Sys.ARCH)")
173-
end
174-
return nothing
175-
end
176-
177152
function format_time(seconds::Float64)::String
178153
days = floor(Int, seconds / 86400) # 86400 seconds in a day
179154
seconds %= 86400

test/benchmark-j2-tangent.jl

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Norma: Copyright 2025 National Technology & Engineering Solutions of
2+
# Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS,
3+
# the U.S. Government retains certain rights in this software. This software
4+
# is released under the BSD license detailed in the file license.txt in the
5+
# top-level Norma.jl directory.
6+
7+
# Benchmark: finite-difference vs analytical consistent tangent for J2Plasticity.
8+
#
9+
# For each of two representative states (elastic step, plastic step):
10+
# • Verifies that the analytical and FD tangents agree to within FD truncation error.
11+
# • Times both implementations over N_REPS repetitions and reports the speedup.
12+
#
13+
# Run standalone:
14+
# cd test && julia --project=.. benchmark-j2-tangent.jl
15+
16+
using LinearAlgebra
17+
using Printf
18+
using StaticArrays
19+
using Test
20+
21+
include("../src/Norma.jl")
22+
23+
const N_REPS = 500
24+
25+
# ── helpers ────────────────────────────────────────────────────────────────────
26+
27+
function time_reps(f, args...; n=N_REPS)
28+
f(args...) # warm-up / JIT
29+
t = @elapsed for _ in 1:n
30+
f(args...)
31+
end
32+
return t / n # seconds per call
33+
end
34+
35+
function max_rel_err(A::SArray{Tuple{3,3,3,3}}, B::SArray{Tuple{3,3,3,3}})
36+
return norm(A - B) / max(norm(A), norm(B), 1.0)
37+
end
38+
39+
# ── material ───────────────────────────────────────────────────────────────────
40+
41+
params = Norma.Parameters(
42+
"model" => "j2 plasticity",
43+
"elastic modulus" => 200.0e9,
44+
"Poisson's ratio" => 0.3,
45+
"density" => 7800.0,
46+
"yield stress" => 250.0e6,
47+
"hardening modulus" => 20.0e9,
48+
)
49+
mat = Norma.J2Plasticity(params)
50+
51+
# ── deformation states ─────────────────────────────────────────────────────────
52+
53+
# State 1 – elastic: small uniaxial stretch (well below yield, σy = 250 MPa)
54+
F_el = SMatrix{3,3,Float64,9}(
55+
1.0, 0.0, 0.0,
56+
0.0, 1.0, 0.0,
57+
0.0, 0.0, 1.001,
58+
)
59+
s_el = Norma.initial_state(mat) # Fp = I, eqps = 0
60+
61+
# State 2 – plastic: larger strain, pre-loaded plastic state
62+
F_pre = SMatrix{3,3,Float64,9}(
63+
0.98, 0.02, 0.0,
64+
0.0, 0.97, 0.01,
65+
0.0, 0.0, 1.05,
66+
)
67+
_, _, s_pre = Norma._j2_stress(mat, F_pre, s_el)
68+
69+
F_pl = SMatrix{3,3,Float64,9}(
70+
0.97, 0.03, 0.0,
71+
0.0, 0.96, 0.02,
72+
0.0, 0.0, 1.08,
73+
)
74+
75+
# ── benchmark ──────────────────────────────────────────────────────────────────
76+
77+
println("\n── J2 Plasticity Tangent Benchmark (N_REPS = $N_REPS) ──\n")
78+
@printf(" %-16s %12s %12s %8s %12s\n",
79+
"case", "FD (μs)", "Analytic (μs)", "Speedup", "Rel error")
80+
println(" ", ""^68)
81+
82+
@testset "J2 Tangent: analytical vs FD" begin
83+
for (label, F, s_old) in [
84+
("elastic step", F_el, s_el),
85+
("plastic step", F_pl, s_pre),
86+
]
87+
88+
W, P, s_new = Norma._j2_stress(mat, F, s_old)
89+
90+
AA_fd = Norma._j2_tangent_fd(mat, F, s_old, P)
91+
AA_an = Norma._j2_tangent_analytical(mat, F, s_old, P, s_new)
92+
93+
t_fd = time_reps(Norma._j2_tangent_fd, mat, F, s_old, P)
94+
t_an = time_reps(Norma._j2_tangent_analytical, mat, F, s_old, P, s_new)
95+
96+
speedup = t_fd / t_an
97+
err = max_rel_err(AA_fd, AA_an)
98+
99+
@printf(" %-16s %12.2f %12.2f %7.1fx %12.2e\n",
100+
label, t_fd*1e6, t_an*1e6, speedup, err)
101+
102+
# The analytical tangent uses the frozen-Fᵖ approximation (Fᵖ_new treated
103+
# as constant when differentiating P). For elastic steps this is exact
104+
# (no plastic flow, so frozen-Fᵖ = true tangent); for plastic steps the
105+
# error is O(Δεᵖ) relative to the full consistent tangent.
106+
tol = label == "elastic step" ? 1e-4 : 1e-2
107+
@test err < tol
108+
end
109+
end
110+
println()

test/constitutive.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,34 @@ end
215215
σvm = sqrt(1.5) * norm(σdev)
216216
@test isapprox(σvm, σy; rtol=1e-3)
217217
end
218+
219+
@testset "J2Plasticity FD Tangent" begin
220+
# Verify _j2_tangent_fd agrees with the analytical tangent for both
221+
# elastic and plastic steps. This keeps coverage on the FD helper,
222+
# which is retained as a reference implementation for future models.
223+
params = Norma.Parameters(
224+
"elastic modulus" => 200.0e9, "Poisson's ratio" => 0.3,
225+
"density" => 7800.0, "yield stress" => 250.0e6, "hardening modulus" => 20.0e9,
226+
)
227+
mat = Norma.J2Plasticity(params)
228+
state0 = Norma.initial_state(mat)
229+
230+
# Elastic step
231+
F_el = @SMatrix [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.001]
232+
W, P, state_new = Norma._j2_stress(mat, F_el, state0)
233+
AA_fd = Norma._j2_tangent_fd(mat, F_el, state0, P)
234+
AA_an = Norma._j2_tangent_analytical(mat, F_el, state0, P, state_new)
235+
@test size(AA_fd) == (3, 3, 3, 3)
236+
@test norm(AA_fd - AA_an) / max(norm(AA_fd), norm(AA_an), 1.0) < 1e-4
237+
238+
# Plastic step
239+
F_pre = @SMatrix [0.98 0.02 0.0; 0.0 0.97 0.01; 0.0 0.0 1.05]
240+
_, _, s_pre = Norma._j2_stress(mat, F_pre, state0)
241+
F_pl = @SMatrix [0.97 0.03 0.0; 0.0 0.96 0.02; 0.0 0.0 1.08]
242+
W2, P2, state_new2 = Norma._j2_stress(mat, F_pl, s_pre)
243+
AA_fd2 = Norma._j2_tangent_fd(mat, F_pl, s_pre, P2)
244+
AA_an2 = Norma._j2_tangent_analytical(mat, F_pl, s_pre, P2, state_new2)
245+
@test size(AA_fd2) == (3, 3, 3, 3)
246+
# Frozen-Fᵖ approximation introduces O(Δεᵖ) error on plastic steps
247+
@test norm(AA_fd2 - AA_an2) / max(norm(AA_fd2), norm(AA_an2), 1.0) < 1e-2
248+
end

test/runtests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const indexed_test_files = [
6666
(50, "smoothing.jl"),
6767
(51, "nnopinf-schwarz-overlap-cuboid-hex8.jl"),
6868
(52, "single-static-solid-j2-plasticity.jl"),
69-
(53, "utils.jl"), # Must go last due to FPE traps
69+
(53, "utils.jl"),
7070
]
7171

7272
# Extract test file names

test/utils.jl

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,4 @@ using Logging
6363
end
6464
end
6565

66-
@testset "Enable Fpe Traps" begin
67-
# This is platform-specific and side-effect prone
68-
# So we test only that it runs without error
69-
try
70-
Norma.enable_fpe_traps()
71-
@test true
72-
catch e
73-
@test false
74-
end
75-
end
7666
end

0 commit comments

Comments
 (0)