fix(core): 补齐 get_rng 类型校验,set_seed 覆盖 numpy 旧全局
get_rng 的"共享单例生成器"设计由用户本人提出,本提交只在其上修 bug。 - random_state 判断从 isinstance(x, int) 放宽到 (int, np.integer): np.int64 不是 int 的子类,之前会静默退回全局流、种子被无声忽略 - 非预期类型改成抛 TypeError(含 bool / float / str / list),不再静默降级 - get_rng 签名补上 np.integer 与 Generator,与 docstring 的承诺一致 - set_seed 同时调用 np.random.seed:sklearn / statsmodels 走的是 numpy 旧全局状态,不覆盖它们"提交能重跑"就有洞(带 noqa: NPY002 与理由) - 测试:TestRandom 由 4 条增到 7 条(np.integer、非法类型、共享生成器推进) - 新增 docs/dev/personal-pitfalls.md:个人易错点清单,只追加、不通读, 首批记录本轮 4 个点;docs/README.md 补上 05 与该文件的索引 - STATUS.md:L0 进度说明更新,决策 24 改写(共享单例)、新增决策 25 (src/ 实现默认由用户本人写)
This commit is contained in:
1 parent
658b266614
commit
c93810ffb1
6 files changed
+158
-35
No files matched your search
+6
-3
@@ -21,12 +21,15 @@
|
|||||||
- `pyproject.toml`:核心依赖只 4 个,其余走 extras
|
- `pyproject.toml`:核心依赖只 4 个,其余走 extras
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `core.get_rng` / `core.set_seed`:实现(L0 第 2 项)。`get_rng` 支持 int 种子 /
|
- `core.get_rng` / `core.set_seed`:实现(L0 第 2 项)。`get_rng` 收 `int` / `np.integer` /
|
||||||
传 Generator 透传 / 无参跟随全局种子;`set_seed` 同时覆盖 numpy 旧全局状态,
|
`Generator` / `None`,其余类型抛 `TypeError`;无参调用复用同一个会推进的共享生成器。
|
||||||
让 sklearn / statsmodels 也可复现。
|
`set_seed` 同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。
|
||||||
- `core.Result`:补上 `__repr__` 与 `summary()` 实现(L0 第 1 项落地)。
|
- `core.Result`:补上 `__repr__` 与 `summary()` 实现(L0 第 1 项落地)。
|
||||||
`repr` 紧凑单行且不打印 `data` 内容;`summary()` 输出可直接贴进论文的 Markdown 表格。
|
`repr` 紧凑单行且不打印 `data` 内容;`summary()` 输出可直接贴进论文的 Markdown 表格。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `docs/dev/personal-pitfalls.md`:个人易错点清单,**只追加、不通读**
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- 除 `core.Result`、`core.get_rng`、`core.set_seed` 外,其余函数体**尚未实现**(统一 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记)。
|
- 除 `core.Result`、`core.get_rng`、`core.set_seed` 外,其余函数体**尚未实现**(统一 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记)。
|
||||||
这是刻意的:先定接口,再按 `docs/03-roadmap.md` 的 L0 → L1 顺序填实现。
|
这是刻意的:先定接口,再按 `docs/03-roadmap.md` 的 L0 → L1 顺序填实现。
|
||||||
|
|||||||
@@ -67,14 +67,14 @@
|
|||||||
float 走 `.6g`,`|` 转义,`message` 以引用行附在表后;幂等无副作用
|
float 走 `.6g`,`|` 转义,`message` 以引用行附在表后;幂等无副作用
|
||||||
- 测试:`tests/test_core.py` 去掉 skip + 新增 3 条
|
- 测试:`tests/test_core.py` 去掉 skip + 新增 3 条
|
||||||
(默认值/失败路径/大数组、Markdown 结构、可重复调用)
|
(默认值/失败路径/大数组、Markdown 结构、可重复调用)
|
||||||
- [x] `core/random.py` 的 `get_rng` / `set_seed`
|
- [x] `core/random.py` 的 `get_rng` / `set_seed`(**设计由用户本人写**,Codex 补齐类型校验)
|
||||||
- `get_rng(42)` 固定种子可复现;`get_rng(gen)` 原样返回;`get_rng()` 跟随全局种子
|
- 无参 `get_rng()` 复用**同一个模块级生成器**且会推进:既保证重跑一致,
|
||||||
- `set_seed(seed)` 除了记住种子,还一并调 `np.random.seed`,
|
又避免两个都省略 `random_state` 的函数抽到同一串数(原决策 24 的取舍已消掉)
|
||||||
让 sklearn / statsmodels 那些走旧全局的库也能复现(全项目**唯一**碰旧接口的地方,
|
- `get_rng` 收 `int` / `np.integer` / `Generator` / `None`,其余类型**抛 `TypeError`**:
|
||||||
带 `# noqa: NPY002` 与理由)
|
`np.int64` 不是 `int` 的子类,曾因此静默退回全局流
|
||||||
- 已知取舍:同一个全局种子下每次 `get_rng()` 拿到**同一条流**(见决策 24)
|
- `set_seed` 同时调 `np.random.seed`,覆盖 sklearn / statsmodels 走的 numpy 旧全局
|
||||||
- 测试:`TestRandom` 去掉 skip,并把原来那条"自己测自己"的空转测试换成
|
(全项目**唯一**碰旧接口的地方,带 `# noqa: NPY002` 与理由)
|
||||||
两条真断言(无参可复现、旧全局被覆盖)
|
- 测试:`TestRandom` 共 7 条(可复现、透传、无参跟随、旧全局、np.integer、非法类型、共享推进)
|
||||||
- [ ] 剩余 10 项(11 个函数):`load_table` `clean_missing`
|
- [ ] 剩余 10 项(11 个函数):`load_table` `clean_missing`
|
||||||
`detect_outliers` `normalize` `standardize` `describe` `correlation_matrix`
|
`detect_outliers` `normalize` `standardize` `describe` `correlation_matrix`
|
||||||
`bootstrap_ci` `monte_carlo` `save_figure`
|
`bootstrap_ci` `monte_carlo` `save_figure`
|
||||||
@@ -83,15 +83,15 @@
|
|||||||
|
|
||||||
| 检查 | venv | conda `mcm-toolkit` |
|
| 检查 | venv | conda `mcm-toolkit` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pytest` | 28 passed, 45 skipped | 28 passed, 45 skipped |
|
| `pytest` | 31 passed, 45 skipped | 31 passed, 45 skipped |
|
||||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||||
| `ruff check` | All checks passed | All checks passed |
|
| `ruff check` | All checks passed | All checks passed |
|
||||||
| `scripts/doctor.ps1` | 跑通 | — |
|
| `scripts/doctor.ps1` | 跑通 | — |
|
||||||
| SALib / pymoo / pyright | 缺失 | 已装 |
|
| SALib / pymoo / pyright | 缺失 | 已装 |
|
||||||
|
|
||||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||||
- skip 的都是等实现的算法行为测试;通过的 28 条 = 20 条公开 API 契约测试
|
- skip 的都是等实现的算法行为测试;通过的 31 条 = 20 条公开 API 契约测试
|
||||||
(`tests/test_api_surface.py`)+ 8 条 `core` 行为测试(`tests/test_core.py`)
|
(`tests/test_api_surface.py`)+ 11 条 `core` 行为测试(`tests/test_core.py`)
|
||||||
- 三个 `.ps1` 通过 PowerShell AST 语法解析
|
- 三个 `.ps1` 通过 PowerShell AST 语法解析
|
||||||
|
|
||||||
### ⚠️ 需要注意 / 遗留
|
### ⚠️ 需要注意 / 遗留
|
||||||
@@ -140,7 +140,8 @@
|
|||||||
| 21 | `Result.__repr__` **不打印 `data` 内容**,只给类型与规模 | 模拟轨迹动辄十万个数,repr 一旦刷屏,traceback / 调试器 / notebook 输出就全废了。要看数据请直接取 `result.data` |
|
| 21 | `Result.__repr__` **不打印 `data` 内容**,只给类型与规模 | 模拟轨迹动辄十万个数,repr 一旦刷屏,traceback / 调试器 / notebook 输出就全废了。要看数据请直接取 `result.data` |
|
||||||
| 22 | `Result.summary()` 的行序 = 字典插入顺序,不排序 | 比赛时"想先看哪个指标"自己决定键的插入顺序即可,工具不该替人排序 |
|
| 22 | `Result.summary()` 的行序 = 字典插入顺序,不排序 | 比赛时"想先看哪个指标"自己决定键的插入顺序即可,工具不该替人排序 |
|
||||||
| 23 | `set_seed` 内部**允许**调用 `np.random.seed`(带 noqa) | sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 `_GLOBAL_SEED` 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同 |
|
| 23 | `set_seed` 内部**允许**调用 `np.random.seed`(带 noqa) | sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 `_GLOBAL_SEED` 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同 |
|
||||||
| 24 | `get_rng(None)` 不做 `SeedSequence.spawn` 拆分,同一种子下多条流相同 | 多引入一个模块级计数器,会让"同一次调用顺序不同 → 结果不同",对刚上手的人更难解释。取舍写进 docstring,真要独立流就显式传不同 `random_state` |
|
| 24 | `get_rng(None)` **复用同一个模块级生成器**(会推进),不每次新建 | **用户本人提出**,取代 Codex 原来"每次新建 `default_rng(全局种子)`"的写法——那样两个都省略 `random_state` 的函数会抽到完全相同的数。共享单例既保证重跑一致,又让流自然推进 |
|
||||||
|
| 25 | `src/` 下的实现默认由**用户本人**写,Codex 只改 `tests/` 与 `docs/` | 目标是"一边学一边自己写"。曾出现 Codex 直接落盘 `random.py`、与用户编辑器缓冲区冲突导致保存失败。需要代写时用户会明说 |
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
| [03-roadmap.md](03-roadmap.md) | 功能清单分级(L0 / L1 / L2 / 不做)与开发纪律 |
|
| [03-roadmap.md](03-roadmap.md) | 功能清单分级(L0 / L1 / L2 / 不做)与开发纪律 |
|
||||||
| [04-environment.md](04-environment.md) | conda / venv 环境配置、镜像、离线约束 |
|
| [04-environment.md](04-environment.md) | conda / venv 环境配置、镜像、离线约束 |
|
||||||
| [dev/function-card-template.md](dev/function-card-template.md) | S 档算法的「函数卡片」模板 |
|
| [dev/function-card-template.md](dev/function-card-template.md) | S 档算法的「函数卡片」模板 |
|
||||||
|
| [05-git-workflow.md](05-git-workflow.md) | Git 工作流、Gitea 远程(SSH 走 Tailscale)、行尾策略 |
|
||||||
|
| [dev/personal-pitfalls.md](dev/personal-pitfalls.md) | **个人易错点清单:只追加**,追加前不通读 |
|
||||||
|
|
||||||
## 阅读顺序
|
## 阅读顺序
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 个人易错点(只追加)
|
||||||
|
|
||||||
|
> **本文件的规矩**
|
||||||
|
> 1. **只追加。** 已有条目一个字都不改、不重排、不润色。发现写错了 → 在末尾补一条"更正",不要回头改。
|
||||||
|
> 2. **不通读。** 追加前只做两件事:`rg -i "<关键词>" docs/dev/personal-pitfalls.md` 看有没有记过;
|
||||||
|
> 看一眼下面的索引表最后一行拿编号。全文只在复盘时才读。
|
||||||
|
> 3. 一条写三行:**现象** / **为什么会错** / **以后怎么防**。
|
||||||
|
> 写不出"为什么会错"就先别记——那说明还没想清楚。
|
||||||
|
> 4. 记**规律**,不记事故。"今天又忘了"不新开条目,在已有条目下把 `复发` 加一。
|
||||||
|
|
||||||
|
## 追加模板
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### N. <一句话>
|
||||||
|
- 现象:
|
||||||
|
- 为什么会错:
|
||||||
|
- 以后怎么防:
|
||||||
|
- 首次:YYYY-MM-DD|复发:0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 索引
|
||||||
|
|
||||||
|
| # | 一句话 | 首次 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 类型判断只写 `int`,漏掉 `np.int64` 这类 numpy 标量 | 2026-10-01 |
|
||||||
|
| 2 | 以为 `# TODO(L0)` 只是普通注释 | 2026-10-01 |
|
||||||
|
| 3 | 签名里的类型注解和 docstring 说的不一致 | 2026-10-01 |
|
||||||
|
| 4 | 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局 | 2026-10-01 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. 类型判断只写 `int`,漏掉 `np.int64` 这类 numpy 标量
|
||||||
|
|
||||||
|
- 现象:`get_rng(np.int64(42))` 不报错,但种子被静默忽略、退回全局流——结果不可复现,且没有任何提示。
|
||||||
|
- 为什么会错:`np.int64` **不是** python `int` 的子类(`isinstance(np.int64(42), int)` 是 `False`),
|
||||||
|
但 `np.float64` **是** `float` 的子类。numpy 标量的继承关系不统一,凭直觉一定会翻车。
|
||||||
|
- 以后怎么防:写类型分支时默认假设"这个值可能来自 numpy"。判断用 `isinstance(x, (int, np.integer))`,
|
||||||
|
取值统一 `int(x)` 归一化;并且给所有非预期类型加 `raise TypeError` 兜底——
|
||||||
|
**宁可报错,也不要静默走默认分支**。
|
||||||
|
- 首次:2026-10-01|复发:0
|
||||||
|
|
||||||
|
### 2. 以为 `# TODO(L0)` 只是普通注释
|
||||||
|
|
||||||
|
- 现象:函数实现完了,`# TODO(L0)` 还挂在末尾。
|
||||||
|
- 为什么会错:本仓库把 `TODO(L0)` / `TODO(L1)` / `TODO(v0.2)` 当**进度标记**用:
|
||||||
|
`docs/03-roadmap.md` 靠它划分档位,`STATUS.md` 靠它统计"还剩哪些没做"。实现完不删,等于骗自己。
|
||||||
|
- 以后怎么防:实现完成时顺手删掉该函数里的 `TODO(...)`。
|
||||||
|
全仓还剩多少没做,一条 `rg "TODO\(" src/` 就能数出来。
|
||||||
|
- 首次:2026-10-01|复发:0
|
||||||
|
|
||||||
|
### 3. 签名里的类型注解和 docstring 说的不一致
|
||||||
|
|
||||||
|
- 现象:`def get_rng(random_state: int | None = None)`,docstring 却写"已经是 `np.random.Generator` 时原样返回"。
|
||||||
|
- 为什么会错:类型注解是给**读代码的人和 pyright** 看的契约。注解说只收 int/None、docstring 说还收 Generator,
|
||||||
|
两处打架时合法用法会被 IDE 标红,真正的错误反而看不出来。
|
||||||
|
- 以后怎么防:改签名和改 docstring 必须同一次做完。
|
||||||
|
本项目所有 `random_state` 的注解统一写 `int | np.integer | np.random.Generator | None`。
|
||||||
|
- 首次:2026-10-01|复发:0
|
||||||
|
|
||||||
|
### 4. 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局
|
||||||
|
|
||||||
|
- 现象:`set_seed` 换掉了本包的生成器,但 sklearn / statsmodels 里 `random_state=None` 的路径
|
||||||
|
仍不可复现(测试直接挂)。
|
||||||
|
- 为什么会错:numpy 有**两套**随机状态——旧的 `np.random.*` 全局单例,和新的 `Generator`。
|
||||||
|
第三方库大量仍在走旧的那套(`check_random_state(None)`),自己新建一个 Generator 管不到它们。
|
||||||
|
- 以后怎么防:凡是"设全局状态"的函数,先列清单——自己的、numpy 旧的、标准库 `random` 的、
|
||||||
|
第三方框架的,逐个决定要不要一起设,并在 docstring 里写清到底覆盖了哪几个。
|
||||||
|
- 首次:2026-10-01|复发:0
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
* 所有含随机的公开函数签名都带 `random_state=None`,内部第一行取 rng。
|
* 所有含随机的公开函数签名都带 `random_state=None`,内部第一行取 rng。
|
||||||
* 禁止使用 `np.random.*` 旧式全局接口,也禁止裸用标准库 `random`。
|
* 禁止使用 `np.random.*` 旧式全局接口,也禁止裸用标准库 `random`。
|
||||||
(唯一例外是 :func:`set_seed` 内部的 `np.random.seed`,见该函数说明。)
|
(唯一例外是 :func:`set_seed` 里的 `np.random.seed`,见该函数说明。)
|
||||||
* 论文附录必须写明 seed —— 提交要能复现。
|
* 论文附录必须写明 seed —— 提交要能复现。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -14,25 +14,32 @@ import numpy as np
|
|||||||
|
|
||||||
__all__ = ["get_rng", "set_seed"]
|
__all__ = ["get_rng", "set_seed"]
|
||||||
|
|
||||||
# set_seed 记下来的全局种子;None 表示用户没设过。
|
# 模块级共享生成器:set_seed 换掉它,无参 get_rng 复用它。
|
||||||
_GLOBAL_SEED: int | None = None
|
_GLOBAL_RNG: np.random.Generator | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_rng(random_state: int | np.random.Generator | None = None) -> np.random.Generator:
|
def get_rng(
|
||||||
|
random_state: int | np.integer | np.random.Generator | None = None,
|
||||||
|
) -> np.random.Generator:
|
||||||
"""取一个随机数生成器。
|
"""取一个随机数生成器。
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
random_state:
|
random_state:
|
||||||
`int` 时返回固定种子的独立生成器(可复现);
|
`int` / `np.integer` 时返回固定种子的独立生成器(可复现);
|
||||||
`None` 时跟随 :func:`set_seed` 设过的全局种子,没设过就交给系统熵源;
|
`None` 时返回共享的全局生成器(由 :func:`set_seed` 设定,**会被推进**);
|
||||||
已经是 :class:`np.random.Generator` 时原样返回(便于函数间串联)。
|
已经是 :class:`np.random.Generator` 时原样返回(便于函数间串联)。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError
|
||||||
|
传进来的既不是整数、也不是 Generator、也不是 None。
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
同一个全局种子下,每次 `get_rng()` 拿到的都是**同一条**流。这保证了
|
无参调用返回的是**同一个**生成器对象,所以连续两次 `get_rng()` 取到的随机数
|
||||||
"脚本重跑一遍结果一样",但也意味着两个都省略 `random_state` 的函数会共用
|
不同(流在推进)。这既保证"重跑一遍结果一样",又避免两个都省略 `random_state`
|
||||||
同一串随机数。想避开,就显式传不同的 `random_state`。
|
的函数抽到完全相同的数。
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
@@ -42,23 +49,40 @@ def get_rng(random_state: int | np.random.Generator | None = None) -> np.random.
|
|||||||
"""
|
"""
|
||||||
if isinstance(random_state, np.random.Generator):
|
if isinstance(random_state, np.random.Generator):
|
||||||
return random_state
|
return random_state
|
||||||
if random_state is None:
|
|
||||||
random_state = _GLOBAL_SEED # 没设过就是 None,交给系统熵源
|
# bool 是 int 的子类,但 True 被当成种子 1 几乎总是笔误,宁可报错也不静默接受。
|
||||||
return np.random.default_rng(random_state)
|
if isinstance(random_state, bool):
|
||||||
|
raise TypeError("random_state 不接受 bool:True 会被当成种子 1,通常是笔误")
|
||||||
|
|
||||||
|
# np.int64 这类 numpy 标量**不是** python int 的子类,必须一起判断;
|
||||||
|
# 只写 int 的话,传进来的 numpy 整数会静默掉进下面的全局分支,种子被无声忽略。
|
||||||
|
if isinstance(random_state, (int, np.integer)):
|
||||||
|
return np.random.default_rng(int(random_state))
|
||||||
|
|
||||||
|
if random_state is not None:
|
||||||
|
raise TypeError(
|
||||||
|
"random_state 只接受 int / np.integer / np.random.Generator / None,"
|
||||||
|
f"收到的是 {type(random_state).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
global _GLOBAL_RNG
|
||||||
|
if _GLOBAL_RNG is None:
|
||||||
|
_GLOBAL_RNG = np.random.default_rng()
|
||||||
|
return _GLOBAL_RNG
|
||||||
|
|
||||||
|
|
||||||
def set_seed(seed: int) -> None:
|
def set_seed(seed: int | np.integer) -> None:
|
||||||
"""设置全局随机种子。
|
"""设置全局随机种子。
|
||||||
|
|
||||||
除了记住种子,还会一并喂给 numpy 的旧全局状态:sklearn / statsmodels
|
除了换掉本包的共享生成器,还会一并设置 numpy 的**旧全局状态**:
|
||||||
内部仍在用它(`check_random_state(None)` 那条路),只设自己的
|
sklearn / statsmodels 内部走 `check_random_state(None)` 时用的就是它,
|
||||||
`_GLOBAL_SEED` 的话这些库照样不可复现。
|
只管自己的生成器的话,这些库照样不可复现。
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> set_seed(42)
|
>>> set_seed(42)
|
||||||
"""
|
"""
|
||||||
global _GLOBAL_SEED
|
global _GLOBAL_RNG
|
||||||
_GLOBAL_SEED = int(seed)
|
_GLOBAL_RNG = np.random.default_rng(seed)
|
||||||
# 这里是全项目唯一允许碰旧接口的地方:它是"设种子",不是"取随机数"。
|
# 全项目唯一允许碰旧接口的地方:这里是"设种子",不是"取随机数"。
|
||||||
np.random.seed(_GLOBAL_SEED) # noqa: NPY002 - 故意的,见本函数 docstring
|
np.random.seed(seed) # noqa: NPY002
|
||||||
@@ -21,6 +21,31 @@ class TestRandom:
|
|||||||
g = np.random.default_rng(0)
|
g = np.random.default_rng(0)
|
||||||
assert get_rng(g) is g
|
assert get_rng(g) is g
|
||||||
|
|
||||||
|
def test_get_rng_accepts_numpy_integer(self):
|
||||||
|
"""np.int64 不是 int 的子类,必须当成整数处理,不能静默退化成全局流。"""
|
||||||
|
from mcm_toolkit.core import get_rng, set_seed
|
||||||
|
|
||||||
|
set_seed(1)
|
||||||
|
first = get_rng(np.int64(42))
|
||||||
|
second = get_rng(np.int64(42))
|
||||||
|
assert first is not second, "传 np.integer 应当新建独立生成器"
|
||||||
|
assert first.random() == np.random.default_rng(42).random()
|
||||||
|
|
||||||
|
def test_get_rng_rejects_bad_types(self):
|
||||||
|
"""类型不对就报错。静默当成"没传种子",换来的只会是不可复现的结果。"""
|
||||||
|
from mcm_toolkit.core import get_rng
|
||||||
|
|
||||||
|
for bad in ["42", 42.0, True, [42]]:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
get_rng(bad)
|
||||||
|
|
||||||
|
def test_get_rng_without_seed_shares_and_advances(self):
|
||||||
|
from mcm_toolkit.core import get_rng, set_seed
|
||||||
|
|
||||||
|
set_seed(3)
|
||||||
|
assert get_rng() is get_rng(), "无参调用应返回同一个共享生成器"
|
||||||
|
assert get_rng().random() != get_rng().random(), "共享生成器必须被推进"
|
||||||
|
|
||||||
def test_get_rng_default_follows_global_seed(self):
|
def test_get_rng_default_follows_global_seed(self):
|
||||||
"""set_seed 之后,不传 random_state 也必须复现——这是"论文能重跑"的底线。"""
|
"""set_seed 之后,不传 random_state 也必须复现——这是"论文能重跑"的底线。"""
|
||||||
from mcm_toolkit.core import get_rng, set_seed
|
from mcm_toolkit.core import get_rng, set_seed
|
||||||
|
|||||||
Reference in new issue
Block a user