Skip to content

Commit 754b0cd

Browse files
master12coderclaude
andcommitted
feat(engine): Family Bond Kundali Phase 1.5 — rashyadhipati, cross-chart, wealth flow
Add 6 new engine compute modules and 3 model files for cross-chart family analysis. Key fix: Rahu/Ketu gemstone recommendations now resolve via sign lord's functional nature per lagna (rashyadhipati principle) instead of defaulting to "neutral". New modules: - functional_nature: any planet's functional classification per lagna + Rahu/Ketu shadow planet resolution via BPHS rashyadhipati - cross_chart: inter-chart planetary overlays (Moon contacts, axis contacts, karmic links with Saturn/Rahu/Ketu) - wealth_flow: earner/accumulator/distributor classification from 2L/10L/11L house placements - conjunction_penalty: gemstone weight reduction when lagnesh conjuncts Rahu/6L/8L/12L (BV Raman insight) - family_gemstone_synergy: cross-family gemstone complementarity detection ("one's poison = other's nectar") - dasha_sync: synchronized Mahadasha/Antardasha timeline for family Extended gem_therapy_core.py to post-process Rahu/Ketu via get_shadow_planet_nature() — fixes the Gomed miss for Vrischik lagna. Also fixes stale worktree path in test_yoga_new_definitions.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aad2b03 commit 754b0cd

18 files changed

Lines changed: 1667 additions & 1 deletion

