217 lines
8.4 KiB
Python
217 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
||
"""Regression tests for BPHS divisional chart mappings."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from hypothesis import given
|
||
from hypothesis import strategies as st
|
||
from varga import (
|
||
SIGNS,
|
||
calc_22nd_drekkana,
|
||
calc_64th_navamsa,
|
||
calc_all_vargas,
|
||
calc_bhrigu_bindu,
|
||
calc_sarpa_drekkana,
|
||
calc_varga,
|
||
varga_map,
|
||
)
|
||
|
||
|
||
def navamsa_ref(lon: float) -> int:
|
||
sign_index = int((lon % 360) / 30) % 12
|
||
degree_in_sign = (lon % 360) - sign_index * 30
|
||
part_index = int(degree_in_sign / (30 / 9))
|
||
if sign_index % 3 == 0:
|
||
start = sign_index
|
||
elif sign_index % 3 == 1:
|
||
start = (sign_index + 8) % 12
|
||
else:
|
||
start = (sign_index + 4) % 12
|
||
return (start + part_index) % 12
|
||
|
||
|
||
def dasamsa_ref(lon: float) -> int:
|
||
sign_index = int((lon % 360) / 30) % 12
|
||
degree_in_sign = (lon % 360) - sign_index * 30
|
||
part_index = int(degree_in_sign / 3)
|
||
start = sign_index if sign_index % 2 == 0 else (sign_index + 8) % 12
|
||
return (start + part_index) % 12
|
||
|
||
|
||
def drekkana_ref(lon: float) -> int:
|
||
sign_index = int((lon % 360) / 30) % 12
|
||
degree_in_sign = (lon % 360) - sign_index * 30
|
||
part_index = int(degree_in_sign / 10)
|
||
return (sign_index + part_index * 4) % 12
|
||
|
||
|
||
def chaturthamsa_ref(lon: float) -> int:
|
||
sign_index = int((lon % 360) / 30) % 12
|
||
degree_in_sign = (lon % 360) - sign_index * 30
|
||
part_index = int(degree_in_sign / (30 / 4))
|
||
return (sign_index + part_index * 3) % 12
|
||
|
||
|
||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||
def test_navamsa_matches_bphs_reference(lon: float) -> None:
|
||
result = calc_varga(lon, 9)
|
||
assert result["sign_idx"] == navamsa_ref(lon)
|
||
assert result["sign"] == SIGNS[navamsa_ref(lon)]
|
||
assert 0 <= result["degree_in_sign"] < 30
|
||
|
||
|
||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||
def test_dasamsa_matches_bphs_reference(lon: float) -> None:
|
||
result = calc_varga(lon, 10)
|
||
assert result["sign_idx"] == dasamsa_ref(lon)
|
||
assert result["sign"] == SIGNS[dasamsa_ref(lon)]
|
||
assert 0 <= result["degree_in_sign"] < 30
|
||
|
||
|
||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||
def test_drekkana_uses_same_plus_four_plus_eight(lon: float) -> None:
|
||
result = calc_varga(lon, 3)
|
||
assert result["sign_idx"] == drekkana_ref(lon)
|
||
|
||
|
||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||
def test_chaturthamsa_matches_pyjhora_parashara_reference(lon: float) -> None:
|
||
result = calc_varga(lon, 4)
|
||
assert result["sign_idx"] == chaturthamsa_ref(lon)
|
||
assert result["sign"] == SIGNS[chaturthamsa_ref(lon)]
|
||
assert 0 <= result["degree_in_sign"] < 30
|
||
|
||
|
||
def test_chaturthamsa_reference_boundary_examples() -> None:
|
||
assert [varga_map(0, part, 4) for part in range(4)] == [0, 3, 6, 9]
|
||
assert [varga_map(8, part, 4) for part in range(4)] == [8, 11, 2, 5]
|
||
assert [varga_map(11, part, 4) for part in range(4)] == [11, 2, 5, 8]
|
||
|
||
|
||
def test_varga_map_boundary_examples() -> None:
|
||
assert varga_map(0, 0, 9) == 0 # Aries Navamsa starts Aries
|
||
assert varga_map(1, 0, 9) == 9 # Taurus Navamsa starts Capricorn (9th from sign)
|
||
assert varga_map(2, 0, 9) == 6 # Gemini Navamsa starts Libra (5th from sign)
|
||
assert varga_map(1, 0, 10) == 9 # Taurus Dasamsa starts Capricorn
|
||
assert varga_map(0, 2, 3) == 8 # Aries third Drekkana = Sagittarius
|
||
|
||
|
||
def test_rudramsa_d11_is_supported_by_native_varga_map() -> None:
|
||
assert varga_map(0, 0, 11) == 0
|
||
assert varga_map(0, 10, 11) == 10
|
||
# 原值: varga_map(1, 0, 11) == 9;varga_map(1, 10, 11) == 7;calc_varga(35) == Aquarius
|
||
# 新值: 11、9、Aries
|
||
# 原因: BUG-1260,D11 改按 PVR 6.2.11 从白羊逆数起点,再顺数各份
|
||
assert varga_map(1, 0, 11) == 11
|
||
assert varga_map(1, 10, 11) == 9
|
||
result = calc_varga(35.0, 11)
|
||
assert result["sign"] == "Aries"
|
||
assert 0 <= result["degree_in_sign"] < 30
|
||
|
||
|
||
def test_native_varga_map_supports_declared_high_value_vargas() -> None:
|
||
for div in (2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24, 60, 81, 108, 144):
|
||
result = calc_varga(35.0, div)
|
||
assert result["sign"] in SIGNS
|
||
assert 0 <= result["degree_in_sign"] < 30
|
||
|
||
|
||
def test_default_varga_batch_includes_declared_vargas() -> None:
|
||
charts = calc_all_vargas({"Moon": 35.0}, 10.0)
|
||
for key in ("D5_Panchamsa", "D6_Shashthamsa", "D8_Ashtamsa", "D11_Rudramsa", "D81_Navamsa-Navamsa", "D108_Dwadasamsa-Navamsa", "D144_Dwadasamsa-Dwadasamsa"):
|
||
assert key in charts
|
||
# 原值: Aquarius / 新值: Aries / 原因: BUG-1260,35° 的 D11 按 PVR 6.2.11 落到白羊
|
||
assert charts["D11_Rudramsa"]["Moon"]["sign"] == "Aries"
|
||
assert "6.2.5" in charts["D5_Panchamsa"]["_meta"]["mapping_source"]
|
||
assert "6.2.11" in charts["D11_Rudramsa"]["_meta"]["mapping_source"]
|
||
|
||
|
||
def test_pvr_d5_d6_d8_d11_worked_examples() -> None:
|
||
"""Hand counts from PVR Integrated Approach 6.2.5 / 6.2.6 / 6.2.8 / 6.2.11."""
|
||
# 6.2.5: odd parts Ar, Aq, Sg, Ge, Li; even Ta, Vi, Pi, Cp, Sc.
|
||
# 11° Gemini is the 2nd part of an odd sign.
|
||
assert calc_varga(60 + 11, 5)["sign"] == "Aquarius"
|
||
# 19° Taurus is the 4th part of an even sign.
|
||
assert calc_varga(30 + 19, 5)["sign"] == "Capricorn"
|
||
# 6.2.6: 11° Gemini -> Gemini; 19° Scorpio -> Capricorn.
|
||
assert calc_varga(60 + 11, 6)["sign"] == "Gemini"
|
||
assert calc_varga(210 + 19, 6)["sign"] == "Capricorn"
|
||
# 6.2.8 counting: 10° Gemini is the 3rd part of a dual sign, 3rd from Leo is Libra.
|
||
# 19° Scorpio is the 6th part of a fixed sign, 6th from Sagittarius is Taurus.
|
||
assert calc_varga(60 + 10, 8)["sign"] == "Libra"
|
||
assert calc_varga(210 + 19, 8)["sign"] == "Taurus"
|
||
# 6.2.11: 11° Gemini -> Gemini; 19° Scorpio -> Pisces.
|
||
assert calc_varga(60 + 11, 11)["sign"] == "Gemini"
|
||
assert calc_varga(210 + 19, 11)["sign"] == "Pisces"
|
||
|
||
|
||
def test_navamsa_matches_user_jhora_pdf_reference_chart() -> None:
|
||
"""Regression from private_chart_reference.pdf: JHora-style D9 table for public sample birth datetime San Francisco."""
|
||
expected = {
|
||
132.355025: "Cancer", # Ascendant 12 Leo 21'18.09"
|
||
3.5226611111111112: "Taurus",
|
||
311.78995555555554: "Capricorn",
|
||
91.33091388888889: "Cancer",
|
||
338.5488: "Virgo",
|
||
163.83150833333335: "Taurus",
|
||
340.5554638888889: "Libra",
|
||
304.3033805555556: "Scorpio",
|
||
231.04509444444443: "Capricorn",
|
||
51.045094444444445: "Cancer",
|
||
}
|
||
for longitude, expected_sign in expected.items():
|
||
assert calc_varga(longitude, 9)["sign"] == expected_sign
|
||
|
||
|
||
def test_64th_navamsa_counts_forward_from_moon_navamsa() -> None:
|
||
moon_lon = 42.3
|
||
result = calc_64th_navamsa(moon_lon)
|
||
expected_sign_idx = (navamsa_ref(moon_lon) + 63) % 12
|
||
assert result["sign_idx"] == expected_sign_idx
|
||
assert result["sign"] == SIGNS[expected_sign_idx]
|
||
assert result["offset_from_moon_navamsa"] == 64
|
||
|
||
|
||
def test_22nd_drekkana_counts_forward_from_lagna_drekkana() -> None:
|
||
asc_lon = 25.5
|
||
result = calc_22nd_drekkana(asc_lon)
|
||
expected_sign_idx = (drekkana_ref(asc_lon) + 21) % 12
|
||
assert result["sign_idx"] == expected_sign_idx
|
||
assert result["sign"] == SIGNS[expected_sign_idx]
|
||
assert result["offset_from_lagna_drekkana"] == 22
|
||
|
||
|
||
def test_bhrigu_bindu_uses_rahu_to_moon_arc_midpoint() -> None:
|
||
result = calc_bhrigu_bindu(270.5, 160.2333333333)
|
||
assert result["longitude"] == 215.3667
|
||
assert result["sign"] == "Scorpio"
|
||
assert result["degree_in_sign"] == 5.3667
|
||
|
||
|
||
def test_bhrigu_bindu_handles_wraparound_across_zero_aries() -> None:
|
||
result = calc_bhrigu_bindu(10.0, 350.0)
|
||
assert result["longitude"] == 0.0
|
||
assert result["sign"] == "Aries"
|
||
assert result["degree_in_sign"] == 0.0
|
||
|
||
|
||
def test_sarpa_drekkana_detects_classical_sensitive_ranges() -> None:
|
||
cancer_second = calc_sarpa_drekkana(105.0)
|
||
scorpio_first = calc_sarpa_drekkana(215.0)
|
||
pisces_third = calc_sarpa_drekkana(355.0)
|
||
safe_aries = calc_sarpa_drekkana(15.0)
|
||
|
||
assert cancer_second["is_sarpa_drekkana"] is True
|
||
assert cancer_second["sign"] == "Cancer"
|
||
assert cancer_second["drekkana_number"] == 2
|
||
|
||
assert scorpio_first["is_sarpa_drekkana"] is True
|
||
assert scorpio_first["sign"] == "Scorpio"
|
||
assert scorpio_first["drekkana_number"] == 1
|
||
|
||
assert pisces_third["is_sarpa_drekkana"] is True
|
||
assert pisces_third["sign"] == "Pisces"
|
||
assert pisces_third["drekkana_number"] == 3
|
||
|
||
assert safe_aries["is_sarpa_drekkana"] is False
|