#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ kalachakra_dasha.py — Kalachakra Dasha System (Variable Cycle) The Kalachakra Dasha is one of the most intricate Nakshatra-based dasha systems. It is based on the Moon's Nakshatra and its Pada (quarter) at birth. Key Concepts: - 27 Nakshatras × 4 Padas = 108 Padas. - Each Pada maps to a specific Rashi (Zodiac sign). - Two directional modes: * Savya (clockwise / forward) * Apasavya (anti-clockwise / backward) - 9 lords (Navatara-like) govern the periods in a fixed sequence. Nakshatra Grouping (per standard texts): Savya (forward) Nakshatras: Ashwini, Bharani, Krittika, Ashlesha, Magha, Moola, Purva Ashadha, Uttara Ashadha, Shravana, Dhanishta, Shatabhisha Apasavya (backward) Nakshatras: Rohini, Mrigashira, Ardra, Punarvasu, Pushya, Chitra, Swati, Vishakha, Anuradha, Jyeshtha, Purva Bhadrapada, Uttara Bhadrapada, Revati (Note: Some authorities include Abhijit; this implementation uses the 27-Nakshatra scheme.) 9-Lord Sequence: The 9 dasha lords correspond to the Navagraha sequence used in Kalachakra: 1. Ketu 2. Venus 3. Sun 4. Moon 5. Mars 6. Mercury 7. Jupiter 8. Saturn 9. Rahu Their year allotments (one common traditional set): Ketu: 7, Venus: 20, Sun: 6, Moon: 10, Mars: 7, Mercury: 18, Jupiter: 16, Saturn: 19, Rahu: 17 Total: 7+20+6+10+7+18+16+19+17 = 120 years However, Kalachakra is more commonly implemented with Rashi-based years. This module uses a simplified Rashi-year model (see _RASHI_YEARS below). Rashi Dasha Sequence and Years (simplified standard model): Savya mode (forward through zodiac): Capricorn 7 → Aquarius 8 → Pisces 9 → Aries 10 → Taurus 11 → Gemini 12 → Cancer 13 → Leo 14 → Virgo 15 → Libra 16 → Scorpio 17 → Sagittarius 18 Total = 150 years Apasavya mode (backward through zodiac): Cancer 13 → Gemini 12 → Taurus 11 → Aries 10 → Pisces 9 → Aquarius 8 → Capricorn 7 → Sagittarius 18 → Scorpio 17 → Libra 16 → Virgo 15 → Leo 14 Total = 150 years Each of the 9 lords rules one consecutive Rashi in the sequence. The starting point is determined by Moon's Nakshatra Pada. Reference: Brihat Parashara Hora Shastra and various classical commentaries on Kalachakra. """ from datetime import datetime, timedelta import math from bisect import bisect_right from fractions import Fraction # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- NAKSHATRAS = [ "Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra", "Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni", "Uttara Phalguni", "Hasta", "Chitra", "Swati", "Vishakha", "Anuradha", "Jyeshtha", "Mula", "Purva Ashadha", "Uttara Ashadha", "Shravana", "Dhanishta", "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", "Revati" ] RASHIS = [ "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces" ] # 9 Kalachakra lords in order KALA_LORDS = [ "Ketu", "Venus", "Sun", "Moon", "Mars", "Mercury", "Jupiter", "Saturn", "Rahu" ] # Savya Rashi sequence (starting from Capricorn, forward) SAVYA_RASHI_SEQ = [9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8] # Apasavya Rashi sequence (starting from Cancer, backward) APASAVYA_RASHI_SEQ = [3, 2, 1, 0, 11, 10, 9, 8, 7, 6, 5, 4] # Rashi year allotments (index 0-11) # Savya: Cap 7, Aqua 8, Pis 9, Ari 10, Tau 11, Gem 12, Can 13, Leo 14, Vir 15, Lib 16, Sco 17, Sag 18 # Apasavya: Can 13, Gem 12, Tau 11, Ari 10, Pis 9, Aqua 8, Cap 7, Sag 18, Sco 17, Lib 16, Vir 15, Leo 14 _RASHI_YEARS_SAVYA = { 9: 7, 10: 8, 11: 9, 0: 10, 1: 11, 2: 12, 3: 13, 4: 14, 5: 15, 6: 16, 7: 17, 8: 18 } _RASHI_YEARS_APASAVYA = { 3: 13, 2: 12, 1: 11, 0: 10, 11: 9, 10: 8, 9: 7, 8: 18, 7: 17, 6: 16, 5: 15, 4: 14 } # Nakshatra → mode mapping (Savya = True, Apasavya = False) # Per standard grouping SAVYA_NAKSHATRAS = {0, 1, 2, 8, 9, 18, 19, 20, 21, 22, 23} APASAVYA_NAKSHATRAS = {3, 4, 5, 6, 7, 13, 14, 15, 16, 17, 24, 25, 26} # Pada → starting Rashi offset within the sequence (simplified model) # For Savya: Pada 1 → Cap(0), Pada 2 → Aqu(1), Pada 3 → Pis(2), Pada 4 → Ari(3) # For Apasavya: Pada 1 → Can(0), Pada 2 → Gem(1), Pada 3 → Tau(2), Pada 4 → Ari(3) # The actual classical mapping is more nuanced; this is a practical approximation. PADA_OFFSET_MAP = {1: 0, 2: 1, 3: 2, 4: 3} # Classical 9-sign Kala Chakra cycles and paramayush values. # This table is used only to improve the native runtime's birth-balance and # Jeeva/Deha visibility; it does not claim external parity closure by itself. CLASSICAL_SAVYA_GROUP_1 = {0, 2, 6, 8, 12, 14, 18, 20, 24} CLASSICAL_SAVYA_GROUP_2 = {1, 7, 13, 19, 25, 26} CLASSICAL_APASAVYA_GROUP_1 = {3, 9, 15, 21} CLASSICAL_GROUP_SWITCH_ON_PADA_4 = {0: 1, 1: 0, 2: 3, 3: 2} CLASSICAL_KALACHAKRA_CYCLES = ( ( (0, 1, 2, 3, 4, 5, 6, 7, 8), (9, 10, 11, 7, 6, 5, 3, 4, 2), (1, 0, 11, 10, 9, 8, 0, 1, 2), (3, 4, 5, 6, 7, 8, 9, 10, 11), ), ( (7, 6, 5, 3, 4, 2, 1, 0, 11), (10, 9, 8, 0, 1, 2, 3, 4, 5), (6, 7, 8, 9, 10, 11, 7, 6, 5), (3, 4, 2, 1, 0, 11, 10, 9, 8), ), ( (8, 9, 10, 11, 0, 1, 2, 4, 3), (5, 6, 7, 11, 10, 9, 8, 7, 6), (5, 4, 3, 2, 1, 0, 8, 9, 10), (11, 0, 1, 2, 4, 3, 5, 6, 7), ), ( (11, 10, 9, 8, 7, 6, 5, 4, 3), (2, 1, 0, 8, 9, 10, 11, 0, 1), (2, 4, 3, 5, 6, 7, 11, 10, 9), (8, 7, 6, 5, 4, 3, 2, 1, 0), ), ) CLASSICAL_PARAMAYUSH = ( (100.0, 85.0, 83.0, 86.0), (100.0, 85.0, 83.0, 86.0), (86.0, 83.0, 85.0, 100.0), (86.0, 83.0, 85.0, 100.0), ) CLASSICAL_RASHI_YEARS = { 0: 7.0, 1: 16.0, 2: 9.0, 3: 21.0, 4: 5.0, 5: 9.0, 6: 16.0, 7: 7.0, 8: 10.0, 9: 4.0, 10: 4.0, 11: 10.0, } # PVR, Integrated Approach, table 48 (printed p.292 / PDF page 304). # Classical Kala Chakra periods are signs; ``lord`` is their sign owner, # not a separately rotated nakshatra-lord sequence. CLASSICAL_RASHI_LORDS = ( "Mars", "Venus", "Mercury", "Moon", "Sun", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Saturn", "Jupiter", ) # --------------------------------------------------------------------------- # Helper Functions # --------------------------------------------------------------------------- def _get_mode(nakshatra_index: int) -> str: """Return 'savya' or 'apasavya' based on Nakshatra.""" if nakshatra_index in SAVYA_NAKSHATRAS: return "savya" elif nakshatra_index in APASAVYA_NAKSHATRAS: return "apasavya" else: # Default fallback (should not happen with valid 0-26 input) return "savya" def _build_major_periods(mode: str, start_offset: int, birth_date: datetime, year_days: float = 365.25) -> list: """ Build major periods for Kalachakra Dasha. Args: mode: 'savya' or 'apasavya'. start_offset: offset into the 12-sign Rashi sequence. birth_date: datetime of birth. Returns: list of period dicts. """ if mode == "savya": rashi_seq = SAVYA_RASHI_SEQ years_map = _RASHI_YEARS_SAVYA else: rashi_seq = APASAVYA_RASHI_SEQ years_map = _RASHI_YEARS_APASAVYA periods = [] current_date = birth_date seq_len = len(rashi_seq) for i in range(seq_len): seq_idx = (start_offset + i) % seq_len rashi_idx = rashi_seq[seq_idx] lord_idx = i % 9 # 9 lords cycle through 12 rashis lord = KALA_LORDS[lord_idx] years = years_map[rashi_idx] end_date = current_date + timedelta(days=years * year_days) periods.append({ "lord": lord, "rashi": RASHIS[rashi_idx], "rashi_index": rashi_idx, "years": years, "start_date": current_date.isoformat(), "end_date": end_date.isoformat(), }) current_date = end_date return periods def _classical_group_for_nakshatra(nakshatra_index: int) -> int: if nakshatra_index in CLASSICAL_SAVYA_GROUP_1: return 0 if nakshatra_index in CLASSICAL_SAVYA_GROUP_2: return 1 if nakshatra_index in CLASSICAL_APASAVYA_GROUP_1: return 2 return 3 def _fraction_completed_in_pada(moon_longitude: float, nakshatra_index: int, pada: int) -> float: one_pada = 360.0 / 108.0 # Form the single rational boundary before converting to float. Adding # separately rounded star/quarter widths can move an exact start. pada_start = (nakshatra_index * 4 + pada - 1) * 10 / 3 progress = (float(moon_longitude) % 360.0) - pada_start fraction = progress / one_pada return max(0.0, min(1.0, fraction)) def _years_to_balance_parts(years: float) -> dict: # PVR example 96 expresses 8.6 years as 8y 7m 6d: nominal 12x30 # display units. Do not mix a sidereal year with 30-day months and add # an undocumented inclusive day to resemble a target printout. nominal_days = int(round(max(years, 0.0) * 360)) whole_years, remainder = divmod(nominal_days, 360) whole_months, day_count = divmod(remainder, 30) return { "years": years, "years_whole": whole_years, "months_whole": whole_months, "days_whole": day_count, "display_basis": "nominal_360_day_year_30_day_month_rounded_day; dates use dasha_year_days", } def _build_classical_major_periods( nakshatra_index: int, pada: int, moon_longitude: float, birth_date: datetime, year_days: float = 365.25, ) -> dict: group_index = _classical_group_for_nakshatra(nakshatra_index) pada_index = max(0, min(3, pada - 1)) cycle0 = list(CLASSICAL_KALACHAKRA_CYCLES[group_index][pada_index]) paramayush = CLASSICAL_PARAMAYUSH[group_index][pada_index] fraction_completed = _fraction_completed_in_pada(moon_longitude, nakshatra_index, pada) completed_years = fraction_completed * paramayush full_years = [CLASSICAL_RASHI_YEARS[sign] for sign in cycle0] cumulative = [] running = 0.0 for years in full_years: running += years cumulative.append(running) idx_at_birth = next((i for i, total in enumerate(cumulative) if total > completed_years), len(cumulative) - 1) remaining_years = cumulative[idx_at_birth] - completed_years next_group = group_index next_pada_index = (pada_index + 1) % 4 if pada_index == 3: next_group = CLASSICAL_GROUP_SWITCH_ON_PADA_4[group_index] cycle1 = list(CLASSICAL_KALACHAKRA_CYCLES[next_group][next_pada_index]) progression = cycle0[idx_at_birth:] + cycle1[:idx_at_birth] progression_years = [CLASSICAL_RASHI_YEARS[sign] for sign in progression] progression_years[0] = remaining_years periods = [] current_date = birth_date for index, (rashi_idx, years) in enumerate(zip(progression, progression_years)): end_date = current_date + timedelta(days=years * year_days) periods.append({ "lord": CLASSICAL_RASHI_LORDS[rashi_idx], "rashi": RASHIS[rashi_idx], "rashi_index": rashi_idx, "years": years, "start_date": current_date.isoformat(), "end_date": end_date.isoformat(), }) current_date = end_date mode = "savya" if group_index in (0, 1) else "apasavya" deha_index = cycle0[0] if mode == "savya" else cycle0[-1] jeeva_index = cycle0[-1] if mode == "savya" else cycle0[0] return { "mode": mode, "major": periods, "total_cycle": paramayush, "starting_lord": periods[0]["lord"] if periods else None, "starting_rashi": periods[0]["rashi"] if periods else None, "deha_rashi": RASHIS[deha_index], "jeeva_rashi": RASHIS[jeeva_index], "dasha_balance_at_birth": { "rashi": periods[0]["rashi"] if periods else None, **_years_to_balance_parts(remaining_years), }, "variant_trace": { "family": "classical_nine_sign_progression", "group_index": group_index, "pada_index": pada_index, "seed_cycle": [RASHIS[idx] for idx in cycle0], "progression_cycle": [RASHIS[idx] for idx in progression], "completed_paramayush_years": completed_years, "paramayush_years": paramayush, "dasha_year_days": year_days, }, } def _rotate_cycle_rows(cycle_rows: list[dict], start_rashi: str | None) -> list[dict]: if not cycle_rows: return [] if not start_rashi: return list(cycle_rows) start_index = next( (index for index, row in enumerate(cycle_rows) if row.get("rashi") == start_rashi), 0, ) return list(cycle_rows[start_index:]) + list(cycle_rows[:start_index]) def _build_subperiods(parent_period: dict, cycle_rows: list[dict], total_cycle_years: float, level_key: str, year_days: float = 365.25) -> list[dict]: if not cycle_rows: return [] rotated_rows = _rotate_cycle_rows(cycle_rows, parent_period.get("rashi")) cycle_basis_total = sum(float(row.get("years") or 0.0) for row in rotated_rows) denominator = cycle_basis_total if cycle_basis_total > 0 else float(total_cycle_years or 0.0) if denominator <= 0: return [] start_dt = datetime.fromisoformat(parent_period["start_date"]) current_dt = start_dt child_periods = [] for row in rotated_rows: child_years = float(parent_period["years"]) * float(row["years"]) / denominator end_dt = current_dt + timedelta(days=child_years * year_days) child_periods.append({ "level": level_key, "lord": row.get("lord"), "rashi": row.get("rashi"), "rashi_index": row.get("rashi_index"), "years": child_years, "start_date": current_dt.isoformat(), "end_date": end_dt.isoformat(), "status": "parameter_sensitive", "derivation": "native_kalachakra_recursive_proportional_variant", }) current_dt = end_dt if child_periods: child_periods[-1]["end_date"] = parent_period["end_date"] return child_periods def _attach_recursive_subperiods(major_periods: list[dict], total_cycle_years: float, year_days: float = 365.25) -> tuple[list[dict], list[dict]]: cycle_rows = [ { "lord": row.get("lord"), "rashi": row.get("rashi"), "rashi_index": row.get("rashi_index"), "years": row.get("years"), } for row in major_periods ] all_antardashas = [] all_pratyantardashas = [] for period in major_periods: antardasha = _build_subperiods(period, cycle_rows, total_cycle_years, "antardasha", year_days) period["antardasha"] = antardasha all_antardashas.extend(antardasha) for child in antardasha: pratyantardasha = _build_subperiods(child, cycle_rows, total_cycle_years, "pratyantardasha", year_days) child["pratyantardasha"] = pratyantardasha all_pratyantardashas.extend(pratyantardasha) return all_antardashas, all_pratyantardashas def _classical_sign_stream(group: int, pada: int, count: int) -> list[int]: """Continue through padas; never rotate the nine visible MD rows. PVR chapter 24, rules (4)-(5), examples 95-97. Repeated signs retain their sequence position, so a later occurrence cannot select an earlier one. """ result = [] while len(result) < count: result.extend(CLASSICAL_KALACHAKRA_CYCLES[group][pada]) pada += 1 if pada == 4: pada = 0 group = CLASSICAL_GROUP_SWITCH_ON_PADA_4[group] return result[:count] def _attach_classical_subperiods(payload: dict) -> tuple[list[dict], list[dict]]: """Apply published AD weights before clipping the birth balance. Keep the selected year convention explicit (default 365.25). Recursive PD is an extension of AD proportional division, not independently certified by the cited AD example. Both layers retain parameter-sensitive status. """ trace = payload['variant_trace'] year_days = trace['dasha_year_days'] stream = _classical_sign_stream(trace['group_index'], trace['pada_index'], 45) elapsed = trace['completed_paramayush_years'] position = 0 while position < 8 and elapsed >= CLASSICAL_RASHI_YEARS[stream[position]]: elapsed -= CLASSICAL_RASHI_YEARS[stream[position]] position += 1 def split(offset, nominal_start, nominal_years, visible_start, visible_end, level): sequence = stream[offset:offset + 9] denominator = sum(CLASSICAL_RASHI_YEARS[sign] for sign in sequence) rows = [] cursor = nominal_start for index, sign in enumerate(sequence): years = nominal_years * CLASSICAL_RASHI_YEARS[sign] / denominator end = cursor + timedelta(days=years * year_days) # Avoid accumulated microsecond rounding at the parent's last edge. if index == 8: end = visible_end start = max(cursor, visible_start) if end > start: row = { 'level': level, 'lord': CLASSICAL_RASHI_LORDS[sign], 'rashi': RASHIS[sign], 'rashi_index': sign, 'years': (end - start).total_seconds() / (year_days * 86400), 'start_date': start.isoformat(), 'end_date': end.isoformat(), 'status': 'parameter_sensitive', 'derivation': ('pvr_ch24_proportional_ad_birth_clipped' if level == 'antardasha' else 'recursive_pd_extension_of_pvr_ad'), } if level == 'antardasha': row['pratyantardasha'] = split( offset + index, cursor, years, start, end, 'pratyantardasha') rows.append(row) cursor = end return rows antardashas, pratyantardashas = [], [] for index, period in enumerate(payload['major']): start = datetime.fromisoformat(period['start_date']) end = datetime.fromisoformat(period['end_date']) full_years = CLASSICAL_RASHI_YEARS[period['rashi_index']] nominal_start = start - timedelta(days=(elapsed if index == 0 else 0) * year_days) children = split(position + index, nominal_start, full_years, start, end, 'antardasha') period['antardasha'] = children antardashas.extend(children) for child in children: pratyantardashas.extend(child['pratyantardasha']) return antardashas, pratyantardashas _PADA_STARTS = tuple(float(Fraction(10 * index, 3)) for index in range(108)) def _nakshatra_and_pada_from_longitude(longitude: float) -> tuple[int, int]: """Own the pada that starts on an exact 10/3 degree boundary. The shared find_nakshatra helper still splits a float span and disagrees at many of these 108 starts. Kalachakra uses the same boundaries as _fraction_completed_in_pada. """ longitude = float(longitude) % 360.0 quarter = bisect_right(_PADA_STARTS, longitude) - 1 if quarter < 0: quarter = 0 return quarter // 4, (quarter % 4) + 1 # --------------------------------------------------------------------------- # Core Functions # --------------------------------------------------------------------------- def calculate_kalachakra_dasha(birth_info: dict) -> dict: """Calculate Kalachakra Dasha for a native. A moon longitude selects the classical nine-sign progression (PVR chapter 24). Without it, the previous simplified savya/apasavya table stays in place so callers that only pass nakshatra and pada keep working. """ nakshatra_idx = birth_info.get("moon_nakshatra_index", 0) pada = birth_info.get("moon_pada", 1) birth_dt = birth_info.get("birth_datetime") moon_longitude = birth_info.get("moon_longitude") year_days = float(birth_info.get("dasha_year_days", 365.25)) if not math.isfinite(year_days) or year_days <= 0: raise ValueError("dasha_year_days must be finite and positive") if isinstance(birth_dt, str): birth_dt = datetime.fromisoformat(birth_dt) if birth_dt is None: birth_dt = datetime.now() if not isinstance(pada, int) or not 1 <= pada <= 4: pada = ((int(pada) - 1) % 4) + 1 classical_payload = None if moon_longitude is not None: longitude = float(moon_longitude) if not math.isfinite(longitude): raise ValueError("moon_longitude must be finite") longitude %= 360.0 nakshatra_idx, pada = _nakshatra_and_pada_from_longitude(longitude) classical_payload = _build_classical_major_periods( nakshatra_index=nakshatra_idx, pada=pada, moon_longitude=longitude, birth_date=birth_dt, year_days=year_days, ) if classical_payload is not None: mode = classical_payload["mode"] major_periods = classical_payload["major"] total_cycle = classical_payload["total_cycle"] starting_lord = classical_payload["starting_lord"] starting_rashi = classical_payload["starting_rashi"] else: mode = _get_mode(nakshatra_idx) start_offset = PADA_OFFSET_MAP.get(pada, 0) major_periods = _build_major_periods(mode, start_offset, birth_dt, year_days) total_cycle = sum(p["years"] for p in major_periods) starting_lord = major_periods[0]["lord"] if major_periods else None starting_rashi = major_periods[0]["rashi"] if major_periods else None now = birth_info.get("as_of") or datetime.now(tz=birth_dt.tzinfo) if isinstance(now, str): now = datetime.fromisoformat(now) current_period = None for period in major_periods: cycle_start = datetime.fromisoformat(period["start_date"]) cycle_end = datetime.fromisoformat(period["end_date"]) if cycle_start <= now < cycle_end: current_period = period.copy() current_period["elapsed_years"] = (now - cycle_start).total_seconds() / (year_days * 86400) current_period["remaining_years"] = (cycle_end - now).total_seconds() / (year_days * 86400) current_period["cycle_number"] = 1 break if classical_payload is not None: antardasha_periods, pratyantardasha_periods = _attach_classical_subperiods(classical_payload) else: antardasha_periods, pratyantardasha_periods = _attach_recursive_subperiods( major_periods, total_cycle, year_days, ) return { "mode": mode, "method_profile": ( "pvr_classical_nine_sign_md_ad" if classical_payload else "legacy_approximation_without_moon_longitude" ), "major": major_periods, "antardasha": antardasha_periods, "pratyantardasha": pratyantardasha_periods, "current": current_period, "total_cycle": total_cycle, "dasha_year_days": year_days, "starting_lord": starting_lord, "starting_rashi": starting_rashi, "jeeva_rashi": classical_payload.get("jeeva_rashi") if classical_payload else None, "deha_rashi": classical_payload.get("deha_rashi") if classical_payload else None, "dasha_balance_at_birth": classical_payload.get("dasha_balance_at_birth") if classical_payload else None, "variant_trace": classical_payload.get("variant_trace") if classical_payload else None, } # --------------------------------------------------------------------------- # Self-test # --------------------------------------------------------------------------- if __name__ == "__main__": # Example 1: Savya mode — Moon in Ashwini (Savya), Pada 1 test_birth_1 = { "moon_nakshatra_index": 0, # Ashwini (Savya) "moon_pada": 1, "birth_datetime": datetime(1990, 6, 15, 10, 30), } result_1 = calculate_kalachakra_dasha(test_birth_1) print("=" * 70) print("Test 1: Moon in Ashwini (Savya), Pada 1") print("=" * 70) print(f"Mode : {result_1['mode']}") print(f"Starting Lord : {result_1['starting_lord']}") print(f"Starting Rashi : {result_1['starting_rashi']}") print(f"Total Cycle : {result_1['total_cycle']} years") print("Major Periods:") for p in result_1["major"]: print(f" {p['lord']:8s} | {p['rashi']:12s} | {p['years']:2d} years | {p['start_date'][:10]} → {p['end_date'][:10]}") print(f"Current Period : {result_1['current']}") print() # Example 2: Apasavya mode — Moon in Rohini (Apasavya), Pada 2 test_birth_2 = { "moon_nakshatra_index": 3, # Rohini (Apasavya) "moon_pada": 2, "birth_datetime": datetime(1985, 3, 20, 8, 0), } result_2 = calculate_kalachakra_dasha(test_birth_2) print("=" * 70) print("Test 2: Moon in Rohini (Apasavya), Pada 2") print("=" * 70) print(f"Mode : {result_2['mode']}") print(f"Starting Lord : {result_2['starting_lord']}") print(f"Starting Rashi : {result_2['starting_rashi']}") print(f"Total Cycle : {result_2['total_cycle']} years") for p in result_2["major"]: print(f" {p['lord']:8s} | {p['rashi']:12s} | {p['years']:2d} years | {p['start_date'][:10]} → {p['end_date'][:10]}") print(f"Current Period : {result_2['current']}") print() # Example 3: Savya mode — Moon in Shravana (Savya), Pada 4 test_birth_3 = { "moon_nakshatra_index": 21, # Shravana (Savya) "moon_pada": 4, "birth_datetime": datetime(2000, 1, 1, 0, 0), } result_3 = calculate_kalachakra_dasha(test_birth_3) print("=" * 70) print("Test 3: Moon in Shravana (Savya), Pada 4") print("=" * 70) print(f"Mode : {result_3['mode']}") print(f"Starting Lord : {result_3['starting_lord']}") print(f"Starting Rashi : {result_3['starting_rashi']}") print(f"Total Cycle : {result_3['total_cycle']} years") for p in result_3["major"]: print(f" {p['lord']:8s} | {p['rashi']:12s} | {p['years']:2d} years | {p['start_date'][:10]} → {p['end_date'][:10]}")