完整解读仍只看大运。小运从该大运自己的 Yogini 起按年数比例排,出生前的段丢掉。 Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
7.2 KiB
Python
185 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
yogini_dasha.py — Yogini Dasha System (36-Year Cycle)
|
|
|
|
The Yogini Dasha is a Nakshatra-based dasha system with a total cycle of 36 years.
|
|
It consists of 8 Yoginis (female planetary deities), each ruling a specific number
|
|
of years. The sequence is fixed and cycles continuously.
|
|
|
|
Order follows Goel, Classical chapter, Table I. The first period is the
|
|
remaining birth balance of that Yogini, not a full period counted from birth.
|
|
Seed is ``(zero_based_moon_nakshatra + 3) mod 8``. Moon longitude is required.
|
|
|
|
Order | Yogini | Years | Planet
|
|
------|-----------|-------|--------
|
|
1 | Mangala | 1 | Moon
|
|
2 | Pingala | 2 | Sun
|
|
3 | Dhanya | 3 | Jupiter
|
|
4 | Bhramari | 4 | Mars
|
|
5 | Bhadrika | 5 | Mercury
|
|
6 | Ulka | 6 | Saturn
|
|
7 | Siddha | 7 | Venus
|
|
8 | Sankata | 8 | Rahu
|
|
|
|
Total: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36 years.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# 12 Rashis (Zodiac signs)
|
|
RASHIS = [
|
|
"Aries", "Taurus", "Gemini", "Cancer",
|
|
"Leo", "Virgo", "Libra", "Scorpio",
|
|
"Sagittarius", "Capricorn", "Aquarius", "Pisces"
|
|
]
|
|
|
|
# Goel Classical chapter Table I. Do not start a full period at birth.
|
|
YOGINI_SEQUENCE = [
|
|
{"name": "Mangala", "years": 1, "planet": "Moon"},
|
|
{"name": "Pingala", "years": 2, "planet": "Sun"},
|
|
{"name": "Dhanya", "years": 3, "planet": "Jupiter"},
|
|
{"name": "Bhramari", "years": 4, "planet": "Mars"},
|
|
{"name": "Bhadrika", "years": 5, "planet": "Mercury"},
|
|
{"name": "Ulka", "years": 6, "planet": "Saturn"},
|
|
{"name": "Siddha", "years": 7, "planet": "Venus"},
|
|
{"name": "Sankata", "years": 8, "planet": "Rahu"},
|
|
]
|
|
|
|
TOTAL_CYCLE = 36 # years
|
|
|
|
|
|
def calculate_yogini_dasha(birth_info: dict) -> dict:
|
|
"""Yogini from the Moon's nakshatra, with the remaining birth balance.
|
|
|
|
birth_info:
|
|
birth_datetime: datetime or ISO string
|
|
moon_longitude: sidereal degrees, required
|
|
as_of / today: observation instant; report callers should pass it
|
|
dasha_year_days: fixed-day year, default 365.25
|
|
|
|
Major rows stay free of sub-periods. Callers that need antardashas use
|
|
``yogini_antardashas`` on a copy.
|
|
"""
|
|
return _calculate_moon_balanced_yogini(birth_info)
|
|
|
|
|
|
def _as_datetime(value):
|
|
if isinstance(value, datetime):
|
|
return value
|
|
return datetime.fromisoformat(str(value))
|
|
|
|
|
|
def yogini_antardashas(major_row: dict, *, birth_dt, year_days: float) -> list:
|
|
"""Proportional antardashas inside one Yogini mahadasha.
|
|
|
|
Eight sub-periods start at this mahadasha's own Yogini and follow Goel
|
|
Table I. Each length is ``full_years * sub_years / 36``. They are laid out
|
|
from ``unclipped_start_date``. Sub-periods that end at or before birth are
|
|
dropped. The one that crosses birth keeps ``unclipped_start_date`` and
|
|
starts at birth. The last sub-period ends on the mahadasha ``end_date``.
|
|
"""
|
|
birth = _as_datetime(birth_dt)
|
|
year_length = float(year_days)
|
|
if year_length <= 0:
|
|
raise ValueError('dasha_year_days must be positive')
|
|
start_idx = next(
|
|
index for index, item in enumerate(YOGINI_SEQUENCE) if item['name'] == major_row['yogini']
|
|
)
|
|
full_years = float(major_row['full_years'])
|
|
cursor = _as_datetime(major_row['unclipped_start_date'])
|
|
major_end = _as_datetime(major_row['end_date'])
|
|
rows = []
|
|
for offset in range(len(YOGINI_SEQUENCE)):
|
|
item = YOGINI_SEQUENCE[(start_idx + offset) % len(YOGINI_SEQUENCE)]
|
|
span_days = full_years * item['years'] / TOTAL_CYCLE * year_length
|
|
end = major_end if offset == len(YOGINI_SEQUENCE) - 1 else cursor + timedelta(days=span_days)
|
|
if end <= birth:
|
|
cursor = end
|
|
continue
|
|
visible_start = birth if cursor < birth else cursor
|
|
row = {
|
|
'level': 'antardasha',
|
|
'planet': item['planet'],
|
|
'yogini': item['name'],
|
|
'lord': item['planet'],
|
|
'years': (end - visible_start).total_seconds() / (86400 * year_length),
|
|
'start_date': visible_start.isoformat(timespec='seconds'),
|
|
'end_date': end.isoformat(timespec='seconds'),
|
|
}
|
|
if cursor < birth:
|
|
row['unclipped_start_date'] = cursor.isoformat(timespec='seconds')
|
|
rows.append(row)
|
|
cursor = end
|
|
return rows
|
|
|
|
|
|
def _calculate_moon_balanced_yogini(birth_info):
|
|
"""Goel Table I order, angular birth balance, explicit fixed-day year."""
|
|
birth_dt = birth_info.get("birth_datetime")
|
|
|
|
if isinstance(birth_dt, str):
|
|
birth_dt = datetime.fromisoformat(birth_dt)
|
|
if birth_dt is None or birth_info.get('moon_longitude') is None:
|
|
return {
|
|
'status': 'blocked',
|
|
'reason': 'birth_datetime_and_moon_longitude_required',
|
|
'major': [],
|
|
'current': None,
|
|
}
|
|
moon = float(birth_info['moon_longitude']) % 360
|
|
nak_length = 360 / 27
|
|
nak = int(moon / nak_length)
|
|
fraction = (moon % nak_length) / nak_length
|
|
start_idx = (nak + 3) % 8
|
|
seed_method = 'moon_nakshatra_plus_3_mod_8_with_angular_balance'
|
|
starting_yogini = YOGINI_SEQUENCE[start_idx]["name"]
|
|
starting_planet = YOGINI_SEQUENCE[start_idx]["planet"]
|
|
|
|
year_days = float(birth_info.get('dasha_year_days', 365.25))
|
|
if year_days <= 0:
|
|
raise ValueError('dasha_year_days must be positive')
|
|
now = birth_info.get('as_of') or birth_info.get('today') or datetime.now()
|
|
if isinstance(now, str):
|
|
now = datetime.fromisoformat(now)
|
|
first_years = YOGINI_SEQUENCE[start_idx]['years']
|
|
cursor = birth_dt - timedelta(days=fraction * first_years * year_days)
|
|
major_periods = []
|
|
cycles = max(3, int(max(0, (now - birth_dt).total_seconds()) / (86400 * year_days * 36)) + 2)
|
|
current_period = None
|
|
for i in range(8 * cycles):
|
|
item = YOGINI_SEQUENCE[(start_idx + i) % 8]
|
|
end = cursor + timedelta(days=item['years'] * year_days)
|
|
visible_start = max(cursor, birth_dt)
|
|
row = {
|
|
'yogini': item['name'],
|
|
'planet': item['planet'],
|
|
'full_years': item['years'],
|
|
'years': (end - visible_start).total_seconds() / (86400 * year_days),
|
|
'start_date': visible_start.isoformat(timespec='seconds'),
|
|
'end_date': end.isoformat(timespec='seconds'),
|
|
'unclipped_start_date': cursor.isoformat(timespec='seconds'),
|
|
'cycle_number': i // 8 + 1,
|
|
}
|
|
major_periods.append(row)
|
|
if visible_start <= now < end:
|
|
current_period = dict(row)
|
|
cursor = end
|
|
|
|
return {
|
|
"major": major_periods,
|
|
"current": current_period,
|
|
"total_cycle": TOTAL_CYCLE,
|
|
"starting_yogini": starting_yogini,
|
|
"starting_planet": starting_planet,
|
|
"seed_method": seed_method,
|
|
"reference_date": now.isoformat(timespec='seconds'),
|
|
"dasha_year_days": year_days,
|
|
"birth_balance_years": first_years * (1 - fraction),
|
|
"source": "BPHS Yogini; Goel Table I; angular balance and fixed-day year",
|
|
}
|