"""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