Finalizes #849 core-world atlas cohesion: GJ0d (Earth/Sol) markers.json cleaned of erroneous data, refine_log updated with Sol body gap notes, atlas_quality_analysis.py added for ongoing metric tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
249 lines
8.9 KiB
Python
249 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
atlas_quality_analysis.py — Atlas content quality audit for Sprint 36 (#849/#838).
|
||
|
||
Queries atlas_* tables in systems.db and reports on:
|
||
1. Cross-body name collisions (same name, same feature type, different bodies)
|
||
2. Cardinal/directional name density per body
|
||
3. Generic/lazy name patterns
|
||
4. Earth-echo concentration in high-visibility systems
|
||
5. Top-stem frequency across all named features
|
||
|
||
Usage:
|
||
python3 tooling/planet-gen/atlas_quality_analysis.py [--db server/data/systems.db]
|
||
python3 tooling/planet-gen/atlas_quality_analysis.py --system GJ380
|
||
python3 tooling/planet-gen/atlas_quality_analysis.py --top-collisions 20
|
||
python3 tooling/planet-gen/atlas_quality_analysis.py --body GJ71c
|
||
|
||
D-191 §8: markers.json is pixel-space [row, col] against 512×256.
|
||
Re-run after any hand-refine pass to verify improvements.
|
||
"""
|
||
|
||
import argparse
|
||
import re
|
||
import sqlite3
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
||
DEFAULT_DB = REPO_ROOT / "server" / "data" / "systems.db"
|
||
|
||
CARDINAL_RE = re.compile(
|
||
r"\b(north|south|east|west|eastern|western|northern|southern|"
|
||
r"upper|lower|new|great|old|central|inner|outer|kita|minami|higashi|nishi)\b",
|
||
re.I,
|
||
)
|
||
GENERIC_RE = re.compile(
|
||
r"\b(hilly|sector|zone|district)\b"
|
||
r"|^(great|the great|hilly)\b"
|
||
r"|^(valley floor|ridge line|ridge crest|flat ground|riverbend)$",
|
||
re.I,
|
||
)
|
||
EARTH_ECHO_RE = re.compile(
|
||
r"\b(manchester|london|paris|berlin|tokyo|beijing|new york|sydney|dubai|"
|
||
r"route \d+|sector \d+|block \d+)\b",
|
||
re.I,
|
||
)
|
||
FEATURE_TABLES = [
|
||
("atlas_cities", "city"),
|
||
("atlas_rivers", "river"),
|
||
("atlas_oceans", "ocean"),
|
||
("atlas_mountain_ranges", "mountain"),
|
||
]
|
||
|
||
|
||
def open_db(path: str) -> sqlite3.Connection:
|
||
return sqlite3.connect(path)
|
||
|
||
|
||
def build_body_index(conn: sqlite3.Connection) -> dict:
|
||
c = conn.cursor()
|
||
c.execute(
|
||
"SELECT body_id, system_id, proper_name, cultural_corridor, population "
|
||
"FROM bodies WHERE inhabited=1"
|
||
)
|
||
return {
|
||
r[0]: {"system_id": r[1], "name": r[2], "corridor": r[3], "pop": r[4]}
|
||
for r in c.fetchall()
|
||
}
|
||
|
||
|
||
def gather_all_names(conn: sqlite3.Connection) -> dict[str, list[tuple[str, str, str]]]:
|
||
"""body_id → [(feature_type, name, local_id), ...]"""
|
||
c = conn.cursor()
|
||
result = defaultdict(list)
|
||
for tbl, feat_type in FEATURE_TABLES:
|
||
try:
|
||
c.execute(f"SELECT body_id, name, local_id FROM {tbl} WHERE name IS NOT NULL AND name != ''")
|
||
for body_id, name, local_id in c.fetchall():
|
||
result[body_id].append((feat_type, name, local_id))
|
||
except sqlite3.OperationalError:
|
||
pass
|
||
return result
|
||
|
||
|
||
def cross_body_collisions(conn: sqlite3.Connection, limit: int = 20) -> dict:
|
||
c = conn.cursor()
|
||
collisions = {}
|
||
for tbl, feat_type in FEATURE_TABLES:
|
||
try:
|
||
c.execute(
|
||
f"SELECT name, COUNT(DISTINCT body_id) as cnt, GROUP_CONCAT(DISTINCT body_id) "
|
||
f"FROM {tbl} WHERE name IS NOT NULL AND name != '' "
|
||
f"GROUP BY name HAVING cnt > 1 ORDER BY cnt DESC LIMIT ?",
|
||
(limit,),
|
||
)
|
||
collisions[feat_type] = [(r[0], r[1], r[2]) for r in c.fetchall()]
|
||
except sqlite3.OperationalError:
|
||
collisions[feat_type] = []
|
||
return collisions
|
||
|
||
|
||
def stem_frequency(names: list[str], top_n: int = 30) -> list[tuple[str, int]]:
|
||
stems = Counter()
|
||
for name in names:
|
||
words = name.split()
|
||
if words:
|
||
stems[words[0].lower()] += 1
|
||
return stems.most_common(top_n)
|
||
|
||
|
||
def body_quality_report(body_id: str, names: list[tuple], conn: sqlite3.Connection) -> dict:
|
||
total = len(names)
|
||
if total == 0:
|
||
return {}
|
||
cardinal = sum(1 for _, n, _ in names if CARDINAL_RE.search(n))
|
||
generic = sum(1 for _, n, _ in names if GENERIC_RE.search(n))
|
||
earth = sum(1 for _, n, _ in names if EARTH_ECHO_RE.search(n))
|
||
|
||
c = conn.cursor()
|
||
# collision count: how many of this body's names appear on other bodies (same type)
|
||
colliding = 0
|
||
for feat_type, name, _ in names:
|
||
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
|
||
try:
|
||
c.execute(
|
||
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
|
||
(name, body_id),
|
||
)
|
||
others = c.fetchone()[0]
|
||
if others > 0:
|
||
colliding += 1
|
||
except sqlite3.OperationalError:
|
||
pass
|
||
|
||
return {
|
||
"total": total,
|
||
"cardinal": cardinal,
|
||
"cardinal_pct": cardinal / total,
|
||
"generic": generic,
|
||
"earth_echo": earth,
|
||
"colliding": colliding,
|
||
"colliding_pct": colliding / total,
|
||
}
|
||
|
||
|
||
def run_analysis(args):
|
||
conn = open_db(args.db)
|
||
body_index = build_body_index(conn)
|
||
all_names_by_body = gather_all_names(conn)
|
||
|
||
# Filter by system or body if requested
|
||
if args.system:
|
||
body_index = {k: v for k, v in body_index.items() if v["system_id"] == args.system}
|
||
if args.body:
|
||
body_index = {k: v for k, v in body_index.items() if k == args.body}
|
||
|
||
print("=" * 70)
|
||
print("ATLAS QUALITY ANALYSIS — The Settled Reach (#849/#838)")
|
||
print(f"DB: {args.db}")
|
||
if args.system:
|
||
print(f"Filter: system={args.system}")
|
||
if args.body:
|
||
print(f"Filter: body={args.body}")
|
||
print("=" * 70)
|
||
|
||
# --- 1. Cross-body collisions ---
|
||
print("\n[ 1. CROSS-BODY NAME COLLISIONS ]")
|
||
collisions = cross_body_collisions(conn, limit=args.top_collisions)
|
||
for feat_type, rows in collisions.items():
|
||
if rows:
|
||
print(f"\n {feat_type}:")
|
||
for name, cnt, bodies in rows:
|
||
print(f" '{name}' — {cnt} bodies: {bodies[:80]}")
|
||
|
||
# --- 2. Per-body quality scores ---
|
||
print("\n[ 2. BODY QUALITY SCORES — ranked by collision % ]")
|
||
reports = []
|
||
for bid, info in body_index.items():
|
||
names = all_names_by_body.get(bid, [])
|
||
if not names:
|
||
continue
|
||
report = body_quality_report(bid, names, conn)
|
||
if not report:
|
||
continue
|
||
reports.append((bid, info, report))
|
||
|
||
reports.sort(key=lambda x: -x[2]["colliding_pct"])
|
||
|
||
print(f"\n {'Body':<28} {'System':<12} {'Corridor':<15} "
|
||
f"{'Coll%':>6} {'Card%':>6} {'Gen':>4} {'Echo':>4}")
|
||
for bid, info, rep in reports[:30]:
|
||
print(
|
||
f" {(info['name'] or bid):<28} {info['system_id']:<12} {info['corridor'] or '?':<15} "
|
||
f"{rep['colliding_pct']:>6.0%} {rep['cardinal_pct']:>6.0%} "
|
||
f"{rep['generic']:>4} {rep['earth_echo']:>4}"
|
||
)
|
||
|
||
# --- 3. Stem frequency ---
|
||
print("\n[ 3. TOP STEM FREQUENCY (first word of name) ]")
|
||
all_names_flat = [n for names in all_names_by_body.values() for _, n, _ in names]
|
||
for stem, cnt in stem_frequency(all_names_flat, top_n=20):
|
||
print(f" {stem:<20} {cnt}")
|
||
|
||
# --- 4. Detailed body report (if --body specified) ---
|
||
if args.body and args.body in all_names_by_body:
|
||
bid = args.body
|
||
info = body_index.get(bid, {})
|
||
names = all_names_by_body[bid]
|
||
print(f"\n[ 4. DETAILED REPORT: {bid} ({info.get('name', '?')}) ]")
|
||
c = conn.cursor()
|
||
for feat_type, name, local_id in sorted(names, key=lambda x: x[0]):
|
||
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
|
||
c.execute(
|
||
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
|
||
(name, bid),
|
||
)
|
||
others = c.fetchone()[0]
|
||
flag = f" *** COLLISION ×{others}" if others > 0 else ""
|
||
cardinal = " [cardinal]" if CARDINAL_RE.search(name) else ""
|
||
generic = " [generic]" if GENERIC_RE.search(name) else ""
|
||
print(f" {feat_type:<10} {local_id:<12} {name}{flag}{cardinal}{generic}")
|
||
|
||
# --- 5. Sol gap check ---
|
||
print("\n[ 5. SOL SYSTEM GAP CHECK ]")
|
||
c = conn.cursor()
|
||
c.execute("SELECT body_id, proper_name, population FROM bodies WHERE system_id='GJ 0' AND inhabited=1")
|
||
sol_bodies = c.fetchall()
|
||
for bid, bname, pop in sol_bodies:
|
||
has_cities = bid in all_names_by_body and any(f == "city" for f, _, _ in all_names_by_body[bid])
|
||
status = "HAS DATA" if has_cities else "*** EMPTY — needs authoring"
|
||
print(f" {bid:<15} {bname or '?':<20} pop={pop or '?'} {status}")
|
||
|
||
conn.close()
|
||
print("\nDone.")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
parser.add_argument("--db", default=str(DEFAULT_DB), help="Path to systems.db")
|
||
parser.add_argument("--system", help="Filter to one system (e.g. GJ380)")
|
||
parser.add_argument("--body", help="Filter to one body (e.g. GJ71c)")
|
||
parser.add_argument("--top-collisions", type=int, default=15, help="Collision list limit")
|
||
args = parser.parse_args()
|
||
run_analysis(args)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|