fix(chart): Sripati Bhava from the real sidereal MC; Bhava chart and tables (T5, BUG-1077)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-28 15:38:24 +08:00
co-authored by Claude Opus 5.5
parent 3f6bcb081a
commit c35e0195e1
16 changed files with 743 additions and 9 deletions
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Sidereal MC for `/api/bhava_chalit` (BUG-1077).
Sripati and Porphyry trisect the quadrants between the ascendant and the MC.
The endpoint used to default a missing `mc_lon` to `asc + 270`, which makes
every quadrant exactly 90 degrees: the chart silently became equal houses and
no planet could ever change house. This module is the only place the endpoint
gets an MC from:
1. an explicit `mc_lon` in the body wins (existing callers and tests);
2. otherwise, with the birth moment and place in the body, the MC comes from
Swiss Ephemeris `houses_ex` in the sidereal frame of the request's ayanamsa,
the same call the full-reading producer uses
(`jyotish_engine._build_natal_foundation_modules`);
3. otherwise a house system that needs the MC is refused, never guessed.
House systems that do not read the MC keep the old placeholder, which only
appears as the echoed `mc_lon`.
"""
from __future__ import annotations
from typing import Any
from ayanamsa_utils import normalize_ayanamsa_name, temporary_ayanamsa
MC_HOUSE_SYSTEMS = frozenset({'sripati', 'porphyry'})
_BIRTH_KEYS = ('year', 'month', 'day', 'hour', 'lat', 'lon')
def _has_birth_moment(body: dict) -> bool:
return all(body.get(key) is not None for key in _BIRTH_KEYS)
def sidereal_mc_from_birth(handler: Any, body: dict) -> float:
"""MC longitude in the request's sidereal frame, from the birth moment."""
import swisseph as swe
year = handler._get_int(body, 'year', 1990, 1800, 2400)
month = handler._get_int(body, 'month', 6, 1, 12)
day = handler._get_int(body, 'day', 15, 1, 31)
hour = handler._get_float(body, 'hour', 12, 0, 23)
minute = handler._get_float(body, 'minute', 0, 0, 59)
second = handler._get_birth_second(body)
lat = handler._get_float(body, 'lat', 0, -90, 90)
lon = handler._get_float(body, 'lon', 0, -180, 180)
tz = handler._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
jd = swe.julday(year, month, day, handler._birth_hour_decimal(hour, minute, second) - tz)
name = normalize_ayanamsa_name(body.get('ayanamsa') or body.get('ayanamsa_name'))
with temporary_ayanamsa(name, swe):
_, ascmc = swe.houses_ex(jd, lat, lon, b'R', swe.FLG_SWIEPH | swe.FLG_SIDEREAL)
return float(ascmc[1]) % 360
def resolve_bhava_mc_lon(handler: Any, body: dict, house_system: str, asc_lon: float, bad_request: type[Exception]) -> float:
if body.get('mc_lon') is not None:
return handler._normalize_degree(body, 'mc_lon', 0)
if _has_birth_moment(body):
return sidereal_mc_from_birth(handler, body)
if house_system in MC_HOUSE_SYSTEMS:
raise bad_request(f'{house_system} needs mc_lon or the birth date, time and place (year, month, day, hour, lat, lon)')
return (asc_lon + 270) % 360
+1 -1
View File
@@ -9596,11 +9596,11 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
asc_lon = self._normalize_degree(body, 'asc_lon', 0)
else:
asc_lon = float(asc_lon) % 360
mc_lon = self._normalize_degree(body, 'mc_lon', (asc_lon + 270) % 360)
house_system = body.get('house_system', 'sripati')
if not isinstance(house_system, str):
raise BadRequest('house_system must be a string')
requested_house_system = house_system.lower().strip()
mc_lon = _load_local_module('bhava_chalit_mc').resolve_bhava_mc_lon(self, body, requested_house_system, asc_lon, BadRequest)
mode = body.get('mode', 'compare')
if not isinstance(mode, str):
raise BadRequest('mode must be a string')