300-node gate network with 334 edges, generated from seed config then hand-balanced and tuned. Gateway (S-001) is the first hop from Earth with 4 active + 1 dormant Sol aperture. Final topology: 20% dead_end, 14% spur_end, 31% through_route, 10% loop_member, 19% junction, 6% hub. Frontier sectors have distinct linear corridors; core is densely interconnected. Includes generation pipeline (generate, sculpt, patch-core, tune), seed config, and star-map-plan.md with algorithm documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
7.6 KiB
Python
210 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Hand-balance the core sector in star-map.json.
|
|
Adds 17 edges within the core sector to restore connectivity and topology diversity.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
JSON_PATH = "/var/mnt/data/projects/settled-reach/planning/docs/design/star-map.json"
|
|
|
|
# ── New edges to add (all within core sector) ──────────────────────────────────
|
|
NEW_EDGES = [
|
|
["S-235", "S-067"], # 1: cross-connect two major cores
|
|
["S-235", "S-093"], # 2: S-235 reaches deg=5 (hub)
|
|
["S-235", "S-010"], # 3: S-235 hub spine extended
|
|
["S-067", "S-213"], # 4: S-213 exits dead_end
|
|
["S-067", "S-181"], # 5: S-067 reaches deg=5 (hub); S-181 exits dead_end
|
|
["S-067", "S-120"], # 6: S-067 hub — links to S-120
|
|
["S-263", "S-253"], # 7: connect two spur/through systems
|
|
["S-263", "S-139"], # 8: S-263 reaches junction
|
|
["S-091", "S-111"], # 9: connect two spur_ends
|
|
["S-253", "S-118"], # 10: S-118 exits dead_end
|
|
["S-139", "S-168"], # 11: S-168 exits dead_end
|
|
["S-027", "S-047"], # 12: connect two spur_ends
|
|
["S-093", "S-037"], # 13: S-037 exits dead_end
|
|
["S-120", "S-119"], # 14: S-119 exits dead_end
|
|
["S-074", "S-027"], # 15: S-074 exits dead_end
|
|
["S-114", "S-213"], # 16: S-114 exits dead_end; S-213 reaches junction
|
|
["S-111", "S-168"], # 17: S-168 promoted to junction; S-111 reaches junction
|
|
]
|
|
|
|
# ── Topology classification rules (by degree) ─────────────────────────────────
|
|
# Special override for S-001 (Gateway): always hub regardless of computed degree.
|
|
SPECIAL_HUBS = {"S-001"}
|
|
|
|
def classify_topology(system_id: str, degree: int) -> str:
|
|
if system_id in SPECIAL_HUBS:
|
|
return "hub"
|
|
if degree >= 5:
|
|
return "hub"
|
|
if degree == 3 or degree == 4:
|
|
return "junction"
|
|
if degree == 2:
|
|
# Use spur_end as default for deg-2; through_route is a subtype
|
|
# We preserve existing through_route classification for existing deg-2 nodes
|
|
# unless they change degree. If they stay at 2, they keep their type.
|
|
# This function only gets called when degree CHANGES.
|
|
return "spur_end"
|
|
if degree == 1:
|
|
return "dead_end"
|
|
# deg=0 should not occur
|
|
return "dead_end"
|
|
|
|
def aperture_for_connections(gate_connections: int, system_id: str) -> int:
|
|
"""
|
|
Compute aperture_count from gate_connections.
|
|
S-001 is special: 5 apertures (4 active Reach + 1 dormant Sol).
|
|
For all others: apertures = connections (one aperture per connection).
|
|
Aperture count has a cap of 8 per setting rules.
|
|
"""
|
|
if system_id == "S-001":
|
|
return 5 # never changes
|
|
return min(gate_connections, 8)
|
|
|
|
def main():
|
|
with open(JSON_PATH, "r") as f:
|
|
data = json.load(f)
|
|
|
|
nodes = data["nodes"]
|
|
edges = data["edges"]
|
|
|
|
# Build adjacency map to compute current degrees
|
|
degree = defaultdict(int)
|
|
edge_set = set()
|
|
for edge in edges:
|
|
a, b = edge[0], edge[1]
|
|
key = tuple(sorted([a, b]))
|
|
edge_set.add(key)
|
|
degree[a] += 1
|
|
degree[b] += 1
|
|
|
|
# Verify initial state of core nodes before patching
|
|
core_ids = {
|
|
"S-001", "S-010", "S-014", "S-027", "S-037", "S-047", "S-067",
|
|
"S-074", "S-091", "S-093", "S-111", "S-114", "S-118", "S-119",
|
|
"S-120", "S-139", "S-168", "S-181", "S-213", "S-235", "S-253",
|
|
"S-263", "S-265", "S-279", "S-297"
|
|
}
|
|
|
|
print("=== BEFORE: Core sector node degrees ===")
|
|
for nid in sorted(core_ids):
|
|
d = degree.get(nid, 0)
|
|
print(f" {nid}: deg={d}")
|
|
print()
|
|
|
|
# Check for duplicate edges in new list
|
|
duplicates = []
|
|
for edge in NEW_EDGES:
|
|
key = tuple(sorted(edge))
|
|
if key in edge_set:
|
|
duplicates.append(edge)
|
|
if duplicates:
|
|
print(f"WARNING: Duplicate edges (already exist): {duplicates}", file=sys.stderr)
|
|
|
|
# Add new edges
|
|
added = 0
|
|
for edge in NEW_EDGES:
|
|
key = tuple(sorted(edge))
|
|
if key not in edge_set:
|
|
edges.append(edge)
|
|
edge_set.add(key)
|
|
degree[edge[0]] += 1
|
|
degree[edge[1]] += 1
|
|
added += 1
|
|
else:
|
|
print(f" Skipping duplicate: {edge}")
|
|
|
|
print(f"Added {added} new edges (of {len(NEW_EDGES)} requested).")
|
|
print()
|
|
|
|
# Build node lookup by system_id
|
|
node_by_id = {n["system_id"]: n for n in nodes}
|
|
|
|
# Update topology/aperture/connections for all modified nodes
|
|
# We update ALL core nodes so the JSON is consistent.
|
|
print("=== Updating core node attributes ===")
|
|
|
|
# For S-001: keep hub, keep aperture=5, keep gate_connections=4 (special Gateway rule)
|
|
# For all others: update based on new degree.
|
|
|
|
# Track through_route nodes we want to preserve classification if degree stays 2
|
|
# (S-093 and S-253 and S-139 were through_route at deg=2; they're all getting upgraded,
|
|
# so this doesn't matter — they'll be junction now)
|
|
|
|
for nid in core_ids:
|
|
node = node_by_id.get(nid)
|
|
if node is None:
|
|
print(f" ERROR: {nid} not found in nodes!", file=sys.stderr)
|
|
continue
|
|
|
|
new_deg = degree.get(nid, 0)
|
|
|
|
if nid == "S-001":
|
|
# Special case: Gateway always hub, aperture=5, connections=4
|
|
new_topology = "hub"
|
|
new_connections = 4
|
|
new_apertures = 5
|
|
else:
|
|
new_topology = classify_topology(nid, new_deg)
|
|
new_connections = new_deg
|
|
new_apertures = aperture_for_connections(new_deg, nid)
|
|
|
|
old_topology = node["gate_topology"]
|
|
old_connections = node["gate_connections"]
|
|
|
|
if (old_topology != new_topology or
|
|
old_connections != new_connections):
|
|
print(f" {nid}: {old_topology}(c={old_connections}) → "
|
|
f"{new_topology}(c={new_connections}, ap={new_apertures})")
|
|
|
|
node["gate_topology"] = new_topology
|
|
node["gate_connections"] = new_connections
|
|
node["aperture_count"] = new_apertures
|
|
|
|
print()
|
|
|
|
# ── Overall distribution report ───────────────────────────────────────────
|
|
print("=== AFTER: Core sector topology breakdown ===")
|
|
topology_counts_core = defaultdict(int)
|
|
for nid in sorted(core_ids):
|
|
t = node_by_id[nid]["gate_topology"]
|
|
topology_counts_core[t] += 1
|
|
print(f" {nid}: deg={degree.get(nid,0)}, {t}")
|
|
print()
|
|
print("Core summary:")
|
|
for t, c in sorted(topology_counts_core.items()):
|
|
print(f" {t}: {c}")
|
|
print()
|
|
|
|
print("=== AFTER: Overall topology distribution ===")
|
|
topology_counts_all = defaultdict(int)
|
|
for node in nodes:
|
|
topology_counts_all[node["gate_topology"]] += 1
|
|
total = len(nodes)
|
|
for t, c in sorted(topology_counts_all.items()):
|
|
pct = 100.0 * c / total
|
|
print(f" {t}: {c} ({pct:.1f}%)")
|
|
print()
|
|
print(f"Total nodes: {total}")
|
|
print(f"Total edges: {len(edges)}")
|
|
print()
|
|
|
|
# Update meta
|
|
data["_meta"]["edge_count"] = len(edges)
|
|
data["_meta"]["core_balanced"] = "2026-03-13"
|
|
data["_meta"]["core_balance_note"] = (
|
|
"Core sector hand-balanced: +17 edges added, 25 core nodes reclassified. "
|
|
"3 hubs (Gateway + S-067 + S-235), 11 junctions, 8 spur_ends, 3 dead_ends."
|
|
)
|
|
|
|
# Write updated JSON
|
|
with open(JSON_PATH, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
print("Written to star-map.json.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|