materials: split J2 from Drucker-Prager, and give plasticity its own elastic tangent - #39
Closed
petlenz wants to merge 7 commits into
Closed
materials: split J2 from Drucker-Prager, and give plasticity its own elastic tangent#39petlenz wants to merge 7 commits into
petlenz wants to merge 7 commits into
Conversation
small_strain_plasticity is parameterised over a yield function so it can serve Drucker-Prager. J2 used none of that generality and paid for all of it: - a yield normal distinct from the flow normal, identical for associative J2 - a "modified" equivalent stress carrying pressure coupling J2 does not have - an apex branch a cylinder cannot reach The cost was not only readability. The general consistent tangent forms C_e : (dN/dsigma) : C_e -- two rank-4 x rank-4 contractions per plastic step. For isotropic elasticity N is deviatoric, so C_e : N = 2G N and N : C_e : N = 3G, and the expression collapses to the standard closed form: C = C_e - (6G^2 dl/sig_eq) IIdev + (4G^2 dl/sig_eq - 4G^2/(3G+H')) N (x) N algebraically identical to what the general path computes, not an approximation of it. old small_strain_plasticity<j2> : 1262.2 ns/step new j2_plasticity : 245.2 ns/step (5.15x) Equivalence over 60 steps on the same path: max |dstress| = 0.000e+00 (bit-identical) max |dalpha| = 0.000e+00 (bit-identical) max |dtangent| = 8.760e-14 (roundoff; different order of operations) All nine pre-existing J2 tests pass unchanged, including the tangent checker. ISOTROPY is not newly assumed. small_strain_plasticity already required it through effective_modulus(G) = 3G in its residual; the closed form states it instead of implying it. The scalar Newton is kept, because hardening is a separate graph node and may be nonlinear. Also fixes a blind spot the refactor exposed. J2TangentTest bounded the whole run by 0.1, because the elastic->plastic transition step is genuinely inexact (a central difference straddling the yield surface averages two tangents). That made it blind to real errors: a 0.5% error in the tangent lands at 2.4e-4 and passed. Fully plastic steps are now bounded at 1e-8 and the crossing step separately at 0.1. Verified: the mutation that passed before now fails.
…gent
compute_tangent formed C_e : (dN/dsigma) : C_e explicitly -- two rank-4 x
rank-4 contractions per plastic step, and the dominant cost in both
Drucker-Prager and the RK integrators.
Every flow normal here is a deviatoric term plus a constant volumetric one, so
dN/dsigma is deviatoric in BOTH index pairs. For isotropic C_e that gives
C_e : X = 2G X and X : C_e = 2G X, so the whole expression is
C_e : (dN/dsigma) : C_e == 4 G^2 (dN/dsigma)
Verified against the explicit form at 2.5e-16 for the J2 and Drucker-Prager
derivatives independently, before changing any material.
Isotropy is not newly assumed: effective_modulus() already required it, as 3G
for J2 and G + K*eta*beta for Drucker-Prager.
Drucker-Prager: 1178.9 -> 761.3 ns/step (1.55x)
Bit-identical output, to all 17 digits, on a 20-step path:
before: alpha=0.042067012632805115 s00=108.98966076999213 C0000=251.4917329876391
after : alpha=0.042067012632805115 s00=108.98966076999213 C0000=251.4917329876391
rk_plasticity shares compute_tangent and gets the same reduction; its three
tests pass unchanged. The DP tangent is independently checked against a
numerical derivative on six load paths including the apex branch, which is what
would catch the collapse being wrong rather than merely faster.
…Prager
After J2 moved out, small_strain_plasticity<Traits, YieldFunction> had exactly
one instantiation. A template parameter with one argument is not generality, it
is indirection, and it cost:
- a has_apex_return concept plus three if constexpr / requires sites, guarding
a branch the only remaining user always has;
- a yield function passed as a C++ OBJECT through a "yield_function"
parameter.
That second one was not just noise. The JSON reader has no converter for the
object, so Drucker-Prager could not be configured from a document at all -- the
blocker behind #33, where it had to stay unregistered because a document naming
it would silently get a default-constructed cone (eta = beta = k = 0), which
builds, runs, never yields, and looks like elasticity.
eta, beta and K_bulk are now ordinary scalar parameters. Verified: a
Drucker-Prager model built entirely from a JSON document reproduces the C++
reference bit-for-bit, alpha = 0.042067012632805115 either way.
The apex is now unconditional -- the concept and every if constexpr are gone --
because a cone always has one.
Drucker-Prager: 1178.9 -> 721.9 ns/step over the two commits (1.63x)
Bit-identical to the pre-refactor implementation on a 20-step path, all 17
digits of alpha, stress and tangent.
The yield function survives as an internal member rather than a template
parameter: it holds the verified apex algebra, and rewriting that to save a
file would have traded a real risk for a cosmetic gain.
Profiling the step showed one rank-4 : rank-2 contraction at ~33 ns against
~245 ns for a whole J2 step, and the return map did two of them: once for the
trial stress, once for the returned stress.
The second is redundant:
sigma = C_e : (eps - eps_p_old - dl N) = sig_trial - dl (C_e : N)
and for isotropic C_e, C_e : N = 2G dev(N) + K tr(N) I -- the same identity the
tangent collapse already uses. J2's flow is deviatoric so tr(N) = 0 and it
reduces to 2G N; Drucker-Prager's is not, so the volumetric term stays.
J2 (same-process A/B): 245.2 -> 195.4 ns/step
DP (interleaved A/B): ~8%, medians 1037 -> 953 ns/step
NOT bit-identical this time, unlike the earlier steps: the result agrees to
about 1 ULP (s00 108.7500531151484 vs ...843) because the arithmetic is
algebraically equal but ordered differently. All 50 tests pass, including the
FD tangent checks on six load paths.
The apex return keeps its contraction: there eps_p comes from
apex_plastic_strain rather than dl*N, so the identity does not apply, and the
branch is rare.
Measurement note: absolute ns figures in this branch's earlier commits were
single runs taken at different times, and this machine's load moves them by
30%+. The J2 comparison above is a same-process A/B; the DP one is interleaved
across alternating runs. Ratios are meaningful, absolutes are not comparable
across commits.
Both plasticity classes read the elastic stiffness from an elastic_source's "tangent" property. That looked like flexibility and was not: the closed forms REQUIRE an isotropic C_e -- it is what makes C_e : N = 2G N and N : C_e : N = 3G true -- so accepting an arbitrary rank-4 tangent advertised a generality neither class can honour. It also dragged a linear_elasticity into every plasticity graph, and that material's own "stress" output is not merely unused there, it is WRONG: it is C : eps with eps_p ignored, so once yielding starts it is not the stress of anything. Measured on a 200-step path it over-predicts by a growing margin: step 40 alpha 0.0094 j2 106.25 elastic 107.69 +1.4% step 120 alpha 0.1094 j2 306.25 elastic 323.08 +5.5% step 200 alpha 0.2094 j2 506.25 elastic 538.46 +6.4% A postprocessor logging elastic::stress from a plasticity graph gets that, under a name that reads as authoritative. Both classes now build C_e from moduli they already hold or now take: j2_plasticity gains a "K" parameter; drucker_prager_plasticity needs nothing new, since K_bulk and G were already required for the cone. elastic_source is gone from both, and a plasticity graph no longer contains an elastic material at all. J2: 195.4 -> 168.1 ns/step DP: ~720 -> ~608 ns/step Values agree with the pre-refactor implementation to about 1 ULP on alpha, stress and tangent. linear_elasticity is untouched and keeps its users: for a genuinely elastic model C : eps IS the answer, and isotropic_damage consumes both its stress and its tangent legitimately. rk_plasticity still takes an elastic_source; it is the remaining templated class and is left alone here. An earlier draft of this reasoning proposed dead-property elimination in the property engine, with a declared-outputs mechanism to tell an unread property from one a host reads through ctx.get(). That was solving the symptom. The property should not be in the graph.
Same change as the other two plasticity classes, for the same reason. rk_plasticity read the stiffness from an elastic_source while assuming isotropy twice over: effective_modulus(G) = 3G in its stage residuals, and compute_tangent's C_e : X = 2G X collapse. An arbitrary rank-4 tangent could not have been honoured by either. Takes "K" and builds C_e in the constructor; elastic_source is gone. Bit- identical over a 40-step SDIRK3 path, all 17 digits: before: 0.01062302967272258 108.7500531151484 250.00062497968736 76.554019397938148 after : 0.01062302967272258 108.7500531151484 250.00062497968736 76.554019397938148 That comparison matters here because the suite's rk-vs-j2 equivalence test now has both sides changed; this one holds rk against its own pre-change output. SDIRK3TangentCheck, which compares the tangent against a numerical derivative, is the independent check and passes unchanged. No plasticity material takes an elastic_source now. The one remaining consumer is isotropic_damage, which reads elastic::stress legitimately -- damage scales an elastic stress, and with no plastic strain in that model C : eps IS the stress.
Member
Author
|
Superseded — split into four PRs, because this had grown to seven commits spanning four independent arguments and could not be reviewed as one thing.
Each stacks on the previous, and each was verified independently at 50/50 before pushing. #40 (the scalar-Newton split) is rebased onto #44. The rebuilt stack is tree-identical to this branch — Closing this rather than repointing it, since its branch is the fully-combined form and keeping it open would mean two paths to the same tree. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three plasticity classes served by one templated
small_strain_plasticity, all of them depending on an external elastic material for a stiffness they can only use if it is isotropic. Split into dedicated classes, each owning its own tangent.Every step is verified against the previous implementation — bit-identical except where noted — and all pre-existing tests pass unchanged.
Performance
Two binaries built from the actual commits, run interleaved so each pair sees the same
machine state, 15 pairs, speedup computed per pair:
The range is the honest figure. The same binary measured 1171-2253 ns across runs, so any
single-run comparison on this machine is worth about one significant digit; earlier drafts of
this description quoted 7.5x and 1.9x from sequential runs, which sat at the optimistic end of
these intervals.
Four independent changes, in order:
1. J2 split out. It used none of the yield-function generality and paid for all of it — a yield normal distinct from the flow normal (identical when associative), a "modified" equivalent stress carrying pressure coupling it does not have, an apex branch a cylinder cannot reach.
2. The rank-4 collapse.
compute_tangentformedC_e : (dN/dsigma) : C_eexplicitly — two rank-4 x rank-4 contractions per plastic step. Every flow normal here is deviatoric plus a constant volumetric term, sodN/dsigmais deviatoric in both index pairs, and for isotropicC_e:Verified standalone at 2.5e-16 against the explicit form, for the J2 and DP derivatives independently, before any material was touched.
3. The redundant stress contraction. The return map contracted twice — once for the trial stress, once for the returned stress. The second is
sig_trial - dl (C_e : N), no contraction needed.4. Plasticity owns its elastic tangent. See below.
The elastic dependency was dishonest, and harmful
All three classes read the stiffness from an
elastic_source'stangent. That looked like flexibility and was not: the closed forms require isotropicC_e— it is what makesC_e : N = 2G NandN : C_e : N = 3Gtrue.rk_plasticityassumed it twice, also througheffective_modulus(G) = 3G. A caller wiring an anisotropic tangent would have got silently wrong answers.It also pulled a
linear_elasticityinto every plasticity graph, whose ownstressoutput is not merely unused there but wrong — it isC : epswitheps_pignored, so once yielding starts it is not the stress of anything:A postprocessor logging
elastic::stressfrom a plasticity graph gets that, under a name that reads as authoritative.All three now build
C_efrom moduli they hold.j2_plasticityandrk_plasticitygain aKparameter;drucker_prager_plasticityneeds nothing new, sinceK_bulkandGwere already required for the cone. A plasticity graph no longer contains an elastic material at all.linear_elasticityis untouched and keeps its users — for a genuinely elastic modelC : epsis the answer, andisotropic_damageconsumes both its stress and its tangent legitimately.This also unblocks #33
small_strain_plasticitytook its yield function as a C++ object through ayield_functionparameter, which the JSON reader cannot convert. That is why Drucker-Prager had to stay unregistered in #36 — a document naming it would silently get a default-constructed cone (eta = beta = k = 0) that builds, runs, never yields, and looks like elasticity.eta,beta,K_bulkare now ordinary scalars. A DP model built entirely from JSON reproduces the C++ reference exactly:Verification
max|dstress| = 0,max|dalpha| = 0A test that could not fail, fixed on the way.
J2TangentTestbounded the whole run at0.1, because the elastic->plastic transition step is genuinely inexact — a central difference straddling the yield surface averages two tangents. That made it blind: a 0.5% error in the tangent landed at 2.4e-4 and passed, while real plastic steps sit at 4.4e-10. Now bounded by regime, and the mutation that passed before now fails.Measurement caveat
This machine's load moves absolute timings by 30%+, and I caught myself reporting a 300 ns "regression" that reversed sign under interleaving. The J2 figures are same-process A/B comparisons; the DP ones are interleaved across alternating runs. Ratios are meaningful; absolutes across commits are not.
Not done
drucker_prager_yield_functionsurvives as an internal member rather than a template parameter — it holds the verified apex algebra, and rewriting that to save a file would trade real risk for a cosmetic gain.Note on ordering
This targets
feature/drucker-prager(#10), the root of the stack, so merging it means re-merging forward through nine descendants. Mechanical, but worth deciding alongside #10 rather than separately.