Merge codex/chart-dasha-yogini-20261021 (#88) into staging
Independent Staging Quality Gate / validate (push) Successful in 14m47s
Independent Staging Quality Gate / publish (push) Successful in 4m43s

This commit is contained in:
Jesse_Chen
2026-10-22 09:18:42 +08:00
26 changed files with 2970 additions and 45 deletions
+17 -2
View File
@@ -12,6 +12,7 @@ This module must not import `jyotish_api_server` or PyJHora.
from __future__ import annotations
import copy
from datetime import datetime
from typing import Any
@@ -287,8 +288,22 @@ def compute_dasha(
body = body or {}
alt, conditional, planet_lons = build_reading_birth_info(birth_dt, planets or {}, ascendant, body)
if key == "yogini":
payload = _load("yogini_dasha").calculate_yogini_dasha({**alt, "moon_longitude": conditional["moon_longitude"]})
return {"periods": _major_rows(payload)}
module = _load("yogini_dasha")
payload = module.calculate_yogini_dasha({**alt, "moon_longitude": conditional["moon_longitude"]})
year_days = 365.25
if isinstance(payload, dict) and payload.get("dasha_year_days") is not None:
year_days = float(payload["dasha_year_days"])
periods = []
for row in _major_rows(payload):
copied = copy.deepcopy(row)
if isinstance(copied, dict):
copied["antardasha"] = module.yogini_antardashas(
copied,
birth_dt=birth_dt,
year_days=year_days,
)
periods.append(copied)
return {"periods": periods}
if key == "ashtottari":
payload = _load("ashtottari_dasha").calculate_ashtottari_dasha({
**alt,
+53
View File
@@ -61,10 +61,63 @@ def calculate_yogini_dasha(birth_info: dict) -> dict:
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")