Files
Jyotisha/tests/test_muhurta.py
T
2026-07-05 18:27:33 +08:00

432 lines
18 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 muhurta.py — Panchanga calculation, auspicious/inauspicious periods."""
from __future__ import annotations
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 muhurta import (
calc_tithi, calc_nakshatra_from_lon, calc_yoga, calc_karana,
calc_vara, calc_hora, calc_abhijit_muhurta, calc_panchanga,
check_activity_muhurta, muhurta_full_report, calc_daytime_inauspicious_periods,
panchanga_range_report, muhurta_range_search, calc_sunrise_sunset_local, calc_panchanga_end_times,
build_muhurta_sidecar,
classify_vrata_tags, classify_panchanga_condition_tags, calc_choghadiya_windows, calc_hora_windows,
TITHI_NAMES, TITHI_QUALITY, NAKSHATRAS, NAKSHATRA_TYPE,
YOGA_NAMES, YOGA_QUALITY, KARANA_NAMES, KARANA_QUALITY,
VARA_LORDS, ACTIVITY_RULES,
)
# ── Tithi Tests ─────────────────────────────────────────────────────
class TestTithi:
def test_new_moon_tithi_30(self):
# Moon-Sun diff = 0 → tithi 1
result = calc_tithi(0.0, 0.0)
assert result['tithi_num'] == 1
def test_full_moon_tithi_15(self):
# Moon-Sun diff = 180° → tithi 16 (Krishna 1)
result = calc_tithi(0.0, 180.0)
assert result['tithi_num'] == 16 # Krishna Pratipada
def test_shukla_paksha(self):
result = calc_tithi(0.0, 60.0) # 60/12=5+1=6
assert result['paksha'] == 'Shukla'
def test_krishna_paksha(self):
result = calc_tithi(0.0, 210.0) # 210/12=17+1=18
assert result['paksha'] == 'Krishna'
def test_purnima_name(self):
result = calc_tithi(0.0, 174.0) # ~15th tithi
if result['tithi_num'] == 15:
assert result['name'] == 'Purnima'
def test_amavasya_name(self):
result = calc_tithi(0.0, 354.0) # ~30th tithi
if result['tithi_num'] == 30:
assert result['name'] == 'Amavasya'
@pytest.mark.parametrize("diff,tithi",
[(0, 1), (12, 2), (24, 3), (168, 15), (180, 16), (348, 30)])
def test_tithi_from_diff(self, diff, tithi):
result = calc_tithi(0.0, float(diff))
assert result['tithi_num'] == tithi
def test_tithi_quality_present(self):
result = calc_tithi(0.0, 60.0)
assert result['quality'] in ('subha', 'asubha', 'mixed')
# ── Nakshatra from Longitude Tests ──────────────────────────────────
class TestNakshatraFromLon:
def test_ashwini_at_0(self):
result = calc_nakshatra_from_lon(0.0)
assert result['nakshatra'] == 'Ashwini'
def test_revati_near_360(self):
result = calc_nakshatra_from_lon(355.0)
assert result['nakshatra'] == 'Revati'
def test_pada_range(self):
for lon in [1.0, 5.0, 8.0, 11.0]:
result = calc_nakshatra_from_lon(lon)
assert 1 <= result['pada'] <= 4
def test_27_nakshatras(self):
assert len(NAKSHATRAS) == 27
def test_nakshatra_type(self):
result = calc_nakshatra_from_lon(0.0) # Ashwini
assert result['type'] == 'laghu'
# ── Yoga Tests ──────────────────────────────────────────────────────
class TestYoga:
def test_vishkambha_at_low_sum(self):
result = calc_yoga(0.0, 0.0) # sum=0 → idx 0
assert result['yoga'] == 'Vishkambha'
def test_yoga_idx_range(self):
result = calc_yoga(180.0, 180.0)
assert 0 <= result['yoga_idx'] < 27
def test_yoga_quality_present(self):
result = calc_yoga(45.0, 120.0)
assert result['quality'] in ('subha', 'asubha', 'mixed')
def test_27_yogas(self):
assert len(YOGA_NAMES) == 27
def test_vaidhriti_asubha(self):
assert YOGA_QUALITY['Vaidhriti'] == 'asubha'
def test_priti_subha(self):
assert YOGA_QUALITY['Priti'] == 'subha'
# ── Karana Tests ────────────────────────────────────────────────────
class TestKarana:
def test_kimstughna_first_half(self):
# Moon-Sun diff near 0 → k_num=0
result = calc_karana(0.0, 1.0)
assert result['karana'] == 'Kimstughna'
def test_vishti_asubha(self):
assert KARANA_QUALITY['Vishti'] == 'asubha'
def test_karana_quality_present(self):
result = calc_karana(0.0, 60.0)
assert result['quality'] in ('subha', 'asubha', 'mixed')
def test_11_karana_names(self):
assert len(KARANA_NAMES) == 11
def test_is_vishti_flag(self):
# Test when karana is Vishti
result = calc_karana(0.0, 60.0)
assert isinstance(result['is_vishti'], bool)
# ── Vara Tests ──────────────────────────────────────────────────────
class TestVara:
def test_sunday(self):
result = calc_vara(0)
assert result['vara'] == 'Sunday'
assert result['vara_lord'] == 'Sun'
assert result['quality'] == 'asubha'
def test_monday(self):
result = calc_vara(1)
assert result['vara_lord'] == 'Moon'
assert result['quality'] == 'subha'
def test_thursday_jupiter(self):
result = calc_vara(4)
assert result['vara_lord'] == 'Jupiter'
def test_7_days(self):
assert len(VARA_LORDS) == 7
@pytest.mark.parametrize("idx,expected_lord",
[(0, 'Sun'), (1, 'Moon'), (2, 'Mars'), (3, 'Mercury'),
(4, 'Jupiter'), (5, 'Venus'), (6, 'Saturn')])
def test_all_vara_lords(self, idx, expected_lord):
result = calc_vara(idx)
assert result['vara_lord'] == expected_lord
# ── Hora Tests ──────────────────────────────────────────────────────
class TestHora:
def test_hora_lord_calculated(self):
result = calc_hora(0, 6.0) # Sunday, 6h from sunrise
assert result['hora_lord'] in ('Sun', 'Moon', 'Mars', 'Mercury',
'Jupiter', 'Venus', 'Saturn')
def test_hora_quality_present(self):
result = calc_hora(0, 6.0)
assert result['quality'] in ('subha', 'asubha', 'mixed')
def test_hora_num_range(self):
result = calc_hora(0, 6.0)
assert 1 <= result['hora_num'] <= 24
# ── Abhijit Muhurta Tests ──────────────────────────────────────────
class TestAbhijitMuhurta:
def test_basic_structure(self):
result = calc_abhijit_muhurta()
assert 'duration_minutes' in result
assert result['duration_minutes'] == 48
def test_wednesday_warning(self):
result = calc_abhijit_muhurta()
assert 'Wednesday' in result['warning']
def test_with_jd_values(self):
result = calc_abhijit_muhurta(2460000.0, 2460000.5)
assert 'abhijit_start_jd' in result
assert 'abhijit_end_jd' in result
class TestDaytimeInauspiciousPeriods:
def test_monday_rahu_kala_uses_second_segment(self):
result = calc_daytime_inauspicious_periods(1, '06:00', '18:00')
assert result['rahu_kala']['segment'] == 2
assert result['rahu_kala']['start'] == '07:30'
assert result['rahu_kala']['end'] == '09:00'
def test_gulika_and_yamaganda_present(self):
result = calc_daytime_inauspicious_periods(4, '06:00', '18:00')
assert result['policy'] == 'daytime_8_segments_local_clock'
assert 'yamaganda' in result
assert 'gulika' in result
def test_location_aware_sunrise_sunset(self):
result = calc_sunrise_sunset_local(2026, 6, 22, 28.6, 77.2, 5.5)
assert result['policy'] in {'swisseph_rise_trans', 'location_aware_solar_event_approximation'}
assert ':' in result['sunrise']
assert ':' in result['sunset']
# ── Panchanga Complete Tests ────────────────────────────────────────
class TestPanchangaComplete:
def test_five_elements_present(self):
result = calc_panchanga(45.0, 120.0, 4, 6.0)
assert 'tithi' in result
assert 'nakshatra' in result
assert 'yoga' in result
assert 'karana' in result
assert 'vara' in result
def test_overall_score_range(self):
result = calc_panchanga(45.0, 120.0, 4, 6.0)
assert 0 <= result['overall_score'] <= 1
def test_auspicious_count(self):
result = calc_panchanga(45.0, 120.0, 4, 6.0)
assert 0 <= result['auspicious_count'] <= 6
def test_warnings_list(self):
result = calc_panchanga(45.0, 120.0, 4, 6.0)
assert isinstance(result['warnings'], list)
def test_panchanga_range_report_includes_inauspicious_periods(self):
result = panchanga_range_report('2026-06-22', '2026-06-24')
assert result['day_count'] == 3
assert result['days'][0]['query_date'] == '2026-06-22'
assert result['days'][0]['inauspicious_periods']['rahu_kala']['label'] == 'Rahu Kala'
assert result['calculation_policy']['inauspicious_periods']
def test_panchanga_range_report_can_use_location_aware_solar_times(self):
result = panchanga_range_report('2026-06-22', '2026-06-22', lat=28.6, lon=77.2, tz=5.5)
assert result['location']['lat'] == 28.6
assert result['calculation_policy']['sunrise_sunset'] in {
'SwissEph rise_trans',
'location-aware solar approximation',
'mixed SwissEph rise_trans with approximation fallback',
}
assert 'solar_times' in result['days'][0]
assert 'end_times' in result['days'][0]
assert 'vrata_tags' in result['days'][0]
assert 'condition_tags' in result['days'][0]
assert 'festival_details' in result['days'][0]
assert 'search_summary' in result
assert 'condition_counts' in result['search_summary']
assert 'choghadiya' in result['days'][0]
assert 'hora_windows' in result['days'][0]
def test_panchanga_range_report_includes_month_grid_for_single_month(self):
result = panchanga_range_report('2026-06-01', '2026-06-30', activity='business', lat=28.6, lon=77.2, tz=5.5)
assert result['month_grid']
cells = [cell for week in result['month_grid'] for cell in week if cell]
assert len(cells) == 30
assert cells[0]['date'] == '2026-06-01'
assert 'best_activities' in cells[0]
assert 'vrata_tags' in cells[0]
assert 'condition_tags' in cells[0]
def test_festival_details_explain_candidate_basis(self):
result = panchanga_range_report('2026-06-01', '2026-06-30', lat=28.6, lon=77.2, tz=5.5)
details = [detail for day in result['days'] for detail in day.get('festival_details', [])]
assert details
assert all('basis' in detail for detail in details)
assert any('confirmation_note' in detail for detail in details)
assert result['search_summary']['query_examples']
def test_panchanga_end_times_are_available_with_swisseph(self):
result = calc_panchanga_end_times(2026, 6, 22, 5.5, 11.4)
assert result is not None
assert result['policy'] == 'swisseph_boundary_bisection'
assert 'tithi' in result
assert 'ends_at' in result['tithi']
def test_basic_vrata_tags(self):
assert classify_vrata_tags({'tithi_num': 11, 'paksha': 'Shukla', 'name': 'Ekadashi'})[0]['key'] == 'ekadashi'
assert classify_vrata_tags({'tithi_num': 28, 'paksha': 'Krishna', 'name': 'Trayodashi'})[0]['key'] == 'pradosham'
assert classify_vrata_tags({'tithi_num': 30, 'paksha': 'Krishna', 'name': 'Amavasya'})[0]['key'] == 'amavasya'
def test_richer_vrata_tags_use_nakshatra_and_vara(self):
tags = classify_vrata_tags(
{'tithi_num': 13, 'paksha': 'Shukla', 'name': 'Trayodashi'},
{'nakshatra': 'Pushya'},
{'vara': 'Thursday'},
)
keys = {tag['key'] for tag in tags}
assert {'pradosham', 'pushya_nakshatra', 'guru_pushya'} <= keys
def test_vrata_tags_mark_masa_dependent_festivals_as_candidates(self):
tags = classify_vrata_tags({'tithi_num': 3, 'paksha': 'Shukla', 'name': 'Tritiya'})
assert any(tag['key'] == 'akshaya_tritiya_candidate' and tag['type'] == 'festival_candidate' for tag in tags)
def test_panchanga_condition_tags_are_searchable(self):
report = {
'panchanga': {'warnings': ['avoid']},
'summary': {'best_activities': ['business'], 'warnings': ['avoid']},
'vrata_tags': [{'key': 'ekadashi', 'type': 'fasting'}],
'activity_checks': {'business': {'verdict': '大吉(Excellent'}},
'choghadiya': {'day': [{'quality': 'auspicious'}], 'night': []},
}
keys = {tag['key'] for tag in classify_panchanga_condition_tags(report, activity='business')}
assert {'has_vrata', 'spiritual_practice', 'auspicious_activity', 'avoid_new_start', 'good_choghadiya', 'selected_activity_good'} <= keys
def test_choghadiya_and_hora_windows(self):
choghadiya = calc_choghadiya_windows(1, '06:00', '18:00')
assert choghadiya['day'][0]['name'] == 'Amrit'
assert len(choghadiya['day']) == 8
assert len(choghadiya['night']) == 8
hora = calc_hora_windows(1, '06:00', '18:00')
assert hora['day'][0]['lord'] == 'Moon'
assert len(hora['day']) == 12
assert len(hora['night']) == 12
# ── Activity Muhurta Tests ─────────────────────────────────────────
class TestActivityMuhurta:
def test_marriage_activity(self):
panchanga = calc_panchanga(45.0, 120.0, 4, 6.0)
result = check_activity_muhurta(panchanga, 'marriage')
assert result['verdict'] is not None
def test_all_5_activities(self):
panchanga = calc_panchanga(45.0, 120.0, 4, 6.0)
for act in ['marriage', 'business', 'travel', 'medical', 'education']:
result = check_activity_muhurta(panchanga, act)
assert 'verdict' in result
def test_unknown_activity_error(self):
panchanga = calc_panchanga(45.0, 120.0, 4, 6.0)
result = check_activity_muhurta(panchanga, 'unknown')
assert 'error' in result
def test_good_tithi_for_marriage(self):
# Create a panchanga with tithi 2 (good for marriage)
panchanga = calc_panchanga(0.0, 24.0, 4, 6.0) # tithi 3
result = check_activity_muhurta(panchanga, 'marriage')
assert result['tithi_eval'] in ('good', 'neutral', 'bad')
def test_pushya_nakshatra_good_for_business(self):
# Pushya nakshatra center ≈ 104°
panchanga = calc_panchanga(80.0, 104.0, 4, 6.0)
result = check_activity_muhurta(panchanga, 'business')
if panchanga['nakshatra']['nakshatra'] == 'Pushya':
assert result['nakshatra_eval'] == 'good'
# ── Full Report Tests ───────────────────────────────────────────────
class TestMuhurtaFullReport:
def test_report_structure(self):
result = muhurta_full_report(45.0, 120.0, 4, 6.0, '2026-06-04')
assert 'panchanga' in result
assert 'abhijit_muhurta' in result
assert 'activity_checks' in result
assert 'summary' in result
def test_best_activities_list(self):
result = muhurta_full_report(45.0, 120.0, 4, 6.0)
assert isinstance(result['summary']['best_activities'], list)
def test_avoid_activities_list(self):
result = muhurta_full_report(45.0, 120.0, 4, 6.0)
assert isinstance(result['summary']['avoid_activities'], list)
def test_query_date_preserved(self):
result = muhurta_full_report(45.0, 120.0, 4, 6.0, '2026-06-04')
assert result['query_date'] == '2026-06-04'
class TestMuhurtaRangeSearch:
def test_returns_ranked_activity_windows_for_date_range(self):
result = muhurta_range_search(
'2026-06-22',
'2026-06-28',
activity='business',
limit=3,
sunrise='06:00',
sunset='18:00',
)
assert result['mode'] == 'muhurta_date_range_solver'
assert result['activity'] == 'business'
assert result['date_range'] == {'start': '2026-06-22', 'end': '2026-06-28'}
assert result['candidate_count'] <= 3
assert result['best_windows']
assert result['rejected_dates']
assert result['constraints']['avoid_inauspicious_periods'] is True
first = result['best_windows'][0]
assert {'date', 'score', 'quality', 'activity_verdict', 'recommended_windows', 'evidence'} <= set(first)
assert first['recommended_windows']
assert first['evidence']['panchanga']
class TestMuhurtaSidecar:
def test_build_muhurta_sidecar_returns_compact_workflow_shape(self):
result = build_muhurta_sidecar(
date_str='2026-07-08',
activity='business',
lat=36.42,
lon=114.2,
tz=8,
)
assert result['status'] == 'ok'
assert result['source'] == 'local_muhurta.py'
assert result['activity'] == 'business'
assert result['report_mode'] == 'muhurta_date_range_solver'
assert result['panchanga']['query_date'] == '2026-07-08'
assert result['best_windows']
assert 'next_action' in result