#!/usr/bin/env python3 """Tests for shadbala.py — 6-fold strength, Kendra 3-tier, Bhava Bala, Hora Bala.""" from __future__ import annotations import json from pathlib import Path import sys, os, pytest SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts') if SCRIPTS not in sys.path: sys.path.insert(0, SCRIPTS) from shadbala import ( calc_shadbala, calc_sthana_bala, calc_dig_bala, calc_kala_bala, calc_chesta_bala, calc_drik_bala, calc_bhava_bala, NAISARGIKA_BALA, MIN_REQUIRED, DIG_BALA_HOUSE, EXALTATION_DEG, DEBILITATION_DEG, FRIENDSHIP, SHADBALA_CONSTANTS_PATH, ) from scripts.shadbala_oracle_comparison import compare_case # Standard test chart data def _sample_planets(): return { 'Sun': {'sign': 'Gemini', 'degree': 70.0, 'house': 3, 'retrograde': False, 'speed': 1.0}, 'Moon': {'sign': 'Taurus', 'degree': 45.0, 'house': 2, 'retrograde': False, 'speed': 13.0}, 'Mars': {'sign': 'Capricorn', 'degree': 280.0, 'house': 10, 'retrograde': False, 'speed': 0.5}, 'Mercury': {'sign': 'Gemini', 'degree': 80.0, 'house': 3, 'retrograde': False, 'speed': 1.2}, 'Jupiter': {'sign': 'Cancer', 'degree': 105.0, 'house': 5, 'retrograde': False, 'speed': 0.08}, 'Venus': {'sign': 'Leo', 'degree': 130.0, 'house': 4, 'retrograde': False, 'speed': 1.2}, 'Saturn': {'sign': 'Aquarius', 'degree': 320.0, 'house': 12, 'retrograde': True, 'speed': -0.03}, } def _shadbala_constants(): with open(os.path.join(os.path.dirname(__file__), '..', 'references', 'shat_bala_constants.json'), 'r', encoding='utf-8') as handle: return json.load(handle) # ── Sthana Bala Tests ─────────────────────────────────────────────── class TestSthanaBala: def test_ucha_bala_range(self): result = calc_sthana_bala('Sun', 70.0, 'Gemini', 3) assert 0 <= result['ucha_bala'] <= 60 def test_exalted_planet_high_ucha(self): # Sun exalted at 10° Aries result = calc_sthana_bala('Sun', 10.0, 'Aries', 1) assert result['ucha_bala'] > 55 # Near max def test_debilitated_planet_low_ucha(self): # Sun debilitated in Libra result = calc_sthana_bala('Sun', 190.0, 'Libra', 7) assert result['ucha_bala'] < 5 def test_sapta_score_has_7_components(self): result = calc_sthana_bala('Jupiter', 105.0, 'Cancer', 5) for key in ['sapta_d1', 'sapta_d2', 'sapta_d3', 'sapta_d4', 'sapta_d7', 'sapta_d9', 'sapta_d12']: assert key in result def test_own_sign_d1_score(self): # Sun in Leo = own sign result = calc_sthana_bala('Sun', 125.0, 'Leo', 5) assert result['sapta_d1'] == 45.0 def test_ojayugma_bala_range(self): result = calc_sthana_bala('Mercury', 80.0, 'Gemini', 3) assert result['ojayugma_bala'] in (0, 15) def test_drekkana_bala_range(self): result = calc_sthana_bala('Sun', 5.0, 'Aries', 1) assert result['drekkana_bala'] in (0, 15) def test_sapta_d3_exaltation_branch_is_capped_below_d1_exaltation(self): result = calc_sthana_bala('Sun', 0.0, 'Aries', 1) assert result['sapta_d1'] == 50.0 assert result['sapta_d3'] == 45.0 def test_sapta_d3_own_sign_branch_is_capped_below_d1_own_sign(self): result = calc_sthana_bala('Venus', 30.0, 'Taurus', 2) assert result['sapta_d1'] == 45.0 assert result['sapta_d3'] == 30.0 def test_total_positive(self): result = calc_sthana_bala('Jupiter', 105.0, 'Cancer', 5) assert result['total'] > 0 # ── Kendra Bala 3-Tier Tests ─────────────────────────────────────── class TestKendraBala: @pytest.mark.precision def test_kendra_house_60(self): result = calc_sthana_bala('Sun', 70.0, 'Gemini', 1) # House 1 = Kendra assert result['kendra_bala'] == 60.0 @pytest.mark.precision def test_panapara_house_30(self): result = calc_sthana_bala('Sun', 70.0, 'Gemini', 2) # House 2 = Panapara assert result['kendra_bala'] == 30.0 @pytest.mark.precision def test_apoklima_house_15(self): result = calc_sthana_bala('Sun', 70.0, 'Gemini', 3) # House 3 = Apoklima assert result['kendra_bala'] == 15.0 @pytest.mark.parametrize("house,expected", [ (1, 60.0), (4, 60.0), (7, 60.0), (10, 60.0), (2, 30.0), (5, 30.0), (8, 30.0), (11, 30.0), (3, 15.0), (6, 15.0), (9, 15.0), (12, 15.0), ]) def test_all_houses_kendra_bala(self, house, expected): result = calc_sthana_bala('Sun', 70.0, 'Gemini', house) assert result['kendra_bala'] == expected # ── Dig Bala Tests ────────────────────────────────────────────────── class TestDigBala: def test_sun_in_10th_max(self): assert calc_dig_bala('Sun', 10) == 60.0 def test_sun_in_4th_min(self): assert calc_dig_bala('Sun', 4) == 0.0 def test_jupiter_in_1st_max(self): assert calc_dig_bala('Jupiter', 1) == 60.0 def test_saturn_in_7th_max(self): assert calc_dig_bala('Saturn', 7) == 60.0 def test_moon_in_4th_max(self): assert calc_dig_bala('Moon', 4) == 60.0 def test_midrange_positive(self): assert 0 < calc_dig_bala('Sun', 7) < 60 def test_dig_bala_jupiter_synthetic_north_china_case_needs_better_than_house_only_linear_model(self): try: comparison = compare_case("references/oracle/dasha_shadbala_oracle_cases.json", "template_synthetic_north_china_shadbala_raman") except KeyError: pytest.skip("synthetic North China oracle case is not distributed in the public release") jupiter_dig_gap = comparison["comparison"]["Jupiter"]["components"]["dig"]["abs_diff_rupa"] assert jupiter_dig_gap < 3.6348 # ── Kala Bala Tests ───────────────────────────────────────────────── class TestKalaBala: def test_mercury_always_60_nathonnata(self): result = calc_kala_bala('Mercury', False, True, 45.0, 120.0, 12.0) assert result['nathonnata'] == 60.0 def test_nathonnata_day_birth_sun(self): result = calc_kala_bala('Sun', False, True, 45.0, 120.0, 12.0) assert result['nathonnata'] == 60.0 # Noon def test_nathonnata_night_birth_moon(self): result = calc_kala_bala('Moon', True, True, 45.0, 120.0, 0.0) assert result['nathonnata'] == 60.0 # Midnight def test_paksha_bala_range(self): result = calc_kala_bala('Jupiter', False, True, 45.0, 120.0, 12.0) assert 0 <= result['paksha'] <= 30 def test_hora_bala_present(self): result = calc_kala_bala('Sun', False, True, 45.0, 120.0, 12.0) assert 'hora' in result def test_hora_lord_gets_60(self): # Test that some planet gets hora = 60 results = [] for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']: r = calc_kala_bala(pname, False, True, 45.0, 120.0, 12.0) results.append(r['hora']) assert 60.0 in results # At least one planet is hora lord def test_total_positive(self): result = calc_kala_bala('Jupiter', False, True, 45.0, 120.0, 12.0) assert result['total'] > 0 # ── Chesta Bala Tests ─────────────────────────────────────────────── class TestChestaBala: def test_retrograde_max(self): assert calc_chesta_bala('Mars', True, -0.5, 45.0, 120.0) == 60.0 def test_moon_full_max(self): # Moon opposite Sun → full moon → 60 assert calc_chesta_bala('Moon', False, 13.0, 0.0, 180.0) == 60.0 def test_moon_new_min(self): assert calc_chesta_bala('Moon', False, 13.0, 0.0, 0.0) == 0.0 def test_sun_speed_based(self): result = calc_chesta_bala('Sun', False, 0.985, 45.0, 120.0) assert 0 <= result <= 60 def test_direct_fast_moderate(self): result = calc_chesta_bala('Mars', False, 1.5, 45.0, 120.0) assert 0 <= result <= 60 # ── Drik Bala Tests ───────────────────────────────────────────────── class TestDrikBala: def test_drik_bala_range(self): planets = _sample_planets() result = calc_drik_bala('Jupiter', 'Cancer', 5, planets) assert -60 <= result <= 60 def test_benefic_aspect_positive(self): planets = _sample_planets() result = calc_drik_bala('Mars', 'Capricorn', 10, planets) assert isinstance(result, float) # ── Full Shadbala Tests ───────────────────────────────────────────── class TestShadbalaFull: def test_shadbala_module_exposes_reference_constants_path(self): constants_path = Path(SHADBALA_CONSTANTS_PATH).resolve() assert constants_path.parts[-2:] == ('references', 'shat_bala_constants.json') assert constants_path.exists() def test_static_shadbala_constants_are_loaded_from_reference_json(self): constants = _shadbala_constants() sun_exalt = constants['exaltation_degrees']['Sun'] saturn_debil = constants['debilitation_degrees']['Saturn'] mercury_rel = constants['natural_relationships']['Mercury'] assert EXALTATION_DEG['Sun'] == pytest.approx(sun_exalt['sign'] * 30 + sun_exalt['degree']) assert DEBILITATION_DEG['Saturn'] == pytest.approx(saturn_debil['sign'] * 30 + saturn_debil['degree']) assert FRIENDSHIP['Mercury']['friend'] == mercury_rel['friends'] assert FRIENDSHIP['Mercury']['enemy'] == mercury_rel['enemies'] assert FRIENDSHIP['Mercury']['neutral'] == mercury_rel['neutrals'] def test_all_7_planets_calculated(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']: assert pname in result['planets'] def test_total_rupas_positive(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) for pname, data in result['planets'].items(): assert data['total_rupas'] > 0 def test_total_virupas_preserves_component_sum_without_global_normalization(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) for pname, data in result['planets'].items(): component_sum = ( data['sthana_bala']['total'] + data['dig_bala'] + data['kala_bala']['total'] + data['chesta_bala'] + data['naisargika_bala'] + data['drik_bala'] ) assert data['total_virupas'] == pytest.approx(component_sum, abs=0.08), pname def test_total_required_strength_is_not_halved_by_global_1200_invariant(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) total_rupas = sum(data['total_rupas'] for data in result['planets'].values()) assert total_rupas >= sum(MIN_REQUIRED.values()) def test_ishta_bala_calculated(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) for pname, data in result['planets'].items(): assert data['ishta_bala_pct'] >= 0 def test_strength_level_present(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) for pname, data in result['planets'].items(): assert data['strength_level'] in ('极强', '强', '充足', '略弱', '弱', '极弱') def test_ranking_present(self): result = calc_shadbala(_sample_planets(), 'Leo', 10.0, 70.0, 45.0) assert len(result['ranking']) == 7 assert result['strongest'] is not None def test_naisargika_bala_standard(self): assert NAISARGIKA_BALA['Sun'] == 60.0 assert NAISARGIKA_BALA['Saturn'] == 8.57 # ── Bhava Bala Tests ──────────────────────────────────────────────── class TestBhavaBala: def test_12_houses_calculated(self): result = calc_bhava_bala(_sample_planets(), 'Leo') for house in range(1, 13): assert house in result def test_benefic_in_house_positive(self): # Jupiter in house 5 = benefic result = calc_bhava_bala(_sample_planets(), 'Leo') assert result[5]['score'] > 0 def test_strength_classification(self): result = calc_bhava_bala(_sample_planets(), 'Leo') for house, data in result.items(): assert data['strength'] in ('Strong', 'Moderate', 'Weak') def test_lord_info_present(self): result = calc_bhava_bala(_sample_planets(), 'Leo') for house, data in result.items(): assert 'lord' in data assert 'sign' in data def test_factors_list(self): result = calc_bhava_bala(_sample_planets(), 'Leo') for house, data in result.items(): assert isinstance(data['factors'], list)