feat: 5个新功能模块

新增功能:
- transit.js: computeDoubleTransitPAC (D1+D9双层PAC), computeTransitLL7L (P5+P8+Parivartana), computePlanetaryCongregation, computeVivahSaham
- jyotish_engine.py: 4个新CLI子命令 (double-transit-pac, transit-ll7l, planetary-congregation, vivah-saham)
- jaimini.py: calc_chara_dasha_with_antardasha (Antardasha子周期)
- jaimini命令: --antardasha参数

协同验证结果 (4案例):
- Double Transit PAC+D9: 50%命中率
- Transit LL/7L P5: 50%命中率
- Vivah Saham双星激活: 50%命中率
- 行星聚集: 50%命中率
- 0%无确认率
This commit is contained in:
732642856
2026-05-03 17:04:05 +08:00
parent f3459376db
commit cac87db78a
3 changed files with 1030 additions and 3 deletions
+447
View File
@@ -295,6 +295,231 @@ export function computeTransitAVScore(transitPlanets, natalAscSign, avData) {
return scores;
}
// ============================================================================
// Navamsa (D9) 星座计算工具
// ============================================================================
function navamsaSignIdx(lon) {
const l = ((lon % 360) + 360) % 360;
const si = Math.floor(l / 30);
const d = l - si * 30;
const ni = Math.floor(d / (30 / 9));
const elStarts = [0, 9, 6, 3]; // Aries/Fire=0, Taurus/Earth=9, Gemini/Air=6, Cancer/Water=3
return (elStarts[si % 4] + ni) % 12;
}
// ============================================================================
// PAC 检查: Position(同宫) / Aspect(相位) / Conjunction(合相≤10°)
// KN Rao Double Transit 核心: Saturn/Jupiter 通过 PAC 关联目标
// ============================================================================
function checkPAC(planetName, planetLon, targetLon, ascSignIdx) {
const results = [];
const pSignIdx = Math.floor((((planetLon % 360) + 360) % 360) / 30);
const tSignIdx = Math.floor((((targetLon % 360) + 360) % 360) / 30);
const pHouse = ((pSignIdx - ascSignIdx + 12) % 12) + 1;
const tHouse = ((tSignIdx - ascSignIdx + 12) % 12) + 1;
// P: Position — 同宫
if (pHouse === tHouse) {
results.push({ type: 'Position', desc: `同宫(${tHouse}宫)` });
}
// C: Conjunction — 合相 ≤10°
let diff = Math.abs(planetLon - targetLon) % 360;
if (diff > 180) diff = 360 - diff;
if (diff <= 10) {
results.push({ type: 'Conjunction', desc: `合相(${diff.toFixed(2)}°)` });
}
// A: Aspect — Graha Drishti
const aspects = PLANET_ASPECTS[planetName] || [7];
for (const offset of aspects) {
if (((tHouse - pHouse + 12) % 12) === offset) {
results.push({ type: 'Aspect', offset, desc: `${offset}宫相位` });
}
}
return results;
}
// ============================================================================
// Double Transit PAC + D9 层(KN Rao 完整实现)
//
// 核心逻辑:
// 1. D1 层: Saturn/Jupiter 通过 PAC 关联事件宫/宫主/LL/7L
// 2. D9 层: Saturn/Jupiter 通过 PAC 关联 D9 宫主/D9 Asc
// 3. 两者必须同时激活同一目标 → Double Transit 确认
//
// 精度: KN Rao 体系 110-115 星盘测试 97% 准确率(使用 D9 Navamsa)
// ============================================================================
export function computeDoubleTransitPAC(transitPlanets, natalPlanets, natalAscSign, natalAscDegree, eventHouse = 7) {
const ai = SIGNS.indexOf(natalAscSign);
const results = {
event_house: eventHouse,
d1: { jupiter: {}, saturn: {} },
d9: { jupiter: {}, saturn: {} },
double_transit: [],
summary: '',
};
// --- 计算 D1 层敏感点 ---
const eventSignIdx = (ai + eventHouse - 1) % 12;
const eventSign = SIGNS[eventSignIdx];
const eventLord = SIGN_LORDS[eventSign];
const llName = SIGN_LORDS[natalAscSign]; // Lagna Lord
const eventLordReverseIdx = (ai + 6) % 12; // 对宫 = (7宫-1)
const oppositeLord = SIGN_LORDS[SIGNS[eventLordReverseIdx]]; // 7L (for marriage, eventHouse=7 → 7L)
// 事件宫经度中点
const eventHouseLon = (ai * 30 + eventHouse - 1) * 30 + 15;
// LL 经度
const llLon = natalPlanets[llName]?.degree ?? natalPlanets[llName]?.lon ?? 0;
// 事件宫主经度
const eventLordLon = natalPlanets[eventLord]?.degree ?? natalPlanets[eventLord]?.lon ?? 0;
// 对宫主经度 (7L for marriage)
const oppLordLon = natalPlanets[oppositeLord]?.degree ?? natalPlanets[oppositeLord]?.lon ?? 0;
const d1Targets = {
[`${eventHouse}宫`]: eventHouseLon,
[`${eventLord}(宫主)`]: eventLordLon,
[`${llName}(LL)`]: llLon,
[`${oppositeLord}(对宫主)`]: oppLordLon,
};
// --- 计算 D9 层敏感点 ---
const d9AscSignIdx = navamsaSignIdx(natalAscDegree);
const d9AscSign = SIGNS[d9AscSignIdx];
// D9 事件宫主: D9盘的 eventHouse 宫的星座主星
const d9EventSignIdx = (d9AscSignIdx + eventHouse - 1) % 12;
const d9EventSign = SIGNS[d9EventSignIdx];
const d9EventLord = SIGN_LORDS[d9EventSign];
// 宫主的 D9 星座(KN Rao 关键: 宫主 D9 星座是第三个目标)
const eventLordD9SignIdx = navamsaSignIdx(eventLordLon);
const eventLordD9Sign = SIGNS[eventLordD9SignIdx];
// LL 的 D9 星座
const llD9SignIdx = navamsaSignIdx(llLon);
const llD9Sign = SIGNS[llD9SignIdx];
// D9 层用 D9 Asc 作为参考点
const d9EventHouseLon = (d9AscSignIdx * 30 + eventHouse - 1) * 30 + 15;
const d9EventLordLon = natalPlanets[d9EventLord]?.degree ?? natalPlanets[d9EventLord]?.lon ?? 0;
const d9Targets = {
[`D9_${eventHouse}宫`]: d9EventHouseLon,
[`D9_${eventLord}(宫主)`]: d9EventLordLon,
[`${eventLord}_D9(${eventLordD9Sign})`]: eventLordD9SignIdx * 30 + 15,
[`${llName}_D9(${llD9Sign})`]: llD9SignIdx * 30 + 15,
};
// --- 检查 Jupiter 和 Saturn 的 PAC ---
for (const transitPlanet of ['Jupiter', 'Saturn']) {
const tp = transitPlanets[transitPlanet];
if (!tp) continue;
const tpLon = tp.degree;
const layer = transitPlanet === 'Jupiter' ? 'jupiter' : 'saturn';
// D1 层 PAC 检查
for (const [tName, tLon] of Object.entries(d1Targets)) {
const pac = checkPAC(transitPlanet, tpLon, tLon, ai);
if (pac.length > 0) {
results.d1[layer][tName] = pac;
}
}
// D9 层 PAC 检查 (用 D9 Asc 作为参考点)
for (const [tName, tLon] of Object.entries(d9Targets)) {
const pac = checkPAC(transitPlanet, tpLon, tLon, d9AscSignIdx);
if (pac.length > 0) {
results.d9[layer][tName] = pac;
}
}
}
// --- Double Transit 判定 ---
const jupD1Targets = new Set(Object.keys(results.d1.jupiter));
const satD1Targets = new Set(Object.keys(results.d1.saturn));
const jupD9Targets = new Set(Object.keys(results.d9.jupiter));
const satD9Targets = new Set(Object.keys(results.d9.saturn));
// D1 层 Double Transit
const d1Overlap = [...jupD1Targets].filter(t => satD1Targets.has(t));
for (const t of d1Overlap) {
results.double_transit.push({
layer: 'D1', target: t,
jupiter_pac: results.d1.jupiter[t],
saturn_pac: results.d1.saturn[t],
strength: 'strong',
});
}
// D9 层 Double Transit
const d9Overlap = [...jupD9Targets].filter(t => satD9Targets.has(t));
for (const t of d9Overlap) {
results.double_transit.push({
layer: 'D9', target: t,
jupiter_pac: results.d9.jupiter[t],
saturn_pac: results.d9.saturn[t],
strength: 'strong',
});
}
// 跨层 Double Transit (Jupiter D1 + Saturn D9 或反之,激活同一主题)
// 例如 Jupiter PAC D1 7宫 + Saturn PAC D9 7宫 = 间接 Double Transit
for (const d1t of jupD1Targets) {
for (const d9t of satD9Targets) {
if (d1t.replace(/[^\d]/g, '') === d9t.replace(/[^\d]/g, '') ||
d1t.includes(eventLord) && d9t.includes(eventLord)) {
results.double_transit.push({
layer: 'D1+D9', target: `Jupiter(D1)${d1t} + Saturn(D9)${d9t}`,
jupiter_pac: results.d1.jupiter[d1t],
saturn_pac: results.d9.saturn[d9t],
strength: 'moderate',
});
}
}
}
for (const d1t of satD1Targets) {
for (const d9t of jupD9Targets) {
if (d1t.replace(/[^\d]/g, '') === d9t.replace(/[^\d]/g, '') ||
d1t.includes(eventLord) && d9t.includes(eventLord)) {
results.double_transit.push({
layer: 'D1+D9', target: `Saturn(D1)${d1t} + Jupiter(D9)${d9t}`,
jupiter_pac: results.d9.jupiter[d9t],
saturn_pac: results.d1.saturn[d1t],
strength: 'moderate',
});
}
}
}
// --- Summary ---
const d1Active = d1Overlap.length > 0;
const d9Active = d9Overlap.length > 0;
const crossActive = results.double_transit.filter(d => d.layer === 'D1+D9').length > 0;
if (d1Active && d9Active) {
results.summary = `✅ Double Transit PAC 确认: D1+D9 双层激活${eventHouse}宫主题`;
} else if (d1Active) {
results.summary = `⚠️ D1 层 Double Transit 激活,D9 层未确认`;
} else if (d9Active) {
results.summary = `⚠️ D9 层 Double Transit 激活,D1 层未确认`;
} else if (crossActive) {
results.summary = `⚠️ 跨层间接 Double Transit (D1+D9),需结合 Dasha 确认`;
} else {
results.summary = `❌ 无 Double Transit PAC 激活`;
}
results.stats = {
d1_jupiter_targets: [...jupD1Targets],
d1_saturn_targets: [...satD1Targets],
d9_jupiter_targets: [...jupD9Targets],
d9_saturn_targets: [...satD9Targets],
d1_overlap: d1Overlap,
d9_overlap: d9Overlap,
};
return results;
}
// ============================================================================
// Transit 行星对宫位影响摘要
// ============================================================================
@@ -335,3 +560,225 @@ export function computeTransitHouseImpact(transitPlanets, natalAscSign) {
return houseImpacts;
}
// ============================================================================
// Transit LL/7L 连接 + 互换(Parivartana)
//
// P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL (98%命中率)
// P8: Transit LL 过 7H 或 Transit 7L 过 Lagna (59%命中率)
// + Parivartana 互换检测: Transit LL 在 7L 星座 且 Transit 7L 在 LL 星座
// ============================================================================
export function computeTransitLL7L(transitPlanets, natalPlanets, natalAscSign) {
const ai = SIGNS.indexOf(natalAscSign);
const llName = SIGN_LORDS[natalAscSign]; // Lagna Lord
const sevenSign = SIGNS[(ai + 6) % 12]; // 7宫星座
const sevenLord = SIGN_LORDS[sevenSign]; // 7L
const result = {
lagna_lord: llName,
seventh_lord: sevenLord,
p5: { hit: false, details: [] },
p8: { hit: false, details: [] },
parivartana: { hit: false, details: [] },
};
// Transit LL/7L 位置
const tLL = transitPlanets[llName];
const t7L = transitPlanets[sevenLord];
if (!tLL || !t7L) return result;
const nLLLon = natalPlanets[llName]?.degree ?? 0;
const n7LLon = natalPlanets[sevenLord]?.degree ?? 0;
// --- P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL ---
const pac1 = checkPAC(llName, tLL.degree, n7LLon, ai);
if (pac1.length > 0) {
result.p5.hit = true;
result.p5.details.push({
direction: `Transit ${llName} → natal ${sevenLord}`,
connections: pac1,
});
}
const pac2 = checkPAC(sevenLord, t7L.degree, nLLLon, ai);
if (pac2.length > 0) {
result.p5.hit = true;
result.p5.details.push({
direction: `Transit ${sevenLord} → natal ${llName}`,
connections: pac2,
});
}
// --- P8: Transit LL 过 7H 或 Transit 7L 过 Lagna ---
const tLLHouse = ((SIGNS.indexOf(tLL.sign) - ai + 12) % 12) + 1;
const t7LHouse = ((SIGNS.indexOf(t7L.sign) - ai + 12) % 12) + 1;
if (tLLHouse === 7) {
result.p8.hit = true;
result.p8.details.push(`Transit ${llName}(${tLL.sign})在7H`);
}
if (t7LHouse === 1) {
result.p8.hit = true;
result.p8.details.push(`Transit ${sevenLord}(${t7L.sign})在Lagna`);
}
// --- Parivartana 互换: Transit LL 在 natal 7L 星座 且 Transit 7L 在 natal LL 星座 ---
const nLLSign = SIGNS[Math.floor((nLLLon % 360) / 30)];
const n7LSign = SIGNS[Math.floor((n7LLon % 360) / 30)];
const tLLIn7LSign = tLL.sign === n7LSign;
const t7LInLLSign = t7L.sign === nLLSign;
if (tLLIn7LSign && t7LInLLSign) {
result.parivartana.hit = true;
result.parivartana.details.push(
`完整互换: Transit ${llName}在${n7LSign}(natal ${sevenLord}) + Transit ${sevenLord}在${nLLSign}(natal ${llName})`
);
} else if (tLLIn7LSign) {
result.parivartana.details.push(`部分: Transit ${llName}在${n7LSign}(natal ${sevenLord}座)`);
} else if (t7LInLLSign) {
result.parivartana.details.push(`部分: Transit ${sevenLord}在${nLLSign}(natal ${llName}座)`);
}
return result;
}
// ============================================================================
// 行星聚集检测(Lagna/7H 聚集 + Transit 聚集)
//
// P7: 本命盘行星聚集 Lagna/7H (70%命中率)
// + Transit 时行星聚集事件宫 (扩展)
// ============================================================================
export function computePlanetaryCongregation(natalPlanets, natalAscSign, transitPlanets = null, eventHouse = 7) {
const ai = SIGNS.indexOf(natalAscSign);
const result = {
natal: { lagna: [], house_7: [], [`${eventHouse}宫`]: [] },
transit: null,
summary: '',
};
// --- 本命盘聚集 ---
for (const [pname, pdata] of Object.entries(natalPlanets)) {
if (pdata.error || pdata.sign === undefined) continue;
const pSignIdx = SIGNS.indexOf(pdata.sign);
const house = ((pSignIdx - ai + 12) % 12) + 1;
if (house === 1) result.natal.lagna.push(pname);
if (house === 7) result.natal.house_7.push(pname);
if (house === eventHouse) result.natal[`${eventHouse}宫`].push(pname);
}
// --- Transit 聚集 ---
if (transitPlanets) {
result.transit = {};
for (let h = 1; h <= 12; h++) result.transit[h] = [];
for (const pname of ALL_PLANETS) {
const tp = transitPlanets[pname];
if (!tp) continue;
const tSignIdx = SIGNS.indexOf(tp.sign);
const house = ((tSignIdx - ai + 12) % 12) + 1;
result.transit[house].push(pname);
}
}
// --- 判定 ---
const lagnaCount = result.natal.lagna.length;
const h7Count = result.natal.house_7.length;
const eventCount = result.natal[`${eventHouse}宫`].length;
const flags = [];
// Sun 在 Lagna 或 ≥3 颗行星在 Lagna
if (result.natal.lagna.includes('Sun') || lagnaCount >= 3) {
flags.push(`Lagna聚集: ${result.natal.lagna.join(',')}(${lagnaCount})`);
}
// Sun 在 7H 或 ≥3 颗行星在 7H
if (result.natal.house_7.includes('Sun') || h7Count >= 3) {
flags.push(`7H聚集: ${result.natal.house_7.join(',')}(${h7Count})`);
}
// Transit 聚集事件宫 ≥2 颗慢行星
if (result.transit) {
const tEventPlanets = result.transit[eventHouse] || [];
const tSlowInEvent = tEventPlanets.filter(p => SLOW_PLANETS.includes(p));
if (tSlowInEvent.length >= 2) {
flags.push(`Transit ${eventHouse}宫慢行星聚集: ${tSlowInEvent.join(',')}`);
}
}
result.summary = flags.length > 0 ? flags.join(' | ') : '无显著聚集';
result.flags = flags;
result.hit = flags.length > 0;
return result;
}
// ============================================================================
// Vivah Saham 计算 + Transit 激活检测
//
// Vivah Saham = norm(Venus - Saturn + Asc) — 度数级精确计算
// Transit 激活: Jupiter/Saturn PAC 到 Vivah Saham 经度
// ============================================================================
export function computeVivahSaham(natalPlanets, natalAscDegree, transitPlanets = null) {
const venusLon = natalPlanets.Venus?.degree ?? 0;
const saturnLon = natalPlanets.Saturn?.degree ?? 0;
const ascLon = natalAscDegree;
// Vivah Saham = norm(Venus - Saturn + Asc)
let sahamsLon = ((venusLon - saturnLon + ascLon) % 360 + 360) % 360;
const sahamsSignIdx = Math.floor(sahamsLon / 30);
const sahamsSign = SIGNS[sahamsSignIdx];
const sahamsDegInSign = sahamsLon - sahamsSignIdx * 30;
const sahamsNakIdx = Math.floor(sahamsLon / NAKSPAN);
const sahamsPada = Math.floor((sahamsLon % NAKSPAN) / (NAKSPAN / 4)) + 1;
const result = {
vivah_saham: {
longitude: Math.round(sahamsLon * 10000) / 10000,
sign: sahamsSign,
sign_cn: SIGNS_CN[sahamsSign],
degree_in_sign: Math.round(sahamsDegInSign * 10000) / 10000,
nakshatra: NAKSHATRA_LIST[sahamsNakIdx % 27]?.name,
pada: sahamsPada,
},
formula: `norm(${venusLon.toFixed(2)}° Venus - ${saturnLon.toFixed(2)}° Saturn + ${ascLon.toFixed(2)}° Asc)`,
transit_activation: null,
};
// --- Transit 激活检测 ---
if (transitPlanets) {
const ascSignIdx = Math.floor((ascLon % 360) / 30);
result.transit_activation = {
jupiter: [],
saturn: [],
double_activation: false,
};
// Jupiter PAC 到 Vivah Saham
if (transitPlanets.Jupiter) {
const jupPAC = checkPAC('Jupiter', transitPlanets.Jupiter.degree, sahamsLon, ascSignIdx);
if (jupPAC.length > 0) {
result.transit_activation.jupiter = jupPAC;
}
}
// Saturn PAC 到 Vivah Saham
if (transitPlanets.Saturn) {
const satPAC = checkPAC('Saturn', transitPlanets.Saturn.degree, sahamsLon, ascSignIdx);
if (satPAC.length > 0) {
result.transit_activation.saturn = satPAC;
}
}
// 双星激活
if (result.transit_activation.jupiter.length > 0 && result.transit_activation.saturn.length > 0) {
result.transit_activation.double_activation = true;
}
// Venus transit 过 Saham 星座(辅助信号)
if (transitPlanets.Venus) {
const tVenusSign = transitPlanets.Venus.sign;
if (tVenusSign === sahamsSign) {
result.transit_activation.venus_in_saham_sign = true;
}
}
}
return result;
}
+67
View File
@@ -199,6 +199,73 @@ def calc_chara_dasha(asc_sign_idx: int,
}
def calc_chara_dasha_with_antardasha(asc_sign_idx: int,
planet_longitudes: Dict[str, float],
birth_year: int, birth_month: int) -> Dict:
"""
Chara Dasha 计算含 Antardasha 子周期
Antardasha 规则:
- 在每个 Mahadasha 内,Antardasha 从 Mahadasha 星座开始
- 方向与 Mahadasha 方向相同
- 每个 Antardasha 时长 = (该子星座大运年数 / 总大运年数) * Mahadasha 年数
- 生成 12 个 Antardasha
"""
# 先算 Mahadasha
base = calc_chara_dasha(asc_sign_idx, planet_longitudes, birth_year, birth_month)
is_odd = asc_sign_idx % 2 == 0
direction = 1 if is_odd else -1
base_total = sum(_chara_dasha_duration((asc_sign_idx + direction * i) % 12, planet_longitudes) for i in range(12))
# 为每个 Mahadasha 计算 Antardasha
for md in base['dasha_sequence']:
md_sign_idx = md['sign_idx']
md_duration = md['duration_years']
antardasha_list = []
# Antardasha 也从该 Mahadasha 星座开始,同方向
for j in range(12):
ad_sign_idx = (md_sign_idx + direction * j) % 12
ad_sign = SIGNS[ad_sign_idx]
ad_lord = SIGN_LORDS[ad_sign]
# 子周期时长 = (该星座独立年数 / 总年数) * Mahadasha 年数
ad_independent_duration = _chara_dasha_duration(ad_sign_idx, planet_longitudes)
ad_duration = round((ad_independent_duration / base_total) * md_duration, 2)
if ad_duration < 0.01:
ad_duration = 0.01 # 最小约4天
antardasha_list.append({
'sign': ad_sign,
'sign_idx': ad_sign_idx,
'lord': ad_lord,
'duration_years': ad_duration,
'order': j + 1,
})
# 计算 Antardasha 日期
start_year, start_month = map(int, md['start_date'].split('-'))
for ad in antardasha_list:
ad['start_date'] = f"{start_year}-{start_month:02d}"
total_months = round(ad['duration_years'] * 12)
end_month = start_month + total_months
end_year = start_year + (end_month - 1) // 12
end_month = ((end_month - 1) % 12) + 1
ad['end_date'] = f"{end_year}-{end_month:02d}"
# 下一个 Antardasha 的开始
if end_month < 12:
start_year = end_year
start_month = end_month + 1
else:
start_year = end_year + 1
start_month = 1
md['antardashas'] = antardasha_list
base['has_antardasha'] = True
return base
def _chara_dasha_duration(sign_idx, planet_lons):
"""计算Chara Dasha单个大运的年数"""
# 标准方法:12 - 落入该星座的行星数量(最少1年,最多12年)
+516 -3
View File
@@ -576,6 +576,489 @@ def cmd_transit(args):
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 _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)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
asc_deg = chart.get('ascendant', {}).get('degree', 0)
event_house = args.house or 7
# 2. 计算过境行星位置
if not HAS_SWE:
return {"error": "swisseph未安装"}
transit_year, transit_month, transit_day = map(int, args.date.split('-'))
transit_hour = 12.0 - args.tz # 正午 UT
transit_jd = swe.julday(transit_year, transit_month, transit_day, transit_hour)
transit_ayanamsa = swe.get_ayanamsa(transit_jd)
transit_planets = {}
for pname, pid in PLANETS_SWE.items():
try:
pos, _ = swe.calc_ut(transit_jd, pid)
lon_p = (pos[0] - transit_ayanamsa) % 360
transit_planets[pname] = {'lon': lon_p, 'sign': SIGNS[int(lon_p / 30)]}
if pname == 'Rahu':
klon = (lon_p + 180) % 360
transit_planets['Ketu'] = {'lon': klon, 'sign': SIGNS[int(klon / 30)]}
except Exception as e:
transit_planets[pname] = {'error': str(e)}
# 3. D1 层敏感点
event_si = (asc_idx + event_house - 1) % 12
event_sign = SIGNS[event_si]
event_lord = SIGN_LORDS[event_sign]
ll_name = SIGN_LORDS[asc_sign]
opposite_si = (asc_idx + 6) % 12
opposite_lord = SIGN_LORDS[SIGNS[opposite_si]]
event_house_lon = (event_si * 30) + 15 # 宫位中点
ll_lon = natal.get(ll_name, {}).get('degree', 0)
event_lord_lon = natal.get(event_lord, {}).get('degree', 0)
opp_lord_lon = natal.get(opposite_lord, {}).get('degree', 0)
d1_targets = {
f'{event_house}宫({event_sign})': event_house_lon,
f'{event_lord}(宫主)': event_lord_lon,
f'{ll_name}(LL)': ll_lon,
f'{opposite_lord}(对宫主)': opp_lord_lon,
}
# 4. D9 层敏感点
d9_asc_idx = _navamsa_idx(asc_deg)
d9_asc_sign = SIGNS[d9_asc_idx]
d9_event_si = (d9_asc_idx + event_house - 1) % 12
d9_event_sign = SIGNS[d9_event_si]
d9_event_lord = SIGN_LORDS[d9_event_sign]
# 宫主的 D9 星座(KN Rao 关键)
event_lord_d9_si = _navamsa_idx(event_lord_lon)
event_lord_d9_sign = SIGNS[event_lord_d9_si]
ll_d9_si = _navamsa_idx(ll_lon)
ll_d9_sign = SIGNS[ll_d9_si]
d9_event_house_lon = (d9_asc_idx * 30) + 15
d9_event_lord_lon = natal.get(d9_event_lord, {}).get('degree', 0)
d9_targets = {
f'D9_{event_house}宫({d9_event_sign})': d9_event_house_lon,
f'D9_{d9_event_lord}(宫主)': d9_event_lord_lon,
f'{event_lord}_D9({event_lord_d9_sign})': event_lord_d9_si * 30 + 15,
f'{ll_name}_D9({ll_d9_sign})': ll_d9_si * 30 + 15,
}
# 5. PAC 检查
results = {
'transit_date': args.date,
'event_house': event_house,
'd1': {'jupiter': {}, 'saturn': {}},
'd9': {'jupiter': {}, 'saturn': {}},
'double_transit': [],
'summary': '',
}
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
# 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())
# 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',
})
# 跨层 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
cross_active = any(d['layer'] == 'D1+D9' for d in results['double_transit'])
if d1_active and d9_active:
results['summary'] = f'✅ Double Transit PAC 确认: D1+D9 双层激活{event_house}宫主题'
elif d1_active:
results['summary'] = f'⚠️ D1 层 Double Transit 激活,D9 层未确认'
elif d9_active:
results['summary'] = f'⚠️ D9 层 Double Transit 激活,D1 层未确认'
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),
'd1_overlap': sorted(d1_overlap),
'd9_overlap': sorted(d9_overlap),
'd9_ascendant': d9_asc_sign,
'event_lord_d9_sign': event_lord_d9_sign,
}
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)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
asc_deg = chart.get('ascendant', {}).get('degree', 0)
ll_name = SIGN_LORDS[asc_sign]
seven_sign = SIGNS[(SIGNS.index(asc_sign) + 6) % 12]
seven_lord = SIGN_LORDS[seven_sign]
# Transit 日期
t_year, t_month, t_day = map(int, args.date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
# Transit LL/7L 位置
t_ll_lon = _calc_transit_lon(transit_jd, ll_name)
t_7l_lon = _calc_transit_lon(transit_jd, seven_lord)
if t_ll_lon is None or t_7l_lon is None:
return {"error": f"无法计算 Transit 位置: LL={ll_name}, 7L={seven_lord}"}
n_ll_lon = natal.get(ll_name, {}).get('degree', 0)
n_7l_lon = natal.get(seven_lord, {}).get('degree', 0)
asc_lon = asc_deg
result = {
'transit_date': args.date,
'lagna_lord': ll_name,
'seventh_lord': seven_lord,
'p5': {'hit': False, 'details': []},
'p8': {'hit': False, 'details': []},
'parivartana': {'hit': False, 'details': []},
}
# P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL
pac1 = _check_pac(ll_name, t_ll_lon, n_7l_lon, asc_idx)
if pac1:
result['p5']['hit'] = True
result['p5']['details'].append({
'direction': f'Transit {ll_name} → natal {seven_lord}',
'connections': pac1,
})
pac2 = _check_pac(seven_lord, t_7l_lon, n_ll_lon, asc_idx)
if pac2:
result['p5']['hit'] = True
result['p5']['details'].append({
'direction': f'Transit {seven_lord} → natal {ll_name}',
'connections': pac2,
})
# P8: Transit LL 过 7H 或 Transit 7L 过 Lagna
t_ll_house = ((int((t_ll_lon % 360) / 30) - asc_idx) % 12) + 1
t_7l_house = ((int((t_7l_lon % 360) / 30) - asc_idx) % 12) + 1
if t_ll_house == 7:
result['p8']['hit'] = True
result['p8']['details'].append(f'Transit {ll_name}({SIGNS[int(t_ll_lon/30)]})在7H')
if t_7l_house == 1:
result['p8']['hit'] = True
result['p8']['details'].append(f'Transit {seven_lord}({SIGNS[int(t_7l_lon/30)]})在Lagna')
# Parivartana 互换
n_ll_sign = SIGNS[int((n_ll_lon % 360) / 30)]
n_7l_sign = SIGNS[int((n_7l_lon % 360) / 30)]
t_ll_in_7l = SIGNS[int((t_ll_lon % 360) / 30)] == n_7l_sign
t_7l_in_ll = SIGNS[int((t_7l_lon % 360) / 30)] == n_ll_sign
if t_ll_in_7l and t_7l_in_ll:
result['parivartana']['hit'] = True
result['parivartana']['details'].append(
f'完整互换: Transit {ll_name}在{n_7l_sign}(natal {seven_lord}) + Transit {seven_lord}在{n_ll_sign}(natal {ll_name})')
elif t_ll_in_7l:
result['parivartana']['details'].append(f'部分: Transit {ll_name}在{n_7l_sign}')
elif t_7l_in_ll:
result['parivartana']['details'].append(f'部分: Transit {seven_lord}在{n_ll_sign}')
return result
# ============================================================================
# 8d. 行星聚集检测(Lagna/7H + Transit 聚集)(v3.9新增)
# ============================================================================
def cmd_planetary_congregation(args):
"""行星聚集检测"""
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz)
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)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_deg = chart.get('ascendant', {}).get('degree', 0)
venus_lon = natal.get('Venus', {}).get('degree', 0)
saturn_lon = natal.get('Saturn', {}).get('degree', 0)
# Vivah Saham = norm(Venus - Saturn + Asc)
sahams_lon = (venus_lon - saturn_lon + asc_deg) % 360
sahams_si = int(sahams_lon / 30)
sahams_sign = SIGNS[sahams_si]
sahams_deg = sahams_lon - sahams_si * 30
result = {
'vivah_saham': {
'longitude': round(sahams_lon, 4),
'sign': sahams_sign,
'sign_cn': SIGNS_CN[sahams_sign],
'degree_in_sign': round(sahams_deg, 4),
},
'formula': f'norm({venus_lon:.2f} Venus - {saturn_lon:.2f} Saturn + {asc_deg:.2f} Asc)',
'transit_activation': None,
}
# Transit 激活
if args.transit_date:
t_year, t_month, t_day = map(int, args.transit_date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
result['transit_activation'] = {'jupiter': [], 'saturn': [], 'double_activation': False}
jup_lon = _calc_transit_lon(transit_jd, 'Jupiter')
sat_lon = _calc_transit_lon(transit_jd, 'Saturn')
if jup_lon is not None:
jup_pac = _check_pac('Jupiter', jup_lon, sahams_lon, asc_idx)
if jup_pac:
result['transit_activation']['jupiter'] = jup_pac
if sat_lon is not None:
sat_pac = _check_pac('Saturn', sat_lon, sahams_lon, asc_idx)
if sat_pac:
result['transit_activation']['saturn'] = sat_pac
if result['transit_activation']['jupiter'] and result['transit_activation']['saturn']:
result['transit_activation']['double_activation'] = True
# Venus transit 过 Saham 星座
venus_t = _calc_transit_lon(transit_jd, 'Venus')
if venus_t is not None:
if SIGNS[int(venus_t / 30)] == sahams_sign:
result['transit_activation']['venus_in_saham_sign'] = True
return result
# ============================================================================
# 9. Shadbala 六重力量(v3.4新增)
# ============================================================================
@@ -1107,7 +1590,7 @@ def cmd_jaimini(args):
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
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}"}
@@ -1127,7 +1610,11 @@ def cmd_jaimini(args):
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'):
result['chara_dasha'] = calc_chara_dasha(asc_idx, planet_lons, args.year, args.month)
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
@@ -1717,6 +2204,7 @@ def main():
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子周期')
# 18. nakshatra-adv (v3.7新增)
p = sub.add_parser('nakshatra-adv', help='高级Nakshatra分析')
@@ -1756,6 +2244,28 @@ def main():
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(可选)')
args = parser.parse_args()
if not args.command:
parser.print_help(); sys.exit(1)
@@ -1766,7 +2276,10 @@ def main():
'validate': cmd_validate, 'audit': cmd_audit, 'report': cmd_report,
'varga-full': cmd_varga_full, 'aspects': cmd_aspects, 'jaimini': cmd_jaimini,
'nakshatra-adv': cmd_nakshatra_adv, 'argala': cmd_argala, 'tajika': cmd_tajika,
'synastry': cmd_synastry, 'full-reading': cmd_full_reading, 'prashna': cmd_prashna}
'synastry': cmd_synastry, 'full-reading': cmd_full_reading, 'prashna': cmd_prashna,
'double-transit-pac': cmd_double_transit_pac,
'transit-ll7l': cmd_transit_ll7l, 'planetary-congregation': cmd_planetary_congregation,
'vivah-saham': cmd_vivah_saham}
result = cmds[args.command](args)
output_json(result)