Files
Jyotisha/scripts/oracle_boundary_audit.py
T
2026-06-25 18:33:26 +08:00

290 lines
12 KiB
Python

#!/usr/bin/env python3
"""Build a combined external-oracle boundary report for Dasha and Shadbala.
The report is intentionally diagnostic. It records whether external references
are strong enough to tune production constants. Single PDF dates and incomplete
Shadbala totals are treated as boundaries, not calibration authority.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(SCRIPT_DIR)
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
import dasha_reference_audit # noqa: E402
import jyotish_engine as engine # noqa: E402
SHADBALA_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
ORACLE_TEMPLATE_READY_STATUSES = {"external_verified"}
def _resolve_path(path: str) -> str:
if os.path.isabs(path):
return path
return os.path.join(ROOT_DIR, path)
def _load_oracle(path: str) -> dict[str, Any]:
with open(_resolve_path(path), "r", encoding="utf-8") as fh:
data = json.load(fh)
if data.get("schema_version") != 1:
raise ValueError("Unsupported oracle schema_version")
return data
def _namespace_from_birth(birth: dict[str, Any], **extra: Any) -> argparse.Namespace:
payload = {
"year": int(birth["year"]),
"month": int(birth["month"]),
"day": int(birth["day"]),
"hour": int(birth["hour"]),
"minute": int(birth["minute"]),
"second": int(birth.get("second", 0) or 0),
"lat": float(birth["lat"]),
"lon": float(birth["lon"]),
"tz": float(birth["tz"]),
"node_mode": birth.get("node_mode", "mean"),
}
payload.update(extra)
return argparse.Namespace(**payload)
def _audit_dasha_case(case: dict[str, Any]) -> dict[str, Any]:
target = case.get("target", {})
args = _namespace_from_birth(
case["birth"],
target_start_date=target["vimshottari_start_date"],
target_source=target.get("source", "external_reference"),
)
report = dasha_reference_audit.build_report(args)
return {
"case_id": case["case_id"],
"reference_kind": case.get("reference_kind"),
"privacy": case.get("privacy"),
"engine_moon_lon": report["engine"]["moon_lon"],
"engine_nakshatra": report["engine"]["nakshatra"],
"engine_start_lord": report["engine"]["start_lord"],
"engine_start_datetime": report["engine"]["start_datetime"],
"target_source": report["target_reference"]["source"],
"target_start_date": report["target_reference"]["start_date"],
"date_delta_days": report["target_reference"]["date_delta_days"],
"exact_delta_days": report["target_reference"]["exact_delta_days"],
"required_moon_delta_degrees": report["target_reference"]["required_moon_delta_degrees"],
"calibration_decision": case.get("calibration_policy", "do_not_tune_single_reference"),
"finding": report["finding"],
}
def _component_totals(planet: dict[str, Any]) -> dict[str, float]:
sthana = planet.get("sthana_bala", {})
kala = planet.get("kala_bala", {})
return {
"sthana_bala": round(float(sthana.get("total", 0.0)), 4),
"dig_bala": round(float(planet.get("dig_bala", 0.0)), 4),
"kala_bala": round(float(kala.get("total", 0.0)), 4),
"chesta_bala": round(float(planet.get("chesta_bala", 0.0)), 4),
"naisargika_bala": round(float(planet.get("naisargika_bala", 0.0)), 4),
"drik_bala": round(float(planet.get("drik_bala", 0.0)), 4),
}
def _audit_shadbala_case(case: dict[str, Any]) -> dict[str, Any]:
args = _namespace_from_birth(case["birth"])
result = engine.cmd_shadbala(args)
if "error" in result:
raise RuntimeError(result["error"])
target = case.get("target", {})
component_targets = target.get("component_targets")
target_authority = target.get("authority")
if component_targets and target_authority == "external_oracle":
component_status = "component_targets_external_oracle"
elif component_targets:
component_status = "component_targets_sample_only"
target_authority = target_authority or "sample_only_not_external_oracle"
else:
component_status = "missing_component_targets"
target_authority = target_authority or "missing_external_oracle"
totals: dict[str, Any] = {}
for name in SHADBALA_PLANETS:
planet = result.get("planets", {}).get(name, {})
if not planet:
continue
totals[name] = {
"total_rupas": round(float(planet.get("total_rupas", 0.0)), 4),
"total_virupas": round(float(planet.get("total_virupas", 0.0)), 4),
"min_required": round(float(planet.get("min_required", 0.0)), 4),
"rank": planet.get("rank"),
"components": _component_totals(planet),
}
return {
"case_id": case["case_id"],
"reference_kind": case.get("reference_kind"),
"privacy": case.get("privacy"),
"engine_method": result.get("method", ""),
"target_source": target.get("source", "external_reference"),
"component_oracle_status": component_status,
"target_authority": target_authority,
"calibration_decision": case.get("calibration_policy", "component_oracle_required"),
"engine_totals": totals,
"finding": (
"Shadbala external calibration requires component-level oracle rows; "
"do not apply a global scaling factor to match one total."
),
}
def _angular_delta_degrees(left: float, right: float) -> float:
return (left - right + 180.0) % 360.0 - 180.0
def _audit_longitude_case(case: dict[str, Any]) -> dict[str, Any]:
chart, _asc_idx, _jd, ayanamsa = engine._compute_chart_from_args(_namespace_from_birth(case["birth"]))
if chart is None:
raise RuntimeError("Swiss Ephemeris is required for longitude oracle audit")
target = case.get("target", {})
threshold_arcsec = float(target.get("threshold_arcsec", 60.0))
comparisons: dict[str, Any] = {}
max_abs_delta_arcsec = 0.0
for planet_name, target_position in target.get("positions", {}).items():
engine_position = chart.get("planets", {}).get(planet_name, {})
if not engine_position:
comparisons[planet_name] = {
"status": "missing_engine_position",
"target_sign": target_position.get("sign"),
"target_sidereal_longitude": target_position.get("sidereal_longitude"),
}
continue
engine_lon = float(engine_position.get("degree_raw", 0.0))
target_lon = float(target_position["sidereal_longitude"])
delta_degrees = _angular_delta_degrees(engine_lon, target_lon)
abs_delta_arcsec = abs(delta_degrees) * 3600.0
max_abs_delta_arcsec = max(max_abs_delta_arcsec, abs_delta_arcsec)
comparisons[planet_name] = {
"status": "compared",
"engine_sign": engine_position.get("sign"),
"target_sign": target_position.get("sign"),
"engine_sidereal_longitude": round(engine_lon, 8),
"target_sidereal_longitude": round(target_lon, 8),
"delta_degrees": round(delta_degrees, 8),
"abs_delta_arcsec": round(abs_delta_arcsec, 4),
"within_threshold": abs_delta_arcsec <= threshold_arcsec,
}
return {
"case_id": case["case_id"],
"reference_kind": case.get("reference_kind"),
"privacy": case.get("privacy"),
"target_source": target.get("source", "external_reference"),
"target_ayanamsa": target.get("ayanamsa"),
"target_node_mode": target.get("node_mode"),
"engine_ayanamsa": round(float(ayanamsa), 8),
"threshold_arcsec": threshold_arcsec,
"max_abs_delta_arcsec": round(max_abs_delta_arcsec, 4),
"within_threshold": max_abs_delta_arcsec <= threshold_arcsec,
"calibration_decision": case.get("calibration_policy", "external_position_reference_only"),
"comparisons": comparisons,
"finding": (
"External longitude rows can explain ephemeris drift, but Dasha/Shadbala tuning still "
"requires explicit Dasha boundary and Shadbala component targets."
),
}
def _missing_target_fields(value: Any, prefix: str = "target") -> list[str]:
missing: list[str] = []
if isinstance(value, dict):
for key, child in value.items():
missing.extend(_missing_target_fields(child, f"{prefix}.{key}"))
elif value is None:
missing.append(prefix)
return missing
def _audit_template_case(case: dict[str, Any]) -> dict[str, Any]:
status = case.get("status", "template_only")
target = case.get("target", {})
missing = _missing_target_fields(target)
return {
"case_id": case.get("id") or case.get("case_id"),
"status": status,
"source": case.get("source"),
"privacy": case.get("privacy"),
"settings": case.get("settings", {}),
"missing_target_fields": missing,
"ready_for_calibration": status in ORACLE_TEMPLATE_READY_STATUSES and not missing,
"verification_note": case.get("verification_note", ""),
}
def _status_counts(rows: list[dict[str, Any]]) -> dict[str, int]:
counts: dict[str, int] = {}
for row in rows:
status = row.get("status", "unknown")
counts[status] = counts.get(status, 0) + 1
return counts
def build_report(oracle: dict[str, Any]) -> dict[str, Any]:
template_rows = [_audit_template_case(case) for case in oracle.get("template_cases", [])]
dasha_rows = [_audit_dasha_case(case) for case in oracle.get("dasha_cases", [])]
longitude_rows = [_audit_longitude_case(case) for case in oracle.get("longitude_cases", [])]
shadbala_rows = [_audit_shadbala_case(case) for case in oracle.get("shadbala_cases", [])]
return {
"scope": "external_oracle_boundary_audit",
"schema_version": oracle.get("schema_version"),
"summary": {
"template_cases": len(template_rows),
"template_status_counts": _status_counts(template_rows),
"dasha_cases": len(dasha_rows),
"longitude_cases": len(longitude_rows),
"shadbala_cases": len(shadbala_rows),
"production_tuning_recommended": False,
"open_items": [
"Promote template cases to external_verified only after filling external target rows.",
"Add multi-source Vimshottari rows with Moon longitude, ayanamsa and start-boundary settings.",
"Add Shadbala component targets before claiming external absolute calibration.",
],
},
"template_cases": template_rows,
"dasha_cases": dasha_rows,
"longitude_cases": longitude_rows,
"shadbala_cases": shadbala_rows,
"boundary": (
"This report is a repeatable audit gate. It should prevent accidental claims that "
"Dasha/Shadbala are fully externally calibrated before the oracle matrix is complete."
),
}
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Audit external Dasha/Shadbala oracle boundaries")
parser.add_argument("--oracle-file", required=True, help="Path to oracle fixture JSON")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
oracle = _load_oracle(args.oracle_file)
print(json.dumps(build_report(oracle), ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())