452 lines
18 KiB
Python
452 lines
18 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"}
|
|
SHADBALA_COMPONENT_FIELD_MAP = {
|
|
"sthana": "sthana_bala",
|
|
"dig": "dig_bala",
|
|
"kala": "kala_bala",
|
|
"chesta": "chesta_bala",
|
|
"naisargika": "naisargika_bala",
|
|
"drik": "drik_bala",
|
|
}
|
|
SHADBALA_COMPONENT_TOLERANCES_RUPA = {
|
|
"sthana": 0.1,
|
|
"dig": 0.5,
|
|
"kala": 0.5,
|
|
"chesta": 1.0,
|
|
"naisargika": 0.05,
|
|
"drik": 1.5,
|
|
}
|
|
SHADBALA_TOTAL_TOLERANCE_RUPA = 1.0
|
|
|
|
|
|
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 _namespace_from_template(case: dict[str, Any]) -> argparse.Namespace:
|
|
birth = dict(case.get("birth", {}))
|
|
settings = case.get("settings", {})
|
|
if "node_mode" not in birth and settings.get("node_mode"):
|
|
birth["node_mode"] = settings["node_mode"]
|
|
return _namespace_from_birth(
|
|
birth,
|
|
ayanamsa=settings.get("ayanamsa", "lahiri"),
|
|
moon_lon=None,
|
|
nakshatra=None,
|
|
pada=None,
|
|
birthdate=None,
|
|
today=None,
|
|
years=120,
|
|
table=False,
|
|
)
|
|
|
|
|
|
def _first_dasha_start(result: dict[str, Any]) -> str | None:
|
|
timeline = result.get("timeline")
|
|
if isinstance(timeline, list) and timeline:
|
|
return timeline[0].get("start")
|
|
return None
|
|
|
|
|
|
def _date_delta_days(engine_date: str | None, target_date: str | None) -> int | None:
|
|
if not engine_date or not target_date:
|
|
return None
|
|
from datetime import date
|
|
|
|
try:
|
|
engine_parts = [int(part) for part in engine_date.split("-")[:3]]
|
|
target_parts = [int(part) for part in target_date.split("-")[:3]]
|
|
return (date(*engine_parts) - date(*target_parts)).days
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _template_shadbala_comparison(case: dict[str, Any]) -> dict[str, Any]:
|
|
target_components = case.get("target", {}).get("shadbala_components")
|
|
if not isinstance(target_components, dict):
|
|
return {"status": "missing_external_shadbala_components", "planets": {}}
|
|
|
|
result = engine.cmd_shadbala(_namespace_from_template(case))
|
|
if "error" in result:
|
|
return {"status": "engine_error", "error": result["error"], "planets": {}}
|
|
|
|
rows: dict[str, Any] = {}
|
|
scale_ratios: list[float] = []
|
|
for planet in SHADBALA_PLANETS:
|
|
external_row = target_components.get(planet, {})
|
|
engine_row = result.get("planets", {}).get(planet, {})
|
|
engine_components = _component_totals(engine_row) if engine_row else {}
|
|
external_total = external_row.get("total_rupa") if isinstance(external_row, dict) else None
|
|
engine_total = round(float(engine_row.get("total_rupas", 0.0)), 4) if engine_row else None
|
|
component_deltas: dict[str, Any] = {}
|
|
if isinstance(external_row, dict):
|
|
for external_name, engine_name in SHADBALA_COMPONENT_FIELD_MAP.items():
|
|
engine_value = engine_components.get(engine_name)
|
|
external_value = external_row.get(external_name)
|
|
tolerance = SHADBALA_COMPONENT_TOLERANCES_RUPA[external_name]
|
|
delta = (
|
|
round(float(engine_value) - float(external_value), 4)
|
|
if isinstance(engine_value, (int, float)) and isinstance(external_value, (int, float))
|
|
else None
|
|
)
|
|
component_deltas[external_name] = {
|
|
"engine_rupa": engine_value,
|
|
"external_rupa": external_value,
|
|
"delta_rupa": delta,
|
|
"abs_delta_rupa": round(abs(delta), 4) if delta is not None else None,
|
|
"tolerance_rupa": tolerance,
|
|
"within_tolerance": abs(delta) <= tolerance if delta is not None else None,
|
|
}
|
|
if isinstance(engine_total, (int, float)) and isinstance(external_total, (int, float)) and external_total:
|
|
scale_ratios.append(round(float(engine_total) / float(external_total), 6))
|
|
total_delta = (
|
|
round(engine_total - float(external_total), 4)
|
|
if engine_total is not None and isinstance(external_total, (int, float))
|
|
else None
|
|
)
|
|
rows[planet] = {
|
|
"engine_total_rupa": engine_total,
|
|
"external_total_rupa": external_total,
|
|
"total_rupa_delta": total_delta,
|
|
"total_abs_delta_rupa": round(abs(total_delta), 4) if total_delta is not None else None,
|
|
"total_tolerance_rupa": SHADBALA_TOTAL_TOLERANCE_RUPA,
|
|
"total_within_tolerance": (
|
|
abs(total_delta) <= SHADBALA_TOTAL_TOLERANCE_RUPA if total_delta is not None else None
|
|
),
|
|
"engine_components": engine_components,
|
|
"external_components": external_row,
|
|
"component_deltas": component_deltas,
|
|
}
|
|
ratio_spread = round(max(scale_ratios) - min(scale_ratios), 6) if len(scale_ratios) >= 2 else None
|
|
return {
|
|
"status": "compared",
|
|
"unit": "rupa",
|
|
"component_tolerances": SHADBALA_COMPONENT_TOLERANCES_RUPA,
|
|
"total_tolerance_rupa": SHADBALA_TOTAL_TOLERANCE_RUPA,
|
|
"global_scaling_check": {
|
|
"allowed": False,
|
|
"recommendation": "reject_global_scaling",
|
|
"engine_to_external_total_ratios": scale_ratios,
|
|
"ratio_spread": ratio_spread,
|
|
"finding": (
|
|
"Do not fix Shadbala with a global multiplier; inspect component-level deltas by planet."
|
|
),
|
|
},
|
|
"planets": rows,
|
|
}
|
|
|
|
|
|
def _audit_external_verified_template_case(case: dict[str, Any]) -> dict[str, Any]:
|
|
target = case.get("target", {})
|
|
dasha_result = engine.cmd_dasha(_namespace_from_template(case))
|
|
engine_start = _first_dasha_start(dasha_result) if "error" not in dasha_result else None
|
|
target_start = target.get("vimshottari_start_date")
|
|
return {
|
|
"case_id": case.get("id") or case.get("case_id"),
|
|
"status": case.get("status"),
|
|
"source": case.get("source"),
|
|
"metadata": case.get("evidence_packet", {}).get("metadata", {}),
|
|
"dasha": {
|
|
"status": "compared" if engine_start and target_start else "missing_dasha_target",
|
|
"engine_start_date": engine_start,
|
|
"target_start_date": target_start,
|
|
"date_delta_days": _date_delta_days(engine_start, target_start),
|
|
},
|
|
"shadbala": _template_shadbala_comparison(case),
|
|
"calibration_decision": "do_not_tune_single_template",
|
|
"finding": (
|
|
"External-verified template rows are comparison evidence. Production constants require "
|
|
"a multi-source sample matrix and must not be tuned to a single packet."
|
|
),
|
|
}
|
|
|
|
|
|
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", [])]
|
|
template_comparisons = [
|
|
_audit_external_verified_template_case(case)
|
|
for case in oracle.get("template_cases", [])
|
|
if case.get("status") in ORACLE_TEMPLATE_READY_STATUSES and not _missing_target_fields(case.get("target", {}))
|
|
]
|
|
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),
|
|
"external_verified_template_cases": len(template_comparisons),
|
|
"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,
|
|
"template_comparisons": template_comparisons,
|
|
"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())
|