Skip to content

Damage laws: power, saturation and Peerlings; isotropic_damage report - #138

Merged
petlenz merged 47 commits into
mainfrom
feature/damage-laws
Oct 27, 2026
Merged

petlenz merged 47 commits into
mainfrom
feature/damage-laws

Conversation

@petlenz

@petlenz petlenz commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Closes #119. Part of the uvwmaterial port (#94); stacked on #137.

Of uvwmaterial's propagation laws, linear and damage_exponential2 already exist (linear_damage_law, exponential_damage_law). Added, all reading kappa of a yield_source and producing damage, d_damage:

  • power_damage_law: d = K (κ − κ₀)ᵐ, capped at d_max (0.999).
  • saturation_damage_law: d = K (1 − e^(−m(κ − κ₀))) (exponential_law of the original).
  • peerlings_damage_law: d = 1 − κ₀/κ (1 − α + α e^(β(κ₀ − κ))), capped at d_max (damage_exponential3, without its hard-coded 0.99 factor). damage_exponential1 (1 − e^(β(κ₀ − κ))) is not carried over; it is Peerlings' law without the κ₀/κ factor and was unused.

Tests (test_damage_laws, 6): each law's value and derivative against the formula and against finite differences of the real chain (strain → von Mises state → threshold yield → law), the threshold, the cap and the saturation limit; creation from JSON.

Report docs/materials/isotropic_damage/report.json: the full damage chain with the exponential law as the point and all laws recorded; checks: σ = (1 − d) σ_elastic, κ monotone, no damage below the threshold, every law monotone in κ, Peerlings and saturation against their formulas, and the tangent check. The report point now takes stress_property / tangent_property (damaged_stress, damaged_tangent).

What the report found: the consistent tangent of isotropic_damage is correct (2e-11 against central differences on damaging and elastic steps; the existing test only verified elastic steps), but a central difference straddles the kink wherever the strain state lies exactly on the damage surface — the first threshold step and the load reversals of a symmetric cycle. The tangent check now skips those steps (|ε_eq − κₙ| > 1e-9). To make that possible an include's named entry (check, case, figure) can be replaced by a local entry of the same name instead of being appended.

Review follow-up: the plumbing lives once in the CRTP detail::scalar_damage_law<Derived> — κ from yield_source, kappa_0, d_max (default 0.999, validated in (0, 1]), the threshold, the cap with a zero slope, the two outputs; a law is its parameters(), its own checks and evaluate(κ) → {d, dd}. The two older laws (exponential_damage_law, linear_damage_law) sit on the same base, so the cap is d_max everywhere (their 0.999 was hard-coded) and the derivative is zero at it. Parameters are validated: power K > 0, m > 0; saturation 0 < K ≤ 1 (K is the final damage — K > 1 gave negative stiffness), m > 0; Peerlings κ₀ > 0, 0 ≤ α ≤ 1, β ≥ 0; exponential κ₀ > 0, β ≥ 0; linear κ_f ≥ κ₀ (equal stays legal: full damage right past the threshold, as the existing test pins). report_spec.h reads name with at(); the report's tangent check excludes loading steps through the recorded yield::is_yielding (report columns now read int flags) and the on-surface steps — threshold step and path reversals, where the central-difference stencil straddles the C⁰ kink — through eq ≠ κ. New tests: every rejection, the cap's zero slope, the older laws on the shared cap.

945/945 tests pass (GCC 14, reports on).

JSON input

New material types: power_damage_law, saturation_damage_law, peerlings_damage_law. A json_model document that loads (the FromJson test of the PR):

{"materials": [
  {"type": "external_strain_source", "name": "strain"},
  {"type": "von_mises_state_function", "name": "state", "strain_source": "strain"},
  {"type": "strain_threshold_yield", "name": "yield", "state_source": "state", "kappa_critical": 0.002},
  {"type": "power_damage_law", "name": "pow", "yield_source": "yield", "K": 200.0, "m": 1.5, "kappa_0": 0.002},
  {"type": "saturation_damage_law", "name": "sat", "yield_source": "yield", "K": 0.9, "m": 300.0, "kappa_0": 0.002},
  {"type": "peerlings_damage_law", "name": "peer", "yield_source": "yield", "kappa_0": 0.002, "alpha": 0.95, "beta": 200.0}
]}

