Files
Jyotisha/tests/test_divisional_charts_extended.py
T
2026-06-25 18:33:26 +08:00

309 lines
11 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.
#!/usr/bin/env python3
"""Tests for divisional_charts_extended module — D2 variants, D3 variants,
composite vargas, custom D-N, and list_available_variants."""
from __future__ import annotations
import sys
import os
import pytest
# Add scripts dir to path
SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts')
if SCRIPTS not in sys.path:
sys.path.insert(0, SCRIPTS)
from divisional_charts_extended import DivisionalChartsCalculator, VargaType
calc = DivisionalChartsCalculator()
# ── D2 Hora Variant Tests ──────────────────────────────────────────
D2_VARIANTS = ['parashara', 'pariveshta', 'parivritta',
'parivritta_trayodamsa', 'surya_chandra', 'ahoratra']
@pytest.mark.parametrize("variant", D2_VARIANTS)
def test_d2_variant_returns_valid_longitude(variant):
"""Each D2 variant must return a longitude in [0, 360)."""
lon = calc._calculate_d2_variant(0, 15.5, variant)
assert 0 <= lon < 360, f"{variant}: lon={lon} out of range"
@pytest.mark.parametrize("variant", D2_VARIANTS)
def test_d2_variant_sign_is_leo_or_cancer_for_odd_first_half(variant):
"""For Aries (odd) 0-15 degrees, Parashara/Surya/Chandra/Ahoratra → Leo."""
if variant in ('parashara', 'surya_chandra', 'ahoratra'):
lon = calc._calculate_d2_variant(0, 5.0, variant)
sign_idx = int(lon // 30)
assert sign_idx == 4, f"{variant}: expected Leo (4), got {calc.SIGNS[sign_idx]}"
@pytest.mark.parametrize("variant", D2_VARIANTS)
def test_d2_variant_second_half_odd_sign(variant):
"""For Aries (odd) 15-30 degrees, Parashara → Cancer."""
if variant in ('parashara', 'surya_chandra', 'ahoratra'):
lon = calc._calculate_d2_variant(0, 20.0, variant)
sign_idx = int(lon // 30)
assert sign_idx == 3, f"{variant}: expected Cancer (3), got {calc.SIGNS[sign_idx]}"
def test_d2_pariveshta_first_half_same_sign():
"""Pariveshta: first half stays in same sign."""
lon = calc._calculate_d2_variant(0, 5.0, 'pariveshta')
assert int(lon // 30) == 0 # Aries
def test_d2_pariveshta_second_half_next_sign():
"""Pariveshta: second half goes to next sign."""
lon = calc._calculate_d2_variant(0, 20.0, 'pariveshta')
assert int(lon // 30) == 1 # Taurus
def test_d2_parivritta_even_sign_reverses():
"""Parivritta: even signs reverse degree order."""
lon = calc._calculate_d2_variant(1, 5.0, 'parivritta') # Taurus even, first half
sign_idx = int(lon // 30)
assert sign_idx == 3 # Cancer for even first half
def test_d2_trayodamsa_13_divisions():
"""Parivritta-Trayodamsa: 13 divisions produce different signs."""
results = set()
for deg in range(0, 30, 2):
lon = calc._calculate_d2_variant(0, float(deg), 'parivritta_trayodamsa')
results.add(int(lon // 30))
assert len(results) > 1, "Should produce more than 1 sign across 13 parts"
def test_d2_unknown_variant_raises():
"""Unknown D2 variant should raise ValueError."""
with pytest.raises(ValueError, match="Unknown D2 variant"):
calc._calculate_d2_variant(0, 15.0, 'nonexistent')
# ── D3 Drekkana Variant Tests ──────────────────────────────────────
D3_VARIANTS = ['parashara', 'parivritta_trayodamsa', 'somaja', 'khara']
@pytest.mark.parametrize("variant", D3_VARIANTS)
def test_d3_variant_returns_valid_longitude(variant):
lon = calc._calculate_d3_variant(0, 15.0, variant)
assert 0 <= lon < 360
def test_d3_parashara_first_drekkana_same_sign():
"""First 10° of Aries stays in Aries."""
lon = calc._calculate_d3_variant(0, 5.0, 'parashara')
assert int(lon // 30) == 0 # Aries
def test_d3_parashara_second_drekkana_plus4():
"""10-20° of Aries → 5th sign (Leo)."""
lon = calc._calculate_d3_variant(0, 15.0, 'parashara')
assert int(lon // 30) == 4 # Leo
def test_d3_parashara_third_drekkana_plus8():
"""20-30° of Aries → 9th sign (Sagittarius)."""
lon = calc._calculate_d3_variant(0, 25.0, 'parashara')
assert int(lon // 30) == 8 # Sagittarius
def test_d3_somaja_moon_signs():
"""Somaja always maps to Cancer/Scorpio/Pisces."""
for deg in [5.0, 15.0, 25.0]:
lon = calc._calculate_d3_variant(0, deg, 'somaja')
sign_idx = int(lon // 30)
assert sign_idx in (3, 7, 11), f"Expected Cancer/Scorpio/Pisces, got {calc.SIGNS[sign_idx]}"
def test_d3_khara_even_sign_offset():
"""Khara: even signs use +5 offset for first drekkana."""
lon = calc._calculate_d3_variant(1, 5.0, 'khara') # Taurus, 1st drekkana
sign_idx = int(lon // 30)
assert sign_idx == (1 + 5) % 12 # Virgo
def test_d3_unknown_variant_raises():
with pytest.raises(ValueError, match="Unknown D3 variant"):
calc._calculate_d3_variant(0, 15.0, 'nonexistent')
# ── Composite Varga Tests ──────────────────────────────────────────
def test_composite_d9_d12_equals_d108():
"""D9×D12 composite should produce consistent sign."""
result = calc.calc_composite_varga(45.0, 9, 12)
assert result['composite_div'] == 108
assert 'sign' in result
assert 0 <= result['sign_idx'] < 12
def test_composite_varga_normalizes_large_intermediate_longitudes():
"""Composite vargas should never expose sign indices outside 0-11."""
result = calc.calc_composite_varga(80.0, 9, 12)
assert 0 <= result['sign_idx'] < 12
assert 0 <= result['intermediate']['outer_sign_idx'] < 12
assert 0 <= result['absolute_degree'] < 360
def test_composite_d12_d12_equals_d144():
result = calc.calc_composite_varga(45.0, 12, 12)
assert result['composite_div'] == 144
def test_composite_d9_d9_equals_d81():
result = calc.calc_composite_varga(45.0, 9, 9)
assert result['composite_div'] == 81
def test_composite_intermediate_preserved():
"""Composite should preserve intermediate outer result."""
result = calc.calc_composite_varga(45.0, 9, 12)
assert 'intermediate' in result
assert 'outer_sign' in result['intermediate']
# ── Custom D-N Tests ───────────────────────────────────────────────
def test_custom_d150_valid():
result = calc.calc_custom_varga(45.0, 150)
assert result['div'] == 150
assert 0 <= result['sign_idx'] < 12
def test_custom_d300_valid():
result = calc.calc_custom_varga(45.0, 300)
assert result['div'] == 300
assert 0 <= result['sign_idx'] < 12
def test_custom_d9_matches_standard():
"""Custom D9 should match standard D9 calculation."""
custom = calc.calc_custom_varga(45.0, 9)
standard = calc._calculate_varga_position(45.0, 9)
assert custom['sign_idx'] == int((standard % 360) // 30)
def test_custom_d2_matches_standard():
custom = calc.calc_custom_varga(30.0, 2)
standard = calc._calculate_varga_position(30.0, 2)
assert custom['sign_idx'] == int((standard % 360) // 30)
def test_standard_extended_vargas_normalize_sign_indices():
chart = calc._calculate_single_varga(
VargaType.D108,
{"Moon": 80.0},
92.0,
)
assert 0 <= chart["ascendant"]["sign_index"] < 12
assert 0 <= chart["planets"]["Moon"]["sign_index"] < 12
assert 0 <= chart["planets"]["Moon"]["absolute_degree"] < 360
def test_custom_out_of_range_raises():
with pytest.raises(ValueError, match="2-300"):
calc.calc_custom_varga(45.0, 1)
with pytest.raises(ValueError, match="2-300"):
calc.calc_custom_varga(45.0, 301)
def test_custom_amsa_size():
"""Amsa size should be 30/N."""
result = calc.calc_custom_varga(45.0, 150)
assert abs(result['amsa_size'] - 30.0 / 150) < 0.001
# ── calc_varga_with_variant Tests ──────────────────────────────────
def test_varga_with_d2_variant():
result = calc.calc_varga_with_variant(15.5, 2, 'pariveshta')
assert result['variant'] == 'pariveshta'
assert result['div'] == 2
def test_varga_without_variant_uses_parashara():
result = calc.calc_varga_with_variant(15.5, 9, None)
assert result['variant'] == 'parashara'
def test_varga_d3_variant():
result = calc.calc_varga_with_variant(15.0, 3, 'somaja')
assert result['variant'] == 'somaja'
# ── list_available_variants Tests ───────────────────────────────────
def test_list_available_variants_structure():
variants = calc.list_available_variants()
assert 'D2' in variants
assert 'D3' in variants
assert 'composite' in variants
assert 'custom' in variants
def test_list_d2_has_six_variants():
variants = calc.list_available_variants()
assert len(variants['D2']['variants']) == 6
def test_list_d3_has_four_variants():
variants = calc.list_available_variants()
assert len(variants['D3']['variants']) == 4
# ── Full varga calculation Tests ────────────────────────────────────
def test_calculate_all_vargas_returns_all_types():
planets = {"Sun": 15.5, "Moon": 125.3}
asc = 10.0
result = calc.calculate_all_vargas(planets, asc)
for vt in VargaType:
assert vt.varga_name in result
def test_house_chart_has_12_houses():
planets = {"Sun": 15.5, "Moon": 125.3}
asc = 10.0
result = calc.calculate_all_vargas(planets, asc)
for varga_name, data in result.items():
assert len(data['house_chart']) == 12
def test_d9_navamsa_aries_0_degrees():
"""Aries 0° → Navamsa sign should be Aries (movable sign, part 0)."""
lon = calc._calculate_varga_position(0.0, 9)
assert int(lon // 30) == 0 # Aries
def test_d9_navamsa_fixed_and_dual_sign_starts_match_jhora_reference():
assert int(calc._calculate_varga_position(30.0, 9) // 30) == 9 # Taurus starts Capricorn
assert int(calc._calculate_varga_position(60.0, 9) // 30) == 6 # Gemini starts Libra
def test_d60_shashtiamsa_many_signs():
"""D60 should traverse many signs across 0-30 degrees."""
signs = set()
for deg in range(0, 30, 1):
lon = calc._calculate_varga_position(float(deg), 60)
signs.add(int(lon // 30))
assert len(signs) >= 5, "D60 should produce at least 5 different signs"
# ── D30 Trimsamsa special algorithm ────────────────────────────────
def test_d30_odd_sign_mars_region():
"""Odd sign, 0-5° → Aries (Mars region)."""
lon = calc._calculate_varga_position(2.0, 30) # Aries 2°
assert int(lon // 30) == 0 # Aries
def test_d30_even_sign_venus_region():
"""Even sign, 0-5° → Taurus (Venus region)."""
lon = calc._calculate_varga_position(32.0, 30) # Taurus 2°
assert int(lon // 30) == 1 # Taurus