从报告日期所在月起列出 36 个月的九星表。报告口径和 KP 岁差口径各一张,另有五级大运月采样。黄经仍走本系统恒星计算。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-23 10:17:29 +08:00
co-authored by Grok (agent loop)
parent 4c405d2a51
commit 355946cd1a
5 changed files with 352 additions and 7 deletions
+5
View File
@@ -1,5 +1,10 @@
# 印度占星 Skill 更新日志
## 2026-10-23 — 完整数据版补上三年逐月星历
- 从报告日期所在月起 36 个月,报告口径和 KP 岁差口径各有一张九星表,另有五级大运的月采样。黄经走本系统恒星计算,不用视位置。
- Skill 版本不变。
## 2026-10-23 — 完整数据版补上一年里的月返照和 Tithi
- 当年和后两年各自列出月返照、原生 Patyayini 和 Tithi Pravesha。已有的年主、Muntha、Mudda、Sahams、Tajika 数值不改写。出生地八年总览会算;72 年的生命周期年返这一版先不做,避免一次生成几十张年返盘。
+181
View File
@@ -0,0 +1,181 @@
"""36-month ephemeris from the report month, on this repository's sidereal frame.
Longitudes come from ``_calc_sidereal_planets_for_jd`` (true precession, not
apparent position). The KP table uses the same function with the KP ayanamsa.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
_PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
SCHEMA = "jyotish.report_month_ephemeris.v1"
def attach_report_month_ephemeris(args: Any, annual: dict | None = None) -> dict:
today = getattr(args, "today", None)
if isinstance(today, str) and len(today) >= 7 and today[4] == "-":
start_year = int(today[0:4])
start_month = int(today[5:7])
start_source = "report_date"
else:
profile = annual.get("profile") if isinstance(annual, dict) and isinstance(annual.get("profile"), dict) else {}
target = profile.get("target_year", getattr(args, "target_year", None))
start_year = int(target if target not in (None, "") else args.year)
start_month = 1
start_source = "target_january_report_date_missing"
settings = {}
if isinstance(annual, dict) and isinstance(annual.get("profile"), dict):
settings = annual["profile"]
ayanamsa = str(getattr(args, "ayanamsa", None) or settings.get("ayanamsa") or "lahiri")
node_mode = str(getattr(args, "node_mode", None) or settings.get("node_mode") or "mean")
return build_report_ephemeris(
birth_year=int(args.year),
birth_month=int(args.month),
birth_day=int(args.day),
birth_hour=int(args.hour),
birth_minute=int(getattr(args, "minute", 0) or 0),
birth_second=int(getattr(args, "second", 0) or 0),
latitude=float(args.lat),
longitude=float(args.lon),
tz_hours=float(args.tz),
ayanamsa=ayanamsa,
node_mode=node_mode,
start_year=start_year,
start_month=start_month,
start_source=start_source,
)
def build_report_ephemeris(
*,
birth_year: int,
birth_month: int,
birth_day: int,
birth_hour: int,
birth_minute: int,
birth_second: int,
latitude: float,
longitude: float,
tz_hours: float,
ayanamsa: str,
node_mode: str,
start_year: int,
start_month: int,
start_source: str,
month_count: int = 36,
) -> dict:
if month_count < 1:
raise ValueError("month_count must be at least 1")
if not 1 <= int(start_month) <= 12:
raise ValueError(f"start_month out of range: {start_month}")
natal_jd = _jd(birth_year, birth_month, birth_day, birth_hour + birth_minute / 60 + birth_second / 3600, tz_hours)
natal = _bodies(natal_jd, node_mode, ayanamsa)
moon_body = natal.get("Moon") if isinstance(natal.get("Moon"), dict) else {}
moon = moon_body.get("degree_raw")
if not isinstance(moon, (int, float)) or isinstance(moon, bool):
raise RuntimeError("natal Moon longitude missing for the monthly Vimshottari sample")
report_rows = []
kp_rows = []
level_rows = []
level_error = None
for offset in range(month_count):
year, month = _add_months(start_year, start_month, offset)
jd = _jd(year, month, 1, 12.0, tz_hours)
month_text = f"{year:04d}-{month:02d}"
report_rows.extend(_planet_rows(month_text, _bodies(jd, node_mode, ayanamsa)))
kp_rows.extend(_planet_rows(month_text, _bodies(jd, node_mode, "kp")))
if level_error is None:
try:
level_rows.append(_levels(birth_year, birth_month, birth_day, birth_hour, birth_minute, birth_second, moon, year, month))
except Exception as exc:
level_error = f"{exc.__class__.__name__}: {exc}"
return {
"schema": SCHEMA,
"status": "partial_verified",
"start_source": start_source,
"start_month": f"{int(start_year):04d}-{int(start_month):02d}",
"month_count": month_count,
"ayanamsa": ayanamsa,
"kp_ayanamsa": "kp",
"node_mode": node_mode,
"anchor": "local_month_start_noon",
"report_rows": report_rows,
"kp_rows": kp_rows,
"vimshottari_rows": level_rows,
"vimshottari_reason": level_error,
"sample": {
"month": report_rows[0]["month"] if report_rows else None,
"jd_ut": _jd(start_year, start_month, 1, 12.0, tz_hours),
"moon_degree_raw": next(row["degree_raw"] for row in report_rows if row["planet"] == "Moon"),
},
}
def _planet_rows(month_text: str, bodies: dict) -> list[dict]:
rows = []
for name in _PLANETS:
body = bodies.get(name)
if not isinstance(body, dict) or not isinstance(body.get("degree_raw"), (int, float)):
raise RuntimeError(f"{name} longitude missing for {month_text}")
rows.append({
"month": month_text,
"planet": name,
"sign": body.get("sign"),
"longitude": round(float(body["degree_raw"]), 6),
"degree_raw": float(body["degree_raw"]),
})
return rows
def _bodies(jd: float, node_mode: str, ayanamsa: str) -> dict:
try:
from jyotish_engine import _calc_sidereal_planets_for_jd
except ImportError:
from scripts.jyotish_engine import _calc_sidereal_planets_for_jd
data, _ayan = _calc_sidereal_planets_for_jd(jd, node_mode=node_mode, ayanamsa_name=ayanamsa)
if not isinstance(data, dict) or "Moon" not in data:
raise RuntimeError(f"sidereal planets missing at jd {jd}")
return data
def _levels(year, month, day, hour, minute, second, moon_lon: float, anchor_year: int, anchor_month: int) -> dict:
try:
from kp_monthly_vimshottari import build_monthly_vimshottari_snapshot
except ImportError:
from scripts.kp_monthly_vimshottari import build_monthly_vimshottari_snapshot
snapshot = build_monthly_vimshottari_snapshot(
birth_dt=datetime(year, month, day, hour, minute, second),
moon_lon=float(moon_lon),
anchor_dt=datetime(anchor_year, anchor_month, 1, 12, 0, 0),
)
levels = snapshot.get("levels") if isinstance(snapshot.get("levels"), dict) else {}
return {
"month": f"{anchor_year:04d}-{anchor_month:02d}",
"mahadasha": _lord(levels.get("mahadasha")),
"antardasha": _lord(levels.get("antardasha")),
"pratyantardasha": _lord(levels.get("pratyantardasha")),
"sookshma": _lord(levels.get("sookshma")),
"prana": _lord(levels.get("prana")),
}
def _lord(value: Any) -> str | None:
if isinstance(value, dict):
lord = value.get("lord") or value.get("planet")
return str(lord) if lord else None
if isinstance(value, str) and value.strip():
return value.strip()
return None
def _jd(year: int, month: int, day: int, local_hours: float, tz_hours: float) -> float:
import swisseph as swe
return float(swe.julday(int(year), int(month), int(day), float(local_hours) - float(tz_hours)))
def _add_months(year: int, month: int, offset: int) -> tuple[int, int]:
index = int(year) * 12 + (int(month) - 1) + int(offset)
return index // 12, index % 12 + 1
+13
View File
@@ -1810,6 +1810,19 @@ def _attach_full_report_pack(packet: dict, args) -> dict:
profile=profile,
)
timing['three_year_predictive_ephemeris_pack'] = three_year_predictive_ephemeris_pack
try:
from annual_native.report_ephemeris import attach_report_month_ephemeris
except ImportError:
from scripts.annual_native.report_ephemeris import attach_report_month_ephemeris
try:
timing['report_month_ephemeris'] = attach_report_month_ephemeris(args, annual if isinstance(annual, dict) else {})
except Exception as exc:
logger.warning('report month ephemeris failed: %s', exc)
timing['report_month_ephemeris'] = {
'schema': 'jyotish.report_month_ephemeris.v1',
'status': 'blocked',
'reason': f'{exc.__class__.__name__}: {exc}',
}
sections = {
'base': {
'status': 'partial_verified' if worksheets.get('birth_and_parameters') or worksheets.get('d1_rasi_bhava') else 'blocked',
+100 -7
View File
@@ -57,6 +57,13 @@ LABELS: dict[str, tuple[str, str]] = {
"neutral": ("Natural neutral", "天然中性"),
"temporary_friends": ("Temporary friends", "临时友"),
"temporary_enemies": ("Temporary enemies", "临时敌"),
"year": ("Year", "年份"),
"month": ("Month", "月份"),
"mahadasha": ("Mahadasha", "大运"),
"antardasha": ("Antardasha", "小运"),
"pratyantardasha": ("Pratyantardasha", "次小运"),
"sookshma": ("Sookshma", "细运"),
"prana": ("Prana", "微运"),
"house": ("House", "宫位"),
"item": ("Item", "项目"),
"karaka": ("Karaka", "卡拉卡"),
@@ -172,6 +179,7 @@ SECTIONS: tuple[tuple[str, str], ...] = (
("Tripataki Chakra Detail", "Tripataki 轮盘明细"),
("Annual Tajika Strength", "年度 Tajika 力量"),
("Annual Saham Formula Trace", "年度特殊点公式轨迹"),
("Annual Chart Numeric Data", "年度图盘数值数据"),
("Shadbala Subcomponent", "六维力量分项"),
("Shadbala Result", "六维力量结果"),
("House Components and Physical Coordinates", "宫位分量与坐标"),
@@ -219,6 +227,7 @@ SECTIONS: tuple[tuple[str, str], ...] = (
("Multi-Reference Transit", "多参考点行运"),
("Double Transit PAC", "木土双重行运 PAC"),
("Lagna / 7th Lord Transit", "上升主与七宫主行运"),
("Three-Year Predictive Ephemeris", "三年逐月星历"),
("Natal Calculation Conventions", "本命计算口径"),
("KP Calculation Conventions", "KP 计算口径"),
("D9 Dignity", "D9 尊贵"),
@@ -229,11 +238,9 @@ SECTIONS: tuple[tuple[str, str], ...] = (
)
FORBIDDEN_TITLES = (
"Annual Chart Numeric Data",
"Bhava Bala Relative Score Components",
"Score Extremes",
"独立年度方法参考",
"三年逐月星历",
"D81",
"D108",
"D144",
@@ -933,6 +940,48 @@ def _trace_table(traces: Any, language: str) -> str:
return _rows_from(rows, language, [("name", "text"), ("rule", "text"), ("phase", "text"), ("longitude", "text")])
def _annual_numeric(packet: dict, language: str) -> str:
rows = []
seen = set()
packs = []
primary = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
if isinstance(primary, dict):
packs.append(primary)
series = _first(packet, ("timing_and_predictive_systems", "annual_tajika_series")) or {}
years = series.get("years") if isinstance(series, dict) else None
if isinstance(years, dict):
for year in sorted(years, key=str):
pack = years.get(year)
if isinstance(pack, dict):
packs.append(pack)
for pack in packs:
pages = pack.get("annual_year_pages") if isinstance(pack.get("annual_year_pages"), list) else []
if not pages:
chart = pack.get("annual_chart") if isinstance(pack.get("annual_chart"), dict) else {}
data = chart.get("data") if isinstance(chart.get("data"), dict) else chart
longitudes = data.get("planet_longitudes") if isinstance(data.get("planet_longitudes"), dict) else {}
profile = pack.get("profile") if isinstance(pack.get("profile"), dict) else {}
pages = [{"target_year": profile.get("target_year"), "planet_longitudes": longitudes}] if longitudes else []
for page in pages:
if not isinstance(page, dict):
continue
longitudes = page.get("planet_longitudes") if isinstance(page.get("planet_longitudes"), dict) else {}
for planet, longitude in longitudes.items():
if not isinstance(longitude, (int, float)) or isinstance(longitude, bool):
continue
key = (page.get("target_year"), planet)
if key in seen:
continue
seen.add(key)
rows.append({
"year": page.get("target_year"),
"planet": planet,
"sign": SIGNS[int(float(longitude) // 30) % 12],
"longitude": round(float(longitude), 6),
})
return _rows_from(rows, language, [("year", "text"), ("planet", "planet"), ("sign", "sign"), ("longitude", "text")])
def _annual_saham_trace(packet: dict, language: str) -> str:
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
sahams = _pack_data(pack.get("sahams") if isinstance(pack, dict) else None)
@@ -2500,18 +2549,61 @@ _BUILDERS = {
"Tripataki Chakra Detail": _tripataki,
"Annual Tajika Strength": _tajika_strength,
"Annual Saham Formula Trace": _annual_saham_trace,
"Annual Chart Numeric Data": _annual_numeric,
}
def _ephemeris(packet: dict, language: str) -> str:
pack = _first(packet, ("timing_and_predictive_systems", "report_month_ephemeris")) or {}
if not isinstance(pack, dict) or not pack.get("report_rows"):
return _blank(language, ["month", "planet", "sign", "longitude"])
columns = [("month", "text"), ("planet", "planet"), ("sign", "sign"), ("longitude", "text")]
report_rows = pack.get("report_rows") or []
kp_rows = pack.get("kp_rows") or []
if language == "zh":
report_note = f"报告口径,{len(report_rows)} 行。首月是报告日期所在月。"
kp_note = f"KP 岁差口径,{len(kp_rows)} 行。黄经仍走本系统恒星计算,不用视位置。"
else:
report_note = f"Report frame, {len(report_rows)} rows. The first month is the report month."
kp_note = f"KP ayanamsa frame, {len(kp_rows)} rows. Longitudes still use this system's sidereal calculation, not apparent position."
parts = [
report_note,
_rows_from(report_rows, language, columns),
kp_note,
_rows_from(kp_rows, language, columns),
]
levels = pack.get("vimshottari_rows") if isinstance(pack.get("vimshottari_rows"), list) else []
if levels:
parts.append(_rows_from(
levels,
language,
[
("month", "text"),
("mahadasha", "planet"),
("antardasha", "planet"),
("pratyantardasha", "planet"),
("sookshma", "planet"),
("prana", "planet"),
],
))
elif pack.get("vimshottari_reason"):
parts.append(
"五级大运月采样没有算出。"
if language == "zh"
else "The five-level monthly sample was not calculated."
)
return "\n\n".join(part for part in parts if part)
def _chapter_sections() -> list[list[str]]:
order = [title for title, _zh in SECTIONS]
groups = [
order[:14],
order[14:20],
order[20:35],
order[35:46],
order[46:63],
order[63:],
order[14:21],
order[21:36],
order[36:47],
order[47:65],
order[65:],
]
return groups
@@ -2568,6 +2660,7 @@ def _render_named(packet: dict, language: str, title: str) -> str:
"Multi-Reference Transit": lambda: _multi_transit(packet, language),
"Double Transit PAC": lambda: _double_transit(packet, language),
"Lagna / 7th Lord Transit": lambda: _lord_transit(packet, language),
"Three-Year Predictive Ephemeris": lambda: _ephemeris(packet, language),
"Natal Calculation Conventions": lambda: _settings(packet, language),
"KP Calculation Conventions": lambda: _kp(packet, language),
"D9 Dignity": lambda: _d9(packet, language),
+53
View File
@@ -0,0 +1,53 @@
"""Report-month ephemeris uses the same sidereal longitudes as the chart function."""
from __future__ import annotations
from scripts.annual_native.report_ephemeris import build_report_ephemeris
from scripts.jyotish_engine import compute_chart_data
from scripts.report_render.parity_chapters import render_parity_chapters
from tests.report_english_fictional import fictional_case
LIMIT = 1e-6
def _delta(left: float, right: float) -> float:
return abs(((left - right + 180.0) % 360.0) - 180.0)
def test_report_frame_moon_matches_the_chart_function() -> None:
birth = fictional_case(0)
pack = build_report_ephemeris(
birth_year=birth["year"],
birth_month=birth["month"],
birth_day=birth["day"],
birth_hour=birth["hour"],
birth_minute=birth["minute"],
birth_second=birth["second"],
latitude=birth["lat"],
longitude=birth["lon"],
tz_hours=birth["tz"],
ayanamsa=birth["ayanamsa"],
node_mode=birth["node_mode"],
start_year=2026,
start_month=4,
start_source="report_date",
month_count=1,
)
assert pack["start_month"] == "2026-04"
assert len(pack["report_rows"]) == 9
assert len(pack["kp_rows"]) == 9
assert len(pack["vimshottari_rows"]) == 1
moon = next(row for row in pack["report_rows"] if row["planet"] == "Moon")
again, *_rest = compute_chart_data(
2026, 4, 1, 12, 0,
birth["lat"], birth["lon"], birth["tz"],
node_mode=birth["node_mode"], second=0, ayanamsa_name=birth["ayanamsa"],
)
assert _delta(float(moon["degree_raw"]), float(again["planets"]["Moon"]["degree_raw"])) <= LIMIT
text = render_parity_chapters(
{"worksheets": {"timing_and_predictive_systems": {"report_month_ephemeris": pack}}},
"en",
)
assert "Three-Year Predictive Ephemeris" in text
assert "Report frame, 9 rows." in text
assert "KP ayanamsa frame, 9 rows." in text