Files
Jyotisha/tests/test_special_lagnas_birth_instant.py
T
jesse-ux 795b8845c7
Independent Staging Quality Gate / validate (push) Successful in 19m4s
Independent Staging Quality Gate / publish (push) Successful in 3m51s
fix(special-lagnas): compute eight birth points without PyJHora
Production images do not install PyJHora, so those eight points were blocked.
Swiss Ephemeris now computes them in-process. Rectification keeps the old
Hora and Ghati formulas.

BUG-1273.
2026-10-08 15:01:44 +08:00

180 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""BUG-1273: eight birth special lagnas are native, and rectification keeps the old formulas.
The frozen PyJHora 4.8.7 answers live in tests/fixtures/special_lagnas_pyjhora_487.json.
These tests do not import jhora. The offline generator is
scripts/research/freeze_special_lagnas_pyjhora_fixture.py.
"""
from __future__ import annotations
import builtins
import importlib
import json
import sys
from datetime import datetime
from pathlib import Path
import pytest
import scripts.ayanamsa_utils as ayanamsa_utils
from scripts.ayanamsa_utils import apply_ayanamsa
from scripts.special_lagnas import SpecialLagnasCalculator, bhava_house_rows
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json"
EIGHT = (
"Bhava_Lagna",
"Hora_Lagna",
"Ghati_Lagna",
"ViGhati_Lagna",
"Sree_Lagna",
"Indu_Lagna",
"Pranapada_Lagna",
"Varnada_Lagna",
)
CASE_IDS = ("steve_jobs_1955_aa", "barack_obama_1961_aa", "fictional_reader_main")
def _fixture():
payload = json.loads(FIXTURE.read_text(encoding="utf-8"))
assert payload["pyjhora_version"] == "4.8.7"
return payload
def _block_jhora(monkeypatch):
"""Fail closed if the runtime path imports PyJHora, including a cached package."""
real_import = builtins.__import__
def guarded(name, globals=None, locals=None, fromlist=(), level=0):
if str(name).split(".", 1)[0] == "jhora":
raise ImportError("jhora blocked")
return real_import(name, globals, locals, fromlist, level)
monkeypatch.setattr(builtins, "__import__", guarded)
real_import_module = importlib.import_module
def guarded_module(name, package=None):
if str(name).split(".", 1)[0] == "jhora":
raise ImportError("jhora blocked")
return real_import_module(name, package)
monkeypatch.setattr(importlib, "import_module", guarded_module)
for key in list(sys.modules):
if key == "jhora" or key.startswith("jhora."):
monkeypatch.delitem(sys.modules, key, raising=False)
def _rows(birth, ayanamsa):
moment = datetime(
birth["year"], birth["month"], birth["day"],
birth["hour"], birth["minute"], birth["second"],
)
return SpecialLagnasCalculator().calculate_all_lagnas(
0.0, 0.0, 0.0, moment, moment.replace(hour=6),
lat=birth["lat"], lon=birth["lon"], tz_offset=birth["tz"], ayanamsa=ayanamsa,
)
@pytest.mark.parametrize("case_id", CASE_IDS)
@pytest.mark.parametrize("ayanamsa", ["lahiri", "raman"])
def test_native_matches_frozen_pyjhora_within_one_hundredth(case_id, ayanamsa, monkeypatch):
_block_jhora(monkeypatch)
spec = _fixture()["cases"][case_id]
before = ayanamsa_utils.ACTIVE_AYANAMSA_NAME
rows = _rows(spec["birth"], ayanamsa)
expected = spec["ayanamsa"][ayanamsa]
for key in EIGHT:
row = rows[key]
assert row["status"] == "computed"
assert row["source"] == "native_special_lagna"
assert "pyjhora" not in row["source"]
assert "pyjhora" not in json.dumps(row).lower()
assert row["sign"] == expected[key]["sign"]
assert row["sign_degree"] == pytest.approx(expected[key]["sign_degree"], abs=0.01)
assert row["degree"] == pytest.approx(expected[key]["longitude"], abs=0.01)
assert row["ayanamsa"] == ayanamsa
assert abs(rows["Hora_Lagna"]["degree"] - rows["Ghati_Lagna"]["degree"]) > 0.01
assert before == ayanamsa_utils.ACTIVE_AYANAMSA_NAME
def test_jobs_lahiri_signs_stay_on_the_birth_instant(monkeypatch):
_block_jhora(monkeypatch)
apply_ayanamsa("raman")
before = ayanamsa_utils.ACTIVE_AYANAMSA_NAME
birth = _fixture()["cases"]["steve_jobs_1955_aa"]["birth"]
rows = _rows(birth, "lahiri")
expected = {
"Bhava_Lagna": ("Leo", 17.12),
"Hora_Lagna": ("Aquarius", 22.58),
"Ghati_Lagna": ("Virgo", 8.94),
}
for key, (sign, degree) in expected.items():
assert rows[key]["sign"] == sign
assert rows[key]["sign_degree"] == pytest.approx(degree, abs=0.01)
assert rows["Hora_Lagna"]["sign"] != rows["Ghati_Lagna"]["sign"]
assert before == ayanamsa_utils.ACTIVE_AYANAMSA_NAME
def test_rectification_hora_and_ghati_formulas_stay_on_the_old_identity():
birth = datetime(2000, 1, 1, 19, 15, 0)
sunrise = birth.replace(hour=6, minute=0, second=0)
calculator = SpecialLagnasCalculator()
hora = calculator.calculate_hora_lagna(10.0, 0.0, birth, sunrise)
ghati = calculator.calculate_ghati_lagna(10.0, birth, sunrise)
assert hora["formula"] == "Asc + (Hora数 × 15°)"
assert ghati["formula"] == "Asc + (Ghati数 × 6°)"
assert hora["degree"] == pytest.approx(208.75, abs=1e-4)
assert ghati["degree"] == pytest.approx(208.75, abs=1e-4)
assert not hasattr(calculator, "calculate_pyjhora_special_lagnas")
def test_missing_coordinates_keep_old_three_and_block_the_other_five():
moment = datetime(2000, 1, 1, 19, 15, 0)
sunrise = moment.replace(hour=6, minute=0, second=0)
rows = SpecialLagnasCalculator().calculate_all_lagnas(
10.0, 20.0, 30.0, moment, sunrise,
)
assert rows["Bhava_Lagna"]["formula"] == "Asc + (Sun - Moon)"
assert rows["Hora_Lagna"]["formula"] == "Asc + (Hora数 × 15°)"
assert rows["Hora_Lagna"]["degree"] == pytest.approx(208.75, abs=1e-4)
assert rows["Ghati_Lagna"]["formula"] == "Asc + (Ghati数 × 6°)"
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
assert rows[key]["status"] == "blocked"
assert rows[key]["reason"] == "birth_coordinates_or_timezone_missing"
assert rows[key]["source"] == "native_special_lagna"
assert "pyjhora" not in rows[key]["source"]
def test_full_reading_eight_points_with_jhora_blocked(monkeypatch):
from scripts.jyotish_engine import cmd_full_reading
from scripts.professional_report_reference import _export_args
_block_jhora(monkeypatch)
for key in list(sys.modules):
if key in {
"special_lagnas",
"scripts.special_lagnas",
"native_special_lagnas",
"scripts.native_special_lagnas",
} or key == "jhora" or key.startswith("jhora."):
monkeypatch.delitem(sys.modules, key, raising=False)
birth = _fixture()["cases"]["steve_jobs_1955_aa"]["birth"]
report = cmd_full_reading(_export_args({**birth, "ayanamsa": "lahiri", "node_mode": "mean"}))
points = report["modules"]["special_lagnas"]
for key in EIGHT:
row = points[key]
assert row["status"] == "computed", key
assert row["source"] == "native_special_lagna"
assert "pyjhora" not in json.dumps(row).lower()
def test_bhava_house_rows_drop_summary_rows():
rows = bhava_house_rows({
"houses": [
{"house": 1, "sign": "Aries"},
{"house": "total", "sign": "summary"},
{"house": 12, "sign": "Pisces"},
]
})
assert [row["house"] for row in rows] == [1, 12]