feat: migrate precise Shadbala parity

This commit is contained in:
732642856
2026-07-17 19:39:45 +08:00
parent 064f985f3a
commit f2c4a0d915
16 changed files with 14364 additions and 173 deletions
+14 -1
View File
@@ -6281,12 +6281,25 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
birth_minute = self._get_float(body, 'birth_minute', body.get('minute', 0), 0, 59)
birth_second = self._get_birth_second(body)
birth_hour_decimal = self._birth_hour_decimal(birth_hour, birth_minute, birth_second)
result = _load_local_module('shadbala').calc_shadbala(
shadbala_module = _load_local_module('shadbala')
context = None
if all(key in body for key in ('year', 'month', 'day', 'lat', 'lon')):
year = self._get_int(body, 'year', 1990, 1800, 2400)
month = self._get_int(body, 'month', 6, 1, 12)
day = self._get_int(body, 'day', 15, 1, 31)
lat = self._get_float(body, 'lat', 0, -90, 90)
lon = self._get_float(body, 'lon', 0, -180, 180)
tz = self._parse_timezone(body, lat, lon, year, month, day, birth_hour, birth_minute, birth_second)
jd = swe.julday(year, month, day, birth_hour_decimal - tz)
context = shadbala_module.build_shadbala_context(jd, lat, lon, body.get('ayanamsa', 'lahiri'))
result = shadbala_module.calc_shadbala(
planets,
SIGNS[asc_sign_idx],
birth_hour_decimal,
planet_lons['Sun'],
planet_lons['Moon'],
birth_minute,
context=context,
)
advanced = self._compute_shadbala_advanced_layer(body, planets, result)
return {
+7 -5
View File
@@ -3356,7 +3356,7 @@ def cmd_shadbala(args):
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import calc_shadbala
from shadbala import build_shadbala_context, calc_shadbala
except ImportError as e:
return {"error": f"shadbala模块导入失败: {e}"}
planets = chart.get("planets", {})
@@ -3364,7 +3364,8 @@ def cmd_shadbala(args):
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planets.get("Sun", {}).get("degree", 0)
moon_lon = planets.get("Moon", {}).get("degree", 0)
return calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args))
return calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
# ============================================================================
@@ -3752,11 +3753,12 @@ def cmd_audit(args):
# P9 Shadbala(六重力量)
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import calc_shadbala
from shadbala import build_shadbala_context, calc_shadbala
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planets.get('Sun', {}).get('degree', 0)
moon_lon = planets.get('Moon', {}).get('degree', 0)
shadbala = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args))
shadbala = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
report['audit']['P9_shadbala'] = {
'summary': shadbala.get('summary', {}),
'ishta_bala_ranking': shadbala.get('ishta_bala_ranking', []),
@@ -5524,7 +5526,7 @@ def cmd_full_reading(args):
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planet_lons.get('Sun', 0)
moon_lon = planet_lons.get('Moon', 0)
shadbala_result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
shadbala_result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
# 添加顶层汇总
if isinstance(shadbala_result, dict):
sb_planets = shadbala_result.get('planets', {})
+186 -51
View File
@@ -20,6 +20,7 @@ v6.9.12 修复:
"""
import math
import swisseph as swe
import sys
import os
import json
@@ -118,11 +119,189 @@ SPECIAL_ASPECTS = {
# Virupas → Rupas 转换(60 Virupas = 1 Rupa
VIRUPAS_PER_RUPA = 60.0
_DIG_POWERLESS_HOUSE = {"Sun": 3, "Moon": 9, "Mars": 3, "Mercury": 6, "Jupiter": 6, "Venus": 9, "Saturn": 0}
_MOOLATRIKONA = {
"Sun": (4, 0.0, 20.0), "Moon": (1, 4.0, 30.0), "Mars": (0, 0.0, 12.0),
"Mercury": (5, 16.0, 20.0), "Jupiter": (8, 0.0, 10.0),
"Venus": (6, 0.0, 15.0), "Saturn": (10, 0.0, 20.0),
}
def build_shadbala_context(jd_ut: float, lat: float, lon: float, ayanamsa: str = "lahiri") -> Dict:
"""Build precise sidereal house context from standard birth inputs."""
sid_mode = swe.SIDM_LAHIRI
if str(ayanamsa).lower() in {"raman"}:
sid_mode = swe.SIDM_RAMAN
elif str(ayanamsa).lower() in {"kp", "krishnamurti"}:
sid_mode = swe.SIDM_KRISHNAMURTI
swe.set_sid_mode(sid_mode)
cusps, _ = swe.houses_ex(float(jd_ut), float(lat), float(lon), b"P", swe.FLG_SIDEREAL)
return {"jd_ut": float(jd_ut), "lat": float(lat), "lon": float(lon), "ayanamsa": str(ayanamsa), "house_midpoints": [float(value) % 360 for value in cusps]}
def calc_dig_bala_precise(pname: str, planet_lon: float, house_midpoints: list[float]) -> float:
"""Classical directional strength from the powerless bhava midpoint."""
powerless = house_midpoints[_DIG_POWERLESS_HOUSE[pname]]
return round(abs(float(powerless) - (float(planet_lon) % 360)) / 3.0, 2)
def _planet_longitude(name: str, planets: Dict) -> float:
data = planets[name]
degree = float(data.get("degree", 0.0))
if degree >= 30.0 or data.get("sign") not in SIGNS:
return degree % 360.0
return (SIGNS.index(data["sign"]) * 30.0 + degree) % 360.0
def classify_drik_planets(planets: Dict) -> tuple[set[str], set[str]]:
"""Classify the seven planets for Drik Bala from lunar phase and Mercury's company."""
benefics = {name for name in ("Jupiter", "Venus") if name in planets}
malefics = {name for name in ("Sun", "Mars", "Saturn") if name in planets}
if "Moon" in planets and "Sun" in planets:
target = benefics if (_planet_longitude("Moon", planets) - _planet_longitude("Sun", planets)) % 360.0 <= 180.0 else malefics
target.add("Moon")
if "Mercury" in planets:
mercury_lon = _planet_longitude("Mercury", planets)
mercury_sign = int(mercury_lon // 30)
companions = [
name for name in benefics | malefics
if name != "Mercury" and int(_planet_longitude(name, planets) // 30) == mercury_sign
]
benefic_count = sum(name in benefics for name in companions)
malefic_count = sum(name in malefics for name in companions)
if benefic_count >= malefic_count and (benefic_count != malefic_count or not companions):
benefics.add("Mercury")
elif malefic_count > benefic_count:
malefics.add("Mercury")
else:
nearest = min(companions, key=lambda name: abs(_planet_longitude(name, planets) - mercury_lon))
(benefics if nearest in benefics else malefics).add("Mercury")
return benefics, malefics
def _sphuta_drishti_virupas(angle: float, aspecting_planet: str) -> float:
angle = round(float(angle) % 360.0, 2)
if angle < 30.0:
strength = 0.0
elif angle < 60.0:
strength = (angle - 30.0) / 2.0
elif angle < 90.0:
strength = angle - 45.0 + (45.0 if aspecting_planet == "Saturn" else 0.0)
elif angle < 120.0:
strength = (120.0 - angle) / 2.0 + 30.0 + (15.0 if aspecting_planet == "Mars" else 0.0)
elif angle < 150.0:
strength = 150.0 - angle + (30.0 if aspecting_planet == "Jupiter" else 0.0)
elif angle < 180.0:
strength = 2.0 * (angle - 150.0)
elif angle < 300.0:
strength = (300.0 - angle) / 2.0
if aspecting_planet == "Mars" and 210.0 <= angle < 240.0:
strength += 15.0
elif aspecting_planet == "Jupiter" and 240.0 <= angle < 270.0:
strength += 30.0
elif aspecting_planet == "Saturn" and 270.0 <= angle < 300.0:
strength += 45.0
else:
strength = 0.0
return round(strength, 2)
def calc_drik_bala_precise(pname: str, all_planets: Dict) -> float:
"""Continuous Sphuta Drishti, quarter-weighted by natural benefic/malefic status."""
if pname not in all_planets:
return 0.0
benefics, malefics = classify_drik_planets(all_planets)
target_lon = _planet_longitude(pname, all_planets)
total = 0.0
for other_name in benefics | malefics:
if other_name == pname:
continue
strength = _sphuta_drishti_virupas(target_lon - _planet_longitude(other_name, all_planets), other_name)
total += strength if other_name in benefics else -strength
return round(total / 4.0, 2)
def _d30_sign(sign_idx: int, degree: float) -> int:
ranges = (
((5, 0), (10, 10), (18, 8), (25, 2), (30, 6))
if sign_idx % 2 == 0 else
((5, 1), (12, 5), (20, 11), (25, 9), (30, 7))
)
return next(sign for upper, sign in ranges if degree <= upper)
def _saptavarga_signs(planets: Dict) -> Dict[int, Dict[str, int]]:
result = {division: {} for division in (1, 2, 3, 7, 9, 12, 30)}
for name in set(planets) & set(_MOOLATRIKONA):
longitude = _planet_longitude(name, planets)
sign_idx, degree = int(longitude // 30), longitude % 30.0
result[1][name] = sign_idx
result[2][name] = (4 if sign_idx % 2 == 0 else 3) if degree < 15.0 else (3 if sign_idx % 2 == 0 else 4)
for division in (3, 7, 9, 12):
result[division][name] = varga_map(sign_idx, min(division - 1, int(degree / (30.0 / division))), division)
result[30][name] = _d30_sign(sign_idx, degree)
return result
def _compound_relation_score(planet: str, owner: str, d1_signs: Dict[str, int]) -> float:
natural = "friend" if owner in FRIENDSHIP[planet]["friend"] else "enemy" if owner in FRIENDSHIP[planet]["enemy"] else "neutral"
separation = (d1_signs[owner] - d1_signs[planet]) % 12
temporary = "friend" if separation in {1, 2, 3, 9, 10, 11} else "enemy"
return {
("friend", "friend"): 22.5,
("neutral", "friend"): 15.0,
("enemy", "friend"): 7.5,
("friend", "enemy"): 7.5,
("neutral", "enemy"): 3.75,
("enemy", "enemy"): 1.875,
}[(natural, temporary)]
def calc_sthana_bala_precise(pname: str, all_planets: Dict, house: int) -> Dict:
longitude = _planet_longitude(pname, all_planets)
degree = longitude % 30.0
vargas = _saptavarga_signs(all_planets)
scores = {}
for division, positions in vargas.items():
sign_idx = positions[pname]
owner = SIGN_LORDS[SIGNS[sign_idx]]
mt_sign, mt_start, mt_end = _MOOLATRIKONA[pname]
if division == 1 and sign_idx == mt_sign and mt_start <= degree < mt_end:
score = 45.0
elif owner == pname:
score = 30.0
else:
score = _compound_relation_score(pname, owner, vargas[1])
scores[f"sapta_d{division}"] = score
offset = (longitude - DEBILITATION_DEG[pname]) % 360.0
ucha_bala = round(min(offset, 360.0 - offset) / 3.0, 2)
wants_even = pname in {"Moon", "Venus"}
ojayugma = 15.0 * sum((vargas[division][pname] % 2 == 1) == wants_even for division in (1, 9))
kendra_bala = 60.0 if house in (1, 4, 7, 10) else 30.0 if house in (2, 5, 8, 11) else 15.0
drekkana_bala = 15.0 if (
(pname in {"Sun", "Mars", "Jupiter"} and degree < 10.0)
or (pname in {"Mercury", "Saturn"} and 10.0 <= degree < 20.0)
or (pname in {"Moon", "Venus"} and degree >= 20.0)
) else 0.0
sapta_score = round(sum(scores.values()), 2)
return {
"ucha_bala": ucha_bala,
**scores,
"sapta_score": sapta_score,
"ojayugma_bala": ojayugma,
"kendra_bala": kendra_bala,
"drekkana_bala": drekkana_bala,
"total": round(ucha_bala + sapta_score + ojayugma + kendra_bala + drekkana_bala, 2),
}
def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
sun_lon: float, moon_lon: float,
birth_minute: float = 0.0) -> Dict:
birth_minute: float = 0.0, context: Dict | None = None) -> Dict:
"""
计算 Shadbala 相对强弱参考(covered;外部绝对值校准前保留置信度上限)
@@ -152,8 +331,8 @@ def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
retro = p.get('retrograde', False)
speed = p.get('speed', 1.0)
sthana = calc_sthana_bala(pname, lon, sign, house)
dig = calc_dig_bala(pname, house)
sthana = calc_sthana_bala_precise(pname, planets, house)
dig = calc_dig_bala_precise(pname, lon, context["house_midpoints"]) if context and context.get("house_midpoints") else calc_dig_bala(pname, house)
kala = calc_kala_bala(pname, is_night, sun_northern, sun_lon, moon_lon, birth_hour, birth_minute)
chesta = calc_chesta_bala(pname, retro, speed, sun_lon, moon_lon)
naisargika = NAISARGIKA_BALA.get(pname, 30.0)
@@ -176,8 +355,8 @@ def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
retro = p.get('retrograde', False)
speed = p.get('speed', 1.0)
sthana = calc_sthana_bala(pname, lon, sign, house)
dig = calc_dig_bala(pname, house)
sthana = calc_sthana_bala_precise(pname, planets, house)
dig = calc_dig_bala_precise(pname, lon, context["house_midpoints"]) if context and context.get("house_midpoints") else calc_dig_bala(pname, house)
kala = calc_kala_bala(pname, is_night, sun_northern, sun_lon, moon_lon, birth_hour, birth_minute)
chesta = calc_chesta_bala(pname, retro, speed, sun_lon, moon_lon)
naisargika = NAISARGIKA_BALA.get(pname, 30.0)
@@ -737,52 +916,8 @@ def calc_yuddha_bala(planets: Dict) -> Dict:
def calc_drik_bala(pname: str, sign: str, house: int,
all_planets: Dict) -> float:
"""Drik Bala(相位力量),基于精确度数差(Sputa Drishti)计算。
v6.1.13: 升级为基于精确角距离的Sputa Drishti(替代简化宫位差)"""
drik = 0.0
p_sign_idx = SIGNS.index(sign) if sign in SIGNS else 0
p_degree_in_sign = (all_planets[pname].get('degree', 0) if pname in all_planets else 0) % 30
p_lon = p_sign_idx * 30 + p_degree_in_sign
for other_name, other_data in all_planets.items():
if other_name == pname or other_name == 'Rahu' or other_name == 'Ketu':
continue
other_sign = other_data.get('sign', '')
if other_sign not in SIGNS:
continue
other_sign_idx = SIGNS.index(other_sign)
other_deg_in_sign = other_data.get('degree', 0) % 30
other_lon = other_sign_idx * 30 + other_deg_in_sign
# 计算精确角度差
diff = abs(p_lon - other_lon)
if diff > 180:
diff = 360 - diff
# 检查传统相位规则(7宫=180°冲,特殊相位=Mars4/8, Jupiter5/9, Saturn3/10
has_aspect = False
house_diff = (p_sign_idx - other_sign_idx) % 12 + 1
if house_diff == 7 or house_diff == 1:
has_aspect = True
if other_name in SPECIAL_ASPECTS:
if house_diff in SPECIAL_ASPECTS[other_name]:
has_aspect = True
if not has_aspect:
continue
# 使用Sputa Drishti计算相位力量 (0-1) → 缩放为0-60 Virupas
sputa_value = _get_sputa_drishti_value(diff, other_name)
if other_name in BENEFICS:
drik += sputa_value * 60.0
elif other_name in MALEFICS:
drik -= sputa_value * 60.0
else:
drik += sputa_value * 30.0 # 中性行星
return max(-60.0, min(60.0, drik))
"""Compatibility wrapper for the continuous Sphuta Drishti calculation."""
return calc_drik_bala_precise(pname, all_planets)
# ============================================================================
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Build same-case D2/D4/D9/D10/AV/Shadbala parity artifacts."""
from __future__ import annotations
import hashlib
import json
import sys
import tempfile
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from benchmarks.jyotish.scripts.run_pyjhora_compare import build_pyjhora_sample
from benchmarks.jyotish.scripts.run_skill_baseline import run_sample
from scripts.three_engine_parity_runner import _capture_jyotishganit_raw
ORACLE = ROOT / "references" / "oracle"
ARTIFACTS = ORACLE / "artifacts"
PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
VED_VARGA_FIELDS = {"D2": "PlanetHoraD2Signs", "D4": "PlanetChaturthamshaD4Sign", "D9": "PlanetNavamshaD9Sign", "D10": "PlanetDashamamshaD10Sign"}
VED_COMPONENT_FIELDS = {
"sthana": "PlanetSthanaBala", "kala": "PlanetKalaBala", "dig": "PlanetDigBala",
"chesta": "PlanetChestaBala", "naisargika": "PlanetNaisargikaBala", "drik": "PlanetDrikBala",
}
SAMPLE = {
"id": "steve_jobs_public_aa", "label": "Steve Jobs public AA", "category": "public", "privacy": "public",
"birth": {"year": 1955, "month": 2, "day": 24, "hour": 19, "minute": 15, "lat": 37.7749, "lon": -122.4194, "tz": -8.0},
"today": "2026-07-17",
}
def _write(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
def _sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _jyotish_planet_signs(chart: dict[str, Any]) -> dict[str, str]:
return {str(p["celestialBody"]): str(p["sign"]) for h in chart.get("houses", []) for p in h.get("occupants", []) if p.get("celestialBody") in PLANETS}
def _jyotish_shadbala(raw: dict[str, Any]) -> tuple[dict[str, float], dict[str, dict[str, float]]]:
totals: dict[str, float] = {}; components: dict[str, dict[str, float]] = {}
for house in raw["d1Chart"]["houses"]:
for p in house.get("occupants", []):
name = p.get("celestialBody")
if name not in PLANETS or "shadbala" not in p:
continue
s = p["shadbala"]
totals[name] = float(s["Shadbala"]["Total"])
components[name] = {"sthana": float(s["Sthanabala"]["Total"]), "kala": float(s["Kaalabala"]["Total"]), "dig": float(s["Digbala"]), "chesta": float(s["Cheshtabala"]), "naisargika": float(s["Naisargikabala"]), "drik": float(s["Drikbala"])}
return totals, components
def _ved_payload(raw: dict[str, Any]) -> dict[str, Any]:
return raw["Payload"]["AllPlanetData"]
def _ved_components(raw: dict[str, Any], chesta: Any | None = None) -> dict[str, float]:
payload = _ved_payload(raw)
values = {name: float(payload[field]) for name, field in VED_COMPONENT_FIELDS.items() if field in payload}
if chesta is not None:
values["chesta"] = float(chesta)
return values
def _status(values: dict[str, Any], tolerance: float | None = None) -> str:
data = list(values.values())
if tolerance is not None and all(isinstance(v, (int, float)) for v in data):
return "match" if max(data) - min(data) <= tolerance else "mismatch"
return "match" if len({json.dumps(v, sort_keys=True) for v in data}) == 1 else "mismatch"
def build() -> dict[str, Any]:
local_result = run_sample(SAMPLE)
if not local_result.get("ok"):
raise RuntimeError(local_result.get("error"))
local = local_result["canonical"]
local["shadbala_method_variants"] = {
"kala": "bphs_local_solar_events_ahargana_declination",
"chesta": "bphs_bounded_surya_mean_motion_seeghrochcha",
"chesta_source": "MIT jyotishganit structure plus public Surya Siddhanta revolution constants",
}
pyjhora = build_pyjhora_sample(SAMPLE)
with tempfile.TemporaryDirectory() as tmp:
jyotish, _ = _capture_jyotishganit_raw(Path(tmp))
ved_path = ARTIFACTS / "vedastro_steve_jobs_public_aa_divisional_raw.json"
ved = json.loads(ved_path.read_text(encoding="utf-8"))
local_path = ARTIFACTS / "local_steve_jobs_high_rigor_raw.json"
py_path = ARTIFACTS / "pyjhora_steve_jobs_high_rigor_raw.json"
jy_path = ARTIFACTS / "jyotishganit_steve_jobs_high_rigor_raw.json"
_write(local_path, local); _write(py_path, pyjhora); _write(jy_path, jyotish)
rows: list[dict[str, Any]] = []
jy_d1 = _jyotish_planet_signs(jyotish["d1Chart"])
for planet in PLANETS:
values = {
"local": local["planets"][planet]["sign"],
"PyJHora_JHora": pyjhora["planets"][planet]["sign"],
"VedAstro": _ved_payload(ved["raw_responses"][planet])["PlanetRasiD1Sign"]["Name"],
"jyotishganit": jy_d1[planet],
}
rows.append({"section": "D1", "field": f"{planet}.sign", "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["planets"][planet]["sign"], **values})})
for section, ved_field in VED_VARGA_FIELDS.items():
jy_signs = _jyotish_planet_signs(jyotish["divisionalCharts"][section.lower()])
for planet in PLANETS:
values = {
"local": local["varga"][section][planet]["sign"],
"PyJHora_JHora": pyjhora["varga"][section][planet]["sign"],
"VedAstro": _ved_payload(ved["raw_responses"][planet])[ved_field]["Name"],
"jyotishganit": jy_signs[planet],
}
rows.append({"section": section, "field": f"{planet}.sign", "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["varga"][section][planet]["sign"], **values})})
ved_bav = ved["scalar_responses"]["ashtakavarga_bav"]["Payload"]["BhinnashtakavargaChart"]
ved_sav = ved["scalar_responses"]["ashtakavarga_sav_chart"]["Payload"]["SarvashtakavargaChart"]["Sarvashtakavarga"]["Rows"]
jy_av = jyotish["ashtakavarga"]
for planet in PLANETS:
values = {"local": local["ashtakavarga"]["bav"][planet], "PyJHora_JHora": pyjhora["ashtakavarga"]["bav"][planet], "VedAstro": ved_bav[planet]["Rows"], "jyotishganit": [jy_av[f"{planet.lower()}Bhav"][s] for s in SIGNS]}
rows.append({"section": "ashtakavarga_bav", "field": planet, "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["ashtakavarga"]["bav"][planet], **values})})
sav_values = {"local": local["ashtakavarga"]["sav"], "PyJHora_JHora": pyjhora["ashtakavarga"]["sav"], "VedAstro": ved_sav, "jyotishganit": [jy_av["sav"][s] for s in SIGNS]}
rows.append({"section": "ashtakavarga_sav", "field": "12_sign_scores", "local_value": sav_values.pop("local"), "oracle_values": sav_values, "status": _status({"local": local["ashtakavarga"]["sav"], **sav_values})})
jy_totals, jy_components = _jyotish_shadbala(jyotish)
for planet in PLANETS:
total_values = {"local": local["shadbala"][planet], "PyJHora_JHora": pyjhora["shadbala"][planet], "VedAstro": float(_ved_payload(ved["raw_responses"][planet])["PlanetShadbalaPinda"]), "jyotishganit": jy_totals[planet]}
rows.append({"section": "shadbala_total", "field": planet, "local_value": total_values.pop("local"), "oracle_values": total_values, "status": _status({"local": local["shadbala"][planet], **total_values}, 0.5)})
ved_chesta = ved["component_responses"][f"{planet}.chesta"]["Payload"]["PlanetChestaBala"]
ved_components = _ved_components(ved["raw_responses"][planet], ved_chesta)
for component in VED_COMPONENT_FIELDS:
values = {"local": local["shadbala_components"][planet][component], "PyJHora_JHora": pyjhora["shadbala_components"][planet][component], "VedAstro": ved_components[component], "jyotishganit": jy_components[planet][component]}
row = {"section": "shadbala_components", "field": f"{planet}.{component}", "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["shadbala_components"][planet][component], **values}, 0.5)}
if component == "chesta":
row["method_conflict"] = "PyJHora unbounded; VedAstro/jyotishganit bounded; Sun/Moon treatment differs across engines."
rows.append(row)
manifest = {
"case_id": "steve_jobs_public_1955_lahiri", "birth_data_policy": "public_case_only", "blocked_reason": "none",
"engines": {
"VedAstro": {"status": "official_verified", "official_raw_response_path": "artifacts/" + ved_path.name, "artifact_hash": _sha(ved_path), "settings": ved["settings"]},
"PyJHora_JHora": {"status": "imported", "raw_output_path": "artifacts/" + py_path.name, "artifact_hash": _sha(py_path), "settings": pyjhora["settings"]},
"jyotishganit": {"status": "imported", "raw_output_path": "artifacts/" + jy_path.name, "artifact_hash": _sha(jy_path), "settings": {"ayanamsa": jyotish["ayanamsa"]}},
},
"comparison_rows": rows,
"method_arbitration": {
"kala": "local_vs_PyJHora_7_of_7_within_0.05_virupa",
"chesta": "blocked_cross_engine_method_conflict",
"chesta_local_variant": "bphs_bounded_surya_mean_motion_seeghrochcha",
},
"runtime_boundary": "Full same-case raw coverage; mismatches remain formula/method differences, not missing artifacts.",
}
_write(ORACLE / "three_engine_parity_replay_manifest.json", manifest)
return manifest
if __name__ == "__main__":
result = build()
print(json.dumps({"status": "built", "rows": len(result["comparison_rows"])}, indent=2))