feat(docs): assign real GJ catalog stars to all 300 systems
Replaced fabricated designations with real Gliese-Jahreiss entries sourced from HYG Database v4.1. Selection: 215 G/K/F + 75 M + 10 unusual (white dwarfs, flare stars, subdwarfs) = 300 systems. Habitable star types (G/K/F) are preferred at each distance shell, spreading usable planets wider through the network. M-dwarfs fill remaining slots — station economies, mining, frontier outposts. Distance range: 4.2-46.8 ly. 51 named stars including Tau Ceti (Gateway), Alpha Centauri, Delta Pavonis, Barnard's Star, Sirius, Procyon, Arcturus, Pollux, Vega, Fomalhaut. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+207
-254
@@ -1,320 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
assign-astro-ids.py
|
||||
Assigns astronomical_id and proper_name fields to all 300 systems in star-map.json.
|
||||
Assigns real astronomical_id values to all 300 systems in star-map.json.
|
||||
|
||||
All systems use Gliese-Jahreiss (GJ) catalog designations as primary identifier.
|
||||
The GJ catalog is the catalog of nearby stars — a natural fit for the Settled Reach.
|
||||
All 300 systems get REAL Gliese-Jahreiss designations from the nearest
|
||||
300 GJ-cataloged stars (sourced from HYG Database v4.1).
|
||||
|
||||
Logic:
|
||||
- 36 real stars assigned by proximity/topology matching (closest to Gateway first)
|
||||
- ~264 remaining systems get fabricated GJ numbers
|
||||
- All assignments are deterministic (seeded RNG from system_id hash)
|
||||
- Gateway (S-001) gets GJ 71 (Tau Ceti)
|
||||
Assignment logic:
|
||||
- Gateway (S-001) pinned to GJ 71 (Tau Ceti)
|
||||
- Systems sorted by hop distance from Gateway + topology rank
|
||||
- Real stars sorted by distance from Sol
|
||||
- Closest stars → most central systems
|
||||
- Binary/companion entries (A/B suffixes) handled: one system per star system
|
||||
|
||||
Also writes spectral class from the real catalog into each node.
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
STAR_MAP_PATH = Path(__file__).parent.parent / "docs/design/star-map.json"
|
||||
CATALOG_PATH = Path(__file__).parent / "wiki/gj-catalog-real.json"
|
||||
|
||||
# ─── Real star pool ────────────────────────────────────────────────────────────
|
||||
# All nearby stars have GJ designations. Fields: (gj_id, proper_name, star_class, distance_ly, notes)
|
||||
REAL_STARS = [
|
||||
("GJ 551", "Proxima Centauri", "M", 4.2, None),
|
||||
("GJ 559A", "Alpha Centauri A", "G", 4.4, None),
|
||||
("GJ 559B", "Alpha Centauri B", "K", 4.4, "binary_companion"),
|
||||
("GJ 699", "Barnard's Star", "M", 5.9, None),
|
||||
("GJ 244", "Sirius", "A", 8.6, "too_hot"),
|
||||
("GJ 411", "Lalande 21185", "M", 8.3, None),
|
||||
("GJ 144", "Epsilon Eridani", "K", 10.5, None),
|
||||
("GJ 447", "Ross 128", "M", 10.9, None),
|
||||
("GJ 887", "Lacaille 9352", "M", 10.7, None),
|
||||
("GJ 280", "Procyon", "F", 11.4, None),
|
||||
("GJ 820", "61 Cygni", "K", 11.4, None),
|
||||
("GJ 845", "Epsilon Indi", "K", 11.8, None),
|
||||
("GJ 71", "Tau Ceti", "G", 11.9, "gateway"),
|
||||
("GJ 380", "Groombridge 1618", "K", 15.9, None),
|
||||
("GJ 702", "70 Ophiuchi", "K", 16.6, None),
|
||||
("GJ 166", "Omicron2 Eridani", "K", 16.4, None),
|
||||
("GJ 768", "Altair", "A", 16.8, "too_hot"),
|
||||
("GJ 764", "Sigma Draconis", "G", 18.8, None),
|
||||
("GJ 570", "Gliese 570", "K", 19.2, None),
|
||||
("GJ 139", "82 Eridani", "G", 19.8, None),
|
||||
("GJ 780", "Delta Pavonis", "G", 19.9, None),
|
||||
("GJ 667C", "Gliese 667 C", "M", 22.7, None),
|
||||
("GJ 68", "107 Piscium", "K", 24.4, None),
|
||||
("GJ 19", "Beta Hydri", "G", 24.4, None),
|
||||
("GJ 34", "Eta Cassiopeiae", "G", 24.6, None),
|
||||
("GJ 53", "Mu Cassiopeiae", "G", 24.6, None),
|
||||
("GJ 881", "Fomalhaut", "A", 25.1, "too_hot"),
|
||||
("GJ 721", "Vega", "A", 25.3, "too_hot"),
|
||||
("GJ 178", "Pi3 Orionis", "F", 26.2, None),
|
||||
("GJ 475", "Chara", "G", 27.4, None),
|
||||
("GJ 506", "61 Virginis", "G", 27.8, None),
|
||||
("GJ 250", "Gliese 250", "K", 28.4, None),
|
||||
("GJ 183", "HR 1614", "K", 28.4, None),
|
||||
("GJ 137", "Kappa1 Ceti", "G", 29.8, None),
|
||||
("GJ 451", "Groombridge 1830", "G", 29.9, None),
|
||||
("GJ 2046", "HD 40307", "K", 42.0, None),
|
||||
]
|
||||
# Pinned assignments: system_id -> gj_id
|
||||
PINNED = {
|
||||
"S-001": "GJ 71", # Tau Ceti — Gateway
|
||||
}
|
||||
|
||||
# Collect all reserved GJ numbers (as strings for consistent comparison)
|
||||
RESERVED_GJ = set()
|
||||
for cat_id, _, _, _, _ in REAL_STARS:
|
||||
RESERVED_GJ.add(cat_id)
|
||||
|
||||
|
||||
def system_rng(system_id: str, salt: str = "") -> random.Random:
|
||||
"""Return a seeded RNG deterministic for this system_id + salt."""
|
||||
h = hashlib.sha256(f"{system_id}:{salt}".encode()).digest()
|
||||
seed = int.from_bytes(h[:8], "big")
|
||||
return random.Random(seed)
|
||||
|
||||
|
||||
def fabricate_gj(system_id: str, used: set[str]) -> str:
|
||||
"""
|
||||
Fabricate a plausible GJ catalog designation.
|
||||
|
||||
Uses the full range of GJ numbering styles for authenticity:
|
||||
- GJ 1-999: main catalog (sparse — most taken by real stars)
|
||||
- GJ 1001-1299: southern extension
|
||||
- GJ 2001-2159: supplement (Gliese & Jahreiss 1979)
|
||||
- GJ 3001-3999: supplement (CNS3, faint nearby stars)
|
||||
- GJ 4001-4383: supplement (CNS4)
|
||||
- GJ 5001+: our fabrication range for "post-settlement survey"
|
||||
|
||||
We weight toward the supplementary catalogs (3xxx, 4xxx, 5xxx) since
|
||||
the main catalog numbers are more likely to collide with real stars.
|
||||
"""
|
||||
rng = system_rng(system_id, "GJ_fab")
|
||||
|
||||
# Weighted range selection:
|
||||
# 10% main (500-999), 10% southern (1001-1299), 10% supplement2 (2001-2159),
|
||||
# 30% supplement3 (3001-3999), 15% supplement4 (4001-4383), 25% post-settlement (5001-6500)
|
||||
ranges = [
|
||||
(500, 999, 0.10),
|
||||
(1001, 1299, 0.10),
|
||||
(2001, 2159, 0.10),
|
||||
(3001, 3999, 0.30),
|
||||
(4001, 4383, 0.15),
|
||||
(5001, 6500, 0.25),
|
||||
]
|
||||
|
||||
for _ in range(2000):
|
||||
# Pick a range by weight
|
||||
roll = rng.random()
|
||||
cumulative = 0.0
|
||||
lo, hi = 3001, 3999 # default
|
||||
for r_lo, r_hi, weight in ranges:
|
||||
cumulative += weight
|
||||
if roll < cumulative:
|
||||
lo, hi = r_lo, r_hi
|
||||
break
|
||||
|
||||
n = rng.randint(lo, hi)
|
||||
candidate = f"GJ {n}"
|
||||
if candidate not in RESERVED_GJ and candidate not in used:
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
|
||||
# Fallback: scan upward in post-settlement range
|
||||
n = 5001
|
||||
while f"GJ {n}" in RESERVED_GJ or f"GJ {n}" in used:
|
||||
n += 1
|
||||
candidate = f"GJ {n}"
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
# Stars to skip (companions that share a system with their primary)
|
||||
SKIP_COMPANIONS = {"GJ 244B", "GJ 65B"} # Sirius B, Luyten 726-8 B
|
||||
|
||||
|
||||
def topology_rank(node: dict) -> int:
|
||||
"""Higher = more notable for real-star assignment priority."""
|
||||
scores = {"hub": 5, "junction": 4, "loop_member": 3, "through_route": 2, "spur_end": 1, "dead_end": 0}
|
||||
return scores.get(node.get("gate_topology", ""), 0)
|
||||
|
||||
|
||||
def simplify_spectral(spect: str) -> str:
|
||||
"""Convert detailed spectral class to simplified star_type for the game.
|
||||
|
||||
Returns: G, K, M, F, A, binary, unusual
|
||||
"""
|
||||
if not spect:
|
||||
return "M" # default for unknowns (most nearby stars are M-dwarfs)
|
||||
|
||||
s = spect.strip()
|
||||
# Check first character for main spectral class
|
||||
first = s[0].upper() if s else "?"
|
||||
|
||||
if first == "D":
|
||||
# White dwarf — unusual
|
||||
return "unusual"
|
||||
if first == "S" and s.startswith("sd"):
|
||||
# Subdwarf — map to the next character
|
||||
first = s[2].upper() if len(s) > 2 else "?"
|
||||
if first in ("M", "m"):
|
||||
return "M"
|
||||
if first == "K":
|
||||
return "K"
|
||||
if first == "G":
|
||||
return "G"
|
||||
if first == "F":
|
||||
return "F"
|
||||
if first == "A":
|
||||
return "F" # A-types are rare; group with F for game purposes
|
||||
if first in ("O", "B"):
|
||||
return "unusual"
|
||||
|
||||
return "M" # default for unrecognized
|
||||
|
||||
|
||||
def main():
|
||||
# Load star map
|
||||
with open(STAR_MAP_PATH, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
nodes = data["nodes"]
|
||||
|
||||
# ─── Step 1: Sort systems for real-star matching ──────────────────────────
|
||||
sortable = sorted(nodes, key=lambda n: (
|
||||
# Load real GJ catalog
|
||||
with open(CATALOG_PATH, "r") as f:
|
||||
catalog = json.load(f)
|
||||
real_stars = catalog["stars"]
|
||||
|
||||
# Filter out companions and build available pool
|
||||
pool = []
|
||||
seen_gj = set()
|
||||
for star in real_stars:
|
||||
gj = star["gj_id"]
|
||||
if gj in SKIP_COMPANIONS:
|
||||
continue
|
||||
if gj in seen_gj:
|
||||
continue
|
||||
seen_gj.add(gj)
|
||||
pool.append(star)
|
||||
|
||||
print(f"Real GJ catalog: {len(real_stars)} entries, {len(pool)} unique after dedup")
|
||||
print(f"Systems to assign: {len(nodes)}")
|
||||
|
||||
if len(pool) < len(nodes):
|
||||
print(f"WARNING: Not enough real stars ({len(pool)}) for {len(nodes)} systems!")
|
||||
print(f"Will need {len(nodes) - len(pool)} fabricated entries.")
|
||||
|
||||
# Sort pool by distance (already sorted, but be explicit)
|
||||
pool.sort(key=lambda s: s["dist_pc"])
|
||||
|
||||
# Sort systems: hop distance ascending, topology rank descending
|
||||
sorted_nodes = sorted(nodes, key=lambda n: (
|
||||
n.get("hop_distance_from_gateway", 999),
|
||||
-topology_rank(n),
|
||||
n["system_id"]
|
||||
))
|
||||
|
||||
# ─── Step 2: Pin Gateway to Tau Ceti (GJ 71) ─────────────────────────────
|
||||
gateway_star = next(s for s in REAL_STARS if s[4] == "gateway")
|
||||
assigned_catalog_ids: set[str] = {gateway_star[0]}
|
||||
assignments: dict[str, tuple[str, str | None]] = {}
|
||||
assignments["S-001"] = (gateway_star[0], gateway_star[1])
|
||||
# Build assignments
|
||||
assignments: dict[str, dict] = {}
|
||||
|
||||
# ─── Step 3: Separate hot A-type stars for special handling ───────────────
|
||||
hot_stars = [s for s in REAL_STARS if s[4] == "too_hot"]
|
||||
normal_stars = [s for s in REAL_STARS if s[4] not in ("gateway", "too_hot", "binary_companion")]
|
||||
# Step 1: Pin fixed assignments
|
||||
for sid, gj_id in PINNED.items():
|
||||
star = next((s for s in pool if s["gj_id"] == gj_id), None)
|
||||
if star:
|
||||
assignments[sid] = star
|
||||
else:
|
||||
print(f"WARNING: Pinned star {gj_id} not found in catalog!")
|
||||
|
||||
# Sort normal stars by distance from Sol
|
||||
normal_stars_sorted = sorted(normal_stars, key=lambda s: s[3])
|
||||
# Step 2: Find a binary-type system for Alpha Centauri (GJ 559A)
|
||||
alpha_a = next((s for s in pool if s["gj_id"] == "GJ 559A"), None)
|
||||
if alpha_a:
|
||||
binary_systems = [n for n in sorted_nodes
|
||||
if n.get("star_type") == "binary"
|
||||
and n["system_id"] not in assignments]
|
||||
if binary_systems:
|
||||
bs = binary_systems[0]
|
||||
assignments[bs["system_id"]] = alpha_a
|
||||
print(f" Binary: {bs['system_id']} -> GJ 559A (Alpha Centauri)")
|
||||
|
||||
# ─── Step 4: Handle Alpha Centauri binary ─────────────────────────────────
|
||||
alpha_a = next(s for s in REAL_STARS if s[0] == "GJ 559A")
|
||||
alpha_b = next(s for s in REAL_STARS if s[0] == "GJ 559B")
|
||||
# Step 3: Distance-first with habitability preference
|
||||
#
|
||||
# The 300 nearest GJ stars are our pool. We want distance to drive
|
||||
# centrality (closest stars = core systems), but within each distance
|
||||
# band, prefer G/K/F stars over M-dwarfs. This means the habitable
|
||||
# stars get picked first at each distance layer, pushing the M-dwarfs
|
||||
# to fill later (more peripheral) slots. The net effect: the web of
|
||||
# systems with usable planets spreads wider through the network because
|
||||
# we're cherry-picking the good ones from each distance shell.
|
||||
#
|
||||
# Implementation: sort the full pool by distance, but with a small
|
||||
# penalty for M-dwarfs that pushes them down within their distance
|
||||
# neighborhood without disrupting the overall distance ordering.
|
||||
used_gj = {a["gj_id"] for a in assignments.values()}
|
||||
remaining_pool = [s for s in pool if s["gj_id"] not in used_gj]
|
||||
|
||||
# Find a binary-type system within hop 5 for Alpha Centauri
|
||||
binary_systems = [n for n in sortable if n.get("star_type") == "binary"]
|
||||
binary_assigned = False
|
||||
for bs in binary_systems:
|
||||
if bs["system_id"] == "S-001":
|
||||
continue
|
||||
if bs.get("hop_distance_from_gateway", 999) <= 5:
|
||||
assignments[bs["system_id"]] = (alpha_a[0], alpha_a[1])
|
||||
assigned_catalog_ids.add(alpha_a[0])
|
||||
assigned_catalog_ids.add(alpha_b[0]) # mark companion as used
|
||||
binary_assigned = True
|
||||
print(f" Binary: {bs['system_id']} -> {alpha_a[0]} (Alpha Centauri A/B)")
|
||||
break
|
||||
def habitability_penalty(star: dict) -> float:
|
||||
"""Small distance penalty for less habitable star types.
|
||||
|
||||
if not binary_assigned:
|
||||
# No binary found; Alpha A stays in normal pool, skip B
|
||||
assigned_catalog_ids.add(alpha_b[0])
|
||||
print(" No binary system within hop 5; Alpha Centauri B skipped.")
|
||||
G/K: no penalty (0 pc). These get picked first at their distance.
|
||||
F: tiny penalty (0.5 pc). Still good, slight deprioritization.
|
||||
M: moderate penalty (3 pc). Pushed down ~3 pc worth of slots.
|
||||
unusual/WD: larger penalty (5 pc). Pushed further down.
|
||||
|
||||
# Remove already-assigned stars from normal pool
|
||||
normal_pool = [s for s in normal_stars_sorted if s[0] not in assigned_catalog_ids]
|
||||
At the scale of our pool (4-33 ly / 1.3-10 pc), a 3 pc penalty
|
||||
means an M-dwarf at 5 pc sorts like it's at 8 pc — it gets
|
||||
overtaken by G/K stars up to ~8 pc but stays ahead of G/K stars
|
||||
at 10+ pc. This is a gentle preference, not a hard filter.
|
||||
"""
|
||||
sp = simplify_spectral(star.get("spect", ""))
|
||||
if sp in ("G", "K"):
|
||||
return 0.0
|
||||
if sp == "F":
|
||||
return 0.5
|
||||
if sp == "M":
|
||||
return 3.0
|
||||
return 5.0 # unusual, white dwarfs
|
||||
|
||||
# ─── Step 5: Assign normal real stars by proximity ────────────────────────
|
||||
remaining_pool.sort(key=lambda s: s["dist_pc"] + habitability_penalty(s))
|
||||
|
||||
# Assign unassigned systems (sorted by importance) from the reordered pool
|
||||
pool_idx = 0
|
||||
for node in sortable:
|
||||
if pool_idx >= len(normal_pool):
|
||||
break
|
||||
for node in sorted_nodes:
|
||||
sid = node["system_id"]
|
||||
if sid in assignments:
|
||||
continue
|
||||
if pool_idx >= len(remaining_pool):
|
||||
break
|
||||
|
||||
star = normal_pool[pool_idx]
|
||||
assignments[sid] = (star[0], star[1])
|
||||
assigned_catalog_ids.add(star[0])
|
||||
assignments[sid] = remaining_pool[pool_idx]
|
||||
pool_idx += 1
|
||||
|
||||
# ─── Step 6: Assign hot A-type stars to notable hubs/junctions ────────────
|
||||
hub_junction_candidates = sorted(
|
||||
[n for n in nodes if n["system_id"] not in assignments
|
||||
and n.get("gate_topology") in ("hub", "junction")],
|
||||
key=lambda n: (-topology_rank(n), n.get("hop_distance_from_gateway", 999))
|
||||
)
|
||||
# Step 4: If we ran out of real stars, fabricate the remainder
|
||||
fabricated = 0
|
||||
if len(assignments) < len(nodes):
|
||||
# Generate fabricated GJ numbers in the 5000+ range
|
||||
import random
|
||||
rng = random.Random(20260313)
|
||||
used_nums = set()
|
||||
for a in assignments.values():
|
||||
gj = a["gj_id"]
|
||||
try:
|
||||
num = int(gj.replace("GJ ", "").rstrip("ABC"))
|
||||
used_nums.add(num)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
for i, hot_star in enumerate(hot_stars):
|
||||
if i < len(hub_junction_candidates):
|
||||
sid = hub_junction_candidates[i]["system_id"]
|
||||
else:
|
||||
# Fallback: any unassigned system
|
||||
sid = next(n["system_id"] for n in sortable if n["system_id"] not in assignments)
|
||||
assignments[sid] = (hot_star[0], hot_star[1])
|
||||
assigned_catalog_ids.add(hot_star[0])
|
||||
for node in sorted_nodes:
|
||||
sid = node["system_id"]
|
||||
if sid in assignments:
|
||||
continue
|
||||
# Fabricate in 5000-8000 range
|
||||
while True:
|
||||
n = rng.randint(5001, 8000)
|
||||
if n not in used_nums:
|
||||
used_nums.add(n)
|
||||
break
|
||||
assignments[sid] = {
|
||||
"gj_id": f"GJ {n}",
|
||||
"proper_name": None,
|
||||
"dist_pc": 0,
|
||||
"dist_ly": 0,
|
||||
"spect": "",
|
||||
"fabricated": True,
|
||||
}
|
||||
fabricated += 1
|
||||
|
||||
real_count = len(assignments)
|
||||
print(f"\nReal star assignments: {real_count}")
|
||||
|
||||
# ─── Step 7: Fabricate GJ IDs for remaining systems ───────────────────────
|
||||
used_gj: set[str] = set(RESERVED_GJ) # protect all real GJ numbers
|
||||
|
||||
fabricated_count = 0
|
||||
# Step 5: Apply to nodes
|
||||
for node in nodes:
|
||||
sid = node["system_id"]
|
||||
if sid not in assignments:
|
||||
fab_id = fabricate_gj(sid, used_gj)
|
||||
assignments[sid] = (fab_id, None)
|
||||
fabricated_count += 1
|
||||
star = assignments[sid]
|
||||
node["astronomical_id"] = star["gj_id"]
|
||||
node["proper_name"] = star.get("proper_name")
|
||||
node["dist_ly"] = round(star.get("dist_ly", 0), 1)
|
||||
node["spectral_class"] = star.get("spect", "")
|
||||
|
||||
# ─── Step 8: Apply to nodes ───────────────────────────────────────────────
|
||||
for node in nodes:
|
||||
sid = node["system_id"]
|
||||
cat_id, proper = assignments[sid]
|
||||
node["astronomical_id"] = cat_id
|
||||
node["proper_name"] = proper
|
||||
|
||||
# ─── Step 9: Write back ───────────────────────────────────────────────────
|
||||
# Step 6: Write back
|
||||
with open(STAR_MAP_PATH, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
# ─── Step 10: Summary report ──────────────────────────────────────────────
|
||||
# Step 7: Summary
|
||||
real_count = len(assignments) - fabricated
|
||||
print(f"\n{'='*60}")
|
||||
print("ASTRONOMICAL ID ASSIGNMENT COMPLETE")
|
||||
print(f"{'='*60}")
|
||||
print(f"Total systems: {len(nodes)}")
|
||||
print(f"Real star assigned: {real_count}")
|
||||
print(f"Fabricated: {fabricated_count}")
|
||||
print(f"Real GJ assigned: {real_count}")
|
||||
print(f"Fabricated: {fabricated}")
|
||||
print(f"Distance range: {pool[0]['dist_ly']:.1f} — {pool[min(len(nodes)-1, len(pool)-1)]['dist_ly']:.1f} ly")
|
||||
|
||||
# GJ range breakdown
|
||||
range_counts = {"main (1-999)": 0, "southern (1001-1299)": 0, "supp2 (2001-2159)": 0,
|
||||
"supp3 (3001-3999)": 0, "supp4 (4001-4383)": 0, "post-settlement (5001+)": 0, "special": 0}
|
||||
for node in nodes:
|
||||
aid = node["astronomical_id"]
|
||||
if not aid.startswith("GJ "):
|
||||
range_counts["special"] += 1
|
||||
continue
|
||||
# Parse the number
|
||||
num_str = aid[3:].rstrip("ABC")
|
||||
try:
|
||||
num = int(num_str)
|
||||
except ValueError:
|
||||
range_counts["special"] += 1
|
||||
continue
|
||||
if num <= 999:
|
||||
range_counts["main (1-999)"] += 1
|
||||
elif num <= 1299:
|
||||
range_counts["southern (1001-1299)"] += 1
|
||||
elif num <= 2159:
|
||||
range_counts["supp2 (2001-2159)"] += 1
|
||||
elif num <= 3999:
|
||||
range_counts["supp3 (3001-3999)"] += 1
|
||||
elif num <= 4383:
|
||||
range_counts["supp4 (4001-4383)"] += 1
|
||||
else:
|
||||
range_counts["post-settlement (5001+)"] += 1
|
||||
|
||||
print("\nGJ range distribution:")
|
||||
for label, count in range_counts.items():
|
||||
if count > 0:
|
||||
pct = count / len(nodes) * 100
|
||||
print(f" {label:28s} {count:3d} ({pct:.1f}%)")
|
||||
|
||||
# Real star assignment table
|
||||
print(f"\nReal star assignments by hop distance:")
|
||||
print(f" {'System':8s} {'Hop':4s} {'Topology':14s} {'Type':6s} {'GJ ID':10s} {'Proper Name'}")
|
||||
print(f" {'─'*8} {'─'*4} {'─'*14} {'─'*6} {'─'*10} {'─'*25}")
|
||||
|
||||
real_sids = {sid for sid, (_, name) in assignments.items() if name is not None}
|
||||
# Named stars
|
||||
named = [(sid, a) for sid, a in assignments.items() if a.get("proper_name")]
|
||||
print(f"\nNamed stars ({len(named)}):")
|
||||
node_by_id = {n["system_id"]: n for n in nodes}
|
||||
for sid, star in sorted(named, key=lambda x: x[1].get("dist_ly", 0)):
|
||||
n = node_by_id[sid]
|
||||
print(f" {star['gj_id']:12s} {star['proper_name']:25s} {star['dist_ly']:5.1f} ly {n['gate_topology']:14s} {sid}")
|
||||
|
||||
for node in sorted(
|
||||
[node_by_id[sid] for sid in real_sids],
|
||||
key=lambda n: (n.get("hop_distance_from_gateway", 999), n["system_id"])
|
||||
):
|
||||
sid = node["system_id"]
|
||||
hop = node.get("hop_distance_from_gateway", "?")
|
||||
topo = node.get("gate_topology", "?")
|
||||
stype = node.get("star_type", "?")
|
||||
cat = node["astronomical_id"]
|
||||
name = node["proper_name"]
|
||||
print(f" {sid:8s} {str(hop):4s} {topo:14s} {stype:6s} {cat:10s} {name}")
|
||||
# Spectral type distribution
|
||||
type_counts: dict[str, int] = {}
|
||||
for node in nodes:
|
||||
sp = simplify_spectral(node.get("spectral_class", ""))
|
||||
type_counts[sp] = type_counts.get(sp, 0) + 1
|
||||
print(f"\nSimplified spectral distribution:")
|
||||
for t in ["G", "K", "M", "F", "unusual"]:
|
||||
print(f" {t:8s} {type_counts.get(t, 0):3d} ({type_counts.get(t, 0)/len(nodes)*100:.0f}%)")
|
||||
|
||||
# Verify no duplicates
|
||||
all_astro_ids = [n["astronomical_id"] for n in nodes]
|
||||
unique_ids = set(all_astro_ids)
|
||||
if len(unique_ids) != len(all_astro_ids):
|
||||
dupes = [aid for aid in all_astro_ids if all_astro_ids.count(aid) > 1]
|
||||
print(f"\nWARNING: {len(all_astro_ids) - len(unique_ids)} duplicate astronomical_id(s)!")
|
||||
for d in sorted(set(dupes)):
|
||||
print(f" DUPE: {d}")
|
||||
# Collision check
|
||||
all_ids = [n["astronomical_id"] for n in nodes]
|
||||
if len(set(all_ids)) != len(all_ids):
|
||||
dupes = [x for x in all_ids if all_ids.count(x) > 1]
|
||||
print(f"\nWARNING: Duplicates found: {set(dupes)}")
|
||||
else:
|
||||
print(f"\nCollision check: PASSED — all {len(unique_ids)} astronomical_ids unique.")
|
||||
print(f"\nCollision check: PASSED — all {len(set(all_ids))} unique.")
|
||||
|
||||
print(f"\nWritten to: {STAR_MAP_PATH}")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user