research(d60): ship the G2/G5 script and report per-switch flips (issue #149)

The G2 precheck and the G5 noise floor had only a results file; the script
that produces them is now in scripts/research/d60_two_way_g2_g5.py, with
a test that reproduces the committed JSON byte for byte.

G5 is now reported per switch, with the ayanamsa offset in arcminutes.
Against the Raman baseline all three alternatives flip 77/77. Between the
alternatives themselves the number follows the offset: Lahiri to KP 21/77
(27.3%, 5.81'), KP to True Chitra 19/77 (24.7%, 5.02'), Lahiri to True
Chitra 2/77 (2.6%, 0.83'). The report says so and no longer claims that
any frame change flips every chart; the registered gate is baseline
relative and still triggers.

The report also states that G2's "not evaluable" comes from the registered
same-day answer policy, not from a measurement that D60 does not help.

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-29 16:32:54 +08:00
co-authored by Grok (agent loop)
parent 09620c6e0a
commit 3bc82acba6
6 changed files with 1819 additions and 911 deletions
+1 -1
View File
@@ -20472,7 +20472,7 @@
- 触发条件:用 D60 换格给候选分段打分,或对外宣称「到秒」。
- 根因:未验证的技法被当成能力宣传;我方没有 D60 换格的评分实现,也没有对照实验。
- 结论(研究单已做完):G1 77/77 通过(BUG-1500 修好映射之后)。G2 按先登记口径**不可评估**:D60 换格题只认同日事件,v5 的 77 例里出生当天事件 0 件,B 组加不上任何判据、与 A 组相同。G3 平凡成立;G4 不适用。G5 触发上限:只换岁差(raman→lahiri / kp / true_citra),D60 上升格 77/77 全变(100%,阈值 20%)。因此**不做**:D60 不进校正判据,技法路由与文案不改;对外不出现「到秒 / 分钟级精确」,与 BUG-1240 同一口径。
- 验证:报告 `docs/research/rectification_d60_two_way_2026_10_29.md`;结果 `docs/research/d60_two_way_results_2026_10_29.json`(G1)与 `docs/research/d60_two_way_g2_g5_results_2026_10_29.json`(G2 预检与 G5);先登记 `docs/research/d60_two_way_preregistration_2026_10_28.json`。
- 验证:报告 `docs/research/rectification_d60_two_way_2026_10_29.md`;结果 `docs/research/d60_two_way_results_2026_10_29.json`(G1)与 `docs/research/d60_two_way_g2_g5_results_2026_10_29.json`(G2 预检与 G5),分别由 `scripts/research/d60_two_way_run.py` 与 `scripts/research/d60_two_way_g2_g5.py` 生成,可逐字节复跑;先登记 `docs/research/d60_two_way_preregistration_2026_10_28.json`。
- 防复发:研究单写死「先登记再跑、真值不可见、必须有安慰剂、负结果照写」。
- 关联:BUG-1240、BUG-560、BUG-1090、BUG-1091。
- 复发自:无。
File diff suppressed because it is too large Load Diff
@@ -1,51 +1,68 @@
# D60 换格要不要进生时校正:双向对比报告(2026-10-29,issue #149)
- 任务书:`docs/tasks/TASK-rectification-d60-two-way-20261028.md`;修复单 `docs/tasks/TASK-rectification-d60-fix-20261029.md`
- 任务书:`docs/tasks/TASK-rectification-d60-two-way-20261028.md`;修复单 `docs/tasks/TASK-rectification-d60-fix-20261029.md`、`docs/tasks/TASK-rectification-d60-t5-fix-20261029.md`
- 先登记:`docs/research/d60_two_way_preregistration_2026_10_28.json`(commit 早于任何计分)
- 数据:v5 77 例、964 件事(`minute_rectification_holdout_v5.json`,sha256 `ab365a3e…`)
- 口径:ayanamsa raman、node mean、候选步长 2 分钟、半径 ±10 / ±30 / ±60
- 脚本:`scripts/research/d60_two_way_run.py`(G1)、`scripts/research/d60_two_way_g2_g5.py`(G2 预检与 G5),输出排序、无时间戳,可逐字节复跑
## 结论表
| 门 | 结果 | 关键数字 | 对照 |
| --- | --- | --- | --- |
| G1 三引擎对账 | **过** | 77/77 与 PyJHora 一致 | 修映射前 33/77(BUG-1500) |
| G2 找回记录分钟 | **不可评估**(按登记口径) | 可用 D60 换格题 **0 条**:77 例里没有一件事与出生同日 | B 组与 A 组完全相同,提升恒为 0,不是「测出来没有帮助」 |
| G2 找回记录分钟 | **不可评估**(口径造成) | 可用 D60 换格题 **0 条**:77 例里出生当天事件 0 件 | B 组与 A 组完全相同,提升恒为 0 |
| G3 不伤真值 | 平凡成立 | B 组不淘汰任何候选,区间与 A 相同 | A 基线 76/77 |
| G4 安慰剂 | **不适用** | 没有可检验的提升 | — |
| G5 噪声地板 | **触发上限** | 换岁差后 D60 上升格 **77/77 全变**(Lahiri / KP / True Chitra 都是 100%,阈值 20%) | — |
| G5 噪声地板 | **触发上限** | 以 Raman 为基准换岁差:三种换法都是 **77/77(100%,阈值 20%)**;换格幅度见下表 | — |
**产品结论:不做。** 维持「D60 只作参考,不驱动结论」;对外文案不得出现「到秒」「分钟级精确」,与 BUG-1240 的纳迪结论同一口径。
## 为什么 G2 是「不可评估」而不是「没有帮助」
登记的口径(任务书 §3.3)是:每次 D60 换格当一题「事件在换格之前还是之后」,**只有事件日期与换格同一天才作答**,否则跳过、不淘汰。D60 换格发生在出生当天,因此只有出生当天的事件能回答这类题。v5 的 77 例、964 件事里,出生当天的事件是 **0** 件(逐例核对,见结果 JSON)。于是 B 组一条额外判据都加不上,A、B 两组逐字节相同。
G2 的计分口径是上一轮修正单 §3.3 定的:每次 D60 换格当一题「事件在换格之前还是之后」,**只有事件日期与换格同一天才作答**。D60 换格发生在出生当天,因此只有出生当天的事件能回答这类题;v5 的 77 例、964 件事里出生当天的事件是 **0** 件(逐例核对,见结果 JSON 的 `g2_precheck.per_case`)。于是 B 组一条额外判据都加不上,A、B 两组逐字节相同。
这条限制来自证据格式,不是 D60 本身:v5 只记录日期(244 件日精度、其余到月或年),没有当天时刻。若要用 D60 换格判分钟,需要带时刻的事件,或另一套能给出时刻的证据;这批数据给不了。
**这是口径设计造成的不可评估,不是把 D60 测出了「没用」。** 规划方(Claude)在本轮验收里也认领了这一点:这个口径注定 0 题。
## G5:D60 在分钟尺度上完全取决于岁差
## G5:分口径的数字
同一批盘、同一个出生时刻,只换岁差(raman → lahiri / kp / true_citra),上升的 D60 星座 **77 例全部改变**(100%,阈值 20%)。换句话说,D60 上升格在 2 分钟量级上本来就会随口径翻格;任何「D60 把关可以把时间收到秒」的说法,在这个噪声地板上站不住。
基准是数据声明的 Raman。换岁差后记录时刻的 D60 上升格变化:
外部资料的说法与此一致:D60 每格 0.5°、上升约每 2 分钟换一格,记录时间按 5 分钟取整就跨两三个 D60 上升格(VedaCharts、Nyovah,见 MEVG)。他们给的也是「出生时间要准到分钟」,不是「用 D60 反推能到秒」。
| 换法 | 变了多少例 | 比例 | 岁差差值(角分,中位) |
| --- | ---: | ---: | ---: |
| Raman → Lahiri | 77 / 77 | 100% | 86.78′ |
| Raman → KP | 77 / 77 | 100% | 80.97′ |
| Raman → True Chitra | 77 / 77 | 100% | 85.95′(85.51–86.27) |
替代岁差两两之间(同一批盘、同一时刻):
| 换法 | 变了多少例 | 比例 | 岁差差值(角分,中位) |
| --- | ---: | ---: | ---: |
| Lahiri → KP | 21 / 77 | 27.3% | 5.81′ |
| KP → True Chitra | 19 / 77 | 24.7% | 5.02′ |
| Lahiri → True Chitra | 2 / 77 | **2.6%** | 0.83′ |
读法:换格比例跟着两组岁差的差走。差值接近或超过一个 D60 格(30′)时必然大面积翻格(Raman 与三种替代都差 80′ 以上,100%);差值只有约 0.8′ 时(Lahiri 与 True Chitra)只有 2.6%。登记的门是「相对基准 Raman 的换法」,三种都 100%,超过 20% 阈值,所以触发「不得出现到秒 / 分钟级精确」。
**对「低于 20% 就重写结论」的处理**:Lahiri→True Chitra 2.6% 确实低于 20%。这一对是两套几乎重合的岁差(差 0.83′),它说明的是「岁差接近时 D60 不会翻」,而不是「D60 对口径不敏感」。因此我把 G5 的措辞改成分口径报数,不再说「换任何岁差都全变」;产品建议不变——主流岁差之间相差 5–87′,而哪一套岁差才是"对"的本身没有共识,用 D60 去把出生时间收到秒,仍然是用一个比答案更不确定的东西去定答案。若验收方认为这仍算「结论要重写」,请以验收意见为准,我按新的措辞改。
## 模型参与的端到端对比
`not_started`。按任务书的让步顺序在 G4 之后,而 G2/G4 没有可评估的提升;跑 40 场完整对话只会得到 A、B 逐字节相同的结果,不再消耗模型预算。
`not_started`。按任务书的让步顺序在 G4 之后,而 G2/G4 没有可评估的提升;跑 40 场完整对话只会得到 A、B 逐字节相同的结果。
## MEVG(AGENTS.md §3)
- **MEVG / Global Web Evidence**:`used`。
- **A(经典原文 / 可复现数据)**:BPHS 的 Shashtyamsa 规则(每星座 60 份、从本星座起算)与 Jataka Parijata / Bhavartha Ratnakara 的分盘权重,是可核对的原典;本仓的 12×60 段枚举与 PyJHora 对账属于可复现数据。原典只定义了几何与命名,**没有**「D60 可把出生时间收到秒」的判据。
- **B(有案例的专业说明)**:[VedaCharts 的 D60 指南](https://vedacharts.com/learn/articles/shastiamsa-d60-subtle-karma) 明确「每格 0.5°、约 2 分钟换格,5 分钟取整的出生时间不可靠」;[Nyovah 的 D60 页](https://www.nyovah.com/learn/shashtiamsa-d60.html) 用自家引擎做了 24 小时逐分钟扫描,量到上升每分钟 0.25°、约 2 分钟换格,并明确说**不**把 D60 喂给问答模型,因为两分钟误差就会翻盘。两者都把「需要准到分钟」当成前提,没有给出「能收到秒」的证据。
- **A(经典原文 / 可复现数据)**:BPHS 的 Shashtyamsa 规则(每星座 60 份、从本星座起算)与 Jataka Parijata / Bhavartha Ratnakara 的分盘权重是可核对的原典;本仓的 12×60 段枚举与 PyJHora 对账是可复现数据。原典只定义几何与命名,**没有**「D60 可把出生时间收到秒」的判据。
- **B(有案例的专业说明)**:[VedaCharts 的 D60 指南](https://vedacharts.com/learn/articles/shastiamsa-d60-subtle-karma) 明确「每格 0.5°、约 2 分钟换格,5 分钟取整的出生时间不可靠」;[Nyovah 的 D60 页](https://www.nyovah.com/learn/shashtiamsa-d60.html) 用自家引擎做了 24 小时逐分钟扫描,量到上升每分钟 0.25°、约 2 分钟换格,并明确说**不**把 D60 喂给问答模型,因为两分钟误差就会翻盘。两者都把「需要准到分钟」当前提,没有给出「能收到秒」的证据。
- **C(无依据的宣传)**:竞品「证据够密时 D60 与纳迪反向校准、收束可到秒」——无可复现数据、无案例、无算法说明,归 C。纳迪那一半已在 BUG-1240 证伪(N3 找回记录分钟 5/77、N4 换岁差 73/73 换段)。
- 冲突仲裁:B 类来源给的是「D60 需要准到分钟」而 C 类结论是「D60 反推能到秒」;前者有测量,后者没有,按有测量的一方取舍,并降级 C 类结论。
- **Real Case Calibration**:`used`。v5 77 例、964 件事,G1 77/77;G2 因证据格式不可评估;G5 100% 翻格。
- 冲突仲裁:B 类给的是「D60 需要准到分钟」,C 类主张「D60 反推能到秒」;前者有测量,后者没有,按有测量的一方取舍,并降级 C 类结论。
- **Real Case Calibration**:`used`。v5 77 例、964 件事;G1 77/77;G2 因口径不可评估;G5 见上表。
## 事后(post-hoc,不参与判门)
- 查了「如果放宽成『事件日期与换格日期至少差一天就作答』会怎样」:那样每条换格题都会被答成「事件在之后」,把窗口切掉一半且与真值无关,属于一边倒淘汰,因此不把它当结果。这一条写在先登记文件之后,只作解释。
- 前端 `report-density-fictional-engine.json` 里仍有旧 D60 定义字符串的同类问题(与映射无关),留给前端轮次。
- 「如果放宽成『事件日期与换格日期至少差一天就作答』」:每条换格题都会被答成「事件在之后」,把窗口切掉一半且与真值无关,属于一边倒淘汰,因此不当结果。此条写在先登记文件之后,只作解释。
- 前端 `report-density-fictional-engine.json` 里仍有旧的 D60 定义字符串(与本次映射无关),留给前端轮次。
## 建议
@@ -57,8 +74,7 @@
```bash
export PYTHONHASHSEED=0
python scripts/research/d60_two_way_run.py # G1,写 docs/research/d60_two_way_results_2026_10_29.json
python - <<'PY' # G2 预检 + G5,写 docs/research/d60_two_way_g2_g5_results_2026_10_29.json
# 见 PROGRESS「第 2 次回报」一节的完整命令
PY
python scripts/research/d60_two_way_run.py # G1 → docs/research/d60_two_way_results_2026_10_29.json
python scripts/research/d60_two_way_g2_g5.py # G2 预检 + G5 → docs/research/d60_two_way_g2_g5_results_2026_10_29.json
python -m pytest tests/test_d60_two_way_g2_g5.py tests/test_d60_two_way_research.py tests/test_d60_varga_mapping.py -q
```
@@ -104,3 +104,16 @@ python scripts/research/d60_two_way_run.py
**结论**:不做。D60 不进校正判据,技法路由与对外文案不改;对外不出现「到秒 / 分钟级精确」。BUG-1497 记 `closed_by_design`。
**偏离**:任务书要求五门都跑;G2/G4 在登记口径下不可评估,按先登记文件的 stop rule 如实记录,不用放宽口径去凑数字;端到端对比按让步顺序未跑。用户可见代码本轮零改动。
## T5 第 1 次打回:补脚本与分口径报数(2026-10-29)
修复单 `docs/tasks/TASK-rectification-d60-t5-fix-20261029.md`。
1. **补脚本**:`scripts/research/d60_two_way_g2_g5.py`(G2 预检 + G5),输出排序、无时间戳;`tests/test_d60_two_way_g2_g5.py` 4 项(同日预检一致、汇总与逐例一致、无出生时刻泄漏、**结果文件逐字节复跑**)。G1 脚本已在上一轮提交。
2. **G5 分口径**:
- 以 Raman 为基准:Lahiri 77/77(86.78′)、KP 77/77(80.97′)、True Chitra 77/77(85.95′)。
- 替代岁差两两:Lahiri→KP 21/77(27.3%,5.81′)、KP→True Chitra 19/77(24.7%,5.02′)、**Lahiri→True Chitra 2/77(2.6%,0.83′)**。
- 结论措辞改成分口径:不再说「换任何岁差都全变」;登记门(相对基准)仍三种都 100%。2.6% 这一对是两套几乎重合的岁差,已在报告里写明,并说明若验收方认定「低于 20% 就要重写结论」,以验收意见为准。
3. **报告补口径说明**:G2 不可评估是「只认同日事件」的口径造成的(Claude 已在验收里认领),不是 D60 被测出没帮助。
跑过:`pytest tests/test_d60_two_way_g2_g5.py` 4 passed;`pytest tests/test_d60_two_way_research.py tests/test_d60_varga_mapping.py` 9 passed。
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""G2 precheck and G5 noise floor for the D60 two-way study (issue #149).
Two measurements, both deterministic (no timestamps, sorted keys, fixed
formatting), so a rerun can be compared byte for byte:
* G2 precheck: how many v5 cases have an event dated on the birth day. The
registered answer policy (`docs/research/d60_two_way_preregistration_2026_10_28.json`)
only answers a D60 boundary probe when an event shares the boundary's date,
so zero same-day events means group B adds no question at all.
* G5: how often the ascendant's D60 sign at the recorded time changes when the
ayanamsa is switched away from Raman, per switch, with the ayanamsa offset in
arcminutes.
Usage:
python scripts/research/d60_two_way_g2_g5.py # writes the results JSON
python scripts/research/d60_two_way_g2_g5.py --json # prints it
PyJHora is not needed here; only the production ephemeris and the D60 mapping.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from scripts.jyotish_engine import compute_chart_data # noqa: E402
DATASET = ROOT / "references/real_case_calibration/minute_rectification_holdout_v5.json"
DEFAULT_OUT = ROOT / "docs/research/d60_two_way_g2_g5_results_2026_10_29.json"
BASELINE = "raman"
ALTERNATIVES = ("lahiri", "kp", "true_citra")
G5_THRESHOLD = 0.20
def _cases() -> list[dict]:
return json.loads(DATASET.read_text(encoding="utf-8"))["cases"]
def _ascendant(case: dict, ayanamsa: str) -> tuple[float, int]:
import varga
birth = case["birth"]
year, month, day = (int(part) for part in birth["date"].split("-"))
hour, minute = (int(part) for part in birth["time"].split(":"))
chart, _asc_idx, _jd, ayanamsa_degrees = compute_chart_data(
year,
month,
day,
hour,
minute,
birth["latitude"],
birth["longitude"],
birth["timezone_offset"],
node_mode="mean",
ayanamsa_name=ayanamsa,
)
longitude = float((chart.get("ascendant") or {})["degree_raw"])
return float(ayanamsa_degrees), int(varga.calc_varga(longitude, 60)["sign_idx"])
def same_day_precheck(cases: list[dict] | None = None) -> dict:
cases = _cases() if cases is None else cases
per_case = []
for case in cases:
birth_date = case["birth"]["date"]
same_day = [e for e in case.get("events", []) if str(e.get("date", "")).startswith(birth_date)]
per_case.append({"case_id": case["case_id"], "events_same_day": len(same_day)})
return {
"cases_with_same_day_events": sum(1 for row in per_case if row["events_same_day"]),
"same_day_events": sum(row["events_same_day"] for row in per_case),
"usable_d60_boundary_probes": 0,
"verdict": "not_evaluable_by_construction",
"reason": (
"the registered answer policy needs an event dated on the same day as the D60 boundary "
"(the birth date); the v5 holdout has none, so group B adds no question and is identical "
"to group A. This is a design consequence of the registered rule, not a measurement that "
"D60 does not help."
),
"per_case": per_case,
}
def g5(cases: list[dict] | None = None) -> dict:
cases = _cases() if cases is None else cases
modes = (BASELINE,) + ALTERNATIVES
rows = []
for case in cases:
base_ayanamsa, base_sign = _ascendant(case, BASELINE)
row = {
"case_id": case["case_id"],
f"d60_{BASELINE}": base_sign,
f"ayanamsa_offset_arcminutes_{BASELINE}": 0.0,
}
for mode in ALTERNATIVES:
ayanamsa, sign = _ascendant(case, mode)
row[f"d60_{mode}"] = sign
row[f"ayanamsa_offset_arcminutes_{mode}"] = round((ayanamsa - base_ayanamsa) * 60.0, 2)
for mode in ALTERNATIVES:
row[f"changes_{mode}"] = row[f"d60_{mode}"] != row[f"d60_{BASELINE}"]
rows.append(row)
count = len(rows)
switches = {}
for mode in ALTERNATIVES:
offsets = sorted(row[f"ayanamsa_offset_arcminutes_{mode}"] for row in rows)
changed = sum(1 for row in rows if row[f"changes_{mode}"])
switches[f"{BASELINE}_to_{mode}"] = {
"changed_cases": changed,
"share": round(changed / count, 4) if count else None,
"ayanamsa_offset_arcminutes": {
"min": offsets[0] if offsets else None,
"median": offsets[len(offsets) // 2] if offsets else None,
"max": offsets[-1] if offsets else None,
},
}
pairwise = {}
for index, left in enumerate(modes):
for right in modes[index + 1:]:
changed = sum(1 for row in rows if row[f"d60_{left}"] != row[f"d60_{right}"])
deltas = sorted(
round(row[f"ayanamsa_offset_arcminutes_{right}"] - row[f"ayanamsa_offset_arcminutes_{left}"], 2)
for row in rows
)
pairwise[f"{left}_to_{right}"] = {
"changed_cases": changed,
"share": round(changed / count, 4) if count else None,
"ayanamsa_offset_arcminutes": {
"min": deltas[0] if deltas else None,
"median": deltas[len(deltas) // 2] if deltas else None,
"max": deltas[-1] if deltas else None,
},
}
return {
"baseline": BASELINE,
"switches": switches,
"pairwise": pairwise,
"threshold": G5_THRESHOLD,
"any_above_threshold": any(item["share"] > G5_THRESHOLD for item in switches.values())
or any(item["share"] > G5_THRESHOLD for item in pairwise.values()),
"cases": rows,
}
def run() -> dict:
cases = _cases()
precheck = same_day_precheck(cases)
noise = g5(cases)
return {
"scope": "d60_two_way_g2_g5",
"task": "docs/tasks/TASK-rectification-d60-two-way-20261028.md",
"fix_brief": "docs/tasks/TASK-rectification-d60-t5-fix-20261029.md",
"issue": 149,
"bug": "BUG-1497",
"preregistration": "docs/research/d60_two_way_preregistration_2026_10_28.json",
"dataset": {
"path": DATASET.relative_to(ROOT).as_posix(),
"sha256": hashlib.sha256(DATASET.read_bytes()).hexdigest(),
"cases": len(cases),
},
"g2_precheck": precheck,
"g3": {
"verdict": "trivially_equal",
"reason": "B adds no elimination, so the delivered range equals A and truth-in-range cannot drop",
},
"g4": {"verdict": "not_applicable", "reason": "no B-minus-A lift exists to test against placebos"},
"g5": noise,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--json", action="store_true")
parser.add_argument("--out", default=None)
args = parser.parse_args()
payload = run()
text = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
if args.json:
print(text, end="")
return 0
out = Path(args.out) if args.out else DEFAULT_OUT
out.write_text(text, encoding="utf-8", newline="\n")
print(out.relative_to(ROOT).as_posix())
return 0
if __name__ == "__main__":
raise SystemExit(main())
+65
View File
@@ -0,0 +1,65 @@
"""G2 precheck and G5 noise floor for the D60 study (issue #149).
The committed results file must be reproducible byte for byte from the script;
the fast checks below verify the summary against the per-case rows without
touching the ephemeris.
"""
from __future__ import annotations
import json
from pathlib import Path
from scripts.research.d60_two_way_g2_g5 import (
DATASET,
DEFAULT_OUT,
ALTERNATIVES,
same_day_precheck,
)
ROOT = Path(__file__).resolve().parents[1]
def _committed() -> dict:
return json.loads(Path(DEFAULT_OUT).read_text(encoding="utf-8"))
def test_same_day_precheck_matches_the_committed_file() -> None:
committed = _committed()
live = same_day_precheck()
assert live["cases_with_same_day_events"] == committed["g2_precheck"]["cases_with_same_day_events"] == 0
assert live["same_day_events"] == committed["g2_precheck"]["same_day_events"] == 0
assert committed["g2_precheck"]["verdict"] == "not_evaluable_by_construction"
def test_g5_summary_matches_the_committed_rows() -> None:
committed = _committed()
rows = committed["g5"]["cases"]
assert len(rows) == committed["dataset"]["cases"] == 77
for mode in ALTERNATIVES:
changed = sum(1 for row in rows if row[f"d60_{mode}"] != row[f"d60_raman"])
assert committed["g5"]["switches"][f"raman_to_{mode}"]["changed_cases"] == changed
for key, switch in committed["g5"]["pairwise"].items():
left, right = key.split("_to_")
changed = sum(1 for row in rows if row[f"d60_{left}"] != row[f"d60_{right}"])
assert switch["changed_cases"] == changed, key
assert committed["g5"]["any_above_threshold"] is True
def test_payload_has_no_recorded_birth_times() -> None:
payload = _committed()
assert "true_minute" not in json.dumps(payload["g5"]["cases"])
for row in payload["g5"]["cases"]:
assert set(row) == {"case_id", "d60_raman", "d60_lahiri", "d60_kp", "d60_true_citra",
"ayanamsa_offset_arcminutes_raman", "ayanamsa_offset_arcminutes_lahiri",
"ayanamsa_offset_arcminutes_kp", "ayanamsa_offset_arcminutes_true_citra",
"changes_lahiri", "changes_kp", "changes_true_citra"}
def test_results_file_is_reproducible_byte_for_byte() -> None:
from scripts.research import d60_two_way_g2_g5 as module
payload = module.run()
text = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
assert text == Path(DEFAULT_OUT).read_text(encoding="utf-8")
assert Path(DATASET).is_file()