feat(tajika): Mudda Dasha from natal Moon nakshatra + completed years, not the year lord (BUG-1216)
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
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
59bb7da385
commit
9d81f15afa
@@ -326,6 +326,8 @@ def _monthly_windows(mudda_dasha: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"source": "mudda_dasha",
|
||||
"lord": period.get("lord"),
|
||||
"duration_months": period.get("months"),
|
||||
"start": period.get("start"),
|
||||
"end": period.get("end"),
|
||||
"status": "partial_verified",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -866,27 +866,41 @@ def _solar_return_local(pack: dict, tz: float) -> datetime | None:
|
||||
return moment + timedelta(hours=float(tz))
|
||||
|
||||
|
||||
def _mudda_segments(periods: list, start: datetime, end: datetime) -> list[dict]:
|
||||
def _mudda_segments(periods: list, start: datetime, end: datetime, anchor: Any = None) -> list[dict]:
|
||||
"""Calendar dates of the native Mudda Dasha segments.
|
||||
|
||||
`tajika.calc_mudda_dasha` gives each lord's share of the year in months
|
||||
(Vimshottari proportion of 12). The year runs from this solar return to the
|
||||
next one; each segment's length is its months / 12 of that span.
|
||||
`tajika_mudda.annual_mudda_dasha` (BUG-1216: first lord from the natal
|
||||
Moon nakshatra + completed years) gives each segment's start / end inside
|
||||
the Varsha year, anchored at the solar-return instant `anchor`. They are
|
||||
re-anchored on this consultation's year [start, end) and the last one is
|
||||
cropped at the next solar return. Rows without dates fall back to the
|
||||
months / 12 share of the span.
|
||||
"""
|
||||
try:
|
||||
origin = datetime.fromisoformat(str(anchor)) if anchor else None
|
||||
except ValueError:
|
||||
origin = None
|
||||
span = end - start
|
||||
cursor = start
|
||||
rows: list[dict] = []
|
||||
for period in periods:
|
||||
if not isinstance(period, dict) or not isinstance(period.get("months"), (int, float)):
|
||||
continue
|
||||
finish = cursor + span * (float(period["months"]) / 12.0)
|
||||
try:
|
||||
seg_start = start + (datetime.fromisoformat(str(period["start"])) - origin)
|
||||
seg_end = start + (datetime.fromisoformat(str(period["end"])) - origin)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
seg_start, seg_end = cursor, cursor + span * (float(period["months"]) / 12.0)
|
||||
seg_end = min(seg_end, end)
|
||||
if seg_end <= seg_start:
|
||||
continue
|
||||
rows.append({
|
||||
"lord": period.get("lord"),
|
||||
"months": period.get("months"),
|
||||
"start": cursor.strftime("%Y-%m-%d"),
|
||||
"end": finish.strftime("%Y-%m-%d"),
|
||||
"start": seg_start.strftime("%Y-%m-%d"),
|
||||
"end": seg_end.strftime("%Y-%m-%d"),
|
||||
})
|
||||
cursor = finish
|
||||
cursor = seg_end
|
||||
return rows
|
||||
|
||||
|
||||
@@ -957,7 +971,7 @@ def build_annual_tajika(birth_payload: dict, chart: dict, *, reference: datetime
|
||||
"house_in_annual_chart": _whole_sign_house(muntha.get("muntha_sign"), annual_lagna_idx),
|
||||
"house_in_natal_chart": _whole_sign_house(muntha.get("muntha_sign"), _lagna_idx(chart)),
|
||||
},
|
||||
"mudda_dasha": _mudda_segments(periods, start, end),
|
||||
"mudda_dasha": _mudda_segments(periods, start, end, _record(fields["mudda_dasha"].get("data")).get("annual_start")),
|
||||
"tajika_aspects": aspects,
|
||||
"tajika_aspects_status": producers.get("tajika_yogas"),
|
||||
"producers": producers,
|
||||
|
||||
+55
-31
@@ -14605,56 +14605,84 @@ def cmd_tajika(args):
|
||||
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
|
||||
if chart is None:
|
||||
return {"error": "swisseph未安装"}
|
||||
try:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
from tajika import calc_muntha, calc_year_lord, calc_mudda_dasha, calc_tri_pataka
|
||||
except ImportError as e:
|
||||
return {"error": f"tajika模块导入失败: {e}"}
|
||||
planets = chart.get('planets', {})
|
||||
planet_lons = {}
|
||||
for pn, pd in planets.items():
|
||||
if isinstance(pd, dict) and 'degree' in pd:
|
||||
planet_lons[pn] = pd['degree']
|
||||
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
|
||||
asc_si = int(asc_deg / 30) % 12 # sign index
|
||||
age = args.age
|
||||
if age is None:
|
||||
return {"error": "请提供 --age 参数(当前年龄)"}
|
||||
return _tajika_annual_core(args, planet_lons, int(asc_deg / 30) % 12, age, args.mode or 'all')
|
||||
|
||||
|
||||
def _tajika_annual_core(args, planet_lons, asc_si, age, mode='all'):
|
||||
"""Muntha / year lord / Mudda / Tri-Pataka for cmd_tajika and full reading Step 9.
|
||||
|
||||
Year lord: Panchadhikari on the solar-return chart (BUG-1214). Mudda:
|
||||
natal Moon nakshatra + completed years (BUG-1216), via the same
|
||||
tajika_mudda helper as solar_return_full_report.
|
||||
"""
|
||||
try:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
from tajika import calc_muntha, calc_year_lord, calc_tri_pataka
|
||||
from tajika_mudda import annual_mudda_dasha
|
||||
except ImportError as e:
|
||||
return {"error": f"tajika模块导入失败: {e}"}
|
||||
|
||||
sr_cache = {}
|
||||
|
||||
def _solar_return():
|
||||
if 'sr' not in sr_cache:
|
||||
from cmd_solar_return import solar_return_location_kwargs
|
||||
from solar_return import calc_solar_return_chart
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME
|
||||
sr_cache['ayanamsa'] = getattr(args, 'ayanamsa', None) or DEFAULT_AYANAMSA_NAME
|
||||
sr_cache['sr'] = calc_solar_return_chart(
|
||||
int(args.year), int(args.month), int(args.day), int(args.hour), int(args.minute),
|
||||
float(args.lat), float(args.lon), float(args.tz), int(args.year) + int(age),
|
||||
ayanamsa_name=sr_cache['ayanamsa'], **solar_return_location_kwargs(args),
|
||||
)
|
||||
return sr_cache['sr']
|
||||
|
||||
def _sr_usable(sr) -> bool:
|
||||
return isinstance(sr, dict) and not sr.get('error') and sr.get('birth_asc_sign_idx') is not None
|
||||
|
||||
def _year_lord() -> Dict:
|
||||
# Panchadhikari on the solar-return chart, same helper as
|
||||
# solar_return_full_report (BUG-1214); Muntha lord only when the
|
||||
# annual chart cannot be computed (recorded as fallback, not hidden).
|
||||
from cmd_solar_return import solar_return_location_kwargs
|
||||
from solar_return import calc_solar_return_chart
|
||||
from tajika_year_lord import select_annual_year_lord
|
||||
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME
|
||||
ayanamsa_name = getattr(args, 'ayanamsa', None) or DEFAULT_AYANAMSA_NAME
|
||||
sr = calc_solar_return_chart(
|
||||
int(args.year), int(args.month), int(args.day), int(args.hour), int(args.minute),
|
||||
float(args.lat), float(args.lon), float(args.tz), int(args.year) + int(age),
|
||||
ayanamsa_name=ayanamsa_name, **solar_return_location_kwargs(args),
|
||||
)
|
||||
if not isinstance(sr, dict) or sr.get('error') or sr.get('birth_asc_sign_idx') is None:
|
||||
sr = _solar_return()
|
||||
if not _sr_usable(sr):
|
||||
fallback = calc_year_lord(asc_si, age)
|
||||
fallback['selection_basis'] = 'fallback_muntha_sign_lord_after_panchadhikari_block'
|
||||
fallback['fallback_reason'] = (sr or {}).get('error', 'solar_return_chart_unavailable') if isinstance(sr, dict) else 'solar_return_chart_unavailable'
|
||||
return fallback
|
||||
return select_annual_year_lord(
|
||||
sr, natal_asc_sign_idx=sr['birth_asc_sign_idx'], completed_years=int(age),
|
||||
ayanamsa_name=ayanamsa_name, birth_lat=float(args.lat), birth_lon=float(args.lon), birth_tz=float(args.tz),
|
||||
ayanamsa_name=sr_cache['ayanamsa'], birth_lat=float(args.lat), birth_lon=float(args.lon), birth_tz=float(args.tz),
|
||||
)
|
||||
|
||||
def _mudda() -> Dict:
|
||||
sr = _solar_return()
|
||||
if not _sr_usable(sr):
|
||||
return {'status': 'blocked', 'reason': 'solar_return_chart_required_for_mudda'}
|
||||
return annual_mudda_dasha(
|
||||
annual_start=(sr.get('solar_return') or {}).get('dt_local'),
|
||||
completed_years=int(age),
|
||||
natal_moon_longitude=sr.get('birth_moon_longitude'),
|
||||
)
|
||||
|
||||
result = {}
|
||||
mode = args.mode or 'all'
|
||||
if mode in ('all', 'muntha'):
|
||||
result['muntha'] = calc_muntha(asc_si, age)
|
||||
year_lord = _year_lord() if mode in ('all', 'yearlord', 'mudda', 'tripataka') else None
|
||||
year_lord = _year_lord() if mode in ('all', 'yearlord', 'tripataka') else None
|
||||
if mode in ('all', 'yearlord'):
|
||||
result['year_lord'] = year_lord
|
||||
if mode in ('all', 'mudda'):
|
||||
varsha_lord = year_lord.get('year_lord', 'Jupiter')
|
||||
result['mudda_dasha'] = calc_mudda_dasha(asc_si, varsha_lord, args.month)
|
||||
result['mudda_dasha'] = _mudda()
|
||||
if mode in ('all', 'tripataka'):
|
||||
varsha_lord = year_lord.get('year_lord', 'Jupiter')
|
||||
muntha_si = (asc_si + age) % 12
|
||||
@@ -16405,16 +16433,12 @@ def cmd_full_reading(args):
|
||||
|
||||
if age is not None:
|
||||
try:
|
||||
from tajika import calc_muntha, calc_year_lord, calc_mudda_dasha, calc_tri_pataka
|
||||
asc_si = int(asc_deg / 30) % 12
|
||||
tajika_result = {}
|
||||
tajika_result['muntha'] = calc_muntha(asc_si, age)
|
||||
yl = calc_year_lord(asc_si, age)
|
||||
tajika_result['year_lord'] = yl
|
||||
varsha_lord = yl.get('year_lord', 'Jupiter')
|
||||
tajika_result['mudda_dasha'] = calc_mudda_dasha(asc_si, varsha_lord, args.month)
|
||||
muntha_si = (asc_si + age) % 12
|
||||
tajika_result['tri_pataka'] = calc_tri_pataka(planet_lons, varsha_lord, muntha_si)
|
||||
# Same core as cmd_tajika (BUG-1216): Panchadhikari year lord and
|
||||
# Mudda from the natal Moon nakshatra, not calc_year_lord /
|
||||
# calc_mudda_dasha (Muntha lord, year-lord-started Mudda).
|
||||
tajika_result = _tajika_annual_core(args, planet_lons, int(asc_deg / 30) % 12, age)
|
||||
if tajika_result.get('error'):
|
||||
raise RuntimeError(tajika_result['error'])
|
||||
report['modules']['tajika'] = tajika_result
|
||||
except Exception as e:
|
||||
report['errors'].append(f"tajika: {e}")
|
||||
|
||||
+13
-6
@@ -379,6 +379,10 @@ def calc_solar_return_chart(
|
||||
|
||||
sun_data = birth_chart.get('planets', {}).get('Sun', {})
|
||||
birth_sun_lon = sun_data.get('degree_raw', sun_data.get('degree', 0))
|
||||
# Natal Moon for Mudda Dasha (BUG-1216: first lord from the natal
|
||||
# Moon nakshatra, astrologer round-5 Q7).
|
||||
moon_data = birth_chart.get('planets', {}).get('Moon', {})
|
||||
birth_moon_lon = moon_data.get('degree_raw', moon_data.get('degree')) if isinstance(moon_data, dict) else None
|
||||
birth_jd_ut = birth_jd
|
||||
|
||||
# Step 2: 计算太阳返照精确时刻
|
||||
@@ -435,6 +439,7 @@ def calc_solar_return_chart(
|
||||
'ascendant': ascendant,
|
||||
'asc_sign_idx': sr_asc_idx,
|
||||
'birth_asc_sign_idx': birth_asc_idx,
|
||||
'birth_moon_longitude': birth_moon_lon,
|
||||
'target_year': target_year,
|
||||
'birth_year': birth_year,
|
||||
'annual_location': {'latitude': annual_lat, 'longitude': annual_lon,
|
||||
@@ -619,16 +624,18 @@ def solar_return_full_report(
|
||||
except Exception as e:
|
||||
result['tri_pataka'] = {'error': str(e)}
|
||||
|
||||
# 7. Mudda Dasha(需 asc_sign_idx + year_lord,降级模式跳过)
|
||||
# 7. Mudda Dasha:出生月宿 + 已满年数起运,与年主无关(占星师第五轮问 7,
|
||||
# BUG-1216)。与 cmd_tajika 共用 tajika_mudda.annual_mudda_dasha。降级模式跳过。
|
||||
if degraded:
|
||||
result['mudda_dasha'] = {'error': 'swisseph未安装,无法计算Mudda Dasha(需完整星盘)'}
|
||||
else:
|
||||
try:
|
||||
from tajika import calc_mudda_dasha
|
||||
yl_data = result.get('year_lord', {})
|
||||
yl = yl_data.get('year_lord', '')
|
||||
mudda = calc_mudda_dasha(asc_sign_idx, yl, birth_month or 1)
|
||||
result['mudda_dasha'] = mudda
|
||||
from tajika_mudda import annual_mudda_dasha
|
||||
result['mudda_dasha'] = annual_mudda_dasha(
|
||||
annual_start=sr['solar_return'].get('dt_local'),
|
||||
completed_years=age,
|
||||
natal_moon_longitude=sr.get('birth_moon_longitude'),
|
||||
)
|
||||
except Exception as e:
|
||||
result['mudda_dasha'] = {'error': str(e)}
|
||||
|
||||
|
||||
+114
-1
@@ -13,7 +13,7 @@ Tajika/Varshaphala年运盘模块 v1.0
|
||||
-年度Lord(Year Lord)
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
@@ -355,6 +355,119 @@ def calc_mudda_dasha(asc_sign_idx: int,
|
||||
}
|
||||
|
||||
|
||||
# Ported verbatim from upstream yinduzhanxing c7117f92 (BUG-1216): Mudda from
|
||||
# completed years + natal Moon nakshatra (astrologer round-5 Q7), not the year lord.
|
||||
def calc_mudda_with_dasha_balance(
|
||||
*,
|
||||
annual_start: datetime,
|
||||
completed_years: int,
|
||||
natal_moon_longitude: float,
|
||||
balance_moon_longitude: float,
|
||||
balance_profile: str,
|
||||
annual_duration_days: float = 365.25,
|
||||
) -> Dict:
|
||||
"""Produce the two Moon-balance Mudda profiles used by annual reports.
|
||||
|
||||
The operating Mudda lord is selected from completed years and the natal
|
||||
Moon's nakshatra. The balance is then calculated from either the natal
|
||||
Moon or the Varshaphala Moon, and the running period is cropped at the
|
||||
true solar-return instant. This keeps p132 and p133 as distinct
|
||||
producers rather than treating a different balance source as a label.
|
||||
"""
|
||||
|
||||
dasha_order = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"]
|
||||
dasha_years = {"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10, "Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17}
|
||||
valid_profiles = {"moon_at_birth_time", "moon_in_varshaphala"}
|
||||
if not isinstance(annual_start, datetime):
|
||||
return {"status": "blocked", "reason": "true_solar_return_datetime_required_for_mudda_balance", "balance_profile": balance_profile}
|
||||
if balance_profile not in valid_profiles:
|
||||
return {"status": "blocked", "reason": "recognized_moon_balance_profile_required", "balance_profile": balance_profile}
|
||||
try:
|
||||
natal_moon_longitude = float(natal_moon_longitude) % 360.0
|
||||
balance_moon_longitude = float(balance_moon_longitude) % 360.0
|
||||
annual_duration_days = float(annual_duration_days)
|
||||
except (TypeError, ValueError):
|
||||
return {"status": "blocked", "reason": "sidereal_natal_and_balance_moon_longitudes_required", "balance_profile": balance_profile}
|
||||
if annual_duration_days <= 0:
|
||||
return {"status": "blocked", "reason": "positive_annual_duration_days_required", "balance_profile": balance_profile}
|
||||
|
||||
nakshatra_span = 360.0 / 27.0
|
||||
natal_nakshatra_number = int(natal_moon_longitude / nakshatra_span) + 1
|
||||
remainder = (int(completed_years) + natal_nakshatra_number - 2) % 9
|
||||
remainder_to_lord = {1: "Sun", 2: "Moon", 3: "Mars", 4: "Rahu", 5: "Jupiter", 6: "Saturn", 7: "Mercury", 8: "Ketu", 0: "Venus"}
|
||||
starting_lord = remainder_to_lord[remainder]
|
||||
balance_fraction_remaining = 1.0 - ((balance_moon_longitude % nakshatra_span) / nakshatra_span)
|
||||
starting_duration = dasha_years[starting_lord] * annual_duration_days / 120.0
|
||||
remaining_starting_duration = starting_duration * balance_fraction_remaining
|
||||
elapsed_starting_duration = starting_duration - remaining_starting_duration
|
||||
full_start = annual_start - timedelta(days=elapsed_starting_duration)
|
||||
annual_end = annual_start + timedelta(days=annual_duration_days)
|
||||
|
||||
periods = []
|
||||
antardasha_periods = []
|
||||
cursor = full_start
|
||||
start_idx = dasha_order.index(starting_lord)
|
||||
# The controlled parity sample displays the running remainder plus the next full recurrence of the
|
||||
# same lord, so this surface has ten visible groups rather than truncating
|
||||
# at the civil 365.25-day boundary.
|
||||
for order in range(10):
|
||||
lord = dasha_order[(start_idx + order) % len(dasha_order)]
|
||||
duration_days = dasha_years[lord] * annual_duration_days / 120.0
|
||||
end = cursor + timedelta(days=duration_days)
|
||||
visible_start = max(cursor, annual_start)
|
||||
visible_end = end
|
||||
if visible_start < visible_end:
|
||||
periods.append({
|
||||
"order": len(periods) + 1,
|
||||
"lord": lord,
|
||||
"duration_days": (visible_end - visible_start).total_seconds() / 86400.0,
|
||||
"full_duration_days": duration_days,
|
||||
"start": visible_start.isoformat(sep=" "),
|
||||
"end": visible_end.isoformat(sep=" "),
|
||||
})
|
||||
sub_cursor = cursor
|
||||
sub_start_idx = dasha_order.index(lord)
|
||||
for sub_order in range(9):
|
||||
sub_lord = dasha_order[(sub_start_idx + sub_order) % len(dasha_order)]
|
||||
sub_duration_days = duration_days * dasha_years[sub_lord] / 120.0
|
||||
sub_end = end if sub_order == 8 else sub_cursor + timedelta(days=sub_duration_days)
|
||||
visible_sub_start = max(sub_cursor, annual_start)
|
||||
visible_sub_end = sub_end
|
||||
if visible_sub_start < visible_sub_end:
|
||||
antardasha_periods.append({
|
||||
"main_order": order + 1,
|
||||
"sub_order": sub_order + 1,
|
||||
"main_lord": lord,
|
||||
"sub_lord": sub_lord,
|
||||
"duration_days": (visible_sub_end - visible_sub_start).total_seconds() / 86400.0,
|
||||
"start": visible_sub_start.isoformat(sep=" "),
|
||||
"end": visible_sub_end.isoformat(sep=" "),
|
||||
})
|
||||
sub_cursor = sub_end
|
||||
cursor = end
|
||||
|
||||
return {
|
||||
"status": "parameter_sensitive",
|
||||
"balance_profile": balance_profile,
|
||||
"boundary_semantics": "dasha_beginning_dates",
|
||||
"annual_start": annual_start.isoformat(sep=" "),
|
||||
"annual_end": annual_end.isoformat(sep=" "),
|
||||
"schedule_end": cursor.isoformat(sep=" "),
|
||||
"annual_duration_days": annual_duration_days,
|
||||
"starting_lord": starting_lord,
|
||||
"starting_lord_basis": "completed_years_plus_natal_moon_nakshatra_minus_two_mod_nine",
|
||||
"completed_years": int(completed_years),
|
||||
"natal_moon_longitude": natal_moon_longitude,
|
||||
"natal_moon_nakshatra_number": natal_nakshatra_number,
|
||||
"balance_moon_longitude": balance_moon_longitude,
|
||||
"balance_fraction_remaining": balance_fraction_remaining,
|
||||
"periods": periods,
|
||||
"antardasha_periods": antardasha_periods,
|
||||
"claim_boundary": "Native Moon-balance Mudda follows the documented annual starting-lord and proportional balance rule. Date parity remains a controlled-sample comparison.",
|
||||
}
|
||||
|
||||
|
||||
|
||||
def calc_tri_pataka(planet_lons: Dict[str, float],
|
||||
varsha_lord: str,
|
||||
muntha_sign_idx: int) -> Dict:
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/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,
|
||||
}
|
||||
Reference in New Issue
Block a user