Chart surfaces start Vimshottari from the birth moment, as reports do (issue #137, BUG-1492)
/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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
699e353887
commit
5fd79a5649
@@ -0,0 +1 @@
|
||||
"""Chart-facing dasha arithmetic that is not part of the frozen scoring files."""
|
||||
@@ -0,0 +1,102 @@
|
||||
"""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
|
||||
@@ -15,11 +15,13 @@ if str(SCRIPT_DIR) not in sys.path:
|
||||
try:
|
||||
import swisseph as swe
|
||||
from ayanamsa_utils import apply_ayanamsa, normalize_ayanamsa_name, sidereal_ayanamsa_degrees
|
||||
from domain_calculation_service import compute_chart, compute_vimshottari_timeline
|
||||
from domain_calculation_service import compute_chart
|
||||
from chart_dasha.birth_moment import compute_vimshottari_timeline # issue #137: same start as the report
|
||||
except ModuleNotFoundError:
|
||||
import swisseph as swe
|
||||
from scripts.ayanamsa_utils import apply_ayanamsa, normalize_ayanamsa_name, sidereal_ayanamsa_degrees
|
||||
from scripts.domain_calculation_service import compute_chart, compute_vimshottari_timeline
|
||||
from scripts.domain_calculation_service import compute_chart
|
||||
from scripts.chart_dasha.birth_moment import compute_vimshottari_timeline
|
||||
|
||||
SIGNS = [
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
|
||||
@@ -2530,6 +2530,12 @@ def execute_consultation_workflow(
|
||||
return apply_commercial_skill_truth(result)
|
||||
|
||||
|
||||
def _birth_moment_vimshottari(**kwargs):
|
||||
"""Chart surfaces start Vimshottari from the birth moment, as reports do (issue #137)."""
|
||||
from scripts.chart_dasha.birth_moment import compute_vimshottari_timeline
|
||||
return compute_vimshottari_timeline(**kwargs)
|
||||
|
||||
|
||||
def _load_local_module(module_name):
|
||||
cached = _LOCAL_MODULE_CACHE.get(module_name)
|
||||
if cached:
|
||||
@@ -6026,7 +6032,7 @@ class JyotishComputeService(VedastroEvidenceMixin, SynastryMixin):
|
||||
|
||||
moon_lon = float(planets_data['Moon']['lon'])
|
||||
birth_dt = datetime(year, month, day, int(hour), int(minute), int(second))
|
||||
canonical_dasha = calculation_service.compute_vimshottari_timeline(
|
||||
canonical_dasha = _birth_moment_vimshottari(
|
||||
birth_dt=birth_dt,
|
||||
moon_lon=moon_lon,
|
||||
current_date=birth_dt,
|
||||
@@ -6128,7 +6134,7 @@ class JyotishComputeService(VedastroEvidenceMixin, SynastryMixin):
|
||||
'ayanamsa': _request_ayanamsa(body),
|
||||
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
|
||||
})
|
||||
canonical_dasha = calculation_service.compute_vimshottari_timeline(
|
||||
canonical_dasha = _birth_moment_vimshottari(
|
||||
birth_dt=birth_dt,
|
||||
moon_lon=moon_lon,
|
||||
current_date=datetime.utcnow(),
|
||||
@@ -6803,7 +6809,7 @@ class JyotishComputeService(VedastroEvidenceMixin, SynastryMixin):
|
||||
computed = None
|
||||
if dasha_key == 'vimshottari':
|
||||
calculation_service = _load_local_module('domain_calculation_service')
|
||||
canonical_dasha = calculation_service.compute_vimshottari_timeline(
|
||||
canonical_dasha = _birth_moment_vimshottari(
|
||||
birth_dt=birth_dt,
|
||||
moon_lon=moon_lon,
|
||||
current_date=(
|
||||
@@ -6859,15 +6865,13 @@ class JyotishComputeService(VedastroEvidenceMixin, SynastryMixin):
|
||||
try:
|
||||
analyzer = _load_local_module('dasha_analyzer')
|
||||
enhanced = _load_local_module('dasha_calculator_enhanced')
|
||||
from scripts.chart_dasha import birth_moment
|
||||
nak_info, nak_progress, pada = analyzer.lon_to_nakshatra(moon_lon % 360)
|
||||
timeline, elapsed, remaining, start_lord = analyzer.build_dasha_timeline(
|
||||
birth_dt.strftime('%Y-%m-%d'),
|
||||
nak_info,
|
||||
nak_progress,
|
||||
)
|
||||
# issue #137: from the birth moment, as cmd_dasha (reports) does.
|
||||
timeline, elapsed, remaining, start_lord = birth_moment.mahadashas(birth_dt, moon_lon)
|
||||
today = self._parse_optional_date(current_date) if current_date else datetime.now()
|
||||
current_idx, current_md = analyzer.find_current(timeline, today)
|
||||
antardashas = analyzer.build_antardasha(current_md)
|
||||
antardashas = birth_moment.antardashas(current_md, visible_from=birth_dt if current_idx == 0 else None)
|
||||
current_ad = analyzer.find_current_sub(antardashas, today)
|
||||
years_into_md = max(0.0, (today - current_md['start']).days / 365.25)
|
||||
five_levels = enhanced.calculate_five_level_dasha(current_md['lord'], years_into_md)
|
||||
|
||||
@@ -7,10 +7,10 @@ from datetime import datetime
|
||||
|
||||
try: # pragma: no cover - import path differs under CLI vs pytest
|
||||
from dasha_calculator_enhanced import calculate_five_level_dasha
|
||||
from domain_calculation_service import compute_vimshottari_timeline
|
||||
from chart_dasha.birth_moment import compute_vimshottari_timeline # issue #137: same start as the report
|
||||
except ImportError: # pragma: no cover
|
||||
from scripts.dasha_calculator_enhanced import calculate_five_level_dasha
|
||||
from scripts.domain_calculation_service import compute_vimshottari_timeline
|
||||
from scripts.chart_dasha.birth_moment import compute_vimshottari_timeline
|
||||
|
||||
|
||||
def build_monthly_vimshottari_snapshot(
|
||||
|
||||
Reference in New Issue
Block a user