Files
2026-06-24 13:37:33 +08:00

589 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ashtakavarga 计算模块 v2.1(八分法)
基于 Brihat Parashara Hora Shastra (BPHS) 完整标准表,并经 PVR/PyJHora 书例校准
核心修正(v2.0 vs v1.0):
- v1.0: BAV_BASE 只有自贡献 + 少量跨源贡献 → SAV=94 ❌
- v2.0: 完整 8×8 贡献矩阵(每颗行星 × 8来源)→ SAV=337 ✅
架构:
- BAV_CONTRIBUTION[planet][source] = 有利宫位列表
- 每颗行星的 BAV = 所有 8 个来源(7行星+Lagna)的贡献之和
- SAV = 7 颗行星 BAV 之和(Lagna 自身 BAV 单独展示,不计入 SAV)
- SAV 总和 = 337(宇宙常数)
PVR/PyJHora 书例校准(v2.1):
- 第六轮 benchmark 使用 PyJHora `pvr_tests.py` 中公开书例仲裁,修正 Moon/Venus 7 个贡献表项。
- 固定不变量保持:7行星 SAV=337,含 Lagna full SAV=386。
"""
from typing import Dict, List, Tuple
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
# ============================================================================
# 完整 BAV 贡献表(BPHS 标准法)
# BAV_CONTRIBUTION[planet][source] = 从 source 数起的有利宫位列表
# 每颗行星的 BAV 总和 = 所有 8 个来源的贡献数之和
# ============================================================================
# --- 太阳 BAV(总48 ---
# 来源: Sun(8) + Moon(4) + Mars(8) + Mercury(7) + Jupiter(4) + Venus(3) + Saturn(8) + Lagna(6) = 48
_SUN_BAV = {
'Sun': [1, 2, 4, 7, 8, 9, 10, 11],
'Moon': [3, 6, 10, 11],
'Mars': [1, 2, 4, 7, 8, 9, 10, 11],
'Mercury': [3, 5, 6, 9, 10, 11, 12],
'Jupiter': [5, 6, 9, 11],
'Venus': [6, 7, 12],
'Saturn': [1, 2, 4, 7, 8, 9, 10, 11],
'Lagna': [3, 4, 6, 10, 11, 12],
}
# --- 月亮 BAV(总49 ---
# 来源: Sun(6) + Moon(7) + Mars(6) + Mercury(8) + Jupiter(7) + Venus(7) + Saturn(4) + Lagna(4) = 49
# v2.1: PVR/PyJHora 书例校准 Moon/Mars/Jupiter/Lagna 贡献项。
_MOON_BAV = {
'Sun': [3, 6, 7, 8, 10, 11],
'Moon': [1, 3, 6, 7, 9, 10, 11],
'Mars': [2, 3, 5, 6, 10, 11],
'Mercury': [1, 3, 4, 5, 7, 8, 10, 11],
'Jupiter': [1, 2, 4, 7, 8, 10, 11],
'Venus': [3, 4, 5, 7, 9, 10, 11],
'Saturn': [3, 5, 6, 11],
'Lagna': [3, 6, 10, 11],
}
# --- 火星 BAV(总39 ---
# 来源: Sun(5) + Moon(3) + Mars(7) + Mercury(4) + Jupiter(4) + Venus(4) + Saturn(7) + Lagna(5) = 39
_MARS_BAV = {
'Sun': [3, 5, 6, 10, 11],
'Moon': [3, 6, 11],
'Mars': [1, 2, 4, 7, 8, 10, 11],
'Mercury': [3, 5, 6, 11],
'Jupiter': [6, 10, 11, 12],
'Venus': [6, 8, 11, 12],
'Saturn': [1, 4, 7, 8, 9, 10, 11],
'Lagna': [1, 3, 6, 10, 11],
}
# --- 水星 BAV(总54 ---
# 来源: Sun(5) + Moon(6) + Mars(8) + Mercury(8) + Jupiter(4) + Venus(8) + Saturn(8) + Lagna(7) = 54
_MERCURY_BAV = {
'Sun': [5, 6, 9, 11, 12],
'Moon': [2, 4, 6, 8, 10, 11],
'Mars': [1, 2, 4, 7, 8, 9, 10, 11],
'Mercury': [1, 3, 5, 6, 9, 10, 11, 12],
'Jupiter': [6, 8, 11, 12],
'Venus': [1, 2, 3, 4, 5, 8, 9, 11],
'Saturn': [1, 2, 4, 7, 8, 9, 10, 11],
'Lagna': [1, 2, 4, 6, 8, 10, 11],
}
# --- 木星 BAV(总56 ---
# 来源: Sun(9) + Moon(5) + Mars(7) + Mercury(8) + Jupiter(8) + Venus(6) + Saturn(4) + Lagna(9) = 56
_JUPITER_BAV = {
'Sun': [1, 2, 3, 4, 7, 8, 9, 10, 11],
'Moon': [2, 5, 7, 9, 11],
'Mars': [1, 2, 4, 7, 8, 10, 11],
'Mercury': [1, 2, 4, 5, 6, 9, 10, 11],
'Jupiter': [1, 2, 3, 4, 7, 8, 10, 11],
'Venus': [2, 5, 6, 9, 10, 11],
'Saturn': [3, 5, 6, 12],
'Lagna': [1, 2, 4, 5, 6, 7, 9, 10, 11],
}
# --- 金星 BAV(总52 ---
# 来源: Sun(3) + Moon(9) + Mars(6) + Mercury(5) + Jupiter(5) + Venus(9) + Saturn(7) + Lagna(8) = 52
# v2.1: PVR/PyJHora 书例校准 Mars/Mercury/Lagna 贡献项。
_VENUS_BAV = {
'Sun': [8, 11, 12],
'Moon': [1, 2, 3, 4, 5, 8, 9, 11, 12],
'Mars': [3, 4, 6, 9, 11, 12],
'Mercury': [3, 5, 6, 9, 11],
'Jupiter': [5, 8, 9, 10, 11],
'Venus': [1, 2, 3, 4, 5, 8, 9, 10, 11],
'Saturn': [3, 4, 5, 8, 9, 10, 11],
'Lagna': [1, 2, 3, 4, 5, 8, 9, 11],
}
# --- 土星 BAV(总39 ---
# 来源: Sun(7) + Moon(3) + Mars(6) + Mercury(6) + Jupiter(4) + Venus(3) + Saturn(4) + Lagna(6) = 39
_SATURN_BAV = {
'Sun': [1, 2, 4, 7, 8, 10, 11],
'Moon': [3, 6, 11],
'Mars': [3, 5, 6, 10, 11, 12],
'Mercury': [6, 8, 9, 10, 11, 12],
'Jupiter': [5, 6, 11, 12],
'Venus': [6, 11, 12],
'Saturn': [3, 5, 6, 11],
'Lagna': [1, 3, 4, 6, 10, 11],
}
# --- Lagna BAV(总49,不计入 SAV 337 ---
# 来源: Sun(6) + Moon(5) + Mars(5) + Mercury(7) + Jupiter(9) + Venus(7) + Saturn(6) + Lagna(4) = 49
_LAGNA_BAV = {
'Sun': [3, 4, 6, 10, 11, 12],
'Moon': [3, 6, 10, 11, 12],
'Mars': [1, 3, 6, 10, 11],
'Mercury': [1, 2, 4, 6, 8, 10, 11],
'Jupiter': [1, 2, 4, 5, 6, 7, 9, 10, 11],
'Venus': [1, 2, 3, 4, 5, 8, 9],
'Saturn': [1, 3, 4, 6, 10, 11],
'Lagna': [3, 6, 10, 11],
}
# 汇总为统一查找表
BAV_CONTRIBUTION = {
'Sun': _SUN_BAV,
'Moon': _MOON_BAV,
'Mars': _MARS_BAV,
'Mercury': _MERCURY_BAV,
'Jupiter': _JUPITER_BAV,
'Venus': _VENUS_BAV,
'Saturn': _SATURN_BAV,
'Lagna': _LAGNA_BAV,
}
# 各行星BAV固定总数(宇宙常数)
BAV_TOTALS = {
'Sun': 48, 'Moon': 49, 'Mars': 39, 'Mercury': 54,
'Jupiter': 56, 'Venus': 52, 'Saturn': 39, 'Lagna': 49
}
# 7颗行星 BAV 总和 = SAV 337
EXPECTED_SAV_TOTAL = 337
# Shodhya Pinda 行星权重
PLANET_WEIGHTS = {
'Sun': 5, 'Moon': 5, 'Mars': 8, 'Mercury': 5,
'Jupiter': 10, 'Venus': 7, 'Saturn': 5
}
# 行星列表
SEVEN_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
ALL_SOURCES = SEVEN_PLANETS + ['Lagna']
def _get_sign_idx(sign_name: str) -> int:
"""星座名称转索引"""
return SIGNS.index(sign_name) if sign_name in SIGNS else 0
def calc_ashtakavarga(planets: Dict, asc_sign_idx: int) -> Dict:
"""
计算完整 Ashtakavargav2.0 BPHS标准法)
Args:
planets: 行星数据 dict,每颗行星需要 {sign, degree, ...}
sign 是星座名称
asc_sign_idx: 上升星座在 SIGNS 中的索引 (0-11)
Returns:
完整的 Ashtakavarga 计算结果,含 BAV、SAV、校验
"""
# 1. 确定每个来源(7行星+Lagna)的星座索引
source_sign_idx = {}
for pname in SEVEN_PLANETS:
if pname in planets:
sign = planets[pname].get('sign', '')
if sign in SIGNS:
source_sign_idx[pname] = SIGNS.index(sign)
source_sign_idx['Lagna'] = asc_sign_idx
# 2. 为每颗行星计算 BAV
# BAV[planet][sign_i] = count of sources that contribute bindhu to sign_i
bav_results = {}
sav = [0] * 12 # 12星座的SAV(仅统计7行星贡献)
lagna_sav = [0] * 12 # Lagna单独的贡献
for planet in ALL_SOURCES:
contribution_rules = BAV_CONTRIBUTION[planet]
bav = [0] * 12 # 12星座的 bindhu 数
for source in ALL_SOURCES:
if source not in source_sign_idx:
continue
# 获取该 source 对该 planet BAV 的有利宫位
favorable_houses = contribution_rules.get(source, [])
source_idx = source_sign_idx[source]
# 从 source 所在星座开始,将有利宫位映射到实际星座
for house in favorable_houses:
target_sign = (source_idx + house - 1) % 12
bav[target_sign] += 1
bav_total = sum(bav)
expected = BAV_TOTALS.get(planet, 0)
bav_results[planet] = {
'bindus': bav,
'total': bav_total,
'expected_total': expected,
'valid': bav_total == expected,
'deviation': abs(bav_total - expected),
}
# 累加到 SAV(仅7行星)
if planet in SEVEN_PLANETS:
for i in range(12):
sav[i] += bav[i]
else:
# Lagna 的贡献单独记录
for i in range(12):
lagna_sav[i] += bav[i]
# 3. 校验 SAV 总分
sav_total = sum(sav)
# 4. SAV 评估
sav_assessment = []
for i in range(12):
score = sav[i]
if score >= 30:
level = "极吉"
elif score >= 28:
level = "吉利"
elif score >= 25:
level = "中等"
else:
level = "挑战"
sav_assessment.append({
'sign': SIGNS[i],
'score': score,
'level': level,
})
# 5. 含 Lagna 的完整 SAV386 = 337 + 49
full_sav = [sav[i] + lagna_sav[i] for i in range(12)]
# 6. Shodhya/Yoga Pinda 计算
yoga_pinda = calc_yoga_pinda(bav_results, planets, asc_sign_idx)
shodhya = {
pname: {
'rashi_pinda': row['rashi_pinda'],
'graha_pinda': row['graha_pinda'],
'total_pinda': row['yoga_pinda'],
'bindu_at_own_sign': row['bindu_at_own_sign'],
}
for pname, row in yoga_pinda.get('planets', {}).items()
}
# 7. 排名
ranked_sav = sorted(enumerate(sav_assessment), key=lambda x: x[1]['score'], reverse=True)
# 8. BAV 校验报告
bav_validation = []
all_valid = True
for planet in ALL_SOURCES:
r = bav_results[planet]
status = "" if r['valid'] else f"❌ 偏差{r['deviation']}"
if not r['valid']:
all_valid = False
bav_validation.append({
'planet': planet,
'actual': r['total'],
'expected': r['expected_total'],
'status': status,
})
return {
'method': 'Ashtakavarga八分法(BPHS/PVR书例校准v2.1',
'version': '2.1',
'bav': bav_results,
'sav': {
'scores': {SIGNS[i]: sav[i] for i in range(12)},
'full_scores_with_lagna': {SIGNS[i]: full_sav[i] for i in range(12)},
'assessment': sav_assessment,
'total': sav_total,
'expected_total': EXPECTED_SAV_TOTAL,
'valid': sav_total == EXPECTED_SAV_TOTAL,
'deviation': abs(sav_total - EXPECTED_SAV_TOTAL),
'full_total_with_lagna': sum(full_sav),
},
'bav_validation': bav_validation,
'all_bav_valid': all_valid,
'shodhya_pinda': shodhya,
'yoga_pinda': yoga_pinda,
'strongest_signs': [SIGNS[i] for i, _ in ranked_sav[:3]],
'weakest_signs': [SIGNS[i] for i, _ in ranked_sav[-3:]],
'house_scores': _map_to_houses(sav_assessment, asc_sign_idx),
'house_scores_full': _map_to_houses_values(full_sav, asc_sign_idx),
}
def _map_to_houses(sav_assessment: List[Dict], asc_sign_idx: int) -> Dict:
"""将 SAV 分数映射到宫位"""
houses = {}
for house_num in range(1, 13):
sign_idx = (asc_sign_idx + house_num - 1) % 12
score = sav_assessment[sign_idx]['score']
level = sav_assessment[sign_idx]['level']
houses[f"house_{house_num}"] = {
'sign': SIGNS[sign_idx],
'sav_score': score,
'level': level,
}
return houses
def _map_to_houses_values(scores: List[int], asc_sign_idx: int) -> Dict:
"""将含Lagna的完整SAV映射到宫位"""
houses = {}
for house_num in range(1, 13):
sign_idx = (asc_sign_idx + house_num - 1) % 12
houses[f"house_{house_num}"] = {
'sign': SIGNS[sign_idx],
'sav_score': scores[sign_idx],
}
return houses
def calc_prastara_av(planets: Dict, asc_sign_idx: int) -> Dict:
"""
计算 Prastara Ashtakavarga (PAV) — 展开式八分行星力量表
PAV 是一个 7×12×8 的三维矩阵:
- 7 颗行星 (Sun, Moon, Mars, Mercury, Jupiter, Venus, Saturn)
- 12 个宫位/星座
- 8 个贡献源 (7行星 + Lagna)
每格为 0 或 1,表示该贡献源是否对该行星的该宫位贡献 bindu。
Args:
planets: 行星数据 dict,每颗行星需要 {sign, degree, ...}
asc_sign_idx: 上升星座在 SIGNS 中的索引 (0-11)
Returns:
{
'pav': {planet: {source: [12个0/1值]}},
'pav_summary': {planet: {source: total_bindus}},
'validation': {...}
}
"""
# 1. 确定每个来源的星座索引
source_sign_idx = {}
for pname in SEVEN_PLANETS:
if pname in planets:
sign = planets[pname].get('sign', '')
if sign in SIGNS:
source_sign_idx[pname] = SIGNS.index(sign)
source_sign_idx['Lagna'] = asc_sign_idx
# 2. 计算 PAV 三维矩阵
pav = {}
pav_summary = {}
for planet in SEVEN_PLANETS:
contribution_rules = BAV_CONTRIBUTION[planet]
pav[planet] = {}
pav_summary[planet] = {}
for source in ALL_SOURCES:
if source not in source_sign_idx:
continue
# 12个宫位的bindu标记(0或1
bindu_markers = [0] * 12
favorable_houses = contribution_rules.get(source, [])
source_idx = source_sign_idx[source]
for house in favorable_houses:
target_sign = (source_idx + house - 1) % 12
bindu_markers[target_sign] = 1
pav[planet][source] = bindu_markers
pav_summary[planet][source] = sum(bindu_markers)
# 3. 验证:每行的总和应等于BAV对应值
validation = []
all_valid = True
for planet in SEVEN_PLANETS:
for source in ALL_SOURCES:
if source not in pav_summary.get(planet, {}):
continue
actual = pav_summary[planet][source]
# 从BAV_CONTRIBUTION查期望的贡献数(该source对planet的宫位数)
expected = len(BAV_CONTRIBUTION[planet].get(source, []))
valid = actual == expected
if not valid:
all_valid = False
validation.append({
'planet': planet,
'source': source,
'actual': actual,
'expected': expected,
'valid': valid,
})
return {
'method': 'Prastara Ashtakavarga (PAV)',
'version': '1.0',
'matrix_shape': {'planets': len(SEVEN_PLANETS), 'signs': 12, 'sources': len(ALL_SOURCES)},
'pav': pav,
'pav_summary': pav_summary,
'validation': validation,
'all_valid': all_valid,
}
def calc_yoga_pinda(bav_results: Dict, planets: Dict, asc_sign_idx: int) -> Dict:
"""
计算 Yoga Pinda / Shodhya Pinda 汇总。
本项目沿用既有 v2.1 口径:取行星本星座的 BAV bindu,分别乘以星座权重与行星权重;
两者相加作为 Yoga Pinda。该函数把原本嵌在 calc_ashtakavarga() 内部的结果提升为
一等契约,便于 API、前端与测试直接引用。
"""
source_sign_idx = {}
for pname in SEVEN_PLANETS:
if pname in planets:
sign = planets[pname].get('sign', '')
if sign in SIGNS:
source_sign_idx[pname] = SIGNS.index(sign)
source_sign_idx['Lagna'] = asc_sign_idx
rows = {}
all_valid = True
for pname in SEVEN_PLANETS:
bav_row = bav_results.get(pname, {})
bindus = bav_row.get('bindus', [])
own_sign = source_sign_idx.get(pname)
if own_sign is None or len(bindus) != 12:
all_valid = False
continue
bindu_at_own = bindus[own_sign]
sign_weight = 4
planet_weight = PLANET_WEIGHTS.get(pname, 5)
rashi_pinda = bindu_at_own * sign_weight
graha_pinda = bindu_at_own * planet_weight
rows[pname] = {
'sign': SIGNS[own_sign],
'bindu_at_own_sign': bindu_at_own,
'rashi_weight': sign_weight,
'planet_weight': planet_weight,
'rashi_pinda': rashi_pinda,
'graha_pinda': graha_pinda,
'yoga_pinda': rashi_pinda + graha_pinda,
}
ranked = sorted(rows.items(), key=lambda item: item[1]['yoga_pinda'], reverse=True)
strongest = ranked[0][0] if ranked else None
weakest = ranked[-1][0] if ranked else None
total = sum(item['yoga_pinda'] for item in rows.values())
return {
'method': 'Yoga Pinda',
'version': '1.0',
'planets': rows,
'summary': {
'strongest_planet': strongest,
'weakest_planet': weakest,
'total_yoga_pinda': total,
'average_yoga_pinda': round(total / len(rows), 2) if rows else 0,
},
'all_valid': all_valid and len(rows) == len(SEVEN_PLANETS),
'validation_note': 'Yoga Pinda = Rashi Pinda + Graha Pinda;当前复用既有 v2.1 Shodhya Pinda 权重口径。',
}
def calc_sodhita_av(bav_results: Dict, planets: Dict, asc_sign_idx: int) -> Dict:
"""
计算 Sodhita Ashtakavarga — 净化后的八分行星力量表
Sodhita 算法(BPHS标准):
从每颗行星的每个宫位BAV中,减去Saturn、Mars和Sun的贡献(若结果为负取0)。
这反映了"减去凶星影响后的纯净力量"
Args:
bav_results: calc_ashtakavarga返回的bav结果
planets: 行星数据 dict
asc_sign_idx: 上升星座索引
Returns:
{
'sodhita_bav': {planet: [12个sodhita值]},
'sodhita_sav': [12个sodhita SAV值],
'reduction_summary': {...}
}
"""
# 1. 获取PAV展开表(需要知道每个贡献源的单独贡献)
pav_data = calc_prastara_av(planets, asc_sign_idx)
pav = pav_data['pav']
# 2. 定义需要减去的"凶星"贡献源
malefic_sources = ['Saturn', 'Mars', 'Sun']
# 3. 计算Sodhita BAV
sodhita_bav = {}
total_reduction = {}
for planet in SEVEN_PLANETS:
if planet not in pav:
continue
# 原始BAV(从所有8个来源)
original_bav = [0] * 12
for source in ALL_SOURCES:
if source in pav.get(planet, {}):
for i in range(12):
original_bav[i] += pav[planet][source][i]
# 减去凶星贡献
reduction = [0] * 12
sodhita = [0] * 12
for i in range(12):
malefic_sum = 0
for source in malefic_sources:
if source in pav.get(planet, {}):
malefic_sum += pav[planet][source][i]
reduction[i] = malefic_sum
sodhita[i] = max(0, original_bav[i] - malefic_sum)
sodhita_bav[planet] = sodhita
total_reduction[planet] = sum(reduction)
# 4. 计算Sodhita SAV
sodhita_sav = [0] * 12
for planet in SEVEN_PLANETS:
if planet in sodhita_bav:
for i in range(12):
sodhita_sav[i] += sodhita_bav[planet][i]
# 5. 评估
sodhita_assessment = []
for i in range(12):
score = sodhita_sav[i]
if score >= 25:
level = "极吉"
elif score >= 22:
level = "吉利"
elif score >= 19:
level = "中等"
else:
level = "挑战"
sodhita_assessment.append({
'sign': SIGNS[i],
'score': score,
'level': level,
})
return {
'method': 'Sodhita Ashtakavarga (BPHS标准)',
'version': '1.0',
'sodhita_bav': sodhita_bav,
'sodhita_sav': {
'scores': {SIGNS[i]: sodhita_sav[i] for i in range(12)},
'assessment': sodhita_assessment,
'total': sum(sodhita_sav),
},
'reduction_summary': total_reduction,
'validation_note': 'Sodhita AV ≤ 原始BAV(逐宫位)',
}