Skip to content

rule_of_mixture: Voigt, Reuss and Hill effective stiffness - #130

Merged
petlenz merged 27 commits into
mainfrom
feature/rule-of-mixture
Oct 27, 2026
Merged

petlenz merged 27 commits into
mainfrom
feature/rule-of-mixture

Conversation

@petlenz

@petlenz petlenz commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Closes #106. Part of the uvwmaterial port (#94); stacked on #129.

phase (introduced here, the phase node of every composite): a phase is a bundle of produced properties, never a material. tangent_source (+ tangent_property), fraction_source (+ fraction_property, default value: a constant_scalar when fixed, "cure", "state" when it evolves — there is no parameter fallback) and, for the mean-field schemes, eshelby_source / depolarisation_source (the nodes of #128/#129, default property tensor). Outputs tangent, volume_fraction and, when wired, eshelby, depolarisation. One callback; the fraction must be non-negative. No material_ref: a composite that consumes a phase consumes its properties and the engine orders everything.

rule_of_mixture is a tangent producer: phases ("phases": ["stiff", "soft"]), kernel voigt (equal strain), reuss (equal stress) or hill (their mean). Outputs tangent and the strain concentration tensors concentration_<phase> with Σ cᵢ Aᵢ = Isym (Voigt: Isym; Reuss: Cᵢ⁻¹ : C_R; Hill: their mean, consistent with the Hill tangent). The stress is a linear_stress on the tangent, as for every tangent producer. The fractions are summed and checked per update, so phases with changing fractions or tangents (cure, damage) work. Replaces small_strain_rule_of_mixture_material and its kernel base.

Tests (test_rule_of_mixture, 8): Voigt is the arithmetic and Reuss the harmonic mean of K and G; Hill is their mean and lies between; a single phase returns its tangent; for every kernel the linear_stress gives C_eff : ε, the concentrations average to Isym and the phase stresses average to the composite stress; bad fractions and kernels throw; creation from JSON; a phase republishes its Eshelby and depolarisation tensors and a bare phase publishes none.

Report docs/materials/rule_of_mixture/report.json: the three kernels in one graph (hill with its linear_stress as the point, Voigt and Reuss recorded), checks against E_V, E_R, E_H and the ordering, the Hill concentrations averaging to one.

Also in this PR: the report generator resolves derived values in dependency order.

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

JSON input

New material types: phase, rule_of_mixture. A json_model document that loads (the FromJson test of the PR):

{"materials": [
  {"type": "external_strain_source", "name": "strain"},
  {"type": "constant_scalar", "name": "Ka", "value": 100.0}, {"type": "constant_scalar", "name": "Ga", "value": 40.0},
  {"type": "constant_scalar", "name": "Kb", "value": 10.0},  {"type": "constant_scalar", "name": "Gb", "value": 3.0},
  {"type": "isotropic_tangent", "name": "Ca", "K_source": "Ka", "G_source": "Ga"},
  {"type": "isotropic_tangent", "name": "Cb", "K_source": "Kb", "G_source": "Gb"},
  {"type": "constant_scalar", "name": "ca", "value": 0.3},
  {"type": "constant_scalar", "name": "cb", "value": 0.7},
  {"type": "phase", "name": "pa", "tangent_source": "Ca", "fraction_source": "ca"},
  {"type": "phase", "name": "pb", "tangent_source": "Cb", "fraction_source": "cb"},
  {"type": "rule_of_mixture", "name": "rom", "phases": ["pa", "pb"], "kernel": "voigt"},
  {"type": "linear_stress", "name": "composite", "tangent_source": "rom", "strain_source": "strain"}
]}

phase: tangent_source/tangent_property, fraction_source/fraction_property, optional eshelby_source/eshelby_property, depolarisation_source/depolarisation_property; outputs tangent, volume_fraction, eshelby, depolarisation. rule_of_mixture: phases, kernel voigt | reuss | hill; outputs tangent, concentration_<phase>.

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

rule_of_mixture — needs changes (minor)

@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch 2 times, most recently from a9613ff to 0050150 Compare October 17, 2026 13:45
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from ed86d1b to 46c99de Compare October 17, 2026 13:45
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from 0050150 to 191fe75 Compare October 17, 2026 15:30
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch 2 times, most recently from cced83c to 7d5a01f Compare October 17, 2026 16:32
@petlenz

petlenz commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

Producer/consumer pass: rule_of_mixture no longer carries a fractions parameter. Its phases are inclusion nodes (now introduced here, below the mean-field PR) that produce tangent and volume_fraction; a fraction is a produced property (constant_scalar when fixed, a cure when it evolves) and the sum-to-one check runs per update on the produced values. The report builds the two phases from c_stiff/c_soft constants.

@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from 191fe75 to dea9c37 Compare October 17, 2026 21:07
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from 7d5a01f to 1a2e21a Compare October 17, 2026 21:07
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from dea9c37 to 1141085 Compare October 17, 2026 22:25
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from 1a2e21a to 13a8bd0 Compare October 17, 2026 22:32
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from 1141085 to daf4cd1 Compare October 18, 2026 11:34
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from 13a8bd0 to f64e188 Compare October 18, 2026 11:42
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from daf4cd1 to 361a892 Compare October 18, 2026 13:38
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from f64e188 to 6528f13 Compare October 18, 2026 13:38
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from 361a892 to 6e9cbdc Compare October 18, 2026 20:03
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from 6528f13 to 5d53585 Compare October 18, 2026 20:03
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from 6e9cbdc to 1d143ff Compare October 18, 2026 20:22
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from 5d53585 to 57ada14 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).
The Markdown pages and committed figures are removed. make_report.py
--out <dir> writes <material>.html with embedded figures and an index;
nothing generated is committed. Each report starts with the parameters of
report.json, where each one is used (material, case or check), the derived
values and the material parameters as resolved. resolve_report records
the substitutions in "parameter_usage".
- core/tensor_identities.h: sym/skew identities, volumetric and deviatoric
  projectors, the symmetric 3D basis in Abaqus order; used by
  linear_elasticity, saint_venant_kirchhoff and plasticity_utils
  (make_IIdev / cached_IIdev now delegate; make_isotropic_tangent keeps
  its K I(x)I form).
