Files
Jyotisha/scripts/tajika_year_lord.py
T
Jesse_ChenandClaude Opus 5.5 0afcc2d9a3 feat(tajika): year lord by the Panchadhikari rule, ported from upstream c7117f92 (BUG-1214)
Round-4 astrologer ruling Q3. tajika.calc_panchadhikari_year_lord and its
missing pieces (Tri-Rasi tables, planet index maps, ayanamsa-aware PyJHora
lookups) plus references/tajika_panchavargiya_rules.json (blob e8c15ac3) are
ported verbatim from yinduzhanxing c7117f92, with one keyword added
(muntha_sign_idx): upstream builds the Muntha-lord candidate from the annual
Lagna + years; ours stays natal Lagna + completed years (BUG-1212).

Both annual producers (solar_return_full_report, cmd_tajika) call the new
tajika_year_lord.select_annual_year_lord, so the pack conflict gate compares
the same rule. Output carries candidates / candidate_list with Panchavargiya
score and engine, aspect flags, selection_basis and a one-sentence basis
(zh/en). The product path always uses the native proxy (PyJHora is AGPL, absent
from the API image and CI); JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1 restores the
upstream PyJHora lookup for research. Card varshesha_basis and the report
'Selection basis' rows show the sentence; the BUG-1212 stand-in label is gone.
Mudda Dasha and Tri-Pataka follow the selected year lord.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 16:30:28 +08:00

