Files
Jyotisha/scripts/jaimini.py
T
732642856 f83db2fac1 Enhance Jyotish validation and Jaimini modules
- add external validation reports and open-source comparison references

- integrate Jaimini arudha/graha pada, enhanced argala, and additional synastry kutas

- update skill docs and capability matrices

- add smoke tests for open-source integrations
2026-06-10 20:50:52 +08:00

435 lines
18 KiB
Python
Raw 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 -*-
"""
Jaimini占星体系模块 v1.2
Parashara传承中的Jaimini子系统
支持:
- Chara Karaka: 7/8个功能指示星(按度数排序)
- Arudha Pada: A1-A12 + UpapadaUL),复用 dashaflow/jaimini-tropical 的 MIT 算法
- Karakamsha: AK在Navamsa中的上升(灵魂方向)
- Chara Dasha: 当前为简化 timing 实现,v6.0.9 后标注为 partial,不得单独作为高置信度应期依据
- Special Lagnas: HL/GL/VL 简化计算(出生时间敏感,作为辅助)
MIT复用来源:
- dashaflow (adarshj322): Arudha/Upapada公式与例外规则
- jaimini-tropical (tunanfang-pixel): Pada命名、Graha Pada与Jaimini特殊点结构
"""
from typing import Dict, List, Tuple, Optional
import math
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
SIGN_LORDS = {'Aries':'Mars','Taurus':'Venus','Gemini':'Mercury','Cancer':'Moon',
'Leo':'Sun','Virgo':'Mercury','Libra':'Venus','Scorpio':'Mars',
'Sagittarius':'Jupiter','Capricorn':'Saturn','Aquarius':'Saturn','Pisces':'Jupiter'}
# Chara Karaka 定义(7星制,排除Rahu
KARAKA_7 = {
1: 'Atmakaraka', # AK - 灵魂指示星(度数最高)
2: 'Amatyakaraka', # AmK - 事业/顾问
3: 'Bhratrikaraka', # BK - 兄弟/勇气
4: 'Matrikaraka', # MK - 母亲
5: 'Putrakaraka', # PK - 子女
6: 'Gnatikaraka', # GK - 敌人/障碍
7: 'Darakaraka', # DK - 配偶(度数最低)
}
# 8星制(含Rahu,用绝对度数)
KARAKA_8 = {
1: 'Atmakaraka', 2: 'Amatyakaraka', 3: 'Bhratrikaraka', 4: 'Matrikaraka',
5: 'Putrakaraka', 6: 'Gnatikaraka', 7: 'Darakaraka', 8: 'Pitrukaraka',
}
KARAKA_CN = {
'Atmakaraka': '灵魂星AK', 'Amatyakaraka': '事业星AmK',
'Bhratrikaraka': '兄弟星BK', 'Matrikaraka': '母亲星MK',
'Putrakaraka': '子女星PK', 'Gnatikaraka': '障碍星GK',
'Darakaraka': '配偶星DK', 'Pitrukaraka': '父亲星PiK',
}
KARAKA_DOMAINS = {
'Atmakaraka': '灵魂使命、核心自我、人生最高目标',
'Amatyakaraka': '事业方向、主要谋士、权力代理',
'Bhratrikaraka': '兄弟姐妹、勇气、冒险精神',
'Matrikaraka': '母亲、家庭根基、情感安全感',
'Putrakaraka': '子女、创造力、学生、智能成果',
'Gnatikaraka': '竞争对手、疾病、障碍、转化力量',
'Darakaraka': '配偶特质、伴侣关系、婚姻质量',
'Pitrukaraka': '父亲、祖先业力、传统传承',
}
ARUDHA_NAMES = {
1: 'Arudha Lagna (AL)', 2: 'Dhana Pada (A2)', 3: 'Vikrama Pada (A3)',
4: 'Sukha Pada (A4)', 5: 'Mantra Pada (A5)', 6: 'Roga Pada (A6)',
7: 'Dara Pada (A7)', 8: 'Mrityu Pada (A8)', 9: 'Dharma Pada (A9)',
10: 'Karma Pada (A10)', 11: 'Labha Pada (A11)', 12: 'Upapada (UL)',
}
def _sign_idx(sign_name: str) -> int:
return SIGNS.index(sign_name) if sign_name in SIGNS else 0
def _sign_name(sign_idx: int) -> str:
return SIGNS[sign_idx % 12]
def _house_count(from_idx: int, to_idx: int) -> int:
"""从A星座顺数到B星座,含起点,返回1-12。"""
return ((to_idx - from_idx) % 12) + 1
def calc_chara_karaka_7(planet_degrees: Dict[str, float]) -> Dict:
"""
计算7星制Chara Karaka(度数最高→AK,最低→DK)
参数: planet_degrees = {'Sun': 12.5, 'Moon': 8.3, ...}
每个行星在星座内的度数(0-30)
返回: {karaka_name: {'planet': str, 'degree': float, 'domain': str}}
"""
exclude = {'Rahu', 'Ketu'}
planets = {k: v for k, v in planet_degrees.items() if k not in exclude}
sorted_planets = sorted(planets.items(), key=lambda x: x[1], reverse=True)
results = {}
for rank, (pname, deg) in enumerate(sorted_planets, 1):
if rank > 7:
break
karaka = KARAKA_7[rank]
results[karaka] = {
'planet': pname,
'degree_in_sign': round(deg, 4),
'rank': rank,
'domain': KARAKA_DOMAINS.get(karaka, ''),
'cn_name': KARAKA_CN.get(karaka, karaka),
}
ak = results.get('Atmakaraka', {})
dk = results.get('Darakaraka', {})
return {
'karaka_table': results,
'summary': {
'AK': f"{ak.get('planet','?')} ({ak.get('degree_in_sign',0):.1f}°)",
'DK': f"{dk.get('planet','?')} ({dk.get('degree_in_sign',0):.1f}°)",
'AK_domain': ak.get('domain', ''),
'DK_domain': dk.get('domain', ''),
}
}
def calc_chara_karaka_8(planet_degrees: Dict[str, float]) -> Dict:
"""计算8星制Chara Karaka(含RahuKetu排除)。"""
planets = {}
for pname, deg in planet_degrees.items():
if pname == 'Ketu':
continue
planets[pname] = deg
sorted_planets = sorted(planets.items(), key=lambda x: x[1], reverse=True)
results = {}
for rank, (pname, deg) in enumerate(sorted_planets, 1):
if rank > 8:
break
karaka = KARAKA_8[rank]
results[karaka] = {
'planet': pname,
'degree_in_sign': round(deg, 4),
'rank': rank,
'domain': KARAKA_DOMAINS.get(karaka, ''),
'cn_name': KARAKA_CN.get(karaka, karaka),
}
return {'karaka_table_8': results}
def calc_arudha_pada_for_house(house_sign_idx: int, planet_longitudes: Dict[str, float]) -> Optional[Dict]:
"""
计算单宫Arudha Pada。
公式复用 dashaflow / jaimini-tropical MIT 实现:
1. 从目标宫星座数到宫主所在星座;
2. 从宫主所在星座再数同样距离;
3. 若落回本宫或本宫第七,则改取本宫第十。
"""
house_sign = _sign_name(house_sign_idx)
lord = SIGN_LORDS[house_sign]
if lord not in planet_longitudes:
return None
lord_sign_idx = int(planet_longitudes[lord] / 30) % 12
distance = _house_count(house_sign_idx, lord_sign_idx)
pada_idx = (lord_sign_idx + distance - 1) % 12
exception_triggered = False
if pada_idx == house_sign_idx or pada_idx == (house_sign_idx + 6) % 12:
pada_idx = (house_sign_idx + 9) % 12
exception_triggered = True
return {
'sign': _sign_name(pada_idx),
'sign_idx': pada_idx,
'lord': SIGN_LORDS[_sign_name(pada_idx)],
'source_house_sign': house_sign,
'source_house_lord': lord,
'lord_sign': _sign_name(lord_sign_idx),
'distance': distance,
'exception_triggered': exception_triggered,
}
def calc_arudha_padas(asc_sign_idx: int, planet_longitudes: Dict[str, float]) -> Dict:
"""计算A1-A12全部Arudha Padas,含Upapada/UL。"""
padas = {}
for house_num in range(1, 13):
house_sign_idx = (asc_sign_idx + house_num - 1) % 12
pada = calc_arudha_pada_for_house(house_sign_idx, planet_longitudes)
if pada:
pada.update({
'house_num': house_num,
'name': ARUDHA_NAMES.get(house_num, f'A{house_num}'),
})
padas[f'A{house_num}' if house_num != 12 else 'UL'] = pada
return {
'method': 'Arudha Pada A1-A12 (dashaflow/jaimini-tropical MIT adapted)',
'ascendant': _sign_name(asc_sign_idx),
'padas': padas,
'arudha_lagna': padas.get('A1'),
'upapada': padas.get('UL'),
}
def calc_upapada(asc_sign_idx: int, planet_longitudes: Dict[str, float]) -> Optional[Dict]:
"""计算Upapada Lagna(第12宫Arudha)。"""
result = calc_arudha_padas(asc_sign_idx, planet_longitudes).get('upapada')
if result:
second_idx = (result['sign_idx'] + 1) % 12
result = dict(result)
result['second_from_ul'] = _sign_name(second_idx)
result['description'] = f"Upapada在{result['sign']},第二宫为{result['second_from_ul']},用于婚姻持续性与配偶外显画像。"
return result
def calc_graha_padas(planet_longitudes: Dict[str, float]) -> Dict:
"""计算行星Graha Pada:行星位置通过其宫主映射出的外显影像。"""
results = {}
for planet, lon in planet_longitudes.items():
if planet in ('Rahu', 'Ketu'):
continue
planet_sign_idx = int(lon / 30) % 12
planet_sign = _sign_name(planet_sign_idx)
lord = SIGN_LORDS[planet_sign]
if lord not in planet_longitudes:
continue
lord_sign_idx = int(planet_longitudes[lord] / 30) % 12
distance = _house_count(planet_sign_idx, lord_sign_idx)
pada_idx = (lord_sign_idx + distance - 1) % 12
results[planet] = {
'planet_sign': planet_sign,
'lord': lord,
'lord_sign': _sign_name(lord_sign_idx),
'graha_pada_sign': _sign_name(pada_idx),
'graha_pada_sign_idx': pada_idx,
'distance': distance,
}
return {'method': 'Graha Pada (jaimini-tropical MIT adapted)', 'graha_padas': results}
def calc_chara_dasha(asc_sign_idx: int,
planet_longitudes: Dict[str, float],
birth_year: int, birth_month: int,
birth_day: int = 1) -> Dict:
"""
Chara Dasha计算(v6.1.10修复:添加出生日平衡计算)
规则:
- 从上升星座开始
- 奇数星座(Aries, Gemini...):正向顺序
- 偶数星座(Taurus, Cancer...):反向顺序
- 大运长度 = 12 - 落入该星座的行星数(Jaimini Sutras 1:1-3标准)
"""
is_odd = asc_sign_idx % 2 == 0
direction = 1 if is_odd else -1
dasha_sequence = []
for i in range(12):
sign_idx = (asc_sign_idx + direction * i) % 12
sign_name = SIGNS[sign_idx]
lord = SIGN_LORDS[sign_name]
count_in_sign = sum(1 for lon in planet_longitudes.values()
if int(lon / 30) % 12 == sign_idx and lon >= 0)
duration = max(1, 12 - count_in_sign)
dasha_sequence.append({
'sign': sign_name,
'sign_idx': sign_idx,
'lord': lord,
'duration_years': duration,
'planets_in_sign': count_in_sign,
'order': i + 1,
})
first_duration_months = dasha_sequence[0]['duration_years'] * 12
days_in_month = 30.44
remaining_days = (days_in_month - birth_day + days_in_month / 2)
remaining_fraction = remaining_days / (first_duration_months * days_in_month)
dasha_sequence[0]['balance_at_birth'] = round(remaining_fraction, 4)
if birth_day <= 15:
start_year, start_month = birth_year, birth_month
else:
start_year, start_month = birth_year, birth_month + 1
if start_month > 12:
start_month = 1
start_year += 1
current_year, current_month = start_year, start_month
for i, d in enumerate(dasha_sequence):
d['start_date'] = f"{current_year}-{current_month:02d}"
actual_duration = d['duration_years']
if i == 0:
actual_duration *= remaining_fraction
end_month = current_month + int(actual_duration * 12)
end_year = current_year + end_month // 12
end_month = end_month % 12
if end_month == 0:
end_month = 12
end_year -= 1
d['end_date'] = f"{end_year}-{end_month:02d}"
current_year, current_month = end_year, end_month + 1
if current_month > 12:
current_month = 1
current_year += 1
return {
'method': 'Chara Dasha (Jaimini Sutras 1:1-3, v6.1.10 balance-at-birth fix)',
'ascendant': SIGNS[asc_sign_idx],
'direction': 'forward' if is_odd else 'backward',
'dasha_sequence': dasha_sequence,
'total_cycle_years': sum(d['duration_years'] for d in dasha_sequence),
'capability_status': 'partial',
}
def calc_chara_dasha_with_antardasha(asc_sign_idx: int,
planet_longitudes: Dict[str, float],
birth_year: int, birth_month: int,
birth_day: int = 1) -> Dict:
"""Chara Dasha 完整3层计算 v6.1.10MD → AD → PD)。"""
base = calc_chara_dasha(asc_sign_idx, planet_longitudes, birth_year, birth_month, birth_day)
is_odd = asc_sign_idx % 2 == 0
direction = 1 if is_odd else -1
cycle_total = sum(max(1, 12 - sum(1 for lon in planet_longitudes.values()
if int(lon / 30) % 12 == (asc_sign_idx + direction * i) % 12 and lon >= 0))
for i in range(12))
for md in base['dasha_sequence']:
md_sign_idx = md['sign_idx']
md_duration = md['duration_years']
antardasha_list = []
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]
count_in_sign = sum(1 for lon in planet_longitudes.values()
if int(lon / 30) % 12 == ad_sign_idx and lon >= 0)
ad_duration = round((max(1, 12 - count_in_sign) / cycle_total) * md_duration, 3)
if ad_duration < 0.003:
ad_duration = 0.003
pratyantar_list = []
for k in range(12):
pd_sign_idx = (ad_sign_idx + direction * k) % 12
pd_sign = SIGNS[pd_sign_idx]
pd_lord = SIGN_LORDS[pd_sign]
pd_count = sum(1 for lon in planet_longitudes.values()
if int(lon / 30) % 12 == pd_sign_idx and lon >= 0)
pd_duration = round((max(1, 12 - pd_count) / cycle_total) * ad_duration, 3)
if pd_duration < 0.001:
pd_duration = 0.001
pratyantar_list.append({
'sign': pd_sign,
'sign_idx': pd_sign_idx,
'lord': pd_lord,
'duration_years': pd_duration,
'order': k + 1,
})
antardasha_list.append({
'sign': ad_sign,
'sign_idx': ad_sign_idx,
'lord': ad_lord,
'duration_years': ad_duration,
'order': j + 1,
'pratyantar_dashas': pratyantar_list,
})
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_days = round(ad['duration_years'] * 365.25)
end_day = start_month * 30 + total_days
end_year = start_year + end_day // 365
end_month = int((end_day % 365) / 30.44) + 1
end_month = max(1, min(12, end_month))
ad['end_date'] = f"{end_year}-{end_month:02d}"
if end_month < 12:
start_year, start_month = end_year, end_month + 1
else:
start_year, start_month = end_year + 1, 1
md['antardashas'] = antardasha_list
base['has_antardasha'] = True
base['has_pratyantar'] = True
return base
def calc_karakamsha(ak_sign_in_d9: str, ak_degree_in_d9: float) -> Dict:
"""Karakamsha分析:AK在D9中的位置作为灵魂上升。"""
lord = SIGN_LORDS.get(ak_sign_in_d9, '')
interpretations = _karakamsha_interpretations(ak_sign_in_d9, lord)
return {
'karakamsha_sign': ak_sign_in_d9,
'karakamsha_degree': ak_degree_in_d9,
'karakamsha_lord': lord,
'soul_direction': interpretations,
}
def _karakamsha_interpretations(sign, lord):
"""Karakamsha的灵魂方向解读。"""
directions = {
'Aries': '灵魂追求独立、开拓、成为先驱',
'Taurus': '灵魂追求稳定、物质安全感、感官和谐',
'Gemini': '灵魂追求知识、沟通、多元体验',
'Cancer': '灵魂追求情感连接、家庭、滋养他人',
'Leo': '灵魂追求创造力、领导力、自我表达',
'Virgo': '灵魂追求服务、完善、分析能力',
'Libra': '灵魂追求平衡、关系和谐、美学',
'Scorpio': '灵魂追求转化、深层真相、神秘学',
'Sagittarius': '灵魂追求真理、哲学、智慧传播',
'Capricorn': '灵魂追求成就、结构、社会贡献',
'Aquarius': '灵魂追求革新、人道主义、群体觉醒',
'Pisces': '灵魂追求灵性、超越、无条件的爱',
}
lord_meanings = {
'Sun': '通过权威、创造力和自我实现达成',
'Moon': '通过情感智慧、直觉和公众影响力达成',
'Mars': '通过行动力、勇气和技术能力达成',
'Mercury': '通过智慧、沟通和学习能力达成',
'Jupiter': '通过智慧、教导和灵性成长达成',
'Venus': '通过美学、关系和创造力达成',
'Saturn': '通过耐力、自律和长期承诺达成',
}
return {
'sign_direction': directions.get(sign, ''),
'lord_method': lord_meanings.get(lord, ''),
}
def calc_special_lagnas(asc_sign_idx: int, hour: int, minute: int = 0) -> Dict:
"""
Jaimini特殊上升点简化版:HL/GL/VL。
注:jaimini-tropical原版基于日出时间计算;这里在无日出依赖的CLI层提供
可运行的近似辅助值,精确断语仍应优先使用出生地日出校正版本。
"""
local_hours = hour + minute / 60.0
ghatis = (local_hours / 24.0) * 60.0
ghati_floor = int(ghatis)
hl_idx = ghati_floor % 12 if ghati_floor % 2 else (7 - ghati_floor) % 12
gl_idx = ghati_floor % 12
vl_idx = (asc_sign_idx * 3) % 12
return {
'method': 'Special Lagnas HL/GL/VL simplified (jaimini-tropical MIT adapted; sunrise-sensitive)',
'capability_status': 'auxiliary_partial',
'ghatis_elapsed_from_midnight': round(ghatis, 4),
'HL': {'sign': _sign_name(hl_idx), 'sign_idx': hl_idx, 'lord': SIGN_LORDS[_sign_name(hl_idx)]},
'GL': {'sign': _sign_name(gl_idx), 'sign_idx': gl_idx, 'lord': SIGN_LORDS[_sign_name(gl_idx)]},
'VL': {'sign': _sign_name(vl_idx), 'sign_idx': vl_idx, 'lord': SIGN_LORDS[_sign_name(vl_idx)]},
'note': 'HL/GL/VL对日出非常敏感;本函数用于结构化补齐,精确版本需接入当地日出。'
}