Production images do not install PyJHora, so those eight points were blocked. Swiss Ephemeris now computes them in-process. Rectification keeps the old Hora and Ghati formulas. BUG-1273.
291 lines
11 KiB
Python
291 lines
11 KiB
Python
#!/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",
|
|
}
|