diff --git a/jyotish-app/transit.js b/jyotish-app/transit.js index a6d2cfc7..8c582478 100644 --- a/jyotish-app/transit.js +++ b/jyotish-app/transit.js @@ -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; +} diff --git a/scripts/jaimini.py b/scripts/jaimini.py index a006fdb7..90ae8cc5 100644 --- a/scripts/jaimini.py +++ b/scripts/jaimini.py @@ -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年) diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index c4737df8..f6215dc4 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -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)