Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ material_context (facade)
- `.reads()` — the parameters alone; `.reads_all_inputs()` — every input the material has at `finalize()`
- `.stateful()` — keeps state of its own or acts outside its outputs: runs on every update
- `.drives(local inputs...)` — reruns their source callbacks (a solver): runs after what those read
- `converges(residual, tolerance, max_passes)` — a fixed point over local edges: `update()` reruns the callbacks
between the local edge and `residual` until it converges, and throws `cutback_error` after `max_passes`
- Debug checks (CMake option `NUMSIM_MATERIALS_DEBUG_CHECKS`, on except in release configurations) rerun the
constants and throw when one changes (`ctx.verify_constants(bool)`), and throw when a callback reads an input
it did not declare (`ctx.check_reads(bool)`); `ctx.is_constant("mat", "prop")`
Expand Down
6 changes: 3 additions & 3 deletions docs/materials/mean_field_damage/report.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"title": "mean_field_strain and mean_field_composite_local_nonlinear with a damaging matrix",
"include": ["../standard_small_strain.json"],
"notes": "Mori-Tanaka with local strains (incremental: the local strains advance with the concentrations of the current tangents, so after a damage-loading reversal they do not retrace and the composite keeps a residual stress at zero strain, visible against the intact Mori-Tanaka): mean_field_strain produces the phase strains from the composite increment and the concentration tensors of the algorithmic tangents (the matrix phase reads its tangent over a local edge, the previous pass); the damage chain and the stiff spheres are evaluated at those strains; mean_field_composite_local_nonlinear averages them. The evaluator repeats the pass until mt::residual is below tolerance. The published tangent sum c_r C_r : A_r is first order in the strain increment (exact as it vanishes, see test_mean_field_local); across the damage-surface kinks a central difference does not approximate it, so the tangent check is off here.",
"notes": "Mori-Tanaka with local strains (incremental: the local strains advance with the concentrations of the current tangents, so after a damage-loading reversal they do not retrace and the composite keeps a residual stress at zero strain, visible against the intact Mori-Tanaka): mean_field_strain produces the phase strains from the composite increment and the concentration tensors of the algorithmic tangents (the matrix phase reads its tangent over a local edge, the previous pass); the damage chain and the stiff spheres are evaluated at those strains; mean_field_composite_local_nonlinear averages them. update() repeats the pass until mt::residual is below the composite's tolerance (converges()). The published tangent sum c_r C_r : A_r is first order in the strain increment (exact as it vanishes, see test_mean_field_local); across the damage-surface kinks a central difference does not approximate it, so the tangent check is off here.",
"parameters": {"Km": 3333.33, "Gm": 1153.85, "Ki": 166666.67, "Gi": 76923.08, "c": 0.3, "cm": 0.7, "kappa_0": 0.002, "beta": 30.0, "amplitude": 0.008, "tangent_every": 0},
"model": {"materials": [
{"type": "external_strain_source", "name": "strain"},
Expand All @@ -23,14 +23,14 @@
{"type": "exponential_damage_law", "name": "law", "yield_source": "yield", "kappa_0": "$kappa_0", "beta": "$beta"},
{"type": "isotropic_damage", "name": "damage", "elastic_source": "elastic", "damage_source": "law", "state_source": "state", "yield_source": "yield"},
{"type": "linear_stress", "name": "stiff", "tangent_source": "C_i", "strain_source": "mt_strain", "strain_property": "strain_spheres"},
{"type": "mean_field_composite_local_nonlinear", "name": "mt", "phases": [
{"type": "mean_field_composite_local_nonlinear", "name": "mt", "tolerance": 1e-10, "max_passes": 200, "phases": [
{"concentration": "mt_strain::concentration_matrix", "stress": "damage::damaged_stress", "tangent": "damage::damaged_tangent",
"fraction": "matrix::volume_fraction", "used_tangent": "matrix::tangent"},
{"concentration": "mt_strain::concentration_spheres", "stress": "stiff::stress", "tangent": "C_i::tangent", "fraction": "spheres::volume_fraction"}]},
{"type": "phase", "name": "matrix_intact", "tangent_source": "C_m", "fraction_source": "cm_"},
{"type": "mean_field_composite_global", "name": "mt_intact", "phases": ["matrix_intact", "spheres"], "kernel": {"type": "mori_tanaka"}},
{"type": "linear_stress", "name": "intact", "tangent_source": "mt_intact", "strain_source": "strain"}]},
"point": {"strain_source": "strain", "stress_source": "mt", "residual_source": "mt::residual", "residual_tolerance": 1e-10, "max_passes": 200},
"point": {"strain_source": "strain", "stress_source": "mt"},
"record": {"d": "law::damage", "eps_m": "mt_strain::strain_matrix", "eps_i": "mt_strain::strain_spheres", "sig_m": "damage::damaged_stress", "sig_i": "stiff::stress", "res": "mt::residual", "sig_intact": "intact::stress"},
"checks": [
{"name": "local strains partition the composite strain", "cases": "*", "value": "(1 - c) * eps_m_11 + c * eps_i_11", "expected": "strain_11", "tolerance": 1e-10, "relative": true},
Expand Down
9 changes: 4 additions & 5 deletions docs/materials/mean_field_reuss_plastic/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"include": [
"../standard_small_strain.json"
],
"notes": "Equal stress in series through the local pair: the concentrations A_r = C_r^-1 : C_R are formed with the algorithmic tangents of the previous pass, the matrix phase reads its tangent over a local edge, and the point repeats the pass until the residual is small. With a hardening phase the phase stresses agree to first order in the step (checked loosely here at 50 steps; see test_mean_field_damage for the refinement). The published tangent is first order in the step too, so the finite-difference check is off.",
"notes": "Equal stress in series through the local pair: the concentrations A_r = C_r^-1 : C_R are formed with the algorithmic tangents of the previous pass, the matrix phase reads its tangent over a local edge, and update() repeats the pass until the residual is below the composite's tolerance. With a hardening phase the phase stresses agree to first order in the step (checked loosely here at 50 steps; see test_mean_field_damage for the refinement). The published tangent is first order in the step too, so the finite-difference check is off.",
"parameters": {
"Km": 3333.33,
"Gm": 1153.85,
Expand Down Expand Up @@ -121,6 +121,8 @@
{
"type": "mean_field_composite_local_nonlinear",
"name": "mt",
"tolerance": 1e-10,
"max_passes": 200,
"phases": [
{
"concentration": "mt_strain::concentration_matrix",
Expand All @@ -141,10 +143,7 @@
},
"point": {
"strain_source": "strain",
"stress_source": "mt",
"residual_source": "mt::residual",
"residual_tolerance": 1e-10,
"max_passes": 200
"stress_source": "mt"
},
"record": {
"eps_m": "mt_strain::strain_matrix",
Expand Down
6 changes: 3 additions & 3 deletions docs/materials/short_fibre_damage/report.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"title": "orientation_phases: damaging short fibres at their local strains",
"include": ["../standard_small_strain.json"],
"notes": "Short fibres in an elastic matrix, orientation tensor diag(0.7, 0.25, 0.05), Lebedev degree 11: orientation_phases expands into 25 fibre phases (one per direction of the half rule), each with its own damage chain at its own local strain, Mori-Tanaka with an Eshelby spheroid per direction. sf localises with the current (damaged) tangents of the fibres (localisation nonlinear: the phase nodes read them over a local edge, the evaluator repeats the pass until sf::residual is below tolerance); sfl localises with the initial tangents (localisation linear: one pass); intact is short_fibre_composite without damage. Fibre 0 lies along x1, fibres 1 and 2 along x2 and x3. A softening fibre attracts strain under Mori-Tanaka, so the nonlinear localisation damages the fibres along the load earlier; under uniaxial strain it is also the softer composite (the linear localisation averages damaged stresses over strains formed with the intact concentrations, so no ordering holds in general). The published tangent sum c_r C_r : A_r holds the concentrations fixed, first order in the strain increment (see test_orientation_phases), so the central-difference tangent check is off.",
"notes": "Short fibres in an elastic matrix, orientation tensor diag(0.7, 0.25, 0.05), Lebedev degree 11: orientation_phases expands into 25 fibre phases (one per direction of the half rule), each with its own damage chain at its own local strain, Mori-Tanaka with an Eshelby spheroid per direction. sf localises with the current (damaged) tangents of the fibres (localisation nonlinear: the phase nodes read them over a local edge, update() repeats the pass until sf::residual is below the composite's tolerance); sfl localises with the initial tangents (localisation linear: one pass); intact is short_fibre_composite without damage. Fibre 0 lies along x1, fibres 1 and 2 along x2 and x3. A softening fibre attracts strain under Mori-Tanaka, so the nonlinear localisation damages the fibres along the load earlier; under uniaxial strain it is also the softer composite (the linear localisation averages damaged stresses over strains formed with the intact concentrations, so no ordering holds in general). The published tangent sum c_r C_r : A_r holds the concentrations fixed, first order in the strain increment (see test_orientation_phases), so the central-difference tangent check is off.",
"parameters": {"Km": 3333.33, "Gm": 1153.85, "Kf": 166666.67, "Gf": 76923.08, "c": 0.2, "cm": 0.8, "aspect_ratio": 15.0, "kappa_0": 0.002, "beta": 30.0, "amplitude": 0.008, "tangent_every": 0},
"model": {"materials": [
{"type": "external_strain_source", "name": "strain"},
Expand All @@ -14,7 +14,7 @@
{"type": "phase", "name": "matrix", "tangent_source": "C_m", "fraction_source": "cm_"},

{"type": "linear_elasticity", "name": "elastic_m", "strain_producer_name": "sf_strain", "strain_property": "strain_matrix", "K": "$Km", "G": "$Gm"},
{"type": "orientation_phases", "name": "sf", "localisation": "nonlinear", "strain_source": "strain", "matrix": "matrix",
{"type": "orientation_phases", "name": "sf", "localisation": "nonlinear", "tolerance": 1e-10, "max_passes": 200, "strain_source": "strain", "matrix": "matrix",
"matrix_stress": "elastic_m::stress", "fraction_source": "c_", "orientation": [0.7, 0.25, 0.05, 0.0, 0.0, 0.0], "precision": 11,
"kernel": {"type": "mori_tanaka", "eshelby": {"type": "eshelby_spheroid", "aspect_ratio": "$aspect_ratio", "nu_source": "moduli", "nu_property": "poisson_ratio"}},
"phase": {"tangent_source": "fibre_{q}", "tangent_property": "damaged_tangent", "tangent_edge": "local", "initial_tangent_source": "C_f"},
Expand Down Expand Up @@ -44,7 +44,7 @@
{"type": "short_fibre_composite", "name": "intact_tangent", "matrix": "matrix", "fibre": "fibre", "orientation": [0.7, 0.25, 0.05, 0.0, 0.0, 0.0], "precision": 11},
{"type": "linear_stress", "name": "intact", "tangent_source": "intact_tangent", "strain_source": "strain"}
]},
"point": {"strain_source": "strain", "stress_source": "sf", "residual_source": "sf::residual", "residual_tolerance": 1e-10, "max_passes": 200},
"point": {"strain_source": "strain", "stress_source": "sf"},
"record": {"d_along": "fl_0::damage", "d_x2": "fl_1::damage", "d_x3": "fl_2::damage", "dl_along": "lfl_0::damage",
"res": "sf::residual", "sig_lin": "sfl::stress", "sig_intact": "intact::stress"},
"checks": [
Expand Down
26 changes: 17 additions & 9 deletions docs/mean_field.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,23 @@ Under Mori–Tanaka a softening fibre then attracts strain, so it damages more t

## 5. Who runs the fixed point

Local edges make every pass acyclic; the fixed point is reached by repeating `update()`:

- `material_point_evaluator`: `residual_property` (`"material::property"`), `residual_tolerance`, `max_passes`
— the graph is updated again per host call until the residual is at most the tolerance; the UMAT and the
drivers go through it. In a report document: `"point": {..., "residual": "mt::residual", "tolerance": 1e-10,
"max_passes": 200}`.
- A solver's Newton loop re-evaluates the material each iteration; the material's own loop above still runs
so that the returned stress is consistent for the given increment.
- In a test: `while (ctx.get<T>("mt", "residual") > tol) ctx.update();`.
Local edges make every pass acyclic. The engine runs the fixed point inside `update()`:

- `mean_field_composite_local_nonlinear` declares its `residual` with `converges()`. Its parameters are
`tolerance` (default 1e-10) and `max_passes` (default 200).
- The engine reruns the iteration group until the residual is at most the tolerance. The group is the
callbacks from the phase that reads its tangent over the local edge to the composite.
- The callbacks before the group (the strain source, the constant tangents) and after it (recorders, plots)
run once per update.
- After `max_passes` passes, `update()` throws `cutback_error`, and the host cuts the step back.
- Every host converges without further setup: the UMAT, the drivers, the report and a test calling
`ctx.update()`. A solver's Newton loop re-evaluates the material each iteration, so the returned stress is
consistent for the given increment.
- Deprecated: the material point's `residual_source` (`residual_property` of `material_point_evaluator`,
`"point": {"residual_source": ...}` in a report) with `residual_tolerance` and `max_passes` repeats the whole
update. With a converging composite it finds the residual converged after the first update. In a report, set
`"tolerance"` and `"max_passes"` on the composite instead (on the `orientation_phases` entry for a
generated one).

It is a Picard iteration on the tangents — linear convergence, fine for damage with moderate softening; near
snap-back it can stall, which the residual then shows. The self-consistent kernel needs none of this: its own
Expand Down
2 changes: 1 addition & 1 deletion docs/uvwmaterial-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ is a `*_source` parameter. `tools/convert_uvwmaterial_card.py` converts the old
| `eigenstrain_curing_material_base`, `degree_of_cure_dependent_material` | `thermal_eigenstrain` with the cure as source; `inclusion.fraction_source`; `weighted_sum` | | #137 |
| `small_strain_rule_of_mixture_history` (Voigt) | `weighted_sum` with `constant_scalar` fractions: every phase reads the composite strain, the sum is Σ cᵢσᵢ, Σ cᵢCᵢ (`tests/test_voigt_composite.cpp`) | `terms`, weights | #117; not from JSON yet (no reader for `terms`) |
| `small_strain_rule_of_mixture_history` (Reuss) | — | | #117, after the vector Newton lands |
| `small_strain_mean_field_composite_history`, `_plastic`, `_damage` | `mean_field_strain` + the phase materials + `mean_field_composite_local_linear` / `_nonlinear` | the strain node produces `strain_<phase>` (history) and the concentrations; the materials consume them (`strain_source`, `strain_property`); the stress node averages what they produce; a nonlinear phase reads its tangent over a local edge (`tangent_edge`) and the point closes the fixed point with `residual` | #116 |
| `small_strain_mean_field_composite_history`, `_plastic`, `_damage` | `mean_field_strain` + the phase materials + `mean_field_composite_local_linear` / `_nonlinear` | the strain node produces `strain_<phase>` (history) and the concentrations; the materials consume them (`strain_source`, `strain_property`); the stress node averages what they produce; a nonlinear phase reads its tangent over a local edge (`tangent_edge`) and the `_nonlinear` composite closes the fixed point in `update()` (`converges()` on its `residual`) | #116 |
| `small_strain_short_fibre_history_composite` | — | | #118 |
| `*_nonlocal*`, `element_deletion` | — | | #121 |
| `finite_strain_mean_field_composite*` | — | | #122 |
Expand Down
18 changes: 18 additions & 0 deletions docs/writing_a_material.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,24 @@ Without `.stateful()`, a stepper that reads nothing would run once. A recorder o
The engine then runs the solver after everything the driven callbacks read globally (the time step, the temperature), whatever order the deck creates them in. Reads of the solver's own outputs (a function reading the current unknown) add no edge: the solver fills those itself.
- **What a driven callback must be:** a repeatable trial evaluation. Every call computes from the same committed history and its current inputs, and writes outputs only. It must not commit, count or record, because the solver calls it once per iteration.
- **Driving any output of a group:** an output filled by another output's callback reruns that callback through `update_source()`.
- **A fixed point over a local edge:** a node that reads another over a local edge sees the previous pass. A material that measures how far two passes disagree declares that output with `converges()`, with the tolerance and the pass limit as parameters:

```cpp
base::converges(m_residual, base::template get_parameter<value_type>("tolerance"), base::template get_parameter<int>("max_passes"));
```

At `finalize()` the engine forms an iteration group: the callbacks downstream of the consumer of a local edge and upstream of the residual. `update()` runs:
- first, the callbacks the group reads, once;
- then the group, until |residual| ≤ tolerance (a NaN never converges);
- last, the rest, once. A recorder or a plot therefore writes one row per update.

After `max_passes` passes, `update()` throws `cutback_error` and names the residual. `update_property()` on a member runs its upstream once, without iterating. These rules throw at `finalize()` and name the node:
- `converges()` with no local edge behind the residual;
- a `.stateful()` callback inside a group;
- a second residual over the callbacks of a group;
- a tolerance below zero or `max_passes` below one.

`mean_field_composite_local_nonlinear` declares its `residual` this way.
- **`material_ref`:** a material reached through `material_ref` (a `local_newton` solver object) is not an input. The engine:
- treats what the target reads as reads of every callback of the referencing material;
- keeps those callbacks varying unless every output of the target is constant (a target with history, state or a bare output never is);
Expand Down
Loading
Loading