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

nordisk/knut

demo/flight_networkx_demo.py

#!/usr/bin/env python3
"""
Arrow Flight → networkx demo (N4).

Connects to a running ``knut-flight-server`` (the plain Arrow Flight server over
knut's rel2graph output), lists the advertised flights, pulls the ``nodes`` and
``edges`` datasets as Arrow tables, and rebuilds a ``networkx.MultiDiGraph`` from
them with the SAME loader the file-export demo (N3) uses. It then ASSERTS the
Flight-path graph agrees with the file-path graph / oracle — proving the two
delivery paths describe the identical graph.

Run:

    # 1. start the server (separate shell, or backgrounded):
    cargo run -p knut-flight --release --bin knut-flight-server -- --addr 0.0.0.0:50055 --scale 1
    # 2. (optional) produce the N3 file export to cross-check against:
    cargo run -p knut-rel2graph-bench --release --bin rel2graph-export -- 1 target/rel2graph-export
    # 3. run this demo:
    python demo/flight_networkx_demo.py [grpc://HOST:PORT] [EXPORT_DIR]
"""

from __future__ import annotations

import os
import sys

import networkx as nx
import pyarrow.flight as flight

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import knut_networkx as kn  # noqa: E402


def main() -> int:
    location = sys.argv[1] if len(sys.argv) > 1 else "grpc://localhost:50055"
    export_dir = sys.argv[2] if len(sys.argv) > 2 else "target/rel2graph-export"

    client = flight.FlightClient(location)
    print(f"== connected to knut-flight-server at {location} ==")

    # ── list_flights — discover what the server advertises ──────────────────────
    flights = list(client.list_flights())
    print(f"\n-- list_flights: {len(flights)} datasets --")
    names = []
    for fi in flights:
        name = fi.descriptor.command.decode() if fi.descriptor.command else "/".join(
            p.decode() for p in fi.descriptor.path
        )
        names.append(name)
        print(f"  {name:<40} rows={fi.total_records:<7} cols={len(fi.schema)}")
    assert "nodes" in names and "edges" in names, "server must advertise nodes + edges"

    # ── do_get: pull the aggregate nodes + edges as Arrow ───────────────────────
    nodes_table = client.do_get(flight.Ticket(b"nodes")).read_all()
    edges_table = client.do_get(flight.Ticket(b"edges")).read_all()
    print(f"\n-- do_get --")
    print(f"  nodes: {nodes_table.num_rows} rows, schema {nodes_table.schema.names}")
    print(f"  edges: {edges_table.num_rows} rows, schema {edges_table.schema.names}")

    # bonus: pull one source relational table over the SAME Flight surface.
    emp = client.do_get(flight.Ticket(b"table/erp.hr.employees")).read_all()
    print(f"  table/erp.hr.employees: {emp.num_rows} rows, schema {emp.schema.names}")

    # ── build the SAME MultiDiGraph from the Arrow tables ───────────────────────
    g = kn.build_multidigraph(
        kn.nodes_from_arrow(nodes_table),
        kn.edges_from_arrow(edges_table),
    )
    print(f"\n-- networkx.MultiDiGraph from Flight --")
    print(f"  nodes={g.number_of_nodes()} edges={g.number_of_edges()} "
          f"self-loops={nx.number_of_selfloops(g)} "
          f"wcc={nx.number_weakly_connected_components(g)}")

    by_label = kn.node_counts_by_label(g)
    by_type = kn.edge_counts_by_type(g)
    reports_to = [(u, v) for u, v, d in g.edges(data=True) if d["rel"] == "REPORTS_TO"]

    # ── ASSERT the Flight-path graph is self-consistent ─────────────────────────
    print("\n== asserting Flight-path graph ==")
    assert g.is_directed() and g.is_multigraph()
    assert g.number_of_nodes() == nodes_table.num_rows
    assert g.number_of_edges() == edges_table.num_rows
    assert nx.number_of_selfloops(g) == 0, "no self-loops (a7208dc REPORTS_TO fix)"
    assert len(reports_to) > 0 and all(u != v for u, v in reports_to)
    print("  OK — MultiDiGraph, counts match the Arrow tables, no self-loops.")

    # ── ASSERT the Flight path AGREES with the file path (N3), if present ───────
    if os.path.isdir(export_dir):
        f_nodes, f_edges, oracle = kn.load_json_export(export_dir)
        fg = kn.build_multidigraph(f_nodes, f_edges)
        print(f"\n== asserting Flight path == file path ({export_dir}) ==")
        assert g.number_of_nodes() == fg.number_of_nodes() == oracle["total_nodes"]
        assert g.number_of_edges() == fg.number_of_edges() == oracle["total_edges"]
        assert by_label == kn.node_counts_by_label(fg)
        assert by_label == oracle["node_counts"]
        oracle_edges = {(e["src"], e["rel"], e["dst"]): e["count"] for e in oracle["edge_counts"]}
        assert by_type == kn.edge_counts_by_type(fg) == oracle_edges
        assert nx.number_of_selfloops(g) == oracle["self_loops"]
        print("  OK — Flight and file paths build the identical graph, and both")
        print("       match knut's oracle (node/edge totals, per-label, per-type,")
        print("       self-ref REPORTS_TO with no self-loop).")
    else:
        print(f"\n(note: {export_dir} not found — skipping file-path cross-check; "
              "run rel2graph-export to enable it.)")

    print("\nALL ASSERTIONS PASSED.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())