Ports calc_mudda_with_dasha_balance verbatim from upstream yinduzhanxing c7117f92 (astrologer round-5 Q7). tajika_mudda.annual_mudda_dasha is the one product entry for solar_return_full_report, cmd_tajika and full-reading Step 9; periods are cropped to the Varsha year and carry months for the month-window consumers. The consultation card re-anchors the engine's dates. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
117 lines
5.1 KiB
Python
117 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Annual Mudda Dasha from the natal Moon nakshatra (BUG-1216).
|
|
|
|
Astrologer round-5 ruling 2026-10-03, Q7: the first Mudda lord is
|
|
(completed years + natal Moon nakshatra number 1..27 - 2) mod 9, remainder
|
|
1 Sun, 2 Moon, 3 Mars, 4 Rahu, 5 Jupiter, 6 Saturn, 7 Mercury, 8 Ketu,
|
|
0 Venus. It does not depend on the year lord.
|
|
|
|
The rule itself is `tajika.calc_mudda_with_dasha_balance`, ported verbatim
|
|
from upstream yinduzhanxing c7117f92. This module is the single product
|
|
entry both annual producers (`solar_return.solar_return_full_report` and
|
|
`jyotish_engine.cmd_tajika`) call, so `annual_tajika_pack` compares two runs
|
|
of the same rule. What it adds on top of the upstream function:
|
|
|
|
- the balance comes from the natal Moon (`moon_at_birth_time`);
|
|
- the year length is named: `solar_year_365_25` (product default, the
|
|
upstream default) or `savana_360` (360-day year, research only);
|
|
- `periods` are cropped to the annual window [annual_start, annual_end) and
|
|
carry `months` (share of 12) for the existing month-window consumers; the
|
|
upstream ten-group surface (the last group runs past the year end) stays
|
|
in `periods_full`. Upstream's sub-periods are not carried (no product
|
|
surface shows them).
|
|
|
|
The legacy `tajika.calc_mudda_dasha` (first lord = year lord) is no longer
|
|
called from any product path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Optional
|
|
|
|
try: # scripts/ on sys.path
|
|
from tajika import calc_mudda_with_dasha_balance
|
|
except ImportError: # pragma: no cover - package import
|
|
from scripts.tajika import calc_mudda_with_dasha_balance
|
|
|
|
BALANCE_PROFILE = "moon_at_birth_time"
|
|
YEAR_LENGTHS = {"solar_year_365_25": 365.25, "savana_360": 360.0}
|
|
DEFAULT_YEAR_LENGTH = "solar_year_365_25"
|
|
SOURCE = "tajika.calc_mudda_with_dasha_balance (upstream yinduzhanxing c7117f92)"
|
|
RULE = "astrologer_round5_q7_completed_years_plus_natal_nakshatra_minus_two_mod_nine"
|
|
RULE_TEXT = "起运星 =(已满年数 + 出生月宿序号 − 2)mod 9,与年主无关"
|
|
RULE_TEXT_EN = "First lord = (completed years + natal Moon nakshatra number - 2) mod 9; independent of the year lord"
|
|
|
|
|
|
def _parse(value: str) -> datetime:
|
|
return datetime.fromisoformat(str(value))
|
|
|
|
|
|
def annual_mudda_dasha(
|
|
*,
|
|
annual_start: Optional[datetime],
|
|
completed_years: Optional[int],
|
|
natal_moon_longitude: Optional[float],
|
|
year_length: str = DEFAULT_YEAR_LENGTH,
|
|
) -> Dict[str, Any]:
|
|
"""Mudda Dasha for one Varsha year, anchored at the solar-return instant."""
|
|
if year_length not in YEAR_LENGTHS:
|
|
return {"status": "blocked", "reason": "unknown_year_length", "year_length_basis": year_length}
|
|
if completed_years is None or natal_moon_longitude is None:
|
|
return {"status": "blocked", "reason": "completed_years_and_natal_moon_required_for_mudda",
|
|
"balance_profile": BALANCE_PROFILE}
|
|
raw = calc_mudda_with_dasha_balance(
|
|
annual_start=annual_start,
|
|
completed_years=int(completed_years),
|
|
natal_moon_longitude=natal_moon_longitude,
|
|
balance_moon_longitude=natal_moon_longitude,
|
|
balance_profile=BALANCE_PROFILE,
|
|
annual_duration_days=YEAR_LENGTHS[year_length],
|
|
)
|
|
if raw.get("status") == "blocked":
|
|
return {**raw, "year_length_basis": year_length, "source": SOURCE}
|
|
start = _parse(raw["annual_start"])
|
|
end = _parse(raw["annual_end"])
|
|
year_days = float(raw["annual_duration_days"])
|
|
periods = []
|
|
for row in raw.get("periods") or []:
|
|
p_start = max(_parse(row["start"]), start)
|
|
p_end = min(_parse(row["end"]), end)
|
|
if p_start >= p_end:
|
|
continue
|
|
days = (p_end - p_start).total_seconds() / 86400.0
|
|
periods.append({
|
|
"order": len(periods) + 1,
|
|
"lord": row["lord"],
|
|
"months": round(days / year_days * 12.0, 2),
|
|
"duration_days": days,
|
|
"full_duration_days": row.get("full_duration_days"),
|
|
"start": p_start.isoformat(sep=" "),
|
|
"end": p_end.isoformat(sep=" "),
|
|
})
|
|
return {
|
|
# The first-lord rule is the astrologer's round-5 ruling; dates are
|
|
# not externally replayed (upstream keeps its own parameter_sensitive).
|
|
"status": "partial_verified",
|
|
"upstream_status": raw.get("status"),
|
|
"starting_lord": raw.get("starting_lord"),
|
|
"starting_lord_basis": raw.get("starting_lord_basis"),
|
|
"rule": RULE,
|
|
"rule_text": RULE_TEXT,
|
|
"rule_text_en": RULE_TEXT_EN,
|
|
"annual_start": raw.get("annual_start"),
|
|
"annual_end": raw.get("annual_end"),
|
|
"annual_duration_days": year_days,
|
|
"year_length_basis": year_length,
|
|
"completed_years": raw.get("completed_years"),
|
|
"natal_moon_longitude": raw.get("natal_moon_longitude"),
|
|
"natal_moon_nakshatra_number": raw.get("natal_moon_nakshatra_number"),
|
|
"balance_profile": raw.get("balance_profile"),
|
|
"balance_fraction_remaining": raw.get("balance_fraction_remaining"),
|
|
"periods": periods,
|
|
"periods_full": raw.get("periods"),
|
|
"independent_of_year_lord": True,
|
|
"source": SOURCE,
|
|
}
|