Each law consumes kappa of its yield_source and produces damage, d_damage for isotropic_damage ("damage_source": "pow"). Power: D = K (κ − κ₀)ᵐ; saturation: D = K (1 − exp(−m (κ − κ₀))); Peerlings: D = 1 − κ₀/κ (1 − α + α exp(−β (κ − κ₀))); d_max caps (default 0.999).

@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.

damage laws — needs changes

  • [must] saturation_damage_law.h:113-115 no cap, no check: K > 1 → D > 1 → negative stiffness. Validate 0 < K ≤ 1 or apply d_max.
  • [should] plumbing copied three times (plus twice in existing laws) → CRTP scalar_damage_law<Derived> owning κ, κ₀, d_max, threshold/cap and compute(); Derived::evaluate(x) → {D, dD}.
  • [should] no parameter validation (power m > 0, Peerlings κ₀ > 0, 0 ≤ α ≤ 1, β ≥ 0, saturation m > 0).
  • [nit] 170-char comment; four members per declarator; report_spec.h:21 e["name"] on const json; test comment wrong about von Mises strain; d_damage == 0 at cap untested. Report where exclusion is principled (C⁰ kinks), but a recorded is_yielding predicate would be more robust than a 1e-9 float equality.

@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from 4a060f0 to bd0004d Compare October 17, 2026 14:40
@petlenz
petlenz force-pushed the feature/damage-laws branch from 37b8050 to 747415f Compare October 17, 2026 14:48
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from bd0004d to 6eff511 Compare October 17, 2026 16:05
@petlenz
petlenz force-pushed the feature/damage-laws branch from 747415f to 30ae6c6 Compare October 17, 2026 16:14
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from 6eff511 to 30fbcba Compare October 17, 2026 17:12
@petlenz
petlenz force-pushed the feature/damage-laws branch from 30ae6c6 to ac4fc2a Compare October 17, 2026 17:20
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from 30fbcba to bc568df Compare October 17, 2026 21:36
@petlenz
petlenz force-pushed the feature/damage-laws branch from ac4fc2a to 90bf8c3 Compare October 17, 2026 21:44
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from bc568df to f964442 Compare October 17, 2026 22:46
@petlenz
petlenz force-pushed the feature/damage-laws branch from 90bf8c3 to 80652e8 Compare October 17, 2026 22:46
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from f964442 to a4bd728 Compare October 18, 2026 12:51
@petlenz
petlenz force-pushed the feature/damage-laws branch from 80652e8 to 28d4b5e Compare October 18, 2026 13:01
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from a4bd728 to 4c1aac1 Compare October 18, 2026 13:38
@petlenz
petlenz force-pushed the feature/damage-laws branch 2 times, most recently from 13fd875 to 485fd9b Compare October 18, 2026 18:55
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch 2 times, most recently from 93a893f to 8a07c8c Compare October 18, 2026 20:03
@petlenz
petlenz force-pushed the feature/damage-laws branch from 485fd9b to 44f79fd Compare October 18, 2026 20:03
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from 8a07c8c to e1819c4 Compare October 18, 2026 20:22
@petlenz
petlenz force-pushed the feature/damage-laws branch from 44f79fd to 056f7b1 Compare October 18, 2026 20:22
load_driver runs one material point under mixed strain/stress control
(uniaxial stress solves the five lateral strains with the material tangent)
and records the Newton residuals; material_report runs the load cases of a
case.json and writes CSV; reports/make_report.py turns them into figures and
the results section of docs/materials/<material>/README.md.
The correction solves (P:C:P + Isym - P + Iskw) : d_eps = -P:(sigma - sigma*)
with tmech::invf instead of a hand-written 6x6 elimination; Iskw makes the
operator regular on the full 9x9 space. Strain, stress and tangent are
tensors, converted to and from the evaluator buffers with tensor_conversion.
A report document holds the model graph, the material point, recorded
columns wired to any graph property ("material::property[i,j]"), the load
cases, the figures and the checks. Includes merge shared cases and figures;
"$name" strings take parameters. material_report writes resolved.json,
graph.json (materials, parameters, produced and consumed properties) and
CSV per case; make_report.py evaluates the checks against closed forms,
draws the figures and writes the results with the wiring diagram. Every
report is a ctest (checks only).
C++: load_control (stress/strain projectors), load_path (steps as a view),
report_json (parsing into value types), case_runner (material_point and
run_case over a step_sink concept), csv_sink, graph_description; the tool
is now an entry point. load_driver keeps no per-step state: a step_result
carries its start state, so tangent_error works for any earlier step.
Produced properties are listed by name (the registry orders by address).

