fix(avastha): keep simultaneous conditions and use the PVR activity formula

Upstream e628118d, 7d545039, f75a6e77, and ac565836. Dignity follows the sign, Deeptadi keeps every condition that applies, and Shayanadi hours come from the previous Hindu sunrise. BUG-1389.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-18 16:57:45 +08:00
co-authored by Grok (agent loop) Cursor
parent 0bd254cf18
commit 83c97a8976
9 changed files with 365 additions and 49 deletions
+160 -46
View File
@@ -19,6 +19,60 @@ Avastha Calculator - 行星状态计算器
from typing import Dict, List, Tuple
from enum import Enum
import json
import math
from bisect import bisect_right
from pathlib import Path
with (Path(__file__).resolve().parents[1] / 'references' / 'shat_bala_constants.json').open(encoding='utf-8') as _handle:
NATURAL_RELATIONSHIPS = json.load(_handle)['natural_relationships']
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
def calculate_pvr_shayanadi(planet, longitude, moon_longitude, asc_longitude, hours_since_sunrise):
"""PVR §15.4.4/Table 36; running ghati, not elapsed fractional ghatis.
Activity intensity requires the name's Sanskrit first-sound index and is
deliberately not guessed from an English display name.
"""
order = ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu')
if planet not in order:
raise ValueError('Unsupported Shayanadi planet')
values = (longitude, moon_longitude, asc_longitude, hours_since_sunrise)
if any(isinstance(value, bool) or not math.isfinite(float(value)) for value in values):
raise ValueError('Finite numeric Shayanadi inputs required')
if hours_since_sunrise < 0:
raise ValueError('Use the preceding sunrise, not a future sunrise')
longitude, moon_longitude, asc_longitude = (float(value) % 360 for value in values[:3])
boundaries = [i * 40 / 3 for i in range(1, 27)]
c = bisect_right(boundaries, longitude) + 1
p = order.index(planet) + 1
a = bisect_right([i * 10 / 3 for i in range(1, 9)], longitude % 30) + 1
m = bisect_right(boundaries, moon_longitude) + 1
g = math.floor(hours_since_sunrise * 2.5) + 1
l = int(asc_longitude // 30) + 1
total = c * p * a + m + g + l
index = (total - 1) % 12 + 1
states = ('Shayana', 'Upaveshana', 'Netrapani', 'Prakashana', 'Gamana', 'Agamana',
'Sabha', 'Agama', 'Bhojana', 'Nrityalipsa', 'Kautuka', 'Nidra')
return {
'state': states[index - 1],
'state_index': index,
'sign': SIGNS[int(longitude // 30)],
'meaning': 'Activity state from the PVR constellation, navamsa and running-ghati formula.',
'method_status': 'pvr_15_4_4_running_ghati',
'components': {
'planet_nakshatra': c,
'planet_index': p,
'navamsa_part': a,
'moon_nakshatra': m,
'running_ghati': g,
'lagna_sign_number': l,
'formula_sum': total,
},
'activity_intensity_status': 'requires_name_first_sound_index',
}
class BalaAvastha(Enum):
"""年龄状态(5种生命阶段)"""
@@ -39,11 +93,12 @@ class DeeptadiAvastha(Enum):
DEEPTA = "Deepta" # 光辉(Exalted)
SWASTHA = "Swastha" # 舒适(Own Sign)
MUDITA = "Mudita" # 快乐(Friend's Sign)
SHANTA = "Shanta" # 平静(Neutral Sign)
DINA = "Dina" # 沮丧(Enemy Sign)
VIKALA = "Vikala" # 残缺(Debilitated)
KHALA = "Khala" # 恶意(Combust)
KOPA = "Kopa" # 愤怒(Defeated in War)
SHANTA = "Shanta" # 平静(Friend's Sign)
DINA = "Dina" # 低落(Neutral Sign)
DUHKHITA = "Duhkhita" # 困苦(Enemy Sign)
VIKALA = "Vikala" # 残缺(Joined by malefics)
KHALA = "Khala" # 负担(Debilitated; Science of Light)
KOPA = "Kopa" # 愤怒(Combust)
GARVITA = "Garvita" # 骄傲(Victorious in War)
KSHUDITA = "Kshudita" # 饥饿(In Sandhi)
TRUSHITA = "Trushita" # 口渴(In Gandanta)
@@ -127,7 +182,7 @@ class AvasthaCalculator:
def calculate_all_avasthas(self, planet: str, sign: str, degree: float,
house: int, conjunctions: List[str] = None,
aspects: List[str] = None, is_combust: bool = False,
is_retrograde: bool = False) -> Dict[str, any]:
is_retrograde: bool = False, activity_context: Dict = None) -> Dict[str, any]:
"""
计算行星的所有Avastha状态
@@ -153,7 +208,7 @@ class AvasthaCalculator:
}
# 1. Bala Avastha(年龄状态)
result["avasthas"]["Bala"] = self._calculate_bala_avastha(degree)
result["avasthas"]["Bala"] = self._calculate_bala_avastha(degree, sign)
# 2. Jagrat Avastha(警觉状态)
result["avasthas"]["Jagrat"] = self._calculate_jagrat_avastha(planet, sign, house)
@@ -170,7 +225,25 @@ class AvasthaCalculator:
# 5. Shayanadi Avastha(姿态状态)
result["avasthas"]["Shayanadi"] = self._calculate_shayanadi_avastha(planet, sign)
for name in ('Deeptadi', 'Lajjitadi', 'Shayanadi'):
result['avasthas'][name]['method_status'] = 'simplified_heuristic'
if activity_context is not None:
result['avasthas']['Shayanadi'] = calculate_pvr_shayanadi(
planet,
SIGNS.index(sign) * 30 + degree,
activity_context['moon_longitude'],
activity_context['asc_longitude'],
activity_context['hours_since_sunrise'],
)
result['inputs'] = {
'conjunctions': conjunctions or [],
'aspects': aspects or [],
'is_combust': is_combust,
'is_retrograde': is_retrograde,
}
if activity_context is not None:
result['inputs']['activity_context'] = dict(activity_context)
# 6. 综合解读
result["interpretation"] = self._generate_interpretation(result["avasthas"])
@@ -179,11 +252,17 @@ class AvasthaCalculator:
return result
def _calculate_bala_avastha(self, degree: float) -> Dict:
def _calculate_bala_avastha(self, degree: float, sign: str = 'Aries') -> Dict:
"""计算Bala Avastha(年龄状态)"""
if not 0 <= degree < 30:
raise ValueError('Avastha degree must be within [0, 30)')
original_degree = degree
even_sign = SIGNS.index(sign) % 2 == 1
band = int(degree // 6)
degree = (4 - band if even_sign else band) * 6
if degree < 6:
state = BalaAvastha.INFANT
strength = 0.2
strength = 0.25
meaning = "婴儿期:行星力量微弱,潜力未开发,需要时间成长"
elif degree < 12:
state = BalaAvastha.YOUNG
@@ -195,40 +274,46 @@ class AvasthaCalculator:
meaning = "青年期:行星力量最强,充满活力,能够充分发挥作用"
elif degree < 24:
state = BalaAvastha.OLD
strength = 0.7
strength = None
meaning = "老年期:行星力量开始衰退,经验丰富但活力下降"
else:
state = BalaAvastha.DEAD
strength = 0.3
strength = 0.0
meaning = "死亡期:行星力量极弱,难以发挥作用,需要补救"
return {
"state": state.value,
"degree_range": f"{int(degree//6)*6}-{int(degree//6)*6+6}°",
"degree_range": f"{int(original_degree//6)*6}-{int(original_degree//6)*6+6}°",
"strength": strength,
"result_grade": 'negligible' if state == BalaAvastha.OLD else 'fraction',
"sign_parity": 'even' if even_sign else 'odd',
"method_status": 'bphs_age_sign_parity',
"meaning": meaning
}
def _calculate_jagrat_avastha(self, planet: str, sign: str, house: int) -> Dict:
"""计算Jagrat Avastha(警觉状态)"""
# 简化规则:
# Awake: 在角宫(1,4,7,10)
# Dreaming: 在续宫(2,5,8,11)
# Deep Sleep: 在果宫(3,6,9,12)
if house in [1, 4, 7, 10]:
if planet not in self.PLANET_SIGNS:
return {
'state': 'Not classified',
'house': house,
'method_status': 'node_dignity_not_assigned',
'meaning': '交点未指定尊贵度流派,不套用七曜警觉状态规则',
}
if self._is_own_sign(planet, sign) or self._is_exalted(planet, sign, 0):
state = JagratAvastha.AWAKE
meaning = "清醒状态:行星完全活跃,能够主动发挥作用,影响显著"
elif house in [2, 5, 8, 11]:
elif not self._is_enemy_sign(planet, sign) and not self._is_debilitated(planet, sign, 0):
state = JagratAvastha.DREAMING
meaning = "梦境状态:行星半活跃,作用不稳定,时强时弱"
else:
state = JagratAvastha.DEEP_SLEEP
meaning = "深睡状态:行星不活跃,难以发挥作用,影响微弱"
return {
"state": state.value,
"house": house,
"method_status": 'bphs_dignity_natural_relationships',
"meaning": meaning
}
@@ -236,29 +321,50 @@ class AvasthaCalculator:
is_combust: bool, aspects: List[str] = None) -> Dict:
"""计算Deeptadi Avastha(情绪状态)"""
aspects = aspects or []
# 检查入庙/落陷
if planet not in self.PLANET_SIGNS:
return {
'state': 'Not classified',
'meaning': '该口径未定义此天体的状态',
'aspect_effects': [],
'label_profile': 'science_of_light_deeptadi_natural_relationship_subset',
'claim_boundary': 'Classical seven-planet profile; no nodal dignity is assumed.',
}
conditions = {
'exalted': self._is_exalted(planet, sign, degree),
'debilitated': self._is_debilitated(planet, sign, degree),
'combust': bool(is_combust),
'own_sign': self._is_own_sign(planet, sign),
'natural_friend_sign': self._is_friend_sign(planet, sign),
'natural_enemy_sign': self._is_enemy_sign(planet, sign),
}
applicable_states = [label for key, label in (
('exalted', 'Deepta'), ('debilitated', 'Khala'), ('combust', 'Kopa'),
('own_sign', 'Swastha'), ('natural_friend_sign', 'Shanta'),
('natural_enemy_sign', 'Duhkhita')) if conditions[key]]
if not any(conditions[key] for key in ('own_sign', 'natural_friend_sign', 'natural_enemy_sign')):
applicable_states.append('Dina')
# 单标签顺序是兼容字段,无经典出处。并存条件全部留在 applicable_states。
if self._is_exalted(planet, sign, degree):
state = DeeptadiAvastha.DEEPTA
meaning = "光辉状态:行星处于最佳位置,能量充沛,带来积极结果"
elif self._is_debilitated(planet, sign, degree):
state = DeeptadiAvastha.VIKALA
meaning = "残缺状态:行星处于最弱位置,能量受损,带来挑战"
elif is_combust:
state = DeeptadiAvastha.KHALA
meaning = "恶意状态:行星被太阳燃烧,能量被压制,难以正常发挥"
meaning = "负担状态:行星落陷;采用 Science of Light 的 Khala 定义"
elif is_combust:
state = DeeptadiAvastha.KOPA
meaning = "愤怒状态:行星受太阳燃烧;对应 Kopa"
elif self._is_own_sign(planet, sign):
state = DeeptadiAvastha.SWASTHA
meaning = "舒适状态:行星在自己的星座,感觉自在,能够稳定发挥"
elif self._is_friend_sign(planet, sign):
state = DeeptadiAvastha.MUDITA
meaning = "快乐状态:行星在友好星座,得到支持,能够良好发挥"
elif self._is_enemy_sign(planet, sign):
state = DeeptadiAvastha.DINA
meaning = "沮丧状态:行星在敌对星座,受到压制,发挥受限"
else:
state = DeeptadiAvastha.SHANTA
meaning = "平静状态:行星在中性星座,不强不弱,平稳发挥"
meaning = "平静状态:行星在自然友星的星座;对应 Shanta"
elif self._is_enemy_sign(planet, sign):
state = DeeptadiAvastha.DUHKHITA
meaning = "困苦状态:行星在自然敌星的星座;对应 Duhkhita"
else:
state = DeeptadiAvastha.DINA
meaning = "低落状态:行星在自然中性星的星座;对应 Dina"
# 检查相位影响
aspect_effects = []
@@ -274,7 +380,15 @@ class AvasthaCalculator:
return {
"state": state.value,
"meaning": meaning,
"aspect_effects": aspect_effects
"aspect_effects": aspect_effects,
"label_profile": "science_of_light_deeptadi_natural_relationship_subset",
"conditions": conditions,
"applicable_states": applicable_states,
"legacy_single_label_note": "无经典出处",
"uncomputed_conditions": ["great_friend", "malefic_conjunction"],
"uncomputed_label": "未计算",
"source": "Science of Light, source p475; Khala is debility in this profile",
"claim_boundary": "Single-label priority is a legacy heuristic. Natural friendship only; great-friend and simultaneous conjunction states are not fully calculated. Khala follows Science of Light debility, not the alternative PVR malefic-sign definition.",
}
def _calculate_lajjitadi_avastha(self, planet: str, sign: str, house: int,
@@ -357,14 +471,14 @@ class AvasthaCalculator:
"""检查是否入庙"""
if planet in self.EXALTATION:
exalt_sign, exalt_degree = self.EXALTATION[planet]
return sign == exalt_sign and abs(degree - exalt_degree) < 5
return sign == exalt_sign
return False
def _is_debilitated(self, planet: str, sign: str, degree: float) -> bool:
"""检查是否落陷"""
if planet in self.DEBILITATION:
debil_sign, debil_degree = self.DEBILITATION[planet]
return sign == debil_sign and abs(degree - debil_degree) < 5
return sign == debil_sign
return False
def _is_own_sign(self, planet: str, sign: str) -> bool:
@@ -372,14 +486,14 @@ class AvasthaCalculator:
return sign in self.PLANET_SIGNS.get(planet, [])
def _is_friend_sign(self, planet: str, sign: str) -> bool:
"""检查是否在友好星座(简化版)"""
# 这里需要完整的行星友好关系表,暂时简化
return False
"""检查是否在自然友星星座。"""
lord = next((name for name, signs in self.PLANET_SIGNS.items() if sign in signs), None)
return lord in NATURAL_RELATIONSHIPS.get(planet, {}).get('friends', [])
def _is_enemy_sign(self, planet: str, sign: str) -> bool:
"""检查是否在敌对星座(简化版)"""
# 这里需要完整的行星敌对关系表,暂时简化
return False
"""检查是否在自然敌星星座。"""
lord = next((name for name, signs in self.PLANET_SIGNS.items() if sign in signs), None)
return lord in NATURAL_RELATIONSHIPS.get(planet, {}).get('enemies', [])
def _is_enemy_planet(self, planet1: str, planet2: str) -> bool:
"""检查两个行星是否敌对(简化版)"""
@@ -437,7 +551,7 @@ class AvasthaCalculator:
# 根据Bala Avastha
bala_strength = avasthas["Bala"]["strength"]
if bala_strength < 0.5:
if bala_strength is not None and bala_strength < 0.5:
remedies.append(f"行星处于弱势年龄阶段,建议佩戴{planet}对应的宝石")
remedies.append(f"在{planet}主管的日子进行布施和祈祷")
@@ -447,7 +561,7 @@ class AvasthaCalculator:
# 根据Deeptadi Avastha
deeptadi_state = avasthas["Deeptadi"]["state"]
if deeptadi_state in ["Vikala", "Khala", "Dina"]:
if deeptadi_state in ["Vikala", "Khala", "Duhkhita", "Kopa"]:
remedies.append(f"行星处于{deeptadi_state}状态,需要特别补救")
remedies.append(f"念诵{planet}的Beeja Mantra(种子咒)")
+13 -1
View File
@@ -16369,6 +16369,15 @@ def cmd_full_reading(args):
# ── Step 10.5: Avasthas 行星状态 (v4.4.0) ──
try:
from avastha_calculator import AvasthaCalculator
from saham_daynight import determine_daytime
avastha_solar_day = determine_daytime(
_birth_datetime_from_args(args), lat=args.lat, lon=args.lon, tz=args.tz, rising='hindu')
hours_since_sunrise = (avastha_solar_day['julian_day_ut'] - avastha_solar_day['sunrise_jd_ut']) * 24
avastha_activity_context = {
'moon_longitude': planet_lons['Moon'],
'asc_longitude': asc_deg,
'hours_since_sunrise': hours_since_sunrise,
}
avast_calc = AvasthaCalculator()
avast_result = {}
for pn, pd in planets.items():
@@ -16381,7 +16390,10 @@ def cmd_full_reading(args):
if isinstance(pd2, dict) and pd2.get('house') == house and p2 != pn]
avast_result[pn] = avast_calc.calculate_all_avasthas(
planet=pn, sign=sign, degree=deg_in_sign,
house=house, conjunctions=conjunctions)
house=house, conjunctions=conjunctions,
is_combust=bool(pd.get('combust', False)),
is_retrograde=bool(pd.get('retrograde', False)),
activity_context=avastha_activity_context)
report['modules']['avasthas'] = avast_result
except Exception as e:
report['errors'].append(f"avasthas: {e}")
+3
View File
@@ -1442,6 +1442,9 @@ def render_pl9_parity_markdown(packet: dict) -> str:
continue
def _state(name: str) -> str:
state = _dict(states.get(name))
applicable = state.get('applicable_states') if name == 'Deeptadi' else None
if isinstance(applicable, list) and applicable:
return _cell('、'.join(str(item) for item in applicable))
return _cell(state.get('state'))
lines.append(
f"| {_cell(_token(planet))} | {_state('Bala')} | {_state('Jagrat')} | {_state('Deeptadi')} | "