Skip to content

Property engine: typed edges, graph export (DOT/JSON) and a named cycle (#213, #188) - #223

Merged
petlenz merged 3 commits into
mainfrom
feature/graph-export
Oct 29, 2026
Merged

petlenz merged 3 commits into
mainfrom
feature/graph-export

Conversation

@petlenz

@petlenz petlenz commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Closes #213, closes #188. First step of #221.

What

  • Typed edges survive the sort. compute_execution_order() records each edge's kind when it is added; a duplicate keeps the first kind seen. The kinds are dependency, input (a material without declarations: every output), declared read, drives, material_ref, callback order, written_by and local. Local edges are recorded separately and take no part in the sort, so the execution order is unchanged.
  • material_context::graph(). Returns a property_graph: nodes in execution order with flags (history, constant, stateful, routed, own callback), and the edges: the sorted global ones, then the sorted local ones. Stateful outputs come from the new material_interface::stateful_outputs().
  • to_dot(ctx) / to_json(ctx) in core/graph_description.h, next to describe_graph(). DOT draws one cluster per material: local edges dashed, constants shaded, history doubled, stateful outputs orange. Every edge is labelled with its kind.
  • Named cycle (Property engine: name the declarations behind a circular dependency #188). When the sort leaves nodes, the engine searches only those for one cycle and reports it with its edge kinds, starting at the smallest key:
    Circular dependency in property graph: a::value -[declared read]-> b::value -[declared read]-> a::value.
    Nodes that are merely downstream of the cycle are not listed. When the cycle contains a callback-order edge, the message names its sources (runs_after() or a read of the material's own output), so ConstantProperties.ACycleOfRunsAfterIsRejected still finds "runs_after".
  • Cached upstream sets. update_property() computes the upstream set of a target once per finalize() (upstream_of()). Property engine: iteration groups, the fixed point moves into update() #214, Property engine: partial derivatives per callback, chain rule by forward accumulation #217 and Property engine: residual systems set up at run time (unknowns and equations from any nodes, Newton/Picard, implicit function theorem) #218 need the same sets.

Cost

Only at finalize(): one map of edge kinds while building, one vector of edges kept per context. update() is untouched. update_property() stops rebuilding its upstream set on every call.

Tests

test_graph_export, written first (red: no graph(), to_dot, to_json):

  • a three-node chain gives exactly the expected edge list with kinds, compared as a sorted vector;
  • a local edge appears as local and does not change the execution order;
  • node flags: history, own callback, constant;
  • a two-node cycle and a three-node cycle are named with their kinds, and a node behind the cycle is not listed;
  • a name with a quote, a backslash and a newline gives valid DOT strings (escaped);
  • DOT: one node per property, one edge per graph edge, the local edge dashed;
  • JSON: nodes and edges with keys and kind names; a name with invalid UTF-8 does not throw (replacement character);
  • a self-loop (runs_after on itself) is named in the cycle message; a second finalize() leaves the graph and update_property() intact; the stateful and routed flags.

Gate

GCC 14, reports on, -DNUMSIM_PREFER_SIBLINGS=OFF: -O1 build 0 warnings, 1466/1466 tests pass.

)

The edges keep their kind after the sort; material_context::graph() returns the nodes with their flags and the typed edges, to_dot()/to_json() export them. A cycle is reported with its properties and edge kinds. update_property() caches the upstream set per target.
…the edge order

to_dot escapes backslash, quote and newline in every name it interpolates. to_json dumps with the replace error handler, so an odd name no longer throws type_error.316. The property_graph comment says what the edge order is: the sorted global edges, then the sorted local ones. Tests add the self-loop cycle message, a second finalize(), and the stateful and routed node flags.
@petlenz

petlenz commented Oct 29, 2026 •

Copy link
Copy Markdown
Member Author

Review findings, addressed in 9eaa781:

  1. to_dot interpolated names raw into "...". A detail::dot_escape (backslash and quote escaped, newline written as \n) now covers every interpolated name: cluster id and label, node id and label, edge ends. Red before the fix: DotEscapesQuotesBackslashesAndNewlines failed all three expectations (the DOT held a raw " and a raw newline inside a string).
  2. to_json: doc.dump(2) threw type_error.316 on invalid UTF-8. It now dumps with error_handler_t::replace. Red before the fix: JsonSurvivesInvalidUtf8, [json.exception.type_error.316] invalid UTF-8 byte at index 6: 0xFF.
  3. The property_graph and m_edges comments said "sorted by key"; they now say sorted global edges followed by sorted local edges. Comment only.
  4. Test gaps in test_graph_export.cpp: SelfLoopCycleIsNamed, UpstreamSurvivesASecondFinalize, StatefulNodeIsFlagged, RoutedNodeHasNoCallbackOfItsOwn. These cover existing behaviour and pass without a code change, so they have no red run. Deviations from the brief: (a) a self-loop cannot come from a read of an own output (add_input_edge drops it), so the fixture uses runs_after(x, x) (deprecated, locally silenced) and the message is a::value -[callback order]-> a::value; (b) finalize() on a finalized context is a no-op and build() runs once, so the second-finalize() test checks that the graph, the JSON and update_property() stay intact rather than a cache invalidation.

Not changed, noted for the record: graph() returns the graph by value (a copy of the edges per call; fine for an export, not for a hot path) and core/graph_description.h includes nlohmann from core/.

PR body updated (edge-order wording, tests, gate). Gate: GCC 14 -O1, build 0 warnings, 1466/1466 tests pass.

@petlenz

petlenz commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Merged main (#222) into this branch. No conflicts.

Gate: g++-14 -O1, 1486 tests, 0 warnings.

This PR is now next in the merge order; after it, #226 and #227.

@petlenz
petlenz merged commit 23ab4fd into main Oct 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant