Skip to content

tests: drive vector_newton through a real return map (kinematic hardening) - #55

Closed
petlenz wants to merge 1 commit into
mainfrom
feature/kinematic-hardening
Closed

petlenz wants to merge 1 commit into
mainfrom
feature/kinematic-hardening

Conversation

@petlenz

@petlenz petlenz commented Sep 19, 2026

Copy link
Copy Markdown
Member

First half of #20, which is #15's acceptance criterion deferred by #17: J2 with a tensor back-stress, unknowns {Δγ (scalar), β (symmetric tensor)} solved as one coupled 7×7.

Until now vector_newton had only been driven through synthetic systems with closed-form roots. Nothing had put it through a return map — which matters, because the review round that produced #54 showed how much of its behaviour was unexercised.

The system

With η = s_tr − dev(β), q = σ_eq(η), N = (3/2)η/q:

R_γ = q − 3G·Δγ − σ_y            yield on the RETURNED state, because
                                 σ_eq(s − β) collapses to q − 3G·Δγ exactly
R_β = β − β_n − (2/3)·H·Δγ·N     Prager evolution

Nothing pre-substitutes the radial-return simplification — N is taken from the current iterate, so it genuinely has to be solved coupled rather than decoupling into a scalar equation. The Jacobian is derived rather than differenced:

dq/dB = −N        dN/dB = −((3/2)IIdev − N⊗N)/q

the same shape as j2's existing flow_normal_stress_derivative.

Four tests, and why each exists

Against the closed form, not against itself. For linear kinematic hardening the return is radial in the shifted space, so Δγ = (q_tr − σ_y)/(3G + H) exactly — an independent oracle. The returned state must also sit on the shifted surface.

The elastic branch is not optional, and I found that the hard way. Without it R_γ = q − 3G·Δγ − σ_y still has a root for any trial state, and the solver finds it. Measured on a step with q_tr = 2.10 against σ_y = 50: it converged to Δγ = −0.0148 with a spurious back stress of |β| = 36.3. Negative plastic flow, reported as success. That was a real bug in my model, hit while writing it.

The Bauschinger effect, because it's what separates kinematic from isotropic hardening: in pure shear the elastic span between forward and reverse yield stays 2σ_y/√3 however far the forward loading went — the surface translates instead of expanding.

A finite-difference check of the Jacobian, which exists because the first three cannot catch a Jacobian error. I verified that rather than assuming: flipping the sign of jacobian_g_B leaves all three passing, because Newton converges to the root of the residual and a wrong Jacobian changes only the path. Mutation-verified across every block:

mutation caught
jacobian_g_g value yes
jacobian_g_B sign yes
jacobian_B_g sign yes
jacobian_B_B 1.5 → 2.0 yes

Still open on #20

The consistent tangent through tangent_checker (needs the strain_derivative wiring — dR/dε for both blocks), and the two-surface model. This PR is the first half; #20 stays open.

306/306 pass.

…ning)

#15's acceptance criterion, deferred by #17: J2 with a tensor back-stress,
unknowns {dgamma (scalar), beta (symmetric tensor)} solved as one coupled 7x7.
Until now vector_newton had only ever been driven through synthetic systems
with closed-form roots -- nothing had put it through a return map.

The system, with eta = s_trial - dev(beta), q = sigma_eq(eta), N = (3/2)eta/q:

  R_g = q - 3G*g - sigma_y            yield on the RETURNED state, because
                                      sigma_eq(s - beta) collapses to q - 3G*g
  R_B = beta - beta_n - (2/3)H*g*N    Prager evolution

Nothing pre-substitutes the radial-return simplification: N is taken from the
current iterate, so it really does have to be solved coupled rather than
decoupling into a scalar equation. The jacobian is derived rather than
differenced -- dq/dB = -N and dN/dB = -((3/2)IIdev - N(x)N)/q, the same shape
as j2's existing flow_normal_stress_derivative.

Four tests.

The coupled solve is checked against the CLOSED FORM, not against itself: for
linear kinematic hardening the return is radial in the shifted space, so
g = (q_tr - sigma_y)/(3G + H) exactly. That is an independent oracle. The
returned state also has to sit on the shifted surface,
sigma_eq(dev(sigma) - beta) == sigma_y.

An elastic step must leave the state alone, and the branch that makes that true
is not optional. Without it R_g = q - 3G*g - sigma_y still has a root for any
trial state, and the solver finds it: measured on a step with q_tr = 2.10
against sigma_y = 50, it converged to g = -0.0148 with a spurious back stress
of |beta| = 36.3. Negative plastic flow, reported as success. That is the bug
this test exists for, and I hit it while writing the model rather than
hypothesising it.

The Bauschinger effect is what separates kinematic hardening from isotropic, so
it gets its own test: in pure shear the elastic span between forward and
reverse yield stays 2*sigma_y/sqrt(3) however far the forward loading went,
because the surface translates instead of expanding.

The fourth test exists because the first three cannot catch a jacobian error,
and I checked rather than assumed -- flipping the sign of jacobian_g_B leaves
all three passing. Newton converges to the root of the RESIDUAL; a wrong
jacobian changes only the path. So the last test finite-differences the
residual and compares every block. Mutation-verified across all four:
jacobian_g_g, jacobian_g_B, jacobian_B_g and the 1.5 factor inside
jacobian_B_B each fail it.

Still open on #20: the consistent tangent through tangent_checker, which needs
the strain_derivative wiring, and the two-surface model.

306/306.
@petlenz

petlenz commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

Review findings (design, tmech expressions instead of component loops, modern C++, comments, tests, correctness). Severity: must = wrong result, hang, build break or unreachable feature; should = design, duplication, accuracy; nit = style.

kinematic hardening — needs changes

  • [must] tests/CMakeLists.txt:9 conflicts with main (macro now wraps numsim_add_test with -Werror + ASan twin); rebase, re-run.
  • [should] IIsym/IIdev rebuilt every residual → plasticity_detail::cached_IIdev; back_stress must become stress_like once STATEV: stress-like tensor history without the engineering-shear factor (#63) #91 lands; FD Jacobian loops → tmech::num_diff_central; 19-line class docblock; 1e-30 magic twice; trial_deviator() thrice per call.

Conflicts and landing order (open PRs on main)

@petlenz

petlenz commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Closing in favour of #162, a shipped J2 material with Armstrong-Frederick kinematic hardening on vector_newton.

The review finding that decided it: linear Prager hardening is radial in the shifted space, and from the cold start the first Newton step is the radial return. With max_iter = 1 all four tests here still pass, and the coupling term in J_ββ is multiplied by Δγ = 0 in that step, so removing it changes nothing in any solve (3 residual evaluations either way; only the FD test notices). The solver-side coverage this adds already exists in test_vector_newton.

What carries over to #162: the closed-form Prager test and the dN/dβ derivation. What does not: the Bauschinger loop, the elastic-branch test and the component-wise FD loop, replaced by a directional check.

@petlenz petlenz closed this Oct 21, 2026
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