v6.2.0: 全面技法宝库升级 — 16个新模块 + 12个文件优化

## 新增模块 (16个)
### P0 精度修复
- ashtakavarga: calc_prastara_av() + calc_sodhita_av()
- kakshya.py: Kakshya评分系统 (8区间×3.75°)
- shadbala.py: Sputa Drishti + Yuddha Bala

### P1 核心升级
- bhava_bala.py: 宫位三元力量 (jyotishganit MIT)
- pancha_mahapurusha.py: PMC完整检测含4层失效条件
- sade_sati.py: Sade Sati+Kantaka Shani
- sudarshana_chakra.py: 三参考点盘+收敛分析
- tajika.py: Sahams 7→36 + Tajika Yogas 10种
- birth_time_rectifier.py: 生时矫正

### P2 覆盖扩展
- kp_system.py: KP Sublord+ABCD Significator (diliprk/VedicAstro MIT)
- synastry.py: 16因子合盘36分制 (dashaflow MIT)
- muhurtha_election.py: 6活动选举 (dashaflow MIT)
- career_analysis.py: 结构化事业引擎
- relationship_analysis.py: 结构化感情引擎
- conditional_dashas.py: Dwisaptati+Shattrimsa+Dwadashottari
- divisional_charts_extended: D81/D108/D144
- remedies.py: 5类补救系统

## 修改文件
jaimini/dasha_calculator/shadbala/SKILL.md/COVERAGE_AUDIT等12个

