Repository navigation
Description
Part of #237. Related to #232 (engine split: this becomes core/engine/groups.h).
Why
property_engine::form_groups is 125 lines after #226 and 163 after #229. It is correct per its tests, but harder than the problem:
- It finds the loops again for each residual. For every
converges()it scans every local edge, every consumer callback and every producer, with reachability walks built inline. The same loop is found again for each residual that sees it. - Overlap checking depends on order. Nesting and splitting are found by counting how many of a loop's callbacks an earlier residual already claimed. The result depends on the residual order, and one rule comes out as three errors: "nested loops are not supported", "one residual per group" and "has no closed local loop".
- Scheduling is a fixpoint loop. It repeatedly picks a group whose inputs have all run, with a "groups read each other" error when none can be picked.
- Everything is inline. Graph maps, two caches and nested lambdas in one function: nothing can be tested without a full material context.
Design
Three steps, each a function on the graph, testable on a hand-built graph:
- Loops. A local edge P→Q closes a loop iff there is a path Q →* P over non-local edges. The loop body is every callback reachable from Q that also reaches P. Bodies that share a callback merge (union-find), so loops are disjoint. Solver-internal pairs that read each other only over local edges (
j2_plasticity↔ its hardening law) have no such path and drop out without a special case. - Assignment. Each
converges()takes the loops upstream of its residual. Its group is those loops plus the callbacks between them and the residual: upstream of the residual and downstream of a loop. One rule, three errors:- no loop is upstream of the residual;
- a loop is upstream of two residuals (covers nested, split and two residuals over one loop; the message names both residuals and the closing edge);
- a stateful callback is inside a group.
- Schedule. Groups have no gaps: any callback on a path between two members is a member. Collapsing each group to one node therefore leaves a DAG, so a topological sort (Kahn) of the collapsed graph gives the stages, and "groups read each other" cannot occur.
Systems with unknowns (#229) keep their own membership rule: downstream of the unknowns and upstream of the residuals. They take part in step 3 like a group. They must not share a callback with another group or system, which is checked in step 2.
Target size: about 50–60 lines for the three steps, plus helpers that are shared with the rest of the engine (upstream_of and a matching downstream_of).
Tests
- All of
test_iteration_groups.cpp(22 tests) and the system tests of Property engine: residual systems set up at run time (#218) #229 pass unchanged. Error-message tests check substrings, and the new messages keep the residual names and the edge. - New unit tests on a hand-built graph:
- loop detection with a local-only pair (no loop);
- two loops sharing a callback (merged);
- a diamond inside a loop (one body);
- assignment of a loop to two residuals (error);
- schedule order of two groups in sequence.
Depends on
#229 merged, so the rewrite covers the final version with residual systems.
Out of scope
- Nested loops (still rejected).
- Behaviour changes beyond the error texts.
Activity