Files
Jyotisha/scripts/argala.py
T
ClaudeandClaude Opus 5.5 198208ac5b feat(sync): astrologer handoff 2e5485f5 — Sodhita entry, Narayana strength profile, 71 unimplemented yogas, Argala tie, Bhava relative points
- A1: ashtakavarga_shodhana.calc_sodhita_av (her entry; ashtakavarga.py frozen),
  level=not_calibrated + prediction_scoring_allowed=False on the chain; legacy
  subtraction stays removed (BUG-1209).
- A2: select_narayana_seed_sign strength_profile rath_empty_sign_lord_strength_v1,
  shared by source-aware seed, MD and AD (matches her function on 6000 draws).
- A3: preflight of logical branches; placeholder not an implementation;
  reference_only rules listed; 71 not_implemented; cmd_yoga not_implemented_count.
- A4: neechabhanga_kendra name/category per her handoff.
- 9e971559: Vimshottari AD record lord and native current PD in dasa convergence.
- B10: Argala equal count stays strength_comparison_required (no Narayana borrow).
- B11: full-reading bhava_bala.reference_relative_points, labelled
  product_relative_points_not_traditional, no bands.

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

304 lines
12 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):
"""Pair verdict by occupant count; an equal count stays unresolved.
原值:同数时借用 Narayana 起运的两级强弱(select_narayana_seed_sign:行星数,
再木星、水星或宫主的星座相位)自动择胜,ruling_pending=True
新值:同数一律 strength_comparison_required,不自动择胜(与占星师主线
scripts/argala.py::_pair_status 相同);strength_comparison 只记录未决原因
原因:占星师 2026-11-07 rulings.md B10:同数借用 Narayana 强弱无原典依据
"""
status = _pair_status(planets, blockers)
if status != 'strength_comparison_required':
return status, {'status': 'not_required', 'basis': 'occupant_count'}
return status, {
'status': 'unresolved',
'basis': 'equal_occupant_count',
'reason': 'equal_count_no_source_tie_break',
'claim_boundary': ('Equal Argala and Virodhargala counts are not decided automatically; '
'no source supports borrowing the Narayana sign-strength order here.'),
}
_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},未见对应阻挡'