## 开源复用: 4个MIT项目
This commit is contained in:
732642856
2026-06-11 19:03:21 +08:00
parent c4c0eff67a
commit a4f8620a71
210 changed files with 49022 additions and 828 deletions
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Shadbala Benchmark v1.0
比较 v6.1.13 yinduzhanxing Shadbala 与 PyJHora 实现。
依赖: pip install jhora numpy
"""
import sys, json, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'scripts')))
from shadbala import calc_shadbala, NAISARGIKA_BALA
# PyJHora
from jhora import const, utils
from jhora.panchanga import drik
from jhora.horoscope.chart import charts
from jhora.horoscope.strength import shadbala as pj_shadbala
TEST_CASES = [
{"id": "beijing_1990_noon", "year":1990, "month":6, "day":15, "hour":12, "minute":0, "lat":39.9, "lon":116.4, "tz":8},
{"id": "newyork_1985_morning", "year":1985, "month":3, "day":22, "hour":9, "minute":30, "lat":40.7, "lon":-74.0, "tz":-5},
{"id": "london_1970_evening", "year":1970, "month":9, "day":12, "hour":19, "minute":0, "lat":51.5, "lon":-0.1, "tz":0},
{"id": "delhi_2000_midnight", "year":2000, "month":1, "day":1, "hour":0, "minute":0, "lat":28.6, "lon":77.2, "tz":5.5},
{"id": "sydney_1999_afternoon", "year":1999, "month":8, "day":7, "hour":15, "minute":45, "lat":-33.9, "lon":151.2, "tz":10},
]
PLANET_NAMES = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
def run_benchmark():
total_planets = 0
within_10pct = 0
within_20pct = 0
details = []
for case in TEST_CASES:
dob = drik.Date(case["year"], case["month"], case["day"])
tob = (case["hour"], case["minute"], 0)
place = drik.Place(f'test_{case["id"]}', case["lat"], case["lon"], case["tz"])
jd = utils.julian_day_number(dob, tob)
pp = charts.divisional_chart(jd, place, divisional_chart_factor=1)[:const._pp_count_upto_ketu]
# 提取行星位置
asc_sign_idx = pp[0][1][0]
sun_sign = pp[const.SUN_ID + 1][1][0]
moon_sign = pp[const.MOON_ID + 1][1][1]
sun_lon = sun_sign * 30 + pp[const.SUN_ID + 1][1][1]
moon_lon = moon_sign
planets = {}
for pid, pname in {const.SUN_ID: 'Sun', const.MOON_ID: 'Moon',
const.MARS_ID: 'Mars', const.MERCURY_ID: 'Mercury',
const.JUPITER_ID: 'Jupiter', const.VENUS_ID: 'Venus',
const.SATURN_ID: 'Saturn'}.items():
sign = pp[pid + 1][1][0]
deg = pp[pid + 1][1][1]
house = (sign - asc_sign_idx) % 12 + 1
retro = pp[pid + 1][4] # retrograde flag in PyJHora
speed = pp[pid + 1][3] # speed in degrees/day
planets[pname] = {
'sign': SIGNS[sign],
'degree': sign * 30 + deg,
'house': house,
'retrograde': retro,
'speed': speed,
}
# 我们的Shadbala
our = calc_shadbala(planets, SIGNS[asc_sign_idx],
case["hour"] + case["minute"] / 60,
sun_lon, moon_lon, case["minute"])
# PyJHora Shadbala
try:
pj = pj_shadbala.get_shadbala_scores(jd, place, divisional_chart_factor=1)
except:
print(f" PyJHora shadbala failed for {case['id']}")
continue
case_match = 0
case_total = 0
case_details = []
for pname in PLANET_NAMES:
if pname not in planets or pname not in our['planets']:
continue
our_rupas = our['planets'][pname]['total_rupas']
pj_rupas = pj.get(pname, {}).get('shadbala', {}).get('total', 0)
if isinstance(pj_rupas, dict):
pj_rupas = pj_rupas.get('rupas', 0)
total_planets += 1
if pj_rupas > 0:
diff_pct = abs(our_rupas - pj_rupas) / pj_rupas * 100
if diff_pct <= 10:
within_10pct += 1
if diff_pct <= 20:
within_20pct += 1
case_details.append(f"{pname}: us={our_rupas:.2f}, pj={pj_rupas:.2f} ({diff_pct:.1f}%)")
details.append({
'case': case['id'],
'planets': case_details,
})
print("=" * 80)
print("Shadbala Benchmark v1.0")
print(f"Total planets: {total_planets}")
print("=" * 80)
print(f"Within 10% of PyJHora: {within_10pct}/{total_planets} = {within_10pct/total_planets*100:.1f}%" if total_planets else "N/A")
print(f"Within 20% of PyJHora: {within_20pct}/{total_planets} = {within_20pct/total_planets*100:.1f}%" if total_planets else "N/A")
for d in details:
print(f"\n{d['case']}:")
for pd in d['planets']:
print(f" {pd}")
if within_20pct / total_planets >= 0.8 if total_planets else False:
print("\n✅ PASS (≥80% within 20%)")
else:
print("\n⚠️ NEEDS IMPROVEMENT")
if __name__ == "__main__":
run_benchmark()
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
Vimshottari Dasha Benchmark v1.1
比较 v6.1.13 yinduzhanxing Vimshottari Dasha 与 PyJHora 实现。
算法来源:基于jyotishganit (MIT License) 核心算法。
使用恒星年 = 365.25636天。
对比字段:
- MD主星(前3个)、MD起始/结束日期
- AD主星(第1个MD的前3个AD)、AD起始日期
依赖: pip install jhora numpy
"""
import sys, json, os
from datetime import datetime, timedelta
# 添加scripts路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'scripts')))
from dasha_calculator_enhanced import (
calculate_dasha_dates, calculate_dasha_start_date,
DASHA_ORDER, VIMSHOTTARI_PERIODS, HUMAN_LIFE_SPAN_VIMSHOTTARI,
YEAR_DURATION_DAYS
)
# PyJHora
from jhora.horoscope.dhasa.graha import vimsottari as pj_vimshottari
from jhora.panchanga import drik
from jhora import utils, const
from jhora.horoscope.chart import charts
# ─────────────────────────────────────────────
# 10个测试案例
# ─────────────────────────────────────────────
TEST_CASES = [
{"id": "smoke_beijing_1990_noon", "year":1990, "month":6, "day":15, "hour":12, "minute":0, "lat":39.9, "lon":116.4, "tz":8},
{"id": "smoke_newyork_1985_morning", "year":1985, "month":3, "day":22, "hour":9, "minute":30, "lat":40.7, "lon":-74.0, "tz":-5},
{"id": "smoke_london_1970_evening", "year":1970, "month":9, "day":12, "hour":19, "minute":0, "lat":51.5, "lon":-0.1, "tz":0},
{"id": "smoke_delhi_2000_midnight", "year":2000, "month":1, "day":1, "hour":0, "minute":0, "lat":28.6, "lon":77.2, "tz":5.5},
{"id": "smoke_sydney_1999_afternoon", "year":1999, "month":8, "day":7, "hour":15, "minute":45, "lat":-33.9, "lon":151.2, "tz":10},
{"id": "smoke_tokyo_1964_noon", "year":1964, "month":10, "day":10,"hour":12, "minute":0, "lat":35.7, "lon":139.7, "tz":9},
{"id": "smoke_cairo_1952_dawn", "year":1952, "month":7, "day":23, "hour":6, "minute":0, "lat":30.0, "lon":31.2, "tz":2},
{"id": "smoke_paris_1989_noon", "year":1989, "month":11,"day":9, "hour":12, "minute":30, "lat":48.9, "lon":2.3, "tz":1},
{"id": "smoke_losangeles_1995_night", "year":1995, "month":2, "day":14, "hour":22, "minute":15, "lat":34.1, "lon":-118.2,"tz":-8},
{"id": "smoke_sao_paulo_2004_morning","year":2004, "month":12,"day":25, "hour":7, "minute":0, "lat":-23.5, "lon":-46.6, "tz":-3},
]
# PyJHora行星ID -> 名称
PLANET_ID_TO_NAME = {0: 'Ketu', 1: 'Venus', 2: 'Sun', 3: 'Moon',
4: 'Mars', 5: 'Rahu', 6: 'Jupiter', 7: 'Saturn', 8: 'Mercury'}
def jd_to_datetime(jd):
"""Julian day -> datetime"""
unix_epoch_jd = 2440587.5
seconds_since_epoch = (jd - unix_epoch_jd) * 86400
return datetime(1970, 1, 1) + timedelta(seconds=seconds_since_epoch)
def run_benchmark():
total_fields = 0
match_fields = 0
detail_lines = []
sample_details = []
for case in TEST_CASES:
# 1. 准备PyJHora数据
dob = drik.Date(case["year"], case["month"], case["day"])
tob = (case["hour"], case["minute"], 0)
place = drik.Place(f'test_{case["id"]}', case["lat"], case["lon"], case["tz"])
jd = utils.julian_day_number(dob, tob)
birth_date = datetime(case["year"], case["month"], case["day"],
case["hour"], case["minute"])
# 2. 获取Moon经度(来自PyJHora
pp = charts.divisional_chart(jd, place, divisional_chart_factor=1)[:const._pp_count_upto_ketu]
moon_sign = pp[const.MOON_ID + 1][1][0]
moon_deg = pp[const.MOON_ID + 1][1][1]
moon_degree = moon_sign * 30 + moon_deg
# 3. 我们的Vimshottari计算
our_md_list = calculate_dasha_dates(birth_date, moon_degree)
# 4. PyJHora Vimshottari MD
pj_md_dict = pj_vimshottari.vimsottari_mahadasa(jd, place, divisional_chart_factor=1)
pj_md_ordered = list(pj_md_dict.items())[:3]
case_match = 0
case_total = 0
mismatches = []
# 比较MD(前3个)- lord + start date
for i in range(min(3, len(our_md_list), len(pj_md_ordered))):
our_md = our_md_list[i]
pj_lord_id, pj_start_jd = pj_md_ordered[i]
# MD lord
case_total += 1
our_lord = our_md['lord']
pj_lord = PLANET_ID_TO_NAME.get(pj_lord_id, str(pj_lord_id))
if our_lord == pj_lord:
case_match += 1
else:
mismatches.append(f"MD[{i}] lord: us={our_lord}, pj={pj_lord}")
# MD start date(比较MD起始时间)
case_total += 1
our_start = our_md['start_date']
pj_start = jd_to_datetime(pj_start_jd)
diff_days = abs((our_start - pj_start).days)
if diff_days <= 90: # 3个月容差(由于恒星年 vs 回归年等差异)
case_match += 1
else:
mismatches.append(f"MD[{i}] start: us={our_start.strftime('%Y-%m-%d')}, pj={pj_start.strftime('%Y-%m-%d')} (diff={diff_days}d)")
# 比较AD(第1个MD的前3个AD
if len(our_md_list) > 0 and len(pj_md_ordered) > 0:
our_first_md = our_md_list[0]
our_md_total_days = our_first_md['years'] * YEAR_DURATION_DAYS
pj_first_lord_id, pj_first_start_jd = pj_md_ordered[0]
pj_ad_dict = pj_vimshottari._vimsottari_bhukti(pj_first_lord_id, pj_first_start_jd)
pj_ad_ordered = list(pj_ad_dict.items())[:3]
for j in range(min(3, len(pj_ad_ordered))):
pj_ad_lord_id, pj_ad_start_jd = pj_ad_ordered[j]
# AD lord
case_total += 1
pj_ad_lord = PLANET_ID_TO_NAME.get(pj_ad_lord_id, str(pj_ad_lord_id))
# 我们的AD lord按Vimshottari顺序
our_first_lord = our_first_md['lord']
first_idx = DASHA_ORDER.index(our_first_lord)
ad_idx = (first_idx + j) % 9
our_ad_lord = DASHA_ORDER[ad_idx]
if our_ad_lord == pj_ad_lord:
case_match += 1
else:
mismatches.append(f"AD[0,{j}] lord: us={our_ad_lord}, pj={pj_ad_lord}")
# AD start date
case_total += 1
pj_ad_start = jd_to_datetime(pj_ad_start_jd)
# 我们的AD start - 基于比例公式
our_ad_start = our_first_md['start_date']
for k in range(j):
ad_lord_dur = VIMSHOTTARI_PERIODS[DASHA_ORDER[(first_idx + k) % 9]]
ad_days = our_md_total_days * (ad_lord_dur / HUMAN_LIFE_SPAN_VIMSHOTTARI)
our_ad_start += timedelta(days=ad_days)
diff_days = abs((our_ad_start - pj_ad_start).days)
if diff_days <= 90:
case_match += 1
else:
mismatches.append(f"AD[0,{j}] start: us={our_ad_start.strftime('%Y-%m-%d')}, pj={pj_ad_start.strftime('%Y-%m-%d')} (diff={diff_days}d)")
total_fields += case_total
match_fields += case_match
sample_details.append({
'case': case['id'],
'match': f"{case_match}/{case_total}",
'mismatches': mismatches[:5],
})
# ── 输出 ──
print("=" * 80)
print("Vimshottari Dasha Benchmark v1.1")
print(f"Skill: v6.1.13 (基于jyotishganit MIT算法 + 恒星年={YEAR_DURATION_DAYS:.5f})")
print("=" * 80)
print(f"\n{'Case':40s} | Match")
print("-" * 80)
for d in sample_details:
print(f" {d['case']:35s} | {d['match']:>10s}")
print("-" * 80)
rate = match_fields / total_fields * 100 if total_fields else 0
print(f"\nTotal Match: {match_fields}/{total_fields} = {rate:.2f}%")
if rate >= 95:
status = "✅ PASS ✓"
elif rate >= 85:
status = "⚠️ BORDERLINE"
else:
status = "❌ FAIL"
print(f"\nBenchmark Result: {status}")
if status != "✅ PASS ✓":
print("\n--- 不匹配详情 ---")
for d in sample_details:
if d["mismatches"]:
print(f"\n{d['case']}:")
for m in d["mismatches"][:3]:
print(f" {m}")
# 保存 JSON
out = os.path.join(os.path.dirname(__file__), "..", "outputs", "vimshottari_benchmark.json")
os.makedirs(os.path.dirname(out), exist_ok=True)
result = {
"version": "v6.1.13",
"benchmark": "vimshottari_dasha",
"total_match": match_fields,
"total_fields": total_fields,
"match_rate": round(rate / 100, 4) if total_fields else 0,
"status": status,
"samples": sample_details,
}
with open(out, 'w') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print(f"\n结果已保存: {out}")
if __name__ == "__main__":
run_benchmark()