Files
Jyotisha/scripts/shadbala_dig_source_of_truth_audit.py
2026-07-22 23:40:59 +08:00

215 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""Compare candidate Dig Bala models against external oracle rows."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
SCRIPT_DIR = ROOT / "scripts"
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
import jyotish_engine as engine # type: ignore
import oracle_boundary_audit # type: ignore
from shadbala import DIG_BALA_HOUSE, calc_dig_bala # type: ignore
MODEL_NAMES = [
"current_linear_house_model",
"house_midpoint_angular_model",
"bhava_madhya_angular_model",
]
def _load_oracle(path: str) -> dict[str, Any]:
resolved = Path(path)
if not resolved.is_absolute():
resolved = ROOT / resolved
return oracle_boundary_audit._load_oracle(str(resolved))
def _resolve_path(path: str) -> Path:
candidate = Path(path)
return candidate if candidate.is_absolute() else ROOT / candidate
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _iter_external_verified_template_cases(oracle: dict[str, Any]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for case in oracle.get("template_cases", []):
if case.get("status") == "external_verified" and isinstance(case.get("target", {}).get("shadbala_components"), dict):
out.append(case)
return out
def _namespace_from_template(case: dict[str, Any]) -> Any:
birth = case["birth"]
settings = case.get("settings", {})
return type(
"Args",
(),
{
"year": birth["year"],
"month": birth["month"],
"day": birth["day"],
"hour": birth["hour"],
"minute": birth.get("minute", 0),
"second": birth.get("second", 0),
"lat": birth["lat"],
"lon": birth["lon"],
"tz": birth["tz"],
"ayanamsa": settings.get("ayanamsa", "lahiri"),
"node_mode": settings.get("node_mode", "mean"),
},
)()
def _planet_lon(chart: dict[str, Any], planet: str) -> float:
return float(chart["planets"][planet]["degree_raw"])
def _asc_lon(chart: dict[str, Any]) -> float:
return float(chart["ascendant"]["degree_raw"])
def _whole_sign_house_midpoint(asc_lon: float, house: int) -> float:
base = (asc_lon + (house - 1) * 30) % 360
return (base + 15) % 360
def _angular_distance(a: float, b: float) -> float:
diff = abs(a - b) % 360
return min(diff, 360 - diff)
def _dig_from_strong_point(planet_lon: float, strong_point_lon: float) -> float:
shorter_arc = _angular_distance(planet_lon, strong_point_lon)
return max(0.0, (180.0 - shorter_arc) / 3.0)
def _best_house_midpoint_lon(asc_lon: float, planet: str) -> float:
best_house = DIG_BALA_HOUSE.get(planet, 1)
return _whole_sign_house_midpoint(asc_lon, best_house)
def _best_bhava_madhya_lon(chart: dict[str, Any], planet: str) -> float:
best_house = DIG_BALA_HOUSE.get(planet, 1)
house_row = chart["houses"].get(f"house_{best_house}", {})
return float(house_row.get("cusp_degree", 0.0))
def build_report(oracle_file: str) -> dict[str, Any]:
oracle = _load_oracle(oracle_file)
cases = _iter_external_verified_template_cases(oracle)
resolved_oracle = _resolve_path(oracle_file)
rows: list[dict[str, Any]] = []
model_diffs: dict[str, list[float]] = {name: [] for name in MODEL_NAMES}
for case in cases:
chart = engine.cmd_chart(_namespace_from_template(case))
asc_lon = _asc_lon(chart)
target_components = case["target"]["shadbala_components"]
for planet, external_row in target_components.items():
if planet not in chart["planets"] or not isinstance(external_row, dict):
continue
external_dig = external_row.get("dig")
if not isinstance(external_dig, (int, float)):
continue
planet_lon = _planet_lon(chart, planet)
house = int(chart["planets"][planet]["house"])
current_linear = calc_dig_bala(planet, house) / 60.0
house_midpoint = _dig_from_strong_point(planet_lon, _best_house_midpoint_lon(asc_lon, planet)) / 60.0
bhava_madhya = _dig_from_strong_point(planet_lon, _best_bhava_madhya_lon(chart, planet)) / 60.0
candidates = {
"current_linear_house_model": current_linear,
"house_midpoint_angular_model": house_midpoint,
"bhava_madhya_angular_model": bhava_madhya,
}
diffs = {name: round(abs(value - float(external_dig)), 4) for name, value in candidates.items()}
for name, diff in diffs.items():
model_diffs[name].append(diff)
rows.append(
{
"case_id": case.get("id") or case.get("case_id"),
"planet": planet,
"external_dig_rupa": float(external_dig),
"house": house,
"planet_lon": round(planet_lon, 4),
"asc_lon": round(asc_lon, 4),
"current_linear_house_model": round(current_linear, 4),
"house_midpoint_angular_model": round(house_midpoint, 4),
"bhava_madhya_angular_model": round(bhava_madhya, 4),
"abs_diffs": diffs,
}
)
avg_diffs = {
name: round(sum(values) / len(values), 4) if values else math.inf
for name, values in model_diffs.items()
}
best_model = min(avg_diffs, key=avg_diffs.get) if rows else None
return {
"scope": "shadbala_dig_source_of_truth_audit",
"schema_version": 1,
"candidate_models": MODEL_NAMES,
"inputs": {
"oracle_file": str(resolved_oracle.relative_to(ROOT)),
"oracle_file_sha256": _sha256(resolved_oracle),
"external_case_count": len(cases),
"external_case_sources": [
{
"case_id": case.get("id") or case.get("case_id"),
"source_artifact": case.get("evidence_packet", {}).get("metadata", {}).get("source_artifact", ""),
"source_artifact_sha256": _sha256(source_path)
if (source_artifact := case.get("evidence_packet", {}).get("metadata", {}).get("source_artifact"))
and (source_path := _resolve_path(source_artifact)).exists()
else None,
}
for case in cases
],
},
"summary": {
"case_count": len(cases),
"row_count": len(rows),
"best_model_by_avg_abs_diff": best_model,
"avg_abs_diff_by_model": avg_diffs,
},
"rows": rows,
"boundary": (
"This audit compares three local Dig Bala candidate models against external oracle rows. "
"It is diagnostic only and does not modify production scoring."
),
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--oracle-file",
default="references/oracle/dasha_shadbala_oracle_cases.json",
)
parser.add_argument("--output", help="Optional JSON snapshot path.")
args = parser.parse_args()
report = build_report(args.oracle_file)
rendered = json.dumps(report, ensure_ascii=False, indent=2)
if args.output:
Path(args.output).write_text(rendered + "\n", encoding="utf-8")
print(rendered)
if __name__ == "__main__":
main()