fix(special-lagnas): compute eight birth points without PyJHora
Independent Staging Quality Gate / validate (push) Successful in 19m4s
Independent Staging Quality Gate / publish (push) Successful in 3m51s

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:
jesse-ux
2026-10-08 15:01:44 +08:00
parent fe833b3576
commit 795b8845c7
16 changed files with 1270 additions and 245 deletions
+1 -1
View File
@@ -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
+290
View File
@@ -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
}
+6 -9
View File
@@ -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()
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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)