从报告日期所在月起列出 36 个月的九星表。报告口径和 KP 岁差口径各一张,另有五级大运月采样。黄经仍走本系统恒星计算。
Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
4c405d2a51
commit
355946cd1a
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-23 — 完整数据版补上三年逐月星历
|
||||
|
||||
- 从报告日期所在月起 36 个月,报告口径和 KP 岁差口径各有一张九星表,另有五级大运的月采样。黄经走本系统恒星计算,不用视位置。
|
||||
- Skill 版本不变。
|
||||
|
||||
## 2026-10-23 — 完整数据版补上一年里的月返照和 Tithi
|
||||
|
||||
- 当年和后两年各自列出月返照、原生 Patyayini 和 Tithi Pravesha。已有的年主、Muntha、Mudda、Sahams、Tajika 数值不改写。出生地八年总览会算;72 年的生命周期年返这一版先不做,避免一次生成几十张年返盘。
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user