"""Positive control for the angle-timing A1 pipeline (Claude acceptance 2026-10-01). Plants three day-precision events per case on dates when transit Saturn sits within 0.15 deg of the TRUE natal ascendant, then ranks the truth minute with the same ranks_for() used by A1. A working pipeline must rank it near 0; moving the same dates by whole years must return it to chance (~0.5). PYTHONHASHSEED=0 python3 scripts/research/angle_timing_positive_control.py """ import sys, statistics, random from datetime import date, timedelta sys.path.insert(0, ".") import swisseph as swe from scripts.research import angle_timing_lib as at from scripts.research.angle_timing_research import ranks_for cases = at.load_cases() res = {"real": [], "planted": [], "planted_shuffled_years": []} rng = random.Random(7) for case in cases: ch = at.CaseChart(case) asc_truth = float(ch.asc[at.RADIUS_MAX]) # find Saturn conjunction dates with the TRUE natal ascendant (tropical, both sides) d = ch.local.date() + timedelta(days=400); hits = [] prev = None while d < date(2026, 9, 1) and len(hits) < 3: jd = swe.julday(d.year, d.month, d.day, 12.0) s = float(at.aspect_sep(__import__("numpy").array([asc_truth]), at.planet_lon(jd, swe.SATURN))[0]) if s < 0.15 and (not hits or (d - hits[-1]).days > 200): hits.append(d) d += timedelta(days=2) if not hits: continue planted = [{"id": f"p{i}", "domain": "career", "precision": "day", "date": h.isoformat()} for i, h in enumerate(hits)] # same dates moved by random whole years (keeps precision, breaks the alignment) moved = [{**e, "date": (date.fromisoformat(e["date"]) + timedelta(days=365 * rng.choice([-3, -2, 2, 3]))).isoformat()} for e in planted] moved = [e for e in moved if ch.local.date() < date.fromisoformat(e["date"]) < date(2026, 9, 1)] for key, evs in (("planted", planted), ("planted_shuffled_years", moved)): r = ranks_for(ch, evs) res[key].append(r["TR"]["1.0"]["30"]) res["real"].append(ranks_for(ch, None)["TR"]["1.0"]["30"]) for k, v in res.items(): print(f"{k:24s} n={len(v):2d} mean truth rank (TR, 1°, ±30) = {statistics.mean(v):.3f} share with rank<0.15: {sum(1 for x in v if x < 0.15)/len(v):.2f}")