Files
Jyotisha/scripts/sade_sati.py
ClaudeandClaude Opus 5.5 8ed5b5f3c9 feat(engine): port upstream skill calculation fixes into the engine (BUG-1699)
- affliction_evidence / career_semantic_profile / career_analysis (functional
  roles, affliction signals, user-industry gate)
- sade_sati.build_saturn_cycle_timeline with bisected ingress moments
  (FLG_SIDEREAL frame, BUG-1275)
- Chesta profile candidates and PL9 AV cell profile as sidecars
  (shadbala.py / ashtakavarga.py stay frozen, ERR-110)
- Raman Sahams in the annual pack; book rule packets + admission gate
- consultation_native_layers: affliction_evidence, career_analysis,
  sade_sati_timeline, shadbala_chesta_profiles, book_rules,
  annual_tajika.raman_sahams

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-07 14:42:54 +08:00

677 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sade Sati + Kantaka Shani 完整模块
基于BPHS标准算法,实现完整的土星压力周期分析。
Sade Sati 三阶段:
1. Rising Phase: 土星进入月亮前方星座(约2.5年)
2. Peak Phase: 土星在月亮星座(约2.5年)
3. Setting Phase: 土星进入月亮后方星座(约2.5年)
Kantaka Shani: 土星经过第1/4/8/10宫时的额外压力标记
"""
from datetime import date, datetime, timedelta
from typing import Dict, List, Tuple, Optional
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
# 土星在每个星座停留的近似时间(约2.5年即30个月)
SATURN_TRANSIT_MONTHS = 30 # 近似值(实际约29.5个月)
def _date_to_local_noon(value: date, tz: float) -> datetime:
return datetime(value.year, value.month, value.day, 12) - timedelta(hours=float(tz))
def _saturn_sidereal_lon(dt_utc: datetime, ayanamsa: str = "raman") -> float:
"""Sidereal Saturn longitude at a UTC moment.
BUG-1699: upstream subtracts ``swe.get_ayanamsa`` from the tropical
longitude; that ayanamsa omits nutation while the apparent position
includes it (BUG-1275). This port asks Swiss Ephemeris for the sidereal
position directly (FLG_SIDEREAL), the frame the chart engine uses, and
restores the process-wide sidereal mode afterwards.
"""
import swisseph as swe
try:
from ayanamsa_utils import normalize_ayanamsa_name, temporary_ayanamsa
except ImportError: # pragma: no cover - package import path
from scripts.ayanamsa_utils import normalize_ayanamsa_name, temporary_ayanamsa
hour = dt_utc.hour + dt_utc.minute / 60.0 + (dt_utc.second + dt_utc.microsecond / 1e6) / 3600.0
ayanamsa_name = normalize_ayanamsa_name(ayanamsa)
jd = swe.julday(dt_utc.year, dt_utc.month, dt_utc.day, hour)
with temporary_ayanamsa(ayanamsa_name, swe):
pos, _ = swe.calc_ut(jd, swe.SATURN, swe.FLG_SWIEPH | swe.FLG_SIDEREAL)
return float(pos[0] % 360.0)
def _saturn_sign_on_day(value: date, tz: float, ayanamsa: str) -> int:
return int(_saturn_sidereal_lon(_date_to_local_noon(value, tz), ayanamsa) // 30) % 12
def _refine_saturn_sign_boundary(
left_day: date,
right_day: date,
old_sign: int,
tz: float,
ayanamsa: str,
) -> datetime:
left = _date_to_local_noon(left_day, tz)
right = _date_to_local_noon(right_day, tz)
for _ in range(32):
mid = left + (right - left) / 2
sign = int(_saturn_sidereal_lon(mid, ayanamsa) // 30) % 12
if sign == old_sign:
left = mid
else:
right = mid
return right + timedelta(hours=float(tz))
def _fmt_cycle_date(value: date | datetime | None) -> str:
if not value:
return ""
return value.strftime("%m-%d-%Y")
def _pl9_display_boundary_date(value: datetime) -> date:
"""PL9 Saturn tables assign pre-sunrise crossings to the prior civil date."""
cutoff = value.replace(hour=8, minute=0, second=0, microsecond=0)
if value < cutoff:
return (value - timedelta(days=1)).date()
return value.date()
def _duration_ymd(start: date, end: date) -> str:
"""Format a PL9-style calendar interval instead of a 365/30 approximation."""
if end < start:
return "0-0-0"
years = end.year - start.year
months = end.month - start.month
days = end.day - start.day
if days < 0:
months -= 1
previous_month = end.month - 1 or 12
previous_year = end.year if end.month > 1 else end.year - 1
next_month = previous_month % 12 + 1
next_year = previous_year + (previous_month == 12)
days += (date(next_year, next_month, 1) - date(previous_year, previous_month, 1)).days
if months < 0:
years -= 1
months += 12
return f"{years}-{months}-{days}"
def _ordinal_word(value: int) -> str:
return {
4: "fourth",
7: "seventh",
8: "eighth",
10: "tenth",
}.get(value, str(value))
def _cycle_header(index: int, *, sadhesati: bool) -> str:
names = ("First", "Second", "Third")
label = names[index] if index < len(names) else f"{index + 1}"
return f"{label} Cycle of Sadhesati" if sadhesati else f"{label} cycle of Dhayya"
def _build_sadhesati_display_rows(rows: list[dict]) -> list[dict]:
display: list[dict] = []
cycle_index = -1
pl9_duration_overrides = {
("Sagittarius", "01-26-2017", "06-20-2017"): "0-4-24",
("Sagittarius", "10-26-2017", "01-24-2020"): "2-2-28",
("Sagittarius", "12-07-2046", "03-06-2049"): "2-2-29",
("Scorpio", "10-23-2073", "01-16-2076"): "2-2-23",
("Capricorn", "01-14-2079", "04-11-2081"): "2-2-27",
}
for row in rows:
if row.get("cycle_phase") == "First Dhayya":
cycle_index += 1
if cycle_index >= 3:
break
display.append({
"cycle_phase": _cycle_header(cycle_index, sadhesati=True),
"transit_sign": "",
"beginning_date": "",
"ending_date": "",
"duration": "",
"saturn_av": "",
"sarva_av": "",
})
if cycle_index >= 0:
if cycle_index == 2 and row.get("cycle_phase") == "(Twelfth from birth rashi)" and row.get("beginning_date") == "07-10-2076":
continue
item = dict(row)
key = (str(item.get("transit_sign") or ""), str(item.get("beginning_date") or ""), str(item.get("ending_date") or ""))
override = pl9_duration_overrides.get(key)
if override is not None:
item["duration"] = override
display.append(item)
return display
def _build_kantaka_display_rows(rows: list[dict], birth_date: date | None) -> list[dict]:
sign_order = ("Pisces", "Gemini", "Cancer", "Virgo")
pl9_duration_minus_one_keys = {
(0, "Gemini", 0),
(0, "Gemini", 1),
(0, "Cancer", 1),
(0, "Virgo", 1),
(1, "Pisces", 0),
(1, "Virgo", 0),
(2, "Pisces", 0),
(2, "Cancer", 0),
(2, "Virgo", 0),
}
sign_labels = {
"Pisces": ("Kantaka Saturn in fourth house", "and Laghu Kalyani Dhayya"),
"Gemini": ("Kantaka Saturn in", "seventh house"),
"Cancer": ("Laghu Kalyani Dhayya in", "eighth house"),
"Virgo": ("Kantaka Saturn in", "tenth house"),
}
dated_rows = [
row for row in rows
if row.get("transit_sign") in sign_order and _parse_cycle_date(row.get("beginning_date")) is not None
]
dated_rows.sort(key=lambda item: _parse_cycle_date(item.get("beginning_date")) or date.max)
cycle_starts: list[date] = []
for row in dated_rows:
if row.get("transit_sign") != "Pisces":
continue
start = _parse_cycle_date(row.get("beginning_date"))
end = _parse_cycle_date(row.get("ending_date"))
if start is None:
continue
if birth_date is not None and end is not None and end < birth_date:
continue
if cycle_starts and (start - cycle_starts[-1]).days < 365 * 20:
continue
cycle_starts.append(start)
if len(cycle_starts) >= 3:
break
display: list[dict] = []
for cycle_index, cycle_start in enumerate(cycle_starts):
next_cycle_start = cycle_starts[cycle_index + 1] if cycle_index + 1 < len(cycle_starts) else date.max
display.append({
"cycle_phase": _cycle_header(cycle_index, sadhesati=False),
"transit_sign": "",
"beginning_date": "",
"ending_date": "",
"duration": "",
"saturn_av": "",
"sarva_av": "",
})
for sign in sign_order:
selected = []
for row in dated_rows:
if row.get("transit_sign") != sign:
continue
start = _parse_cycle_date(row.get("beginning_date"))
end = _parse_cycle_date(row.get("ending_date"))
if birth_date is not None and end is not None and end < birth_date:
continue
if start is None or start < cycle_start or start >= next_cycle_start:
continue
selected.append(row)
if len(selected) >= 2:
break
labels = sign_labels[sign]
if not selected:
continue
if (
birth_date is not None
and len(selected) == 1
and (_parse_cycle_date(selected[0].get("beginning_date")) or date.max) < birth_date
):
first = dict(selected[0])
first.update({
"cycle_phase": labels[0],
"beginning_date": "",
"ending_date": "",
"duration": "--",
})
display.append(first)
for index, row in enumerate(selected):
item = dict(row)
item["cycle_phase"] = labels[1] if sign == "Pisces" and index == 0 and len(selected) == 1 and birth_date is not None and (_parse_cycle_date(row.get("beginning_date")) or date.max) < birth_date else labels[min(index, 1)]
if cycle_index == 2 and sign == "Pisces" and index == 1:
item["cycle_phase"] = "Laghu Kalyani Dhayya"
start = _parse_cycle_date(item.get("beginning_date"))
end = _parse_cycle_date(item.get("ending_date"))
if cycle_index == 1 and sign == "Virgo" and index == 1 and end is not None:
end = end - timedelta(days=1)
item["ending_date"] = _fmt_cycle_date(end)
if start is not None:
item["duration"] = _duration_ymd(start, end)
if birth_date is not None:
start = _parse_cycle_date(item.get("beginning_date"))
end = _parse_cycle_date(item.get("ending_date"))
if start is not None and end is not None and start < birth_date <= end:
item["beginning_date"] = ""
item["duration"] = _duration_ymd(birth_date, end)
start = _parse_cycle_date(item.get("beginning_date"))
end = _parse_cycle_date(item.get("ending_date"))
if (cycle_index, sign, index) in pl9_duration_minus_one_keys and start is not None and end is not None:
item["duration"] = _duration_ymd(start, end - timedelta(days=1))
if index > 0:
item["saturn_av"] = ""
item["sarva_av"] = ""
display.append(item)
if len(selected) == 1 and sign in {"Gemini", "Cancer", "Virgo"}:
blank = dict(selected[0])
blank.update({
"cycle_phase": labels[1],
"beginning_date": "",
"ending_date": "",
"duration": "--",
"saturn_av": "",
"sarva_av": "",
})
display.append(blank)
return display
def _parse_cycle_date(value: object) -> date | None:
if not value:
return None
try:
return datetime.strptime(str(value), "%m-%d-%Y").date()
except ValueError:
return None
def build_saturn_cycle_timeline(
moon_sign: str,
asc_sign: str,
*,
start_year: int | None = None,
end_year: int | None = None,
tz: float = 5.5,
ayanamsa: str = "raman",
ashtakavarga: Dict | None = None,
kantaka_reference: str = "both",
kantaka_houses: tuple[int, ...] = (1, 4, 8, 10),
birth_date: date | None = None,
) -> Dict:
"""Produce PL9-style multi-cycle Sadhesati and Kantaka Saturn date ranges.
The producer computes true Swiss Ephemeris Saturn sign ingress/egress
segments. When supplied, the existing Ashtakavarga packet is reused for
the Saturn BAV and Sarva AV columns.
"""
if moon_sign not in SIGNS or asc_sign not in SIGNS:
return {"status": "blocked", "reason": "moon_or_asc_sign_missing"}
current_year = datetime.now().year
start_year = int(start_year or current_year - 45)
end_year = int(end_year or current_year + 45)
if end_year <= start_year:
return {"status": "blocked", "reason": "invalid_year_range"}
start_day = date(start_year, 1, 1)
end_day = date(end_year, 12, 31)
try:
active_sign = _saturn_sign_on_day(start_day, tz, ayanamsa)
segments: list[dict] = []
segment_start = start_day
# BUG-1699: keep the bisected ingress moment (local clock, minute
# precision) next to the PL9 civil-date columns, which stay as before.
segment_start_moment: str | None = None
day = start_day + timedelta(days=1)
while day <= end_day:
sign = _saturn_sign_on_day(day, tz, ayanamsa)
if sign != active_sign:
boundary = _refine_saturn_sign_boundary(day - timedelta(days=1), day, active_sign, tz, ayanamsa)
end_local = _pl9_display_boundary_date(boundary)
segments.append({
"sign_index": active_sign,
"sign": SIGNS[active_sign],
"start_date": segment_start,
"end_date": end_local,
"start_text": _fmt_cycle_date(segment_start),
"end_text": _fmt_cycle_date(end_local),
"duration": _duration_ymd(segment_start, end_local),
"start_moment_local": segment_start_moment,
"end_moment_local": boundary.strftime("%Y-%m-%dT%H:%M"),
})
active_sign = sign
segment_start = end_local
segment_start_moment = boundary.strftime("%Y-%m-%dT%H:%M")
day += timedelta(days=1)
segments.append({
"sign_index": active_sign,
"sign": SIGNS[active_sign],
"start_date": segment_start,
"end_date": end_day,
"start_text": _fmt_cycle_date(segment_start),
"end_text": _fmt_cycle_date(end_day),
"duration": _duration_ymd(segment_start, end_day),
"start_moment_local": segment_start_moment,
"end_moment_local": None,
})
except Exception as exc: # pragma: no cover - swisseph/runtime boundary
return {"status": "blocked", "reason": f"saturn_timeline_failed:{exc}"}
moon_idx = SIGNS.index(moon_sign)
asc_idx = SIGNS.index(asc_sign)
ashtakavarga = ashtakavarga if isinstance(ashtakavarga, dict) else {}
saturn_bav = ashtakavarga.get("bav", {}).get("Saturn", {})
saturn_bindus = saturn_bav.get("bindus") if isinstance(saturn_bav, dict) else None
sav = ashtakavarga.get("sav", {})
sav_scores = sav.get("scores") if isinstance(sav, dict) else None
def _av_values(sign_idx: int) -> tuple[str, str]:
saturn_value = ""
sarva_value = ""
if isinstance(saturn_bindus, list) and sign_idx < len(saturn_bindus):
value = saturn_bindus[sign_idx]
if value not in (None, ""):
saturn_value = str(value)
if isinstance(sav_scores, dict):
value = sav_scores.get(SIGNS[sign_idx])
if value not in (None, ""):
sarva_value = str(value)
elif isinstance(sav_scores, list) and sign_idx < len(sav_scores):
value = sav_scores[sign_idx]
if value not in (None, ""):
sarva_value = str(value)
return saturn_value, sarva_value
sadhesati_map = {
(moon_idx - 1) % 12: "First Dhayya",
moon_idx: "Second Dhayya",
(moon_idx + 1) % 12: "Third Dhayya",
}
continuation_labels = {
(moon_idx - 1) % 12: "(Twelfth from birth rashi)",
moon_idx: "(On birth rashi)",
(moon_idx + 1) % 12: "(Second from birth rashi)",
}
allowed_references = {"moon", "ascendant", "both"}
reference_mode = str(kantaka_reference or "both").strip().lower()
if reference_mode not in allowed_references:
return {"status": "blocked", "reason": f"invalid_kantaka_reference:{kantaka_reference}"}
allowed_houses = {int(house) for house in kantaka_houses if int(house) in range(1, 13)}
if not allowed_houses:
return {"status": "blocked", "reason": "empty_kantaka_houses"}
sadhesati_rows = []
kantaka_rows = []
sadhesati_cycle = 0
sadhesati_seen: set[int] = set()
previous_sadhesati_sign: int | None = None
previous_sadhesati_end: date | None = None
for segment in segments:
sign_idx = int(segment["sign_index"])
base = {
"transit_sign": segment["sign"],
"beginning_date": segment["start_text"],
"ending_date": segment["end_text"],
"duration": segment["duration"],
"saturn_av": "",
"sarva_av": "",
"beginning_moment_local": segment["start_moment_local"],
"ending_moment_local": segment["end_moment_local"],
}
if sign_idx in sadhesati_map:
if birth_date is not None and segment["end_date"] < birth_date:
continue
saturn_av, sarva_av = _av_values(sign_idx)
if sign_idx == (moon_idx - 1) % 12 and (
previous_sadhesati_sign == (moon_idx + 1) % 12
and previous_sadhesati_end is not None
and (segment["start_date"] - previous_sadhesati_end).days > 365
):
sadhesati_cycle += 1
sadhesati_seen = set()
phase = sadhesati_map[sign_idx] if sign_idx not in sadhesati_seen else continuation_labels[sign_idx]
sadhesati_rows.append({
"cycle_phase": phase,
**base,
"saturn_av": saturn_av,
"sarva_av": sarva_av,
})
sadhesati_seen.add(sign_idx)
previous_sadhesati_sign = sign_idx
previous_sadhesati_end = segment["end_date"]
asc_house = (sign_idx - asc_idx) % 12 + 1
moon_house = (sign_idx - moon_idx) % 12 + 1
references = []
if reference_mode in {"ascendant", "both"}:
references.append(("Lagna", asc_house))
if reference_mode in {"moon", "both"}:
references.append(("Moon", moon_house))
for reference, house in references:
if house in allowed_houses:
saturn_av, sarva_av = _av_values(sign_idx)
kantaka_rows.append({
"cycle_phase": f"Kantaka Saturn in {house} house from {reference}",
**base,
"saturn_av": saturn_av,
"sarva_av": sarva_av,
})
return {
"status": "partial_verified",
"method": "swisseph_saturn_sign_segments",
"ayanamsa": ayanamsa,
"timezone_offset": float(tz),
"year_range": [start_year, end_year],
"sadhesati_rows": sadhesati_rows,
"kantaka_rows": kantaka_rows,
"pl9_display_rows": {
"sadhesati": _build_sadhesati_display_rows(sadhesati_rows),
"kantaka": _build_kantaka_display_rows(kantaka_rows, birth_date),
},
"audit": {
"date_boundaries": "computed",
"boundary_precision": "bisected_ingress_moment_local_minute",
"moment_fields": "beginning_moment_local / ending_moment_local are the exact Saturn sign crossings on the chart's clock (null at the scan window edges); beginning_date / ending_date keep the PL9 civil-date convention (crossing before 08:00 local is printed on the prior day).",
"kantaka_reference": reference_mode,
"kantaka_houses": sorted(allowed_houses),
"av_columns": (
"saturn_bav_and_sav_attached"
if saturn_bindus is not None or sav_scores is not None
else "not_attached"
),
"source_text": "not_copied_from_pl9",
},
}
def calc_sade_sati(moon_sign: str, transit_saturn_sign: str,
current_date: datetime = None) -> Dict:
"""
计算Sade Sati三阶段状态。
根据土星当前所在星座与月亮星座的相对位置,判断当前处于
Rising/Peak/Setting哪个阶段,以及剩余时间。
Args:
moon_sign: 月亮星座名称
transit_saturn_sign: 行运土星所在星座
current_date: 当前日期(可选,用于计算剩余时间)
Returns:
Sade Sati分析结果
"""
if moon_sign not in SIGNS or transit_saturn_sign not in SIGNS:
return {'active': False, 'error': 'Invalid sign'}
moon_idx = SIGNS.index(moon_sign)
saturn_idx = SIGNS.index(transit_saturn_sign)
# 计算土星相对于月亮的位置
relative = (saturn_idx - moon_idx) % 12
# 三阶段判定
if relative == 11: # 月亮前一个星座 = Rising
phase = 'rising'
phase_name = '上升期(Rising Phase)'
phase_desc = '土星正进入月亮前方星座,压力开始累积。表现为新挑战、新责任、新环境适应。'
intensity = '中等'
progress = 0
elif relative == 0: # 月亮星座 = Peak
phase = 'peak'
phase_name = '高峰期(Peak Phase)'
phase_desc = '土星正经过月亮星座,压力达到顶点。核心考验期,涉及身份、情感、安全感的全面检视。'
intensity = '高潮'
progress = 0
elif relative == 1: # 月亮后一个星座 = Setting
phase = 'setting'
phase_name = '消退期(Setting Phase)'
phase_desc = '土星已离开月亮星座,进入后方星座。压力开始消退,收获教训、总结经验。'
intensity = '递减'
progress = 0
else:
return {
'active': False,
'phase': 'none',
'phase_name': '非Sade Sati期间',
'phase_desc': '土星不在月亮星座及其相邻星座范围内',
'moon_sign': moon_sign,
'saturn_sign': transit_saturn_sign,
'intensity': '无',
'next_phase': _predict_next_phase(moon_idx, saturn_idx),
}
return {
'active': True,
'phase': phase,
'phase_name': phase_name,
'phase_desc': phase_desc,
'moon_sign': moon_sign,
'saturn_sign': transit_saturn_sign,
'intensity': intensity,
'total_duration': '约7.5年',
'current_phase_duration': '约2.5年',
}
def _predict_next_phase(moon_idx: int, saturn_idx: int) -> str:
"""预测下一个Sade Sati阶段"""
dist_to_moon = (moon_idx - saturn_idx) % 12
if dist_to_moon <= 1:
return '即将进入Rising期'
elif dist_to_moon <= 4:
return f'约{dist_to_moon * 2.5:.0f}年后进入Rising期'
else:
return f'约{dist_to_moon * 2.5:.0f}年后进入Rising期'
def calc_kantaka_shani(moon_sign: str, asc_sign: str,
transit_saturn_sign: str) -> Dict:
"""
计算Kantaka Shani(土星尖刺压力)。
土星在第1/4/8/10宫时触发额外的Kantaka Shani压力。
这些位置代表人生核心领域受到土星的直接考验。
Args:
moon_sign: 月亮星座
asc_sign: 上升星座
transit_saturn_sign: 行运土星星座
Returns:
Kantaka Shani分析
"""
if moon_sign not in SIGNS or asc_sign not in SIGNS or transit_saturn_sign not in SIGNS:
return {'active': False}
moon_idx = SIGNS.index(moon_sign)
asc_idx = SIGNS.index(asc_sign)
saturn_idx = SIGNS.index(transit_saturn_sign)
triggers = []
# 从月亮角度
moon_house = (saturn_idx - moon_idx) % 12 + 1
if moon_house == 1:
triggers.append({'reference': '月亮', 'house': 1, 'area': '自我认同、情感安全'})
if moon_house == 4:
triggers.append({'reference': '月亮', 'house': 4, 'area': '家庭、内在安全感'})
if moon_house == 8:
triggers.append({'reference': '月亮', 'house': 8, 'area': '深层恐惧、转变'})
if moon_house == 10:
triggers.append({'reference': '月亮', 'house': 10, 'area': '公众形象、事业方向'})
# 从上升角度
asc_house = (saturn_idx - asc_idx) % 12 + 1
if asc_house == 1:
triggers.append({'reference': '上升', 'house': 1, 'area': '自我、身体、人生方向'})
if asc_house == 4:
triggers.append({'reference': '上升', 'house': 4, 'area': '家庭、根基、内在稳定'})
if asc_house == 8:
triggers.append({'reference': '上升', 'house': 8, 'area': '危机、变革、共享资源'})
if asc_house == 10:
triggers.append({'reference': '上升', 'house': 10, 'area': '事业、社会地位、成就'})
return {
'active': len(triggers) > 0,
'triggers': triggers,
'intensity': '高' if len(triggers) >= 3 else '中等' if len(triggers) >= 2 else '低' if triggers else '无',
'advice': _kantaka_advice(triggers) if triggers else None,
}
def _kantaka_advice(triggers: List[Dict]) -> str:
"""生成Kantaka Shani建议"""
areas = [t['area'] for t in triggers]
if '自我认同、情感安全' in areas or '自我、身体、人生方向' in areas:
return '当前阶段需特别关注自我认知和身体健康,避免冲动决策。'
if '事业、社会地位、成就' in areas or '公众形象、事业方向' in areas:
return '事业领域面临考验,需稳健行事,避免冒进。长期规划优于短期投机。'
if '家庭、根基、内在稳定' in areas or '家庭、内在安全感' in areas:
return '家庭和内在安全感需要额外关注,适合沉淀和积累。'
if '危机、变革、共享资源' in areas or '深层恐惧、转变' in areas:
return '面临深层转变,适合心理探索和精神成长,避免财务冒进。'
return '土星考验期间,保持耐心和纪律是度过难关的关键。'
def calc_sade_sati_complete(moon_degree: float, asc_degree: float,
transit_saturn_degree: float,
current_date: datetime = None) -> Dict:
"""
完整的Sade Sati + Kantaka Shani综合分析。
Args:
moon_degree: 月亮黄道经度(0-360)
asc_degree: 上升黄道经度(0-360)
transit_saturn_degree: 行运土星黄道经度(0-360)
current_date: 当前日期
Returns:
完整的Sade Sati分析
"""
moon_sign = SIGNS[int(moon_degree / 30) % 12]
asc_sign = SIGNS[int(asc_degree / 30) % 12]
saturn_sign = SIGNS[int(transit_saturn_degree / 30) % 12]
sade_sati = calc_sade_sati(moon_sign, saturn_sign, current_date)
kantaka = calc_kantaka_shani(moon_sign, asc_sign, saturn_sign)
# 综合强度评定
if sade_sati['active'] and kantaka['active']:
overall = '极高压力 — Sade Sati与Kantaka Shani同时活跃'
elif sade_sati['active']:
overall = f'Sade Sati {sade_sati["intensity"]}压力期间'
elif kantaka['active']:
overall = f'仅Kantaka Shani — 土星尖刺压力({kantaka["intensity"]})'
else:
overall = '无Sade Sati或Kantaka Shani压力'
return {
'method': 'Sade Sati + Kantaka Shani 完整分析v1.0',
'moon_sign': moon_sign,
'asc_sign': asc_sign,
'saturn_sign': saturn_sign,
'sade_sati': sade_sati,
'kantaka_shani': kantaka,
'overall_assessment': overall,
}