Files
Jyotisha/references/open_source_sources/jaimini-tropical/jaimini/tests/test_engine.py
T
732642856 f83db2fac1 Enhance Jyotish validation and Jaimini modules
- add external validation reports and open-source comparison references

- integrate Jaimini arudha/graha pada, enhanced argala, and additional synastry kutas

- update skill docs and capability matrices

- add smoke tests for open-source integrations
2026-06-10 20:50:52 +08:00

228 lines
7.8 KiB
Python

"""Validation tests for Jaimini engine — planetary positions, houses, karakas.
Tests against known reference values from Swiss Ephemeris (swetest64.exe),
manual calculations, and edge cases across different times and locations.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import math
from jaimini.engine.ephemeris import get_all_planets, get_planet_position, julian_day
from jaimini.engine.houses import calc_ascendant, calc_houses, calc_sunrise
from jaimini.engine.time_utils import parse_dms, to_dms, zodiac_position, utc_to_jd, local_to_utc, parse_timezone
def test_sun_position_1949():
"""Verify Sun position against swetest64.exe reference value.
Reference: 1949-10-01 07:00 UTC, Sun = 187.766607° (Libra 7°45'59.786")
"""
pos = get_planet_position('Su', 1949, 10, 1, 7, 0, 0)
expected = 187.766607
diff = abs(pos['lon'] - expected)
# DE421 vs DE431: expect < 0.002° (~7 arcsec)
assert diff < 0.002, f"Sun position off by {diff*3600:.1f} arcsec"
assert pos['sign'] == 'Lib'
assert 7.7 < pos['sign_deg'] < 7.8
def test_moon_position_1949():
"""Verify Moon position against swetest64.exe reference value.
Reference: 1949-10-01 07:00 UTC, Moon = 303.048826° (Aquarius 3°02'55.773")
"""
pos = get_planet_position('Mo', 1949, 10, 1, 7, 0, 0)
expected = 303.048826
diff = abs(pos['lon'] - expected)
# Moon accuracy: DE421 vs DE431, allow up to 0.01° (~0.6 arcmin)
assert diff < 0.01, f"Moon position off by {diff*3600:.1f} arcsec"
assert pos['sign'] == 'Aqr'
assert 3.0 < pos['sign_deg'] < 3.1
def test_ascendant_1949():
"""Verify Ascendant for New China chart.
1949-10-01 15:00 CST (07:00 UTC), Beijing (39.907N, 116.397E)
Reference: Leo approx 11°30' (from user's article)
"""
asc = calc_ascendant(1949, 10, 1, 7, 0, 0, 39.907, 116.397)
assert 120 < asc < 150 # Leo = 120-150°
assert 11.0 < (asc % 30) < 12.0 # ~11°30' in sign
def test_all_planets_consistent():
"""All planets should return valid positions at any time."""
planets = get_all_planets(2025, 6, 15, 12, 0, 0)
required = ['Su', 'Mo', 'Ma', 'Me', 'Ju', 'Ve', 'Sa']
for p in required:
assert p in planets
assert 0 <= planets[p]['lon'] <= 360
assert 0 <= planets[p]['sign_idx'] <= 11
assert planets[p]['sign'] is not None
def test_different_latitudes():
"""Ascendant calculation should work across different latitudes."""
# Equator (0°)
asc_eq = calc_ascendant(2025, 6, 15, 12, 0, 0, 0.0, 77.0)
assert 0 <= asc_eq <= 360
# Mid-latitude (45°N)
asc_mid = calc_ascendant(2025, 6, 15, 12, 0, 0, 45.0, 10.0)
assert 0 <= asc_mid <= 360
# High latitude (60°N)
asc_high = calc_ascendant(2025, 6, 15, 12, 0, 0, 60.0, 30.0)
assert 0 <= asc_high <= 360
# Southern hemisphere (33°S)
asc_south = calc_ascendant(2025, 12, 25, 10, 0, 0, -33.0, 151.0)
assert 0 <= asc_south <= 360
def test_different_dates():
"""Engine should work across wide date ranges (1900-2100)."""
test_cases = [
(1900, 1, 1, 12, 0, 0),
(1950, 6, 15, 6, 30, 0),
(2000, 1, 1, 0, 0, 0),
(2020, 6, 15, 12, 0, 0),
(2050, 1, 1, 12, 0, 0), # DE421 max ~2053-10-09
]
for y, m, d, h, mi, s in test_cases:
planets = get_all_planets(y, m, d, h, mi, s)
assert 'Su' in planets
assert 0 <= planets['Su']['lon'] <= 360
assert 'Mo' in planets
assert 0 <= planets['Mo']['lon'] <= 360
def test_sunrise_calculation():
"""Sunrise should be between 0-24h and reasonable for given location."""
# Beijing summer sunrise should be ~5:00 local = ~21:00 UTC previous day
sr = calc_sunrise(2025, 6, 15, 39.9, 116.4)
assert 0 <= sr <= 24
# Winter sunrise should be later
sr_winter = calc_sunrise(2025, 12, 21, 39.9, 116.4)
assert sr_winter > sr # Winter sunrise later than summer
# Equator sunrise should be ~6:00 year-round
sr_eq = calc_sunrise(2025, 3, 20, 0.0, 0.0)
assert 5.5 < sr_eq < 6.5
def test_house_positions_whole_sign():
"""Whole Sign houses: each house = one sign, 1st = ascendant sign."""
houses = calc_houses(2025, 6, 15, 12, 0, 0, 40.0, -74.0, system='W')
assert len(houses) == 12
for i, h in enumerate(houses):
assert h['house'] == i + 1
# Whole Sign: each cusp should be at 0° of its sign
assert h['sign_deg'] == 0.0
assert 0 <= h['sign_idx'] <= 11
# Signs should be consecutive (asc + 0, asc + 1, ..., asc + 11)
asc_sign = houses[0]['sign_idx']
for i, h in enumerate(houses):
assert h['sign_idx'] == (asc_sign + i) % 12
def test_dms_conversion():
"""DMS parsing and formatting should be accurate and round-trip."""
# Parse
assert abs(parse_dms("39°54'25\"") - 39.906944) < 0.0001
assert abs(parse_dms("116°23'50\"") - 116.397222) < 0.0001
assert abs(parse_dms("39.907") - 39.907) < 0.0001
assert abs(parse_dms("0°0'0\"") - 0.0) < 0.0001
assert abs(parse_dms("180°0'0\"") - 180.0) < 0.0001
# Format
d, m, s = to_dms(39.906944)
assert d == 39
assert m == 54
assert abs(s - 25.0) < 0.1
# Zodiac position
sign_idx, deg, label = zodiac_position(187.766607)
assert sign_idx == 6 # Libra = index 6
assert abs(deg - 7.766607) < 0.0001
def test_timezone_parsing():
"""Timezone parsing should handle various formats."""
assert parse_timezone('+8') == 8.0
assert parse_timezone('-5') == -5.0
assert parse_timezone('+5.5') == 5.5
assert parse_timezone('-5:30') == -5.5
assert parse_timezone('+0') == 0.0
def test_planet_retrograde_detection():
"""Speed calculation should detect retrograde motion."""
# Mercury retrograde periods known from astronomical data
pos = get_planet_position('Me', 2025, 7, 18, 12, 0, 0)
# Mercury typically retrogrades 3-4 times per year
# Just verify speed is calculated (can be retro or direct)
# Speed should always be positive (we store abs(speed))
assert pos['speed'] > 0
# retrograde should be Python bool
assert isinstance(pos['retrograde'], bool)
assert pos['speed'] < 15.0
def test_julian_day_consistency():
"""Julian Day should be consistent with known astronomy."""
# J2000.0 = 2451545.0
jd = julian_day(2000, 1, 1, 12, 0, 0)
assert abs(jd - 2451545.0) < 0.01
# 1949-10-01 JD should be ~2433190
jd2 = julian_day(1949, 10, 1, 7, 0, 0)
assert 2433190 < jd2 < 2433191
def test_sun_never_below_zero():
"""Sun longitude should always be 0-360. Regression test for modulo bugs."""
for year in [1950, 2000, 2025, 2050]:
pos = get_planet_position('Su', year, 6, 15, 12, 0, 0)
assert 0 <= pos['lon'] <= 360
if __name__ == '__main__':
# Run all tests and report
tests = [
test_sun_position_1949,
test_moon_position_1949,
test_ascendant_1949,
test_all_planets_consistent,
test_different_latitudes,
test_different_dates,
test_sunrise_calculation,
test_house_positions_whole_sign,
test_dms_conversion,
test_timezone_parsing,
test_planet_retrograde_detection,
test_julian_day_consistency,
test_sun_never_below_zero,
]
passed = 0
failed = 0
for test in tests:
try:
test()
print(f" PASS: {test.__name__}")
passed += 1
except AssertionError as e:
print(f" FAIL: {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {test.__name__}: {type(e).__name__}: {e}")
failed += 1
print(f"\n{'='*50}")
print(f"Results: {passed} passed, {failed} failed, {len(tests)} total")
assert failed == 0, f"{failed} tests failed!"