Files
Jyotisha/scripts/research/nadi_seconds_run.py
T
jesse-ux 217cd4a767 research(rectification): nadi second-level study helpers and N0 checks (BUG-1240)
Five-level Vimshottari and equal-division D150 helpers, plus a runner that
refuses to score until the preregistration file exists. First three lords
match the production chain on the v5 events. No production files changed.
2026-10-05 00:58:00 +08:00

677 lines
31 KiB
Python

#!/usr/bin/env python3
"""Run the pre-registered nadi / sookshma / prana falsification study.
Reads ``docs/research/nadi_seconds_preregistration_2026_10_05.json`` and
refuses to score without it. The output JSON has no timestamps, so two runs
with ``PYTHONHASHSEED=0`` are byte-identical. This script does not import
``true_minute``.
"""
from __future__ import annotations
import argparse
import json
import sys
import traceback
from datetime import date
from pathlib import Path
from random import Random
from typing import Any, Mapping, Sequence
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.research import nadi_seconds_lib as lib # noqa: E402
REPORT = ROOT / "docs" / "research" / "nadi_seconds_results_2026_10_05.json"
def load_prereg() -> dict[str, Any]:
if not lib.PREREG_PATH.is_file():
raise SystemExit(f"missing preregistration: {lib.PREREG_PATH}")
return json.loads(lib.PREREG_PATH.read_text(encoding="utf-8"))
def offsets_of(radius: int, step: int) -> list[int]:
return list(range(-int(radius), int(radius) + 1, int(step)))
def truth_minute_offsets() -> list[int]:
return list(range(0, 60))
def collect_skies(birth: Mapping[str, Any], offsets: Sequence[int], **kwargs: Any) -> dict[int, dict[str, Any]]:
return {int(offset): lib.sky_at(birth, lib.candidate_moment(birth, int(offset)), **kwargs) for offset in offsets}
def lord_bundle(skies: Mapping[int, Mapping[str, Any]], events: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
lords: dict[int, list] = {}
asc: dict[int, int] = {}
segment: dict[int, str] = {}
part: dict[int, int] = {}
for offset, sky in skies.items():
lords[int(offset)] = lib.lords_for_events(str(sky["birth_date"]), float(sky["moon_lon"]), events)
asc[int(offset)] = int(sky["asc_index"])
row = lib.d150_equal(float(sky["asc_lon"]))
segment[int(offset)] = str(row["sign_lord"])
part[int(offset)] = int(row["part_index"])
return {"lords": lords, "asc": asc, "segment": segment, "part": part}
def rule_counts(bundle: Mapping[str, Any], events: Sequence[Mapping[str, Any]], rule: str) -> dict[int, int]:
domains = [str(event["domain"]) for event in events]
use_segment = rule.startswith("N5")
counts: dict[int, int] = {}
for offset, rows in bundle["lords"].items():
targets = [lib.domain_target_lords(bundle["asc"][offset], domain) for domain in domains]
segment = bundle["segment"][offset] if use_segment else None
counts[offset] = sum(
lib.explained(lords, target, rule, segment)
for lords, target in zip(rows, targets, strict=True)
)
return counts
def rule_bits(bundle: Mapping[str, Any], events: Sequence[Mapping[str, Any]], rule: str) -> dict[int, list[bool]]:
domains = [str(event["domain"]) for event in events]
use_segment = rule.startswith("N5")
bits: dict[int, list[bool]] = {}
for offset, rows in bundle["lords"].items():
targets = [lib.domain_target_lords(bundle["asc"][offset], domain) for domain in domains]
segment = bundle["segment"][offset] if use_segment else None
bits[offset] = [
lib.explained(lords, target, rule, segment)
for lords, target in zip(rows, targets, strict=True)
]
return bits
def mean_defined(values: Sequence[float | None]) -> float | None:
nums = [float(value) for value in values if value is not None]
if not nums:
return None
return lib.round6(sum(nums) / len(nums))
def rate(flags: Sequence[bool]) -> float | None:
if not flags:
return None
return lib.round6(sum(bool(flag) for flag in flags) / len(flags))
def split_rates(rows: Sequence[Mapping[str, Any]], key: str, rounded: set[str]) -> dict[str, float | None]:
return {
"all": rate([bool(row[key]) for row in rows]),
"rounded_5min": rate([bool(row[key]) for row in rows if row["case_id"] in rounded]),
"unrounded": rate([bool(row[key]) for row in rows if row["case_id"] not in rounded]),
"n_all": len(rows),
"n_rounded_5min": sum(1 for row in rows if row["case_id"] in rounded),
"n_unrounded": sum(1 for row in rows if row["case_id"] not in rounded),
}
def delivery_offsets(start: str | None, end: str | None, recorded: str, radius_minutes: int) -> list[int]:
"""One-second offsets covering each delivered clock minute, clipped to the search radius."""
if not start or not end:
return []
cap = int(radius_minutes) * 60
start_min = lib.clock_delta_minutes(start, recorded)
end_min = lib.clock_delta_minutes(end, recorded)
def clip(lo: int, hi: int) -> list[int]:
lo = max(lo, -cap)
hi = min(hi, cap)
if lo > hi:
return []
return list(range(lo, hi + 1))
if start_min <= end_min:
return clip(start_min * 60, end_min * 60 + 59)
return sorted(set(clip(start_min * 60, cap) + clip(-cap, end_min * 60 + 59)))
def six_probe(case: Mapping[str, Any], radius: int) -> dict[str, Any]:
"""futile_collect_stop_replay d1: six probes, no guided-window injection."""
from scripts.active_rectification_event_engine import AYANAMSA, NODE_MODE, compute_candidate_static_contexts
from scripts.rectification.event_probes import discriminating_event_probes
from scripts.rectification.refinement_packet import window_scan
from scripts.rectification.scoring_service import build_event_contribution_matrix, score_from_matrix
from scripts.research.fewer_probes_card_replay import _outcome
from scripts.research.guided_collect_holdout_replay import _hhmm, posterior_state
from scripts.research.minute_resolution_sweep import MINUTE_STEP, scoring_request_for
from scripts.research.probe_supply_after_six import ASK_COUNT, TODAY
recorded = str(case["birth"]["time"])[:5]
request = scoring_request_for(dict(case), radius)
request["ayanamsa"] = AYANAMSA
request["node_mode"] = NODE_MODE
request["minute_step"] = MINUTE_STEP
static_contexts = compute_candidate_static_contexts(request)
built = build_event_contribution_matrix(request, static_contexts=static_contexts)
rows = score_from_matrix(request, built)
times = [stamp for row in rows if (stamp := _hhmm(row.get("time")))]
probes = discriminating_event_probes(
{**request, "refresh_probes": False, "asked_probe_keys": []},
built,
scan=window_scan(built),
candidate_times=times,
representative_time=recorded,
today=TODAY,
)[:ASK_COUNT]
state = posterior_state(rows=rows, contexts=static_contexts, probes=probes, true_time=recorded)
outcome = _outcome(state, recorded)
scores = state["scores"]
valid = list(state["valid"])
if not valid:
leaders: list[str] = []
else:
def _score(row: Mapping[str, Any]) -> float:
stamp = _hhmm(row.get("time")) or ""
return float(scores.get(stamp, row.get("score") or 0))
best = max(_score(row) for row in valid)
leaders = sorted({
str(row.get("time"))[:5]
for row in valid
if abs(_score(row) - best) <= 1e-9 and str(row.get("time") or "")[:5]
})
deltas = [lib.clock_delta_minutes(stamp, recorded) for stamp in leaders]
return {
"start": outcome["start"],
"end": outcome["end"],
"width": outcome["width"],
"truth_in_range": bool(outcome["truth_in_range"]),
"leader_times": leaders,
"all_within_1min": bool(deltas) and all(abs(delta) <= 1 for delta in deltas),
"any_exact": recorded in leaders,
"error": None,
}
def gate_p(result: Mapping[str, Any]) -> bool:
if result.get("mean") is None or result.get("extreme") is None:
return False
exact = (int(result["extreme"]) + 1) / (int(result["permutations"]) + 1)
return bool(result["mean"] > 0 and exact < 0.05)
def summarize_rule(
rows: Sequence[Mapping[str, Any]],
*,
seed: int,
permutations: int,
) -> dict[str, Any]:
diffs = [float(row["diff"]) for row in rows]
test = lib.paired_sign_flip_p(diffs, seed=seed, permutations=permutations)
return {
**test,
"pass": gate_p(test),
"truth_mean": mean_defined([row["truth"] for row in rows]),
"placebo_mean": mean_defined([row["placebo"] for row in rows]),
"window_mean": mean_defined([row["window"] for row in rows]),
"fraction_grid_explains_all_mean": mean_defined([row["fraction_all"] for row in rows]),
}
def n2_case(
bits: Mapping[int, Sequence[bool]],
coarse: Sequence[int],
events: Sequence[Mapping[str, Any]],
placebo_bits: Mapping[int, Sequence[bool]],
*,
case_id: str,
seed: str,
) -> dict[str, Any]:
deltas_random = []
deltas_placebo = []
held_rates = []
for index, event in enumerate(events):
counts = {
offset: sum(flag for event_index, flag in enumerate(flags) if event_index != index)
for offset, flags in bits.items()
if offset in coarse
}
leaders = [offset for offset in lib.rank_offsets(counts) if offset in set(coarse)]
if not leaders:
continue
held = sum(1 for offset in leaders if bits[offset][index]) / len(leaders)
rng = Random(f"{seed}:{case_id}:{event['id']}")
if len(leaders) >= len(coarse):
sample = list(coarse)
else:
sample = rng.sample(list(coarse), len(leaders))
random_rate = sum(1 for offset in sample if bits[offset][index]) / len(sample)
placebo_rate = sum(1 for offset in leaders if placebo_bits[offset][index]) / len(leaders)
held_rates.append(held)
deltas_random.append(held - random_rate)
deltas_placebo.append(held - placebo_rate)
return {
"held_rate": mean_defined(held_rates),
"delta_random": mean_defined(deltas_random),
"delta_placebo": mean_defined(deltas_placebo),
}
def level_change(base: Sequence[Sequence[str]], other: Sequence[Sequence[str]], index: int) -> float | None:
if not base or len(base) != len(other):
return None
changed = sum(left[index] != right[index] for left, right in zip(base, other, strict=True))
return lib.round6(changed / len(base))
def build(prereg: Mapping[str, Any], *, limit: int, radii: Sequence[int]) -> dict[str, Any]:
cases = [lib.public_fields(case) for case in lib.load_cases()]
if limit:
cases = cases[:limit]
grids = prereg["grids"]
coarse = offsets_of(grids["coarse_radius_seconds"], grids["coarse_step_seconds"])
fine = offsets_of(grids["fine_radius_seconds"], grids["fine_step_seconds"])
truth_offsets = truth_minute_offsets()
union = sorted(set(coarse) | set(fine) | set(truth_offsets))
rules = list(prereg["rules"]["computed"])
primary = list(prereg["rules"]["n1_primary"])
n1_min = int(prereg["samples"]["n1_min_day_events"])
n2_min = int(prereg["samples"]["n2_min_day_events"])
permutations = int(prereg["tests"]["permutations"])
resamples = int(prereg["tests"]["bootstrap_resamples"])
shift_low = int(prereg["placebo"]["shift_low_days"])
shift_high = int(prereg["placebo"]["shift_high_days"])
swap = lib.placebo_swap_dates(cases, seed=int(prereg["placebo"]["swap_seed"]))
rounded = {case_id for case_id in lib.truth_audit(cases)["rounded_5min_case_ids"]}
audit = lib.truth_audit(cases)
recon = [lib.reconcile_case(case) for case in cases]
n1_rows: dict[str, list[dict[str, Any]]] = {rule: [] for rule in primary}
n1_placebos = ("P1", "P2")
n5_rows: dict[str, list[dict[str, Any]]] = {rule: [] for rule in prereg["rules"]["n5_primary"]}
n2_rows = []
n3a_rows = []
n4_rows = []
fraction_rows: dict[str, list[float]] = {rule: [] for rule in ("A4", "A5", "B1", "B2")}
case_fit: list[dict[str, Any]] = []
for index, case in enumerate(cases, start=1):
events = lib.day_events(case)
birth = case["birth"]
print(f"nadi {index}/{len(cases)} {case['case_id']} day_events={len(events)}", flush=True)
skies = collect_skies(birth, union)
real = lord_bundle(skies, events) if events else None
shifted = lib.placebo_shift_dates(
events,
birth["date"],
seed=prereg["placebo"]["shift_seed_template"].format(case_id=case["case_id"]),
low_days=shift_low,
high_days=shift_high,
) if events else []
p1 = lord_bundle(skies, shifted) if shifted else None
swapped = swap.get(case["case_id"], [])
p2 = lord_bundle(skies, swapped) if swapped else None
fit_row: dict[str, Any] = {
"case_id": case["case_id"],
"day_events": len(events),
"rounded_5min": case["case_id"] in rounded,
}
if real is not None:
for rule in rules:
if rule.startswith("N5") and rule not in prereg["rules"]["n5_primary"] and rule not in primary:
continue
counts = rule_counts(real, events, rule)
truth = lib.mean_rate(counts, truth_offsets, len(events))
window = lib.mean_rate(counts, coarse, len(events))
fraction = lib.fraction_explaining_all(counts, coarse, len(events))
fit_row[rule] = {
"truth": None if truth is None else lib.round6(truth),
"window": None if window is None else lib.round6(window),
"fraction_all": None if fraction is None else lib.round6(fraction),
}
if rule in fraction_rows and fraction is not None and len(events) >= n1_min:
fraction_rows[rule].append(fraction)
if rule in primary and len(events) >= n1_min and p1 is not None and p2 is not None:
for label, bundle in (("P1", p1), ("P2", p2)):
placebo_rate = lib.mean_rate(rule_counts(bundle, shifted if label == "P1" else swapped, rule), truth_offsets, len(events))
n1_rows[rule].append({
"case_id": case["case_id"],
"placebo": label,
"truth": lib.round6(truth or 0.0),
"placebo_rate": None if placebo_rate is None else lib.round6(placebo_rate),
"diff": None if placebo_rate is None else lib.round6((truth or 0.0) - placebo_rate),
"window": None if window is None else lib.round6(window),
"fraction_all": None if fraction is None else lib.round6(fraction),
})
if rule in n5_rows and len(events) >= n1_min and p1 is not None and p2 is not None:
for label, bundle in (("P1", p1), ("P2", p2)):
placebo_rate = lib.mean_rate(rule_counts(bundle, shifted if label == "P1" else swapped, rule), truth_offsets, len(events))
n5_rows[rule].append({
"case_id": case["case_id"],
"placebo": label,
"truth": lib.round6(truth or 0.0),
"placebo_rate": None if placebo_rate is None else lib.round6(placebo_rate),
"diff": None if placebo_rate is None else lib.round6((truth or 0.0) - placebo_rate),
"window": None if window is None else lib.round6(window),
"fraction_all": None if fraction is None else lib.round6(fraction),
})
if len(events) >= n2_min and p1 is not None:
real_bits = rule_bits(real, events, prereg["rules"]["n2_primary"])
p1_bits = rule_bits(p1, shifted, prereg["rules"]["n2_primary"])
# P1 bits are aligned to shifted events, same index as real events.
n2_rows.append({
"case_id": case["case_id"],
"rounded_5min": case["case_id"] in rounded,
**n2_case(
real_bits, coarse, events, p1_bits,
case_id=case["case_id"], seed=str(prereg["tests"]["n2_random_second_seed"]),
),
})
a5_counts = rule_counts(real, events, prereg["rules"]["n3_primary"]) if events else {}
leaders = lib.rank_offsets({offset: a5_counts.get(offset, 0) for offset in coarse}) if events else []
n3a_rows.append({
"case_id": case["case_id"],
"rounded_5min": case["case_id"] in rounded,
"day_events": len(events),
"hit_60s": lib.leaders_within(leaders, 60) if events else False,
"hit_120s": lib.leaders_within(leaders, 120) if events else False,
"tied_leaders": len(leaders),
})
else:
n3a_rows.append({
"case_id": case["case_id"],
"rounded_5min": case["case_id"] in rounded,
"day_events": 0,
"hit_60s": False,
"hit_120s": False,
"tied_leaders": len(coarse),
})
case_fit.append(fit_row)
# N4 at the recorded second only.
if events:
base_sky = skies[0]
base_lords = lib.lords_for_events(base_sky["birth_date"], base_sky["moon_lon"], events)
base_part = int(lib.d150_equal(base_sky["asc_lon"])["part_index"])
perturbations: dict[str, Any] = {}
for name in prereg["n4"]["ayanamsas"]:
if name == prereg["ayanamsa"]:
continue
sky = lib.sky_at(birth, lib.candidate_moment(birth, 0), ayanamsa=name)
other = lib.lords_for_events(sky["birth_date"], sky["moon_lon"], events)
perturbations[f"ayanamsa:{name}"] = {
"level4": level_change(base_lords, other, 3),
"level5": level_change(base_lords, other, 4),
"d150_changed": int(lib.d150_equal(sky["asc_lon"])["part_index"]) != base_part,
}
for seconds in prereg["n4"]["time_offsets_seconds"]:
sky = skies.get(int(seconds)) or lib.sky_at(birth, lib.candidate_moment(birth, int(seconds)))
other = lib.lords_for_events(sky["birth_date"], sky["moon_lon"], events)
perturbations[f"time:{int(seconds)}"] = {
"level4": level_change(base_lords, other, 3),
"level5": level_change(base_lords, other, 4),
"d150_changed": int(lib.d150_equal(sky["asc_lon"])["part_index"]) != base_part,
}
for km in prereg["n4"]["longitude_shifts_km"]:
try:
moved = lib.shift_longitude_km(float(birth["longitude"]), float(birth["latitude"]), float(km))
sky = lib.sky_at(birth, lib.candidate_moment(birth, 0), longitude=moved)
other = lib.lords_for_events(sky["birth_date"], sky["moon_lon"], events)
perturbations[f"east_km:{km}"] = {
"level4": level_change(base_lords, other, 3),
"level5": level_change(base_lords, other, 4),
"d150_changed": int(lib.d150_equal(sky["asc_lon"])["part_index"]) != base_part,
"blocked": False,
}
except ValueError:
perturbations[f"east_km:{km}"] = {"blocked": True}
node_sky = lib.sky_at(birth, lib.candidate_moment(birth, 0), node_mode="true")
perturbations["node:true"] = {
"moon_changed": abs(float(node_sky["moon_lon"]) - float(base_sky["moon_lon"])) > 1e-6,
"asc_changed": abs(float(node_sky["asc_lon"]) - float(base_sky["asc_lon"])) > 1e-6,
"rahu_changed": node_sky["rahu_lon"] != base_sky["rahu_lon"],
"level5": level_change(base_lords, lib.lords_for_events(node_sky["birth_date"], node_sky["moon_lon"], events), 4),
}
n4_rows.append({"case_id": case["case_id"], "day_events": len(events), "perturbations": perturbations})
def pack_comparisons(bucket: Mapping[str, list[dict[str, Any]]], seed_base: int) -> dict[str, Any]:
packed = {}
cursor = 0
for rule, rows in bucket.items():
packed[rule] = {}
for label in n1_placebos:
subset = [
{
"diff": row["diff"],
"truth": row["truth"],
"placebo": row["placebo_rate"],
"window": row["window"],
"fraction_all": row["fraction_all"],
}
for row in rows
if row["placebo"] == label and row["diff"] is not None
]
packed[rule][label] = summarize_rule(subset, seed=seed_base + cursor, permutations=permutations)
cursor += 1
return packed
n1 = pack_comparisons(n1_rows, int(prereg["tests"]["n1_permutation_seed_base"]))
n5 = pack_comparisons(n5_rows, int(prereg["tests"]["n5_permutation_seed_base"]))
n1_pass = all(item["pass"] for rule in n1.values() for item in rule.values())
n5_pass = all(item["pass"] for rule in n5.values() for item in rule.values())
n2_values = [float(row["delta_random"]) for row in n2_rows if row["delta_random"] is not None]
n2_placebo_values = [float(row["delta_placebo"]) for row in n2_rows if row["delta_placebo"] is not None]
n2_ci = lib.bootstrap_mean_ci(n2_values, seed=int(prereg["tests"]["n2_bootstrap_seed"]), resamples=resamples)
n2_placebo_ci = lib.bootstrap_mean_ci(
n2_placebo_values, seed=int(prereg["tests"]["n2_bootstrap_seed"]) + 1, resamples=resamples,
)
uniform_60 = lib.round6(sum(1 for offset in coarse if abs(offset) <= 60) / len(coarse))
uniform_120 = lib.round6(sum(1 for offset in coarse if abs(offset) <= 120) / len(coarse))
# N3b. Separate from the fit loop so a replay failure does not drop N1.
n3b_rows = []
gate_radius = int(prereg["n3_protocol"]["radius_gate"])
for index, case in enumerate(cases, start=1):
print(f"replay {index}/{len(cases)} {case['case_id']}", flush=True)
by_radius = {}
for radius in radii:
try:
snapshot = six_probe(case, radius)
except Exception as exc: # noqa: BLE001 — recorded as a miss, message dropped
snapshot = {
"start": None,
"end": None,
"width": None,
"truth_in_range": False,
"leader_times": [],
"all_within_1min": False,
"any_exact": False,
"error": type(exc).__name__,
}
traceback.print_exc(limit=2)
events = lib.day_events(case)
span = delivery_offsets(snapshot["start"], snapshot["end"], case["birth"]["time"], radius)
if events and span:
skies = collect_skies(case["birth"], span)
bundle = lord_bundle(skies, events)
counts = rule_counts(bundle, events, prereg["rules"]["n3_primary"])
leaders = lib.rank_offsets(counts)
nadi_hit = lib.leaders_within(leaders, int(prereg["n3_protocol"]["nadi_limit_seconds"]))
tied = len(leaders)
else:
nadi_hit = False
tied = 0
by_radius[str(radius)] = {
"truth_in_range": snapshot["truth_in_range"],
"width": snapshot["width"],
"baseline_all_within_1min": snapshot["all_within_1min"],
"baseline_any_exact": snapshot["any_exact"],
"nadi_all_within_60s": nadi_hit,
"nadi_tied_leaders": tied,
"improvement": int(nadi_hit) - int(snapshot["all_within_1min"]),
"error": snapshot["error"],
}
n3b_rows.append({
"case_id": case["case_id"],
"rounded_5min": case["case_id"] in rounded,
"radii": by_radius,
})
def n3b_summary(radius: int) -> dict[str, Any]:
key = str(radius)
improvements = [int(row["radii"][key]["improvement"]) for row in n3b_rows]
inside = sum(1 for row in n3b_rows if row["radii"][key]["truth_in_range"])
ci = lib.bootstrap_mean_ci(improvements, seed=int(prereg["tests"]["n3_bootstrap_seed"]) + radius, resamples=resamples)
return {
"radius": radius,
"truth_in_range": f"{inside}/{len(n3b_rows)}",
"truth_in_range_count": inside,
"truth_in_range_pass": inside >= 76 if len(n3b_rows) == 77 else None,
"baseline_all_within_1min": split_rates(
[{"case_id": row["case_id"], "hit": row["radii"][key]["baseline_all_within_1min"]} for row in n3b_rows],
"hit", rounded,
),
"baseline_any_exact": rate([row["radii"][key]["baseline_any_exact"] for row in n3b_rows]),
"nadi_all_within_60s": split_rates(
[{"case_id": row["case_id"], "hit": row["radii"][key]["nadi_all_within_60s"]} for row in n3b_rows],
"hit", rounded,
),
"improvement_ci": ci,
"pass": bool(ci["excludes_zero_positive"] and len(n3b_rows) == 77 and inside >= 76),
}
def n4_aggregate(label: str, field: str) -> dict[str, Any]:
values = []
d150_flags = []
weights = []
for row in n4_rows:
item = row["perturbations"].get(label) or {}
if item.get("blocked") or item.get(field) is None:
continue
values.append(float(item[field]))
weights.append(int(row["day_events"]))
if "d150_changed" in item:
d150_flags.append(bool(item["d150_changed"]))
event_rate = None
if weights and sum(weights):
event_rate = lib.round6(sum(value * weight for value, weight in zip(values, weights, strict=True)) / sum(weights))
return {
"event_rate": event_rate,
"case_rate": rate(d150_flags) if field == "level5" else None,
"d150_case_rate": rate(d150_flags),
"cases": len(values),
}
wording_hits = []
for name in prereg["n4"]["ayanamsas"]:
if name == prereg["ayanamsa"]:
continue
label = f"ayanamsa:{name}"
level5 = n4_aggregate(label, "level5")
wording_hits.append({
"switch": f"{prereg['ayanamsa']}->{name}",
"level5_event_rate": level5["event_rate"],
"d150_case_rate": level5["d150_case_rate"],
"over_20pct": bool(
(level5["event_rate"] is not None and level5["event_rate"] > 0.20)
or (level5["d150_case_rate"] is not None and level5["d150_case_rate"] > 0.20)
),
})
wording_ban = any(item["over_20pct"] for item in wording_hits)
n3a = {
"grid": "coarse",
"uniform_within_60s": uniform_60,
"uniform_within_120s": uniform_120,
"published_engine_prior_top1_pm10": 0.18,
"hit_60s": split_rates(
[{"case_id": row["case_id"], "hit": row["hit_60s"]} for row in n3a_rows], "hit", rounded,
),
"hit_120s": split_rates(
[{"case_id": row["case_id"], "hit": row["hit_120s"]} for row in n3a_rows], "hit", rounded,
),
}
n3b = {str(radius): n3b_summary(radius) for radius in radii}
n3_pass = bool(n3b.get(str(gate_radius), {}).get("pass"))
return {
"prereg_sha256": lib.file_sha256(lib.PREREG_PATH),
"library_sha256": lib.file_sha256(Path(__file__).with_name("nadi_seconds_lib.py")),
"runner_sha256": lib.file_sha256(Path(__file__)),
"dataset_sha256": lib.file_sha256(lib.HOLDOUT_V5),
"swisseph": __import__("swisseph").version,
"limit": limit,
"n0": {
"audit": {key: value for key, value in audit.items() if key != "per_case"},
"per_case_day_events": audit["per_case"],
"reconciliation_mismatch_cases": [row["case_id"] for row in recon if row["mismatches"]],
"reconciliation_events": sum(row["events"] for row in recon),
},
"n1": {
"pass": n1_pass and not limit,
"comparisons": n1,
"fraction_grid_explains_all_mean": {rule: mean_defined(values) for rule, values in fraction_rows.items()},
"cases": case_fit,
},
"n2": {
"pass": bool(n2_ci["excludes_zero_positive"]) and not limit,
"versus_random": n2_ci,
"versus_placebo_date": n2_placebo_ci,
"cases": n2_rows,
},
"n3": {
"pass": n3_pass and not limit,
"n3a": n3a,
"n3b": n3b,
"cases_n3a": n3a_rows,
"cases_n3b": n3b_rows,
},
"n4": {
"wording_ban_second_scale": wording_ban,
"ayanamsa_switches": wording_hits,
"time": {str(seconds): n4_aggregate(f"time:{int(seconds)}", "level5") for seconds in prereg["n4"]["time_offsets_seconds"]},
"time_level4": {str(seconds): n4_aggregate(f"time:{int(seconds)}", "level4") for seconds in prereg["n4"]["time_offsets_seconds"]},
"longitude_km": {str(km): n4_aggregate(f"east_km:{km}", "level5") for km in prereg["n4"]["longitude_shifts_km"]},
"longitude_km_d150": {str(km): n4_aggregate(f"east_km:{km}", "level5")["d150_case_rate"] for km in prereg["n4"]["longitude_shifts_km"]},
"node_mean_to_true": {
"moon_changed_cases": sum(1 for row in n4_rows if row["perturbations"]["node:true"]["moon_changed"]),
"asc_changed_cases": sum(1 for row in n4_rows if row["perturbations"]["node:true"]["asc_changed"]),
"rahu_changed_cases": sum(1 for row in n4_rows if row["perturbations"]["node:true"]["rahu_changed"]),
"level5_event_rate": n4_aggregate("node:true", "level5")["event_rate"],
},
"cases": n4_rows,
},
"n5": {
"pass": n5_pass and not limit,
"named_text": "blocked",
"named_text_reason": "no legal Chandra Kala Nadi source in the repository",
"classical_index_status": lib.CLASSICAL_NADI_STATUS,
"comparisons": n5,
},
"gates": {
"n1": n1_pass and not limit,
"n2": bool(n2_ci["excludes_zero_positive"]) and not limit,
"n3": n3_pass and not limit,
"n4_blocks_second_scale_wording": wording_ban,
"n5": n5_pass and not limit,
},
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--limit", type=int, default=0)
parser.add_argument("--radii", default="10,30,60")
parser.add_argument("--out", default=str(REPORT))
args = parser.parse_args()
prereg = load_prereg()
radii = tuple(int(item) for item in str(args.radii).split(",") if item.strip())
payload = build(prereg, limit=int(args.limit), radii=radii)
text = json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(text, encoding="utf-8")
print(json.dumps(payload["gates"], ensure_ascii=False, sort_keys=True), flush=True)
print(f"wrote {out}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())