fix(special-lagnas): compute eight birth points without PyJHora
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.
This commit is contained in:
@@ -15664,7 +15664,7 @@ def cmd_full_reading(args):
|
||||
from special_lagnas import SpecialLagnasCalculator
|
||||
sl_calc = SpecialLagnasCalculator()
|
||||
birth_dt = _birth_datetime_from_args(args)
|
||||
# 6:00 只在缺坐标时给公式回退。有坐标时 PyJHora 按出生时刻覆盖 Bhava / Hora / Ghati。
|
||||
# 6:00 只在缺坐标时给公式回退。有坐标时 Swiss Ephemeris 按出生时刻覆盖八个特殊上升点。
|
||||
sunrise_dt = datetime(args.year, args.month, args.day, 6, 0)
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
#!/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",
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Optional in-process Panchavargiya lookup for the year-lord research switch.
|
||||
|
||||
Not on the default full-reading, report, or consultation path. Tajika calls
|
||||
this only when a score was requested with an annual Julian day. The product
|
||||
year-lord path leaves that day empty unless
|
||||
``JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def external_panchavargiya_score(
|
||||
planet: str,
|
||||
*,
|
||||
annual_jd: float,
|
||||
lat: float,
|
||||
lon: float,
|
||||
tz: float,
|
||||
planet_index: int | None,
|
||||
) -> dict | None:
|
||||
del planet
|
||||
from jhora.horoscope.chart import strength as j_strength
|
||||
from jhora.panchanga import drik as j_drik
|
||||
|
||||
place = j_drik.Place("annual_year_lord_probe", float(lat), float(lon), float(tz))
|
||||
# Annual-chart APIs take the local civil JD. ``annual_jd`` arrives as UT.
|
||||
scores = j_strength.pancha_vargeeya_bala(float(annual_jd) + float(tz) / 24.0, place)
|
||||
if planet_index is None:
|
||||
return None
|
||||
score = float(scores[planet_index])
|
||||
return {
|
||||
"score": score,
|
||||
"engine": "pyjhora",
|
||||
"components": {"pyjhora_pancha_vargeeya_bala": score},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Optional PyJHora D1 signs for the rectification parity packet.
|
||||
|
||||
The product packet calls this only for the PyJHora column. A missing install
|
||||
raises, and the packet records that column as blocked.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
SIGNS = (
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
)
|
||||
|
||||
|
||||
def pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
utils = importlib.import_module("jhora.utils")
|
||||
charts = importlib.import_module("jhora.horoscope.chart.charts")
|
||||
drik = importlib.import_module("jhora.panchanga.drik")
|
||||
jd = utils.julian_day_number(
|
||||
(case["year"], case["month"], case["day"]),
|
||||
(case["hour"], case["minute"], case.get("second", 0)),
|
||||
)
|
||||
drik.set_ayanamsa_mode("LAHIRI", jd=jd)
|
||||
place = drik.Place("request-level", case["lat"], case["lon"], case["tz"])
|
||||
index_to_planet = {
|
||||
0: "Sun", 1: "Moon", 2: "Mars", 3: "Mercury", 4: "Jupiter", 5: "Venus", 6: "Saturn",
|
||||
}
|
||||
return {
|
||||
index_to_planet[body]: SIGNS[int(position[0])]
|
||||
for body, position in charts.rasi_chart(jd, place)
|
||||
if body in index_to_planet
|
||||
}
|
||||
@@ -27,7 +27,6 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
|
||||
JYOTISHGANIT_DATA_DIR = ROOT / ".cache" / "jyotishganit"
|
||||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
||||
SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces")
|
||||
|
||||
|
||||
def canonical_case_input(case: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -46,14 +45,12 @@ def _local_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
|
||||
|
||||
def _pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
utils = importlib.import_module("jhora.utils")
|
||||
charts = importlib.import_module("jhora.horoscope.chart.charts")
|
||||
drik = importlib.import_module("jhora.panchanga.drik")
|
||||
jd = utils.julian_day_number((case["year"], case["month"], case["day"]), (case["hour"], case["minute"], case.get("second", 0)))
|
||||
drik.set_ayanamsa_mode("LAHIRI", jd=jd)
|
||||
place = drik.Place("request-level", case["lat"], case["lon"], case["tz"])
|
||||
index_to_planet = {0: "Sun", 1: "Moon", 2: "Mars", 3: "Mercury", 4: "Jupiter", 5: "Venus", 6: "Saturn"}
|
||||
return {index_to_planet[body]: SIGNS[int(position[0])] for body, position in charts.rasi_chart(jd, place) if body in index_to_planet}
|
||||
"""PyJHora D1 signs. The jhora import stays in the reference module."""
|
||||
try:
|
||||
reference = importlib.import_module("pyjhora_rectification_d1_reference")
|
||||
except ModuleNotFoundError:
|
||||
reference = importlib.import_module("scripts.pyjhora_rectification_d1_reference")
|
||||
return reference.pyjhora_d1(case)
|
||||
|
||||
|
||||
def _ensure_jyotishganit_data_dir() -> str:
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze PyJHora 4.8.7 special-lagna longitudes. Offline only.
|
||||
|
||||
Run from the repo root, with a Python that has PyJHora 4.8.7 installed:
|
||||
|
||||
python scripts/research/freeze_special_lagnas_pyjhora_fixture.py
|
||||
|
||||
The runtime calculator does not import this module. Tests read the JSON.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / "scripts")]
|
||||
|
||||
import swisseph as swe # noqa: E402
|
||||
from jhora.horoscope.chart import charts # noqa: E402
|
||||
from jhora.panchanga import drik # noqa: E402
|
||||
from jhora.panchanga.drik import Place # noqa: E402
|
||||
|
||||
OUT = ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json"
|
||||
CASES = {
|
||||
"steve_jobs_1955_aa": {
|
||||
"source": "references/public_oracle_cases.json id steve_jobs_1955_aa",
|
||||
"birth": dict(year=1955, month=2, day=24, hour=19, minute=15, second=0, lat=37.7749, lon=-122.4194, tz=-8),
|
||||
},
|
||||
"barack_obama_1961_aa": {
|
||||
"source": "references/public_oracle_cases.json id barack_obama_1961_aa",
|
||||
"birth": dict(year=1961, month=8, day=4, hour=19, minute=24, second=0, lat=21.3069, lon=-157.8583, tz=-10),
|
||||
},
|
||||
"fictional_reader_main": {
|
||||
"source": "tests/test_report_reader_main.py FICTIONAL",
|
||||
"birth": dict(year=1991, month=4, day=7, hour=9, minute=15, second=0, lat=30.5728, lon=104.0668, tz=8.0),
|
||||
},
|
||||
}
|
||||
MODES = {"lahiri": "LAHIRI", "raman": "RAMAN"}
|
||||
FUNCS = (
|
||||
("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"),
|
||||
)
|
||||
SIGNS = [
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
]
|
||||
|
||||
|
||||
def _point(raw) -> dict:
|
||||
sign = int(raw[0]) % 12
|
||||
degree = float(raw[1]) % 30.0
|
||||
return {
|
||||
"sign": SIGNS[sign],
|
||||
"sign_index": sign,
|
||||
"sign_degree": degree,
|
||||
"longitude": sign * 30.0 + degree,
|
||||
}
|
||||
|
||||
|
||||
def _case(birth: dict, mode: str) -> dict:
|
||||
local_hour = birth["hour"] + birth["minute"] / 60.0 + birth["second"] / 3600.0
|
||||
with redirect_stdout(io.StringIO()):
|
||||
drik.set_ayanamsa_mode(mode)
|
||||
jd = swe.julday(birth["year"], birth["month"], birth["day"], local_hour)
|
||||
place = Place("birth_place", birth["lat"], birth["lon"], birth["tz"])
|
||||
points = {key: _point(getattr(drik, name)(jd, place)) for key, name in FUNCS}
|
||||
raw = charts.varnada_lagna(
|
||||
(birth["year"], birth["month"], birth["day"]),
|
||||
(birth["hour"], birth["minute"], birth["second"]),
|
||||
place,
|
||||
varnada_method=1,
|
||||
)
|
||||
points["Varnada_Lagna"] = _point(raw)
|
||||
return points
|
||||
|
||||
|
||||
def main() -> None:
|
||||
version = importlib.metadata.version("PyJHora")
|
||||
payload = {
|
||||
"pyjhora_version": version,
|
||||
"generator": "python scripts/research/freeze_special_lagnas_pyjhora_fixture.py",
|
||||
"generated_on": datetime.now().strftime("%Y-%m-%d"),
|
||||
"cases": {},
|
||||
}
|
||||
for case_id, spec in CASES.items():
|
||||
payload["cases"][case_id] = {
|
||||
"source": spec["source"],
|
||||
"birth": spec["birth"],
|
||||
"ayanamsa": {name: _case(spec["birth"], mode) for name, mode in MODES.items()},
|
||||
}
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -128,6 +128,8 @@ CORE_PYTEST_TARGETS = [
|
||||
"tests/test_vedastro_external_technique_evidence.py",
|
||||
# Self-hosted heading font slices: 6500-character coverage, no overlap, 120 KB cap.
|
||||
"tests/test_serif_font_slices.py",
|
||||
# Chart, report, and consult entries must not import PyJHora (BUG-1273).
|
||||
"tests/test_runtime_jhora_import_scan.py",
|
||||
]
|
||||
|
||||
RUNTIME_TRUTH_PYTEST_TARGETS = [
|
||||
|
||||
+9
-180
@@ -14,10 +14,6 @@ Special Lagnas Calculator - 特殊上升点计算器
|
||||
|
||||
from typing import Dict, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
|
||||
def jaimini_special_lagna_view(points):
|
||||
"""Project canonical birth points without recalculating approximate aliases."""
|
||||
aliases = {
|
||||
@@ -51,21 +47,6 @@ def bhava_house_rows(bhava):
|
||||
]
|
||||
|
||||
|
||||
def _pyjhora_ayanamsa_mode(name):
|
||||
key = str(name or '').strip().lower().replace('-', '_').replace(' ', '_')
|
||||
return {
|
||||
'lahiri': 'LAHIRI',
|
||||
'raman': 'RAMAN',
|
||||
'kp': 'KP',
|
||||
'krishnamurti': 'KP',
|
||||
'fagan_bradley': 'FAGAN',
|
||||
'fagan': 'FAGAN',
|
||||
'true_citra': 'TRUE_CITRA',
|
||||
'true_chitra': 'TRUE_CITRA',
|
||||
'true_pushya': 'TRUE_PUSHYA',
|
||||
}.get(key)
|
||||
|
||||
|
||||
class SpecialLagnasCalculator:
|
||||
"""特殊上升点计算器"""
|
||||
|
||||
@@ -108,7 +89,13 @@ class SpecialLagnasCalculator:
|
||||
# 由 full reading 用它覆盖这两个占位。
|
||||
result["Arudha_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
||||
result["Upapada_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
||||
pyjhora = self.calculate_pyjhora_special_lagnas(
|
||||
# Rectification keeps calculate_hora_lagna / calculate_ghati_lagna above.
|
||||
# With coordinates, the eight birth points come from Swiss Ephemeris.
|
||||
try:
|
||||
from native_special_lagnas import native_special_lagna_rows
|
||||
except ImportError:
|
||||
from scripts.native_special_lagnas import native_special_lagna_rows
|
||||
native = native_special_lagna_rows(
|
||||
birth_time=birth_time,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
@@ -118,169 +105,11 @@ class SpecialLagnasCalculator:
|
||||
)
|
||||
if lat is None or lon is None or tz_offset is None:
|
||||
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
||||
result[key] = pyjhora[key]
|
||||
result[key] = native[key]
|
||||
else:
|
||||
result.update(pyjhora)
|
||||
result.update(native)
|
||||
return result
|
||||
|
||||
def _format_special_lagna_payload(self, label: str, raw, asc_degree: float, method: str) -> Dict:
|
||||
if isinstance(raw, dict):
|
||||
return raw
|
||||
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
|
||||
sign_idx = int(raw[0]) % 12
|
||||
degree = float(raw[1]) % 30
|
||||
longitude = sign_idx * 30 + degree
|
||||
else:
|
||||
longitude = float(raw) % 360
|
||||
sign_idx = int(longitude // 30) % 12
|
||||
degree = longitude % 30
|
||||
return {
|
||||
"full_name": label,
|
||||
"degree": round(longitude, 4),
|
||||
"longitude": round(longitude, 4),
|
||||
"sign": self.SIGNS[sign_idx],
|
||||
"sign_degree": round(degree, 4),
|
||||
"house": ((sign_idx - int(asc_degree // 30)) % 12) + 1,
|
||||
"method": method,
|
||||
"source": "pyjhora_drik",
|
||||
"status": "computed",
|
||||
}
|
||||
|
||||
def calculate_pyjhora_special_lagnas(
|
||||
self,
|
||||
birth_time: datetime,
|
||||
lat: float | None,
|
||||
lon: float | None,
|
||||
tz_offset: float | None,
|
||||
asc_degree: float,
|
||||
ayanamsa: str | None = None,
|
||||
) -> Dict[str, Dict]:
|
||||
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"),
|
||||
)
|
||||
blocked = {
|
||||
key: {"full_name": full_name, "status": "blocked", "source": "pyjhora_drik"}
|
||||
for key, full_name in names
|
||||
}
|
||||
if lat is None or lon is None or tz_offset is None:
|
||||
for row in blocked.values():
|
||||
row["reason"] = "birth_coordinates_or_timezone_missing"
|
||||
return blocked
|
||||
producer = (
|
||||
("Bhava_Lagna", "bhava_lagna", "Bhava Lagna"),
|
||||
("Hora_Lagna", "hora_lagna", "Hora Lagna"),
|
||||
("Ghati_Lagna", "ghati_lagna", "Ghati Lagna"),
|
||||
("ViGhati_Lagna", "vighati_lagna", "Vighati Lagna"),
|
||||
("Sree_Lagna", "sree_lagna", "Sree Lagna"),
|
||||
("Indu_Lagna", "indu_lagna", "Indu Lagna"),
|
||||
("Pranapada_Lagna", "pranapada_lagna", "Pranapada Lagna"),
|
||||
)
|
||||
try:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
import swisseph as swe
|
||||
from jhora import const as jhora_const
|
||||
from jhora.panchanga import drik
|
||||
from jhora.panchanga.drik import Place
|
||||
except Exception as exc:
|
||||
for row in blocked.values():
|
||||
row["reason"] = f"pyjhora_unavailable:{exc}"
|
||||
return blocked
|
||||
|
||||
mode = None
|
||||
if ayanamsa is not None:
|
||||
mode = _pyjhora_ayanamsa_mode(ayanamsa)
|
||||
if mode is None:
|
||||
for row in blocked.values():
|
||||
row["reason"] = f"pyjhora_ayanamsa_unsupported:{ayanamsa}"
|
||||
return blocked
|
||||
|
||||
previous_pyjhora = getattr(jhora_const, "_DEFAULT_AYANAMSA_MODE", None)
|
||||
previous_swe = None
|
||||
apply_ayanamsa = None
|
||||
try:
|
||||
from ayanamsa_utils import ACTIVE_AYANAMSA_NAME, apply_ayanamsa as _apply
|
||||
previous_swe = ACTIVE_AYANAMSA_NAME
|
||||
apply_ayanamsa = _apply
|
||||
except Exception:
|
||||
apply_ayanamsa = None
|
||||
|
||||
results = {}
|
||||
try:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
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)
|
||||
place = Place("birth_place", lat, lon, tz_offset)
|
||||
if mode is not None:
|
||||
drik.set_ayanamsa_mode(mode)
|
||||
for output_key, func_name, full_name in producer:
|
||||
func = getattr(drik, func_name, None)
|
||||
if func is None:
|
||||
results[output_key] = {
|
||||
"full_name": full_name,
|
||||
"status": "blocked",
|
||||
"source": "pyjhora_drik",
|
||||
"reason": f"pyjhora_missing_function:{func_name}",
|
||||
}
|
||||
continue
|
||||
try:
|
||||
raw = func(jd_local, place)
|
||||
results[output_key] = self._format_special_lagna_payload(
|
||||
full_name, raw, asc_degree, method=f"PyJHora drik.{func_name}",
|
||||
)
|
||||
except Exception as exc:
|
||||
results[output_key] = {
|
||||
"full_name": full_name,
|
||||
"status": "blocked",
|
||||
"source": "pyjhora_drik",
|
||||
"reason": str(exc),
|
||||
}
|
||||
try:
|
||||
from jhora.horoscope.chart import charts
|
||||
if not hasattr(charts, "varnada_lagna"):
|
||||
raise AttributeError("pyjhora_missing_function:varnada_lagna")
|
||||
raw = charts.varnada_lagna(
|
||||
(birth_time.year, birth_time.month, birth_time.day),
|
||||
(birth_time.hour, birth_time.minute, birth_time.second),
|
||||
place,
|
||||
varnada_method=1,
|
||||
)
|
||||
results["Varnada_Lagna"] = self._format_special_lagna_payload(
|
||||
"Varnada Lagna", raw, asc_degree, "PyJHora varnada method 1",
|
||||
)
|
||||
except Exception as exc:
|
||||
reason = str(exc) or "pyjhora_missing_function:varnada_lagna"
|
||||
results["Varnada_Lagna"] = {
|
||||
"full_name": "Varnada Lagna",
|
||||
"status": "blocked",
|
||||
"source": "pyjhora_drik",
|
||||
"reason": reason,
|
||||
}
|
||||
if ayanamsa is not None:
|
||||
for row in results.values():
|
||||
row["ayanamsa"] = ayanamsa
|
||||
finally:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
if previous_pyjhora:
|
||||
try:
|
||||
drik.set_ayanamsa_mode(previous_pyjhora)
|
||||
except Exception:
|
||||
jhora_const._DEFAULT_AYANAMSA_MODE = previous_pyjhora
|
||||
if apply_ayanamsa is not None and previous_swe:
|
||||
apply_ayanamsa(previous_swe)
|
||||
return results
|
||||
|
||||
def calculate_bhava_lagna(self, asc_degree: float, sun_degree: float,
|
||||
moon_degree: float) -> Dict:
|
||||
"""
|
||||
|
||||
+15
-15
@@ -15,6 +15,7 @@ Tajika/Varshaphala年运盘模块 v1.0
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import importlib
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
@@ -1403,21 +1404,20 @@ def _external_pyjhora_panchavargiya_score(
|
||||
if annual_jd is None or lat is None or lon is None or tz is None:
|
||||
return None
|
||||
try:
|
||||
from jhora.panchanga import drik as j_drik
|
||||
from jhora.horoscope.chart import strength as j_strength
|
||||
place = j_drik.Place('annual_year_lord_probe', float(lat), float(lon), float(tz))
|
||||
# PyJHora annual-chart APIs use the local civil JD for a Place whose
|
||||
# timezone is already supplied. ``annual_jd`` reaches us as UT.
|
||||
scores = j_strength.pancha_vargeeya_bala(float(annual_jd) + float(tz) / 24.0, place)
|
||||
planet_idx = PLANET_TO_INDEX.get(planet)
|
||||
if planet_idx is None:
|
||||
return None
|
||||
score = scores[planet_idx]
|
||||
return {
|
||||
'score': float(score),
|
||||
'engine': 'pyjhora',
|
||||
'components': {'pyjhora_pancha_vargeeya_bala': float(score)},
|
||||
}
|
||||
# The in-process reference engine lives in a PyJHora adapter module.
|
||||
# This file does not import it unless a caller actually asks for a score.
|
||||
try:
|
||||
reference = importlib.import_module("pyjhora_panchavargiya_reference")
|
||||
except ModuleNotFoundError:
|
||||
reference = importlib.import_module("scripts.pyjhora_panchavargiya_reference")
|
||||
return reference.external_panchavargiya_score(
|
||||
planet,
|
||||
annual_jd=float(annual_jd),
|
||||
lat=float(lat),
|
||||
lon=float(lon),
|
||||
tz=float(tz),
|
||||
planet_index=PLANET_TO_INDEX.get(planet),
|
||||
)
|
||||
except Exception:
|
||||
return _external_pyjhora_panchavargiya_score_sidecar(planet, annual_jd, lat, lon, tz, ayanamsa_name)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user