Spaces:
Running
Running
Download vedic/engine.py from sisithefox/sisi-astrology-api: direct link, hf CLI and curl.
- Browser
- Download file 36.6 kB
-
https://huggingface.co/spaces/sisithefox/sisi-astrology-api/resolve/main/vedic/engine.py
- Command line
-
hf download hf://spaces/sisithefox/sisi-astrology-api/vedic/engine.py
-
curl -L -o engine.py https://huggingface.co/spaces/sisithefox/sisi-astrology-api/resolve/main/vedic/engine.py
36.6 kB
| """ | |
| vedic-calculator v0.5 - PyJHora 精确计算引擎 | |
| 基于 pysweph 天文核心 + PyJHora 精确算法(含 9 项 Shadbala bug 修正) | |
| 输出完整的 structured_data 所需数据 | |
| v0.5: 移除所有 dashaflow fallback(错误结果比无结果更糟),fail-fast | |
| v0.4: Dasha 接入 PyJHora(≤2天) | |
| v0.3: SAV/Shadbala fallback 显式 WARNING | |
| """ | |
| import swisseph as swe | |
| from datetime import datetime, timedelta | |
| import pytz | |
| import json | |
| import sys | |
| from calculation_profile import ( | |
| configure_swe as _profile_configure_swe, | |
| node_planet_id, | |
| profile_meta, | |
| ) | |
| # dashaflow — 仅用于 dignity/jaimini(这些无 PyJHora bug,不需要修正) | |
| from dashaflow.dignity import get_dignity, get_compound_relationship, check_combustion, get_digbala | |
| from dashaflow.jaimini import calculate_jaimini_karakas | |
| # ── PyJHora 精确模块(必须全部加载,否则 fail-fast)── | |
| _SETUP_HINT = ( | |
| "\n╔══════════════════════════════════════════════════════╗\n" | |
| "║ PyJHora 未正确安装!请运行: ║\n" | |
| "║ python vedic-calculator/scripts/setup_env.py ║\n" | |
| "╚══════════════════════════════════════════════════════╝" | |
| ) | |
| _load_errors = [] | |
| try: | |
| from ashtakavarga_pyjhora import calculate_ashtakavarga_fixed as _av_pyjhora | |
| except ImportError as e: | |
| _av_pyjhora = None | |
| _load_errors.append(f'ashtakavarga_pyjhora: {e}') | |
| try: | |
| from shadbala_pyjhora import calculate_shadbala_fixed as _shadbala_pyjhora | |
| except ImportError as e: | |
| _shadbala_pyjhora = None | |
| _load_errors.append(f'shadbala_pyjhora: {e}') | |
| try: | |
| from dasha_pyjhora import calculate_dasha_fixed as _dasha_pyjhora | |
| except ImportError as e: | |
| _dasha_pyjhora = None | |
| _load_errors.append(f'dasha_pyjhora: {e}') | |
| try: | |
| from divisional_pyjhora import calculate_divisional_charts as _div_pyjhora | |
| except ImportError as e: | |
| _div_pyjhora = None | |
| _load_errors.append(f'divisional_pyjhora: {e}') | |
| try: | |
| from extras_pyjhora import ( | |
| calculate_bhava_bala as _bhava_bala_pyjhora, | |
| calculate_special_lagnas as _special_lagnas_pyjhora, | |
| calculate_vargeeya_bala as _vargeeya_bala_pyjhora, | |
| ) | |
| except ImportError as e: | |
| _bhava_bala_pyjhora = None | |
| _special_lagnas_pyjhora = None | |
| _vargeeya_bala_pyjhora = None | |
| _load_errors.append(f'extras_pyjhora: {e}') | |
| # Fail-fast: 核心模块必须全部加载 | |
| _REQUIRED = {'SAV': _av_pyjhora, 'Shadbala': _shadbala_pyjhora, 'Dasha': _dasha_pyjhora} | |
| _missing = [name for name, mod in _REQUIRED.items() if mod is None] | |
| if _missing: | |
| print(f"\n❌ FATAL: 以下核心模块加载失败: {', '.join(_missing)}", file=sys.stderr) | |
| for err in _load_errors: | |
| print(f" → {err}", file=sys.stderr) | |
| print(_SETUP_HINT, file=sys.stderr) | |
| raise ImportError(f"vedic-calculator 核心模块缺失: {', '.join(_missing)}. 请运行 setup_env.py") | |
| # === 配置 === | |
| # ⚠️ swe 是进程级全局状态:PyJHora 模块(SAV/Shadbala/Dasha)每次调用都会把 | |
| # ayanamsa/node 是 pysweph 进程级全局状态。PyJHora wrappers 也会改这些值, | |
| # 所以每次计算前都必须按 calculation_profile 显式重设,避免上一个请求污染 | |
| # lagna、行星、星宿/pada 和 dasha。 | |
| def _configure_swe(calculation_profile=None): | |
| return _profile_configure_swe(calculation_profile) | |
| _configure_swe() | |
| SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo', | |
| 'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'] | |
| SIGN_ABBR = ['Ar','Ta','Ge','Cn','Le','Vi','Li','Sc','Sg','Cp','Aq','Pi'] | |
| PLANETS_SWE = { | |
| 'Sun': swe.SUN, 'Moon': swe.MOON, 'Mars': swe.MARS, | |
| 'Mercury': swe.MERCURY, 'Jupiter': swe.JUPITER, | |
| 'Venus': swe.VENUS, 'Saturn': swe.SATURN | |
| } | |
| SIGN_LORDS = { | |
| 0: 'Mars', 1: 'Venus', 2: 'Mercury', 3: 'Moon', | |
| 4: 'Sun', 5: 'Mercury', 6: 'Venus', 7: 'Mars', | |
| 8: 'Jupiter', 9: 'Saturn', 10: 'Saturn', 11: 'Jupiter' | |
| } | |
| NAKSHATRAS = [ | |
| ('Ashwini','Ketu'), ('Bharani','Venus'), ('Krittika','Sun'), | |
| ('Rohini','Moon'), ('Mrigashira','Mars'), ('Ardra','Rahu'), | |
| ('Punarvasu','Jupiter'), ('Pushya','Saturn'), ('Ashlesha','Mercury'), | |
| ('Magha','Ketu'), ('Purva Phalguni','Venus'), ('Uttara Phalguni','Sun'), | |
| ('Hasta','Moon'), ('Chitra','Mars'), ('Swati','Rahu'), | |
| ('Vishakha','Jupiter'), ('Anuradha','Saturn'), ('Jyeshtha','Mercury'), | |
| ('Moola','Ketu'), ('Purva Ashadha','Venus'), ('Uttara Ashadha','Sun'), | |
| ('Shravana','Moon'), ('Dhanishta','Mars'), ('Shatabhisha','Rahu'), | |
| ('Purva Bhadrapada','Jupiter'), ('Uttara Bhadrapada','Saturn'), ('Revati','Mercury') | |
| ] | |
| DASHA_ORDER = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury'] | |
| DASHA_YEARS = {'Ketu':7,'Venus':20,'Sun':6,'Moon':10,'Mars':7,'Rahu':18,'Jupiter':16,'Saturn':19,'Mercury':17} | |
| HOUSE_DOMAINS = { | |
| 1:'自我', 2:'财富', 3:'兄弟', 4:'家庭', 5:'子女', | |
| 6:'疾病', 7:'婚姻', 8:'变故', 9:'运势', 10:'事业', | |
| 11:'收入', 12:'损耗' | |
| } | |
| # === 核心计算函数 === | |
| def to_jd(year, month, day, hour, minute, tz_str): | |
| tz = pytz.timezone(tz_str) | |
| local_dt = tz.localize(datetime(year, month, day, hour, minute)) | |
| utc_dt = local_dt.astimezone(pytz.utc) | |
| ut_hour = utc_dt.hour + utc_dt.minute/60.0 + utc_dt.second/3600.0 | |
| return swe.julday(utc_dt.year, utc_dt.month, utc_dt.day, ut_hour) | |
| def calc_planet(jd, planet_id): | |
| flags = swe.FLG_SIDEREAL | swe.FLG_SPEED | |
| result = swe.calc_ut(jd, planet_id, flags) | |
| lon = result[0][0] | |
| speed = result[0][3] | |
| sign_idx = int(lon / 30) | |
| degree = lon % 30 | |
| deg_int = int(degree) | |
| min_int = round((degree - deg_int) * 60) | |
| return { | |
| 'longitude': lon, 'sign': SIGNS[sign_idx], 'sign_idx': sign_idx, | |
| 'degree': degree, 'deg_str': f"{deg_int}°{min_int:02d}'", | |
| 'retrograde': speed < 0, 'speed': speed | |
| } | |
| def calc_lagna(jd, lat, lon): | |
| flags = swe.FLG_SIDEREAL | |
| cusps, ascmc = swe.houses_ex(jd, lat, lon, b'W', flags) | |
| asc_lon = ascmc[0] | |
| sign_idx = int(asc_lon / 30) | |
| degree = asc_lon % 30 | |
| deg_int = int(degree) | |
| min_int = round((degree - deg_int) * 60) | |
| return { | |
| 'longitude': asc_lon, 'sign': SIGNS[sign_idx], 'sign_idx': sign_idx, | |
| 'degree': degree, 'deg_str': f"{deg_int}°{min_int:02d}'" | |
| } | |
| def get_nakshatra(longitude): | |
| nak_idx = int(longitude / (360/27)) | |
| pada = int((longitude % (360/27)) / (360/108)) + 1 | |
| name, lord = NAKSHATRAS[nak_idx] | |
| return {'name': name, 'pada': pada, 'lord': lord} | |
| def get_house(planet_sign_idx, lagna_sign_idx): | |
| return ((planet_sign_idx - lagna_sign_idx) % 12) + 1 | |
| def calc_navamsha(longitude): | |
| """D9 Navamsha: each sign divided into 9 parts of 3°20'""" | |
| nav_part = int((longitude % 30) / (30/9)) | |
| sign_idx = int(longitude / 30) | |
| # Navamsha starts from: Fire→Ar, Earth→Cp, Air→Li, Water→Cn | |
| element_start = [0, 9, 6, 3] # Ar, Cp, Li, Cn | |
| element = sign_idx % 4 # 0=fire, 1=earth, 2=air, 3=water | |
| d9_sign_idx = (element_start[element] + nav_part) % 12 | |
| return SIGNS[d9_sign_idx], d9_sign_idx | |
| def calc_dashamsha(longitude): | |
| """D10 Dashamsha: each sign divided into 10 parts of 3° | |
| Odd signs: count from same sign | |
| Even signs: count from 9th sign (= +8 in 0-indexed) | |
| """ | |
| part = int((longitude % 30) / 3) | |
| sign_idx = int(longitude / 30) | |
| if sign_idx % 2 == 0: # Odd signs (Ar, Ge, Le...) | |
| d10_sign_idx = (sign_idx + part) % 12 | |
| else: # Even signs: 9th from sign = +8 (0-indexed) | |
| d10_sign_idx = (sign_idx + part + 8) % 12 | |
| return SIGNS[d10_sign_idx], d10_sign_idx | |
| def calc_chaturthamsha(longitude): | |
| """D4""" | |
| part = int((longitude % 30) / (30/4)) | |
| sign_idx = int(longitude / 30) | |
| d4_sign_idx = (sign_idx + part * 3) % 12 | |
| return SIGNS[d4_sign_idx], d4_sign_idx | |
| def calc_panchamsha(longitude): | |
| """D5 Panchamsha: each sign divided into 5 parts of 6° | |
| BPHS lords: | |
| Odd signs: Mars(Ar), Saturn(Aq), Jupiter(Sg), Mercury(Ge), Venus(Li) | |
| Even signs: Venus(Ta), Mercury(Vi), Jupiter(Pi), Saturn(Cp), Mars(Sc) | |
| """ | |
| part = int((longitude % 30) / 6) | |
| sign_idx = int(longitude / 30) | |
| if sign_idx % 2 == 0: # odd sign (0-indexed even = zodiac odd) | |
| starts = [0, 10, 8, 2, 6] # Ar, Aq, Sg, Ge, Li | |
| else: # even sign | |
| starts = [1, 5, 11, 9, 7] # Ta, Vi, Pi, Cp, Sc | |
| d5_sign_idx = starts[part] if part < 5 else sign_idx | |
| return SIGNS[d5_sign_idx], d5_sign_idx | |
| def calc_chara_karakas_7k8k(planets): | |
| """Calculate both 7K and 8K Chara Karakas""" | |
| # Effective degree = degree in sign (for Rahu: 30 - degree) | |
| karaka_data = [] | |
| for name in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']: | |
| karaka_data.append((name, planets[name]['degree'])) | |
| # 7K: sort by degree descending, top 7 | |
| sorted_7k = sorted(karaka_data, key=lambda x: x[1], reverse=True) | |
| karaka_names_7k = ['AK','AmK','BK','MK','PK','GK','DK'] | |
| karakas_7k = [(karaka_names_7k[i], sorted_7k[i][0], sorted_7k[i][1]) | |
| for i in range(7)] | |
| # 8K: add Rahu (30 - degree) | |
| rahu_eff_deg = 30 - planets['Rahu']['degree'] | |
| karaka_data_8k = karaka_data + [('Rahu', rahu_eff_deg)] | |
| sorted_8k = sorted(karaka_data_8k, key=lambda x: x[1], reverse=True) | |
| karaka_names_8k = ['AK','AmK','BK','MK','PiK','PK','GK','DK'] | |
| karakas_8k = [(karaka_names_8k[i], sorted_8k[i][0], sorted_8k[i][1]) | |
| for i in range(8)] | |
| # DK争议: 比较7K的第7位 vs 8K的第7位 | |
| # 目的:检测加入Rahu后,"第7位(配偶指示星位)"是否换了行星 | |
| dk_7k = karakas_7k[6][1] # 7K第7位 | |
| dk_8k = sorted_8k[6][0] # 8K第7位(不是第8位!) | |
| dk_consistent = dk_7k == dk_8k | |
| return { | |
| '7k': karakas_7k, | |
| '8k': karakas_8k, | |
| 'dk_7k': dk_7k, | |
| 'dk_8k': dk_8k, | |
| 'dk_consistent': dk_consistent | |
| } | |
| def calc_aspects(planets): | |
| """Calculate major aspects between planets""" | |
| aspects = [] | |
| planet_names = list(planets.keys()) | |
| for i in range(len(planet_names)): | |
| for j in range(i+1, len(planet_names)): | |
| p1, p2 = planet_names[i], planet_names[j] | |
| lon1, lon2 = planets[p1]['longitude'], planets[p2]['longitude'] | |
| diff = abs(lon1 - lon2) | |
| if diff > 180: diff = 360 - diff | |
| # Check aspect types | |
| aspect_type = None | |
| orb = None | |
| if diff < 10: | |
| aspect_type = '合相' | |
| orb = diff | |
| elif abs(diff - 60) < 8: | |
| aspect_type = '六合(60°)' | |
| orb = abs(diff - 60) | |
| elif abs(diff - 90) < 8: | |
| aspect_type = '刑(90°)' | |
| orb = abs(diff - 90) | |
| elif abs(diff - 120) < 8: | |
| aspect_type = '三合(120°)' | |
| orb = abs(diff - 120) | |
| elif abs(diff - 180) < 10: | |
| aspect_type = '对冲(180°)' | |
| orb = abs(diff - 180) | |
| if aspect_type and orb is not None: | |
| aspects.append({ | |
| 'p1': p1, 'p2': p2, 'type': aspect_type, | |
| 'degree_diff': round(diff, 2), 'orb': round(orb, 2) | |
| }) | |
| # Sort by orb (tighter aspects first) | |
| aspects.sort(key=lambda x: x['orb']) | |
| return aspects[:8] # Top 8 most significant | |
| def calc_house_lords(lagna_sign_idx): | |
| """Calculate house lord table""" | |
| lords = {} | |
| for house in range(1, 13): | |
| sign_idx = (lagna_sign_idx + house - 1) % 12 | |
| lord = SIGN_LORDS[sign_idx] | |
| lords[house] = {'sign': SIGNS[sign_idx], 'lord': lord, 'domain': HOUSE_DOMAINS[house]} | |
| return lords | |
| def calc_vimsottari_dasha(moon_lon, birth_year, birth_month, birth_day, birth_hour, birth_minute): | |
| """Calculate Vimsottari Dasha periods""" | |
| nak = get_nakshatra(moon_lon) | |
| nak_lord = nak['lord'] | |
| start_idx = DASHA_ORDER.index(nak_lord) | |
| nak_span = 360/27 | |
| elapsed_in_nak = moon_lon % nak_span | |
| remaining_fraction = 1 - (elapsed_in_nak / nak_span) | |
| birth_dt = datetime(birth_year, birth_month, birth_day, birth_hour, birth_minute) | |
| dashas = [] | |
| # 第一个大运起始 = 出生日 - 已过大运年数(回溯到出生前) | |
| first_planet_years = DASHA_YEARS[DASHA_ORDER[start_idx]] | |
| elapsed_years = first_planet_years * (1 - remaining_fraction) | |
| current_dt = birth_dt - timedelta(days=elapsed_years * 365.25) | |
| for i in range(9): | |
| idx = (start_idx + i) % 9 | |
| planet = DASHA_ORDER[idx] | |
| years = DASHA_YEARS[planet] | |
| days = years * 365.25 | |
| end_dt = current_dt + timedelta(days=days) | |
| # Mark current | |
| now = datetime.now() | |
| is_current = current_dt <= now <= end_dt | |
| # Antardasha子期计算 | |
| antardashas = [] | |
| ad_start = current_dt | |
| for j in range(9): | |
| ad_idx = (idx + j) % 9 | |
| ad_planet = DASHA_ORDER[ad_idx] | |
| ad_years = years * DASHA_YEARS[ad_planet] / 120 | |
| ad_days = ad_years * 365.25 | |
| ad_end = ad_start + timedelta(days=ad_days) | |
| ad_is_current = ad_start <= now <= ad_end | |
| antardashas.append({ | |
| 'planet': ad_planet, | |
| 'start': ad_start.strftime('%Y-%m-%d'), | |
| 'end': ad_end.strftime('%Y-%m-%d'), | |
| 'is_current': ad_is_current | |
| }) | |
| ad_start = ad_end | |
| dashas.append({ | |
| 'planet': planet, | |
| 'start': current_dt.strftime('%Y-%m'), | |
| 'end': end_dt.strftime('%Y-%m'), | |
| 'years': round(years, 1), | |
| 'is_current': is_current, | |
| 'antardashas': antardashas | |
| }) | |
| current_dt = end_dt | |
| return dashas | |
| def calc_special_points(lagna, planets): | |
| """Calculate AL, UL and other special Jaimini points. | |
| Note: JHora may use different settings (Rahu/Ketu lord rules, dual-sign lords). | |
| Our implementation follows standard BPHS Jaimini method. | |
| """ | |
| lagna_idx = lagna['sign_idx'] | |
| def calc_arudha(house_num, lagna_idx): | |
| """Calculate Arudha Pada for a given house (BPHS standard)""" | |
| sign_idx = (lagna_idx + house_num - 1) % 12 | |
| lord = SIGN_LORDS[sign_idx] | |
| lord_sign_idx = planets[lord]['sign_idx'] | |
| # Count from house sign to lord (1-based: lord in same sign = 1st) | |
| dist = (lord_sign_idx - sign_idx) % 12 | |
| # Arudha = same distance from lord's position | |
| arudha_idx = (lord_sign_idx + dist) % 12 | |
| # BPHS Exception: arudha cannot be in 1st or 7th from house sign | |
| # If 1st → use 10th from house sign | |
| # If 7th → use 4th from house sign | |
| if arudha_idx == sign_idx: | |
| arudha_idx = (sign_idx + 9) % 12 # 10th from house sign | |
| elif arudha_idx == (sign_idx + 6) % 12: | |
| arudha_idx = (sign_idx + 3) % 12 # 4th from house sign | |
| return SIGNS[arudha_idx], arudha_idx | |
| al_sign, al_idx = calc_arudha(1, lagna_idx) | |
| al_house = get_house(al_idx, lagna_idx) | |
| ul_sign, ul_idx = calc_arudha(12, lagna_idx) | |
| ul_house = get_house(ul_idx, lagna_idx) | |
| return { | |
| 'AL': {'sign': al_sign, 'sign_idx': al_idx, 'house': al_house}, | |
| 'UL': {'sign': ul_sign, 'sign_idx': ul_idx, 'house': ul_house}, | |
| } | |
| def calc_transits(lagna_sign_idx, moon_sign_idx, calculation_profile=None): | |
| """Calculate current transit positions for slow planets. | |
| Used by core-pro for Sade Sati, BAV transit calibration, double transit. | |
| """ | |
| now = datetime.utcnow() # swe.julday expects UT, not server-local time | |
| jd_now = swe.julday(now.year, now.month, now.day, now.hour + now.minute/60) | |
| profile = _configure_swe(calculation_profile) | |
| flags = swe.FLG_SIDEREAL | swe.FLG_SPEED | |
| transits = {} | |
| # Slow planets: Saturn, Jupiter, Rahu, Ketu | |
| slow_planets = {'Saturn': swe.SATURN, 'Jupiter': swe.JUPITER} | |
| for name, pid in slow_planets.items(): | |
| result = swe.calc_ut(jd_now, pid, flags) | |
| lon = result[0][0] | |
| sign_idx = int(lon / 30) | |
| house = get_house(sign_idx, lagna_sign_idx) | |
| transits[name] = {'sign': SIGNS[sign_idx], 'sign_idx': sign_idx, 'house': house} | |
| result = swe.calc_ut(jd_now, node_planet_id(profile['id']), flags) | |
| rahu_lon = result[0][0] | |
| rahu_idx = int(rahu_lon / 30) | |
| transits['Rahu'] = {'sign': SIGNS[rahu_idx], 'sign_idx': rahu_idx, | |
| 'house': get_house(rahu_idx, lagna_sign_idx)} | |
| ketu_idx = (rahu_idx + 6) % 12 | |
| transits['Ketu'] = {'sign': SIGNS[ketu_idx], 'sign_idx': ketu_idx, | |
| 'house': get_house(ketu_idx, lagna_sign_idx)} | |
| # Sade Sati check | |
| saturn_idx = transits['Saturn']['sign_idx'] | |
| sade_sati = 'inactive' | |
| if saturn_idx == (moon_sign_idx - 1) % 12: | |
| sade_sati = 'phase1_rising' | |
| elif saturn_idx == moon_sign_idx: | |
| sade_sati = 'phase2_peak' | |
| elif saturn_idx == (moon_sign_idx + 1) % 12: | |
| sade_sati = 'phase3_fading' | |
| transits['sade_sati'] = sade_sati | |
| # Double transit (Saturn-Jupiter intersection) | |
| sat_houses = {transits['Saturn']['house']} | |
| # Saturn aspects: 3rd, 7th, 10th | |
| sat_h = transits['Saturn']['house'] | |
| for asp in [3, 7, 10]: | |
| sat_houses.add(((sat_h - 1 + asp - 1) % 12) + 1) | |
| jup_houses = {transits['Jupiter']['house']} | |
| # Jupiter aspects: 5th, 7th, 9th | |
| jup_h = transits['Jupiter']['house'] | |
| for asp in [5, 7, 9]: | |
| jup_houses.add(((jup_h - 1 + asp - 1) % 12) + 1) | |
| double_transit = sorted(sat_houses & jup_houses) | |
| transits['double_transit_houses'] = double_transit | |
| transits['timestamp'] = now.strftime('%Y-%m-%d') | |
| return transits | |
| # 各行星的额外全相位(除所有行星都有的第7位外);whole-sign | |
| _SPECIAL_DRISHTI = {'Mars': [4, 8], 'Jupiter': [5, 9], 'Saturn': [3, 10]} | |
| def calc_graha_drishti(planets, lagna_sign_idx): | |
| """经典吠陀相位 (graha drishti, whole-sign):所有行星看第7位; | |
| 火星另看4/8、木星5/9、土星3/10。与 calc_aspects(西式度数相位)互补—— | |
| 格局审计查"凶星压制/吉星扶持"必须用这个,不是西式相位。 | |
| 只算七正曜(Rahu/Ketu 的 drishti 各派有争议,不输出免得误导)。 | |
| """ | |
| drishti = {} | |
| for name in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']: | |
| p_sign = planets[name]['sign_idx'] | |
| offsets = [7] + _SPECIAL_DRISHTI.get(name, []) | |
| aspected_signs = [(p_sign + off - 1) % 12 for off in offsets] | |
| aspects_houses = sorted(get_house(s, lagna_sign_idx) for s in aspected_signs) | |
| aspects_planets = sorted( | |
| other for other in planets | |
| if other != name and planets[other]['sign_idx'] in aspected_signs | |
| ) | |
| drishti[name] = {'houses': aspects_houses, 'planets': aspects_planets} | |
| return drishti | |
| def calc_transit_timeline(lagna_sign_idx, moon_sign_idx, years=5, calculation_profile=None): | |
| """未来 N 年土星/木星过运时间线(按月采样,合并成段)。 | |
| 动态预测的弹药:四色时间线、Sade Sati 阶段、双过运节点都从这里拿精确年月, | |
| 不再靠 AI 凭训练数据猜星历。whole-sign,标注到月。 | |
| """ | |
| _configure_swe(calculation_profile) | |
| flags = swe.FLG_SIDEREAL | |
| now = datetime.utcnow() | |
| samples = [] # (yyyy_mm, sat_sign_idx, jup_sign_idx) | |
| for m in range(years * 12 + 1): | |
| y = now.year + (now.month - 1 + m) // 12 | |
| mo = (now.month - 1 + m) % 12 + 1 | |
| jd = swe.julday(y, mo, 15, 0) # mid-month sample | |
| sat_idx = int(swe.calc_ut(jd, swe.SATURN, flags)[0][0] / 30) | |
| jup_idx = int(swe.calc_ut(jd, swe.JUPITER, flags)[0][0] / 30) | |
| samples.append((f"{y:04d}-{mo:02d}", sat_idx, jup_idx)) | |
| def _segments(idx_of): | |
| segs = [] | |
| for ym, sat, jup in samples: | |
| sign_idx = idx_of(sat, jup) | |
| if segs and segs[-1]['sign_idx'] == sign_idx: | |
| segs[-1]['end'] = ym | |
| else: | |
| segs.append({'sign_idx': sign_idx, 'sign': SIGNS[sign_idx], | |
| 'house': get_house(sign_idx, lagna_sign_idx), | |
| 'start': ym, 'end': ym}) | |
| return segs | |
| saturn_segments = _segments(lambda s, j: s) | |
| jupiter_segments = _segments(lambda s, j: j) | |
| # Sade Sati 阶段窗口(土星过月亮的12/1/2位) | |
| _PHASES = {-1: 'phase1_rising', 0: 'phase2_peak', 1: 'phase3_fading'} | |
| sade_sati_windows = [] | |
| for seg in saturn_segments: | |
| for off, phase in _PHASES.items(): | |
| if seg['sign_idx'] == (moon_sign_idx + off) % 12: | |
| sade_sati_windows.append({'phase': phase, 'start': seg['start'], 'end': seg['end']}) | |
| # 双过运窗口:土(本位+3/7/10相)与木(本位+5/7/9相)同时激活的宫位,按月合并 | |
| def _houses_hit(sign_idx, extra_offsets): | |
| h = get_house(sign_idx, lagna_sign_idx) | |
| hit = {h} | |
| for asp in extra_offsets: | |
| hit.add(((h - 1 + asp - 1) % 12) + 1) | |
| return hit | |
| double_transit_windows = [] | |
| for ym, sat, jup in samples: | |
| overlap = sorted(_houses_hit(sat, [3, 7, 10]) & _houses_hit(jup, [5, 7, 9])) | |
| if double_transit_windows and double_transit_windows[-1]['houses'] == overlap: | |
| double_transit_windows[-1]['end'] = ym | |
| else: | |
| double_transit_windows.append({'houses': overlap, 'start': ym, 'end': ym}) | |
| return { | |
| 'saturn_segments': saturn_segments, | |
| 'jupiter_segments': jupiter_segments, | |
| 'sade_sati_windows': sade_sati_windows, | |
| 'double_transit_windows': double_transit_windows, | |
| 'sampled': f"monthly x {years}y from {samples[0][0]}", | |
| } | |
| # === 主计算函数 === | |
| def calculate_full_chart(year, month, day, hour, minute, lat, lon, tz_str="Asia/Kolkata", | |
| calculation_profile=None): | |
| """计算完整星盘数据""" | |
| profile = _configure_swe(calculation_profile) # 必须每次重设:上个请求的 PyJHora 调用会改全局 swe 状态 | |
| jd = to_jd(year, month, day, hour, minute, tz_str) | |
| ayanamsa = swe.get_ayanamsa_ut(jd) | |
| # 1. Lagna | |
| lagna = calc_lagna(jd, lat, lon) | |
| lagna['nakshatra'] = get_nakshatra(lagna['longitude']) | |
| lagna['house'] = 1 | |
| # 2. Planets (7 main) | |
| planets = {} | |
| for name, pid in PLANETS_SWE.items(): | |
| p = calc_planet(jd, pid) | |
| p['house'] = get_house(p['sign_idx'], lagna['sign_idx']) | |
| p['nakshatra'] = get_nakshatra(p['longitude']) | |
| planets[name] = p | |
| # 3. Rahu & Ketu | |
| flags = swe.FLG_SIDEREAL | swe.FLG_SPEED | |
| result = swe.calc_ut(jd, node_planet_id(profile['id']), flags) | |
| rahu_lon = result[0][0] | |
| rahu_sign_idx = int(rahu_lon / 30) | |
| rahu_deg = rahu_lon % 30 | |
| planets['Rahu'] = { | |
| 'longitude': rahu_lon, 'sign': SIGNS[rahu_sign_idx], | |
| 'sign_idx': rahu_sign_idx, 'degree': rahu_deg, | |
| 'deg_str': f"{int(rahu_deg)}°{round((rahu_deg%1)*60):02d}'", | |
| 'retrograde': True, 'speed': result[0][3], | |
| 'house': get_house(rahu_sign_idx, lagna['sign_idx']), | |
| 'nakshatra': get_nakshatra(rahu_lon) | |
| } | |
| ketu_lon = (rahu_lon + 180) % 360 | |
| ketu_sign_idx = int(ketu_lon / 30) | |
| ketu_deg = ketu_lon % 30 | |
| planets['Ketu'] = { | |
| 'longitude': ketu_lon, 'sign': SIGNS[ketu_sign_idx], | |
| 'sign_idx': ketu_sign_idx, 'degree': ketu_deg, | |
| 'deg_str': f"{int(ketu_deg)}°{round((ketu_deg%1)*60):02d}'", | |
| 'retrograde': True, 'speed': -result[0][3], | |
| 'house': get_house(ketu_sign_idx, lagna['sign_idx']), | |
| 'nakshatra': get_nakshatra(ketu_lon) | |
| } | |
| # 4. SAV/BAV (PyJHora — no fallback) | |
| tz = pytz.timezone(tz_str) | |
| _tz_dt = tz.localize(datetime(year, month, day, hour, minute)) | |
| _tz_offset = _tz_dt.utcoffset().total_seconds() / 3600.0 | |
| ashtak = _av_pyjhora(year, month, day, hour, minute, lat, lon, _tz_offset, profile['id']) | |
| # Map SAV to houses | |
| sav_by_house = {} | |
| for h in range(1, 13): | |
| sign_idx = (lagna['sign_idx'] + h - 1) % 12 | |
| sign_name = SIGNS[sign_idx] | |
| sav_by_house[h] = {'sign': sign_name, 'value': ashtak['sarvashtakavarga'].get(sign_name, 0)} | |
| # 5. Divisional charts (PyJHora: 15 charts) | |
| if _div_pyjhora is not None: | |
| divisional_charts = _div_pyjhora( | |
| year, month, day, hour, minute, lat, lon, _tz_offset, | |
| calculation_profile=profile['id'], | |
| ) | |
| else: | |
| divisional_charts = {} | |
| # Extract d9/d10/d4/d5 in legacy (sign_name, sign_idx) format for backward compat | |
| if divisional_charts: | |
| def _legacy_fmt(chart_key): | |
| ch = divisional_charts.get(chart_key, {}) | |
| return {p: (ch[p]['sign'], ch[p]['sign_idx']) for p in ch if 'error' not in ch} | |
| d9 = _legacy_fmt('D9') | |
| d10 = _legacy_fmt('D10') | |
| d4 = _legacy_fmt('D4') | |
| d5 = _legacy_fmt('D5') | |
| else: | |
| d9, d10, d4, d5 = {}, {}, {}, {} | |
| # Vargottama check | |
| vargottama = {} | |
| for name in planets: | |
| d9_sign = d9.get(name, (None, None)) | |
| d9_sign_name = d9_sign[0] if isinstance(d9_sign, tuple) else d9_sign.get('sign', None) if isinstance(d9_sign, dict) else None | |
| vargottama[name] = planets[name]['sign'] == d9_sign_name | |
| # 6. Dignity & Compound Relationship (自建,不依赖dashaflow) | |
| # BPHS Panchadha Maitri 算法 | |
| # Step 1: 旺/入庙/陷检测 | |
| EXALTATION = {'Sun':'Aries','Moon':'Taurus','Mars':'Capricorn', | |
| 'Mercury':'Virgo','Jupiter':'Cancer','Venus':'Pisces','Saturn':'Libra'} | |
| DEBILITATION = {'Sun':'Libra','Moon':'Scorpio','Mars':'Cancer', | |
| 'Mercury':'Pisces','Jupiter':'Capricorn','Venus':'Virgo','Saturn':'Aries'} | |
| OWN_SIGNS = {'Sun':['Leo'],'Moon':['Cancer'],'Mars':['Aries','Scorpio'], | |
| 'Mercury':['Gemini','Virgo'],'Jupiter':['Sagittarius','Pisces'], | |
| 'Venus':['Taurus','Libra'],'Saturn':['Capricorn','Aquarius']} | |
| # Step 2: 自然关系表 (Naisargika Maitri) - BPHS标准 | |
| NATURAL_REL = { | |
| 'Sun': {'friend':['Moon','Mars','Jupiter'], 'enemy':['Venus','Saturn'], 'neutral':['Mercury']}, | |
| 'Moon': {'friend':['Sun','Mercury'], 'enemy':[], 'neutral':['Mars','Jupiter','Venus','Saturn']}, | |
| 'Mars': {'friend':['Sun','Moon','Jupiter'], 'enemy':['Mercury'], 'neutral':['Venus','Saturn']}, | |
| 'Mercury': {'friend':['Sun','Venus'], 'enemy':['Moon'], 'neutral':['Mars','Jupiter','Saturn']}, | |
| 'Jupiter': {'friend':['Sun','Moon','Mars'], 'enemy':['Mercury','Venus'], 'neutral':['Saturn']}, | |
| 'Venus': {'friend':['Mercury','Saturn'], 'enemy':['Sun','Moon'], 'neutral':['Mars','Jupiter']}, | |
| 'Saturn': {'friend':['Mercury','Venus'], 'enemy':['Sun','Moon','Mars'], 'neutral':['Jupiter']}, | |
| } | |
| def get_natural_rel(planet, lord): | |
| """查自然关系: friend/enemy/neutral""" | |
| if planet == lord: | |
| return 'own' | |
| rel = NATURAL_REL.get(planet, {}) | |
| if lord in rel.get('friend', []): return 'friend' | |
| if lord in rel.get('enemy', []): return 'enemy' | |
| return 'neutral' | |
| def get_temporal_rel(planet_sign_idx, lord_sign_idx): | |
| """查临时关系: 座主落在行星的2/3/4/10/11/12宫=临时友""" | |
| dist = (lord_sign_idx - planet_sign_idx) % 12 | |
| # 距离2,3,4,10,11,12宫 = dist值1,2,3,9,10,11 (0-indexed) | |
| return 'temp_friend' if dist in {1,2,3,9,10,11} else 'temp_enemy' | |
| # Step 4: Panchadha合成表 | |
| COMPOUND_TABLE = { | |
| ('friend', 'temp_friend'): 'great_friend', | |
| ('friend', 'temp_enemy'): 'neutral', | |
| ('enemy', 'temp_friend'): 'neutral', | |
| ('enemy', 'temp_enemy'): 'great_enemy', | |
| ('neutral','temp_friend'): 'friend', | |
| ('neutral','temp_enemy'): 'enemy', | |
| } | |
| dignity_data = {} | |
| for name in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']: | |
| p = planets[name] | |
| sign = p['sign'] | |
| lord = SIGN_LORDS[p['sign_idx']] | |
| # Step 1: 旺/入庙/陷直接确定 | |
| if EXALTATION.get(name) == sign: | |
| compound = 'exalted' | |
| elif DEBILITATION.get(name) == sign: | |
| compound = 'debilitated' | |
| elif sign in OWN_SIGNS.get(name, []): | |
| compound = 'own_sign' | |
| else: | |
| # Step 2+3+4: 自然+临时→合成 | |
| natural = get_natural_rel(name, lord) | |
| if natural == 'own': | |
| compound = 'own_sign' | |
| else: | |
| # 找座主的实际位置 | |
| lord_sign_idx = planets[lord]['sign_idx'] if lord in planets else p['sign_idx'] | |
| temporal = get_temporal_rel(p['sign_idx'], lord_sign_idx) | |
| compound = COMPOUND_TABLE.get((natural, temporal), 'neutral') | |
| dignity_data[name] = {'basic': compound, 'compound': compound} | |
| # 7. Combustion check | |
| sun_lon = planets['Sun']['longitude'] | |
| combustion = {} | |
| for name in ['Moon','Mars','Mercury','Jupiter','Venus','Saturn']: | |
| is_retro = planets[name]['retrograde'] | |
| comb_result = check_combustion(name, planets[name]['longitude'], sun_lon, is_retro) | |
| is_combust = comb_result if isinstance(comb_result, bool) else comb_result.get('is_combust', False) | |
| if is_combust: | |
| diff = abs(planets[name]['longitude'] - sun_lon) | |
| if diff > 180: diff = 360 - diff | |
| combustion[name] = {'distance': round(diff, 2)} | |
| # 8. Chara Karakas | |
| karakas = calc_chara_karakas_7k8k(planets) | |
| # 9. Aspects | |
| aspects = calc_aspects(planets) | |
| # 10. House lords | |
| house_lords = calc_house_lords(lagna['sign_idx']) | |
| # Add planet positions to house lords | |
| for h, info in house_lords.items(): | |
| planet = info['lord'] | |
| if planet in planets: | |
| info['lord_house'] = planets[planet]['house'] | |
| # 11. Vimsottari Dasha (PyJHora — no fallback) | |
| dashas = _dasha_pyjhora(year, month, day, hour, minute, lat, lon, _tz_offset, profile['id']) | |
| # 12. Shadbala (PyJHora + 9 bug fixes — no fallback) | |
| shadbala_data = _shadbala_pyjhora( | |
| year, month, day, hour, minute, lat, lon, _tz_offset, profile['id'] | |
| ) | |
| # 13. Moon phase | |
| moon_sun_diff = (planets['Moon']['longitude'] - planets['Sun']['longitude']) % 360 | |
| is_waxing = moon_sun_diff < 180 | |
| # 14. Digbala | |
| digbala = {} | |
| for name in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']: | |
| digbala[name] = get_digbala(name, planets[name]['house']) | |
| # 15. Special Points (AL, UL) | |
| special_points = calc_special_points(lagna, planets) | |
| # 16. Transit positions (current slow planet positions) | |
| transits = calc_transits(lagna['sign_idx'], planets['Moon']['sign_idx'], profile['id']) | |
| # 16b. Graha drishti (吠陀相位) + 未来5年过运时间线 (动态预测弹药) | |
| graha_drishti = calc_graha_drishti(planets, lagna['sign_idx']) | |
| transit_timeline = calc_transit_timeline(lagna['sign_idx'], planets['Moon']['sign_idx'], calculation_profile=profile['id']) | |
| # 17. Bhava Bala, Special Lagnas, Vargeeya Bala (via PyJHora) | |
| bhava_bala = None | |
| special_lagnas = None | |
| vargeeya_bala = None | |
| if any([_bhava_bala_pyjhora, _special_lagnas_pyjhora, _vargeeya_bala_pyjhora]): | |
| try: | |
| tz = pytz.timezone(tz_str) | |
| _tz_dt = tz.localize(datetime(year, month, day, hour, minute)) | |
| _tz_offset = _tz_dt.utcoffset().total_seconds() / 3600.0 | |
| if _bhava_bala_pyjhora: | |
| bhava_bala = _bhava_bala_pyjhora(year, month, day, hour, minute, lat, lon, _tz_offset, profile['id']) | |
| if _special_lagnas_pyjhora: | |
| special_lagnas = _special_lagnas_pyjhora(year, month, day, hour, minute, lat, lon, _tz_offset, profile['id']) | |
| if _vargeeya_bala_pyjhora: | |
| vargeeya_bala = _vargeeya_bala_pyjhora(year, month, day, hour, minute, lat, lon, _tz_offset, profile['id']) | |
| except Exception: | |
| pass | |
| return { | |
| 'calculation_profile': profile_meta(profile['id']), | |
| 'ayanamsa': ayanamsa, | |
| 'lagna': lagna, | |
| 'planets': planets, | |
| 'sav': ashtak['sarvashtakavarga'], | |
| 'sav_by_house': sav_by_house, | |
| 'bav': ashtak['bhinnashtakavarga'], | |
| 'd9': d9, 'd10': d10, 'd4': d4, 'd5': d5, | |
| 'divisional_charts': divisional_charts, | |
| 'vargottama': vargottama, | |
| 'dignity': dignity_data, | |
| 'combustion': combustion, | |
| 'karakas': karakas, | |
| 'aspects': aspects, | |
| 'house_lords': house_lords, | |
| 'dashas': dashas, | |
| 'shadbala': shadbala_data, | |
| 'moon_phase': {'waxing': is_waxing, 'sun_moon_diff': round(moon_sun_diff, 1)}, | |
| 'digbala': digbala, | |
| 'special_points': special_points, | |
| 'transits': transits, | |
| 'graha_drishti': graha_drishti, | |
| 'transit_timeline': transit_timeline, | |
| 'bhava_bala': bhava_bala, | |
| 'special_lagnas': special_lagnas, | |
| 'vargeeya_bala': vargeeya_bala, | |
| } | |
| # === TEST === | |
| if __name__ == '__main__': | |
| print("=== vedic-calculator v0.2 Full Test ===\n") | |
| # Gandhi: 1869-10-02, 07:12, Porbandar | |
| chart = calculate_full_chart(1869, 10, 2, 7, 12, 21.6417, 69.6293, "Asia/Kolkata") | |
| print(f"Ayanamsa (Lahiri): {chart['ayanamsa']:.4f}°") | |
| print(f"Lagna: {chart['lagna']['sign']} {chart['lagna']['deg_str']}") | |
| print(f"\n--- Planets ---") | |
| print(f" {'Planet':<10} {'Sign':<12} {'Deg':>8} {'H':>3} {'R':>2} {'Dignity':<16} {'Compound'}") | |
| for name in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu']: | |
| p = chart['planets'][name] | |
| r = 'R' if p['retrograde'] else '' | |
| dig = chart['dignity'].get(name, {}) | |
| basic = dig.get('basic', '-') | |
| compound = dig.get('compound', '-') | |
| print(f" {name:<10} {p['sign']:<12} {p['deg_str']:>8} {p['house']:>3} {r:>2} {str(basic):<16} {compound}") | |
| print(f"\n--- SAV by House ---") | |
| total = 0 | |
| for h in range(1, 13): | |
| s = chart['sav_by_house'][h] | |
| total += s['value'] | |
| print(f" {h}宫({s['sign'][:2]}): {s['value']}", end=' ') | |
| if h % 6 == 0: print() | |
| print(f" Total: {total}") | |
| print(f"\n--- Chara Karakas (7K) ---") | |
| for k, planet, deg in chart['karakas']['7k']: | |
| print(f" {k}: {planet} ({deg:.1f}°)") | |
| print(f" DK争议: 7K={chart['karakas']['dk_7k']}, 8K={chart['karakas']['dk_8k']}, 一致={chart['karakas']['dk_consistent']}") | |
| print(f"\n--- Aspects (top 5) ---") | |
| for a in chart['aspects'][:5]: | |
| print(f" {a['p1']}-{a['p2']}: {a['type']} ({a['degree_diff']}°, orb {a['orb']}°)") | |
| print(f"\n--- Dasha ---") | |
| for d in chart['dashas']: | |
| marker = '→' if d['is_current'] else ' ' | |
| print(f" {marker} {d['planet']:<10} {d['start']} ~ {d['end']} ({d['years']}yr)") | |
| print(f"\n--- D9 Navamsha ---") | |
| for name in ['Lagna','Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu']: | |
| sign = chart['d9'][name][0] | |
| varg = ' ★V' if chart['vargottama'].get(name, False) else '' | |
| print(f" {name:<10} → {sign}{varg}") | |
| print(f"\n--- Shadbala ---") | |
| if 'error' in chart['shadbala']: | |
| print(f" Error: {chart['shadbala']['error']}") | |
| else: | |
| for name, data in chart['shadbala'].items(): | |
| if isinstance(data, dict): | |
| total = data.get('total_rupas', data.get('total', '?')) | |
| print(f" {name:<10} {total}") | |
| print(f"\n--- Moon Phase ---") | |
| phase = chart['moon_phase'] | |
| print(f" {'盈月(Shukla)' if phase['waxing'] else '亏月(Krishna)'}, 距Sun {phase['sun_moon_diff']}°") | |
| print(f"\n--- Combustion ---") | |
| if chart['combustion']: | |
| for name, data in chart['combustion'].items(): | |
| print(f" {name}: {data}") | |
| else: | |
| print(" 无燃烧行星") | |
| print(f"\n--- House Lords ---") | |
| for h in range(1, 13): | |
| info = chart['house_lords'][h] | |
| print(f" {h}宫({info['domain']}): {info['lord']} → {info.get('lord_house','?')}宫") | |
| print(f"\n✅ 全部14个数据板块计算完成!") | |