feat(tajika): year lord compared by the Tajika five-fold Panchavargiya (BUG-1217)
Ports _calc_native_panchavargiya_bala and its helpers verbatim from upstream yinduzhanxing c7117f92 (astrologer round-5 Q5). calc_panchadhikari_year_lord now scores candidates with it (engine tajika_native_five_fold); the D1/D2/D3/D9/D12 dignity proxy no longer selects a year lord, and a missing annual planet blocks instead of falling back to it. Basis note reads 「按 Tajika 五分力量」 / "by Tajika five-fold strength". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9d81f15afa
commit
1c39988b3c
@@ -153,6 +153,8 @@ _PHRASES: dict[str, str] = {
|
||||
# BUG-1214: tajika_year_lord.selection_basis_text (zh) → its en twin.
|
||||
"年盘不全,暂用 Muntha 主星": "Annual chart incomplete; Muntha sign lord used for now",
|
||||
"(Panchavargiya 为本地近似计算)": " (Panchavargiya is a local approximation)",
|
||||
# BUG-1217: tajika_year_lord.ENGINE_NOTE["tajika_native_five_fold"]
|
||||
"(按 Tajika 五分力量)": " (by Tajika five-fold strength)",
|
||||
"(Panchavargiya 取 PyJHora 外部参照)": " (Panchavargiya from the PyJHora reference engine)",
|
||||
"两种计算口径给出的年主不同,暂不合并": "The two methods give different year lords; not merged",
|
||||
"(两者不同)": " (they differ)",
|
||||
|
||||
+179
-5
@@ -14,6 +14,7 @@ Tajika/Varshaphala年运盘模块 v1.0
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
@@ -211,6 +212,13 @@ def calc_panchadhikari_year_lord(
|
||||
('tri_rasi_lord', TRI_RASI_DAYTIME_LORDS[annual_asc_sign_idx] if is_daytime else TRI_RASI_NIGHTTIME_LORDS[annual_asc_sign_idx]),
|
||||
]
|
||||
|
||||
# Local divergence (BUG-1217, astrologer round-5 Q5): candidates are
|
||||
# compared by the Tajika five-fold Panchavargiya (upstream
|
||||
# _calc_native_panchavargiya_bala), not the D1/D2/D3/D9/D12 dignity proxy.
|
||||
# Upstream c7117f92 still falls back to the proxy here (upstream bug).
|
||||
native_scores = _calc_native_panchavargiya_bala(
|
||||
{planet: planet_lons[planet] for planet in CLASSICAL_PLANETS if planet in planet_lons}
|
||||
)
|
||||
candidates = []
|
||||
seen = set()
|
||||
for source, planet in raw_candidates:
|
||||
@@ -226,7 +234,11 @@ def calc_panchadhikari_year_lord(
|
||||
lon=lon,
|
||||
tz=tz,
|
||||
ayanamsa_name=ayanamsa_name,
|
||||
native_scores=native_scores,
|
||||
)
|
||||
if pvb is None:
|
||||
return {'status': 'blocked', 'reason': 'tajika_five_fold_panchavargiya_unavailable',
|
||||
'detail': (native_scores.get(planet) or {}).get('reason')}
|
||||
candidates.append(
|
||||
{
|
||||
'source': source,
|
||||
@@ -236,6 +248,7 @@ def calc_panchadhikari_year_lord(
|
||||
'benefic_aspects_asc': annual_asc_sign_idx in _benefic_aspect_targets(planet_house),
|
||||
'malefic_aspects_asc': annual_asc_sign_idx in _malefic_aspect_targets(planet_house),
|
||||
'panchavargiya_score': pvb['score'],
|
||||
'panchavargiya_raw_total': pvb.get('raw_total'),
|
||||
'panchavargiya_engine': pvb['engine'],
|
||||
'panchavargiya_components': pvb['components'],
|
||||
}
|
||||
@@ -626,6 +639,154 @@ def calc_tajika_strength_layers(
|
||||
}
|
||||
|
||||
|
||||
# Ported verbatim from upstream yinduzhanxing c7117f92 (BUG-1217): Tajika five-fold
|
||||
# Panchavargiya Bala (Kshetra/graha, Uchcha, Hadda, Drekkana, Navamsa; raw /80, score /20),
|
||||
# rules in references/tajika_panchavargiya_rules.json. Astrologer round-5 Q5.
|
||||
def _tajika_relation(planet: str, lord: str, annual_signs: Dict[str, int]) -> Dict:
|
||||
"""Return the Tajika aspect relation used by Panchavargeeya Bala.
|
||||
|
||||
Tajika uses houses from the lord's annual-chart position to the planet's
|
||||
annual-chart position: 3/5/9/11 friendly, 2/6/8/12 neutral, and
|
||||
1/4/7/10 inimical. This intentionally does not use natural friendship or
|
||||
Parashari Panchadha Maitri.
|
||||
"""
|
||||
if planet == lord:
|
||||
return {'relation': 'own', 'house_from_lord': None}
|
||||
house = ((annual_signs[planet] - annual_signs[lord]) % 12) + 1
|
||||
relation_rules = _PANCHAVARGIYA_RULES['tajika_relation']
|
||||
if house in relation_rules['friendly_houses']:
|
||||
relation = 'friendly'
|
||||
elif house in relation_rules['neutral_houses']:
|
||||
relation = 'neutral'
|
||||
else:
|
||||
relation = 'inimical'
|
||||
return {'relation': relation, 'house_from_lord': house}
|
||||
|
||||
|
||||
def _panchavargiya_component(
|
||||
planet: str,
|
||||
lord: str,
|
||||
annual_signs: Dict[str, int],
|
||||
points: Dict[str, float],
|
||||
*,
|
||||
subdivision: str,
|
||||
) -> Dict:
|
||||
trace = _tajika_relation(planet, lord, annual_signs)
|
||||
return {
|
||||
'score': float(points[trace['relation']]),
|
||||
'lord': lord,
|
||||
'relation': trace['relation'],
|
||||
'house_from_lord': trace['house_from_lord'],
|
||||
'subdivision': subdivision,
|
||||
}
|
||||
|
||||
|
||||
def _hudda_lord(lon: float) -> str:
|
||||
sign_idx = _sign_idx_from_lon(lon)
|
||||
degree = float(lon) % 30.0
|
||||
for boundary, lord in _PANCHAVARGIYA_RULES['hudda'][sign_idx]:
|
||||
if degree < float(boundary) or math.isclose(degree, float(boundary)) and float(boundary) == 30.0:
|
||||
return lord
|
||||
raise ValueError(f'No Tajika Hudda lord for longitude {lon}')
|
||||
|
||||
|
||||
def _tajika_drekkana_lord(lon: float) -> str:
|
||||
sign_idx = _sign_idx_from_lon(lon)
|
||||
division = min(2, int((float(lon) % 30.0) / 10.0))
|
||||
return _PANCHAVARGIYA_RULES['drekkana_lords'][sign_idx][division]
|
||||
|
||||
|
||||
def _navamsha_sign_idx(lon: float) -> int:
|
||||
sign_idx = _sign_idx_from_lon(lon)
|
||||
division = min(8, int((float(lon) % 30.0) / (30.0 / 9.0)))
|
||||
if sign_idx in (0, 3, 6, 9):
|
||||
start = sign_idx
|
||||
elif sign_idx in (1, 4, 7, 10):
|
||||
start = (sign_idx + 8) % 12
|
||||
else:
|
||||
start = (sign_idx + 4) % 12
|
||||
return (start + division) % 12
|
||||
|
||||
|
||||
def _tajika_uchcha_bala(planet: str, lon: float) -> float:
|
||||
deep_debilitation = {
|
||||
'Sun': 190.0, 'Moon': 213.0, 'Mars': 118.0, 'Mercury': 345.0,
|
||||
'Jupiter': 275.0, 'Venus': 177.0, 'Saturn': 20.0,
|
||||
}[planet]
|
||||
distance = (float(lon) % 360.0 - deep_debilitation) % 360.0
|
||||
if distance > 180.0:
|
||||
distance = 360.0 - distance
|
||||
return _round_half_up(distance / 9.0)
|
||||
|
||||
|
||||
def _round_half_up(value: float, places: int = 2) -> float:
|
||||
quantizer = Decimal('1').scaleb(-places)
|
||||
return float(Decimal(str(value)).quantize(quantizer, rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _calc_native_panchavargiya_bala(planet_lons: Dict[str, float]) -> Dict[str, Dict]:
|
||||
annual_signs = {planet: _sign_idx_from_lon(lon) for planet, lon in planet_lons.items()}
|
||||
required = set(CLASSICAL_PLANETS)
|
||||
if not required.issubset(annual_signs):
|
||||
missing = sorted(required - set(annual_signs))
|
||||
return {
|
||||
planet: {'status': 'blocked', 'reason': f'annual_planet_positions_missing:{",".join(missing)}'}
|
||||
for planet in planet_lons
|
||||
}
|
||||
result = {}
|
||||
for planet, longitude in planet_lons.items():
|
||||
sign_lord = SIGN_LORDS[SIGNS[annual_signs[planet]]]
|
||||
graha = _panchavargiya_component(
|
||||
planet, sign_lord, annual_signs,
|
||||
{'own': 30.0, 'friendly': 22.5, 'neutral': 15.0, 'inimical': 7.5},
|
||||
subdivision='rasi',
|
||||
)
|
||||
hudda = _panchavargiya_component(
|
||||
planet, _hudda_lord(longitude), annual_signs,
|
||||
{'own': 15.0, 'friendly': 11.25, 'neutral': 7.5, 'inimical': 3.75},
|
||||
subdivision='hudda',
|
||||
)
|
||||
drekkana = _panchavargiya_component(
|
||||
planet, _tajika_drekkana_lord(longitude), annual_signs,
|
||||
{'own': 10.0, 'friendly': 7.5, 'neutral': 5.0, 'inimical': 2.5},
|
||||
subdivision='tajika_drekkana',
|
||||
)
|
||||
navamsha_sign = _navamsha_sign_idx(longitude)
|
||||
navamsha = _panchavargiya_component(
|
||||
planet, SIGN_LORDS[SIGNS[navamsha_sign]], annual_signs,
|
||||
{'own': 5.0, 'friendly': 3.75, 'neutral': 2.5, 'inimical': 1.25},
|
||||
subdivision='navamsha',
|
||||
)
|
||||
components = {
|
||||
'graha_bala': graha['score'],
|
||||
'uchcha_bala': _tajika_uchcha_bala(planet, longitude),
|
||||
'hudda_bala': hudda['score'],
|
||||
'drekkana_bala': drekkana['score'],
|
||||
'navamsha_bala': navamsha['score'],
|
||||
}
|
||||
raw_total = _round_half_up(sum(components.values()))
|
||||
result[planet] = {
|
||||
'status': 'native_rule_backed',
|
||||
'engine': 'native_tajika_panchavargiya',
|
||||
'score': _round_half_up(raw_total / 4.0),
|
||||
'raw_total': raw_total,
|
||||
'components': components,
|
||||
'component_trace': {
|
||||
'graha_bala': graha,
|
||||
'hudda_bala': hudda,
|
||||
'drekkana_bala': drekkana,
|
||||
'navamsha_bala': {**navamsha, 'navamsha_sign': SIGNS[navamsha_sign]},
|
||||
'uchcha_bala': {'score': components['uchcha_bala'], 'formula': 'shortest_arc_from_deep_debilitation_divided_by_9'},
|
||||
},
|
||||
'component_total_consistency': {
|
||||
'status': 'consistent', 'formula': 'sum(Graha, Uchcha, Hudda, Drekkana, Navamsha) / 4',
|
||||
'raw_total': raw_total, 'score': _round_half_up(raw_total / 4.0),
|
||||
},
|
||||
'rule_source': 'references/tajika_panchavargiya_rules.json',
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _is_number(value) -> bool:
|
||||
try:
|
||||
float(value)
|
||||
@@ -1215,7 +1376,8 @@ def _resolve_year_lord_panchavargiya_score(
|
||||
lon: Optional[float],
|
||||
tz: Optional[float],
|
||||
ayanamsa_name: Optional[str],
|
||||
) -> Dict:
|
||||
native_scores: Optional[Dict[str, Dict]] = None,
|
||||
) -> Optional[Dict]:
|
||||
external = _external_pyjhora_panchavargiya_score(
|
||||
planet,
|
||||
annual_jd=annual_jd,
|
||||
@@ -1226,11 +1388,23 @@ def _resolve_year_lord_panchavargiya_score(
|
||||
)
|
||||
if external is not None:
|
||||
return external
|
||||
proxy = _calc_panchavargiya_bala_for_planet(planet, lon_value)
|
||||
if native_scores is None:
|
||||
# Upstream c7117f92 behaviour (dignity proxy); no product caller.
|
||||
proxy = _calc_panchavargiya_bala_for_planet(planet, lon_value)
|
||||
return {
|
||||
'score': proxy['score'],
|
||||
'engine': 'native_proxy',
|
||||
'components': proxy['components'],
|
||||
}
|
||||
# BUG-1217: Tajika five-fold Panchavargiya; None when it cannot be computed.
|
||||
native = native_scores.get(planet) or {}
|
||||
if native.get('status') != 'native_rule_backed':
|
||||
return None
|
||||
return {
|
||||
'score': proxy['score'],
|
||||
'engine': 'native_proxy',
|
||||
'components': proxy['components'],
|
||||
'score': native['score'],
|
||||
'engine': 'tajika_native_five_fold',
|
||||
'raw_total': native['raw_total'],
|
||||
'components': native['components'],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,14 +14,18 @@ producers (`solar_return.solar_return_full_report` and
|
||||
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).
|
||||
Panchavargiya engine (local divergence from upstream, BUG-1214 / BUG-1217):
|
||||
upstream hands the annual JD to the selector, which then imports PyJHora
|
||||
in-process when the interpreter has it, else falls back to a D1/D2/D3/D9/D12
|
||||
dignity proxy. PyJHora is AGPL and stays an external reference engine
|
||||
(requirements-reference-engines.txt); the API image and CI do not have it, so
|
||||
the product path never hands it the JD. Since BUG-1217 (astrologer round-5
|
||||
Q5) the candidates are compared by the Tajika five-fold Panchavargiya
|
||||
(upstream `_calc_native_panchavargiya_bala`: Kshetra/graha, Uchcha, Hadda,
|
||||
Drekkana, Navamsa; raw out of 80, score = raw / 4 out of 20;
|
||||
`engine: tajika_native_five_fold`); the dignity proxy no longer selects a year
|
||||
lord. 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
|
||||
@@ -42,6 +46,7 @@ MUNTHA_BASIS = "natal_ascendant_plus_completed_years"
|
||||
FALLBACK_BASIS = "fallback_muntha_sign_lord_after_panchadhikari_block"
|
||||
|
||||
ENGINE_NOTE = {
|
||||
"tajika_native_five_fold": ("按 Tajika 五分力量", "by Tajika five-fold strength"),
|
||||
"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"),
|
||||
@@ -94,6 +99,7 @@ def _candidate_list(candidates: List[Dict[str, Any]], winner: Optional[str]) ->
|
||||
"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_raw_total": row.get("panchavargiya_raw_total"),
|
||||
"panchavargiya_engine": row.get("panchavargiya_engine"),
|
||||
"selected": row.get("planet") == winner,
|
||||
} for row in candidates if isinstance(row, dict)]
|
||||
|
||||
Reference in New Issue
Block a user