#!/usr/bin/env python3 """Birth special lagnas from Swiss Ephemeris. No PyJHora import. Numeric rates are the ones named by P.V.R. Narasimha Rao, *Vedic Astrology: An Integrated Approach*, section 5.2, and by the B.V. Raman methods below. ``references/`` does not contain the BPHS chapter 4 verse text, so a point is not given a rate that those named sources do not state. - Bhava: section 5.2 step (2) divides the sunrise-to-birth minutes by 4 (0.25 degree per minute) and adds that to the sidereal Sun at sunrise. - Hora: 0.5 degree per minute, one rasi per hour, from that same sunrise Sun. - Ghati: 1.25 degrees per minute, one rasi per 24-minute ghati. - Vighati: 15 degrees per minute. A literal "one rasi per vighati" (24 seconds) would be 75 degrees per minute; section 5.2's special-ascendant family, as recorded for this rate, uses 15. That is the frozen comparison rate. - Sree: ascendant plus the Moon's leftover degrees inside its nakshatra, times 27 (classical Sri Lagna). - Indu: B.V. Raman. Kala factors of the 9th lords from Lagna and from the Moon are 30, 16, 6, 8, 10, 12, 1 for Sun through Saturn. The degree is the Moon's degree inside its own sign. - Pranapada: ishta ghatis from sunrise (previous sunrise when birth is earlier), times 4 signs, plus the birth Sun, plus 8 signs for a fixed Sun sign, 4 for a dual Sun sign, and 0 for a movable Sun sign. The clock is truncated to whole seconds before the ghati count, matching the frozen comparison. - Varnada: B.V. Raman method 1. Odd signs are counted forward from Aries, even signs backward from Pisces. The returned degree is the ascendant's degree inside its own sign, placed in the Varnada sign. Sunrise is the centre of the solar disc, Hindu rising, no refraction (Swiss Ephemeris). Sidereal mode is the caller's ayanamsa and is restored. """ from __future__ import annotations from contextlib import nullcontext from datetime import datetime SIGNS = [ "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", ] SOURCE = "native_special_lagna" POINT_NAMES = ( ("Bhava_Lagna", "Bhava Lagna"), ("Hora_Lagna", "Hora Lagna"), ("Ghati_Lagna", "Ghati Lagna"), ("ViGhati_Lagna", "Vighati Lagna"), ("Sree_Lagna", "Sree Lagna"), ("Indu_Lagna", "Indu Lagna"), ("Pranapada_Lagna", "Pranapada Lagna"), ("Varnada_Lagna", "Varnada Lagna"), ) # Sun..Saturn lords of Aries..Pisces. Planet index 0 is the Sun. HOUSE_LORDS = (2, 5, 3, 1, 0, 3, 5, 2, 4, 6, 6, 4) INDU_KALAS = (30, 16, 6, 8, 10, 12, 1) ODD_SIGNS = frozenset({0, 2, 4, 6, 8, 10}) FIXED_SIGNS = frozenset({1, 4, 7, 10}) DUAL_SIGNS = frozenset({2, 5, 8, 11}) ONE_ARC_SECOND = 1.0 / 3600.0 RATES = { "Bhava_Lagna": 0.25, "Hora_Lagna": 0.5, "Ghati_Lagna": 1.25, "ViGhati_Lagna": 15.0, } def native_special_lagna_rows( birth_time: datetime, lat: float | None, lon: float | None, tz_offset: float | None, asc_degree: float, ayanamsa: str | None = None, ) -> dict[str, dict]: """Eight birth points. Missing coordinates block the rows and invent nothing.""" if lat is None or lon is None or tz_offset is None: return _blocked("birth_coordinates_or_timezone_missing") try: import swisseph as swe except Exception as exc: return _blocked(f"swisseph_unavailable:{exc}") try: from ayanamsa_utils import temporary_ayanamsa except ImportError: # package import from scripts.ayanamsa_utils import temporary_ayanamsa context = temporary_ayanamsa(ayanamsa) if ayanamsa else nullcontext() try: with context: rows = _compute(swe, birth_time, float(lat), float(lon), float(tz_offset), float(asc_degree)) except Exception as exc: reason = exc.__class__.__name__ if exc.__class__.__name__ == "UnsupportedAyanamsaError": reason = f"native_ayanamsa_unsupported:{ayanamsa}" else: reason = f"native_special_lagna_failed:{exc}" return _blocked(reason) if ayanamsa is not None: for row in rows.values(): row["ayanamsa"] = ayanamsa return rows def _blocked(reason: str) -> dict[str, dict]: return { key: {"full_name": full_name, "status": "blocked", "source": SOURCE, "reason": reason} for key, full_name in POINT_NAMES } def _compute(swe, birth_time: datetime, lat: float, lon: float, tz_offset: float, asc_degree: float) -> dict[str, dict]: planet_flags = ( swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.FLG_TRUEPOS | swe.FLG_NOGDEFL | swe.FLG_NONUT | swe.FLG_SPEED ) rise_flags = swe.BIT_HINDU_RISING | swe.BIT_NO_REFRACTION | swe.BIT_DISC_CENTER | swe.CALC_RISE asc_flags = swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.BIT_HINDU_RISING | swe.FLG_TRUEPOS | swe.FLG_SPEED local_hour = ( birth_time.hour + birth_time.minute / 60.0 + birth_time.second / 3600.0 + birth_time.microsecond / 3_600_000_000.0 ) jd_local = swe.julday(birth_time.year, birth_time.month, birth_time.day, local_hour) birth_hours = swe.revjul(jd_local, swe.GREG_CAL)[3] sun_at_sunrise, sunrise_hours = _sun_at_sunrise(swe, jd_local, lat, lon, tz_offset, planet_flags, rise_flags) minutes = (birth_hours - sunrise_hours) * 60.0 jd_birth_ut = jd_local - tz_offset / 24.0 sun_birth = _sidereal(swe, jd_birth_ut, swe.SUN, planet_flags) moon_birth = _sidereal(swe, jd_birth_ut, swe.MOON, planet_flags) asc_sign, asc_in_sign = _ascendant(swe, jd_local, lat, lon, tz_offset, asc_flags) asc_long = (asc_sign * 30.0 + asc_in_sign) % 360.0 longitudes = { key: (sun_at_sunrise + minutes * rate) % 360.0 for key, rate in RATES.items() } longitudes["Sree_Lagna"] = (asc_long + (moon_birth % (360.0 / 27.0)) * 27.0) % 360.0 longitudes["Indu_Lagna"] = _indu_longitude(asc_sign, moon_birth) longitudes["Pranapada_Lagna"] = _pranapada_longitude( swe, jd_local, birth_hours, lat, lon, tz_offset, sun_birth, planet_flags, rise_flags, ) varnada_sign = _varnada_sign(asc_sign, _sign_index(longitudes["Hora_Lagna"])) rows = {} for key, full_name in POINT_NAMES: if key == "Varnada_Lagna": longitude = varnada_sign * 30.0 + (asc_in_sign % 30.0) elif key == "Indu_Lagna": longitude = longitudes[key] else: longitude = longitudes[key] rows[key] = _payload(full_name, longitude, asc_degree) return rows def _sun_at_sunrise(swe, jd_local: float, lat: float, lon: float, tz_offset: float, planet_flags: int, rise_flags: int): year, month, day, _hour = swe.revjul(jd_local, swe.GREG_CAL) jd_midnight = swe.julday(int(year), int(month), int(day), 0.0) _status, times = swe.rise_trans( jd_midnight - tz_offset / 24.0, swe.SUN, rise_flags, (lon, lat, 0.0), 0.0, 0.0, planet_flags, ) rise_jd_ut = times[0] rise_local = (rise_jd_ut - jd_midnight) * 24.0 + tz_offset hour, minute, second = _clock_hms(rise_local) truncated = swe.julday(int(year), int(month), int(day), hour + minute / 60.0 + second / 3600.0) # Chart helpers treat the sunrise JD as local and subtract the timezone. sun = _sidereal(swe, truncated, swe.SUN, planet_flags) return sun, rise_local def _pranapada_longitude(swe, jd_local, birth_hours, lat, lon, tz_offset, sun_birth, planet_flags, rise_flags): sunrise_hours = _sun_at_sunrise(swe, jd_local, lat, lon, tz_offset, planet_flags, rise_flags)[1] if birth_hours < sunrise_hours: sunrise_hours = _sun_at_sunrise(swe, jd_local - 1.0, lat, lon, tz_offset, planet_flags, rise_flags)[1] elapsed = 24.0 + birth_hours - sunrise_hours else: elapsed = birth_hours - sunrise_hours hours, minutes, seconds = _clock_hms(elapsed) tharparai = int(hours) * 9000 + int(minutes) * 150 + int(seconds) birth_signs = ((tharparai / 3600.0) * 4.0) % 12.0 sun_sign = _sign_index(sun_birth) if sun_sign in FIXED_SIGNS: extra = 240.0 elif sun_sign in DUAL_SIGNS: extra = 120.0 else: extra = 0.0 return (birth_signs * 30.0 + sun_birth + extra) % 360.0 def _indu_longitude(asc_sign: int, moon_long: float) -> float: moon_sign = _sign_index(moon_long) ninth = HOUSE_LORDS[(asc_sign + 8) % 12] ninth_from_moon = HOUSE_LORDS[(moon_sign + 8) % 12] kalas = (INDU_KALAS[ninth] + INDU_KALAS[ninth_from_moon]) % 12 if kalas == 0: kalas = 12 indu_sign = (moon_sign + kalas - 1) % 12 return indu_sign * 30.0 + (moon_long % 30.0) def _varnada_sign(lagna: int, hora_sign: int) -> int: lagna_odd = lagna in ODD_SIGNS hora_sign = hora_sign % 12 hora_odd = hora_sign in ODD_SIGNS count1 = _count_rasis(0, lagna, 1) if lagna_odd else _count_rasis(11, lagna, -1) count2 = _count_rasis(0, hora_sign, 1) if hora_odd else _count_rasis(11, hora_sign, -1) same_parity = (count1 + count2) % 12 opposite_parity = (max(count1, count2) - min(count1, count2)) % 12 count = same_parity if hora_odd == lagna_odd else opposite_parity counted = _count_rasis(1, count, 1) if lagna_odd else _count_rasis(12, count, -1) return (counted - 1) % 12 def _count_rasis(start: int, end: int, direction: int, total: int = 12) -> int: return ((total + direction * (end - start)) % total) + 1 def _ascendant(swe, jd_local: float, lat: float, lon: float, tz_offset: float, flags: int): jd_ut = jd_local - tz_offset / 24.0 _cusps, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", flags) longitude = ascmc[0] % 360.0 sign = int(longitude // 30) % 12 return sign, longitude - sign * 30.0 def _sidereal(swe, jd_ut: float, planet: int, flags: int) -> float: position, _speed = swe.calc_ut(jd_ut, planet, flags) return position[0] % 360.0 def _sign_index(longitude: float) -> int: sign, _degree = _dasavarga(longitude) return sign def _dasavarga(longitude: float) -> tuple[int, float]: one_sign = 360.0 fraction = (longitude / one_sign) % 1.0 sign = int(fraction * 12.0) degree = (longitude - sign * 30.0) % 30.0 if int(degree + ONE_ARC_SECOND) == 30: degree = 0.0 sign = (sign + 1) % 12 return sign, degree def _clock_hms(hours: float) -> tuple[int, int, int]: """Whole hours, minutes, and rounded seconds, matching the frozen oracle clock.""" day_part = int(hours) minutes_float = (hours - day_part) * 60.0 minute = int(minutes_float) second = round((minutes_float - minute) * 60.0) if day_part > 23: day_part = day_part % 24 elif day_part < 0: day_part = abs(day_part) % 24 minute = abs(minute) second = abs(second) if second == 60: minute += 1 second = 0 if minute == 60: day_part += 1 minute = 0 return int(day_part), int(minute), int(second) def _payload(full_name: str, longitude: float, asc_degree: float) -> dict: longitude = longitude % 360.0 sign, degree = _dasavarga(longitude) full = sign * 30.0 + degree return { "full_name": full_name, "degree": round(full, 4), "longitude": round(full, 4), "sign": SIGNS[sign], "sign_degree": round(degree, 4), "house": ((sign - int(asc_degree // 30)) % 12) + 1, "method": "swiss_ephemeris_special_lagna", "source": SOURCE, "status": "computed", }