Python: dataclasses for runs, cases and check results; expressions, checks,
panel renderers (registry by kind) and Markdown sections as separate
functions; unit tests in reports/test_make_report.py (ctest report_script).
…ution

Spheroidal fibres in an isotropic matrix with the second-order orientation
tensor: Mori-Tanaka with the orientation-averaged dilute concentration, or
the averaged unidirectional stiffness. Eshelby tensors per direction are
cached for the matrix Poisson ratio. Report with aligned, injection-moulded
and random states against the unidirectional composite and isotropy.
core/orientation.h: fit_acg solves a_i = R_D(b_j, b_k, b_i) / 3 with
det b = 1 by Newton in the principal frame of the orientation tensor (any
A, not only diagonal), the aligned case as a single direction, planar
cases by an eigenvalue floor; nodes maps the Lebedev rule through B^-1/2;
average integrates a function of the direction.
… in the fibre frame

Both phases come in as produced properties (tangent, fraction, the fibre's
Eshelby tensor along x1); the composite consumes them, forms the dilute
concentration once in the fibre frame, rotates onto the quadrature directions
with otimesu(R, R) fixed per direction, and produces the tangent and the two
concentrations. The stress is a linear_stress on the tangent. No material_ref.
…history

sigma_{n+1} = sigma_n + C : (eps_{n+1} - eps_n) with the tangent read from
another material, for path-dependent tangents. Report: equals the total
form on every path, a closed path returns to zero, exact for any step.
Drivers: the convergence test also scales with |C| |d eps| (|A| |dF|), so
a hypoelastic stress crossing zero under stress control converges.
eps_th = alpha (T - T_ref) I from a scalar temperature source; linear
elasticity subtracts an optional eigenstrain: sigma = C : (eps - eps_eig).
dz/dt = (k1 + k2 z^m) (1 - z)^n with Arrhenius rate constants, as a rate
law for rk_integrator next to curing_rate (the autocatalytic term alone;
nth order is curing_rate with m = 0).
inclusion takes a fraction_source ("material::property") so the volume
fraction can follow the degree of cure; the chemical shrinkage
beta xi I is thermal_eigenstrain with the cure state as its source.
Three propagation laws of uvwmaterial next to the linear and exponential
ones (damage_exponential2 is the existing exponential law). Report of
isotropic_damage with every law recorded; the report point takes
stress_property and tangent_property; named entries of an include can be
replaced, so the tangent check skips the steps on the damage surface.
@petlenz
petlenz force-pushed the feature/curing-eigenstrain branch from e1819c4 to 9af23f2 Compare October 19, 2026 04:07
@petlenz
petlenz force-pushed the feature/damage-laws branch from 056f7b1 to fc0f56b Compare October 19, 2026 04:07
Main has the reviewed split of this branch (#199-#203), so main's version is
taken everywhere; the branch no longer differs from main.
…ls-and-constants

# Conflicts:
#	tests/CMakeLists.txt
@petlenz
petlenz changed the base branch from feature/curing-eigenstrain to main October 24, 2026 21:16
…eshelby-tensors

# Conflicts:
#	tests/CMakeLists.txt
…v-depolarisation

# Conflicts:
#	tests/CMakeLists.txt
…/rule-of-mixture

# Conflicts:
#	tests/CMakeLists.txt
# Conflicts:
#	include/numsim-materials/report/report_json.h
#	tests/CMakeLists.txt
residual_source, residual_tolerance and max_passes are driver parameters
(material_point's defaults) and keys of the report's "point"; the mean-field
tangent report uses them for the self-consistent fixed point.
…averaging

# Conflicts:
#	tests/CMakeLists.txt
…-composite

# Conflicts:
#	tests/CMakeLists.txt
…-elasticity

# Conflicts:
#	include/numsim-materials/umat/deformation_gradient_driver.h
#	include/numsim-materials/umat/material_point_driver.h
#	tests/CMakeLists.txt
…genstrain

# Conflicts:
#	tests/CMakeLists.txt
# Conflicts:
#	include/numsim-materials/report/report_json.h
#	tests/CMakeLists.txt
#	tests/test_report_case.cpp
The report's "point" names them for materials that publish e.g.
damaged_stress / damaged_tangent (isotropic_damage report).
@petlenz
petlenz marked this pull request as ready for review October 27, 2026 17:50
@petlenz
petlenz merged commit 0db528c into main Oct 27, 2026
2 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.

Damage laws and state functions missing from numsim-materials

1 participant