Skip to content

materials: let weighted_sum and isotropic_damage source a tangent separately - #29

Merged
petlenz merged 14 commits into
mainfrom
feature/tangent-source-consumers
Sep 6, 2026
Merged

materials: let weighted_sum and isotropic_damage source a tangent separately#29
petlenz merged 14 commits into
mainfrom
feature/tangent-source-consumers

Conversation

@petlenz

@petlenz petlenz commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #28.

Both materials assumed the material producing a constituent's stress also produces its tangent. That holds for linear_elasticity and stops holding the moment the stiffness is its own material (#28), so both gain an optional override.

weighted_sum: tangent_sources

One entry per term. Positional matching is a trap on its own — a shorter list applies each override to the wrong constituent: both names resolve, wire_inputs() succeeds, and the only symptom is a wrong summed tangent, i.e. degraded Newton convergence while the stresses still converge correctly. Nothing in a log would show it.

So the length must match the term count exactly, and "" keeps a term's own tangent — which is also what lets a non-leading term be overridden alone.

isotropic_damage: tangent_source

Declared optional and checked with contains() at the use site rather than defaulted to "", so unset and deliberately-empty stay distinguishable.

Tests

weighted_sum had no test file at all. It has five now: the mixture rule itself, absent tangent_sources, an override, the empty-entry escape, and the length check in both directions.

Split out of #26.

…arately

Both assumed the material producing a constituent's stress also produces its
tangent. That holds for linear_elasticity and stops holding as soon as the
stiffness is its own material, so both gain an optional override.

weighted_sum takes tangent_sources, one entry per term. Positional matching is
a trap on its own: a shorter list applies each override to the WRONG
constituent, both names resolve, wire_inputs() succeeds, and the only symptom
is a wrong summed tangent -- degraded Newton convergence while the stresses
still converge correctly. So the length must match the term count exactly, and
"" keeps a term's own tangent, which is also what lets a non-leading term be
overridden alone.

isotropic_damage takes tangent_source, declared optional and checked with
contains() at the use site rather than defaulted to "", so unset and
deliberately-empty stay distinguishable.

weighted_sum had no test file at all; it has five now, covering the mixture
rule itself and every branch of tangent_sources including the length check.

@petlenz petlenz left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical review — probed rather than read. No correctness defects found. One dead-code trap and one limitation worth documenting.

What I checked and could not break:

  • The "" is an error, not a fallback claim holds. An empty tangent_source resolves as a material name of "", which fails at wire time like any unknown name. So unset and deliberately-empty really do stay distinguishable, as the comment says.
  • The length check covers both directions, and the "" escape does let a non-leading term be overridden alone — that combination is what makes positional matching safe here.
  • isotropic_damage's contains() at the use site is right: a set_default of "" would have collapsed the two states.

Two notes inline.


Dead code: weighted_sum::update() (line 95, outside the diff)

This override is never called. property_engine::update() dispatches per property — prop->traits().update() — and add_output("stress", &weighted_sum::update_stress) installs those callbacks directly. Nothing invokes the material's virtual update(); only backward_euler and vector_newton route it deliberately, by assigning traits().update = [this]{ this->update(); }.

So today it is harmless duplication. The trap is directional: someone adding a third output and wiring it only into update() — the function that looks like the entry point — gets a property that silently never recomputes. That is the same failure mode as the intra-material ordering hazard this stack exists to fix, arrived at from the other side.

Pre-existing, not introduced here, but this PR is the one touching the file. I would delete it, or keep it and say in a comment that it exists for a solver-driven context and is not the engine's entry point.

// the wrong constituent — both names resolve and the only symptom is a
// wrong summed tangent. Require one entry per term, and let "" mean "this
// term keeps its own", so a non-leading term can be overridden alone.
if (!m_tangent_sources.empty() &&

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limitation worth stating in the doc comment rather than a defect.

The length check closes the case where a short list shifts every override onto the wrong constituent. It cannot close the case where a correct-length list names the wrong material — say tangent_sources = {"", "matA_stiff"} when term 1 is matB. Both names resolve, wire_inputs() succeeds, and the symptom is identical to the bug this check was added for: correct stresses, a wrong summed tangent, degraded Newton convergence and nothing in any log.

That is inherent to decoupling stress from tangent — the whole point of the parameter — so no validation can catch it. But the class comment currently reads as though supplying one entry per term is sufficient for correctness, and it is only sufficient for alignment. One sentence saying the entry must name the material that produced that term's stiffness would earn its place.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both done, no behaviour change.

The dead update() is deleted, with a comment in its place saying why there isn't one — that the engine drives each output through its add_output callback, and that backward_euler/vector_newton only use update() because they route it themselves. Better than silence: the absence is now deliberate rather than looking like an oversight.

The class comment now states the limitation outright — one entry per term buys alignment, not correctness, and no validation can close the wrong-name case because decoupling stress from tangent is the point of the parameter.

…sources cannot check

Two review notes, no behaviour change.

The update() override was never called. property_engine drives each output
through the callback registered with add_output, so nothing invokes a material's
virtual update() unless the material routes it itself, as backward_euler and
vector_newton do. Harmless duplication today, and a trap in one direction: an
output wired only into update() -- the function that looks like the entry
point -- would silently never recompute.

The class comment read as though one entry per term were sufficient for
correctness. It is sufficient for ALIGNMENT. A correct-length list naming the
wrong material wires, runs, and produces the same symptom the length check was
added to prevent: correct stresses, a wrong summed tangent, degraded Newton
convergence and nothing in any log. No validation can catch it, since
decoupling stress from tangent is the point of the parameter -- so it is said
outright instead.
@petlenz

petlenz commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Second review: no findings. Re-probed the earlier ones and both are closed — the split messages now carry both counts and the offending slot, and the weighted_sum/isotropic_damage notes are documentation-only so there was nothing behavioural to re-verify beyond the suite (194/194).

Findings this round are on #30 (one low) and #31 (one medium).

@petlenz

petlenz commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Third review pass, across all open PRs. No new findings here — everything from the previous two rounds is closed and stays closed.

Re-verified one path I had stopped exercising: with Eigen and nlohmann found installed rather than fetched, configure/build/test are clean end to end. Recent rounds only tested the fetch path, which is what CI does but not what a developer machine does. Both now pass.

Findings this round are on #10 (high: the Drucker-Prager apex return is reached by no test) and #17 (medium: zero_blocks is validated by name, never by value). There is also a pre-existing packaging defect affecting every PR — the installed package cannot be consumed, because the exported target set references numsim-core::numsim-core, which is fetched and never exported. Reproduced on main, so it predates this stack; detail on #10.

@petlenz

petlenz commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Critical review. No defects — but the feature has no consumer, which is worth deciding deliberately.

tangent_sources (weighted_sum) and tangent_source (isotropic_damage) are declared, tested, and used by nothing else in the repository. Grepped across the tip of the stack: outside weighted_sum.h itself, no occurrence.

The rationale was real — once #28 decomposes the elastic stiffness, a constituent's stress and tangent have different owners — but no model in the repo is built that way yet. That makes this speculative generality: correct, tested, and answering a question nobody has asked.

The counter-argument, which I think wins: this PR is also the first test file weighted_sum has ever had. Five tests covering the mixture rule itself, not just the new parameter. Closing the PR discards those too.

So the options are:

  1. Merge as-is — accept a small unused parameter to keep the tests.
  2. Split — keep the tests, drop tangent_sources until something needs it. Costs a rebase; leaves weighted_sum covered and the API unchanged.
  3. Close — loses both.

I would take 1 or 2 over 3. Flagging it because "tested" and "needed" are different claims, and I made the first one earlier without checking the second.

Also note: this PR and #38 both add lines to tests/CMakeLists.txt and conflict there — trivial to resolve, but it will surface when both land.

@petlenz
petlenz changed the base branch from feature/elastic-stiffness-material to main September 6, 2026 19:46
petlenz added a commit that referenced this pull request Sep 6, 2026
main did not build. tests/test_tangent_generator.cpp, added by #28, includes
materials/small_strain_plasticity.h, which #43 renamed away.

The test it belonged to asserted that isotropic_tangent is a drop-in for
linear_elasticity as a plasticity "elastic_source". Plasticity no longer has an
elastic_source: #44 removed it because the closed-form stress and tangent
require an isotropic C_e, so accepting an arbitrary rank-4 tangent advertised a
generality the material cannot honour. The premise is gone.

The drop-in claim is still covered against consumers that do source a tangent:
TangentSource.* in the same file, and weighted_sum's tangent_sources tests in
#29. The other nine tests in this file exercise isotropic_tangent directly and
are untouched.

How this reached main: the PR was retargeted from its stacked base to main, and
retargeting did not trigger a fresh CI run. The green check I read predated the
retarget, so it had been tested against the old base where the header still
existed. Git reported no conflict either -- nothing edited the same lines. From
here every merge is verified by building the merge result locally, not by
trusting a check-run whose base may be stale.

217/217 tests pass.
@petlenz
petlenz merged commit e19bef3 into main Sep 6, 2026
1 check passed
@petlenz
petlenz deleted the feature/tangent-source-consumers branch September 6, 2026 19:50
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