Files
Jyotisha/tests/test_kemadruma_main_detector.py
T
Jesse_ChenandClaude Opus 5.5 6916bbaecc fix(yoga): Kemadruma has one main detector; lunar/solar neighbour yogas count five planets (T2, BUG-1206)
Astrologer ruling 甲7: yoga_expansion.detect_kemadruma is the main detector (2nd/12th
from the Moon count only Mars..Saturn, no kendra cancellation, nodes in kendras do
not cancel). kemadruma_yoga (ky2 kendra cancellation) and kemadruma_variant (nodes as
neighbours) are reference_only; YogaEngine.detect appends the main row.

BUG-1187 omission: the algorithmic _detect_solar_lunar_yogas block still counted the
Sun, Moon and nodes, so Ketu next to the Moon gave 'Anapha Yoga' beside Kemadruma
(Taylor) and Sun/nodes gave Durudhura beside Sunaphaa. Both blocks now count the
five true planets. divisional_yoga compared a 'house' key vargas never carry (always
'isolated') and counted the Sun; it now uses sign adjacency and the same five.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 11:40:54 +08:00

134 lines
6.6 KiB
Python

"""Kemadruma has one main detector (BUG-1206; astrologer rulings 2026-10-03 甲7).
`yoga_expansion.detect_kemadruma`: the 2nd / 12th from the Moon count only Mars,
Mercury, Jupiter, Venus, Saturn; no kendra cancellation, so a kendra holding
only Rahu / Ketu (or any planet) does not cancel it. The two data-driven rules
are `reference_only`; the rule engine appends the main row; the algorithmic
solar / lunar neighbour yogas count the same five planets (the part BUG-1187
missed), so one chart never carries Kemadruma together with Sunapha / Anapha /
Durudhura. `divisional_yoga` uses the same rule on vargas.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
import pytest
from scripts.divisional_yoga import _detect_moon_varga_yogas
from scripts.jyotish_engine import compute_chart_data
from scripts.main_yoga_detectors import kemadruma_rows
from scripts.yoga_engine import YogaEngine
from scripts.yoga_expansion import detect_kemadruma
ROOT = Path(__file__).resolve().parents[1]
RULES = ROOT / "references" / "yoga_rules.json"
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
LUNAR_NEIGHBOUR = re.compile(r"sunaph|anaph|durudh|duradh", re.IGNORECASE)
def _chart(asc: str = "Aries", **signs: str) -> dict:
asc_idx = SIGNS.index(asc)
return {name: {"sign": sign, "house": (SIGNS.index(sign) - asc_idx) % 12 + 1, "degree": 15.0,
"lon": SIGNS.index(sign) * 30 + 15.0}
for name, sign in signs.items()}
def _detect(planets: dict, asc: str = "Aries") -> list[dict]:
return YogaEngine(str(RULES)).detect(planets, asc)
# Moon in Libra (7th from Aries); five planets kept away from Virgo / Scorpio.
ISOLATED = dict(Sun="Aquarius", Moon="Libra", Mars="Aquarius", Mercury="Aquarius",
Jupiter="Cancer", Venus="Pisces", Saturn="Capricorn", Rahu="Pisces", Ketu="Virgo")
def test_data_driven_rules_are_reference_only() -> None:
rules = {rule["id"]: rule for rule in json.loads(RULES.read_text(encoding="utf-8"))["rules"]}
for rule_id in ("kemadruma_yoga", "kemadruma_variant"):
assert rules[rule_id]["reference_only"] is True
assert "BUG-1206" in rules[rule_id]["reference_only_reason"]
assert not {"kemadruma_yoga", "kemadruma_variant"} & {rule["id"] for rule in YogaEngine(str(RULES)).rules}
def test_kendra_with_only_nodes_does_not_cancel() -> None:
# Lagna kendras from Aries: Aries, Cancer, Libra, Capricorn. Only the nodes
# sit in kendras besides the Moon and Jupiter / Saturn (Cancer, Capricorn).
planets = _chart(**{**ISOLATED, "Rahu": "Aries", "Ketu": "Libra"})
assert detect_kemadruma(planets)["present"] is True
names = [row["name"] for row in _detect(planets)]
assert names.count("Kemadruma Yoga") == 1
def test_planets_in_kendras_do_not_cancel() -> None:
planets = _chart(**ISOLATED)
assert any(row["house"] in (1, 4, 7, 10) for name, row in planets.items() if name not in ("Moon", "Rahu", "Ketu"))
rows = [row for row in _detect(planets) if row["name"] == "Kemadruma Yoga"]
assert rows == kemadruma_rows(planets)
assert rows[0]["rule_id"] == "main_detector:kemadruma"
def test_ketu_next_to_the_moon_forms_no_lunar_neighbour_yoga() -> None:
names = {row["name"] for row in _detect(_chart(**ISOLATED))}
assert "Kemadruma Yoga" in names
assert not {name for name in names if LUNAR_NEIGHBOUR.search(name)}
def test_a_true_planet_next_to_the_moon_removes_kemadruma() -> None:
planets = _chart(**{**ISOLATED, "Saturn": "Virgo"})
names = {row["name"] for row in _detect(planets)}
assert "Kemadruma Yoga" not in names
assert {"Anapha Yoga", "Anaphaa Yoga"} <= names
def test_sun_or_nodes_next_to_the_sun_form_no_solar_neighbour_yoga() -> None:
planets = _chart(**{**ISOLATED, "Sun": "Leo", "Moon": "Virgo", "Ketu": "Virgo", "Rahu": "Pisces", "Jupiter": "Sagittarius"})
names = {row["name"] for row in _detect(planets)}
assert not {"Veshi Yoga", "Voshi Yoga", "Ubhayachari Yoga"} & names
def test_divisional_kemadruma_uses_the_same_rule() -> None:
def varga(**signs: str) -> dict:
return {name: {"sign": sign, "sign_idx": SIGNS.index(sign), "degree": 1.0} for name, sign in signs.items()}
isolated = varga(Moon="Libra", Sun="Virgo", Rahu="Scorpio", Mars="Aries", Mercury="Aries",
Jupiter="Aries", Venus="Aries", Saturn="Aries")
assert [row["name"] for row in _detect_moon_varga_yogas(isolated)] == ["Kemadruma (分盘)"]
supported = varga(Moon="Libra", Sun="Aries", Mars="Scorpio", Mercury="Aries", Jupiter="Aries",
Venus="Aries", Saturn="Aries")
assert _detect_moon_varga_yogas(supported) == []
FIGURES = (
("minute_rectification_development_v1.json", "steve_jobs_1955_development"),
("minute_rectification_holdout_v4.json", "barack_obama_1961_aa_v4_holdout"),
("minute_rectification_holdout_v4.json", "elizabeth_taylor_1932_aa_v4_holdout"),
("minute_rectification_holdout_v5.json", "marilyn_monroe_1926_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "judy_garland_1922_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "edith_piaf_1915_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "frida_kahlo_1907_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "george_w_bush_1946_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "zinedine_zidane_1972_aa_v5_holdout"),
)
@pytest.mark.parametrize("library,case_id", FIGURES)
def test_public_figures_never_carry_kemadruma_with_a_lunar_neighbour_yoga(library: str, case_id: str) -> None:
cases = json.loads((ROOT / "references/real_case_calibration" / library).read_text(encoding="utf-8"))["cases"]
birth = next(case for case in cases if case["case_id"] == case_id)["birth"]
year, month, day = (int(part) for part in birth["date"].split("-"))
hour, minute = (int(part) for part in birth["time"].split(":"))
chart, *_ = compute_chart_data(year, month, day, hour, minute, birth["latitude"], birth["longitude"],
birth["timezone_offset"], ayanamsa_name="raman")
planets = {name: {"sign": row["sign"], "house": row["house"], "degree": row["degree_in_sign"],
"lon": row["degree_raw"], "retrograde": row["retrograde"]}
for name, row in chart["planets"].items()}
names = {row["name"] for row in _detect(planets, chart["ascendant"]["sign"])}
kemadruma = "Kemadruma Yoga" in names
assert kemadruma == detect_kemadruma(planets)["present"]
if kemadruma:
assert not {name for name in names if LUNAR_NEIGHBOUR.search(name)}, case_id