154 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Tajika year lord (Varshesha) by the Panchadhikari rule (BUG-1214).
Astrologer round-4 ruling 2026-10-03, Q3: five candidates (day/night new-year
lord, natal Lagna lord, Muntha lord, annual Lagna lord, Tri-Rasi lord) ->
those casting a benefic Tajika aspect on the annual Lagna, strongest by
Panchavargiya -> else those casting a malefic aspect, strongest -> else the
strongest of all (`panchadhikari_pvb_fallback_all_candidates`).
The selection itself is `tajika.calc_panchadhikari_year_lord`, ported from
upstream yinduzhanxing c7117f92. This module is the one place both annual
producers (`solar_return.solar_return_full_report` and
`jyotish_engine.cmd_tajika`) call, so `annual_tajika_pack`'s conflict gate
compares two runs of the same rule. Muntha stays natal Lagna + completed
years (BUG-1212); it is handed to the selector explicitly.
Panchavargiya engine (local divergence from upstream, BUG-1214): upstream
hands the annual JD to the selector, which then imports PyJHora in-process
when the interpreter has it. PyJHora is AGPL and stays an external reference
engine (requirements-reference-engines.txt); the API image and CI do not have
it, so a developer machine that does would pick a different year lord than
production. The product path therefore always uses the native proxy
(`engine: native_proxy`); set JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1 to pass the
annual JD and let the upstream PyJHora lookup run (research comparison only).
"""
from __future__ import annotations
import contextlib
import io
import os
from typing import Any, Dict, List, Optional, Tuple
try: # scripts/ on sys.path
from tajika import SIGNS, SIGN_LORDS, calc_panchadhikari_year_lord
except ImportError: # pragma: no cover - package import
from scripts.tajika import SIGNS, SIGN_LORDS, calc_panchadhikari_year_lord
PYJHORA_REFERENCE_ENV = "JYOTISH_YEAR_LORD_PYJHORA_REFERENCE"
SOURCE = "tajika.calc_panchadhikari_year_lord (upstream yinduzhanxing c7117f92)"
MUNTHA_BASIS = "natal_ascendant_plus_completed_years"
FALLBACK_BASIS = "fallback_muntha_sign_lord_after_panchadhikari_block"
ENGINE_NOTE = {
"native_proxy": ("Panchavargiya 为本地近似计算", "Panchavargiya is a local approximation"),
"pyjhora": ("Panchavargiya 取 PyJHora 外部参照", "Panchavargiya from the PyJHora reference engine"),
"pyjhora_sidecar": ("Panchavargiya 取 PyJHora 外部参照", "Panchavargiya from the PyJHora reference engine"),
}
def _names(rows: List[Dict[str, Any]], sep: str) -> str:
return sep.join(str(row.get("planet")) for row in rows)
def selection_basis_text(data: Dict[str, Any]) -> Tuple[str, str]:
"""One sentence (zh, en) saying why this planet is the year lord."""
basis = data.get("selection_basis")
lord = data.get("year_lord")
if basis == FALLBACK_BASIS:
return ("年盘不全,暂用 Muntha 主星", "Annual chart incomplete; Muntha sign lord used for now")
candidates = [row for row in data.get("candidates") or [] if isinstance(row, dict)]
benefic = [row for row in candidates if row.get("benefic_aspects_asc")]
malefic = [row for row in candidates if row.get("malefic_aspects_asc")]
if basis == "panchadhikari_single_benefic":
zh = f"五类候选中只有{lord}以吉相位照年盘上升,取为年主"
en = f"Of the five candidates only {lord} casts a benefic aspect on the annual Lagna, so it is the year lord"
elif basis == "panchadhikari_benefic_shortlist_pvb":
zh = f"{_names(benefic, '、')}以吉相位照年盘上升,取其中 Panchavargiya 最强的{lord}"
en = f"{_names(benefic, ', ')} cast a benefic aspect on the annual Lagna; the strongest by Panchavargiya, {lord}, is the year lord"
elif basis == "panchadhikari_single_malefic":
zh = f"没有候选以吉相位照年盘上升;只有{lord}以凶相位照上升,取为年主"
en = f"No candidate casts a benefic aspect on the annual Lagna; only {lord} casts a malefic one, so it is the year lord"
elif basis == "panchadhikari_malefic_shortlist_pvb":
zh = f"没有候选以吉相位照年盘上升;{_names(malefic, '、')}以凶相位照上升,取其中 Panchavargiya 最强的{lord}"
en = (f"No candidate casts a benefic aspect on the annual Lagna; {_names(malefic, ', ')} cast a malefic one; "
f"the strongest by Panchavargiya, {lord}, is the year lord")
elif basis == "panchadhikari_pvb_fallback_all_candidates":
zh = f"五类候选都不照年盘上升,取全体 Panchavargiya 最强的{lord}"
en = f"None of the five candidates aspects the annual Lagna; the strongest of all by Panchavargiya, {lord}, is the year lord"
else:
return ("-", "-")
note = ENGINE_NOTE.get(str(data.get("panchavargiya_engine")))
if note:
zh += f"({note[0]})"
en += f" ({note[1]})"
return zh, en
def _candidate_list(candidates: List[Dict[str, Any]], winner: Optional[str]) -> List[Dict[str, Any]]:
return [{
"source": row.get("source"),
"planet": row.get("planet"),
"planet_sign": row.get("planet_sign"),
"benefic_aspects_asc": bool(row.get("benefic_aspects_asc")),
"malefic_aspects_asc": bool(row.get("malefic_aspects_asc")),
"panchavargiya_score": row.get("panchavargiya_score"),
"panchavargiya_engine": row.get("panchavargiya_engine"),
"selected": row.get("planet") == winner,
} for row in candidates if isinstance(row, dict)]
def _select(sr, natal_asc_sign_idx, completed_years, muntha_sign_idx,
ayanamsa_name, birth_lat, birth_lon, birth_tz) -> Dict[str, Any]:
info = sr.get("solar_return") or {}
location = sr.get("annual_location") or {}
return calc_panchadhikari_year_lord(
sr.get("planet_lons") or {},
annual_asc_sign_idx=sr["asc_sign_idx"],
natal_lagna_sign_idx=int(natal_asc_sign_idx),
years_from_dob=int(completed_years),
annual_jd=info.get("jd_ut") if os.environ.get(PYJHORA_REFERENCE_ENV) == "1" else None,
annual_dt=info.get("dt_local") or info.get("dt_ut"),
lat=location.get("latitude", birth_lat),
lon=location.get("longitude", birth_lon),
tz=location.get("utc_offset", birth_tz),
ayanamsa_name=ayanamsa_name,
muntha_sign_idx=muntha_sign_idx,
)
def select_annual_year_lord(
sr: Dict[str, Any],
*,
natal_asc_sign_idx: int,
completed_years: int,
ayanamsa_name: Optional[str],
birth_lat: float,
birth_lon: float,
birth_tz: float,
) -> Dict[str, Any]:
"""Year lord for one solar-return chart (`solar_return.calc_solar_return_chart`)."""
muntha_sign_idx = (int(natal_asc_sign_idx) + int(completed_years)) % 12
# PyJHora (optional reference engine, absent from the API image) prints
# on import; keep stdout clean for the CLI JSON, as upstream cmd_tajika does.
with contextlib.redirect_stdout(io.StringIO()):
data = _select(sr, natal_asc_sign_idx, completed_years, muntha_sign_idx,
ayanamsa_name, birth_lat, birth_lon, birth_tz)
if data.get("status") != "partial_verified":
lord = SIGN_LORDS[SIGNS[muntha_sign_idx]]
data = {
"year_lord": lord,
"year_lord_sign_idx": muntha_sign_idx,
"year_lord_sign": SIGNS[muntha_sign_idx],
"selection_basis": FALLBACK_BASIS,
"fallback_reason": data.get("reason", "panchadhikari_blocked"),
}
data["muntha_basis"] = MUNTHA_BASIS
data["candidate_list"] = _candidate_list(data.get("candidates") or [], data.get("year_lord"))
data["source"] = SOURCE
zh, en = selection_basis_text(data)
data["selection_basis_text"] = zh
data["selection_basis_text_en"] = en
return data