Repository navigation
Conversation
…to feature/issue-214
#214) material_base::converges(residual, tolerance, max_passes) declares a fixed point. The engine forms one group per declaration from the callbacks between a local edge's consumer and the residual, splits the callback list into stages and reruns each group until the residual meets its tolerance, or throws cutback_error after max_passes or on NaN. The nonlinear mean-field composite declares its residual; material_point's residual_source, the UMAT residual_property and the report "point" residual keys are deprecated.
…on (#217) Callbacks declare partials() / independent_of() next to reads(); the engine plans a requested derivative over the target's upstream and accumulates the Mandel-packed partials. Hosts request derivatives via material_context and material_point. check_partials() verifies declared partials by central differences. thermal_eigenstrain and linear_elasticity declare their partials, giving the first DDSDDT.
…issue-218 # Conflicts: # include/numsim-materials/core/property_engine.h
A "system" node owns unknowns and equations from any nodes. update() solves it between what it reads and what reads it: newton with J from the declared partials (structural zero blocks from missing paths), newton_fd, or picard; converges() is a picard system without unknowns. Non-convergence throws cutback_error naming the system. Derivatives through a system follow from the implicit function theorem at the converged point; "seed": "history" warm-starts an unknown per point. check_partials packs every analytic block before the perturbed reruns.
With the partials requested, a rerun at a perturbed read refilled the blocks, so a later partial of the same callback was compared at the perturbed state. Found while building #218.
…, fill at once check_partials perturbs every read declared independent_of() and requires the outputs of the callback to stay put; its step follows the read block, its roundoff allowance is bounded by the analytic block, and the reads and outputs are restored when a perturbed rerun throws. derivative<R> accepts only the scalar or a tensor of the context's dimension, refuses calls from a callback, builds its message on the error path only, and on an evaluated context reruns the callbacks of the new plan so the result is filled at once. has_path() tells independence from a typo.
…nverges() arguments (#214) form_groups seeds a group only from a local edge P->Q whose producer is downstream of its consumer and upstream of the residual, so solver-internal pairs (both edges local) are no longer iterated. A loop that runs through the group of another residual is rejected at finalize, naming both residuals. iterate() treats a non-finite residual as not converged before the tolerance test and rechecks the settings on every update. The nonlinear composite keeps a NaN residual (std::max dropped it). converges() and declare_converges() delete the overloads that take a temporary tolerance or max_passes.
…ewton_fd steps by the size of x update_property() runs the solve of a system whose unknowns or members the target reads. fold() throws a cutback_error naming the system when dx/dp is not finite and reuses J, B and dx/dp. newton_fd takes a step relative to ||x||_inf and puts the unknowns back before a throw. iterations() is set before a throw, zero_blocks() before finalize() throws, a seed that a passed system reruns throws, two unknowns may not share a residual, and the JSON readers require lists and reject unknown keys.
|
Review fixes for the residual systems, in three commits on top of the previous head: two forward merges and one fix commit (e192818). Merge order stays: after #226 and #227. Forward merges
The merged tree passed the full gate before the first push (1666 tests, 0 warnings). The first merge commit was made locally before the second merge, since git cannot merge while one is open; it was only pushed together with the gated result. Review findings
The gate on the fix commit: BUILD=0, TEST=0, warnings=0, 1692 tests. The PR body is updated (merge order, newton_fd step, new behaviours, test counts). |
Closes #218 (part of epic #221).
Stacked: merge after #226 and #227 (#222 and #223 are already in main). This branch contains both (their latest commits merged in, conflicts in
form_groups,partial_check.hand the derivative API resolved in the two merge commits); the diff against main shrinks to this PR once they are in.What and why
A
systemnode (registered as"system", classresidual_system,solvers/system.h) owns unknowns and equations set up at run time:{ "name": "plastic", "type": "system", "method": "newton", "unknowns": [{ "name": "dlambda", "kind": "scalar", "seed": 0 }, { "name": "dbeta", "kind": "sym_tensor", "seed": "history" }], "equations": [{ "residual": "flow::r_l", "for": "dlambda" }, { "residual": "flow::r_b", "for": "dbeta" }], "tolerance": 1e-12, "max_iter": 25 }"seed": "history"). The residuals are outputs of any nodes and are read over local edges, so the topological sort runs unknowns -> members -> residuals.converges(), picard) follows the closed-loop rule of Property engine: iteration groups converge local edges inside update() (#214) #226 instead. The Property engine: iteration groups, the fixed point moves into update() #214 rules still apply: nostateful()member, and a system with unknowns shares no member with another system or group. A system's unknowns count as part of it when stages are placed, so a system reading another system's unknowns runs after it."for"pairs each residual with an unknown and fixes the row order. Both use the Mandel layout ofcore/derivatives.h(the packing Property engine: partial derivatives per callback, chain rule by forward accumulation #217 uses, shared withsolvers/unknown_layout.h).residual_system::zero_blocks()lists those blocks, derived from the graph (this replaces the hand-declaredzero_blocks/verify_zero_blocks).x <- x - R. With no unknowns it is the Property engine: iteration groups, the fixed point moves into update() #214 iteration group, with the closed-loop rule of Property engine: iteration groups converge local edges inside update() (#214) #226.material_base::converges()now declares such a system (one residual, no unknowns, passes counted as before).convergence_declaration/converging_outputs()becomesystem_declaration/systems(), so there is one group mechanism inform_groups()/solve().vector_newton: evaluate-first convergence (|R|_inf <= toleranceafter the update;max_itercounts Newton updates, so a linear system converges withmax_iter = 1), no hidden projection, and an LU per iteration. Not converging, a non-finite residual (never converged, even with an infinite tolerance), or a singular Jacobian (non-finite dx) throwscutback_errorthat names the system. On a singular Jacobian the unknowns keep the last finite iterate.iterations()is set before a throw;zero_blocks()throws beforefinalize().update_property()on a target that reads a system's unknowns or members runs that system's solve (aconverges()group still runs once, as Property engine: iteration groups converge local edges inside update() (#214) #226 tests).differentiate(): a requested d target / d p whose upstream reaches a system's unknowns (also through another system) gets those unknowns as extra seed columns and their residuals as extra targets. One accumulation pass gives D[R] = [B | J]. Thenfold()computes dx/dp = -J^-1 B for each system in stage order and adds D_x dx/dp to the p columns of every node; a non-finite dx/dp (singular J at the converged point) throws acutback_errornaming the system. A seed that is an output of a passed system's members throwslogic_error(the system solves it again). This matches the issue'stangent_through(): unknownsout[x] = dx/dpand members read outside getD_p + D_x dx/dp. Doing the fold after one pass over all nodes avoids per-system bookkeeping.material_point::derivative()therefore works through a system."seed": "history": the unknown is a history output. The solve starts from its old value, whichstatev_map/material_pointkeep per point. A numeric seed sets a scalar's start value. A tensor seed must be 0.Decisions / deviations
<=everywhere (the Property engine: iteration groups, the fixed point moves into update() #214 semantics;vector_newtonused<). This only matters when |R| equals the tolerance exactly.check_partialsfix (from Partial derivatives: per-callback declarations and forward accumulation (#217) #227): it packed each partial's analytic block only after the previous partial's perturbed reruns had overwritten the blocks. That surfaces once a callback's partials stay requested (Newton members) and depend on the state. Fixed by packing all of a callback's blocks right after the base run.tests/test_system_plasticity.cpp, flow node with residuals + history, stress node with partials). The shipped material is unchanged; migrating it is Migrate materials to declared partials and residual systems #219. "Matches to 1e-12" is checked as relative norm difference of stress, tangent and back stress, both Prager and Armstrong-Frederick, over an elastic / plastic / plastic / unloading path.vector_newtonandbackward_eulerare untouched (they become wrappers in Migrate materials to declared partials and residual systems #219).unknowns/equationsrequire lists of objects and reject unknown keys.newton_fdstep uses 1 as the scale when all unknowns are zero (a floor oftinywould make the first step vanish).Tests
test_residual_system(new, 36 cases):test_vector_newtonscalar cases as systems take exactly the iterationsvector_newtonneeds (fewestmax_iterthat converges) and land on its root; one fewermax_itergives a cutback.max_iter, cutback naming the system, singular Jacobian, point-local cold start, a failed solve does not move the next.vector_newton.[["x","y"]], the old declaredzero_blocks; the linear split and the single node have none.material_point::derivativematch the analytic IFT result (major-asymmetric). A residual read outside has total derivative 0."seed": "history": two points evaluated alternately in one context give the same x and iterations as each alone, with fewer iterations than a cold start.derivative(); NaN and Inf residuals;tolerance = 0andmax_iter = 0with a converged seed; small-scale unknowns;iterations()after a throw;zero_blocks()before finalize; unknowns restored after a singular newton_fd step; singular J infold(); a member output as seed.test_system_plasticity(new): j2_kinematic_plasticity as a system matches the shipped stress, tangent and back stress to 1e-12 (relative); its tangent matches central differences of the stress;update_propertyon its stress equalsupdate(). Only recovery needs more than one Newton step. No zero blocks, and the test model's partials passcheck_partials.test_iteration_groups: all Property engine: iteration groups, the fixed point moves into update() #214 tests unchanged and passing. New: a picardsystemwithout unknowns over the same relaxer reproduces the group (same x, probe once per update, cutback naming the system).Gate
GCC 14 + ASan locked gate: BUILD=0, TEST=0, warnings=0 — 1692 tests (0 warnings).