完整数据版的 Yogi / Ava Yogi 以前因缺少模块被记成 blocked。改为读取本命日月黄经计算,空候选和空落入行星不再印一整行未判定。
Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
c1aa72fcbf
commit
1c60926a13
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-22 — 完整数据版写出 Jaimini Yogi 与 Ava Yogi 坐标
|
||||
|
||||
- 这一节以前因为找不到计算模块,每张盘都是 blocked。现在用本命太阳、月亮黄经按 93°20′ 和 186°40′ 算出两点,中英附录各有一张表。空的 Tajika 候选改成一句「本年没有候选」;Goel 星座强度里没有落入行星时写「无」。
|
||||
- Skill 版本不变(BUG-1420)。
|
||||
|
||||
## 2026-10-22 — 星盘大运右栏可以切到 Yogini
|
||||
|
||||
- 大运页左栏仍是 Vimshottari。右栏默认 Chara,可以切到 Yogini。点开当前大运能看到小运。Yogini 的小运下面写「小运未经外部软件核对。」
|
||||
|
||||
@@ -37275,3 +37275,19 @@
|
||||
- 相关记录:BUG-1416。issue #91。
|
||||
- 复发自:无(#91 A 段第 1 次打回的残留)。
|
||||
- 修复版本:分支 `codex/full-data-parity-1-fix2-20261022`。
|
||||
|
||||
## BUG-1420 | Yogi / Ava Yogi 每张盘都是 blocked
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-21
|
||||
- 最近更新:2026-10-22
|
||||
- 影响面:完整数据版「Jaimini Yogi 与 Ava Yogi」;`modules.jaimini.yogi_avayogi_local`。
|
||||
- 用户现象:这一节不出坐标。资料包里该点的状态一直是 blocked。
|
||||
- 触发条件:任意本命盘走 full-reading 组装 Jaimini。
|
||||
- 根因:`jyotish_engine` 执行 `from yogi_sphuta import build_yogi_sphuta_local`,仓库里没有这个模块。`ImportError` 被包成 `status: blocked`、`reason: yogi_sphuta_local_unavailable:ModuleNotFoundError`。计算本身只需要盘上已经算好的日、月黄经,不需要再排一次盘。
|
||||
- 修复:模块放进 `scripts/annual_native/yogi_sphuta.py`,读 `chart.planets.Sun/Moon.degree_raw`。导入失败写入 `report.errors` 并带上 `ImportError` 原文,不再记成 blocked。算出的点写入 `yogi_avayogi_local`,中英附录出表。
|
||||
- 验证:`tests/test_yogi_sphuta_native.py`(虚构盘日、月与 `degree_raw` 差 ≤ 1e-6°;公式 93°20′ / 186°40′)。`tests/test_full_data_parity_chapters.py` 三张虚构盘中英都有 Yogi 与 Ava Yogi,格子不是「未判定」。
|
||||
- 防复发:引擎源码不得再出现 `yogi_sphuta_local_unavailable`。缺经度时 reason 写 `sun_or_moon_longitude_missing`。
|
||||
- 相关记录:issue #91 T4。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/full-data-parity-1-20261021`。
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Native annual and special-point producers for issue #91 section B.
|
||||
|
||||
Longitudes come from this repository's chart functions. This package does not
|
||||
import PyJHora and does not use Moshier or apparent-position flags.
|
||||
"""
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Local Yogi/Ava Yogi Sphuta from a chart that is already computed.
|
||||
|
||||
The Sun and Moon longitudes are read from ``chart.planets.*.degree_raw``.
|
||||
This module does not call an ephemeris of its own.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from bisect import bisect_right
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
|
||||
SCHEMA = "jyotish.yogi_sphuta.local.v1"
|
||||
FORMULA_PROFILE = "pyjhora_yogi_sphuta_v1"
|
||||
SIGNS = (
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
)
|
||||
SIGN_LORDS = {
|
||||
"Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon",
|
||||
"Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars",
|
||||
"Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter",
|
||||
}
|
||||
NAKSHATRA_NAMES = (
|
||||
"Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra", "Punarvasu",
|
||||
"Pushya", "Ashlesha", "Magha", "Purva Phalguni", "Uttara Phalguni", "Hasta",
|
||||
"Chitra", "Swati", "Vishakha", "Anuradha", "Jyeshtha", "Mula", "Purva Ashadha",
|
||||
"Uttara Ashadha", "Shravana", "Dhanishta", "Shatabhisha", "Purva Bhadrapada",
|
||||
"Uttara Bhadrapada", "Revati",
|
||||
)
|
||||
NAKSHATRA_LORDS = (
|
||||
"Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury",
|
||||
"Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury",
|
||||
"Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury",
|
||||
)
|
||||
NAKSHATRA_BOUNDARIES = tuple(index * 360.0 / 27.0 for index in range(27))
|
||||
YOGI_OFFSET = 93.0 + 20.0 / 60.0
|
||||
AVA_OFFSET = 186.0 + 40.0 / 60.0
|
||||
|
||||
|
||||
def _longitude(value: Any) -> float | None:
|
||||
try:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
number = float(value)
|
||||
return number % 360.0 if math.isfinite(number) else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _sign_from_longitude(longitude: float) -> str:
|
||||
return SIGNS[int((float(longitude) % 360.0) // 30.0) % 12]
|
||||
|
||||
|
||||
def _house_from_longitude(longitude: float, ascendant_sign: str | None) -> int | None:
|
||||
if ascendant_sign not in SIGN_LORDS:
|
||||
return None
|
||||
asc_idx = SIGNS.index(ascendant_sign)
|
||||
sign_idx = int((float(longitude) % 360.0) // 30.0) % 12
|
||||
return ((sign_idx - asc_idx) % 12) + 1
|
||||
|
||||
|
||||
def _point(longitude: float, ascendant_sign: str | None = None) -> dict[str, Any]:
|
||||
value = float(longitude) % 360.0
|
||||
sign = _sign_from_longitude(value)
|
||||
nakshatra_index = bisect_right(NAKSHATRA_BOUNDARIES, value) - 1
|
||||
return {
|
||||
"longitude": round(value, 8),
|
||||
"sign": sign,
|
||||
"degree_in_sign": round(value % 30.0, 8),
|
||||
"nakshatra": NAKSHATRA_NAMES[nakshatra_index],
|
||||
"nakshatra_index": nakshatra_index,
|
||||
"nakshatra_lord": NAKSHATRA_LORDS[nakshatra_index],
|
||||
"sign_lord": SIGN_LORDS[sign],
|
||||
"house": _house_from_longitude(value, ascendant_sign),
|
||||
}
|
||||
|
||||
|
||||
def build_yogi_sphuta_local(chart: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Calculate local Yogi/Ava Yogi points without interpretive claims."""
|
||||
if not isinstance(chart, dict):
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"status": "blocked",
|
||||
"reason": "chart_payload_missing",
|
||||
"formula_profile": FORMULA_PROFILE,
|
||||
}
|
||||
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
||||
sun = planets.get("Sun") if isinstance(planets.get("Sun"), dict) else {}
|
||||
moon = planets.get("Moon") if isinstance(planets.get("Moon"), dict) else {}
|
||||
sun_lon = _longitude(sun.get("degree_raw", sun.get("lon", sun.get("degree"))))
|
||||
moon_lon = _longitude(moon.get("degree_raw", moon.get("lon", moon.get("degree"))))
|
||||
if sun_lon is None or moon_lon is None:
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"status": "blocked",
|
||||
"reason": "sun_or_moon_longitude_missing",
|
||||
"formula_profile": FORMULA_PROFILE,
|
||||
}
|
||||
ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
|
||||
ascendant_sign = ascendant.get("sign") if isinstance(ascendant.get("sign"), str) else None
|
||||
yogi_longitude = (sun_lon + moon_lon + YOGI_OFFSET) % 360.0
|
||||
ava_longitude = (yogi_longitude + AVA_OFFSET) % 360.0
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"status": "partial_verified",
|
||||
"formula_profile": FORMULA_PROFILE,
|
||||
"formula": "sidereal_sun_longitude + sidereal_moon_longitude + 93d20m; Ava Yogi = Yogi + 186d40m",
|
||||
"claim_boundary": (
|
||||
"Local Yogi/Ava Yogi Sphuta is a restricted supplementary point producer. "
|
||||
"It cannot create event, wealth, timing, remedy, or final-judgment claims by itself."
|
||||
),
|
||||
"source_fields": ["chart.planets.Sun.degree_raw", "chart.planets.Moon.degree_raw", "chart.ascendant.sign"],
|
||||
"source_longitudes": {"Sun": sun_lon, "Moon": moon_lon},
|
||||
"yogi": _point(yogi_longitude, ascendant_sign),
|
||||
"ava_yogi": _point(ava_longitude, ascendant_sign),
|
||||
}
|
||||
|
||||
|
||||
def build_luminary_sphuta_report(
|
||||
*,
|
||||
sun_longitude: float,
|
||||
moon_longitude: float,
|
||||
ayanamsa: str,
|
||||
ascendant_sign: str | None = None,
|
||||
) -> dict:
|
||||
"""Supplied sidereal longitudes; sum/difference plus the existing Yogi profile.
|
||||
|
||||
No ephemeris, period or event prediction is inferred from these points.
|
||||
"""
|
||||
try:
|
||||
from scripts.ayanamsa_utils import normalize_ayanamsa_name
|
||||
except ImportError:
|
||||
from ayanamsa_utils import normalize_ayanamsa_name
|
||||
if not isinstance(ayanamsa, str) or not ayanamsa.strip():
|
||||
raise ValueError("explicit ayanamsa required")
|
||||
mode = normalize_ayanamsa_name(ayanamsa)
|
||||
for number in (sun_longitude, moon_longitude):
|
||||
if (
|
||||
isinstance(number, bool)
|
||||
or not isinstance(number, (int, float))
|
||||
or not math.isfinite(number)
|
||||
or not 0 <= number < 360
|
||||
):
|
||||
raise ValueError("sidereal absolute longitudes must be finite in [0,360)")
|
||||
if ascendant_sign is not None and ascendant_sign not in SIGNS:
|
||||
raise ValueError("unknown ascendant sign")
|
||||
local = build_yogi_sphuta_local({
|
||||
"planets": {
|
||||
"Sun": {"degree_raw": sun_longitude},
|
||||
"Moon": {"degree_raw": moon_longitude},
|
||||
},
|
||||
"ascendant": {"sign": ascendant_sign},
|
||||
})
|
||||
points = {
|
||||
"tithi_sphuta": _point((moon_longitude - sun_longitude) % 360, ascendant_sign),
|
||||
"yoga_sphuta": _point((moon_longitude + sun_longitude) % 360, ascendant_sign),
|
||||
"yogi": local["yogi"],
|
||||
"ava_yogi": local["ava_yogi"],
|
||||
}
|
||||
evidence = {
|
||||
"schema": "jyotish.luminary_sphuta.v1",
|
||||
"status": "calculated",
|
||||
"variant_profile_id": "jh761_luminary_existing_yogi_v1",
|
||||
"tradition_profile": "jh761_luminary_pyjhora_yogi",
|
||||
"ayanamsa": mode,
|
||||
"points": points,
|
||||
"house_basis": "whole_sign" if ascendant_sign else "not_supplied",
|
||||
"duplicate_yogi": points["yogi"]["sign_lord"],
|
||||
"formulae": {
|
||||
"tithi_sphuta": "(Moon-Sun)%360",
|
||||
"yoga_sphuta": "(Moon+Sun)%360",
|
||||
"yogi": "(Moon+Sun+93d20m)%360",
|
||||
"ava_yogi": "(Yogi+186d40m)%360",
|
||||
},
|
||||
"sources": [
|
||||
"https://www.vedicastrologer.org/jh/update_7.61.htm",
|
||||
],
|
||||
"claim_boundary": (
|
||||
"Calculated sensitive points only; no Dasha, event dates or standalone outcome verdict. "
|
||||
"Supplied ayanamsa labels input provenance, not conversion."
|
||||
),
|
||||
}
|
||||
lines = [
|
||||
"# Luminary sphuta",
|
||||
"",
|
||||
evidence["claim_boundary"],
|
||||
"",
|
||||
"| Point | Longitude | Sign | Nakshatra | Lord |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for key, point in points.items():
|
||||
lines.append(
|
||||
f"| {key} | {point['longitude']} | {point['sign']} | {point['nakshatra']} | {point['nakshatra_lord']} |"
|
||||
)
|
||||
return {
|
||||
"status": "calculated",
|
||||
"customer_export_admitted": False,
|
||||
"evidence_appendix": [evidence],
|
||||
"exports": {"ai_evidence_bundle": deepcopy(evidence), "markdown": "\n".join(lines)},
|
||||
}
|
||||
|
||||
|
||||
def compare_yogi_sphuta_to_replay(
|
||||
local: dict[str, Any],
|
||||
replay: dict[str, Any],
|
||||
tolerance_deg: float = 0.05,
|
||||
) -> dict[str, Any]:
|
||||
"""Compare local points to an external replay without making multi-engine claims."""
|
||||
if not isinstance(local, dict) or local.get("status") != "partial_verified":
|
||||
return {"status": "blocked", "reason": "local_yogi_sphuta_unavailable"}
|
||||
if not isinstance(replay, dict) or replay.get("status") != "partial_verified":
|
||||
return {"status": "blocked", "reason": "external_yogi_replay_unavailable"}
|
||||
rows = []
|
||||
for field, key in (("Yogi", "yogi"), ("Ava Yogi", "ava_yogi")):
|
||||
local_point = local.get(key) if isinstance(local.get(key), dict) else {}
|
||||
replay_point = replay.get(key) if isinstance(replay.get(key), dict) else {}
|
||||
local_lon = _longitude(local_point.get("longitude"))
|
||||
replay_lon = _longitude(replay_point.get("longitude"))
|
||||
if local_lon is None or replay_lon is None:
|
||||
status = "blocked"
|
||||
delta = None
|
||||
else:
|
||||
delta = abs((local_lon - replay_lon + 180.0) % 360.0 - 180.0)
|
||||
status = "match" if delta <= tolerance_deg else "mismatch"
|
||||
rows.append({
|
||||
"field": field,
|
||||
"status": status,
|
||||
"local_longitude": local_lon,
|
||||
"external_longitude": replay_lon,
|
||||
"delta_deg": round(delta, 8) if delta is not None else None,
|
||||
})
|
||||
if all(row["status"] == "match" for row in rows):
|
||||
status = "partial_verified"
|
||||
elif any(row["status"] == "mismatch" for row in rows):
|
||||
status = "parameter_sensitive"
|
||||
else:
|
||||
status = "blocked"
|
||||
return {
|
||||
"schema": "jyotish.yogi_sphuta.local_external_comparison.v1",
|
||||
"status": status,
|
||||
"claim_boundary": "This is same-formula local-vs-replay alignment, not independent multi-engine parity.",
|
||||
"tolerance_deg": tolerance_deg,
|
||||
"rows": rows,
|
||||
}
|
||||
+45
-20
@@ -16240,15 +16240,32 @@ def cmd_full_reading(args):
|
||||
jaimini_result['special_lagnas'] = calc_special_lagnas(asc_idx, args.hour, args.minute + _arg_second(args) / 60.0)
|
||||
|
||||
# Restricted Yogi/Ava Yogi point producer: coordinates only, no verdict text.
|
||||
# ImportError is logged with the missing module. It is not stored as status=blocked.
|
||||
yogi_sphuta_mod = None
|
||||
try:
|
||||
from yogi_sphuta import build_yogi_sphuta_local
|
||||
jaimini_result['yogi_avayogi_local'] = build_yogi_sphuta_local(report.get('chart') or {})
|
||||
except Exception as yogi_local_exc:
|
||||
jaimini_result['yogi_avayogi_local'] = {
|
||||
'schema': 'jyotish.yogi_sphuta.local.v1',
|
||||
'status': 'blocked',
|
||||
'reason': f'yogi_sphuta_local_unavailable:{yogi_local_exc.__class__.__name__}',
|
||||
}
|
||||
try:
|
||||
from annual_native import yogi_sphuta as yogi_sphuta_mod
|
||||
except ImportError:
|
||||
from scripts.annual_native import yogi_sphuta as yogi_sphuta_mod
|
||||
except ImportError as yogi_import_exc:
|
||||
report['errors'].append(f"yogi_sphuta: ImportError: {yogi_import_exc}")
|
||||
if yogi_sphuta_mod is not None:
|
||||
try:
|
||||
jaimini_result['yogi_avayogi_local'] = yogi_sphuta_mod.build_yogi_sphuta_local(
|
||||
report.get('chart') or {}
|
||||
)
|
||||
except Exception as yogi_local_exc:
|
||||
report['errors'].append(
|
||||
f"yogi_sphuta: {yogi_local_exc.__class__.__name__}: {yogi_local_exc}"
|
||||
)
|
||||
jaimini_result['yogi_avayogi_local'] = {
|
||||
'schema': 'jyotish.yogi_sphuta.local.v1',
|
||||
'status': 'blocked',
|
||||
'reason': (
|
||||
f'yogi_sphuta_calculation_failed:'
|
||||
f'{yogi_local_exc.__class__.__name__}:{yogi_local_exc}'
|
||||
),
|
||||
}
|
||||
|
||||
# External raw replay: comparison support only, never a strict verdict input.
|
||||
try:
|
||||
@@ -16275,18 +16292,26 @@ def cmd_full_reading(args):
|
||||
'evidence_scope': 'pyjhora_behavior_only',
|
||||
'parity_status': 'not_multiengine_parity',
|
||||
}
|
||||
try:
|
||||
from yogi_sphuta import compare_yogi_sphuta_to_replay
|
||||
jaimini_result['yogi_avayogi_local_external_comparison'] = compare_yogi_sphuta_to_replay(
|
||||
jaimini_result.get('yogi_avayogi_local') or {},
|
||||
jaimini_result.get('yogi_avayogi_external_replay') or {},
|
||||
)
|
||||
except Exception as yogi_cmp_exc:
|
||||
jaimini_result['yogi_avayogi_local_external_comparison'] = {
|
||||
'schema': 'jyotish.yogi_sphuta.local_external_comparison.v1',
|
||||
'status': 'blocked',
|
||||
'reason': f'yogi_sphuta_comparison_unavailable:{yogi_cmp_exc.__class__.__name__}',
|
||||
}
|
||||
if yogi_sphuta_mod is not None:
|
||||
try:
|
||||
jaimini_result['yogi_avayogi_local_external_comparison'] = (
|
||||
yogi_sphuta_mod.compare_yogi_sphuta_to_replay(
|
||||
jaimini_result.get('yogi_avayogi_local') or {},
|
||||
jaimini_result.get('yogi_avayogi_external_replay') or {},
|
||||
)
|
||||
)
|
||||
except Exception as yogi_cmp_exc:
|
||||
report['errors'].append(
|
||||
f"yogi_sphuta comparison: {yogi_cmp_exc.__class__.__name__}: {yogi_cmp_exc}"
|
||||
)
|
||||
jaimini_result['yogi_avayogi_local_external_comparison'] = {
|
||||
'schema': 'jyotish.yogi_sphuta.local_external_comparison.v1',
|
||||
'status': 'blocked',
|
||||
'reason': (
|
||||
f'yogi_sphuta_comparison_failed:'
|
||||
f'{yogi_cmp_exc.__class__.__name__}:{yogi_cmp_exc}'
|
||||
),
|
||||
}
|
||||
|
||||
# Darakaraka 深度解读(v6.1.10)
|
||||
# Registry 已将 modules.jaimini.darakaraka 标为 covered;这里把独立 DK
|
||||
|
||||
@@ -205,6 +205,7 @@ SECTIONS: tuple[tuple[str, str], ...] = (
|
||||
("Arudha / UL / A10", "Arudha、Upapada 与 A10"),
|
||||
("Jaimini Calculation Details", "Jaimini 计算细节"),
|
||||
("Graha Pada", "Graha Pada"),
|
||||
("Jaimini Yogi and Ava Yogi", "Jaimini Yogi 与 Ava Yogi"),
|
||||
("Karakamsha", "Karakamsha"),
|
||||
("Darakaraka", "Darakaraka"),
|
||||
("Rashi Tulya Navamsa", "Rashi Tulya Navamsa"),
|
||||
@@ -228,7 +229,6 @@ FORBIDDEN_TITLES = (
|
||||
"Tripataki Chakra Detail",
|
||||
"Annual Saham Formula Trace",
|
||||
"Natal Saham Formula Trace",
|
||||
"Jaimini Yogi and Ava Yogi",
|
||||
"Annual Chart Numeric Data",
|
||||
"Bhava Bala Relative Score Components",
|
||||
"Score Extremes",
|
||||
@@ -884,6 +884,8 @@ def _tajika_candidates(packet: dict, language: str) -> str:
|
||||
"residual": item.get("residual"),
|
||||
"status": item.get("present") if "present" in item else item.get("motion") or item.get("status"),
|
||||
})
|
||||
if not rows:
|
||||
return "本年没有候选" if language == "zh" else "No candidates this year"
|
||||
return _rows_from(rows, language, [("name", "text"), ("planet", "planets"), ("aspect", "text"), ("residual", "text"), ("status", "text")])
|
||||
|
||||
|
||||
@@ -1148,11 +1150,18 @@ def _goel(packet: dict, language: str) -> str:
|
||||
for item in strength.get("rashi_strengths") or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
occupants = item.get("occupants")
|
||||
if isinstance(occupants, list) and not occupants:
|
||||
occupant_cell = "无" if language == "zh" else "None"
|
||||
elif isinstance(occupants, list):
|
||||
occupant_cell = planet_list(occupants, language) or ("无" if language == "zh" else "None")
|
||||
else:
|
||||
occupant_cell = occupants
|
||||
rashi_rows.append({
|
||||
"sign": item.get("sign"),
|
||||
"lord": item.get("lord"),
|
||||
"permanent": item.get("permanent"),
|
||||
"occupant": item.get("occupants"),
|
||||
"occupant": occupant_cell,
|
||||
"total": item.get("total"),
|
||||
"example_total": example_by_sign.get(item.get("sign")),
|
||||
})
|
||||
@@ -1171,7 +1180,7 @@ def _goel(packet: dict, language: str) -> str:
|
||||
citation,
|
||||
intro,
|
||||
_rows_from(planet_rows, language, [("planet", "planet"), ("sign", "sign"), ("degree", "text"), ("dignity", "text"), ("total", "text"), ("example_total", "text")]),
|
||||
_rows_from(rashi_rows, language, [("sign", "sign"), ("lord", "planet"), ("permanent", "text"), ("occupant", "planets"), ("total", "text"), ("example_total", "text")]),
|
||||
_rows_from(rashi_rows, language, [("sign", "sign"), ("lord", "planet"), ("permanent", "text"), ("occupant", "text"), ("total", "text"), ("example_total", "text")]),
|
||||
_rows_from(point_rows, language, [("point", "text"), ("status", "text"), ("value", "planet"), ("rule", "text")]),
|
||||
])
|
||||
|
||||
@@ -2225,6 +2234,37 @@ def _tara_name(entry: dict, key_en: str, key_zh: str, language: str) -> str | No
|
||||
return head or None
|
||||
|
||||
|
||||
def _yogi_avayogi(packet: dict, language: str) -> str:
|
||||
jaimini = _first(packet, ("advanced_systems", "jaimini")) or {}
|
||||
local = jaimini.get("yogi_avayogi_local") if isinstance(jaimini, dict) else None
|
||||
if not isinstance(local, dict) or local.get("status") == "blocked":
|
||||
return _blank(language, ["point", "sign", "degree", "nakshatra", "lord", "house"])
|
||||
rows = []
|
||||
for key, label in (("yogi", "Yogi"), ("ava_yogi", "Ava Yogi")):
|
||||
point = local.get(key) if isinstance(local.get(key), dict) else None
|
||||
if not point:
|
||||
continue
|
||||
rows.append({
|
||||
"point": label,
|
||||
"sign": point.get("sign"),
|
||||
"degree": point.get("degree_in_sign"),
|
||||
"nakshatra": point.get("nakshatra"),
|
||||
"lord": point.get("nakshatra_lord"),
|
||||
"house": point.get("house"),
|
||||
})
|
||||
if not rows:
|
||||
return _blank(language, ["point", "sign", "degree", "nakshatra", "lord", "house"])
|
||||
note = (
|
||||
"由本命太阳、月亮黄经按 93°20′ 与 186°40′ 算出,只给坐标,不单独下判断。"
|
||||
if language == "zh"
|
||||
else "Computed from the natal Sun and Moon by 93°20' and 186°40'. Coordinates only; not a judgment by itself."
|
||||
)
|
||||
return note + "\n\n" + _rows_from(rows, language, [
|
||||
("point", "text"), ("sign", "sign"), ("degree", "text"),
|
||||
("nakshatra", "text"), ("lord", "planet"), ("house", "text"),
|
||||
])
|
||||
|
||||
|
||||
def _tara(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("advanced_systems", "nakshatra_adv")) or {}
|
||||
combined = module.get("tara_chandra_combined") if isinstance(module, dict) else None
|
||||
@@ -2371,8 +2411,8 @@ def _chapter_sections() -> list[list[str]]:
|
||||
order[14:17],
|
||||
order[17:32],
|
||||
order[32:43],
|
||||
order[43:58],
|
||||
order[58:],
|
||||
order[43:59],
|
||||
order[59:],
|
||||
]
|
||||
return groups
|
||||
|
||||
@@ -2418,6 +2458,7 @@ def _render_named(packet: dict, language: str, title: str) -> str:
|
||||
"Arudha / UL / A10": lambda: _special_points(packet, language),
|
||||
"Jaimini Calculation Details": lambda: _jaimini_details(packet, language),
|
||||
"Graha Pada": lambda: _graha_pada(packet, language),
|
||||
"Jaimini Yogi and Ava Yogi": lambda: _yogi_avayogi(packet, language),
|
||||
"Karakamsha": lambda: _one_point(packet, language, "karakamsha"),
|
||||
"Darakaraka": lambda: _one_point(packet, language, "darakaraka"),
|
||||
"Rashi Tulya Navamsa": lambda: _rashi_tulya(packet, language),
|
||||
|
||||
@@ -144,6 +144,12 @@ def test_three_fictional_charts_render_the_same_chapters(fictional_reports: dict
|
||||
assert "印刷页 10" in zh and "printed pages 10-17" in en
|
||||
assert "#### 来源感知口径" in zh and "#### 既有口径" in zh
|
||||
assert "#### Source-aware method" in en and "#### Legacy method" in en
|
||||
zh_yogi = _section_body(zh, "Jaimini Yogi 与 Ava Yogi")
|
||||
en_yogi = _section_body(en, "Jaimini Yogi and Ava Yogi")
|
||||
assert "Yogi" in zh_yogi and "Ava Yogi" in zh_yogi
|
||||
assert "Yogi" in en_yogi and "Ava Yogi" in en_yogi
|
||||
assert "未判定" not in zh_yogi
|
||||
assert "Not determined" not in en_yogi
|
||||
assert "再小运" in _section_body(zh, "Chara 大运时长")
|
||||
assert "Pratyantardasha" in _section_body(en, "Chara Dasha Period Durations")
|
||||
assert "marriage_windows" not in _section_body(zh, "Bhrigu 近似推进序列")
|
||||
@@ -363,6 +369,56 @@ def test_chinese_yoga_combinations_are_not_blanked(fictional_reports: dict[int,
|
||||
assert HAN.findall(english) == []
|
||||
|
||||
|
||||
def test_empty_tajika_candidates_are_one_sentence() -> None:
|
||||
"""Acceptance note: an empty candidate list is a sentence, not a row of 未判定."""
|
||||
packet = {
|
||||
"worksheets": {
|
||||
"timing_and_predictive_systems": {
|
||||
"annual_tajika_pack": {"tajika_yogas": {"data": {"candidate_yogas": []}}},
|
||||
}
|
||||
}
|
||||
}
|
||||
chinese = render_parity_chapters(packet, "zh")
|
||||
english = render_parity_chapters(packet, "en")
|
||||
zh_body = _section_body(chinese, "Tajika 相位与瑜伽候选")
|
||||
en_body = _section_body(english, "Tajika Aspect and Yoga Candidates")
|
||||
assert "本年没有候选" in zh_body
|
||||
assert "No candidates this year" in en_body
|
||||
assert "未判定" not in zh_body
|
||||
assert "Not determined" not in en_body
|
||||
assert "|" not in zh_body
|
||||
|
||||
|
||||
def test_empty_goel_occupants_say_none() -> None:
|
||||
"""Acceptance note: an empty sign-strength occupant cell is 无 / None."""
|
||||
packet = {
|
||||
"worksheets": {
|
||||
"advanced_systems": {
|
||||
"jaimini": {
|
||||
"goel_full_strength": {
|
||||
"status": "computed",
|
||||
"atmakaraka": "Sun",
|
||||
"node_occupancy_policy": "include_nodes_text",
|
||||
"planets": {
|
||||
"Sun": {"sign_idx": 0, "degree_in_sign": 1.0, "dignity": "own", "total": 10},
|
||||
},
|
||||
"rashi_strengths": [
|
||||
{"sign": "Aries", "lord": "Mars", "permanent": 1, "occupants": [], "total": 10},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
chinese = render_parity_chapters(packet, "zh")
|
||||
english = render_parity_chapters(packet, "en")
|
||||
zh_body = _section_body(chinese, "Goel Jaimini 强度与特殊点")
|
||||
en_body = _section_body(english, "Goel Jaimini Strength and Special Points")
|
||||
assert "| 白羊座 | 火星 | 1 | 无 | 10 |" in zh_body or "| 白羊 | 火星 | 1 | 无 | 10 |" in zh_body
|
||||
assert "None" in en_body
|
||||
assert "未判定" not in zh_body.split("落入行星", 1)[-1].split("\n", 3)[2]
|
||||
|
||||
|
||||
def test_chart_nine_partial_kaal_and_friendship(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
||||
chinese, english = fictional_reports[9]
|
||||
assert _KAAL_ZH.search(chinese)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""BUG-1420: Yogi / Ava Yogi is computed from the chart, not left blocked."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.annual_native.yogi_sphuta import (
|
||||
build_luminary_sphuta_report,
|
||||
build_yogi_sphuta_local,
|
||||
compare_yogi_sphuta_to_replay,
|
||||
)
|
||||
from scripts.jyotish_engine import compute_chart_data
|
||||
from tests.report_english_fictional import fictional_case
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LIMIT = 1e-6
|
||||
|
||||
|
||||
def _delta(left: float, right: float) -> float:
|
||||
return ((left - right + 180.0) % 360.0) - 180.0
|
||||
|
||||
|
||||
def test_formula_matches_the_declared_offsets() -> None:
|
||||
result = build_yogi_sphuta_local({
|
||||
"ascendant": {"sign": "Aries"},
|
||||
"planets": {"Sun": {"degree_raw": 0.0}, "Moon": {"degree_raw": 0.0}},
|
||||
})
|
||||
assert result["status"] == "partial_verified"
|
||||
assert result["formula_profile"] == "pyjhora_yogi_sphuta_v1"
|
||||
assert result["yogi"]["longitude"] == pytest.approx(93.33333333)
|
||||
assert result["yogi"]["sign"] == "Cancer"
|
||||
assert result["ava_yogi"]["longitude"] == pytest.approx(280.0)
|
||||
assert result["source_longitudes"] == {"Sun": 0.0, "Moon": 0.0}
|
||||
|
||||
|
||||
def test_birth_longitudes_match_the_chart_within_one_microdegree() -> None:
|
||||
birth = fictional_case(0)
|
||||
chart, _asc, _jd, _ayanamsa = compute_chart_data(
|
||||
birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"],
|
||||
birth["lat"], birth["lon"], birth["tz"],
|
||||
node_mode=birth["node_mode"], second=birth["second"], ayanamsa_name=birth["ayanamsa"],
|
||||
)
|
||||
result = build_yogi_sphuta_local(chart)
|
||||
assert result["status"] != "blocked"
|
||||
for name in ("Sun", "Moon"):
|
||||
assert abs(result["source_longitudes"][name] - chart["planets"][name]["degree_raw"]) <= LIMIT
|
||||
expected = (result["source_longitudes"]["Sun"] + result["source_longitudes"]["Moon"] + 93.0 + 20.0 / 60.0) % 360.0
|
||||
assert abs(_delta(result["yogi"]["longitude"], expected)) <= LIMIT
|
||||
|
||||
|
||||
def test_missing_longitude_states_the_reason() -> None:
|
||||
result = build_yogi_sphuta_local({"planets": {"Sun": {"degree_raw": 1.0}}})
|
||||
assert result["status"] == "blocked"
|
||||
assert result["reason"] == "sun_or_moon_longitude_missing"
|
||||
|
||||
|
||||
def test_import_failure_is_not_stored_as_blocked() -> None:
|
||||
source = (ROOT / "scripts" / "jyotish_engine.py").read_text(encoding="utf-8")
|
||||
assert "yogi_sphuta_local_unavailable" not in source
|
||||
assert "yogi_sphuta: ImportError:" in source
|
||||
|
||||
|
||||
def test_luminary_bundle_and_replay_comparison() -> None:
|
||||
report = build_luminary_sphuta_report(sun_longitude=350, moon_longitude=20, ayanamsa="lahiri", ascendant_sign="Aries")
|
||||
points = report["evidence_appendix"][0]["points"]
|
||||
assert points["yogi"]["longitude"] == pytest.approx(103.33333333)
|
||||
local = build_yogi_sphuta_local({
|
||||
"ascendant": {"sign": "Aries"},
|
||||
"planets": {"Sun": {"degree_raw": 350.0}, "Moon": {"degree_raw": 20.0}},
|
||||
})
|
||||
comparison = compare_yogi_sphuta_to_replay(local, {
|
||||
"status": "partial_verified",
|
||||
"yogi": {"longitude": 103.33333333},
|
||||
"ava_yogi": {"longitude": 290.0},
|
||||
})
|
||||
assert comparison["status"] == "partial_verified"
|
||||
assert "not independent multi-engine parity" in comparison["claim_boundary"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("number", [True, float("nan"), float("inf")])
|
||||
def test_nonfinite_sun_is_blocked(number) -> None:
|
||||
result = build_yogi_sphuta_local({"planets": {"Sun": {"degree_raw": number}, "Moon": {"degree_raw": 0}}})
|
||||
assert result["status"] == "blocked"
|
||||
assert result["reason"] == "sun_or_moon_longitude_missing"
|
||||
Reference in New Issue
Block a user