- core/property_value.h: value of a plain or history property; used by
  material_point_evaluator::resolve_property and the report columns.
- core/graph_description.h: materials with produced (history flag, sorted)
  and consumed properties; the report adds types and parameters.
- umat/load_control.h and umat/material_point_driver.h (was
  report/load_driver.h): mixed strain/stress control of a material point.
make_report.py is the entry point of reports/numsim_report. Expressions
own the document; Raw marks unescaped cells; ReportSummary replaces the
index tuples; axis labels show tensor components as symbols. Tests per
module in reports/tests (ctest report_script discovers them).
parse_point and parse_columns live in report_json.h; step_record and the
step_sink concept in step_record.h, so csv_sink no longer includes the
model builder. load_path::at checks its range, include nesting is capped
at 16 levels, and the finite-difference step of tangent_error is a driver
option (fd_step).
A plain output "iterations", so reports can record the return-map
convergence of the material next to the driver's own Newton.
A check with "plot" draws value and expected per case next to its error
against the tolerance (relative for relative checks); the separate
closed-form figures are gone. Cases take tangent_every (default 1, 0 =
never); the standard cases check every fifth step. The J2 reports record
solver::iterations and show the return map next to the driver's Newton.
…n_property)

Every small-strain consumer took the strain from the fixed property "strain"
of its source. The composites that follow produce one strain per phase
("strain_matrix", "strain_<phase>"), and the phase materials have to read
those. strain_property (default "strain") names the property next to the
existing strain_source / strain_producer_name, the same way tangent_property
and nu_property do; nothing changes for existing documents.

