Skip to content

Declared partials for the elastic producers and the damage chain (#219, part 1) - #230

Open
petlenz wants to merge 8 commits into
mainfrom
feature/issue-219-elastic-damage
Open

petlenz wants to merge 8 commits into
mainfrom
feature/issue-219-elastic-damage

Conversation

@petlenz

@petlenz petlenz commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Part of #219 (epic #221): the checklist items Elasticity and stress producers and Damage. Stacked on #227 (partials API, #217), which sits on #223. Merge after #223 and #227.

What / why

The elastic producers and the damage chain declare their partials, so material_point::derivative("…::stress" / "…::damaged_stress", "strain") accumulates the tangent from the graph instead of the hand-written chain rule in isotropic_damage.

material declared partials
linear_stress ∂σ/∂ε = C, ∂σ/∂C = 𝕀ˢ ⊗ ε
isotropic_tangent ∂C/∂K = I ⊗ I, ∂C/∂G = 2 ℙdev
isotropic_moduli ∂K/∂C = ℙvol/3, ∂G/∂C = ℙdev/10, ∂ν/∂C via K and G
incremental_linear_elasticity ∂σ/∂ε = tangent, ∂σ/∂tangent = 𝕀ˢ ⊗ Δε
von_mises_state_function, vector_strain_state_function ∂ε_eq/∂ε (= d_equivalent_strain)
strain_energy_state_function ∂ε_eq/∂ε = (C + Cᵀ):ε / (2E ε_eq) (Cᵀ: major transpose), ∂ε_eq/∂C = ε ⊗ ε / (2E ε_eq)
strain_threshold_yield ∂κ/∂ε_eq = 1 while yielding, else 0
damage laws (power, saturation, Peerlings, exponential, linear; in detail/scalar_damage_law.h) ∂D/∂κ (0 below κ₀ and at the cap)
isotropic_damage ∂σd/∂σ = (1 − D) 𝕀ˢ, ∂σd/∂D = −σ

linear_elasticity and thermal_eigenstrain (also the curing shrinkage) already declare theirs in #227.

All existing outputs stay: tangent, damaged_tangent, d_damage, d_equivalent_strain, and the is_yielding read. Removing them is a follow-up once the hosts read derivatives.

The damaged_tangent, d_damage and d_equivalent_strain callbacks have no partials yet: they remain hand-written outputs, and a derivative request does not go through them.

Decisions

  • isotropic_damage now has two callbacks. One computes damaged_stress from (σ, D) and declares its partials. The other computes damaged_tangent from the old reads. Partials are declared per callback. With a single callback, a derivative of damaged_stress would have to pass through d_equivalent_strain, d_damage and the tangent. Those reads would then need Hessians, or an independent_of() that would be false for damaged_tangent.
  • The damage law declares the true slope ∂D/∂κ, also while unloading. The zero during unloading comes from the yield node (∂κ/∂ε_eq = 0 when not yielding). The law only reads κ, so a finite-difference check of the law alone gives the slope. The issue text puts the zero in the law. Here the product along the chain is the same, and every node passes its own check.
  • neo_hooke and hencky_elasticity are not migrated. Partial derivatives: per-callback declarations and forward accumulation (#217) #227 packs only symmetric tensors: tensor2 is a 6-component Mandel block, and packing projects onto the symmetric part. P and F are not symmetric, so declaring dP/dF would silently give the symmetrised derivative. Also, external_deformation_gradient_source sets inverse and determinant in bind(), without a callback, so they carry no derivative from F. This needs the 9-component layout from the Property engine: partial derivatives per callback, chain rule by forward accumulation #217 table in the engine, which is a follow-up to Property engine: partial derivatives per callback, chain rule by forward accumulation #217.
  • incremental_linear_elasticity::copy_tangent declares nothing, because it would need a rank-8 identity. A derivative through a strain-dependent tangent throws and names that path.
  • isotropic_moduli uses reads(m_C) instead of reads_all_inputs(). C is its only input; the change lets the callback declare partials.
  • Convention at the kink. At ε_eq = κ the yield node declares the left derivative of κ(ε_eq), zero. The accumulated derivative of the damage is zero at a re-evaluation of a committed strain; the right derivative is not reported.
  • test_material_conformance: the finite-difference model now covers every new material. Its state is scaled to strains of about 0.1, so central differences of the nonlinear state functions stay within 1e-6. The header scan also counts materials that include a detail/ base that declares partials (the damage laws).

Tests

Gate from the issue: the accumulated derivative equals the hand-written tangent to 1e-12 (relative) at every step, for loading, unloading and reloading.

  • New test_accumulated_tangents: runs the load cases of the report decks linear_elasticity, incremental_linear_elasticity, isotropic_damage, mean_field_damage (phases at their local strains), short_fibre_damage (all 50 fibre chains, linear and nonlinear localisation), mean_field_composite_global, …_rule_of_mixture and short_fibre_composite. The cases go through zero to compression and back, under strain and stress control, with fewer steps than the reports. short_fibre_damage runs only its uniaxial-strain case: its nonlinear localisation needs small steps.
  • Gates added next to the existing test models: test_damage (the stepper model, and the chain over all three state functions), test_damage_laws (each law against is_yielding · d_damage · d_equivalent_strain, up to the cap), test_mean_field_damage, test_orientation_phases (damaging fibres, both localisations), test_curing_eigenstrain (∂σ/∂ε and ∂σ/∂ξ = −C:βI), test_uncovered_materials (state functions) and test_incremental_elasticity.
  • TDD: before the partials, every derivative request threw and named the path, e.g. stepper::strain -> state_func::equivalent_strain -> state_func::d_equivalent_strain -> damaged::damaged_stress.

No accumulated tangent differed from the hand-written one.

Review fixes (second commit):

  • strain_energy_state_function used C:ε/(E ε_eq) for ∂ε_eq/∂ε, which is only right for a major-symmetric C. It now uses (C + Cᵀ):ε / (2E ε_eq), for the declared partial and for d_equivalent_strain. Test with a minor-symmetric, major-asymmetric tangent against central differences.
  • The finite-difference conformance test also runs while unloading (κ_old > ε_eq) and at the damage cap (just below it, where the law still has its slope, and above it).
  • Kink test for all five laws: after a commit, a re-evaluation at the same strain gives the left derivative, zero.
  • The damage chain over each state function, the five laws and the state functions are now also compared with central differences of the stress, damage or equivalent strain, not only with the material's own hand-written outputs. The accumulated-vs-hand-written tests of the composites remain chain-consistency checks; every node in them is finite-difference checked by test_material_conformance.

Gate

GCC 14 (-DNUMSIM_PREFER_SIBLINGS=OFF, reports on): BUILD=0, 1600/1600 ctest tests passed (incl. ASan variants and report decks), 0 warnings. The first commit passed 1540 before the merge of #227 (1592 after).

…on (#217)

Callbacks declare partials() / independent_of() next to reads(); the engine
plans a requested derivative over the target's upstream and accumulates the
Mandel-packed partials. Hosts request derivatives via material_context and
material_point. check_partials() verifies declared partials by central
differences. thermal_eigenstrain and linear_elasticity declare their partials,
giving the first DDSDDT.
linear_stress, isotropic_tangent, isotropic_moduli, incremental_linear_elasticity,
the state functions, strain_threshold_yield, the scalar damage laws and
isotropic_damage declare their partials; isotropic_damage splits the damaged
stress from the damaged tangent so the stress carries a derivative. The
accumulated derivative equals the hand-written tangents to 1e-12 along the
report load paths and the existing test models, loading and unloading. All
existing tangent outputs stay.
With the partials requested, a rerun at a perturbed read refilled the blocks, so a later partial of the same callback was compared at the perturbed state. Found while building #218.
…, fill at once

check_partials perturbs every read declared independent_of() and requires the
outputs of the callback to stay put; its step follows the read block, its
roundoff allowance is bounded by the analytic block, and the reads and outputs
are restored when a perturbed rerun throws.

derivative<R> accepts only the scalar or a tensor of the context's dimension,
refuses calls from a callback, builds its message on the error path only, and
on an evaluated context reruns the callbacks of the new plan so the result is
filled at once. has_path() tells independence from a typo.
@petlenz

petlenz commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Merged feature/issue-217 (#227 with its review fixes, and main with #222 and #223) into this branch.

One conflict, in tests/test_material_conformance.cpp:

Gate: g++-14 -O1, 1592 tests, 0 warnings.

Merge order unchanged: after #227. The review fixes for this PR follow in a separate commit.

…ifference checks of the damage partials

strain_energy_state_function: d eps_eq / d eps = (C + C^T) : eps / (2 E eps_eq) for the
declared partial and d_equivalent_strain. The conformance finite differences also run
while unloading and at the damage cap, the kink at eps_eq = kappa is tested (left
derivative), and the damage chains are compared with central differences besides the
hand-written outputs.
@petlenz

petlenz commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Review fixes in c514322.

  • Strain-energy partial. dε_eq/dε was C:ε/(E ε_eq), right only for a major-symmetric C. It is now (C + Cᵀ):ε / (2E ε_eq) with tmech's major transpose, for the declared partial and for d_equivalent_strain. Test (red first: the directional derivative was 0.980 against 0.987 from central differences, and check_partials reported the mismatch): a minor-symmetric, major-asymmetric tangent against central differences, in four directions.
  • FD conformance at two more states. DeclaredPartialsMatchFiniteDifferencesWhileUnloading (κ_old > ε_eq, damage kept) and …AtTheDamageCap (ε_eq just below the cap, where the law still has its slope, and above it, where the slope is zero). The model takes the damage law and κ_f as arguments.
  • Kink test. DamageLaws.AtTheKinkTheAccumulatedDerivativeIsTheLeftOne, for all five laws: after a commit, a re-evaluation at the same strain puts ε_eq exactly on κ; the accumulated derivative is zero, equal to the left difference quotient, while the right one is positive. The convention is a one-line comment in strain_threshold_yield. This test was green at once (the code already took the left derivative); it pins the convention.
  • Circular tests. The damage chain over each state function, the five damage laws and the state functions are now also compared with central differences, taken at the committed history and skipped at kinks (decided by the one-sided quotients). The composite tests (mean-field, fibres, report decks) still compare the accumulation with the hand-written tangents; they check the chain, and each node in them is finite-difference checked in test_material_conformance. I did not add finite differences to those.
  • PR body. States that damaged_tangent, d_damage and d_equivalent_strain have no partials yet, the kink convention and the strain-energy formula.

Gate: g++-14 -O1, 1600 tests, 0 warnings.

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