Files
Jyotisha/scripts/argala.py
T
jesse-uxGrok (agent loop)Cursor
aa87c0f127 fix(argala): keep equal occupancy unresolved until the two-level tiebreak
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>
2026-10-18 16:41:30 +08:00

320 lines
13 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 -*-
"""
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},未见对应阻挡'