docs/products/family-bond.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Family Bond Kundali (परिवार बंध कुण्डली)
2+
3+
> Cross-chart family analysis — gemstone synergy, wealth flow, dasha sync
4+
5+
## Status
6+
7+
- **Phase 1.5 (Engine)**: DONE — 6 compute modules, 3 model files, gem_therapy_core extended
8+
- **Phase 4 (Plugin)**: NOT STARTED — plugin, prompts, CLI commands deferred
9+
10+
## The Discovery
11+
12+
Individual kundali analysis treats each person in isolation. A real Varanasi
13+
Pandit considers cross-chart interactions:
14+
15+
1. **Rashyadhipati**: Rahu/Ketu give results of their sign lord's functional
16+
nature. Without this, Gomed was incorrectly "neutral" for Vrischik lagna
17+
when it should be SAFE (Rahu in Dhanu, sign lord = Jupiter = 2+5L = most benefic).
18+
19+
2. **Karmic Complementarity**: One spouse's prohibited stone = the other's
20+
recommended stone. "जो एक के लिए विष, दूसरे के लिए अमृत"
21+
22+
3. **Wealth Flow**: Who earns vs who accumulates — determines which spouse
23+
should manage finances.
24+
25+
4. **Conjunction Penalty**: Lagnesh conjunct Rahu/6L/8L/12L reduces gemstone
26+
weight (stone amplifies ALL conjunct energies).
27+
28+
## Engine Modules (Phase 1.5)
29+
30+
| Module | Purpose | Lines |
31+
|--------|---------|-------|
32+
| `compute/functional_nature.py` | Functional nature of any planet per lagna + Rahu/Ketu rashyadhipati | ~140 |
33+
| `compute/cross_chart.py` | Cross-chart planetary interactions between two charts | ~210 |
34+
| `compute/wealth_flow.py` | Earner/accumulator/distributor classification | ~120 |
35+
| `compute/conjunction_penalty.py` | Gemstone weight penalty from malefic conjunctions | ~80 |
36+
| `compute/family_gemstone_synergy.py` | Cross-family gemstone complementarity analysis | ~170 |
37+
| `compute/dasha_sync.py` | Synchronised dasha timeline for family members | ~125 |
38+
| `models/functional_nature.py` | FunctionalNature, ShadowPlanetNature | ~50 |
39+
| `models/cross_chart.py` | PlanetOverlay, CrossChartResult | ~50 |
40+
| `models/family_bond.py` | WealthFlowProfile, ConjunctionPenalty, FamilyGemSynergyResult, DashaSyncResult | ~110 |
41+
42+
### Key Extension: gem_therapy_core.py
43+
44+
Post-processes Rahu/Ketu via `get_shadow_planet_nature()`. Rahu/Ketu no longer
45+
default to "neutral" — they resolve through their sign lord's functional nature
46+
per the lagna. This is the fix for the Gomed miss.
47+
48+
## API
49+
50+
```python
51+
# Functional nature (any planet, any lagna)
52+
from daivai_engine.compute.functional_nature import get_functional_nature, get_shadow_planet_nature
53+
nature = get_functional_nature("Jupiter", "Scorpio") # yogakaraka, 2+5L
54+
shadow = get_shadow_planet_nature("Rahu", chart) # sign lord resolution
55+
56+
# Cross-chart interactions
57+
from daivai_engine.compute.cross_chart import compute_cross_chart_interactions
58+
result = compute_cross_chart_interactions(chart_a, chart_b)
59+
60+
# Wealth flow
61+
from daivai_engine.compute.wealth_flow import classify_wealth_flow
62+
profile = classify_wealth_flow(chart) # "accumulator", "distributor", etc.
63+
64+
# Conjunction penalty
65+
from daivai_engine.compute.conjunction_penalty import compute_conjunction_penalty
66+
penalty = compute_conjunction_penalty(chart, "Mercury") # penalty_factor: 0.50
67+
68+
# Family gemstone synergy
69+
from daivai_engine.compute.family_gemstone_synergy import compute_family_gem_synergy
70+
synergy = compute_family_gem_synergy([chart_a, chart_b])
71+
72+
# Dasha sync
73+
from daivai_engine.compute.dasha_sync import compute_dasha_sync
74+
sync = compute_dasha_sync([chart_a, chart_b])
75+
```
76+
77+
## Validation Cases
78+
79+
| Case | Individual | Family Bond |
80+
|------|-----------|-------------|
81+
| Gomed for Vrischik lagna (Rahu in Dhanu) | neutral | SAFE (Jupiter = 2+5L) |
82+
| Pukhraj: Manish (Mithuna) vs Vaishali (Vrischik) | Manish: prohibited | Karmic complement |
83+
| Mercury conjunct Rahu (Manish) | Panna: recommended | Panna: recommended at 50% weight |
84+
| Wealth flow (Manish: 2L+11L+10L in 12th) | Not analysed | Distributor pattern |
85+
86+
## Deferred to Phase 4
87+
88+
- `products/plugins/family_bond/` plugin
89+
- LLM prompt templates (couple_analysis, wealth_strategy, gemstone_family)
90+
- CLI commands (`daivai family bond`, `daivai family add`)
91+
- Multi-school validation matrix (7 schools, weighted voting)
92+
- PDF report rendering
93+
- `family_bond_rules.yaml` knowledge file
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Gemstone weight penalty for planets conjunct malefic influences.
2+
3+
When a recommended planet (e.g., Lagnesh) conjuncts Rahu, 6th lord, 8th lord,
4+
or 12th lord, the gemstone amplifies ALL conjunct energies — not just the
5+
target planet. This module computes a penalty factor (0.0-1.0) to reduce
6+
the recommended stone weight.
7+
8+
Source: BV Raman, *How to Judge a Horoscope* Vol.II — stone activation
9+
risk from malefic conjunction partners.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from daivai_engine.compute.chart import are_conjunct, get_house_lord
15+
from daivai_engine.models.chart import ChartData
16+
from daivai_engine.models.family_bond import ConjunctionPenalty
17+
18+
19+
# Penalty factors by conjunct partner type. 1.0 = no penalty.
20+
_PENALTY_RAHU = 0.65 # Rahu amplifies obsession / shadow qualities
21+
_PENALTY_KETU = 0.70 # Ketu amplifies detachment / sudden events
22+
_PENALTY_6L = 0.75 # 6th lord = enemies, disease, debt
23+
_PENALTY_8L = 0.70 # 8th lord = sudden events, surgery, accident
24+
_PENALTY_12L = 0.75 # 12th lord = expenditure, loss, foreign lands
25+
26+
# The minimum aggregate penalty factor (floor).
27+
_MIN_PENALTY = 0.50
28+
29+
30+
def compute_conjunction_penalty(chart: ChartData, planet: str) -> ConjunctionPenalty:
31+
"""Check if *planet* conjuncts Rahu, Ketu, 6L, 8L, or 12L and compute penalty.
32+
33+
The penalty factor is multiplicative: if the planet conjuncts both Rahu
34+
(0.65) and 12L (0.75) the combined factor is 0.65 * 0.75 = ~0.49, floored
35+
at 0.50.
36+
37+
Args:
38+
chart: Computed birth chart.
39+
planet: The planet whose gemstone weight we are adjusting.
40+
41+
Returns:
42+
ConjunctionPenalty with has_penalty, penalty_factor, conjunct list,
43+
and reasoning.
44+
"""
45+
conjunct_with: list[str] = []
46+
factor = 1.0
47+
48+
# Check shadow planets
49+
for shadow, pen in [("Rahu", _PENALTY_RAHU), ("Ketu", _PENALTY_KETU)]:
50+
if shadow != planet and are_conjunct(chart, planet, shadow):
51+
conjunct_with.append(shadow)
52+
factor *= pen
53+
54+
# Check dusthana lords (6, 8, 12)
55+
dusthana_map = {6: _PENALTY_6L, 8: _PENALTY_8L, 12: _PENALTY_12L}
56+
seen_lords: set[str] = set()
57+
58+
for house, pen in dusthana_map.items():
59+
lord = get_house_lord(chart, house)
60+
if lord == planet or lord in seen_lords:
61+
continue
62+
seen_lords.add(lord)
63+
if are_conjunct(chart, planet, lord):
64+
conjunct_with.append(f"{lord} ({house}L)")
65+
factor *= pen
66+
67+
factor = max(factor, _MIN_PENALTY)
68+
has_penalty = factor < 1.0
69+
70+
if has_penalty:
71+
reasoning = (
72+
f"{planet} conjuncts {', '.join(conjunct_with)} → "
73+
f"reduce stone weight to {factor:.0%} of base recommendation."
74+
)
75+
else:
76+
reasoning = f"{planet} has no malefic conjunction partners."
77+
78+
return ConjunctionPenalty(
79+
planet=planet,
80+
has_penalty=has_penalty,
81+
penalty_factor=round(factor, 2),
82+
conjunct_with=conjunct_with,
83+
reasoning=reasoning,
84+
)
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"""Cross-chart (synastry) planetary interaction analysis.
2+
3+
Computes planet-on-planet overlays between two birth charts to identify
4+
emotional influence (Moon contacts), relationship dynamics (axis contacts),
5+
karmic bonds (Saturn/Rahu contacts), and general synastry interactions.
6+
7+
Source: Parashari synastry principles adapted for computational analysis.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from daivai_engine.compute.chart import get_house_lord
13+
from daivai_engine.constants.misc import FULL_CIRCLE_DEG, HALF_CIRCLE_DEG
14+
from daivai_engine.constants.signs import SIGN_LORDS, SIGNS
15+
from daivai_engine.models.chart import ChartData
16+
from daivai_engine.models.cross_chart import CrossChartResult, PlanetOverlay
17+
18+
19+
_ALL_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]
20+
_KARMIC_PLANETS = {"Saturn", "Rahu", "Ketu"}
21+
22+
# Aspect orbs (degrees)
23+
_ORB_CONJUNCTION = 10.0
24+
_ORB_OPPOSITION = 10.0
25+
_ORB_TRINE = 8.0
26+
_ORB_SQUARE = 7.0
27+
_ORB_SEXTILE = 6.0
28+
29+
# Aspect definitions: (target_degrees, name, orb)
30+
_ASPECTS: list[tuple[float, str, float]] = [
31+
(0.0, "conjunction", _ORB_CONJUNCTION),
32+
(180.0, "opposition", _ORB_OPPOSITION),
33+
(120.0, "trine", _ORB_TRINE),
34+
(240.0, "trine", _ORB_TRINE),
35+
(90.0, "square", _ORB_SQUARE),
36+
(270.0, "square", _ORB_SQUARE),
37+
(60.0, "sextile", _ORB_SEXTILE),
38+
(300.0, "sextile", _ORB_SEXTILE),
39+
]
40+
41+
42+
def compute_cross_chart_interactions(
43+
chart_a: ChartData,
44+
chart_b: ChartData,
45+
orb: float = _ORB_CONJUNCTION,
46+
) -> CrossChartResult:
47+
"""Find where Person A's planets overlay Person B's chart.
48+
49+
Analyses all planet pairs across two charts and categorises them into:
50+
- overlays: all significant aspects between charts
51+
- moon_contacts: any planet of A on B's Moon (or vice versa)
52+
- axis_contacts: Lagnesh-on-Moon, 7th-lord-on-Lagna cross-references
53+
- karmic_links: Saturn or Rahu/Ketu contacts indicating deep karmic bonds
54+
55+
Args:
56+
chart_a: First person's birth chart.
57+
chart_b: Second person's birth chart.
58+
orb: Default orb for conjunction detection (degrees).
59+
60+
Returns:
61+
CrossChartResult with categorised overlays and a bond strength score.
62+
"""
63+
overlays: list[PlanetOverlay] = []
64+
moon_contacts: list[PlanetOverlay] = []
65+
axis_contacts: list[PlanetOverlay] = []
66+
karmic_links: list[PlanetOverlay] = []
67+
68+
# Compute all inter-chart aspects
69+
for pa_name in _ALL_PLANETS:
70+
pa = chart_a.planets[pa_name]
71+
for pb_name in _ALL_PLANETS:
72+
pb = chart_b.planets[pb_name]
73+
aspect = _find_aspect(pa.longitude, pb.longitude)
74+
if aspect is None:
75+
continue
76+
77+
aspect_type, orb_deg = aspect
78+
effect = _classify_effect(aspect_type, pa_name, pb_name)
79+
sign = SIGNS[pa.sign_index]
80+
81+
overlay = PlanetOverlay(
82+
person_a_name=chart_a.name,
83+
person_b_name=chart_b.name,
84+
planet_a=pa_name,
85+
planet_b=pb_name,
86+
sign=sign,
87+
orb_degrees=round(orb_deg, 2),
88+
interaction_type=aspect_type,
89+
effect=effect,
90+
description=_describe(
91+
chart_a.name, pa_name, chart_b.name, pb_name, aspect_type, effect
92+
),
93+
)
94+
overlays.append(overlay)
95+
96+
# Categorise
97+
if pb_name == "Moon" or pa_name == "Moon":
98+
moon_contacts.append(overlay)
99+
if pa_name in _KARMIC_PLANETS or pb_name in _KARMIC_PLANETS:
100+
karmic_links.append(overlay)
101+
102+
# Axis contacts: Lagnesh-on-Moon, 7L-on-Lagna
103+
axis_contacts = _find_axis_contacts(chart_a, chart_b, overlays)
104+
105+
bond_strength = _compute_bond_strength(overlays, moon_contacts, axis_contacts, karmic_links)
106+
107+
summary = _build_summary(chart_a.name, chart_b.name, overlays, moon_contacts, karmic_links)
108+
109+
return CrossChartResult(
110+
person_a=chart_a.name,
111+
person_b=chart_b.name,
112+
overlays=overlays,
113+
moon_contacts=moon_contacts,
114+
axis_contacts=axis_contacts,
115+
karmic_links=karmic_links,
116+
bond_strength=round(bond_strength, 1),
117+
summary=summary,
118+
)
119+
120+
121+
def _find_aspect(lon_a: float, lon_b: float) -> tuple[str, float] | None:
122+
"""Check if two longitudes form a significant aspect."""
123+
diff = (lon_b - lon_a) % FULL_CIRCLE_DEG
124+
for target, name, max_orb in _ASPECTS:
125+
orb_deg = abs(diff - target)
126+
if orb_deg > HALF_CIRCLE_DEG:
127+
orb_deg = FULL_CIRCLE_DEG - orb_deg
128+
if orb_deg <= max_orb:
129+
return name, orb_deg
130+
return None
131+
132+
133+
def _classify_effect(aspect_type: str, planet_a: str, planet_b: str) -> str:
134+
"""Classify the qualitative effect of an aspect."""
135+
if aspect_type in ("trine", "sextile"):
136+
return "supportive"
137+
if aspect_type in ("square", "opposition"):
138+
if planet_a in _KARMIC_PLANETS or planet_b in _KARMIC_PLANETS:
139+
return "karmic"
140+
return "challenging"
141+
# Conjunction
142+
if planet_a in _KARMIC_PLANETS or planet_b in _KARMIC_PLANETS:
143+
return "karmic"
144+
return "neutral"
145+
146+
147+
def _find_axis_contacts(
148+
chart_a: ChartData,
149+
chart_b: ChartData,
150+
overlays: list[PlanetOverlay],
151+
) -> list[PlanetOverlay]:
152+
"""Find Lagnesh-on-Moon and 7L-on-Lagna cross-references."""
153+
axis: list[PlanetOverlay] = []
154+
lagnesh_a = SIGN_LORDS[chart_a.lagna_sign_index]
155+
lagnesh_b = SIGN_LORDS[chart_b.lagna_sign_index]
156+
seventh_lord_a = get_house_lord(chart_a, 7)
157+
seventh_lord_b = get_house_lord(chart_b, 7)
158+
159+
axis_pairs = {
160+
(lagnesh_a, "Moon"), # A's identity on B's emotions
161+
("Moon", lagnesh_b), # B's identity on A's emotions
162+
(seventh_lord_a, lagnesh_b), # A's 7L on B's Lagna
163+
(seventh_lord_b, lagnesh_a), # B's 7L on A's Lagna
164+
}
165+
166+
for ov in overlays:
167+
if (ov.planet_a, ov.planet_b) in axis_pairs:
168+
axis.append(ov)
169+
return axis
170+
171+
172+
def _compute_bond_strength(
173+
overlays: list[PlanetOverlay],
174+
moon_contacts: list[PlanetOverlay],
175+
axis_contacts: list[PlanetOverlay],
176+
karmic_links: list[PlanetOverlay],
177+
) -> float:
178+
"""Compute a 0-100 bond strength score."""
179+
score = 0.0
180+
# Each overlay contributes based on type
181+
weights = {"conjunction": 4.0, "trine": 3.0, "sextile": 2.0, "opposition": 2.5, "square": 1.5}
182+
for ov in overlays:
183+
score += weights.get(ov.interaction_type, 1.0)
184+
# Moon contacts are emotionally significant
185+
score += len(moon_contacts) * 3.0
186+
# Axis contacts indicate deep relationship dynamics
187+
score += len(axis_contacts) * 5.0
188+
# Karmic links indicate lasting bond
189+
score += len(karmic_links) * 2.0
190+
return min(score, 100.0)
191+
192+
193+
def _describe(
194+
name_a: str, planet_a: str, name_b: str, planet_b: str, aspect: str, effect: str
195+
) -> str:
196+
"""Build a human-readable description of an overlay."""
197+
return f"{name_a}'s {planet_a} {aspect} {name_b}'s {planet_b}{effect} influence."
198+
199+
200+
def _build_summary(
201+
name_a: str,
202+
name_b: str,
203+
overlays: list[PlanetOverlay],
204+
moon_contacts: list[PlanetOverlay],
205+
karmic_links: list[PlanetOverlay],
206+
) -> str:
207+
"""Build a one-paragraph summary of the cross-chart analysis."""
208+
parts = [f"{name_a} x {name_b}: {len(overlays)} cross-chart aspects found."]
209+
if moon_contacts:
210+
parts.append(f"{len(moon_contacts)} Moon contacts (emotional bond).")
211+
if karmic_links:
212+
parts.append(f"{len(karmic_links)} karmic links (Saturn/Rahu/Ketu).")
213+
return " ".join(parts)

0 commit comments

Comments
 (0)