Files
Jyotisha/scripts/jaimini.py
T
ClaudeandClaude Opus 5.5 4b223eb8ea fix(engine): apply #166 batch 2 rulings to functional roles, yogas, Chara / Narayana, Gulika, sunrise records and varga methods (BUG-1590..1598)
Astrologer ruling 2026-11-04 on #166, product decisions the same day (Q9
display only, later; Q12 record, do not switch).

- Q4 (BUG-1590): Gemini Saturn (8, 9) and Virgo Saturn (5, 6) are mixed
  (neutral), not pure benefics by trine priority; the astrologer's table for
  the eight ambiguous cells rides along as ambiguous_cell_notes. Other
  lordships are kept.
- Q10 (BUG-1591): Chara Dasha with both Scorpio / Aquarius lords in the sign
  is fixed at 12 years (Ketu exalted in Scorpio gave 13).
- Q11 (BUG-1592): legacy Narayana counts only the nine grahas as occupants.
- Q8 (BUG-1593): Narayana period reports the adjustment that took effect
  (Mercury in Virgo: 12, +0, not "12 + 1" capped silently).
- Q15 (BUG-1594): upagrahas carry both raw Saturn-part points and name the
  mapping (PyJHora-compatible Gulika = start, Maandi = midpoint; PVR 4.3 is the
  reverse and is listed, never claimed).
- Q12 / Q13 (BUG-1595): every day/night window, Shadbala and the birth
  Panchanga record their sunrise convention and hora method. No value changes.
- Q5 (BUG-1596): "Gajakesari Yoga" is PVR's strict definition (benefic
  association, not debilitated, combust or in a natural enemy's sign); the
  strong-Jupiter variant and the bare Moon-Jupiter kendra relation are
  separate rules outside the card's closed list. New sandbox helpers combust()
  and in_natural_enemy_sign().
- Q6 (BUG-1597): Matrunasa split into Raman 198 (named Parashari aspects,
  nodes for conjunction / hemming only) and Raman 199 (same parents-card row
  via canonical_mapping); each hit names its trigger; "high confidence" and
  mother's-health effects removed. Custom rules may set `triggers`.
- Q31 (BUG-1598): calc_varga names the D3 / D24 / D30 / D60 method; D3 / D24 /
  D60 of varga.py and divisional_charts_extended agree on every 0.1 deg.
- Q29: the 7-karaka note says PiK does not apply in that scheme.

tests/test_astrologer_rulings_166_batch2.py (25 tests). Rectification
identity and re-freeze follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-04 22:24:40 +08:00

