# 生时校正三项离线研究(2026-09-26) - 任务书:`docs/tasks/TASK-rectification-offline-research-20260926.md`;进度:`docs/tasks/PROGRESS-rectification-offline-research-20260926.md` - 分支:`codex/rectification-offline-research-20260926`,测量在 `origin/staging` @ `7ddce2c9` 上完成(引擎代码与任务书基线 `abb05b67` 相同),提交已变基到 `7203d94e`(其间只新增 `fewer_probes_card_replay.py` 与文档,引擎与打分代码未变)。 - 性质:**离线测量。不改线上代码、打分、阈值、出题闸门、Skill。** 研究脚本读引擎内部函数,个别测量在一次调用内临时替换模块属性,调用结束立即恢复,脚本末尾断言已恢复。 - 数据:`references/real_case_calibration/minute_rectification_holdout_v4.json`,20 例公开 Rodden-AA 名人。R3a 只用随机虚构时刻。**这是开放集回放,不是盲测,下面的数字不是准确率。** - 口径:ayanamsa `raman`,node mode `mean`,步长 2 分钟,半径 ±10 / ±30 / ±60。交付区间 = 未淘汰、且落后头名不足 8 分的簇的并集(线上 `unionStillValidRange`)。六题回放沿用 09-14 研究:同一份六道题,不随作答重新出题。 - 同机 A/B:R1、R3a 在 `PYTHONHASHSEED` 不同的两次运行下 JSON 逐字节一致;R2、R3c 换 `PYTHONHASHSEED` 复跑,汇总与已存 JSON 完全一致(见进度记录)。不与其他机器的浮点结果比对(BUG-985)。 ## 结论 | 项 | 一句话结论 | 是否建议立实现单 | | --- | --- | --- | | R1 答错容错 | 答错 1 题,真值几乎都还在区间里(98–100%),但头名命中降三到四成。答错 2 题,±30 / ±60 上约 7–10% 的回放会把真值挤出区间,半数以上的例子至少有一种两题组合会挤出。原因是两道反答正好造成 8 分差,等于淘汰线 | **不立实现单**。给产品一条口径:区间变窄不等于更准(±10 反答后宽度反而从 15 收到 11) | | R2 V1/V2 分盘配权 | 权重确实生效了:研究计分器在 V0 与线上逐分相同。V1/V2 在 ±30 / ±60 上按定义就是恒等,0 个候选分数变化。在 ±10 上改了约 3/4 候选的分数(最多 0.4–0.6 分),六题后指标与基线完全相同。**判定由 `not_measured` 改为 `no_benefit`(已实测)** | **不立实现单**。补充的 V1n(按敏感度归一化重新分配权重)头名 +1/+1/+2 例,但宽度没降,±60 宽度 56→73,不过门 | | R3 算术改正 | 1 分钟对应大运边界位移中位 **3.8 天**,不是 1.1 天(p10 1.6、p90 5.0,范围 1.3–5.9),按出生星宿主星分两档。45 天闸门对应 **8–34 分钟**(中位 12,p10–p90 为 9–29),不是 40 分钟。`_representative_pairs` 推断**被推翻**:改成全部两两配对后,带日期的题一道都没多。真正的卡点在逐月评估:只有 3%(刷新阶段 0.4%)的边界月份能让代表候选分到两边 | **不立实现单**。另记一个新发现:`_boundary_windows` 有两处绕过闸门的漏洞(跨 1 月 1 日豁免、按位置 zip 错位),记为 BUG-1048 `investigating`,是否修要等产品决定 | --- ## R1 · 答错 1–2 题的容错 **方法。** 每例每个半径用线上 G0 闸门出题,取前六题。按真值给出最优是 / 否,再从能作答的题里随机选 1 题或 2 题反过来答(固定种子 `20260926`,按例、半径、题数、重复序号生成随机数,每格重复 30 次)。另外对所有组合穷举一遍作对照。能作答的题少于 k 的例子不进该格;所以每格另列「同一批例子不答错」的基线。 复跑:`python3 scripts/research/answer_flip_tolerance.py`(约 1 分钟)→ `docs/research/answer_flip_tolerance_2026_09_26.json` | 半径 | 答错题数 | 例数 | 回放次数 | 真值在区间 | 头名命中 | 宽度中位(分钟) | 至少一次挤出真值的例子 | 同批例子不答错:命中 / 宽度 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | ±10 | 0 | 20 | 20 | 1.000 | 0.80 | 15 | 0 | — | | ±10 | 1 | 16 | 480 | 1.000 | 0.58 | 13 | 0 | 0.94 / 12 | | ±10 | 2 | 15 | 450 | 0.996 | 0.40 | 11 | 1 | 0.93 / 11 | | ±30 | 0 | 20 | 20 | 1.000 | 0.55 | 33 | 0 | — | | ±30 | 1 | 18 | 540 | 0.989 | 0.33 | 35 | 1 | 0.61 / 32 | | ±30 | 2 | 18 | 540 | 0.926 | 0.08 | 33 | 9 | 0.61 / 32 | | ±60 | 0 | 20 | 20 | 1.000 | 0.40 | 56 | 0 | — | | ±60 | 1 | 18 | 540 | 0.981 | 0.24 | 73 | 2 | 0.44 / 54 | | ±60 | 2 | 18 | 540 | 0.898 | 0.06 | 71 | 12 | 0.44 / 54 | 穷举对照(每例所有 1 题 / 2 题组合):真值在区间 ±10 1.000 / 0.990,±30 0.991 / 0.919,±60 0.981 / 0.907;头名 ±10 0.66 / 0.43,±30 0.33 / 0.09,±60 0.23 / 0.06。随机抽样与穷举一致。 不答错的基线与 09-14 精度闸门研究的 G0 完全一致(0.80 / 0.55 / 0.40;15 / 33 / 56;20/20),说明回放链路没有漂移。 **读法。** 1. **真值从未被淘汰。** 强冲突淘汰要 3 次;答错 2 题最多给真值 2 次冲突。 2. **挤出真值靠的是 8 分线。** 一道反答让真值 −2、另一侧 +2,相对差 4 分;两道反答正好 8 分,等于 `SEPARATION_LEAD = 8`。所以 k=2 开始挤出真值,k=1 只有真值原本就贴线的例子才会被挤出。 3. **±10 上答错反而让区间更窄**(15 → 13 → 11),头名却从 0.94 掉到 0.40。窄是因为错误答案把一个假头名推高了,不是因为更准。**产品口径:不要把「范围变窄」讲成「更确定了」。** 4. 回放的六道题不随作答重出,这一点对答对和答错两边都一样。真人答错后,后续题目可能被带偏,实际影响可能更大,本回放测不到。 **是否建议立实现单:不建议。** 容错与淘汰线(8 分、3 次冲突)是同一组常数,要改就会同时影响答对时的宽度,需要另立研究。可选方向(均未测):答题之间出现矛盾时放宽交付并集;在交付卡上允许「改一道答案」。 --- ## R2 · V1/V2 分盘敏感度配权重跑 **背景。** 09-14 精度闸门研究的 M1b 里,V1/V2 与 V0 数字完全一样。BUG-692 把判定改成 `not_measured`,文中怀疑「权重没真正打上」。本轮先证明权重有没有生效,再重跑。 复跑:`python3 scripts/research/varga_sensitivity_rerun.py`(约 16 分钟)→ `docs/research/varga_sensitivity_rerun_2026_09_26.json` ### 第一步:权重是否改了分数 | 半径 | 研究计分器 V0 vs 线上 | V1 vs V0:分数变化的候选 | V1 最大分差 | V1 头名变化例 | V2 vs V0:分数变化的候选 | V2 最大分差 | V2 头名变化例 | V2 丢掉某领域分盘的例子 | | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | ±10 | 0 / 220 不同 | 168 / 220(17 例) | 0.428 | 0 | 159 / 220(17 例) | 0.625 | 1 | 12 | | ±30 | 0 / 620 不同 | **0 / 620** | 0 | 0 | **0 / 620** | 0 | 0 | 0 | | ±60 | 0 / 1220 不同 | **0 / 1220** | 0 | 0 | **0 / 1220** | 0 | 0 | 0 | V1 系数 `min(窗宽 / 分盘换一次上升的分钟数, 1)`: | 窗宽 | D2 | D3 | D5 | 其余 8 张(D4/D7/D9/D10/D11/D12/D24/D30) | | --- | ---: | ---: | ---: | ---: | | 20 分钟(±10) | 0.33 | 0.50 | 0.83 | 1.00 | | 60 分钟(±30) | 1.00 | 1.00 | 1.00 | 1.00 | | 120 分钟(±60) | 1.00 | 1.00 | 1.00 | 1.00 | **结论:权重生效了,是 V1/V2 的定义本身在宽窗上等于恒等。** 11 张计分分盘里最慢的 D2 每 60 分钟换一次上升,窗宽 ≥ 60 分钟时 V1 每个系数都截断到 1。V2 只丢掉窗内一次都不变的分盘,窗宽 ≥ 60 分钟时每张都会变。±10 上两者确实改了分数,但幅度小于候选之间的分差。09-14「与 V0 相同」的数字是真实结果,不是没打上权重。 ### 第二步:按 closure 口径重跑(G0 闸门、六题回放) | 半径 | 方案 | 头名命中 | 真值在区间 | 宽度中位 | 引擎头名(答题前) | 六题后再出 | 判定 | | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | | ±10 | V0 | 0.80 | 20/20 | 15 | 0.30 | 3.25 | 基线 | | ±10 | V1 | 0.80 | 20/20 | 15 | 0.30 | 3.25 | no_benefit | | ±10 | V2 | 0.80 | 20/20 | 15 | 0.35 | 3.25 | no_benefit | | ±10 | V1n(补充) | 0.85 | 20/20 | 15 | 0.45 | 3.25 | no_benefit(宽度未降) | | ±30 | V0 | 0.55 | 20/20 | 33 | 0.20 | 5.45 | 基线 | | ±30 | V1 / V2 | 0.55 | 20/20 | 33 | 0.20 | 5.45 | no_benefit(恒等) | | ±30 | V1n(补充) | 0.60 | 20/20 | 33 | 0.15 | 5.45 | no_benefit(宽度未降) | | ±60 | V0 | 0.40 | 20/20 | 56 | 0.05 | 5.75 | 基线 | | ±60 | V1 / V2 | 0.40 | 20/20 | 56 | 0.05 | 5.75 | no_benefit(恒等) | | ±60 | V1n(补充) | 0.50 | 20/20 | **73** | 0.10 | 5.90 | no_benefit(宽度变宽) | 判定沿用 `precision_gate_lib.gate_verdict`:头名不降**且**宽度下降才算有收益,覆盖不得下降。 **V1n 是什么。** 这是 09-14 设计里没有的补充方案。每张分盘的系数与「换一次上升的分钟数」成反比,再归一化到 11 张盘的平均系数等于 1。总分量不变,只把权重从慢盘挪到快盘(D2 0.17、D3 0.26、D5 0.43、D7 0.60、D9 0.77、D10 0.85、D11 0.94、D12 1.02、D4 1.36、D24 2.05、D30 2.56)。它改了全部候选的分数,头名多中 1 / 1 / 2 例(20 例里),但宽度没有收窄,±60 反而变宽。样本只有 20 例,+1 例在噪声范围内。 **是否建议立实现单:不建议。** V1/V2 结论更新为 `no_benefit`(已实测)。V1n 若要继续,先补 ±7 天日期偏移和 R1 答错两项稳健性,再谈实现。 --- ## R3 · 改正「1 分钟 ≈ 1.1 天」与 `_representative_pairs` 推断 ### R3a · 1 分钟对应多少天 复跑:`python3 scripts/research/dasha_shift_per_minute.py`(1 秒)→ `docs/research/dasha_shift_per_minute_2026_09_26.json` **推导。** 第一段大运的余额是 `(1 − 星宿内进度) × 主星年数`。之后每一个 MD / AD / PD 的起点都等于「出生 + 余额 + 固定时长」,所以出生时间挪 1 分钟,**所有边界整体平移同一个天数**: > 平移天数 / 分钟 = 月亮每分钟走的度数 ÷ 13.333° × 主星年数 × 365.25 月亮平均每分钟 0.0091°。Vimshottari 120 年覆盖 9 个星宿(120°),**平均 1° ≈ 365 天**,按主星从 164 天(太阳 6 年)到 548 天(金星 20 年)不等。09-14 文档写的「1° ≈ 122 天」是错的,于是算成 0.0092 × 122 ≈ 1.1 天,把位移低估了约 3.5 倍。 **实测(2000 个虚构时刻,种子 7,swisseph 恒星黄道 Raman):** | 测法 | n | 平均 | p0 | p10 | p25 | 中位 | p75 | p90 | p100 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | 解析式(月速 × 主星年数) | 2000 | 3.32 | 1.34 | 1.58 | 1.80 | **3.82** | 4.54 | 5.00 | 5.85 | | 差分(余额在 t 与 t+1 分钟) | 1997 | 3.32 | 1.34 | 1.58 | 1.80 | 3.82 | 4.54 | 5.00 | 5.85 | | 仓库 `_vim_start_dates`(t 与 t+20 分钟,÷20) | 299 | 3.50 | 1.35 | 1.60 | 1.95 | 4.05 | 4.55 | 5.00 | 5.75 | 仓库函数的所有 MD / AD 起点在 20 分钟内平移同一个天数(对齐后差值最大相差 1 天,是日期取整)。它是前 300 个时刻的子样本,中位 4.05 与全样本 3.82 的差异来自抽样。 **按出生星宿主星分两档**(中位,天 / 分钟):太阳 1.50、火星 1.71、计都 1.77、月亮 2.47 | 木星 3.96、水星 4.18、罗睺 4.54、土星 4.71、金星 4.93。前一档约占 45%。所以「中位 3.8」落在两档之间,单个人的实际值要么约 1.5–2.5,要么约 4–5。 ### 闸门与窗宽换算(改正后) | 候选相隔 | 边界相差(天,中位 / p10 / p90) | ≥45 天的比例 | ≥30 天的比例 | 旧文档写的 | | --- | --- | ---: | ---: | --- | | 2 分钟 | 7.6 / 3.2 / 10.0 | 0% | 0% | ≈ 2 天 | | 4 分钟 | 15.3 / 6.3 / 20.0 | 0% | 0% | — | | 10 分钟 | 38.2 / 15.8 / 50.0 | 27% | 54% | — | | 20 分钟 | 76.5 / 31.5 / 100.0 | 66% | 94% | ≈ 22 天、不可出题 | | 30 分钟 | 114.7 / 47.3 / 149.9 | 94% | 100% | — | | 40 分钟 | 153.0 / 63.1 / 199.9 | 100% | 100% | ≈ 45 天 | - **45 天闸门 ↔ 中位 11.8 分钟**(p10 9.0,p90 28.5,全范围 7.7–33.5)。 - **30 天刷新闸门 ↔ 中位 7.8 分钟**(6.0–19.0,全范围 5.1–22.3)。 ### 不同精度的证据能分开多近的候选 证据日期要落在两个候选的边界之间,才能把它们分到两边。所需边界差约等于证据的日期跨度,再除以每分钟平移天数: | 证据精度 | 日期跨度 | 稳定可分的候选间距(分钟) 快档 p90 | 中位 | 慢档 p10 | 最慢 | | --- | ---: | ---: | ---: | ---: | ---: | | 确切某一天 | 1 天 | 0.2 | 0.3 | 0.6 | 0.7 | | 某天 ±3 天 | 7 天 | 1.4 | 1.8 | 4.4 | 5.2 | | 某天 ±7 天 | 15 天 | 3.0 | 3.9 | 9.5 | 11.2 | | 记得到月 | 30.4 天 | 6.1 | 8.0 | 19.3 | 22.6 | | 线上 45 天闸门 | 45 天 | 9.0 | 11.8 | 28.5 | 33.5 | | 只记得年 | 365 天 | 73 | 95 | 232 | 272 | 低于这个间距时,只有两条边界恰好跨在月初(或年初)两侧才能分开,概率约为「边界差 ÷ 跨度」。例如按中位平移,相隔 2 分钟的一对候选:跨月概率 25%,跨年概率 2%;相隔 20 分钟:跨年概率 21%。 **所以:** 记得到月的事件,在候选相隔约 8 分钟以上(慢档约 20 分钟以上)时就够用;记得到天(容许几天误差)的事件,能分开相隔 2–5 分钟的候选。只记得年的事件单靠大运边界分不开 ±60 以内的候选,只有跨年的那一小部分边界例外。 ### R3c · `_representative_pairs` 推断核实 **任务书的推断:** 「出不来题」的真正原因可能是 `_representative_pairs` 只比较相邻代表分钟和首尾一对,相邻代表太近,边界差过不了闸门。 复跑:`python3 scripts/research/representative_pairs_probe.py`(约 6 分钟)→ `docs/research/representative_pairs_probe_2026_09_26.json` 对每例每个半径,在「初始」(全部候选)与「刷新」(六题回放后剩余候选,与 09-14 相同)两个阶段,比较三种设置:线上配对;研究补丁返回**全部两两配对**;研究补丁把 `_boundary_windows` 改成**严格闸门**(对所有配对都卡最小间隔、不豁免跨年,并把错位的 zip 对齐)。题目数在公开截断之前统计。 | 半径 · 阶段 | 代表数中位 | 线上配对:过闸对数均 | 全配对:过闸对数均 | 带日期的题(截断前,每例均值)线上 / 全配对 / 严格闸门 | 公开题里没有新的带日期题的例子 线上 / 全配对 / 严格闸门 | 边界月份评估次数均 | 能分开代表的比例 | | --- | ---: | ---: | ---: | --- | --- | ---: | ---: | | ±10 初始 | 6 | 0.60 | 3.30 | 4.85 / 4.80 / 4.10 | 4 / 4 / **8** | 168 | 3.2% | | ±10 刷新 | 4 | 0.90 | 1.65 | 3.25 / 3.30 / 3.10 | 3 / 3 / 4 | 809 | 0.4% | | ±30 初始 | 16 | 1.00 | 76.5 | 6.75 / 6.75 / 6.55 | 2 / 2 / 2 | 226 | 4.2% | | ±30 刷新 | 5 | 1.80 | 4.95 | 5.30 / 5.25 / 5.30 | 2 / 2 / 2 | 1130 | 0.5% | | ±60 初始 | 31 | 1.00 | 400.6 | 6.95 / 7.05 / 6.85 | 2 / 2 / 2 | 255 | 4.5% | | ±60 刷新 | 5 | 1.95 | 5.55 | 5.55 / 5.60 / 5.60 | 2 / 2 / 2 | 1057 | 0.5% | 「没有带日期题」的例子里,每格都有 2 例是 1900 年前出生的(毕加索 1881、弗洛伊德 1856):`_birth_year` 只接受 1900–2100,出题器对它们直接返回空,与闸门无关。 **结论:推断被推翻。** 1. **全部两两配对没有多出一道带日期的题。** 过闸的配对多了好几倍,日期并集也变大,但带日期的题数不变(差异 ≤0.1 道),没有题的例子数完全相同。首尾那一对已经是间距最大的一对,中间补上的配对只贡献更靠内的月份,这些月份照样分不开代表。 2. **真正的卡点在逐月评估。** `_evaluate_contexts` 在每个边界月份判断各代表在该领域是否被激活,要求一边激活、一边不激活。初始阶段每例评估约 170–250 个边界月份,只有 3–4.5% 能分开;刷新阶段带上 PD 后评估 800–1100 个,只有约 0.5% 能分开。 3. **v4 回放里六题后题池并不空。** ±10 刷新阶段平均还能出 3.25 道新的带日期题,1900 年后出生的 18 例里只有 1 例为 0。真机上「问完了」的原因用 v4 复现不出来。候选原因包括:采集线已关或被拒、已问年份、封存年份、存在性封锁年份、真人事件多为月精度,都未测。需要真机统计(见 `TASK-rectification-telemetry-20260926.md`)才能定性。 4. **新发现:`_boundary_windows` 有两处绕过闸门。** - 跨年豁免:代码写的是 `one.year == two.year and 差 < 阈值` 才跳过。两条边界只差 7 天、但分别落在 12 月底和 1 月初,也会被放行。例:某例 ±10 初始阶段,放行的 6 个日期全是这种一周内跨年的对(12-27 / 01-03 这类),逐月评估全部失败。 - zip 错位:两个候选的边界列表按位置配对。一旦某条边界跨出 `[出生+5, 出生+80]` 的年份边缘,列表会差一到两个元素(MD 起点与其第一个 AD 起点同一天,所以一次可能差两个)。此后每一对比的都是不对应的边界,间隔很大,于是全部放行。20 分钟虚构对(只算 MD+AD)有 10.7% 错位;刷新阶段(含 PD)线上配对有 35–44% 错位。 - 改成严格闸门后,±10 初始阶段没有带日期题的例子从 4 例变成 8 例,刷新阶段从 3 例变成 4 例,宽窗基本不变。也就是说,**窄窗上现有的一部分带日期题是靠这两个漏洞出来的**。这些题问的是「边界只差几天的那个月」,按月精度回答是否可靠,本轮没有评估。已记为 BUG-1048(`investigating`),不改代码。 ### 改正与文案建议 **已加勘误段(不删原文):** - `docs/tasks/TASK-rectification-precision-adaptive-boundary-research-20260914.md`(1.1 天、122 天 / 度、换算表) - `docs/tasks/TASK-rectification-open-collect-invite-20260914.md`(D1b 的依据句) - `docs/tasks/TASK-rectification-precision-gate-guided-collect-20260916.md`(实证第 5 条「20 分钟 ≈ 22 天,题池必然为空」) - `docs/research/precision_gate_2026_09_14.md`(M1b:V1/V2 改为已实测 `no_benefit`) - `docs/research/rectification_minute_resolution_closure_2026_09_14.md`(索引里 V1/V2「未测」的说明) **引导邀请语的依据:建议改写(不改线上文案)。** 线上现在的引导题是「YYYY 年 M 月前后,有没有 X?」加年 / 月选择器,日期可选(BUG-740 起)。BUG-689 / D1b 的依据句建议改成: > 出生时间每差 1 分钟,Vimshottari 边界整体平移约 1.3–5.9 天(中位约 3.8 天,取决于出生星宿的主星)。记得到月的事件,在候选相隔约 8 分钟以上(慢的星宿约 20 分钟以上)时就能把它们分开;记得到具体哪天(容许几天误差)的事件,能分开相隔 2–5 分钟的候选。但 v4 开放集上把日精度降成月精度,头名与宽度都没有变化(精度闸门研究 M0),所以不能写成「记得到天明显更有用」已经证实,更不能承诺能定到分钟。 对用户文案的建议(仅供产品决定):选择器保持「日可选」,旁边一句「记得哪天就选上,记不清选到月也可以」。只在剩余候选相隔不到约 8 分钟时(交付区间 ≤ 10 分钟的收尾阶段)才值得主动追问具体哪天。不写任何「能定到分钟」的承诺。 --- ## 附:全部复跑命令 ```bash python3 scripts/research/answer_flip_tolerance.py # R1,约 1 分钟 python3 scripts/research/varga_sensitivity_rerun.py # R2,约 16 分钟 python3 scripts/research/dasha_shift_per_minute.py # R3a,1 秒 python3 scripts/research/representative_pairs_probe.py # R3c,约 6 分钟 python3 -m pytest tests/test_offline_research_20260926.py # 纯函数回归 ```