fix(special-lagnas): compute eight birth points without PyJHora
Production images do not install PyJHora, so those eight points were blocked. Swiss Ephemeris now computes them in-process. Rectification keeps the old Hora and Ghati formulas. BUG-1273.
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# 印度占星 Skill 更新日志
|
# 印度占星 Skill 更新日志
|
||||||
|
|
||||||
|
## 2026-10-08 — 特殊上升点在没有 PyJHora 的环境里也能算出(未上线)
|
||||||
|
|
||||||
|
- 生产镜像装不了 PyJHora,Bhava、Hora、Ghati、Vighati、Sree、Indu、Pranapada、Varnada 这八个点以前算不出来。现在用 Swiss Ephemeris 在本进程里算(BUG-1273)。
|
||||||
|
- 生时校正仍用原来的 Hora、Ghati 公式。校正算法身份不升,Skill 版本不变。
|
||||||
|
|
||||||
## 2026-10-07 — 分盘、Yogini 和特殊上升按原书重算(未上线)
|
## 2026-10-07 — 分盘、Yogini 和特殊上升按原书重算(未上线)
|
||||||
|
|
||||||
- D5、D6、D8、D11 改按 PVR《Integrated Approach》6.2 的分段起算。学业、健康、财富这几张分盘上的星座会和以前不同(BUG-1260)。
|
- D5、D6、D8、D11 改按 PVR《Integrated Approach》6.2 的分段起算。学业、健康、财富这几张分盘上的星座会和以前不同(BUG-1260)。
|
||||||
|
|||||||
+19
-2
@@ -16986,9 +16986,9 @@
|
|||||||
|
|
||||||
## BUG-1265 | 特殊上升点用同一套公式,Hora 与 Ghati 永远相同
|
## BUG-1265 | 特殊上升点用同一套公式,Hora 与 Ghati 永远相同
|
||||||
|
|
||||||
- 状态:resolved
|
- 状态:resolved(生产镜像缺 PyJHora 的回归由 BUG-1273 关闭;`4b9c0198` 在无 PyJHora 的镜像上并未算出这八个点)
|
||||||
- 首次发现:2026-10-07
|
- 首次发现:2026-10-07
|
||||||
- 最近更新:2026-10-07
|
- 最近更新:2026-10-08
|
||||||
- 影响面:全盘报告的特殊上升点;`scripts/special_lagnas.py`
|
- 影响面:全盘报告的特殊上升点;`scripts/special_lagnas.py`
|
||||||
- 用户现象:公开盘上 Hora 与 Ghati 落在同一星座同一度数,报告里没有 Sree、Indu、Pranapada、Varnada
|
- 用户现象:公开盘上 Hora 与 Ghati 落在同一星座同一度数,报告里没有 Sree、Indu、Pranapada、Varnada
|
||||||
- 触发条件:全盘计算特殊上升点
|
- 触发条件:全盘计算特殊上升点
|
||||||
@@ -17000,6 +17000,7 @@
|
|||||||
- 复发自:无
|
- 复发自:无
|
||||||
- 修复版本:本分支 `4b9c0198`
|
- 修复版本:本分支 `4b9c0198`
|
||||||
- 验收补记(Claude,2026-10-08):生产与 staging 镜像只装 `requirements.txt`,不含 PyJHora(AGPL,只能作外部对照)。屏蔽 `jhora` 导入后,乔布斯盘 `Hora_Lagna`、`Ghati_Lagna`、`Sree_Lagna`、`Indu_Lagna`、`Pranapada_Lagna` 全部 `blocked`(`pyjhora_unavailable`)。即 staging `2df6ee72` 上这些点不显示。sync6 验收机装了 PyJHora,没看出。修复单 `docs/tasks/TASK-special-lagnas-native-20261008.md`(原生实现,PyJHora 只作冻结对照答案)。状态应视为 regressed,待修复单关闭。
|
- 验收补记(Claude,2026-10-08):生产与 staging 镜像只装 `requirements.txt`,不含 PyJHora(AGPL,只能作外部对照)。屏蔽 `jhora` 导入后,乔布斯盘 `Hora_Lagna`、`Ghati_Lagna`、`Sree_Lagna`、`Indu_Lagna`、`Pranapada_Lagna` 全部 `blocked`(`pyjhora_unavailable`)。即 staging `2df6ee72` 上这些点不显示。sync6 验收机装了 PyJHora,没看出。修复单 `docs/tasks/TASK-special-lagnas-native-20261008.md`(原生实现,PyJHora 只作冻结对照答案)。状态应视为 regressed,待修复单关闭。
|
||||||
|
- 回归关闭(2026-10-08,BUG-1273):同一条「屏蔽 `jhora` 再算全盘」的路径上,八个点(含 Vighati、Varnada)都是 `computed`,`source` 为 `native_special_lagna`。历史修复段仍指 `4b9c0198` 的 PyJHora 调用,生产不可用不是那一版能单独解决的。这版代码尚未换成线上镜像。
|
||||||
|
|
||||||
## BUG-1269 | 导入引擎时的岁差引导盖掉调用方已选的模式
|
## BUG-1269 | 导入引擎时的岁差引导盖掉调用方已选的模式
|
||||||
|
|
||||||
@@ -17113,3 +17114,19 @@
|
|||||||
- 相关记录:BUG-1254(数据库槽位排队)、BUG-266(runner 资源回收)、BUG-1260、BUG-1268。
|
- 相关记录:BUG-1254(数据库槽位排队)、BUG-266(runner 资源回收)、BUG-1260、BUG-1268。
|
||||||
- 复发自:无。
|
- 复发自:无。
|
||||||
- 修复版本:无(运行机侧)。
|
- 修复版本:无(运行机侧)。
|
||||||
|
|
||||||
|
## BUG-1273 | 生产镜像没有 PyJHora,八个特殊上升点算不出来
|
||||||
|
|
||||||
|
- 状态:resolved(代码已在本分支;未上线,部署以 `/api/health` 的 `deployment.gitCommit` 为准)
|
||||||
|
- 首次发现:2026-10-08
|
||||||
|
- 最近更新:2026-10-08
|
||||||
|
- 影响面:全盘、报告和对话卡里的 Bhava、Hora、Ghati、Vighati、Sree、Indu、Pranapada、Varnada。生时校正打分不走这条路径
|
||||||
|
- 用户现象:staging `2df6ee72` 上这些点不显示。生产环境因此暂时没有这八个点
|
||||||
|
- 触发条件:镜像只装 `requirements.txt`(没有 PyJHora)时计算特殊上升点
|
||||||
|
- 根因:BUG-1265 的修复在有经纬度时调用 PyJHora。PyJHora 在 `requirements-reference-engines.txt`,许可证是 AGPL,不能进生产镜像。验收机装了 PyJHora,门禁 `.venv` 和生产镜像都没有。复发类型:验收环境与生产不一致
|
||||||
|
- 修复:八个点改由 Swiss Ephemeris 在本进程计算,日出用出生地经纬度和请求的岁差。`source` 为 `native_special_lagna`。PyJHora 4.8.7 只用来离线冻结对照答案。运行时的 `calculate_pyjhora_special_lagnas` 已删除。生时校正仍调用原来的 Hora / Ghati / Bhava 公式,不升算法身份。Tajika 的进程内 PyJHora 打分和校正三引擎对照里的 PyJHora 列,分别挪到文件名含 pyjhora 的参考模块,默认全盘不调用
|
||||||
|
- 验证:`tests/test_special_lagnas_birth_instant.py`。三张公开或虚构盘、Lahiri 与 Raman,八点与冻结的 PyJHora 4.8.7 相差不超过 0.01°(实测最大绝对差 4.9e-5°);Hora 与 Ghati 度数不同。屏蔽 `jhora` 导入后跑 `cmd_full_reading`,八点都是 `computed`,`source` 不含 `pyjhora`。缺坐标时旧的 Bhava / Hora / Ghati 公式仍在,另外五点为 blocked。`tests/test_runtime_jhora_import_scan.py` 扫描全盘、报告、对话卡入口的导入图。本机有 PyJHora,验收用的是屏蔽导入,不是一个未安装 PyJHora 的虚拟环境。Linux 全量 pytest 与快速门整段未在本机跑完
|
||||||
|
- 防复发:上述度数对照,以及快速门里的导入扫描(`scripts/run_quality_gate.py` 的 quick 列表)
|
||||||
|
- 相关记录:BUG-1265
|
||||||
|
- 复发自:BUG-1265
|
||||||
|
- 修复版本:本分支 `codex/special-lagnas-native-20261008`
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# PROGRESS · 特殊上升点改为本仓原生计算(2026-10-08)
|
||||||
|
|
||||||
|
执行分支 `codex/special-lagnas-native-20261008`。工作树从当时的 `origin/staging` `fe833b35` 拉出(任务书写的基线是 `c04786f7`,其后 staging 又有纯文档提交)。状态:待验收。未上线。
|
||||||
|
|
||||||
|
## 做了什么
|
||||||
|
|
||||||
|
- 八个特殊上升点在有经纬度时改由 Swiss Ephemeris 计算,`source` 为 `native_special_lagna`。日出用出生地经纬度和请求的岁差,算完恢复原来的岁差。
|
||||||
|
- 公式按本仓已引用的出处:P.V.R. Narasimha Rao《Vedic Astrology: An Integrated Approach》§5.2(Bhava 0.25°/分、Hora 0.5°/分、Ghati 1.25°/分、Vighati 15°/分),Sree 为月亮在星宿内剩余度数乘 27 再加上升,Indu 与 Varnada 用 B.V. Raman。没有把「一宫每一 vighati」写成 75°/分。`references/` 里没有 BPHS 第 4 章原文,八点都有上述具名出处,没有写 blocked。
|
||||||
|
- 运行时删除 `calculate_pyjhora_special_lagnas`。PyJHora 4.8.7 只离线生成 `tests/fixtures/special_lagnas_pyjhora_487.json`。生成命令:`python scripts/research/freeze_special_lagnas_pyjhora_fixture.py`。
|
||||||
|
- 缺坐标时仍用原来的 Bhava / Hora / Ghati 公式。生时校正继续调用这三个函数,算法身份不升。
|
||||||
|
- Tajika 的进程内 PyJHora 打分、校正三引擎对照的 PyJHora 列,分别挪到 `scripts/pyjhora_panchavargiya_reference.py` 和 `scripts/pyjhora_rectification_d1_reference.py`。默认全盘不调用它们。
|
||||||
|
- 快速门增加 `tests/test_runtime_jhora_import_scan.py`。
|
||||||
|
|
||||||
|
## 对照
|
||||||
|
|
||||||
|
三张公开或虚构盘 × Lahiri、Raman × 八个点,相对冻结的 PyJHora 4.8.7,最大绝对差 4.9e-5°。Hora 与 Ghati 度数不同。公开盘 Lahiri 的 Bhava / Hora / Ghati 仍是狮子 17.12°、水瓶 22.58°、处女 8.94°。
|
||||||
|
|
||||||
|
## 本机验证
|
||||||
|
|
||||||
|
| 检查 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| ruff(本轮改过的 Python 文件) | 通过 |
|
||||||
|
| py_compile(本轮改过的模块) | 通过 |
|
||||||
|
| `test_runtime_jhora_import_scan.py`、`test_special_lagnas_birth_instant.py`(不含全盘那条)、`test_rectification_three_engine_packet.py` | 18 通过 |
|
||||||
|
| 屏蔽 `jhora` 后的 `cmd_full_reading`,加上三引擎包测试 | 5 通过;八点 `computed`,`source` 为 `native_special_lagna` |
|
||||||
|
| 隐私扫描 `tests/test_repo_privacy_markers.py` | 通过 |
|
||||||
|
|
||||||
|
未跑、不能写成通过:没有 PyJHora 的门禁虚拟环境(本机 Anaconda 装了 PyJHora 4.8.7,测试是屏蔽导入);Linux 全量 pytest 相对 62 条基线;快速门整段(含 npm test 与 turbopack 构建);部署后的 `/api/health`、`/login`、未登录 `/api/account`。
|
||||||
|
|
||||||
|
## 开工预检
|
||||||
|
|
||||||
|
`python scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 在本工作树跑完。远端可见性已核对,外部引擎适配器诊断通过,Python 运行时通过。碎片扫描在 90 秒超时,聚焦测试在 45 秒超时。不把这次退出当成通过,也没有改断言或补 `.workbuddy` 镜像。
|
||||||
|
|
||||||
|
## 偏离
|
||||||
|
|
||||||
|
- 导入扫描会走到 `jyotish_api_server` 再走到校正三引擎包。那里的 `import_module("jhora...")` 挪进 pyjhora 文件名的参考模块,包函数 `_pyjhora_d1` 仍在原处,现有测试照旧替换它。
|
||||||
|
- 全盘年运段仍会按需导入 `annual_pyjhora_replay`(文件名含 pyjhora,扫描不进入)。导入失败时该段记 blocked,不挡八个特殊上升点。
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
| `TASK-rectification-probe-supply-research-20260913.md` | — | 研究单:六题后引擎在剩余候选上再出带年月题的四种放宽规则,20 例公开 AA 数据离线量收益,有收益才立实现单 | 已合入(离线测量,不改线上) | `b063c668` |
|
| `TASK-rectification-probe-supply-research-20260913.md` | — | 研究单:六题后引擎在剩余候选上再出带年月题的四种放宽规则,20 例公开 AA 数据离线量收益,有收益才立实现单 | 已合入(离线测量,不改线上) | `b063c668` |
|
||||||
| `TASK-rectification-refresh-r3-r4-20260913.md` | `PROGRESS-rectification-refresh-r3-r4-20260913.md` | 刷新阶段 R3(MIN_BOUNDARY_DAYS 45→30)+ R4(pratyantar 与 D9/D10 上升 Narayana);首轮出题不变。即使放宽仍有 0 题例子,定向补事另线保留(BUG-664/665) | 已实现 `ab1ade59`,已部署;2026-09-13 验收通过(实现单由执行方自拟,无产品决策记录段;收益口径见研究文档) | `codex/rectification-refresh-r3-r4-20260913` |
|
| `TASK-rectification-refresh-r3-r4-20260913.md` | `PROGRESS-rectification-refresh-r3-r4-20260913.md` | 刷新阶段 R3(MIN_BOUNDARY_DAYS 45→30)+ R4(pratyantar 与 D9/D10 上升 Narayana);首轮出题不变。即使放宽仍有 0 题例子,定向补事另线保留(BUG-664/665) | 已实现 `ab1ade59`,已部署;2026-09-13 验收通过(实现单由执行方自拟,无产品决策记录段;收益口径见研究文档) | `codex/rectification-refresh-r3-r4-20260913` |
|
||||||
| `TASK-upstream-sync6-20261007.md` | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) | **上游同步第六轮(`23be1807`)**:10-05 五个计算修正一行未合——D5/D6/D8/D11 映射(乔布斯盘四张分盘星座全不同)、Vimsopaka 权重、Yogini 顺序与出生余额、Panchadhikari 宫位参照、Narayana 并列、特殊上升、出生秒数、Neecha Bhanga 两条件、首段大运子运、岁差被覆盖;逐项对照原书核实;最先做 | 已验收(门禁补修 `2df6ee72`,已部署 staging) | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) |
|
| `TASK-upstream-sync6-20261007.md` | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) | **上游同步第六轮(`23be1807`)**:10-05 五个计算修正一行未合——D5/D6/D8/D11 映射(乔布斯盘四张分盘星座全不同)、Vimsopaka 权重、Yogini 顺序与出生余额、Panchadhikari 宫位参照、Narayana 并列、特殊上升、出生秒数、Neecha Bhanga 两条件、首段大运子运、岁差被覆盖;逐项对照原书核实;最先做 | 已验收(门禁补修 `2df6ee72`,已部署 staging) | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) |
|
||||||
| `TASK-special-lagnas-native-20261008.md` | — | **特殊上升点原生计算(线上缺陷)**:sync6 U6 运行时调 PyJHora,生产镜像没有 PyJHora → staging 上 Hora/Ghati/Sree/Indu/Pranapada 全 blocked;原生实现、PyJHora 只作冻结对照、快速门禁止运行时 import jhora;最先做 | 待领取 | — |
|
| `TASK-special-lagnas-native-20261008.md` | `PROGRESS-special-lagnas-native-20261008.md` | **特殊上升点原生计算(线上缺陷)**:sync6 U6 运行时调 PyJHora,生产镜像没有 PyJHora → staging 上 Hora/Ghati/Sree/Indu/Pranapada 全 blocked;原生实现、PyJHora 只作冻结对照、快速门禁止运行时 import jhora;最先做 | 待验收 | `codex/special-lagnas-native-20261008`(未上线;部署以 `/api/health` 为准) |
|
||||||
| `TASK-sync6-gate-red-20261007.md` | — | **sync6 后门禁仍红**:run 3213 快速门已过,「Frontend and database tests」开跑 20 秒后日志中断、15 分钟后判失败,后续步骤 failure 0 秒;疑运行机掉线;先 Re-run,红则本机带 Docker 跑 `CI=1 npm test` 对照 `0ed45a4b` | 已完成(Re-run 通过,BUG-1271,部署 `2df6ee72`) | — |
|
| `TASK-sync6-gate-red-20261007.md` | — | **sync6 后门禁仍红**:run 3213 快速门已过,「Frontend and database tests」开跑 20 秒后日志中断、15 分钟后判失败,后续步骤 failure 0 秒;疑运行机掉线;先 Re-run,红则本机带 Docker 跑 `CI=1 npm test` 对照 `0ed45a4b` | 已完成(Re-run 通过,BUG-1271,部署 `2df6ee72`) | — |
|
||||||
|
|
||||||
| `TASK-rectification-tie-break-entry-fix-20260913.md` | `PROGRESS-rectification-tie-break-entry-fix-20260913.md` | 修复单:参考题入口只读 `window_scan` 标志位 → 两道答完后仍显示、再点弹「现在没有可答的参考题」;并按产品拍板改成点一次连出 D9+D10(BUG-666~668,Skill 10.0.26) | 已合入(无独立验收记录) | `99127601`(BUG-666~668 resolved;已部署 staging,run 2589) |
|
| `TASK-rectification-tie-break-entry-fix-20260913.md` | `PROGRESS-rectification-tie-break-entry-fix-20260913.md` | 修复单:参考题入口只读 `window_scan` 标志位 → 两道答完后仍显示、再点弹「现在没有可答的参考题」;并按产品拍板改成点一次连出 D9+D10(BUG-666~668,Skill 10.0.26) | 已合入(无独立验收记录) | `99127601`(BUG-666~668 resolved;已部署 staging,run 2589) |
|
||||||
|
|||||||
@@ -15664,7 +15664,7 @@ def cmd_full_reading(args):
|
|||||||
from special_lagnas import SpecialLagnasCalculator
|
from special_lagnas import SpecialLagnasCalculator
|
||||||
sl_calc = SpecialLagnasCalculator()
|
sl_calc = SpecialLagnasCalculator()
|
||||||
birth_dt = _birth_datetime_from_args(args)
|
birth_dt = _birth_datetime_from_args(args)
|
||||||
# 6:00 只在缺坐标时给公式回退。有坐标时 PyJHora 按出生时刻覆盖 Bhava / Hora / Ghati。
|
# 6:00 只在缺坐标时给公式回退。有坐标时 Swiss Ephemeris 按出生时刻覆盖八个特殊上升点。
|
||||||
sunrise_dt = datetime(args.year, args.month, args.day, 6, 0)
|
sunrise_dt = datetime(args.year, args.month, args.day, 6, 0)
|
||||||
import io
|
import io
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Birth special lagnas from Swiss Ephemeris. No PyJHora import.
|
||||||
|
|
||||||
|
Numeric rates are the ones named by P.V.R. Narasimha Rao, *Vedic Astrology:
|
||||||
|
An Integrated Approach*, section 5.2, and by the B.V. Raman methods below.
|
||||||
|
``references/`` does not contain the BPHS chapter 4 verse text, so a point is
|
||||||
|
not given a rate that those named sources do not state.
|
||||||
|
|
||||||
|
- Bhava: section 5.2 step (2) divides the sunrise-to-birth minutes by 4
|
||||||
|
(0.25 degree per minute) and adds that to the sidereal Sun at sunrise.
|
||||||
|
- Hora: 0.5 degree per minute, one rasi per hour, from that same sunrise Sun.
|
||||||
|
- Ghati: 1.25 degrees per minute, one rasi per 24-minute ghati.
|
||||||
|
- Vighati: 15 degrees per minute. A literal "one rasi per vighati" (24 seconds)
|
||||||
|
would be 75 degrees per minute; section 5.2's special-ascendant family, as
|
||||||
|
recorded for this rate, uses 15. That is the frozen comparison rate.
|
||||||
|
- Sree: ascendant plus the Moon's leftover degrees inside its nakshatra,
|
||||||
|
times 27 (classical Sri Lagna).
|
||||||
|
- Indu: B.V. Raman. Kala factors of the 9th lords from Lagna and from the Moon
|
||||||
|
are 30, 16, 6, 8, 10, 12, 1 for Sun through Saturn. The degree is the Moon's
|
||||||
|
degree inside its own sign.
|
||||||
|
- Pranapada: ishta ghatis from sunrise (previous sunrise when birth is earlier),
|
||||||
|
times 4 signs, plus the birth Sun, plus 8 signs for a fixed Sun sign, 4 for
|
||||||
|
a dual Sun sign, and 0 for a movable Sun sign. The clock is truncated to
|
||||||
|
whole seconds before the ghati count, matching the frozen comparison.
|
||||||
|
- Varnada: B.V. Raman method 1. Odd signs are counted forward from Aries, even
|
||||||
|
signs backward from Pisces. The returned degree is the ascendant's degree
|
||||||
|
inside its own sign, placed in the Varnada sign.
|
||||||
|
|
||||||
|
Sunrise is the centre of the solar disc, Hindu rising, no refraction
|
||||||
|
(Swiss Ephemeris). Sidereal mode is the caller's ayanamsa and is restored.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import nullcontext
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
SIGNS = [
|
||||||
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||||
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||||
|
]
|
||||||
|
SOURCE = "native_special_lagna"
|
||||||
|
POINT_NAMES = (
|
||||||
|
("Bhava_Lagna", "Bhava Lagna"),
|
||||||
|
("Hora_Lagna", "Hora Lagna"),
|
||||||
|
("Ghati_Lagna", "Ghati Lagna"),
|
||||||
|
("ViGhati_Lagna", "Vighati Lagna"),
|
||||||
|
("Sree_Lagna", "Sree Lagna"),
|
||||||
|
("Indu_Lagna", "Indu Lagna"),
|
||||||
|
("Pranapada_Lagna", "Pranapada Lagna"),
|
||||||
|
("Varnada_Lagna", "Varnada Lagna"),
|
||||||
|
)
|
||||||
|
# Sun..Saturn lords of Aries..Pisces. Planet index 0 is the Sun.
|
||||||
|
HOUSE_LORDS = (2, 5, 3, 1, 0, 3, 5, 2, 4, 6, 6, 4)
|
||||||
|
INDU_KALAS = (30, 16, 6, 8, 10, 12, 1)
|
||||||
|
ODD_SIGNS = frozenset({0, 2, 4, 6, 8, 10})
|
||||||
|
FIXED_SIGNS = frozenset({1, 4, 7, 10})
|
||||||
|
DUAL_SIGNS = frozenset({2, 5, 8, 11})
|
||||||
|
ONE_ARC_SECOND = 1.0 / 3600.0
|
||||||
|
RATES = {
|
||||||
|
"Bhava_Lagna": 0.25,
|
||||||
|
"Hora_Lagna": 0.5,
|
||||||
|
"Ghati_Lagna": 1.25,
|
||||||
|
"ViGhati_Lagna": 15.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def native_special_lagna_rows(
|
||||||
|
birth_time: datetime,
|
||||||
|
lat: float | None,
|
||||||
|
lon: float | None,
|
||||||
|
tz_offset: float | None,
|
||||||
|
asc_degree: float,
|
||||||
|
ayanamsa: str | None = None,
|
||||||
|
) -> dict[str, dict]:
|
||||||
|
"""Eight birth points. Missing coordinates block the rows and invent nothing."""
|
||||||
|
if lat is None or lon is None or tz_offset is None:
|
||||||
|
return _blocked("birth_coordinates_or_timezone_missing")
|
||||||
|
try:
|
||||||
|
import swisseph as swe
|
||||||
|
except Exception as exc:
|
||||||
|
return _blocked(f"swisseph_unavailable:{exc}")
|
||||||
|
try:
|
||||||
|
from ayanamsa_utils import temporary_ayanamsa
|
||||||
|
except ImportError: # package import
|
||||||
|
from scripts.ayanamsa_utils import temporary_ayanamsa
|
||||||
|
|
||||||
|
context = temporary_ayanamsa(ayanamsa) if ayanamsa else nullcontext()
|
||||||
|
try:
|
||||||
|
with context:
|
||||||
|
rows = _compute(swe, birth_time, float(lat), float(lon), float(tz_offset), float(asc_degree))
|
||||||
|
except Exception as exc:
|
||||||
|
reason = exc.__class__.__name__
|
||||||
|
if exc.__class__.__name__ == "UnsupportedAyanamsaError":
|
||||||
|
reason = f"native_ayanamsa_unsupported:{ayanamsa}"
|
||||||
|
else:
|
||||||
|
reason = f"native_special_lagna_failed:{exc}"
|
||||||
|
return _blocked(reason)
|
||||||
|
if ayanamsa is not None:
|
||||||
|
for row in rows.values():
|
||||||
|
row["ayanamsa"] = ayanamsa
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked(reason: str) -> dict[str, dict]:
|
||||||
|
return {
|
||||||
|
key: {"full_name": full_name, "status": "blocked", "source": SOURCE, "reason": reason}
|
||||||
|
for key, full_name in POINT_NAMES
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute(swe, birth_time: datetime, lat: float, lon: float, tz_offset: float, asc_degree: float) -> dict[str, dict]:
|
||||||
|
planet_flags = (
|
||||||
|
swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.FLG_TRUEPOS
|
||||||
|
| swe.FLG_NOGDEFL | swe.FLG_NONUT | swe.FLG_SPEED
|
||||||
|
)
|
||||||
|
rise_flags = swe.BIT_HINDU_RISING | swe.BIT_NO_REFRACTION | swe.BIT_DISC_CENTER | swe.CALC_RISE
|
||||||
|
asc_flags = swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.BIT_HINDU_RISING | swe.FLG_TRUEPOS | swe.FLG_SPEED
|
||||||
|
local_hour = (
|
||||||
|
birth_time.hour
|
||||||
|
+ birth_time.minute / 60.0
|
||||||
|
+ birth_time.second / 3600.0
|
||||||
|
+ birth_time.microsecond / 3_600_000_000.0
|
||||||
|
)
|
||||||
|
jd_local = swe.julday(birth_time.year, birth_time.month, birth_time.day, local_hour)
|
||||||
|
birth_hours = swe.revjul(jd_local, swe.GREG_CAL)[3]
|
||||||
|
sun_at_sunrise, sunrise_hours = _sun_at_sunrise(swe, jd_local, lat, lon, tz_offset, planet_flags, rise_flags)
|
||||||
|
minutes = (birth_hours - sunrise_hours) * 60.0
|
||||||
|
jd_birth_ut = jd_local - tz_offset / 24.0
|
||||||
|
sun_birth = _sidereal(swe, jd_birth_ut, swe.SUN, planet_flags)
|
||||||
|
moon_birth = _sidereal(swe, jd_birth_ut, swe.MOON, planet_flags)
|
||||||
|
asc_sign, asc_in_sign = _ascendant(swe, jd_local, lat, lon, tz_offset, asc_flags)
|
||||||
|
asc_long = (asc_sign * 30.0 + asc_in_sign) % 360.0
|
||||||
|
|
||||||
|
longitudes = {
|
||||||
|
key: (sun_at_sunrise + minutes * rate) % 360.0
|
||||||
|
for key, rate in RATES.items()
|
||||||
|
}
|
||||||
|
longitudes["Sree_Lagna"] = (asc_long + (moon_birth % (360.0 / 27.0)) * 27.0) % 360.0
|
||||||
|
longitudes["Indu_Lagna"] = _indu_longitude(asc_sign, moon_birth)
|
||||||
|
longitudes["Pranapada_Lagna"] = _pranapada_longitude(
|
||||||
|
swe, jd_local, birth_hours, lat, lon, tz_offset, sun_birth, planet_flags, rise_flags,
|
||||||
|
)
|
||||||
|
varnada_sign = _varnada_sign(asc_sign, _sign_index(longitudes["Hora_Lagna"]))
|
||||||
|
rows = {}
|
||||||
|
for key, full_name in POINT_NAMES:
|
||||||
|
if key == "Varnada_Lagna":
|
||||||
|
longitude = varnada_sign * 30.0 + (asc_in_sign % 30.0)
|
||||||
|
elif key == "Indu_Lagna":
|
||||||
|
longitude = longitudes[key]
|
||||||
|
else:
|
||||||
|
longitude = longitudes[key]
|
||||||
|
rows[key] = _payload(full_name, longitude, asc_degree)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _sun_at_sunrise(swe, jd_local: float, lat: float, lon: float, tz_offset: float, planet_flags: int, rise_flags: int):
|
||||||
|
year, month, day, _hour = swe.revjul(jd_local, swe.GREG_CAL)
|
||||||
|
jd_midnight = swe.julday(int(year), int(month), int(day), 0.0)
|
||||||
|
_status, times = swe.rise_trans(
|
||||||
|
jd_midnight - tz_offset / 24.0,
|
||||||
|
swe.SUN,
|
||||||
|
rise_flags,
|
||||||
|
(lon, lat, 0.0),
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
planet_flags,
|
||||||
|
)
|
||||||
|
rise_jd_ut = times[0]
|
||||||
|
rise_local = (rise_jd_ut - jd_midnight) * 24.0 + tz_offset
|
||||||
|
hour, minute, second = _clock_hms(rise_local)
|
||||||
|
truncated = swe.julday(int(year), int(month), int(day), hour + minute / 60.0 + second / 3600.0)
|
||||||
|
# Chart helpers treat the sunrise JD as local and subtract the timezone.
|
||||||
|
sun = _sidereal(swe, truncated, swe.SUN, planet_flags)
|
||||||
|
return sun, rise_local
|
||||||
|
|
||||||
|
|
||||||
|
def _pranapada_longitude(swe, jd_local, birth_hours, lat, lon, tz_offset, sun_birth, planet_flags, rise_flags):
|
||||||
|
sunrise_hours = _sun_at_sunrise(swe, jd_local, lat, lon, tz_offset, planet_flags, rise_flags)[1]
|
||||||
|
if birth_hours < sunrise_hours:
|
||||||
|
sunrise_hours = _sun_at_sunrise(swe, jd_local - 1.0, lat, lon, tz_offset, planet_flags, rise_flags)[1]
|
||||||
|
elapsed = 24.0 + birth_hours - sunrise_hours
|
||||||
|
else:
|
||||||
|
elapsed = birth_hours - sunrise_hours
|
||||||
|
hours, minutes, seconds = _clock_hms(elapsed)
|
||||||
|
tharparai = int(hours) * 9000 + int(minutes) * 150 + int(seconds)
|
||||||
|
birth_signs = ((tharparai / 3600.0) * 4.0) % 12.0
|
||||||
|
sun_sign = _sign_index(sun_birth)
|
||||||
|
if sun_sign in FIXED_SIGNS:
|
||||||
|
extra = 240.0
|
||||||
|
elif sun_sign in DUAL_SIGNS:
|
||||||
|
extra = 120.0
|
||||||
|
else:
|
||||||
|
extra = 0.0
|
||||||
|
return (birth_signs * 30.0 + sun_birth + extra) % 360.0
|
||||||
|
|
||||||
|
|
||||||
|
def _indu_longitude(asc_sign: int, moon_long: float) -> float:
|
||||||
|
moon_sign = _sign_index(moon_long)
|
||||||
|
ninth = HOUSE_LORDS[(asc_sign + 8) % 12]
|
||||||
|
ninth_from_moon = HOUSE_LORDS[(moon_sign + 8) % 12]
|
||||||
|
kalas = (INDU_KALAS[ninth] + INDU_KALAS[ninth_from_moon]) % 12
|
||||||
|
if kalas == 0:
|
||||||
|
kalas = 12
|
||||||
|
indu_sign = (moon_sign + kalas - 1) % 12
|
||||||
|
return indu_sign * 30.0 + (moon_long % 30.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _varnada_sign(lagna: int, hora_sign: int) -> int:
|
||||||
|
lagna_odd = lagna in ODD_SIGNS
|
||||||
|
hora_sign = hora_sign % 12
|
||||||
|
hora_odd = hora_sign in ODD_SIGNS
|
||||||
|
count1 = _count_rasis(0, lagna, 1) if lagna_odd else _count_rasis(11, lagna, -1)
|
||||||
|
count2 = _count_rasis(0, hora_sign, 1) if hora_odd else _count_rasis(11, hora_sign, -1)
|
||||||
|
same_parity = (count1 + count2) % 12
|
||||||
|
opposite_parity = (max(count1, count2) - min(count1, count2)) % 12
|
||||||
|
count = same_parity if hora_odd == lagna_odd else opposite_parity
|
||||||
|
counted = _count_rasis(1, count, 1) if lagna_odd else _count_rasis(12, count, -1)
|
||||||
|
return (counted - 1) % 12
|
||||||
|
|
||||||
|
|
||||||
|
def _count_rasis(start: int, end: int, direction: int, total: int = 12) -> int:
|
||||||
|
return ((total + direction * (end - start)) % total) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _ascendant(swe, jd_local: float, lat: float, lon: float, tz_offset: float, flags: int):
|
||||||
|
jd_ut = jd_local - tz_offset / 24.0
|
||||||
|
_cusps, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", flags)
|
||||||
|
longitude = ascmc[0] % 360.0
|
||||||
|
sign = int(longitude // 30) % 12
|
||||||
|
return sign, longitude - sign * 30.0
|
||||||
|
|
||||||
|
|
||||||
|
def _sidereal(swe, jd_ut: float, planet: int, flags: int) -> float:
|
||||||
|
position, _speed = swe.calc_ut(jd_ut, planet, flags)
|
||||||
|
return position[0] % 360.0
|
||||||
|
|
||||||
|
|
||||||
|
def _sign_index(longitude: float) -> int:
|
||||||
|
sign, _degree = _dasavarga(longitude)
|
||||||
|
return sign
|
||||||
|
|
||||||
|
|
||||||
|
def _dasavarga(longitude: float) -> tuple[int, float]:
|
||||||
|
one_sign = 360.0
|
||||||
|
fraction = (longitude / one_sign) % 1.0
|
||||||
|
sign = int(fraction * 12.0)
|
||||||
|
degree = (longitude - sign * 30.0) % 30.0
|
||||||
|
if int(degree + ONE_ARC_SECOND) == 30:
|
||||||
|
degree = 0.0
|
||||||
|
sign = (sign + 1) % 12
|
||||||
|
return sign, degree
|
||||||
|
|
||||||
|
|
||||||
|
def _clock_hms(hours: float) -> tuple[int, int, int]:
|
||||||
|
"""Whole hours, minutes, and rounded seconds, matching the frozen oracle clock."""
|
||||||
|
day_part = int(hours)
|
||||||
|
minutes_float = (hours - day_part) * 60.0
|
||||||
|
minute = int(minutes_float)
|
||||||
|
second = round((minutes_float - minute) * 60.0)
|
||||||
|
if day_part > 23:
|
||||||
|
day_part = day_part % 24
|
||||||
|
elif day_part < 0:
|
||||||
|
day_part = abs(day_part) % 24
|
||||||
|
minute = abs(minute)
|
||||||
|
second = abs(second)
|
||||||
|
if second == 60:
|
||||||
|
minute += 1
|
||||||
|
second = 0
|
||||||
|
if minute == 60:
|
||||||
|
day_part += 1
|
||||||
|
minute = 0
|
||||||
|
return int(day_part), int(minute), int(second)
|
||||||
|
|
||||||
|
|
||||||
|
def _payload(full_name: str, longitude: float, asc_degree: float) -> dict:
|
||||||
|
longitude = longitude % 360.0
|
||||||
|
sign, degree = _dasavarga(longitude)
|
||||||
|
full = sign * 30.0 + degree
|
||||||
|
return {
|
||||||
|
"full_name": full_name,
|
||||||
|
"degree": round(full, 4),
|
||||||
|
"longitude": round(full, 4),
|
||||||
|
"sign": SIGNS[sign],
|
||||||
|
"sign_degree": round(degree, 4),
|
||||||
|
"house": ((sign - int(asc_degree // 30)) % 12) + 1,
|
||||||
|
"method": "swiss_ephemeris_special_lagna",
|
||||||
|
"source": SOURCE,
|
||||||
|
"status": "computed",
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Optional in-process Panchavargiya lookup for the year-lord research switch.
|
||||||
|
|
||||||
|
Not on the default full-reading, report, or consultation path. Tajika calls
|
||||||
|
this only when a score was requested with an annual Julian day. The product
|
||||||
|
year-lord path leaves that day empty unless
|
||||||
|
``JYOTISH_YEAR_LORD_PYJHORA_REFERENCE=1``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def external_panchavargiya_score(
|
||||||
|
planet: str,
|
||||||
|
*,
|
||||||
|
annual_jd: float,
|
||||||
|
lat: float,
|
||||||
|
lon: float,
|
||||||
|
tz: float,
|
||||||
|
planet_index: int | None,
|
||||||
|
) -> dict | None:
|
||||||
|
del planet
|
||||||
|
from jhora.horoscope.chart import strength as j_strength
|
||||||
|
from jhora.panchanga import drik as j_drik
|
||||||
|
|
||||||
|
place = j_drik.Place("annual_year_lord_probe", float(lat), float(lon), float(tz))
|
||||||
|
# Annual-chart APIs take the local civil JD. ``annual_jd`` arrives as UT.
|
||||||
|
scores = j_strength.pancha_vargeeya_bala(float(annual_jd) + float(tz) / 24.0, place)
|
||||||
|
if planet_index is None:
|
||||||
|
return None
|
||||||
|
score = float(scores[planet_index])
|
||||||
|
return {
|
||||||
|
"score": score,
|
||||||
|
"engine": "pyjhora",
|
||||||
|
"components": {"pyjhora_pancha_vargeeya_bala": score},
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Optional PyJHora D1 signs for the rectification parity packet.
|
||||||
|
|
||||||
|
The product packet calls this only for the PyJHora column. A missing install
|
||||||
|
raises, and the packet records that column as blocked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
SIGNS = (
|
||||||
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||||
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||||
|
utils = importlib.import_module("jhora.utils")
|
||||||
|
charts = importlib.import_module("jhora.horoscope.chart.charts")
|
||||||
|
drik = importlib.import_module("jhora.panchanga.drik")
|
||||||
|
jd = utils.julian_day_number(
|
||||||
|
(case["year"], case["month"], case["day"]),
|
||||||
|
(case["hour"], case["minute"], case.get("second", 0)),
|
||||||
|
)
|
||||||
|
drik.set_ayanamsa_mode("LAHIRI", jd=jd)
|
||||||
|
place = drik.Place("request-level", case["lat"], case["lon"], case["tz"])
|
||||||
|
index_to_planet = {
|
||||||
|
0: "Sun", 1: "Moon", 2: "Mars", 3: "Mercury", 4: "Jupiter", 5: "Venus", 6: "Saturn",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
index_to_planet[body]: SIGNS[int(position[0])]
|
||||||
|
for body, position in charts.rasi_chart(jd, place)
|
||||||
|
if body in index_to_planet
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
|
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
|
||||||
JYOTISHGANIT_DATA_DIR = ROOT / ".cache" / "jyotishganit"
|
JYOTISHGANIT_DATA_DIR = ROOT / ".cache" / "jyotishganit"
|
||||||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
||||||
SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces")
|
|
||||||
|
|
||||||
|
|
||||||
def canonical_case_input(case: dict[str, Any]) -> dict[str, Any]:
|
def canonical_case_input(case: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -46,14 +45,12 @@ def _local_d1(case: dict[str, Any]) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
|
def _pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||||
utils = importlib.import_module("jhora.utils")
|
"""PyJHora D1 signs. The jhora import stays in the reference module."""
|
||||||
charts = importlib.import_module("jhora.horoscope.chart.charts")
|
try:
|
||||||
drik = importlib.import_module("jhora.panchanga.drik")
|
reference = importlib.import_module("pyjhora_rectification_d1_reference")
|
||||||
jd = utils.julian_day_number((case["year"], case["month"], case["day"]), (case["hour"], case["minute"], case.get("second", 0)))
|
except ModuleNotFoundError:
|
||||||
drik.set_ayanamsa_mode("LAHIRI", jd=jd)
|
reference = importlib.import_module("scripts.pyjhora_rectification_d1_reference")
|
||||||
place = drik.Place("request-level", case["lat"], case["lon"], case["tz"])
|
return reference.pyjhora_d1(case)
|
||||||
index_to_planet = {0: "Sun", 1: "Moon", 2: "Mars", 3: "Mercury", 4: "Jupiter", 5: "Venus", 6: "Saturn"}
|
|
||||||
return {index_to_planet[body]: SIGNS[int(position[0])] for body, position in charts.rasi_chart(jd, place) if body in index_to_planet}
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_jyotishganit_data_dir() -> str:
|
def _ensure_jyotishganit_data_dir() -> str:
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Freeze PyJHora 4.8.7 special-lagna longitudes. Offline only.
|
||||||
|
|
||||||
|
Run from the repo root, with a Python that has PyJHora 4.8.7 installed:
|
||||||
|
|
||||||
|
python scripts/research/freeze_special_lagnas_pyjhora_fixture.py
|
||||||
|
|
||||||
|
The runtime calculator does not import this module. Tests read the JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path[:0] = [str(ROOT), str(ROOT / "scripts")]
|
||||||
|
|
||||||
|
import swisseph as swe # noqa: E402
|
||||||
|
from jhora.horoscope.chart import charts # noqa: E402
|
||||||
|
from jhora.panchanga import drik # noqa: E402
|
||||||
|
from jhora.panchanga.drik import Place # noqa: E402
|
||||||
|
|
||||||
|
OUT = ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json"
|
||||||
|
CASES = {
|
||||||
|
"steve_jobs_1955_aa": {
|
||||||
|
"source": "references/public_oracle_cases.json id steve_jobs_1955_aa",
|
||||||
|
"birth": dict(year=1955, month=2, day=24, hour=19, minute=15, second=0, lat=37.7749, lon=-122.4194, tz=-8),
|
||||||
|
},
|
||||||
|
"barack_obama_1961_aa": {
|
||||||
|
"source": "references/public_oracle_cases.json id barack_obama_1961_aa",
|
||||||
|
"birth": dict(year=1961, month=8, day=4, hour=19, minute=24, second=0, lat=21.3069, lon=-157.8583, tz=-10),
|
||||||
|
},
|
||||||
|
"fictional_reader_main": {
|
||||||
|
"source": "tests/test_report_reader_main.py FICTIONAL",
|
||||||
|
"birth": dict(year=1991, month=4, day=7, hour=9, minute=15, second=0, lat=30.5728, lon=104.0668, tz=8.0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
MODES = {"lahiri": "LAHIRI", "raman": "RAMAN"}
|
||||||
|
FUNCS = (
|
||||||
|
("Bhava_Lagna", "bhava_lagna"),
|
||||||
|
("Hora_Lagna", "hora_lagna"),
|
||||||
|
("Ghati_Lagna", "ghati_lagna"),
|
||||||
|
("ViGhati_Lagna", "vighati_lagna"),
|
||||||
|
("Sree_Lagna", "sree_lagna"),
|
||||||
|
("Indu_Lagna", "indu_lagna"),
|
||||||
|
("Pranapada_Lagna", "pranapada_lagna"),
|
||||||
|
)
|
||||||
|
SIGNS = [
|
||||||
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||||
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _point(raw) -> dict:
|
||||||
|
sign = int(raw[0]) % 12
|
||||||
|
degree = float(raw[1]) % 30.0
|
||||||
|
return {
|
||||||
|
"sign": SIGNS[sign],
|
||||||
|
"sign_index": sign,
|
||||||
|
"sign_degree": degree,
|
||||||
|
"longitude": sign * 30.0 + degree,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _case(birth: dict, mode: str) -> dict:
|
||||||
|
local_hour = birth["hour"] + birth["minute"] / 60.0 + birth["second"] / 3600.0
|
||||||
|
with redirect_stdout(io.StringIO()):
|
||||||
|
drik.set_ayanamsa_mode(mode)
|
||||||
|
jd = swe.julday(birth["year"], birth["month"], birth["day"], local_hour)
|
||||||
|
place = Place("birth_place", birth["lat"], birth["lon"], birth["tz"])
|
||||||
|
points = {key: _point(getattr(drik, name)(jd, place)) for key, name in FUNCS}
|
||||||
|
raw = charts.varnada_lagna(
|
||||||
|
(birth["year"], birth["month"], birth["day"]),
|
||||||
|
(birth["hour"], birth["minute"], birth["second"]),
|
||||||
|
place,
|
||||||
|
varnada_method=1,
|
||||||
|
)
|
||||||
|
points["Varnada_Lagna"] = _point(raw)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
version = importlib.metadata.version("PyJHora")
|
||||||
|
payload = {
|
||||||
|
"pyjhora_version": version,
|
||||||
|
"generator": "python scripts/research/freeze_special_lagnas_pyjhora_fixture.py",
|
||||||
|
"generated_on": datetime.now().strftime("%Y-%m-%d"),
|
||||||
|
"cases": {},
|
||||||
|
}
|
||||||
|
for case_id, spec in CASES.items():
|
||||||
|
payload["cases"][case_id] = {
|
||||||
|
"source": spec["source"],
|
||||||
|
"birth": spec["birth"],
|
||||||
|
"ayanamsa": {name: _case(spec["birth"], mode) for name, mode in MODES.items()},
|
||||||
|
}
|
||||||
|
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()
|
||||||
@@ -128,6 +128,8 @@ CORE_PYTEST_TARGETS = [
|
|||||||
"tests/test_vedastro_external_technique_evidence.py",
|
"tests/test_vedastro_external_technique_evidence.py",
|
||||||
# Self-hosted heading font slices: 6500-character coverage, no overlap, 120 KB cap.
|
# Self-hosted heading font slices: 6500-character coverage, no overlap, 120 KB cap.
|
||||||
"tests/test_serif_font_slices.py",
|
"tests/test_serif_font_slices.py",
|
||||||
|
# Chart, report, and consult entries must not import PyJHora (BUG-1273).
|
||||||
|
"tests/test_runtime_jhora_import_scan.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
RUNTIME_TRUTH_PYTEST_TARGETS = [
|
RUNTIME_TRUTH_PYTEST_TARGETS = [
|
||||||
|
|||||||
+9
-180
@@ -14,10 +14,6 @@ Special Lagnas Calculator - 特殊上升点计算器
|
|||||||
|
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import io
|
|
||||||
from contextlib import redirect_stdout
|
|
||||||
|
|
||||||
|
|
||||||
def jaimini_special_lagna_view(points):
|
def jaimini_special_lagna_view(points):
|
||||||
"""Project canonical birth points without recalculating approximate aliases."""
|
"""Project canonical birth points without recalculating approximate aliases."""
|
||||||
aliases = {
|
aliases = {
|
||||||
@@ -51,21 +47,6 @@ def bhava_house_rows(bhava):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _pyjhora_ayanamsa_mode(name):
|
|
||||||
key = str(name or '').strip().lower().replace('-', '_').replace(' ', '_')
|
|
||||||
return {
|
|
||||||
'lahiri': 'LAHIRI',
|
|
||||||
'raman': 'RAMAN',
|
|
||||||
'kp': 'KP',
|
|
||||||
'krishnamurti': 'KP',
|
|
||||||
'fagan_bradley': 'FAGAN',
|
|
||||||
'fagan': 'FAGAN',
|
|
||||||
'true_citra': 'TRUE_CITRA',
|
|
||||||
'true_chitra': 'TRUE_CITRA',
|
|
||||||
'true_pushya': 'TRUE_PUSHYA',
|
|
||||||
}.get(key)
|
|
||||||
|
|
||||||
|
|
||||||
class SpecialLagnasCalculator:
|
class SpecialLagnasCalculator:
|
||||||
"""特殊上升点计算器"""
|
"""特殊上升点计算器"""
|
||||||
|
|
||||||
@@ -108,7 +89,13 @@ class SpecialLagnasCalculator:
|
|||||||
# 由 full reading 用它覆盖这两个占位。
|
# 由 full reading 用它覆盖这两个占位。
|
||||||
result["Arudha_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
result["Arudha_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
||||||
result["Upapada_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
result["Upapada_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
||||||
pyjhora = self.calculate_pyjhora_special_lagnas(
|
# Rectification keeps calculate_hora_lagna / calculate_ghati_lagna above.
|
||||||
|
# With coordinates, the eight birth points come from Swiss Ephemeris.
|
||||||
|
try:
|
||||||
|
from native_special_lagnas import native_special_lagna_rows
|
||||||
|
except ImportError:
|
||||||
|
from scripts.native_special_lagnas import native_special_lagna_rows
|
||||||
|
native = native_special_lagna_rows(
|
||||||
birth_time=birth_time,
|
birth_time=birth_time,
|
||||||
lat=lat,
|
lat=lat,
|
||||||
lon=lon,
|
lon=lon,
|
||||||
@@ -118,169 +105,11 @@ class SpecialLagnasCalculator:
|
|||||||
)
|
)
|
||||||
if lat is None or lon is None or tz_offset is None:
|
if lat is None or lon is None or tz_offset is None:
|
||||||
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
||||||
result[key] = pyjhora[key]
|
result[key] = native[key]
|
||||||
else:
|
else:
|
||||||
result.update(pyjhora)
|
result.update(native)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _format_special_lagna_payload(self, label: str, raw, asc_degree: float, method: str) -> Dict:
|
|
||||||
if isinstance(raw, dict):
|
|
||||||
return raw
|
|
||||||
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
|
|
||||||
sign_idx = int(raw[0]) % 12
|
|
||||||
degree = float(raw[1]) % 30
|
|
||||||
longitude = sign_idx * 30 + degree
|
|
||||||
else:
|
|
||||||
longitude = float(raw) % 360
|
|
||||||
sign_idx = int(longitude // 30) % 12
|
|
||||||
degree = longitude % 30
|
|
||||||
return {
|
|
||||||
"full_name": label,
|
|
||||||
"degree": round(longitude, 4),
|
|
||||||
"longitude": round(longitude, 4),
|
|
||||||
"sign": self.SIGNS[sign_idx],
|
|
||||||
"sign_degree": round(degree, 4),
|
|
||||||
"house": ((sign_idx - int(asc_degree // 30)) % 12) + 1,
|
|
||||||
"method": method,
|
|
||||||
"source": "pyjhora_drik",
|
|
||||||
"status": "computed",
|
|
||||||
}
|
|
||||||
|
|
||||||
def calculate_pyjhora_special_lagnas(
|
|
||||||
self,
|
|
||||||
birth_time: datetime,
|
|
||||||
lat: float | None,
|
|
||||||
lon: float | None,
|
|
||||||
tz_offset: float | None,
|
|
||||||
asc_degree: float,
|
|
||||||
ayanamsa: str | None = None,
|
|
||||||
) -> Dict[str, Dict]:
|
|
||||||
names = (
|
|
||||||
("Bhava_Lagna", "Bhava Lagna"),
|
|
||||||
("Hora_Lagna", "Hora Lagna"),
|
|
||||||
("Ghati_Lagna", "Ghati Lagna"),
|
|
||||||
("ViGhati_Lagna", "Vighati Lagna"),
|
|
||||||
("Sree_Lagna", "Sree Lagna"),
|
|
||||||
("Indu_Lagna", "Indu Lagna"),
|
|
||||||
("Pranapada_Lagna", "Pranapada Lagna"),
|
|
||||||
("Varnada_Lagna", "Varnada Lagna"),
|
|
||||||
)
|
|
||||||
blocked = {
|
|
||||||
key: {"full_name": full_name, "status": "blocked", "source": "pyjhora_drik"}
|
|
||||||
for key, full_name in names
|
|
||||||
}
|
|
||||||
if lat is None or lon is None or tz_offset is None:
|
|
||||||
for row in blocked.values():
|
|
||||||
row["reason"] = "birth_coordinates_or_timezone_missing"
|
|
||||||
return blocked
|
|
||||||
producer = (
|
|
||||||
("Bhava_Lagna", "bhava_lagna", "Bhava Lagna"),
|
|
||||||
("Hora_Lagna", "hora_lagna", "Hora Lagna"),
|
|
||||||
("Ghati_Lagna", "ghati_lagna", "Ghati Lagna"),
|
|
||||||
("ViGhati_Lagna", "vighati_lagna", "Vighati Lagna"),
|
|
||||||
("Sree_Lagna", "sree_lagna", "Sree Lagna"),
|
|
||||||
("Indu_Lagna", "indu_lagna", "Indu Lagna"),
|
|
||||||
("Pranapada_Lagna", "pranapada_lagna", "Pranapada Lagna"),
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with redirect_stdout(io.StringIO()):
|
|
||||||
import swisseph as swe
|
|
||||||
from jhora import const as jhora_const
|
|
||||||
from jhora.panchanga import drik
|
|
||||||
from jhora.panchanga.drik import Place
|
|
||||||
except Exception as exc:
|
|
||||||
for row in blocked.values():
|
|
||||||
row["reason"] = f"pyjhora_unavailable:{exc}"
|
|
||||||
return blocked
|
|
||||||
|
|
||||||
mode = None
|
|
||||||
if ayanamsa is not None:
|
|
||||||
mode = _pyjhora_ayanamsa_mode(ayanamsa)
|
|
||||||
if mode is None:
|
|
||||||
for row in blocked.values():
|
|
||||||
row["reason"] = f"pyjhora_ayanamsa_unsupported:{ayanamsa}"
|
|
||||||
return blocked
|
|
||||||
|
|
||||||
previous_pyjhora = getattr(jhora_const, "_DEFAULT_AYANAMSA_MODE", None)
|
|
||||||
previous_swe = None
|
|
||||||
apply_ayanamsa = None
|
|
||||||
try:
|
|
||||||
from ayanamsa_utils import ACTIVE_AYANAMSA_NAME, apply_ayanamsa as _apply
|
|
||||||
previous_swe = ACTIVE_AYANAMSA_NAME
|
|
||||||
apply_ayanamsa = _apply
|
|
||||||
except Exception:
|
|
||||||
apply_ayanamsa = None
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
try:
|
|
||||||
with redirect_stdout(io.StringIO()):
|
|
||||||
local_hour = (
|
|
||||||
birth_time.hour
|
|
||||||
+ birth_time.minute / 60.0
|
|
||||||
+ birth_time.second / 3600.0
|
|
||||||
+ birth_time.microsecond / 3_600_000_000.0
|
|
||||||
)
|
|
||||||
jd_local = swe.julday(birth_time.year, birth_time.month, birth_time.day, local_hour)
|
|
||||||
place = Place("birth_place", lat, lon, tz_offset)
|
|
||||||
if mode is not None:
|
|
||||||
drik.set_ayanamsa_mode(mode)
|
|
||||||
for output_key, func_name, full_name in producer:
|
|
||||||
func = getattr(drik, func_name, None)
|
|
||||||
if func is None:
|
|
||||||
results[output_key] = {
|
|
||||||
"full_name": full_name,
|
|
||||||
"status": "blocked",
|
|
||||||
"source": "pyjhora_drik",
|
|
||||||
"reason": f"pyjhora_missing_function:{func_name}",
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
raw = func(jd_local, place)
|
|
||||||
results[output_key] = self._format_special_lagna_payload(
|
|
||||||
full_name, raw, asc_degree, method=f"PyJHora drik.{func_name}",
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
results[output_key] = {
|
|
||||||
"full_name": full_name,
|
|
||||||
"status": "blocked",
|
|
||||||
"source": "pyjhora_drik",
|
|
||||||
"reason": str(exc),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
from jhora.horoscope.chart import charts
|
|
||||||
if not hasattr(charts, "varnada_lagna"):
|
|
||||||
raise AttributeError("pyjhora_missing_function:varnada_lagna")
|
|
||||||
raw = charts.varnada_lagna(
|
|
||||||
(birth_time.year, birth_time.month, birth_time.day),
|
|
||||||
(birth_time.hour, birth_time.minute, birth_time.second),
|
|
||||||
place,
|
|
||||||
varnada_method=1,
|
|
||||||
)
|
|
||||||
results["Varnada_Lagna"] = self._format_special_lagna_payload(
|
|
||||||
"Varnada Lagna", raw, asc_degree, "PyJHora varnada method 1",
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
reason = str(exc) or "pyjhora_missing_function:varnada_lagna"
|
|
||||||
results["Varnada_Lagna"] = {
|
|
||||||
"full_name": "Varnada Lagna",
|
|
||||||
"status": "blocked",
|
|
||||||
"source": "pyjhora_drik",
|
|
||||||
"reason": reason,
|
|
||||||
}
|
|
||||||
if ayanamsa is not None:
|
|
||||||
for row in results.values():
|
|
||||||
row["ayanamsa"] = ayanamsa
|
|
||||||
finally:
|
|
||||||
with redirect_stdout(io.StringIO()):
|
|
||||||
if previous_pyjhora:
|
|
||||||
try:
|
|
||||||
drik.set_ayanamsa_mode(previous_pyjhora)
|
|
||||||
except Exception:
|
|
||||||
jhora_const._DEFAULT_AYANAMSA_MODE = previous_pyjhora
|
|
||||||
if apply_ayanamsa is not None and previous_swe:
|
|
||||||
apply_ayanamsa(previous_swe)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def calculate_bhava_lagna(self, asc_degree: float, sun_degree: float,
|
def calculate_bhava_lagna(self, asc_degree: float, sun_degree: float,
|
||||||
moon_degree: float) -> Dict:
|
moon_degree: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+15
-15
@@ -15,6 +15,7 @@ Tajika/Varshaphala年运盘模块 v1.0
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
import importlib
|
||||||
import math
|
import math
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -1403,21 +1404,20 @@ def _external_pyjhora_panchavargiya_score(
|
|||||||
if annual_jd is None or lat is None or lon is None or tz is None:
|
if annual_jd is None or lat is None or lon is None or tz is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
from jhora.panchanga import drik as j_drik
|
# The in-process reference engine lives in a PyJHora adapter module.
|
||||||
from jhora.horoscope.chart import strength as j_strength
|
# This file does not import it unless a caller actually asks for a score.
|
||||||
place = j_drik.Place('annual_year_lord_probe', float(lat), float(lon), float(tz))
|
try:
|
||||||
# PyJHora annual-chart APIs use the local civil JD for a Place whose
|
reference = importlib.import_module("pyjhora_panchavargiya_reference")
|
||||||
# timezone is already supplied. ``annual_jd`` reaches us as UT.
|
except ModuleNotFoundError:
|
||||||
scores = j_strength.pancha_vargeeya_bala(float(annual_jd) + float(tz) / 24.0, place)
|
reference = importlib.import_module("scripts.pyjhora_panchavargiya_reference")
|
||||||
planet_idx = PLANET_TO_INDEX.get(planet)
|
return reference.external_panchavargiya_score(
|
||||||
if planet_idx is None:
|
planet,
|
||||||
return None
|
annual_jd=float(annual_jd),
|
||||||
score = scores[planet_idx]
|
lat=float(lat),
|
||||||
return {
|
lon=float(lon),
|
||||||
'score': float(score),
|
tz=float(tz),
|
||||||
'engine': 'pyjhora',
|
planet_index=PLANET_TO_INDEX.get(planet),
|
||||||
'components': {'pyjhora_pancha_vargeeya_bala': float(score)},
|
)
|
||||||
}
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return _external_pyjhora_panchavargiya_score_sidecar(planet, annual_jd, lat, lon, tz, ayanamsa_name)
|
return _external_pyjhora_panchavargiya_score_sidecar(planet, annual_jd, lat, lon, tz, ayanamsa_name)
|
||||||
|
|
||||||
|
|||||||
+355
@@ -0,0 +1,355 @@
|
|||||||
|
{
|
||||||
|
"pyjhora_version": "4.8.7",
|
||||||
|
"generator": "python scripts/research/freeze_special_lagnas_pyjhora_fixture.py",
|
||||||
|
"generated_on": "2026-10-08",
|
||||||
|
"cases": {
|
||||||
|
"steve_jobs_1955_aa": {
|
||||||
|
"source": "references/public_oracle_cases.json id steve_jobs_1955_aa",
|
||||||
|
"birth": {
|
||||||
|
"year": 1955,
|
||||||
|
"month": 2,
|
||||||
|
"day": 24,
|
||||||
|
"hour": 19,
|
||||||
|
"minute": 15,
|
||||||
|
"second": 0,
|
||||||
|
"lat": 37.7749,
|
||||||
|
"lon": -122.4194,
|
||||||
|
"tz": -8
|
||||||
|
},
|
||||||
|
"ayanamsa": {
|
||||||
|
"lahiri": {
|
||||||
|
"Bhava_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 17.119787409502464,
|
||||||
|
"longitude": 137.11978740950246
|
||||||
|
},
|
||||||
|
"Hora_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 22.57519513328691,
|
||||||
|
"longitude": 322.5751951332869
|
||||||
|
},
|
||||||
|
"Ghati_Lagna": {
|
||||||
|
"sign": "Virgo",
|
||||||
|
"sign_index": 5,
|
||||||
|
"sign_degree": 8.941418304640138,
|
||||||
|
"longitude": 158.94141830464014
|
||||||
|
},
|
||||||
|
"ViGhati_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 8.988843112785617,
|
||||||
|
"longitude": 278.9888431127856
|
||||||
|
},
|
||||||
|
"Sree_Lagna": {
|
||||||
|
"sign": "Cancer",
|
||||||
|
"sign_index": 3,
|
||||||
|
"sign_degree": 0.8709657052586977,
|
||||||
|
"longitude": 90.8709657052587
|
||||||
|
},
|
||||||
|
"Indu_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 14.511681812210952,
|
||||||
|
"longitude": 314.51168181221095
|
||||||
|
},
|
||||||
|
"Pranapada_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 29.151528565147373,
|
||||||
|
"longitude": 299.1515285651474
|
||||||
|
},
|
||||||
|
"Varnada_Lagna": {
|
||||||
|
"sign": "Cancer",
|
||||||
|
"sign_index": 3,
|
||||||
|
"sign_degree": 29.05555677556339,
|
||||||
|
"longitude": 119.05555677556339
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"raman": {
|
||||||
|
"Bhava_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 18.56608878347214,
|
||||||
|
"longitude": 138.56608878347214
|
||||||
|
},
|
||||||
|
"Hora_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 24.021496507256643,
|
||||||
|
"longitude": 324.02149650725664
|
||||||
|
},
|
||||||
|
"Ghati_Lagna": {
|
||||||
|
"sign": "Virgo",
|
||||||
|
"sign_index": 5,
|
||||||
|
"sign_degree": 10.387719678609983,
|
||||||
|
"longitude": 160.38771967860998
|
||||||
|
},
|
||||||
|
"ViGhati_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 10.435144486755235,
|
||||||
|
"longitude": 280.43514448675523
|
||||||
|
},
|
||||||
|
"Sree_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 11.36740417631404,
|
||||||
|
"longitude": 131.36740417631404
|
||||||
|
},
|
||||||
|
"Indu_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 15.957983186177216,
|
||||||
|
"longitude": 135.95798318617722
|
||||||
|
},
|
||||||
|
"Pranapada_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 0.5978299391135806,
|
||||||
|
"longitude": 300.5978299391136
|
||||||
|
},
|
||||||
|
"Varnada_Lagna": {
|
||||||
|
"sign": "Sagittarius",
|
||||||
|
"sign_index": 8,
|
||||||
|
"sign_degree": 0.5018581495296246,
|
||||||
|
"longitude": 240.50185814952962
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"barack_obama_1961_aa": {
|
||||||
|
"source": "references/public_oracle_cases.json id barack_obama_1961_aa",
|
||||||
|
"birth": {
|
||||||
|
"year": 1961,
|
||||||
|
"month": 8,
|
||||||
|
"day": 4,
|
||||||
|
"hour": 19,
|
||||||
|
"minute": 24,
|
||||||
|
"second": 0,
|
||||||
|
"lat": 21.3069,
|
||||||
|
"lon": -157.8583,
|
||||||
|
"tz": -10
|
||||||
|
},
|
||||||
|
"ayanamsa": {
|
||||||
|
"lahiri": {
|
||||||
|
"Bhava_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 6.8803782487445915,
|
||||||
|
"longitude": 306.8803782487446
|
||||||
|
},
|
||||||
|
"Hora_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 25.45288250839053,
|
||||||
|
"longitude": 145.45288250839053
|
||||||
|
},
|
||||||
|
"Ghati_Lagna": {
|
||||||
|
"sign": "Aries",
|
||||||
|
"sign_index": 0,
|
||||||
|
"sign_degree": 21.170395287328347,
|
||||||
|
"longitude": 21.170395287328347
|
||||||
|
},
|
||||||
|
"ViGhati_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 22.658129567855212,
|
||||||
|
"longitude": 142.6581295678552
|
||||||
|
},
|
||||||
|
"Sree_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 25.798783888142964,
|
||||||
|
"longitude": 295.79878388814296
|
||||||
|
},
|
||||||
|
"Indu_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 10.039455365709898,
|
||||||
|
"longitude": 280.0394553657099
|
||||||
|
},
|
||||||
|
"Pranapada_Lagna": {
|
||||||
|
"sign": "Cancer",
|
||||||
|
"sign_index": 3,
|
||||||
|
"sign_degree": 29.801856125186973,
|
||||||
|
"longitude": 119.80185612518697
|
||||||
|
},
|
||||||
|
"Varnada_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 24.73348901397577,
|
||||||
|
"longitude": 324.73348901397577
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"raman": {
|
||||||
|
"Bhava_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 8.326679613187252,
|
||||||
|
"longitude": 308.32667961318725
|
||||||
|
},
|
||||||
|
"Hora_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 26.89918387283319,
|
||||||
|
"longitude": 146.8991838728332
|
||||||
|
},
|
||||||
|
"Ghati_Lagna": {
|
||||||
|
"sign": "Aries",
|
||||||
|
"sign_index": 0,
|
||||||
|
"sign_degree": 22.616696651771008,
|
||||||
|
"longitude": 22.616696651771008
|
||||||
|
},
|
||||||
|
"ViGhati_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 24.104430932296964,
|
||||||
|
"longitude": 144.10443093229696
|
||||||
|
},
|
||||||
|
"Sree_Lagna": {
|
||||||
|
"sign": "Pisces",
|
||||||
|
"sign_index": 11,
|
||||||
|
"sign_degree": 6.29522209242748,
|
||||||
|
"longitude": 336.2952220924275
|
||||||
|
},
|
||||||
|
"Indu_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 11.48575673014863,
|
||||||
|
"longitude": 281.48575673014864
|
||||||
|
},
|
||||||
|
"Pranapada_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 1.2481574896256973,
|
||||||
|
"longitude": 121.2481574896257
|
||||||
|
},
|
||||||
|
"Varnada_Lagna": {
|
||||||
|
"sign": "Aquarius",
|
||||||
|
"sign_index": 10,
|
||||||
|
"sign_degree": 26.17979037841451,
|
||||||
|
"longitude": 326.1797903784145
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fictional_reader_main": {
|
||||||
|
"source": "tests/test_report_reader_main.py FICTIONAL",
|
||||||
|
"birth": {
|
||||||
|
"year": 1991,
|
||||||
|
"month": 4,
|
||||||
|
"day": 7,
|
||||||
|
"hour": 9,
|
||||||
|
"minute": 15,
|
||||||
|
"second": 0,
|
||||||
|
"lat": 30.5728,
|
||||||
|
"lon": 104.0668,
|
||||||
|
"tz": 8.0
|
||||||
|
},
|
||||||
|
"ayanamsa": {
|
||||||
|
"lahiri": {
|
||||||
|
"Bhava_Lagna": {
|
||||||
|
"sign": "Aries",
|
||||||
|
"sign_index": 0,
|
||||||
|
"sign_degree": 29.32196728818468,
|
||||||
|
"longitude": 29.32196728818468
|
||||||
|
},
|
||||||
|
"Hora_Lagna": {
|
||||||
|
"sign": "Gemini",
|
||||||
|
"sign_index": 2,
|
||||||
|
"sign_degree": 5.418736449707069,
|
||||||
|
"longitude": 65.41873644970707
|
||||||
|
},
|
||||||
|
"Ghati_Lagna": {
|
||||||
|
"sign": "Virgo",
|
||||||
|
"sign_index": 5,
|
||||||
|
"sign_degree": 23.709043934274177,
|
||||||
|
"longitude": 173.70904393427418
|
||||||
|
},
|
||||||
|
"ViGhati_Lagna": {
|
||||||
|
"sign": "Pisces",
|
||||||
|
"sign_index": 11,
|
||||||
|
"sign_degree": 29.031347818005543,
|
||||||
|
"longitude": 359.03134781800554
|
||||||
|
},
|
||||||
|
"Sree_Lagna": {
|
||||||
|
"sign": "Scorpio",
|
||||||
|
"sign_index": 7,
|
||||||
|
"sign_degree": 22.29743005005116,
|
||||||
|
"longitude": 232.29743005005116
|
||||||
|
},
|
||||||
|
"Indu_Lagna": {
|
||||||
|
"sign": "Gemini",
|
||||||
|
"sign_index": 2,
|
||||||
|
"sign_degree": 20.498014260250045,
|
||||||
|
"longitude": 80.49801426025005
|
||||||
|
},
|
||||||
|
"Pranapada_Lagna": {
|
||||||
|
"sign": "Cancer",
|
||||||
|
"sign_index": 3,
|
||||||
|
"sign_degree": 23.762593993954397,
|
||||||
|
"longitude": 113.7625939939544
|
||||||
|
},
|
||||||
|
"Varnada_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 8.851045023300273,
|
||||||
|
"longitude": 128.8510450233003
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"raman": {
|
||||||
|
"Bhava_Lagna": {
|
||||||
|
"sign": "Taurus",
|
||||||
|
"sign_index": 1,
|
||||||
|
"sign_degree": 0.7682686123160352,
|
||||||
|
"longitude": 30.768268612316035
|
||||||
|
},
|
||||||
|
"Hora_Lagna": {
|
||||||
|
"sign": "Gemini",
|
||||||
|
"sign_index": 2,
|
||||||
|
"sign_degree": 6.865037773838424,
|
||||||
|
"longitude": 66.86503777383842
|
||||||
|
},
|
||||||
|
"Ghati_Lagna": {
|
||||||
|
"sign": "Virgo",
|
||||||
|
"sign_index": 5,
|
||||||
|
"sign_degree": 25.155345258405532,
|
||||||
|
"longitude": 175.15534525840553
|
||||||
|
},
|
||||||
|
"ViGhati_Lagna": {
|
||||||
|
"sign": "Aries",
|
||||||
|
"sign_index": 0,
|
||||||
|
"sign_degree": 0.4776491421371247,
|
||||||
|
"longitude": 0.4776491421371247
|
||||||
|
},
|
||||||
|
"Sree_Lagna": {
|
||||||
|
"sign": "Capricorn",
|
||||||
|
"sign_index": 9,
|
||||||
|
"sign_degree": 2.793867125751376,
|
||||||
|
"longitude": 272.7938671257514
|
||||||
|
},
|
||||||
|
"Indu_Lagna": {
|
||||||
|
"sign": "Gemini",
|
||||||
|
"sign_index": 2,
|
||||||
|
"sign_degree": 21.944315584382196,
|
||||||
|
"longitude": 81.9443155843822
|
||||||
|
},
|
||||||
|
"Pranapada_Lagna": {
|
||||||
|
"sign": "Cancer",
|
||||||
|
"sign_index": 3,
|
||||||
|
"sign_degree": 25.208895318086547,
|
||||||
|
"longitude": 115.20889531808655
|
||||||
|
},
|
||||||
|
"Varnada_Lagna": {
|
||||||
|
"sign": "Leo",
|
||||||
|
"sign_index": 4,
|
||||||
|
"sign_degree": 10.297346347432423,
|
||||||
|
"longitude": 130.29734634743244
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
"""BUG-1273: chart, report, and consult entries must not import PyJHora.
|
||||||
|
|
||||||
|
The walk starts at cmd_full_reading, the report builder, and the consult-card
|
||||||
|
entries. It follows AST imports and constant import_module strings. Adapter
|
||||||
|
files whose names contain pyjhora, and scripts/research, stay off the walk.
|
||||||
|
A string that merely mentions ``from jhora`` is not an import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ENTRIES = (
|
||||||
|
"scripts/jyotish_engine.py",
|
||||||
|
"scripts/professional_report_reference.py",
|
||||||
|
"scripts/consultation_workflow_service.py",
|
||||||
|
"scripts/consultation_native_layers.py",
|
||||||
|
)
|
||||||
|
SELF = "tests/test_runtime_jhora_import_scan.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_jhora_module(module: str) -> bool:
|
||||||
|
return module == "jhora" or module.startswith("jhora.")
|
||||||
|
|
||||||
|
|
||||||
|
def _skip_module_name(module: str) -> bool:
|
||||||
|
leaf = module.rsplit(".", 1)[-1]
|
||||||
|
if "pyjhora" in leaf or leaf.startswith("jhora"):
|
||||||
|
return True
|
||||||
|
return module == "research" or module.startswith(("research.", "scripts.research"))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_script(root: Path, module: str) -> Path | None:
|
||||||
|
parts = module.split(".")
|
||||||
|
if parts[0] == "scripts":
|
||||||
|
parts = parts[1:]
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
direct = root.joinpath("scripts", *parts)
|
||||||
|
python_file = direct.with_suffix(".py")
|
||||||
|
if python_file.is_file():
|
||||||
|
return python_file
|
||||||
|
init = direct / "__init__.py"
|
||||||
|
if init.is_file():
|
||||||
|
return init
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _import_module_argument(node: ast.Call) -> str | None:
|
||||||
|
func = node.func
|
||||||
|
if isinstance(func, ast.Name):
|
||||||
|
name = func.id
|
||||||
|
elif isinstance(func, ast.Attribute):
|
||||||
|
name = func.attr
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
if name != "import_module" or not node.args:
|
||||||
|
return None
|
||||||
|
arg = node.args[0]
|
||||||
|
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||||
|
return arg.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _enqueue_relative(path: Path, node: ast.ImportFrom, enqueue) -> None:
|
||||||
|
anchor = path.parent
|
||||||
|
for _ in range(node.level - 1):
|
||||||
|
anchor = anchor.parent
|
||||||
|
candidates = []
|
||||||
|
if node.module:
|
||||||
|
candidates.append(anchor.joinpath(*node.module.split(".")))
|
||||||
|
else:
|
||||||
|
candidates.extend(anchor / alias.name for alias in node.names)
|
||||||
|
for candidate in candidates:
|
||||||
|
python_file = candidate.with_suffix(".py")
|
||||||
|
if python_file.is_file():
|
||||||
|
enqueue(python_file)
|
||||||
|
elif (candidate / "__init__.py").is_file():
|
||||||
|
enqueue(candidate / "__init__.py")
|
||||||
|
|
||||||
|
|
||||||
|
def product_jhora_import_hits(root: Path, entries: tuple[str, ...]) -> tuple[list[str], list[str]]:
|
||||||
|
"""Return ``path:line:module`` hits and the product files the walk opened."""
|
||||||
|
scripts = (root / "scripts").resolve()
|
||||||
|
root_resolved = root.resolve()
|
||||||
|
hits: list[str] = []
|
||||||
|
visited: list[str] = []
|
||||||
|
seen: set[Path] = set()
|
||||||
|
queue: list[Path] = []
|
||||||
|
|
||||||
|
def rel(path: Path) -> str:
|
||||||
|
return path.resolve().relative_to(root_resolved).as_posix()
|
||||||
|
|
||||||
|
def enqueue(path: Path) -> None:
|
||||||
|
resolved = path.resolve()
|
||||||
|
if resolved in seen:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
script_rel = resolved.relative_to(scripts).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
if script_rel == "research" or script_rel.startswith("research/"):
|
||||||
|
return
|
||||||
|
stem = resolved.stem
|
||||||
|
if "pyjhora" in stem or stem.startswith("jhora"):
|
||||||
|
return
|
||||||
|
seen.add(resolved)
|
||||||
|
queue.append(resolved)
|
||||||
|
|
||||||
|
def consider(module: str, lineno: int, source_rel: str) -> None:
|
||||||
|
if _is_jhora_module(module):
|
||||||
|
hits.append(f"{source_rel}:{lineno}:{module}")
|
||||||
|
return
|
||||||
|
if _skip_module_name(module):
|
||||||
|
return
|
||||||
|
found = _resolve_script(root, module)
|
||||||
|
if found is not None:
|
||||||
|
enqueue(found)
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
enqueue(root / entry)
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
path = queue.pop()
|
||||||
|
source_rel = rel(path)
|
||||||
|
visited.append(source_rel)
|
||||||
|
try:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=source_rel)
|
||||||
|
except (OSError, SyntaxError) as exc:
|
||||||
|
hits.append(f"{source_rel}:0:{exc.__class__.__name__}")
|
||||||
|
continue
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
consider(alias.name, node.lineno, source_rel)
|
||||||
|
elif isinstance(node, ast.ImportFrom):
|
||||||
|
if node.level:
|
||||||
|
_enqueue_relative(path, node, enqueue)
|
||||||
|
continue
|
||||||
|
if node.module:
|
||||||
|
consider(node.module, node.lineno, source_rel)
|
||||||
|
elif isinstance(node, ast.Call):
|
||||||
|
module = _import_module_argument(node)
|
||||||
|
if module:
|
||||||
|
consider(module, node.lineno, source_rel)
|
||||||
|
return hits, visited
|
||||||
|
|
||||||
|
|
||||||
|
def test_product_entries_do_not_import_jhora():
|
||||||
|
hits, visited = product_jhora_import_hits(ROOT, ENTRIES)
|
||||||
|
assert hits == []
|
||||||
|
for required in (
|
||||||
|
"scripts/jyotish_engine.py",
|
||||||
|
"scripts/professional_report_reference.py",
|
||||||
|
"scripts/consultation_workflow_service.py",
|
||||||
|
"scripts/consultation_native_layers.py",
|
||||||
|
"scripts/special_lagnas.py",
|
||||||
|
"scripts/native_special_lagnas.py",
|
||||||
|
"scripts/tajika.py",
|
||||||
|
"scripts/pl9_reader_export.py",
|
||||||
|
):
|
||||||
|
assert required in visited
|
||||||
|
assert all("pyjhora" not in path and "/research/" not in path for path in visited)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scanner_is_on_the_quick_gate():
|
||||||
|
assert SELF in CORE_PYTEST_TARGETS
|
||||||
|
|
||||||
|
|
||||||
|
def test_scanner_flags_jhora_and_skips_adapters_research_and_strings(tmp_path):
|
||||||
|
scripts = tmp_path / "scripts"
|
||||||
|
research = scripts / "research"
|
||||||
|
research.mkdir(parents=True)
|
||||||
|
(scripts / "entry.py").write_text(
|
||||||
|
"import helper\n"
|
||||||
|
"import pyjhora_adapter\n"
|
||||||
|
"from jhora.panchanga import drik\n"
|
||||||
|
"import importlib\n"
|
||||||
|
"importlib.import_module('scripts.clean')\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(scripts / "helper.py").write_text(
|
||||||
|
'code = "from jhora.panchanga import drik"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(scripts / "pyjhora_adapter.py").write_text("import jhora\n", encoding="utf-8")
|
||||||
|
(scripts / "clean.py").write_text("value = 1\n", encoding="utf-8")
|
||||||
|
(research / "probe.py").write_text("import jhora\n", encoding="utf-8")
|
||||||
|
(scripts / "caller.py").write_text(
|
||||||
|
"import importlib\nimportlib.import_module('jhora.panchanga.drik')\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
hits, visited = product_jhora_import_hits(tmp_path, ("scripts/entry.py", "scripts/caller.py"))
|
||||||
|
assert "scripts/entry.py:3:jhora.panchanga" in hits
|
||||||
|
assert "scripts/caller.py:2:jhora.panchanga.drik" in hits
|
||||||
|
assert not any("helper.py" in hit for hit in hits)
|
||||||
|
assert "scripts/helper.py" in visited
|
||||||
|
assert "scripts/clean.py" in visited
|
||||||
|
assert all("pyjhora_adapter" not in path and "research/" not in path for path in visited)
|
||||||
|
|
||||||
|
|
||||||
|
def test_relative_import_of_jhora_string_is_not_required(tmp_path):
|
||||||
|
scripts = tmp_path / "scripts"
|
||||||
|
scripts.mkdir()
|
||||||
|
(scripts / "entry.py").write_text("from . import sibling\n", encoding="utf-8")
|
||||||
|
(scripts / "sibling.py").write_text("ready = True\n", encoding="utf-8")
|
||||||
|
hits, visited = product_jhora_import_hits(tmp_path, ("scripts/entry.py",))
|
||||||
|
assert hits == []
|
||||||
|
assert "scripts/sibling.py" in visited
|
||||||
@@ -1,71 +1,171 @@
|
|||||||
"""BUG-1265: special lagnas come from the birth instant, not the identical local formulas."""
|
"""BUG-1273: eight birth special lagnas are native, and rectification keeps the old formulas.
|
||||||
|
|
||||||
|
The frozen PyJHora 4.8.7 answers live in tests/fixtures/special_lagnas_pyjhora_487.json.
|
||||||
|
These tests do not import jhora. The offline generator is
|
||||||
|
scripts/research/freeze_special_lagnas_pyjhora_fixture.py.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from scripts.ayanamsa_utils import ACTIVE_AYANAMSA_NAME, apply_ayanamsa
|
import scripts.ayanamsa_utils as ayanamsa_utils
|
||||||
|
from scripts.ayanamsa_utils import apply_ayanamsa
|
||||||
from scripts.special_lagnas import SpecialLagnasCalculator, bhava_house_rows
|
from scripts.special_lagnas import SpecialLagnasCalculator, bhava_house_rows
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FIXTURE = ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json"
|
||||||
|
EIGHT = (
|
||||||
|
"Bhava_Lagna",
|
||||||
|
"Hora_Lagna",
|
||||||
|
"Ghati_Lagna",
|
||||||
|
"ViGhati_Lagna",
|
||||||
|
"Sree_Lagna",
|
||||||
|
"Indu_Lagna",
|
||||||
|
"Pranapada_Lagna",
|
||||||
|
"Varnada_Lagna",
|
||||||
|
)
|
||||||
|
CASE_IDS = ("steve_jobs_1955_aa", "barack_obama_1961_aa", "fictional_reader_main")
|
||||||
|
|
||||||
|
|
||||||
def _jobs_birth():
|
def _fixture():
|
||||||
payload = json.loads((ROOT / "references" / "public_oracle_cases.json").read_text(encoding="utf-8"))
|
payload = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||||
birth = next(row["birth"] for row in payload["cases"] if row["id"] == "steve_jobs_1955_aa")
|
assert payload["pyjhora_version"] == "4.8.7"
|
||||||
return birth
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def test_public_jobs_hora_and_ghati_differ_and_match_birth_instant():
|
def _block_jhora(monkeypatch):
|
||||||
birth = _jobs_birth()
|
"""Fail closed if the runtime path imports PyJHora, including a cached package."""
|
||||||
apply_ayanamsa("raman")
|
real_import = builtins.__import__
|
||||||
before = ACTIVE_AYANAMSA_NAME
|
|
||||||
moment = datetime(birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], birth["second"])
|
def guarded(name, globals=None, locals=None, fromlist=(), level=0):
|
||||||
rows = SpecialLagnasCalculator().calculate_all_lagnas(
|
if str(name).split(".", 1)[0] == "jhora":
|
||||||
0.0, 0.0, 0.0, moment, moment.replace(hour=6),
|
raise ImportError("jhora blocked")
|
||||||
lat=birth["lat"], lon=birth["lon"], tz_offset=birth["tz"], ayanamsa="lahiri",
|
return real_import(name, globals, locals, fromlist, level)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", guarded)
|
||||||
|
real_import_module = importlib.import_module
|
||||||
|
|
||||||
|
def guarded_module(name, package=None):
|
||||||
|
if str(name).split(".", 1)[0] == "jhora":
|
||||||
|
raise ImportError("jhora blocked")
|
||||||
|
return real_import_module(name, package)
|
||||||
|
|
||||||
|
monkeypatch.setattr(importlib, "import_module", guarded_module)
|
||||||
|
for key in list(sys.modules):
|
||||||
|
if key == "jhora" or key.startswith("jhora."):
|
||||||
|
monkeypatch.delitem(sys.modules, key, raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _rows(birth, ayanamsa):
|
||||||
|
moment = datetime(
|
||||||
|
birth["year"], birth["month"], birth["day"],
|
||||||
|
birth["hour"], birth["minute"], birth["second"],
|
||||||
)
|
)
|
||||||
|
return SpecialLagnasCalculator().calculate_all_lagnas(
|
||||||
|
0.0, 0.0, 0.0, moment, moment.replace(hour=6),
|
||||||
|
lat=birth["lat"], lon=birth["lon"], tz_offset=birth["tz"], ayanamsa=ayanamsa,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("case_id", CASE_IDS)
|
||||||
|
@pytest.mark.parametrize("ayanamsa", ["lahiri", "raman"])
|
||||||
|
def test_native_matches_frozen_pyjhora_within_one_hundredth(case_id, ayanamsa, monkeypatch):
|
||||||
|
_block_jhora(monkeypatch)
|
||||||
|
spec = _fixture()["cases"][case_id]
|
||||||
|
before = ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||||||
|
rows = _rows(spec["birth"], ayanamsa)
|
||||||
|
expected = spec["ayanamsa"][ayanamsa]
|
||||||
|
for key in EIGHT:
|
||||||
|
row = rows[key]
|
||||||
|
assert row["status"] == "computed"
|
||||||
|
assert row["source"] == "native_special_lagna"
|
||||||
|
assert "pyjhora" not in row["source"]
|
||||||
|
assert "pyjhora" not in json.dumps(row).lower()
|
||||||
|
assert row["sign"] == expected[key]["sign"]
|
||||||
|
assert row["sign_degree"] == pytest.approx(expected[key]["sign_degree"], abs=0.01)
|
||||||
|
assert row["degree"] == pytest.approx(expected[key]["longitude"], abs=0.01)
|
||||||
|
assert row["ayanamsa"] == ayanamsa
|
||||||
|
assert abs(rows["Hora_Lagna"]["degree"] - rows["Ghati_Lagna"]["degree"]) > 0.01
|
||||||
|
assert before == ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def test_jobs_lahiri_signs_stay_on_the_birth_instant(monkeypatch):
|
||||||
|
_block_jhora(monkeypatch)
|
||||||
|
apply_ayanamsa("raman")
|
||||||
|
before = ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||||||
|
birth = _fixture()["cases"]["steve_jobs_1955_aa"]["birth"]
|
||||||
|
rows = _rows(birth, "lahiri")
|
||||||
expected = {
|
expected = {
|
||||||
"Bhava_Lagna": ("Leo", 17.12),
|
"Bhava_Lagna": ("Leo", 17.12),
|
||||||
"Hora_Lagna": ("Aquarius", 22.58),
|
"Hora_Lagna": ("Aquarius", 22.58),
|
||||||
"Ghati_Lagna": ("Virgo", 8.94),
|
"Ghati_Lagna": ("Virgo", 8.94),
|
||||||
}
|
}
|
||||||
for key, (sign, degree) in expected.items():
|
for key, (sign, degree) in expected.items():
|
||||||
assert rows[key]["status"] == "computed"
|
|
||||||
assert rows[key]["sign"] == sign
|
assert rows[key]["sign"] == sign
|
||||||
assert rows[key]["sign_degree"] == pytest.approx(degree, abs=0.01)
|
assert rows[key]["sign_degree"] == pytest.approx(degree, abs=0.01)
|
||||||
assert rows[key]["source"] == "pyjhora_drik"
|
|
||||||
assert rows[key]["ayanamsa"] == "lahiri"
|
|
||||||
assert rows["Hora_Lagna"]["sign"] != rows["Ghati_Lagna"]["sign"]
|
assert rows["Hora_Lagna"]["sign"] != rows["Ghati_Lagna"]["sign"]
|
||||||
for key in ("Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
assert before == ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||||||
assert rows[key]["status"] == "computed"
|
|
||||||
assert rows[key]["sign"]
|
|
||||||
assert ACTIVE_AYANAMSA_NAME == before
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_coordinates_block_pyjhora_points_without_inventing_them():
|
def test_rectification_hora_and_ghati_formulas_stay_on_the_old_identity():
|
||||||
moment = datetime(1955, 2, 24, 19, 15, 0)
|
birth = datetime(2000, 1, 1, 19, 15, 0)
|
||||||
rows = SpecialLagnasCalculator().calculate_all_lagnas(10.0, 20.0, 30.0, moment, moment.replace(hour=6))
|
sunrise = birth.replace(hour=6, minute=0, second=0)
|
||||||
assert rows["Sree_Lagna"]["status"] == "blocked"
|
calculator = SpecialLagnasCalculator()
|
||||||
assert rows["Sree_Lagna"]["reason"] == "birth_coordinates_or_timezone_missing"
|
hora = calculator.calculate_hora_lagna(10.0, 0.0, birth, sunrise)
|
||||||
assert rows["Hora_Lagna"]["formula"]
|
ghati = calculator.calculate_ghati_lagna(10.0, birth, sunrise)
|
||||||
|
assert hora["formula"] == "Asc + (Hora数 × 15°)"
|
||||||
|
assert ghati["formula"] == "Asc + (Ghati数 × 6°)"
|
||||||
|
assert hora["degree"] == pytest.approx(208.75, abs=1e-4)
|
||||||
|
assert ghati["degree"] == pytest.approx(208.75, abs=1e-4)
|
||||||
|
assert not hasattr(calculator, "calculate_pyjhora_special_lagnas")
|
||||||
|
|
||||||
|
|
||||||
def test_missing_pyjhora_function_is_blocked(monkeypatch):
|
def test_missing_coordinates_keep_old_three_and_block_the_other_five():
|
||||||
import jhora.panchanga.drik as drik
|
moment = datetime(2000, 1, 1, 19, 15, 0)
|
||||||
|
sunrise = moment.replace(hour=6, minute=0, second=0)
|
||||||
monkeypatch.delattr(drik, "pranapada_lagna", raising=False)
|
rows = SpecialLagnasCalculator().calculate_all_lagnas(
|
||||||
birth = _jobs_birth()
|
10.0, 20.0, 30.0, moment, sunrise,
|
||||||
moment = datetime(birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], birth["second"])
|
|
||||||
rows = SpecialLagnasCalculator().calculate_pyjhora_special_lagnas(
|
|
||||||
moment, birth["lat"], birth["lon"], birth["tz"], 0.0, ayanamsa="lahiri",
|
|
||||||
)
|
)
|
||||||
assert rows["Pranapada_Lagna"]["status"] == "blocked"
|
assert rows["Bhava_Lagna"]["formula"] == "Asc + (Sun - Moon)"
|
||||||
assert "pranapada_lagna" in rows["Pranapada_Lagna"]["reason"]
|
assert rows["Hora_Lagna"]["formula"] == "Asc + (Hora数 × 15°)"
|
||||||
|
assert rows["Hora_Lagna"]["degree"] == pytest.approx(208.75, abs=1e-4)
|
||||||
|
assert rows["Ghati_Lagna"]["formula"] == "Asc + (Ghati数 × 6°)"
|
||||||
|
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
||||||
|
assert rows[key]["status"] == "blocked"
|
||||||
|
assert rows[key]["reason"] == "birth_coordinates_or_timezone_missing"
|
||||||
|
assert rows[key]["source"] == "native_special_lagna"
|
||||||
|
assert "pyjhora" not in rows[key]["source"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_reading_eight_points_with_jhora_blocked(monkeypatch):
|
||||||
|
from scripts.jyotish_engine import cmd_full_reading
|
||||||
|
from scripts.professional_report_reference import _export_args
|
||||||
|
|
||||||
|
_block_jhora(monkeypatch)
|
||||||
|
for key in list(sys.modules):
|
||||||
|
if key in {
|
||||||
|
"special_lagnas",
|
||||||
|
"scripts.special_lagnas",
|
||||||
|
"native_special_lagnas",
|
||||||
|
"scripts.native_special_lagnas",
|
||||||
|
} or key == "jhora" or key.startswith("jhora."):
|
||||||
|
monkeypatch.delitem(sys.modules, key, raising=False)
|
||||||
|
birth = _fixture()["cases"]["steve_jobs_1955_aa"]["birth"]
|
||||||
|
report = cmd_full_reading(_export_args({**birth, "ayanamsa": "lahiri", "node_mode": "mean"}))
|
||||||
|
points = report["modules"]["special_lagnas"]
|
||||||
|
for key in EIGHT:
|
||||||
|
row = points[key]
|
||||||
|
assert row["status"] == "computed", key
|
||||||
|
assert row["source"] == "native_special_lagna"
|
||||||
|
assert "pyjhora" not in json.dumps(row).lower()
|
||||||
|
|
||||||
|
|
||||||
def test_bhava_house_rows_drop_summary_rows():
|
def test_bhava_house_rows_drop_summary_rows():
|
||||||
|
|||||||
Reference in New Issue
Block a user