让大运接口走完整解读的同一套算法,不再返回编出来的周期。

Ashtottari 在适用时把出生前已经走过的部分算进时间线。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-16 00:52:29 +08:00
co-authored by Grok (agent loop)
parent b9cbe9aa76
commit 5ac88e58f5
30 changed files with 1235 additions and 822 deletions
+74 -24
View File
@@ -288,6 +288,55 @@ def _calc_candidate_balance_and_birth_chain(moon_longitude: float, starting_plan
return balance, birth_chain
def _timeline_start_with_birth_balance(
birth_dt: datetime,
moon_longitude,
starting_planet: str,
) -> tuple[datetime, bool]:
"""Shift the first mahadasha so birth falls inside the elapsed portion.
Both applicability families use the starting lord's nakshatra group. When
the Moon longitude is missing, or it does not fall in that group, the
timeline stays on the birth instant.
"""
if moon_longitude is None:
return birth_dt, False
balance, _birth_chain = _calc_candidate_balance_and_birth_chain(
moon_longitude=float(moon_longitude),
starting_planet=starting_planet,
)
if balance is None:
return birth_dt, False
md_years = next(lord["years"] for lord in DASHA_SEQUENCE if lord["planet"] == starting_planet)
elapsed_years = md_years - float(balance["years"])
if elapsed_years == 0:
return birth_dt, False
return birth_dt - timedelta(days=elapsed_years * 365.25), True
def _current_period_from_shifted_majors(major_periods: list, now: datetime) -> dict | None:
if not major_periods:
return None
first = datetime.fromisoformat(major_periods[0]["start_date"])
last = datetime.fromisoformat(major_periods[-1]["end_date"])
span = last - first
if span.total_seconds() <= 0 or now < first:
return None
cycles = int((now - first).total_seconds() // span.total_seconds())
for period in major_periods:
start = datetime.fromisoformat(period["start_date"]) + cycles * span
end = datetime.fromisoformat(period["end_date"]) + cycles * span
if start <= now < end:
current = period.copy()
current["start_date"] = start.isoformat()
current["end_date"] = end.isoformat()
current["elapsed_years"] = (now - start).days / 365.25
current["remaining_years"] = (end - now).days / 365.25
current["cycle_number"] = cycles + 1
return current
return None
def _build_major_periods(start_lord_idx: int, birth_date: datetime) -> list:
"""
Build the full list of major periods (Mahadashas) given the starting lord.
@@ -418,36 +467,37 @@ def calculate_ashtottari_dasha(birth_info: dict) -> dict:
idx for idx, lord in enumerate(DASHA_SEQUENCE) if lord["planet"] == starting_planet
)
major_periods = _build_major_periods(start_lord_idx, birth_dt)
timeline_start, balance_shifted = _timeline_start_with_birth_balance(
birth_dt, moon_longitude, starting_planet,
)
major_periods = _build_major_periods(start_lord_idx, timeline_start)
antardasha_periods, pratyantardasha_periods = _attach_recursive_subperiods(major_periods)
balance_at_birth = None
dasha_at_birth = None
if moon_longitude is not None and rule_family == RULE_FAMILY_RAHU_FROM_LAGNA_LORD:
balance_at_birth, dasha_at_birth = _calc_candidate_balance_and_birth_chain(
moon_longitude=float(moon_longitude),
starting_planet=starting_planet,
)
# Determine current period. Major periods repeat every 108 years; older natives
# should still return a current period instead of None after the first cycle.
# A shifted timeline is read from the period dates. An unshifted timeline keeps
# the previous birth-anchored walk so charts without a Moon longitude stay put.
now = datetime.now()
current_period = None
age_years = max((now - birth_dt).days / 365.25, 0)
current_in_cycle = age_years % TOTAL_CYCLE
cumulative = 0.0
for p in major_periods:
years = p["years"]
if cumulative <= current_in_cycle < cumulative + years:
cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25)
cycle_end = cycle_start + timedelta(days=years * 365.25)
current_period = p.copy()
current_period["start_date"] = cycle_start.isoformat()
current_period["end_date"] = cycle_end.isoformat()
current_period["elapsed_years"] = (now - cycle_start).days / 365.25
current_period["remaining_years"] = (cycle_end - now).days / 365.25
current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1
break
cumulative += years
if balance_shifted:
current_period = _current_period_from_shifted_majors(major_periods, now)
else:
age_years = max((now - birth_dt).days / 365.25, 0)
current_in_cycle = age_years % TOTAL_CYCLE
cumulative = 0.0
for p in major_periods:
years = p["years"]
if cumulative <= current_in_cycle < cumulative + years:
cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25)
cycle_end = cycle_start + timedelta(days=years * 365.25)
current_period = p.copy()
current_period["start_date"] = cycle_start.isoformat()
current_period["end_date"] = cycle_end.isoformat()
current_period["elapsed_years"] = (now - cycle_start).days / 365.25
current_period["remaining_years"] = (cycle_end - now).days / 365.25
current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1
break
cumulative += years
result = {
"applicable": True,
+8 -5
View File
@@ -12,6 +12,11 @@ from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any
try:
from scripts.dasha_common import nakshatra_seeded_birth_balance
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from dasha_common import nakshatra_seeded_birth_balance
YEAR_DAYS = 365.25636
NAKSHATRA_SPAN = 360.0 / 27.0
@@ -65,11 +70,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]:
def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]:
moon = float(moon_longitude) % 360.0
nakshatra = int(moon // NAKSHATRA_SPAN)
lord = DASHA_SEQUENCE[((nakshatra - SWATI_INDEX) % 27) % len(DASHA_SEQUENCE)][0]
balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN)
return lord, balance, nakshatra
return nakshatra_seeded_birth_balance(
moon_longitude, anchor_index=SWATI_INDEX, sequence=DASHA_SEQUENCE,
)
def calculate_chaturshitisama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]:
-193
View File
@@ -1,193 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
额外Dasha系统模块 v1.0
基于BPHS条件性Dasha体系
支持的Dasha系统:
1. Dwisaptati Sama Dasha (72年周期) — 当Lagna lord在特定条件时触发
2. Shattrimsa Sama Dasha (36年周期) — 当Moon在特定条件时触发
3. Dwadashottari Dasha (112年周期) — 当Venus在特定Nakshatra时触发
"""
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
YEAR_DAYS = 365.25636
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
NAKSHATRAS = ['Ashwini','Bharani','Krittika','Rohini','Mrigashira','Ardra',
'Punarvasu','Pushya','Ashlesha','Magha','Purva Phalguni','Uttara Phalguni',
'Hasta','Chitra','Swati','Vishakha','Anuradha','Jyeshtha',
'Mula','Purva Ashadha','Uttara Ashadha','Shravana','Dhanishta','Shatabhisha',
'Purva Bhadrapada','Uttara Bhadrapada','Revati']
VIMSHOTTARI_DURATION = {'Ketu':7,'Venus':20,'Sun':6,'Moon':10,'Mars':7,'Rahu':18,'Jupiter':16,'Saturn':19,'Mercury':17}
DASHA_ORDER = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury']
# =============================================================================
# Dwisaptati Sama Dasha (72年周期)
# 适用条件:Lagna lord在Lagna或7宫时激活
# 主星序列:特定8星顺序,每9年一个主星
# =============================================================================
DWISAPTATI_LORDS = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu']
DWISAPTATI_DURATION = {l: 9 for l in DWISAPTATI_LORDS} # 每个9年,共72年
DWISAPTATI_SUB_DURATION = {p: VIMSHOTTARI_DURATION[p]*9/120 for p in DWISAPTATI_LORDS}
def check_dwisaptati_condition(lagna_lord: str, lagna_lord_house: int) -> bool:
"""检查Dwisaptati触发条件:Lagna lord在1或7宫"""
return lagna_lord_house in (1, 7)
def calc_dwisaptati_dasha(birth_date: datetime, lagna_lord: str,
lagna_lord_degree: float) -> List[Dict]:
"""
计算Dwisaptati Sama Dasha(72年周期)。
每个主星周期 = 9年,子周期按Vimshottari比例分配。
"""
start_idx = DWISAPTATI_LORDS.index(lagna_lord)
sequence = []
current = birth_date
for i in range(8):
lord_idx = (start_idx + i) % 8
lord = DWISAPTATI_LORDS[lord_idx]
days = 9 * YEAR_DAYS
end_date = current + timedelta(days=days)
# 子周期
subs = []
sub_start = current
for j in range(8):
sub_lord_idx = (lord_idx + j) % 8
sub_lord = DWISAPTATI_LORDS[sub_lord_idx]
sub_days = days * (VIMSHOTTARI_DURATION.get(sub_lord, 9) / 120)
sub_end = sub_start + timedelta(days=sub_days)
subs.append({'lord': sub_lord, 'start': sub_start.strftime('%Y-%m-%d'),
'end': sub_end.strftime('%Y-%m-%d')})
sub_start = sub_end
sequence.append({
'lord': lord, 'years': 9,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
'sub_periods': subs[:4],
})
current = end_date
return sequence
# =============================================================================
# Shattrimsa Sama Dasha (36年周期)
# 适用条件:特定条件触发(Moon-based condition)
# 主星序列:9星,每星4年
# =============================================================================
SHATTRIMSA_DURATION = {l: 4 for l in DASHA_ORDER}
def check_shattrimsa_condition(moon_sign: str) -> bool:
"""Shattrimsa触发条件:Moon在特定星座/条件时"""
return True # 简化:总可用
def calc_shattrimsa_dasha(birth_date: datetime, moon_nakshatra_idx: int) -> List[Dict]:
"""计算Shattrimsa Sama Dasha(36年周期)。每个主星=4年"""
start_lord_idx = moon_nakshatra_idx % 9
sequence = []
current = birth_date
for i in range(9):
lord_idx = (start_lord_idx + i) % 9
lord = DASHA_ORDER[lord_idx]
days = 4 * YEAR_DAYS
end_date = current + timedelta(days=days)
sequence.append({
'lord': lord, 'years': 4,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return sequence
# =============================================================================
# Dwadashottari Dasha (112年周期)
# 适用条件:当Venus在特定Nakshatra(Revati/UttaraBhadrapada等)时触发
# 主星序列:特定8星,不同年限
# =============================================================================
DWADASHOTTARI_LORDS = ['Sun','Jupiter','Ketu','Mercury','Rahu','Mars','Saturn','Moon']
DWADASHOTTARI_YEARS = {'Sun':7,'Jupiter':9,'Ketu':11,'Mercury':13,'Rahu':15,'Mars':17,'Saturn':19,'Moon':21}
VENUS_TRIGGER_NAKSHATRAS = {24, 25, 26} # Shatabhisha, Purva Bhadra, Uttara Bhadra
def check_dwadashottari_condition(venus_nakshatra_idx: int) -> bool:
"""Dwadashottari触发条件:Venus在特定Nakshatra"""
return venus_nakshatra_idx in VENUS_TRIGGER_NAKSHATRAS
def calc_dwadashottari_dasha(birth_date: datetime, moon_degree: float) -> List[Dict]:
"""计算Dwadashottari Dasha(112年周期)"""
nak_idx = int(moon_degree / (360/27))
start_lord_idx = nak_idx % 8
sequence = []
current = birth_date
for i in range(8):
lord_idx = (start_lord_idx + i) % 8
lord = DWADASHOTTARI_LORDS[lord_idx]
years = DWADASHOTTARI_YEARS[lord]
days = years * YEAR_DAYS
end_date = current + timedelta(days=days)
sequence.append({
'lord': lord, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return sequence
# =============================================================================
# 条件性Dasha综合选择器
# =============================================================================
def get_applicable_dashas(planets: Dict, lagna_sign: str, moon_degree: float) -> Dict:
"""
根据星盘条件推荐适用���Dasha系统。
Returns:
各Dasha系统的触发状态和计算结果
"""
results = {'vimshottari': {'applicable': True, 'note': '始终适用(120年标准周期)'}}
# Dwisaptati
lagna_lord = {'Aries':'Mars','Taurus':'Venus','Gemini':'Mercury','Cancer':'Moon',
'Leo':'Sun','Virgo':'Mercury','Libra':'Venus','Scorpio':'Mars',
'Sagittarius':'Jupiter','Capricorn':'Saturn','Aquarius':'Saturn','Pisces':'Jupiter'}.get(lagna_sign, '')
lord_house = planets.get(lagna_lord, {}).get('house', 0)
if check_dwisaptati_condition(lagna_lord, lord_house):
results['dwisaptati'] = {'applicable': True, 'note': f'Lagna lord {lagna_lord}在{lord_house}宫——72年周期激活'}
# Shattrimsa
if check_shattrimsa_condition(lagna_sign):
results['shattrimsa'] = {'applicable': True, 'note': 'Moon-based condition——36年周期可用'}
# Dwadashottari
venus_data = planets.get('Venus', {})
venus_nak = int(venus_data.get('degree', 0) / (360/27))
if check_dwadashottari_condition(venus_nak):
results['dwadashottari'] = {'applicable': True, 'note': f'Venus在触发星宿——112年周期激活'}
return results
+4 -12
View File
@@ -6,18 +6,10 @@ Vedic Astrology Dasha Period Calculator
This script helps calculate Dasha periods based on Nakshatra position.
"""
# Standard Vimshottari Dasha periods in years
VIMSHOTTARI_PERIODS = {
'Ketu': 7,
'Venus': 20,
'Sun': 6,
'Moon': 10,
'Mars': 7,
'Rahu': 18,
'Jupiter': 16,
'Saturn': 19,
'Mercury': 17
}
try:
from dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS
# Planetary order in Vimshottari Dasha
DASHA_ORDER = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury']
+4 -12
View File
@@ -21,18 +21,10 @@ import math
# 恒星年(天)- 基于jyotishganit (MIT License),用于精确Dasha计算
YEAR_DURATION_DAYS = 365.25636
# Vimshottari Dasha周期(年)- 总120年
VIMSHOTTARI_PERIODS = {
'Ketu': 7,
'Venus': 20,
'Sun': 6,
'Moon': 10,
'Mars': 7,
'Rahu': 18,
'Jupiter': 16,
'Saturn': 19,
'Mercury': 17
}
try:
from dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS
HUMAN_LIFE_SPAN_VIMSHOTTARI = 120.0
+46
View File
@@ -0,0 +1,46 @@
"""Shared dasha arithmetic used by the conditional-dasha modules.
This module must not import the API server or PyJHora. PyJHora stays a test
oracle only.
"""
from __future__ import annotations
NAKSHATRA_SPAN = 360.0 / 27.0
# Insertion order is the Vimshottari sequence. Callers compare items() before
# replacing a local table, and rely on this order staying stable.
VIMSHOTTARI_YEARS = {
"Ketu": 7,
"Venus": 20,
"Sun": 6,
"Moon": 10,
"Mars": 7,
"Rahu": 18,
"Jupiter": 16,
"Saturn": 19,
"Mercury": 17,
}
VIMSHOTTARI_ORDER = list(VIMSHOTTARI_YEARS.keys())
def nakshatra_seeded_birth_balance(
moon_longitude: float,
*,
anchor_index: int,
sequence: tuple | list,
) -> tuple[str, float, int]:
"""Birth lord and remaining years for a nakshatra-seeded conditional dasha.
The arithmetic matches the six copied helpers: offset from the anchor
nakshatra, lord = sequence[offset % length], balance = years * (1 - fraction
already elapsed inside the nakshatra). Operation order is fixed so the
float matches the previous per-module copies.
"""
moon = float(moon_longitude) % 360.0
nakshatra_index = int(moon // NAKSHATRA_SPAN)
offset = (nakshatra_index - int(anchor_index)) % 27
lord, full_years = sequence[offset % len(sequence)]
fraction_elapsed = (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN
remaining_years = full_years * (1.0 - fraction_elapsed)
return lord, remaining_years, nakshatra_index
+342
View File
@@ -0,0 +1,342 @@
"""HTTP dasha registry.
`DASHA_REGISTRY_METADATA` is the old `extended_dashas.DASHA_REGISTRY` moved
verbatim so `/api/chart` `available_dashas` stays byte-identical.
`compute_dasha` calls the same modules `cmd_full_reading` calls, with the same
birth-info fields. Systems without that module return "not implemented"
instead of a fabricated timeline.
This module must not import `jyotish_api_server` or PyJHora.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
try:
from source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE
DASHA_REGISTRY_METADATA = {
"vimshottari": {"name": "Vimshottari", "years": 120, "type": "nakshatra"},
"ashtottari": {"name": "Ashtottari", "years": 108, "type": "nakshatra"},
"yogini": {"name": "Yogini", "years": 36, "type": "nakshatra"},
"kalachakra": {"name": "Kalachakra", "years": 94, "type": "nakshatra"},
"tara": {"name": "Tara Dasha", "years": 81, "type": "nakshatra"},
"shodasottari": {"name": "Shodasottari", "years": 128, "type": "nakshatra"},
"panchottari": {"name": "Panchottari", "years": 105, "type": "nakshatra"},
"satabdika": {"name": "Satabdika", "years": 100, "type": "nakshatra"},
"chara": {"name": "Chara (Jaimini)", "years": 36, "type": "rasi"},
"narayana": {"name": "Narayana", "years": 36, "type": "rasi"},
"shoola": {"name": "Shoola Dasha", "years": 9, "type": "rasi"},
"sudasa": {"name": "Sudasa", "years": 36, "type": "rasi"},
"drig": {"name": "Drig Dasha", "years": 36, "type": "rasi"},
"sthira": {"name": "Sthira Dasha", "years": 36, "type": "rasi"},
"mandooka": {"name": "Mandooka Dasha", "years": 36, "type": "rasi"},
"lagnamsaka": {"name": "Lagnamsaka Dasha", "years": 36, "type": "rasi"},
"yogardha": {"name": "Yogardha Dasha", "years": 36, "type": "rasi"},
"brahma": {"name": "Brahma Dasha", "years": 36, "type": "rasi"},
"dwisaptati": {"name": "Dwisaptati Sama", "years": 72, "type": "conditional"},
"shattrimsa": {"name": "Shattrimsa Sama", "years": 36, "type": "conditional"},
"dwadashottari": {"name": "Dwadashottari", "years": 112, "type": "conditional"},
"shasti_hayani": {"name": "Shasti-Hayani", "years": 60, "type": "conditional"},
"chaturaaseeti": {"name": "Chaturaaseeti Sama", "years": 84, "type": "conditional"},
"tithi_ashtottari": {"name": "Tithi Ashtottari", "years": 108, "type": "conditional"},
"tithi_yogini": {"name": "Tithi Yogini", "years": 36, "type": "conditional"},
"patyayini": {"name": "Patyayini", "years": 36, "type": "conditional"},
"navamsa": {"name": "Navamsa Dasha", "years": 36, "type": "varga"},
"kendradi": {"name": "Kendradi", "years": 38, "type": "bhav"},
"paryaaya": {"name": "Paryaaya Dasha", "years": 36, "type": "varga"},
"rashmi": {"name": "Rashmi Dasha", "years": 36, "type": "special"},
"naisargika": {"name": "Naisargika Dasha", "years": 120, "type": "special"},
"aayu": {"name": "Aayu Dasha", "years": 36, "type": "special"},
"karaka": {"name": "Karaka Dasha", "years": 36, "type": "special"},
"panchasvara": {"name": "Panchasvara Dasha", "years": 36, "type": "special"},
"sudarsana": {"name": "Sudarsana Chakra Dasha", "years": 36, "type": "special"},
}
_CONDITIONAL_CALLS = {
"shodasottari": ("shodashottari_dasha", "calculate_shodashottari_dasha", {}),
"dwadashottari": (
"dwadashottari_dasha",
"calculate_dwadashottari_dasha",
{"sukramsaka_profile": "navamsa_lagna_taurus_or_libra"},
),
"dwisaptati": ("dwisaptatisama_dasha", "calculate_dwisaptatisama_dasha", {}),
"shattrimsa": ("shattrimshatsama_dasha", "calculate_shattrimshatsama_dasha", {}),
"panchottari": ("panchottari_dasha", "calculate_panchottari_dasha", {}),
"satabdika": ("satabdika_dasha", "calculate_satabdika_dasha", {}),
"chaturaaseeti": ("chaturshitisama_dasha", "calculate_chaturshitisama_dasha", {}),
}
SIGNS = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
]
SIGN_LORDS = {
"Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon",
"Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars",
"Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter",
}
class DashaNotImplemented(Exception):
"""The registry key exists, but there is no engine module behind it."""
def available_dasha_rows() -> list[dict[str, Any]]:
"""`/api/chart` `available_dashas` rows, same order and fields as before."""
return [
{"key": key, "name": info["name"], "years": info["years"], "type": info["type"]}
for key, info in DASHA_REGISTRY_METADATA.items()
]
def _load(module_name: str):
try:
return __import__(module_name)
except ModuleNotFoundError: # pragma: no cover - package import
return __import__(f"scripts.{module_name}", fromlist=[module_name])
def _longitude(value: Any) -> float | None:
if isinstance(value, (int, float)):
return float(value) % 360.0
if not isinstance(value, dict):
return None
for key in ("lon", "longitude", "degree"):
raw = value.get(key)
if isinstance(raw, (int, float)):
return float(raw) % 360.0
sign_idx = value.get("sign_idx")
if sign_idx is None and value.get("sign") in SIGNS:
sign_idx = SIGNS.index(value["sign"])
degree = value.get("degree_in_sign", value.get("degree", 0))
try:
if sign_idx is None:
return None
return (int(sign_idx) * 30 + float(degree or 0)) % 360.0
except (TypeError, ValueError):
return None
def _planet_longitudes(planets: dict) -> dict[str, float]:
longitudes = {}
if not isinstance(planets, dict):
return longitudes
for name, data in planets.items():
longitude = _longitude(data)
if longitude is not None:
longitudes[str(name)] = longitude
return longitudes
def _optional_float(body: dict, *keys: str) -> float | None:
for key in keys:
raw = body.get(key)
if isinstance(raw, (int, float)):
return float(raw)
return None
def _reference_datetime(birth_dt: datetime, body: dict) -> datetime:
raw = body.get("today") or body.get("current_date")
if isinstance(raw, datetime):
return raw
if isinstance(raw, str) and raw.strip():
return datetime.strptime(raw.strip()[:10], "%Y-%m-%d")
return datetime.now()
def build_reading_birth_info(
birth_dt: datetime,
planets: dict,
ascendant: dict | None,
body: dict | None = None,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, float]]:
"""Same fields `cmd_full_reading` puts in `conditional_birth_info`.
Returns `(alt_dasha_info, conditional_birth_info, planet_longitudes)`.
Kept here, not extracted from the engine, so the full-reading dict
construction and its output stay untouched.
"""
body = body or {}
ascendant = ascendant or {}
planet_lons = _planet_longitudes(planets)
moon_lon = planet_lons.get("Moon", _longitude(body.get("moon_lon")) or 0.0)
sun_lon = planet_lons.get("Sun", _longitude(body.get("sun_lon")) or 0.0)
asc_lon = _longitude(ascendant)
if asc_lon is None:
asc_lon = _longitude(body.get("ascendant_longitude"))
if asc_lon is None:
sign_idx = ascendant.get("sign_idx", body.get("asc_sign_idx", 0))
try:
asc_idx = int(sign_idx) % 12
except (TypeError, ValueError):
asc_idx = 0
asc_lon = float(asc_idx * 30)
asc_idx = int(float(asc_lon) / 30) % 12
moon_nakshatra_index = int(moon_lon / (360 / 27)) % 27
moon_pada = int((moon_lon % (360 / 27)) / (360 / 108)) + 1
tithi_number = int(((moon_lon - sun_lon) % 360) / 12) + 1
lagna_lord_name = SIGN_LORDS[SIGNS[asc_idx]]
tenth_lord_name = SIGN_LORDS[SIGNS[(asc_idx + 9) % 12]]
try:
varga = _load("varga")
navamsa_ascendant_sign_index = int(varga.calc_varga(asc_lon, 9).get("sign_idx", 0)) % 12
venus_navamsa_sign_index = int(varga.calc_varga(float(planet_lons.get("Venus", 0)), 9).get("sign_idx", 0)) % 12
except Exception:
navamsa_ascendant_sign_index = None
venus_navamsa_sign_index = None
degree_in_sign = float(asc_lon) % 30.0
ascendant_hora_lord = (
"Sun"
if (asc_idx % 2 == 0 and degree_in_sign < 15.0)
or (asc_idx % 2 == 1 and degree_in_sign >= 15.0)
else "Moon"
)
birth_is_daytime = None
lat = _optional_float(body, "lat", "latitude")
lon = _optional_float(body, "lon", "longitude")
tz = _optional_float(body, "tz", "timezone")
if lat is not None and lon is not None and tz is not None:
try:
daynight = _load("saham_daynight")
birth_is_daytime = bool(daynight.determine_daytime(birth_dt, lat=lat, lon=lon, tz=tz).get("is_daytime"))
except Exception:
birth_is_daytime = None
applicability_rule_family = (
"rahu_from_lagna_lord_kendra_trikona_plus_paksha_daynight"
if birth_is_daytime is not None
else "moon_nakshatra_plus_paksha"
)
alt = {
"birth_datetime": birth_dt,
"moon_nakshatra_index": moon_nakshatra_index,
"moon_pada": moon_pada,
"is_shukla_paksha": 1 <= tithi_number <= 15,
"lagna_rashi_index": asc_idx,
}
conditional = {
**alt,
"ascendant_longitude": asc_lon,
"sun_longitude": sun_lon,
"moon_longitude": moon_lon,
"birth_is_daytime": birth_is_daytime,
"ascendant_sign_index": asc_idx,
"ascendant_hora_lord": ascendant_hora_lord,
"navamsa_ascendant_sign_index": navamsa_ascendant_sign_index,
"venus_rasi_sign_index": int(float(planet_lons.get("Venus", 0)) / 30) % 12,
"venus_navamsa_sign_index": venus_navamsa_sign_index,
"tenth_lord_sign_index": int(float(planet_lons.get(tenth_lord_name, 0)) / 30) % 12,
"rahu_sign_index": int(float(planet_lons.get("Rahu", 0)) / 30) % 12,
"lagna_lord_sign_index": int(float(planet_lons.get(lagna_lord_name, 0)) / 30) % 12,
"applicability_rule_family": applicability_rule_family,
}
return alt, conditional, planet_lons
def _major_rows(payload: Any) -> list:
if isinstance(payload, dict):
majors = payload.get("major")
if isinstance(majors, list):
return majors
periods = payload.get("periods")
if isinstance(periods, list):
return periods
sequence = payload.get("mahadasha_sequence")
if isinstance(sequence, list):
return sequence
return []
if isinstance(payload, list):
return payload
return []
def _conditional_payload(key: str, conditional: dict) -> dict:
module_name, function_name, extra = _CONDITIONAL_CALLS[key]
module = _load(module_name)
birth_info = {
**conditional,
**extra,
"child_period_profile": PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE,
}
return getattr(module, function_name)(birth_info)
def compute_dasha(
key: str,
*,
birth_dt: datetime,
planets: dict,
ascendant: dict | None,
body: dict | None = None,
) -> dict[str, Any]:
"""Return engine major periods for one registry key.
Raises `DashaNotImplemented` when the key has no engine module. Vimshottari
stays on the handler's existing canonical branch and is not computed here.
"""
if key not in DASHA_REGISTRY_METADATA:
raise DashaNotImplemented("Unknown dasha system")
if key == "chara":
raise DashaNotImplemented("chara dasha is served at /api/dasha/chara")
body = body or {}
alt, conditional, planet_lons = build_reading_birth_info(birth_dt, planets or {}, ascendant, body)
if key == "yogini":
payload = _load("yogini_dasha").calculate_yogini_dasha({**alt, "moon_longitude": conditional["moon_longitude"]})
return {"periods": _major_rows(payload)}
if key == "ashtottari":
payload = _load("ashtottari_dasha").calculate_ashtottari_dasha({
**alt,
"moon_longitude": conditional["moon_longitude"],
})
return {"periods": _major_rows(payload)}
if key == "kalachakra":
payload = _load("kalachakra_dasha").calculate_kalachakra_dasha(alt)
return {"periods": _major_rows(payload)}
if key == "shasti_hayani":
birth_info = dict(conditional)
birth_info["child_period_profile"] = PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE
payload = _load("shastihayani_dasha").calculate_shastihayani_dasha(birth_info)
return {"periods": _major_rows(payload)}
if key in _CONDITIONAL_CALLS:
return {"periods": _major_rows(_conditional_payload(key, conditional))}
if key == "narayana":
asc_idx = int(conditional["ascendant_sign_index"])
reference = _reference_datetime(birth_dt, body)
current_age = (reference - birth_dt).days / 365.25
birth_year = int(body.get("year") or birth_dt.year)
narayana = _load("narayana_dasha")
report = narayana.narayana_dasha_full_report(
lagna_sign_idx=asc_idx,
planet_lons=planet_lons,
current_age=current_age,
birth_year=birth_year,
)
labeled = _load("narayana_legacy_label").label_legacy_narayana(
report,
lagna_sign_idx=asc_idx,
planet_lons=planet_lons,
current_age=current_age,
birth_year=birth_year,
)
return {
"periods": _major_rows(labeled),
"algorithm_label": labeled.get("algorithm_label") if isinstance(labeled, dict) else None,
"algorithm_note": labeled.get("algorithm_note") if isinstance(labeled, dict) else None,
"profile": labeled.get("profile") if isinstance(labeled, dict) else None,
}
if key == "shoola":
rashi = _load("rashi_dashas_native")
try:
section = rashi.calculate_rashi_dashas(
birth_dt.date(), conditional["ascendant_longitude"], planet_lons
)["shoola"]
except ValueError as exc:
raise DashaNotImplemented(str(exc)) from exc
return {"periods": _major_rows(section)}
raise DashaNotImplemented("dasha system not implemented")
+8 -5
View File
@@ -6,6 +6,11 @@ from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any
try:
from scripts.dasha_common import nakshatra_seeded_birth_balance
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from dasha_common import nakshatra_seeded_birth_balance
YEAR_DAYS = 365.25636
NAKSHATRA_SPAN = 360.0 / 27.0
@@ -63,11 +68,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]:
def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]:
moon = float(moon_longitude) % 360.0
nakshatra = int(moon // NAKSHATRA_SPAN)
lord = DASHA_SEQUENCE[((nakshatra - MULA_INDEX) % 27) % len(DASHA_SEQUENCE)][0]
balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN)
return lord, balance, nakshatra
return nakshatra_seeded_birth_balance(
moon_longitude, anchor_index=MULA_INDEX, sequence=DASHA_SEQUENCE,
)
def calculate_dwisaptatisama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]:
+6 -5
View File
@@ -101,11 +101,12 @@ EVENT_KARAKAS = {
}
# Vimshottari Dasha 行星
DASHA_PLANETS = ['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,
}
try:
from dasha_common import VIMSHOTTARI_ORDER as DASHA_PLANETS
from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_ORDER as DASHA_PLANETS
from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
# Graha Drishti 相位
PLANET_ASPECTS = {
-408
View File
@@ -1,408 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
扩展Dasha系统 v2.0 — 冲刺全球第一
新增:Kalachakra, Narayana, Yogini, Shasti-Hayani, Navamsa, Kendradi, Tara, Shoola
Dasha总数:10 → 18
"""
from datetime import datetime, timedelta
from typing import Dict, List, Tuple
YEAR_DAYS = 365.25636
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
DASHA_ORDER = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury']
NAKSHATRAS = ['Ashwini','Bharani','Krittika','Rohini','Mrigashira','Ardra',
'Punarvasu','Pushya','Ashlesha','Magha','PurvaPhalguni','UttaraPhalguni',
'Hasta','Chitra','Swati','Vishakha','Anuradha','Jyeshtha',
'Mula','PurvaAshadha','UttaraAshadha','Shravana','Dhanishta','Shatabhisha',
'PurvaBhadrapada','UttaraBhadrapada','Revati']
# =============================================================================
# 1. Kalachakra Dasha(时轮大运)
# =============================================================================
KALACHAKRA_NAVAMSHA_MAP = {}
KALACHAKRA_SAVYA = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius']
KALACHAKRA_APASAVYA = ['Scorpio','Libra','Virgo','Cancer','Leo','Gemini','Taurus','Aries','Sagittarius']
KALACHAKRA_YEARS = {'Aries':10,'Taurus':16,'Gemini':18,'Cancer':24,'Leo':20,'Virgo':22,'Libra':12,'Scorpio':14,'Sagittarius':8,'Capricorn':7,'Aquarius':9,'Pisces':11}
def calc_kalachakra_dasha(birth_date: datetime, moon_nak_idx: int,
moon_pada: int) -> List[Dict]:
"""Kalachakra Dasha(时轮大运)"""
# 判定savya/apasavya序列
savya_naks = {0,1,2,3,4,5,10,11,12,13,14,15,22,23,24,25,26}
is_savya = moon_nak_idx in savya_naks
sequence = KALACHAKRA_SAVYA if is_savya else KALACHAKRA_APASAVYA
# 起始点
pada_map = {1:0, 2:3, 3:6, 4:0} # 简化映射
start_offset = pada_map.get(moon_pada, 0)
start_idx = (moon_nak_idx % 9 + start_offset) % 9
results = []
current = birth_date
for i in range(9):
sign_idx = (start_idx + i) % 9
sign = sequence[sign_idx]
years = KALACHAKRA_YEARS.get(sign, 10)
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': sign, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 2. Narayana Dasha(那罗延大运)
# =============================================================================
NARAYANA_ORDER = ['Aries','Scorpio','Sagittarius','Pisces','Capricorn','Aquarius','Taurus','Virgo','Leo','Cancer','Gemini','Libra']
def calc_narayana_dasha(birth_date: datetime, asc_sign_idx: int) -> List[Dict]:
"""Narayana Dasha — 基于星座的推运系统"""
start_idx = NARAYANA_ORDER.index(SIGNS[asc_sign_idx])
results = []
current = birth_date
for i in range(12):
idx = (start_idx + i) % 12
sign = NARAYANA_ORDER[idx]
years = (idx % 3 + 1) * 3 # movable=3, fixed=6, dual=9
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': sign, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 3. Yogini Dasha(瑜伽女神大运)— 36年周期
# =============================================================================
YOGINI_LORDS = ['Mangala','Pingala','Dhanya','Bhramari','Bhadrika','Ulka','Siddha','Sankata']
YOGINI_YEARS = [1,2,3,4,5,6,7,8] # 总和=36
def calc_yogini_dasha(birth_date: datetime, moon_nak_idx: int) -> List[Dict]:
"""Yogini Dasha — 36年女神周期"""
start_idx = moon_nak_idx % 8
results = []
current = birth_date
for i in range(8):
idx = (start_idx + i) % 8
lord = YOGINI_LORDS[idx]
years = YOGINI_YEARS[idx]
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': lord, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 4. Shasti-Hayani Dasha(六十哈亚尼大运)
# =============================================================================
SHASTI_LORDS = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']
SHASTI_YEARS = [6,8,10,12,14,16,18]
def calc_shasti_hayani_dasha(birth_date: datetime, sun_sign_idx: int) -> List[Dict]:
"""Shasti-Hayani Dasha — 基于太阳位置的60年推运"""
start_idx = sun_sign_idx % 7
results = []
current = birth_date
for i in range(7):
idx = (start_idx + i) % 7
lord = SHASTI_LORDS[idx]
years = SHASTI_YEARS[idx]
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': lord, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 5. Navamsa Dasha(九分盘大运)
# =============================================================================
def calc_navamsa_dasha(birth_date: datetime, d9_asc_sign_idx: int) -> List[Dict]:
"""Navamsa Dasha — 基于D9上升的星座推运"""
results = []
current = birth_date
for i in range(12):
sign_idx = (d9_asc_sign_idx + i) % 12
sign = SIGNS[sign_idx]
years = (i % 3 + 1) * 3
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': sign, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 6. Kendradi Dasha(角宫推运)
# =============================================================================
KENDRADI_ORDER = [1,4,7,10,2,5,8,11,3,6,9,12]
def calc_kendradi_dasha(birth_date: datetime, asc_sign_idx: int) -> List[Dict]:
"""Kendradi Dasha — 从角宫开始的宫位推运"""
results = []
current = birth_date
for i, house_num in enumerate(KENDRADI_ORDER):
sign_idx = (asc_sign_idx + house_num - 1) % 12
sign = SIGNS[sign_idx]
years = 5 if i < 4 else 3 # 角宫5年,其他3年
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': f'House {house_num} ({sign})', 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 7. Tara Dasha(星宿推运)
# =============================================================================
def calc_tara_dasha(birth_date: datetime, moon_nak_idx: int) -> List[Dict]:
"""Tara Dasha — 从出生星宿开始的推运"""
results = []
current = birth_date
for i in range(27):
nak_idx = (moon_nak_idx + i) % 27
nak = NAKSHATRAS[nak_idx]
years = 3
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': nak, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# 8. Shoola Dasha(尖刺推运)
# =============================================================================
def calc_shoola_dasha(birth_date: datetime, moon_sign_idx: int) -> List[Dict]:
"""Shoola Dasha — 基于月亮星座的9年周期"""
results = []
current = birth_date
for i in range(9):
sign_idx = (moon_sign_idx + i) % 12
sign = SIGNS[sign_idx]
years = 9 - i if i < 9 else 1
end_date = current + timedelta(days=years * YEAR_DAYS)
results.append({
'lord': sign, 'years': years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
# =============================================================================
# Dasha注册表
# =============================================================================
DASHA_REGISTRY = {
# Nakshatra-based
'vimshottari': {'name': 'Vimshottari', 'years': 120, 'type': 'nakshatra'},
'ashtottari': {'name': 'Ashtottari', 'years': 108, 'type': 'nakshatra'},
'yogini': {'name': 'Yogini', 'years': 36, 'type': 'nakshatra'},
'kalachakra': {'name': 'Kalachakra', 'years': 94, 'type': 'nakshatra'},
'tara': {'name': 'Tara Dasha', 'years': 81, 'type': 'nakshatra'},
'shodasottari': {'name': 'Shodasottari', 'years': 128, 'type': 'nakshatra'},
'panchottari': {'name': 'Panchottari', 'years': 105, 'type': 'nakshatra'},
'satabdika': {'name': 'Satabdika', 'years': 100, 'type': 'nakshatra'},
# Rasi-based
'chara': {'name': 'Chara (Jaimini)', 'years': 36, 'type': 'rasi'},
'narayana': {'name': 'Narayana', 'years': 36, 'type': 'rasi'},
'shoola': {'name': 'Shoola Dasha', 'years': 9, 'type': 'rasi'},
'sudasa': {'name': 'Sudasa', 'years': 36, 'type': 'rasi'},
'drig': {'name': 'Drig Dasha', 'years': 36, 'type': 'rasi'},
'sthira': {'name': 'Sthira Dasha', 'years': 36, 'type': 'rasi'},
'mandooka': {'name': 'Mandooka Dasha', 'years': 36, 'type': 'rasi'},
'lagnamsaka': {'name': 'Lagnamsaka Dasha', 'years': 36, 'type': 'rasi'},
'yogardha': {'name': 'Yogardha Dasha', 'years': 36, 'type': 'rasi'},
'brahma': {'name': 'Brahma Dasha', 'years': 36, 'type': 'rasi'},
# Conditional/Tithi-based
'dwisaptati': {'name': 'Dwisaptati Sama', 'years': 72, 'type': 'conditional'},
'shattrimsa': {'name': 'Shattrimsa Sama', 'years': 36, 'type': 'conditional'},
'dwadashottari': {'name': 'Dwadashottari', 'years': 112, 'type': 'conditional'},
'shasti_hayani': {'name': 'Shasti-Hayani', 'years': 60, 'type': 'conditional'},
'chaturaaseeti': {'name': 'Chaturaaseeti Sama', 'years': 84, 'type': 'conditional'},
'tithi_ashtottari': {'name': 'Tithi Ashtottari', 'years': 108, 'type': 'conditional'},
'tithi_yogini': {'name': 'Tithi Yogini', 'years': 36, 'type': 'conditional'},
'patyayini': {'name': 'Patyayini', 'years': 36, 'type': 'conditional'},
# Varga/Bhava-based
'navamsa': {'name': 'Navamsa Dasha', 'years': 36, 'type': 'varga'},
'kendradi': {'name': 'Kendradi', 'years': 38, 'type': 'bhav'},
'paryaaya': {'name': 'Paryaaya Dasha', 'years': 36, 'type': 'varga'},
# Special
'rashmi': {'name': 'Rashmi Dasha', 'years': 36, 'type': 'special'},
'naisargika': {'name': 'Naisargika Dasha', 'years': 120, 'type': 'special'},
'aayu': {'name': 'Aayu Dasha', 'years': 36, 'type': 'special'},
'karaka': {'name': 'Karaka Dasha', 'years': 36, 'type': 'special'},
'panchasvara': {'name': 'Panchasvara Dasha', 'years': 36, 'type': 'special'},
'sudarsana': {'name': 'Sudarsana Chakra Dasha', 'years': 36, 'type': 'special'},
}
def get_available_dashas() -> List[str]:
return list(DASHA_REGISTRY.keys())
def get_dasha_info(name: str) -> Dict:
return DASHA_REGISTRY.get(name, {})
# =============================================================================
# v6.7.6: 通用Dasha函数 — 所有35种注册Dasha现在都可计算
# =============================================================================
def calc_generic_dasha(birth_date: datetime, lords: List[str], years: List[int],
lord_names: Dict = None) -> List[Dict]:
"""通用Dasha计算器 — 任意lord序列和年限组合"""
results = []
current = birth_date
for lord, yrs in zip(lords, years):
end_date = current + timedelta(days=yrs * YEAR_DAYS)
results.append({
'lord': lord_names.get(lord, lord) if lord_names else lord,
'years': yrs,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return results
def calc_shodasottari(birth_date, moon_nak_idx):
"""Shodasottari Dasha (128年周期) — 16种大运"""
lords = DASHA_ORDER * 2
return calc_generic_dasha(birth_date, lords[:16], [8]*16)
def calc_panchottari(birth_date, moon_nak_idx):
"""Panchottari Dasha (105年周期)"""
return calc_generic_dasha(birth_date, DASHA_ORDER, [12]*9)[:9]
def calc_satabdika(birth_date, moon_nak_idx):
"""Satabdika Dasha (100年周期)"""
return calc_generic_dasha(birth_date, DASHA_ORDER, [11]*9)[:9]
def calc_chaturaaseeti(birth_date, moon_nak_idx):
"""Chaturaaseeti Sama Dasha (84年周期)"""
return calc_generic_dasha(birth_date, DASHA_ORDER, [9]*9)[:9]
def calc_tithi_ashtottari(birth_date, tithi_num):
"""Tithi Ashtottari Dasha (108年周期)"""
return calc_generic_dasha(birth_date, DASHA_ORDER, [8,12,6,10,7,18,16,19,17])[:9]
def calc_tithi_yogini(birth_date, tithi_num):
"""Tithi Yogini Dasha (36年周期)"""
return calc_generic_dasha(birth_date, YOGINI_LORDS, YOGINI_YEARS)
def calc_patyayini(birth_date, asc_sign_idx):
"""Patyayini Dasha (36年周期)"""
return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9)
def calc_rashmi(birth_date, moon_nak_idx):
"""Rashmi Dasha (36年周期)"""
return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9)
def calc_naisargika(birth_date):
"""Naisargika Dasha (120年周期) — 自然生命周期"""
lords = ['Moon','Mars','Mercury','Venus','Jupiter','Sun','Saturn']
years_list = [1,2,9,20,18,20,50]
return calc_generic_dasha(birth_date, lords, years_list)
# Dasha计算函数注册表
DASHA_CALCULATORS = {
'kalachakra': calc_kalachakra_dasha,
'narayana': calc_narayana_dasha,
'yogini': calc_yogini_dasha,
'shasti_hayani': calc_shasti_hayani_dasha,
'navamsa': calc_navamsa_dasha,
'kendradi': calc_kendradi_dasha,
'tara': calc_tara_dasha,
'shoola': calc_shoola_dasha,
'shodasottari': calc_shodasottari,
'panchottari': calc_panchottari,
'satabdika': calc_satabdika,
'chaturaaseeti': calc_chaturaaseeti,
'tithi_ashtottari': calc_tithi_ashtottari,
'tithi_yogini': calc_tithi_yogini,
'patyayini': calc_patyayini,
'rashmi': calc_rashmi,
'naisargika': calc_naisargika,
# 此下为简化映射(使用通用函数或现有函数)
'dwisaptati': calc_generic_dasha,
'shattrimsa': calc_generic_dasha,
'dwadashottari': calc_generic_dasha,
'sudasa': calc_generic_dasha,
'drig': calc_generic_dasha,
'sthira': calc_generic_dasha,
'mandooka': calc_generic_dasha,
'lagnamsaka': calc_generic_dasha,
'yogardha': calc_generic_dasha,
'brahma': calc_generic_dasha,
'paryaaya': calc_generic_dasha,
'aayu': calc_generic_dasha,
'karaka': calc_generic_dasha,
'panchasvara': calc_generic_dasha,
'sudarsana': calc_generic_dasha,
}
def calc_any_dasha(name: str, birth_date, **kwargs) -> List[Dict]:
"""根据名称计算任意Dasha"""
calc_fn = DASHA_CALCULATORS.get(name)
if calc_fn == calc_generic_dasha:
return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9)
if calc_fn:
try:
if name == 'kalachakra':
return calc_fn(birth_date, kwargs.get('moon_nak_idx', 0), kwargs.get('moon_pada', 1))
if name in {'yogini', 'tara', 'shodasottari', 'panchottari', 'satabdika', 'chaturaaseeti', 'rashmi'}:
return calc_fn(birth_date, kwargs.get('moon_nak_idx', 0))
if name == 'narayana':
return calc_fn(birth_date, kwargs.get('asc_sign_idx', 0))
if name == 'shoola':
return calc_fn(birth_date, kwargs.get('moon_sign_idx', 0))
if name == 'shasti_hayani':
return calc_fn(birth_date, kwargs.get('sun_sign_idx', 0))
if name == 'navamsa':
return calc_fn(birth_date, kwargs.get('d9_asc_sign_idx', kwargs.get('asc_sign_idx', 0)))
if name == 'kendradi':
return calc_fn(birth_date, kwargs.get('asc_sign_idx', 0))
if name in {'tithi_ashtottari', 'tithi_yogini'}:
return calc_fn(birth_date, kwargs.get('tithi_num', 1))
if name == 'patyayini':
return calc_fn(birth_date, kwargs.get('asc_sign_idx', 0))
if name == 'naisargika':
return calc_fn(birth_date)
return calc_fn(birth_date, **kwargs)
except Exception:
return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9)
return []
+22 -57
View File
@@ -6349,26 +6349,6 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
except (TypeError, ValueError):
return None
def _fallback_dasha_periods(self, birth_dt, dasha_key, info):
extended_dashas = _load_local_module('extended_dashas')
DASHA_ORDER = extended_dashas.DASHA_ORDER
cycle_years = float(info.get('years') or 36)
if cycle_years <= 0:
cycle_years = 36
period_years = round(cycle_years / len(DASHA_ORDER), 2)
periods = []
current = birth_dt
for lord in DASHA_ORDER:
end_date = current + timedelta(days=period_years * 365.25)
periods.append({
'lord': lord,
'years': period_years,
'start': current.strftime('%Y-%m-%d'),
'end': end_date.strftime('%Y-%m-%d'),
})
current = end_date
return periods
def _import_chart_text(self, body):
filename = str(body.get('filename', ''))[:160]
text = body.get('text')
@@ -6534,10 +6514,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
ayanamsa=ayanamsa_name,
)
# Dasha清单
extended_dashas = _load_local_module('extended_dashas')
dashas = extended_dashas.get_available_dashas()
dasha_list = [{'key': k, 'name': extended_dashas.DASHA_REGISTRY[k]['name'], 'years': extended_dashas.DASHA_REGISTRY[k]['years'], 'type': extended_dashas.DASHA_REGISTRY[k]['type']} for k in dashas]
# Dasha清单(元数据与旧注册表逐项相同,计算不走占位表)
dasha_registry = _load_local_module('dasha_registry')
dasha_list = dasha_registry.available_dasha_rows()
try:
jaimini = _load_local_module('jaimini')
@@ -7266,8 +7245,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
if not isinstance(dasha_key, str):
raise BadRequest('dasha must be a string')
dasha_key = dasha_key.strip().lower()
extended_dashas = _load_local_module('extended_dashas')
info = extended_dashas.DASHA_REGISTRY.get(dasha_key)
dasha_registry = _load_local_module('dasha_registry')
info = dasha_registry.DASHA_REGISTRY_METADATA.get(dasha_key)
if not info:
raise BadRequest('Unknown dasha system')
@@ -7280,24 +7259,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
moon_lon = self._planet_lon(planets, 'Moon')
if moon_lon is None:
moon_lon = self._normalize_degree(body, 'moon_lon', 0)
sun_lon = self._planet_lon(planets, 'Sun')
if sun_lon is None:
sun_lon = self._normalize_degree(body, 'sun_lon', 0)
nak_size = 360 / 27
moon_nak_idx = int(moon_lon / nak_size) % 27
moon_pada = int((moon_lon % nak_size) / (nak_size / 4)) + 1
moon_sign_idx = int(moon_lon / 30) % 12
sun_sign_idx = int(sun_lon / 30) % 12
asc_sign_idx = self._sign_idx_from_value(
body.get('asc_sign_idx', ascendant.get('sign_idx', ascendant.get('sign'))),
0,
)
d9_asc_sign_idx = self._sign_idx_from_value(body.get('d9_asc_sign_idx', asc_sign_idx), asc_sign_idx)
tithi_num = self._get_int(body, 'tithi_num', 1, 1, 30)
vimshottari_analysis = None
canonical_dasha = None
computed = None
if dasha_key == 'vimshottari':
calculation_service = _load_local_module('domain_calculation_service')
canonical_dasha = calculation_service.compute_vimshottari_timeline(
@@ -7317,22 +7282,18 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
body.get('today') or body.get('current_date'),
)
else:
periods = extended_dashas.calc_any_dasha(
dasha_key,
birth_dt,
moon_nak_idx=moon_nak_idx,
moon_pada=moon_pada,
asc_sign_idx=asc_sign_idx,
moon_sign_idx=moon_sign_idx,
sun_sign_idx=sun_sign_idx,
d9_asc_sign_idx=d9_asc_sign_idx,
tithi_num=tithi_num,
)
calc_fn = extended_dashas.DASHA_CALCULATORS.get(dasha_key)
precision = 'generic' if not calc_fn or calc_fn.__name__ == 'calc_generic_dasha' else 'calculator'
if not periods:
periods = self._fallback_dasha_periods(birth_dt, dasha_key, info)
precision = 'generic'
try:
computed = dasha_registry.compute_dasha(
dasha_key,
birth_dt=birth_dt,
planets=planets,
ascendant=ascendant or {},
body=body,
)
except dasha_registry.DashaNotImplemented as exc:
raise BadRequest(str(exc)) from exc
periods = computed['periods']
precision = 'engine_module'
result = {
'success': True,
@@ -7350,6 +7311,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
result['birth_balance'] = canonical_dasha['birth_balance']
result['calculation_contract'] = canonical_dasha['calculation_contract']
result['result_hash'] = canonical_dasha['result_hash']
if isinstance(computed, dict):
for field in ('algorithm_label', 'algorithm_note', 'profile'):
if computed.get(field):
result[field] = computed[field]
return result
def _compute_vimshottari_analysis_layer(self, birth_dt, moon_lon, current_date=None):
+8 -2
View File
@@ -291,7 +291,10 @@ NAKSHATRA_LIST = [
("Purva Bhadrapada", "Jupiter", 16), ("Uttara Bhadrapada", "Saturn", 19), ("Revati", "Mercury", 17),
]
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}
try:
from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座', 'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座', 'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座', 'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'}
SIGN_LORDS = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'}
@@ -16532,7 +16535,10 @@ def cmd_full_reading(args):
try:
from ashtottari_dasha import calculate_ashtottari_dasha
ashtottari_data = calculate_ashtottari_dasha(birth_info_for_alt_dasha)
ashtottari_data = calculate_ashtottari_dasha({
**birth_info_for_alt_dasha,
'moon_longitude': moon_lon,
})
report['modules']['ashtottari_dasha'] = ashtottari_data
except Exception as alt_e:
ashtottari_data = {'error': str(alt_e)}
+5 -1
View File
@@ -33,8 +33,12 @@ NAKSHATRAS = [
]
# Vimshottari年限(KP系统使用相同比例划分sublord)
VIMSHOTTARI_DURATION = [7, 20, 6, 10, 7, 18, 16, 19, 17]
KP_LORDS = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"]
try:
from dasha_common import VIMSHOTTARI_YEARS as _SHARED_VIMSHOTTARI_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_YEARS as _SHARED_VIMSHOTTARI_YEARS
VIMSHOTTARI_DURATION = [_SHARED_VIMSHOTTARI_YEARS[lord] for lord in KP_LORDS]
STAR_LORDS = KP_LORDS * 3 # 27 Nakshatras = 3 cycles of 9 lords
VIMSHOTTARI_YEARS = dict(zip(KP_LORDS, VIMSHOTTARI_DURATION))
+4 -2
View File
@@ -305,8 +305,10 @@ def _calc_ashtottari_antardasha(md_lord: str, md_years: float) -> List[Dict]:
# ============================================================================
# Vimshottari Dasha 顺序(9星循环)
VIMSHOTTARI_ORDER = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury']
VIMSHOTTARI_YEARS = {'Ketu': 7, 'Venus': 20, 'Sun': 6, 'Moon': 10, 'Mars': 7, 'Rahu': 18, 'Jupiter': 16, 'Saturn': 19, 'Mercury': 17}
try:
from dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS
def calc_nakshatra_dasha_breakdown(
+5 -7
View File
@@ -13,8 +13,10 @@ from typing import Any
try:
from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children
from scripts.dasha_common import nakshatra_seeded_birth_balance
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from source_bounded_child_periods import expand_parent_seeded_equal_split_children
from dasha_common import nakshatra_seeded_birth_balance
YEAR_DAYS = 365.25636
@@ -67,13 +69,9 @@ def _sequence_from(starting_lord: str) -> list[tuple[str, int]]:
def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]:
moon_longitude = float(moon_longitude) % 360.0
nakshatra_index = int(moon_longitude // NAKSHATRA_SPAN)
offset = (nakshatra_index - ANURADHA_INDEX) % 27
starting_lord = DASHA_SEQUENCE[offset % len(DASHA_SEQUENCE)][0]
fraction_elapsed = (moon_longitude % NAKSHATRA_SPAN) / NAKSHATRA_SPAN
remaining_years = _YEARS_BY_LORD[starting_lord] * (1.0 - fraction_elapsed)
return starting_lord, remaining_years, nakshatra_index
return nakshatra_seeded_birth_balance(
moon_longitude, anchor_index=ANURADHA_INDEX, sequence=DASHA_SEQUENCE,
)
def _iso(value: datetime) -> str:
+4 -2
View File
@@ -235,8 +235,10 @@ NAK_SPAN = 360.0 / 27.0 # 13.333...° per nakshatra
SUB_SPAN = NAK_SPAN / 9.0 # ~1.481° per sub (每个sub由nakshatra lord的一个行星段构成)
# Vimshottari年数用于计算sub比例
VIM_DURATIONS = {'Ketu':7,'Venus':20,'Sun':6,'Moon':10,'Mars':7,
'Rahu':18,'Jupiter':16,'Saturn':19,'Mercury':17}
try:
from dasha_common import VIMSHOTTARI_YEARS as VIM_DURATIONS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_YEARS as VIM_DURATIONS
VIM_TOTAL = 120.0
+5 -5
View File
@@ -12,8 +12,10 @@ from typing import Any
try:
from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children
from scripts.dasha_common import nakshatra_seeded_birth_balance
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from source_bounded_child_periods import expand_parent_seeded_equal_split_children
from dasha_common import nakshatra_seeded_birth_balance
YEAR_DAYS = 365.25636
@@ -50,11 +52,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]:
def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]:
moon = float(moon_longitude) % 360.0
nakshatra = int(moon // NAKSHATRA_SPAN)
lord = DASHA_SEQUENCE[((nakshatra - REVATI_INDEX) % 27) % len(DASHA_SEQUENCE)][0]
balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN)
return lord, balance, nakshatra
return nakshatra_seeded_birth_balance(
moon_longitude, anchor_index=REVATI_INDEX, sequence=DASHA_SEQUENCE,
)
def calculate_satabdika_dasha(birth_info: dict[str, Any]) -> dict[str, Any]:
+8 -5
View File
@@ -6,6 +6,11 @@ from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any
try:
from scripts.dasha_common import nakshatra_seeded_birth_balance
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from dasha_common import nakshatra_seeded_birth_balance
YEAR_DAYS = 365.25636
NAKSHATRA_SPAN = 360.0 / 27.0
@@ -49,11 +54,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]:
def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]:
moon = float(moon_longitude) % 360.0
nakshatra = int(moon // NAKSHATRA_SPAN)
lord = DASHA_SEQUENCE[((nakshatra - SRAVANA_INDEX) % 27) % len(DASHA_SEQUENCE)][0]
balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN)
return lord, balance, nakshatra
return nakshatra_seeded_birth_balance(
moon_longitude, anchor_index=SRAVANA_INDEX, sequence=DASHA_SEQUENCE,
)
def calculate_shattrimshatsama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]:
+5 -7
View File
@@ -14,8 +14,10 @@ from typing import Any
try:
from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children
from scripts.dasha_common import nakshatra_seeded_birth_balance
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from source_bounded_child_periods import expand_parent_seeded_equal_split_children
from dasha_common import nakshatra_seeded_birth_balance
YEAR_DAYS = 365.25636
@@ -79,13 +81,9 @@ def _sequence_from(starting_lord: str) -> list[tuple[str, int]]:
def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]:
moon_longitude = float(moon_longitude) % 360.0
nakshatra_index = int(moon_longitude // NAKSHATRA_SPAN)
offset = (nakshatra_index - PUSHYA_INDEX) % 27
starting_lord = DASHA_SEQUENCE[offset % len(DASHA_SEQUENCE)][0]
fraction_elapsed = (moon_longitude % NAKSHATRA_SPAN) / NAKSHATRA_SPAN
remaining_years = _YEARS_BY_LORD[starting_lord] * (1.0 - fraction_elapsed)
return starting_lord, remaining_years, nakshatra_index
return nakshatra_seeded_birth_balance(
moon_longitude, anchor_index=PUSHYA_INDEX, sequence=DASHA_SEQUENCE,
)
def _iso(value: datetime) -> str:
+11 -4
View File
@@ -26,6 +26,13 @@ import logging
logger = logging.getLogger(__name__)
try:
from dasha_common import VIMSHOTTARI_ORDER as _VIMSHOTTARI_ORDER
from dasha_common import VIMSHOTTARI_YEARS as _VIMSHOTTARI_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_ORDER as _VIMSHOTTARI_ORDER
from scripts.dasha_common import VIMSHOTTARI_YEARS as _VIMSHOTTARI_YEARS
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
SIGN_LORDS = {'Aries':'Mars','Taurus':'Venus','Gemini':'Mercury','Cancer':'Moon',
@@ -342,8 +349,8 @@ def calc_mudda_dasha(asc_sign_idx: int,
规则: 从年度守护星开始,按Vimshottari顺序排列
每个大运按比例分配12个月
"""
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}
DASHA_ORDER = list(_VIMSHOTTARI_ORDER)
DASHA_YEARS = _VIMSHOTTARI_YEARS
start_idx = DASHA_ORDER.index(varsha_lord) if varsha_lord in DASHA_ORDER else 0
@@ -395,8 +402,8 @@ def calc_mudda_with_dasha_balance(
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}
dasha_order = list(_VIMSHOTTARI_ORDER)
dasha_years = _VIMSHOTTARI_YEARS
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}
+4 -4
View File
@@ -24,10 +24,10 @@ import math
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
DASHA_YEARS = {
"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10,
"Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17
}
try:
from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
EXPECTED_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']
+6 -2
View File
@@ -13,8 +13,12 @@ from typing import Any
ROOT = Path(__file__).resolve().parents[1]
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}
try:
from dasha_common import VIMSHOTTARI_ORDER as DASHA_ORDER
from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.dasha_common import VIMSHOTTARI_ORDER as DASHA_ORDER
from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS
def _parse_date(value: str) -> datetime: