f83db2fac1
- add external validation reports and open-source comparison references - integrate Jaimini arudha/graha pada, enhanced argala, and additional synastry kutas - update skill docs and capability matrices - add smoke tests for open-source integrations
436 lines
15 KiB
Python
436 lines
15 KiB
Python
"""
|
|
Shadbala — Six-fold Planetary Strength System (BPHS)
|
|
Calculates a numerical strength score for each planet based on:
|
|
1. Sthana Bala (Positional Strength) — Uchcha, Saptavargaja, Ojayugmarasyamsha, Kendra, Drekkana
|
|
2. Dig Bala (Directional Strength) — Based on house position
|
|
3. Kala Bala (Temporal Strength) — Day/night birth, hora lord, etc. (simplified)
|
|
4. Chesta Bala (Motional Strength) — Based on speed/retrograde
|
|
5. Naisargika Bala (Natural Strength) — Fixed hierarchy Sun > Moon > Venus > Jupiter > Mercury > Mars > Saturn
|
|
6. Drik Bala (Aspectual Strength) — Based on aspects received (simplified)
|
|
|
|
All values are in Shashtiamshas (60ths of a Rupa). 1 Rupa = 60 Shashtiamshas.
|
|
"""
|
|
|
|
from .constants import (
|
|
ZODIAC_SIGNS, EXALTATION, DEBILITATION, OWN_SIGNS,
|
|
MOOLTRIKONA, NATURAL_FRIENDS, NATURAL_ENEMIES, NATURAL_NEUTRALS,
|
|
DIGBALA_HOUSES, SIGN_LORDS
|
|
)
|
|
import math
|
|
|
|
|
|
# ============================================================
|
|
# 1. STHANA BALA (Positional Strength)
|
|
# ============================================================
|
|
|
|
def _uchcha_bala(planet_name, planet_lon):
|
|
"""
|
|
Exaltation strength. Max 60 Shashtiamshas at exact exaltation degree,
|
|
0 at exact debilitation degree. Linear interpolation.
|
|
"""
|
|
if planet_name not in EXALTATION:
|
|
return 0.0
|
|
|
|
exalt_sign, exalt_deg = EXALTATION[planet_name]
|
|
exalt_sign_idx = ZODIAC_SIGNS.index(exalt_sign)
|
|
exalt_lon = exalt_sign_idx * 30 + exalt_deg
|
|
|
|
# Angular distance from exaltation point
|
|
diff = abs(planet_lon - exalt_lon)
|
|
if diff > 180:
|
|
diff = 360 - diff
|
|
|
|
# Max strength at 0 diff (exalted), min at 180 (debilitated)
|
|
bala = (180 - diff) / 180.0 * 60.0
|
|
return round(bala, 2)
|
|
|
|
|
|
def _varga_dignity(planet_name, varga_sign):
|
|
"""
|
|
Determine a planet's dignity in a given varga chart sign.
|
|
Returns a score: exalted=30, mooltrikona=22.5, own=20, friend=15, neutral=10, enemy=5, debilitated=2.
|
|
"""
|
|
if not varga_sign or planet_name in ("Rahu", "Ketu"):
|
|
return 10.0
|
|
# Exaltation check
|
|
if planet_name in EXALTATION and EXALTATION[planet_name][0] == varga_sign:
|
|
return 30.0
|
|
# Debilitation check
|
|
if planet_name in DEBILITATION and DEBILITATION[planet_name][0] == varga_sign:
|
|
return 2.0
|
|
# Mooltrikona check
|
|
if planet_name in MOOLTRIKONA and MOOLTRIKONA[planet_name][0] == varga_sign:
|
|
return 22.5
|
|
# Own sign check
|
|
if planet_name in OWN_SIGNS and varga_sign in OWN_SIGNS[planet_name]:
|
|
return 20.0
|
|
# Relationship with sign lord
|
|
sign_lord = SIGN_LORDS.get(varga_sign)
|
|
if sign_lord and sign_lord != planet_name:
|
|
if planet_name in NATURAL_FRIENDS and sign_lord in NATURAL_FRIENDS[planet_name]:
|
|
return 15.0
|
|
if planet_name in NATURAL_ENEMIES and sign_lord in NATURAL_ENEMIES[planet_name]:
|
|
return 5.0
|
|
return 10.0
|
|
|
|
|
|
def _saptavargaja_bala(planet_name, dignity, planet_data=None):
|
|
"""
|
|
Strength based on dignity in the Saptavarga (7 divisional charts):
|
|
D1 (Rasi), D2 (Hora), D3 (Drekkana), D7 (Saptamsha),
|
|
D9 (Navamsha), D12 (Dwadashamsha), D30 (Trimshamsha).
|
|
Each varga contributes up to 30 points; total is averaged.
|
|
"""
|
|
dignity_scores = {
|
|
"exalted": 30.0,
|
|
"mooltrikona": 22.5,
|
|
"own_sign": 20.0,
|
|
"friend": 15.0,
|
|
"neutral": 10.0,
|
|
"enemy": 5.0,
|
|
"debilitated": 2.0,
|
|
}
|
|
d1_score = dignity_scores.get(dignity, 10.0)
|
|
|
|
if not planet_data:
|
|
return d1_score
|
|
|
|
varga_keys = ["d2_sign", "d3_sign", "d7_sign", "d9_sign", "d12_sign", "d30_sign"]
|
|
scores = [d1_score]
|
|
for key in varga_keys:
|
|
varga_sign = planet_data.get(key)
|
|
scores.append(_varga_dignity(planet_name, varga_sign))
|
|
|
|
return round(sum(scores) / len(scores), 2)
|
|
|
|
|
|
def _ojayugmarasyamsha_bala(planet_name, sign_idx):
|
|
"""
|
|
Strength from odd/even sign placement.
|
|
Sun, Mars, Jupiter, Saturn gain strength in odd signs.
|
|
Moon, Venus, Mercury (and Rahu/Ketu) gain in even signs.
|
|
"""
|
|
is_odd_sign = (sign_idx % 2 == 0) # Aries=0 is odd (index 0)
|
|
odd_planets = ["Sun", "Mars", "Jupiter", "Saturn"]
|
|
if planet_name in odd_planets:
|
|
return 15.0 if is_odd_sign else 0.0
|
|
else:
|
|
return 15.0 if not is_odd_sign else 0.0
|
|
|
|
|
|
def _kendra_bala(house):
|
|
"""
|
|
Planets in Kendras (1,4,7,10) get 60, Panapara (2,5,8,11) get 30,
|
|
Apoklima (3,6,9,12) get 15.
|
|
"""
|
|
if house in [1, 4, 7, 10]:
|
|
return 60.0
|
|
elif house in [2, 5, 8, 11]:
|
|
return 30.0
|
|
else:
|
|
return 15.0
|
|
|
|
|
|
def _drekkana_bala(planet_name, degree_in_sign):
|
|
"""
|
|
Male planets (Sun, Mars, Jupiter) strong in 1st drekkana (0-10°),
|
|
Neutral planets (Mercury, Saturn) in 2nd drekkana (10-20°),
|
|
Female planets (Moon, Venus) in 3rd drekkana (20-30°).
|
|
"""
|
|
if degree_in_sign < 10:
|
|
drekkana = 1
|
|
elif degree_in_sign < 20:
|
|
drekkana = 2
|
|
else:
|
|
drekkana = 3
|
|
|
|
male = ["Sun", "Mars", "Jupiter"]
|
|
female = ["Moon", "Venus"]
|
|
|
|
if planet_name in male and drekkana == 1:
|
|
return 15.0
|
|
elif planet_name in female and drekkana == 3:
|
|
return 15.0
|
|
elif planet_name not in male and planet_name not in female and drekkana == 2:
|
|
return 15.0
|
|
return 0.0
|
|
|
|
|
|
def _sthana_bala(planet_name, planet_lon, dignity, sign_idx, house, degree, planet_data=None):
|
|
uchcha = _uchcha_bala(planet_name, planet_lon)
|
|
saptavarga = _saptavargaja_bala(planet_name, dignity, planet_data)
|
|
ojayugma = _ojayugmarasyamsha_bala(planet_name, sign_idx)
|
|
kendra = _kendra_bala(house)
|
|
drekkana = _drekkana_bala(planet_name, degree)
|
|
total = uchcha + saptavarga + ojayugma + kendra + drekkana
|
|
return {
|
|
"uchcha": uchcha,
|
|
"saptavargaja": saptavarga,
|
|
"ojayugmarasyamsha": ojayugma,
|
|
"kendra": kendra,
|
|
"drekkana": drekkana,
|
|
"total": round(total, 2)
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# 2. DIG BALA (Directional Strength)
|
|
# ============================================================
|
|
|
|
def _dig_bala(planet_name, house):
|
|
"""
|
|
Max 60 when planet is in its Digbala house, 0 when opposite.
|
|
Linear interpolation based on house distance.
|
|
"""
|
|
if planet_name not in DIGBALA_HOUSES:
|
|
return 0.0
|
|
|
|
ideal_house = DIGBALA_HOUSES[planet_name]
|
|
# House distance (1-indexed, circular)
|
|
dist = abs(house - ideal_house)
|
|
if dist > 6:
|
|
dist = 12 - dist
|
|
# Max at 0 distance, 0 at 6 houses away
|
|
bala = (6 - dist) / 6.0 * 60.0
|
|
return round(bala, 2)
|
|
|
|
|
|
# ============================================================
|
|
# 3. KALA BALA (Temporal Strength) — Simplified
|
|
# ============================================================
|
|
|
|
def _kala_bala(planet_name, is_day_birth=True, moon_lon=None, sun_lon=None):
|
|
"""
|
|
Kala Bala with three sub-components:
|
|
1. Natonnata Bala (day/night strength)
|
|
2. Paksha Bala (lunar phase strength)
|
|
3. Ayana Bala (Sun's declination / seasonal strength)
|
|
"""
|
|
# 1. Natonnata Bala (day/night)
|
|
day_planets = ["Sun", "Jupiter", "Venus"]
|
|
night_planets = ["Moon", "Mars", "Saturn"]
|
|
if planet_name == "Mercury":
|
|
natonnata = 30.0
|
|
elif is_day_birth and planet_name in day_planets:
|
|
natonnata = 60.0
|
|
elif not is_day_birth and planet_name in night_planets:
|
|
natonnata = 60.0
|
|
else:
|
|
natonnata = 0.0
|
|
|
|
# 2. Paksha Bala (Moon phase — benefics strong in Shukla Paksha, malefics in Krishna)
|
|
paksha = 0.0
|
|
if moon_lon is not None and sun_lon is not None:
|
|
tithi_angle = (moon_lon - sun_lon) % 360
|
|
is_shukla = tithi_angle < 180
|
|
# Strength proportional to how close to Full/New Moon
|
|
if is_shukla:
|
|
phase_ratio = tithi_angle / 180.0
|
|
else:
|
|
phase_ratio = (360 - tithi_angle) / 180.0
|
|
benefics = ["Jupiter", "Venus", "Mercury", "Moon"]
|
|
if planet_name in benefics:
|
|
paksha = phase_ratio * 60.0 # benefics strong near Full Moon
|
|
else:
|
|
paksha = (1 - phase_ratio) * 60.0 # malefics strong near New Moon
|
|
|
|
# 3. Ayana Bala (seasonal — based on Sun's longitude)
|
|
ayana = 0.0
|
|
if sun_lon is not None:
|
|
# Sun 0-180° = Uttarayana (northern), 180-360° = Dakshinayana (southern)
|
|
# Benefics strong in Uttarayana, malefics in Dakshinayana
|
|
sun_norm = sun_lon % 360
|
|
benefics = ["Jupiter", "Venus", "Mercury", "Moon"]
|
|
if sun_norm < 180:
|
|
ratio = (180 - abs(sun_norm - 90)) / 180.0
|
|
else:
|
|
ratio = (180 - abs(sun_norm - 270)) / 180.0
|
|
if planet_name in benefics:
|
|
ayana = ratio * 30.0 if sun_norm < 180 else (1 - ratio) * 30.0
|
|
else:
|
|
ayana = (1 - ratio) * 30.0 if sun_norm < 180 else ratio * 30.0
|
|
|
|
return round(natonnata + paksha + ayana, 2)
|
|
|
|
|
|
# ============================================================
|
|
# 4. CHESTA BALA (Motional Strength)
|
|
# ============================================================
|
|
|
|
# Average daily speeds in degrees (approximate)
|
|
_AVG_SPEEDS = {
|
|
"Mars": 0.524,
|
|
"Mercury": 1.383,
|
|
"Jupiter": 0.083,
|
|
"Venus": 1.200,
|
|
"Saturn": 0.034,
|
|
}
|
|
|
|
|
|
def _chesta_bala(planet_name, speed, is_retrograde):
|
|
"""
|
|
Motional strength based on actual speed.
|
|
Retrograde=60, Stationary(very slow)=45, Direct uses speed ratio.
|
|
Sun=Chesta from longitude, Moon=Paksha-based (set in Kala Bala).
|
|
"""
|
|
if planet_name in ("Sun", "Moon"):
|
|
return 30.0
|
|
if planet_name in ("Rahu", "Ketu"):
|
|
return 0.0
|
|
if is_retrograde:
|
|
return 60.0
|
|
avg = _AVG_SPEEDS.get(planet_name, 1.0)
|
|
abs_speed = abs(speed)
|
|
if abs_speed < avg * 0.1:
|
|
return 45.0 # near-stationary
|
|
# Scale linearly: 0 speed → 45, avg speed → 30, 2x avg → 15
|
|
ratio = min(abs_speed / avg, 2.0)
|
|
return round(60.0 - ratio * 15.0, 2)
|
|
|
|
|
|
# ============================================================
|
|
# 5. NAISARGIKA BALA (Natural Strength) — Fixed
|
|
# ============================================================
|
|
|
|
NAISARGIKA_BALA = {
|
|
"Sun": 60.0,
|
|
"Moon": 51.43,
|
|
"Venus": 42.86,
|
|
"Jupiter": 34.29,
|
|
"Mercury": 25.71,
|
|
"Mars": 17.14,
|
|
"Saturn": 8.57,
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# 6. DRIK BALA (Aspectual Strength) — Simplified
|
|
# ============================================================
|
|
|
|
# BPHS partial aspect strengths (house distance → fraction)
|
|
_ASPECT_STRENGTH = {
|
|
3: 0.25, 4: 0.75, 5: 0.50, 7: 1.00, 8: 0.75, 9: 0.50, 10: 0.25,
|
|
}
|
|
# Special full aspects override
|
|
_SPECIAL_ASPECTS = {
|
|
"Mars": {4: 1.00, 8: 1.00},
|
|
"Jupiter": {5: 1.00, 9: 1.00},
|
|
"Saturn": {3: 1.00, 10: 1.00},
|
|
}
|
|
|
|
|
|
def _drik_bala(planet_name, house, planets_data):
|
|
"""
|
|
Aspectual strength using BPHS weighted partial aspects.
|
|
Benefics aspecting add strength; malefics reduce it.
|
|
Aspect weight varies by house distance and special aspects.
|
|
"""
|
|
benefics = ["Jupiter", "Venus", "Mercury", "Moon"]
|
|
malefics = ["Saturn", "Mars", "Sun", "Rahu", "Ketu"]
|
|
|
|
target_sign_idx = ZODIAC_SIGNS.index(planets_data[planet_name]["sign"])
|
|
score = 0.0
|
|
|
|
for name, data in planets_data.items():
|
|
if name == planet_name or name in ("Rahu", "Ketu"):
|
|
continue
|
|
aspector_sign_idx = ZODIAC_SIGNS.index(data["sign"])
|
|
house_dist = ((target_sign_idx - aspector_sign_idx) % 12) + 1
|
|
if house_dist == 1:
|
|
continue # conjunction, not aspect
|
|
|
|
# Check if this planet aspects at this distance
|
|
special = _SPECIAL_ASPECTS.get(name, {})
|
|
strength = special.get(house_dist, _ASPECT_STRENGTH.get(house_dist, 0.0))
|
|
if strength <= 0:
|
|
continue
|
|
|
|
if name in benefics:
|
|
score += strength * 15.0
|
|
elif name in malefics:
|
|
score -= strength * 15.0
|
|
|
|
# Normalize to [0, 60] range with 30 as midpoint
|
|
return round(max(0.0, min(60.0, score + 30.0)), 2)
|
|
|
|
|
|
# ============================================================
|
|
# MAIN CALCULATOR
|
|
# ============================================================
|
|
|
|
# Minimum required Shadbala (in Rupas) per BPHS
|
|
REQUIRED_SHADBALA = {
|
|
"Sun": 6.5,
|
|
"Moon": 6.0,
|
|
"Mars": 5.0,
|
|
"Mercury": 7.0,
|
|
"Jupiter": 6.5,
|
|
"Venus": 5.5,
|
|
"Saturn": 5.0,
|
|
}
|
|
|
|
|
|
def calculate_shadbala(planets_data: dict, raw_planets: dict, is_day_birth: bool = True):
|
|
"""
|
|
Calculates the six-fold strength for all 7 planets.
|
|
|
|
Parameters
|
|
----------
|
|
planets_data : dict — The enriched 'planets' output from calculate_vedic_chart
|
|
raw_planets : dict — The raw planet data with 'lon', 'sign_idx', 'speed'
|
|
is_day_birth : bool — Whether birth occurred during daytime
|
|
|
|
Returns
|
|
-------
|
|
dict: Shadbala breakdown for each planet with total in Shashtiamshas and Rupas.
|
|
"""
|
|
result = {}
|
|
|
|
# Extract Sun and Moon longitudes for Kala Bala sub-components
|
|
sun_lon = raw_planets.get("Sun", {}).get("lon")
|
|
moon_lon = raw_planets.get("Moon", {}).get("lon")
|
|
|
|
for planet_name in ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]:
|
|
if planet_name not in planets_data or planet_name not in raw_planets:
|
|
continue
|
|
|
|
pd = planets_data[planet_name]
|
|
rp = raw_planets[planet_name]
|
|
|
|
sthana = _sthana_bala(planet_name, rp["lon"], pd["dignity"], rp["sign_idx"], pd["house"], pd["degree"], pd)
|
|
dig = _dig_bala(planet_name, pd["house"])
|
|
kala = _kala_bala(planet_name, is_day_birth, moon_lon, sun_lon)
|
|
chesta = _chesta_bala(planet_name, rp["speed"], pd["is_retrograde"])
|
|
naisargika = NAISARGIKA_BALA.get(planet_name, 0.0)
|
|
drik = _drik_bala(planet_name, pd["house"], planets_data)
|
|
|
|
total_shashtiamshas = sthana["total"] + dig + kala + chesta + naisargika + drik
|
|
total_rupas = round(total_shashtiamshas / 60.0, 2)
|
|
required = REQUIRED_SHADBALA.get(planet_name, 5.0)
|
|
is_strong = total_rupas >= required
|
|
|
|
# Ishta Phala and Kashta Phala (BPHS)
|
|
# Ishta = sqrt(Uchcha Bala * Chesta Bala)
|
|
# Kashta = sqrt((60 - Uchcha Bala) * (60 - Chesta Bala))
|
|
uchcha = sthana["uchcha"]
|
|
ishta_phala = round(math.sqrt(max(0, uchcha * chesta)), 2)
|
|
kashta_phala = round(math.sqrt(max(0, (60.0 - uchcha) * (60.0 - chesta))), 2)
|
|
|
|
result[planet_name] = {
|
|
"sthana_bala": sthana,
|
|
"dig_bala": dig,
|
|
"kala_bala": kala,
|
|
"chesta_bala": chesta,
|
|
"naisargika_bala": naisargika,
|
|
"drik_bala": drik,
|
|
"total_shashtiamshas": round(total_shashtiamshas, 2),
|
|
"total_rupas": total_rupas,
|
|
"required_rupas": required,
|
|
"is_strong": is_strong,
|
|
"strength_ratio": round(total_rupas / required, 2),
|
|
"ishta_phala": ishta_phala,
|
|
"kashta_phala": kashta_phala,
|
|
}
|
|
|
|
return result
|