diff --git a/CHANGELOG.md b/CHANGELOG.md index eedd976e..04301aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 印度占星 Skill 更新日志 +## 2026-10-09 — 完整数据版补上六种星座大运(未合入,未上线) + +- 完整数据版增加 Sthira、Niryaana Shoola、Drig、Navamsha、Shoola、Chara。每节写本站选用的方法和书名页码,并写明未与外部软件逐一核对。 +- Chara 用已有的 K.N. Rao 算法,单独一节,与 Narayana 分开。 +- Lagna Kendradi 没有选定的安全默认算法,报告写明不列出,不放空表。 +- 这七种不进对话卡,不进生时校正打分,不升算法身份。不改数据库。Skill 版本不变。未推送,未合入,未上线。 + ## 2026-10-08 — 完整数据版报告合入 staging:中英两版、对话年运卡不受影响(未上线) - 「生成报告」只出完整数据版,中文与英文各一份;以前生成的阅读版报告仍按当时的正文只读打开。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 0226fff8..bae4e55c 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17294,3 +17294,35 @@ - 相关记录:BUG-1272、BUG-1274、BUG-1276~1281。 - 复发自:无。 - 修复版本:分支补修提交(见 `git log`)。 + +## BUG-1283 | 旧 Niryaana Shoola 用回归年日、平手就停,报告仍可能把它当成现行算法 + +- 状态:mitigated +- 首次发现 / 最近更新:2026-10-09 / 2026-10-09 +- 来源:`TASK-rashi-dashas-native-20261008`。 +- 影响面:`scripts/niryaana_shoola_dasha.py`。完整数据版报告不再调用它。 +- 现象:旧函数按 365.25636 日一年展开,上升与第 7 宫力相等就停,并且没有日历月的子运。所选 Goel 口径是 9 个日历年,平手时用宫主的 Charakaraka 继续选择。 +- 触发条件:若报告或新模块再调用 `calculate_niryaana_shoola_dasha`。 +- 根因:该文件早于本单的出处档案。它只被自己的旧测试调用。 +- 修复:报告改走 `scripts/rashi_dashas_native.py` 的 `goel_nirayana_shoola_v1`。旧函数不改,避免打断旧测试。 +- 验证:`tests/test_rashi_dashas_native.py` 用语法树确认三个新模块不导入 `niryaana_shoola_dasha`。`tests/test_niryaana_shoola_dasha.py` 仍过。 +- 防复发:上述导入断言。新报告路径不得再调用旧函数。 +- 相关记录:BUG-1284。 +- 复发自:无。 +- 修复版本:分支 `codex/rashi-dashas-native-20261008`,未合入,未上线。 + +## BUG-1284 | 旧 Shoola 从月亮星座起并且逐年缩短,注册表仍留着 + +- 状态:mitigated +- 首次发现 / 最近更新:2026-10-09 / 2026-10-09 +- 来源:`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`。注册表和接口方法体不动。 +- 验证:三个新模块的语法树不导入 `extended_dashas`。定向 pytest 退出码 0。 +- 防复发:上述导入断言。报告章不得再调用 `calc_shoola_dasha`。 +- 相关记录:BUG-1283。 +- 复发自:无。 +- 修复版本:分支 `codex/rashi-dashas-native-20261008`,未合入,未上线。 diff --git a/docs/tasks/PROGRESS-rashi-dashas-native-20261008.md b/docs/tasks/PROGRESS-rashi-dashas-native-20261008.md new file mode 100644 index 00000000..2c56ce4b --- /dev/null +++ b/docs/tasks/PROGRESS-rashi-dashas-native-20261008.md @@ -0,0 +1,127 @@ +# 进度 · 报告补 7 种星座大运(2026-10-09) + +- 分支:`codex/rashi-dashas-native-20261008` +- 基线:`e1c7383c`(当时的 `origin/staging`,已含章动 BUG-1275 与完整数据版)。本分支第一行是 `## codex/rashi-dashas-native-20261008`,不跟踪 `origin/staging`。 +- 状态:代码在本分支,**未推送**,**未合入**,未上线。等验收。 +- 上游:`G:\Ferti\yinduzhanxing` 的 `origin/main` 本轮核对为 `0337b77fce353252f7d115025c8703c3aec4204a`。`scripts/drig_dasha.py` 的 blob 是 `b80cf089e2e7901f8b5cdbe989341309b4fee0f4`。`23be1807` 不当参照。没有使用上游本地脏 HEAD。 +- 生成器:`C:\Users\74082\anaconda3\python.exe`(3.11)装有 PyJHora 4.8.7,已用来生成 `tests/fixtures/rashi_dasha_pyjhora_20261008.json`。这不是无 jhora 环境。产品模块运行时不导入 jhora。pytest 只读这份 JSON。 +- 本机不能声称 Linux 全量 ⊆ 基线、快速门通过或浏览器走查。已知 Windows 失败 `tests/test_consultation_native_layers.py::test_the_new_layer_leaves_every_existing_output_unchanged` 不是发布依据。 + +## 开工预检 + +`python scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 退出码 **1**。`fragment_scan` 在 90 秒超时,`focused_tests` 在 45 秒超时。超时退出不算通过。本轮没有把这次预检改写成通过。 + +## 结论 + +六种大运进入完整数据版,表头写档案名、书名页码,并写「未与外部软件逐一核对」。Lagna Kendradi **不上报告**,用一句缺席说明代替空表。不进对话卡,不进生时校正打分,不升算法身份,不进大运解释包。起始星座、方向、年数由本仓盘面算出。与 PyJHora 不一致的地方没有调参。 + +中文标题故意不带 `Dasha`。完整数据版清理器会把这个词换成「大运」,再删掉它和下一句中文之间的换行。英文标题保留 Dasha。报告正文不写英文单词 blocked。 + +`source_bounded_child_periods.py` 在 `0337b77f` 上有 3 处 jhora 字样,都是档案名字符串,不是 `import`。没有把运行时导入移植过来。 + +没有整文件拷贝上游展开器,也没有把书籍原文拷进本仓。没有重跑上游同盘整篇导出做逐字节对照。上游展开器自己不选起始星座,同盘导出里的表取决于当时填进去的输入。 + +## 7×4 规则 + +星座序号 0 是白羊,11 是双鱼。子运都不写更小一级。 + +| 大运 | 起始星座 | 方向 | 年数 | 子运 | +| --- | --- | --- | --- | --- | +| Sthira `goel_rao_short_brahma_method_v1` | Brahma 所在星座。Brahma 取上升与第 7 较强者的 6/8/12 宫主,排除土星。力量是 Goel / BVB 简法的星力加宫力,不是 Shadbala,也不是 PyJHora 的 `house.brahma`。上升与第 7 总分相同,用两宫主里 Charakaraka 名次更高的一个。出处:Predicting through Jaimini Astrology,印页 100–108 | 一律顺行。印页 107。上游按奇偶换向的展开器不用 | 基本 7、固定 8、变动 9。日历周年 | 第一段子运就是本运星座,然后顺行。月数等于本运年数。日历月 | +| Drig `drig_rath_aspect_sequence_v1` | 永远从上升的第 9 宫起。Crux of Vedic Astrology,印页 288 | 不是一条黄道方向。第 9、第 10、第 11 三个锚点各自带上它所看的星座;偶数序号顺行看,奇数序号逆行看。印页 395。上游候选档案 `drig_rath_narayana_duration_v1` 是普通黄道奇偶,**报告不用这个名字** | 本仓 Narayana `jaimini_odd_footed_v1`,传统宫主,不计尊贵,不计交点。印页 288 写与 Narayana 相同。书里另一句「15 到 25 年」是 10 年,没有拿来改年数 | 本运宫主与第 7 宫主比简法星力,较强者所在星座作子运种子;其后按本运星座奇偶。12 段平分本运的日历区间。平手则该运不列子运 | +| Shoola `shoola_forward_md_direct_ad_v1` | 上升与第 7 比简法宫力,取较强者。总分相同再用宫主的 Charakaraka。仍分不出就不列表。印页 395 的句子只规定主运 | 顺行(书里的 regular) | 每段 9 个日历年 | 12 段顺行,每段 9 个日历月,第一段就是本运星座。这是档案名里的子运,不是印页那一句单独写明的 | +| Navamsha `goel_method_1_v1` | 九分上升的序号为偶数则从九分上升起,否则从它的第 7 起。九分按元素:火白羊、土摩羯、风天秤、水巨蟹,一脚 3°20′。Predicting through Jaimini Astrology,印页 139,只用方法 1 | 一律顺行 | 在九分里数到传统宫主。奇数星座顺数,偶数逆数。距离 0 记 12,否则距离加 1。天蝎火星、水瓶土星,不双主,不减 1 | 第一段子运就是本运星座,顺行,月数等于年数。让步允许砍子运,本轮保留 | +| Niryaana Shoola `goel_nirayana_shoola_v1` | 与 Shoola 相同的简法种子。同盘印页的 Jaimini 宫力从双鱼起;简法总分打平后 Karaka 选处女。报告保持简法,不改成双鱼。印页 162 | 一律顺行 | 每段 9 个日历年 | 12 段,每段 9 个日历月,从本运星座顺行 | +| Lagna Kendradi | blocked | blocked | blocked | blocked。上游注册表写没有安全默认值。报告不列表 | +| Chara `kn_rao` | 已有模块:从上升起。Predicting through Jaimini's Chara Dasha,印页 32–33 | 第 9 若是偶足则逆行,否则顺行。偶足是 3、4、5、9、10、11 | 数到解析后的宫主再减 1;结果 ≤0 记 12;入旺加 1,落陷减 1。天蝎看火星与计都,水瓶看土星与罗睺。0 年合法 | 已有模块把上升序旋转 1 位,再平分本运区间。印页 35–36 要求每一运用该星座自己的顺逆,且本运星座在最后。两者只在第一运重合。没有改 `jaimini.py`。报告写明这一节是 K.N. Rao 的 Chara,不是 Narayana | + +## 书内例题与书内分叉 + +照 BUG-1218:同一本书或同一张例盘有两种算法时,写明选了哪一边,不把另一边的数填进报告。 + +- Sthira:简法例盘的星力是 150、120、180、130、70、150、50,宫力是 280、260、250、260、250、260、230、300、250、130、150、260。Brahma 为火星,从天蝎顺行,年数 8、9、7、8、9、7、8、9、7、8、9、7。同一张盘若用书里的整套 Jaimini 宫力,会从双鱼(金星)起。本站选简法。Charan Singh 表是整套方法,不混用。测试锚点是构造日期,不把印页上的时钟写进本文。 +- Navamsha:方法 1、2、3 彼此不同。本站只选方法 1。九分上升为双鱼时,从处女起。这是起始规则。印页年数行有缺口,不把整行年数说成已复现。 +- Niryaana Shoola:Goel 是上升或第 7、顺行、9 年。PyJHora 是第 2 或第 8、按奇偶、7/8/9 年。Raman 又是另一套奇偶。同一例盘上,印页 Jaimini 宫力起双鱼,简法起处女。报告用简法。 +- Sthira 方向:Goel 与 PyJHora 都是顺行。上游 `sthira_dasha.py` 按奇偶换向。本站跟 Goel。 +- Drig:印页 395 是相位组顺序。上游候选档案是普通黄道。PyJHora 接近相位组,并多一轮 12 减 n。巨蟹上升的相位序是 11、8、5、2、0、4、7、10、1、9、6、3。年数不改去凑「15 到 25 年」。 +- Shoola:第 7 较强且第 7 是摩羯时,顺行每段 9 年,白羊落在第 27 到 36 年。这是用简法宫力造出来的盘,不是手填种子。子运从本运星座起;PyJHora 的第一段子运不是本运星座。印页那一句只写主运。 +- Chara:白羊上升的主运是十二星座顺行,第一段子运从金牛起、最后回到白羊,与印页 32–36 的第一运一致。其后各运的子运仍用模块的旋转,不改成书里的「每运自定顺逆」。木星在摩羯时,构造盘的射手运是 0 年。 +- Lagna Kendradi:上游注册表说没有安全默认值,上游另一个运行入口仍会吐出一张表。本站跟注册表和决策 3,不采用那张表。 + +## 四张盘与 PyJHora 4.8.7 + +前 6 段的星座序号与年数。差只记在起始星座、方向、年数、子运、日期基准。年数按「星座与年数的配对」比较:同一套年数只是换了起点,不算年数差。日期基准是星座和年数都相同、下一段起点不同,来自日历年与 PyJHora 的年日长度,没有改年数去对齐。Lagna Kendradi 四张盘都是四步全差,因为本站不列表;下面不重复。交点用本仓默认平交点,PyJHora 用真交点。章动 BUG-1275 已在基线里,宫头可以和 PyJHora 4.8.7 的 Lahiri 差一个小角度。这两处都不回改大运。 + +`steve_jobs_1955_aa` + +| 大运 | 差在 | 本站前 6 段 | PyJHora 前 6 段 | 哪边符合所选出处 | +| --- | --- | --- | --- | --- | +| Sthira | 起始、子运 | 8 起,年 9、7、8、9、7、8 | 11 起,同年数模式 | 本站是简法 Brahma。PyJHora 用它自己的 Brahma | +| Drig | 四步都有 | 1、9、6、3、2、5;年 7、3、2、4、7、8 | 0、4、7、10、1、3;年 12、6、6、1、7、4 | 本站是印页 395 的一轮相位组,年数是 Narayana 奇足。PyJHora 另有第二轮 | +| Shoola | 起始、子运 | 11 起,各 9 年,顺行 | 10 起,各 9 年,顺行 | 本站按简法宫力。子运从本运起;PyJHora 的第一段子运不是本运 | +| Navamsha | 起始、年数、子运 | 3 起,年 9、7、3、3、6、7,顺行 | 0 起,各 9 年 | 本站是方法 1。PyJHora 不是这套年数 | +| Niryaana Shoola | 方向、年数、子运 | 11 起,顺行,各 9 | 11 起,逆行,年 9、8、7、9、8、7 | 本站跟 Goel。PyJHora 是第 2/第 8 与 7/8/9 | +| Chara | 起始、年数、子运 | 5 起,年 8、2、6、6、4、5 | 4 起,年 6、8、2、6、6、4 | 两边都顺行。前 6 段像差一个起点,但星座与年数的配对并不相同,所以年数仍记下。没有改 `jaimini.py` | + +`barack_obama_1961_aa` + +| 大运 | 差在 | 本站前 6 段 | PyJHora 前 6 段 | +| --- | --- | --- | --- | +| Sthira | 起始、子运 | 3 起,年 7、8、9、7、8、9 | 9 起,同年数模式 | +| Drig | 方向、年数、子运 | 5、2、11、8、6、10;年 2、1、2、1、8、1 | 星座相同;年 2、1、1、0、8、1。后面的顺序不同 | +| Shoola | 子运、日期基准 | 3 起,各 9,星座相同 | 星座与 9 年相同 | +| Navamsha | 起始、年数、子运 | 4 起,年 6、12、3、11、4、12 | 0 起,各 9 | +| Niryaana Shoola | 四步都有 | 3 起,各 9,顺行 | 4 起,年 8、9、7、8、9、7 | +| Chara | 年数、子运 | 9 起逆行,年 12、0、3、8、2、1 | 星座相同,年 12、0、9、8、2、1。第二段都是 0 年。第三段 3 对 9 | + +`einstein_1879_aa` + +| 大运 | 差在 | 本站前 6 段 | PyJHora 前 6 段 | +| --- | --- | --- | --- | +| Sthira | 只有日期基准 | 11 起,年 9、7、8、9、7、8 | 星座与年数相同 | +| Drig | 方向、年数、子运 | 10、0、3、6、11、8;年 11、9、8、5、1、2 | 10 起;10、6、3、0、11、8;年 11、6、7、10、1、2 | +| Shoola | 子运、日期基准 | 2 起,各 9 | 星座与 9 年相同 | +| Navamsha | 起始、年数、子运 | 5 起,年 12、5、10、12、3、10 | 10 起,各 9 | +| Niryaana Shoola | 四步都有 | 2 起,各 9 | 9 起,年 7、9、8、7、9、8 | +| Chara | 年数、子运 | 2 起逆行,前 6 段年数 8、11、10、1、11、10 | 星座与这 6 段年数相同。全表里天蝎是 8 年对 3 年,所以年数仍记下 | + +`fictional_reader_main` + +| 大运 | 差在 | 本站前 6 段 | PyJHora 前 6 段 | +| --- | --- | --- | --- | +| Sthira | 起始、子运 | 2 起,年 9、7、8、9、7、8 | 0 起,年 7、8、9、7、8、9。星座与年数的配对相同,只是起点不同,年数不另记 | +| Drig | 方向、年数、子运 | 9、7、4、1、10、0;年 12、7、5、12、1、2 | 9、7、4、1、10、6;年 12、8、5、11、1、6 | +| Shoola | 子运、日期基准 | 7 起,各 9 | 星座与 9 年相同 | +| Navamsha | 起始、年数、子运 | 0 起,年 9、5、12、10、7、5 | 10 起,各 9 | +| Niryaana Shoola | 四步都有 | 7 起,各 9 | 8 起,年 9、7、8、9、7、8 | +| Chara | 年数、子运 | 1 起逆行,年 12、2、9、1、12、8 | 星座相同,年 11、2、9、1、12、8。第一段 12 对 11 | + +乔布斯盘的坐标沿用已有 fixture(ERR-086),没有另造出生地。 + +## 报告 + +乔布斯盘的完整数据版中文章出现 Sthira、Niryaana Shoola、Drig、Navamsha、Shoola、Chara 和「本站选用的方法」。Lagna Kendradi 一节只有缺席句:「本轮不上表:没有选定的安全默认算法,故本报告不列出该大运。」这一节没有星座表。英文是 “This report does not include Lagna Kendradi: no safe default method is selected.” 英文章 0 汉字。没有盘面经度时这一章不出现,旧的无盘样例仍不出现 Sthira。 + +Chara 另写:「这一节是 K.N. Rao 的 Chara 表,不是 Narayana 表。」 + +## 本机测试 + +| 命令 | 退出码 | +| --- | --- | +| `C:\Users\74082\anaconda3\python.exe -m pytest tests/test_rashi_dashas_native.py tests/test_report_full_data_fix.py tests/test_niryaana_shoola_dasha.py tests/test_runtime_jhora_import_scan.py -q --tb=line` | 0(27 过) | +| `C:\Users\74082\anaconda3\python.exe -m py_compile scripts/goel_short_strength.py scripts/rashi_dashas_native.py scripts/rashi_dasha_report.py scripts/research/freeze_rashi_dasha_pyjhora_fixture.py scripts/pl9_full_data_export.py` | 0 | +| 开工 `scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` | 1(超时,不算通过) | + +`git add` 之后另跑 `tests/test_repo_privacy_markers.py`,结果写在提交说明旁边的验收回报里。没有跑 Linux 快速门,没有跑浏览器走查。 + +## 让步 + +只用了 Lagna Kendradi:不上表。Navamsha 子运和 Drig 子运都保留。Sthira、Shoola、Niryaana Shoola、Chara 的主运没有砍。 + +## 下轮问占星师 + +Lagna Kendradi 没有可以安全默认的算法,所以这轮报告只写缺席。请定四件事:种子是上升,还是上升与第 7 里较强的一个;方向按星座奇偶,还是按土星、计都;年数是否像 Narayana 那样再有一轮 12 减 n;顺序是沿黄道,还是按 Kendra、Panaphara、Apoklima。定下来之前不补表。 + +Drig 也请看一眼:报告用的是 Crux 印页 395 的相位组,不是上游候选档案里的普通黄道顺序。 + +真人核对清单:`docs/testing/rashi-dashas-native-20261008.md`。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 8fbd45fe..17625a6d 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -204,7 +204,7 @@ | `TASK-report-full-data-edition-20261007.md` | `PROGRESS-report-full-data-edition-20261007.md` | **报告改为完整数据版、删除阅读版**:网站阅读版中文 7.6 万字符 vs 上游 `pl9_ai_density` 97 万;移植原始数据附录 / 清理器 / 时间补充,中英文;我方报告口径(BUG-1199~1229)全部保持;旧报告仍可打开;排在 sync6 之后 | 已验收(Claude 补修 BUG-1282 后合入 staging) | `codex/report-full-data-edition-20261007`(未推 staging) | | `TASK-report-full-data-fix-20261008.md` | `PROGRESS-report-full-data-fix-20261008.md` | **完整数据版补齐**(同分支继续):解释段、年运字段、开篇重点、英文汉字清零。Patyayini 不新算。生成按钮文案不改。等验收后再合入 staging | 已验收(Claude 补修 BUG-1282 后合入 staging) | `codex/report-full-data-edition-20261007`(未合入 staging) | | `TASK-report-full-data-fix2-20261008.md` | `PROGRESS-report-full-data-fix2-20261008.md` | **完整数据版第二轮**(同分支续):年运章投影已有字段,大运解释补「可能」段落,瑜伽作用不再断言寿命,术语不进标识符,罗睺/计都不写「守护第未列出宫」。章动单不在本分支 | 已验收(Claude 补修 BUG-1282 后合入 staging) | `codex/report-full-data-edition-20261007`(未合入) | -| `TASK-rashi-dashas-native-20261008.md` | — | **报告补 7 种星座大运**(Sthira / Drig / Shoola / Navamsha / Niryaana Shoola / Lagna Kendradi / Chara):上游是「显式输入展开器」+「运行候选档案」,起始星座、方向、年数要由我方盘面算;PyJHora 只作冻结对照;只进报告并标口径;Lagna Kendradi 无安全默认值先调研 | 待领取 | — | +| `TASK-rashi-dashas-native-20261008.md` | `PROGRESS-rashi-dashas-native-20261008.md` | **报告补 7 种星座大运**(Sthira / Drig / Shoola / Navamsha / Niryaana Shoola / Lagna Kendradi / Chara):六种上完整数据版并标口径;Lagna Kendradi 没有安全默认值,报告写明缺席、不上表 | 待验收 | `codex/rashi-dashas-native-20261008`(未推送,未合入) | | `TASK-report-reader-main-fix-20260925.md` | `PROGRESS-report-reader-main-fix-20260925.md` | **读者版修复**:真实年主比较、分字段绑定与未来返照;保留 Bhava Bala 和标题,隔离报告/聊天规则 | 已验收(Claude 09-25:年运表逐年填齐且不同、Bhava Bala 保留、泄漏 0) | `4d801e53`(已部署,health 核对一致) | | `TASK-report-reader-main-20260924.md` | `PROGRESS-report-reader-main-20260924.md` | **报告正文换上游读者版 reader_main + KP / Muntha 计算缺陷 + 投影截断(BUG-1026/1027/1028)**:正文一直是上游审计版(读者版 09-09 才出、从未同步),本仓又自加「结论等级规则」/英文段/异常原文;投影黑名单不认 PL9 词汇且截断半句、删表留说明;KP args `vars()` 拷贝为空恒失败;Muntha 漏合 `birth_asc_sign_idx` → Year Lord 恒火星。产品拍板接入读者版、旧报告不回填。 | 部分通过(Claude 09-25:读者版切换、泄漏 0 命中、KP 已修;年运 Year Lord/Muntha 全为「-」P1、投影误删 Bhava Bala P2 → 修复单) | `3c2f7bd5`(已部署) | | `TASK-report-reader-actions-20260924.md` | — | **报告操作收敛**:列表只留「查看报告」;阅读页删「下载原始附录」「打印」,导出改图标按钮;导出对话框 ≥860 居中、窄屏仍贴底(修订 chapter-export D9/D10)。产品变更,不开 BUG 号 | 基本通过(Claude 09-24:入口收敛、图标、≥860 居中均符合;原始附录三文件因执行方权限未删、PROGRESS/清单未写 → 并入修复单) | `f125fae0`(未部署) | diff --git a/docs/testing/rashi-dashas-native-20261008.md b/docs/testing/rashi-dashas-native-20261008.md new file mode 100644 index 00000000..d506be68 --- /dev/null +++ b/docs/testing/rashi-dashas-native-20261008.md @@ -0,0 +1,12 @@ +# 星座大运报告核对(2026-10-09) + +本机没有登录态,也没有做浏览器走查。下面留给真人在重新生成的完整数据版上看。不要把出生时刻写进回复。 + +盘:公开盘 `steve_jobs_1955_aa`,再抽 `barack_obama_1961_aa` 或 `einstein_1879_aa` 一张即可。 + +1. 中文完整数据版有 Sthira、Niryaana Shoola、Drig、Navamsha、Shoola、Chara 六节。每节有「本站选用的方法」和书名页码,并写着未与外部软件逐一核对。 +2. Lagna Kendradi 只有一句缺席,没有星座表。中文清理后标题里的 Lagna 会变成「上升」。这是既有清理,不是漏章。 +3. Chara 写明是 K.N. Rao 的表,不是 Narayana 表。 +4. 英文版这几节没有汉字。 +5. 对话里的年运卡和生时校正没有这七种新表。 +6. 若要改 Lagna Kendradi 或改用 Drig 的普通黄道顺序,先回答进度记录里的占星师问题,再改代码。 diff --git a/scripts/goel_short_strength.py b/scripts/goel_short_strength.py new file mode 100644 index 00000000..bda6a0e0 --- /dev/null +++ b/scripts/goel_short_strength.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Goel / BVB short graha and rashi strength for the report rashi dashas. + +Dignity, amsha, and kendradi units follow the short method in +Predicting through Jaimini Astrology, printed pages 100-108. +This is not Shadbala and not a house-strength table from another program. +""" + +from __future__ import annotations + +from typing import Dict, Mapping + +try: + from jaimini import FRIENDSHIP, KARAKA_7, SIGNS, calc_chara_karaka_7 + from narayana_dasha import SIGN_LORDS, _rasi_aspects +except ImportError: # pragma: no cover - package import + from scripts.jaimini import FRIENDSHIP, KARAKA_7, SIGNS, calc_chara_karaka_7 + from scripts.narayana_dasha import SIGN_LORDS, _rasi_aspects + +CLASSICAL_PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn") +_EXALT_SIGN = { + "Sun": 0, + "Moon": 1, + "Mars": 9, + "Mercury": 5, + "Jupiter": 3, + "Venus": 11, + "Saturn": 6, +} +_DEBIL_SIGN = { + "Sun": 6, + "Moon": 7, + "Mars": 3, + "Mercury": 11, + "Jupiter": 9, + "Venus": 5, + "Saturn": 0, +} +_OWN_SIGNS = { + "Sun": {4}, + "Moon": {3}, + "Mars": {0, 7}, + "Mercury": {2, 5}, + "Jupiter": {8, 11}, + "Venus": {1, 6}, + "Saturn": {9, 10}, +} +# Moolatrikona bands are degree-in-sign, end exclusive. +_MOOLATRIKONA = { + "Sun": ((4, 0.0, 20.0),), + "Moon": ((1, 3.0, 30.0),), + "Mars": ((0, 0.0, 12.0),), + "Mercury": ((5, 15.0, 20.0),), + "Jupiter": ((8, 0.0, 10.0),), + "Venus": ((6, 0.0, 15.0),), + "Saturn": ((10, 0.0, 20.0),), +} +_AMSHA_BY_RANK = {1: 70, 2: 60, 3: 50, 4: 40, 5: 30, 6: 20, 7: 10} +_CHARA_RASHI = {0: 20, 1: 40, 2: 60} + + +def sign_index(longitude: float) -> int: + return int(float(longitude) % 360.0 // 30.0) + + +def degree_in_sign(longitude: float) -> float: + return float(longitude) % 30.0 + + +def _in_moolatrikona(planet: str, sign_idx: int, degree: float) -> bool: + for sign, start, end in _MOOLATRIKONA.get(planet, ()): + if sign == sign_idx and start <= degree < end: + return True + return False + + +def dignity_units(planet: str, sign_idx: int, degree: float) -> int: + """Short-method dignity. Whole exalt and debil signs, except Mercury in Virgo.""" + if planet not in CLASSICAL_PLANETS: + raise KeyError(planet) + if sign_idx == _DEBIL_SIGN[planet]: + return 10 + if planet == "Mercury" and sign_idx == 5: + if degree < 15.0: + return 70 + if degree < 20.0: + return 60 + return 50 + if sign_idx == _EXALT_SIGN[planet]: + return 70 + if _in_moolatrikona(planet, sign_idx, degree): + return 60 + if sign_idx in _OWN_SIGNS[planet]: + return 50 + lord = SIGN_LORDS[SIGNS[sign_idx]] + relation = FRIENDSHIP.get(planet, {}) + if lord in relation.get("friend", []): + return 40 + if lord in relation.get("enemy", []): + return 20 + return 30 + + +def kendradi_units(sign_idx: int, ak_sign_idx: int) -> int: + offset = (int(sign_idx) - int(ak_sign_idx)) % 12 + if offset % 3 == 0: + return 60 + if offset % 3 == 1: + return 40 + return 20 + + +def karaka_ranks(planet_lons: Mapping[str, float]) -> Dict[str, int]: + degrees = { + planet: degree_in_sign(planet_lons[planet]) + for planet in CLASSICAL_PLANETS + if planet in planet_lons + } + table = calc_chara_karaka_7(degrees).get("karaka_table") or {} + ranks = {} + for rank, name in KARAKA_7.items(): + row = table.get(name) or {} + planet = row.get("planet") + if planet: + ranks[str(planet)] = int(rank) + return ranks + + +def graha_strengths(planet_lons: Mapping[str, float]) -> Dict[str, Dict[str, int]]: + ranks = karaka_ranks(planet_lons) + if "Mars" not in ranks and len(ranks) < 7: + raise ValueError("short graha strength needs the seven classical planets") + ak_planet = next(planet for planet, rank in ranks.items() if rank == 1) + ak_sign = sign_index(planet_lons[ak_planet]) + rows = {} + for planet in CLASSICAL_PLANETS: + sign_idx = sign_index(planet_lons[planet]) + degree = degree_in_sign(planet_lons[planet]) + dignity = dignity_units(planet, sign_idx, degree) + amsha = _AMSHA_BY_RANK[ranks[planet]] + kendradi = kendradi_units(sign_idx, ak_sign) + rows[planet] = { + "sign_index": sign_idx, + "dignity": dignity, + "amsha": amsha, + "kendradi": kendradi, + "total": dignity + amsha + kendradi, + "karaka_rank": ranks[planet], + } + return rows + + +def _aspect_units(sign_idx: int, planet_lons: Mapping[str, float], lord: str) -> int: + contributors = [] + for planet in (lord, "Jupiter", "Mercury"): + if planet not in contributors: + contributors.append(planet) + total = 0 + for planet in contributors: + if planet not in planet_lons: + continue + source = sign_index(planet_lons[planet]) + if source != sign_idx and not _rasi_aspects(source, sign_idx): + continue + if planet == lord and planet in {"Jupiter", "Mercury"}: + total += 120 + else: + total += 60 + return total + + +def rashi_strengths( + planet_lons: Mapping[str, float], + graha: Mapping[str, Mapping[str, int]] | None = None, +) -> Dict[int, Dict[str, int]]: + graha_rows = graha if graha is not None else graha_strengths(planet_lons) + occupants: Dict[int, int] = {index: 0 for index in range(12)} + for planet in CLASSICAL_PLANETS: + occupants[sign_index(planet_lons[planet])] += 1 + rows = {} + for sign_idx in range(12): + lord = SIGN_LORDS[SIGNS[sign_idx]] + chara = _CHARA_RASHI[sign_idx % 3] + sthira = 10 * occupants[sign_idx] + aspect = _aspect_units(sign_idx, planet_lons, lord) + lord_graha = int(graha_rows[lord]["total"]) + rows[sign_idx] = { + "chara": chara, + "sthira": sthira, + "aspect": aspect, + "lord_graha": lord_graha, + "total": chara + sthira + aspect + lord_graha, + } + return rows + + +def stronger_sign(left: int, right: int, strengths: Mapping[int, Mapping[str, int]], planet_lons: Mapping[str, float]) -> Dict[str, object]: + """Pick the stronger sign. Equal totals use the higher chara-karaka lord.""" + left_total = int(strengths[left]["total"]) + right_total = int(strengths[right]["total"]) + if left_total != right_total: + selected = left if left_total > right_total else right + return {"status": "selected", "sign_index": selected, "tie_break": "rashi_total"} + ranks = karaka_ranks(planet_lons) + left_lord = SIGN_LORDS[SIGNS[left]] + right_lord = SIGN_LORDS[SIGNS[right]] + left_rank = ranks.get(left_lord) + right_rank = ranks.get(right_lord) + if left_rank is None or right_rank is None or left_rank == right_rank: + return { + "status": "unresolved", + "reason": "equal_rashi_strength_and_karaka", + "candidates": (left, right), + } + selected = left if left_rank < right_rank else right + return {"status": "selected", "sign_index": selected, "tie_break": "lord_karaka_rank"} diff --git a/scripts/pl9_full_data_export.py b/scripts/pl9_full_data_export.py index ea4b9242..e948977e 100644 --- a/scripts/pl9_full_data_export.py +++ b/scripts/pl9_full_data_export.py @@ -2185,6 +2185,18 @@ def _apply_local_report_conventions(markdown: str, *, language: str = "zh") -> s return text +def _append_native_rashi_dashas(markdown: str, packet: dict) -> str: + """Full-data edition only. The parity reader and the consult card do not call this.""" + try: + from rashi_dasha_report import render_rashi_dasha_chapter + except ModuleNotFoundError: # pragma: no cover - package import compatibility + from scripts.rashi_dasha_report import render_rashi_dasha_chapter + chapter = render_rashi_dasha_chapter(packet if isinstance(packet, dict) else {}) + if not chapter.strip(): + return markdown + return markdown.rstrip() + "\n\n" + chapter + + def _insert_opening_focus(markdown: str, focus: str) -> str: """Put the current-period focus directly under the title.""" first = next((line.strip() for line in focus.splitlines() if line.strip()), "") @@ -2221,7 +2233,10 @@ def render_full_data_markdown(packet: dict) -> str: count=1, flags=re.DOTALL, ).strip() - english_markdown = _insert_opening_focus(render_pl9_parity_markdown(packet), focus) + english_markdown = _append_native_rashi_dashas( + _insert_opening_focus(render_pl9_parity_markdown(packet), focus), + packet, + ) if structured_appendix: english_markdown = ( english_markdown.rstrip() @@ -2246,7 +2261,10 @@ def render_full_data_markdown(packet: dict) -> str: count=1, flags=re.DOTALL, ).strip() - zh_markdown = _insert_opening_focus(render_pl9_parity_markdown(packet), focus) + zh_markdown = _append_native_rashi_dashas( + _insert_opening_focus(render_pl9_parity_markdown(packet), focus), + packet, + ) if zh_structured_appendix and "## 结构化资料附录" not in zh_markdown: zh_markdown = ( zh_markdown.rstrip() diff --git a/scripts/rashi_dasha_report.py b/scripts/rashi_dasha_report.py new file mode 100644 index 00000000..6bd0bb13 --- /dev/null +++ b/scripts/rashi_dasha_report.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Markdown chapter for the seven report-only rashi dashas.""" + +from __future__ import annotations + +from typing import Mapping + +try: + from rashi_dashas_native import calculate_rashi_dashas +except ImportError: # pragma: no cover - package import + from scripts.rashi_dashas_native import calculate_rashi_dashas + +_SIGN_CN = { + "Aries": "白羊座", + "Taurus": "金牛座", + "Gemini": "双子座", + "Cancer": "巨蟹座", + "Leo": "狮子座", + "Virgo": "处女座", + "Libra": "天秤座", + "Scorpio": "天蝎座", + "Sagittarius": "射手座", + "Capricorn": "摩羯座", + "Aquarius": "水瓶座", + "Pisces": "双鱼座", +} +# Chinese headings avoid the token Dasha. The report cleaner turns that token +# into 大运 and then deletes the linebreak before the next Chinese sentence. +_ORDER = ( + ("sthira", "Sthira", "Sthira Dasha"), + ("niryaana_shoola", "Niryaana Shoola", "Niryaana Shoola Dasha"), + ("drig", "Drig", "Drig Dasha"), + ("navamsha", "Navamsha", "Navamsha Dasha"), + ("shoola", "Shoola", "Shoola Dasha"), + ("chara", "Chara", "Chara Dasha"), + ("lagna_kendradi", "Lagna Kendradi", "Lagna Kendradi Rashi Dasha"), +) +_CLASSICAL = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn") + + +def _language(packet: Mapping) -> str: + raw = packet.get("report_language") or packet.get("language") or "zh" + return "en" if str(raw).lower().startswith("en") else "zh" + + +def _longitude(row: Mapping) -> float | None: + # Ascendant "degree" is the degree inside the sign. Full longitude is degree_raw / lon. + for key in ("degree_raw", "lon", "longitude"): + value = row.get(key) + if isinstance(value, (int, float)): + return float(value) + sign = row.get("sign") + degree = row.get("degree_in_sign") + if sign in _SIGN_CN and isinstance(degree, (int, float)): + signs = list(_SIGN_CN) + return signs.index(str(sign)) * 30.0 + float(degree) + return None + + +def _birth_date(packet: Mapping) -> str | None: + birth = packet.get("birth_info") + if not isinstance(birth, Mapping): + return None + if birth.get("date"): + return str(birth.get("date")) + year, month, day = birth.get("year"), birth.get("month"), birth.get("day") + if all(isinstance(part, int) for part in (year, month, day)): + return f"{int(year):04d}-{int(month):02d}-{int(day):02d}" + return None + + +def chart_inputs(packet: Mapping) -> dict | None: + core = packet.get("core_chart") + if not isinstance(core, Mapping): + return None + asc = core.get("ascendant") + planets = core.get("planets") + if not isinstance(asc, Mapping) or not isinstance(planets, Mapping): + return None + asc_lon = _longitude(asc) + birth = _birth_date(packet) + if asc_lon is None or not birth: + return None + longitudes = {} + for name in list(_CLASSICAL) + ["Rahu", "Ketu"]: + row = planets.get(name) + if isinstance(row, Mapping): + value = _longitude(row) + if value is not None: + longitudes[name] = value + if any(name not in longitudes for name in _CLASSICAL): + return None + return {"birth": birth, "ascendant_longitude": asc_lon, "planet_lons": longitudes} + + +def _sign_label(sign: str, language: str) -> str: + if language == "zh": + return _SIGN_CN.get(sign, sign) + return sign + + +def _method_line(section: Mapping, language: str) -> str: + if section.get("status") != "listed": + return str(section.get("reason_en") if language == "en" else section.get("reason_zh") or "") + profile = section.get("profile_id") or "" + if language == "en": + return ( + f"Method used on this site: {profile}, source: {section.get('source_en')}; " + "not checked one-by-one against external software." + ) + return f"本站选用的方法:{profile},出处:{section.get('source_zh')};未与外部软件逐一核对。" + + +def render_rashi_dasha_chapter(packet: Mapping) -> str: + """Return the chapter, or an empty string when the packet has no longitudes.""" + inputs = chart_inputs(packet) + if inputs is None: + return "" + language = _language(packet) + sections = calculate_rashi_dashas(inputs["birth"], inputs["ascendant_longitude"], inputs["planet_lons"]) + if language == "en": + lines = [ + "## Sign dashas calculated on this site", + "", + "These tables list signs and dates only.", + "", + ] + sign_header = "| Index | Sign | Start | End | Years |" + child_header = "| Index | Maha | Antara | Start | End |" + else: + lines = [ + "## 星座大运(本站计算)", + "", + "以下只列出星座和日期。", + "", + ] + sign_header = "| 序号 | 星座 | 起始 | 结束 | 年数 |" + child_header = "| 序号 | 主运 | 子运 | 起始 | 结束 |" + for key, title_zh, title_en in _ORDER: + section = sections[key] + title = title_en if language == "en" else title_zh + lines.extend([f"### {title}", "", _method_line(section, language), ""]) + if key == "chara" and section.get("status") == "listed": + lines.extend([ + "This section is the K.N. Rao Chara table. It is not the Narayana table." + if language == "en" + else "这一节是 K.N. Rao 的 Chara 表,不是 Narayana 表。", + "", + ]) + periods = section.get("periods") or [] + if not periods: + continue + lines.extend([sign_header, "|------|------|------|------|------|"]) + child_rows = [] + for index, period in enumerate(periods, start=1): + label = _sign_label(str(period.get("sign")), language) + lines.append( + f"| {index} | {label} | {period.get('start')} | {period.get('end')} | {period.get('years')} |" + ) + for child in period.get("antardasha") or []: + child_rows.append((label, _sign_label(str(child.get("sign")), language), child.get("start"), child.get("end"))) + lines.append("") + if child_rows: + lines.extend([child_header, "|------|------|------|------|------|"]) + for index, (parent, child, start, end) in enumerate(child_rows, start=1): + lines.append(f"| {index} | {parent} | {child} | {start} | {end} |") + lines.append("") + return "\n".join(lines).rstrip() + "\n" diff --git a/scripts/rashi_dashas_native.py b/scripts/rashi_dashas_native.py new file mode 100644 index 00000000..cd83737d --- /dev/null +++ b/scripts/rashi_dashas_native.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +"""Report-only rashi dashas computed from this site's longitudes. + +The seven sections are Sthira, Drig, Shoola, Navamsha, Niryaana Shoola, +Lagna Kendradi, and Chara. Lagna Kendradi has no safe default and stays off +the table. Chara dates the existing kn_rao rows; it does not recalculate them. +""" + +from __future__ import annotations + +import calendar +from datetime import date, datetime, timedelta +from typing import Dict, Mapping, Sequence + +try: + from goel_short_strength import ( + CLASSICAL_PLANETS, + degree_in_sign, + graha_strengths, + rashi_strengths, + sign_index, + stronger_sign, + ) + from jaimini import SIGNS, calc_chara_dasha_with_antardasha + from narayana_dasha import SIGN_LORDS, calculate_narayana_period_years +except ImportError: # pragma: no cover - package import + from scripts.goel_short_strength import ( + CLASSICAL_PLANETS, + degree_in_sign, + graha_strengths, + rashi_strengths, + sign_index, + stronger_sign, + ) + from scripts.jaimini import SIGNS, calc_chara_dasha_with_antardasha + from scripts.narayana_dasha import SIGN_LORDS, calculate_narayana_period_years + +_MOVABLE = {0, 3, 6, 9} +_FIXED = {1, 4, 7, 10} +_DUAL = {2, 5, 8, 11} + + +def _as_date(value: date | datetime | str) -> date: + if isinstance(value, datetime): + return value.date() + if isinstance(value, date): + return value + text = str(value).strip() + if "T" in text: + text = text.split("T", 1)[0] + if " " in text: + text = text.split(" ", 1)[0] + year, month, day = (int(part) for part in text[:10].split("-")) + return date(year, month, day) + + +def add_years(day: date, years: int) -> date: + try: + return day.replace(year=day.year + int(years)) + except ValueError: + return day.replace(year=day.year + int(years), day=28) + + +def add_months(day: date, months: int) -> date: + month_index = day.month - 1 + int(months) + year = day.year + month_index // 12 + month = month_index % 12 + 1 + last = calendar.monthrange(year, month)[1] + return date(year, month, min(day.day, last)) + + +def _iso(day: date) -> str: + return day.isoformat() + + +def _forward(sign_idx: int, count: int = 12) -> list[int]: + return [(int(sign_idx) + offset) % 12 for offset in range(count)] + + +def _reverse(sign_idx: int, count: int = 12) -> list[int]: + return [(int(sign_idx) - offset) % 12 for offset in range(count)] + + +def _aspecting_signs(sign_idx: int) -> set[int]: + sign_idx = int(sign_idx) % 12 + if sign_idx in _MOVABLE: + return {candidate for candidate in _FIXED if candidate != (sign_idx + 1) % 12} + if sign_idx in _FIXED: + return {candidate for candidate in _MOVABLE if candidate != (sign_idx - 1) % 12} + return {candidate for candidate in _DUAL if candidate != sign_idx} + + +def drig_aspect_sequence(lagna_sign: int) -> list[int]: + """Ninth, tenth, and eleventh anchors, each followed by the signs it aspects. + + Crux printed page 395: an even-footed ninth is followed in reverse. + Zero-based even indexes are the odd zodiac signs, so they go forward. + """ + rows: list[int] = [] + for offset in (8, 9, 10): + anchor = (int(lagna_sign) + offset) % 12 + scan = _forward(anchor) if anchor % 2 == 0 else _reverse(anchor) + allowed = _aspecting_signs(anchor) + rows.extend([anchor] + [sign for sign in scan[1:] if sign in allowed]) + return rows[:12] + + +def navamsha_sign(longitude: float) -> int: + longitude = float(longitude) % 360.0 + rashi = int(longitude // 30.0) + pada = int((longitude % 30.0) / (10.0 / 3.0)) + if pada > 8: + pada = 8 + start = (0, 9, 6, 3)[rashi % 4] + return (start + pada) % 12 + + +def navamsha_years(sign_idx: int, lord_sign_idx: int) -> int: + """Odd signs count forward, even signs backward. No dual lords and no minus one.""" + if int(sign_idx) % 2 == 0: + distance = (int(lord_sign_idx) - int(sign_idx)) % 12 + else: + distance = (int(sign_idx) - int(lord_sign_idx)) % 12 + if distance == 0: + return 12 + return distance + 1 + + +def sthira_years(sign_idx: int) -> int: + return (7, 8, 9)[int(sign_idx) % 3] + + +def _period(sign_idx: int, years: int, start: date, end: date, children: list[dict] | None = None) -> dict: + row = { + "sign": SIGNS[int(sign_idx) % 12], + "sign_index": int(sign_idx) % 12, + "years": int(years), + "start": _iso(start), + "end": _iso(end), + } + if children is not None: + row["antardasha"] = children + return row + + +def _calendar_children(parent_sign: int, start: date, *, months_each: int, count: int = 12) -> list[dict]: + rows = [] + cursor = start + for offset in range(count): + child = (int(parent_sign) + offset) % 12 + nxt = add_months(cursor, months_each) + rows.append({ + "sign": SIGNS[child], + "sign_index": child, + "start": _iso(cursor), + "end": _iso(nxt), + }) + cursor = nxt + return rows + + +def _equal_children(order: Sequence[int], start: date, end: date) -> list[dict]: + if not order: + return [] + total = (datetime.combine(end, datetime.min.time()) - datetime.combine(start, datetime.min.time())).total_seconds() + if total <= 0: + return [] + origin = datetime.combine(start, datetime.min.time()) + finish = datetime.combine(end, datetime.min.time()) + rows = [] + for offset, sign_idx in enumerate(order): + child_end = finish if offset == len(order) - 1 else origin + timedelta(seconds=total * (offset + 1) / len(order)) + child_start = origin if offset == 0 else origin + timedelta(seconds=total * offset / len(order)) + rows.append({ + "sign": SIGNS[int(sign_idx) % 12], + "sign_index": int(sign_idx) % 12, + "start": child_start.date().isoformat(), + "end": child_end.date().isoformat(), + }) + return rows + + +def _require_planets(planet_lons: Mapping[str, float]) -> None: + missing = [planet for planet in CLASSICAL_PLANETS if planet not in planet_lons] + if missing: + raise ValueError("missing planets: " + ", ".join(missing)) + + +def select_brahma(lagna_sign: int, planet_lons: Mapping[str, float], graha: Mapping, rashi: Mapping) -> dict: + seventh = (int(lagna_sign) + 6) % 12 + picked = stronger_sign(lagna_sign, seventh, rashi, planet_lons) + if picked.get("status") != "selected": + return {"status": "unresolved", "reason": picked.get("reason") or "lagna_seventh_unresolved", "step": "start_sign"} + anchor = int(picked["sign_index"]) + candidates = [] + for house in (6, 8, 12): + sign_idx = (anchor + house - 1) % 12 + lord = SIGN_LORDS[SIGNS[sign_idx]] + if lord == "Saturn": + continue + candidates.append(( + int(graha[lord]["total"]), + degree_in_sign(planet_lons[lord]), + lord, + sign_index(planet_lons[lord]), + )) + if not candidates: + return {"status": "unresolved", "reason": "no_brahma_lord_after_excluding_saturn", "step": "start_sign"} + candidates.sort(key=lambda row: (row[0], row[1]), reverse=True) + best = candidates[0] + tied = [row for row in candidates if row[0] == best[0] and row[1] == best[1] and row[2] != best[2]] + if tied: + return {"status": "unresolved", "reason": "brahma_lord_tie", "step": "start_sign"} + return { + "status": "selected", + "planet": best[2], + "sign_index": best[3], + "anchor_sign_index": anchor, + "tie_break": picked.get("tie_break"), + } + + +def _listed(profile_id: str, source_zh: str, source_en: str, periods: list[dict], **extra) -> dict: + row = { + "profile_id": profile_id, + "source_zh": source_zh, + "source_en": source_en, + "status": "listed", + "periods": periods, + } + row.update(extra) + return row + + +def _omitted(profile_id: str, source_zh: str, source_en: str, reason_zh: str, reason_en: str) -> dict: + return { + "profile_id": profile_id, + "source_zh": source_zh, + "source_en": source_en, + "status": "omitted", + "reason_zh": reason_zh, + "reason_en": reason_en, + "periods": [], + } + + +def _sthira(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Mapping, rashi: Mapping) -> dict: + source_zh = "Predicting through Jaimini Astrology,印页 100–108" + source_en = "Predicting through Jaimini Astrology, printed pages 100-108" + profile = "goel_rao_short_brahma_method_v1" + picked = select_brahma(lagna, planet_lons, graha, rashi) + if picked.get("status") != "selected": + return _omitted(profile, source_zh, source_en, "本轮不上表:Brahma 星座没有选出。", "This report does not include Sthira: the Brahma sign was not selected.") + start_sign = int(picked["sign_index"]) + periods = [] + cursor = birth + for sign_idx in _forward(start_sign): + years = sthira_years(sign_idx) + end = add_years(cursor, years) + children = _calendar_children(sign_idx, cursor, months_each=years) + periods.append(_period(sign_idx, years, cursor, end, children)) + cursor = end + return _listed(profile, source_zh, source_en, periods, brahma_planet=picked["planet"], direction="direct") + + +def _narayana_years(sign_idx: int, planet_lons: Mapping[str, float]) -> int: + lord = SIGN_LORDS[SIGNS[sign_idx]] + lord_sign = sign_index(planet_lons[lord]) + years, _adjustment = calculate_narayana_period_years( + sign_idx, + lord, + lord_sign, + profile="jaimini_odd_footed_v1", + ) + return int(years) + + +def _drig(birth: date, lagna: int, planet_lons: Mapping[str, float], graha: Mapping) -> dict: + source_zh = "Crux of Vedic Astrology,印页 288、395" + source_en = "Crux of Vedic Astrology, printed pages 288 and 395" + profile = "drig_rath_aspect_sequence_v1" + periods = [] + cursor = birth + for sign_idx in drig_aspect_sequence(lagna): + years = _narayana_years(sign_idx, planet_lons) + end = add_years(cursor, years) + children = _drig_children(sign_idx, cursor, end, graha, planet_lons) + periods.append(_period(sign_idx, years, cursor, end, children)) + cursor = end + return _listed(profile, source_zh, source_en, periods, direction="aspect_groups") + + +def _drig_children(md_sign: int, start: date, end: date, graha: Mapping, planet_lons: Mapping[str, float]) -> list[dict]: + lord = SIGN_LORDS[SIGNS[md_sign]] + seventh_lord = SIGN_LORDS[SIGNS[(md_sign + 6) % 12]] + left = int(graha[lord]["total"]) + right = int(graha[seventh_lord]["total"]) + if left == right: + return [] + chosen = lord if left > right else seventh_lord + seed = sign_index(planet_lons[chosen]) + order = _forward(seed) if md_sign % 2 == 0 else _reverse(seed) + return _equal_children(order, start, end) + + +def _nine_year(birth: date, lagna: int, planet_lons: Mapping[str, float], rashi: Mapping, profile: str, source_zh: str, source_en: str) -> dict: + seventh = (lagna + 6) % 12 + picked = stronger_sign(lagna, seventh, rashi, planet_lons) + if picked.get("status") != "selected": + return _omitted( + profile, + source_zh, + source_en, + "本轮不上表:上升与第七宫的短法力量没有分出强弱。", + "This report does not include this dasha: Lagna and the seventh are equal in the short-method strength.", + ) + start_sign = int(picked["sign_index"]) + periods = [] + cursor = birth + for sign_idx in _forward(start_sign): + end = add_years(cursor, 9) + children = _calendar_children(sign_idx, cursor, months_each=9) + periods.append(_period(sign_idx, 9, cursor, end, children)) + cursor = end + return _listed(profile, source_zh, source_en, periods, direction="direct", start_tie_break=picked.get("tie_break")) + + +def _navamsha(birth: date, asc_lon: float, planet_lons: Mapping[str, float]) -> dict: + source_zh = "Predicting through Jaimini Astrology,印页 139" + source_en = "Predicting through Jaimini Astrology, printed page 139" + profile = "goel_method_1_v1" + d9_lagna = navamsha_sign(asc_lon) + d9_planets = {planet: navamsha_sign(planet_lons[planet]) for planet in CLASSICAL_PLANETS} + start_sign = d9_lagna if d9_lagna % 2 == 0 else (d9_lagna + 6) % 12 + periods = [] + cursor = birth + for sign_idx in _forward(start_sign): + lord = SIGN_LORDS[SIGNS[sign_idx]] + years = navamsha_years(sign_idx, d9_planets[lord]) + end = add_years(cursor, years) + children = _calendar_children(sign_idx, cursor, months_each=years) + periods.append(_period(sign_idx, years, cursor, end, children)) + cursor = end + return _listed( + profile, + source_zh, + source_en, + periods, + direction="direct", + d9_lagna_index=d9_lagna, + ) + + +def _chara(birth: date, lagna: int, planet_lons: Mapping[str, float]) -> dict: + source_zh = "Predicting through Jaimini's Chara Dasha,印页 32–36" + source_en = "Predicting through Jaimini's Chara Dasha, printed pages 32-36" + profile = "kn_rao" + longitudes = {planet: float(value) for planet, value in planet_lons.items()} + calculated = calc_chara_dasha_with_antardasha( + lagna, + longitudes, + birth.year, + birth.month, + birth.day, + profile="kn_rao", + ) + periods = [] + cursor = birth + for row in calculated.get("dasha_sequence") or []: + years = int(row["duration_years"]) + end = add_years(cursor, years) + order = [int(child["sign_idx"]) for child in row.get("antardashas") or []] + children = _equal_children(order, cursor, end) + periods.append(_period(int(row["sign_idx"]), years, cursor, end, children)) + cursor = end + return _listed(profile, source_zh, source_en, periods, direction="ninth_house_even_footed") + + +def calculate_rashi_dashas(birth: date | datetime | str, ascendant_longitude: float, planet_lons: Mapping[str, float]) -> Dict[str, dict]: + """Return the seven report sections. Strength and seeds come from these longitudes.""" + _require_planets(planet_lons) + birth_day = _as_date(birth) + longitudes = {planet: float(planet_lons[planet]) for planet in planet_lons} + lagna = sign_index(float(ascendant_longitude)) + graha = graha_strengths(longitudes) + rashi = rashi_strengths(longitudes, graha) + return { + "sthira": _sthira(birth_day, lagna, longitudes, graha, rashi), + "drig": _drig(birth_day, lagna, longitudes, graha), + "shoola": _nine_year( + birth_day, + lagna, + longitudes, + rashi, + "shoola_forward_md_direct_ad_v1", + "Crux of Vedic Astrology,印页 395", + "Crux of Vedic Astrology, printed page 395", + ), + "navamsha": _navamsha(birth_day, float(ascendant_longitude), longitudes), + "niryaana_shoola": _nine_year( + birth_day, + lagna, + longitudes, + rashi, + "goel_nirayana_shoola_v1", + "Predicting through Jaimini Astrology,印页 162", + "Predicting through Jaimini Astrology, printed page 162", + ), + "lagna_kendradi": _omitted( + "", + "", + "", + "本轮不上表:没有选定的安全默认算法,故本报告不列出该大运。", + "This report does not include Lagna Kendradi: no safe default method is selected.", + ), + "chara": _chara(birth_day, lagna, longitudes), + } diff --git a/scripts/research/freeze_rashi_dasha_pyjhora_fixture.py b/scripts/research/freeze_rashi_dasha_pyjhora_fixture.py new file mode 100644 index 00000000..47dced74 --- /dev/null +++ b/scripts/research/freeze_rashi_dasha_pyjhora_fixture.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Freeze PyJHora 4.8.7 rashi-dasha rows for four public or fictional charts. + +Run with the Anaconda interpreter that has PyJHora 4.8.7. Product code does +not import this module. Tests read the JSON only. +""" + +from __future__ import annotations + +import importlib +import importlib.metadata +import io +import json +import sys +from contextlib import redirect_stdout +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path[:0] = [str(ROOT), str(ROOT / "scripts")] + +OUT = ROOT / "tests" / "fixtures" / "rashi_dasha_pyjhora_20261008.json" +CASE_IDS = ( + "steve_jobs_1955_aa", + "barack_obama_1961_aa", + "einstein_1879_aa", + "fictional_reader_main", +) + + +def _births() -> dict: + public = json.loads((ROOT / "references" / "public_oracle_cases.json").read_text(encoding="utf-8")) + births = {case["id"]: case["birth"] for case in public["cases"]} + special = json.loads((ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json").read_text(encoding="utf-8")) + births["fictional_reader_main"] = special["cases"]["fictional_reader_main"]["birth"] + return births + + +def _parse_row(row): + if not isinstance(row, (list, tuple)) or len(row) < 3: + return None + if ( + len(row) >= 4 + and isinstance(row[0], int) + and isinstance(row[1], int) + and not isinstance(row[2], (int, float)) + ): + signs = [int(row[0]), int(row[1])] + start, years = row[2], row[3] + else: + lord, start, years = row[0], row[1], row[2] + signs = [int(item) for item in lord] if isinstance(lord, (list, tuple)) else [int(lord)] + if isinstance(start, (list, tuple)): + start_iso = f"{int(start[0]):04d}-{int(start[1]):02d}-{int(start[2]):02d}" + else: + start_iso = str(start)[:10] + years_n = float(years) + years_out = int(round(years_n)) if abs(years_n - round(years_n)) < 1e-6 else years_n + return signs, start_iso, years_out + + +def _call(fn, args): + with redirect_stdout(io.StringIO()): + return fn(*args) + + +def _pyjhora_case(birth: dict, modules, swe, drik, charts, Place) -> dict: + local_hour = birth["hour"] + birth["minute"] / 60.0 + birth.get("second", 0) / 3600.0 + with redirect_stdout(io.StringIO()): + drik.set_ayanamsa_mode("LAHIRI") + jd = swe.julday(birth["year"], birth["month"], birth["day"], local_hour) + place = Place("birth_place", birth["lat"], birth["lon"], birth["tz"]) + positions = charts.rasi_chart(jd, place) + dob = (birth["year"], birth["month"], birth["day"]) + tob = (birth["hour"], birth["minute"], birth["second"]) + calls = { + "sthira": lambda level: modules["sthira"].get_dhasa_antardhasa(dob, tob, place, dhasa_level_index=level), + "shoola": lambda level: modules["shoola"].shoola_dhasa(positions, dob, tob, dhasa_level_index=level), + "niryaana_shoola": lambda level: modules["nirayana"].nirayana_shoola_dhasa(positions, dob, tob, dhasa_level_index=level), + "drig": lambda level: modules["drig"].drig_dhasa(positions, dob, tob, dhasa_level_index=level), + "navamsha": lambda level: modules["navamsa"].get_dhasa_antardhasa(dob, tob, place, dhasa_level_index=level), + "chara": lambda level: modules["chara"].get_dhasa_antardhasa(dob, tob, place, dhasa_level_index=level), + "lagna_kendradi": lambda level: modules["lagna_kendraadhi"].lagna_kendradhi_rasi_dhasa(dob, tob, place, dhasa_level_index=level), + } + parsed = {} + for key, call in calls.items(): + level_1 = [_parse_row(row) for row in _call(call, (1,))] + level_2 = [_parse_row(row) for row in _call(call, (2,))] + majors = [row for row in level_1 if row and len(row[0]) == 1] + if not majors: + majors = [row for row in level_2 if row and len(row[0]) == 1] + subs = [row for row in level_2 if row and len(row[0]) == 2] + first_sign = majors[0][0][0] if majors else None + first_subs = [row[0][1] for row in subs if row[0][0] == first_sign] + parsed[key] = { + "sign_indexes": [row[0][0] for row in majors], + "years": [row[2] for row in majors], + "second_start": majors[1][1] if len(majors) > 1 else None, + "first_sub_sign_indexes": first_subs, + } + return parsed + + +def _succession(signs: list[int]) -> tuple[int, ...]: + return tuple((signs[index] - signs[index - 1]) % 12 for index in range(1, len(signs))) + + +def _diff_steps(section: dict, pyjhora: dict) -> list[str]: + our_signs = [row["sign_index"] for row in section["periods"]] + their_signs = pyjhora["sign_indexes"] + our_years = [row["years"] for row in section["periods"]] + their_years = pyjhora["years"] + steps = [] + if not our_signs or not their_signs or our_signs[0] != their_signs[0]: + steps.append("start_sign") + if _succession(our_signs) != _succession(their_signs): + steps.append("direction") + if sorted(zip(our_signs, our_years)) != sorted(zip(their_signs, their_years)): + steps.append("years") + our_sub = [row["sign_index"] for row in (section["periods"][0].get("antardasha") or [])] if section["periods"] else [] + if our_sub != (pyjhora.get("first_sub_sign_indexes") or []): + steps.append("subperiod") + if our_signs == their_signs and our_years == their_years and len(section["periods"]) > 1: + if section["periods"][1]["start"] != pyjhora.get("second_start"): + steps.append("date_basis") + return steps + + +def main() -> None: + with redirect_stdout(io.StringIO()): + import swisseph as swe + from jhora.horoscope.chart import charts + from jhora.panchanga import drik + from jhora.panchanga.drik import Place + + modules = { + name: importlib.import_module("jhora.horoscope.dhasa.raasi." + module_name) + for name, module_name in ( + ("sthira", "sthira"), + ("shoola", "shoola"), + ("nirayana", "nirayana"), + ("drig", "drig"), + ("navamsa", "navamsa"), + ("chara", "chara"), + ("lagna_kendraadhi", "lagna_kendraadhi"), + ) + } + from jyotish_engine import compute_chart_data + from rashi_dashas_native import calculate_rashi_dashas + + payload = { + "pyjhora_version": importlib.metadata.version("PyJHora"), + "generator_python_has_pyjhora": True, + "generator": "scripts/research/freeze_rashi_dasha_pyjhora_fixture.py", + "note": "Anaconda PyJHora 4.8.7 produced the external rows. Pytest reads this file and does not import PyJHora.", + "cases": {}, + } + births = _births() + for case_id in CASE_IDS: + birth = births[case_id] + chart, _asc, _jd, _ayanamsa = compute_chart_data( + birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], + birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, + ) + longitudes = { + name: float(row["degree_raw"]) + for name, row in chart["planets"].items() + if isinstance(row, dict) and "degree_raw" in row + } + native = calculate_rashi_dashas(chart["birth_info"]["date"], float(chart["ascendant"]["degree_raw"]), longitudes) + external = _pyjhora_case(birth, modules, swe, drik, charts, Place) + native_view = {} + diff_steps = {} + for key, section in native.items(): + if key == "lagna_kendradi": + diff_steps[key] = ["start_sign", "direction", "years", "subperiod"] + native_view[key] = {"status": "omitted", "sign_indexes": [], "years": []} + continue + native_view[key] = { + "sign_indexes": [row["sign_index"] for row in section["periods"]], + "years": [row["years"] for row in section["periods"]], + } + diff_steps[key] = _diff_steps(section, external[key]) + payload["cases"][case_id] = { + "birth_source": birth, + "pyjhora": external, + "native": native_view, + "diff_steps": diff_steps, + } + print(case_id, {key: value for key, value in diff_steps.items()}, flush=True) + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(OUT) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/rashi_dasha_pyjhora_20261008.json b/tests/fixtures/rashi_dasha_pyjhora_20261008.json new file mode 100644 index 00000000..0088963b --- /dev/null +++ b/tests/fixtures/rashi_dasha_pyjhora_20261008.json @@ -0,0 +1,2676 @@ +{ + "pyjhora_version": "4.8.7", + "generator_python_has_pyjhora": true, + "generator": "scripts/research/freeze_rashi_dasha_pyjhora_fixture.py", + "note": "Anaconda PyJHora 4.8.7 produced the external rows. Pytest reads this file and does not import PyJHora.", + "cases": { + "steve_jobs_1955_aa": { + "birth_source": { + "year": 1955, + "month": 2, + "day": 24, + "hour": 19, + "minute": 15, + "second": 0, + "lat": 37.7749, + "lon": -122.4194, + "tz": -8 + }, + "pyjhora": { + "sthira": { + "sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "years": [ + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8 + ], + "second_start": "1964-02-25", + "first_sub_sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ] + }, + "shoola": { + "sign_indexes": [ + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "1964-02-25", + "first_sub_sign_indexes": [ + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0 + ], + "years": [ + 9, + 8, + 7, + 9, + 8, + 7, + 9, + 8, + 7, + 9, + 8, + 7, + 3, + 4, + 5, + 3, + 4, + 5, + 3, + 4, + 5, + 3, + 4, + 5 + ], + "second_start": "1964-02-25", + "first_sub_sign_indexes": [ + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0 + ] + }, + "drig": { + "sign_indexes": [ + 0, + 4, + 7, + 10, + 1, + 3, + 6, + 9, + 2, + 5, + 8, + 11, + 0, + 4, + 7, + 10, + 1, + 3, + 6, + 9, + 2, + 5, + 8, + 11 + ], + "years": [ + 12, + 6, + 6, + 1, + 7, + 4, + 2, + 4, + 7, + 8, + 6, + 9, + 0, + 6, + 6, + 11, + 5, + 8, + 10, + 8, + 5, + 4, + 6, + 3 + ], + "second_start": "1967-02-24", + "first_sub_sign_indexes": [ + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + "navamsha": { + "sign_indexes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "1964-02-25", + "first_sub_sign_indexes": [ + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + "chara": { + "sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ], + "years": [ + 6, + 8, + 2, + 6, + 6, + 4, + 1, + 9, + 12, + 7, + 7, + 4 + ], + "second_start": "1961-02-24", + "first_sub_sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ] + }, + "lagna_kendradi": { + "sign_indexes": [ + 10, + 1, + 4, + 7, + 11, + 2, + 5, + 8, + 0, + 3, + 6, + 9, + 10, + 1, + 4, + 7, + 11, + 2, + 5, + 8, + 0, + 3, + 6, + 9 + ], + "years": [ + 1, + 7, + 6, + 6, + 9, + 7, + 8, + 6, + 12, + 4, + 2, + 4, + 11, + 5, + 6, + 6, + 3, + 5, + 4, + 6, + 0, + 8, + 10, + 8 + ], + "second_start": "1956-02-25", + "first_sub_sign_indexes": [ + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + } + }, + "native": { + "sthira": { + "sign_indexes": [ + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "years": [ + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8 + ] + }, + "drig": { + "sign_indexes": [ + 1, + 9, + 6, + 3, + 2, + 5, + 8, + 11, + 3, + 1, + 10, + 7 + ], + "years": [ + 7, + 3, + 2, + 4, + 7, + 8, + 6, + 9, + 4, + 7, + 4, + 5 + ] + }, + "shoola": { + "sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "navamsha": { + "sign_indexes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2 + ], + "years": [ + 9, + 7, + 3, + 3, + 6, + 7, + 8, + 5, + 10, + 3, + 6, + 2 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "lagna_kendradi": { + "status": "omitted", + "sign_indexes": [], + "years": [] + }, + "chara": { + "sign_indexes": [ + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4 + ], + "years": [ + 8, + 2, + 6, + 6, + 4, + 5, + 9, + 12, + 7, + 7, + 4, + 6 + ] + } + }, + "diff_steps": { + "sthira": [ + "start_sign", + "subperiod" + ], + "drig": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "shoola": [ + "start_sign", + "subperiod" + ], + "navamsha": [ + "start_sign", + "years", + "subperiod" + ], + "niryaana_shoola": [ + "direction", + "years", + "subperiod" + ], + "lagna_kendradi": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "chara": [ + "start_sign", + "years", + "subperiod" + ] + } + }, + "barack_obama_1961_aa": { + "birth_source": { + "year": 1961, + "month": 8, + "day": 4, + "hour": 19, + "minute": 24, + "second": 0, + "lat": 21.3069, + "lon": -157.8583, + "tz": -10 + }, + "pyjhora": { + "sthira": { + "sign_indexes": [ + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "years": [ + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9 + ], + "second_start": "1968-08-04", + "first_sub_sign_indexes": [ + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "shoola": { + "sign_indexes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "1970-08-05", + "first_sub_sign_indexes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ], + "years": [ + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 4, + 3, + 5, + 4, + 3, + 5, + 4, + 3, + 5, + 4, + 3, + 5 + ], + "second_start": "1969-08-04", + "first_sub_sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ] + }, + "drig": { + "sign_indexes": [ + 5, + 2, + 11, + 8, + 6, + 10, + 1, + 4, + 7, + 9, + 0, + 3, + 5, + 2, + 11, + 8, + 6, + 10, + 1, + 4, + 7, + 9, + 0, + 3 + ], + "years": [ + 2, + 1, + 1, + 0, + 8, + 1, + 1, + 1, + 9, + 12, + 4, + 3, + 10, + 11, + 11, + 12, + 4, + 11, + 11, + 11, + 3, + 0, + 8, + 9 + ], + "second_start": "1963-08-05", + "first_sub_sign_indexes": [ + 5, + 2, + 11, + 8, + 1, + 10, + 7, + 4, + 9, + 6, + 3, + 0, + 5, + 2, + 11, + 8, + 1, + 10, + 7, + 4, + 9, + 6, + 3, + 0 + ] + }, + "navamsha": { + "sign_indexes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "1970-08-05", + "first_sub_sign_indexes": [ + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + "chara": { + "sign_indexes": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10 + ], + "years": [ + 12, + 0, + 9, + 8, + 2, + 1, + 3, + 1, + 1, + 4, + 1, + 1 + ], + "second_start": "1973-08-04", + "first_sub_sign_indexes": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10 + ] + }, + "lagna_kendradi": { + "sign_indexes": [ + 3, + 0, + 9, + 6, + 2, + 11, + 8, + 5, + 1, + 10, + 7, + 4, + 3, + 0, + 9, + 6, + 2, + 11, + 8, + 5, + 1, + 10, + 7, + 4 + ], + "years": [ + 3, + 4, + 12, + 8, + 1, + 1, + 0, + 2, + 1, + 1, + 9, + 1, + 9, + 8, + 0, + 4, + 11, + 11, + 12, + 10, + 11, + 11, + 3, + 11 + ], + "second_start": "1964-08-04", + "first_sub_sign_indexes": [ + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4 + ] + } + }, + "native": { + "sthira": { + "sign_indexes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2 + ], + "years": [ + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9 + ] + }, + "drig": { + "sign_indexes": [ + 5, + 2, + 11, + 8, + 6, + 10, + 1, + 4, + 7, + 3, + 0, + 9 + ], + "years": [ + 2, + 1, + 2, + 1, + 8, + 1, + 1, + 1, + 10, + 2, + 5, + 12 + ] + }, + "shoola": { + "sign_indexes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "navamsha": { + "sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ], + "years": [ + 6, + 12, + 3, + 11, + 4, + 12, + 12, + 12, + 10, + 6, + 5, + 4 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "lagna_kendradi": { + "status": "omitted", + "sign_indexes": [], + "years": [] + }, + "chara": { + "sign_indexes": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10 + ], + "years": [ + 12, + 0, + 3, + 8, + 2, + 1, + 3, + 1, + 1, + 5, + 1, + 1 + ] + } + }, + "diff_steps": { + "sthira": [ + "start_sign", + "subperiod" + ], + "drig": [ + "direction", + "years", + "subperiod" + ], + "shoola": [ + "subperiod", + "date_basis" + ], + "navamsha": [ + "start_sign", + "years", + "subperiod" + ], + "niryaana_shoola": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "lagna_kendradi": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "chara": [ + "years", + "subperiod" + ] + } + }, + "einstein_1879_aa": { + "birth_source": { + "year": 1879, + "month": 3, + "day": 14, + "hour": 11, + "minute": 30, + "second": 0, + "lat": 48.4, + "lon": 9.983333, + "tz": 0.883333 + }, + "pyjhora": { + "sthira": { + "sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "years": [ + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8 + ], + "second_start": "1888-03-13", + "first_sub_sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ] + }, + "shoola": { + "sign_indexes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "1888-03-13", + "first_sub_sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10 + ], + "years": [ + 7, + 9, + 8, + 7, + 9, + 8, + 7, + 9, + 8, + 7, + 9, + 8, + 5, + 3, + 4, + 5, + 3, + 4, + 5, + 3, + 4, + 5, + 3, + 4 + ], + "second_start": "1886-03-14", + "first_sub_sign_indexes": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10 + ] + }, + "drig": { + "sign_indexes": [ + 10, + 6, + 3, + 0, + 11, + 8, + 5, + 2, + 0, + 4, + 7, + 10, + 10, + 6, + 3, + 0, + 11, + 8, + 5, + 2, + 0, + 4, + 7, + 10 + ], + "years": [ + 11, + 6, + 7, + 10, + 1, + 2, + 5, + 8, + 10, + 5, + 3, + 11, + 1, + 6, + 5, + 2, + 11, + 10, + 7, + 4, + 2, + 7, + 9, + 1 + ], + "second_start": "1890-03-14", + "first_sub_sign_indexes": [ + 10, + 3, + 8, + 1, + 6, + 11, + 4, + 9, + 2, + 7, + 0, + 5, + 10, + 3, + 8, + 1, + 6, + 11, + 4, + 9, + 2, + 7, + 0, + 5, + 10, + 3, + 8, + 1, + 6, + 11, + 4, + 9, + 2, + 7, + 0, + 5, + 10, + 3, + 8, + 1, + 6, + 11, + 4, + 9, + 2, + 7, + 0, + 5 + ] + }, + "navamsha": { + "sign_indexes": [ + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "1888-03-13", + "first_sub_sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ] + }, + "chara": { + "sign_indexes": [ + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3 + ], + "years": [ + 8, + 11, + 10, + 1, + 11, + 10, + 2, + 3, + 6, + 5, + 5, + 7 + ], + "second_start": "1887-03-14", + "first_sub_sign_indexes": [ + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3 + ] + }, + "lagna_kendradi": { + "sign_indexes": [ + 2, + 5, + 8, + 11, + 3, + 6, + 9, + 0, + 4, + 7, + 10, + 1, + 2, + 5, + 8, + 11, + 3, + 6, + 9, + 0, + 4, + 7, + 10, + 1 + ], + "years": [ + 8, + 5, + 2, + 1, + 7, + 6, + 10, + 10, + 5, + 3, + 11, + 11, + 4, + 7, + 10, + 11, + 5, + 6, + 2, + 2, + 7, + 9, + 1, + 1 + ], + "second_start": "1887-03-14", + "first_sub_sign_indexes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1 + ] + } + }, + "native": { + "sthira": { + "sign_indexes": [ + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "years": [ + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8 + ] + }, + "drig": { + "sign_indexes": [ + 10, + 0, + 3, + 6, + 11, + 8, + 5, + 2, + 0, + 4, + 7, + 10 + ], + "years": [ + 11, + 9, + 8, + 5, + 1, + 2, + 6, + 9, + 9, + 5, + 2, + 11 + ] + }, + "shoola": { + "sign_indexes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "navamsha": { + "sign_indexes": [ + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4 + ], + "years": [ + 12, + 5, + 10, + 12, + 3, + 10, + 4, + 11, + 4, + 5, + 6, + 12 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "lagna_kendradi": { + "status": "omitted", + "sign_indexes": [], + "years": [] + }, + "chara": { + "sign_indexes": [ + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3 + ], + "years": [ + 8, + 11, + 10, + 1, + 11, + 10, + 2, + 8, + 6, + 5, + 5, + 7 + ] + } + }, + "diff_steps": { + "sthira": [ + "date_basis" + ], + "drig": [ + "direction", + "years", + "subperiod" + ], + "shoola": [ + "subperiod", + "date_basis" + ], + "navamsha": [ + "start_sign", + "years", + "subperiod" + ], + "niryaana_shoola": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "lagna_kendradi": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "chara": [ + "years", + "subperiod" + ] + } + }, + "fictional_reader_main": { + "birth_source": { + "year": 1991, + "month": 4, + "day": 7, + "hour": 9, + "minute": 15, + "second": 0, + "lat": 30.5728, + "lon": 104.0668, + "tz": 8.0 + }, + "pyjhora": { + "sthira": { + "sign_indexes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "years": [ + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9 + ], + "second_start": "1998-04-07", + "first_sub_sign_indexes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] + }, + "shoola": { + "sign_indexes": [ + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "2000-04-06", + "first_sub_sign_indexes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "years": [ + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 3, + 5, + 4, + 3, + 5, + 4, + 3, + 5, + 4, + 3, + 5, + 4 + ], + "second_start": "2000-04-06", + "first_sub_sign_indexes": [ + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "drig": { + "sign_indexes": [ + 9, + 7, + 4, + 1, + 10, + 6, + 3, + 0, + 11, + 8, + 5, + 2, + 9, + 7, + 4, + 1, + 10, + 6, + 3, + 0, + 11, + 8, + 5, + 2 + ], + "years": [ + 12, + 8, + 5, + 11, + 1, + 6, + 7, + 2, + 9, + 8, + 5, + 10, + 0, + 4, + 7, + 1, + 11, + 6, + 5, + 10, + 3, + 4, + 7, + 2 + ], + "second_start": "2003-04-07", + "first_sub_sign_indexes": [ + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4 + ] + }, + "navamsha": { + "sign_indexes": [ + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ], + "second_start": "2000-04-06", + "first_sub_sign_indexes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3 + ] + }, + "chara": { + "sign_indexes": [ + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2 + ], + "years": [ + 11, + 2, + 9, + 1, + 12, + 8, + 8, + 6, + 5, + 5, + 7, + 10 + ], + "second_start": "2002-04-07", + "first_sub_sign_indexes": [ + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2 + ] + }, + "lagna_kendradi": { + "sign_indexes": [ + 7, + 4, + 1, + 10, + 6, + 3, + 0, + 9, + 5, + 2, + 11, + 8, + 7, + 4, + 1, + 10, + 6, + 3, + 0, + 9, + 5, + 2, + 11, + 8 + ], + "years": [ + 8, + 5, + 11, + 1, + 6, + 7, + 2, + 12, + 5, + 10, + 9, + 8, + 4, + 7, + 1, + 11, + 6, + 5, + 10, + 0, + 7, + 2, + 3, + 4 + ], + "second_start": "1999-04-07", + "first_sub_sign_indexes": [ + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 11, + 10, + 9, + 8 + ] + } + }, + "native": { + "sthira": { + "sign_indexes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 0, + 1 + ], + "years": [ + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8, + 9, + 7, + 8 + ] + }, + "drig": { + "sign_indexes": [ + 9, + 7, + 4, + 1, + 10, + 0, + 3, + 6, + 11, + 8, + 5, + 2 + ], + "years": [ + 12, + 7, + 5, + 12, + 1, + 2, + 7, + 7, + 8, + 7, + 5, + 10 + ] + }, + "shoola": { + "sign_indexes": [ + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "navamsha": { + "sign_indexes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "years": [ + 9, + 5, + 12, + 10, + 7, + 5, + 4, + 12, + 11, + 10, + 3, + 6 + ] + }, + "niryaana_shoola": { + "sign_indexes": [ + 7, + 8, + 9, + 10, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "years": [ + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9, + 9 + ] + }, + "lagna_kendradi": { + "status": "omitted", + "sign_indexes": [], + "years": [] + }, + "chara": { + "sign_indexes": [ + 1, + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2 + ], + "years": [ + 12, + 2, + 9, + 1, + 12, + 8, + 8, + 7, + 5, + 5, + 7, + 10 + ] + } + }, + "diff_steps": { + "sthira": [ + "start_sign", + "subperiod" + ], + "drig": [ + "direction", + "years", + "subperiod" + ], + "shoola": [ + "subperiod", + "date_basis" + ], + "navamsha": [ + "start_sign", + "years", + "subperiod" + ], + "niryaana_shoola": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "lagna_kendradi": [ + "start_sign", + "direction", + "years", + "subperiod" + ], + "chara": [ + "years", + "subperiod" + ] + } + } + } +} diff --git a/tests/test_rashi_dashas_native.py b/tests/test_rashi_dashas_native.py new file mode 100644 index 00000000..ca487bdd --- /dev/null +++ b/tests/test_rashi_dashas_native.py @@ -0,0 +1,276 @@ +"""Book-shaped rashi dasha rows. These tests do not import PyJHora.""" + +from __future__ import annotations + +import ast +import json +from datetime import date +from pathlib import Path + +from scripts.goel_short_strength import dignity_units, graha_strengths, rashi_strengths +from scripts.rashi_dashas_native import ( + calculate_rashi_dashas, + drig_aspect_sequence, + navamsha_sign, + navamsha_years, +) +from scripts.rashi_dasha_report import render_rashi_dasha_chapter + +ROOT = Path(__file__).resolve().parents[1] +FIXTURE = ROOT / "tests" / "fixtures" / "rashi_dasha_pyjhora_20261008.json" +GOEL_LONS = { + "Sun": 0 * 30 + 20.18, + "Moon": 7 * 30 + 20.15, + "Mars": 7 * 30 + 23.02, + "Mercury": 1 * 30 + 11.12, + "Jupiter": 5 * 30 + 3.13, + "Venus": 11 * 30 + 17.28, + "Saturn": 0 * 30 + 7.07, +} +CAPRICORN_STRONGER = { + "Sun": 9 * 30 + 1, + "Moon": 9 * 30 + 2, + "Mars": 9 * 30 + 3, + "Mercury": 9 * 30 + 4, + "Jupiter": 9 * 30 + 5, + "Venus": 9 * 30 + 6, + "Saturn": 6 * 30 + 15, + "Rahu": 15.0, + "Ketu": 195.0, +} + + +def test_goel_short_method_matches_the_printed_totals_and_direct_sthira() -> None: + graha = graha_strengths(GOEL_LONS) + assert [graha[name]["total"] for name in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")] == [ + 150, 120, 180, 130, 70, 150, 50, + ] + rashi = rashi_strengths(GOEL_LONS, graha) + assert [rashi[index]["total"] for index in range(12)] == [ + 280, 260, 250, 260, 250, 260, 230, 300, 250, 130, 150, 260, + ] + packet = calculate_rashi_dashas(date(2001, 1, 1), 5 * 30 + 1.55, GOEL_LONS) + sthira = packet["sthira"] + assert sthira["profile_id"] == "goel_rao_short_brahma_method_v1" + assert sthira["brahma_planet"] == "Mars" + assert [row["sign_index"] for row in sthira["periods"]] == list(range(7, 12)) + list(range(0, 7)) + assert [row["years"] for row in sthira["periods"]] == [8, 9, 7, 8, 9, 7, 8, 9, 7, 8, 9, 7] + aquarius = sthira["periods"][3] + assert aquarius["sign"] == "Aquarius" + assert aquarius["start"] == "2025-01-01" + assert [row["sign"] for row in aquarius["antardasha"][:2]] == ["Aquarius", "Pisces"] + assert aquarius["antardasha"][0]["end"] == "2025-09-01" + assert "pratyantar" not in aquarius + + +def test_same_goel_chart_niryaana_uses_short_bala_not_the_printed_jaimini_start() -> None: + # Printed Jaimini rashi bala on this chart starts Pisces. Short-method totals tie, + # and the karaka rank of the lords selects Virgo. The report keeps the short method. + packet = calculate_rashi_dashas(date(2001, 1, 1), 5 * 30 + 1.55, GOEL_LONS) + niryaana = packet["niryaana_shoola"] + assert niryaana["periods"][0]["sign"] == "Virgo" + assert [row["years"] for row in niryaana["periods"]] == [9] * 12 + assert niryaana["periods"][0]["antardasha"][0]["sign"] == "Virgo" + assert niryaana["periods"][0]["antardasha"][0]["end"] == "2001-10-01" + + +def test_drig_aspect_order_for_a_cancer_lagna_and_narayana_style_years() -> None: + assert drig_aspect_sequence(3) == [11, 8, 5, 2, 0, 4, 7, 10, 1, 9, 6, 3] + packet = calculate_rashi_dashas(date(2001, 1, 1), 3 * 30 + 1.0, GOEL_LONS) + drig = packet["drig"] + assert drig["profile_id"] == "drig_rath_aspect_sequence_v1" + assert [row["sign_index"] for row in drig["periods"]] == drig_aspect_sequence(3) + assert all(isinstance(row["years"], int) and 0 <= row["years"] <= 12 for row in drig["periods"]) + + +def test_shoola_from_the_stronger_seventh_puts_aries_at_year_27() -> None: + packet = calculate_rashi_dashas(date(2001, 1, 1), 3 * 30 + 1.0, CAPRICORN_STRONGER) + shoola = packet["shoola"] + assert shoola["profile_id"] == "shoola_forward_md_direct_ad_v1" + assert [row["sign"] for row in shoola["periods"][:4]] == ["Capricorn", "Aquarius", "Pisces", "Aries"] + assert shoola["periods"][3]["start"] == "2028-01-01" + assert shoola["periods"][3]["years"] == 9 + assert shoola["periods"][0]["antardasha"][0]["sign"] == "Capricorn" + + +def test_navamsha_method_1_starts_from_the_seventh_when_d9_lagna_is_even() -> None: + longitude = 3 * 30 + 26.7 + assert navamsha_sign(longitude) == 11 + assert navamsha_years(0, 0) == 12 + assert navamsha_years(0, 1) == 2 + assert navamsha_years(1, 0) == 2 + packet = calculate_rashi_dashas(date(2001, 1, 1), longitude, GOEL_LONS) + navamsha = packet["navamsha"] + assert navamsha["profile_id"] == "goel_method_1_v1" + assert navamsha["d9_lagna_index"] == 11 + assert navamsha["periods"][0]["sign"] == "Virgo" + assert navamsha["periods"][0]["antardasha"][0]["sign"] == "Virgo" + assert "pratyantar" not in navamsha["periods"][0] + + +def test_chara_keeps_the_existing_kn_rao_order_and_a_zero_year_row() -> None: + longitudes = {name: 10.0 for name in ("Sun", "Moon", "Mars", "Mercury", "Venus", "Saturn", "Rahu")} + longitudes["Jupiter"] = 9 * 30 + 5.0 + longitudes["Ketu"] = 190.0 + packet = calculate_rashi_dashas(date(2001, 1, 1), 10.0, longitudes) + chara = packet["chara"] + assert chara["profile_id"] == "kn_rao" + assert [row["sign_index"] for row in chara["periods"]] == list(range(12)) + sagittarius = chara["periods"][8] + assert sagittarius["sign"] == "Sagittarius" + assert sagittarius["years"] == 0 + assert sagittarius["start"] == sagittarius["end"] + assert chara["periods"][0]["antardasha"][0]["sign"] == "Taurus" + assert chara["periods"][0]["antardasha"][-1]["sign"] == "Aries" + + +def test_lagna_kendradi_is_omitted() -> None: + packet = calculate_rashi_dashas(date(2001, 1, 1), 10.0, GOEL_LONS) + section = packet["lagna_kendradi"] + assert section["status"] == "omitted" + assert section["periods"] == [] + + +def test_mercury_in_virgo_uses_longitude_bands() -> None: + assert dignity_units("Mercury", 5, 10.0) == 70 + assert dignity_units("Mercury", 5, 16.0) == 60 + assert dignity_units("Mercury", 5, 22.0) == 50 + + +def test_report_modules_do_not_call_the_old_shoola_or_niryaana_helpers() -> None: + for name in ("rashi_dashas_native.py", "rashi_dasha_report.py", "goel_short_strength.py"): + source = (ROOT / "scripts" / name).read_text(encoding="utf-8") + tree = ast.parse(source) + imported = [] + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module: + imported.append(node.module) + elif isinstance(node, ast.Import): + imported.extend(alias.name for alias in node.names) + assert "niryaana_shoola_dasha" not in imported + assert "extended_dashas" not in imported + assert all(not item.split(".")[0] == "jhora" for item in imported) + + +def test_pyjhora_fixture_diffs_stay_on_the_recorded_steps() -> None: + payload = json.loads(FIXTURE.read_text(encoding="utf-8")) + assert payload["pyjhora_version"] == "4.8.7" + assert payload["generator_python_has_pyjhora"] is True + from scripts.jyotish_engine import compute_chart_data + + for case_id, case in payload["cases"].items(): + birth = case["birth_source"] + chart, _asc, _jd, _ayanamsa = compute_chart_data( + birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], + birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, + ) + assert chart is not None + longitudes = { + name: float(row["degree_raw"]) + for name, row in chart["planets"].items() + if isinstance(row, dict) and "degree_raw" in row + } + native = calculate_rashi_dashas(chart["birth_info"]["date"], float(chart["ascendant"]["degree_raw"]), longitudes) + for key, expected in case["diff_steps"].items(): + section = native[key] + recorded = case["native"][key] + if key == "lagna_kendradi": + assert section["status"] == "omitted" + assert expected == ["start_sign", "direction", "years", "subperiod"] + continue + assert [row["sign_index"] for row in section["periods"]] == recorded["sign_indexes"] + assert [row["years"] for row in section["periods"]] == recorded["years"] + assert _diff_steps(section, case["pyjhora"][key]) == expected + + +def _succession(signs: list[int]) -> tuple[int, ...]: + return tuple((signs[index] - signs[index - 1]) % 12 for index in range(1, len(signs))) + + +def _diff_steps(section: dict, pyjhora: dict) -> list[str]: + our_signs = [row["sign_index"] for row in section["periods"]] + their_signs = pyjhora["sign_indexes"] + our_years = [row["years"] for row in section["periods"]] + their_years = pyjhora["years"] + steps = [] + if not our_signs or not their_signs or our_signs[0] != their_signs[0]: + steps.append("start_sign") + if _succession(our_signs) != _succession(their_signs): + steps.append("direction") + if sorted(zip(our_signs, our_years)) != sorted(zip(their_signs, their_years)): + steps.append("years") + our_sub = [row["sign_index"] for row in (section["periods"][0].get("antardasha") or [])] if section["periods"] else [] + their_sub = pyjhora.get("first_sub_sign_indexes") or [] + if our_sub != their_sub: + steps.append("subperiod") + if our_signs == their_signs and our_years == their_years and section["periods"][1:2]: + if section["periods"][1]["start"] != pyjhora.get("second_start"): + steps.append("date_basis") + return steps + + +def test_jobs_chapter_names_the_method_and_leaves_kendradi_out() -> None: + payload = json.loads(FIXTURE.read_text(encoding="utf-8")) + birth = payload["cases"]["steve_jobs_1955_aa"]["birth_source"] + from scripts.jyotish_engine import compute_chart_data + + chart, _asc, _jd, _ayanamsa = compute_chart_data( + birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], + birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, + ) + packet = { + "report_language": "zh", + "birth_info": {"date": chart["birth_info"]["date"], "name": "steve_jobs_1955_aa"}, + "core_chart": {"ascendant": chart["ascendant"], "planets": chart["planets"]}, + } + text = render_rashi_dasha_chapter(packet) + for title in ("Sthira", "Niryaana Shoola", "Drig", "Navamsha", "Shoola", "Chara"): + assert f"### {title}" in text + assert "本站选用的方法:" in text + assert "未与外部软件逐一核对" in text + assert "K.N. Rao" in text + assert "不是 Narayana 表" in text + kendradi = text.split("### Lagna Kendradi", 1)[1] + assert "本轮不上表" in kendradi + assert "| 序号 | 星座 |" not in kendradi + english = render_rashi_dasha_chapter({**packet, "report_language": "en"}) + assert "Method used on this site:" in english + assert "This report does not include Lagna Kendradi" in english + assert "not checked one-by-one against external software." in english + leaked = [char for char in english if "\u4e00" <= char <= "\u9fff"] + assert leaked == [] + + +def test_full_data_report_appends_the_chapter_without_touching_a_chartless_packet() -> None: + from scripts.pl9_full_data_export import render_full_data_markdown + from tests.test_report_full_data_fix import _packet + + chartless = render_full_data_markdown(_packet("zh")) + assert "Sthira Dasha" not in chartless + payload = json.loads(FIXTURE.read_text(encoding="utf-8")) + birth = payload["cases"]["steve_jobs_1955_aa"]["birth_source"] + from scripts.jyotish_engine import compute_chart_data + + chart, _asc, _jd, _ayanamsa = compute_chart_data( + birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], + birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, + ) + packet = _packet("zh") + packet["birth_info"]["date"] = chart["birth_info"]["date"] + packet["core_chart"] = {"ascendant": chart["ascendant"], "planets": chart["planets"]} + text = render_full_data_markdown(packet) + assert "星座大运" in text + assert "Sthira" in text + assert "本站选用的方法" in text + assert "Kendradi" in text + assert "本轮不上表" in text + kendradi = text.split("Kendradi", 1)[1] + assert "| 序号 | 星座 |" not in kendradi + english_packet = _packet("en") + english_packet["birth_info"]["date"] = chart["birth_info"]["date"] + english_packet["core_chart"] = packet["core_chart"] + english = render_full_data_markdown(english_packet) + assert "Sthira" in english + assert "Method used on this site:" in english + assert "This report does not include Lagna Kendradi" in english + assert not any("\u4e00" <= char <= "\u9fff" for char in english)