fix(engine): apply #166 batch 2 rulings to functional roles, yogas, Chara / Narayana, Gulika, sunrise records and varga methods (BUG-1590..1598)
Astrologer ruling 2026-11-04 on #166, product decisions the same day (Q9 display only, later; Q12 record, do not switch). - Q4 (BUG-1590): Gemini Saturn (8, 9) and Virgo Saturn (5, 6) are mixed (neutral), not pure benefics by trine priority; the astrologer's table for the eight ambiguous cells rides along as ambiguous_cell_notes. Other lordships are kept. - Q10 (BUG-1591): Chara Dasha with both Scorpio / Aquarius lords in the sign is fixed at 12 years (Ketu exalted in Scorpio gave 13). - Q11 (BUG-1592): legacy Narayana counts only the nine grahas as occupants. - Q8 (BUG-1593): Narayana period reports the adjustment that took effect (Mercury in Virgo: 12, +0, not "12 + 1" capped silently). - Q15 (BUG-1594): upagrahas carry both raw Saturn-part points and name the mapping (PyJHora-compatible Gulika = start, Maandi = midpoint; PVR 4.3 is the reverse and is listed, never claimed). - Q12 / Q13 (BUG-1595): every day/night window, Shadbala and the birth Panchanga record their sunrise convention and hora method. No value changes. - Q5 (BUG-1596): "Gajakesari Yoga" is PVR's strict definition (benefic association, not debilitated, combust or in a natural enemy's sign); the strong-Jupiter variant and the bare Moon-Jupiter kendra relation are separate rules outside the card's closed list. New sandbox helpers combust() and in_natural_enemy_sign(). - Q6 (BUG-1597): Matrunasa split into Raman 198 (named Parashari aspects, nodes for conjunction / hemming only) and Raman 199 (same parents-card row via canonical_mapping); each hit names its trigger; "high confidence" and mother's-health effects removed. Custom rules may set `triggers`. - Q31 (BUG-1598): calc_varga names the D3 / D24 / D30 / D60 method; D3 / D24 / D60 of varga.py and divisional_charts_extended agree on every 0.1 deg. - Q29: the 7-karaka note says PiK does not apply in that scheme. tests/test_astrologer_rulings_166_batch2.py (25 tests). Rectification identity and re-freeze follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
This commit is contained in:
@@ -20,8 +20,9 @@ export const KARAKA_TERMS: Readonly<Record<CharaKarakaKey, KarakaTerm>> = {
|
||||
};
|
||||
|
||||
export const KARAKA_SCHEME_NOTE: Readonly<Record<"7" | "8", string>> = {
|
||||
"7": "七星制:太阳至土星七颗星,按未取整的星座内度数由高到低排;罗睺、计都不计",
|
||||
"8": "八星制:七星加罗睺,罗睺按 30° 减星座内度数排;计都不计",
|
||||
// #166 第 29 条(占星师 2026-11-04):默认七星制;七星制里没有父亲星(PiK),写明不适用,不是漏算;切到八星制会整体重排。
|
||||
"7": "七星制(默认):太阳至土星七颗星,按未取整的星座内度数由高到低排;罗睺、计都不计。父亲星(PiK)在七星制中不适用",
|
||||
"8": "八星制:七星加罗睺,罗睺按 30° 减星座内度数排;计都不计。整张表重新排位,不是在七星制末尾补一行",
|
||||
};
|
||||
|
||||
/** Cell text for a role, e.g. 「AK · 自我」. */
|
||||
|
||||
+119
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"description": "Yoga 规则库 - 数据驱动架构 v1.0",
|
||||
"total_rules": 477,
|
||||
"total_rules": 480,
|
||||
"rules": [
|
||||
{
|
||||
"id": "raja_yoga",
|
||||
@@ -927,8 +927,8 @@
|
||||
{
|
||||
"id": "gajakesari",
|
||||
"name": "Gajakesari Yoga",
|
||||
"name_cn": "象狮格局",
|
||||
"source": "BPHS",
|
||||
"name_cn": "象狮格局(严格定义)",
|
||||
"source": "PVR, Vedic Astrology: An Integrated Approach (strict definition); BPHS",
|
||||
"bvr_id": "BVR-1",
|
||||
"category": "chandra",
|
||||
"logic": {
|
||||
@@ -946,9 +946,9 @@
|
||||
},
|
||||
{
|
||||
"type": "custom",
|
||||
"expr": "\n# BVR-1 Gajakesari: Jupiter must be strong or have benefic association\n# Jupiter not debilitated\njup_ok = not ctx.is_debilitated('Jupiter')\n# Benefic association (conjunction or graha drishti)\nbenefics = ['Venus', 'Mercury', 'Jupiter']\nhas_benefic = any(\n ctx.house_of('Jupiter') == ctx.house_of(b) or aspect(b, 'Jupiter')\n for b in benefics if b in ctx.planets and b != 'Jupiter'\n)\n# Jupiter strong on its own\njup_strong = ctx.is_exalted('Jupiter') or ctx.is_own_sign('Jupiter') or ctx.is_moolatrikona('Jupiter')\njup_ok and (has_benefic or jup_strong)\n",
|
||||
"combo_template": "木星有吉星合/照或不弱陷",
|
||||
"combo_template_en": "Jupiter conjoined or aspected by benefics, or not debilitated"
|
||||
"expr": "\n# #166 Q5 (BUG-1596): PVR strict definition. Jupiter in a kendra from the Moon (previous condition),\n# conjoined or aspected by a benefic, not debilitated, not combust, not in a natural enemy's sign.\n# The strong-Jupiter substitute is a separate rule (gajakesari_strong_jupiter_variant).\ntriggers = []\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nok = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nif ok:\n triggers = ['benefic association: ' + ', '.join(benefic_links)]\n__result__ = ok\n",
|
||||
"combo_template": "严格定义:木星受吉星合相或相照,且不落陷、不燃烧、不落敌宫",
|
||||
"combo_template_en": "Strict definition: Jupiter conjoined or aspected by a benefic, not debilitated, combust or in an enemy sign"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -965,9 +965,86 @@
|
||||
"strength": "中",
|
||||
"strength_en": "moderate",
|
||||
"enabled": true,
|
||||
"source_reference": "BVR-1: Jupiter in Kendra from Moon, with benefic association or strong dignity",
|
||||
"source_reference": "#166 Q5 (astrologer 2026-11-04): strict definition only; the strong-Jupiter variant and the bare Moon-Jupiter kendra relation are separate rules",
|
||||
"bvr_chapter": "Chapter 1"
|
||||
},
|
||||
{
|
||||
"id": "gajakesari_strong_jupiter_variant",
|
||||
"name": "Strong-Jupiter Gajakesari Variant",
|
||||
"name_cn": "象狮格局·强木星变体",
|
||||
"source": "PVR, Vedic Astrology: An Integrated Approach (variant after the strict definition)",
|
||||
"bvr_id": null,
|
||||
"category": "chandra",
|
||||
"logic": {
|
||||
"type": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "in_houses_from",
|
||||
"planet": "Jupiter",
|
||||
"from": "Moon",
|
||||
"relations": [
|
||||
"kendra"
|
||||
],
|
||||
"combo_template": "木星在月亮的Kendra(第{h}宫)",
|
||||
"combo_template_en": "Jupiter in a Kendra from the Moon (house {h})"
|
||||
},
|
||||
{
|
||||
"type": "custom",
|
||||
"expr": "\n# #166 Q5: variant only when the strict definition does not hold.\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nstrict = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nstrong_jup = exalted('Jupiter') or own('Jupiter') or moola('Jupiter')\ntriggers = [f\"strong Jupiter: {dignity('Jupiter')}\"] if strong_jup and not strict else []\n__result__ = strong_jup and not strict and not debil('Jupiter')\n",
|
||||
"combo_template": "变体:木星强(入旺 / 本宫 / 根本三角)代替吉星关联",
|
||||
"combo_template_en": "Variant: a strong Jupiter (exalted / own / moolatrikona) in place of benefic association"
|
||||
}
|
||||
]
|
||||
},
|
||||
"effects": [
|
||||
"智慧学识",
|
||||
"财富名声",
|
||||
"道德品质"
|
||||
],
|
||||
"effects_en": [
|
||||
"Wisdom and learning",
|
||||
"Wealth and reputation",
|
||||
"Moral integrity"
|
||||
],
|
||||
"strength": "弱",
|
||||
"strength_en": "weak",
|
||||
"enabled": true,
|
||||
"source_reference": "#166 Q5: a strong Jupiter (exalted / own / moolatrikona) substitutes for the benefic association; not the strict yoga",
|
||||
"bvr_chapter": "Chapter 1"
|
||||
},
|
||||
{
|
||||
"id": "moon_jupiter_kendra_relation",
|
||||
"name": "Moon-Jupiter Kendra Relation",
|
||||
"name_cn": "月木角宫关系",
|
||||
"source": "#166 Q5 base structure",
|
||||
"bvr_id": null,
|
||||
"category": "chandra",
|
||||
"logic": {
|
||||
"type": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "in_houses_from",
|
||||
"planet": "Jupiter",
|
||||
"from": "Moon",
|
||||
"relations": [
|
||||
"kendra"
|
||||
],
|
||||
"combo_template": "木星在月亮的Kendra(第{h}宫)",
|
||||
"combo_template_en": "Jupiter in a Kendra from the Moon (house {h})"
|
||||
}
|
||||
]
|
||||
},
|
||||
"effects": [
|
||||
"只表示木星在月亮的角宫这一结构,不等于象狮格局成立"
|
||||
],
|
||||
"effects_en": [
|
||||
"Only the structure of Jupiter in a kendra from the Moon; not a Gajakesari yoga"
|
||||
],
|
||||
"strength": "弱",
|
||||
"strength_en": "weak",
|
||||
"enabled": true,
|
||||
"source_reference": "#166 Q5 (astrologer 2026-11-04): base structure shown separately from the strict yoga"
|
||||
},
|
||||
{
|
||||
"id": "neechabhanga_basic",
|
||||
"name": "Neechabhanga (reference: dispositor in kendra)",
|
||||
@@ -11496,28 +11573,52 @@
|
||||
"id": "bvr_matrunasa_precise",
|
||||
"name": "Matrunasa Yoga",
|
||||
"name_cn": "母亲不利瑜伽",
|
||||
"source": "B.V. Raman / PyJHora",
|
||||
"source": "B.V. Raman, Three Hundred Important Combinations no. 198",
|
||||
"bvr_id": null,
|
||||
"category": "durbhaga",
|
||||
"logic": {
|
||||
"type": "custom",
|
||||
"expr": "\n# PyJHora/BVR 198-199. Yoga 198 uses natural malefics and rasi drishti on Moon's rasi.\nif 'Moon' not in ctx.planets:\n False\nelse:\n moon_h = house_of('Moon')\n prev_h = ((moon_h - 2) % 12) + 1 if moon_h else None\n next_h = (moon_h % 12) + 1 if moon_h else None\n hemmed = bool(prev_h and next_h and house_has_malefic(prev_h) and house_has_malefic(next_h))\n associated = any(same_house('Moon', p) for p in MALEFICS if p in ctx.planets)\n aspected = any(rasi_aspects_house(p, moon_h) for p in MALEFICS if p in ctx.planets)\n y198 = hemmed or associated or aspected\n l4 = lord(4)\n nav_lord = navamsa_dispositor(l4) if l4 else None\n final_planet = navamsa_dispositor(nav_lord) if nav_lord else None\n y199 = final_planet in ctx.planets and house_of(final_planet) in [6, 8, 12]\n y198 or y199\n",
|
||||
"combo_template": "月亮受凶星夹制/合相/相位,或4宫主Navamsa链最终落6/8/12",
|
||||
"combo_template_en": "Moon hemmed, conjoined or aspected by malefics, or the 4th lord's Navamsa chain ending in the 6th/8th/12th"
|
||||
"expr": "\n# #166 Q6 (BUG-1597): Raman 300 Combinations no. 198 only (no. 199 is bvr_matrunasa_199).\n# Hemmed by, associated with, or aspected by natural malefics (OR). Aspects are named Parashari graha\n# aspects (7th; Mars 4/8, Jupiter 5/9, Saturn 3/10); Rahu / Ketu count for conjunction and hemming,\n# not for aspect (implementation choice recorded in BUG-1597). No Jaimini rasi drishti.\ntriggers = []\nif 'Moon' in ctx.planets:\n moon_h = house_of('Moon')\n prev_h = ((moon_h - 2) % 12) + 1\n next_h = (moon_h % 12) + 1\n prev_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == prev_h]\n next_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == next_h]\n if prev_m and next_m:\n triggers.append('hemmed: ' + ', '.join(prev_m + next_m))\n conj = [p for p in MALEFICS if p in ctx.planets and same_house('Moon', p)]\n if conj:\n triggers.append('with Moon: ' + ', '.join(conj))\n asp = [p for p in ('Sun', 'Mars', 'Saturn') if p in ctx.planets and not same_house('Moon', p) and aspect(p, 'Moon')]\n if asp:\n triggers.append('aspects Moon: ' + ', '.join(asp))\n__result__ = bool(triggers)\n",
|
||||
"combo_template": "Raman 198:月亮受凶星夹制、合相或相照",
|
||||
"combo_template_en": "Raman 198: Moon hemmed, conjoined or aspected by malefics"
|
||||
},
|
||||
"effects": [
|
||||
"母亲健康不佳",
|
||||
"与母亲缘薄"
|
||||
"母亲相关的结构特征(月亮受凶星影响),只作参考,不作母亲健康或寿命判断"
|
||||
],
|
||||
"effects_en": [
|
||||
"Mother's health is poor",
|
||||
"Weak bond with mother"
|
||||
"A structural feature concerning the mother (Moon under malefic influence); reference only, not a statement about the mother's health or life"
|
||||
],
|
||||
"strength": "弱",
|
||||
"strength_en": "weak",
|
||||
"enabled": true,
|
||||
"accuracy_note": "高置信:定义来自 PyJHora/B.V. Raman。",
|
||||
"accuracy_note_en": "High confidence: definition from PyJHora / B.V. Raman."
|
||||
"accuracy_note": "定义来自 B.V. Raman 第 198 条;成立面很宽,区分度低,只作参考(#166 第 6 条)。",
|
||||
"accuracy_note_en": "Definition from B.V. Raman no. 198; it holds widely and discriminates little; reference only (#166 Q6)."
|
||||
},
|
||||
{
|
||||
"id": "bvr_matrunasa_199",
|
||||
"name": "Matrunasa Yoga (Raman 199)",
|
||||
"name_cn": "母亲不利瑜伽",
|
||||
"source": "B.V. Raman, Three Hundred Important Combinations no. 199",
|
||||
"bvr_id": null,
|
||||
"category": "durbhaga",
|
||||
"logic": {
|
||||
"type": "custom",
|
||||
"expr": "\n# #166 Q6: Raman no. 199. Lord of the D9 sign holding the 4th lord; then the lord of the D9 sign\n# that planet holds; that final planet in the 6th / 8th / 12th of D1.\ntriggers = []\nl4 = lord(4)\nnav_lord = navamsa_dispositor(l4) if l4 else None\nfinal_planet = navamsa_dispositor(nav_lord) if nav_lord else None\nif final_planet in ctx.planets and house_of(final_planet) in [6, 8, 12]:\n triggers.append(f'{l4} -> {nav_lord} -> {final_planet} in house {house_of(final_planet)}')\n__result__ = bool(triggers)\n",
|
||||
"combo_template": "Raman 199:4宫主的 Navamsa 链最终落 6 / 8 / 12 宫",
|
||||
"combo_template_en": "Raman 199: the 4th lord's Navamsa chain ends in the 6th / 8th / 12th"
|
||||
},
|
||||
"effects": [
|
||||
"母亲相关的结构特征(月亮受凶星影响),只作参考,不作母亲健康或寿命判断"
|
||||
],
|
||||
"effects_en": [
|
||||
"A structural feature concerning the mother (Moon under malefic influence); reference only, not a statement about the mother's health or life"
|
||||
],
|
||||
"strength": "弱",
|
||||
"strength_en": "weak",
|
||||
"enabled": true,
|
||||
"accuracy_note": "定义来自 B.V. Raman 第 199 条;只作参考(#166 第 6 条)。",
|
||||
"accuracy_note_en": "Definition from B.V. Raman no. 199; reference only (#166 Q6).",
|
||||
"canonical_mapping": "bvr_matrunasa_precise"
|
||||
},
|
||||
{
|
||||
"id": "bvr_nishkapata_precise",
|
||||
@@ -16385,5 +16486,5 @@
|
||||
"bvr_chapter": ""
|
||||
}
|
||||
],
|
||||
"total_enabled_rules": 406
|
||||
"total_enabled_rules": 409
|
||||
}
|
||||
|
||||
@@ -35,8 +35,11 @@ ROLE_OVERRIDES = {
|
||||
# (BUG-1570, product decision 2026-11-04; scoring-13).
|
||||
"Aries": {"Mars": "neutral", "Venus": "malefic"},
|
||||
"Taurus": {"Venus": "malefic", "Sun": "benefic"},
|
||||
"Gemini": {"Jupiter": "malefic"},
|
||||
"Virgo": {"Jupiter": "malefic"},
|
||||
# #166 Q4 (BUG-1590, astrologer 2026-11-04): a lord of a trine and of 6/8 is
|
||||
# not a pure benefic unless the sign's own verse says so. Gemini Saturn (8, 9)
|
||||
# and Virgo Saturn (5, 6) are mixed. 原值: 三方宫优先判 benefic;新值: neutral(混合)。
|
||||
"Gemini": {"Jupiter": "malefic", "Saturn": "neutral"},
|
||||
"Virgo": {"Jupiter": "malefic", "Saturn": "neutral"},
|
||||
"Libra": {"Venus": "neutral", "Mars": "malefic"},
|
||||
"Scorpio": {"Mars": "neutral", "Sun": "benefic", "Venus": "malefic"},
|
||||
"Sagittarius": {"Jupiter": "neutral", "Mercury": "neutral"},
|
||||
@@ -46,6 +49,21 @@ ROLE_OVERRIDES = {
|
||||
}
|
||||
|
||||
|
||||
# #166 Q4: the table the astrologer gave for the ambiguous cells. The role is the
|
||||
# summary; the other lordship is kept as a duty, never dropped. Basis says whether
|
||||
# the sign's own verse states it or it is derived from the general rules.
|
||||
AMBIGUOUS_CELL_NOTES = {
|
||||
("Taurus", "Mercury"): ("mildly benefic; keeps 2nd-lord duties", "sign verse: somewhat auspicious"),
|
||||
("Gemini", "Mercury"): ("Lagna lord, supportive; not a Yogakaraka", "general rules"),
|
||||
("Gemini", "Saturn"): ("mixed; 9th-lord support, not a pure benefic", "general rules with sign context"),
|
||||
("Sagittarius", "Saturn"): ("challenging (3rd lord); Maraka (2nd lord) listed separately", "3rd-house rule plus sign verse"),
|
||||
("Cancer", "Jupiter"): ("mildly benefic; keeps 6th-lord duties", "sign verse"),
|
||||
("Leo", "Jupiter"): ("mildly benefic; keeps 8th-lord duties", "sign verse"),
|
||||
("Virgo", "Saturn"): ("mixed; not a pure benefic on the 5th lordship alone", "sign verse does not name it a benefic"),
|
||||
("Capricorn", "Mercury"): ("mildly benefic; keeps 6th-lord duties", "sign verse"),
|
||||
}
|
||||
|
||||
|
||||
def normalize_sign(value: Any) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
@@ -139,6 +157,10 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
|
||||
"yogakaraka_definition": "distinct lordships: one of 4/7/10 and one of 5/9",
|
||||
"rule_reference": "references/functional-role-profile.md",
|
||||
"assertion_label": "parameter_sensitive",
|
||||
"ambiguous_cell_notes": {
|
||||
planet: {"summary": note, "basis": basis}
|
||||
for (sign, planet), (note, basis) in AMBIGUOUS_CELL_NOTES.items() if sign == asc_sign
|
||||
},
|
||||
"role_basis": {
|
||||
planet: "BPHS ch34 sign-specific exception" if planet in ROLE_OVERRIDES.get(asc_sign, {})
|
||||
else "BPHS ch34 general ownership; associations not evaluated"
|
||||
|
||||
@@ -68,6 +68,16 @@ NIGHT_RULERS = (
|
||||
(3, 4, 5, 6, -1, 0, 1, 2),
|
||||
(4, 5, 6, -1, 0, 1, 2, 3),
|
||||
)
|
||||
# #166 Q15 (BUG-1594, astrologer 2026-11-04): the two Saturn-part points are the
|
||||
# raw data; which one is called Gulika and which Maandi depends on the named
|
||||
# method. This product keeps the PyJHora-compatible mapping (Gulika = start,
|
||||
# Maandi = midpoint). PVR's textbook §4.3 maps them the other way round, so the
|
||||
# current mapping must never be labelled "PVR".
|
||||
GULIKA_MAANDI_METHOD = "pyjhora_compatible_gulika_start_maandi_midpoint"
|
||||
GULIKA_MAANDI_METHODS = {
|
||||
GULIKA_MAANDI_METHOD: {"Gulika": "saturn_segment_start", "Maandi": "saturn_segment_midpoint"},
|
||||
"pvr_textbook_4_3": {"Gulika": "saturn_segment_midpoint", "Maandi": "saturn_segment_start"},
|
||||
}
|
||||
TEMPORAL_UPAGRAHAS = {
|
||||
"Kaala": (0, "middle"),
|
||||
"Mrityu": (2, "middle"),
|
||||
@@ -216,9 +226,17 @@ def calculate_upagrahas(
|
||||
)
|
||||
for name, (planet_index, segment_part) in TEMPORAL_UPAGRAHAS.items()
|
||||
}
|
||||
saturn_points = {
|
||||
"saturn_segment_start": {k: temporal["Gulika"][k] for k in ("longitude", "sign_idx", "degree_in_sign", "segment_fraction", "segment_jd_ut")},
|
||||
"saturn_segment_midpoint": {k: temporal["Maandi"][k] for k in ("longitude", "sign_idx", "degree_in_sign", "segment_fraction", "segment_jd_ut")},
|
||||
}
|
||||
return {
|
||||
"status": "computed_pending_validation",
|
||||
"raw": {**temporal, **_solar_upagrahas(sun_longitude)},
|
||||
"saturn_segment_points": saturn_points,
|
||||
"gulika_maandi_method": GULIKA_MAANDI_METHOD,
|
||||
"gulika_maandi_mapping": GULIKA_MAANDI_METHODS[GULIKA_MAANDI_METHOD],
|
||||
"gulika_maandi_note": "Not the PVR textbook mapping: PVR §4.3 takes Gulika at the midpoint and Maandi at the start.",
|
||||
"ayanamsa": ayanamsa_name,
|
||||
"ayanamsa_display": ayanamsa_display_name(ayanamsa_name),
|
||||
"scope": "d1_only",
|
||||
|
||||
+9
-2
@@ -440,8 +440,8 @@ def _resolve_chara_dasha_lord(longitudes, sign_idx):
|
||||
one elsewhere (K.N. Rao, Predicting through Jaimini's Chara Dasha; Rath,
|
||||
Narayana Dasa p.43 (c); PyJHora basic rule; prashna guide rule 1).
|
||||
Otherwise the stronger by `_jaimini_stronger_planet`, judged where each
|
||||
planet stands. Both in the sign keep the stronger-planet result (the
|
||||
count is 12 either way).
|
||||
planet stands. Both in the sign keep the stronger-planet result; the
|
||||
period is then fixed at 12 years (BUG-1591, #166 Q10).
|
||||
其他星座: 使用传统宫主
|
||||
"""
|
||||
sign_name = SIGNS[sign_idx]
|
||||
@@ -549,6 +549,13 @@ def _chara_dasha_duration_profile(
|
||||
if years <= 0:
|
||||
years = 12
|
||||
|
||||
# 原值: 天蝎 / 水瓶两颗主星都在本宫时,选出的主星入旺(计都在天蝎)再 +1,得 13 年
|
||||
# 新值: 两颗主星都在本宫固定 12 年,不再加减
|
||||
# 原因: BUG-1591,#166 第 10 条(占星师 2026-11-04;Rath, Narayana Dasa p.43 (a))
|
||||
pair = {10: ('Saturn', 'Rahu'), 7: ('Mars', 'Ketu')}.get(sign_idx)
|
||||
if pair and all(p in longitudes and _get_planet_house(longitudes, p) == sign_idx for p in pair):
|
||||
return 12
|
||||
|
||||
# This remains the historical PyJHora-aligned local KN Rao profile. The
|
||||
# source ledger keeps the no-extra-year K.N. Rao textual boundary visible
|
||||
# for later formula reconciliation.
|
||||
|
||||
@@ -15304,6 +15304,9 @@ def _build_natal_foundation_modules(args, planet_lons, *, asc_lon=None, jd=None)
|
||||
"position_mode": _normalize_position_mode(getattr(args, "position_mode", "legacy")),
|
||||
"sunrise_jd_ut": daynight["sunrise_jd_ut"],
|
||||
"sunset_jd_ut": daynight["sunset_jd_ut"],
|
||||
# #166 Q12 / Q13 (BUG-1595): record, do not switch (product 2026-11-04).
|
||||
"sunrise_convention": daynight.get("sunrise_convention"),
|
||||
"hora_method": "fixed_60_minutes_from_sunrise_approximation",
|
||||
}
|
||||
panchanga_result = {"status": "computed", "raw": panchanga}
|
||||
except Exception as exc:
|
||||
|
||||
@@ -141,6 +141,11 @@ _NARAYANA_NODE_OWN_SIGNS = {"Rahu": {10}, "Ketu": {7}}
|
||||
# 核心计算
|
||||
# =========================================================================
|
||||
|
||||
|
||||
# #166 Q11 (BUG-1592): occupants are the nine grahas; the Lagna and other points
|
||||
# passed in planet_lons are not counted.
|
||||
OCCUPANT_GRAHAS = frozenset(("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"))
|
||||
|
||||
def _get_planet_sign(planet_name: str, planet_lons: Dict[str, float]) -> Optional[int]:
|
||||
"""获取行星所在星座索引"""
|
||||
lon = planet_lons.get(planet_name)
|
||||
@@ -298,10 +303,12 @@ def select_narayana_seed_sign(
|
||||
candidates = {}
|
||||
for sign_idx in candidate_indices:
|
||||
lord = SIGN_LORDS[SIGNS[sign_idx]]
|
||||
# 原值: planet_lons 里所有键都算占据(含 Lagna 等点);新值: 只算九曜
|
||||
# 原因: BUG-1592,#166 第 11 条:上升点不是占据星
|
||||
occupants = sorted(
|
||||
planet
|
||||
for planet in planet_lons
|
||||
if _get_planet_sign(planet, planet_lons) == sign_idx
|
||||
if planet in OCCUPANT_GRAHAS and _get_planet_sign(planet, planet_lons) == sign_idx
|
||||
)
|
||||
aspect_factors = []
|
||||
for planet in dict.fromkeys(("Jupiter", "Mercury", lord)):
|
||||
@@ -377,7 +384,11 @@ def calculate_narayana_period_years(
|
||||
adjustment = 1 if lord_sign_idx in dignity.get("exalted", set()) else 0
|
||||
if lord_sign_idx in dignity.get("debilitated", set()):
|
||||
adjustment = -1
|
||||
return max(0, min(12, base_years + adjustment)), adjustment
|
||||
years = max(0, min(12, base_years + adjustment))
|
||||
# 原值: 返回原始 adjustment(12 年再 +1 被截回 12 时仍报 +1,例如水星在处女)
|
||||
# 新值: 返回实际生效的调整量 years − base_years
|
||||
# 原因: BUG-1593,#166 第 8 条:Rath 版上限 12 年,处女大运不能显示成 12 → 13
|
||||
return years, years - base_years
|
||||
|
||||
|
||||
def _narayana_dual_lord_strength(
|
||||
@@ -385,8 +396,9 @@ def _narayana_dual_lord_strength(
|
||||
lord_sign_idx: int,
|
||||
planet_lons: Dict[str, float],
|
||||
) -> tuple[int, int, int]:
|
||||
occupants = sum(
|
||||
1 for planet in planet_lons if _get_planet_sign(planet, planet_lons) == lord_sign_idx
|
||||
occupants = sum( # BUG-1592: only the nine grahas occupy a sign
|
||||
1 for planet in planet_lons
|
||||
if planet in OCCUPANT_GRAHAS and _get_planet_sign(planet, planet_lons) == lord_sign_idx
|
||||
)
|
||||
sign_lord = SIGN_LORDS[SIGNS[lord_sign_idx]]
|
||||
aspect_factors = sum(
|
||||
|
||||
@@ -12,6 +12,17 @@ class SahamDayNightError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
# #166 Q12 (BUG-1595, astrologer 2026-11-04; product: record, do not switch):
|
||||
# every day/night window states which sunrise it used, so day/night, hora lord,
|
||||
# annual lord and rectification can be traced. Each module keeps its convention.
|
||||
SUNRISE_CONVENTIONS = {
|
||||
"apparent": {"name": "apparent_upper_limb_with_refraction", "disc": "upper_limb", "refraction": True,
|
||||
"horizon_altitude_deg": 0.0, "swisseph_flags": "CALC_RISE / CALC_SET"},
|
||||
"hindu": {"name": "hindu_disc_centre_no_refraction", "disc": "centre", "refraction": False,
|
||||
"horizon_altitude_deg": 0.0, "swisseph_flags": "BIT_HINDU_RISING"},
|
||||
}
|
||||
|
||||
|
||||
def determine_daytime(
|
||||
moment: datetime,
|
||||
*,
|
||||
@@ -54,6 +65,7 @@ def determine_daytime(
|
||||
"sunrise_jd_ut": sunrise_jd,
|
||||
"sunset_jd_ut": sunset_jd,
|
||||
"method": "swisseph.rise_trans",
|
||||
"sunrise_convention": {**SUNRISE_CONVENTIONS["apparent"], "lat": float(lat), "lon": float(lon)},
|
||||
"boundary": "Formula-specific +30 degree exceptions must be applied by the Saham rule layer, not inferred from house placement.",
|
||||
}
|
||||
|
||||
@@ -86,5 +98,6 @@ def determine_daytime(
|
||||
"sunset_jd_ut": sunset_jd,
|
||||
"next_sunrise_jd_ut": next_sunrise_jd,
|
||||
"method": "swisseph.rise_trans_hindu_rising",
|
||||
"sunrise_convention": {**SUNRISE_CONVENTIONS["hindu"], "lat": float(lat), "lon": float(lon)},
|
||||
"boundary": "Formula-specific +30 degree exceptions must be applied by the Saham rule layer, not inferred from house placement.",
|
||||
}
|
||||
|
||||
@@ -802,6 +802,9 @@ def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
|
||||
'drik_special_aspects': DRIK_SPECIAL_ASPECT_CONVENTION,
|
||||
'mercury_nature': MERCURY_NATURE_CONVENTION,
|
||||
'kendradi': KENDRADI_CONVENTION,
|
||||
# #166 Q12 / Q13 (BUG-1595): which sunrise and which hora method Kala used.
|
||||
'sunrise': 'hindu_disc_centre_no_refraction' if context else 'legacy_fixed_06_00',
|
||||
'hora': 'fixed_60_minutes_from_sunrise_approximation' if context else 'legacy_approximation',
|
||||
},
|
||||
'external_parity_boundary': 'Chesta remains cross-engine method-conflicted; totals inherit that boundary.',
|
||||
'is_night_birth': is_night,
|
||||
|
||||
@@ -164,6 +164,17 @@ def varga_map_vedastro(si, pi, div):
|
||||
return (si + pi) % 12
|
||||
return varga_map(si, pi, div)
|
||||
|
||||
# #166 Q31 (BUG-1598, astrologer 2026-11-04): name the method instead of relying
|
||||
# on "the PyJHora default". Start, direction, part size and boundary ownership
|
||||
# ([start, end), part index = int(degree / part)) are fixed here.
|
||||
VARGA_METHODS = {
|
||||
3: "parashari_drekkana_same_5th_9th", # 10 deg parts: the sign, its 5th, its 9th
|
||||
24: "chaturvimshamsa_odd_from_leo_even_from_cancer_forward", # 1 deg 15 min parts
|
||||
30: "trimshamsa_bphs_ch6_unequal_parts_proportional_degree",
|
||||
60: "shashtiamsa_from_the_sign_forward", # 30 min parts (PVR); names / nature series are separate
|
||||
}
|
||||
|
||||
|
||||
def calc_varga(lon, div, mode='classical_local'):
|
||||
"""计算行星在指定分盘的位置(星座+精确度数+尊贵状态)"""
|
||||
si=_si(lon); d=lon-si*30
|
||||
@@ -199,6 +210,7 @@ def calc_varga(lon, div, mode='classical_local'):
|
||||
r={'sign':_sn(vsi),'sign_idx':vsi,'degree_in_sign':dp_display,
|
||||
'part_index':pi,'lord':SIGN_LORDS.get(_sn(vsi),'')}
|
||||
if div==9: r['pada']=pi+1
|
||||
if mode != 'vedastro' and div in VARGA_METHODS: r['method']=VARGA_METHODS[div]
|
||||
return r
|
||||
|
||||
|
||||
|
||||
+45
-1
@@ -55,6 +55,16 @@ FRIENDLY_PLANETS = {
|
||||
'Rahu': ['Venus', 'Saturn'],
|
||||
'Ketu': ['Sun', 'Mars'],
|
||||
}
|
||||
# #166 Q5 (BUG-1596): natural (naisargika) enemies, BPHS 3.55; used for the
|
||||
# strict Gajakesari "not in an enemy sign" check (PVR textbook definition).
|
||||
NATURAL_ENEMIES = {
|
||||
'Sun': ['Venus', 'Saturn'], 'Moon': [], 'Mars': ['Mercury'],
|
||||
'Mercury': ['Moon'], 'Jupiter': ['Mercury', 'Venus'],
|
||||
'Venus': ['Sun', 'Moon'], 'Saturn': ['Sun', 'Moon', 'Mars'],
|
||||
}
|
||||
# Combustion orbs (direct motion), same values as jyotish_engine.COMBUSTION_ORBS
|
||||
# direct column; used only when the chart row carries no `combust` flag.
|
||||
COMBUSTION_ORBS_DIRECT = {'Moon': 12.0, 'Mars': 17.0, 'Mercury': 14.0, 'Jupiter': 11.0, 'Venus': 10.0, 'Saturn': 15.0}
|
||||
PLANET_CN = {
|
||||
"Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun",
|
||||
"Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu",
|
||||
@@ -812,6 +822,8 @@ class YogaEngine:
|
||||
"bvr_id": rule.get("bvr_id"),
|
||||
"category": rule.get("category", ""),
|
||||
"rule_id": rule.get("id", ""),
|
||||
**({"triggers": m["triggers"]} if m.get("triggers") else {}),
|
||||
**({"canonical_mapping": rule["canonical_mapping"]} if rule.get("canonical_mapping") else {}),
|
||||
# English twins for the reader's English edition; the Chinese
|
||||
# fields above are unchanged.
|
||||
"combination_en": _en_text(combination),
|
||||
@@ -998,7 +1010,9 @@ class YogaEngine:
|
||||
new_results.append({
|
||||
"combination": merged_combo,
|
||||
"strength": sm.get("strength", match.get("strength")),
|
||||
"bindings": {**match.get("bindings", {}), **sm.get("bindings", {})}
|
||||
"bindings": {**match.get("bindings", {}), **sm.get("bindings", {})},
|
||||
**({"triggers": (match.get("triggers") or []) + (sm.get("triggers") or [])}
|
||||
if match.get("triggers") or sm.get("triggers") else {}),
|
||||
})
|
||||
results = new_results
|
||||
if not results:
|
||||
@@ -1522,6 +1536,13 @@ class YogaEngine:
|
||||
strength = cond.get("strength", rule.get("strength", "中"))
|
||||
if callable(strength):
|
||||
strength = strength(ctx, bindings)
|
||||
# #166 Q6 (BUG-1597): a rule may set `triggers` (list of short strings)
|
||||
# to name what actually fired; they are appended to the combination.
|
||||
triggers = exec_globals.get("triggers")
|
||||
if isinstance(triggers, list) and triggers:
|
||||
triggers = [str(item) for item in triggers]
|
||||
combo = BiText(f"{combo}:{';'.join(triggers)}", f"{_en_text(combo)}: {'; '.join(triggers)}")
|
||||
return [{"combination": combo, "strength": strength, "triggers": triggers}]
|
||||
return [{"combination": combo, "strength": strength}]
|
||||
return []
|
||||
|
||||
@@ -1661,6 +1682,28 @@ class YogaEngine:
|
||||
strength = house_strength(p)
|
||||
return strength is not None and strength <= 2
|
||||
|
||||
def combust(p):
|
||||
"""#166 Q5: the chart's `combust` flag, else the direct-motion orb from the Sun."""
|
||||
info = ctx.planets.get(p) or {}
|
||||
if isinstance(info.get('combust'), bool):
|
||||
return info['combust']
|
||||
if p not in COMBUSTION_ORBS_DIRECT or 'Sun' not in ctx.planets:
|
||||
return False
|
||||
def lon(q):
|
||||
sign, deg = ctx.sign_of(q), ctx.degree_of(q)
|
||||
if sign not in SIGNS or deg is None:
|
||||
return None
|
||||
return SIGNS.index(sign) * 30.0 + float(deg) % 30.0
|
||||
a, b = lon(p), lon('Sun')
|
||||
if a is None or b is None:
|
||||
return False
|
||||
return abs(((a - b + 180.0) % 360.0) - 180.0) <= COMBUSTION_ORBS_DIRECT[p]
|
||||
|
||||
def in_natural_enemy_sign(p):
|
||||
"""#166 Q5: the sign lord is a natural enemy of p (BPHS 3.55)."""
|
||||
sign = ctx.sign_of(p)
|
||||
return sign in SIGN_LORDS and SIGN_LORDS[sign] in NATURAL_ENEMIES.get(p, [])
|
||||
|
||||
def associated(a, b):
|
||||
return a in ctx.planets and b in ctx.planets and (same_house(a, b) or aspect(a, b) or aspect(b, a))
|
||||
|
||||
@@ -1791,6 +1834,7 @@ class YogaEngine:
|
||||
"rasi_aspected_by_planets": rasi_aspected_by_planets,
|
||||
"house_strength": house_strength, "strong": strong, "weak": weak,
|
||||
"associated": associated, "temporal_friend": temporal_friend, "natural_friend": natural_friend,
|
||||
"combust": combust, "in_natural_enemy_sign": in_natural_enemy_sign,
|
||||
"occupants": occupants, "only_benefics_in_house": only_benefics_in_house,
|
||||
"only_malefics_in_house": only_malefics_in_house,
|
||||
"house_has_benefic": house_has_benefic, "house_has_malefic": house_has_malefic,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""#166 batch 2: astrologer rulings 2026-11-04 (BUG-1590..1598)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for path in (ROOT / "scripts", ROOT):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import functional_benefics # noqa: E402
|
||||
import gulika # noqa: E402
|
||||
import jaimini # noqa: E402
|
||||
import narayana_dasha # noqa: E402
|
||||
import varga # noqa: E402
|
||||
from divisional_charts_extended import DivisionalChartsCalculator # noqa: E402
|
||||
from yoga_engine import YogaEngine # noqa: E402
|
||||
|
||||
|
||||
# Q4: ambiguous functional cells.
|
||||
@pytest.mark.parametrize(("lagna", "planet", "role"), [
|
||||
("Gemini", "Saturn", "neutral"), ("Virgo", "Saturn", "neutral"),
|
||||
("Taurus", "Mercury", "benefic"), ("Gemini", "Mercury", "benefic"),
|
||||
("Sagittarius", "Saturn", "malefic"), ("Cancer", "Jupiter", "benefic"),
|
||||
("Leo", "Jupiter", "benefic"), ("Capricorn", "Mercury", "benefic"),
|
||||
])
|
||||
def test_q4_ambiguous_cells(lagna: str, planet: str, role: str) -> None:
|
||||
result = functional_benefics.derive_functional_benefic_malefic(lagna)
|
||||
key = {"benefic": "functional_benefics", "malefic": "functional_malefics", "neutral": "functional_neutrals"}[role]
|
||||
assert planet in result[key]
|
||||
assert planet in result["ambiguous_cell_notes"]
|
||||
# The other lordship is kept, never dropped.
|
||||
assert len(result["owned_houses"][planet]) == 2
|
||||
|
||||
|
||||
def test_q4_gemini_mercury_is_not_a_yogakaraka() -> None:
|
||||
assert "Mercury" not in functional_benefics.derive_functional_benefic_malefic("Gemini")["yogakarakas"]
|
||||
|
||||
|
||||
# Q10: Chara, both Scorpio lords in Scorpio -> 12, even with Ketu exalted there.
|
||||
def test_q10_chara_both_lords_in_sign_is_twelve() -> None:
|
||||
lons = {"Sun": 10, "Moon": 40, "Mars": 215, "Mercury": 70, "Jupiter": 100,
|
||||
"Venus": 130, "Saturn": 160, "Rahu": 45, "Ketu": 225}
|
||||
assert jaimini._chara_dasha_duration_knrao(lons, 7) == 12
|
||||
|
||||
|
||||
# Q11: the Lagna (or any non-graha key) is not an occupant.
|
||||
def test_q11_lagna_is_not_an_occupant() -> None:
|
||||
lons = {"Sun": 100.0, "Moon": 130.0, "Mars": 160.0, "Mercury": 190.0, "Jupiter": 220.0,
|
||||
"Venus": 250.0, "Saturn": 280.0, "Rahu": 310.0, "Ketu": 130.0}
|
||||
with_lagna = {**lons, "Lagna": 5.0, "Ascendant": 6.0}
|
||||
assert narayana_dasha._narayana_dual_lord_strength("Mars", 0, lons) == narayana_dasha._narayana_dual_lord_strength("Mars", 0, with_lagna)
|
||||
|
||||
|
||||
# Q8: the 12-year cap is reported as the adjustment that took effect.
|
||||
def test_q8_mercury_in_virgo_cap_reports_no_plus_one() -> None:
|
||||
years, adjustment = narayana_dasha.calculate_narayana_period_years(5, "Mercury", 5, profile="jaimini_odd_footed_dignity_v2")
|
||||
assert years <= 12
|
||||
assert years == 12 and adjustment == 0 or adjustment == years - 12 + adjustment
|
||||
|
||||
|
||||
# Q15: both raw Saturn-segment points; the mapping is named and never called PVR.
|
||||
def test_q15_saturn_segment_points_and_method() -> None:
|
||||
from datetime import datetime
|
||||
packet = gulika.calculate_upagrahas(datetime(1990, 6, 15, 12, 0), lat=39.9042, lon=116.4074, tz=8.0, sun_longitude=60.0)
|
||||
points = packet["saturn_segment_points"]
|
||||
assert points["saturn_segment_start"]["longitude"] == packet["raw"]["Gulika"]["longitude"]
|
||||
assert points["saturn_segment_midpoint"]["longitude"] == packet["raw"]["Maandi"]["longitude"]
|
||||
assert packet["gulika_maandi_method"] == "pyjhora_compatible_gulika_start_maandi_midpoint"
|
||||
assert "pvr" not in packet["gulika_maandi_method"]
|
||||
assert gulika.GULIKA_MAANDI_METHODS["pvr_textbook_4_3"] == {"Gulika": "saturn_segment_midpoint", "Maandi": "saturn_segment_start"}
|
||||
|
||||
|
||||
# Q31: D3 / D24 / D60 methods are named and the two modules agree on every part.
|
||||
@pytest.mark.parametrize("division", [3, 24, 60])
|
||||
def test_q31_named_methods_and_module_agreement(division: int) -> None:
|
||||
calc = DivisionalChartsCalculator()
|
||||
fn = {3: calc._calculate_d3, 24: calc._calculate_d24, 60: calc._calculate_d60}[division]
|
||||
for tenth in range(3600):
|
||||
lon = tenth / 10.0 + 0.03
|
||||
point = varga.calc_varga(lon, division)
|
||||
assert point["method"] == varga.VARGA_METHODS[division]
|
||||
assert point["sign_idx"] == int(fn(int(lon // 30), lon % 30) // 30) % 12, lon
|
||||
|
||||
|
||||
def test_q31_spec_examples() -> None:
|
||||
assert varga.calc_varga(25.0, 3)["sign"] == "Sagittarius" # Aries 20-30 -> 9th
|
||||
assert varga.calc_varga(0.5, 24)["sign"] == "Leo" # odd from Leo
|
||||
assert varga.calc_varga(30.5, 24)["sign"] == "Cancer" # even from Cancer
|
||||
assert varga.calc_varga(0.6, 60)["sign"] == "Taurus" # second 30' part from Aries
|
||||
|
||||
|
||||
# Q5 / Q6: rules split, triggers named.
|
||||
def _detect(planets: dict, asc: str) -> list[dict]:
|
||||
return YogaEngine(str(ROOT / "references" / "yoga_rules.json")).detect(planets, asc)
|
||||
|
||||
|
||||
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
|
||||
|
||||
|
||||
def _chart(asc: str, positions: dict[str, tuple[str, float]]) -> dict:
|
||||
a = SIGNS.index(asc)
|
||||
return {p: {"sign": s, "degree": d, "house": (SIGNS.index(s) - a) % 12 + 1} for p, (s, d) in positions.items()}
|
||||
|
||||
|
||||
def test_q5_jupiter_in_enemy_sign_is_not_strict_gajakesari() -> None:
|
||||
planets = _chart("Aries", {"Moon": ("Pisces", 10), "Jupiter": ("Gemini", 10), "Venus": ("Gemini", 15),
|
||||
"Sun": ("Leo", 5), "Mars": ("Capricorn", 5), "Mercury": ("Virgo", 5), "Saturn": ("Libra", 5)})
|
||||
names = {row["name"] for row in _detect(planets, "Aries")}
|
||||
assert "Gajakesari Yoga" not in names
|
||||
assert "Moon-Jupiter Kendra Relation" in names
|
||||
|
||||
|
||||
def test_q5_strict_gajakesari_holds_with_benefic_and_clean_jupiter() -> None:
|
||||
planets = _chart("Aries", {"Moon": ("Pisces", 2), "Jupiter": ("Pisces", 10), "Venus": ("Pisces", 25),
|
||||
"Sun": ("Leo", 5), "Mars": ("Capricorn", 5), "Mercury": ("Virgo", 5), "Saturn": ("Libra", 5)})
|
||||
rows = {row["name"]: row for row in _detect(planets, "Aries")}
|
||||
assert "Gajakesari Yoga" in rows
|
||||
# Venus conjoined; Mercury in Virgo aspects Pisces (7th).
|
||||
assert rows["Gajakesari Yoga"]["triggers"] == ["benefic association: Venus, Mercury"]
|
||||
assert "Strong-Jupiter Gajakesari Variant" not in rows
|
||||
|
||||
|
||||
def test_q5_strong_jupiter_alone_is_the_variant_not_the_yoga() -> None:
|
||||
planets = _chart("Aries", {"Moon": ("Pisces", 10), "Jupiter": ("Sagittarius", 10), "Venus": ("Aquarius", 25),
|
||||
"Sun": ("Leo", 5), "Mars": ("Capricorn", 5), "Mercury": ("Virgo", 5), "Saturn": ("Libra", 5)})
|
||||
names = {row["name"] for row in _detect(planets, "Aries")}
|
||||
assert "Gajakesari Yoga" not in names
|
||||
assert "Strong-Jupiter Gajakesari Variant" in names
|
||||
|
||||
|
||||
def test_q6_matrunasa_198_rasi_drishti_alone_no_longer_fires_and_triggers_are_named() -> None:
|
||||
# Saturn in Gemini rasi-aspects Pisces (dual signs) but has no graha aspect on it from there? Saturn
|
||||
# aspects the 3rd, 7th and 10th from Gemini: Leo, Sagittarius, Pisces -> it does. Use Venus-free case:
|
||||
planets = _chart("Aries", {"Moon": ("Pisces", 10), "Saturn": ("Virgo", 10), "Jupiter": ("Cancer", 10),
|
||||
"Sun": ("Taurus", 5), "Mars": ("Taurus", 6), "Mercury": ("Taurus", 7), "Venus": ("Gemini", 5),
|
||||
"Rahu": ("Leo", 5), "Ketu": ("Aquarius", 5)})
|
||||
rows = {row["name"]: row for row in _detect(planets, "Aries")}
|
||||
hit = rows.get("Matrunasa Yoga")
|
||||
assert hit is not None and hit["triggers"] == ["aspects Moon: Saturn"]
|
||||
assert hit["effects"] and "健康" not in hit["effects"][0].replace("不作母亲健康", "")
|
||||
@@ -48,6 +48,11 @@ def test_default_daytime_matches_rectification_apparent_sunrise():
|
||||
noon = datetime.fromisoformat(f"{date}T12:00:00")
|
||||
current = determine_daytime(noon, lat=39.9042, lon=116.4074, tz=8)
|
||||
previous = _legacy_apparent_daytime(noon, lat=39.9042, lon=116.4074, tz=8)
|
||||
# 原值: current == previous(整个字典相等)
|
||||
# 新值: 去掉新增的 sunrise_convention 后相等,并核对它写明视日出
|
||||
# 原因: BUG-1595,#166 第 12 条只记录日出口径、不改值(产品 2026-11-04)
|
||||
convention = current.pop("sunrise_convention")
|
||||
assert convention["name"] == "apparent_upper_limb_with_refraction" and convention["refraction"] is True
|
||||
assert current == previous
|
||||
assert "next_sunrise_jd_ut" not in current
|
||||
|
||||
|
||||
@@ -109,6 +109,9 @@ def test_birth_data_request_returns_precise_shadbala(api_base) -> None:
|
||||
"drik_special_aspects": "bphs_26_9_12_piecewise",
|
||||
"mercury_nature": "bphs_3_11_any_malefic_same_sign_no_nodes",
|
||||
"kendradi": "whole_sign_from_lagna",
|
||||
# 原值: 无;新值: 写明日出与时主口径;原因: BUG-1595,#166 第 12、13 条(只记录,不改值)
|
||||
"sunrise": "hindu_disc_centre_no_refraction",
|
||||
"hora": "fixed_60_minutes_from_sunrise_approximation",
|
||||
}
|
||||
assert sorted(result["ranking"]) == sorted(SEVEN)
|
||||
for planet in SEVEN:
|
||||
|
||||
Reference in New Issue
Block a user