Upstream e7301feb, 86935c0a, and 4f1536f2. Ketu reverses the count, the ninth is the fifth's obstruction, and a remaining tie stays strength_comparison_required so the consultation card shows 待定. BUG-1386. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
320 lines
13 KiB
Python
320 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Argala(门闩)行星干预模块 v1.1
|
||
|
||
从旧版“逐行星Argala”升级为“逐参考点/逐宫完整Argala”:
|
||
- 主Argala: 2/4/11宫
|
||
- Virodhargala: 12/10/3宫分别阻挡2/4/11宫
|
||
- 特殊Argala: 第3宫有2颗以上凶星
|
||
- 次级:第5宫是 Argala,第9宫是它的 Virodhargala,不再把第9宫另算一条次级 Argala
|
||
- Argala Rajayoga分类: Poornargala / Tripadargala / Ardhargala / Padargala
|
||
|
||
MIT复用来源:
|
||
- jaimini-tropical/jaimini/core/argala.py (tunanfang-pixel, MIT)
|
||
适配:使用本项目 chart 输出的英文行星名与恒星黄道星座索引。
|
||
"""
|
||
from typing import Dict, List
|
||
|
||
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'}
|
||
|
||
NATURAL_MALEFICS = {'Sun', 'Mars', 'Saturn'}
|
||
NATURAL_BENEFICS = {'Moon', 'Mercury', 'Jupiter', 'Venus'}
|
||
NODES = {'Rahu', 'Ketu'}
|
||
|
||
PRIMARY_ARGALA = {2, 4, 11}
|
||
VIRODHARGALA_MAP = {2: 12, 4: 10, 11: 3}
|
||
SECONDARY_ARGALA = {5, 9}
|
||
|
||
|
||
def _house_from(ref_sign_idx: int, offset: int) -> int:
|
||
"""从参考星座起算第offset宫对应的星座索引。"""
|
||
return (ref_sign_idx + offset - 1) % 12
|
||
|
||
|
||
def _planets_in_sign(sign_idx: int, planet_sign_indices: Dict[str, int], include_nodes: bool = False) -> List[str]:
|
||
planets = []
|
||
for pname, psi in planet_sign_indices.items():
|
||
if not include_nodes and pname in NODES:
|
||
continue
|
||
if psi % 12 == sign_idx % 12:
|
||
planets.append(pname)
|
||
return planets
|
||
|
||
|
||
def classify_argala_rajayoga(argala_result: Dict) -> Dict:
|
||
"""按主Argala占据数量分类Rajayoga强度。"""
|
||
primary = argala_result.get('primary', {})
|
||
occupied = sum(1 for h in ['H2', 'H4', 'H11'] if primary.get(h, {}).get('planets'))
|
||
blocked = sum(1 for h in ['H2', 'H4', 'H11'] if primary.get(h, {}).get('blockers'))
|
||
if occupied == 3:
|
||
return {'type': 'Poornargala', 'level': 4, 'cn': '完整Argala,最强支持'}
|
||
if occupied == 2:
|
||
return {'type': 'Tripadargala', 'level': 3, 'cn': '三足Argala,强支持'}
|
||
if occupied == 1:
|
||
return {'type': 'Ardhargala', 'level': 2, 'cn': '半Argala,中等支持'}
|
||
if blocked:
|
||
return {'type': 'Padargala', 'level': 1, 'cn': '弱Argala且受阻'}
|
||
return {'type': 'None', 'level': 0, 'cn': '无明显Argala'}
|
||
|
||
|
||
def _pair_status(planets, blockers):
|
||
if not planets:
|
||
return 'absent'
|
||
if len(planets) == len(blockers):
|
||
return 'strength_comparison_required'
|
||
return 'effective' if len(planets) > len(blockers) else 'obstructed'
|
||
|
||
|
||
def _compare_pair(planets, blockers, sign_idx, block_idx, chart, include_nodes):
|
||
"""Equal occupancy uses the two Narayana levels already in select_narayana_seed_sign.
|
||
|
||
Later strength layers are not ported. A remaining tie stays unresolved.
|
||
"""
|
||
status = _pair_status(planets, blockers)
|
||
if status != 'strength_comparison_required':
|
||
return status, {'status': 'not_required', 'basis': 'occupant_count'}
|
||
metadata = dict(
|
||
profile='rath_argala_role_contacts_primary_lords_v1',
|
||
source='Rath Jaimini Upadesa Sutra course, source p363',
|
||
claim_boundary='First two levels only; traditional primary lords; later ties unresolved.',
|
||
)
|
||
missing = sorted((NATURAL_BENEFICS | NATURAL_MALEFICS) - set(chart))
|
||
if include_nodes:
|
||
missing += sorted(NODES - set(chart))
|
||
if missing:
|
||
return status, dict(metadata, status='blocked', reason='missing_classical_planet_signs', missing=missing)
|
||
try:
|
||
from scripts.narayana_dasha import select_narayana_seed_sign
|
||
except ModuleNotFoundError:
|
||
from narayana_dasha import select_narayana_seed_sign
|
||
classical = NATURAL_BENEFICS | NATURAL_MALEFICS | (NODES if include_nodes else set())
|
||
decision = select_narayana_seed_sign(
|
||
sign_idx,
|
||
{p: (s % 12) * 30.0 for p, s in chart.items() if p in classical},
|
||
comparison_sign_idx=block_idx,
|
||
)
|
||
decision['implementation_profile'] = decision.pop('profile')
|
||
decision.update(metadata)
|
||
if decision['status'] == 'selected':
|
||
status = 'effective' if decision['selected_sign_idx'] == sign_idx else 'obstructed'
|
||
else:
|
||
decision['reason'] = 'later_argala_sign_strength_rules_required'
|
||
return status, decision
|
||
|
||
|
||
_VERDICT_LABEL = {
|
||
'strength_comparison_required': '待定',
|
||
'special_argala_adjudication_required': '待定',
|
||
'mixed': '吉凶交错',
|
||
'supported': '支持',
|
||
'obstructed': '受阻',
|
||
'neutral': '中性',
|
||
}
|
||
|
||
|
||
def calc_argala_for_reference(ref_sign_idx: int, planet_sign_indices: Dict[str, int], include_nodes: bool = False) -> Dict:
|
||
"""计算单一参考点的完整Argala。"""
|
||
# Ketu in the reference sign reverses counting whether or not nodes count as occupants.
|
||
direction = -1 if planet_sign_indices.get('Ketu') == ref_sign_idx else 1
|
||
|
||
def reference_house(offset):
|
||
return (ref_sign_idx + direction * (offset - 1)) % 12
|
||
|
||
result = {
|
||
'source_reference_packets': ['science_of_light_v1_argala_reference_points'],
|
||
'ref_sign': SIGNS[ref_sign_idx],
|
||
'ref_sign_idx': ref_sign_idx,
|
||
'primary': {},
|
||
'specific': None,
|
||
'secondary': {},
|
||
'argala_count': 0,
|
||
'virodhargala_count': 0,
|
||
'net_result': 'neutral',
|
||
'counting_direction': 'antizodiacal' if direction == -1 else 'zodiacal',
|
||
'rule_source': 'PVR Integrated Approach §§10.5–10.7, source pages 117–118',
|
||
}
|
||
|
||
for h in sorted(PRIMARY_ARGALA):
|
||
argala_sign = reference_house(h)
|
||
planets = _planets_in_sign(argala_sign, planet_sign_indices, include_nodes)
|
||
block_h = VIRODHARGALA_MAP[h]
|
||
block_sign = reference_house(block_h)
|
||
blockers = _planets_in_sign(block_sign, planet_sign_indices, include_nodes)
|
||
status, comparison = _compare_pair(
|
||
planets, blockers, argala_sign, block_sign, planet_sign_indices, include_nodes,
|
||
)
|
||
effective = None if status == 'strength_comparison_required' else status == 'effective'
|
||
result['primary'][f'H{h}'] = {
|
||
'house_from_reference': h,
|
||
'sign': SIGNS[argala_sign],
|
||
'sign_idx': argala_sign,
|
||
'planets': planets,
|
||
'blocked_by_house': block_h,
|
||
'blocked_by_sign': SIGNS[block_sign],
|
||
'blockers': blockers,
|
||
'effective': effective,
|
||
'comparison_status': status,
|
||
'strength_comparison': comparison,
|
||
'effect': _argala_effect(h, planets, blockers, status),
|
||
}
|
||
if planets:
|
||
result['argala_count'] += 1
|
||
if blockers:
|
||
result['virodhargala_count'] += 1
|
||
|
||
h3_sign = reference_house(3)
|
||
h3_planets = _planets_in_sign(h3_sign, planet_sign_indices, include_nodes)
|
||
h3_malefics = [p for p in h3_planets if p in NATURAL_MALEFICS]
|
||
result['specific'] = {
|
||
'house': 3,
|
||
'sign': SIGNS[h3_sign],
|
||
'sign_idx': h3_sign,
|
||
'planets': h3_planets,
|
||
'malefics': h3_malefics,
|
||
'effective': len(h3_malefics) >= 2,
|
||
'rule': '第3宫有2颗以上天然凶星形成特殊Argala',
|
||
'method_status': 'parameter_sensitive_two_malefic_threshold',
|
||
'obstruction_rule': 'Once formed, special third-house Argala has no obstruction (Rath source p363). Formation threshold remains profile-dependent.',
|
||
}
|
||
if result['specific']['effective']:
|
||
result['argala_count'] += 1
|
||
|
||
for h in sorted(SECONDARY_ARGALA):
|
||
sign_idx = reference_house(h)
|
||
planets = _planets_in_sign(sign_idx, planet_sign_indices, include_nodes)
|
||
result['secondary'][f'H{h}'] = {
|
||
'house_from_reference': h,
|
||
'sign': SIGNS[sign_idx],
|
||
'sign_idx': sign_idx,
|
||
'planets': planets,
|
||
'role': 'argala' if h == 5 else 'virodhargala',
|
||
}
|
||
|
||
fifth, ninth = result['secondary']['H5'], result['secondary']['H9']
|
||
secondary_status, comparison = _compare_pair(
|
||
fifth['planets'], ninth['planets'], fifth['sign_idx'], ninth['sign_idx'],
|
||
planet_sign_indices, include_nodes,
|
||
)
|
||
fifth.update(
|
||
blocked_by_house=9,
|
||
blocked_by_sign=ninth['sign'],
|
||
strength_comparison=comparison,
|
||
blockers=ninth['planets'],
|
||
comparison_status=secondary_status,
|
||
effective=None if secondary_status == 'strength_comparison_required' else secondary_status == 'effective',
|
||
)
|
||
ninth['obstructs_house'] = 5
|
||
|
||
statuses = [row['comparison_status'] for row in result['primary'].values()]
|
||
statuses.append(secondary_status)
|
||
if 'strength_comparison_required' in statuses:
|
||
result['net_result'] = 'strength_comparison_required'
|
||
elif 'effective' in statuses and 'obstructed' in statuses:
|
||
result['net_result'] = 'mixed'
|
||
elif 'effective' in statuses:
|
||
result['net_result'] = 'supported'
|
||
elif 'obstructed' in statuses:
|
||
result['net_result'] = 'obstructed'
|
||
if result['specific']['effective']:
|
||
result['net_result'] = 'special_argala_adjudication_required'
|
||
result['verdict_label'] = _VERDICT_LABEL[result['net_result']]
|
||
result['rajayoga_classification'] = classify_argala_rajayoga(result)
|
||
result['rajayoga_classification']['method_status'] = 'legacy_occupancy_heuristic'
|
||
result['rajayoga_classification']['claim_boundary'] = (
|
||
'Occupancy label only; not an adjudication of obstruction or a verified Rajayoga.'
|
||
)
|
||
return result
|
||
|
||
|
||
def calc_argala(planet_sign_indices: Dict[str, int], asc_sign_idx: int, include_nodes: bool = False) -> Dict:
|
||
"""
|
||
计算全盘Argala。
|
||
|
||
Args:
|
||
planet_sign_indices: {'Sun': 0, 'Moon': 3, ...}
|
||
asc_sign_idx: 上升星座索引
|
||
include_nodes: 是否把Rahu/Ketu纳入占位阻挡统计(默认False,遵循Jaimini七行星口径)
|
||
"""
|
||
houses = {}
|
||
for house_num in range(1, 13):
|
||
ref_sign_idx = (asc_sign_idx + house_num - 1) % 12
|
||
houses[f'house_{house_num}'] = calc_argala_for_reference(ref_sign_idx, planet_sign_indices, include_nodes)
|
||
|
||
planet_refs = {}
|
||
for pname, sign_idx in planet_sign_indices.items():
|
||
if pname in NODES and not include_nodes:
|
||
continue
|
||
planet_refs[pname] = calc_argala_for_reference(sign_idx, planet_sign_indices, include_nodes)
|
||
|
||
summary = _summarize_argala(houses)
|
||
return {
|
||
'method': 'Jaimini Argala + Virodhargala (jaimini-tropical MIT adapted)',
|
||
'version': '1.1',
|
||
'include_nodes': include_nodes,
|
||
'ascendant': SIGNS[asc_sign_idx],
|
||
'houses': houses,
|
||
'planets': planet_refs,
|
||
'summary': summary,
|
||
}
|
||
|
||
|
||
def _summarize_argala(houses: Dict) -> Dict:
|
||
supported = []
|
||
obstructed = []
|
||
strongest = []
|
||
unresolved = []
|
||
mixed = []
|
||
for house_key, data in houses.items():
|
||
if data['net_result'] == 'supported':
|
||
supported.append(house_key)
|
||
elif data['net_result'] == 'obstructed':
|
||
obstructed.append(house_key)
|
||
elif data['net_result'] == 'mixed':
|
||
mixed.append(house_key)
|
||
elif str(data['net_result']).endswith('_required'):
|
||
unresolved.append(house_key)
|
||
if house_key not in unresolved:
|
||
level = sum(row.get('effective') is True for row in data['primary'].values())
|
||
level += int(data['secondary']['H5'].get('effective') is True)
|
||
strongest.append((house_key, level, 'effective_pair_count'))
|
||
strongest.sort(key=lambda x: x[1], reverse=True)
|
||
summary = {
|
||
'supported_houses': supported,
|
||
'obstructed_houses': obstructed,
|
||
'top_argala_houses': [{'house': h, 'level': lvl, 'type': typ} for h, lvl, typ in strongest[:5] if lvl > 0],
|
||
'supported_count': len(supported),
|
||
'obstructed_count': len(obstructed),
|
||
'unresolved_houses': unresolved,
|
||
'mixed_houses': mixed,
|
||
'ranking_basis': 'effective_pair_count_not_classical_rajayoga_strength',
|
||
}
|
||
if unresolved:
|
||
summary['unresolved_label'] = '待定'
|
||
return summary
|
||
|
||
|
||
def _argala_effect(house: int, planets: List[str], blockers: List[str], status=None) -> str:
|
||
if not planets:
|
||
return '无行星形成此类Argala'
|
||
base = {
|
||
2: '资源、语言、家族与财务支持',
|
||
4: '内在稳定、住所、母亲、教育与幸福感支持',
|
||
11: '收益、人脉、愿望实现与社会网络支持',
|
||
}.get(house, '一般支持')
|
||
if blockers and len(blockers) == len(planets):
|
||
if status == 'effective':
|
||
return f'{base},双方数量相等,所选强弱口径判为有效'
|
||
if status == 'obstructed':
|
||
return f'{base},双方数量相等,所选强弱口径判为受阻'
|
||
return f'待定:{base},双方数量相等,须比较强弱后裁决'
|
||
if blockers and len(blockers) > len(planets):
|
||
return f'{base},但被{len(blockers)}个Virodhargala阻挡'
|
||
if blockers:
|
||
return f'{base},有轻度阻挡但仍可能有效'
|
||
return f'{base},未见对应阻挡'
|