/api/chart, /api/dasha and the analysis layer, daily guidance and the KP monthly packet used compute_vimshottari_timeline, which hands build_dasha_timeline only the birth date, so periods started at midnight; cmd_dasha (reports) starts from the birth clock and lays antardashas from the full parent with float days. scripts/chart_dasha/birth_moment.py reproduces cmd_dasha; 60 standard charts: MD and AD 60/60 equal (old chart path 9/60), 51 charts move by at most one day. Frozen scoring files are untouched, so rectification probes keep the day start (left for a product decision). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.7 KiB
Python
103 lines
4.7 KiB
Python
"""Vimshottari from the real birth moment, as the report computes it (issue #137).
|
|
|
|
`/api/chart` and the dasha endpoints went through
|
|
``domain_calculation_service.compute_vimshottari_timeline``, which hands
|
|
``build_dasha_timeline`` only the birth *date*, so every period started from
|
|
midnight of the birth day. ``jyotish_engine.cmd_dasha`` (reports, full
|
|
reading) starts from the birth clock. The two disagreed by up to a day.
|
|
|
|
This module reproduces ``cmd_dasha``'s arithmetic for the chart surfaces:
|
|
the balance is taken back from the birth moment, years are 365.25 days, and
|
|
antardashas run from the full parent start and are clipped to birth for the
|
|
first (balance) mahadasha. The frozen scoring files
|
|
(``domain_calculation_service``, ``dasha_analyzer``) are read, never changed,
|
|
so rectification keeps its own frozen results.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
try:
|
|
from scripts.dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS
|
|
from scripts.dasha_analyzer import lon_to_nakshatra
|
|
except ModuleNotFoundError: # pragma: no cover - script execution path
|
|
from dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS
|
|
from dasha_analyzer import lon_to_nakshatra
|
|
|
|
YEAR_DAYS = 365.25
|
|
START_BASIS = "birth_moment"
|
|
|
|
|
|
def mahadashas(birth_dt: datetime, moon_lon: float) -> tuple[list[dict[str, Any]], float, float, str]:
|
|
"""Same tuple as ``dasha_analyzer.build_dasha_timeline``, from the birth moment."""
|
|
(nak_name, start_lord, start_years), progress, _pada = lon_to_nakshatra(float(moon_lon) % 360)
|
|
elapsed = progress * start_years
|
|
remaining = start_years - elapsed
|
|
start = birth_dt - timedelta(days=elapsed * YEAR_DAYS)
|
|
first = VIMSHOTTARI_ORDER.index(start_lord)
|
|
timeline = []
|
|
for offset in range(9):
|
|
lord = VIMSHOTTARI_ORDER[(first + offset) % 9]
|
|
years = VIMSHOTTARI_YEARS[lord]
|
|
end = start + timedelta(days=years * YEAR_DAYS)
|
|
timeline.append({"lord": lord, "start": start, "end": end, "years": years})
|
|
start = end
|
|
return timeline, round(elapsed, 2), round(remaining, 2), start_lord
|
|
|
|
|
|
def antardashas(mahadasha: dict[str, Any], *, visible_from: datetime | None = None) -> list[dict[str, Any]]:
|
|
"""Sub-periods as ``cmd_dasha`` lays them out; ``visible_from`` clips the balance period at birth."""
|
|
lord = mahadasha["lord"]
|
|
parent_start, parent_end = mahadasha["start"], mahadasha["end"]
|
|
total = timedelta(days=float(VIMSHOTTARI_YEARS[lord]) * YEAR_DAYS)
|
|
first = VIMSHOTTARI_ORDER.index(lord)
|
|
rows = []
|
|
start = parent_start
|
|
for offset in range(9):
|
|
sub_lord = VIMSHOTTARI_ORDER[(first + offset) % 9]
|
|
end = parent_end if offset == 8 else start + total * (VIMSHOTTARI_YEARS[sub_lord] / 120.0)
|
|
if visible_from is None or end > visible_from:
|
|
rows.append({
|
|
"lord": sub_lord,
|
|
"start": max(start, visible_from) if visible_from else start,
|
|
"end": end,
|
|
"years": VIMSHOTTARI_YEARS[sub_lord],
|
|
})
|
|
start = end
|
|
return rows
|
|
|
|
|
|
def _canonical_hash(payload: dict[str, Any]) -> str:
|
|
encoded = json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"), default=str)
|
|
return hashlib.sha256(encoded.encode("ascii")).hexdigest()
|
|
|
|
|
|
def compute_vimshottari_timeline(*, birth_dt: datetime, moon_lon: float, current_date: datetime | None = None) -> dict[str, Any]:
|
|
"""Drop-in for ``domain_calculation_service.compute_vimshottari_timeline``, from the birth moment."""
|
|
try: # read-only: the frozen service's contract version
|
|
from scripts.domain_calculation_service import CONTRACT_VERSION
|
|
except ModuleNotFoundError: # pragma: no cover - script execution path
|
|
from domain_calculation_service import CONTRACT_VERSION
|
|
|
|
(nak_name, _lord, _years), _progress, pada = lon_to_nakshatra(float(moon_lon) % 360)
|
|
timeline, elapsed, remaining, start_lord = mahadashas(birth_dt, moon_lon)
|
|
result = {
|
|
"periods": [
|
|
{"lord": row["lord"], "years": row["years"], "start": row["start"].strftime("%Y-%m-%d"), "end": row["end"].strftime("%Y-%m-%d")}
|
|
for row in timeline
|
|
],
|
|
"birth_balance": {"lord": start_lord, "elapsed_years": elapsed, "remaining_years": remaining},
|
|
"calculation_contract": {
|
|
"contract_version": CONTRACT_VERSION,
|
|
"algorithm": "vimshottari_birth_balance",
|
|
"requested": {"moon_longitude": float(moon_lon) % 360},
|
|
"effective": {"year_basis_days": YEAR_DAYS, "nakshatra": nak_name, "pada": pada, "start_basis": START_BASIS},
|
|
},
|
|
}
|
|
result["result_hash"] = _canonical_hash(result)
|
|
return result
|