Upstream 30b43643. Yoga Pinda multiplied only the bindu in the planet's own sign. It now uses the Trikona and Ekadhipatya rows, with PVR table 28 including Virgo 6. PVR example 43 is 152. BUG-1384. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
608 lines
22 KiB
Python
608 lines
22 KiB
Python
#!/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
|
||
}
|
||
# PVR §12.7 表 28。处女座是 6。不改用 Prasna Marga 的 5。
|
||
RASHI_PINDA_WEIGHTS = (7, 10, 8, 4, 10, 6, 7, 8, 9, 5, 11, 12)
|
||
|
||
# 行星列表
|
||
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:
|
||
"""
|
||
计算完整 Ashtakavarga(v2.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 的完整 SAV(386 = 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 from the same Trikona and Ekadhipatya rows as the Sodhita table.
|
||
|
||
PVR §12.7 tables 28–29: twelve reduced signs times rashi weights (Virgo 6),
|
||
plus the seven planets' weights at the signs they occupy. A full seven-row
|
||
chart reuses ``sodhita_chain`` so the report cannot show two reductions.
|
||
"""
|
||
try:
|
||
from ashtakavarga_shodhana import occupied_signs, sodhita_chain
|
||
except ImportError:
|
||
from scripts.ashtakavarga_shodhana import occupied_signs, sodhita_chain
|
||
|
||
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
|
||
note = (
|
||
'PVR §12.7 表 28–29:十二座削减值乘星座权重(处女座 6),'
|
||
'再加七曜所在座的行星权重。与 Sodhita 表同一份削减。'
|
||
'同值且一占一空时按 Raman 15(c) 清空空宫。'
|
||
)
|
||
if any(pname not in source_sign_idx for pname in SEVEN_PLANETS):
|
||
return {
|
||
'method': 'Yoga Pinda', 'version': '2.0',
|
||
'profile': 'pvr_trikona_ekadhipatya_tables28_29',
|
||
'planets': {}, 'summary': {}, 'all_valid': False,
|
||
'validation_note': note + ' 七曜星座不全,不算 Graha Pinda。',
|
||
}
|
||
|
||
available = {}
|
||
for pname, row in bav_results.items():
|
||
bindus = row.get('bindus') if isinstance(row, dict) else None
|
||
if isinstance(bindus, list) and len(bindus) == 12:
|
||
available[pname] = {'bindus': bindus}
|
||
if set(SEVEN_PLANETS).issubset(available):
|
||
chain = sodhita_chain(available, planets)
|
||
trikona_rows = chain['trikona_bav']
|
||
reduced_rows = chain['sodhita_bav']
|
||
else:
|
||
trikona_rows = {
|
||
pname: calc_trikona_shodhana(row['bindus'])['reduced_scores']
|
||
for pname, row in available.items()
|
||
}
|
||
reduced_rows = calc_ekadhipatya_shodhana(trikona_rows, occupied_signs(planets))['rows']
|
||
|
||
rows = {}
|
||
all_valid = True
|
||
for pname in SEVEN_PLANETS:
|
||
reduced = reduced_rows.get(pname)
|
||
own_sign = source_sign_idx.get(pname)
|
||
bindus = (available.get(pname) or {}).get('bindus')
|
||
if reduced is None or own_sign is None or bindus is None:
|
||
all_valid = False
|
||
continue
|
||
rashi_pinda = sum(value * weight for value, weight in zip(reduced, RASHI_PINDA_WEIGHTS))
|
||
graha_pinda = sum(
|
||
reduced[source_sign_idx[body]] * PLANET_WEIGHTS[body] for body in SEVEN_PLANETS
|
||
)
|
||
rows[pname] = {
|
||
'sign': SIGNS[own_sign],
|
||
'bindu_at_own_sign': bindus[own_sign],
|
||
'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': '2.0',
|
||
'profile': 'pvr_trikona_ekadhipatya_tables28_29',
|
||
'rashi_weights': list(RASHI_PINDA_WEIGHTS),
|
||
'planet_weights': dict(PLANET_WEIGHTS),
|
||
'trikona_rows': trikona_rows,
|
||
'sodhita_rows': reduced_rows,
|
||
'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': note,
|
||
}
|
||
|
||
|
||
# calc_sodhita_av (removed, BUG-1209): it subtracted the Sun, Mars and Saturn
|
||
# contributions from every BAV and was labelled "BPHS标准"; that is not the BPHS
|
||
# Sodhita. The standard chain (Trikona -> Ekadhipatya per BAV row) is
|
||
# scripts/ashtakavarga_shodhana.py, built on the two functions below.
|
||
|
||
|
||
TRIKONA_SIGN_GROUPS = (
|
||
(0, 4, 8), # Aries, Leo, Sagittarius
|
||
(1, 5, 9), # Taurus, Virgo, Capricorn
|
||
(2, 6, 10), # Gemini, Libra, Aquarius
|
||
(3, 7, 11), # Cancer, Scorpio, Pisces
|
||
)
|
||
|
||
|
||
def calc_trikona_shodhana(sav_scores: List[int]) -> Dict:
|
||
"""Apply the classical Trikona Shodhana reduction to a 12-sign SAV row.
|
||
|
||
The minimum value in each elemental trine is removed from all three signs;
|
||
the original row remains in the result for auditability.
|
||
"""
|
||
if len(sav_scores) != 12:
|
||
raise ValueError('SAV scores must contain exactly 12 signs')
|
||
original = [int(value) for value in sav_scores]
|
||
reduced = original[:]
|
||
groups = []
|
||
for indexes in TRIKONA_SIGN_GROUPS:
|
||
baseline = min(original[index] for index in indexes)
|
||
for index in indexes:
|
||
reduced[index] -= baseline
|
||
groups.append({'sign_indexes': list(indexes), 'baseline_removed': baseline})
|
||
return {
|
||
'method': 'Trikona Shodhana',
|
||
'original_scores': original,
|
||
'reduced_scores': reduced,
|
||
'trikona_groups': groups,
|
||
'status': 'executed',
|
||
'claim_boundary': 'Standalone classical reduction; Ekadhipatya Shodhana remains a separate layer.',
|
||
}
|
||
|
||
|
||
EKADHIPATYA_SIGN_PAIRS = ((0, 7), (1, 6), (2, 5), (8, 11), (9, 10))
|
||
|
||
|
||
def calc_ekadhipatya_shodhana(
|
||
rows: Dict[str, List[int]], occupied_signs: set[int]
|
||
) -> Dict:
|
||
"""Apply PyJHora/PVR Ekadhipatya Shodhana to post-Trikona rows."""
|
||
reduced = {planet: list(values) for planet, values in rows.items()}
|
||
audit = []
|
||
for planet, values in reduced.items():
|
||
if len(values) != 12:
|
||
raise ValueError('each BAV row must contain exactly 12 signs')
|
||
for first, second in EKADHIPATYA_SIGN_PAIRS:
|
||
first_value, second_value = values[first], values[second]
|
||
if first_value == 0 or second_value == 0:
|
||
continue
|
||
first_occupied = first in occupied_signs
|
||
second_occupied = second in occupied_signs
|
||
before = [first_value, second_value]
|
||
if first_occupied and second_occupied:
|
||
continue
|
||
if first_occupied != second_occupied:
|
||
occupied, vacant = (first, second) if first_occupied else (second, first)
|
||
values[vacant] = values[occupied] if values[occupied] < values[vacant] else 0
|
||
elif first_value == second_value:
|
||
values[first] = values[second] = 0
|
||
elif first_value > second_value:
|
||
values[first] = second_value
|
||
else:
|
||
values[second] = first_value
|
||
audit.append({'planet': planet, 'pair': [first, second], 'before': before,
|
||
'after': [values[first], values[second]]})
|
||
return {
|
||
'method': 'Ekadhipatya Shodhana',
|
||
'rows': reduced,
|
||
'occupied_signs': sorted(occupied_signs),
|
||
'audit': audit,
|
||
'status': 'executed',
|
||
'claim_boundary': 'Applied after Trikona Shodhana. One occupied and one vacant sign with the same value clears the vacant sign (Raman 15(c)).',
|
||
}
|