From db55aef19137ab82c8f883da465aa639e9695d6a Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 2 Oct 2026 14:05:24 +0800 Subject: [PATCH] fix(arudha): AL and UL have one source, jaimini.calc_arudha_padas (BUG-1190) TASK-upstream-sync4-20261002 T6, decision 4. Visible parallel calculators were found in three places and all now read jaimini: - yogas_doshas.calc_arudha_lagna / calc_upapada_lagna (house number taken as an Aries-based sign index, no 1/7 exception) are deleted; the module's arudha_lagna / upapada_lagna fields come from jaimini. - special_lagnas.calculate_arudha_lagna / calculate_upapada_lagna ("count from the landing" 1/7 exception, which disagrees with jaimini when the lord is 4th/10th from the source) are deleted; the full reading's Arudha_Lagna / Upapada_Lagna rows are built from jaimini padas. - the transit multi-reference table took "ascendant degree + 12th-lord degree" as AL; it now uses jaimini's A1. jaimini's own exception counting is unchanged (astrologer question 5); the upstream "count from the landing" variant is not ported. A10 in special_lagnas keeps its own exception rule (outside decision 4; listed in progress). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- scripts/jyotish_engine.py | 58 +++++++----- scripts/special_lagnas.py | 113 +---------------------- scripts/yogas_doshas.py | 143 ++++++++--------------------- tests/test_arudha_single_source.py | 41 +++++++++ 4 files changed, 121 insertions(+), 234 deletions(-) create mode 100644 tests/test_arudha_single_source.py diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 8a350bd9..a623fba2 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -14628,15 +14628,12 @@ def _calc_transit_multi_reference(planets, asc_idx, asc_deg, planet_lons, transi moon_lon = planet_lons.get('Moon', 0) chandra_idx = int(moon_lon / 30) % 12 - # Arudha Lagna(从special_lagnas模块逻辑简化:AL = (Ascendant度数+12宫主度数)%360 对应的星座) - twelfth_sign_idx = (asc_idx + 11) % 12 - twelfth_lord = SIGN_LORDS.get(SIGNS[twelfth_sign_idx], '') - twelfth_lord_lon = planet_lons.get(twelfth_lord, 0) - al_raw = (asc_deg + twelfth_lord_lon) % 360 - # AL 特殊规则:如果结果落在原始宫或第7宫,取对宫 - al_idx = int(al_raw / 30) % 12 - if al_idx == asc_idx or al_idx == (asc_idx + 6) % 12: - al_idx = (al_idx + 7) % 12 # 取第8个 = 对宫再移一位 + # Arudha Lagna:只有一个来源 jaimini.calc_arudha_padas(BUG-1190)。旧写法把 + # 「上升度数 + 12 宫主度数」当 AL,与任何 Arudha 定义都不符。 + sys.path.insert(0, SCRIPT_DIR) + from jaimini import calc_arudha_padas + _al = calc_arudha_padas(asc_idx, planet_lons).get('arudha_lagna') or {} + al_idx = _al.get('sign_idx', asc_idx) # Navamsa Lagna d9_asc_idx = _navamsa_idx(asc_deg) @@ -15462,14 +15459,36 @@ def cmd_full_reading(args): birth_time=birth_dt, sunrise_time=sunrise_dt ) - # 补充 Arudha Lagna、A10/Karma Pada 和 Upapada Lagna + # 补充 Arudha Lagna、A10/Karma Pada 和 Upapada Lagna。 + # AL / UL 只有一个来源 jaimini.calc_arudha_padas(BUG-1190,TASK-upstream-sync4 T6): + # special_lagnas 的同名方法 1/7 宫例外「从落点数」,与 jaimini「从源宫数」不同。 try: - first_house_sign_idx = asc_idx - first_lord = SIGN_LORDS.get(SIGNS[first_house_sign_idx], '') - first_lord_deg = planet_lons.get(first_lord, 0) - sl_result['Arudha_Lagna'] = sl_calc.calculate_arudha_lagna(asc_deg, first_lord_deg) + from jaimini import calc_arudha_padas as _single_source_padas + _padas = _single_source_padas(asc_idx, planet_lons).get('padas') or {} except Exception as e: - sl_result['Arudha_Lagna'] = {"error": str(e)} + _padas = {} + report['errors'].append(f"special-lagnas arudha: {e}") + + def _jaimini_pada_row(key, meaning, formula): + pada = _padas.get(key) + if not pada: + return {"error": f"jaimini.calc_arudha_padas 未给出 {key}"} + lord_deg = planet_lons.get(pada['source_house_lord'], 0) + degree = (pada['sign_idx'] * 30 + (lord_deg % 30)) % 360 + return { + "degree": round(degree, 4), + "sign": pada['sign'], + "sign_degree": round(degree % 30, 4), + "house": ((pada['sign_idx'] - asc_idx) % 12) + 1, + "distance_from_source": pada['distance'], + "exception_applied": pada['exception_triggered'], + "source": "jaimini.calc_arudha_padas", + "formula": formula, + "meaning": meaning, + } + + sl_result['Arudha_Lagna'] = _jaimini_pada_row( + 'A1', "公众形象和社会地位,他人如何看待你", "A1:从1宫主星数相同宫位数(1/7宫例外,取 jaimini 口径)") try: tenth_house_sign_idx = (asc_idx + 9) % 12 tenth_lord = SIGN_LORDS.get(SIGNS[tenth_house_sign_idx], '') @@ -15477,13 +15496,8 @@ def cmd_full_reading(args): sl_result['A10_Karma_Pada'] = sl_calc.calculate_a10(asc_deg, tenth_lord_deg) except Exception as e: sl_result['A10_Karma_Pada'] = {"error": str(e)} - try: - twelfth_house_sign_idx = (asc_idx + 11) % 12 - twelfth_lord = SIGN_LORDS.get(SIGNS[twelfth_house_sign_idx], '') - twelfth_lord_deg = planet_lons.get(twelfth_lord, 0) - sl_result['Upapada_Lagna'] = sl_calc.calculate_upapada_lagna(asc_deg, twelfth_lord_deg) - except Exception as e: - sl_result['Upapada_Lagna'] = {"error": str(e)} + sl_result['Upapada_Lagna'] = _jaimini_pada_row( + 'UL', "配偶的公众形象和婚姻的社会表现", "UL:从12宫主星数相同宫位数(12/6宫例外,取 jaimini 口径)") report['modules']['special_lagnas'] = sl_result except Exception as e: report['errors'].append(f"special-lagnas: {e}") diff --git a/scripts/special_lagnas.py b/scripts/special_lagnas.py index e4f28361..97eb3b7e 100644 --- a/scripts/special_lagnas.py +++ b/scripts/special_lagnas.py @@ -51,12 +51,10 @@ class SpecialLagnasCalculator: # 3. Ghati Lagna result["Ghati_Lagna"] = self.calculate_ghati_lagna(asc_degree, birth_time, sunrise_time) - # 4. Arudha Lagna (需要1宫主星位置) - # 注意:这里需要额外的行星位置信息,暂时返回占位符 - result["Arudha_Lagna"] = {"note": "Requires 1st house lord position"} - - # 5. Upapada Lagna (需要12宫主星位置) - result["Upapada_Lagna"] = {"note": "Requires 12th house lord position"} + # 4/5. Arudha Lagna、Upapada Lagna 只有一个来源 jaimini.calc_arudha_padas(BUG-1190), + # 由 full reading 用它覆盖这两个占位。 + result["Arudha_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"} + result["Upapada_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"} return result @@ -172,52 +170,6 @@ class SpecialLagnasCalculator: "meaning": "基于Ghati单位的精确上升点,用于精确预测" } - def calculate_arudha_lagna(self, asc_degree: float, first_lord_degree: float) -> Dict: - """ - 计算Arudha Lagna(映像上升/AL) - - 公式: - 1. 计算1宫主星距离上升点的宫位数(A) - 2. 从1宫主星再数A个宫位,得到AL - 3. 特殊规则:如果AL落在1宫或7宫,则从该位置再数10个宫位 - - 含义: - - 公众形象和社会地位 - - 他人如何看待你 - - 外在表现与内在本质的差异 - """ - # 计算上升点和1宫主星的星座 - asc_sign = int(asc_degree // 30) - lord_sign = int(first_lord_degree // 30) - - # 计算1宫主星距离上升点的宫位数 - distance = (lord_sign - asc_sign) % 12 - if distance == 0: - distance = 12 - - # 从1宫主星再数相同的宫位数 - # 注意:distance 为 0-indexed 差值,标准 Jaimini 用 1-based count = distance + 1(含两端) - # 从 lord 数 count 宫(含 lord)= lord_sign + count - 1 = lord_sign + distance - count = distance + 1 - al_sign = (lord_sign + count - 1) % 12 - - # 应用1/7宫例外规则 - if al_sign == asc_sign or al_sign == (asc_sign + 6) % 12: - al_sign = (al_sign + 9) % 12 # 再数10个宫位(索引+9) - - # 计算AL的度数(使用1宫主星的度数) - al_degree = (al_sign * 30 + (first_lord_degree % 30)) % 360 - - return { - "degree": round(al_degree, 4), - "sign": self.SIGNS[al_sign], - "sign_degree": round(al_degree % 30, 4), - "house": ((al_sign - int(asc_degree // 30)) % 12) + 1, - "distance_from_asc": distance, - "formula": "从1宫主星数相同宫位数(1/7宫例外)", - "meaning": "公众形象和社会地位,他人如何看待你" - } - def calculate_arudha_pada(self, asc_degree: float, source_house: int, source_lord_degree: float, label: str = "Arudha_Pada") -> Dict: """ @@ -272,53 +224,6 @@ class SpecialLagnasCalculator: result["meaning"] = "A10/Karma Pada/Rajya Pada:事业名声、职业可见度、社会身份与公众层面的职业结果" return result - def calculate_upapada_lagna(self, asc_degree: float, twelfth_lord_degree: float) -> Dict: - """ - 计算Upapada Lagna(配偶映像上升/UL) - - 公式: - 1. 计算12宫主星距离12宫的宫位数(A) - 2. 从12宫主星再数A个宫位,得到UL - 3. 特殊规则:如果UL落在12宫或6宫,则从该位置再数10个宫位 - - 含义: - - 配偶的公众形象 - - 婚姻在社会中的表现 - - 配偶的社会地位和声誉 - """ - # 计算12宫的起始度数 - asc_sign = int(asc_degree // 30) - twelfth_house_sign = (asc_sign + 11) % 12 - - # 计算12宫主星的星座 - lord_sign = int(twelfth_lord_degree // 30) - - # 计算12宫主星距离12宫的宫位数 - distance = (lord_sign - twelfth_house_sign) % 12 - if distance == 0: - distance = 12 - - # 从12宫主星再数相同的宫位数 - count = distance + 1 - ul_sign = (lord_sign + count - 1) % 12 - - # 应用12/6宫例外规则 - if ul_sign == twelfth_house_sign or ul_sign == (twelfth_house_sign + 6) % 12: - ul_sign = (ul_sign + 9) % 12 # 再数10个宫位 - - # 计算UL的度数 - ul_degree = (ul_sign * 30 + (twelfth_lord_degree % 30)) % 360 - - return { - "degree": round(ul_degree, 4), - "sign": self.SIGNS[ul_sign], - "sign_degree": round(ul_degree % 30, 4), - "house": ((ul_sign - int(asc_degree // 30)) % 12) + 1, - "distance_from_12th": distance, - "formula": "从12宫主星数相同宫位数(12/6宫例外)", - "meaning": "配偶的公众形象和婚姻的社会表现" - } - def format_output(self, lagnas: Dict) -> str: """格式化输出所有特殊上升点""" lines = ["=" * 60, "Special Lagnas Calculation", "=" * 60, ""] @@ -348,9 +253,7 @@ def main(): parser.add_argument("--moon", type=float, required=True, help="Moon degree (0-360)") parser.add_argument("--birth-time", type=str, help="Birth time (YYYY-MM-DD HH:MM:SS)") parser.add_argument("--sunrise-time", type=str, help="Sunrise time (YYYY-MM-DD HH:MM:SS)") - parser.add_argument("--first-lord", type=float, help="1st house lord degree (for AL)") parser.add_argument("--tenth-lord", type=float, help="10th house lord degree (for A10/Karma Pada)") - parser.add_argument("--twelfth-lord", type=float, help="12th house lord degree (for UL)") args = parser.parse_args() @@ -363,18 +266,10 @@ def main(): # 计算所有特殊上升点 lagnas = calc.calculate_all_lagnas(args.asc, args.sun, args.moon, birth_time, sunrise_time) - # 如果提供了1宫主星位置,计算AL - if args.first_lord is not None: - lagnas["Arudha_Lagna"] = calc.calculate_arudha_lagna(args.asc, args.first_lord) - # 如果提供了10宫主星位置,计算A10/Karma Pada if args.tenth_lord is not None: lagnas["A10_Karma_Pada"] = calc.calculate_a10(args.asc, args.tenth_lord) - # 如果提供了12宫主星位置,计算UL - if args.twelfth_lord is not None: - lagnas["Upapada_Lagna"] = calc.calculate_upapada_lagna(args.asc, args.twelfth_lord) - # 输出结果 print(calc.format_output(lagnas)) diff --git a/scripts/yogas_doshas.py b/scripts/yogas_doshas.py index d3c2427f..59dcf3a8 100644 --- a/scripts/yogas_doshas.py +++ b/scripts/yogas_doshas.py @@ -12,8 +12,8 @@ Yogas / Doshas / Special Lagnas 计算模块 v6.0.14 - Kaal Sarp Dosha: 时间蛇煞(所有行星在Rahu-Ketu轴线一侧) - Pitra Dosha: 父辈煞(Sun与Rahu合相/受克) - Sade Sati: 土星七年(土星过Moon前后各1星座) - - Arudha Lagna (AL): 镜像上升(从Lagna看Lagna主的落点) - - Upapada Lagna (UL): 配偶镜像上升(从12宫看12宫主的落点) + - Arudha Lagna (AL): 镜像上升,取自 jaimini.calc_arudha_padas(单一来源) + - Upapada Lagna (UL): 第12宫 Arudha,取自 jaimini.calc_arudha_padas(单一来源) """ from typing import Dict, List, Tuple, Optional @@ -744,110 +744,48 @@ def calc_sade_sati(moon_sign: str, current_year: int = 2026) -> Dict: return results -def calc_arudha_lagna(asc_sign: str, asc_lord: str, planets_data: Dict) -> Dict: +def _arudha_from_jaimini(asc_sign: str, planets_data: Dict) -> Dict: + """AL / UL 只有一个来源:jaimini.calc_arudha_padas(BUG-1190,TASK-upstream-sync4-20261002 T6)。 + + 旧的 calc_arudha_lagna / calc_upapada_lagna 把「第几宫」当成白羊起算的星座序号, + 且没有 1/7 宫例外,同一张盘与 jaimini 结果不同,已删除。1/7 宫例外的数法沿用 + jaimini 现行口径(占星师清单第 5 题未决,本单不改)。 """ - 计算 Arudha Lagna (AL) —— 镜像上升点 - - 规则:从Lagna(上升)看Lagna Lord(上升主星)的落点, - 再从该落点的"镜像位置"得到AL - 公式:AL = 从Asc看Lord的宫位数,再同样宫位数从Lord往外数 - - 例子:Asc=Leo, Lord=Sun在10宫 → 从Asc(1)看Sun(10) = 9宫远 - 从Sun(10)再数9宫 = 10+9-1=18 → 18-12=6宫 → AL在6宫 - - 意义:AL代表"他人如何看待命主",镜像自我认知 - """ - results = {'arudha_lagna': None, 'al_house': None, 'interpretation': ''} - + empty = { + 'arudha_lagna': {'arudha_lagna': None, 'al_house': None, 'source': 'jaimini.calc_arudha_padas'}, + 'upapada_lagna': {'upapada': None, 'ul_house': None, 'source': 'jaimini.calc_arudha_padas'}, + } if not asc_sign or asc_sign not in SIGNS: - results['summary'] = 'Asc星座未知,无法计算Arudha Lagna' - return results - + return empty + try: + from jaimini import calc_arudha_padas + except ImportError: # package import (scripts.yogas_doshas) + from scripts.jaimini import calc_arudha_padas + planet_lons = { + name: data.get('degree_raw', data.get('degree')) + for name, data in (planets_data or {}).items() + if isinstance(data, dict) and isinstance(data.get('degree_raw', data.get('degree')), (int, float)) + } asc_idx = SIGNS.index(asc_sign) + padas = calc_arudha_padas(asc_idx, planet_lons) - # 找Asc Lord的宫位 - lord_data = planets_data.get(asc_lord, {}) - if not isinstance(lord_data, dict) or 'house' not in lord_data: - results['summary'] = f'Asc Lord({asc_lord})宫位未知,无法计算AL' - return results + def _house(pada): + return ((pada['sign_idx'] - asc_idx) % 12) + 1 - lord_house = lord_data['house'] # 1-12 - lord_idx = (lord_house - 1) # 转为0-11 - - # 计算镜像:从Asc到Lord的宫位数 = lord_idx - asc_idx (mod 12) - distance = (lord_idx - asc_idx) % 12 # 0-11 - if distance == 0: - distance = 12 # 同宫=12宫远 - - # AL = 从Lord再数同样距离 - al_idx = (lord_idx + distance) % 12 - al_house = al_idx + 1 # 转回1-12 - - al_sign = SIGNS[al_idx] - - results['arudha_lagna'] = al_sign - results['al_house'] = al_house - results['distance'] = distance - results['lord_house'] = lord_house - results['interpretation'] = ( - f'Arudha Lagna (AL) = {al_sign}(第{al_house}宫)——' - f'他人眼中的你;Asc={asc_sign}, Lord={asc_lord}在{lord_house}宫,距离={distance}' - ) - results['summary'] = results['interpretation'] - return results - - -def calc_upapada_lagna(asc_sign: str, house12_lord: str, planets_data: Dict) -> Dict: - """ - 计算 Upapada Lagna (UL) —— 配偶镜像上升点 - - 规则:从12宫(损失/海外/床事宫)看12宫主星的落点, - 镜像计算方式同Arudha Lagna - 公式:UL = 从12宫看12宫主星的宫位数,再从主星落点往外数同样宫位数 - - 意义:UL代表"配偶/婚姻伙伴",是婚姻质量的关键指标 - UL受克/受损 → 婚姻困难 - UL有吉星 → 婚姻幸福 - """ - results = {'upapada': None, 'ul_house': None, 'interpretation': ''} - - if not asc_sign or asc_sign not in SIGNS: - results['summary'] = 'Asc星座未知,无法计算Upapada' - return results - - asc_idx = SIGNS.index(asc_sign) - h12_idx = (asc_idx + 11) % 12 # 12宫 index (0-11) - - # 找12宫主星的宫位 - lord_data = planets_data.get(house12_lord, {}) - if not isinstance(lord_data, dict) or 'house' not in lord_data: - results['summary'] = f'12宫主星({house12_lord})宫位未知,无法计算UL' - return results - - lord_house = lord_data['house'] # 1-12 - lord_idx = (lord_house - 1) # 0-11 - - # 计算镜像:从12宫到12宫主星的宫位数 - distance = (lord_idx - h12_idx) % 12 - if distance == 0: - distance = 12 - - # UL = 从Lord再数同样距离 - ul_idx = (lord_idx + distance) % 12 - ul_house = ul_idx + 1 - - ul_sign = SIGNS[ul_idx] - - results['upapada'] = ul_sign - results['ul_house'] = ul_house - results['distance'] = distance - results['lord_house'] = lord_house - results['interpretation'] = ( - f'Upapada Lagna (UL) = {ul_sign}(第{ul_house}宫)——' - f'配偶/婚姻质量指标;12宫主星={house12_lord}在{lord_house}宫,距离={distance}' - ) - results['summary'] = results['interpretation'] - return results + al = padas.get('arudha_lagna') + ul = padas.get('upapada') + result = empty + if al: + result['arudha_lagna'] = { + 'arudha_lagna': al['sign'], 'al_house': _house(al), 'source': 'jaimini.calc_arudha_padas', + 'summary': f"Arudha Lagna (AL) = {al['sign']}(第{_house(al)}宫),取自 jaimini.calc_arudha_padas", + } + if ul: + result['upapada_lagna'] = { + 'upapada': ul['sign'], 'ul_house': _house(ul), 'source': 'jaimini.calc_arudha_padas', + 'summary': f"Upapada Lagna (UL) = {ul['sign']}(第{_house(ul)}宫),取自 jaimini.calc_arudha_padas", + } + return result def calc_all_yogas_doshas(planets_data: Dict, houses: Dict, @@ -886,8 +824,7 @@ def calc_all_yogas_doshas(planets_data: Dict, houses: Dict, results['sade_sati'] = calc_sade_sati(moon_sign) # Special Lagnas - results['arudha_lagna'] = calc_arudha_lagna(asc_sign, asc_lord, planets_data) - results['upapada_lagna'] = calc_upapada_lagna(asc_sign, house12_lord, planets_data) + results.update(_arudha_from_jaimini(asc_sign, planets_data)) # 总结 total_yogas_count = (len(results['raj_yogas']['yogas']) + diff --git a/tests/test_arudha_single_source.py b/tests/test_arudha_single_source.py new file mode 100644 index 00000000..6c178463 --- /dev/null +++ b/tests/test_arudha_single_source.py @@ -0,0 +1,41 @@ +"""BUG-1190 (TASK-upstream-sync4-20261002 T6): AL / UL have one source. + +Every place a full reading shows Arudha Lagna or Upapada Lagna (yogas_doshas, +special_lagnas, the transit multi-reference table) must equal +``jaimini.calc_arudha_padas``. Fictitious charts only. 00:30 puts the lagna +lord 4th from the lagna and 02:30 puts the 12th lord 4th from the 12th, the two +cases where the removed "count from the landing" 1/7 exception disagreed. +""" + +from __future__ import annotations + +import pytest + +from scripts.jyotish_engine import cmd_full_reading +from scripts.professional_report_reference import _export_args +import scripts.special_lagnas as special_lagnas +import scripts.yogas_doshas as yogas_doshas + +BASE = dict(year=1990, month=6, day=15, second=0, lat=39.9042, lon=116.4074, tz=8, + ayanamsa="lahiri", node_mode="mean") + + +@pytest.mark.parametrize("hour", [10, 0, 2]) +def test_full_reading_al_ul_equal_jaimini(hour: int) -> None: + modules = cmd_full_reading(_export_args({**BASE, "hour": hour, "minute": 30}))["modules"] + padas = modules["jaimini"]["arudha_padas"] + al, ul = padas["arudha_lagna"]["sign"], padas["upapada"]["sign"] + + assert modules["yogas_doshas"]["arudha_lagna"]["arudha_lagna"] == al + assert modules["yogas_doshas"]["upapada_lagna"]["upapada"] == ul + assert modules["special_lagnas"]["Arudha_Lagna"]["sign"] == al + assert modules["special_lagnas"]["Upapada_Lagna"]["sign"] == ul + assert modules["special_lagnas"]["Arudha_Lagna"]["source"] == "jaimini.calc_arudha_padas" + assert modules["transit_multi_reference"]["references"]["Arudha_Lagna"]["sign"] == al + + +def test_parallel_arudha_calculators_are_gone() -> None: + assert not hasattr(yogas_doshas, "calc_arudha_lagna") + assert not hasattr(yogas_doshas, "calc_upapada_lagna") + assert not hasattr(special_lagnas.SpecialLagnasCalculator, "calculate_arudha_lagna") + assert not hasattr(special_lagnas.SpecialLagnasCalculator, "calculate_upapada_lagna")