strengthen external parity diagnostics

This commit is contained in:
732642856
2026-07-12 15:13:30 +08:00
parent b336bdd9ac
commit 85e420cba4
10 changed files with 159 additions and 11 deletions
+19 -2
View File
@@ -39,6 +39,21 @@ def _varga_ascendant(payload: dict[str, Any], varga: str) -> str | None:
return None
def _all_varga_ascendants(payload: dict[str, Any]) -> dict[str, str | None]:
values = {varga.upper(): None for varga in _VARGAS}
try:
raw = _engine_json("varga", {**payload, "varga": "all"})
except subprocess.CalledProcessError:
return values
for name, chart in (raw.get("divisional_charts") or {}).items():
if not isinstance(chart, dict):
continue
for varga in _VARGAS:
if name.startswith(varga.upper() + "_"):
values[varga.upper()] = chart.get("ascendant")
return values
def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int = 30, step_minutes: int = 1) -> dict[str, Any]:
required = ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")
missing = [key for key in required if payload.get(key) is None]
@@ -53,7 +68,7 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
point = {**payload, "year": moment.year, "month": moment.month, "day": moment.day, "hour": moment.hour, "minute": moment.minute}
chart = _engine_json("chart", point)
asc = chart.get("ascendant", {})
divisional = {varga.upper(): _varga_ascendant(point, varga) for varga in _VARGAS}
divisional = _all_varga_ascendants(point)
rows.append({
"time": moment.strftime("%Y-%m-%d %H:%M"),
"offset_minutes": offset,
@@ -62,6 +77,8 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
"divisional_ascendants": divisional,
})
signatures = [tuple([row["d1_ascendant"], *row["divisional_ascendants"].values()]) for row in rows]
unavailable_vargas = [varga.upper() for varga in _VARGAS if all(row["divisional_ascendants"][varga.upper()] is None for row in rows)]
supported_vargas = [varga.lower() for varga in _VARGAS if varga.upper() not in unavailable_vargas]
modal = Counter(signatures).most_common(1)[0][0]
for row, signature in zip(rows, signatures):
row["sensitivity_count"] = sum(left != right for left, right in zip(signature, modal))
@@ -69,7 +86,6 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
name for name, current, typical in zip(("D1", "D4", "D9", "D10", "D24", "D30"), signature, modal)
if current != typical
]
unavailable_vargas = [varga.upper() for varga in _VARGAS if all(row["divisional_ascendants"][varga.upper()] is None for row in rows)]
transitions = []
for previous, current in zip(rows, rows[1:]):
changed = [name for name in ("d1_ascendant", "divisional_ascendants") if previous[name] != current[name]]
@@ -85,6 +101,7 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
"step_minutes": step_minutes,
"rows": rows,
"transitions": transitions,
"supported_vargas": [varga.upper() for varga in supported_vargas],
"unavailable_vargas": unavailable_vargas,
"pending_layers": ["UL", "A7", "A10", "KP_cusp"],
"boundary": "Actual local D1/Varga differences only. Unsupported Varga CLI flags are explicitly unavailable. Event answers still require an explicit event-to-candidate adjudication model before minute-level rectification.",
+4 -2
View File
@@ -1308,10 +1308,12 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
elif path == '/api/rectification/questionnaire':
self._json(build_rectification_questionnaire(body))
elif path == '/api/rectification/sensitivity_scan':
uncertainty = int(body.get('time_uncertainty_minutes') or 30)
step_minutes = int(body.get('step_minutes') or (5 if uncertainty > 15 else 1))
self._json(scan_candidate_times(
body,
uncertainty_minutes=int(body.get('time_uncertainty_minutes') or 30),
step_minutes=int(body.get('step_minutes') or 1),
uncertainty_minutes=uncertainty,
step_minutes=step_minutes,
))
elif path == '/api/rectification/answers':
questionnaire = body.get('questionnaire')
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Summarize reviewable PyJHora comparison matrices without overstating coverage."""
from __future__ import annotations
import argparse
import csv
import json
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
REQUIRED_FULL_PARITY = ("D1", "D9", "D10", "D2", "D4", "Vimshottari", "Shadbala", "Ashtakavarga")
MATRIX_SECTION_MAP = {"ascendant": "D1", "planet": "D1", "dasha": "Vimshottari", "D9": "D9", "D10": "D10"}
def summarize_matrix(path: str | Path, *, settings: dict[str, Any]) -> dict[str, Any]:
path = Path(path)
rows = list(csv.DictReader(path.open(encoding="utf-8")))
status_counts = Counter(str(row.get("status") or "unknown") for row in rows)
sections: dict[str, dict[str, int]] = defaultdict(lambda: {"total": 0, "match": 0, "mismatch": 0})
covered = set()
for row in rows:
section = MATRIX_SECTION_MAP.get(str(row.get("section") or ""))
if not section:
continue
covered.add(section)
sections[section]["total"] += 1
if row.get("status") == "match":
sections[section]["match"] += 1
elif row.get("status") == "mismatch":
sections[section]["mismatch"] += 1
missing = [field for field in REQUIRED_FULL_PARITY if field not in covered]
return {
"scope": "pyjhora_same_chart_parity_summary",
"matrix_path": str(path),
"tested": bool(rows),
"settings": settings,
"row_counts": dict(status_counts),
"coverage": dict(sorted(sections.items())),
"covered_outputs": sorted(covered),
"missing_required_outputs": missing,
"status": "partial_verified" if rows and not status_counts.get("mismatch") else "partial_mismatch",
"full_parity_verified": not missing and not status_counts.get("mismatch"),
"boundary": "Only covered outputs are compared. This summary cannot promote full parity while required outputs are absent.",
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("matrix")
parser.add_argument("--ayanamsa", default="lahiri")
parser.add_argument("--node-mode", default="mean", choices=["mean", "true"])
args = parser.parse_args()
print(json.dumps(summarize_matrix(args.matrix, settings={"ayanamsa": args.ayanamsa, "node_mode": args.node_mode}), ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())