#!/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", }