Research-Stack/5-Applications/scripts/build_manifold_graphml.py

167 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""
build_manifold_graphml.py — Build GraphML from Research Stack manifold geometry.
Reads manifold_intrinsic_geometry.json (output of manifold_geometry.py)
and writes a GraphML file with full geometric attributes per node and edge.
Nodes: Lean modules with curvature, centrality, in/out degree, component, type
Edges: import dependencies with geodesic weight
Usage:
python3 5-Applications/scripts/build_manifold_graphml.py
"""
import json
import os
import sys
from pathlib import Path
import networkx as nx
PROJECT_ROOT = Path(__file__).parent.parent.parent
DATA_DIR = PROJECT_ROOT / "data"
ARTIFACTS_DIR = PROJECT_ROOT / "artifacts"
def load_geometry(path: Path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_topology(path: Path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def build_manifold_graph(geometry: dict, topology: dict) -> nx.DiGraph:
G = nx.DiGraph()
meta = geometry.get("meta", {})
for k, v in meta.items():
# GraphML can't serialize dict/bool; flatten primitive values only
if isinstance(v, (str, int, float)):
G.graph[k] = v
full = topology.get("full_graph", {})
nodes = full.get("nodes", [])
edges_map = full.get("edges", {})
# Index curvature and centrality
curvature = {}
for item in geometry.get("positive_curvature", []):
curvature[item["module"]] = item["curvature"]
for item in geometry.get("negative_curvature", []):
curvature[item["module"]] = item["curvature"]
centrality = {}
for item in geometry.get("hubs", []):
centrality[item["module"]] = item["centrality"]
# Index sources/sinks
source_degree = {}
for item in geometry.get("sources", []):
source_degree[item["module"]] = item.get("out_degree", 0)
sink_degree = {}
for item in geometry.get("sinks", []):
sink_degree[item["module"]] = item.get("in_degree", 0)
# Components index
component_map = {}
for comp in topology.get("components", []):
cid = comp.get("id", "unknown")
for member in comp.get("members", []):
component_map[member] = cid
# Holes index (gap modules)
gap_modules = set()
for hole in topology.get("holes", []):
for mod in hole.get("missing_modules", []):
gap_modules.add(mod)
# Add nodes (nodes are plain module name strings)
for name in nodes:
G.add_node(
name,
type="module",
centrality=centrality.get(name, 0.0),
curvature=curvature.get(name, 0.0),
component=component_map.get(name, "unknown"),
is_source=str(name in source_degree),
is_sink=str(name in sink_degree),
is_gap=str(name in gap_modules),
)
# Add edges (edges_map is dict: src -> [dst, ...])
for src, dsts in edges_map.items():
for dst in dsts:
G.add_edge(src, dst, relation="IMPORTS", weight=1.0)
return G
def write_graphml(G: nx.DiGraph, path: Path):
path.parent.mkdir(parents=True, exist_ok=True)
nx.write_graphml(G, str(path))
print(f"GraphML saved: {path}")
print(f" Nodes: {G.number_of_nodes()}")
print(f" Edges: {G.number_of_edges()}")
meta = G.graph.get("meta", {})
print(f" Diameter: {meta.get('diameter', 'N/A')}")
print(f" Components: {meta.get('component_count', 'N/A')}")
print(f" Cycles: {meta.get('cycle_count', 'N/A')}")
def write_mermaid_hub(G: nx.DiGraph, path: Path, top_n: int = 50):
"""Write a Mermaid diagram of top hubs for quick visualization."""
hubs = sorted(
((n, d["centrality"]) for n, d in G.nodes(data=True) if d.get("centrality", 0) > 0),
key=lambda x: x[1],
reverse=True,
)[:top_n]
lines = ["graph TD"]
for name, cent in hubs:
safe = name.replace(".", "_")
label = f"{name}({name}\\nC={cent:.2f})"
lines.append(f" {safe}[\"{label}\"]")
# Add edges between hubs
hub_names = {n for n, _ in hubs}
for src, dst, data in G.edges(data=True):
if src in hub_names and dst in hub_names:
lines.append(f" {src.replace('.', '_')} --> {dst.replace('.', '_')}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines), encoding="utf-8")
print(f"Mermaid hub diagram: {path}")
def main():
geom_path = DATA_DIR / "manifold_intrinsic_geometry.json"
topo_path = DATA_DIR / "manifold_topology_report.json"
if not geom_path.exists():
print(f"Geometry data not found: {geom_path}")
sys.exit(1)
if not topo_path.exists():
print(f"Topology data not found: {topo_path}")
sys.exit(1)
geometry = load_geometry(geom_path)
# The intrinsic geometry JSON contains both geometry metrics AND the full graph
# (nodes/edges/full_graph). topology report is a different schema from manifold_perception.
G = build_manifold_graph(geometry, geometry)
graphml_path = ARTIFACTS_DIR / "manifold_geometry.graphml"
write_graphml(G, graphml_path)
mermaid_path = ARTIFACTS_DIR / "manifold_hubs.mmd"
write_mermaid_hub(G, mermaid_path, top_n=30)
print("Done.")
if __name__ == "__main__":
main()