Files
Jyotisha/scripts/rashi_dashas_native.py
T
jesse-uxGrok (agent loop)Cursor
4d2506c8c1 fix(reading): give Niryaana Shoola its own PVR table
The report used the same nine-year rule as Shoola. Start from the stronger second or eighth sign and list mahadashas only.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 12:21:35 +08:00

586 lines
22 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}
# Nine grahas only. Outer planets and upagrahas are ignored. Nodes are not weighted.
_AYUR_RASI_STRENGTH_BODIES = (
"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu",
)
_EXALTED_SIGNS = {
"Sun": {0}, "Moon": {1}, "Mars": {9}, "Mercury": {5},
"Jupiter": {3}, "Venus": {11}, "Saturn": {6},
}
_NIRYAANA_SOURCE_ZH = "PVR《Vedic Astrology: An Integrated Approach》"
_NIRYAANA_SOURCE_EN = "PVR, Vedic Astrology: An Integrated Approach"
_NIRYAANA_PROFILE = "pvr_niryaana_shoola_v1"
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 niryaana_shoola_years(sign_idx: int) -> int:
"""Movable, fixed, and dual signs last 7, 8, and 9 years."""
sign_idx = int(sign_idx) % 12
if sign_idx in _FIXED:
return 8
if sign_idx in _DUAL:
return 9
return 7
def select_niryaana_shoola_start_sign(
*,
ascendant_longitude: float,
planet_longitudes: Mapping[str, float],
) -> dict:
"""Pick the stronger of the second and eighth signs for a longevity dasha.
Compare occupant count, then every planet's Jaimini sign aspect (the sign
lord is not weighted), then whether an occupant is exalted. A three-way
tie stays off the table. Count only the nine grahas.
"""
lagna = sign_index(float(ascendant_longitude))
second, eighth = (lagna + 1) % 12, (lagna + 7) % 12
positions = {
name: float(longitude)
for name, longitude in planet_longitudes.items()
if name in _AYUR_RASI_STRENGTH_BODIES and isinstance(longitude, (int, float))
}
candidates = {}
for sign_idx in (second, eighth):
occupants = sorted(
name for name, longitude in positions.items()
if sign_index(longitude) == sign_idx
)
aspecting = sorted(
name for name, longitude in positions.items()
if sign_index(longitude) != sign_idx and sign_idx in _aspecting_signs(sign_index(longitude))
)
exalted = sorted(
name for name in occupants
if sign_idx in _EXALTED_SIGNS.get(name, set())
)
candidates[sign_idx] = {
"sign_index": sign_idx,
"sign": SIGNS[sign_idx],
"occupants": occupants,
"occupant_count": len(occupants),
"rasi_aspecting_planets": aspecting,
"rasi_aspect_count": len(aspecting),
"exalted_occupants": exalted,
}
second_score = (
candidates[second]["occupant_count"],
candidates[second]["rasi_aspect_count"],
bool(candidates[second]["exalted_occupants"]),
)
eighth_score = (
candidates[eighth]["occupant_count"],
candidates[eighth]["rasi_aspect_count"],
bool(candidates[eighth]["exalted_occupants"]),
)
payload = {
"profile": "pvr_niryaana_shoola_ayur_rasi_strength_v1",
"included_bodies": list(_AYUR_RASI_STRENGTH_BODIES),
"ascendant_sign_index": lagna,
"second_sign_index": second,
"eighth_sign_index": eighth,
"candidates": candidates,
"selection_levels": [
"occupant_count",
"all_planet_rasi_aspect_count",
"exalted_occupant_presence",
],
}
if second_score == eighth_score:
return {
**payload,
"status": "blocked",
"reason": "niryaana_shoola_ayur_rasi_strength_tie_requires_source_rule",
}
selected = second if second_score > eighth_score else eighth
return {
**payload,
"status": "selected",
"selected_sign_idx": selected,
"selected_sign": SIGNS[selected],
}
def niryaana_shoola_applicability(
*,
ascendant_longitude: float,
second_strength: float,
eighth_strength: float,
) -> dict:
"""Start from the stronger given second or eighth strength. Equal strength blocks."""
lagna = sign_index(float(ascendant_longitude))
second, eighth = (lagna + 1) % 12, (lagna + 7) % 12
second_strength, eighth_strength = float(second_strength), float(eighth_strength)
if second_strength == eighth_strength:
return {
"applicable": False,
"execution_status": "blocked",
"ascendant_sign_index": lagna,
"second_sign_index": second,
"eighth_sign_index": eighth,
"second_strength": second_strength,
"eighth_strength": eighth_strength,
"rule_family": "pvr_niryaana_shoola_2nd_8th_modality",
"reason": "The cited Niryaana Shoola rule does not provide a tie-break for equal second/eighth strength.",
}
start = second if second_strength > eighth_strength else eighth
return {
"applicable": True,
"ascendant_sign_index": lagna,
"second_sign_index": second,
"eighth_sign_index": eighth,
"second_strength": second_strength,
"eighth_strength": eighth_strength,
"starting_sign_index": start,
"starting_sign": SIGNS[start],
"direction": "forward" if start % 2 == 0 else "reverse",
"rule_family": "pvr_niryaana_shoola_2nd_8th_modality",
"reason": "Niryaana Shoola starts from the stronger second/eighth sign; odd seeds run forward and even seeds reverse.",
}
def niryaana_shoola_mahadashas(birth: date, start_sign: int) -> list[dict]:
"""Twelve mahadashas only. Odd signs go forward; even signs go backward."""
start_sign = int(start_sign) % 12
order = _forward(start_sign) if start_sign % 2 == 0 else _reverse(start_sign)
periods = []
cursor = birth
for sign_idx in order:
years = niryaana_shoola_years(sign_idx)
end = add_years(cursor, years)
periods.append(_period(sign_idx, years, cursor, end))
cursor = end
return periods
def _niryaana_shoola(birth: date, ascendant_longitude: float, planet_lons: Mapping[str, float]) -> dict:
picked = select_niryaana_shoola_start_sign(
ascendant_longitude=ascendant_longitude,
planet_longitudes=planet_lons,
)
if picked.get("status") != "selected":
return _omitted(
_NIRYAANA_PROFILE,
_NIRYAANA_SOURCE_ZH,
_NIRYAANA_SOURCE_EN,
"本轮不上表:二宫与八宫力量相同,原书没有给出进一步的裁决规则。",
"This report does not include Niryaana Shoola: the second and eighth signs are equal, and the source gives no further tie-break.",
)
start_sign = int(picked["selected_sign_idx"])
return _listed(
_NIRYAANA_PROFILE,
_NIRYAANA_SOURCE_ZH,
_NIRYAANA_SOURCE_EN,
niryaana_shoola_mahadashas(birth, start_sign),
direction="forward" if start_sign % 2 == 0 else "reverse",
strength_profile=picked["profile"],
)
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": _niryaana_shoola(birth_day, float(ascendant_longitude), longitudes),
"lagna_kendradi": _omitted(
"",
"",
"",
"本轮不上表:没有选定的安全默认算法,故本报告不列出该大运。",
"This report does not include Lagna Kendradi: no safe default method is selected.",
),
"chara": _chara(birth_day, lagna, longitudes),
}