#!/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), }