From 5ac88e58f50b451f860482ea6de107c846780ae5 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 16 Oct 2026 00:52:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A9=E5=A4=A7=E8=BF=90=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=B5=B0=E5=AE=8C=E6=95=B4=E8=A7=A3=E8=AF=BB=E7=9A=84=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E5=A5=97=E7=AE=97=E6=B3=95=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E7=BC=96=E5=87=BA=E6=9D=A5=E7=9A=84=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ashtottari 在适用时把出生前已经走过的部分算进时间线。 Co-Authored-By: Grok (agent loop) --- CHANGELOG.md | 5 + docs/BUG_HISTORY.md | 44 +- docs/tasks/PROGRESS-dasha-unify-20261013.md | 77 ++++ scripts/ashtottari_dasha.py | 98 +++-- scripts/chaturshitisama_dasha.py | 13 +- scripts/conditional_dashas.py | 193 --------- scripts/dasha_calculator.py | 16 +- scripts/dasha_calculator_enhanced.py | 16 +- scripts/dasha_common.py | 46 +++ scripts/dasha_registry.py | 342 ++++++++++++++++ scripts/dwisaptatisama_dasha.py | 13 +- scripts/event_prediction_model.py | 11 +- scripts/extended_dashas.py | 408 ------------------- scripts/jyotish_api_server.py | 79 +--- scripts/jyotish_engine.py | 10 +- scripts/kp_system.py | 6 +- scripts/nakshatra_dasha.py | 6 +- scripts/panchottari_dasha.py | 12 +- scripts/prashna.py | 6 +- scripts/satabdika_dasha.py | 10 +- scripts/shattrimshatsama_dasha.py | 13 +- scripts/shodashottari_dasha.py | 12 +- scripts/tajika.py | 15 +- scripts/validate.py | 8 +- scripts/vimshottari_subperiod_timeline.py | 8 +- tests/run_all.py | 114 ++++-- tests/test_api_server_growth_contract.py | 5 +- tests/test_api_server_security.py | 9 +- tests/test_dasha_api_engine_parity.py | 423 ++++++++++++++++++++ tests/test_dasha_year_length_alignment.py | 39 +- 30 files changed, 1235 insertions(+), 822 deletions(-) create mode 100644 docs/tasks/PROGRESS-dasha-unify-20261013.md delete mode 100644 scripts/conditional_dashas.py create mode 100644 scripts/dasha_common.py create mode 100644 scripts/dasha_registry.py delete mode 100644 scripts/extended_dashas.py create mode 100644 tests/test_dasha_api_engine_parity.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2e201c..62d6dc4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-10-16 — 大运接口不再编造周期,Ashtottari 计入出生余额 + +- 选择一套还没接上引擎的大运时,接口会明确回复未实现,不再返回编出来的周期。已经接上的大运与完整解读使用同一套算法。Vimshottari 的算法和返回字段不变。 +- 完整解读里的 Ashtottari 大运日期改为从出生时已经走过的那一段算起,后面的大运、小运和「当前大运」一起平移。Skill 版本不变(BUG-1341、BUG-1344)。 + ## 2026-10-15 — 免费层限流状态与真实行为对齐 - 诊断和审计里的「免费层是否在限流」改为按真实限流计算。旧的三个排队开关名字还在,但不再控制限流,诊断里会标明它们已废弃。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 35bb1877..4d18c521 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17314,14 +17314,14 @@ ## BUG-1284 | 旧 Shoola 从月亮星座起并且逐年缩短,注册表仍留着 -- 状态:mitigated -- 首次发现 / 最近更新:2026-10-09 / 2026-10-09 +- 状态:resolved(issue #69,分支 `codex/dasha-unify-20261013`:占位注册表已删除,`/api/dasha` 的 shoola 改走 `rashi_dashas_native` 的 `shoola_forward_md_direct_ad_v1`) +- 首次发现 / 最近更新:2026-10-09 / 2026-10-16 - 来源:`TASK-rashi-dashas-native-20261008`。 - 影响面:`scripts/extended_dashas.py` 的 `calc_shoola_dasha` 与 `'shoola'` 注册项。接口层仍能按这个键调用。完整数据版报告不走这条。 - 现象:旧函数从月亮星座起,年数从 9 减到 1,一年按 365.25636 日。所选 Rath 口径是上升或第 7 里较强的星座、一律顺行、每段 9 个日历年。 - 触发条件:调用扩展大运注册表里的 `shoola`,或报告误用该函数。 - 根因:早期简化实现留在注册表。本单不得改 `scripts/jyotish_api_server.py` 的方法体,所以注册项还在。 -- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。注册表和接口方法体不动。 +- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。2026-10-09 当时不改接口方法体。2026-10-16 issue #69 删除 `extended_dashas.py`,接口上的 shoola 改为直接复用同一段。 - 验证:三个新模块的语法树不导入 `extended_dashas`。定向 pytest 退出码 0。 - 防复发:上述导入断言。报告章不得再调用 `calc_shoola_dasha`。 - 相关记录:BUG-1283。 @@ -35412,14 +35412,14 @@ ## BUG-1284 | 旧 Shoola 从月亮星座起并且逐年缩短,注册表仍留着 -- 状态:mitigated -- 首次发现 / 最近更新:2026-10-09 / 2026-10-09 +- 状态:resolved(issue #69,分支 `codex/dasha-unify-20261013`:占位注册表已删除,`/api/dasha` 的 shoola 改走 `rashi_dashas_native` 的 `shoola_forward_md_direct_ad_v1`) +- 首次发现 / 最近更新:2026-10-09 / 2026-10-16 - 来源:`TASK-rashi-dashas-native-20261008`。 - 影响面:`scripts/extended_dashas.py` 的 `calc_shoola_dasha` 与 `'shoola'` 注册项。接口层仍能按这个键调用。完整数据版报告不走这条。 - 现象:旧函数从月亮星座起,年数从 9 减到 1,一年按 365.25636 日。所选 Rath 口径是上升或第 7 里较强的星座、一律顺行、每段 9 个日历年。 - 触发条件:调用扩展大运注册表里的 `shoola`,或报告误用该函数。 - 根因:早期简化实现留在注册表。本单不得改 `scripts/jyotish_api_server.py` 的方法体,所以注册项还在。 -- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。注册表和接口方法体不动。 +- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。2026-10-09 当时不改接口方法体。2026-10-16 issue #69 删除 `extended_dashas.py`,接口上的 shoola 改为直接复用同一段。 - 验证:三个新模块的语法树不导入 `extended_dashas`。定向 pytest 退出码 0。 - 防复发:上述导入断言。报告章不得再调用 `calc_shoola_dasha`。 - 相关记录:BUG-1283。 @@ -36319,4 +36319,36 @@ - 复发自:无 - 修复版本:分支 `codex/python-hygiene-20261013`。本号是任务书预留的空号;本分支最大已用编号是 BUG-1357,1340 未被占用,因此不顺延。 +## BUG-1341 | 大运接口用占位周期冒充计算结果 + +- 状态:resolved +- 首次发现:2026-10-13 +- 最近更新:2026-10-16 +- 影响面:`POST /api/dasha` 里除 Vimshottari 以外的体系。排盘接口里的大运名单和数量不变。 +- 用户现象:选 Yogini、Panchottari 等大运时,接口返回的主星和日期不随星盘变化,年数是按总数平均编的,却标成已经算过。 +- 触发条件:请求一个注册表里有、但不是 Vimshottari 的大运键。 +- 根因:`extended_dashas.py` 和 `conditional_dashas.py` 是早期为了凑体系数量写的占位。完整解读后来改走独立模块,接口还留在占位函数上。 +- 修复:接口改为调用完整解读使用的同一批模块。没有真实现的体系返回「未实现」。占位文件删除。Narayana 仍标明旧算法,不换成另一套。shoola 复用报告里已有的那一段。 +- 验证:`tests/test_dasha_api_engine_parity.py` 9 通过。任务书列出的 pytest 共 247 通过、0 失败。`tests/run_all.py` 102/102。虚构盘排盘响应除缓存时间戳外逐字段相同,`dasha_count` 仍为 35。 +- 防复发:上述对照测试。响应里不再出现 `precision` 为 `generic` 或 `calculator`。 +- 相关记录:BUG-1262、BUG-1284、BUG-1215、BUG-1218、BUG-1344 +- 复发自:无 +- 修复版本:分支 `codex/dasha-unify-20261013`。本号是任务书预留号;本分支最大已用编号是 BUG-1357,1341 与 1344 都未被占用,因此不顺延。Narayana 旧算法与 Rath 版仍未裁定。 + +## BUG-1344 | Ashtottari 大运从出生日满额起算,整条时间线偏了 + +- 状态:resolved +- 首次发现:2026-10-14 +- 最近更新:2026-10-16 +- 影响面:完整解读和接口里的 Ashtottari 时间线。适用规则(哪些盘适用、两种规则族怎么判定)没改。另一套从 Mars 起算的星宿大运没有用户界面在读,这次不动。 +- 用户现象:适用 Ashtottari 时,第一段大运从出生时刻按整段年数起算,后面每一段都一起偏出一段出生前已经走过的时间。 +- 触发条件:盘面已被判定适用,而且月亮落在起运星所辖的星宿组里。月亮经度没传、或不在该组内时,时间线仍从出生起算。 +- 根因:余额只在一种规则族里计算,而且算完没有用来移动第一段的起点。完整解读调用时也没有把月亮经度传进去。 +- 修复:两种规则族在适用时都按月亮在该组里已走过的比例,把第一段起点改到出生时刻减去已走过的部分。后续大运、小运和当前大运一起平移。完整解读只给这次调用补月亮经度,不改与 Yogini、Kalachakra 共用的那份出生信息。 +- 验证:虚构盘与两张公开盘,起止日与 PyJHora 4.8.7 相差不超过 1 天,起运星一致;出生时刻落在第一段内;当前大运与按日期查表一致。全量解读除 Ashtottari 段和因此改变的结果哈希外,其余路径 diff 为空。 +- 防复发:`tests/test_dasha_api_engine_parity.py` 里的 Ashtottari 对照。PyJHora 缺失时对照部分跳过,与引擎自身的当前大运查表仍保留。 +- 相关记录:BUG-1341 +- 复发自:无 +- 修复版本:分支 `codex/dasha-unify-20261013`。 + diff --git a/docs/tasks/PROGRESS-dasha-unify-20261013.md b/docs/tasks/PROGRESS-dasha-unify-20261013.md new file mode 100644 index 00000000..5d60ee46 --- /dev/null +++ b/docs/tasks/PROGRESS-dasha-unify-20261013.md @@ -0,0 +1,77 @@ +# PROGRESS · dasha-unify · 2026-10-16 + +分支 `codex/dasha-unify-20261013`,从 `origin/staging` `b9cbe9aa` 拉出。issue #69。 + +## 结论 + +`/api/dasha` 除 Vimshottari 外改为调用完整解读同一批模块。没有真实现的体系返回「未实现」,不再编周期。占位文件 `scripts/extended_dashas.py`、`scripts/conditional_dashas.py` 已删除。Ashtottari 在已判定适用、且月亮落在起运星宿组内时,第一段从出生时刻减去已走过的部分起算。Narayana 仍标「旧算法」,没有换成 Rath 版。 + +## 映射(最终) + +| 注册键 | 实现 | +| --- | --- | +| vimshottari | 原 handler 分支,`domain_calculation_service.compute_vimshottari_timeline` | +| yogini / ashtottari / kalachakra | 与完整解读相同的函数和入参。Ashtottari 额外带月亮经度 | +| panchottari、satabdika、chaturaaseeti、shasti_hayani、shodasottari、dwadashottari、dwisaptati、shattrimsa | 独立模块 `calculate_*`,入参与 `conditional_birth_info` 相同,并带 `PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE`。不传强制出表档,不适用的盘主运为空 | +| narayana | `narayana_dasha_full_report` + `label_legacy_narayana` | +| shoola | `rashi_dashas_native.calculate_rashi_dashas` 的 `shoola` 段(`shoola_forward_md_direct_ad_v1`) | +| chara | 400,消息含 `/api/dasha/chara` | +| 其余 | 400,`dasha system not implemented` | + +`precision`:权威实现为 `engine_module`。删掉 `generic` 和 `calculator`。 + +## 基线红 / 修后绿 + +开工时测试还没写,没有在旧代码上跑新 pytest。占位输出用 `git show HEAD:scripts/extended_dashas.py` 复现(虚构盘、月宿 22): + +| 体系 | 占位前 3 段 | +| --- | --- | +| panchottari | Ketu 1990-01-01、Venus 2002-01-01、Sun 2014-01-01 | +| satabdika | Ketu 1990-01-01、Venus 2001-01-01、Sun 2012-01-02 | +| chaturaaseeti | Ketu 1990-01-01、Venus 1999-01-01、Sun 2008-01-02 | +| narayana | Pisces 3 年、Capricorn 6 年、Aquarius 9 年 | + +修后 `tests/test_dasha_api_engine_parity.py`:9 通过,0 失败。接口主运与完整解读同一模块逐项相等。Panchottari / Satabdika / Chaturshitisama 在标准出表档下,主运起始日与 PyJHora 4.8.7 逐日相同。Ashtottari 三张盘(虚构盘走默认规则族,一张公开盘走 Rahu 规则族,另一张公开盘走默认规则族)起止日与 PyJHora 相差不超过 1 天,起运星一致,出生时刻落在第一段内,当前大运与按日期查表一致。 + +## 自测 + +| 检查 | 命令 | 结果 | +| --- | --- | --- | +| 任务书 pytest 加年长对齐 | `.venv\Scripts\python.exe -m pytest` 任务书第 8 节文件 + `tests/test_dasha_year_length_alignment.py` | 247 通过,0 失败,退出码 0 | +| release 门 | `.venv\Scripts\python.exe tests\run_all.py` | 102/102,失败 0,0.2 秒 | +| 排盘字节 | 清空 `scratch/local/api_chart_cache` 后 `_compute_chart_sync`,对照开工前基线 | 94348 字节。8 处差异全是语义缓存的 `cache_created_at` / `cache_expires_at`。`available_dashas` 与 `dasha_count` 35 未变 | +| 全量解读字节 | 同一虚构盘、`PYTHONHASHSEED=0`、`VEDASTRO_ENABLE_NETWORK=0`、today 与 transit-date 2026-01-01 | 去掉墙钟字段后 3492 条差异:3490 条在 `modules.ashtottari_dasha`;另外 2 条是顶层 `result_hash` 与 `result_binding.result_hash`(同一哈希,因 Ashtottari 进入绑定载荷)。`input_hash` 未变。其余模块 diff 为空 | + +类方法数基线:原值 218,新值 217,原因是删除 `_fallback_dasha_periods`,没有新增方法。行数基线仍是 10924(实测 10906,净减少,不下调基线)。 + +## T4 年数表 + +已改为引用 `scripts/dasha_common.py` 的 `VIMSHOTTARI_YEARS`(顺序 Ketu、Venus、Sun、Moon、Mars、Rahu、Jupiter、Saturn、Mercury)。替换前用 `HEAD` 字面量核对 `list(items)` 一致的文件: + +- `scripts/jyotish_engine.py` 的 `DASHA_YEARS` +- `scripts/dasha_calculator.py`、`scripts/dasha_calculator_enhanced.py` 的 `VIMSHOTTARI_PERIODS` +- `scripts/vimshottari_subperiod_timeline.py` 的 `DASHA_YEARS` +- `scripts/event_prediction_model.py` 的 `DASHA_YEARS` +- `scripts/nakshatra_dasha.py` 的 Vimshottari 年数表(`calc_ashtottari_dasha` 未改) +- `scripts/prashna.py` 的 `VIM_DURATIONS` +- `scripts/validate.py` 的 `DASHA_YEARS` +- `scripts/kp_system.py` 的年限列表 `[7, 20, 6, 10, 7, 18, 16, 19, 17]`,按原 `KP_LORDS` 顺序取共享表 +- `scripts/tajika.py` 两处函数内字面量 + +未换、也未改:冻结的 `scripts/dasha_analyzer.py`;`scripts/divisional_charts_extended.py` 根上没有这份年数表;`scripts/research/offline_research_20260926_lib.py` 属于研究脚本。六个条件大运的 `_birth_lord_and_balance` 改为一行调用。2000 个固定种子月亮经度与源码副本公式 `==`,0 差异。`shastihayani`、`dwadashottari` 的余额函数未动。 + +## 冻结文件 + +`sealed_holdout_rerun` 的 PRODUCTION / RESEARCH 与 `minute_rectification_holdout_v3.json` 的 `frozen_scoring.files` 均未出现在本次 diff 中。未改 `narayana_dasha.py`、`narayana_rath.py`、`narayana_legacy_label.py`、`domain_calculation_service.py`、`dasha_analyzer.py`。 + +## 历史证据未改 + +`references/oracle/full_technique_invocation_matrix_2026_07_22.json`、`references/oracle/rectification_missing_layer_integration_plan_2026_07_19.json` 仍写着已删除文件的路径。`references/cross_project_contract/sync_ledger.json` 只提到测试文件名。这些 JSON 不改。 + +## 偏离 + +- 入参构造放在 `scripts/dasha_registry.py`,没有从 `cmd_full_reading` 抽出共享函数,避免改动全量解读里 Yogini / Kalachakra 的出生信息。等价由对照测试锁住。 +- `scripts/orchestrator_bridge.py` 的 Ashtottari 调用已经从盘面带上月亮经度,这次没有改代码。 +- `nakshatra_dasha.calc_ashtottari_dasha`(从 Mars 起算)前端没有读取。全量解读字节对照里它没有变化。留到后续阶段,不在本单替换。 +- 新 pytest 没有在改代码之前跑红。红的证据是上面从 `HEAD` 复现的占位输出,加上任务书第 2.1 节的对照表。 +- PyJHora 4.8.7 只装在本工作树的虚拟环境里做对照,没有写入依赖清单。 diff --git a/scripts/ashtottari_dasha.py b/scripts/ashtottari_dasha.py index 55bd68dc..12e3fd3c 100644 --- a/scripts/ashtottari_dasha.py +++ b/scripts/ashtottari_dasha.py @@ -288,6 +288,55 @@ def _calc_candidate_balance_and_birth_chain(moon_longitude: float, starting_plan return balance, birth_chain +def _timeline_start_with_birth_balance( + birth_dt: datetime, + moon_longitude, + starting_planet: str, +) -> tuple[datetime, bool]: + """Shift the first mahadasha so birth falls inside the elapsed portion. + + Both applicability families use the starting lord's nakshatra group. When + the Moon longitude is missing, or it does not fall in that group, the + timeline stays on the birth instant. + """ + if moon_longitude is None: + return birth_dt, False + balance, _birth_chain = _calc_candidate_balance_and_birth_chain( + moon_longitude=float(moon_longitude), + starting_planet=starting_planet, + ) + if balance is None: + return birth_dt, False + md_years = next(lord["years"] for lord in DASHA_SEQUENCE if lord["planet"] == starting_planet) + elapsed_years = md_years - float(balance["years"]) + if elapsed_years == 0: + return birth_dt, False + return birth_dt - timedelta(days=elapsed_years * 365.25), True + + +def _current_period_from_shifted_majors(major_periods: list, now: datetime) -> dict | None: + if not major_periods: + return None + first = datetime.fromisoformat(major_periods[0]["start_date"]) + last = datetime.fromisoformat(major_periods[-1]["end_date"]) + span = last - first + if span.total_seconds() <= 0 or now < first: + return None + cycles = int((now - first).total_seconds() // span.total_seconds()) + for period in major_periods: + start = datetime.fromisoformat(period["start_date"]) + cycles * span + end = datetime.fromisoformat(period["end_date"]) + cycles * span + if start <= now < end: + current = period.copy() + current["start_date"] = start.isoformat() + current["end_date"] = end.isoformat() + current["elapsed_years"] = (now - start).days / 365.25 + current["remaining_years"] = (end - now).days / 365.25 + current["cycle_number"] = cycles + 1 + return current + return None + + def _build_major_periods(start_lord_idx: int, birth_date: datetime) -> list: """ Build the full list of major periods (Mahadashas) given the starting lord. @@ -418,36 +467,37 @@ def calculate_ashtottari_dasha(birth_info: dict) -> dict: idx for idx, lord in enumerate(DASHA_SEQUENCE) if lord["planet"] == starting_planet ) - major_periods = _build_major_periods(start_lord_idx, birth_dt) + timeline_start, balance_shifted = _timeline_start_with_birth_balance( + birth_dt, moon_longitude, starting_planet, + ) + major_periods = _build_major_periods(start_lord_idx, timeline_start) antardasha_periods, pratyantardasha_periods = _attach_recursive_subperiods(major_periods) - balance_at_birth = None - dasha_at_birth = None - if moon_longitude is not None and rule_family == RULE_FAMILY_RAHU_FROM_LAGNA_LORD: - balance_at_birth, dasha_at_birth = _calc_candidate_balance_and_birth_chain( - moon_longitude=float(moon_longitude), - starting_planet=starting_planet, - ) # Determine current period. Major periods repeat every 108 years; older natives # should still return a current period instead of None after the first cycle. + # A shifted timeline is read from the period dates. An unshifted timeline keeps + # the previous birth-anchored walk so charts without a Moon longitude stay put. now = datetime.now() current_period = None - age_years = max((now - birth_dt).days / 365.25, 0) - current_in_cycle = age_years % TOTAL_CYCLE - cumulative = 0.0 - for p in major_periods: - years = p["years"] - if cumulative <= current_in_cycle < cumulative + years: - cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25) - cycle_end = cycle_start + timedelta(days=years * 365.25) - current_period = p.copy() - current_period["start_date"] = cycle_start.isoformat() - current_period["end_date"] = cycle_end.isoformat() - current_period["elapsed_years"] = (now - cycle_start).days / 365.25 - current_period["remaining_years"] = (cycle_end - now).days / 365.25 - current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1 - break - cumulative += years + if balance_shifted: + current_period = _current_period_from_shifted_majors(major_periods, now) + else: + age_years = max((now - birth_dt).days / 365.25, 0) + current_in_cycle = age_years % TOTAL_CYCLE + cumulative = 0.0 + for p in major_periods: + years = p["years"] + if cumulative <= current_in_cycle < cumulative + years: + cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25) + cycle_end = cycle_start + timedelta(days=years * 365.25) + current_period = p.copy() + current_period["start_date"] = cycle_start.isoformat() + current_period["end_date"] = cycle_end.isoformat() + current_period["elapsed_years"] = (now - cycle_start).days / 365.25 + current_period["remaining_years"] = (cycle_end - now).days / 365.25 + current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1 + break + cumulative += years result = { "applicable": True, diff --git a/scripts/chaturshitisama_dasha.py b/scripts/chaturshitisama_dasha.py index 9e7e8edf..df8424b2 100644 --- a/scripts/chaturshitisama_dasha.py +++ b/scripts/chaturshitisama_dasha.py @@ -12,6 +12,11 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import Any +try: + from scripts.dasha_common import nakshatra_seeded_birth_balance +except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path + from dasha_common import nakshatra_seeded_birth_balance + YEAR_DAYS = 365.25636 NAKSHATRA_SPAN = 360.0 / 27.0 @@ -65,11 +70,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]: def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: - moon = float(moon_longitude) % 360.0 - nakshatra = int(moon // NAKSHATRA_SPAN) - lord = DASHA_SEQUENCE[((nakshatra - SWATI_INDEX) % 27) % len(DASHA_SEQUENCE)][0] - balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN) - return lord, balance, nakshatra + return nakshatra_seeded_birth_balance( + moon_longitude, anchor_index=SWATI_INDEX, sequence=DASHA_SEQUENCE, + ) def calculate_chaturshitisama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]: diff --git a/scripts/conditional_dashas.py b/scripts/conditional_dashas.py deleted file mode 100644 index 2459a38b..00000000 --- a/scripts/conditional_dashas.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -额外Dasha系统模块 v1.0 -基于BPHS条件性Dasha体系 - -支持的Dasha系统: -1. Dwisaptati Sama Dasha (72年周期) — 当Lagna lord在特定条件时触发 -2. Shattrimsa Sama Dasha (36年周期) — 当Moon在特定条件时触发 -3. Dwadashottari Dasha (112年周期) — 当Venus在特定Nakshatra时触发 -""" - -from datetime import datetime, timedelta -from typing import Dict, List, Tuple, Optional - -YEAR_DAYS = 365.25636 -SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo', - 'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'] -NAKSHATRAS = ['Ashwini','Bharani','Krittika','Rohini','Mrigashira','Ardra', - 'Punarvasu','Pushya','Ashlesha','Magha','Purva Phalguni','Uttara Phalguni', - 'Hasta','Chitra','Swati','Vishakha','Anuradha','Jyeshtha', - 'Mula','Purva Ashadha','Uttara Ashadha','Shravana','Dhanishta','Shatabhisha', - 'Purva Bhadrapada','Uttara Bhadrapada','Revati'] - -VIMSHOTTARI_DURATION = {'Ketu':7,'Venus':20,'Sun':6,'Moon':10,'Mars':7,'Rahu':18,'Jupiter':16,'Saturn':19,'Mercury':17} -DASHA_ORDER = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury'] - - -# ============================================================================= -# Dwisaptati Sama Dasha (72年周期) -# 适用条件:Lagna lord在Lagna或7宫时激活 -# 主星序列:特定8星顺序,每9年一个主星 -# ============================================================================= - -DWISAPTATI_LORDS = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu'] -DWISAPTATI_DURATION = {l: 9 for l in DWISAPTATI_LORDS} # 每个9年,共72年 -DWISAPTATI_SUB_DURATION = {p: VIMSHOTTARI_DURATION[p]*9/120 for p in DWISAPTATI_LORDS} - - -def check_dwisaptati_condition(lagna_lord: str, lagna_lord_house: int) -> bool: - """检查Dwisaptati触发条件:Lagna lord在1或7宫""" - return lagna_lord_house in (1, 7) - - -def calc_dwisaptati_dasha(birth_date: datetime, lagna_lord: str, - lagna_lord_degree: float) -> List[Dict]: - """ - 计算Dwisaptati Sama Dasha(72年周期)。 - - 每个主星周期 = 9年,子周期按Vimshottari比例分配。 - """ - start_idx = DWISAPTATI_LORDS.index(lagna_lord) - sequence = [] - current = birth_date - - for i in range(8): - lord_idx = (start_idx + i) % 8 - lord = DWISAPTATI_LORDS[lord_idx] - days = 9 * YEAR_DAYS - end_date = current + timedelta(days=days) - - # 子周期 - subs = [] - sub_start = current - for j in range(8): - sub_lord_idx = (lord_idx + j) % 8 - sub_lord = DWISAPTATI_LORDS[sub_lord_idx] - sub_days = days * (VIMSHOTTARI_DURATION.get(sub_lord, 9) / 120) - sub_end = sub_start + timedelta(days=sub_days) - subs.append({'lord': sub_lord, 'start': sub_start.strftime('%Y-%m-%d'), - 'end': sub_end.strftime('%Y-%m-%d')}) - sub_start = sub_end - - sequence.append({ - 'lord': lord, 'years': 9, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - 'sub_periods': subs[:4], - }) - current = end_date - - return sequence - - -# ============================================================================= -# Shattrimsa Sama Dasha (36年周期) -# 适用条件:特定条件触发(Moon-based condition) -# 主星序列:9星,每星4年 -# ============================================================================= - -SHATTRIMSA_DURATION = {l: 4 for l in DASHA_ORDER} - - -def check_shattrimsa_condition(moon_sign: str) -> bool: - """Shattrimsa触发条件:Moon在特定星座/条件时""" - return True # 简化:总可用 - - -def calc_shattrimsa_dasha(birth_date: datetime, moon_nakshatra_idx: int) -> List[Dict]: - """计算Shattrimsa Sama Dasha(36年周期)。每个主星=4年""" - start_lord_idx = moon_nakshatra_idx % 9 - sequence = [] - current = birth_date - - for i in range(9): - lord_idx = (start_lord_idx + i) % 9 - lord = DASHA_ORDER[lord_idx] - days = 4 * YEAR_DAYS - end_date = current + timedelta(days=days) - - sequence.append({ - 'lord': lord, 'years': 4, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - - return sequence - - -# ============================================================================= -# Dwadashottari Dasha (112年周期) -# 适用条件:当Venus在特定Nakshatra(Revati/UttaraBhadrapada等)时触发 -# 主星序列:特定8星,不同年限 -# ============================================================================= - -DWADASHOTTARI_LORDS = ['Sun','Jupiter','Ketu','Mercury','Rahu','Mars','Saturn','Moon'] -DWADASHOTTARI_YEARS = {'Sun':7,'Jupiter':9,'Ketu':11,'Mercury':13,'Rahu':15,'Mars':17,'Saturn':19,'Moon':21} - -VENUS_TRIGGER_NAKSHATRAS = {24, 25, 26} # Shatabhisha, Purva Bhadra, Uttara Bhadra - - -def check_dwadashottari_condition(venus_nakshatra_idx: int) -> bool: - """Dwadashottari触发条件:Venus在特定Nakshatra""" - return venus_nakshatra_idx in VENUS_TRIGGER_NAKSHATRAS - - -def calc_dwadashottari_dasha(birth_date: datetime, moon_degree: float) -> List[Dict]: - """计算Dwadashottari Dasha(112年周期)""" - nak_idx = int(moon_degree / (360/27)) - start_lord_idx = nak_idx % 8 - sequence = [] - current = birth_date - - for i in range(8): - lord_idx = (start_lord_idx + i) % 8 - lord = DWADASHOTTARI_LORDS[lord_idx] - years = DWADASHOTTARI_YEARS[lord] - days = years * YEAR_DAYS - end_date = current + timedelta(days=days) - - sequence.append({ - 'lord': lord, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - - return sequence - - -# ============================================================================= -# 条件性Dasha综合选择器 -# ============================================================================= - -def get_applicable_dashas(planets: Dict, lagna_sign: str, moon_degree: float) -> Dict: - """ - 根据星盘条件推荐适用���Dasha系统。 - - Returns: - 各Dasha系统的触发状态和计算结果 - """ - results = {'vimshottari': {'applicable': True, 'note': '始终适用(120年标准周期)'}} - - # Dwisaptati - lagna_lord = {'Aries':'Mars','Taurus':'Venus','Gemini':'Mercury','Cancer':'Moon', - 'Leo':'Sun','Virgo':'Mercury','Libra':'Venus','Scorpio':'Mars', - 'Sagittarius':'Jupiter','Capricorn':'Saturn','Aquarius':'Saturn','Pisces':'Jupiter'}.get(lagna_sign, '') - lord_house = planets.get(lagna_lord, {}).get('house', 0) - if check_dwisaptati_condition(lagna_lord, lord_house): - results['dwisaptati'] = {'applicable': True, 'note': f'Lagna lord {lagna_lord}在{lord_house}宫——72年周期激活'} - - # Shattrimsa - if check_shattrimsa_condition(lagna_sign): - results['shattrimsa'] = {'applicable': True, 'note': 'Moon-based condition——36年周期可用'} - - # Dwadashottari - venus_data = planets.get('Venus', {}) - venus_nak = int(venus_data.get('degree', 0) / (360/27)) - if check_dwadashottari_condition(venus_nak): - results['dwadashottari'] = {'applicable': True, 'note': f'Venus在触发星宿——112年周期激活'} - - return results diff --git a/scripts/dasha_calculator.py b/scripts/dasha_calculator.py index fbc2d3d0..d0dc8f82 100644 --- a/scripts/dasha_calculator.py +++ b/scripts/dasha_calculator.py @@ -6,18 +6,10 @@ Vedic Astrology Dasha Period Calculator This script helps calculate Dasha periods based on Nakshatra position. """ -# Standard Vimshottari Dasha periods in years -VIMSHOTTARI_PERIODS = { - 'Ketu': 7, - 'Venus': 20, - 'Sun': 6, - 'Moon': 10, - 'Mars': 7, - 'Rahu': 18, - 'Jupiter': 16, - 'Saturn': 19, - 'Mercury': 17 -} +try: + from dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS # Planetary order in Vimshottari Dasha DASHA_ORDER = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury'] diff --git a/scripts/dasha_calculator_enhanced.py b/scripts/dasha_calculator_enhanced.py index f63bb4a9..42cc65cc 100644 --- a/scripts/dasha_calculator_enhanced.py +++ b/scripts/dasha_calculator_enhanced.py @@ -21,18 +21,10 @@ import math # 恒星年(天)- 基于jyotishganit (MIT License),用于精确Dasha计算 YEAR_DURATION_DAYS = 365.25636 -# Vimshottari Dasha周期(年)- 总120年 -VIMSHOTTARI_PERIODS = { - 'Ketu': 7, - 'Venus': 20, - 'Sun': 6, - 'Moon': 10, - 'Mars': 7, - 'Rahu': 18, - 'Jupiter': 16, - 'Saturn': 19, - 'Mercury': 17 -} +try: + from dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_YEARS as VIMSHOTTARI_PERIODS HUMAN_LIFE_SPAN_VIMSHOTTARI = 120.0 diff --git a/scripts/dasha_common.py b/scripts/dasha_common.py new file mode 100644 index 00000000..3c2d07c5 --- /dev/null +++ b/scripts/dasha_common.py @@ -0,0 +1,46 @@ +"""Shared dasha arithmetic used by the conditional-dasha modules. + +This module must not import the API server or PyJHora. PyJHora stays a test +oracle only. +""" + +from __future__ import annotations + +NAKSHATRA_SPAN = 360.0 / 27.0 + +# Insertion order is the Vimshottari sequence. Callers compare items() before +# replacing a local table, and rely on this order staying stable. +VIMSHOTTARI_YEARS = { + "Ketu": 7, + "Venus": 20, + "Sun": 6, + "Moon": 10, + "Mars": 7, + "Rahu": 18, + "Jupiter": 16, + "Saturn": 19, + "Mercury": 17, +} +VIMSHOTTARI_ORDER = list(VIMSHOTTARI_YEARS.keys()) + + +def nakshatra_seeded_birth_balance( + moon_longitude: float, + *, + anchor_index: int, + sequence: tuple | list, +) -> tuple[str, float, int]: + """Birth lord and remaining years for a nakshatra-seeded conditional dasha. + + The arithmetic matches the six copied helpers: offset from the anchor + nakshatra, lord = sequence[offset % length], balance = years * (1 - fraction + already elapsed inside the nakshatra). Operation order is fixed so the + float matches the previous per-module copies. + """ + moon = float(moon_longitude) % 360.0 + nakshatra_index = int(moon // NAKSHATRA_SPAN) + offset = (nakshatra_index - int(anchor_index)) % 27 + lord, full_years = sequence[offset % len(sequence)] + fraction_elapsed = (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN + remaining_years = full_years * (1.0 - fraction_elapsed) + return lord, remaining_years, nakshatra_index diff --git a/scripts/dasha_registry.py b/scripts/dasha_registry.py new file mode 100644 index 00000000..87aaa1e4 --- /dev/null +++ b/scripts/dasha_registry.py @@ -0,0 +1,342 @@ +"""HTTP dasha registry. + +`DASHA_REGISTRY_METADATA` is the old `extended_dashas.DASHA_REGISTRY` moved +verbatim so `/api/chart` `available_dashas` stays byte-identical. + +`compute_dasha` calls the same modules `cmd_full_reading` calls, with the same +birth-info fields. Systems without that module return "not implemented" +instead of a fabricated timeline. + +This module must not import `jyotish_api_server` or PyJHora. +""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +try: + from source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE + +DASHA_REGISTRY_METADATA = { + "vimshottari": {"name": "Vimshottari", "years": 120, "type": "nakshatra"}, + "ashtottari": {"name": "Ashtottari", "years": 108, "type": "nakshatra"}, + "yogini": {"name": "Yogini", "years": 36, "type": "nakshatra"}, + "kalachakra": {"name": "Kalachakra", "years": 94, "type": "nakshatra"}, + "tara": {"name": "Tara Dasha", "years": 81, "type": "nakshatra"}, + "shodasottari": {"name": "Shodasottari", "years": 128, "type": "nakshatra"}, + "panchottari": {"name": "Panchottari", "years": 105, "type": "nakshatra"}, + "satabdika": {"name": "Satabdika", "years": 100, "type": "nakshatra"}, + "chara": {"name": "Chara (Jaimini)", "years": 36, "type": "rasi"}, + "narayana": {"name": "Narayana", "years": 36, "type": "rasi"}, + "shoola": {"name": "Shoola Dasha", "years": 9, "type": "rasi"}, + "sudasa": {"name": "Sudasa", "years": 36, "type": "rasi"}, + "drig": {"name": "Drig Dasha", "years": 36, "type": "rasi"}, + "sthira": {"name": "Sthira Dasha", "years": 36, "type": "rasi"}, + "mandooka": {"name": "Mandooka Dasha", "years": 36, "type": "rasi"}, + "lagnamsaka": {"name": "Lagnamsaka Dasha", "years": 36, "type": "rasi"}, + "yogardha": {"name": "Yogardha Dasha", "years": 36, "type": "rasi"}, + "brahma": {"name": "Brahma Dasha", "years": 36, "type": "rasi"}, + "dwisaptati": {"name": "Dwisaptati Sama", "years": 72, "type": "conditional"}, + "shattrimsa": {"name": "Shattrimsa Sama", "years": 36, "type": "conditional"}, + "dwadashottari": {"name": "Dwadashottari", "years": 112, "type": "conditional"}, + "shasti_hayani": {"name": "Shasti-Hayani", "years": 60, "type": "conditional"}, + "chaturaaseeti": {"name": "Chaturaaseeti Sama", "years": 84, "type": "conditional"}, + "tithi_ashtottari": {"name": "Tithi Ashtottari", "years": 108, "type": "conditional"}, + "tithi_yogini": {"name": "Tithi Yogini", "years": 36, "type": "conditional"}, + "patyayini": {"name": "Patyayini", "years": 36, "type": "conditional"}, + "navamsa": {"name": "Navamsa Dasha", "years": 36, "type": "varga"}, + "kendradi": {"name": "Kendradi", "years": 38, "type": "bhav"}, + "paryaaya": {"name": "Paryaaya Dasha", "years": 36, "type": "varga"}, + "rashmi": {"name": "Rashmi Dasha", "years": 36, "type": "special"}, + "naisargika": {"name": "Naisargika Dasha", "years": 120, "type": "special"}, + "aayu": {"name": "Aayu Dasha", "years": 36, "type": "special"}, + "karaka": {"name": "Karaka Dasha", "years": 36, "type": "special"}, + "panchasvara": {"name": "Panchasvara Dasha", "years": 36, "type": "special"}, + "sudarsana": {"name": "Sudarsana Chakra Dasha", "years": 36, "type": "special"}, +} + +_CONDITIONAL_CALLS = { + "shodasottari": ("shodashottari_dasha", "calculate_shodashottari_dasha", {}), + "dwadashottari": ( + "dwadashottari_dasha", + "calculate_dwadashottari_dasha", + {"sukramsaka_profile": "navamsa_lagna_taurus_or_libra"}, + ), + "dwisaptati": ("dwisaptatisama_dasha", "calculate_dwisaptatisama_dasha", {}), + "shattrimsa": ("shattrimshatsama_dasha", "calculate_shattrimshatsama_dasha", {}), + "panchottari": ("panchottari_dasha", "calculate_panchottari_dasha", {}), + "satabdika": ("satabdika_dasha", "calculate_satabdika_dasha", {}), + "chaturaaseeti": ("chaturshitisama_dasha", "calculate_chaturshitisama_dasha", {}), +} + +SIGNS = [ + "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", + "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", +] +SIGN_LORDS = { + "Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon", + "Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars", + "Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter", +} + + +class DashaNotImplemented(Exception): + """The registry key exists, but there is no engine module behind it.""" + + +def available_dasha_rows() -> list[dict[str, Any]]: + """`/api/chart` `available_dashas` rows, same order and fields as before.""" + return [ + {"key": key, "name": info["name"], "years": info["years"], "type": info["type"]} + for key, info in DASHA_REGISTRY_METADATA.items() + ] + + +def _load(module_name: str): + try: + return __import__(module_name) + except ModuleNotFoundError: # pragma: no cover - package import + return __import__(f"scripts.{module_name}", fromlist=[module_name]) + + +def _longitude(value: Any) -> float | None: + if isinstance(value, (int, float)): + return float(value) % 360.0 + if not isinstance(value, dict): + return None + for key in ("lon", "longitude", "degree"): + raw = value.get(key) + if isinstance(raw, (int, float)): + return float(raw) % 360.0 + sign_idx = value.get("sign_idx") + if sign_idx is None and value.get("sign") in SIGNS: + sign_idx = SIGNS.index(value["sign"]) + degree = value.get("degree_in_sign", value.get("degree", 0)) + try: + if sign_idx is None: + return None + return (int(sign_idx) * 30 + float(degree or 0)) % 360.0 + except (TypeError, ValueError): + return None + + +def _planet_longitudes(planets: dict) -> dict[str, float]: + longitudes = {} + if not isinstance(planets, dict): + return longitudes + for name, data in planets.items(): + longitude = _longitude(data) + if longitude is not None: + longitudes[str(name)] = longitude + return longitudes + + +def _optional_float(body: dict, *keys: str) -> float | None: + for key in keys: + raw = body.get(key) + if isinstance(raw, (int, float)): + return float(raw) + return None + + +def _reference_datetime(birth_dt: datetime, body: dict) -> datetime: + raw = body.get("today") or body.get("current_date") + if isinstance(raw, datetime): + return raw + if isinstance(raw, str) and raw.strip(): + return datetime.strptime(raw.strip()[:10], "%Y-%m-%d") + return datetime.now() + + +def build_reading_birth_info( + birth_dt: datetime, + planets: dict, + ascendant: dict | None, + body: dict | None = None, +) -> tuple[dict[str, Any], dict[str, Any], dict[str, float]]: + """Same fields `cmd_full_reading` puts in `conditional_birth_info`. + + Returns `(alt_dasha_info, conditional_birth_info, planet_longitudes)`. + Kept here, not extracted from the engine, so the full-reading dict + construction and its output stay untouched. + """ + body = body or {} + ascendant = ascendant or {} + planet_lons = _planet_longitudes(planets) + moon_lon = planet_lons.get("Moon", _longitude(body.get("moon_lon")) or 0.0) + sun_lon = planet_lons.get("Sun", _longitude(body.get("sun_lon")) or 0.0) + asc_lon = _longitude(ascendant) + if asc_lon is None: + asc_lon = _longitude(body.get("ascendant_longitude")) + if asc_lon is None: + sign_idx = ascendant.get("sign_idx", body.get("asc_sign_idx", 0)) + try: + asc_idx = int(sign_idx) % 12 + except (TypeError, ValueError): + asc_idx = 0 + asc_lon = float(asc_idx * 30) + asc_idx = int(float(asc_lon) / 30) % 12 + moon_nakshatra_index = int(moon_lon / (360 / 27)) % 27 + moon_pada = int((moon_lon % (360 / 27)) / (360 / 108)) + 1 + tithi_number = int(((moon_lon - sun_lon) % 360) / 12) + 1 + lagna_lord_name = SIGN_LORDS[SIGNS[asc_idx]] + tenth_lord_name = SIGN_LORDS[SIGNS[(asc_idx + 9) % 12]] + try: + varga = _load("varga") + navamsa_ascendant_sign_index = int(varga.calc_varga(asc_lon, 9).get("sign_idx", 0)) % 12 + venus_navamsa_sign_index = int(varga.calc_varga(float(planet_lons.get("Venus", 0)), 9).get("sign_idx", 0)) % 12 + except Exception: + navamsa_ascendant_sign_index = None + venus_navamsa_sign_index = None + degree_in_sign = float(asc_lon) % 30.0 + ascendant_hora_lord = ( + "Sun" + if (asc_idx % 2 == 0 and degree_in_sign < 15.0) + or (asc_idx % 2 == 1 and degree_in_sign >= 15.0) + else "Moon" + ) + birth_is_daytime = None + lat = _optional_float(body, "lat", "latitude") + lon = _optional_float(body, "lon", "longitude") + tz = _optional_float(body, "tz", "timezone") + if lat is not None and lon is not None and tz is not None: + try: + daynight = _load("saham_daynight") + birth_is_daytime = bool(daynight.determine_daytime(birth_dt, lat=lat, lon=lon, tz=tz).get("is_daytime")) + except Exception: + birth_is_daytime = None + applicability_rule_family = ( + "rahu_from_lagna_lord_kendra_trikona_plus_paksha_daynight" + if birth_is_daytime is not None + else "moon_nakshatra_plus_paksha" + ) + alt = { + "birth_datetime": birth_dt, + "moon_nakshatra_index": moon_nakshatra_index, + "moon_pada": moon_pada, + "is_shukla_paksha": 1 <= tithi_number <= 15, + "lagna_rashi_index": asc_idx, + } + conditional = { + **alt, + "ascendant_longitude": asc_lon, + "sun_longitude": sun_lon, + "moon_longitude": moon_lon, + "birth_is_daytime": birth_is_daytime, + "ascendant_sign_index": asc_idx, + "ascendant_hora_lord": ascendant_hora_lord, + "navamsa_ascendant_sign_index": navamsa_ascendant_sign_index, + "venus_rasi_sign_index": int(float(planet_lons.get("Venus", 0)) / 30) % 12, + "venus_navamsa_sign_index": venus_navamsa_sign_index, + "tenth_lord_sign_index": int(float(planet_lons.get(tenth_lord_name, 0)) / 30) % 12, + "rahu_sign_index": int(float(planet_lons.get("Rahu", 0)) / 30) % 12, + "lagna_lord_sign_index": int(float(planet_lons.get(lagna_lord_name, 0)) / 30) % 12, + "applicability_rule_family": applicability_rule_family, + } + return alt, conditional, planet_lons + + +def _major_rows(payload: Any) -> list: + if isinstance(payload, dict): + majors = payload.get("major") + if isinstance(majors, list): + return majors + periods = payload.get("periods") + if isinstance(periods, list): + return periods + sequence = payload.get("mahadasha_sequence") + if isinstance(sequence, list): + return sequence + return [] + if isinstance(payload, list): + return payload + return [] + + +def _conditional_payload(key: str, conditional: dict) -> dict: + module_name, function_name, extra = _CONDITIONAL_CALLS[key] + module = _load(module_name) + birth_info = { + **conditional, + **extra, + "child_period_profile": PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE, + } + return getattr(module, function_name)(birth_info) + + +def compute_dasha( + key: str, + *, + birth_dt: datetime, + planets: dict, + ascendant: dict | None, + body: dict | None = None, +) -> dict[str, Any]: + """Return engine major periods for one registry key. + + Raises `DashaNotImplemented` when the key has no engine module. Vimshottari + stays on the handler's existing canonical branch and is not computed here. + """ + if key not in DASHA_REGISTRY_METADATA: + raise DashaNotImplemented("Unknown dasha system") + if key == "chara": + raise DashaNotImplemented("chara dasha is served at /api/dasha/chara") + body = body or {} + alt, conditional, planet_lons = build_reading_birth_info(birth_dt, planets or {}, ascendant, body) + if key == "yogini": + payload = _load("yogini_dasha").calculate_yogini_dasha({**alt, "moon_longitude": conditional["moon_longitude"]}) + return {"periods": _major_rows(payload)} + if key == "ashtottari": + payload = _load("ashtottari_dasha").calculate_ashtottari_dasha({ + **alt, + "moon_longitude": conditional["moon_longitude"], + }) + return {"periods": _major_rows(payload)} + if key == "kalachakra": + payload = _load("kalachakra_dasha").calculate_kalachakra_dasha(alt) + return {"periods": _major_rows(payload)} + if key == "shasti_hayani": + birth_info = dict(conditional) + birth_info["child_period_profile"] = PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE + payload = _load("shastihayani_dasha").calculate_shastihayani_dasha(birth_info) + return {"periods": _major_rows(payload)} + if key in _CONDITIONAL_CALLS: + return {"periods": _major_rows(_conditional_payload(key, conditional))} + if key == "narayana": + asc_idx = int(conditional["ascendant_sign_index"]) + reference = _reference_datetime(birth_dt, body) + current_age = (reference - birth_dt).days / 365.25 + birth_year = int(body.get("year") or birth_dt.year) + narayana = _load("narayana_dasha") + report = narayana.narayana_dasha_full_report( + lagna_sign_idx=asc_idx, + planet_lons=planet_lons, + current_age=current_age, + birth_year=birth_year, + ) + labeled = _load("narayana_legacy_label").label_legacy_narayana( + report, + lagna_sign_idx=asc_idx, + planet_lons=planet_lons, + current_age=current_age, + birth_year=birth_year, + ) + return { + "periods": _major_rows(labeled), + "algorithm_label": labeled.get("algorithm_label") if isinstance(labeled, dict) else None, + "algorithm_note": labeled.get("algorithm_note") if isinstance(labeled, dict) else None, + "profile": labeled.get("profile") if isinstance(labeled, dict) else None, + } + if key == "shoola": + rashi = _load("rashi_dashas_native") + try: + section = rashi.calculate_rashi_dashas( + birth_dt.date(), conditional["ascendant_longitude"], planet_lons + )["shoola"] + except ValueError as exc: + raise DashaNotImplemented(str(exc)) from exc + return {"periods": _major_rows(section)} + raise DashaNotImplemented("dasha system not implemented") diff --git a/scripts/dwisaptatisama_dasha.py b/scripts/dwisaptatisama_dasha.py index e2ff6f1d..d8d632b9 100644 --- a/scripts/dwisaptatisama_dasha.py +++ b/scripts/dwisaptatisama_dasha.py @@ -6,6 +6,11 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import Any +try: + from scripts.dasha_common import nakshatra_seeded_birth_balance +except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path + from dasha_common import nakshatra_seeded_birth_balance + YEAR_DAYS = 365.25636 NAKSHATRA_SPAN = 360.0 / 27.0 @@ -63,11 +68,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]: def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: - moon = float(moon_longitude) % 360.0 - nakshatra = int(moon // NAKSHATRA_SPAN) - lord = DASHA_SEQUENCE[((nakshatra - MULA_INDEX) % 27) % len(DASHA_SEQUENCE)][0] - balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN) - return lord, balance, nakshatra + return nakshatra_seeded_birth_balance( + moon_longitude, anchor_index=MULA_INDEX, sequence=DASHA_SEQUENCE, + ) def calculate_dwisaptatisama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]: diff --git a/scripts/event_prediction_model.py b/scripts/event_prediction_model.py index 7ea33848..1028f28c 100644 --- a/scripts/event_prediction_model.py +++ b/scripts/event_prediction_model.py @@ -101,11 +101,12 @@ EVENT_KARAKAS = { } # Vimshottari Dasha 行星 -DASHA_PLANETS = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury'] -DASHA_YEARS = { - 'Ketu': 7, 'Venus': 20, 'Sun': 6, 'Moon': 10, 'Mars': 7, - 'Rahu': 18, 'Jupiter': 16, 'Saturn': 19, 'Mercury': 17, -} +try: + from dasha_common import VIMSHOTTARI_ORDER as DASHA_PLANETS + from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_ORDER as DASHA_PLANETS + from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS # Graha Drishti 相位 PLANET_ASPECTS = { diff --git a/scripts/extended_dashas.py b/scripts/extended_dashas.py deleted file mode 100644 index aaec9480..00000000 --- a/scripts/extended_dashas.py +++ /dev/null @@ -1,408 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -扩展Dasha系统 v2.0 — 冲刺全球第一 -新增:Kalachakra, Narayana, Yogini, Shasti-Hayani, Navamsa, Kendradi, Tara, Shoola - -Dasha总数:10 → 18 -""" - -from datetime import datetime, timedelta -from typing import Dict, List, Tuple - -YEAR_DAYS = 365.25636 -SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo', - 'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'] -DASHA_ORDER = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury'] -NAKSHATRAS = ['Ashwini','Bharani','Krittika','Rohini','Mrigashira','Ardra', - 'Punarvasu','Pushya','Ashlesha','Magha','PurvaPhalguni','UttaraPhalguni', - 'Hasta','Chitra','Swati','Vishakha','Anuradha','Jyeshtha', - 'Mula','PurvaAshadha','UttaraAshadha','Shravana','Dhanishta','Shatabhisha', - 'PurvaBhadrapada','UttaraBhadrapada','Revati'] - - -# ============================================================================= -# 1. Kalachakra Dasha(时轮大运) -# ============================================================================= - -KALACHAKRA_NAVAMSHA_MAP = {} -KALACHAKRA_SAVYA = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius'] -KALACHAKRA_APASAVYA = ['Scorpio','Libra','Virgo','Cancer','Leo','Gemini','Taurus','Aries','Sagittarius'] -KALACHAKRA_YEARS = {'Aries':10,'Taurus':16,'Gemini':18,'Cancer':24,'Leo':20,'Virgo':22,'Libra':12,'Scorpio':14,'Sagittarius':8,'Capricorn':7,'Aquarius':9,'Pisces':11} - -def calc_kalachakra_dasha(birth_date: datetime, moon_nak_idx: int, - moon_pada: int) -> List[Dict]: - """Kalachakra Dasha(时轮大运)""" - # 判定savya/apasavya序列 - savya_naks = {0,1,2,3,4,5,10,11,12,13,14,15,22,23,24,25,26} - is_savya = moon_nak_idx in savya_naks - sequence = KALACHAKRA_SAVYA if is_savya else KALACHAKRA_APASAVYA - - # 起始点 - pada_map = {1:0, 2:3, 3:6, 4:0} # 简化映射 - start_offset = pada_map.get(moon_pada, 0) - start_idx = (moon_nak_idx % 9 + start_offset) % 9 - - results = [] - current = birth_date - for i in range(9): - sign_idx = (start_idx + i) % 9 - sign = sequence[sign_idx] - years = KALACHAKRA_YEARS.get(sign, 10) - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': sign, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 2. Narayana Dasha(那罗延大运) -# ============================================================================= - -NARAYANA_ORDER = ['Aries','Scorpio','Sagittarius','Pisces','Capricorn','Aquarius','Taurus','Virgo','Leo','Cancer','Gemini','Libra'] - -def calc_narayana_dasha(birth_date: datetime, asc_sign_idx: int) -> List[Dict]: - """Narayana Dasha — 基于星座的推运系统""" - start_idx = NARAYANA_ORDER.index(SIGNS[asc_sign_idx]) - results = [] - current = birth_date - for i in range(12): - idx = (start_idx + i) % 12 - sign = NARAYANA_ORDER[idx] - years = (idx % 3 + 1) * 3 # movable=3, fixed=6, dual=9 - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': sign, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 3. Yogini Dasha(瑜伽女神大运)— 36年周期 -# ============================================================================= - -YOGINI_LORDS = ['Mangala','Pingala','Dhanya','Bhramari','Bhadrika','Ulka','Siddha','Sankata'] -YOGINI_YEARS = [1,2,3,4,5,6,7,8] # 总和=36 - -def calc_yogini_dasha(birth_date: datetime, moon_nak_idx: int) -> List[Dict]: - """Yogini Dasha — 36年女神周期""" - start_idx = moon_nak_idx % 8 - results = [] - current = birth_date - for i in range(8): - idx = (start_idx + i) % 8 - lord = YOGINI_LORDS[idx] - years = YOGINI_YEARS[idx] - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': lord, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 4. Shasti-Hayani Dasha(六十哈亚尼大运) -# ============================================================================= - -SHASTI_LORDS = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn'] -SHASTI_YEARS = [6,8,10,12,14,16,18] - -def calc_shasti_hayani_dasha(birth_date: datetime, sun_sign_idx: int) -> List[Dict]: - """Shasti-Hayani Dasha — 基于太阳位置的60年推运""" - start_idx = sun_sign_idx % 7 - results = [] - current = birth_date - for i in range(7): - idx = (start_idx + i) % 7 - lord = SHASTI_LORDS[idx] - years = SHASTI_YEARS[idx] - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': lord, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 5. Navamsa Dasha(九分盘大运) -# ============================================================================= - -def calc_navamsa_dasha(birth_date: datetime, d9_asc_sign_idx: int) -> List[Dict]: - """Navamsa Dasha — 基于D9上升的星座推运""" - results = [] - current = birth_date - for i in range(12): - sign_idx = (d9_asc_sign_idx + i) % 12 - sign = SIGNS[sign_idx] - years = (i % 3 + 1) * 3 - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': sign, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 6. Kendradi Dasha(角宫推运) -# ============================================================================= - -KENDRADI_ORDER = [1,4,7,10,2,5,8,11,3,6,9,12] - -def calc_kendradi_dasha(birth_date: datetime, asc_sign_idx: int) -> List[Dict]: - """Kendradi Dasha — 从角宫开始的宫位推运""" - results = [] - current = birth_date - for i, house_num in enumerate(KENDRADI_ORDER): - sign_idx = (asc_sign_idx + house_num - 1) % 12 - sign = SIGNS[sign_idx] - years = 5 if i < 4 else 3 # 角宫5年,其他3年 - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': f'House {house_num} ({sign})', 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 7. Tara Dasha(星宿推运) -# ============================================================================= - -def calc_tara_dasha(birth_date: datetime, moon_nak_idx: int) -> List[Dict]: - """Tara Dasha — 从出生星宿开始的推运""" - results = [] - current = birth_date - for i in range(27): - nak_idx = (moon_nak_idx + i) % 27 - nak = NAKSHATRAS[nak_idx] - years = 3 - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': nak, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# 8. Shoola Dasha(尖刺推运) -# ============================================================================= - -def calc_shoola_dasha(birth_date: datetime, moon_sign_idx: int) -> List[Dict]: - """Shoola Dasha — 基于月亮星座的9年周期""" - results = [] - current = birth_date - for i in range(9): - sign_idx = (moon_sign_idx + i) % 12 - sign = SIGNS[sign_idx] - years = 9 - i if i < 9 else 1 - end_date = current + timedelta(days=years * YEAR_DAYS) - results.append({ - 'lord': sign, 'years': years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - - -# ============================================================================= -# Dasha注册表 -# ============================================================================= - -DASHA_REGISTRY = { - # Nakshatra-based - 'vimshottari': {'name': 'Vimshottari', 'years': 120, 'type': 'nakshatra'}, - 'ashtottari': {'name': 'Ashtottari', 'years': 108, 'type': 'nakshatra'}, - 'yogini': {'name': 'Yogini', 'years': 36, 'type': 'nakshatra'}, - 'kalachakra': {'name': 'Kalachakra', 'years': 94, 'type': 'nakshatra'}, - 'tara': {'name': 'Tara Dasha', 'years': 81, 'type': 'nakshatra'}, - 'shodasottari': {'name': 'Shodasottari', 'years': 128, 'type': 'nakshatra'}, - 'panchottari': {'name': 'Panchottari', 'years': 105, 'type': 'nakshatra'}, - 'satabdika': {'name': 'Satabdika', 'years': 100, 'type': 'nakshatra'}, - # Rasi-based - 'chara': {'name': 'Chara (Jaimini)', 'years': 36, 'type': 'rasi'}, - 'narayana': {'name': 'Narayana', 'years': 36, 'type': 'rasi'}, - 'shoola': {'name': 'Shoola Dasha', 'years': 9, 'type': 'rasi'}, - 'sudasa': {'name': 'Sudasa', 'years': 36, 'type': 'rasi'}, - 'drig': {'name': 'Drig Dasha', 'years': 36, 'type': 'rasi'}, - 'sthira': {'name': 'Sthira Dasha', 'years': 36, 'type': 'rasi'}, - 'mandooka': {'name': 'Mandooka Dasha', 'years': 36, 'type': 'rasi'}, - 'lagnamsaka': {'name': 'Lagnamsaka Dasha', 'years': 36, 'type': 'rasi'}, - 'yogardha': {'name': 'Yogardha Dasha', 'years': 36, 'type': 'rasi'}, - 'brahma': {'name': 'Brahma Dasha', 'years': 36, 'type': 'rasi'}, - # Conditional/Tithi-based - 'dwisaptati': {'name': 'Dwisaptati Sama', 'years': 72, 'type': 'conditional'}, - 'shattrimsa': {'name': 'Shattrimsa Sama', 'years': 36, 'type': 'conditional'}, - 'dwadashottari': {'name': 'Dwadashottari', 'years': 112, 'type': 'conditional'}, - 'shasti_hayani': {'name': 'Shasti-Hayani', 'years': 60, 'type': 'conditional'}, - 'chaturaaseeti': {'name': 'Chaturaaseeti Sama', 'years': 84, 'type': 'conditional'}, - 'tithi_ashtottari': {'name': 'Tithi Ashtottari', 'years': 108, 'type': 'conditional'}, - 'tithi_yogini': {'name': 'Tithi Yogini', 'years': 36, 'type': 'conditional'}, - 'patyayini': {'name': 'Patyayini', 'years': 36, 'type': 'conditional'}, - # Varga/Bhava-based - 'navamsa': {'name': 'Navamsa Dasha', 'years': 36, 'type': 'varga'}, - 'kendradi': {'name': 'Kendradi', 'years': 38, 'type': 'bhav'}, - 'paryaaya': {'name': 'Paryaaya Dasha', 'years': 36, 'type': 'varga'}, - # Special - 'rashmi': {'name': 'Rashmi Dasha', 'years': 36, 'type': 'special'}, - 'naisargika': {'name': 'Naisargika Dasha', 'years': 120, 'type': 'special'}, - 'aayu': {'name': 'Aayu Dasha', 'years': 36, 'type': 'special'}, - 'karaka': {'name': 'Karaka Dasha', 'years': 36, 'type': 'special'}, - 'panchasvara': {'name': 'Panchasvara Dasha', 'years': 36, 'type': 'special'}, - 'sudarsana': {'name': 'Sudarsana Chakra Dasha', 'years': 36, 'type': 'special'}, -} - -def get_available_dashas() -> List[str]: - return list(DASHA_REGISTRY.keys()) - -def get_dasha_info(name: str) -> Dict: - return DASHA_REGISTRY.get(name, {}) - -# ============================================================================= -# v6.7.6: 通用Dasha函数 — 所有35种注册Dasha现在都可计算 -# ============================================================================= - -def calc_generic_dasha(birth_date: datetime, lords: List[str], years: List[int], - lord_names: Dict = None) -> List[Dict]: - """通用Dasha计算器 — 任意lord序列和年限组合""" - results = [] - current = birth_date - for lord, yrs in zip(lords, years): - end_date = current + timedelta(days=yrs * YEAR_DAYS) - results.append({ - 'lord': lord_names.get(lord, lord) if lord_names else lord, - 'years': yrs, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return results - -def calc_shodasottari(birth_date, moon_nak_idx): - """Shodasottari Dasha (128年周期) — 16种大运""" - lords = DASHA_ORDER * 2 - return calc_generic_dasha(birth_date, lords[:16], [8]*16) - -def calc_panchottari(birth_date, moon_nak_idx): - """Panchottari Dasha (105年周期)""" - return calc_generic_dasha(birth_date, DASHA_ORDER, [12]*9)[:9] - -def calc_satabdika(birth_date, moon_nak_idx): - """Satabdika Dasha (100年周期)""" - return calc_generic_dasha(birth_date, DASHA_ORDER, [11]*9)[:9] - -def calc_chaturaaseeti(birth_date, moon_nak_idx): - """Chaturaaseeti Sama Dasha (84年周期)""" - return calc_generic_dasha(birth_date, DASHA_ORDER, [9]*9)[:9] - -def calc_tithi_ashtottari(birth_date, tithi_num): - """Tithi Ashtottari Dasha (108年周期)""" - return calc_generic_dasha(birth_date, DASHA_ORDER, [8,12,6,10,7,18,16,19,17])[:9] - -def calc_tithi_yogini(birth_date, tithi_num): - """Tithi Yogini Dasha (36年周期)""" - return calc_generic_dasha(birth_date, YOGINI_LORDS, YOGINI_YEARS) - -def calc_patyayini(birth_date, asc_sign_idx): - """Patyayini Dasha (36年周期)""" - return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9) - -def calc_rashmi(birth_date, moon_nak_idx): - """Rashmi Dasha (36年周期)""" - return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9) - -def calc_naisargika(birth_date): - """Naisargika Dasha (120年周期) — 自然生命周期""" - lords = ['Moon','Mars','Mercury','Venus','Jupiter','Sun','Saturn'] - years_list = [1,2,9,20,18,20,50] - return calc_generic_dasha(birth_date, lords, years_list) - -# Dasha计算函数注册表 -DASHA_CALCULATORS = { - 'kalachakra': calc_kalachakra_dasha, - 'narayana': calc_narayana_dasha, - 'yogini': calc_yogini_dasha, - 'shasti_hayani': calc_shasti_hayani_dasha, - 'navamsa': calc_navamsa_dasha, - 'kendradi': calc_kendradi_dasha, - 'tara': calc_tara_dasha, - 'shoola': calc_shoola_dasha, - 'shodasottari': calc_shodasottari, - 'panchottari': calc_panchottari, - 'satabdika': calc_satabdika, - 'chaturaaseeti': calc_chaturaaseeti, - 'tithi_ashtottari': calc_tithi_ashtottari, - 'tithi_yogini': calc_tithi_yogini, - 'patyayini': calc_patyayini, - 'rashmi': calc_rashmi, - 'naisargika': calc_naisargika, - # 此下为简化映射(使用通用函数或现有函数) - 'dwisaptati': calc_generic_dasha, - 'shattrimsa': calc_generic_dasha, - 'dwadashottari': calc_generic_dasha, - 'sudasa': calc_generic_dasha, - 'drig': calc_generic_dasha, - 'sthira': calc_generic_dasha, - 'mandooka': calc_generic_dasha, - 'lagnamsaka': calc_generic_dasha, - 'yogardha': calc_generic_dasha, - 'brahma': calc_generic_dasha, - 'paryaaya': calc_generic_dasha, - 'aayu': calc_generic_dasha, - 'karaka': calc_generic_dasha, - 'panchasvara': calc_generic_dasha, - 'sudarsana': calc_generic_dasha, -} - -def calc_any_dasha(name: str, birth_date, **kwargs) -> List[Dict]: - """根据名称计算任意Dasha""" - calc_fn = DASHA_CALCULATORS.get(name) - if calc_fn == calc_generic_dasha: - return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9) - if calc_fn: - try: - if name == 'kalachakra': - return calc_fn(birth_date, kwargs.get('moon_nak_idx', 0), kwargs.get('moon_pada', 1)) - if name in {'yogini', 'tara', 'shodasottari', 'panchottari', 'satabdika', 'chaturaaseeti', 'rashmi'}: - return calc_fn(birth_date, kwargs.get('moon_nak_idx', 0)) - if name == 'narayana': - return calc_fn(birth_date, kwargs.get('asc_sign_idx', 0)) - if name == 'shoola': - return calc_fn(birth_date, kwargs.get('moon_sign_idx', 0)) - if name == 'shasti_hayani': - return calc_fn(birth_date, kwargs.get('sun_sign_idx', 0)) - if name == 'navamsa': - return calc_fn(birth_date, kwargs.get('d9_asc_sign_idx', kwargs.get('asc_sign_idx', 0))) - if name == 'kendradi': - return calc_fn(birth_date, kwargs.get('asc_sign_idx', 0)) - if name in {'tithi_ashtottari', 'tithi_yogini'}: - return calc_fn(birth_date, kwargs.get('tithi_num', 1)) - if name == 'patyayini': - return calc_fn(birth_date, kwargs.get('asc_sign_idx', 0)) - if name == 'naisargika': - return calc_fn(birth_date) - return calc_fn(birth_date, **kwargs) - except Exception: - return calc_generic_dasha(birth_date, DASHA_ORDER, [4]*9) - return [] diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 56e4d1b8..8a4c53d2 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -6349,26 +6349,6 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM except (TypeError, ValueError): return None - def _fallback_dasha_periods(self, birth_dt, dasha_key, info): - extended_dashas = _load_local_module('extended_dashas') - DASHA_ORDER = extended_dashas.DASHA_ORDER - cycle_years = float(info.get('years') or 36) - if cycle_years <= 0: - cycle_years = 36 - period_years = round(cycle_years / len(DASHA_ORDER), 2) - periods = [] - current = birth_dt - for lord in DASHA_ORDER: - end_date = current + timedelta(days=period_years * 365.25) - periods.append({ - 'lord': lord, - 'years': period_years, - 'start': current.strftime('%Y-%m-%d'), - 'end': end_date.strftime('%Y-%m-%d'), - }) - current = end_date - return periods - def _import_chart_text(self, body): filename = str(body.get('filename', ''))[:160] text = body.get('text') @@ -6534,10 +6514,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM ayanamsa=ayanamsa_name, ) - # Dasha清单 - extended_dashas = _load_local_module('extended_dashas') - dashas = extended_dashas.get_available_dashas() - dasha_list = [{'key': k, 'name': extended_dashas.DASHA_REGISTRY[k]['name'], 'years': extended_dashas.DASHA_REGISTRY[k]['years'], 'type': extended_dashas.DASHA_REGISTRY[k]['type']} for k in dashas] + # Dasha清单(元数据与旧注册表逐项相同,计算不走占位表) + dasha_registry = _load_local_module('dasha_registry') + dasha_list = dasha_registry.available_dasha_rows() try: jaimini = _load_local_module('jaimini') @@ -7266,8 +7245,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM if not isinstance(dasha_key, str): raise BadRequest('dasha must be a string') dasha_key = dasha_key.strip().lower() - extended_dashas = _load_local_module('extended_dashas') - info = extended_dashas.DASHA_REGISTRY.get(dasha_key) + dasha_registry = _load_local_module('dasha_registry') + info = dasha_registry.DASHA_REGISTRY_METADATA.get(dasha_key) if not info: raise BadRequest('Unknown dasha system') @@ -7280,24 +7259,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM moon_lon = self._planet_lon(planets, 'Moon') if moon_lon is None: moon_lon = self._normalize_degree(body, 'moon_lon', 0) - sun_lon = self._planet_lon(planets, 'Sun') - if sun_lon is None: - sun_lon = self._normalize_degree(body, 'sun_lon', 0) - - nak_size = 360 / 27 - moon_nak_idx = int(moon_lon / nak_size) % 27 - moon_pada = int((moon_lon % nak_size) / (nak_size / 4)) + 1 - moon_sign_idx = int(moon_lon / 30) % 12 - sun_sign_idx = int(sun_lon / 30) % 12 - asc_sign_idx = self._sign_idx_from_value( - body.get('asc_sign_idx', ascendant.get('sign_idx', ascendant.get('sign'))), - 0, - ) - d9_asc_sign_idx = self._sign_idx_from_value(body.get('d9_asc_sign_idx', asc_sign_idx), asc_sign_idx) - tithi_num = self._get_int(body, 'tithi_num', 1, 1, 30) vimshottari_analysis = None canonical_dasha = None + computed = None if dasha_key == 'vimshottari': calculation_service = _load_local_module('domain_calculation_service') canonical_dasha = calculation_service.compute_vimshottari_timeline( @@ -7317,22 +7282,18 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM body.get('today') or body.get('current_date'), ) else: - periods = extended_dashas.calc_any_dasha( - dasha_key, - birth_dt, - moon_nak_idx=moon_nak_idx, - moon_pada=moon_pada, - asc_sign_idx=asc_sign_idx, - moon_sign_idx=moon_sign_idx, - sun_sign_idx=sun_sign_idx, - d9_asc_sign_idx=d9_asc_sign_idx, - tithi_num=tithi_num, - ) - calc_fn = extended_dashas.DASHA_CALCULATORS.get(dasha_key) - precision = 'generic' if not calc_fn or calc_fn.__name__ == 'calc_generic_dasha' else 'calculator' - if not periods: - periods = self._fallback_dasha_periods(birth_dt, dasha_key, info) - precision = 'generic' + try: + computed = dasha_registry.compute_dasha( + dasha_key, + birth_dt=birth_dt, + planets=planets, + ascendant=ascendant or {}, + body=body, + ) + except dasha_registry.DashaNotImplemented as exc: + raise BadRequest(str(exc)) from exc + periods = computed['periods'] + precision = 'engine_module' result = { 'success': True, @@ -7350,6 +7311,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM result['birth_balance'] = canonical_dasha['birth_balance'] result['calculation_contract'] = canonical_dasha['calculation_contract'] result['result_hash'] = canonical_dasha['result_hash'] + if isinstance(computed, dict): + for field in ('algorithm_label', 'algorithm_note', 'profile'): + if computed.get(field): + result[field] = computed[field] return result def _compute_vimshottari_analysis_layer(self, birth_dt, moon_lon, current_date=None): diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 19e1518a..f66aa24a 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -291,7 +291,10 @@ NAKSHATRA_LIST = [ ("Purva Bhadrapada", "Jupiter", 16), ("Uttara Bhadrapada", "Saturn", 19), ("Revati", "Mercury", 17), ] DASHA_ORDER = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"] -DASHA_YEARS = {"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10, "Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17} +try: + from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'] SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座', 'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座', 'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座', 'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'} SIGN_LORDS = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'} @@ -16532,7 +16535,10 @@ def cmd_full_reading(args): try: from ashtottari_dasha import calculate_ashtottari_dasha - ashtottari_data = calculate_ashtottari_dasha(birth_info_for_alt_dasha) + ashtottari_data = calculate_ashtottari_dasha({ + **birth_info_for_alt_dasha, + 'moon_longitude': moon_lon, + }) report['modules']['ashtottari_dasha'] = ashtottari_data except Exception as alt_e: ashtottari_data = {'error': str(alt_e)} diff --git a/scripts/kp_system.py b/scripts/kp_system.py index 09067377..661a9dce 100644 --- a/scripts/kp_system.py +++ b/scripts/kp_system.py @@ -33,8 +33,12 @@ NAKSHATRAS = [ ] # Vimshottari年限(KP系统使用相同比例划分sublord) -VIMSHOTTARI_DURATION = [7, 20, 6, 10, 7, 18, 16, 19, 17] KP_LORDS = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"] +try: + from dasha_common import VIMSHOTTARI_YEARS as _SHARED_VIMSHOTTARI_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_YEARS as _SHARED_VIMSHOTTARI_YEARS +VIMSHOTTARI_DURATION = [_SHARED_VIMSHOTTARI_YEARS[lord] for lord in KP_LORDS] STAR_LORDS = KP_LORDS * 3 # 27 Nakshatras = 3 cycles of 9 lords VIMSHOTTARI_YEARS = dict(zip(KP_LORDS, VIMSHOTTARI_DURATION)) diff --git a/scripts/nakshatra_dasha.py b/scripts/nakshatra_dasha.py index 2dc429a3..3d5eccfc 100644 --- a/scripts/nakshatra_dasha.py +++ b/scripts/nakshatra_dasha.py @@ -305,8 +305,10 @@ def _calc_ashtottari_antardasha(md_lord: str, md_years: float) -> List[Dict]: # ============================================================================ # Vimshottari Dasha 顺序(9星循环) -VIMSHOTTARI_ORDER = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury'] -VIMSHOTTARI_YEARS = {'Ketu': 7, 'Venus': 20, 'Sun': 6, 'Moon': 10, 'Mars': 7, 'Rahu': 18, 'Jupiter': 16, 'Saturn': 19, 'Mercury': 17} +try: + from dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS def calc_nakshatra_dasha_breakdown( diff --git a/scripts/panchottari_dasha.py b/scripts/panchottari_dasha.py index 8e483370..1271d4f9 100644 --- a/scripts/panchottari_dasha.py +++ b/scripts/panchottari_dasha.py @@ -13,8 +13,10 @@ from typing import Any try: from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children + from scripts.dasha_common import nakshatra_seeded_birth_balance except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path from source_bounded_child_periods import expand_parent_seeded_equal_split_children + from dasha_common import nakshatra_seeded_birth_balance YEAR_DAYS = 365.25636 @@ -67,13 +69,9 @@ def _sequence_from(starting_lord: str) -> list[tuple[str, int]]: def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: - moon_longitude = float(moon_longitude) % 360.0 - nakshatra_index = int(moon_longitude // NAKSHATRA_SPAN) - offset = (nakshatra_index - ANURADHA_INDEX) % 27 - starting_lord = DASHA_SEQUENCE[offset % len(DASHA_SEQUENCE)][0] - fraction_elapsed = (moon_longitude % NAKSHATRA_SPAN) / NAKSHATRA_SPAN - remaining_years = _YEARS_BY_LORD[starting_lord] * (1.0 - fraction_elapsed) - return starting_lord, remaining_years, nakshatra_index + return nakshatra_seeded_birth_balance( + moon_longitude, anchor_index=ANURADHA_INDEX, sequence=DASHA_SEQUENCE, + ) def _iso(value: datetime) -> str: diff --git a/scripts/prashna.py b/scripts/prashna.py index 554eb6f0..1b579b6b 100644 --- a/scripts/prashna.py +++ b/scripts/prashna.py @@ -235,8 +235,10 @@ NAK_SPAN = 360.0 / 27.0 # 13.333...° per nakshatra SUB_SPAN = NAK_SPAN / 9.0 # ~1.481° per sub (每个sub由nakshatra lord的一个行星段构成) # Vimshottari年数用于计算sub比例 -VIM_DURATIONS = {'Ketu':7,'Venus':20,'Sun':6,'Moon':10,'Mars':7, - 'Rahu':18,'Jupiter':16,'Saturn':19,'Mercury':17} +try: + from dasha_common import VIMSHOTTARI_YEARS as VIM_DURATIONS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_YEARS as VIM_DURATIONS VIM_TOTAL = 120.0 diff --git a/scripts/satabdika_dasha.py b/scripts/satabdika_dasha.py index 5391b4a3..4210ef43 100644 --- a/scripts/satabdika_dasha.py +++ b/scripts/satabdika_dasha.py @@ -12,8 +12,10 @@ from typing import Any try: from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children + from scripts.dasha_common import nakshatra_seeded_birth_balance except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path from source_bounded_child_periods import expand_parent_seeded_equal_split_children + from dasha_common import nakshatra_seeded_birth_balance YEAR_DAYS = 365.25636 @@ -50,11 +52,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]: def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: - moon = float(moon_longitude) % 360.0 - nakshatra = int(moon // NAKSHATRA_SPAN) - lord = DASHA_SEQUENCE[((nakshatra - REVATI_INDEX) % 27) % len(DASHA_SEQUENCE)][0] - balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN) - return lord, balance, nakshatra + return nakshatra_seeded_birth_balance( + moon_longitude, anchor_index=REVATI_INDEX, sequence=DASHA_SEQUENCE, + ) def calculate_satabdika_dasha(birth_info: dict[str, Any]) -> dict[str, Any]: diff --git a/scripts/shattrimshatsama_dasha.py b/scripts/shattrimshatsama_dasha.py index 2b448bf0..17835131 100644 --- a/scripts/shattrimshatsama_dasha.py +++ b/scripts/shattrimshatsama_dasha.py @@ -6,6 +6,11 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import Any +try: + from scripts.dasha_common import nakshatra_seeded_birth_balance +except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path + from dasha_common import nakshatra_seeded_birth_balance + YEAR_DAYS = 365.25636 NAKSHATRA_SPAN = 360.0 / 27.0 @@ -49,11 +54,9 @@ def _sequence_from(lord: str) -> list[tuple[str, int]]: def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: - moon = float(moon_longitude) % 360.0 - nakshatra = int(moon // NAKSHATRA_SPAN) - lord = DASHA_SEQUENCE[((nakshatra - SRAVANA_INDEX) % 27) % len(DASHA_SEQUENCE)][0] - balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN) - return lord, balance, nakshatra + return nakshatra_seeded_birth_balance( + moon_longitude, anchor_index=SRAVANA_INDEX, sequence=DASHA_SEQUENCE, + ) def calculate_shattrimshatsama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]: diff --git a/scripts/shodashottari_dasha.py b/scripts/shodashottari_dasha.py index ba0131b7..5da4cafe 100644 --- a/scripts/shodashottari_dasha.py +++ b/scripts/shodashottari_dasha.py @@ -14,8 +14,10 @@ from typing import Any try: from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children + from scripts.dasha_common import nakshatra_seeded_birth_balance except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path from source_bounded_child_periods import expand_parent_seeded_equal_split_children + from dasha_common import nakshatra_seeded_birth_balance YEAR_DAYS = 365.25636 @@ -79,13 +81,9 @@ def _sequence_from(starting_lord: str) -> list[tuple[str, int]]: def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: - moon_longitude = float(moon_longitude) % 360.0 - nakshatra_index = int(moon_longitude // NAKSHATRA_SPAN) - offset = (nakshatra_index - PUSHYA_INDEX) % 27 - starting_lord = DASHA_SEQUENCE[offset % len(DASHA_SEQUENCE)][0] - fraction_elapsed = (moon_longitude % NAKSHATRA_SPAN) / NAKSHATRA_SPAN - remaining_years = _YEARS_BY_LORD[starting_lord] * (1.0 - fraction_elapsed) - return starting_lord, remaining_years, nakshatra_index + return nakshatra_seeded_birth_balance( + moon_longitude, anchor_index=PUSHYA_INDEX, sequence=DASHA_SEQUENCE, + ) def _iso(value: datetime) -> str: diff --git a/scripts/tajika.py b/scripts/tajika.py index 05df3e6f..10cbe3e0 100644 --- a/scripts/tajika.py +++ b/scripts/tajika.py @@ -26,6 +26,13 @@ import logging logger = logging.getLogger(__name__) +try: + from dasha_common import VIMSHOTTARI_ORDER as _VIMSHOTTARI_ORDER + from dasha_common import VIMSHOTTARI_YEARS as _VIMSHOTTARI_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_ORDER as _VIMSHOTTARI_ORDER + from scripts.dasha_common import VIMSHOTTARI_YEARS as _VIMSHOTTARI_YEARS + SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo', 'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'] SIGN_LORDS = {'Aries':'Mars','Taurus':'Venus','Gemini':'Mercury','Cancer':'Moon', @@ -342,8 +349,8 @@ def calc_mudda_dasha(asc_sign_idx: int, 规则: 从年度守护星开始,按Vimshottari顺序排列 每个大运按比例分配12个月 """ - DASHA_ORDER = ["Ketu","Venus","Sun","Moon","Mars","Rahu","Jupiter","Saturn","Mercury"] - DASHA_YEARS = {"Ketu":7,"Venus":20,"Sun":6,"Moon":10,"Mars":7,"Rahu":18,"Jupiter":16,"Saturn":19,"Mercury":17} + DASHA_ORDER = list(_VIMSHOTTARI_ORDER) + DASHA_YEARS = _VIMSHOTTARI_YEARS start_idx = DASHA_ORDER.index(varsha_lord) if varsha_lord in DASHA_ORDER else 0 @@ -395,8 +402,8 @@ def calc_mudda_with_dasha_balance( producers rather than treating a different balance source as a label. """ - dasha_order = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"] - dasha_years = {"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10, "Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17} + dasha_order = list(_VIMSHOTTARI_ORDER) + dasha_years = _VIMSHOTTARI_YEARS valid_profiles = {"moon_at_birth_time", "moon_in_varshaphala"} if not isinstance(annual_start, datetime): return {"status": "blocked", "reason": "true_solar_return_datetime_required_for_mudda_balance", "balance_profile": balance_profile} diff --git a/scripts/validate.py b/scripts/validate.py index 4a5a66a5..1917aa4c 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -24,10 +24,10 @@ import math SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'] -DASHA_YEARS = { - "Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10, - "Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17 -} +try: + from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS EXPECTED_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'] diff --git a/scripts/vimshottari_subperiod_timeline.py b/scripts/vimshottari_subperiod_timeline.py index 0dcb09bc..4fefd6ed 100644 --- a/scripts/vimshottari_subperiod_timeline.py +++ b/scripts/vimshottari_subperiod_timeline.py @@ -13,8 +13,12 @@ from typing import Any ROOT = Path(__file__).resolve().parents[1] -DASHA_ORDER = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"] -DASHA_YEARS = {"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10, "Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17} +try: + from dasha_common import VIMSHOTTARI_ORDER as DASHA_ORDER + from dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS +except ModuleNotFoundError: # pragma: no cover - package import + from scripts.dasha_common import VIMSHOTTARI_ORDER as DASHA_ORDER + from scripts.dasha_common import VIMSHOTTARI_YEARS as DASHA_YEARS def _parse_date(value: str) -> datetime: diff --git a/tests/run_all.py b/tests/run_all.py index 32d1522b..a7296594 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -68,11 +68,22 @@ def t16(): from yoga_expansion import detect_all_yogas @test("birth_time_rectifier import") def t17(): from birth_time_rectifier import check_lagna_boundary, get_effective_accuracy -@test("conditional_dashas import") -def t18(): from conditional_dashas import calc_dwisaptati_dasha, check_dwisaptati_condition +@test("conditional dasha modules import") +def t18(): + # 原值:from conditional_dashas import calc_dwisaptati_dasha, check_dwisaptati_condition + # 新值:导入独立模块的 calculate_* + # 原因:conditional_dashas.py 是占位副本,已删除 + from dwisaptatisama_dasha import calculate_dwisaptatisama_dasha + from dwadashottari_dasha import calculate_dwadashottari_dasha + assert callable(calculate_dwisaptatisama_dasha) and callable(calculate_dwadashottari_dasha) -@test("extended_dashas import") -def t19(): from extended_dashas import get_available_dashas, DASHA_REGISTRY +@test("dasha registry import") +def t19(): + # 原值:from extended_dashas import get_available_dashas, DASHA_REGISTRY + # 新值:dasha_registry.DASHA_REGISTRY_METADATA + # 原因:extended_dashas.py 占位注册表已删除,元数据原样搬到新模块 + from dasha_registry import DASHA_REGISTRY_METADATA, available_dasha_rows + assert len(DASHA_REGISTRY_METADATA) == len(available_dasha_rows()) @test("chart_renderer import") def t20(): from chart_renderer import render_south_indian_chart, render_html_report @@ -126,8 +137,11 @@ def t27(): @test("Dasha count") def t28(): - from extended_dashas import get_available_dashas - assert len(get_available_dashas()) >= 30, "Dasha count should be >= 30" + # 原值:extended_dashas.get_available_dashas() >= 30 + # 新值:DASHA_REGISTRY_METADATA 键数 >= 30 + # 原因:清单改读新注册表,35 个键和顺序未改 + from dasha_registry import DASHA_REGISTRY_METADATA + assert len(DASHA_REGISTRY_METADATA) >= 30, "Dasha count should be >= 30" @test("Rectifier boundary check") def t29(): @@ -216,21 +230,40 @@ def t40(): @test("Conditional Dasha trigger") def t41(): - from conditional_dashas import check_dwisaptati_condition - assert check_dwisaptati_condition('Sun', 1) == True + # 原值:check_dwisaptati_condition('Sun', 1) == True + # 新值:dwisaptatisama_applicability 在命主落 1 宫时 applicable 为 True + # 原因:占位条件函数已删除,独立模块用同一条「1 或 7 宫」规则 + from dwisaptatisama_dasha import dwisaptatisama_applicability + result = dwisaptatisama_applicability(ascendant_longitude=0.0, lagna_lord_sign_index=0) + assert result["applicable"] is True + assert result["lagna_lord_house"] == 1 @test("Conditional Dasha calc") def t42(): - from conditional_dashas import calc_dwisaptati_dasha + # 原值:calc_dwisaptati_dasha(...) 返回长度为 8 的占位列表 + # 新值:标准出表档下 major 非空,且每段带 lord / years / start_date + # 原因:占位函数已删除,改为独立模块的真实行 from datetime import datetime - r = calc_dwisaptati_dasha(datetime(1990,6,15), 'Sun', 15.5) - assert len(r) == 8 + from dwisaptatisama_dasha import calculate_dwisaptatisama_dasha + result = calculate_dwisaptatisama_dasha({ + "birth_datetime": datetime(1990, 6, 15), + "ascendant_longitude": 0.0, + "lagna_lord_sign_index": 0, + "moon_longitude": 15.5, + "standard_table_profile": "pl9_reports_all_dasha_tables_v1", + }) + rows = result["major"] + assert rows + assert {"lord", "years", "start_date"} <= set(rows[0]) @test("Extended Dasha registry") def t43(): - from extended_dashas import get_dasha_info, get_available_dashas - assert get_dasha_info('kalachakra')['type'] == 'nakshatra' - assert len(get_available_dashas()) >= 35 + # 原值:get_dasha_info('kalachakra')['type'] == 'nakshatra' 且可用键 >= 35 + # 新值:元数据里 kalachakra 的 type 仍是 nakshatra,键数 >= 35 + # 原因:元数据原样搬迁 + from dasha_registry import DASHA_REGISTRY_METADATA + assert DASHA_REGISTRY_METADATA['kalachakra']['type'] == 'nakshatra' + assert len(DASHA_REGISTRY_METADATA) >= 35 @test("Yoga expansion detect") def t44(): @@ -399,17 +432,34 @@ def t60(): @test("Yogini Dasha 8 periods") def t61(): - from extended_dashas import calc_yogini_dasha + # 原值:占位 calc_yogini_dasha 返回 8 段,lord 为女神名 + # 新值:引擎模块 major 至少 8 段,且 yogini 名属于八位女神 + # 原因:接口与全量解读改为同一模块,模块会按周期重复排 from datetime import datetime - r = calc_yogini_dasha(datetime(1990,6,15), 5) - assert len(r) == 8 + from yogini_dasha import calculate_yogini_dasha + result = calculate_yogini_dasha({ + "birth_datetime": datetime(1990, 6, 15), + "moon_longitude": 5 * (360 / 27), + "as_of": datetime(2026, 1, 1), + }) + names = {row["yogini"] for row in result["major"]} + assert len(result["major"]) >= 8 + assert names <= {"Mangala", "Pingala", "Dhanya", "Bhramari", "Bhadrika", "Ulka", "Siddha", "Sankata"} + assert len(names) == 8 @test("Kalachakra Dasha") def t62(): - from extended_dashas import calc_kalachakra_dasha + # 原值:占位 calc_kalachakra_dasha 返回至少 8 段 + # 新值:引擎模块 major 至少 8 段 + # 原因:改为全量解读使用的同一函数 from datetime import datetime - r = calc_kalachakra_dasha(datetime(1990,6,15), 5, 1) - assert len(r) >= 8 + from kalachakra_dasha import calculate_kalachakra_dasha + result = calculate_kalachakra_dasha({ + "birth_datetime": datetime(1990, 6, 15), + "moon_nakshatra_index": 5, + "moon_pada": 1, + }) + assert len(result["major"]) >= 8 # ── KP system tests ── @test("KP sublord calculation") @@ -621,16 +671,28 @@ def t89(): # ── Multiple Dasha co-existence tests ── @test("Dasha registry entries unique") def t90(): - from extended_dashas import DASHA_REGISTRY, DASHA_CALCULATORS - assert len(DASHA_REGISTRY) == 35 - assert len(DASHA_CALCULATORS) >= 32 + # 原值:DASHA_REGISTRY == 35 且 DASHA_CALCULATORS >= 32 + # 新值:元数据仍是 35;占位计算器字典删除 + # 原因:那 32 个里大部分是不读盘的占位函数 + from dasha_registry import DASHA_REGISTRY_METADATA + assert len(DASHA_REGISTRY_METADATA) == 35 + assert len({row["name"] for row in DASHA_REGISTRY_METADATA.values()}) == 35 @test("Generic Dasha produces results") def t91(): - from extended_dashas import calc_any_dasha + # 原值:calc_any_dasha('shodasottari') 占位结果长度 >= 8 + # 新值:独立模块在标准出表档下 major >= 8 + # 原因:占位通用函数已删除 from datetime import datetime - r = calc_any_dasha('shodasottari', datetime(1990,6,15), moon_nak_idx=0) - assert len(r) >= 8 + from shodashottari_dasha import calculate_shodashottari_dasha + result = calculate_shodashottari_dasha({ + "birth_datetime": datetime(1990, 6, 15), + "ascendant_longitude": 10.0, + "sun_longitude": 80.0, + "moon_longitude": 15.5, + "standard_table_profile": "pl9_reports_all_dasha_tables_v1", + }) + assert len(result["major"]) >= 8 # ── Tajika tests ── @test("Tajika detection with close planets") diff --git a/tests/test_api_server_growth_contract.py b/tests/test_api_server_growth_contract.py index 4209f1c9..4350b81a 100644 --- a/tests/test_api_server_growth_contract.py +++ b/tests/test_api_server_growth_contract.py @@ -30,7 +30,10 @@ JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300 # TASK-freeze-metric-change-20260915 §1. Re-measured 2026-09-16 after # TASK-api-server-backdoor-close-20260916 lifted 7 compute-only methods into # scripts/offline_compute_mixins.py (225 -> 218). -JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 218 +# 原值:218(删占位回退之前) +# 新值:217 +# 原因:BUG-1341 删除 JyotishAPIHandler._fallback_dasha_periods,类方法少 1 个,没有新增方法 +JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 217 # `JyotishAPIHandler.__new__` in scripts/ and tests/ `*.py`, excluding this file. # Re-measured 2026-09-16 after TASK-api-server-backdoor-close-20260916: diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index cf1650ed..03e59585 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -1000,9 +1000,12 @@ def test_dasha_system_returns_periods_for_valid_request() -> None: }) assert result['success'] is True assert result['key'] == 'yogini' - assert result['precision'] == 'calculator' + # 原值:precision == 'calculator',periods[0] 含 lord/start/end,lord 为女神名 + # 新值:precision == 'engine_module',periods 与 yogini 模块的 major 同行(yogini/planet/start_date/end_date/years) + # 原因:/api/dasha 改为返回引擎模块主运,不再标成计算器占位 + assert result['precision'] == 'engine_module' assert result['periods'] - assert result['periods'][0]['lord'] in { + assert result['periods'][0]['yogini'] in { 'Mangala', 'Pingala', 'Dhanya', @@ -1012,7 +1015,7 @@ def test_dasha_system_returns_periods_for_valid_request() -> None: 'Siddha', 'Sankata', } - assert {'lord', 'start', 'end', 'years'} <= set(result['periods'][0]) + assert {'yogini', 'planet', 'start_date', 'end_date', 'years'} <= set(result['periods'][0]) assert 'vimshottari_analysis' not in result diff --git a/tests/test_dasha_api_engine_parity.py b/tests/test_dasha_api_engine_parity.py new file mode 100644 index 00000000..7bda9ff4 --- /dev/null +++ b/tests/test_dasha_api_engine_parity.py @@ -0,0 +1,423 @@ +"""API dasha periods must match the full-reading modules (BUG-1341, BUG-1344). + +fixtureProvenance: + fictional chart from docs/tasks/TASK-dasha-unify-20261013.md section 1 + public charts already stored in tests/smoke_test_runner.py (Elvis Presley) + and tests/verify_jyotish_v6_1.py (Britney Spears, UT clock, tz 0) + PyJHora 4.8.7: drik.set_ayanamsa_mode("LAHIRI"); + graha get_dhasa_bhukthi(dob, tob, place, dhasa_level_index=1); + ashtottari get_ashtottari_dhasa_bhukthi(julian_day_number(dob, tob), place, dhasa_level_index=1) +""" + +from __future__ import annotations + +import importlib.util +import os +import random +import re +import sys +from datetime import date, datetime +from functools import lru_cache +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +os.environ.setdefault("VEDASTRO_ENABLE_NETWORK", "0") + +FICTION = { + "year": 1990, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "second": 0, + "lat": 28.6139, + "lon": 77.2090, + "tz": 5.5, + "ayanamsa": "lahiri", + "node_mode": "mean", + "today": "2026-01-01", +} + +# Registry key -> full-reading module id. These are the systems T1 compares. +READING_KEYS = ( + ("panchottari", "panchottari_dasha"), + ("satabdika", "satabdika_dasha"), + ("chaturaaseeti", "chaturshitisama_dasha"), + ("shasti_hayani", "shastihayani_dasha"), + ("shodasottari", "shodashottari_dasha"), + ("dwadashottari", "dwadashottari_dasha"), + ("dwisaptati", "dwisaptatisama_dasha"), + ("shattrimsa", "shattrimshatsama_dasha"), + ("yogini", "yogini_dasha"), + ("ashtottari", "ashtottari_dasha"), + ("kalachakra", "kalachakra_dasha"), + ("narayana", "narayana_dasha"), +) + +# Anchors copied from the six modules before they shared one helper. +BALANCE_ANCHORS = ( + ("panchottari_dasha", "ANURADHA_INDEX"), + ("satabdika_dasha", "REVATI_INDEX"), + ("chaturshitisama_dasha", "SWATI_INDEX"), + ("dwisaptatisama_dasha", "MULA_INDEX"), + ("shattrimshatsama_dasha", "SRAVANA_INDEX"), + ("shodashottari_dasha", "PUSHYA_INDEX"), +) + +FROZEN_YEAR_TABLES = {"dasha_analyzer.py", "divisional_charts_extended.py"} +YEAR_TABLE = re.compile( + r"""['"]Ketu['"]\s*:\s*7\b.*?['"]Venus['"]\s*:\s*20\b.*?['"]Mercury['"]\s*:\s*17\b""", + re.DOTALL, +) +PYJHORA_LORDS = { + 0: "Sun", + 1: "Moon", + 2: "Mars", + 3: "Mercury", + 4: "Jupiter", + 5: "Venus", + 6: "Saturn", + 7: "Rahu", + 8: "Ketu", +} + + +def _handler(): + path = ROOT / "tests" / "test_api_server_security.py" + spec = importlib.util.spec_from_file_location("dasha_parity_security_helpers", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module._handler() + + +def _drop_injected_repo_paths() -> None: + """PyJHora inserts the main checkout onto sys.path. Keep this worktree.""" + banned = { + os.path.normcase(str(ROOT.parent)), + os.path.normcase(str(ROOT.parent.parent)), + } + sys.path[:] = [ + item + for item in sys.path + if not item or os.path.normcase(os.path.abspath(item)) not in banned + ] + scripts = str(SCRIPTS) + if scripts not in sys.path: + sys.path.insert(0, scripts) + + +def _major_rows(payload): + if not isinstance(payload, dict): + return [] + if isinstance(payload.get("major"), list): + return payload["major"] + if isinstance(payload.get("mahadasha_sequence"), list): + return payload["mahadasha_sequence"] + if isinstance(payload.get("periods"), list): + return payload["periods"] + return [] + + +def _chart_body(report: dict, dasha: str) -> dict: + chart = report["chart"] + planets = { + name: {"lon": row["lon"]} + for name, row in chart["planets"].items() + if isinstance(row, dict) and isinstance(row.get("lon"), (int, float)) + } + return { + "dasha": dasha, + "year": FICTION["year"], + "month": FICTION["month"], + "day": FICTION["day"], + "hour": FICTION["hour"], + "minute": FICTION["minute"], + "lat": FICTION["lat"], + "lon": FICTION["lon"], + "tz": FICTION["tz"], + "today": FICTION["today"], + "planets": planets, + "ascendant": {"lon": chart["ascendant"]["lon"]}, + } + + +@lru_cache(maxsize=1) +def _fiction_reading() -> dict: + from scripts.jyotish_engine import cmd_full_reading + from scripts.professional_report_reference import _export_args + + return cmd_full_reading(_export_args(dict(FICTION))) + + +def _legacy_birth_balance(moon_longitude, anchor_index, sequence): + """Source copy of the six helpers before they called dasha_common. + + Operation order matches the deleted bodies: modulo, nakshatra index, + offset, lord from the sequence, fraction, then years times (1 - fraction). + """ + moon_longitude = float(moon_longitude) % 360.0 + span = 360.0 / 27.0 + nakshatra_index = int(moon_longitude // span) + offset = (nakshatra_index - anchor_index) % 27 + starting_lord = sequence[offset % len(sequence)][0] + years_by_lord = dict(sequence) + fraction_elapsed = (moon_longitude % span) / span + remaining_years = years_by_lord[starting_lord] * (1.0 - fraction_elapsed) + return starting_lord, remaining_years, nakshatra_index + + +def _as_date(value) -> date: + if isinstance(value, datetime): + return value.date() + if isinstance(value, date): + return value + text = str(value) + return datetime.fromisoformat(text[:19]).date() + + +def _period_covering(majors, moment: datetime): + first = datetime.fromisoformat(majors[0]["start_date"]) + last = datetime.fromisoformat(majors[-1]["end_date"]) + span = last - first + if span.total_seconds() <= 0 or moment < first: + return None + cycles = int((moment - first).total_seconds() // span.total_seconds()) + for period in majors: + start = datetime.fromisoformat(period["start_date"]) + cycles * span + end = datetime.fromisoformat(period["end_date"]) + cycles * span + if start <= moment < end: + return period["planet"], start, end + return None + + +def test_api_major_periods_match_full_reading_modules() -> None: + report = _fiction_reading() + modules = report["modules"] + handler = _handler() + for key, module_id in READING_KEYS: + engine_rows = _major_rows(modules[module_id]) + result = handler._compute_dasha_system(_chart_body(report, key)) + assert result["precision"] == "engine_module", key + assert result["precision"] not in {"generic", "calculator"} + assert result["periods"] == engine_rows, key + + +def test_unimplemented_dasha_keys_are_rejected() -> None: + handler = _handler() + body = _chart_body(_fiction_reading(), "tara") + from scripts.jyotish_api_server import BadRequest + + with pytest.raises(BadRequest, match="dasha system not implemented"): + handler._compute_dasha_system(body) + body["dasha"] = "chara" + with pytest.raises(BadRequest, match=r"/api/dasha/chara"): + handler._compute_dasha_system(body) + source = (SCRIPTS / "jyotish_api_server.py").read_text(encoding="utf-8") + assert "except BadRequest as e:" in source + assert "_error_json(str(e), 400, 'ERR_BAD_REQUEST')" in source + + +def test_narayana_keeps_the_legacy_label() -> None: + result = _handler()._compute_dasha_system(_chart_body(_fiction_reading(), "narayana")) + assert "旧算法" in result["algorithm_label"] + assert result["profile"] == "legacy_reference_only" + assert "Sanjay Rath" not in result["algorithm_label"] + + +def test_shoola_uses_the_native_rashi_section() -> None: + report = _fiction_reading() + from scripts.rashi_dashas_native import calculate_rashi_dashas + + chart = report["chart"] + planet_lons = {name: row["lon"] for name, row in chart["planets"].items()} + expected = calculate_rashi_dashas( + date(1990, 1, 1), + chart["ascendant"]["lon"], + planet_lons, + )["shoola"]["periods"] + result = _handler()._compute_dasha_system(_chart_body(report, "shoola")) + assert result["precision"] == "engine_module" + assert result["periods"] == expected + + +def test_birth_balance_matches_the_old_formula() -> None: + from scripts.dasha_common import nakshatra_seeded_birth_balance + + rng = random.Random(1341) + samples = [rng.uniform(-40.0, 400.0) for _ in range(2000)] + for module_name, anchor_name in BALANCE_ANCHORS: + module = importlib.import_module(f"scripts.{module_name}") + sequence = module.DASHA_SEQUENCE + anchor = getattr(module, anchor_name) + for moon in samples: + assert module._birth_lord_and_balance(moon) == nakshatra_seeded_birth_balance( + moon, anchor_index=anchor, sequence=sequence + ) + assert nakshatra_seeded_birth_balance( + moon, anchor_index=anchor, sequence=sequence + ) == _legacy_birth_balance(moon, anchor, sequence) + + +def test_vimshottari_year_literals_stay_at_the_shared_table() -> None: + matches = sorted( + path.name + for path in SCRIPTS.glob("*.py") + if path.name not in FROZEN_YEAR_TABLES and YEAR_TABLE.search(path.read_text(encoding="utf-8")) + ) + assert matches == ["dasha_common.py"] + frozen = SCRIPTS / "dasha_analyzer.py" + assert YEAR_TABLE.search(frozen.read_text(encoding="utf-8")) + + +def _pyjhora_rows(module_name: str, birth: dict): + pytest.importorskip("jhora") + _drop_injected_repo_paths() + from jhora import utils + from jhora.panchanga import drik + + drik.set_ayanamsa_mode("LAHIRI") + place = drik.Place("f", birth["lat"], birth["lon"], birth["tz"]) + dob = drik.Date(birth["year"], birth["month"], birth["day"]) + tob = (birth["hour"], birth["minute"], int(birth.get("second", 0))) + if module_name == "ashtottari": + from jhora.horoscope.dhasa.graha.ashtottari import get_ashtottari_dhasa_bhukthi + + jd = utils.julian_day_number((birth["year"], birth["month"], birth["day"]), tob) + return get_ashtottari_dhasa_bhukthi(jd, place, dhasa_level_index=1) + module = importlib.import_module(f"jhora.horoscope.dhasa.graha.{module_name}") + return module.get_dhasa_bhukthi(dob, tob, place, dhasa_level_index=1) + + +def _assert_same_boundaries(engine_rows, py_rows, birth: datetime, slack_days: int) -> None: + engine = [(_row_lord(row), _as_date(row.get("start_date"))) for row in engine_rows] + oracle = [(PYJHORA_LORDS[row[0][0]], date(*row[1][:3])) for row in py_rows] + assert engine[0][0] == oracle[0][0] + birth_day = birth.date() + if engine[0][1] == birth_day and oracle[0][1] < birth_day: + pairs = zip(engine[1:], oracle[1:]) + limit = 0 + else: + pairs = zip(engine, oracle) + limit = slack_days + compared = 0 + for (engine_lord, engine_start), (oracle_lord, oracle_start) in pairs: + assert engine_lord == oracle_lord + assert abs((engine_start - oracle_start).days) <= limit + compared += 1 + assert compared >= 3 + + +def _row_lord(row: dict) -> str: + return row.get("lord") or row.get("planet") + + +def test_conditional_tables_match_pyjhora_start_days() -> None: + from scripts.chaturshitisama_dasha import calculate_chaturshitisama_dasha + from scripts.dasha_registry import build_reading_birth_info + from scripts.domain_calculation_service import compute_chart + from scripts.panchottari_dasha import calculate_panchottari_dasha + from scripts.satabdika_dasha import calculate_satabdika_dasha + from scripts.source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE + + chart = compute_chart({**FICTION, "ayanamsa": "lahiri"}) + birth = datetime(1990, 1, 1, 12, 0) + _alt, conditional, _lons = build_reading_birth_info( + birth, + chart["planets"], + chart["ascendant"], + {"lat": FICTION["lat"], "lon": FICTION["lon"], "tz": FICTION["tz"], "year": 1990}, + ) + info = { + **conditional, + "child_period_profile": PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE, + "standard_table_profile": "pl9_reports_all_dasha_tables_v1", + } + calls = ( + ("panchottari", calculate_panchottari_dasha), + ("sataatbika", calculate_satabdika_dasha), + ("chathuraaseethi_sama", calculate_chaturshitisama_dasha), + ) + for module_name, calculator in calls: + rows = calculator(info)["major"] + assert rows + _assert_same_boundaries(rows, _pyjhora_rows(module_name, FICTION), birth, 0) + + +def _public_case(name: str, birth: dict, family: str) -> None: + from scripts.ashtottari_dasha import calculate_ashtottari_dasha + from scripts.ashtottari_rule_profiles import ( + RULE_FAMILY_MOON_NAKSHATRA_PLUS_PAKSHA, + RULE_FAMILY_RAHU_FROM_LAGNA_LORD, + ) + from scripts.dasha_registry import build_reading_birth_info + from scripts.domain_calculation_service import compute_chart + + families = { + "moon": RULE_FAMILY_MOON_NAKSHATRA_PLUS_PAKSHA, + "rahu": RULE_FAMILY_RAHU_FROM_LAGNA_LORD, + } + chart = compute_chart({**birth, "second": 0, "node_mode": "mean", "ayanamsa": "lahiri"}) + born = datetime(birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"]) + alt, conditional, _lons = build_reading_birth_info( + born, + chart["planets"], + chart["ascendant"], + {"lat": birth["lat"], "lon": birth["lon"], "tz": birth["tz"], "year": birth["year"]}, + ) + result = calculate_ashtottari_dasha({ + **alt, + "moon_longitude": conditional["moon_longitude"], + "birth_is_daytime": conditional["birth_is_daytime"], + "rahu_sign_index": conditional["rahu_sign_index"], + "lagna_lord_sign_index": conditional["lagna_lord_sign_index"], + "applicability_rule_family": families[family], + }) + assert result["applicable"] is True, name + majors = result["major"] + start = datetime.fromisoformat(majors[0]["start_date"]) + end = datetime.fromisoformat(majors[0]["end_date"]) + assert start <= born < end + now = datetime.now() + covered = _period_covering(majors, now) + assert covered is not None, name + current = result["current"] + assert current["planet"] == covered[0] + assert datetime.fromisoformat(current["start_date"]) == covered[1] + assert datetime.fromisoformat(current["end_date"]) == covered[2] + _assert_same_boundaries(majors, _pyjhora_rows("ashtottari", birth), born, 1) + + +def test_ashtottari_birth_balance_matches_pyjhora_within_one_day() -> None: + fiction = {key: FICTION[key] for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")} + presley = { + "year": 1935, + "month": 1, + "day": 8, + "hour": 4, + "minute": 35, + "lat": 34.3, + "lon": -88.4, + "tz": -6.0, + } + spears = { + "year": 1981, + "month": 12, + "day": 2, + "hour": 19, + "minute": 55, + "lat": 31.17, + "lon": -89.18, + "tz": 0.0, + } + _public_case("fiction", fiction, "moon") + _public_case("presley", presley, "rahu") + _public_case("spears", spears, "moon") + + +def test_placeholder_modules_are_gone() -> None: + assert not (SCRIPTS / "extended_dashas.py").exists() + assert not (SCRIPTS / "conditional_dashas.py").exists() + assert not (ROOT / "skills" / "jyotish-vedic-astrology" / "scripts" / "extended_dashas.py").exists() diff --git a/tests/test_dasha_year_length_alignment.py b/tests/test_dasha_year_length_alignment.py index 525dd043..dfda6400 100644 --- a/tests/test_dasha_year_length_alignment.py +++ b/tests/test_dasha_year_length_alignment.py @@ -9,14 +9,12 @@ from __future__ import annotations import importlib.util import os import sys -from datetime import datetime, timedelta +from datetime import datetime SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts") if SCRIPTS not in sys.path: sys.path.insert(0, SCRIPTS) -from dasha_analyzer import build_dasha_timeline, lon_to_nakshatra # noqa: E402 - def _api_handler(): """Reuse the existing unbound handler helper. Do not add another forgery site.""" @@ -27,28 +25,21 @@ def _api_handler(): return module._handler() -def test_fallback_timeline_matches_analyzer_to_the_day() -> None: - birth = datetime(2000, 1, 1) - nak_info, progress, _pada = lon_to_nakshatra(0.0) - timeline, _elapsed, _remaining, _lord = build_dasha_timeline("2000-01-01", nak_info, progress) - handler = _api_handler() - years = timeline[0]["years"] - periods = handler._fallback_dasha_periods(birth, "generic", {"years": years * 9}) - assert periods[0]["lord"] == timeline[0]["lord"] - assert periods[0]["start"] == timeline[0]["start"].strftime("%Y-%m-%d") - assert periods[0]["end"] == timeline[0]["end"].strftime("%Y-%m-%d") - fallback_days = ( - datetime.strptime(periods[0]["end"], "%Y-%m-%d") - - datetime.strptime(periods[0]["start"], "%Y-%m-%d") - ).days - analyzer_days = (timeline[0]["end"].date() - timeline[0]["start"].date()).days - assert fallback_days - analyzer_days == 0 +def test_placeholder_fallback_periods_are_gone() -> None: + """原值:_fallback_dasha_periods 用 365.25 日对齐分析器。 - long_periods = handler._fallback_dasha_periods(birth, "generic", {"years": 1422}) - expected = (birth + timedelta(days=158 * 365.25)).strftime("%Y-%m-%d") - stale = (birth + timedelta(days=158 * 365.25636)).strftime("%Y-%m-%d") - assert expected != stale - assert long_periods[0]["end"] == expected + 新值:该方法不存在。 + 原因:占位回退会编造周期,BUG-1341 要求删掉;仍在用的年长对齐由下一条 years_into 测试锁住。 + """ + handler = _api_handler() + assert not hasattr(handler, "_fallback_dasha_periods") + source = open( + os.path.join(os.path.dirname(__file__), "..", "scripts", "jyotish_api_server.py"), + encoding="utf-8", + ).read() + assert "_fallback_dasha_periods" not in source + assert "precision = 'generic'" not in source + assert "precision = 'calculator'" not in source def test_years_into_mahadasha_uses_the_analyzer_year() -> None: