Files
Jyotisha/scripts/jyotish_engine.py
T

3841 lines
180 KiB
Python
Raw 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 -*-
"""
印度占星统一引擎 v3.7.1 (Jyotish Unified Engine)
整合所有计算能力为单一CLI入口,供Skill调用
子命令:
chart 计算完整星盘(基于Swiss Ephemeris
dasha 计算Vimshottari Dasha大运时间线
yoga Yoga格局识别
predict 三层验证法事件预测(优先EventPredictionModel规则引擎)
varga 分盘计算(D9/D10
varga-full BPHS十六分盘完整计算(D2-D60)(v3.7新增)
aspects 度数精确相位系统(v3.7新增)
jaimini Jaimini系统(Chara Karaka/KarakamshaChara Dasha timing partial)(v3.7新增)
nakshatra-adv 高级Nakshatra分析(Tara Bala/Sub-Lord/兼容性)(v3.7新增)
argala Argala门闩系统(v3.7新增)
tajika Tajika/Varshaphala年运盘(v3.7新增)
synastry 合盘分析(Ashta Koota 36分制+Mangal Dosha)(v3.7新增)
full-reading 全自动综合解盘(出生信息→全链路计算→完整报告)(v3.7.1新增)
celebrity 名人案例查询(15,807条数据+SQLite验证库)
db-stats 验证数据库统计
transit 行星过境查询
shadbala 六重力量计算(内部一致;外部绝对值校准前partial)(v3.4新增)
ashtakavarga 八分法计算(v3.5升级BPHS完整表,SAV=337
memory Hermes记忆系统(v3.4新增)
validate R1-R10数学验证(v3.5新增,含R2b BAV列→SAV校验)
audit P1-P12行星审计管线(v3.6升级含P3仓库耦合+P8年龄状态+冲突仲裁)
report MD→HTML报告生成(v3.6新增,羊皮纸主题)
用法示例:
python3 jyotish_engine.py chart --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8
python3 jyotish_engine.py shadbala --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8
python3 jyotish_engine.py ashtakavarga --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8
python3 jyotish_engine.py memory --action store --content "测试记忆"
"""
import argparse
import json
import sys
import os
import csv
import math
import sqlite3
from datetime import datetime, timedelta
from typing import Dict, List
# ============================================================================
# 路径常量
# ============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HOME_DIR = os.path.expanduser('~')
CLAW_DIR = os.path.join(HOME_DIR, 'WorkBuddy', 'Claw')
DB_PATH = os.path.join(CLAW_DIR, 'vedic_astrology_validation.db')
PERSON_CSV = os.path.join(CLAW_DIR, 'vedastro_data', 'PersonList-15k.csv')
TRANSIT_JSON = os.path.join(CLAW_DIR, '月运过境配置-2026-2028.json')
try:
import swisseph as swe
swe.set_sid_mode(swe.SIDM_LAHIRI) # P0修复:必须设置Lahiri恒星黄道模式
HAS_SWE = True
except ImportError:
HAS_SWE = False
# ============================================================================
# 常量
# ============================================================================
NAKSHATRA_LIST = [
("Ashwini", "Ketu", 7), ("Bharani", "Venus", 20), ("Krittika", "Sun", 6),
("Rohini", "Moon", 10), ("Mrigashira", "Mars", 7), ("Ardra", "Rahu", 18),
("Punarvasu", "Jupiter", 16), ("Pushya", "Saturn", 19), ("Ashlesha", "Mercury", 17),
("Magha", "Ketu", 7), ("Purva Phalguni", "Venus", 20), ("Uttara Phalguni", "Sun", 6),
("Hasta", "Moon", 10), ("Chitra", "Mars", 7), ("Swati", "Rahu", 18),
("Vishakha", "Jupiter", 16), ("Anuradha", "Saturn", 19), ("Jyeshtha", "Mercury", 17),
("Mula", "Ketu", 7), ("Purva Ashadha", "Venus", 20), ("Uttara Ashadha", "Sun", 6),
("Shravana", "Moon", 10), ("Dhanishta", "Mars", 7), ("Shatabhisha", "Rahu", 18),
("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}
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'}
EXALTATION = {'Sun': 'Aries', 'Moon': 'Taurus', 'Mars': 'Capricorn', 'Mercury': 'Virgo', 'Jupiter': 'Cancer', 'Venus': 'Pisces', 'Saturn': 'Libra'}
DEBILITATION = {'Sun': 'Libra', 'Moon': 'Scorpio', 'Mars': 'Cancer', 'Mercury': 'Pisces', 'Jupiter': 'Capricorn', 'Venus': 'Virgo', 'Saturn': 'Aries'}
# Moolatrikona(三方本宫): 行星 → (星座, 度数范围起始)
MOOLATRIKONA = {'Sun': ('Leo', 0, 20), 'Moon': ('Taurus', 0, 3), 'Mars': ('Aries', 0, 12), 'Mercury': ('Virgo', 16, 20), 'Jupiter': ('Sagittarius', 0, 10), 'Venus': ('Libra', 0, 15), 'Saturn': ('Aquarius', 0, 20)}
# BPHS 永久友好关系表 (Naisargika Mitra)
PERMANENT_FRIENDS = {
'Sun': ['Moon', 'Mars', 'Jupiter'],
'Moon': ['Sun', 'Mercury'],
'Mars': ['Sun', 'Moon', 'Jupiter'],
'Mercury': ['Sun', 'Venus'],
'Jupiter': ['Sun', 'Moon', 'Mars'],
'Venus': ['Mercury', 'Saturn'],
'Saturn': ['Mercury', 'Venus'],
'Rahu': ['Venus', 'Saturn'],
'Ketu': ['Mars', 'Saturn'],
}
PERMANENT_ENEMIES = {
'Sun': ['Saturn', 'Venus'],
'Moon': [],
'Mars': ['Mercury'],
'Mercury': ['Moon'],
'Jupiter': ['Mercury', 'Venus'],
'Venus': ['Sun', 'Moon'],
'Saturn': ['Sun', 'Moon', 'Mars'],
'Rahu': ['Sun', 'Moon', 'Jupiter'],
'Ketu': ['Sun', 'Moon'],
}
PUSHKARA_NAVAMSA_RANGES = {
'fire': [(6 + 40/60, 10), (23 + 20/60, 26 + 40/60)],
'earth': [(3 + 20/60, 6 + 40/60), (16 + 40/60, 20)],
'air': [(13 + 20/60, 16 + 40/60), (26 + 40/60, 30)],
'water': [(0, 3 + 20/60), (10, 13 + 20/60)],
}
PUSHKARA_BHAGA_DEGREES = {'fire': 21, 'earth': 14, 'air': 24, 'water': 7}
SIGN_ELEMENTS = {
'Aries': 'fire', 'Leo': 'fire', 'Sagittarius': 'fire',
'Taurus': 'earth', 'Virgo': 'earth', 'Capricorn': 'earth',
'Gemini': 'air', 'Libra': 'air', 'Aquarius': 'air',
'Cancer': 'water', 'Scorpio': 'water', 'Pisces': 'water',
}
def _element_key(sign):
return SIGN_ELEMENTS.get(sign)
def _is_pushkara_navamsa(sign, deg_in_sign):
element = _element_key(sign)
if element is None or deg_in_sign is None:
return False, None
for start, end in PUSHKARA_NAVAMSA_RANGES[element]:
if start <= deg_in_sign < end:
return True, {"element": element, "range": [round(start, 4), round(end, 4)]}
return False, None
def _is_pushkara_bhaga(sign, deg_in_sign, orb=1.0):
element = _element_key(sign)
if element is None or deg_in_sign is None:
return False, None
target = PUSHKARA_BHAGA_DEGREES[element]
delta = abs(deg_in_sign - target)
return delta <= orb, {"element": element, "target_degree": target, "orb": orb, "delta": round(delta, 4)}
def _calc_vargottama(planets, varga_full):
d9 = varga_full.get('D9_Navamsa', {}) if isinstance(varga_full, dict) else {}
result = {}
for pn, pd in planets.items():
if not isinstance(pd, dict) or 'sign' not in pd:
continue
d9_pd = d9.get(pn, {}) if isinstance(d9, dict) else {}
d9_sign = d9_pd.get('sign') if isinstance(d9_pd, dict) else None
result[pn] = {
'd1_sign': pd.get('sign'),
'd9_sign': d9_sign,
'is_vargottama': bool(d9_sign and pd.get('sign') == d9_sign),
}
return result
def _calc_pushkara_flags(planets):
result = {}
for pn, pd in planets.items():
if not isinstance(pd, dict) or 'sign' not in pd:
continue
sign = pd.get('sign')
deg = pd.get('degree_in_sign', pd.get('degree', 0) % 30)
in_pna, pna_meta = _is_pushkara_navamsa(sign, deg)
in_pb, pb_meta = _is_pushkara_bhaga(sign, deg)
result[pn] = {
'sign': sign,
'sign_cn': SIGNS_CN.get(sign, ''),
'degree_in_sign': round(deg, 4),
'pushkara_navamsa': in_pna,
'pushkara_navamsa_meta': pna_meta,
'pushkara_bhaga': in_pb,
'pushkara_bhaga_meta': pb_meta,
}
return result
def _calc_dasha_sandhi(dasha_result, reference_date=None, orb_days=90):
ref = datetime.strptime(reference_date, "%Y-%m-%d") if reference_date else datetime.now()
sandhi = []
timeline = dasha_result.get('timeline', []) if isinstance(dasha_result, dict) else []
for md in timeline:
for boundary_key in ['start', 'end']:
if boundary_key not in md:
continue
bdt = datetime.strptime(md[boundary_key], "%Y-%m-%d")
delta = (bdt - ref).days
if abs(delta) <= orb_days:
sandhi.append({
'level': 'mahadasha',
'lord': md.get('lord'),
'boundary': boundary_key,
'date': md.get(boundary_key),
'days_from_reference': delta,
'within_orb': True,
})
for ad in md.get('antardasha_timeline', []):
for boundary_key in ['start', 'end']:
if boundary_key not in ad:
continue
bdt = datetime.strptime(ad[boundary_key], "%Y-%m-%d")
delta = (bdt - ref).days
if abs(delta) <= orb_days:
sandhi.append({
'level': 'antardasha',
'mahadasha_lord': md.get('lord'),
'lord': ad.get('lord'),
'boundary': boundary_key,
'date': ad.get(boundary_key),
'days_from_reference': delta,
'within_orb': True,
})
return {'reference_date': ref.strftime('%Y-%m-%d'), 'orb_days': orb_days, 'sandhi_windows': sandhi}
def _get_dignity_level(planet, sign, deg_in_sign=None):
"""判断行星在某星座的尊严等级 (用于 Vimsopaka 映射)"""
if EXALTATION.get(planet) == sign:
return 'EXALTED'
# Moolatrikona: 需要检查度数范围
mt = MOOLATRIKONA.get(planet)
if mt and mt[0] == sign:
if deg_in_sign is not None and mt[1] <= deg_in_sign < mt[2]:
return 'MOOLATRIKONA'
if SIGN_LORDS.get(sign) == planet:
return 'OWN_SIGN'
if DEBILITATION.get(planet) == sign:
return 'DEBILITATED'
# 友好/敌对
sign_lord = SIGN_LORDS.get(sign, '')
if planet in PERMANENT_FRIENDS.get(sign_lord, []):
return 'FRIEND'
if planet in PERMANENT_ENEMIES.get(sign_lord, []):
return 'ENEMY'
return 'NEUTRAL'
PLANET_CN = {"Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun", "Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu", "Jupiter": "木星Jupiter", "Saturn": "土星Saturn", "Mercury": "水星Mercury"}
BASE_PLANETS_SWE = {'Sun': swe.SUN, 'Moon': swe.MOON, 'Mars': swe.MARS, 'Mercury': swe.MERCURY, 'Jupiter': swe.JUPITER, 'Venus': swe.VENUS, 'Saturn': swe.SATURN} if HAS_SWE else {}
PLANETS_SWE = {**BASE_PLANETS_SWE, 'Rahu': swe.MEAN_NODE} if HAS_SWE else {}
def _node_pid(node_mode='mean'):
"""返回 Rahu/Ketu 节点计算口径对应的 Swiss Ephemeris id。"""
if not HAS_SWE:
return None
return swe.TRUE_NODE if (node_mode or 'mean').lower() == 'true' else swe.MEAN_NODE
def _planet_map_for_node_mode(node_mode='mean'):
"""返回七曜+Rahu 的行星映射,Rahu 口径由 node_mode 决定。"""
if not HAS_SWE:
return {}
return {**BASE_PLANETS_SWE, 'Rahu': _node_pid(node_mode)}
def _calc_sidereal_planets_for_jd(jd, node_mode='mean', include_ketu=True):
"""使用 Swiss Ephemeris 计算指定 Julian Day 的恒星黄道行星位置。"""
if not HAS_SWE:
return {}, None
ayanamsa = swe.get_ayanamsa(jd)
data = {}
for pname, pid in _planet_map_for_node_mode(node_mode).items():
pos, ret = swe.calc_ut(jd, pid)
if ret < 0:
continue
lon = (pos[0] - ayanamsa) % 360
sign_idx = int(lon / 30) % 12
speed = pos[3] if len(pos) > 3 else 0
data[pname] = {
'sign': SIGNS[sign_idx],
'sign_cn': SIGNS_CN[SIGNS[sign_idx]],
'degree': round(lon, 4),
'degree_raw': lon,
'degree_in_sign': round(lon % 30, 2),
'degree_in_sign_raw': lon % 30,
'speed': round(speed, 4),
'retrograde': speed < 0,
}
if include_ketu and 'Rahu' in data:
rahu_lon = data['Rahu']['degree_raw']
ketu_lon = (rahu_lon + 180) % 360
ketu_idx = int(ketu_lon / 30) % 12
data['Ketu'] = {
'sign': SIGNS[ketu_idx],
'sign_cn': SIGNS_CN[SIGNS[ketu_idx]],
'degree': round(ketu_lon, 4),
'degree_raw': ketu_lon,
'degree_in_sign': round(ketu_lon % 30, 2),
'degree_in_sign_raw': ketu_lon % 30,
'speed': data['Rahu'].get('speed', 0),
'retrograde': data['Rahu'].get('retrograde', True),
}
return data, ayanamsa
def output_json(data):
"""统一JSON输出"""
print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
def _add_chart_args(p):
"""为需要出生数据的子命令添加公共参数"""
p.add_argument('--year', type=int, required=True)
p.add_argument('--month', type=int, required=True)
p.add_argument('--day', type=int, required=True)
p.add_argument('--hour', type=int, required=True)
p.add_argument('--minute', type=int, required=True)
p.add_argument('--lat', type=float, required=True)
p.add_argument('--lon', type=float, required=True)
p.add_argument('--tz', type=float, default=0)
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'], help='Rahu/Ketu节点口径:mean=Mean Node(默认,JHora常用/Swiss direct baseline),true=True NodePyJHora默认)')
# ============================================================================
# 公共星盘计算(供 chart/shadbala/ashtakavarga 共用,v3.4提取)
# ============================================================================
def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='mean'):
"""计算星盘核心数据,返回 (result_dict, asc_idx, jd, ayanamsa)。node_mode: mean|true。"""
if not HAS_SWE:
return None, None, None, None
swe.set_ephe_path('')
hour_decimal = hour + minute / 60.0 - tz
jd = swe.julday(year, month, day, hour_decimal)
ayanamsa = swe.get_ayanamsa(jd)
node_mode = (node_mode or 'mean').lower()
if node_mode not in ('mean', 'true'):
node_mode = 'mean'
node_pid = _node_pid(node_mode)
result = {"birth_info": {
"date": f"{year}-{month:02d}-{day:02d}", "time": f"{hour:02d}:{minute:02d}",
"tz": f"UTC{'+' if tz >= 0 else ''}{tz}", "lat": lat, "lon": lon,
"julian_day": round(jd, 6), "ayanamsa": round(ayanamsa, 4),
"node_mode": node_mode, "node_mode_note": "mean=Mean Node; true=True Node. PyJHora默认true,本skill默认mean。"
}, "ascendant": None, "planets": {}, "houses": {}}
asc_lon, _ = swe.houses(jd, lat, lon, b'A')
asc_deg = (asc_lon[0] - ayanamsa) % 360
asc_idx = int(asc_deg / 30)
asc_sign = SIGNS[asc_idx]
result["ascendant"] = {"sign": asc_sign, "sign_cn": SIGNS_CN[asc_sign],
"degree": round(asc_deg, 4), "degree_in_sign": round(asc_deg - asc_idx * 30, 4),
"lord": SIGN_LORDS[asc_sign]}
for i in range(12):
c = (asc_lon[i] - ayanamsa) % 360
si = int(c / 30)
result["houses"][f"house_{i+1}"] = {"cusp_sign": SIGNS[si],
"cusp_sign_cn": SIGNS_CN[SIGNS[si]], "cusp_degree": round(c, 4),
"lord": SIGN_LORDS[SIGNS[si]]}
nak_span = 360.0 / 27
planets_swe = {**BASE_PLANETS_SWE, 'Rahu': node_pid}
for pname, pid in planets_swe.items():
try:
pos, _ = swe.calc_ut(jd, pid)
lon_p = (pos[0] - ayanamsa) % 360; lat_p = pos[1]; spd = pos[3]
si = int(lon_p / 30); d_in_s = lon_p - si * 30; sign = SIGNS[si]
retro = spd < 0
house = ((si - asc_idx) % 12) + 1
status = "中性"
if EXALTATION.get(pname) == sign: status = "入旺(Exalted)"
elif DEBILITATION.get(pname) == sign: status = "落陷(Debilitated)"
elif SIGN_LORDS.get(sign) == pname: status = "入庙(Own Sign)"
ni = int(lon_p / nak_span); pada = int((lon_p % nak_span) / (nak_span / 4)) + 1
nak_n, nak_l, _ = NAKSHATRA_LIST[ni % 27]
result["planets"][pname] = {
"sign": sign, "sign_cn": SIGNS_CN[sign], "degree": round(lon_p, 4),
"degree_raw": lon_p,
"degree_in_sign": round(d_in_s, 4), "degree_in_sign_raw": d_in_s,
"house": house, "status": status,
"retrograde": retro, "speed": round(spd, 6),
"nakshatra": nak_n, "nakshatra_pada": pada, "nakshatra_lord": nak_l}
if pname == 'Rahu':
klon = (lon_p + 180) % 360; ksi = int(klon / 30); kd = klon - ksi * 30
kni = int(klon / nak_span); kp = int((klon % nak_span) / (nak_span / 4)) + 1
kn, kl, _ = NAKSHATRA_LIST[kni % 27]
result["planets"]["Ketu"] = {
"sign": SIGNS[ksi], "sign_cn": SIGNS_CN[SIGNS[ksi]],
"degree": round(klon, 4), "degree_raw": klon,
"degree_in_sign": round(kd, 4), "degree_in_sign_raw": kd,
"house": ((ksi - asc_idx) % 12) + 1, "status": "中性",
"retrograde": True, "speed": round(spd, 6),
"nakshatra": kn, "nakshatra_pada": kp, "nakshatra_lord": kl}
except Exception as e:
result["planets"][pname] = {"error": str(e)}
return result, asc_idx, jd, ayanamsa
# ============================================================================
# 1. 星盘计算
# ============================================================================
def cmd_chart(args):
result, asc_idx, jd, ayanamsa = compute_chart_data(args.year, args.month, args.day, args.hour, args.minute, args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if result is None:
return {"error": "swisseph未安装"}
# v3.5: --validate 触发 R1-R10 校验
if getattr(args, 'validate', False):
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_result = calc_ashtakavarga(result.get('planets', {}), asc_idx)
from validate import validate_chart
validation = validate_chart(result, asht_result)
result['validation'] = validation
except Exception as e:
result['validation'] = {"error": str(e), "valid": False}
return result
# ============================================================================
# 2. Dasha计算
# ============================================================================
def cmd_dasha(args):
nak_info = None; progress = 0.5
if args.moon_lon is not None:
ns = 360.0 / 27; idx = int(args.moon_lon / ns); progress = (args.moon_lon % ns) / ns
nak_info = NAKSHATRA_LIST[idx % 27]
elif args.nakshatra:
nl = args.nakshatra.lower().replace(" ", "").replace("-", "")
for n in NAKSHATRA_LIST:
if nl in n[0].lower().replace(" ", "") or n[0].lower().replace(" ", "").startswith(nl[:5]):
nak_info = n; break
if not nak_info: return {"error": f"未找到Nakshatra: {args.nakshatra}"}
if args.pada: progress = (max(1, min(4, args.pada)) - 1) / 4 + 0.125
else:
return {"error": "请提供 --nakshatra 或 --moon-lon"}
nak_name, start_lord, start_years = nak_info
birth_dt = datetime.strptime(args.birthdate, "%Y-%m-%d")
elapsed = progress * start_years; remaining = start_years - elapsed
dt = birth_dt - timedelta(days=elapsed * 365.25)
si = DASHA_ORDER.index(start_lord)
timeline = []
for i in range(9):
lord = DASHA_ORDER[(si + i) % 9]; years = DASHA_YEARS[lord]
end_dt = dt + timedelta(days=years * 365.25)
# 第一个 MD 的展示 years 用 balance,实际日期计算用完整年数(数学等价)
display_years = round(remaining, 2) if i == 0 else years
timeline.append({"lord": lord, "lord_cn": PLANET_CN[lord], "start": dt.strftime("%Y-%m-%d"), "end": end_dt.strftime("%Y-%m-%d"), "years": display_years, "full_years": years, "is_current": False, "is_balance": i == 0, "balance_years": round(remaining, 2) if i == 0 else None, "elapsed_at_birth": round(elapsed, 2) if i == 0 else None})
dt = end_dt
today = datetime.strptime(args.today, "%Y-%m-%d") if args.today else datetime.now()
current = None
for d in timeline:
ds = datetime.strptime(d["start"], "%Y-%m-%d"); de = datetime.strptime(d["end"], "%Y-%m-%d")
total_days = (de - ds).days; li = DASHA_ORDER.index(d["lord"])
# v3.7.2: 为所有 Mahadasha 计算 Antardasha(不仅当前大运)
sub = []; sdt = ds
for j in range(9):
sl = DASHA_ORDER[(li + j) % 9]; sd = total_days * DASHA_YEARS[sl] / 120
se = sdt + timedelta(days=sd)
is_cur = sdt <= today < se
sub.append({"lord": sl, "lord_cn": PLANET_CN[sl], "start": sdt.strftime("%Y-%m-%d"), "end": se.strftime("%Y-%m-%d"), "is_current": is_cur})
sdt = se
d["antardasha_timeline"] = sub
if ds <= today < de:
d["is_current"] = True
# 从 antardasha 列表中提取当前正在运行的 antardasha
current_ad = None
for ad in sub:
if ad.get("is_current"):
current_ad = ad
break
d["mahadasha"] = d.get("lord", "")
d["mahadasha_cn"] = d.get("lord_cn", "")
d["antardasha"] = current_ad or (sub[0] if sub else None)
current = d
return {"moon_nakshatra": nak_name, "birth_date": args.birthdate, "reference_date": today.strftime("%Y-%m-%d"), "timeline": timeline, "current_dasha": current}
# ============================================================================
# 3. Yoga识别
# ============================================================================
def cmd_yoga(args):
planets = {}
if args.planets:
for item in args.planets.split(','):
parts = item.strip().split(':')
if len(parts) >= 3:
planets[parts[0].strip()] = {"sign": parts[1].strip(), "house": int(parts[2].strip())}
asc = args.ascendant or "Aries"
ai = SIGNS.index(asc) if asc in SIGNS else 0
kl = list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 4, 7, 10]]))
tl = list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 5, 9]]))
yogas = []
# Raja Yoga
for k in kl:
for t in tl:
if k != t and k in planets and t in planets and planets[k]["house"] == planets[t]["house"]:
yogas.append({"name": "Raja Yoga", "name_cn": "王者格局", "combination": f"{k}+{t}同在第{planets[k]['house']}", "effects": ["权力地位", "事业成功", "社会影响力"], "strength": "" if planets[k]["sign"] in [EXALTATION.get(k, ''), SIGN_LORDS.get(planets[k]["sign"], '')] else ""})
# Mahapurusha Yoga
yoga_names = {'Mars': 'Ruchaka', 'Mercury': 'Bhadra', 'Jupiter': 'Hamsa', 'Venus': 'Malavya', 'Saturn': 'Sasa'}
for p, info in planets.items():
if info["house"] in [1, 4, 7, 10] and p in yoga_names:
if EXALTATION.get(p) == info["sign"] or SIGN_LORDS.get(info["sign"]) == p:
st = "入旺" if EXALTATION.get(p) == info["sign"] else "入庙"
yogas.append({"name": f"{yoga_names[p]} Yoga", "name_cn": f"{PLANET_CN.get(p, p)}{st}格局", "combination": f"{p}{st}{info['sign']}(第{info['house']}宫)", "effects": ["卓越才能", "领域领军", "人格魅力"], "strength": "极强" if st == "入旺" else ""})
# Gajakesari
if 'Jupiter' in planets and 'Moon' in planets:
jh = planets['Jupiter']['house']; mh = planets['Moon']['house']
if jh in [1, 4, 7, 10] and mh in [1, 4, 7, 10]:
yogas.append({"name": "Gajakesari Yoga", "name_cn": "象狮格局", "combination": f"木星第{jh}宫+月亮第{mh}", "effects": ["智慧学识", "财富名声", "道德品质"], "strength": ""})
# Neechabhanga
for p, info in planets.items():
if DEBILITATION.get(p) == info["sign"]:
dl = SIGN_LORDS[info["sign"]]
if dl in planets and planets[dl]["house"] in [1, 4, 7, 10]:
yogas.append({"name": "Neechabhanga Raja Yoga", "name_cn": "落陷取消格局", "combination": f"{p}落陷在{info['sign']}{dl}在角宫化解", "effects": ["克服困难", "逆境崛起", "转化能力"], "strength": "中强"})
# Dhana Yoga
wl = set(); wh = [2, 5, 9, 11]
for h in wh:
wl.add(SIGN_LORDS[SIGNS[(ai + h - 1) % 12]])
wc = sum(1 for w in wl if w in planets and planets[w]["house"] in wh)
if wc >= 2:
yogas.append({"name": "Dhana Yoga", "name_cn": "财富格局", "combination": f"{wc}个财富宫主星落入财富宫", "effects": ["财富积累", "物质成功", "投资收益"], "strength": "中强" if wc >= 3 else ""})
# Solar Yogas (Veshi/Voshi/Ubhayachari)
if 'Sun' in planets:
sun_h = planets['Sun']['house']
h2_from_sun = ((sun_h - 1 + 1) % 12) + 1 # 2nd from Sun
h12_from_sun = ((sun_h - 1 + 11) % 12) + 1 # 12th from Sun
planets_in_h2 = [p for p, info in planets.items() if info['house'] == h2_from_sun]
planets_in_h12 = [p for p, info in planets.items() if info['house'] == h12_from_sun]
if planets_in_h12 and not planets_in_h2:
yogas.append({"name": "Voshi Yoga", "name_cn": "太阳前瑜伽", "combination": f"{'+'.join(planets_in_h12)}在太阳第12宫", "effects": ["口才出众", "善于表达", "受人尊敬"], "strength": ""})
if planets_in_h2 and not planets_in_h12:
yogas.append({"name": "Veshi Yoga", "name_cn": "太阳后瑜伽", "combination": f"{'+'.join(planets_in_h2)}在太阳第2宫", "effects": ["财富充裕", "生活舒适", "性情愉快"], "strength": ""})
if planets_in_h2 and planets_in_h12:
yogas.append({"name": "Ubhayachari Yoga", "name_cn": "太阳双夹瑜伽", "combination": f"太阳两侧均有行星", "effects": ["性格坚毅", "口才与财富兼备", "受人爱戴"], "strength": ""})
# Lunar Yogas (Sunapha/Anapha/Durudhura/Kemadruma)
if 'Moon' in planets:
moon_h = planets['Moon']['house']
h2_from_moon = ((moon_h - 1 + 1) % 12) + 1
h12_from_moon = ((moon_h - 1 + 11) % 12) + 1
p_in_h2_moon = [p for p, info in planets.items() if p != 'Moon' and info['house'] == h2_from_moon]
p_in_h12_moon = [p for p, info in planets.items() if p != 'Moon' and info['house'] == h12_from_moon]
if p_in_h12_moon and not p_in_h2_moon:
yogas.append({"name": "Anapha Yoga", "name_cn": "月后瑜伽", "combination": f"{'+'.join(p_in_h12_moon)}在月亮第12宫", "effects": ["体格健壮", "名声良好", "品德高尚"], "strength": ""})
if p_in_h2_moon and not p_in_h12_moon:
yogas.append({"name": "Sunapha Yoga", "name_cn": "月前瑜伽", "combination": f"{'+'.join(p_in_h2_moon)}在月亮第2宫", "effects": ["自力更生", "财富充裕", "受人尊敬"], "strength": ""})
if p_in_h2_moon and p_in_h12_moon:
yogas.append({"name": "Durudhura Yoga", "name_cn": "月双夹瑜伽", "combination": f"月亮两侧均有行星", "effects": ["享受丰富", "善于辞令", "性格坚定"], "strength": ""})
# Kemadruma Yoga: Moon两侧(2宫和12宫)均无行星
if not p_in_h2_moon and not p_in_h12_moon:
yogas.append({"name": "Kemadruma Yoga", "name_cn": "空劫瑜伽", "combination": f"月亮两侧均无行星(2宫/12宫空)", "effects": ["人生艰辛", "需自力更生", "精神挑战"], "strength": ""})
# ========================================================================
# 扩展组合型 Yogav3.7.2 新增,共 25 条)
# ========================================================================
# --- 辅助函数 ---
def _house_of(planet_name):
"""获取行星所在宫位,不存在返回 None"""
return planets[planet_name]["house"] if planet_name in planets else None
def _sign_of(planet_name):
"""获取行星所在星座,不存在返回 None"""
return planets[planet_name]["sign"] if planet_name in planets else None
def _is_exalted(planet_name):
"""行星是否入旺"""
s = _sign_of(planet_name)
return s is not None and EXALTATION.get(planet_name) == s
def _is_own_sign(planet_name):
"""行星是否入庙"""
s = _sign_of(planet_name)
return s is not None and SIGN_LORDS.get(s) == planet_name
def _is_debilitated(planet_name):
"""行星是否落陷"""
s = _sign_of(planet_name)
return s is not None and DEBILITATION.get(planet_name) == s
def _is_kendra(house_num):
return house_num in [1, 4, 7, 10]
def _is_trikona(house_num):
return house_num in [1, 5, 9]
def _is_dusthana(house_num):
return house_num in [6, 8, 12]
def _is_upachaya(house_num):
return house_num in [3, 6, 10, 11]
def _planets_in_house(h):
"""获取落入第 h 宫的所有行星列表"""
return [p for p, info in planets.items() if info["house"] == h]
def _lord_of_house(h):
"""获取第 h 宫的宫主星"""
return SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]
def _planet_in_kendra_or_trikona(planet_name):
"""行星是否在角宫或三方宫"""
h = _house_of(planet_name)
return h is not None and (_is_kendra(h) or _is_trikona(h))
# ========================================================================
# 1. Vipreet Raja Yoga(逆行王者格局)—— Harsha / Sarala / Vimala
# 来源:Phaladeepika。6/8/12 宫主星落入 6/8/12 宫(凶宫互落)→ 因祸得福
# ========================================================================
for dh_h, yoga_sub, yoga_cn in [(6, "Harsha", "喜悦逆行"), (8, "Sarala", "锐利逆行"), (12, "Vimala", "纯净逆行")]:
lord_dh = _lord_of_house(dh_h)
if lord_dh in planets:
lh = _house_of(lord_dh)
if lh in [6, 8, 12] and lh != dh_h:
yogas.append({"name": f"{yoga_sub} Vipreet Raja Yoga", "name_cn": f"{yoga_cn}格局", "combination": f"{dh_h}宫主星{lord_dh}落入第{lh}宫(凶宫互落)", "effects": ["因祸得福", "逆境崛起", "困境中成长"], "strength": "中强"})
# ========================================================================
# 2. Amala Yoga(无瑕格局)
# 来源:BPHS。10 宫(天顶)及其前方(从10宫算2宫即11宫)有吉星
# 简化版:10 宫或 11 宫有木星/金星/明亮水星
# ========================================================================
benefics_10_11 = [p for p in ['Jupiter', 'Venus', 'Mercury'] if p in planets and _house_of(p) in [10, 11] and not _is_debilitated(p)]
if benefics_10_11:
yogas.append({"name": "Amala Yoga", "name_cn": "无瑕格局", "combination": f"{''.join(benefics_10_11)}在10宫或11宫", "effects": ["名声清白", "受人敬仰", "事业有成"], "strength": "中强"})
# ========================================================================
# 3. Parvata Yoga(山岳格局)
# 来源:BPHS。角宫主星有力 + 6/8 宫主星落入对应凶宫
# 简化版:至少两个角宫主星在角宫或三方宫 + 6/8宫主不在1宫
# ========================================================================
kendra_lords_strong = sum(1 for k in kl if k in planets and _planet_in_kendra_or_trikona(k))
if kendra_lords_strong >= 2:
lord_6 = _lord_of_house(6)
lord_8 = _lord_of_house(8)
lord_6_ok = lord_6 in planets and _house_of(lord_6) != 1
lord_8_ok = lord_8 in planets and _house_of(lord_8) != 1
if lord_6_ok or lord_8_ok:
yogas.append({"name": "Parvata Yoga", "name_cn": "山岳格局", "combination": f"角宫主星有力+凶宫主星不落入1宫", "effects": ["智慧卓越", "富有口才", "品格高尚"], "strength": ""})
# ========================================================================
# 4. Kahala Yoga(勇气格局)
# 来源:BPHS。3 宫主与 10 宫主有连接(同宫/互换/相位),且落入角宫
# 简化版:3 宫主与 10 宫主同在一宫
# ========================================================================
lord_3 = _lord_of_house(3)
lord_10 = _lord_of_house(10)
if lord_3 in planets and lord_10 in planets:
if _house_of(lord_3) == _house_of(lord_10):
yogas.append({"name": "Kahala Yoga", "name_cn": "勇气格局", "combination": f"3宫主{lord_3}与10宫主{lord_10}同在第{_house_of(lord_3)}", "effects": ["意志坚定", "领导才能", "充满活力"], "strength": ""})
# ========================================================================
# 5. Sankha Yoga(海螺格局)
# 来源:BPHS。5 宫主 + 6 宫主落入角宫/三方宫,且上升主星有力
# 简化版:5宫主和6宫主均在角宫或三方宫
# ========================================================================
lord_5 = _lord_of_house(5)
lord_6 = _lord_of_house(6)
if lord_5 in planets and lord_6 in planets:
l5h = _house_of(lord_5)
l6h = _house_of(lord_6)
if (_is_kendra(l5h) or _is_trikona(l5h)) and (_is_kendra(l6h) or _is_trikona(l6h)):
yogas.append({"name": "Sankha Yoga", "name_cn": "海螺格局", "combination": f"5宫主{lord_5}在第{l5h}宫+6宫主{lord_6}在第{l6h}宫(均有力)", "effects": ["学识渊博", "品德高尚", "物质充裕"], "strength": "中强"})
# ========================================================================
# 6. Bheri Yoga(旗帜格局)
# 来源:BPHS。9 宫主落入 2 宫 + 木星/金星在角宫/三方宫 + 上升主与 9 宫主有关联
# 简化版:9 宫主在 2 宫 + 木星或金星在角宫
# ========================================================================
lord_9 = _lord_of_house(9)
if lord_9 in planets and _house_of(lord_9) == 2:
jv_in_kendra = [p for p in ['Jupiter', 'Venus'] if p in planets and _is_kendra(_house_of(p))]
if jv_in_kendra:
yogas.append({"name": "Bheri Yoga", "name_cn": "旗帜格局", "combination": f"9宫主{lord_9}在2宫+{''.join(jv_in_kendra)}在角宫", "effects": ["品德高尚", "长寿幸福", "受人敬仰"], "strength": ""})
# ========================================================================
# 7. Mridanga Yoga(鼓格局)
# 来源:BPHS。上升主星在入旺/入庙 + 其他行星在自身入旺/入庙星座
# 简化版:上升主星入旺或入庙
# ========================================================================
asc_lord = SIGN_LORDS.get(asc, '')
if asc_lord in planets and (_is_exalted(asc_lord) or _is_own_sign(asc_lord)):
other_strong = [p for p in planets if p != asc_lord and p not in ['Rahu', 'Ketu'] and (_is_exalted(p) or _is_own_sign(p))]
if other_strong:
st = "入旺" if _is_exalted(asc_lord) else "入庙"
yogas.append({"name": "Mridanga Yoga", "name_cn": "鼓格局", "combination": f"上升主星{asc_lord}{st}+{''.join(other_strong)}有力", "effects": ["权力权威", "个人魅力", "领导气质"], "strength": ""})
# ========================================================================
# 8. Sreenatha Yoga(幸运之主格局)
# 来源:BPHS。7 宫主星落入 10 宫且 10 宫主落入 1 宫
# ========================================================================
lord_7 = _lord_of_house(7)
if lord_7 in planets and lord_10 in planets:
if _house_of(lord_7) == 10 and _house_of(lord_10) == 1:
yogas.append({"name": "Sreenatha Yoga", "name_cn": "幸运之主格局", "combination": f"7宫主{lord_7}在10宫+10宫主{lord_10}在1宫", "effects": ["事业辉煌", "婚姻美满", "贵人相助"], "strength": ""})
# ========================================================================
# 9. Matsya Yoga(鱼格局)
# 来源:BPHS。1/5/9 宫中有吉星 + 6/8 宫有凶星 + 上升与木星/金星有连接
# 简化版:三方宫(5/9)有吉星 + 6/8宫有凶星
# ========================================================================
benefics = ['Jupiter', 'Venus', 'Mercury']
malefics = ['Mars', 'Saturn', 'Sun', 'Rahu', 'Ketu']
ben_in_trikona = [p for p in benefics if p in planets and _house_of(p) in [5, 9]]
mal_in_dusthana = [p for p in malefics if p in planets and _house_of(p) in [6, 8]]
if ben_in_trikona and mal_in_dusthana:
yogas.append({"name": "Matsya Yoga", "name_cn": "鱼格局", "combination": f"{''.join(ben_in_trikona)}在三方宫+{''.join(mal_in_dusthana)}在凶宫", "effects": ["聪慧过人", "直觉敏锐", "灵性成长"], "strength": ""})
# ========================================================================
# 10. Koorma Yoga(龟格局)
# 来源:BPHS。5/9 宫主落入三方宫 + 1 宫有吉星
# 简化版:5宫主或9宫主在1宫,且1宫有吉星
# ========================================================================
ben_in_1 = [p for p in benefics if p in planets and _house_of(p) == 1]
lord_5_or_9_in_1 = False
for lp in [lord_5, lord_9]:
if lp in planets and _house_of(lp) == 1:
lord_5_or_9_in_1 = True
if ben_in_1 and lord_5_or_9_in_1:
yogas.append({"name": "Koorma Yoga", "name_cn": "龟格局", "combination": f"吉星在1宫+5/9宫主在1宫", "effects": ["智慧深厚", "耐心坚韧", "精神修养"], "strength": ""})
# ========================================================================
# 11. Khadga Yoga(剑格局)
# 来源:BPHS。2 宫主落入 2 宫且不被凶星占据 + 9 宫主有力
# 简化版:2 宫主在 2 宫且 2 宫无凶星
# ========================================================================
lord_2 = _lord_of_house(2)
if lord_2 in planets and _house_of(lord_2) == 2:
mal_in_2 = [p for p in malefics if p in planets and _house_of(p) == 2]
if not mal_in_2:
yogas.append({"name": "Khadga Yoga", "name_cn": "剑格局", "combination": f"2宫主{lord_2}在2宫且2宫无凶星", "effects": ["财富充裕", "口才出众", "品格坚定"], "strength": ""})
# ========================================================================
# 12. Kusuma Yoga(花格局)
# 来源:BPHS。木星在 10 宫 + 月亮在 1 宫 + 太阳在 2 宫 + 金星在 9 宫
# ========================================================================
jup_10 = 'Jupiter' in planets and _house_of('Jupiter') == 10
moon_1 = 'Moon' in planets and _house_of('Moon') == 1
sun_2 = 'Sun' in planets and _house_of('Sun') == 2
ven_9 = 'Venus' in planets and _house_of('Venus') == 9
checks_kusuma = [jup_10, moon_1, sun_2, ven_9]
met_kusuma = sum(checks_kusuma)
if met_kusuma >= 3:
parts = []
if jup_10: parts.append("木星在10宫")
if moon_1: parts.append("月亮在1宫")
if sun_2: parts.append("太阳在2宫")
if ven_9: parts.append("金星在9宫")
yogas.append({"name": "Kusuma Yoga", "name_cn": "花格局", "combination": "+".join(parts), "effects": ["才貌双全", "受人爱戴", "幸福美满"], "strength": "" if met_kusuma == 4 else "中强"})
# ========================================================================
# 13. Chandra-Mangala Yoga(日月-火星格局)
# 来源:BPHS。火星与月亮同宫或在 2/12 宫关系
# ========================================================================
if 'Moon' in planets and 'Mars' in planets:
mh = _house_of('Moon')
marsh = _house_of('Mars')
if mh == marsh:
yogas.append({"name": "Chandra-Mangala Yoga", "name_cn": "日月火星同宫格局", "combination": f"火星与月亮同在第{mh}", "effects": ["财富积累", "行动力强", "情绪驱动成功"], "strength": "" if _is_exalted('Mars') or _is_own_sign('Mars') else ""})
elif abs(mh - marsh) in [1, 11]:
yogas.append({"name": "Chandra-Mangala Yoga", "name_cn": "日月火星关联格局", "combination": f"火星在第{marsh}宫,月亮在第{mh}宫(2/12宫关系)", "effects": ["财运亨通", "精力充沛", "商业头脑"], "strength": ""})
# ========================================================================
# 14. Dwi-Gurupada Yoga / Guru-Mangala Yoga(木火格局)
# 来源:木星与火星同宫或互换星座
# ========================================================================
if 'Jupiter' in planets and 'Mars' in planets:
jup_s = _sign_of('Jupiter')
mars_s = _sign_of('Mars')
jup_h = _house_of('Jupiter')
mars_h = _house_of('Mars')
if jup_h == mars_h:
yogas.append({"name": "Guru-Mangala Yoga", "name_cn": "木火同宫格局", "combination": f"木星与火星同在第{jup_h}", "effects": ["智慧与行动力兼备", "正义感强", "领导能力"], "strength": "中强"})
elif SIGN_LORDS.get(jup_s) == 'Mars' and SIGN_LORDS.get(mars_s) == 'Jupiter':
yogas.append({"name": "Guru-Mangala Yoga", "name_cn": "木火互容格局", "combination": f"木星在{jup_s}(火星守护)+火星在{mars_s}(木星守护)", "effects": ["智慧与行动力兼备", "正义感强", "领导能力"], "strength": ""})
# ========================================================================
# 15. Budhaditya Yoga(水日格局)
# 来源:BPHS。水星与太阳同宫
# ========================================================================
if 'Mercury' in planets and 'Sun' in planets:
if _house_of('Mercury') == _house_of('Sun'):
yogas.append({"name": "Budhaditya Yoga", "name_cn": "水日同宫格局", "combination": f"水星与太阳同在第{_house_of('Sun')}", "effects": ["智力超群", "口才出众", "学识渊博"], "strength": "中强" if not _is_debilitated('Mercury') else ""})
# ========================================================================
# 16. Lakshmi Yoga(拉克什米格局/财富女神格局)
# 来源:BPHS。9 宫主在入旺/入庙星座且落入 1 宫
# ========================================================================
if lord_9 in planets:
l9h = _house_of(lord_9)
if l9h == 1 and (_is_exalted(lord_9) or _is_own_sign(lord_9)):
st = "入旺" if _is_exalted(lord_9) else "入庙"
yogas.append({"name": "Lakshmi Yoga", "name_cn": "财富女神格局", "combination": f"9宫主{lord_9}{st}在1宫", "effects": ["财富充裕", "生活幸福", "品德高尚"], "strength": ""})
# ========================================================================
# 17. Vasumati Yoga(大地格局)
# 来源:BPHS。所有吉星(木星/金星/水星/月亮)在上升盘的 3/6/10/11 宫(成长宫)
# ========================================================================
upachaya_benefics = [p for p in ['Jupiter', 'Venus', 'Mercury', 'Moon'] if p in planets and _is_upachaya(_house_of(p))]
if len(upachaya_benefics) >= 3:
yogas.append({"name": "Vasumati Yoga", "name_cn": "大地格局", "combination": f"{len(upachaya_benefics)}颗吉星在成长宫(3/6/10/11)", "effects": ["财富丰厚", "生活富足", "物质成功"], "strength": "中强" if len(upachaya_benefics) >= 4 else ""})
# ========================================================================
# 18. Vanchana Kalusha Yoga / Papakartari Yoga(凶星夹宫格局)
# 来源:Brihat Jataka。某宫被两侧凶星夹击(前一宫和后一宫均有凶星)
# ========================================================================
for target_house in [1, 4, 7, 10, 5, 9]:
prev_h = ((target_house - 2) % 12) + 1
next_h = (target_house % 12) + 1
mal_before = [p for p in malefics if p in planets and _house_of(p) == prev_h]
mal_after = [p for p in malefics if p in planets and _house_of(p) == next_h]
if mal_before and mal_after:
house_names = {1: "命宫", 4: "田宅宫", 7: "夫妻宫", 10: "事业宫", 5: "子女宫", 9: "迁移宫"}
yogas.append({"name": "Papakartari Yoga", "name_cn": f"凶星夹{house_names.get(target_house, f'{target_house}')}格局", "combination": f"{target_house}宫被{''.join(mal_before)}{''.join(mal_after)}前后夹击", "effects": [f"{house_names.get(target_house, f'{target_house}')}领域受限", "需要额外努力", "阻碍与挑战"], "strength": ""})
break # 只报告一次
# ========================================================================
# 19. Subhakartari Yoga(吉星夹宫格局)
# 来源:Brihat Jataka。与 Papakartari 相反——某宫被两侧吉星包围
# ========================================================================
for target_house in [1, 4, 7, 10, 5, 9]:
prev_h = ((target_house - 2) % 12) + 1
next_h = (target_house % 12) + 1
ben_before = [p for p in benefics if p in planets and _house_of(p) == prev_h]
ben_after = [p for p in benefics if p in planets and _house_of(p) == next_h]
if ben_before and ben_after:
house_names = {1: "命宫", 4: "田宅宫", 7: "夫妻宫", 10: "事业宫", 5: "子女宫", 9: "迁移宫"}
yogas.append({"name": "Subhakartari Yoga", "name_cn": f"吉星护{house_names.get(target_house, f'{target_house}')}格局", "combination": f"{target_house}宫被{''.join(ben_before)}{''.join(ben_after)}吉星守护", "effects": [f"{house_names.get(target_house, f'{target_house}')}领域受保护", "顺利发展", "贵人助力"], "strength": ""})
break
# ========================================================================
# 20. Saraswati Yoga(辩才天女格局)
# 来源:BPHS。木星/金星/水星在 2/4/7/9/10 宫,且木星在自身友好星座
# ========================================================================
saraswati_houses = [2, 4, 7, 9, 10]
saraswati_planets = [p for p in ['Jupiter', 'Venus', 'Mercury'] if p in planets and _house_of(p) in saraswati_houses]
if len(saraswati_planets) >= 3 or (len(saraswati_planets) >= 2 and 'Jupiter' in saraswati_planets):
jup_dignity = _get_dignity_level('Jupiter', _sign_of('Jupiter')) if 'Jupiter' in planets else ''
if jup_dignity in ['EXALTED', 'OWN_SIGN', 'MOOLATRIKONA', 'FRIEND']:
yogas.append({"name": "Saraswati Yoga", "name_cn": "辩才天女格局", "combination": f"{''.join(saraswati_planets)}在吉宫(2/4/7/9/10)+木星有力", "effects": ["学问渊博", "艺术才华", "表达卓越"], "strength": "" if jup_dignity in ['EXALTED', 'OWN_SIGN'] else "中强"})
# ========================================================================
# 21. Hamsa Yoga(天鹅格局)—— 增强版 Mahapurusha
# 来源:BPHS。木星入旺/入庙在角宫(1/4/7/10)
# (注:基本版已包含在 Mahapurusha Yoga 中,这里增加三方宫扩展检测)
# ========================================================================
if 'Jupiter' in planets:
jup_h = _house_of('Jupiter')
if _is_trikona(jup_h) and (_is_exalted('Jupiter') or _is_own_sign('Jupiter')):
st = "入旺" if _is_exalted('Jupiter') else "入庙"
already_hamsa = any(y.get("name") == "Hamsa Yoga" for y in yogas)
if not already_hamsa:
yogas.append({"name": "Hamsa Yoga (Trikona)", "name_cn": f"木星{st}三方宫格局", "combination": f"木星{st}{_sign_of('Jupiter')}(第{jup_h}宫,三方宫)", "effects": ["智慧卓越", "精神导师", "品德高尚"], "strength": ""})
# ========================================================================
# 22. Chamara Yoga(拂尘格局)
# 来源:BPHS。上升主星在入旺/入庙 + 两个或以上吉星在角宫/三方宫
# ========================================================================
if asc_lord in planets and (_is_exalted(asc_lord) or _is_own_sign(asc_lord)):
ben_kt = [p for p in benefics if p in planets and _planet_in_kendra_or_trikona(p)]
if len(ben_kt) >= 2:
st = "入旺" if _is_exalted(asc_lord) else "入庙"
yogas.append({"name": "Chamara Yoga", "name_cn": "拂尘格局", "combination": f"上升主星{asc_lord}{st}+{''.join(ben_kt)}在角宫/三方宫", "effects": ["长寿健康", "受人尊敬", "智慧通达"], "strength": ""})
# ========================================================================
# 23. Akhanda Samrajya Yoga(永恒帝王格局)
# 来源:BPHS。木星在 2/5/9/11 宫 + 2 宫主不在 6/8/12 宫 + 11 宫主在 11 宫
# 简化版:木星在 2/5/9/11 + 11 宫主在 11 宫或角宫
# ========================================================================
if 'Jupiter' in planets and _house_of('Jupiter') in [2, 5, 9, 11]:
lord_11 = _lord_of_house(11)
if lord_11 in planets:
l11h = _house_of(lord_11)
if l11h == 11 or _is_kendra(l11h):
yogas.append({"name": "Akhanda Samrajya Yoga", "name_cn": "永恒帝王格局", "combination": f"木星在{_house_of('Jupiter')}宫+11宫主{lord_11}{l11h}", "effects": ["权力持久", "事业辉煌", "影响力广泛"], "strength": ""})
# ========================================================================
# 24. Gurumauli Yoga / Maha Bhagya Yoga(大运格局)
# 来源:经典文献。木星在 9 宫或与 9 宫主同宫
# ========================================================================
if 'Jupiter' in planets:
jup_h = _house_of('Jupiter')
if jup_h == 9:
yogas.append({"name": "Gurumauli Yoga", "name_cn": "至上师格局", "combination": f"木星在9宫(命运宫)", "effects": ["命运眷顾", "精神导师指引", "福德深厚"], "strength": ""})
elif lord_9 in planets and jup_h == _house_of(lord_9):
yogas.append({"name": "Gurumauli Yoga", "name_cn": "至上师关联格局", "combination": f"木星与9宫主{lord_9}同在第{jup_h}", "effects": ["命运眷顾", "贵人运强", "智慧通达"], "strength": "中强"})
# ========================================================================
# 25. Shakata Yoga(车格局——凶)
# 来源:BPHS。木星在 6/8/12 宫(从月亮算起),且月亮与木星不在 1/4/7/10 宫关系
# 简化版(从上升算):木星落入 6/8/12 宫
# ========================================================================
if 'Jupiter' in planets and _house_of('Jupiter') in [6, 8, 12]:
yogas.append({"name": "Shakata Yoga", "name_cn": "车格局(凶)", "combination": f"木星落入第{_house_of('Jupiter')}宫(凶宫)", "effects": ["财富不稳", "健康需注意", "精神起伏"], "strength": ""})
# ========================================================================
# 26. Amarishta Yoga / Daridra Yoga(贫困格局——凶)
# 来源:BPHS。11 宫主落入 6/8/12 宫,或 2 宫主落陷且在凶宫
# ========================================================================
lord_11 = _lord_of_house(11)
if lord_11 in planets and _house_of(lord_11) in [6, 8, 12]:
yogas.append({"name": "Daridra Yoga", "name_cn": "贫困格局(凶)", "combination": f"11宫主{lord_11}落入第{_house_of(lord_11)}宫(凶宫)", "effects": ["财务困难", "需努力积累", "谨慎理财"], "strength": ""})
if lord_2 in planets and _is_debilitated(lord_2) and _is_dusthana(_house_of(lord_2)):
yogas.append({"name": "Daridra Yoga (variant)", "name_cn": "贫困格局变体(凶)", "combination": f"2宫主{lord_2}落陷在凶宫第{_house_of(lord_2)}", "effects": ["财务压力", "家庭纠纷", "需节俭持家"], "strength": ""})
# ========================================================================
# 27. Dhan Yoga 增强版 —— Grahi Dhan Yoga
# 来源:Phaladeepika。1 宫主 + 2 宫主 + 11 宫主同在 2/11 宫
# ========================================================================
lords_wealth = {1: SIGN_LORDS[SIGNS[(ai + 0) % 12]], 2: lord_2, 11: _lord_of_house(11)}
for wh in [2, 11]:
count = sum(1 for h_num, lp in lords_wealth.items() if lp in planets and _house_of(lp) == wh)
if count >= 2:
in_house = [f"{h_num}宫主{lp}" for h_num, lp in lords_wealth.items() if lp in planets and _house_of(lp) == wh]
yogas.append({"name": "Grahi Dhana Yoga", "name_cn": "星聚财富格局", "combination": f"{''.join(in_house)}同在第{wh}", "effects": ["财运亨通", "投资有利", "收入丰厚"], "strength": ""})
break
return {"ascendant": asc, "planets_analyzed": len(planets), "kendra_lords": kl, "trikona_lords": tl, "yogas_detected": len(yogas), "yogas": yogas}
# ============================================================================
# 4. 三层验证法事件预测(v3.4增强:优先EventPredictionModel规则引擎)
# ============================================================================
def cmd_predict(args):
# 验前事模式(v3.5新增)
if getattr(args, 'past_verify', False) and args.year:
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
return _past_event_verify(chart, asc_idx, args)
chart = json.loads(args.chart) if args.chart else {}
evt = args.event_type or "all"
# 尝试加载 EventPredictionModel(替代LAM神经网络)
try:
sys.path.insert(0, SCRIPT_DIR)
from event_prediction_model import EventPredictionModel
# 直接传完整chart数据给EventPredictionModelv5.0需要ascendant dict和planets dict
# 同时从 full-reading 输出中提取所有模块数据传入(v5.1修复:之前丢失dasha/congregation等)
modules = chart.get("modules", {})
model = EventPredictionModel(
chart_data={
"ascendant": chart.get("ascendant", {}),
"planets": chart.get("planets", {}),
},
dasha_data=modules.get("dasha"),
congregation_data=modules.get("congregation"),
vivah_saham_data=modules.get("vivah_saham"),
chara_dasha_data=modules.get("jaimini", {}).get("chara_dasha"),
)
raw_preds = model.predict_all_events()
# 将 Prediction dataclass 转为可序列化 dictv5.1补充缺失字段)
predictions = []
for p in raw_preds:
predictions.append({
"event_type": str(p.event_type.value) if hasattr(p.event_type, 'value') else str(p.event_type),
"description": p.description,
"probability": p.probability,
"risk_level": str(p.risk_level.value) if hasattr(p.risk_level, 'value') else str(p.risk_level),
"confidence": str(p.confidence.value) if hasattr(p.confidence, 'value') else str(p.confidence),
"timing": p.timing,
"key_factors": p.key_factors,
"recommendations": p.recommendations,
"dasha_signals": p.dasha_signals,
"transit_signals": p.transit_signals,
"timing_windows": p.timing_windows,
})
return {
"method": "三层验证法(EventPredictionModel规则引擎)",
"engine": "event_prediction_model.py",
"event_type": evt,
"predictions": predictions,
"note": "基于规则引擎的三层验证法,替代LAM神经网络(准确率从0.17%大幅提升)"
}
except Exception as e:
# 降级到简化版
result = {"method": "三层验证法(简化版)", "fallback_reason": str(e),
"event_type": evt, "predictions": []}
planets = chart.get("planets", {})
indicators_map = {
"marriage": {"houses": [7], "karaka": "Venus", "cn": "婚姻"},
"career": {"houses": [10, 6], "karaka": "Sun", "cn": "职业"},
"wealth": {"houses": [2, 11], "karaka": "Jupiter", "cn": "财富"},
"health": {"houses": [6, 8, 12], "karaka": "Saturn", "cn": "健康"},
}
for ek, ei in indicators_map.items():
if evt != "all" and evt != ek: continue
found = []
for hn in ei["houses"]:
for pn, pd in planets.items():
if isinstance(pd, dict) and pd.get("house") == hn:
found.append({"planet": pn, "house": hn, "sign": pd.get("sign", ""), "status": pd.get("status", "中性")})
if found:
result["predictions"].append({"event": ei["cn"], "key": ek, "static_indicators": found, "note": "需要结合Dasha和Transit进行精确预测"})
return result
# ============================================================================
# 验前事模式(v3.5新增,避免冷读效应)
# ============================================================================
def _past_event_verify(chart: Dict, asc_idx: int, args) -> Dict:
"""
验前事:从星盘数据推断 2-4 个高信号历史时段,供用户确认。
AI 先推断,用户后确认——避免冷读效应。
"""
planets = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
birth_year = args.year
signals = []
# 1. 土星回归(约29.5年一次)
saturn_sign = planets.get('Saturn', {}).get('sign', '')
saturn_house = planets.get('Saturn', {}).get('house', 0)
# 土星绕黄道一圈约29.5年
for cycle_age in [29, 58]:
event_year = birth_year + cycle_age
signals.append({
'type': '土星回归',
'age': cycle_age,
'year': event_year,
'description': f'{event_year}年({cycle_age}岁),土星回归周期',
'confidence': '',
'indicators': [f'土星在{saturn_sign}(第{saturn_house}宫)'],
})
# 2. 木星回归(约12年一次)
jupiter_sign = planets.get('Jupiter', {}).get('sign', '')
jupiter_house = planets.get('Jupiter', {}).get('house', 0)
for cycle_age in [12, 24, 36, 48]:
event_year = birth_year + cycle_age
signals.append({
'type': '木星回归',
'age': cycle_age,
'year': event_year,
'description': f'{event_year}年({cycle_age}岁),木星回归周期',
'confidence': '中高',
'indicators': [f'木星在{jupiter_sign}(第{jupiter_house}宫)'],
})
# 3. Rahu-Ketu 对冲过境(约18.6年半周期)
rahu_sign = planets.get('Rahu', {}).get('sign', '')
for half_cycle in [9, 18, 27, 36]:
event_year = birth_year + half_cycle
signals.append({
'type': 'Rahu-Ketu半周期',
'age': half_cycle,
'year': event_year,
'description': f'{event_year}年({half_cycle}岁),Rahu-Ketu对冲轴变化',
'confidence': '',
'indicators': [f'本命Rahu在{rahu_sign}'],
})
# 4. 关键宫位激活(基于 Dasha 可能性)
# 7宫主星相关 → 婚姻/合作时间窗
libra_idx = SIGNS.index('Libra') if 'Libra' in SIGNS else 6
sign_7 = SIGNS[(asc_idx + 6) % 12]
lord_7 = SIGN_LORDS.get(sign_7, 'Unknown')
lord_7_info = planets.get(lord_7, {})
if lord_7_info:
signals.append({
'type': '7宫主星活跃期',
'age_range': '24-32',
'year_range': f'{birth_year + 24}-{birth_year + 32}',
'description': f'{lord_7}7宫主星,7宫={sign_7})活跃期,可能涉及婚姻/重要合作',
'confidence': '',
'indicators': [f'{lord_7}{lord_7_info.get("sign", "")}(第{lord_7_info.get("house", 0)}宫)'],
})
# 5. 10宫主星相关 → 事业突破
sign_10 = SIGNS[(asc_idx + 9) % 12]
lord_10 = SIGN_LORDS.get(sign_10, 'Unknown')
lord_10_info = planets.get(lord_10, {})
if lord_10_info:
signals.append({
'type': '10宫主星活跃期',
'age_range': '28-40',
'year_range': f'{birth_year + 28}-{birth_year + 40}',
'description': f'{lord_10}10宫主星,10宫={sign_10})活跃期,可能涉及事业突破',
'confidence': '',
'indicators': [f'{lord_10}{lord_10_info.get("sign", "")}(第{lord_10_info.get("house", 0)}宫)'],
})
# 按置信度排序,取前4
priority = {'': 3, '中高': 2, '': 1, '': 0}
signals.sort(key=lambda s: priority.get(s.get('confidence', ''), 0), reverse=True)
top_signals = signals[:4]
return {
'method': '验前事(Past Event Reverse Verification',
'version': '3.5',
'note': 'AI从星盘推断的高信号历史时段,请用户确认——避免冷读效应',
'birth_year': birth_year,
'ascendant': asc_sign,
'inferred_periods': top_signals,
'disclaimer': '这些是基于星盘结构推断的可能时段,需要用户确认是否实际发生了相关事件。',
}
# ============================================================================
# 5. 分盘计算
# ============================================================================
def cmd_varga(args):
if not HAS_SWE: return {"error": "swisseph未安装"}
swe.set_ephe_path('')
hd = args.hour + args.minute / 60.0 - args.tz
jd = swe.julday(args.year, args.month, args.day, hd)
# Lahiri Ayanamsa(恒星黄道修正,与cmd_chart一致)
ayanamsa = swe.get_ayanamsa(jd)
natal = {}
for pn, pid in PLANETS_SWE.items():
pos, _ = swe.calc_ut(jd, pid); natal[pn] = (pos[0] - ayanamsa) % 360 # 恒星黄道
if 'Rahu' in natal: natal['Ketu'] = (natal['Rahu'] + 180) % 360
asc_lon, _ = swe.houses(jd, args.lat, args.lon, b'A'); asc_deg = (asc_lon[0] - ayanamsa) % 360 # 恒星黄道
def navamsa(lon):
si = int(lon / 30); d = lon - si * 30; ni = int(d / (30/9))
el_starts = {0: 0, 1: 9, 2: 6, 3: 3}; return SIGNS[(el_starts[si % 4] + ni) % 12]
def dasamsa(lon):
si = int(lon / 30); d = lon - si * 30; di = int(d / 3)
return SIGNS[di % 12] if si % 2 == 0 else SIGNS[(6 + di) % 12]
result = {"birth_info": f"{args.year}-{args.month:02d}-{args.day:02d} {args.hour:02d}:{args.minute:02d}", "divisional_charts": {}}
if args.d9 or args.all:
d9 = {"ascendant": navamsa(asc_deg)}
for p, l in natal.items(): d9[p] = {"sign": navamsa(l), "sign_cn": SIGNS_CN[navamsa(l)]}
result["divisional_charts"]["D9_Navamsa"] = d9
if args.d10 or args.all:
d10 = {"ascendant": dasamsa(asc_deg)}
for p, l in natal.items(): d10[p] = {"sign": dasamsa(l), "sign_cn": SIGNS_CN[dasamsa(l)]}
result["divisional_charts"]["D10_Dasamsa"] = d10
if not result["divisional_charts"]: result["note"] = "请指定 --d9, --d10 或 --all"
return result
# ============================================================================
# 6. 名人案例查询
# ============================================================================
def cmd_celebrity(args):
result = {"query": args.name or "all", "results": []}
if os.path.exists(DB_PATH):
try:
conn = sqlite3.connect(DB_PATH); c = conn.cursor()
if args.name: c.execute("SELECT * FROM cases WHERE name LIKE ?", (f'%{args.name}%',))
else: c.execute("SELECT * FROM cases LIMIT ?", (args.limit or 20,))
cols = [d[0] for d in c.description]
for r in c.fetchall(): result["results"].append(dict(zip(cols, r)))
conn.close()
except Exception as e: result["db_error"] = str(e)
if os.path.exists(PERSON_CSV) and args.name:
try:
matches = []
with open(PERSON_CSV, 'r', encoding='utf-8') as f:
for row in csv.DictReader(f):
if args.name.lower() in row.get('Name', '').lower():
matches.append({"name": row.get('Name', ''), "birth_time": row.get('BirthTime', ''), "gender": row.get('Gender', '')})
if len(matches) >= 10: break
result["person_list_matches"] = matches; result["person_list_total"] = 15807
except Exception as e: result["csv_error"] = str(e)
return result
# ============================================================================
# 7. 数据库统计
# ============================================================================
def cmd_db_stats(args):
result = {}
if os.path.exists(DB_PATH):
try:
conn = sqlite3.connect(DB_PATH); c = conn.cursor()
c.execute("SELECT COUNT(*) FROM cases"); result["total_cases"] = c.fetchone()[0]
c.execute("SELECT case_type, COUNT(*) FROM cases GROUP BY case_type"); result["by_type"] = dict(c.fetchall())
c.execute("SELECT difficulty, COUNT(*) FROM cases GROUP BY difficulty"); result["by_difficulty"] = dict(c.fetchall())
c.execute("SELECT name, accuracy_rate, sample_size, correct_predictions FROM techniques"); result["techniques"] = [{"name": r[0], "accuracy": r[1], "samples": r[2], "correct": r[3]} for r in c.fetchall()]
conn.close()
except Exception as e: result["error"] = str(e)
else:
result["error"] = f"数据库不存在: {DB_PATH}"
return result
# ============================================================================
# 8. 过境查询
# ============================================================================
def cmd_transit(args):
"""
实时 Transit 行星过境计算(v3.7.2 改用 Swiss Ephemeris
不再依赖静态 JSON,直接用 swe 计算任意日期的行星位置。
支持指定日期范围(--year/--month)和目标行星(--planet)。
"""
if not HAS_SWE:
return {"error": "swisseph未安装,无法计算实时Transit"}
swe.set_ephe_path('')
# --- 参数解析 ---
t_year = args.year
t_month = args.month
# 可选:指定某日(默认取月中15号做代表)
t_day = getattr(args, 'day', 15) or 15
# 可选:指定目标行星(默认全部七曜+Rahu/Ketu)
target_planets_str = getattr(args, 'planet', None)
target_planets = [p.strip() for p in target_planets_str.split(',')] if target_planets_str else None
# --- 计算该月月中行星位置 ---
try:
hd = 12.0 - (getattr(args, 'tz', 8) or 8) # 默认UTC+8中午
jd_mid = swe.julday(t_year, t_month, t_day, hd)
except Exception as e:
return {"error": f"日期计算失败: {e}"}
ayanamsa = swe.get_ayanamsa(jd_mid)
# --- 计算行星位置 ---
node_mode = getattr(args, 'node_mode', 'mean')
transit_data, ayanamsa = _calc_sidereal_planets_for_jd(jd_mid, node_mode=node_mode, include_ketu=True)
if target_planets:
target_set = set(target_planets)
transit_data = {p: d for p, d in transit_data.items() if p in target_set}
# --- 计算行星间相位(互相在对方星座的宫位关系)---
aspects_found = []
planet_names = list(transit_data.keys())
for i, p1 in enumerate(planet_names):
for p2 in planet_names[i+1:]:
if p1 == 'Ketu' or p2 == 'Ketu':
continue
d1 = transit_data[p1]['degree']
d2 = transit_data[p2]['degree']
diff = abs(d1 - d2) % 360
if diff > 180:
diff = 360 - diff
# 合相(≤10°)
if diff <= 10:
aspects_found.append({
'type': 'conjunction',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}合相({diff:.1f}°)'
})
# 对冲(180°±8°)
elif 172 <= diff <= 188:
aspects_found.append({
'type': 'opposition',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}对冲({diff:.1f}°)'
})
# 三方(120°±6°)
elif 114 <= diff <= 126:
aspects_found.append({
'type': 'trine',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}三方相位({diff:.1f}°)'
})
# 四分(90°±6°)
elif 84 <= diff <= 96:
aspects_found.append({
'type': 'square',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}四分相位({diff:.1f}°)'
})
# --- 构建结果 ---
result = {
'method': 'Swiss Ephemeris 实时计算(v3.7.2',
'target_date': f'{t_year}-{t_month:02d}-{t_day:02d}',
'ayanamsa': round(ayanamsa, 4),
'node_mode': node_mode,
'data_layer': 'true_transit_positions',
'planets': transit_data,
'aspects': aspects_found,
'note': f'使用Swiss Ephemeris实时计算{t_year}{t_month}月行星过境位置,不再依赖静态JSON'
}
return result
# ============================================================================
# 8b. Double Transit PAC + D9 层(KN Rao 完整实现 v3.9新增)
#
# 核心逻辑:
# 1. D1 层: Saturn/Jupiter 通过 PAC 关联事件宫/宫主/LL/对宫主
# 2. D9 层: Saturn/Jupiter 通过 PAC 关联 D9 宫主/D9 Asc/宫主D9星座/LL D9星座
# 3. 两者必须同时激活同一目标 -> Double Transit 确认
#
# 精度: KN Rao 体系 110-115 星盘测试 97% 准确率(使用 D9 Navamsa
# ============================================================================
def _navamsa_idx(lon):
"""Navamsa 星座索引"""
lon = lon % 360
si = int(lon / 30)
d = lon - si * 30
ni = int(d / (30 / 9))
el_starts = [0, 9, 6, 3] # Aries/Fire=0, Taurus/Earth=9, Gemini/Air=6, Cancer/Water=3
return (el_starts[si % 4] + ni) % 12
def _calc_planetary_congregation(planets: Dict, asc_idx: int) -> Dict:
"""
本命盘行星聚集检测(供 cmd_full_reading 调用)
检测 3+ 行星同宫的聚集效应,返回聚集宫位、行星列表、影响领域
"""
houses = {}
for pn, pd in planets.items():
if not isinstance(pd, dict) or 'sign' not in pd:
continue
if pn in ['Rahu', 'Ketu']:
continue # Rahu/Ketu 不计入聚集
si = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
h = ((si - asc_idx) % 12) + 1
houses.setdefault(h, []).append(pn)
congregations = []
for h, plist in houses.items():
if len(plist) >= 3:
h_sign = SIGNS[(asc_idx + h - 1) % 12]
# 判断影响领域
impact = _house_theme(h)
# 判断聚集力量(有无吉星/凶星)
benefics = [p for p in plist if p in ['Jupiter', 'Venus', 'Mercury', 'Moon']]
malefics = [p for p in plist if p in ['Saturn', 'Mars', 'Sun', 'Rahu']]
strength = 'strong' if len(benefics) > len(malefics) else 'mixed'
if len(malefics) >= 3:
strength = 'malefic_heavy'
congregations.append({
'house': h,
'sign': h_sign,
'planets': plist,
'count': len(plist),
'benefics': benefics,
'malefics': malefics,
'strength': strength,
'impact': impact,
'description': f'{",".join(plist)} 聚集于{h}宫({h_sign})',
})
return {
'congregations': congregations,
'total': len(congregations),
'note': '3+ 行星同宫为显著聚集,影响该宫主题领域',
}
def _house_theme(house: int) -> List[str]:
"""返回宫位影响领域"""
themes = {
1: ['自我', '健康', '性格'],
2: ['财富', '家庭', '言语'],
3: ['沟通', '旅行', '兄弟'],
4: ['母亲', '房产', '情感'],
5: ['子女', '投资', '创意'],
6: ['疾病', '敌人', '债务'],
7: ['婚姻', '合作', '伴侣'],
8: ['转型', '意外', '遗产'],
9: ['命运', '父亲', '灵性'],
10: ['事业', '声望', '成就'],
11: ['收益', '社交', '愿望'],
12: ['损失', '外迁', '解脱'],
}
return themes.get(house, ['未知'])
def _calc_vivah_saham(planets: Dict, asc_deg: float) -> Dict:
"""
计算本命 Vivah Saham 婚姻敏感点(供 cmd_full_reading 调用)
公式: Saham = (Venus_lon - Saturn_lon + Asc_deg) % 360
"""
venus_lon = planets.get('Venus', {}).get('degree', 0)
saturn_lon = planets.get('Saturn', {}).get('degree', 0)
if venus_lon == 0 or saturn_lon == 0:
return {'error': '缺少金星或土星数据', 'saham': None}
sahams_lon = (venus_lon - saturn_lon + asc_deg) % 360
sahams_sign = SIGNS[int(sahams_lon / 30) % 12]
sahams_deg_in_sign = sahams_lon % 30
sahams_si = int(sahams_lon / 30) % 12
# 检查哪些本命行星与 Saham 同宫/合相
conjuncts = []
for pn, pd in planets.items():
if pn in ['Rahu', 'Ketu']:
continue
if not isinstance(pd, dict) or 'degree' not in pd:
continue
p_lon = pd['degree']
diff = abs(p_lon - sahams_lon) % 360
if diff > 180:
diff = 360 - diff
if diff <= 5:
conjuncts.append({'planet': pn, 'diff_deg': round(diff, 2)})
# 从 Saham 位置反推婚姻相关宫位
asc_si = int(asc_deg / 30) % 12
sahams_house = ((sahams_si - asc_si) % 12) + 1
return {
'saham_lon': round(sahams_lon, 4),
'saham_sign': sahams_sign,
'saham_deg_in_sign': round(sahams_deg_in_sign, 2),
'saham_house': sahams_house,
'formula': f'Venus({venus_lon:.2f}°) - Saturn({saturn_lon:.2f}°) + Asc({asc_deg:.2f}°)',
'natal_conjuncts': conjuncts,
'marriage_relevance': 'high' if sahams_house in [7, 1, 5, 9] else 'moderate',
'note': 'Vivah Saham 是度数级婚姻敏感点,Transit 木星/土星过境此点时触发婚姻事件窗',
}
def _check_pac(planet_name, planet_lon, target_lon, asc_idx):
"""PAC检查: Position(同宫)/Aspect(相位)/Conjunction(合相<=10度)"""
results = []
p_si = int((planet_lon % 360) / 30)
t_si = int((target_lon % 360) / 30)
p_house = ((p_si - asc_idx) % 12) + 1
t_house = ((t_si - asc_idx) % 12) + 1
# P: Position - 同宫
if p_house == t_house:
results.append({'type': 'Position', 'desc': f'同宫({t_house}宫)'})
# C: Conjunction - 合相 <=10度
diff = abs(planet_lon - target_lon) % 360
if diff > 180:
diff = 360 - diff
if diff <= 10:
results.append({'type': 'Conjunction', 'desc': f'合相({diff:.2f}\u00b0)'})
# A: Aspect - Graha Drishti
planet_aspects = {
'Sun': [7], 'Moon': [7], 'Mars': [4, 7, 8], 'Mercury': [7],
'Jupiter': [5, 7, 9], 'Venus': [7], 'Saturn': [3, 7, 10],
'Rahu': [5, 7, 9], 'Ketu': [5, 7, 9],
}
aspects = planet_aspects.get(planet_name, [7])
for offset in aspects:
if ((t_house - p_house + 12) % 12) == offset:
results.append({'type': 'Aspect', 'offset': offset, 'desc': f'{offset}宫相位'})
return results
def cmd_double_transit_pac(args):
"""Double Transit PAC + D9 层计算"""
# 1. 计算本命星盘
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
asc_deg = chart.get('ascendant', {}).get('degree', 0)
event_house = args.house or 7
# 2. 计算过境行星位置
if not HAS_SWE:
return {"error": "swisseph未安装"}
transit_year, transit_month, transit_day = map(int, args.date.split('-'))
transit_hour = 12.0 - args.tz # 正午 UT
transit_jd = swe.julday(transit_year, transit_month, transit_day, transit_hour)
transit_ayanamsa = swe.get_ayanamsa(transit_jd)
transit_planets = {}
for pname, pid in PLANETS_SWE.items():
try:
pos, _ = swe.calc_ut(transit_jd, pid)
lon_p = (pos[0] - transit_ayanamsa) % 360
transit_planets[pname] = {'lon': lon_p, 'sign': SIGNS[int(lon_p / 30)]}
if pname == 'Rahu':
klon = (lon_p + 180) % 360
transit_planets['Ketu'] = {'lon': klon, 'sign': SIGNS[int(klon / 30)]}
except Exception as e:
transit_planets[pname] = {'error': str(e)}
# 3. D1 层敏感点
event_si = (asc_idx + event_house - 1) % 12
event_sign = SIGNS[event_si]
event_lord = SIGN_LORDS[event_sign]
ll_name = SIGN_LORDS[asc_sign]
opposite_si = (asc_idx + 6) % 12
opposite_lord = SIGN_LORDS[SIGNS[opposite_si]]
event_house_lon = (event_si * 30) + 15 # 宫位中点
ll_lon = natal.get(ll_name, {}).get('degree', 0)
event_lord_lon = natal.get(event_lord, {}).get('degree', 0)
opp_lord_lon = natal.get(opposite_lord, {}).get('degree', 0)
d1_targets = {
f'{event_house}宫({event_sign})': event_house_lon,
f'{event_lord}(宫主)': event_lord_lon,
f'{ll_name}(LL)': ll_lon,
f'{opposite_lord}(对宫主)': opp_lord_lon,
}
# 4. D9 层敏感点
d9_asc_idx = _navamsa_idx(asc_deg)
d9_asc_sign = SIGNS[d9_asc_idx]
d9_event_si = (d9_asc_idx + event_house - 1) % 12
d9_event_sign = SIGNS[d9_event_si]
d9_event_lord = SIGN_LORDS[d9_event_sign]
# 宫主的 D9 星座(KN Rao 关键)
event_lord_d9_si = _navamsa_idx(event_lord_lon)
event_lord_d9_sign = SIGNS[event_lord_d9_si]
ll_d9_si = _navamsa_idx(ll_lon)
ll_d9_sign = SIGNS[ll_d9_si]
d9_event_house_lon = (d9_asc_idx * 30) + 15
d9_event_lord_lon = natal.get(d9_event_lord, {}).get('degree', 0)
d9_targets = {
f'D9_{event_house}宫({d9_event_sign})': d9_event_house_lon,
f'D9_{d9_event_lord}(宫主)': d9_event_lord_lon,
f'{event_lord}_D9({event_lord_d9_sign})': event_lord_d9_si * 30 + 15,
f'{ll_name}_D9({ll_d9_sign})': ll_d9_si * 30 + 15,
}
# 5. PAC 检查
results = {
'transit_date': args.date,
'event_house': event_house,
'd1': {'jupiter': {}, 'saturn': {}},
'd9': {'jupiter': {}, 'saturn': {}},
'cl': {'jupiter': {}, 'saturn': {}},
'double_transit': [],
'summary': '',
}
# Chandra Lagna 层敏感点
moon_lon = natal.get('Moon', {}).get('degree', 0)
moon_idx = int((moon_lon % 360) / 30)
cl_event_sign = SIGNS[(moon_idx + event_house - 1) % 12]
cl_event_lord = SIGN_LORDS[cl_event_sign]
cl_event_house_lon = ((moon_idx + event_house - 1) % 12) * 30 + 15
cl_event_lord_lon = natal.get(cl_event_lord, {}).get('degree', 0)
cl_targets = {
f'CL_{event_house}宫({cl_event_sign})': cl_event_house_lon,
f'CL_{cl_event_lord}(宫主)': cl_event_lord_lon,
}
for tp_name in ['Jupiter', 'Saturn']:
tp = transit_planets.get(tp_name, {})
if 'error' in tp:
continue
tp_lon = tp['lon']
layer = tp_name.lower()
for t_name, t_lon in d1_targets.items():
pac = _check_pac(tp_name, tp_lon, t_lon, asc_idx)
if pac:
results['d1'][layer][t_name] = pac
for t_name, t_lon in d9_targets.items():
pac = _check_pac(tp_name, tp_lon, t_lon, d9_asc_idx)
if pac:
results['d9'][layer][t_name] = pac
for t_name, t_lon in cl_targets.items():
pac = _check_pac(tp_name, tp_lon, t_lon, moon_idx)
if pac:
results['cl'][layer][t_name] = pac
# 6. Double Transit 判定
jup_d1 = set(results['d1']['jupiter'].keys())
sat_d1 = set(results['d1']['saturn'].keys())
jup_d9 = set(results['d9']['jupiter'].keys())
sat_d9 = set(results['d9']['saturn'].keys())
jup_cl = set(results['cl']['jupiter'].keys())
sat_cl = set(results['cl']['saturn'].keys())
# D1 层 overlap
d1_overlap = jup_d1 & sat_d1
for t in d1_overlap:
results['double_transit'].append({
'layer': 'D1', 'target': t,
'jupiter_pac': results['d1']['jupiter'][t],
'saturn_pac': results['d1']['saturn'][t],
'strength': 'strong',
})
# D9 层 overlap
d9_overlap = jup_d9 & sat_d9
for t in d9_overlap:
results['double_transit'].append({
'layer': 'D9', 'target': t,
'jupiter_pac': results['d9']['jupiter'][t],
'saturn_pac': results['d9']['saturn'][t],
'strength': 'strong',
})
# Chandra Lagna 层 overlap
cl_overlap = jup_cl & sat_cl
for t in cl_overlap:
results['double_transit'].append({
'layer': 'CL', 'target': t,
'jupiter_pac': results['cl']['jupiter'][t],
'saturn_pac': results['cl']['saturn'][t],
'strength': 'strong',
})
# 跨层 Double Transit
for d1t in jup_d1:
for d9t in sat_d9:
d1_nums = ''.join(c for c in d1t if c.isdigit())
d9_nums = ''.join(c for c in d9t if c.isdigit())
if d1_nums == d9_nums or (event_lord in d1t and event_lord in d9t):
results['double_transit'].append({
'layer': 'D1+D9', 'target': f'Jupiter(D1){d1t} + Saturn(D9){d9t}',
'jupiter_pac': results['d1']['jupiter'][d1t],
'saturn_pac': results['d9']['saturn'][d9t],
'strength': 'moderate',
})
for d1t in sat_d1:
for d9t in jup_d9:
d1_nums = ''.join(c for c in d1t if c.isdigit())
d9_nums = ''.join(c for c in d9t if c.isdigit())
if d1_nums == d9_nums or (event_lord in d1t and event_lord in d9t):
results['double_transit'].append({
'layer': 'D1+D9', 'target': f'Saturn(D1){d1t} + Jupiter(D9){d9t}',
'jupiter_pac': results['d9']['jupiter'][d9t],
'saturn_pac': results['d1']['saturn'][d1t],
'strength': 'moderate',
})
# Summary
d1_active = len(d1_overlap) > 0
d9_active = len(d9_overlap) > 0
cl_active = len(cl_overlap) > 0
cross_active = any(d['layer'] == 'D1+D9' for d in results['double_transit'])
active_layers = []
if d1_active: active_layers.append('D1')
if d9_active: active_layers.append('D9')
if cl_active: active_layers.append('CL')
if len(active_layers) >= 2:
results['summary'] = f'✅ Double Transit PAC 确认: {"+".join(active_layers)} 多层激活{event_house}宫主题'
elif d1_active:
results['summary'] = f'⚠️ D1 层 Double Transit 激活,D9/CL 层未确认'
elif d9_active:
results['summary'] = f'⚠️ D9 层 Double Transit 激活,D1/CL 层未确认'
elif cl_active:
results['summary'] = f'⚠️ Chandra Lagna 层 Double Transit 激活,D1/D9 未确认'
elif cross_active:
results['summary'] = f'⚠️ 跨层间接 Double Transit (D1+D9),需结合 Dasha 确认'
else:
results['summary'] = f'❌ 无 Double Transit PAC 激活'
results['stats'] = {
'd1_jupiter_targets': sorted(jup_d1),
'd1_saturn_targets': sorted(sat_d1),
'd9_jupiter_targets': sorted(jup_d9),
'd9_saturn_targets': sorted(sat_d9),
'cl_jupiter_targets': sorted(jup_cl),
'cl_saturn_targets': sorted(sat_cl),
'd1_overlap': sorted(d1_overlap),
'd9_overlap': sorted(d9_overlap),
'cl_overlap': sorted(cl_overlap),
'd9_ascendant': d9_asc_sign,
'event_lord_d9_sign': event_lord_d9_sign,
'chandra_lagna': SIGNS[moon_idx],
}
return results
# ============================================================================
# 8c. Transit LL/7L 连接 + 互换(Parivartana)(v3.9新增)
#
# P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL (98%命中率)
# P8: Transit LL 过 7H 或 Transit 7L 过 Lagna (59%命中率)
# + Parivartana 互换检测
# ============================================================================
def _calc_transit_lon(jd, planet_name):
"""计算指定 Julian Day 的行星恒星黄经"""
pid_map = {'Sun': swe.SUN, 'Moon': swe.MOON, 'Mars': swe.MARS, 'Mercury': swe.MERCURY,
'Jupiter': swe.JUPITER, 'Venus': swe.VENUS, 'Saturn': swe.SATURN, 'Rahu': swe.MEAN_NODE}
pid = pid_map.get(planet_name)
if pid is None:
return None
pos, _ = swe.calc_ut(jd, pid)
aya = swe.get_ayanamsa_ut(jd)
return (pos[0] - aya) % 360
def cmd_transit_ll7l(args):
"""Transit LL/7L 连接 + 互换检测"""
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
asc_deg = chart.get('ascendant', {}).get('degree', 0)
ll_name = SIGN_LORDS[asc_sign]
seven_sign = SIGNS[(SIGNS.index(asc_sign) + 6) % 12]
seven_lord = SIGN_LORDS[seven_sign]
# Transit 日期
t_year, t_month, t_day = map(int, args.date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
# Transit LL/7L 位置
t_ll_lon = _calc_transit_lon(transit_jd, ll_name)
t_7l_lon = _calc_transit_lon(transit_jd, seven_lord)
if t_ll_lon is None or t_7l_lon is None:
return {"error": f"无法计算 Transit 位置: LL={ll_name}, 7L={seven_lord}"}
n_ll_lon = natal.get(ll_name, {}).get('degree', 0)
n_7l_lon = natal.get(seven_lord, {}).get('degree', 0)
asc_lon = asc_deg
result = {
'transit_date': args.date,
'lagna_lord': ll_name,
'seventh_lord': seven_lord,
'p5': {'hit': False, 'details': []},
'p8': {'hit': False, 'details': []},
'parivartana': {'hit': False, 'details': []},
}
# P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL
pac1 = _check_pac(ll_name, t_ll_lon, n_7l_lon, asc_idx)
if pac1:
result['p5']['hit'] = True
result['p5']['details'].append({
'direction': f'Transit {ll_name} → natal {seven_lord}',
'connections': pac1,
})
pac2 = _check_pac(seven_lord, t_7l_lon, n_ll_lon, asc_idx)
if pac2:
result['p5']['hit'] = True
result['p5']['details'].append({
'direction': f'Transit {seven_lord} → natal {ll_name}',
'connections': pac2,
})
# P8: Transit LL 过 7H 或 Transit 7L 过 Lagna
t_ll_house = ((int((t_ll_lon % 360) / 30) - asc_idx) % 12) + 1
t_7l_house = ((int((t_7l_lon % 360) / 30) - asc_idx) % 12) + 1
if t_ll_house == 7:
result['p8']['hit'] = True
result['p8']['details'].append(f'Transit {ll_name}({SIGNS[int(t_ll_lon/30)]})在7H')
if t_7l_house == 1:
result['p8']['hit'] = True
result['p8']['details'].append(f'Transit {seven_lord}({SIGNS[int(t_7l_lon/30)]})在Lagna')
# Parivartana 互换
n_ll_sign = SIGNS[int((n_ll_lon % 360) / 30)]
n_7l_sign = SIGNS[int((n_7l_lon % 360) / 30)]
t_ll_in_7l = SIGNS[int((t_ll_lon % 360) / 30)] == n_7l_sign
t_7l_in_ll = SIGNS[int((t_7l_lon % 360) / 30)] == n_ll_sign
if t_ll_in_7l and t_7l_in_ll:
result['parivartana']['hit'] = True
result['parivartana']['details'].append(
f'完整互换: Transit {ll_name}{n_7l_sign}(natal {seven_lord}) + Transit {seven_lord}{n_ll_sign}(natal {ll_name})')
elif t_ll_in_7l:
result['parivartana']['details'].append(f'部分: Transit {ll_name}{n_7l_sign}')
elif t_7l_in_ll:
result['parivartana']['details'].append(f'部分: Transit {seven_lord}{n_ll_sign}')
return result
# ============================================================================
# 8d. 行星聚集检测(Lagna/7H + Transit 聚集)(v3.9新增)
# ============================================================================
def cmd_planetary_congregation(args):
"""行星聚集检测"""
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
event_house = args.house or 7
result = {
'natal': {'lagna': [], 'house_7': [], f'house_{event_house}': []},
'transit': None,
'summary': '',
}
# 本命盘聚集
for pname, pdata in natal.items():
if 'error' in pdata or 'sign' not in pdata:
continue
p_si = SIGNS.index(pdata['sign'])
house = ((p_si - asc_idx) % 12) + 1
if house == 1:
result['natal']['lagna'].append(pname)
if house == 7:
result['natal']['house_7'].append(pname)
if house == event_house:
result['natal'][f'house_{event_house}'].append(pname)
# Transit 聚集
if args.transit_date:
t_year, t_month, t_day = map(int, args.transit_date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
transit_aya = swe.get_ayanamsa(transit_jd)
result['transit'] = {str(h): [] for h in range(1, 13)}
for pname, pid in PLANETS_SWE.items():
try:
pos, _ = swe.calc_ut(transit_jd, pid)
lon_p = (pos[0] - transit_aya) % 360
si = int(lon_p / 30)
house = ((si - asc_idx) % 12) + 1
result['transit'][str(house)].append(pname)
if pname == 'Rahu':
klon = (lon_p + 180) % 360
ksi = int(klon / 30)
khouse = ((ksi - asc_idx) % 12) + 1
result['transit'][str(khouse)].append('Ketu')
except Exception:
pass
# 判定
flags = []
lagna_count = len(result['natal']['lagna'])
h7_count = len(result['natal']['house_7'])
if 'Sun' in result['natal']['lagna'] or lagna_count >= 3:
flags.append(f"Lagna聚集: {','.join(result['natal']['lagna'])}({lagna_count})")
if 'Sun' in result['natal']['house_7'] or h7_count >= 3:
flags.append(f"7H聚集: {','.join(result['natal']['house_7'])}({h7_count})")
if result['transit']:
slow = {'Saturn', 'Jupiter', 'Rahu', 'Ketu'}
t_event = result['transit'].get(str(event_house), [])
t_slow = [p for p in t_event if p in slow]
if len(t_slow) >= 2:
flags.append(f"Transit {event_house}宫慢行星聚集: {','.join(t_slow)}")
result['flags'] = flags
result['hit'] = len(flags) > 0
result['summary'] = ' | '.join(flags) if flags else '无显著聚集'
return result
# ============================================================================
# 8e. Vivah Saham + 婚姻计时管线(v3.9新增)
#
# Vivah Saham = norm(Venus - Saturn + Asc) — 度数级精确计算
# Transit 激活: Jupiter/Saturn PAC 到 Vivah Saham
# ============================================================================
def cmd_vivah_saham(args):
"""Vivah Saham 计算 + Transit 激活"""
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_deg = chart.get('ascendant', {}).get('degree', 0)
venus_lon = natal.get('Venus', {}).get('degree', 0)
saturn_lon = natal.get('Saturn', {}).get('degree', 0)
# Vivah Saham = norm(Venus - Saturn + Asc)
sahams_lon = (venus_lon - saturn_lon + asc_deg) % 360
sahams_si = int(sahams_lon / 30)
sahams_sign = SIGNS[sahams_si]
sahams_deg = sahams_lon - sahams_si * 30
result = {
'vivah_saham': {
'longitude': round(sahams_lon, 4),
'sign': sahams_sign,
'sign_cn': SIGNS_CN[sahams_sign],
'degree_in_sign': round(sahams_deg, 4),
},
'formula': f'norm({venus_lon:.2f} Venus - {saturn_lon:.2f} Saturn + {asc_deg:.2f} Asc)',
'transit_activation': None,
}
# Transit 激活
if args.transit_date:
t_year, t_month, t_day = map(int, args.transit_date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
result['transit_activation'] = {'jupiter': [], 'saturn': [], 'double_activation': False}
jup_lon = _calc_transit_lon(transit_jd, 'Jupiter')
sat_lon = _calc_transit_lon(transit_jd, 'Saturn')
if jup_lon is not None:
jup_pac = _check_pac('Jupiter', jup_lon, sahams_lon, asc_idx)
if jup_pac:
result['transit_activation']['jupiter'] = jup_pac
if sat_lon is not None:
sat_pac = _check_pac('Saturn', sat_lon, sahams_lon, asc_idx)
if sat_pac:
result['transit_activation']['saturn'] = sat_pac
if result['transit_activation']['jupiter'] and result['transit_activation']['saturn']:
result['transit_activation']['double_activation'] = True
# Venus transit 过 Saham 星座
venus_t = _calc_transit_lon(transit_jd, 'Venus')
if venus_t is not None:
if SIGNS[int(venus_t / 30)] == sahams_sign:
result['transit_activation']['venus_in_saham_sign'] = True
return result
# ============================================================================
# 9. Shadbala 六重力量(v3.4新增)
# ============================================================================
def cmd_shadbala(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import calc_shadbala
except ImportError as e:
return {"error": f"shadbala模块导入失败: {e}"}
planets = chart.get("planets", {})
asc_sign = chart.get("ascendant", {}).get("sign", "Aries")
birth_hour = args.hour + args.minute / 60.0
sun_lon = planets.get("Sun", {}).get("degree", 0)
moon_lon = planets.get("Moon", {}).get("degree", 0)
return calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
# ============================================================================
# 10. Ashtakavarga 八分法(v3.4新增)
# ============================================================================
def cmd_ashtakavarga(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
except ImportError as e:
return {"error": f"ashtakavarga模块导入失败: {e}"}
planets = chart.get("planets", {})
return calc_ashtakavarga(planets, asc_idx)
# ============================================================================
# 11. Hermes 记忆系统(v3.4新增)
# ============================================================================
def cmd_memory(args):
try:
sys.path.insert(0, SCRIPT_DIR)
from hermes_memory_core import HermesMemoryCore
except ImportError as e:
return {"error": f"Hermes记忆模块导入失败: {e}", "hint": "确保hermes_memory_core.py在scripts/目录下"}
db_file = os.path.join(SCRIPT_DIR, 'hermes_memory.db')
mem = HermesMemoryCore(db_file)
result = {"action": args.action}
if args.action == "store":
if not args.content:
return {"error": "store操作需要 --content 参数"}
tags = args.tags.split(',') if args.tags else []
importance = args.importance if args.importance else 5
metadata = {"tags": tags, "importance": importance}
mem_id = mem.store_memory(args.content, metadata)
result.update({"stored": True, "memory_id": mem_id, "content": args.content, "tags": tags})
elif args.action == "search":
if not args.query:
return {"error": "search操作需要 --query 参数"}
results = mem.search(args.query, limit=args.limit or 10)
result.update({"query": args.query, "found": len(results), "results": results})
elif args.action == "context":
session_id = f"jyotish-cli-{datetime.now().strftime('%Y%m%d%H%M%S')}"
ctx = mem.get_context_for_session(session_id)
result.update({"session_id": session_id, "context": ctx})
elif args.action == "stats":
# Hermes没有get_stats,用搜索空串获取总数
try:
all_mem = mem.search("", limit=1000)
result["total_memories"] = len(all_mem)
except:
result["total_memories"] = "unknown"
result["db_path"] = db_file
else:
result["error"] = f"未知action: {args.action},支持: store/search/context/stats"
return result
# ============================================================================
# 12. R1-R10 数学验证(v3.5新增)
# ============================================================================
def cmd_validate(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_result = calc_ashtakavarga(chart.get('planets', {}), asc_idx)
except ImportError:
asht_result = None
try:
from validate import validate_chart
return validate_chart(chart, asht_result)
except ImportError as e:
return {"error": f"validate模块导入失败: {e}"}
# ============================================================================
# 13. P1-P12 行星审计管线(v3.5新增)
# ============================================================================
def _assess_conjunction_quality(lord, houses, planets):
"""评估仓库耦合的吉凶质量"""
lord_info = planets.get(lord, {})
lord_status = lord_info.get('status', '中性')
# 凶宫组合
dusthana = {6, 8, 12}
trikona = {1, 5, 9}
kendra = {1, 4, 7, 10}
has_dusthana = any(h in dusthana for h in houses)
has_trikona = any(h in trikona for h in houses)
has_kendra = any(h in kendra for h in houses)
if has_dusthana and has_trikona:
return f"凶吉混合 — 挑战与成长并存"
elif has_dusthana and has_kendra:
return f"压力型 — 通过努力获取成就"
elif has_trikona:
return f"吉庆型 — 自然流畅的支持"
elif has_dusthana:
return f"消耗型 — 需要额外努力维持"
else:
return f"中性 — 标准互动"
def _conflict_arbitration(report):
"""
冲突仲裁规则(CNWU16框架):
1. P1清理者+P7入旺 = "带毒高价值资产",禁止说逢凶化吉
2. P5凶宫+BAV高 = "乱世出英雄"
3. P1吉+P2受损 = "空有雄心无着力点"
"""
conflicts = []
planets = report.get('planets', {})
audit = report.get('audit', {})
p1 = audit.get('P1_identity', {})
p7 = audit.get('P7_dignity', {})
p2 = audit.get('P2_health', {})
asc_lord = p1.get('asc_lord', '')
# 规则1: P1清理者+P7入旺 → 检查上升主是否掌管8/12宫(清理者角色)
# 清理者定义:掌管8宫或12宫的行星
SIGN_LORDS_MAP = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon',
'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars',
'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'}
asc_sign = p1.get('asc_sign', '')
asc_idx_local = SIGNS.index(asc_sign) if asc_sign in SIGNS else 0
# 找8宫主和12宫主
h8_sign = SIGNS[(asc_idx_local + 7) % 12]
h12_sign = SIGNS[(asc_idx_local + 11) % 12]
destroyer_lords = {SIGN_LORDS_MAP.get(h8_sign, ''), SIGN_LORDS_MAP.get(h12_sign, '')}
for dl in destroyer_lords:
if dl and dl in p7:
dl_status = p7[dl].get('status', '')
if 'Exalted' in dl_status or 'Own' in dl_status:
conflicts.append({
'rule': 'Destroyer+Exalted',
'planets': [dl],
'verdict': '带毒高价值资产',
'instruction': f"{dl}既是清理者(掌8/12宫)又入旺/入庙,力量极强但方向凶险——禁止说逢凶化吉",
})
# 规则2: P5凶宫+BAV高 → 检查6/8/12宫的SAV是否 >28
asht_data = report.get('ashtakavarga', {})
house_scores = {}
if asht_data:
# 从ashtakavarga原始数据获取house_scores
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_result = calc_ashtakavarga(planets, asc_idx_local)
house_scores = asht_result.get('house_scores', {})
except:
pass
for h in [6, 8, 12]:
hs = house_scores.get(f'house_{h}', {})
sav_score = hs.get('score', 0)
if sav_score > 28:
conflicts.append({
'rule': 'Dusthana+HighBAV',
'house': h,
'sav': sav_score,
'verdict': '乱世出英雄',
'instruction': f"{h}宫是凶宫但SAV={sav_score}>28),在困境中反而能出成就",
})
# 规则3: P1吉+P2受损 → 上升主星状态好但太阳(健康指标)受损
sun_info = p2.get('sun_status', '')
lord_info = p7.get(asc_lord, {})
lord_status = lord_info.get('status', '')
if ('Exalted' in lord_status or 'Own' in lord_status) and ('Debilitated' in sun_info or 'Enemy' in sun_info):
conflicts.append({
'rule': 'GoodP1+DamagedP2',
'planets': [asc_lord, 'Sun'],
'verdict': '空有雄心无着力点',
'instruction': f"上升主{asc_lord}强健但太阳受损,有野心但执行力/健康跟不上",
})
return conflicts
def cmd_audit(args):
"""P1-P12 行星审计:调用 chart→shadbala→ashtakavarga→yoga,输出统一审计报告"""
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
planets = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
report = {
'version': '3.5',
'birth_info': chart.get('birth_info', {}),
'ascendant': chart.get('ascendant', {}),
'planets': planets,
'audit': {},
}
# P1 Identity(本命身份): 上升星座 + 上升主星
asc_lord = chart.get('ascendant', {}).get('lord', 'Unknown')
lord_info = planets.get(asc_lord, {})
report['audit']['P1_identity'] = {
'asc_sign': asc_sign,
'asc_lord': asc_lord,
'lord_sign': lord_info.get('sign', 'Unknown'),
'lord_house': lord_info.get('house', 0),
'lord_status': lord_info.get('status', '未知'),
}
# P2 Health(健康指标): 6宫 + 8宫 + 12宫主星 + 太阳状态
health_houses = [6, 8, 12]
health_lords = set()
health_info = {}
for h in health_houses:
sign_idx = (asc_idx + h - 1) % 12
sign_name = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sign_name, 'Unknown')
health_lords.add(lord)
health_info[f'house_{h}'] = {'sign': sign_name, 'lord': lord}
sun_info = planets.get('Sun', {})
report['audit']['P2_health'] = {
'houses': health_info,
'sun_status': sun_info.get('status', '未知'),
'sun_house': sun_info.get('house', 0),
}
# P3 Warehouse Coupling(仓库耦合): 双宫掌管=货物捆绑
# CNWU16逻辑:如果一颗行星同时掌管两个宫位,则两个宫位的事务被"捆绑"
house_lord_map = {}
for h in range(1, 13):
sign_idx = (asc_idx + h - 1) % 12
sname = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sname, 'Unknown')
if lord not in house_lord_map:
house_lord_map[lord] = []
house_lord_map[lord].append(h)
warehouse_coupling = {}
for lord, houses in house_lord_map.items():
if len(houses) > 1:
warehouse_coupling[lord] = {
'houses': houses,
'meaning': f"{lord}同时掌管{houses[0]}宫和{houses[1]}宫,事务捆绑",
'conjunction_quality': _assess_conjunction_quality(lord, houses, planets),
}
report['audit']['P3_warehouse_coupling'] = warehouse_coupling
# P8 Age Status(年龄状态): 青壮=主动, 老婴=辅助, 死=自动执行
# 基于行星在星座中的度数区间判定生命周期
age_status_map = {}
for pname, pd in planets.items():
deg_in_sign = pd.get('degree_in_sign', 0)
if pname in ['Rahu', 'Ketu']:
age_status_map[pname] = {'status': '永远逆行', 'phase': 'Rahu/Ketu无年龄状态'}
continue
if deg_in_sign < 10:
phase = '婴幼(0-10°)'
quality = '辅助型 — 能量尚未完全展开'
elif deg_in_sign < 20:
phase = '青壮(10-20°)'
quality = '主动型 — 能量最活跃,主导性强'
else:
phase = '老年(20-30°)'
quality = '自动执行型 — 已内化的能力,自动化运作'
age_status_map[pname] = {
'degree_in_sign': round(deg_in_sign, 2),
'phase': phase,
'quality': quality,
}
report['audit']['P8_age_status'] = age_status_map
# P4 Resource SAV(资源SAV& P6 Exit SAV(退出SAV
# 需要 Ashtakavarga 数据
asht_data = None
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_data = calc_ashtakavarga(planets, asc_idx)
report['ashtakavarga'] = {
'sav_total': asht_data.get('sav', {}).get('total', 0),
'sav_valid': asht_data.get('sav', {}).get('valid', False),
'strongest': asht_data.get('strongest_signs', []),
'weakest': asht_data.get('weakest_signs', []),
}
house_scores = asht_data.get('house_scores', {})
# P4: 财富宫(2,11) SAV
p4_info = {}
for h in [2, 11]:
hs = house_scores.get(f'house_{h}', {})
p4_info[f'house_{h}'] = hs
report['audit']['P4_resource_sav'] = p4_info
# P6: 退出宫(12) SAV + 8宫
p6_info = {}
for h in [8, 12]:
hs = house_scores.get(f'house_{h}', {})
p6_info[f'house_{h}'] = hs
report['audit']['P6_exit_sav'] = p6_info
# P5: 路况(1,5,9三宫) SAV
p5_info = {}
for h in [1, 5, 9]:
hs = house_scores.get(f'house_{h}', {})
p5_info[f'house_{h}'] = hs
report['audit']['P5_road_condition'] = p5_info
except Exception as e:
report['audit']['ashtakavarga_error'] = str(e)
# P7 Dignity(尊严状态)
dignity_map = {}
for pname, pd in planets.items():
dignity_map[pname] = {
'sign': pd.get('sign', ''),
'status': pd.get('status', '中性'),
'house': pd.get('house', 0),
'retrograde': pd.get('retrograde', False),
}
report['audit']['P7_dignity'] = dignity_map
# P9 Shadbala(六重力量)
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import calc_shadbala
birth_hour = args.hour + args.minute / 60.0
sun_lon = planets.get('Sun', {}).get('degree', 0)
moon_lon = planets.get('Moon', {}).get('degree', 0)
shadbala = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
report['audit']['P9_shadbala'] = {
'summary': shadbala.get('summary', {}),
'ishta_bala_ranking': shadbala.get('ishta_bala_ranking', []),
}
except Exception as e:
report['audit']['P9_shadbala_error'] = str(e)
# P10 Aspects(相位)简化版
aspect_map = {}
for pname, pd in planets.items():
if pname in ['Rahu', 'Ketu']:
continue
house = pd.get('house', 0)
# 标准 7/4 相位(从该行星宫位数起)
aspects_from_house = {1: [7], 2: [7], 3: [5, 9, 7], 4: [7, 10],
5: [7], 6: [7], 7: [7], 8: [7],
9: [5, 7], 10: [7], 11: [7], 12: [7]}
# 特殊相位
special = {'Mars': [4, 7, 8], 'Jupiter': [5, 7, 9], 'Saturn': [3, 7, 10]}
if pname in special:
aspect_houses = special[pname]
else:
aspect_houses = [7] # 标准对宫
aspect_map[pname] = {
'from_house': house,
'aspect_houses': aspect_houses,
}
report['audit']['P10_aspects'] = aspect_map
# P11 Nakshatra
nak_map = {}
for pname, pd in planets.items():
nak_map[pname] = {
'nakshatra': pd.get('nakshatra', ''),
'pada': pd.get('nakshatra_pada', 0),
'lord': pd.get('nakshatra_lord', ''),
}
report['audit']['P11_nakshatra'] = nak_map
# P12 Yogas(格局识别)
try:
yoga_planets = {}
for pname, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'house' in pd:
yoga_planets[pname] = {'sign': pd['sign'], 'house': pd['house']}
yoga_result = cmd_yoga(type('Args', (), {
'ascendant': asc_sign,
'planets': ','.join([f"{k}:{v['sign']}:{v['house']}" for k, v in yoga_planets.items()])
})())
report['audit']['P12_yogas'] = {
'count': yoga_result.get('yogas_detected', 0),
'yogas': yoga_result.get('yogas', []),
}
except Exception as e:
report['audit']['P12_yogas_error'] = str(e)
# 验证
try:
from validate import validate_chart
validation = validate_chart(chart, asht_data)
report['validation'] = validation
except Exception as e:
report['validation'] = {"error": str(e)}
# 冲突仲裁(CNWU16框架3条规则)
report['conflict_arbitration'] = _conflict_arbitration(report)
return report
# ============================================================================
# 14. 报告生成(v3.6新增)
# ============================================================================
def cmd_report(args):
"""调用 report_builder.py 生成 HTML 报告"""
try:
sys.path.insert(0, SCRIPT_DIR)
from report_builder import main as report_main
except ImportError as e:
return {"error": f"report_builder模块导入失败: {e}"}
# 构造 sys.argv 并调用 report_builder
folder = args.folder
if not os.path.isdir(folder):
return {"error": f"目录不存在: {folder}"}
report_argv = [
'report_builder.py', folder,
'--name', args.name,
'--lagna', args.lagna,
'--gender', args.gender,
'--status', args.status,
'--lang', args.lang,
]
if args.output:
report_argv.extend(['--output', args.output])
old_argv = sys.argv
sys.argv = report_argv
try:
report_main()
output_path = args.output or os.path.join(folder, 'report.html')
return {
'status': 'ok',
'output': output_path,
'name': args.name,
'lagna': args.lagna,
'lang': args.lang,
}
except SystemExit:
return {"error": "report_builder执行出错,请检查MD文件格式"}
except Exception as e:
return {"error": f"报告生成失败: {e}"}
finally:
sys.argv = old_argv
# ============================================================================
# 15. BPHS十六分盘完整计算(v3.7新增)
# ============================================================================
def cmd_varga_full(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from varga import calc_all_vargas
except ImportError as e:
return {"error": f"varga模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('degree', 0)
divisions = [int(d.strip().replace('D','')) for d in args.divisions.split(',')] if args.divisions else None
return calc_all_vargas(planet_lons, asc_deg, divisions)
# ============================================================================
# 16. 度数精确相位系统(v3.7新增)
# ============================================================================
def cmd_aspects(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from aspects import calc_all_aspects
except ImportError as e:
return {"error": f"aspects模块导入失败: {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('degree', 0)
return calc_all_aspects(planet_lons, asc_deg)
# ============================================================================
# 17. Jaimini系统(v3.7新增)
# ============================================================================
def cmd_jaimini(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from jaimini import calc_chara_karaka_7, calc_chara_karaka_8, calc_chara_dasha, calc_karakamsha, calc_chara_dasha_with_antardasha
from varga import calc_varga
except ImportError as e:
return {"error": f"jaimini模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {}
planet_degs = {} # 星座内度数(0-30),用于Jaimini Karaka计算
for pn, pd in planets.items():
if isinstance(pd, dict) and 'degree' in pd:
planet_lons[pn] = pd['degree']
planet_degs[pn] = pd.get('degree_in_sign', pd['degree'] % 30)
asc_deg = chart.get('ascendant', {}).get('degree', 0)
result = {}
# Chara Karaka(必须传星座内度数0-30,不是完整经度0-360)
mode = args.mode or 'all'
if mode in ('all', 'karaka'):
result['chara_karaka_7'] = calc_chara_karaka_7(planet_degs)
result['chara_karaka_8'] = calc_chara_karaka_8(planet_degs)
if mode in ('all', 'dasha'):
use_antardasha = getattr(args, 'antardasha', False)
if use_antardasha:
result['chara_dasha'] = calc_chara_dasha_with_antardasha(asc_idx, planet_lons, args.year, args.month)
else:
result['chara_dasha'] = calc_chara_dasha(asc_idx, planet_lons, args.year, args.month)
if mode in ('all', 'karakamsha'):
# AK(灵魂星)的D9位置 — Karakamsha定义是AK在Navamsa中的星座
# ⚠️ 2026-05-03修正:此前错误使用DK,现已修正为AK
ck7 = calc_chara_karaka_7(planet_degs)
ak_name = ck7['karaka_table']['Atmakaraka']['planet']
ak_lon = planet_lons.get(ak_name, 0)
ak_d9 = calc_varga(ak_lon, 9)
result['karakamsha'] = calc_karakamsha(ak_d9.get('sign', 'Aries'), ak_d9.get('degree_in_sign', 0))
return result
# ============================================================================
# 18. 高级Nakshatra分析(v3.7新增)
# ============================================================================
def cmd_nakshatra_adv(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from nakshatra_advanced import find_nakshatra, calc_all_tara_balas, calc_sub_lord, nakshatra_compatibility
except ImportError as e:
return {"error": f"nakshatra_advanced模块导入失败: {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']
result = {'planets': {}}
mode = args.mode or 'all'
if mode in ('all', 'detail'):
for pn, lon in planet_lons.items():
result['planets'][pn] = find_nakshatra(lon)
if mode in ('all', 'tara'):
moon_lon = planet_lons.get('Moon', 0)
moon_nak_idx = int(moon_lon / (360/27)) % 27
result['tara_bala'] = calc_all_tara_balas(moon_nak_idx, planet_lons)
if mode in ('all', 'sublord'):
result['sub_lords'] = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()}
return result
# ============================================================================
# 19. Argala门闩系统(v3.7新增)
# ============================================================================
def cmd_argala(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from argala import calc_argala
except ImportError as e:
return {"error": f"argala模块导入失败: {e}"}
planets = chart.get('planets', {})
# 构建宫位到行星的映射 - argala需要sign_indices
planet_sign_indices = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd:
si = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
planet_sign_indices[pn] = si
return calc_argala(planet_sign_indices, asc_idx)
# ============================================================================
# 20. Tajika/Varshaphala年运盘(v3.7新增)
# ============================================================================
def cmd_tajika(args):
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
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('degree', 0)
asc_si = int(asc_deg / 30) % 12 # sign index
age = args.age
if age is None:
return {"error": "请提供 --age 参数(当前年龄)"}
result = {}
mode = args.mode or 'all'
if mode in ('all', 'muntha'):
result['muntha'] = calc_muntha(asc_si, age)
if mode in ('all', 'yearlord'):
result['year_lord'] = calc_year_lord(asc_si, age)
if mode in ('all', 'mudda'):
# 需要先获取 varsha_lord
yl = calc_year_lord(asc_si, age)
varsha_lord = yl.get('year_lord', 'Jupiter')
result['mudda_dasha'] = calc_mudda_dasha(asc_si, varsha_lord, args.month)
if mode in ('all', 'tripataka'):
yl = calc_year_lord(asc_si, age)
varsha_lord = yl.get('year_lord', 'Jupiter')
muntha_si = (asc_si + age) % 12
result['tri_pataka'] = calc_tri_pataka(planet_lons, varsha_lord, muntha_si)
return result
# ============================================================================
# 21. 合盘分析(v3.7新增)
# ============================================================================
def cmd_synastry(args):
try:
sys.path.insert(0, SCRIPT_DIR)
from synastry import calc_synastry
except ImportError as e:
return {"error": f"synastry模块导入失败: {e}"}
# 构建两人数据
p1 = {'moon_lon': args.moon1, 'gender': args.gender1 or 'M'}
p2 = {'moon_lon': args.moon2, 'gender': args.gender2 or 'F'}
if args.mars1 is not None: p1['mars_lon'] = args.mars1
if args.mars2 is not None: p2['mars_lon'] = args.mars2
if args.asc1 is not None: p1['asc_lon'] = args.asc1
if args.asc2 is not None: p2['asc_lon'] = args.asc2
return calc_synastry(p1, p2)
# ============================================================================
# 22. 全自动综合解盘 full-readingv3.7.1新增)
# ============================================================================
# ============================================================================
# Transit 多参考点分析(v4.5.0 P1补齐)
# 基于 transit-multi-reference-guide.md 强制规范
# 四参考点:Lagna / Chandra Lagna / Arudha Lagna / Navamsa Lagna
# ============================================================================
def _calc_transit_multi_reference(planets, asc_idx, asc_deg, planet_lons, transit_planets=None, transit_date=None, node_mode='mean'):
"""
⭐ v4.5.0: Transit多参考点分析(强制规范)
每次Transit分析必须同时从四个参考点评估:
1. Lagna(上升点)— 实际生活事件
2. Chandra Lagna(月亮星座)— 心理状态、职业变动
3. Arudha LagnaAL)— 公众形象
4. Navamsa LagnaD9上升)— 灵魂层面
"""
data_layer = 'true_transit_positions' if transit_planets else 'natal_positions_fallback'
analysis_planets = transit_planets if transit_planets else planets
# 四个参考点的星座索引
moon_lon = planet_lons.get('Moon', 0)
chandra_idx = int(moon_lon / 30) % 12
# Arudha Lagna(从special_lagnas模块逻辑简化:AL = (Ascendant度数+12宫主度数)%360 对应的星座)
twelfth_sign_idx = (asc_idx + 11) % 12
twelfth_lord = SIGN_LORDS.get(SIGNS[twelfth_sign_idx], '')
twelfth_lord_lon = planet_lons.get(twelfth_lord, 0)
al_raw = (asc_deg + twelfth_lord_lon) % 360
# AL 特殊规则:如果结果落在原始宫或第7宫,取对宫
al_idx = int(al_raw / 30) % 12
if al_idx == asc_idx or al_idx == (asc_idx + 6) % 12:
al_idx = (al_idx + 7) % 12 # 取第8个 = 对宫再移一位
# Navamsa Lagna
d9_asc_idx = _navamsa_idx(asc_deg)
references = {
'Lagna': {
'name': 'Lagna',
'cn': '上升点',
'sign': SIGNS[asc_idx],
'sign_cn': SIGNS_CN[SIGNS[asc_idx]],
'sign_idx': asc_idx,
'priority': 'P1',
'scope': '实际生活事件、身体健康',
},
'Chandra_Lagna': {
'name': 'Chandra Lagna',
'cn': '月亮上升',
'sign': SIGNS[chandra_idx],
'sign_cn': SIGNS_CN[SIGNS[chandra_idx]],
'sign_idx': chandra_idx,
'priority': 'P1',
'scope': '心理状态、职业变动、情感体验',
},
'Arudha_Lagna': {
'name': 'Arudha Lagna',
'cn': '形象上升',
'sign': SIGNS[al_idx],
'sign_cn': SIGNS_CN[SIGNS[al_idx]],
'sign_idx': al_idx,
'priority': 'P2',
'scope': '公众形象、社会认知、他人如何看待你',
},
'Navamsa_Lagna': {
'name': 'Navamsa Lagna',
'cn': '灵性上升',
'sign': SIGNS[d9_asc_idx],
'sign_cn': SIGNS_CN[SIGNS[d9_asc_idx]],
'sign_idx': d9_asc_idx,
'priority': 'P3',
'scope': '灵魂层面的实际影响、内在真实',
},
}
# 对每个外行星(Jupiter/Saturn/Rahu/Ketu),计算从四个参考点看的宫位
OUTER_PLANETS = ['Jupiter', 'Saturn', 'Rahu', 'Ketu']
transit_analysis = {}
for pn in OUTER_PLANETS:
pd = analysis_planets.get(pn, {})
if not isinstance(pd, dict) or 'sign' not in pd:
continue
p_sign_idx = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
transit_analysis[pn] = {
'sign': pd['sign'],
'sign_cn': SIGNS_CN.get(pd['sign'], ''),
'degree_in_sign': pd.get('degree_in_sign', pd.get('degree', 0) % 30),
'house_from_ref': {},
}
for ref_name, ref_info in references.items():
ref_idx = ref_info['sign_idx']
house = ((p_sign_idx - ref_idx) % 12) + 1
house_meaning = _house_theme(house)
transit_analysis[pn]['house_from_ref'][ref_name] = {
'house': house,
'meaning': house_meaning,
}
# 差异检测:同一行星在不同参考点的宫位含义是否矛盾
divergences = []
for pn, pa in transit_analysis.items():
houses = {ref: info['house'] for ref, info in pa['house_from_ref'].items()}
unique_houses = set(houses.values())
if len(unique_houses) > 1:
divergences.append({
'planet': pn,
'houses': houses,
'divergence': f'{pn}在四个参考点分别落在不同宫位,需综合判断',
})
# Sade Sati / Ashtama Shani 检测(基于Chandra Lagna
special_checks = {}
saturn_sign_idx = SIGNS.index(analysis_planets.get('Saturn', {}).get('sign', 'Aries')) if isinstance(analysis_planets.get('Saturn'), dict) and analysis_planets.get('Saturn', {}).get('sign') in SIGNS else 0
# Sade Sati: Saturn 在月亮星座或前后1宫
sade_sati_phase = None
if saturn_sign_idx == chandra_idx:
sade_sati_phase = 'peak'
elif saturn_sign_idx == (chandra_idx - 1) % 12:
sade_sati_phase = 'rising'
elif saturn_sign_idx == (chandra_idx + 1) % 12:
sade_sati_phase = 'setting'
if sade_sati_phase:
special_checks['sade_sati'] = {
'active': True,
'phase': sade_sati_phase,
'note': f'土星过境月亮{SIGNS_CN[SIGNS[chandra_idx]]}附近,Sade Sati {sade_sati_phase}',
}
# Ashtama Shani: Saturn在月亮第8宫
saturn_from_chandra = ((saturn_sign_idx - chandra_idx) % 12) + 1
if saturn_from_chandra == 8:
special_checks['ashtama_shani'] = {
'active': True,
'note': '土星过境月亮第8宫(Ashtama Shani),压力期',
}
return {
'references': references,
'target_date': transit_date,
'node_mode': node_mode,
'data_layer': data_layer,
'transit_analysis': transit_analysis,
'divergences': divergences,
'divergence_count': len(divergences),
'special_checks': special_checks,
'protocol': 'v6.0.10 Transit多参考点强制规范:AI必须用真实过境行星位置,并同时呈现Lagna和Chandra Lagna两个视角。任何矛盾信号必须记录并解释。',
}
# ============================================================================
# Dasa Convergence 三系统交叉验证(v4.5.0 P1补齐)
# 基于 dasa-convergence-methodology.md
# 三系统:Vimshottari + Chara Dasha + Yogini
# ============================================================================
# Yogini Dasha 常量
YOGINI_ORDER = ['Mangala', 'Pingala', 'Dhanya', 'Bhramari', 'Bhadrika', 'Ulka', 'Siddha', 'Sankata']
YOGINI_YEARS = {'Mangala': 1, 'Pingala': 2, 'Dhanya': 3, 'Bhramari': 4, 'Bhadrika': 5, 'Ulka': 6, 'Siddha': 7, 'Sankata': 8}
# Yogini 从月亮 Nakshatra 的第3个 NakshatraDhanishta)开始计数
YOGINI_NAK_START = 23 # Dhanishta 在 NAKSHATRA_LIST 中的索引
def _calc_yogini_dasha(moon_lon, birthdate_str):
"""
计算 Yogini Dasha 时间线
Yogini 基于 8 位女神循环,总周期 36 年
起始点由月亮所在 Nakshatra 决定
"""
nak_idx = int(moon_lon / (360 / 27)) % 27
# Yogini 起始索引 = (nak_idx - YOGINI_NAK_START) % 8
yog_start = (nak_idx - YOGINI_NAK_START) % 8
# 余数 = 在当前 Yogini 周期中的已过比例
nak_in_yog = nak_idx % 8 # 在8分组的第几个
pada = int((moon_lon % (360/27)) / (360/108)) + 1
# 余数比例
balance_frac = (nak_in_yog * 4 + pada - 1) / 32 # 8 Nakshatra × 4 Pada = 32 份
balance_frac = min(balance_frac, 1.0)
birth_date = datetime.strptime(birthdate_str, '%Y-%m-%d')
maha_periods = []
total_years = 0
for i in range(8):
idx = (yog_start + i) % 8
name = YOGINI_ORDER[idx]
years = YOGINI_YEARS[name]
if i == 0:
elapsed_years = years * balance_frac
actual_years = years - elapsed_years
start_offset = total_years
maha_periods.append({
'yogini': name,
'full_years': years,
'balance_years': round(actual_years, 3),
'start_offset_years': round(start_offset, 3),
'start_date': (birth_date + timedelta(days=round(start_offset * 365.25))).strftime('%Y-%m-%d'),
'end_date': (birth_date + timedelta(days=round((start_offset + actual_years) * 365.25))).strftime('%Y-%m-%d'),
'is_current_start': True,
})
total_years += actual_years
else:
start_offset = total_years
maha_periods.append({
'yogini': name,
'full_years': years,
'start_offset_years': round(start_offset, 3),
'start_date': (birth_date + timedelta(days=round(start_offset * 365.25))).strftime('%Y-%m-%d'),
'end_date': (birth_date + timedelta(days=round((start_offset + years) * 365.25))).strftime('%Y-%m-%d'),
})
total_years += years
# 计算当前 Yogini
today = datetime.now()
age_days = (today - birth_date).days
age_years = age_days / 365.25
cycle_years = 36 # Yogini 总周期
current_in_cycle = age_years % cycle_years
current_yogini = None
cumulative = 0
for yp in maha_periods:
dur = yp.get('balance_years', yp['full_years'])
if cumulative <= current_in_cycle < cumulative + dur:
current_yogini = yp
break
cumulative += dur
return {
'moon_nakshatra_idx': nak_idx,
'yogini_start_index': yog_start,
'total_cycle_years': 36,
'maha_periods': maha_periods,
'current_yogini': current_yogini,
}
def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, planet_lons, asc_idx):
"""
⭐ v4.5.0: Dasa Convergence 三系统交叉验证
Vimshottari + Chara Dasha + Yogini 三系统同时激活同一生活领域时,概率大幅提升
"""
# 提取各系统当前周期
convergence_data = {'systems': {}}
# 系统1: Vimshottari
if isinstance(dasha_result, dict):
current_d = dasha_result.get('current_dasha', {})
if isinstance(current_d, dict):
maha = current_d.get('mahadasha', current_d.get('maha'))
antar = current_d.get('antardasha', current_d.get('antar'))
convergence_data['systems']['vimshottari'] = {
'maha': maha,
'antar': antar,
'pratyantar': current_d.get('pratyantar'),
'basis': 'Nakshatra (Moon)',
}
# 系统2: Chara Dasha
if isinstance(chara_dasha_result, dict):
cd_maha = chara_dasha_result.get('current_maha', chara_dasha_result.get('current'))
cd_antar = chara_dasha_result.get('current_antar')
if isinstance(cd_maha, dict):
cd_sign = cd_maha.get('sign', cd_maha.get('rashi'))
elif isinstance(cd_maha, str):
cd_sign = cd_maha
else:
cd_sign = None
convergence_data['systems']['chara_dasha'] = {
'maha_sign': cd_sign,
'antar_sign': cd_antar.get('sign', cd_antar) if isinstance(cd_antar, dict) else cd_antar,
'basis': 'Rashi (Sign-based)',
}
# 系统3: Yogini
if isinstance(yogini_result, dict):
cur_yog = yogini_result.get('current_yogini', {})
convergence_data['systems']['yogini'] = {
'yogini': cur_yog.get('yogini') if isinstance(cur_yog, dict) else None,
'years': cur_yog.get('full_years') if isinstance(cur_yog, dict) else None,
'basis': 'Nakshatra (8-goddess cycle)',
}
# 宫位主题映射
house_themes_map = {
1: 'self_health', 2: 'wealth_family', 3: 'communication_skill', 4: 'home_mother',
5: 'creativity_children', 6: 'health_service', 7: 'marriage_partnership',
8: 'transformation', 9: 'fortune_dharma', 10: 'career_status',
11: 'gains_wishes', 12: 'loss_spirituality',
}
# 逐领域检测三系统激活
domain_activations = {}
for house, domain in house_themes_map.items():
activations = []
# Vimshottari: 检查大运/小运行星是否关联该宫
vims = convergence_data['systems'].get('vimshottari', {})
if vims:
for level in ['maha', 'antar']:
planet = vims.get(level)
if planet and isinstance(planet, str):
# 该行星是否掌管此宫?
target_sign_idx = (asc_idx + house - 1) % 12
target_sign = SIGNS[target_sign_idx]
target_lord = SIGN_LORDS.get(target_sign, '')
if planet == target_lord:
activations.append({
'system': 'Vimshottari',
'level': level,
'planet': planet,
'reason': f'{planet}{house}宫({target_sign})的宫主星',
})
# 该行星是否落在此宫?
p_sign_idx = int(planet_lons.get(planet, 0) / 30) % 12
p_house = ((p_sign_idx - asc_idx) % 12) + 1
if p_house == house:
activations.append({
'system': 'Vimshottari',
'level': level,
'planet': planet,
'reason': f'{planet}落在{house}',
})
# Chara Dasha: 检查当前星座是否关联该宫
cd = convergence_data['systems'].get('chara_dasha', {})
if cd and cd.get('maha_sign'):
cd_sign = cd['maha_sign']
if cd_sign in SIGNS:
cd_sign_idx = SIGNS.index(cd_sign)
cd_house_from_asc = ((cd_sign_idx - asc_idx) % 12) + 1
if cd_house_from_asc == house:
activations.append({
'system': 'Chara Dasha',
'level': 'maha',
'sign': cd_sign,
'reason': f'Chara大运星座{cd_sign}{house}',
})
if activations:
domain_activations[domain] = {
'house': house,
'activations': activations,
'system_count': len(set(a['system'] for a in activations)),
}
# 收敛等级评估
for domain, info in domain_activations.items():
sc = info['system_count']
if sc >= 3:
info['convergence_level'] = 'L4'
info['probability'] = '75-85%'
info['interpretation'] = '三系统同时激活,极强信号'
elif sc >= 2:
info['convergence_level'] = 'L3'
info['probability'] = '50-65%'
info['interpretation'] = '双系统激活,强信号'
else:
info['convergence_level'] = 'L1'
info['probability'] = '+15-20%'
info['interpretation'] = '单系统激活,需Transit确认'
# 收敛窗口(最高优先级的领域)
top_domains = sorted(domain_activations.items(), key=lambda x: x[1]['system_count'], reverse=True)[:5]
return {
'systems_summary': convergence_data['systems'],
'domain_activations': domain_activations,
'top_convergent_domains': [(d, info['convergence_level']) for d, info in top_domains],
'protocol': 'v4.5.0 Dasa Convergence 三系统交叉验证。收敛等级: L1(单系统)→L3(双系统)→L4(三系统)。所有预测必须标注收敛等级。',
}
def _calc_actionable_context(planets, asc_idx):
"""⭐ v4.1.0: 计算Transit Actionable Output所需的上下文数据
输出:宫位激活映射 + 关键行星宫位关系 → 供AI生成Actionable Output时直接引用
"""
# 宫位主星映射(哪个行星掌管哪个宫)
house_lord_map = {}
for h in range(1, 13):
sign_idx = (asc_idx + h - 1) % 12
sname = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sname, 'Unknown')
house_lord_map[h] = {'sign': sname, 'lord': lord}
# 行星落宫映射(哪个行星落在哪个宫)
planet_house_map = {}
for pn, pd in planets.items():
if isinstance(pd, dict):
planet_house_map[pn] = {
'sign': pd.get('sign', 'Unknown'),
'house': pd.get('house', 0),
'degree_in_sign': pd.get('degree_in_sign', 0),
}
# 双宫掌管检测(仓库耦合)
lord_to_houses = {}
for h, info in house_lord_map.items():
lord = info['lord']
if lord not in lord_to_houses:
lord_to_houses[lord] = []
lord_to_houses[lord].append(h)
warehouse_coupling = {l: hs for l, hs in lord_to_houses.items() if len(hs) > 1}
# 关键宫位激活条件(Transit激活该宫需要哪些行星过境相关星座)
KEY_HOUSES = {
2: '财富/家庭/语言',
3: '沟通/技能/短途/内容创作',
5: '创造力/子女/投机/恋爱',
7: '婚姻/合作/公开对手',
9: '长途旅行/高等教育/宗教/出版',
10: '事业/社会地位/权威',
11: '收入/愿望/社交网络/贵人',
12: '海外/灵性/损失/解脱',
}
activation_map = {}
for h, meaning in KEY_HOUSES.items():
sign_idx = (asc_idx + h - 1) % 12
sname = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sname, 'Unknown')
lord_house = planet_house_map.get(lord, {}).get('house', 0)
activation_map[h] = {
'house_meaning': meaning,
'sign': sname,
'lord': lord,
'lord_house': lord_house,
'transit_triggers': f'{lord}过境{h}宫({sname})或{lord}本身过境相关宫位',
'double_transit_hint': f'需要Saturn和Jupiter同时激活{h}宫或{h}宫主所在宫',
}
# 内容创作相关信号(3宫/5宫/9宫/10宫交叉)
content_signals = {}
for h in [3, 5, 9, 10]:
if h in activation_map:
content_signals[f'house_{h}'] = activation_map[h]
# 贵人相关信号(7宫/9宫/11宫)
mentor_signals = {}
for h in [7, 9, 11]:
if h in activation_map:
mentor_signals[f'house_{h}'] = activation_map[h]
return {
'house_lord_map': house_lord_map,
'planet_house_map': planet_house_map,
'warehouse_coupling': warehouse_coupling,
'key_house_activations': activation_map,
'content_creation_context': content_signals,
'mentor_discovery_context': mentor_signals,
'actionable_hint': 'AI应基于此上下文生成Transit Actionable Output:每条Transit预测必须包含时间段+行动类型+置信度。详见SKILL.md Transit Actionable Output规范。',
}
def cmd_full_reading(args):
"""
用户只需提供出生信息,引擎自动串起全链路分析:
chart → dasha → yoga → varga-full → aspects → jaimini → nakshatra-adv
→ argala → tajika → shadbala → ashtakavarga → validate → audit
→ 综合报告输出
"""
import time
t0 = time.time()
report = {
'version': '4.4.0-full-reading',
'birth_info': {
'date': f"{args.year}-{args.month:02d}-{args.day:02d}",
'time': f"{args.hour:02d}:{args.minute:02d}",
'lat': args.lat, 'lon': args.lon,
'tz': f"UTC{'+' if args.tz >= 0 else ''}{args.tz}",
},
'modules': {},
'errors': [],
'warnings': [],
}
# ── Step 1: 核心星盘 ──
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装,无法计算星盘"}
report['chart'] = chart
report['modules']['chart'] = chart
planets = chart.get('planets', {})
asc_deg = chart.get('ascendant', {}).get('degree', 0)
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
planet_degs = {pn: pd.get('degree_in_sign_raw', pd.get('degree_in_sign', pd['degree'] % 30)) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
planet_sign_indices = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd:
planet_sign_indices[pn] = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
# ── Step 1.5: Special Lagnas 特殊上升点 (v4.4.0) ──
try:
sys.path.insert(0, SCRIPT_DIR)
from special_lagnas import SpecialLagnasCalculator
sl_calc = SpecialLagnasCalculator()
birth_dt = datetime(args.year, args.month, args.day, args.hour, args.minute)
# 简化处理:sunrise 近似为 6:00 当地时间
sunrise_dt = datetime(args.year, args.month, args.day, 6, 0)
sl_result = sl_calc.calculate_all_lagnas(
asc_degree=asc_deg,
sun_degree=planet_lons.get('Sun', 0),
moon_degree=planet_lons.get('Moon', 0),
birth_time=birth_dt,
sunrise_time=sunrise_dt
)
# 补充 Arudha Lagna、A10/Karma Pada 和 Upapada Lagna
try:
first_house_sign_idx = asc_idx
first_lord = SIGN_LORDS.get(SIGNS[first_house_sign_idx], '')
first_lord_deg = planet_lons.get(first_lord, 0)
sl_result['Arudha_Lagna'] = sl_calc.calculate_arudha_lagna(asc_deg, first_lord_deg)
except Exception as e:
sl_result['Arudha_Lagna'] = {"error": str(e)}
try:
tenth_house_sign_idx = (asc_idx + 9) % 12
tenth_lord = SIGN_LORDS.get(SIGNS[tenth_house_sign_idx], '')
tenth_lord_deg = planet_lons.get(tenth_lord, 0)
sl_result['A10_Karma_Pada'] = sl_calc.calculate_a10(asc_deg, tenth_lord_deg)
except Exception as e:
sl_result['A10_Karma_Pada'] = {"error": str(e)}
try:
twelfth_house_sign_idx = (asc_idx + 11) % 12
twelfth_lord = SIGN_LORDS.get(SIGNS[twelfth_house_sign_idx], '')
twelfth_lord_deg = planet_lons.get(twelfth_lord, 0)
sl_result['Upapada_Lagna'] = sl_calc.calculate_upapada_lagna(asc_deg, twelfth_lord_deg)
except Exception as e:
sl_result['Upapada_Lagna'] = {"error": str(e)}
report['modules']['special_lagnas'] = sl_result
except Exception as e:
report['errors'].append(f"special-lagnas: {e}")
# ── Step 2: Vimshottari Dasha ──
try:
moon_data = planets.get('Moon', {})
moon_lon = moon_data.get('degree_raw', moon_data.get('degree', 0))
nak_idx = int(moon_lon / (360 / 27)) % 27
nak_name = NAKSHATRA_LIST[nak_idx][0]
nak_lord = NAKSHATRA_LIST[nak_idx][1]
pada = int((moon_lon % (360/27)) / (360/108)) + 1
birthdate = f"{args.year}-{args.month:02d}-{args.day:02d}"
today_str = getattr(args, 'today', None) or datetime.now().strftime('%Y-%m-%d')
dasha_result = cmd_dasha(type('Args', (), {
'nakshatra': nak_name, 'pada': pada,
'moon_lon': moon_lon, 'birthdate': birthdate, 'today': today_str
})())
report['modules']['dasha'] = dasha_result
report['modules']['dasha_sandhi'] = _calc_dasha_sandhi(dasha_result, today_str)
except Exception as e:
report['errors'].append(f"dasha: {e}")
# ── Step 3: Yoga格局 ──
try:
yoga_planets = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'house' in pd:
yoga_planets[pn] = {'sign': pd['sign'], 'house': pd['house']}
yoga_result = cmd_yoga(type('Args', (), {
'ascendant': asc_sign,
'planets': ','.join([f"{k}:{v['sign']}:{v['house']}" for k, v in yoga_planets.items()])
})())
report['modules']['yoga'] = yoga_result
except Exception as e:
report['errors'].append(f"yoga: {e}")
# ── Step 4: BPHS十六分盘 ──
try:
sys.path.insert(0, SCRIPT_DIR)
from varga import calc_all_vargas
varga_result = calc_all_vargas(planet_lons, asc_deg, None) # None = 全部分盘
# 补充 D1_Rashi 到 varga_full(使用 chart 中的行星位置)
d1_data = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd:
d1_data[pn] = {
"sign": pd["sign"],
"degree": pd.get("degree", 0),
"degree_in_sign": pd.get("degree_in_sign", pd.get("degree", 0) % 30),
"house": pd.get("house"),
"nakshatra": pd.get("nakshatra"),
"nakshatra_pada": pd.get("nakshatra_pada"),
}
if d1_data:
varga_result["D1_Rashi"] = d1_data
report['modules']['varga_full'] = varga_result
report['modules']['vargottama'] = _calc_vargottama(planets, varga_result)
report['modules']['pushkara'] = _calc_pushkara_flags(planets)
except Exception as e:
report['errors'].append(f"varga-full: {e}")
# ── Step 4.5: Vimsopaka Bala 分盘力量 (v4.4.0) ──
try:
from vimsopaka_calculator import VimsopakaBalaCalculator, VargaType as VimsVargaType, DignityLevel
vims_calc = VimsopakaBalaCalculator(mode="shodasavarga")
# 将 varga_result 转换为 Vimsopaka 所需的 planet_vargas 格式
# varga_result 结构: {varga_name: {planet: {sign, degree, ...}}}
# 需要转为: {planet: {VargaType: DignityLevel}}
dignity_map = {
'EXALTED': DignityLevel.EXALTED, 'MOOLATRIKONA': DignityLevel.MOOLATRIKONA,
'OWN_SIGN': DignityLevel.OWN_SIGN, 'FRIEND': DignityLevel.FRIEND,
'NEUTRAL': DignityLevel.NEUTRAL, 'ENEMY': DignityLevel.ENEMY,
'DEBILITATED': DignityLevel.DEBILITATED,
}
# 建立 VargaType → varga_result key 的映射(修复:每个分盘独立计算尊贵)
_vt_div = {VimsVargaType.D1:1, VimsVargaType.D2:2, VimsVargaType.D3:3,
VimsVargaType.D4:4, VimsVargaType.D7:7, VimsVargaType.D9:9,
VimsVargaType.D10:10, VimsVargaType.D12:12, VimsVargaType.D16:16,
VimsVargaType.D20:20, VimsVargaType.D24:24, VimsVargaType.D27:27,
VimsVargaType.D30:30, VimsVargaType.D40:40, VimsVargaType.D45:45,
VimsVargaType.D60:60}
_div_meta = {2:'Hora',3:'Drekkana',4:'Turyamsa',7:'Saptamsa',9:'Navamsa',
10:'Dasamsa',12:'Dwadashamsa',16:'Shodasamsa',20:'Vimsamsa',
24:'Siddhamsa',27:'Bhamsa',30:'Trimsamsa',40:'Khavedamsa',
45:'Akshavedamsa',60:'Shashtiamsa'}
planet_vargas_input = {}
for pn in planet_lons:
planet_vargas_input[pn] = {}
for vt in VimsVargaType:
div = _vt_div.get(vt)
if div and varga_result:
# 构造对应的 key,如 D9_Navamsa
vkey = f"D{div}_{_div_meta.get(div, f'D{div}')}"
vdata = varga_result.get(vkey, {})
# 使用 _get_dignity_level 计算完整5-fold尊贵(含Friend/Enemy
# 不使用 _dignity 字典(只有 Exalted/Debilitated/Own Sign
pinfo = vdata.get(pn, {})
if isinstance(pinfo, dict) and 'sign' in pinfo:
dl_key = _get_dignity_level(pn, pinfo['sign'],
pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30))
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
elif div == 1:
# D1 直接用本命盘数据
p_sign = SIGNS[int(planet_lons.get(pn, 0) / 30) % 12]
d_in_s = planet_lons.get(pn, 0) % 30
dl_key = _get_dignity_level(pn, p_sign, d_in_s)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
elif div == 1 and not varga_result:
# fallback: D1 从 planet_lons 计算
p_sign = SIGNS[int(planet_lons.get(pn, 0) / 30) % 12]
d_in_s = planet_lons.get(pn, 0) % 30
dl_key = _get_dignity_level(pn, p_sign, d_in_s)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
vimsopaka_result = vims_calc.calculate_vimsopaka_bala(planet_vargas_input)
# 添加顶层汇总
if isinstance(vimsopaka_result, dict):
total_scores = []
for pn, pdata in vimsopaka_result.items():
if isinstance(pdata, dict) and 'total_score' in pdata:
total_scores.append(pdata['total_score'])
if total_scores:
avg_score = round(sum(total_scores) / len(total_scores), 2)
vimsopaka_result['total_score'] = avg_score
vimsopaka_result['total_score_max'] = 20.0
vimsopaka_result['status'] = '优秀' if avg_score >= 15 else '良好' if avg_score >= 10 else '一般' if avg_score >= 7 else '偏弱'
report['modules']['vimsopaka'] = vimsopaka_result
except Exception as e:
report['errors'].append(f"vimsopaka: {e}")
# ── Step 4.6: Divisional Charts Extended 扩展分盘 (v4.4.0) ──
try:
from divisional_charts_extended import DivisionalChartsCalculator
dc_calc = DivisionalChartsCalculator()
# 只计算 D5/D6/D8/D11(扩展分盘,与 Step 4 的标准分盘互补)
ext_vargas = dc_calc.calculate_all_vargas(planet_lons, asc_deg)
# 过滤只保留 Step 4 没有的分盘
ext_filtered = {k: v for k, v in ext_vargas.items()
if v.get('division') in [5, 6, 8, 11]}
report['modules']['varga_extended'] = ext_filtered
except Exception as e:
report['errors'].append(f"varga-extended: {e}")
# ── Step 5: 精确相位 ──
try:
from aspects import calc_all_aspects
aspects_result = calc_all_aspects(planet_lons, asc_deg)
report['modules']['aspects'] = aspects_result
except Exception as e:
report['errors'].append(f"aspects: {e}")
# ── Step 6: Jaimini系统 ──
try:
from jaimini import calc_chara_karaka_7, calc_chara_karaka_8, calc_chara_dasha, calc_karakamsha, calc_chara_dasha_with_antardasha
from varga import calc_varga
jaimini_result = {}
jaimini_result['chara_karaka_7'] = calc_chara_karaka_7(planet_degs)
jaimini_result['chara_karaka_8'] = calc_chara_karaka_8(planet_degs)
# Step 6+: Karaka JH Compatible Mode (v4.4.0)
try:
from karaka_calculator import KarakaCalculator, KarakaMode
kc_jh = KarakaCalculator(mode=KarakaMode.JH_COMPATIBLE)
jaimini_result['chara_karaka_jh'] = kc_jh.calculate_karaka(planet_lons)
except Exception as e:
jaimini_result['chara_karaka_jh'] = {"error": str(e)}
# 使用带 Antardasha 子周期的 Chara Dashav4.3.0v6.0.9后标注为partial
jaimini_result['chara_dasha'] = calc_chara_dasha_with_antardasha(asc_idx, planet_lons, args.year, args.month)
jaimini_result['has_antardasha'] = True
jaimini_result['chara_dasha_capability'] = {
'status': 'partial',
'reason': 'Round 7 vs PyJHora KN Rao matched 58/240 fields; current implementation is simplified, not full KN Rao/PVN Rao/Iranganti.',
'usage_rule': 'Use Chara Karaka/Karakamsha normally; use Chara Dasha timing only as low-weight corroboration.'
}
# Karakamsha(用AK灵魂星,非DK配偶星)
# ⚠️ 2026-05-03修正:此前错误使用DK,现已修正为AK
ck7 = jaimini_result['chara_karaka_7']
ak_name = ck7['karaka_table']['Atmakaraka']['planet']
ak_lon = planet_lons.get(ak_name, 0)
ak_d9 = calc_varga(ak_lon, 9)
jaimini_result['karakamsha'] = calc_karakamsha(
ak_d9.get('sign', 'Aries'), ak_d9.get('degree_in_sign', 0))
report['modules']['jaimini'] = jaimini_result
except Exception as e:
report['errors'].append(f"jaimini: {e}")
# ── Step 7: 高级Nakshatra ──
try:
from nakshatra_advanced import find_nakshatra, calc_all_tara_balas, calc_sub_lord
nak_result = {'planets': {}}
for pn, lon in planet_lons.items():
nak_result['planets'][pn] = find_nakshatra(lon)
moon_nak_idx = int(planet_lons.get('Moon', 0) / (360/27)) % 27
nak_result['tara_bala'] = calc_all_tara_balas(moon_nak_idx, planet_lons)
nak_result['sub_lords'] = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()}
report['modules']['nakshatra_advanced'] = nak_result
except Exception as e:
report['errors'].append(f"nakshatra-adv: {e}")
# ── Step 8: Argala门闩 ──
try:
from argala import calc_argala
report['modules']['argala'] = calc_argala(planet_sign_indices, asc_idx)
except Exception as e:
report['errors'].append(f"argala: {e}")
# ── Step 9: Tajika年运盘(需要年龄) ──
age = args.age
if age is None:
# 自动计算年龄
try:
birth_date = datetime(args.year, args.month, args.day)
age = (datetime.now() - birth_date).days // 365
report['warnings'].append(f"未提供年龄,自动计算为 {age}")
except:
age = None
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)
report['modules']['tajika'] = tajika_result
except Exception as e:
report['errors'].append(f"tajika: {e}")
# ── Step 10: Shadbala六重力量 ──
try:
from shadbala import calc_shadbala
birth_hour = args.hour + args.minute / 60.0
sun_lon = planet_lons.get('Sun', 0)
moon_lon = planet_lons.get('Moon', 0)
shadbala_result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
# 添加顶层汇总
if isinstance(shadbala_result, dict):
sb_planets = shadbala_result.get('planets', {})
total_rupas = 0
min_req_total = 0
strong_count = 0
for pn, pdata in sb_planets.items():
if isinstance(pdata, dict):
total_rupas += pdata.get('total_rupas', 0)
min_req_total += pdata.get('min_required', 0)
if pdata.get('total_rupas', 0) >= pdata.get('min_required', 0):
strong_count += 1
shadbala_result['total_shadbala'] = round(total_rupas, 2)
shadbala_result['total_min_required'] = round(min_req_total, 2)
shadbala_result['strong_planets'] = strong_count
shadbala_result['weak_planets'] = len(sb_planets) - strong_count
shadbala_result['status'] = '优秀' if strong_count >= 5 else '良好' if strong_count >= 3 else '一般'
report['modules']['shadbala'] = shadbala_result
except Exception as e:
report['errors'].append(f"shadbala: {e}")
# ── Step 10.5: Avasthas 行星状态 (v4.4.0) ──
try:
from avastha_calculator import AvasthaCalculator
avast_calc = AvasthaCalculator()
avast_result = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'degree' in pd:
sign = pd['sign']
deg_in_sign = pd.get('degree_in_sign', pd['degree'] % 30)
house = pd.get('house', 0)
# 收集同宫行星作为 conjunctions
conjunctions = [p2 for p2, pd2 in planets.items()
if isinstance(pd2, dict) and pd2.get('house') == house and p2 != pn]
avast_result[pn] = avast_calc.calculate_all_avasthas(
planet=pn, sign=sign, degree=deg_in_sign,
house=house, conjunctions=conjunctions)
report['modules']['avasthas'] = avast_result
except Exception as e:
report['errors'].append(f"avasthas: {e}")
# ── Step 11: Ashtakavarga八分法 ──
asht_data = None
try:
from ashtakavarga import calc_ashtakavarga
asht_data = calc_ashtakavarga(planets, asc_idx)
report['modules']['ashtakavarga'] = asht_data
except Exception as e:
report['errors'].append(f"ashtakavarga: {e}")
# ── Step 12: R1-R10数学验证 ──
try:
from validate import validate_chart
report['modules']['validation'] = validate_chart(chart, asht_data)
except Exception as e:
report['errors'].append(f"validate: {e}")
# ── Step 13: P1-P12行星审计 ──
try:
audit_result = cmd_audit(args)
report['modules']['audit'] = audit_result
except Exception as e:
report['errors'].append(f"audit: {e}")
# ── Step 14: ⭐ Transit Actionable Context (v4.1.0) ──
try:
actionable_ctx = _calc_actionable_context(planets, asc_idx)
report['modules']['actionable_context'] = actionable_ctx
except Exception as e:
report['errors'].append(f"actionable-context: {e}")
# ── Step 15: Planetary Congregation 行星聚集 (v4.3.0) ──
try:
congregation = _calc_planetary_congregation(planets, asc_idx)
report['modules']['congregation'] = congregation
except Exception as e:
report['errors'].append(f"congregation: {e}")
# ── Step 16: Vivah Saham 婚姻敏感点 (v4.3.0) ──
try:
sahams = _calc_vivah_saham(planets, asc_deg)
# 包装为标准结构,与 standalone vivah-saham 子命令一致
if 'error' not in sahams:
report['modules']['vivah_saham'] = {
'vivah_saham': {
'longitude': sahams['saham_lon'],
'sign': sahams['saham_sign'],
'degree_in_sign': sahams['saham_deg_in_sign'],
'house': sahams['saham_house'],
},
**sahams
}
else:
report['modules']['vivah_saham'] = sahams
except Exception as e:
report['errors'].append(f"vivah-saham: {e}")
# ── Step 17: Transit 多参考点分析 (v6.0.10 true transit) ──
try:
transit_reference_date = getattr(args, 'transit_date', None) or getattr(args, 'today', None) or datetime.now().strftime('%Y-%m-%d')
ty, tm, td = map(int, transit_reference_date.split('-'))
transit_jd = swe.julday(ty, tm, td, 12.0 - args.tz)
transit_planets, transit_ayanamsa = _calc_sidereal_planets_for_jd(transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True)
report['modules']['transit_positions'] = {
'method': 'Swiss Ephemeris true transit positions',
'target_date': transit_reference_date,
'ayanamsa': round(transit_ayanamsa, 4) if transit_ayanamsa is not None else None,
'node_mode': getattr(args, 'node_mode', 'mean'),
'data_layer': 'true_transit_positions',
'planets': transit_planets,
}
transit_multi = _calc_transit_multi_reference(planets, asc_idx, asc_deg, planet_lons, transit_planets=transit_planets, transit_date=transit_reference_date, node_mode=getattr(args, 'node_mode', 'mean'))
report['modules']['transit_multi_reference'] = transit_multi
except Exception as e:
report['errors'].append(f"transit-multi-ref: {e}")
# ── Step 18: Dasa Convergence 三系统交叉验证 (v4.5.0 P1) ──
try:
dasha_data = report['modules'].get('dasha', {})
jaimini_data = report['modules'].get('jaimini', {})
chara_dasha_data = jaimini_data.get('chara_dasha', {}) if isinstance(jaimini_data, dict) else {}
birthdate_str = f"{args.year}-{args.month:02d}-{args.day:02d}"
yogini_data = _calc_yogini_dasha(planet_lons.get('Moon', 0), birthdate_str)
report['modules']['yogini_dasha'] = yogini_data
convergence = _calc_dasa_convergence(dasha_data, chara_dasha_data, yogini_data, planet_lons, asc_idx)
report['modules']['dasa_convergence'] = convergence
except Exception as e:
report['errors'].append(f"dasa-convergence: {e}")
# ── Step 19: D9 Navamsa 逐行星尊严展开 (v4.5.0 P1) ──
try:
varga_data = report['modules'].get('varga_full', {})
d9_data = varga_data.get('D9_Navamsa', {}) if isinstance(varga_data, dict) else {}
if d9_data:
d9_expanded = {}
for pn, pd in d9_data.items():
if pn == '_meta' or not isinstance(pd, dict) or 'sign' not in pd:
continue
d9_sign = pd['sign']
d9_deg = pd.get('degree_in_sign', pd.get('degree', 0) % 30)
dignity = _get_dignity_level(pn, d9_sign, d9_deg)
# D9 宫位(从D9 Asc计算)
d9_asc_data = d9_data.get('Ascendant', {})
d9_asc_sign_idx = SIGNS.index(d9_asc_data.get('sign', 'Aries')) if isinstance(d9_asc_data, dict) and d9_asc_data.get('sign') in SIGNS else 0
p_sign_idx = SIGNS.index(d9_sign) if d9_sign in SIGNS else 0
d9_house = ((p_sign_idx - d9_asc_sign_idx) % 12) + 1
# 关系状态
d9_sign_lord = SIGN_LORDS.get(d9_sign, '')
is_own = (d9_sign_lord == pn)
is_exalted = (EXALTATION.get(pn) == d9_sign)
is_debilitated = (DEBILITATION.get(pn) == d9_sign)
is_moola = False
if pn in MOOLATRIKONA:
mt_sign, mt_start, mt_end = MOOLATRIKONA[pn]
if mt_sign == d9_sign and mt_start <= d9_deg < mt_end:
is_moola = True
d9_expanded[pn] = {
'sign': d9_sign,
'sign_cn': SIGNS_CN.get(d9_sign, ''),
'house_in_d9': d9_house,
'dignity': dignity,
'is_own_sign': is_own,
'is_exalted': is_exalted,
'is_debilitated': is_debilitated,
'is_moolatrikona': is_moola,
'pada': pd.get('pada'),
'lord': d9_sign_lord,
'degree_in_sign': round(d9_deg, 4),
}
report['modules']['d9_navamsa_expanded'] = d9_expanded
except Exception as e:
report['errors'].append(f"d9-expanded: {e}")
# ── 汇总 ──
elapsed = round(time.time() - t0, 2)
module_count = len(report['modules'])
error_count = len(report['errors'])
report['summary'] = {
'elapsed_seconds': elapsed,
'modules_computed': module_count,
'errors': error_count,
'status': 'complete' if error_count == 0 else f'{error_count} errors',
'next_step': '⭐ v4.5.0: P1缺口已补齐。新增 transit_multi_reference(四参考点) + dasa_convergence(三系统交叉) + yogini_dasha + d9_navamsa_expanded(逐行星尊严展开)。AI必须使用四参考点分析Transit,Dasa预测必须标注收敛等级。',
}
return report
# ============================================================================
# 23. Prashna 问事占星 (v3.9新增)
# ============================================================================
def cmd_prashna(args):
"""Prashna 问事占星:基于提问时刻的即时星盘分析"""
try:
from prashna import cast_prashna, calc_arudha, calc_sphutas, calc_life_sphutas, calc_sahams, analyze_lost_item, kunda_verify, calc_gulika_simple
except ImportError:
# 尝试从同目录导入
import importlib.util, os
spec = importlib.util.spec_from_file_location("prashna", os.path.join(os.path.dirname(__file__), "prashna.py"))
prashna_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(prashna_mod)
cast_prashna = prashna_mod.cast_prashna
calc_arudha = prashna_mod.calc_arudha
calc_sphutas = prashna_mod.calc_sphutas
calc_life_sphutas = prashna_mod.calc_life_sphutas
calc_sahams = prashna_mod.calc_sahams
analyze_lost_item = prashna_mod.analyze_lost_item
kunda_verify = prashna_mod.kunda_verify
calc_gulika_simple = prashna_mod.calc_gulika_simple
if args.mode == 'chart':
return cast_prashna(args.datetime, args.lat, args.lon)
# 其他模式需要先铸盘获取行星位置
chart = cast_prashna(args.datetime, args.lat, args.lon)
if 'error' in chart:
return chart
asc_lon = chart['ascendant']['lon']
p_lons = {n: d['lon'] for n, d in chart['planets'].items()}
if args.mode == 'arudha':
return {'arudha_lagna': calc_arudha(asc_lon, p_lons),
'ascendant': chart['ascendant']}
elif args.mode == 'sphutas':
return calc_sphutas(p_lons, 0)
elif args.mode == 'sahams':
return calc_sahams(p_lons, asc_lon)
elif args.mode == 'lost-item':
return analyze_lost_item(p_lons, asc_lon)
elif args.mode == 'life':
return calc_life_sphutas(asc_lon, p_lons.get('Moon',0), p_lons.get('Sun',0), 0)
elif args.mode == 'kunda':
return kunda_verify(asc_lon)
else:
return cast_prashna(args.datetime, args.lat, args.lon)
# ============================================================================
# CLI入口
# ============================================================================
def main():
parser = argparse.ArgumentParser(description='印度占星统一引擎 v3.7.0', formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest='command', help='子命令')
# 1. chart
p = sub.add_parser('chart', help='计算完整星盘')
_add_chart_args(p)
p.add_argument('--validate', action='store_true', help='附加R1-R10数学验证')
# 2. dasha
p = sub.add_parser('dasha', help='计算Dasha大运')
p.add_argument('--nakshatra', default=None); p.add_argument('--pada', type=int, default=None)
p.add_argument('--moon-lon', type=float, default=None); p.add_argument('--birthdate', required=True)
p.add_argument('--today', default=None)
# 3. yoga
p = sub.add_parser('yoga', help='Yoga格局识别')
p.add_argument('--ascendant', default=None)
p.add_argument('--planets', default=None, help="格式: 'Sun:Aries:9,Moon:Aquarius:7,...'")
# 4. predict
p = sub.add_parser('predict', help='三层验证法事件预测')
p.add_argument('--chart', default=None, help='星盘JSON字符串')
p.add_argument('--event-type', default='all', choices=['all', 'marriage', 'career', 'wealth', 'health'])
p.add_argument('--past-verify', action='store_true', help='验前事模式:推断2-4个高信号历史时段')
p.add_argument('--year', type=int, default=None, help='出生年(验前事模式需要)')
p.add_argument('--month', type=int, default=None, help='出生月')
p.add_argument('--day', type=int, default=None, help='出生日')
p.add_argument('--hour', type=int, default=None, help='出生时')
p.add_argument('--minute', type=int, default=None, help='出生分')
p.add_argument('--lat', type=float, default=None, help='纬度')
p.add_argument('--lon', type=float, default=None, help='经度')
p.add_argument('--tz', type=float, default=0, help='时区')
# 5. varga
p = sub.add_parser('varga', help='分盘计算')
_add_chart_args(p)
p.add_argument('--d9', action='store_true'); p.add_argument('--d10', action='store_true')
p.add_argument('--all', action='store_true')
# 6. celebrity
p = sub.add_parser('celebrity', help='名人案例查询')
p.add_argument('--name', default=None); p.add_argument('--limit', type=int, default=20)
# 7. db-stats
sub.add_parser('db-stats', help='验证数据库统计')
# 8. transit
p = sub.add_parser('transit', help='行星过境查询(Swiss Ephemeris实时计算)')
p.add_argument('--year', type=int, required=True); p.add_argument('--month', type=int, required=True)
p.add_argument('--day', type=int, default=15, help='指定日期(默认15日取月中代表)')
p.add_argument('--planet', default=None, help='目标行星,逗号分隔(默认全部,如:Jupiter,Saturn')
p.add_argument('--tz', type=float, default=8, help='时区(默认UTC+8')
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'], help='Rahu/Ketu节点口径:mean=Mean Node(默认),true=True Node')
# 9. shadbala (v3.4新增)
p = sub.add_parser('shadbala', help='Shadbala六重力量计算(内部一致;外部绝对值校准前partial)')
_add_chart_args(p)
# 10. ashtakavarga (v3.4新增)
p = sub.add_parser('ashtakavarga', help='Ashtakavarga八分法计算')
_add_chart_args(p)
# 11. memory (v3.4新增)
p = sub.add_parser('memory', help='Hermes记忆系统')
p.add_argument('--action', default='stats', choices=['store', 'search', 'context', 'stats'])
p.add_argument('--content', default=None, help='存储内容(store操作必填)')
p.add_argument('--query', default=None, help='搜索查询(search操作必填)')
p.add_argument('--tags', default=None, help='标签,逗号分隔')
p.add_argument('--importance', type=int, default=5, help='重要性 1-10')
p.add_argument('--limit', type=int, default=10, help='搜索结果数量')
# 12. validate (v3.5新增)
p = sub.add_parser('validate', help='R1-R10数学验证')
_add_chart_args(p)
# 13. audit (v3.5新增)
p = sub.add_parser('audit', help='P1-P12行星审计管线')
_add_chart_args(p)
# 14. report (v3.6新增)
p = sub.add_parser('report', help='MD→HTML报告生成(羊皮纸主题)')
p.add_argument('folder', help='包含MD文件的目录路径')
p.add_argument('--name', default='Client', help='客户姓名')
p.add_argument('--lagna', default='', help='上升星座')
p.add_argument('--gender', default='', help='性别')
p.add_argument('--status', default='', help='当前状态')
p.add_argument('--lang', default='cn', choices=['cn', 'en'], help='语言 (默认cn)')
p.add_argument('--output', default=None, help='输出HTML路径')
# 15. varga-full (v3.7新增)
p = sub.add_parser('varga-full', help='BPHS十六分盘完整计算')
_add_chart_args(p)
p.add_argument('--divisions', default=None, help='指定分盘,逗号分隔(如 D2,D9,D60),空=全部')
# 16. aspects (v3.7新增)
p = sub.add_parser('aspects', help='度数精确相位系统')
_add_chart_args(p)
# 17. jaimini (v3.7新增)
p = sub.add_parser('jaimini', help='Jaimini系统(Chara Karaka/Dasha/Karakamsha')
_add_chart_args(p)
p.add_argument('--mode', default='all', choices=['all','karaka','dasha','karakamsha'], help='分析模式')
p.add_argument('--antardasha', action='store_true', help='Chara Dasha含Antardasha子周期(当前timing实现为partial')
# 18. nakshatra-adv (v3.7新增)
p = sub.add_parser('nakshatra-adv', help='高级Nakshatra分析')
_add_chart_args(p)
p.add_argument('--mode', default='all', choices=['all','detail','tara','sublord'], help='分析模式')
# 19. argala (v3.7新增)
p = sub.add_parser('argala', help='Argala门闩系统')
_add_chart_args(p)
# 20. tajika (v3.7新增)
p = sub.add_parser('tajika', help='Tajika/Varshaphala年运盘')
_add_chart_args(p)
p.add_argument('--age', type=int, required=True, help='当前年龄')
p.add_argument('--mode', default='all', choices=['all','muntha','yearlord','mudda','tripataka'], help='分析模式')
# 21. synastry (v3.7新增)
p = sub.add_parser('synastry', help='合盘分析(Ashta Koota 36分制)')
p.add_argument('--moon1', type=float, required=True, help='Person1月亮黄经')
p.add_argument('--moon2', type=float, required=True, help='Person2月亮黄经')
p.add_argument('--mars1', type=float, default=None, help='Person1火星黄经')
p.add_argument('--mars2', type=float, default=None, help='Person2火星黄经')
p.add_argument('--asc1', type=float, default=None, help='Person1上升黄经')
p.add_argument('--asc2', type=float, default=None, help='Person2上升黄经')
p.add_argument('--gender1', default='M', help='Person1性别')
p.add_argument('--gender2', default='F', help='Person2性别')
# 22. full-reading (v3.7.1新增)
p = sub.add_parser('full-reading', help='全自动综合解盘(出生信息→全链路→完整报告)')
_add_chart_args(p)
p.add_argument('--age', type=int, default=None, help='当前年龄(不提供则自动计算)')
p.add_argument('--today', default=None, help='Dasha/Sandhi参考日期 YYYY-MM-DD(默认今天)')
p.add_argument('--transit-date', default=None, help='Transit真实过境参考日期 YYYY-MM-DD(默认跟随--today或今天)')
# 23. prashna (v3.9新增)
p = sub.add_parser('prashna', help='Prashna问事占星(提问时刻星盘+Arudha+Sphuta+Sahams')
p.add_argument('--datetime', required=True, help='提问时间 YYYY-MM-DD HH:MM')
p.add_argument('--lat', type=float, required=True, help='纬度')
p.add_argument('--lon', type=float, required=True, help='经度')
p.add_argument('--mode', default='chart', choices=['chart','arudha','sphutas','sahams','lost-item','life','kunda'], help='分析模式')
# 24. double-transit-pac (v3.9新增)
p = sub.add_parser('double-transit-pac', help='Double Transit PAC + D9层(KN Rao完整实现)')
_add_chart_args(p)
p.add_argument('--date', required=True, help='过境日期 YYYY-MM-DD')
p.add_argument('--house', type=int, default=7, help='目标宫位(默认7=婚姻)')
# 25. transit-ll7l (v3.9新增)
p = sub.add_parser('transit-ll7l', help='Transit LL/7L连接+互换检测')
_add_chart_args(p)
p.add_argument('--date', required=True, help='过境日期 YYYY-MM-DD')
# 26. planetary-congregation (v3.9新增)
p = sub.add_parser('planetary-congregation', help='行星聚集检测(Lagna/7H+Transit')
_add_chart_args(p)
p.add_argument('--house', type=int, default=7, help='目标宫位')
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(可选)')
# 27. vivah-saham (v3.9新增)
p = sub.add_parser('vivah-saham', help='Vivah Saham计算+Transit激活')
_add_chart_args(p)
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(可选)')
# 28. audit-capabilities (v6.0.3新增)
p = sub.add_parser('audit-capabilities', help='校验 technique registry 并输出能力覆盖审计')
p.add_argument('--registry', default=None, help='technique_registry.json 路径(默认 references/technique_registry.json')
p.add_argument('--mode', default='validate', choices=['validate', 'table'], help='validate=校验注册表;table=输出路由审计表')
p.add_argument('--route', default=None, help='table模式下的 route id,如 career_timing_strict')
args = parser.parse_args()
if not args.command:
parser.print_help(); sys.exit(1)
cmds = {'chart': cmd_chart, 'dasha': cmd_dasha, 'yoga': cmd_yoga, 'predict': cmd_predict,
'varga': cmd_varga, 'celebrity': cmd_celebrity, 'db-stats': cmd_db_stats, 'transit': cmd_transit,
'shadbala': cmd_shadbala, 'ashtakavarga': cmd_ashtakavarga, 'memory': cmd_memory,
'validate': cmd_validate, 'audit': cmd_audit, 'report': cmd_report,
'varga-full': cmd_varga_full, 'aspects': cmd_aspects, 'jaimini': cmd_jaimini,
'nakshatra-adv': cmd_nakshatra_adv, 'argala': cmd_argala, 'tajika': cmd_tajika,
'synastry': cmd_synastry, 'full-reading': cmd_full_reading, 'prashna': cmd_prashna,
'double-transit-pac': cmd_double_transit_pac,
'transit-ll7l': cmd_transit_ll7l, 'planetary-congregation': cmd_planetary_congregation,
'vivah-saham': cmd_vivah_saham}
if args.command == 'audit-capabilities':
from audit_capabilities import build_audit_table, load_registry, validate_registry
registry = load_registry(args.registry) if args.registry else load_registry()
result = validate_registry(registry) if args.mode == 'validate' else build_audit_table(registry, args.route)
output_json(result)
sys.exit(0 if result.get('valid', True) else 1)
result = cmds[args.command](args)
output_json(result)
if __name__ == '__main__':
main()