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)
669 lines
26 KiB
Python
669 lines
26 KiB
Python
#!/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]}")
|