Skip to content

Minor-symmetric consistent tangents (#35 follow-up) - #81

Merged
petlenz merged 2 commits into
mainfrom
phase-3b-1-tangent-minor-symmetry
Jun 8, 2026
Merged

Minor-symmetric consistent tangents (#35 follow-up)#81
petlenz merged 2 commits into
mainfrom
phase-3b-1-tangent-minor-symmetry

Conversation

@petlenz

@petlenz petlenz commented Jun 8, 2026

Copy link
Copy Markdown
Member

Minor-symmetric consistent tangents (#35 follow-up)

Resolves the one open MINOR from the PR #80 review (math finding Q3): the emitted consistent tangent lacked minor symmetry.

Problem

∂(2μ ε)/∂ε emitted the non-symmetrized rank-4 identity δ_ik δ_jl (tmech::otimesu(eye,eye)). The contraction C:ε is correct for symmetric ε, but the tangent object lacks minor symmetry C_ijkl = C_ijlk that a Voigt/Mandel FE assembler expects. Root cause: codegen tensor inputs carried no tensor_space, so cas::diff fell back to the bare identity instead of P_sym.

Fix

add_tensor_input now sets a symmetric tensor_space ({Symmetric, AnyTraceTag}) for symmetric-role rank-2 inputs (e.g. roles::Strain). cas::diff then returns the symmetric rank-4 identity, so the tangent emits minor-symmetric:

// dσ/dε for σ = 2μ ε, eps declared roles::Strain:
auto t2 = 0.5 * (tmech::otimesu(tmech::eye<double,3,2>(), tmech::eye<double,3,2>())
                 + tmech::otimesl(tmech::eye<double,3,2>(), tmech::eye<double,3,2>()));
auto t3 = t0 * t2;   // 2μ · ½(I⊗ᵘI + I⊗ˡI) = 2μ I⁴ˢ

Why it's safe / narrow

  • Opt-in via the role. Plain add_tensor_input (roles::Other) stays unconstrained — backward compatible. (New test PlainInputTangentStaysNonSymmetrized locks this.)
  • No CSE/hash ripple. The tensor space is metadata, not part of the leaf hash_value(), so non-derivative outputs and temp numbering are byte-identical. Confirmed: of 209 prior tests, exactly one changed — the minor-symmetry lock test I wrote in PR Phase 3b-1: AlgorithmicTangentPass scaffolding (#35) #80 round 2 specifically to catch this transition. Every other recipe (MOOSE/Integration/Newton using roles::Strain) emits identically.
  • Aligns with TensorSpaceConsistencyPass, which already cross-checks Role.is_symmetric against the tensor space.

Tests

Full suite 210/210 (the round-2 ElasticTangentEmitsNonSymmetrizedIdentityForNow lock is superseded by ElasticTangentIsMinorSymmetric; added PlainInputTangentStaysNonSymmetrized).

🤖 Generated with Claude Code

petlenz added 2 commits June 8, 2026 21:08
@petlenz

petlenz commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Critical review — 2 lenses (correctness/blast-radius + CAS/continuum-mechanics semantics)

Both lenses came back clean — no CRITICAL/MAJOR findings. The three load-bearing claims (space propagates, zero hash/CSE ripple, exactly one prior test changed) were each verified by static tracing and an empirical revert-probe.

Correctness / blast-radius — all claims confirmed

  • Propagation is real and load-bearing. expression_holder::data() mutates the shared leaf node (holder is a shared_ptr), so the stored m_tensor_symbols copy and the returned handle share the space. Decisively, cas::diff's leaf rule reads the space from its argument handle (m_arg), not from the leaf inside the forward expression — so the CAS "2-arg ctors drop space" / move-ctor caveats are irrelevant to this path. Empirically proven: disabling only the set_space block makes ElasticTangentIsMinorSymmetric fail (emission reverts to bare otimesu). Not a no-op passing for the wrong reason.
  • Hash claim confirmed. symbol_base::update_hash_value() combines only m_name; m_tensor_space is never hashed → no CSE/leaf-match shift.
  • Blast radius confirmed. With the change, exactly one prior test flips; the other 201 pass unchanged. MOOSE/standalone roles::Strain emit tests are unaffected (stress is not a derivative).
  • Rank guard correct. rank==2 aligns with the CAS leaf handler, which only synthesizes a projector for a rank-2 argument; rank-4 symmetric inputs are correctly skipped.

CAS / continuum-mechanics semantics — correct

  • AnyTraceTag is the only correct trace tag (the crux). Strain has a non-zero volumetric part, so it must carry no trace constraint. DeviatoricTag/VolumetricTag would map ∂ε/∂ε to P_dev/P_vol and silently project the volumetric (bulk/pressure) response out of the tangent — physically wrong. Verified against classify_space + the diff leaf rule.
  • P_sym = the correct minor-symmetric identity ½(δ_ik δ_jl + δ_il δ_jk), lowered to 0.5*(otimesu+otimesl). ∂(2με)/∂ε = 2μ·P_sym is exact.
  • The forward stress 2με is unchanged (space is hash-invariant metadata; it propagates as a tag through 2μ·ε only to enable the diff, not to rewrite the value).

Two MINOR notes (both independently raised; addressed in 70bd111)

  • The guard is keyed on is_symmetric && rank==2, so it also covers a (rare) roles::Stress input — which is correct (stress is symmetric). Documented the breadth + intent.
  • Documented the AnyTraceTag rationale and that propagation rides on the diff-argument handle (so a future reader isn't misled by the CAS "drops space on copy" caveat). Noted the σ=2με tangent is in fact major-symmetric too (the PR claimed only the load-bearing minor symmetry).

Verification

Full suite 210/210 after the doc clarifications. No code-behavior change in this round — comments only.

Verdict

Semantically and mechanically correct; claims accurate and independently verified. Ready to merge.

@petlenz
petlenz merged commit 2f5bfcc into main Jun 8, 2026
4 checks passed
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