feat(core): 实现 get_rng 与 set_seed

L0 第 2 项落地。

- get_rng(int) 固定种子可复现;get_rng(Generator) 原样透传;
  get_rng() 跟随 set_seed 设过的全局种子,没设过则交给系统熵源
- set_seed(seed) 除记录 _GLOBAL_SEED 外,还调用 np.random.seed,
  让 sklearn / statsmodels 这些走 numpy 旧全局状态的库也能复现;
  这是全项目唯一碰旧接口的位置,标注了 noqa: NPY002 与理由
- 已知取舍写进 docstring:同一全局种子下多次 get_rng() 是同一条流
- tests/test_core.py:TestRandom 去掉 skip;把原来那条自证式的
  test_set_seed_makes_global_reproducible 换成两条真断言
  (不传 random_state 也要可复现、旧全局状态被覆盖)
- STATUS.md:L0 进度 2/12,新增决策 23(允许 set_seed 碰旧接口)
  与 24(不做 SeedSequence.spawn 拆分);CHANGELOG.md 同步
This commit is contained in:
guzhujushi committed 2026-10-01 19:19:15 +08:00
1 parent cb96c847dd
commit 658b266614
4 files changed
+62 -20

No files matched your search

+17 -7
View File
@@ -58,7 +58,7 @@
- [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`),
`~/.ssh/config` 加了一条域名别名,remote 里仍写域名
**E. L0 实现进度(1/12)**
**E. L0 实现进度(2/12)**
- [x] `core/result.py` 的 `Result`
- `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印
(十万级数组的 repr 实测仍 < 200 字符);`success` 恒在
@@ -67,7 +67,15 @@
float 走 `.6g`,`|` 转义,`message` 以引用行附在表后;幂等无副作用
- 测试:`tests/test_core.py` 去掉 skip + 新增 3 条
(默认值/失败路径/大数组、Markdown 结构、可重复调用)
- [ ] 剩余 11 项(13 个函数):`get_rng` `set_seed` `load_table` `clean_missing`
- [x] `core/random.py` 的 `get_rng` / `set_seed`
- `get_rng(42)` 固定种子可复现;`get_rng(gen)` 原样返回;`get_rng()` 跟随全局种子
- `set_seed(seed)` 除了记住种子,还一并调 `np.random.seed`,
让 sklearn / statsmodels 那些走旧全局的库也能复现(全项目**唯一**碰旧接口的地方,
带 `# noqa: NPY002` 与理由)
- 已知取舍:同一个全局种子下每次 `get_rng()` 拿到**同一条流**(见决策 24)
- 测试:`TestRandom` 去掉 skip,并把原来那条"自己测自己"的空转测试换成
两条真断言(无参可复现、旧全局被覆盖)
- [ ] 剩余 10 项(11 个函数):`load_table` `clean_missing`
`detect_outliers` `normalize` `standardize` `describe` `correlation_matrix`
`bootstrap_ci` `monte_carlo` `save_figure`
@@ -75,15 +83,15 @@
| 检查 | venv | conda `mcm-toolkit` |
|---|---|---|
| `pytest` | 24 passed, 48 skipped | 24 passed, 48 skipped |
| `pytest` | 28 passed, 45 skipped | 28 passed, 45 skipped |
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
| `ruff check` | All checks passed | All checks passed |
| `scripts/doctor.ps1` | 跑通 | — |
| SALib / pymoo / pyright | 缺失 | 已装 |
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
- skip 的都是等实现的算法行为测试;通过的 24 条 = 20 条公开 API 契约测试
(`tests/test_api_surface.py`)+ 4 条 `Result` 行为测试(`tests/test_core.py`)
- skip 的都是等实现的算法行为测试;通过的 28 条 = 20 条公开 API 契约测试
(`tests/test_api_surface.py`)+ 8 条 `core` 行为测试(`tests/test_core.py`)
- 三个 `.ps1` 通过 PowerShell AST 语法解析
### ⚠️ 需要注意 / 遗留
@@ -93,7 +101,7 @@
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
- [ ] L0 完成 1/12(`Result`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进;
- [ ] L0 完成 2/12(`Result`、`get_rng`/`set_seed`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进;
详情见上面 E 节
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
(`helper_unknown_error: setup refresh had errors`),只有提权
@@ -131,11 +139,13 @@
| 20 | 首次推送用 `--force-with-lease` **覆盖** Gitea 的 `Initial commit` | 远端那个提交是建仓时自动初始化产生的,与本地无共同祖先,普通 push 必被拒。推送前已把其中的 LICENSE 与手写简介并入本地 `README.md`,原提交仍留在本机对象库(`1c683cf`) |
| 21 | `Result.__repr__` **不打印 `data` 内容**,只给类型与规模 | 模拟轨迹动辄十万个数,repr 一旦刷屏,traceback / 调试器 / notebook 输出就全废了。要看数据请直接取 `result.data` |
| 22 | `Result.summary()` 的行序 = 字典插入顺序,不排序 | 比赛时"想先看哪个指标"自己决定键的插入顺序即可,工具不该替人排序 |
| 23 | `set_seed` 内部**允许**调用 `np.random.seed`(带 noqa) | sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 `_GLOBAL_SEED` 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同 |
| 24 | `get_rng(None)` 不做 `SeedSequence.spawn` 拆分,同一种子下多条流相同 | 多引入一个模块级计数器,会让"同一次调用顺序不同 → 结果不同",对刚上手的人更难解释。取舍写进 docstring,真要独立流就显式传不同 `random_state` |
## 下一步
1. ~~接入 Gitea~~ **已完成**。以后推送前先确认 Tailscale 在线,然后 `git push`
(细节与换机步骤见 `docs/05-git-workflow.md` §3)。
2. 继续 L0,**下一个:`core/random.py` 的 `get_rng` / `set_seed`**(先写测试再写代码)。
2. 继续 L0,**下一个:`data/io.py` 的 `load_table`**(`save_table` 属 L1,顺手也可做)。
3. 每写完一个函数,在 `examples/` 里加一行最小用法,并更新本文件。
4. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。