Files
Jyotisha/scripts/kalachakra_dasha.py
T
jesse-uxandGrok (agent loop) 96312893b7 fix(kalachakra): classical sign periods when the Moon longitude is present (BUG-1476, upstream f353a096)
Without a Moon longitude the simplified table remains. Exact pada starts use a local boundary table because shared find_nakshatra misclassifies 50 of 108 starts. Two existing child-period assertions now expect antardasha; original, new value, and reason are in the test comments. 116 source tests passed.

Co-Authored-By: Grok (agent loop)
2026-10-28 09:45:08 +08:00

669 lines
26 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
# -*- 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]}")