f83db2fac1
- 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
228 lines
7.8 KiB
Python
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!"
|