Files
Jyotisha/scripts/jyotish_engine.py
T
2026-06-05 14:30:20 +08:00

3968 lines
174 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
from cmd_solar_return import cmd_solar_return # v6.0.18
from cmd_narayana_dasha import cmd_narayana_dasha as _cmd_narayana_dasha_impl # v6.0.20
from cmd_muhurta import cmd_muhurta # v6.0.21
from yoga_engine import detect_yogas # v6.0.26: data-driven Yoga engine
# ============================================================================
# 常量
# ============================================================================
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}
# ============================================================================
# 新增:Dispositor Chain + Inter-chart Linkage (v6.0.12)
# ============================================================================
def calc_dispositor_chain(planet_name, planets_data, max_depth=12):
"""
计算行星的定位星链(dispositor chain)。
返回列表:[{planet, sign, dispositor, dispositor_sign}, ...]
直到循环或达到 max_depth。
"""
chain = []
current_planet = planet_name
visited = set()
for _ in range(max_depth):
if current_planet in visited:
break
visited.add(current_planet)
pdata = planets_data.get(current_planet, {})
if not isinstance(pdata, dict) or 'sign' not in pdata:
break
sign = pdata['sign']
dispositor = SIGN_LORDS.get(sign, '')
chain.append({
'planet': current_planet,
'sign': sign,
'sign_cn': SIGNS_CN.get(sign, ''),
'dispositor': dispositor,
'dispositor_sign': sign, # 定位星所在的星座(即当前行星所在星座)
})
if not dispositor:
break
current_planet = dispositor
return chain
def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None):
"""
计算行星在 D1/D9/D10 分盘之间的飞星落宫。
返回 {chart_type: {sign, house_in_chart, lord_of_sign_in_chart}}
"""
result = {}
charts = [
('D1', d1_data, d1_asc_idx),
('D9', d9_data, d9_asc_idx),
('D10', d10_data, d10_asc_idx),
]
if d12_data:
charts.append(('D12', d12_data, None))
for chart_name, chart_data, asc_idx in charts:
if not isinstance(chart_data, dict):
continue
pdata = chart_data.get(planet_name, {})
if not isinstance(pdata, dict) or 'sign' not in pdata:
continue
sign = pdata['sign']
sign_idx = SIGNS.index(sign) if sign in SIGNS else 0
house = None
if asc_idx is not None:
house = ((sign_idx - asc_idx) % 12) + 1
lord = SIGN_LORDS.get(sign, '')
result[chart_name] = {
'planet': planet_name,
'sign': sign,
'sign_cn': SIGNS_CN.get(sign, ''),
'house': house,
'lord': lord,
}
return result
def calc_all_dispositor_chains(planets_data):
"""为所有行星计算定位星链"""
result = {}
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']:
if pname in planets_data:
result[pname] = calc_dispositor_chain(pname, planets_data)
return result
def calc_all_inter_chart_linkages(d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None):
"""
为所有行星计算 D1/D9/D10 分盘间飞星。
需要各分盘的 asc_idx 来正确计算落宫。
"""
result = {}
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']:
result[pname] = calc_inter_chart_linkage(
pname, d1_data, d9_data, d10_data, d12_data,
d1_asc_idx, d9_asc_idx, d10_asc_idx
)
return result
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]
deg_in_sign = asc_deg - asc_idx * 30
result["ascendant"] = {"sign": asc_sign, "sign_cn": SIGNS_CN[asc_sign],
"degree": round(deg_in_sign, 4), "degree_raw": round(asc_deg, 4),
"degree_in_sign": round(deg_in_sign, 4),
"degree_in_sign_raw": asc_deg,
"lon": round(asc_deg, 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:
# v6.0.27: Auto-calculate Moon's Nakshatra from birth datetime
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未安装,无法自动计算Nakshatra"}
moon = chart.get("planets", {}).get("Moon", {})
moon_lon = moon.get("degree_raw")
if moon_lon is None:
return {"error": "无法计算Moon位置"}
ns = 360.0 / 27; idx = int(moon_lon / ns); progress = (moon_lon % ns) / ns
nak_info = NAKSHATRA_LIST[idx % 27]
nak_name, start_lord, start_years = nak_info
birthdate = args.birthdate or f"{args.year}-{args.month:02d}-{args.day:02d}"
birth_dt = datetime.strptime(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": birthdate, "reference_date": today.strftime("%Y-%m-%d"), "timeline": timeline, "current_dasha": current}
# ============================================================================
# 3. Yoga识别
# ============================================================================
def cmd_yoga(args):
"""
Yoga 识别 —— v6.0.26 数据驱动引擎。
支持两种输入:
1. 兼容旧接口:--ascendant + --planets "Sun:Aries:1[:10.5],Moon:Cancer:4[:15]"
2. 出生信息直算:--year --month --day --hour --minute --lat --lon --tz [--node-mode]
"""
planets = {}
asc = args.ascendant or "Aries"
if args.planets:
for item in args.planets.split(','):
parts = item.strip().split(':')
if len(parts) >= 3:
pdata = {"sign": parts[1].strip(), "house": int(parts[2].strip())}
if len(parts) >= 4 and parts[3].strip() != "":
try:
pdata["degree"] = float(parts[3].strip())
except ValueError:
pass
planets[parts[0].strip()] = pdata
else:
required_birth_fields = ["year", "month", "day", "hour", "minute", "lat", "lon"]
has_birth_input = all(getattr(args, field, None) is not None for field in required_birth_fields)
if has_birth_input:
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, getattr(args, 'tz', 0), getattr(args, 'node_mode', 'mean')
)
if chart is None:
return {"error": "swisseph未安装"}
asc = chart.get("ascendant", {}).get("sign", asc)
for pname, pd in chart.get("planets", {}).items():
if isinstance(pd, dict) and "sign" in pd and "house" in pd:
planets[pname] = {
"sign": pd["sign"],
"house": pd["house"],
"degree": pd.get("degree_in_sign", pd.get("degree")),
}
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]]))
# 调用数据驱动引擎(yoga_engine.py
yogas = detect_yogas(planets, asc)
return {
"ascendant": asc,
"planets_analyzed": len(planets),
"kendra_lords": kl,
"trikona_lords": tl,
"yogas_detected": len(yogas),
"yogas": yogas,
"detected_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))
# BPHS: movable(0,3,6,9)=same, fixed(1,4,7,10)=+4, dual(2,5,8,11)=+8
if si % 3 == 0: # movable: 0,3,6,9
start = si
elif si % 3 == 1: # fixed: 1,4,7,10
start = (si + 4) % 12
else: # dual: 2,5,8,11
start = (si + 8) % 12
return SIGNS[(start + ni) % 12]
def dasamsa(lon):
si = int(lon / 30); d = lon - si * 30; di = int(d / 3)
# BPHS: odd signs(0,2,4,6,8,10)=same, even signs(1,3,5,7,9,11)=+8 (9th)
start = si if si % 2 == 0 else (si + 8) % 12
return SIGNS[(start + 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('lon', 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('lon', 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('lon', 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'],
'degree': pd.get('degree_in_sign', pd.get('degree')),
}
yoga_result = cmd_yoga(type('Args', (), {
'ascendant': asc_sign,
'planets': ','.join([
f"{k}:{v['sign']}:{v['house']}" + (f":{v['degree']}" if v.get('degree') is not None else "")
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('lon', 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('lon', 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('lon', 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 → v6.0.22 移至 cmd_nakshatra_adv.py
# ============================================================================
def cmd_narayana_dasha(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 not None:
chart['ascendant_index'] = asc_idx
return _cmd_narayana_dasha_impl(args, chart)
def cmd_nakshatra_adv(args):
from cmd_nakshatra_adv import cmd_nakshatra_adv as _impl
return _impl(args)
def cmd_nakshatra_dasha(args):
from cmd_nakshatra_adv import cmd_nakshatra_dasha as _impl
return _impl(args)
def cmd_nakshatra_full(args):
from cmd_nakshatra_adv import cmd_nakshatra_full as _impl
return _impl(args)
# ============================================================================
# 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('lon', chart.get('ascendant', {}).get('degree', 0))
asc_si = int(asc_deg / 30) % 12 # sign index
age = args.age
if age is None:
return {"error": "请提供 --age 参数(当前年龄)"}
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()
def _build_whole_sign_houses(asc_index, planets_data):
"""Build a compatibility house map for add-on modules.
compute_chart_data() exposes Placidus/equal-style cusp keys (house_1...),
while several v6.0.14-16 add-on modules expect 1..12 keys plus Hn_Lord.
This adapter keeps those modules wired without changing their public API.
"""
house_map = {}
for house_num in range(1, 13):
sign_idx = (asc_index + house_num - 1) % 12
sign_name = SIGNS[sign_idx]
house_map[house_num] = {
'sign': sign_name,
'lord': SIGN_LORDS.get(sign_name, ''),
'planets': [],
'strength': 'Neutral',
}
house_map[str(house_num)] = house_map[house_num]
house_map[f'H{house_num}_Lord'] = house_map[house_num]['lord']
for planet_name, planet_data in planets_data.items():
if isinstance(planet_data, dict):
house_num = planet_data.get('house')
if isinstance(house_num, int) and house_num in house_map:
house_map[house_num]['planets'].append(planet_name)
return house_map
def _varga_planet_lons(varga_chart):
"""Convert calc_all_vargas() planet sign/degree data to longitude dict."""
lons = {}
if not isinstance(varga_chart, dict):
return lons
for planet_name, planet_data in varga_chart.items():
if planet_name.startswith('_') or planet_name == 'Ascendant':
continue
if isinstance(planet_data, dict) and 'sign_idx' in planet_data:
lons[planet_name] = planet_data['sign_idx'] * 30 + planet_data.get('degree_in_sign', 0)
return lons
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('lon', 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}
houses = _build_whole_sign_houses(asc_idx, planets)
report['modules']['house_map'] = houses
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, 'transit_date', None) or 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 4.7: Dispositor Chains + Inter-chart Linkage (v6.0.12) ──
try:
# 准备分盘数据:从 varga_full 提取 D1/D9/D10/D12
vf = report['modules'].get('varga_full', {})
d1_for_dc = vf.get('D1_Rashi', {})
d9_for_ic = vf.get('D9_Navamsa', {}) if isinstance(vf.get('D9_Navamsa'), dict) else {}
d10_for_ic = vf.get('D10_Dasamsa', {}) if isinstance(vf.get('D10_Dasamsa'), dict) else {}
d12_for_ic = vf.get('D12_Dwadashamsa', {}) if isinstance(vf.get('D12_Dwadashamsa'), dict) else None
# Dispositor Chains:基于 D1 行星数据
dc_result = calc_all_dispositor_chains(planets)
report['modules']['dispositor_chains'] = dc_result
# Inter-chart Linkage:需要各分盘的 asc_idx
# D1 asc_idx 已知;D9/D10/D12 的 asc_idx 需要从各自分盘数据提取
d9_asc_idx = None
d10_asc_idx = None
d12_asc_idx = None
if isinstance(d9_for_ic, dict):
d9_asc = d9_for_ic.get('Ascendant', {})
if isinstance(d9_asc, dict) and 'sign' in d9_asc:
d9_asc_idx = SIGNS.index(d9_asc['sign']) if d9_asc['sign'] in SIGNS else None
if isinstance(d10_for_ic, dict):
d10_asc = d10_for_ic.get('Ascendant', {})
if isinstance(d10_asc, dict) and 'sign' in d10_asc:
d10_asc_idx = SIGNS.index(d10_asc['sign']) if d10_asc['sign'] in SIGNS else None
if d12_for_ic and isinstance(d12_for_ic, dict):
d12_asc = d12_for_ic.get('Ascendant', {})
if isinstance(d12_asc, dict) and 'sign' in d12_asc:
d12_asc_idx = SIGNS.index(d12_asc['sign']) if d12_asc['sign'] in SIGNS else None
ic_result = calc_all_inter_chart_linkages(
d1_for_dc, d9_for_ic, d10_for_ic, d12_for_ic,
asc_idx, d9_asc_idx, d10_asc_idx
)
report['modules']['inter_chart_linkage'] = ic_result
# 补充:每个行星的"最终定位星"Dispositor Chain 的最后一个非循环元素)
final_dispositors = {}
for pname, chain in dc_result.items():
if chain:
# 找到最后一个不重复的元素
seen = set()
last_valid = None
for item in chain:
if item['dispositor'] in seen:
break
seen.add(item['dispositor'])
last_valid = item
if last_valid:
final_dispositors[pname] = last_valid['dispositor']
else:
final_dispositors[pname] = chain[-1]['dispositor'] if chain else None
else:
final_dispositors[pname] = None
report['modules']['final_dispositors'] = final_dispositors
except Exception as e:
report['errors'].append(f"dispositor-chain+inter-chart: {e}")
# ── Step 4.8: Tajika Yogas + Sahams (v6.0.13) ──
try:
from tajika import calc_tajika_yogas, calc_all_sahams
# Tajika Yogas(用本命盘行星经度)
tc_yogas = calc_tajika_yogas(planet_lons)
report['modules']['tajika_yogas'] = tc_yogas
# Sahams(特殊点)—— 需要出生时间
birth_dt = getattr(args, 'birth_datetime', None)
if birth_dt and planet_lons:
sahams_result = calc_all_sahams(planet_lons, asc_deg, birth_dt)
report['modules']['sahams'] = sahams_result
else:
report['modules']['sahams'] = {'warning': 'birth_datetime or planet_lons missing, skip saham calc'}
except Exception as e:
report['errors'].append(f"tajika-yogas+sahams: {e}")
# ── Step 4.9: Yogas + Doshas + Special Lagnas (v6.0.14) ──
try:
from yogas_doshas import calc_all_yogas_doshas
# 准备参数
asc_sign = SIGNS[asc_idx] if 'asc_idx' in dir() else SIGNS[0]
asc_lord = SIGN_LORDS.get(asc_sign, '')
moon_data = planets.get('Moon', {})
moon_sign = moon_data.get('sign', '') if isinstance(moon_data, dict) else ''
moon_house = moon_data.get('house', 0) if isinstance(moon_data, dict) else 0
mars_data = planets.get('Mars', {})
mars_house = mars_data.get('house', 0) if isinstance(mars_data, dict) else 0
sun_data = planets.get('Sun', {})
sun_house = sun_data.get('house', 0) if isinstance(sun_data, dict) else 0
# 12宫主星
h12_lord = SIGN_LORDS.get(SIGNS[(asc_idx + 11) % 12], '')
yd_result = calc_all_yogas_doshas(
planets, houses,
asc_sign, asc_lord,
moon_sign, moon_house,
mars_house, sun_house,
h12_lord
)
report['modules']['yogas_doshas'] = yd_result
except Exception as e:
report['errors'].append(f"yogas-doshas: {e}")
# ── Step 4.10: Tithi Lord + Pancha Pakshi + Rashi Tulya Navamsa (v6.0.15) ──
try:
# Tithi Lord(出生 Tithi + Lord 分析)
from tithi_lord import calc_tithi_lord_full
sun_deg = planet_lons.get('Sun', 0)
moon_deg = planet_lons.get('Moon', 0)
tithi_result = calc_tithi_lord_full(sun_deg, moon_deg, planets, houses)
report['modules']['tithi_lord'] = tithi_result
except Exception as e:
report['errors'].append(f"tithi-lord: {e}")
try:
# Pancha Pakshi(五鸟系统,需要出生 Nakshatra)
# 先尝试从 nakshatra_advanced 获取出生 Nakshatra
nakshatra_num = None
if 'moon_nakshatra' in dir() or 'moon_nak' in locals():
pass # 动态获取
# 从 planets 数据推算 NakshatraMoon 的度数为基准)
moon_deg = planet_lons.get('Moon', 0)
nak_num = int(moon_deg / 13.3333333) + 1
if nak_num > 27:
nak_num = 27
from pancha_pakshi import calc_pakshi_full_analysis
pk_result = calc_pakshi_full_analysis(nak_num, target_weekday=0, target_period=0)
report['modules']['pancha_pakshi'] = pk_result
except Exception as e:
report['errors'].append(f"pancha-pakshi: {e}")
try:
# Rashi Tulya NavamsaD1 与 D9 同宫对比分析)
from rashi_tulya_navamsa import analyze_rashi_tulya_navamsa, rashi_tulya_navamsa_summary
varga_full = report['modules'].get('varga_full', {})
d9_data = varga_full.get('D9_Navamsa', {})
if d9_data and d9_data.get('planets') and d9_data.get('houses'):
rt_result = analyze_rashi_tulya_navamsa(
planets, houses,
d9_data['planets'], d9_data['houses']
)
rt_summary = rashi_tulya_navamsa_summary(rt_result)
report['modules']['rashi_tulya_navamsa'] = {
'analysis': rt_result,
'summary': rt_summary
}
else:
report['modules']['rashi_tulya_navamsa'] = {'note': 'D9 data incomplete, skip Rashi Tulya Navamsa'}
except Exception as e:
report['errors'].append(f"rashi-tulya-navamsa: {e}")
# ── Step 4.11: Marriage Counting + Muntha + D30 + Prashna (v6.0.16) ──
try:
# Marriage Counting Method (Bhrigu)
from marriage_counting import marriage_counting_full_analysis
d1_house7_lord = houses.get('7', {}).get('lord', '') if isinstance(houses.get('7'), dict) else ''
if d1_house7_lord and 'varga_full' in report['modules']:
d9_data = report['modules']['varga_full'].get('D9_Navamsa', {})
d9_planet_lons = _varga_planet_lons(d9_data)
if d9_planet_lons:
mc_result = marriage_counting_full_analysis(
d1_house7_lord, planet_lons, d9_planet_lons,
houses, None
)
report['modules']['marriage_counting'] = mc_result
except Exception as e:
report['errors'].append(f"marriage-counting: {e}")
try:
# Bhrigu Pada Dasha(通用近似版,精确公式因流派而异)
from bhrigu_pada_dasha import bhrigu_pada_dasha_full_report
moon_lon = planet_lons.get('Moon', 0)
birth_jd = report.get('metadata', {}).get('birth_jd', 0) or 0
if moon_lon and birth_jd:
bpd_result = bhrigu_pada_dasha_full_report(
moon_lon, birth_jd,
d9_7lord_sign=None # 将在 D9 数据可用时补充
)
report['modules']['bhrigu_pada_dasha'] = bpd_result
except Exception as e:
report['errors'].append(f"bhrigu-pada-dasha: {e}")
try:
# Muntha (Tajika Varshaphala) — 需要 Varshaphala 盘数据
# Varshaphala 盘需另外排盘(太阳返照时刻),full-reading 中暂不可用
# 如需 Varshaphala 分析,请使用 cmd_prashna(独立命令支持 Varshaphala 排盘)
report['modules']['muntha'] = {
'note': 'Muntha 需要 Varshaphala(太阳返照)盘数据。请使用 cmd_prashna 独立命令获取 Varshaphala 分析。',
'varshaphala_available': False
}
except Exception as e:
report['errors'].append(f"muntha: {e}")
try:
# D30 Trimshamsa analysis
from trimshamsa_d30 import d30_full_report
d30_result = d30_full_report(planet_lons)
report['modules']['trimshamsa_d30'] = d30_result
except Exception as e:
report['errors'].append(f"trimshamsa-d30: {e}")
try:
# Prashna(问事占星)—— full-reading 中暂不接入
# 原因:Prashna 需要问卜时刻(用户提问时间)排盘,full-reading 只有出生数据
# 如需 Prashna 分析:使用 cmd_prashna 独立命令
# 该命令接受问卜时间 + 地点,排 Prashna 盘并做分析
report['modules']['prashna'] = {
'note': 'Prashna 需要问卜时间排盘,请使用 cmd_prashna 独立命令。',
'cmd': 'cmd_prashna',
'available': True
}
except Exception as e:
report['errors'].append(f"prashna: {e}")
# ── Step 4.12: Solar Return (Varshaphala) + Muntha proper (v6.0.18) ──
try:
# Solar Return(太阳返照盘)计算
if hasattr(args, 'target_year') and args.target_year:
from solar_return import solar_return_full_report
sr_result = solar_return_full_report(
args.year, args.month, args.day,
args.hour, args.minute,
args.lat, args.lon, args.tz,
args.target_year
)
report['modules']['solar_return'] = sr_result
# 更新 muntha 为正确值(来自太阳返照盘)
if 'muntha' in sr_result:
report['modules']['muntha'] = {
'source': 'solar_return_proper',
'muntha_sign': sr_result.get('muntha', {}).get('muntha_sign', '?'),
'muntha_lord': sr_result.get('muntha', {}).get('muntha_lord', '?'),
'note': '来自太阳返照盘的正确 Muntha 计算',
}
else:
report['modules']['solar_return'] = {
'note': '未提供 --target-year,跳过太阳返照盘计算',
'skipped': True,
'hint': '使用 --target-year <年份> 重新运行以获取 Varshaphala 分析'
}
except Exception as e:
report['errors'].append(f"solar-return: {e}")
# ── Step 4.13: Narayana DashaRishi Dashav6.0.20)──
try:
from narayana_dasha import narayana_dasha_full_report
# 计算当前年龄(从出生到 today)
birth_dt = datetime(args.year, args.month, args.day, args.hour, args.minute)
today_dt = datetime.strptime(args.today, '%Y-%m-%d') if hasattr(args, 'today') and args.today else datetime.now()
current_age = (today_dt - birth_dt).days / 365.25
narayana_result = narayana_dasha_full_report(
lagna_sign_idx=asc_idx,
planet_lons=planet_lons,
current_age=current_age,
birth_year=args.year,
)
report['modules']['narayana_dasha'] = narayana_result
except Exception as e:
report['errors'].append(f"narayana-dasha: {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: 高级Nakshatrav6.0.22 升级:含 Chandra Bala + 综合报告)──
try:
from nakshatra_advanced import nakshatra_full_report as nakshatra_report
nak_full = nakshatra_report(chart)
report['modules']['nakshatra_advanced'] = nak_full
except Exception as e:
report['errors'].append(f"nakshatra-adv: {e}")
# ── Step 7.5: Nakshatra Dasha 星宿大运(v6.0.22 新增)──
try:
from nakshatra_dasha import nakshatra_dasha_full_report
birth_date_str = f"{args.year}-{args.month:02d}-{args.day:02d}"
# 年龄
nak_age = args.age
if nak_age is None:
try:
birth_date = datetime(args.year, args.month, args.day)
nak_age = (datetime.now() - birth_date).days / 365.25
except:
nak_age = None
if nak_age is not None:
# 获取过境星宿数据
transit_lons_for_nak = None
try:
transit_ref = getattr(args, 'transit_date', None) or datetime.now().strftime('%Y-%m-%d')
ty, tm, td = map(int, transit_ref.split('-'))
transit_jd_nak = swe.julday(ty, tm, td, 12.0 - args.tz)
transit_pl_nak, _ = _calc_sidereal_planets_for_jd(
transit_jd_nak, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True)
transit_lons_for_nak = {}
for pn, pd in transit_pl_nak.items():
if isinstance(pd, dict) and 'degree' in pd:
transit_lons_for_nak[pn] = pd['degree']
except:
pass
nak_dasha = nakshatra_dasha_full_report(
chart, birth_date_str, nak_age, transit_lons_for_nak)
report['modules']['nakshatra_dasha'] = nak_dasha
except Exception as e:
report['errors'].append(f"nakshatra-dasha: {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大运')
# Birth datetime args: required only when nakshatra/moon-lon not provided
p.add_argument('--year', type=int, required=False)
p.add_argument('--month', type=int, required=False)
p.add_argument('--day', type=int, required=False)
p.add_argument('--hour', type=int, required=False)
p.add_argument('--minute', type=int, required=False)
p.add_argument('--lat', type=float, required=False)
p.add_argument('--lon', type=float, required=False)
p.add_argument('--tz', type=float, default=0)
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'])
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', default=None)
p.add_argument('--today', default=None)
p.add_argument('--years', type=int, default=10)
# 3. yoga
p = sub.add_parser('yoga', help='Yoga格局识别')
_add_chart_args(p)
p.add_argument('--ascendant', default=None)
p.add_argument('--planets', default=None, help="格式: 'Sun:Aries:9[:10.5],Moon:Aquarius:7[:15.2],...';若提供则优先使用并忽略出生信息")
for action in p._actions:
if action.dest in ['year', 'month', 'day', 'hour', 'minute', 'lat', 'lon']:
action.required = False
# 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新增 → v6.0.22 升级)
p = sub.add_parser('nakshatra-adv', help='高级Nakshatra分析(Tara/Chandra/Sub-Lord/综合)')
_add_chart_args(p)
p.add_argument('--mode', default='all',
choices=['all','detail','tara','chandra','combined','sublord','full'],
help='分析模式')
# 18.5 nakshatra-dasha (v6.0.22新增)
p = sub.add_parser('nakshatra-dasha', help='星宿大运推演(Ashtottari / Vimshottari Nakshatra-level / Transit Overlay')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄(自动计算如果不提供)')
p.add_argument('--mode', default='all',
choices=['all','ashtottari','vimshottari','overlay'],
help='分析模式')
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DDoverlay/all模式,默认今天)')
# 18.6 nakshatra-full (v6.0.22新增)
p = sub.add_parser('nakshatra-full', help='综合星宿完整报告(本命 + 大运 + 过境)')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄')
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(默认今天)')
# 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.5 solar-return (v6.0.18新增)
p = sub.add_parser('solar-return', help='太阳返照盘 Varshaphala 年运分析')
_add_chart_args(p)
p.add_argument('--target-year', type=int, required=True, help='目标年份(计算该年太阳返照)')
# 21.6 narayana-dasha (v6.0.20新增)
p = sub.add_parser('narayana-dasha', help='Narayana DashaRishi Dasha)星座大运分析')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄(用于定位大运位置)')
# 21.7 muhurta (v6.0.21新增)
p = sub.add_parser('muhurta', help='Muhurta 择时分析(Panchanga 五要素)')
p.add_argument('--date', default=None, help='查询日期 YYYY-MM-DD(默认今天)')
p.add_argument('--activity', default=None,
choices=['marriage', 'business', 'travel', 'medical', 'education'],
help='指定活动类型(默认检查所有)')
p.add_argument('--scan-days', type=int, default=1, help='扫描天数(默认1天,最多30天)')
p.add_argument('--hour-from-sunrise', type=float, default=6.0,
help='从日出起算的小时数(默认6h约正午)')
# 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或今天)')
p.add_argument('--target-year', type=int, default=None, help='太阳返照盘目标年份(默认不计算 Varshaphala')
# 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, 'nakshatra-dasha': cmd_nakshatra_dasha,
'nakshatra-full': cmd_nakshatra_full, 'argala': cmd_argala, 'tajika': cmd_tajika,
'synastry': cmd_synastry, 'solar-return': cmd_solar_return,
'narayana-dasha': cmd_narayana_dasha, 'muhurta': cmd_muhurta,
'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()