Files
Jyotisha/scripts/rashi_dashas_native.py
T
jesse-ux 9d3eb23ee4 feat: 完整数据版加入六种星座大运
Sthira、Drig、Shoola、Navamsha、Niryaana Shoola 与 K.N. Rao 的 Chara 从本盘算出。Lagna Kendradi 不上表。未推送。
2026-10-09 00:43:00 +08:00

418 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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),
}