1046 lines
43 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 + Upapada(UL),复用 dashaflow/jaimini-tropical 的 MIT 算法
- Karakamsha: AK在Navamsa中的上升(灵魂方向)
- Chara Dasha: KN Rao benchmark overall 95.83%,作为 covered timing 模块使用;事件应期仍需多系统确认
- 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
from datetime import datetime, timedelta
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'}
# 行星友好/敌对关系(用于Jaimini共主比较)
FRIENDSHIP = {
'Sun': {'friend': ['Moon', 'Mars', 'Jupiter'], 'enemy': ['Saturn', 'Venus'], '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']},
'Rahu': {'friend': ['Jupiter', 'Venus', 'Saturn'], 'enemy': ['Sun', 'Moon', 'Mars'], 'neutral': ['Mercury']},
'Ketu': {'friend': ['Mars', 'Venus', 'Saturn'], 'enemy': ['Sun', 'Moon'], 'neutral': ['Mercury', '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;Rahu 按 30° 减星座内度数排位)。BPHS 第 32 章次序:
# AK、AmK、BK、MK、PiK、PK、GK、DK,即 PiK 第 5、PK 第 6、GK 第 7、DK 第 8
# (度数最低者为 DK)。旧表把第 5/7/8 位写成 PK/DK/PiK(BUG-1204,上游
# yinduzhanxing 同样写错)。
KARAKA_8 = {
1: 'Atmakaraka', 2: 'Amatyakaraka', 3: 'Bhratrikaraka', 4: 'Matrikaraka',
5: 'Pitrukaraka', 6: 'Putrakaraka', 7: 'Gnatikaraka', 8: 'Darakaraka',
}
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(含Rahu,Ketu排除)。
8星制按传统规则以 ``30 - Rahu 的星座内度数`` 排序;其余行星
使用原始星座内度数。原始值和排序值都保留在结果中,便于审计。
"""
planets = {}
for pname, deg in planet_degrees.items():
if pname == 'Ketu':
continue
raw_degree = float(deg) % 30.0
effective_degree = (30.0 - raw_degree) % 30.0 if pname == 'Rahu' else raw_degree
planets[pname] = (raw_degree, effective_degree)
sorted_planets = sorted(planets.items(), key=lambda x: x[1][1], reverse=True)
results = {}
for rank, (pname, (raw_degree, effective_degree)) in enumerate(sorted_planets, 1):
if rank > 8:
break
karaka = KARAKA_8[rank]
results[karaka] = {
'planet': pname,
'degree_in_sign': round(raw_degree, 4),
'effective_degree_in_sign': round(effective_degree, 4),
'rahu_reverse_applied': pname == 'Rahu',
'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. 若落回本宫或本宫第七,则改取初算落点起第十宫(PVR §9.2):
落本宫 → 本宫第十;落本宫第七 → 本宫第四。
"""
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:
# 原值: (house_sign_idx + 9) % 12,两种例外都取原宫第十宫
# 新值: (pada_idx + 9) % 12,初算落点起第十宫(落第七宫时 = 原宫第四宫)
# 原因: BUG-1588,#166 第 27 条(占星师 2026-11-04,PVR 教材 §9.2);与
# special_lagnas.calculate_arudha_pada 统一,产品不再出现两个不同的 A10
pada_idx = (pada_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_darapada(asc_sign_idx: int, planet_longitudes: Dict[str, float]) -> Optional[Dict]:
"""计算Darapada/A7(第7宫Arudha),用于伴侣外显画像与关系维持审计。"""
seventh_house_idx = (asc_sign_idx + 6) % 12
result = calc_arudha_pada_for_house(seventh_house_idx, planet_longitudes)
if not result:
return None
result = dict(result)
second_idx = (result['sign_idx'] + 1) % 12
eighth_idx = (result['sign_idx'] + 7) % 12
result.update({
'house_num': 7,
'source_house_num': 7,
'name': 'Darapada (A7)',
'second_from_a7': _sign_name(second_idx),
'second_from_a7_lord': SIGN_LORDS[_sign_name(second_idx)],
'eighth_from_a7': _sign_name(eighth_idx),
'eighth_from_a7_lord': SIGN_LORDS[_sign_name(eighth_idx)],
'description': (
f"A7在{result['sign']},第二宫为{_sign_name(second_idx)},"
f"第八宫为{_sign_name(eighth_idx)},用于伴侣外显画像与关系维持压力审计。"
),
})
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}
# ───────────────────────────────────────────────
# Chara Dasha 重写:KN Rao Method (v6.1.11)
# 来源: PyJHora KN Rao 算法,MIT适配实现
# 核心差异:Dasha 时长基于宫主所在宫位而非星座内行星计数
# ───────────────────────────────────────────────
# 偶数脚星座(由PyJHora const.even_footed_signs定义)
# PyJHora v6: [3,4,5,9,10,11] = Cancer, Leo, Virgo, Capricorn, Aquarius, Pisces
# 注意: 这是"偶数季度"星座,不是传统samapada
_EVEN_FOOTED_SIGNS = {3, 4, 5, 9, 10, 11} # Cancer, Leo, Virgo, Capricorn, Aquarius, Pisces
# 行星尊贵对照(KN Rao Chara Dasha专用)
# 对齐PyJHora house_strengths_of_planets表的_EXALTED_UCCHAM(4)和_DEBILITATED_NEECHAM(0)
# 关键: Mercury在Virgo(5)是own sign (strength=5)非exalted(=4),不+1;在Gemini(2)同样own sign
# Rahu: exalted 1,2(Taurus,Gemini=strength=4); debilitated 7,8(Scorpio,Sagittarius=strength=0)
# Ketu: exalted 7,8(Scorpio,Sagittarius=strength=4); debilitated 1,2(Taurus,Gemini=strength=0)
_PLANET_DIGNITY_KNRAO = {
'Sun': {'exalted': {0}, 'debilitated': {6}}, # Aries / Libra
'Moon': {'exalted': {1}, 'debilitated': {7}}, # Taurus / Scorpio
'Mars': {'exalted': {9}, 'debilitated': {3}}, # Capricorn / Cancer
'Mercury': {'exalted': set(), 'debilitated': {11}}, # own sign in 2,5 → no exalted; Pisces=deb
'Jupiter': {'exalted': {3}, 'debilitated': {9}}, # Cancer / Capricorn
'Venus': {'exalted': {11}, 'debilitated': {5}}, # Pisces / Virgo
'Saturn': {'exalted': {6}, 'debilitated': {0}}, # Libra / Aries
'Rahu': {'exalted': {1, 2}, 'debilitated': {7, 8}}, # Taurus,Gemini / Scorpio,Sag
'Ketu': {'exalted': {7, 8}, 'debilitated': {1, 2}}, # Scorpio,Sag / Taurus,Gemini
}
# Chara Dasha 宫主动态判定
# Aquarius (sign 10): 传统主Saturn vs 共主Rahu — PyJHora用stronger_planet动态判定
# Scorpio (sign 7): 传统主Mars vs 共主Ketu — PyJHora用stronger_planet动态判定
_CHARA_DASHA_CO_LORD_SIGNS = {10, 7} # Aquarius, Scorpio 有共主争议
CHARA_DASHA_PROFILES = {
"kn_rao",
"goel_lagna_start",
"goel_gendered",
"goel_shastri_method2",
}
def _jaimini_planet_dignity_level(planet: str, sign_idx: int) -> int:
"""
计算行星在指定星座的尊严层级(用于Jaimini共主比较)。
基于PyJHora _stronger_planet_new 尊严比较链。
层级值: 6=exalted, 5=own_sign, 4=friendly, 3=neutral, 2=enemy, 1=debilitated
"""
dignities = _PLANET_DIGNITY_KNRAO.get(planet, {})
if not dignities:
return 2 # default: enemy level for unknown planets
# Check exalted (highest dignity)
if sign_idx in dignities.get('exalted', set()):
return 6
# Check debilitated (lowest)
if sign_idx in dignities.get('debilitated', set()):
return 1
# Check own sign (lord association)
sign_name = SIGNS[sign_idx]
traditional_lord = SIGN_LORDS.get(sign_name, '')
if planet == traditional_lord:
# For Mercury: own sign is Gemini or Virgo (not exalted)
return 5
# Check friendship
friendship = FRIENDSHIP.get(planet, {})
if planet in friendship.get('friend', []):
friend_planets = friendship['friend']
if traditional_lord in friend_planets:
return 4
if planet in friendship.get('enemy', []):
enemy_planets = friendship['enemy']
if traditional_lord in enemy_planets:
return 2
# Neutral
return 3
_CO_LORD_GRAHAS = ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu')
def _rasi_drishti(source_sign_idx: int, target_sign_idx: int) -> bool:
"""Jaimini sign aspect: movable -> fixed (not the next), fixed -> movable
(not the previous), dual -> the other duals."""
if source_sign_idx in (0, 3, 6, 9):
return target_sign_idx in (1, 4, 7, 10) and target_sign_idx != (source_sign_idx + 1) % 12
if source_sign_idx in (1, 4, 7, 10):
return target_sign_idx in (0, 3, 6, 9) and target_sign_idx != (source_sign_idx - 1) % 12
return target_sign_idx in (2, 5, 8, 11) and target_sign_idx != source_sign_idx
def _co_lord_strength(longitudes: Dict[str, float], planet: str) -> Tuple[int, int, int, int, float]:
"""Strength key of one Scorpio / Aquarius co-lord where it stands (BUG-1566).
Same order as PyJHora `house._stronger_planet_new`, Rath (Narayana Dasa
p.43 (d)) and references/prashna-complete-guide.md (dual lords):
1. more other bodies in its sign;
2. more of Jupiter, Mercury and its dispositor in or aspecting (Rasi
Drishti) its sign;
3. dignity in the sign it occupies (exalted > own > other > debilitated);
4. dual > fixed > movable sign;
5. more degrees in the sign.
"""
sign_idx = _get_planet_house(longitudes, planet)
companions = sum(
1 for other in _CO_LORD_GRAHAS
if other != planet and other in longitudes and _get_planet_house(longitudes, other) == sign_idx
)
dispositor = SIGN_LORDS[SIGNS[sign_idx]]
support = 0
for role in ('Jupiter', 'Mercury', dispositor):
if role not in longitudes:
continue
role_sign = _get_planet_house(longitudes, role)
if role_sign == sign_idx or _rasi_drishti(role_sign, sign_idx):
support += 1
dignity = _jaimini_planet_dignity_level(planet, sign_idx)
modality = 2 if sign_idx % 3 == 2 else 1 if sign_idx % 3 == 1 else 0
return companions, support, dignity, modality, float(longitudes[planet]) % 30
def _jaimini_stronger_planet(longitudes: Dict[str, float], planet_a: str,
planet_b: str, sign_idx: int) -> str:
"""
Stronger of two co-lords of ``sign_idx``, judged where each one stands.
Before BUG-1566 this compared both planets' dignity *in ``sign_idx``*
(the dasha sign) rather than in the signs they occupy, so Scorpio always
went to Ketu (exalted there) and Aquarius always to Saturn (own sign),
whatever the chart. Now: `_co_lord_strength` levels 1-5, then the
natural-strength order as a last tie-break.
"""
key_a = _co_lord_strength(longitudes, planet_a)
key_b = _co_lord_strength(longitudes, planet_b)
if key_a > key_b:
return planet_a
if key_b > key_a:
return planet_b
naisargika_order = ['Sun', 'Moon', 'Venus', 'Jupiter', 'Mercury', 'Mars', 'Saturn']
idx_a = naisargika_order.index(planet_a) if planet_a in naisargika_order else 99
idx_b = naisargika_order.index(planet_b) if planet_b in naisargika_order else 99
return planet_a if idx_a <= idx_b else planet_b
def _resolve_chara_dasha_lord(longitudes, sign_idx):
"""
解析Chara Dasha宫主(天蝎:火星 / 计都;水瓶:土星 / 罗睺)。
BUG-1566: one co-lord in the sign itself and the other elsewhere -> the
one elsewhere (K.N. Rao, Predicting through Jaimini's Chara Dasha; Rath,
Narayana Dasa p.43 (c); PyJHora basic rule; prashna guide rule 1).
Otherwise the stronger by `_jaimini_stronger_planet`, judged where each
planet stands. Both in the sign keep the stronger-planet result; the
period is then fixed at 12 years (BUG-1591, #166 Q10).
其他星座: 使用传统宫主
"""
sign_name = SIGNS[sign_idx]
traditional_lord = SIGN_LORDS.get(sign_name, '')
pair = {10: ('Saturn', 'Rahu'), 7: ('Mars', 'Ketu')}.get(sign_idx)
if pair is None:
return traditional_lord
first, second = pair
first_present = first in longitudes and longitudes[first] >= 0
second_present = second in longitudes and longitudes[second] >= 0
if not (first_present and second_present):
return first if first_present else second
first_in = _get_planet_house(longitudes, first) == sign_idx
second_in = _get_planet_house(longitudes, second) == sign_idx
if first_in and not second_in:
return second
if second_in and not first_in:
return first
return _jaimini_stronger_planet(longitudes, first, second, sign_idx)
def _sign_is_even_footed(sign_idx: int) -> bool:
"""判断星座是否为偶数脚星座(用于KN Rao方向判定)。"""
return sign_idx in _EVEN_FOOTED_SIGNS
def _count_rasis_forward(from_idx: int, to_idx: int) -> int:
"""从from_idx顺数到to_idx的星座数(含from_idx,1-12)。"""
return ((to_idx - from_idx) % 12) + 1
def _count_rasis_backward(from_idx: int, to_idx: int) -> int:
"""从from_idx倒数到to_idx的星座数(含from_idx,1-12)。"""
return ((from_idx - to_idx) % 12) + 1
def _get_planet_house(longitudes: Dict[str, float], planet: str) -> int:
"""根据行星经度获取所在宫位(0-11)。"""
return int(longitudes.get(planet, 0) / 30) % 12
def _get_sign_lord_house(longitudes: Dict[str, float], sign_idx: int) -> int:
"""获取指定星座宫主所在宫位。"""
sign_name = SIGNS[sign_idx]
lord = SIGN_LORDS[sign_name]
return _get_planet_house(longitudes, lord)
def _chara_dasha_duration_profile(
longitudes: Dict[str, float],
sign_idx: int,
*,
profile: str = "kn_rao",
) -> int:
"""
KN Rao Chara Dasha 大运时长计算 v6.1.12。
对齐PyJHora _dhasa_duration_knrao_method(已验证95.42%→目标100%):
1. 获取当前星座的宫主(含Rahu/Ketu共主覆写)
2. 获取宫主所在宫位
3. 若星座为偶数脚:从宫主数到本星座(顺数);否则从本星座数到宫主(顺数)
4. count - 1 → years
5. 若years ≤ 0:years = 12(先于尊贵调整,对齐PyJHora)
6. 尊贵调整(对齐PyJHora house_strengths_of_planets):
若宫主在所在宫位 ⟹ Exalted(+1);Debilitated(-1)
Mercury在Virgo/Gemini是own sign非exalted,不+1
"""
if profile not in CHARA_DASHA_PROFILES:
raise ValueError(f"unsupported Chara Dasha profile: {profile}")
if profile == "goel_lagna_start":
lord = SIGN_LORDS[SIGNS[sign_idx]]
lord_house = _get_planet_house(longitudes, lord)
if lord_house == sign_idx:
return 12
return _count_rasis_forward(sign_idx, lord_house)
if profile == "goel_gendered":
lord = SIGN_LORDS[SIGNS[sign_idx]]
lord_house = _get_planet_house(longitudes, lord)
if lord_house == sign_idx:
return 12
if (lord_house - sign_idx) % 12 == 6:
return 10
if sign_idx % 2 == 0:
count = _count_rasis_forward(lord_house, sign_idx)
else:
count = _count_rasis_backward(lord_house, sign_idx)
return max(1, count - 1)
# KN Rao keeps Scorpio/Aquarius dual-lord separation; start/order rules are
# handled by the progression helper.
lord = _resolve_chara_dasha_lord(longitudes, sign_idx)
lord_house = _get_planet_house(longitudes, lord)
if _sign_is_even_footed(sign_idx):
count = _count_rasis_forward(lord_house, sign_idx)
else:
count = _count_rasis_forward(sign_idx, lord_house)
years = count - 1
# 对齐PyJHora: 先检查≤0再加减尊贵
if years <= 0:
years = 12
# 原值: 天蝎 / 水瓶两颗主星都在本宫时,选出的主星入旺(计都在天蝎)再 +1,得 13 年
# 新值: 两颗主星都在本宫固定 12 年,不再加减
# 原因: BUG-1591,#166 第 10 条(占星师 2026-11-04;Rath, Narayana Dasa p.43 (a))
pair = {10: ('Saturn', 'Rahu'), 7: ('Mars', 'Ketu')}.get(sign_idx)
if pair and all(p in longitudes and _get_planet_house(longitudes, p) == sign_idx for p in pair):
return 12
# This remains the historical PyJHora-aligned local KN Rao profile. The
# source ledger keeps the no-extra-year K.N. Rao textual boundary visible
# for later formula reconciliation.
dignities = _PLANET_DIGNITY_KNRAO.get(lord, {})
if dignities:
if lord_house in dignities.get('exalted', set()):
years += 1
elif lord_house in dignities.get('debilitated', set()):
years -= 1
return years
def _chara_dasha_duration_knrao(longitudes: Dict[str, float], sign_idx: int) -> int:
return _chara_dasha_duration_profile(longitudes, sign_idx, profile="kn_rao")
def _fourth_from_lagna_prakriti(asc_sign_idx: int) -> int:
if asc_sign_idx % 2 == 0:
return (asc_sign_idx + 3) % 12
return (asc_sign_idx - 3) % 12
def _chara_progression_profile(
asc_sign_idx: int,
longitudes: Dict[str, float],
*,
profile: str = "kn_rao",
gender: str | None = None,
) -> list:
"""
KN Rao Chara Dasha 星座序列生成。
算法(对齐PyJHora _dhasa_progression_knrao_method):
1. 起始 = 上升星座
2. 检查第9宫:若为偶数脚 → 逆向,否则正向
3. 生成12个星座的顺序
"""
if profile not in CHARA_DASHA_PROFILES:
raise ValueError(f"unsupported Chara Dasha profile: {profile}")
start_idx = asc_sign_idx
if profile == "goel_gendered" and (gender or "").lower().startswith("f"):
start_idx = _fourth_from_lagna_prakriti(asc_sign_idx)
ninth_idx = (asc_sign_idx + 8) % 12
if _sign_is_even_footed(ninth_idx):
return [(start_idx + 12 - i) % 12 for i in range(12)]
return [(start_idx + i) % 12 for i in range(12)]
def _chara_progression_knrao(asc_sign_idx: int, longitudes: Dict[str, float]) -> list:
return _chara_progression_profile(asc_sign_idx, longitudes, profile="kn_rao")
def _jd_to_date_tuple(jd: float):
"""将Julian Day转换为(year, month, day)元组。使用简化算法。"""
try:
import datetime
import math
# 简化转换:约化儒略日
jd_i = int(jd + 0.5)
f = (jd + 0.5) - jd_i
if jd_i >= 2299161:
a = (jd_i - 1867216.25) / 36524.25
jd_i += 1 + int(a - int(a) / 4)
b = jd_i + 1524
c = (b - 122.1) / 365.25
d = int(365.25 * c)
e = (b - d) / 30.6001
day = b - d - int(30.6001 * e) + f
month = e - 1 if e < 14 else e - 13
year = c - 4716 if month > 2 else c - 4715
return (int(year), int(month), int(day))
except Exception:
return (2000, 1, 1)
def calc_chara_dasha(asc_sign_idx: int,
planet_longitudes: Dict[str, float],
birth_year: int, birth_month: int,
birth_day: int = 1,
profile: str = "kn_rao",
gender: str | None = None) -> Dict:
"""
Chara Dasha计算(v6.1.11重写:KN Rao Method)
来源:PyJHora chara.py KN Rao方法(AGPL算法 -> MIT独立实现)
对齐目标:与PyJHora KN Rao method ≥95%匹配
规则:
- 序列:自上升起,第9宫决定顺逆
- 时长:基于宫主所在宫位而非行星计数
- 尊贵:Exalted +1年 / Debilitated -1年
"""
progression = _chara_progression_profile(
asc_sign_idx,
planet_longitudes,
profile=profile,
gender=gender,
)
dasha_sequence = []
for i, sign_idx in enumerate(progression):
sign_name = SIGNS[sign_idx]
if profile == "goel_lagna_start":
lord = SIGN_LORDS[sign_name]
else:
lord = _resolve_chara_dasha_lord(planet_longitudes, sign_idx)
duration = _chara_dasha_duration_profile(planet_longitudes, sign_idx, profile=profile)
# 宮主所在宫位
lord_house = _get_planet_house(planet_longitudes, lord)
lord_house_name = SIGNS[lord_house]
# 尊贵状态
dignities = _PLANET_DIGNITY_KNRAO.get(lord, {})
dignity_status = 'none'
if profile == "goel_lagna_start":
dignity_status = 'not_applied'
elif dignities:
exalted_set = dignities.get('exalted', set())
debil_set = dignities.get('debilitated', set())
if lord_house in exalted_set:
dignity_status = 'exalted'
elif lord_house in debil_set:
dignity_status = 'debilitated'
dasha_sequence.append({
'sign': sign_name,
'sign_idx': sign_idx,
'lord': lord,
'lord_in_sign': lord_house_name,
'lord_in_sign_idx': lord_house,
'duration_years': duration,
'dignity_adjustment': dignity_status,
'order': i + 1,
})
total_years = sum(d['duration_years'] for d in dasha_sequence)
return {
'method': f'Chara Dasha ({profile})',
'variant_profile': profile,
'variant_profile_status': 'profiled',
'gender': gender,
'ascendant': SIGNS[asc_sign_idx],
'ascendant_idx': asc_sign_idx,
'progression_source': '9th_house_direction',
'dasha_sequence': dasha_sequence,
'total_cycle_years': total_years,
'capability_status': 'covered',
}
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,
profile: str = "kn_rao",
gender: str | None = None) -> Dict:
"""
Chara Dasha 完整3层计算(MD → AD → PD)
使用KN Rao方法(v6.1.11重写)。
Antardasha:等分法(parent/12),序列为Maha序列偏移1位。
"""
base = calc_chara_dasha(
asc_sign_idx,
planet_longitudes,
birth_year,
birth_month,
birth_day,
profile=profile,
gender=gender,
)
progression = _chara_progression_profile(
asc_sign_idx,
planet_longitudes,
profile=profile,
gender=gender,
)
for md in base['dasha_sequence']:
md_sign_idx = md['sign_idx']
md_duration_years = md['duration_years']
if profile == "goel_shastri_method2":
# Goel's Method 2 worked example gives the first sub-period to the
# mahadasha sign and then follows that sign's own direct/indirect order.
ninth_from_md = (md_sign_idx + 8) % 12
if _sign_is_even_footed(ninth_from_md):
antar_sequence = [(md_sign_idx + 12 - i) % 12 for i in range(12)]
else:
antar_sequence = [(md_sign_idx + i) % 12 for i in range(12)]
elif profile == "kn_rao":
# K.N. Rao, Predicting through Jaimini's Chara Dasha, printed
# pp.35-36: each mahadasha sign runs its own direction (reverse
# when the 9th from it is even-footed) and its own sign comes
# last. The book's Scorpio MD (pp.49-50) runs Libra .. Scorpio and
# its Capricorn MD (pp.51-52) opens with Sagittarius (golden rows
# in references/oracle/chara_narayana_book_golden_cases_2026_08_28.json).
# Before BUG-1567 every MD reused the lagna order shifted by one,
# which matches the book only for the first MD.
if _sign_is_even_footed((md_sign_idx + 8) % 12):
antar_sequence = [(md_sign_idx - i) % 12 for i in range(1, 13)]
else:
antar_sequence = [(md_sign_idx + i) % 12 for i in range(1, 13)]
else:
# Antardasha序列:Mahadasha序列偏移1(Goel 两档未找到出处,沿用旧序)
antar_sequence = progression[1:] + progression[:1]
antardasha_list = []
for j, ad_sign_idx in enumerate(antar_sequence):
ad_sign = SIGNS[ad_sign_idx]
ad_lord = SIGN_LORDS[ad_sign]
ad_duration = round(md_duration_years / 12.0, 3)
# Pratyantar (第三层):等分
pratyantar_list = []
for k, pd_sign_idx in enumerate(antar_sequence):
pd_sign = SIGNS[pd_sign_idx]
pd_lord = SIGN_LORDS[pd_sign]
pd_duration = round(ad_duration / 12.0, 4)
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,
})
md['antardashas'] = antardasha_list
base['has_antardasha'] = True
base['has_pratyantar'] = True
base['antardasha_method'] = (
'equal_division_12; kn_rao: own direction per mahadasha sign, mahadasha sign last (K.N. Rao pp.35-36)'
if profile == 'kn_rao' else
'equal_division_12; goel_shastri_method2: mahadasha sign first, its own direction'
if profile == 'goel_shastri_method2' else
'equal_division_12; lagna order shifted by one (unsourced, unchanged)'
)
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 _julian_day(year: int, month: int, day: int, hour: float = 12.0) -> float:
if month <= 2:
year -= 1
month += 12
a = int(year / 100)
b = 2 - a + int(a / 4)
return (
int(365.25 * (year + 4716))
+ int(30.6001 * (month + 1))
+ day
+ b
- 1524.5
+ hour / 24.0
)
def _solar_obliquity(jd: float) -> float:
t = (jd - 2451545.0) / 36525.0
eps0 = 84381.448 - 46.8150 * t - 0.00059 * t * t + 0.001813 * t * t * t
return eps0 / 3600.0
def calc_sunrise_utc_hours(year: int, month: int, day: int, lat: float, lon: float) -> float:
"""
Calculate sunrise UTC decimal hours for a date/location.
MIT-adapted from jaimini-tropical's sunrise helper; accuracy is sufficient for
product-facing Special Lagna timing and avoids a hard dependency on SwissEph.
"""
jd = _julian_day(year, month, day, 12.0)
mean_anomaly = (357.5291 + 0.98560028 * (jd - 2451545.0)) % 360
center = (
1.9148 * math.sin(math.radians(mean_anomaly))
+ 0.0200 * math.sin(math.radians(2 * mean_anomaly))
+ 0.0003 * math.sin(math.radians(3 * mean_anomaly))
)
sun_lon = (mean_anomaly + center + 180.10248 + 0.000048 * (jd - 2451545.0) * 360) % 360
eps = _solar_obliquity(jd)
declination = math.degrees(
math.asin(math.sin(math.radians(sun_lon)) * math.sin(math.radians(eps)))
)
lat_rad = math.radians(lat)
dec_rad = math.radians(declination)
cos_ha = (
math.cos(math.radians(90.833))
- math.sin(lat_rad) * math.sin(dec_rad)
) / (math.cos(lat_rad) * math.cos(dec_rad))
cos_ha = max(-1.0, min(1.0, cos_ha))
hour_angle = math.degrees(math.acos(cos_ha))
b = math.radians(360.0 * (jd - 2451545.0 - 0.5) / 365.25)
eq_time = 229.18 * (
0.000075
+ 0.001868 * math.cos(b)
- 0.032077 * math.sin(b)
- 0.014615 * math.cos(2 * b)
- 0.040849 * math.sin(2 * b)
)
solar_noon = (720.0 - 4.0 * lon - eq_time) / 60.0
return (solar_noon - hour_angle / 15.0) % 24.0
def _elapsed_ghatis_from_sunrise(sunrise_utc_hours: float, birth_utc_hours: float) -> float:
diff_hours = birth_utc_hours - sunrise_utc_hours
if diff_hours < 0:
diff_hours += 24
return diff_hours / 0.4
def _special_lagna_payload(name: str, full_name: str, sign_idx: int, degree_in_sign: float,
ghatis_elapsed: float, night_birth: bool = False) -> Dict:
sign = _sign_name(sign_idx)
payload = {
'name': name,
'full_name': full_name,
'sign': sign,
'sign_idx': sign_idx % 12,
'lord': SIGN_LORDS[sign],
'degree_in_sign': round(degree_in_sign % 30, 4),
'longitude': round((sign_idx % 12) * 30 + (degree_in_sign % 30), 4),
'ghatis_elapsed': round(ghatis_elapsed, 4),
}
if night_birth:
payload['night_birth'] = True
return payload
def calc_special_lagnas_precise(
asc_sign_idx: int,
year: int,
month: int,
day: int,
hour: int,
minute: float = 0,
lat: float = 0.0,
lon: float = 0.0,
tz_offset: float = 0.0,
second: float = 0.0,
) -> Dict:
"""
Sunrise-correct Jaimini Special Lagnas: HL/GL/VL.
Uses local birth time, converts it to UTC, calculates local sunrise in UTC,
then maps elapsed Ghatis from sunrise to HL/GL. VL remains Ascendant-derived.
"""
whole_minute = int(minute)
second_total = (float(minute) - whole_minute) * 60.0 + float(second)
whole_second = int(second_total)
microsecond = int(round((second_total - whole_second) * 1_000_000))
if microsecond >= 1_000_000:
whole_second += 1
microsecond -= 1_000_000
local_dt = datetime(year, month, day, int(hour), whole_minute, whole_second, microsecond)
utc_dt = local_dt - timedelta(hours=tz_offset)
birth_utc_hours = utc_dt.hour + utc_dt.minute / 60.0 + utc_dt.second / 3600.0
sunrise_utc = calc_sunrise_utc_hours(utc_dt.year, utc_dt.month, utc_dt.day, lat, lon)
ghatis = _elapsed_ghatis_from_sunrise(sunrise_utc, birth_utc_hours)
ghati_floor = int(ghatis)
fraction = ghatis - ghati_floor
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
vl_from_hl_idx = (hl_idx * 3) % 12
pp_idx = (hl_idx + gl_idx) % 12
vil_idx = int((ghatis * 2)) % 12
sunrise_local_hours = (sunrise_utc + tz_offset) % 24
sunrise_local_minutes = int(round(sunrise_local_hours * 60)) % (24 * 60)
local_birth_hours = int(hour) + whole_minute / 60.0 + whole_second / 3600.0 + microsecond / 3_600_000_000.0
before_sunrise = local_birth_hours < sunrise_local_hours
return {
'method': 'Special Lagnas HL/GL/VL sunrise-correct (jaimini-tropical MIT adapted)',
'capability_status': 'covered',
'precision': 'sunrise_correct',
'sunrise_utc_hours': round(sunrise_utc, 4),
'sunrise_local_time': f'{sunrise_local_minutes // 60:02d}:{sunrise_local_minutes % 60:02d}',
'birth_utc_hours': round(birth_utc_hours, 4),
'ghatis_elapsed_from_sunrise': round(ghatis, 4),
'HL': _special_lagna_payload('HL', 'Hora Lagna', hl_idx, fraction * 30.0, ghatis, before_sunrise),
'GL': _special_lagna_payload('GL', 'Ghati Lagna', gl_idx, fraction * 30.0, ghatis, before_sunrise),
'PP': _special_lagna_payload('PP', 'Pranapada Lagna', pp_idx, fraction * 30.0, ghatis, before_sunrise),
'ViL': _special_lagna_payload('ViL', 'Vighati Lagna', vil_idx, fraction * 30.0, ghatis, before_sunrise),
'VL': {
'name': 'VL',
'full_name': 'Varnada Lagna',
'sign': _sign_name(vl_idx),
'sign_idx': vl_idx,
'lord': SIGN_LORDS[_sign_name(vl_idx)],
'method': 'Ascendant sign × 3',
'vl_from_hl': {
'sign': _sign_name(vl_from_hl_idx),
'sign_idx': vl_from_hl_idx,
'lord': SIGN_LORDS[_sign_name(vl_from_hl_idx)],
},
},
'note': 'HL/GL/PP/ViL以出生地日出为起点计算;GL与ViL对细分钟边界敏感,出生时间不准时应结合生时校正。'
}
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
vl_from_hl_idx = (hl_idx * 3) % 12
pp_idx = (hl_idx + gl_idx) % 12
vil_idx = int(ghatis * 2) % 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)]},
'PP': {'sign': _sign_name(pp_idx), 'sign_idx': pp_idx, 'lord': SIGN_LORDS[_sign_name(pp_idx)]},
'ViL': {'sign': _sign_name(vil_idx), 'sign_idx': vil_idx, 'lord': SIGN_LORDS[_sign_name(vil_idx)]},
'VL': {
'sign': _sign_name(vl_idx),
'sign_idx': vl_idx,
'lord': SIGN_LORDS[_sign_name(vl_idx)],
'vl_from_hl': {'sign': _sign_name(vl_from_hl_idx), 'sign_idx': vl_from_hl_idx, 'lord': SIGN_LORDS[_sign_name(vl_from_hl_idx)]},
},
'note': 'HL/GL/VL/PP/ViL对日出非常敏感;本函数用于结构化补齐,精确版本需接入当地日出。'
}