mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
350 lines
12 KiB
Python
350 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Manifold Intrinsic Geometry — Computing the Actual Shape of the Research Stack
|
|
|
|
This script extracts the dependency graph from Lean imports and computes:
|
|
- Geodesic distances (shortest import path)
|
|
- Curvature (information flow divergence/convergence at each module)
|
|
- Ricci curvature (neighborhood flow density)
|
|
- Central hubs (high betweenness centrality)
|
|
- Boundary modules (low in-degree, high out-degree = sources; high in, low out = sinks)
|
|
- Cyclic dependencies (genus / non-trivial topology)
|
|
- Module clustering (connected components)
|
|
|
|
The output is a JSON file with the full geometric characterization.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from collections import defaultdict, deque
|
|
from itertools import combinations
|
|
|
|
SEMANTICS_DIR = Path("/home/allaun/Documents/Research Stack/0-Core-Formalism/lean/Semantics/Semantics")
|
|
OUTPUT_PATH = Path("/home/allaun/Documents/Research Stack/data/manifold_intrinsic_geometry.json")
|
|
|
|
|
|
def extract_imports(filepath: Path) -> list:
|
|
"""Extract Semantics.* imports from a Lean file."""
|
|
imports = []
|
|
try:
|
|
text = filepath.read_text()
|
|
for line in text.splitlines():
|
|
m = re.match(r'^\s*import\s+Semantics\.(\S+)', line)
|
|
if m:
|
|
imports.append(m.group(1))
|
|
except Exception:
|
|
pass
|
|
return imports
|
|
|
|
|
|
def build_graph():
|
|
"""Build adjacency list of the import graph."""
|
|
nodes = set()
|
|
edges = defaultdict(set) # a -> {b, c} means a imports b and c
|
|
reverse_edges = defaultdict(set) # b -> {a, c} means b is imported by a and c
|
|
|
|
if not SEMANTICS_DIR.exists():
|
|
print(f"ERROR: {SEMANTICS_DIR} not found")
|
|
sys.exit(1)
|
|
|
|
# Find all .lean files in Semantics directory (excluding .lake)
|
|
lean_files = []
|
|
for root, dirs, files in os.walk(SEMANTICS_DIR):
|
|
if '.lake' in root:
|
|
continue
|
|
dirs[:] = [d for d in dirs if d != '.lake']
|
|
for f in files:
|
|
if f.endswith('.lean'):
|
|
lean_files.append(Path(root) / f)
|
|
|
|
for fpath in lean_files:
|
|
rel = fpath.relative_to(SEMANTICS_DIR)
|
|
# Module name: path with / replaced by . and .lean stripped
|
|
mod_name = str(rel).replace('/', '.').replace('.lean', '')
|
|
nodes.add(mod_name)
|
|
imports = extract_imports(fpath)
|
|
for imp in imports:
|
|
edges[mod_name].add(imp)
|
|
reverse_edges[imp].add(mod_name)
|
|
nodes.add(imp)
|
|
|
|
return nodes, edges, reverse_edges
|
|
|
|
|
|
def bfs_distance(start, edges, all_nodes):
|
|
"""Compute shortest path distances from start to all reachable nodes."""
|
|
dist = {n: float('inf') for n in all_nodes}
|
|
dist[start] = 0
|
|
q = deque([start])
|
|
while q:
|
|
u = q.popleft()
|
|
for v in edges.get(u, []):
|
|
if dist[v] == float('inf'):
|
|
dist[v] = dist[u] + 1
|
|
q.append(v)
|
|
return dist
|
|
|
|
|
|
def compute_all_pairs_distances(nodes, edges):
|
|
"""Compute all-pairs shortest path distances."""
|
|
distances = {}
|
|
for n in nodes:
|
|
distances[n] = bfs_distance(n, edges, nodes)
|
|
return distances
|
|
|
|
|
|
def compute_curvature(node, edges, reverse_edges):
|
|
"""
|
|
Compute Ollivier-Ricci curvature approximation for a node.
|
|
Roughly: if many modules import this node (converge), positive curvature.
|
|
If this node imports many and is imported by few (diverge), negative curvature.
|
|
"""
|
|
out_deg = len(edges.get(node, set()))
|
|
in_deg = len(reverse_edges.get(node, set()))
|
|
if out_deg + in_deg == 0:
|
|
return 0.0
|
|
# Simple approximation: curvature = (in - out) / (in + out)
|
|
# Positive = sink (information converges here)
|
|
# Negative = source (information diverges from here)
|
|
return (in_deg - out_deg) / (in_deg + out_deg)
|
|
|
|
|
|
def compute_betweenness_centrality(nodes, edges):
|
|
"""Compute betweenness centrality (approximate, for connected pairs)."""
|
|
# For each pair (s, t), count how many shortest paths go through each node
|
|
centrality = {n: 0 for n in nodes}
|
|
node_list = list(nodes)
|
|
|
|
for s in node_list:
|
|
# BFS from s
|
|
dist = {n: float('inf') for n in nodes}
|
|
dist[s] = 0
|
|
pred = {n: [] for n in nodes}
|
|
q = deque([s])
|
|
while q:
|
|
u = q.popleft()
|
|
for v in edges.get(u, set()):
|
|
if dist[v] == float('inf'):
|
|
dist[v] = dist[u] + 1
|
|
q.append(v)
|
|
pred[v].append(u)
|
|
elif dist[v] == dist[u] + 1:
|
|
pred[v].append(u)
|
|
|
|
# Count paths (simplified: assume each edge contributes 1 path)
|
|
for t in node_list:
|
|
if t == s or dist[t] == float('inf'):
|
|
continue
|
|
# Mark all nodes on any shortest path from s to t
|
|
visited = set()
|
|
stack = [t]
|
|
while stack:
|
|
u = stack.pop()
|
|
if u in visited or u == s:
|
|
continue
|
|
visited.add(u)
|
|
for p in pred.get(u, []):
|
|
if p not in visited:
|
|
stack.append(p)
|
|
for u in visited:
|
|
if u != t:
|
|
centrality[u] += 1
|
|
|
|
# Normalize
|
|
max_c = max(centrality.values()) if centrality else 1
|
|
if max_c > 0:
|
|
centrality = {k: v / max_c for k, v in centrality.items()}
|
|
return centrality
|
|
|
|
|
|
def find_cycles(nodes, edges):
|
|
"""Find all simple cycles in the graph (up to length 5 for performance)."""
|
|
cycles = []
|
|
visited = set()
|
|
|
|
def dfs(node, path, depth):
|
|
if depth > 5:
|
|
return
|
|
for neighbor in edges.get(node, set()):
|
|
if neighbor == path[0] and len(path) >= 2:
|
|
cycles.append(path + [neighbor])
|
|
elif neighbor not in path and neighbor not in visited:
|
|
visited.add(neighbor)
|
|
dfs(neighbor, path + [neighbor], depth + 1)
|
|
visited.remove(neighbor)
|
|
|
|
for n in nodes:
|
|
visited.clear()
|
|
visited.add(n)
|
|
dfs(n, [n], 1)
|
|
|
|
# Deduplicate (same cycle, different start points)
|
|
unique_cycles = []
|
|
seen = set()
|
|
for c in cycles:
|
|
# Normalize: start from smallest element, keep direction
|
|
start_idx = c.index(min(c[:-1]))
|
|
normalized = tuple(c[start_idx:-1] + c[:start_idx] + [c[start_idx]])
|
|
if normalized not in seen:
|
|
seen.add(normalized)
|
|
unique_cycles.append(c)
|
|
|
|
return unique_cycles
|
|
|
|
|
|
def find_connected_components(nodes, edges):
|
|
"""Find weakly connected components (treating edges as undirected)."""
|
|
visited = set()
|
|
components = []
|
|
|
|
# Build undirected adjacency
|
|
undirected = defaultdict(set)
|
|
for u, vs in edges.items():
|
|
for v in vs:
|
|
undirected[u].add(v)
|
|
undirected[v].add(u)
|
|
|
|
def bfs(start):
|
|
comp = []
|
|
q = deque([start])
|
|
visited.add(start)
|
|
while q:
|
|
u = q.popleft()
|
|
comp.append(u)
|
|
for v in undirected[u]:
|
|
if v not in visited:
|
|
visited.add(v)
|
|
q.append(v)
|
|
return comp
|
|
|
|
for n in nodes:
|
|
if n not in visited:
|
|
components.append(bfs(n))
|
|
|
|
return components
|
|
|
|
|
|
def main():
|
|
print("[ManifoldGeometry] Building dependency graph...")
|
|
nodes, edges, reverse_edges = build_graph()
|
|
print(f" Nodes: {len(nodes)}")
|
|
print(f" Edges: {sum(len(v) for v in edges.values())}")
|
|
|
|
print("[ManifoldGeometry] Computing geodesic distances...")
|
|
distances = compute_all_pairs_distances(nodes, edges)
|
|
|
|
# Compute diameter (longest shortest path)
|
|
finite_dists = [d for dd in distances.values() for d in dd.values() if d != float('inf') and d > 0]
|
|
diameter = max(finite_dists) if finite_dists else 0
|
|
avg_dist = sum(finite_dists) / len(finite_dists) if finite_dists else 0
|
|
|
|
print(f" Diameter: {diameter}")
|
|
print(f" Average distance: {avg_dist:.2f}")
|
|
|
|
print("[ManifoldGeometry] Computing curvature...")
|
|
curvature = {n: compute_curvature(n, edges, reverse_edges) for n in nodes}
|
|
|
|
print("[ManifoldGeometry] Computing centrality...")
|
|
centrality = compute_betweenness_centrality(nodes, edges)
|
|
|
|
print("[ManifoldGeometry] Finding cycles...")
|
|
cycles = find_cycles(nodes, edges)
|
|
print(f" Cycles found: {len(cycles)}")
|
|
|
|
print("[ManifoldGeometry] Finding connected components...")
|
|
components = find_connected_components(nodes, edges)
|
|
print(f" Components: {len(components)}")
|
|
|
|
# Identify key geometric features
|
|
hubs = sorted(centrality.items(), key=lambda x: x[1], reverse=True)[:15]
|
|
high_curv = sorted(curvature.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
low_curv = sorted(curvature.items(), key=lambda x: x[1])[:10]
|
|
|
|
# Boundary detection
|
|
sources = [(n, len(edges.get(n, set())), len(reverse_edges.get(n, set())))
|
|
for n in nodes if len(reverse_edges.get(n, set())) == 0 and len(edges.get(n, set())) > 0]
|
|
sinks = [(n, len(edges.get(n, set())), len(reverse_edges.get(n, set())))
|
|
for n in nodes if len(edges.get(n, set())) == 0 and len(reverse_edges.get(n, set())) > 0]
|
|
|
|
# Modules with no imports and no importers (isolated points)
|
|
isolated = [n for n in nodes if len(edges.get(n, set())) == 0 and len(reverse_edges.get(n, set())) == 0]
|
|
|
|
report = {
|
|
"meta": {
|
|
"node_count": len(nodes),
|
|
"edge_count": sum(len(v) for v in edges.values()),
|
|
"diameter": diameter,
|
|
"average_distance": avg_dist,
|
|
"cycle_count": len(cycles),
|
|
"component_count": len(components),
|
|
},
|
|
"hubs": [{"module": n, "centrality": round(c, 4)} for n, c in hubs],
|
|
"positive_curvature": [{"module": n, "curvature": round(c, 4)} for n, c in high_curv],
|
|
"negative_curvature": [{"module": n, "curvature": round(c, 4)} for n, c in low_curv],
|
|
"sources": [{"module": n, "out_degree": o, "in_degree": i} for n, o, i in sources],
|
|
"sinks": [{"module": n, "out_degree": o, "in_degree": i} for n, o, i in sinks],
|
|
"isolated": isolated,
|
|
"cycles": [c for c in cycles[:20]], # Limit output size
|
|
"components": [
|
|
{"size": len(comp), "modules": comp[:50]} # Truncate large components
|
|
for comp in sorted(components, key=len, reverse=True)
|
|
],
|
|
"full_graph": {
|
|
"nodes": list(nodes),
|
|
"edges": {k: list(v) for k, v in edges.items()},
|
|
},
|
|
}
|
|
|
|
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(OUTPUT_PATH, 'w') as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"[ManifoldGeometry] Wrote: {OUTPUT_PATH}")
|
|
|
|
# Print summary
|
|
print("\n" + "=" * 70)
|
|
print("INTRINSIC GEOMETRY OF THE RESEARCH STACK")
|
|
print("=" * 70)
|
|
print(f"\nScale: {len(nodes)} modules, {sum(len(v) for v in edges.values())} import edges")
|
|
print(f"Diameter (longest shortest path): {diameter}")
|
|
print(f"Average geodesic distance: {avg_dist:.2f}")
|
|
print(f"Cycles (non-trivial topology): {len(cycles)}")
|
|
print(f"Connected components: {len(components)}")
|
|
|
|
print(f"\n--- HUBS (High Betweenness Centrality) ---")
|
|
for n, c in hubs[:10]:
|
|
print(f" {n:40s} centrality={c:.4f}")
|
|
|
|
print(f"\n--- POSITIVE CURVATURE (Information Converges Here) ---")
|
|
for n, c in high_curv[:5]:
|
|
print(f" {n:40s} curvature={c:+.4f}")
|
|
|
|
print(f"\n--- NEGATIVE CURVATURE (Information Diverges From Here) ---")
|
|
for n, c in low_curv[:5]:
|
|
print(f" {n:40s} curvature={c:+.4f}")
|
|
|
|
print(f"\n--- SOURCES (No imports, pure origin) ---")
|
|
for n, o, i in sources[:5]:
|
|
print(f" {n:40s} out={o}")
|
|
|
|
print(f"\n--- SINKS (No exports, dead ends) ---")
|
|
for n, o, i in sinks[:5]:
|
|
print(f" {n:40s} in={i}")
|
|
|
|
if isolated:
|
|
print(f"\n--- ISOLATED POINTS (No connections) ---")
|
|
for n in isolated[:10]:
|
|
print(f" {n}")
|
|
|
|
if cycles:
|
|
print(f"\n--- CYCLES (Non-contractible loops) ---")
|
|
for c in cycles[:5]:
|
|
print(f" {' -> '.join(c)}")
|
|
|
|
print("\n" + "=" * 70)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|