Sthira、Drig、Shoola、Navamsha、Niryaana Shoola 与 K.N. Rao 的 Chara 从本盘算出。Lagna Kendradi 不上表。未推送。
418 lines
15 KiB
Python
418 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""Report-only rashi dashas computed from this site's longitudes.
|
||
|
||
The seven sections are Sthira, Drig, Shoola, Navamsha, Niryaana Shoola,
|
||
Lagna Kendradi, and Chara. Lagna Kendradi has no safe default and stays off
|
||
the table. Chara dates the existing kn_rao rows; it does not recalculate them.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import calendar
|
||
from datetime import date, datetime, timedelta
|
||
from typing import Dict, Mapping, Sequence
|
||
|
||
try:
|
||
from goel_short_strength import (
|
||
CLASSICAL_PLANETS,
|
||
degree_in_sign,
|
||
graha_strengths,
|
||
rashi_strengths,
|
||
sign_index,
|
||
stronger_sign,
|
||
)
|
||
from jaimini import SIGNS, calc_chara_dasha_with_antardasha
|
||
from narayana_dasha import SIGN_LORDS, calculate_narayana_period_years
|
||
except ImportError: # pragma: no cover - package import
|
||
from scripts.goel_short_strength import (
|
||
CLASSICAL_PLANETS,
|
||
degree_in_sign,
|
||
graha_strengths,
|
||
rashi_strengths,
|
||
sign_index,
|
||
stronger_sign,
|
||
)
|
||
from scripts.jaimini import SIGNS, calc_chara_dasha_with_antardasha
|
||
from scripts.narayana_dasha import SIGN_LORDS, calculate_narayana_period_years
|
||
|
||
_MOVABLE = {0, 3, 6, 9}
|
||
_FIXED = {1, 4, 7, 10}
|
||
_DUAL = {2, 5, 8, 11}
|
||
|
||
|
||
def _as_date(value: date | datetime | str) -> date:
|
||
if isinstance(value, datetime):
|
||
return value.date()
|
||
if isinstance(value, date):
|
||
return value
|
||
text = str(value).strip()
|
||
if "T" in text:
|
||
text = text.split("T", 1)[0]
|
||
if " " in text:
|
||
text = text.split(" ", 1)[0]
|
||
year, month, day = (int(part) for part in text[:10].split("-"))
|
||
return date(year, month, day)
|
||
|
||
|
||
def add_years(day: date, years: int) -> date:
|
||
try:
|
||
return day.replace(year=day.year + int(years))
|
||
except ValueError:
|
||
return day.replace(year=day.year + int(years), day=28)
|
||
|
||
|
||
def add_months(day: date, months: int) -> date:
|
||
month_index = day.month - 1 + int(months)
|
||
year = day.year + month_index // 12
|
||
month = month_index % 12 + 1
|
||
last = calendar.monthrange(year, month)[1]
|
||
return date(year, month, min(day.day, last))
|
||
|
||
|
||
def _iso(day: date) -> str:
|
||
return day.isoformat()
|
||
|
||
|
||
def _forward(sign_idx: int, count: int = 12) -> list[int]:
|
||
return [(int(sign_idx) + offset) % 12 for offset in range(count)]
|
||
|
||
|
||
def _reverse(sign_idx: int, count: int = 12) -> list[int]:
|
||
return [(int(sign_idx) - offset) % 12 for offset in range(count)]
|
||
|
||
|
||
def _aspecting_signs(sign_idx: int) -> set[int]:
|
||
sign_idx = int(sign_idx) % 12
|
||
if sign_idx in _MOVABLE:
|
||
return {candidate for candidate in _FIXED if candidate != (sign_idx + 1) % 12}
|
||
if sign_idx in _FIXED:
|
||
return {candidate for candidate in _MOVABLE if candidate != (sign_idx - 1) % 12}
|
||
return {candidate for candidate in _DUAL if candidate != sign_idx}
|
||
|
||
|
||
def drig_aspect_sequence(lagna_sign: int) -> list[int]:
|
||
"""Ninth, tenth, and eleventh anchors, each followed by the signs it aspects.
|
||
|
||
Crux printed page 395: an even-footed ninth is followed in reverse.
|
||
Zero-based even indexes are the odd zodiac signs, so they go forward.
|
||
"""
|
||
rows: list[int] = []
|
||
for offset in (8, 9, 10):
|
||
anchor = (int(lagna_sign) + offset) % 12
|
||
scan = _forward(anchor) if anchor % 2 == 0 else _reverse(anchor)
|
||
allowed = _aspecting_signs(anchor)
|
||
rows.extend([anchor] + [sign for sign in scan[1:] if sign in allowed])
|
||
return rows[:12]
|
||
|
||
|
||
def navamsha_sign(longitude: float) -> int:
|
||
longitude = float(longitude) % 360.0
|
||
rashi = int(longitude // 30.0)
|
||
pada = int((longitude % 30.0) / (10.0 / 3.0))
|
||
if pada > 8:
|
||
pada = 8
|
||
start = (0, 9, 6, 3)[rashi % 4]
|
||
return (start + pada) % 12
|
||
|
||
|
||
def navamsha_years(sign_idx: int, lord_sign_idx: int) -> int:
|
||
"""Odd signs count forward, even signs backward. No dual lords and no minus one."""
|
||
if int(sign_idx) % 2 == 0:
|
||
distance = (int(lord_sign_idx) - int(sign_idx)) % 12
|
||
else:
|
||
distance = (int(sign_idx) - int(lord_sign_idx)) % 12
|
||
if distance == 0:
|
||
return 12
|
||
return distance + 1
|
||
|
||
|
||
def sthira_years(sign_idx: int) -> int:
|
||
return (7, 8, 9)[int(sign_idx) % 3]
|
||
|
||
|
||
def _period(sign_idx: int, years: int, start: date, end: date, children: list[dict] | None = None) -> dict:
|
||
row = {
|
||
"sign": SIGNS[int(sign_idx) % 12],
|
||
"sign_index": int(sign_idx) % 12,
|
||
"years": int(years),
|
||
"start": _iso(start),
|
||
"end": _iso(end),
|
||
}
|
||
if children is not None:
|
||
row["antardasha"] = children
|
||
return row
|
||
|
||
|
||
def _calendar_children(parent_sign: int, start: date, *, months_each: int, count: int = 12) -> list[dict]:
|
||
rows = []
|
||
cursor = start
|
||
for offset in range(count):
|
||
child = (int(parent_sign) + offset) % 12
|
||
nxt = add_months(cursor, months_each)
|
||
rows.append({
|
||
"sign": SIGNS[child],
|
||
"sign_index": child,
|
||
"start": _iso(cursor),
|
||
"end": _iso(nxt),
|
||
})
|
||
cursor = nxt
|
||
return rows
|
||
|
||
|
||
def _equal_children(order: Sequence[int], start: date, end: date) -> list[dict]:
|
||
if not order:
|
||
return []
|
||
total = (datetime.combine(end, datetime.min.time()) - datetime.combine(start, datetime.min.time())).total_seconds()
|
||
if total <= 0:
|
||
return []
|
||
origin = datetime.combine(start, datetime.min.time())
|
||
finish = datetime.combine(end, datetime.min.time())
|
||
rows = []
|
||
for offset, sign_idx in enumerate(order):
|
||
child_end = finish if offset == len(order) - 1 else origin + timedelta(seconds=total * (offset + 1) / len(order))
|
||
child_start = origin if offset == 0 else origin + timedelta(seconds=total * offset / len(order))
|
||
rows.append({
|
||
"sign": SIGNS[int(sign_idx) % 12],
|
||
"sign_index": int(sign_idx) % 12,
|
||
"start": child_start.date().isoformat(),
|
||
"end": child_end.date().isoformat(),
|
||
})
|
||
return rows
|
||
|
||
|
||
def _require_planets(planet_lons: Mapping[str, float]) -> None:
|
||
missing = [planet for planet in CLASSICAL_PLANETS if planet not in planet_lons]
|
||
if missing:
|
||
raise ValueError("missing planets: " + ", ".join(missing))
|
||
|
||
|
||
def select_brahma(lagna_sign: int, planet_lons: Mapping[str, float], graha: Mapping, rashi: Mapping) -> dict:
|
||
seventh = (int(lagna_sign) + 6) % 12
|
||
picked = stronger_sign(lagna_sign, seventh, rashi, planet_lons)
|
||
if picked.get("status") != "selected":
|
||
return {"status": "unresolved", "reason": picked.get("reason") or "lagna_seventh_unresolved", "step": "start_sign"}
|
||
anchor = int(picked["sign_index"])
|
||
candidates = []
|
||
for house in (6, 8, 12):
|
||
sign_idx = (anchor + house - 1) % 12
|
||
lord = SIGN_LORDS[SIGNS[sign_idx]]
|
||
if lord == "Saturn":
|
||
continue
|
||
candidates.append((
|
||
int(graha[lord]["total"]),
|
||
degree_in_sign(planet_lons[lord]),
|
||
lord,
|
||
sign_index(planet_lons[lord]),
|
||
))
|
||
if not candidates:
|
||
return {"status": "unresolved", "reason": "no_brahma_lord_after_excluding_saturn", "step": "start_sign"}
|
||
candidates.sort(key=lambda row: (row[0], row[1]), reverse=True)
|
||
best = candidates[0]
|
||
tied = [row for row in candidates if row[0] == best[0] and row[1] == best[1] and row[2] != best[2]]
|
||
if tied:
|
||
return {"status": "unresolved", "reason": "brahma_lord_tie", "step": "start_sign"}
|
||
return {
|
||
"status": "selected",
|
||
"planet": best[2],
|
||
"sign_index": best[3],
|
||
"anchor_sign_index": anchor,
|
||
"tie_break": picked.get("tie_break"),
|
||
}
|
||
|
||
|
||
def _listed(profile_id: str, source_zh: str, source_en: str, periods: list[dict], **extra) -> dict:
|
||
row = {
|
||
"profile_id": profile_id,
|
||
"source_zh": source_zh,
|
||
"source_en": source_en,
|
||
"status": "listed",
|
||
"periods": periods,
|
||
}
|
||
row.update(extra)
|
||
return row
|
||
|
||
|
||
def _omitted(profile_id: str, source_zh: str, source_en: str, reason_zh: str, reason_en: str) -> dict:
|
||
return {
|
||
"profile_id": profile_id,
|
||
"source_zh": source_zh,
|
||
"source_en": source_en,
|
||
"status": "omitted",
|
||
"reason_zh": reason_zh,
|
||
"reason_en": reason_en,
|
||
"periods": [],
|
||
}
|
||
|
||
|
||
def _sthira(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Mapping, rashi: Mapping) -> dict:
|
||
source_zh = "Predicting through Jaimini Astrology,印页 100–108"
|
||
source_en = "Predicting through Jaimini Astrology, printed pages 100-108"
|
||
profile = "goel_rao_short_brahma_method_v1"
|
||
picked = select_brahma(lagna, planet_lons, graha, rashi)
|
||
if picked.get("status") != "selected":
|
||
return _omitted(profile, source_zh, source_en, "本轮不上表:Brahma 星座没有选出。", "This report does not include Sthira: the Brahma sign was not selected.")
|
||
start_sign = int(picked["sign_index"])
|
||
periods = []
|
||
cursor = birth
|
||
for sign_idx in _forward(start_sign):
|
||
years = sthira_years(sign_idx)
|
||
end = add_years(cursor, years)
|
||
children = _calendar_children(sign_idx, cursor, months_each=years)
|
||
periods.append(_period(sign_idx, years, cursor, end, children))
|
||
cursor = end
|
||
return _listed(profile, source_zh, source_en, periods, brahma_planet=picked["planet"], direction="direct")
|
||
|
||
|
||
def _narayana_years(sign_idx: int, planet_lons: Mapping[str, float]) -> int:
|
||
lord = SIGN_LORDS[SIGNS[sign_idx]]
|
||
lord_sign = sign_index(planet_lons[lord])
|
||
years, _adjustment = calculate_narayana_period_years(
|
||
sign_idx,
|
||
lord,
|
||
lord_sign,
|
||
profile="jaimini_odd_footed_v1",
|
||
)
|
||
return int(years)
|
||
|
||
|
||
def _drig(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Mapping) -> dict:
|
||
source_zh = "Crux of Vedic Astrology,印页 288、395"
|
||
source_en = "Crux of Vedic Astrology, printed pages 288 and 395"
|
||
profile = "drig_rath_aspect_sequence_v1"
|
||
periods = []
|
||
cursor = birth
|
||
for sign_idx in drig_aspect_sequence(lagna):
|
||
years = _narayana_years(sign_idx, planet_lons)
|
||
end = add_years(cursor, years)
|
||
children = _drig_children(sign_idx, cursor, end, graha, planet_lons)
|
||
periods.append(_period(sign_idx, years, cursor, end, children))
|
||
cursor = end
|
||
return _listed(profile, source_zh, source_en, periods, direction="aspect_groups")
|
||
|
||
|
||
def _drig_children(md_sign: int, start: date, end: date, graha: Mapping, planet_lons: Mapping[str, float]) -> list[dict]:
|
||
lord = SIGN_LORDS[SIGNS[md_sign]]
|
||
seventh_lord = SIGN_LORDS[SIGNS[(md_sign + 6) % 12]]
|
||
left = int(graha[lord]["total"])
|
||
right = int(graha[seventh_lord]["total"])
|
||
if left == right:
|
||
return []
|
||
chosen = lord if left > right else seventh_lord
|
||
seed = sign_index(planet_lons[chosen])
|
||
order = _forward(seed) if md_sign % 2 == 0 else _reverse(seed)
|
||
return _equal_children(order, start, end)
|
||
|
||
|
||
def _nine_year(birth: date, lagna: int, planet_lons: Mapping[str, float], rashi: Mapping, profile: str, source_zh: str, source_en: str) -> dict:
|
||
seventh = (lagna + 6) % 12
|
||
picked = stronger_sign(lagna, seventh, rashi, planet_lons)
|
||
if picked.get("status") != "selected":
|
||
return _omitted(
|
||
profile,
|
||
source_zh,
|
||
source_en,
|
||
"本轮不上表:上升与第七宫的短法力量没有分出强弱。",
|
||
"This report does not include this dasha: Lagna and the seventh are equal in the short-method strength.",
|
||
)
|
||
start_sign = int(picked["sign_index"])
|
||
periods = []
|
||
cursor = birth
|
||
for sign_idx in _forward(start_sign):
|
||
end = add_years(cursor, 9)
|
||
children = _calendar_children(sign_idx, cursor, months_each=9)
|
||
periods.append(_period(sign_idx, 9, cursor, end, children))
|
||
cursor = end
|
||
return _listed(profile, source_zh, source_en, periods, direction="direct", start_tie_break=picked.get("tie_break"))
|
||
|
||
|
||
def _navamsha(birth: date, asc_lon: float, planet_lons: Mapping[str, float]) -> dict:
|
||
source_zh = "Predicting through Jaimini Astrology,印页 139"
|
||
source_en = "Predicting through Jaimini Astrology, printed page 139"
|
||
profile = "goel_method_1_v1"
|
||
d9_lagna = navamsha_sign(asc_lon)
|
||
d9_planets = {planet: navamsha_sign(planet_lons[planet]) for planet in CLASSICAL_PLANETS}
|
||
start_sign = d9_lagna if d9_lagna % 2 == 0 else (d9_lagna + 6) % 12
|
||
periods = []
|
||
cursor = birth
|
||
for sign_idx in _forward(start_sign):
|
||
lord = SIGN_LORDS[SIGNS[sign_idx]]
|
||
years = navamsha_years(sign_idx, d9_planets[lord])
|
||
end = add_years(cursor, years)
|
||
children = _calendar_children(sign_idx, cursor, months_each=years)
|
||
periods.append(_period(sign_idx, years, cursor, end, children))
|
||
cursor = end
|
||
return _listed(
|
||
profile,
|
||
source_zh,
|
||
source_en,
|
||
periods,
|
||
direction="direct",
|
||
d9_lagna_index=d9_lagna,
|
||
)
|
||
|
||
|
||
def _chara(birth: date, lagna: int, planet_lons: Mapping[str, float]) -> dict:
|
||
source_zh = "Predicting through Jaimini's Chara Dasha,印页 32–36"
|
||
source_en = "Predicting through Jaimini's Chara Dasha, printed pages 32-36"
|
||
profile = "kn_rao"
|
||
longitudes = {planet: float(value) for planet, value in planet_lons.items()}
|
||
calculated = calc_chara_dasha_with_antardasha(
|
||
lagna,
|
||
longitudes,
|
||
birth.year,
|
||
birth.month,
|
||
birth.day,
|
||
profile="kn_rao",
|
||
)
|
||
periods = []
|
||
cursor = birth
|
||
for row in calculated.get("dasha_sequence") or []:
|
||
years = int(row["duration_years"])
|
||
end = add_years(cursor, years)
|
||
order = [int(child["sign_idx"]) for child in row.get("antardashas") or []]
|
||
children = _equal_children(order, cursor, end)
|
||
periods.append(_period(int(row["sign_idx"]), years, cursor, end, children))
|
||
cursor = end
|
||
return _listed(profile, source_zh, source_en, periods, direction="ninth_house_even_footed")
|
||
|
||
|
||
def calculate_rashi_dashas(birth: date | datetime | str, ascendant_longitude: float, planet_lons: Mapping[str, float]) -> Dict[str, dict]:
|
||
"""Return the seven report sections. Strength and seeds come from these longitudes."""
|
||
_require_planets(planet_lons)
|
||
birth_day = _as_date(birth)
|
||
longitudes = {planet: float(planet_lons[planet]) for planet in planet_lons}
|
||
lagna = sign_index(float(ascendant_longitude))
|
||
graha = graha_strengths(longitudes)
|
||
rashi = rashi_strengths(longitudes, graha)
|
||
return {
|
||
"sthira": _sthira(birth_day, lagna, longitudes, graha, rashi),
|
||
"drig": _drig(birth_day, lagna, longitudes, graha),
|
||
"shoola": _nine_year(
|
||
birth_day,
|
||
lagna,
|
||
longitudes,
|
||
rashi,
|
||
"shoola_forward_md_direct_ad_v1",
|
||
"Crux of Vedic Astrology,印页 395",
|
||
"Crux of Vedic Astrology, printed page 395",
|
||
),
|
||
"navamsha": _navamsha(birth_day, float(ascendant_longitude), longitudes),
|
||
"niryaana_shoola": _nine_year(
|
||
birth_day,
|
||
lagna,
|
||
longitudes,
|
||
rashi,
|
||
"goel_nirayana_shoola_v1",
|
||
"Predicting through Jaimini Astrology,印页 162",
|
||
"Predicting through Jaimini Astrology, printed page 162",
|
||
),
|
||
"lagna_kendradi": _omitted(
|
||
"",
|
||
"",
|
||
"",
|
||
"本轮不上表:没有选定的安全默认算法,故本报告不列出该大运。",
|
||
"This report does not include Lagna Kendradi: no safe default method is selected.",
|
||
),
|
||
"chara": _chara(birth_day, lagna, longitudes),
|
||
}
|