Files
Jyotisha/scripts/goel_jaimini_strength.py
T
ClaudeandClaude Opus 5.5 578a361460 feat(chart): functional roles, 7/8 Chara Karaka and three new 特殊点 groups (#163 1-3)
/api/chart_points:
- karaka_scheme 7 (default) / 8 / both; shared ranking in
  chart_view_points/karaka_rank.py (unrounded in-sign degree, Rahu reversed
  in the 8 scheme, Ketu out). Bodies equal at full precision mark only the
  roles they span as tied; Goel uses the same ranking (BUG-1512).
- functional_roles: v2 BPHS ch.34 profile laid out per body, nodes pending,
  Yogakaraka / Badhaka / Kendradhipati (conditions not evaluated) as
  separate labels.
- sensitive_points (Bhrigu Bindu per chart node_mode, 64th Navamsa, 22nd
  Drekkana sign-only), sahams (PVR Table 74, nine first, all kept),
  goel_points (printed-text policy, worked example beside it).

盘面数据: 行星 gets Chara Karaka (AK · 自我, 七星/八星 switch), 自然属性,
管辖宫位, 功能角色, 补充身份; 特殊点 splits into 特殊上升 / 敏感点 / Saham /
Goel 特殊点 and the board's 特殊点 layer follows the open group.

jaimini.py, functional_benefics.py and varga.py are untouched (rectification
frozen identity).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
2026-11-02 20:35:31 +08:00

148 lines
8.6 KiB
Python

"""Goel's full Jaimini strength and special-point profile, not Bhava Bala.
Source: Predicting through Jaimini Astrology, printed pp10-17 (PDF pp14-21).
The published p15 example is the numeric regression. Dignity degree ranges
reuse the explicitly named Raman manual profile; no universal lineage claim.
"""
import math
from scripts.trikona_raman import CLASSICAL, DIGNITY_PROFILE, _dignity, _validate_positions
from scripts.narayana_dasha import _rasi_aspects
from scripts.sthira_brahma_goel_short_method import SIGNS, SIGN_LORDS
from scripts.chart_view_points.karaka_rank import rank_chara_karakas
PROFILE = 'goel_full_jaimini_strength_ch1_v1'
SOURCE = 'V. P. Goel, Predicting through Jaimini Astrology'
KARAKA_POINTS = (60, 45, 30, 22.5, 15, 7.5, 3.75)
MALEFICS = ('Sun', 'Mars', 'Saturn', 'Rahu', 'Ketu')
def calculate_goel_strength(planet_longitudes, *, node_occupancy_policy='include_nodes_text'):
if node_occupancy_policy not in {'include_nodes_text', 'exclude_nodes_worked_example'}:
raise ValueError('explicit Goel node occupancy policy required')
_validate_positions(planet_longitudes)
signs = {p: int(v // 30) for p, v in planet_longitudes.items()}
degrees = {p: planet_longitudes[p] % 30 for p in CLASSICAL}
# Shared with the chart page's karaka column: explicit ties, never input order.
ranking = rank_chara_karakas(degrees, 7)
if ranking['ties']:
return dict(status='blocked', reason='equal_degree_seven_karaka_rank_requires_finer_positions',
profile_id=PROFILE, karaka_ties=ranking['ties'])
ranked = ranking['order']
ak = ranked[0]
karakas = dict(zip(ranked, KARAKA_POINTS))
def contacts(sign, candidates):
return [p for p in candidates if signs[p] == sign or _rasi_aspects(signs[p], sign)]
planets = {}
for p in CLASSICAL:
sign = signs[p]
dignity, moola = _dignity(p, planet_longitudes[p])
kendra = (60, 30, 15)[(sign - signs[ak]) % 3]
before = [q for q, s in signs.items() if s == (sign - 1) % 12]
after = [q for q, s in signs.items() if s == (sign + 1) % 12]
kartari = 60 if sign % 2 == 0 and before and after else 0
malefic_contacts = contacts(sign, [q for q in MALEFICS if q != p])
adhipati = 60 if p != 'Moon' and malefic_contacts else 0
planets[p] = dict(sign_idx=sign, degree_in_sign=degrees[p],
dignity=dignity, mooltrikona=moola, karaka=karakas[p], kendriya=kendra,
kartari=kartari, adhipati=adhipati, malefic_contacts=malefic_contacts,
kartari_before=before, kartari_after=after,
total=moola + karakas[p] + kendra + kartari + adhipati)
rows = []
for sign in range(12):
lord = SIGN_LORDS[sign]
occupants = [p for p, s in signs.items() if s == sign]
permanent = (15, 30, 60)[sign % 3]
karaka = sum(karakas.get(p, 0) for p in occupants)
counted = occupants if node_occupancy_policy == 'include_nodes_text' else [p for p in occupants if p in CLASSICAL]
sthana = 45 + 15 * len(counted) if counted else 0
# Three roles, not distinct planets: Goel p103 explicitly explains
# double credit when Mercury/Jupiter is also the sign lord.
aspect_roles = [role for role, p in [('Jupiter', 'Jupiter'), ('Mercury', 'Mercury'), ('lord', lord)]
if contacts(sign, [p])]
aspect = 60 * len(aspect_roles)
rows.append(dict(sign_idx=sign, sign=SIGNS[sign], lord=lord,
permanent=permanent, karaka=karaka, sthana=sthana, occupants=occupants, counted_occupants=counted,
aspect=aspect, aspect_roles=aspect_roles, lord_strength=planets[lord]['total'],
total=permanent + karaka + sthana + aspect + planets[lord]['total']))
return dict(status='executed', confidence_status='parameter_sensitive', profile_id=PROFILE,
source=SOURCE, source_locator='PDF pp14-21; printed pp10-17; lines461-736',
dignity_profile=DIGNITY_PROFILE, malefic_profile='Sun_Mars_Saturn_Rahu_Ketu_excluding_self',
node_occupancy_policy=node_occupancy_policy,
aspect_profile='jaimini_rasi_drishti', aspect_role_policy='distinct_roles',
atmakaraka=ak, planets=planets, rashi_strengths=rows,
claim_boundary='Named Goel full method with declared dignity ranges; not PL9 or all-lineage parity.')
def build_goel_chart_evidence(planet_longitudes, ascendant_longitude, *, node_occupancy_policy='include_nodes_text'):
"""Strength and Brahma / Maheshwara / Rudra under one node occupancy policy.
The default is the printed p14 text (nodes count in Sthana). Passing
``exclude_nodes_worked_example`` selects the special points from the p15
worked-example totals instead, so a caller can show where they differ.
"""
if (type(ascendant_longitude) not in (int, float)
or not math.isfinite(ascendant_longitude)
or not 0 <= ascendant_longitude < 360):
raise ValueError('ascendant longitude must be a finite number in [0,360)')
strength = calculate_goel_strength(planet_longitudes, node_occupancy_policy=node_occupancy_policy)
result = dict(strength=strength, special_points={})
if strength['status'] != 'executed':
result['special_points'] = {
name: dict(status='blocked', reason=strength['reason'], profile_id=PROFILE)
for name in ('Brahma', 'Maheshwara', 'Rudra')
}
return result
if node_occupancy_policy == 'include_nodes_text':
strength['worked_example_node_exclusion'] = calculate_goel_strength(
planet_longitudes, node_occupancy_policy='exclude_nodes_worked_example')
strength['source_discrepancy'] = (
'Printed p14 includes nodes in Sthana; printed p15 worked totals omit them. '
'Both numeric alternatives are preserved; the explicit text is the active profile.')
lagna = int(float(ascendant_longitude) % 360 // 30)
seventh = (lagna + 6) % 12
planets = strength['planets']
rashis = strength['rashi_strengths']
ak_sign = planets[strength['atmakaraka']]['sign_idx']
def choose(candidates, rule):
candidates = list(dict.fromkeys(candidates))
scores = {p: planets[p]['total'] for p in candidates}
winners = [p for p in candidates if scores[p] == max(scores.values())]
return dict(status='executed' if len(winners) == 1 else 'blocked',
value=winners[0] if len(winners) == 1 else None,
candidates=scores, rule=rule,
reason=None if len(winners) == 1 else 'equal_Goel_strength_no_unique_selector',
profile_id=PROFILE, confidence_status='parameter_sensitive',
source=SOURCE, source_locator='PDF pp20-21; printed pp16-17; lines701-736')
eighth_ak = SIGN_LORDS[(ak_sign + 7) % 12]
if planets[eighth_ak]['sign_idx'] == (ak_sign + 7) % 12:
brahma = choose([eighth_ak], 'eighth_lord_from_AK_occupies_eighth_from_AK')
elif rashis[lagna]['total'] == rashis[seventh]['total']:
brahma = dict(status='blocked', reason='equal_Lagna_seventh_Goel_strength', profile_id=PROFILE)
else:
anchor = lagna if rashis[lagna]['total'] > rashis[seventh]['total'] else seventh
brahma = choose([SIGN_LORDS[(anchor + h - 1) % 12] for h in (6, 8, 12)
if SIGN_LORDS[(anchor + h - 1) % 12] != 'Saturn'],
'strongest_6_8_12_lord_from_stronger_Lagna_seventh_excluding_Saturn')
brahma['anchor_sign_idx'] = anchor
maheswara = choose([eighth_ak] + ([SIGN_LORDS[(ak_sign + 11) % 12]]
if planets[eighth_ak]['dignity'] == 'exalted' else []),
'eighth_lord_from_AK_compare_twelfth_if_exalted')
rudra_candidates = list(dict.fromkeys([SIGN_LORDS[(lagna + 7) % 12], SIGN_LORDS[(lagna + 1) % 12]]))
afflicted = [p for p in rudra_candidates if planets[p]['malefic_contacts']]
rudra = choose(afflicted if len(afflicted) == 1 else rudra_candidates,
'stronger_8th_2nd_lord_unless_only_weaker_has_malefic_contact')
result['special_points'] = dict(Brahma=brahma, Maheshwara=maheswara, Rudra=rudra)
result['lagna_seventh_strength'] = dict(lagna=rashis[lagna]['total'],
seventh=rashis[seventh]['total'], profile_id=PROFILE, source=SOURCE,
comparison='lagna_vs_seventh')
# Frozen-export compatibility only. PVR Niryaana is second/eighth Ayur
# comparison; callers must not route these Goel totals into that selector.
result['niryaana_shoola_strength'] = dict(result['lagna_seventh_strength'],
deprecated=True, replacement='lagna_seventh_strength',
not_for='pvr_niryaana_shoola_ayur_rasi_strength_v1')
return result