linear_elasticity, linear_stress, j2_plasticity, drucker_prager_plasticity and
the three state functions; tests wire a material to another's stress under
the new name and check the default and a missing property.
"studies" rerun a case with several step counts; the error of a final
quantity against the finest run is fitted with a power of the step size,
and errors below ten times the driver tolerance count as exact. Cases take
"points" (strain states in order) for non-proportional paths; the load
step carries its segment and fraction. The J2 reports show the radial
return exact on a proportional path and first order on tension followed
by shear.
core/elliptic_integrals.h: R_F, R_C, R_D, R_J by Carlson's duplication
algorithm and the complete integrals K, E, so the Eshelby tensors of
ellipsoids and the orientation averaging need no Boost.
core/elastic_constants.h: K, G, E, nu, lambda conversions, the isotropic
stiffness, rotation of rank-2 and rank-4 tensors, rotations from axis and
angle and onto a direction.
umat/deformation_gradient_driver.h drives a material point by F under
mixed F / P control, Newton on the full nine components with dP/dF;
deformation_control names them (21, 31, 32 included). A report whose point
has a deformation_gradient_source records F and P with nine components,
the Cauchy stress as stress_11..23 and J; tensors in cases may have nine
components. Reports for neo_hooke and saint_venant_kirchhoff with exact
checks (S = F^-1 P under uniaxial stress, simple shear, volumetric).
core/eshelby.h: sphere in closed form from the projectors; elliptic and
circular cylinders along any axis or direction (Mura 11.21); ellipsoids
with any semi-axes from Mura's I-integrals through Carlson's R_D, the
degenerate pairs (spheroid, sphere) from the sum rules, an infinite axis
dispatching to the cylinder. Tests against the closed forms, the limits,
uvwmaterial's spheroid formula and the identity S_iikk = (1 + nu)/(1 - nu).
…ses, projectors of the controls, load path accessors
…n tensor, float-safe rotation_to

The elliptic integrals come from Boost.Math where they are first used (the
Eshelby tensors). rotation() builds [axis]_x as -(epsilon . axis); rotation_to()
uses 8 eps of the scalar type instead of 1e-14, which did not compile for float
and would have been c > 1 there. [[nodiscard]] on the pure helpers.
…per shape

eshelby_sphere, eshelby_cylinder, eshelby_spheroid and eshelby_ellipsoid consume
nu_source / nu_property and publish the output "tensor"; the engine orders and
re-evaluates them with nu. Two scalar producers feed them: poisson_ratio_from_moduli
(K_source, G_source -> value) and isotropic_moduli (tangent_source -> K, G,
poisson_ratio through isotropize, rejecting an anisotropic tangent). Composites
hold a node through eshelby_tensor_interface (tensor() at the wired nu, tensor(nu)
for the self-consistent iterate). core/eshelby.h keeps the closed forms: the
integrals come from Boost.Math (ellint_rd), the tensors are assembled on the
eigenprojectors, equal axes are detected at sqrt(eps).
core/isotropize.h: K = Pvol::C / Dim, G = Pdev::C / (2 (Dim(Dim+1)/2 - 1)),
and the isotropic part built from them. core/lebedev.h with generated
tables (tools/extract_lebedev.py from uvwmaterial's rule, precisions 3 to
23, up to 194 points): spherical means and integrals of tensor-valued
functions of the direction. eshelby::depolarisation for ellipsoids via
Carlson's R_J, the elliptic cylinder in closed form, and the cylinder along
a direction.
Phases are materials providing a tangent, with volume fractions; outputs
the effective tangent, the stress and the strain concentration tensors
(sum c_i A_i = Isym). Report with the closed forms of the three kernels
and their ordering. Report generator: derived values resolve in
dependency order, independent of the key order.
@petlenz
petlenz force-pushed the feature/isotropize-lebedev-depolarisation branch from 1d143ff to 207536a Compare October 19, 2026 04:06
@petlenz
petlenz force-pushed the feature/rule-of-mixture branch from 57ada14 to 6520437 Compare October 19, 2026 04:06
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/isotropize-lebedev-depolarisation 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
@petlenz
petlenz marked this pull request as ready for review October 27, 2026 17:49
@petlenz
petlenz merged commit 8a001af 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.

rule_of_mixture: Voigt, Reuss and Voigt-Reuss-Hill effective stiffness

1 participant