Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

.nornir/rel2graph-networkx-design.md

rel2graph → networkx — the file-export bridge (N3)

Class: design. A second, DB-free road from a knut rel2graph result to a Python networkx graph, complementary to networkx-export.md (the live FalkorDB → Flight SQL → pandas road). Here the source is the GraphMutation the mapping fabric builds directly — no FalkorDB, no network — dumped as a stable node-list + edge-list + oracle that Python loads into a networkx.MultiDiGraph. Source: exporter crates/knut-rel2graph-bench/src/export.rs + bin src/bin/export.rs (rel2graph-export); Python loader demo/knut_networkx.py; demo demo/rel2graph_networkx_demo.py.

Why a second path

networkx-export.md turns a live FalkorDB graph into a networkx object over Flight SQL. This path answers a different question: is the graph the rel2graph engine PRODUCES correct, and can I explore it in Python without standing up a database? Its source is the GraphMutation — the exact seam the correctness bench asserts on — so the networkx graph is the mapping output itself, one hop from the transform fabric.

The shape of a rel2graph result

A GraphMutation is VertexSets (label + id column + property columns) and EdgeSets (src/dst id columns, src_label/edge_type/dst_label, property columns, and the composite-key / self-referential cases the 12-table bench covers). Two facts drive the encoding:

  1. A node id is unique only WITHIN its label. Order 1 and Employee 1 are different nodes. So the networkx node identifier must be the (label, id) pair, never the bare id.
  2. Parallel edges are real. Two different rel-types (or two rows of the same rel-type) can join the same endpoint pair. A plain DiGraph collapses them; a MultiDiGraph keeps them. This path always builds a MultiDiGraph.

The export format

rel2graph-export [SCALE] [OUT_DIR] runs the real fabric over the seeded 12-table ERP schema and writes three files under OUT_DIR (default target/rel2graph-export, seed DEFAULT_SEED — deterministic):

file one line per fields
nodes.jsonl distinct node label, id, props{…}
edges.jsonl resolved edge src_label, rel, dst_label, src, dst, props{…}
oracle.json — total_nodes, total_edges, dangling, null_endpoint_rows, self_loops, node_counts{label→n}, edge_counts[{src,rel,dst,count}]

Only rows whose both endpoints are non-null are written to edges.jsonl (a null FK = "no edge", standard SQL — e.g. an org root's REPORTS_TO), so the edge-list count equals the oracle's resolved-edge count exactly. Property columns are read for the three Arrow types the schema uses (Int64/Float64/Utf8).

oracle.json is computed by the harness's independent summarize straight from the mapping output — the same oracle the correctness bench uses. It is the demo's ground truth.

The loader — demo/knut_networkx.py

A tiny pure-Python helper (only networkx + stdlib). load_json_export(dir) reads the three files; build_multidigraph(nodes, edges) builds the graph with node id (label, id), node attrs = label + props, edge attrs = rel + props. The same build_multidigraph is fed by the Flight path (flight-graph-server-design.md) via nodes_from_arrow / edges_from_arrow — one builder, two sources, so the two roads provably agree.

The demo is a correctness demo

rel2graph_networkx_demo.py loads the export, builds the MultiDiGraph, prints node/edge counts by label/type, a degree distribution, the weakly-connected component count, and a shortest-path example (Order → Product → Category), then asserts every count it computed equals oracle.json:

At scale 1 the graph is 1106 nodes, 2955 edges, 1 weakly-connected component, REPORTS_TO = 21 (24 employees − 3 org roots), 0 self-loops — all asserted.

Run

cargo run -p knut-rel2graph-bench --release --bin rel2graph-export -- 1 target/rel2graph-export
python -m venv .venv && .venv/bin/pip install -r demo/requirements.txt
.venv/bin/python demo/rel2graph_networkx_demo.py target/rel2graph-export

Live data, not just the bench

The GraphMutation this exporter dumps comes from the engine's canonical seam, knut_bifrost::rel2graph::build_mutation, over a TableSource — so the SAME export runs over real data by swapping the source. rel2graph-export … --parquet also writes the source tables as <table>.parquet (via write_source_parquet); that on-disk export is exactly what knut-flight-server --parquet <dir> reads back through the same engine to serve a live graph. See rel2graph-engine-seam.md for the one entrypoint korp and every DB-free consumer share.

See also