Skip to content

solvers: cold-start vector_newton instead of seeding from its own outputs - #50

Open
petlenz wants to merge 1 commit into
mainfrom
fix/vector-newton-seed
Open

petlenz wants to merge 1 commit into
mainfrom
fix/vector-newton-seed

Conversation

@petlenz

@petlenz petlenz commented Sep 13, 2026

Copy link
Copy Markdown
Member

vector_newton::solve() seeded m_x by reading back its own output properties. That looks like a warm start and is not one.

The unknowns are created with add_output, not add_history_output, so statev_map never collects them — it skips anything !is_history() — and unpack() never restores them. One instance serves every integration point on a thread: umat_interface builds one material_context per thread and states why that is sufficient —

a consequence of the evaluator being stateless: all point state lives in the host's STATEV array

and material_point_evaluator is blunter —

anything the evaluator remembered across calls would be state from a trial the host has since discarded

Seeding from the properties is exactly such a memory. Point N starts from point N−1's converged answer, and after a failure from a diverged iterate. For a system with several real roots that does not merely change the iteration count — it selects which root is returned, converged and plausible and not the root this point is on.

The fix

Cold start every solve, which makes the solver point-local by construction. backward_euler already works this way: it passes a fixed literal seed rather than reading back its own delta.

Tests

The first is the one that matters: it drives two different problems through one instance, changing them in place with set_parameter, and requires each answer to equal a fresh instance's. x² + y = a ; x + y² = b has several real roots, so the seed is observable in the result, not just in the iteration count.

test catches
SolvesArePointLocal point-order dependence — the previous point's answer selecting this point's root
AFailedSolveDoesNotMoveTheNext the same across a diverging solve

Mutation-verified against three implementations:

implementation SolvesArePointLocal AFailedSolveDoesNotMoveTheNext
main (seed from properties) fails fails
this PR's first attempt (hold the seed back only after a failure) fails passes
cold start (this PR) passes passes

What changed since the first version of this PR

The first version kept the property seeding and only held it back after a failure. That was the wrong level — it removed failure-dependence and left point-order-dependence, and it added a second piece of hidden cross-call state to a class whose surrounding contract is that nothing is retained between calls. SolvesArePointLocal fails against that version too, which is why it is written as a cross-problem test rather than a cross-attempt one.

The first version also accidentally carried ~400 lines of unrelated CalculiX adapter work that was sitting uncommitted in the working tree. That is out, and preserved.

Left to #21

Warm-starting is worth having — plane_stress_evaluator keeps eps_33 in its own STATEV slot precisely because a cold start costs several extra graph evaluations per Gauss point — but it has to be per point, which means the unknowns becoming history properties. The zero seed also assumes a non-singular Jacobian at the origin; a model that needs otherwise wants an explicit initial guess, which is the same follow-up.

No production caller: vector_newton appears only in the factory registration and its own tests. 282/282 pass.

…puts

solve() seeded m_x by reading back its own output properties. That looks like a
warm start and is not one.

The unknowns are created with add_output, not add_history_output, so statev_map
never collects them -- it skips anything !is_history() -- and unpack() never
restores them. One instance serves every integration point on a thread:
umat_interface builds one material_context per thread and states why that is
sufficient, "a consequence of the evaluator being stateless: all point state
lives in the host's STATEV array". material_point_evaluator is blunter:
"anything the evaluator remembered across calls would be state from a trial the
host has since discarded".

Seeding from the properties is exactly such a memory. Point N starts from point
N-1's converged answer, and after a failure from a diverged iterate. For a
system with several real roots that does not merely change the iteration count,
it selects WHICH root is returned -- converged, plausible, and not the root this
point is on.

Cold start every solve, which makes the solver point-local by construction. That
is what backward_euler already does: it passes a fixed literal seed rather than
reading back its own "delta".

Two tests, and the first is the one that matters. It drives two different
problems through ONE instance, changing them in place with set_parameter, and
requires each answer to equal a fresh instance's. x^2 + y = a; x + y^2 = b has
several real roots, so the seed is observable in the result rather than only in
the iteration count. The second does the same with a diverging problem in
between, for the failure path.

An earlier version of this change kept the property seeding and only held it
back after a failure. That was the wrong level: it removed failure-dependence
and left point-order-dependence, and added a second piece of hidden cross-call
state to a class whose surrounding contract is that nothing is retained between
calls. SolvesArePointLocal fails against that version too, which is the point of
writing it this way.

Warm-starting is still worth having -- plane_stress_evaluator keeps eps_33 in
its own STATEV slot precisely because a cold start costs several extra graph
evaluations per Gauss point -- but it has to be per point, which means the
unknowns becoming history properties. Left to #21 with the zero-seed caveat for
a model whose jacobian is singular at the origin.

No production caller: vector_newton appears only in the factory registration and
its own tests. 282/282 pass.
@petlenz
petlenz force-pushed the fix/vector-newton-seed branch from be28b3b to 1264719 Compare September 13, 2026 16:37
@petlenz petlenz changed the title solvers: do not seed a vector_newton retry from a failed iterate solvers: cold-start vector_newton instead of seeding from its own outputs Sep 13, 2026
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