Files
Jyotisha/tests/test_vimshottari_second_cycle.py
ClaudeandClaude Opus 5.5 20a00ebef7 fix(dasha): continue Vimshottari past the first 120-year cycle (BUG-1556)
The first mahadasha starts before birth, so the nine periods end before the
120th birthday. Historical charts past that point had no covering period:
cmd_dasha's current_dasha was null and the consultation card's
timing.vimshottari was empty (Frida Kahlo at 2026-09-27).

next_cycle_mahadashas (chart_dasha/birth_moment.py) continues with the same
lords, order and full 365.25-day years until a period covers the reference
day; cmd_dasha, the chart surfaces and the consultation sub-periods share it.
Nothing is added inside the first cycle. The frozen rectification identity
(dasha_analyzer, domain_calculation_service) is untouched, so no re-freeze.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
2026-11-04 14:54:19 +08:00

131 lines
6.2 KiB
Python

"""BUG-1556: Vimshottari past the first 120-year cycle.
The first mahadasha starts before birth, so the nine periods end less than 120
years after birth. Old (historical / public) charts then had no period covering
the reference date: the engine's current_dasha was null and the consultation
card's timing.vimshottari was empty. The timeline now continues with the same
lords, order and full years (365.25-day years, as the first cycle) until a
period covers the reference date; inside the first cycle nothing changes.
"""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from scripts.chart_dasha import birth_moment
from scripts.jyotish_engine import cmd_dasha
import domain_calculation_service as dcs
import jyotish_api_server as api
# Frida Kahlo, Rodden AA: 1907-07-06 08:30, Mexico City (public chart).
KAHLO = dict(year=1907, month=7, day=6, hour=8, minute=30, second=0, lat=19.3333, lon=-99.1667, tz=-6.6167)
KAHLO_BIRTH = datetime(1907, 7, 6, 8, 30)
REFERENCE = "2026-09-27"
def _days(a: str, b: str) -> int:
return abs((datetime.strptime(a, "%Y-%m-%d") - datetime.strptime(b, "%Y-%m-%d")).days)
def _engine(today: str) -> dict:
args = SimpleNamespace(**KAHLO, ayanamsa="raman", node_mode="mean", today=today, moon_lon=None,
nakshatra=None, pada=None, birthdate=None)
out = cmd_dasha(args)
return out.get("result") or out
def _kahlo_moon() -> float:
chart = dcs.compute_chart({**KAHLO, "ayanamsa": "raman", "node_mode": "mean"})
return float(chart["planets"]["Moon"]["lon"])
def test_kahlo_current_mahadasha_and_antardasha_in_the_second_cycle():
result = _engine(REFERENCE)
current = result["current_dasha"]
assert current is not None
assert (current["lord"], current["start"], current["end"]) == ("Sun", "2022-02-20", "2028-02-21")
assert current["cycle"] == 2
ad = current["antardasha"]
assert (ad["lord"], ad["start"], ad["end"]) == ("Mercury", "2025-12-09", "2026-10-16")
assert any(pd["is_current"] for pd in ad["pratyantar_dasha_timeline"])
# Independent arithmetic (365.25-day years, from the birth date): Sun 2022-02-21 to
# 2028-02-21, Mercury 2025-12-10 to 2026-10-16. The engine starts from the birth clock.
for got, independent in ((current["start"], "2022-02-21"), (current["end"], "2028-02-21"),
(ad["start"], "2025-12-10"), (ad["end"], "2026-10-16")):
assert _days(got, independent) <= 1
# The second cycle is the first one shifted by exactly 120 years of 365.25 days.
first_sun = result["timeline"][0]
shift = datetime.fromisoformat(current["start_datetime"]) - datetime.fromisoformat(first_sun["start_datetime"])
assert shift.total_seconds() == 120 * 365.25 * 86400
def test_an_age_inside_the_first_cycle_is_unchanged():
inside = _engine("2010-01-01")
assert len(inside["timeline"]) == 9
assert all("cycle" not in row for row in inside["timeline"])
assert inside["current_dasha"]["lord"] == "Venus"
# Same nine periods as at the later date, which only appends.
later = _engine(REFERENCE)
keys = ("lord", "start_datetime", "end_datetime", "years", "is_balance")
assert [{k: r[k] for k in keys} for r in later["timeline"][:9]] == [{k: r[k] for k in keys} for r in inside["timeline"]]
moon = _kahlo_moon()
plain = birth_moment.compute_vimshottari_timeline(birth_dt=KAHLO_BIRTH, moon_lon=moon)
dated = birth_moment.compute_vimshottari_timeline(birth_dt=KAHLO_BIRTH, moon_lon=moon, current_date=datetime(2010, 1, 1))
assert dated == plain
assert len(plain["periods"]) == 9
def test_the_boundary_at_exactly_the_cycle_end():
moon = _kahlo_moon()
first, *_ = birth_moment.mahadashas(KAHLO_BIRTH, moon)
cycle_end = first[-1]["end"]
assert cycle_end.strftime("%Y-%m-%d") == "2022-02-20"
before = _engine("2022-02-19")
assert len(before["timeline"]) == 9
assert before["current_dasha"]["lord"] == "Venus"
# On the end day the next period is current, as at every other boundary (date granularity).
on = _engine("2022-02-20")
assert len(on["timeline"]) == 10
assert (on["current_dasha"]["lord"], on["current_dasha"]["start"]) == ("Sun", "2022-02-20")
assert on["timeline"][9]["start_datetime"] == on["timeline"][8]["end_datetime"]
rows, *_ = birth_moment.mahadashas(KAHLO_BIRTH, moon, through=cycle_end)
assert [row["lord"] for row in rows] == [row["lord"] for row in first] + ["Sun"]
assert rows[9]["start"] == cycle_end and rows[9]["cycle"] == 2
def test_next_cycle_keeps_lord_order_and_full_years():
moon = _kahlo_moon()
first, *_ = birth_moment.mahadashas(KAHLO_BIRTH, moon)
rows, *_ = birth_moment.mahadashas(KAHLO_BIRTH, moon, through=datetime(2142, 1, 1))
assert [r["lord"] for r in rows[9:18]] == [r["lord"] for r in first]
assert [r["years"] for r in rows[9:18]] == [r["years"] for r in first]
assert {r["cycle"] for r in rows[9:18]} == {2}
for a, b in zip(rows, rows[1:]):
assert a["end"] == b["start"]
def test_consultation_sub_periods_cover_the_reference_past_the_first_cycle():
moon = _kahlo_moon()
# A chart computed for a day inside the first cycle stops at nine periods.
periods = birth_moment.compute_vimshottari_timeline(birth_dt=KAHLO_BIRTH, moon_lon=moon)["periods"]
chart = {
"birth": {"date": "1907-07-06", "hour": 8, "minute": 30, "second": 0},
"planets": {"Moon": {"lon": moon}},
"dasha": {"periods": periods},
}
sub = api._consultation_dasha_sub_periods(chart, {"reference_date": REFERENCE})
assert sub is not None
assert sub["current"]["mahadasha"] == {"lord": "Sun", "start": "2022-02-20", "end": "2028-02-21"}
assert sub["current"]["antardasha"]["lord"] == "Mercury"
# The chart surface carries the period itself when computed for the reference day.
dated = birth_moment.compute_vimshottari_timeline(birth_dt=KAHLO_BIRTH, moon_lon=moon, current_date=datetime(2026, 9, 27))
assert dated["periods"][-1] == {**sub["current"]["mahadasha"], "years": 6, "cycle": 2}
# A rebuilt timeline that does not reproduce the chart's periods is not used.
chart["planets"]["Moon"]["lon"] = moon + 5
assert api._consultation_dasha_sub_periods(chart, {"reference_date": REFERENCE}) is None