年运附表补上整宫、Tripataki 和特殊点公式轨迹。黄经沿用已算好的星盘,不改原有 Saham 数值。

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-22 23:11:48 +08:00
co-authored by Grok (agent loop) Cursor
parent 6b906f85c4
commit 9b2e6aa0b4
9 changed files with 560 additions and 23 deletions
+73
View File
@@ -0,0 +1,73 @@
"""Whole-sign houses for an annual chart that is already computed."""
from __future__ import annotations
from typing import Any
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",
}
def _longitude(value: Any) -> float | None:
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
return float(value) % 360.0
def build_annual_whole_sign_houses(chart: dict | None) -> dict[str, Any]:
"""Twelve whole-sign houses from the annual ascendant already on the chart."""
chart = chart if isinstance(chart, dict) else {}
ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
asc_lon = _longitude(ascendant.get("degree_raw"))
if asc_lon is None:
asc_lon = _longitude(ascendant.get("lon") if ascendant.get("lon") is not None else ascendant.get("degree"))
if asc_lon is None:
return {
"schema": "jyotish.annual_whole_sign_houses.v1",
"status": "blocked",
"reason": "annual_ascendant_longitude_missing",
"houses": [],
"source_longitudes": {},
}
asc_index = int(asc_lon // 30.0) % 12
placed: dict[int, list[str]] = {index: [] for index in range(12)}
source_longitudes = {}
for name in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"):
data = planets.get(name)
if not isinstance(data, dict):
continue
longitude = _longitude(data.get("degree_raw"))
if longitude is None:
longitude = _longitude(data.get("degree"))
if longitude is None:
continue
if name in ("Sun", "Moon") and data.get("degree_raw") is not None and not isinstance(data.get("degree_raw"), bool):
source_longitudes[name] = float(data["degree_raw"]) % 360.0
placed[int(longitude // 30.0) % 12].append(name)
houses = []
for house in range(1, 13):
sign_index = (asc_index + house - 1) % 12
sign = SIGNS[sign_index]
houses.append({
"house": house,
"sign": sign,
"lord": SIGN_LORDS[sign],
"occupants": placed[sign_index],
"cusp_longitude": sign_index * 30.0,
})
return {
"schema": "jyotish.annual_whole_sign_houses.v1",
"status": "partial_verified",
"house_system": "whole_sign",
"houses": houses,
"source_longitudes": source_longitudes,
}
+96
View File
@@ -0,0 +1,96 @@
"""Formula traces for Sahams that were already calculated.
The trace copies the stored longitude. It does not replace it.
"""
from __future__ import annotations
from typing import Any
# (result key, day (p1, p2), night (p1, p2) or None when day and night match)
# _saham(p1, p2) = Asc + p2 - p1.
_LOCAL = (
("karya_saham", ("Moon", "Mars"), ("Mars", "Moon")),
("vivah_saham", ("Mars", "Venus"), None),
("rajya_saham", ("Jupiter", "Sun"), None),
("shatru_saham", ("Saturn", "Mars"), None),
("labha_saham", ("Moon", "Jupiter"), None),
("parakrama_saham", ("Sun", "Mars"), None),
("putra_saham", ("Moon", "Jupiter"), None),
("jnana_saham", ("Mercury", "Jupiter"), None),
("raja_saham", ("Moon", "Sun"), None),
("yasha_saham", ("Sun", "Jupiter"), None),
("karma_saham", ("Mars", "Saturn"), None),
("bandhu_saham", ("Mercury", "Mars"), None),
("matri_saham", ("Venus", "Moon"), None),
("pitri_saham", ("Saturn", "Sun"), None),
("janma_saham", ("Sun", "Asc"), None),
("mrityu_saham", ("Mars", "Saturn"), None),
("rogha_saham", ("Saturn", "Mercury"), None),
("aarogya_saham", ("Saturn", "Jupiter"), None),
("bhraatri_saham", ("Moon", "Mars"), None),
("ghataka_saham", ("Mars", "Rahu"), None),
("paradesa_saham", ("Moon", "Saturn"), None),
("parabharya_saham", ("Mars", "Venus"), None),
("dhan_saham", ("Venus", "Jupiter"), None),
("maya_saham", ("Ketu", "Rahu"), None),
("moksha_saham", ("Jupiter", "Ketu"), None),
("buddha_saham", ("Jupiter", "Mercury"), None),
("shastra_saham", ("Ketu", "Mars"), None),
("kala_saham", ("Sun", "Saturn"), None),
("shakti_saham", ("Moon", "Venus"), None),
("bhrigu_saham", ("Jupiter", "Venus"), None),
("sundara_saham", ("Mercury", "Venus"), None),
("jnati_saham", ("Saturn", "Mars"), None),
("artha_saham", ("Venus", "Sun"), None),
("dharma_saham", ("Saturn", "Jupiter"), None),
("sangrama_saham", ("Rahu", "Mars"), None),
)
def _rule_text(meta: dict) -> str:
operands = meta.get("formula_operands") or ()
text = " + ".join(str(part).replace("_", " ") for part in operands) if operands else str(meta.get("rule_name") or "rule")
if meta.get("zodiacal_order_exception_applied"):
text += " (+30 deg)"
return text
def attach_formula_traces(
results: dict[str, Any],
*,
is_day: bool,
formula_meta: dict[str, dict] | None = None,
) -> list[dict]:
"""Add ``formula_traces`` without changing any existing Saham longitude."""
formula_meta = formula_meta or {}
traces = []
punya = formula_meta.get("punya_saham")
if punya and isinstance(results.get("punya_saham"), dict):
traces.append({
"name": results["punya_saham"].get("name") or "Punya Saham",
"key": "punya_saham",
"rule": _rule_text(punya),
"phase": "day" if is_day else "night",
"longitude": results["punya_saham"].get("longitude"),
})
for key, day_pair, night_pair in _LOCAL:
item = results.get(key)
if not isinstance(item, dict) or not isinstance(item.get("longitude"), (int, float)):
continue
meta = formula_meta.get(key)
if meta:
rule = _rule_text(meta)
else:
pair = day_pair if is_day or night_pair is None else night_pair
rule = f"Asc + {pair[1]} - {pair[0]}"
traces.append({
"name": item.get("name") or key,
"key": key,
"rule": rule,
"phase": "day" if is_day else "night",
"longitude": item.get("longitude"),
})
results["formula_traces"] = traces
return traces
+104
View File
@@ -0,0 +1,104 @@
"""Tripataki placement grid and Vedha rows from an annual chart.
Longitudes are the ones already on our chart. This module does not call an
ephemeris and does not import PyJHora.
"""
from __future__ import annotations
from typing import Any
SIGNS = (
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
)
VEDHA_TARGETS = {
"Moon": ("Sun", "Mars", "Mercury", "Venus", "Saturn"),
"Lagna": ("Jupiter", "Ketu"),
"Sun": ("Moon", "Mars", "Mercury", "Venus", "Saturn"),
"Mars": ("Sun", "Moon", "Mercury", "Venus", "Saturn"),
"Mercury": ("Sun", "Moon", "Mars", "Venus", "Saturn"),
"Jupiter": ("Ketu",),
"Venus": ("Sun", "Moon", "Mars", "Mercury", "Saturn"),
"Saturn": ("Sun", "Moon", "Mars", "Mercury", "Venus"),
}
def build_tripataki_chakra(
planet_longitudes: dict[str, Any],
ascendant_longitude: float | int | None,
) -> dict[str, Any]:
if not isinstance(planet_longitudes, dict):
return _blocked("annual_planet_longitudes_missing")
try:
if isinstance(ascendant_longitude, bool):
raise TypeError
ascendant = float(ascendant_longitude) % 360
except (TypeError, ValueError):
return _blocked("annual_ascendant_longitude_missing")
placements = [_placement("Lagna", ascendant, is_lagna=True)]
for planet in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"):
try:
raw = planet_longitudes[planet]
if isinstance(raw, bool):
raise TypeError
placements.append(_placement(planet, float(raw) % 360))
except (KeyError, TypeError, ValueError):
continue
sign_cells = []
for sign_index, sign in enumerate(SIGNS):
members = [row["body"] for row in placements if row["sign_index"] == sign_index]
sign_cells.append({"sign_index": sign_index, "sign": sign, "bodies": members})
available = {row["body"] for row in placements}
vedha_rows = []
for target, sources in VEDHA_TARGETS.items():
ready_sources = [source for source in sources if source in available]
missing_sources = [source for source in sources if source not in available]
vedha_rows.append({
"target": target,
"sources": list(sources),
"available_sources": ready_sources,
"missing_sources": missing_sources,
"status": "partial_verified" if target in available and not missing_sources else "blocked",
})
return {
"schema": "pl9.tripataki_chakra.v1",
"status": "partial_verified" if len(placements) >= 8 else "blocked",
"reason": None if len(placements) >= 8 else "fewer_than_eight_bodies",
"placements": placements,
"sign_cells": sign_cells,
"vedha_rows": vedha_rows,
"source_longitudes": {
name: next(row["longitude"] for row in placements if row["body"] == name)
for name in ("Sun", "Moon")
if any(row["body"] == name for row in placements)
},
}
def _placement(body: str, longitude: float, *, is_lagna: bool = False) -> dict[str, Any]:
sign_index = int(longitude // 30) % 12
return {
"body": body,
"longitude": longitude,
"sign_index": sign_index,
"sign": SIGNS[sign_index],
"degree_in_sign": longitude % 30,
"is_lagna": is_lagna,
}
def _blocked(reason: str) -> dict[str, Any]:
return {
"schema": "pl9.tripataki_chakra.v1",
"status": "blocked",
"reason": reason,
"placements": [],
"sign_cells": [],
"vedha_rows": [],
"source_longitudes": {},
}