Skip to content

Decompose the elastic stiffness into its own material so the graph can order (and recompute) it #24

Description

@petlenz

Summary

linear_elasticity owns its tangent and builds it once in the constructor. That is
optimal while K and G are fixed, but it leaves no way to recompute it — and,
more awkwardly, no way to recompute it correctly even if one were added.

The ordering problem

A material reading its own property creates no edge in the property graph. The
engine builds edges from input_dependencies and from material-level input wiring,
so elastic::stress and elastic::tangent both hang off the same single edge from
the strain source, and their relative order falls out of constructor registration
order. Measured:

=== Property execution order ===
  strain_in::strain
  elastic::stress          <- reads m_C
  elastic::tangent         <- writes m_C, too late

Attaching an update callback to tangent — the obvious, idiomatic fix — therefore
produces a stress lagging the constants by one call, silently. Verified: the
order is unchanged with the callback attached.

This is a latent trap beyond this material. Anyone who binds a callback to a
material's second output that its first output reads gets the same staleness, with
no diagnostic.

Proposal

Split the stiffness into its own material. Consuming another material's property
creates a real Global edge, which the topological sort honours:

  stiffness::tangent       <- produced first
  elastic::stress

Same physics, different graph shape, ordering guaranteed by construction rather than
by registration order.

  • isotropic_tangent — params K, G, recompute; output tangent
  • linear_stress — inputs tangent + strain; output stress

recompute

The update callback is bound only when recompute is true. add_output ignores
a null callback, so with recompute=false the property carries no callback at all
and the engine skips it outright — per-call cost is zero, not merely small, matching
linear_elasticity. The tangent is built once in the constructor either way, so it
is valid before the first update() regardless.

recompute = false constants fixed (Abaqus: PROPS cannot vary per name)
recompute = true constants vary per call (CalculiX temperature interpolation)

Defaults to false, matching linear_elasticity. The failure mode of the wrong
setting is worth naming: recompute=false while the constants move leaves a stale
tangent with the stress still correct, so it costs convergence rate rather than
accuracy and produces no diagnostic. A debug assertion catches it, and
RecomputeFalseIgnoresALaterParameterWrite pins the behaviour so it is recorded
rather than discovered.

Why this is additive

Nothing existing changes. linear_elasticity is untouched and remains the better
choice whenever the moduli are fixed — one fewer material in the graph, tangent
computed once. j2_plasticity needs no change either: its elastic_source only
requires some material producing tangent.

Beyond the immediate problem, the generators become interchangeable — anisotropic,
temperature-dependent, damage-degraded stiffness all producing the same property,
with consumers indifferent to which. This is the decomposition isotropic_damage
already uses (state function + yield + damage law + assembler).

Open question

Whether recompute should default to false (matches linear_elasticity; a
CalculiX user who forgets gets a stale tangent) or true (safe by default; everyone
else pays a rank-4 rebuild per integration point until they opt out).

Branch

feature/tangent-generator (89b640b), stacked on feature/material-set-parameter
(#23) — the write-and-retrigger test needs set_parameter, and recompute=true has
no purpose without it. 191/191 tests pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions