feat(solve): random_walk 落地;修默认采样器真 bug(integers 不收 float)
random_walk 由用户本人写;Codex 改一处真 bug + 工程侧。 - 真 bug:默认采样器 rng.integers(0, 2, size=size, dtype=float) 被 numpy 拒掉 (Unsupported dtype dtype(float64) for integers),默认路径 100% success=False、 data=None,连 docstring 的 Examples 都跑不通。改成 integers(size=size) * 2 - 1, 由后面的 np.asarray(..., dtype=float) 转 float;实测 (5,101)、步长恒 ±1、同种子可复现 - 工程:ruff W293 x2;docstring 补全(n_steps/n_paths/start/random_state + Returns / Raises / Notes,写清 step_sampler 收二维 size=(n_paths, n_steps)) - tests:random_walk 首次有测试,新增 6 条(形状 + start、步长恒 ±1、可复现、自定义 高斯采样器、3 类非法入参、docstring Example) - monte_carlo_integral:用户第二轮已收 ndarray + 把三行提出 if/else(决策 52) - 验证:205 收集 / 174 passed / 31 skipped / 0 xfail,ruff 0,pyright 0 - docs:STATUS 决策 53 + random_walk 4 条待办;CHANGELOG;易错点 24/25,第 18 条复发 +1
This commit is contained in:
1 parent
dcda080be4
commit
8ec08ea9ce
5 files changed
+249
-27
No files matched your search
@@ -443,6 +443,29 @@
|
|||||||
- `STATUS.md`:integral 的 3 条待办标记办结、概率函数的 6 条待办全部办结;新增**决策 52** 与 2 条新口径
|
- `STATUS.md`:integral 的 3 条待办标记办结、概率函数的 6 条待办全部办结;新增**决策 52** 与 2 条新口径
|
||||||
(`bounds` 收不收 ndarray、`if`/`else` 里那三行重复);验证计数更新为 168 / 31。
|
(`bounds` 收不收 ndarray、`if`/`else` 里那三行重复);验证计数更新为 168 / 31。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `solve.random_walk` 落地(**L1,S 档**,实现由用户本人写):`step_sampler(rng, size=(n_paths, n_steps))` 一次抽完整批增量 →
|
||||||
|
`np.cumsum(..., axis=1)` → 首列填 `start`、其余列 `start + cumsum`;`data` 形状 `(n_paths, n_steps + 1)`。
|
||||||
|
配套 `tests/test_solve.py` **新增 6 条**(此前这个函数一条测试都没有)。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`random_walk` 的默认采样器整个不可用**(真 bug,按决策 31 修):`rng.integers(0, 2, size=size, dtype=float)`
|
||||||
|
被 numpy 拒掉(`TypeError: Unsupported dtype dtype('float64') for integers`)—— 默认路径 100% `success=False`、
|
||||||
|
`data=None`,连 docstring 的 Examples 都跑不通。改成 `rng.integers(0, 2, size=size) * 2 - 1`
|
||||||
|
(由后面的 `np.asarray(..., dtype=float)` 统一转 float);实测形状 `(5, 101)`、步长恒 ±1、同种子可复现。
|
||||||
|
- 格式:`simulate.py` 又两行"只有空格的行"(ruff W293 ×2)。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `monte_carlo_integral` 按用户第二轮口径收口(**决策 52**):`bounds` 现在也收 `np.ndarray`;
|
||||||
|
`name` / `params["bounds"]` / `params["dimension"]` 从 `if` / `else` 两支提到 `if` 之前,只让 `message` 分两支。
|
||||||
|
- `random_walk` 的 docstring 补全(原文只有 `step_sampler` 一节):补齐 `n_steps` / `n_paths` / `start` / `random_state`
|
||||||
|
与 `Returns` / `Raises` / `Notes`,并写清 `step_sampler` 收的是**二维** `size=(n_paths, n_steps)`。
|
||||||
|
- `tests/test_solve.py`:新增 6 条 `random_walk` 测试(形状与 `start` 列、默认步长恒 ±1、同种子可复现、
|
||||||
|
自定义高斯采样器、3 类非法入参、docstring Example 真能跑);`solve` 段 32 → **38** 条,
|
||||||
|
全量 **205 收集 / 174 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
|
||||||
|
- `STATUS.md`:新增**决策 53** 与 `random_walk` 的 4 条待办;`monte_carlo_integral` 的 2 条新口径办结 1 条
|
||||||
|
(剩"第一道 `isinstance` 分支已冗余");「下一步」推进到 `markov_simulate` / `markov_steady_state`。
|
||||||
|
|
||||||
## [0.0.1] - 2026-10-01
|
## [0.0.1] - 2026-10-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
当前 **L0 11/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
当前 **L0 11/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
||||||
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
||||||
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
||||||
`monte_carlo_probability`、`monte_carlo_integral`(**用户写**,2026-10-05)。剩余 1 项:`save_figure`(Codex 写)。
|
`monte_carlo_probability`、`monte_carlo_integral`、`random_walk`(**用户写**,2026-10-05)。剩余 1 项:`save_figure`(Codex 写)。
|
||||||
`monte_carlo` 的口径已拍板(决策 48);概率 / 积分两个函数的待办**均已办结**,
|
`monte_carlo` 的口径已拍板(决策 48);概率 / 积分两个函数的待办**均已办结**,
|
||||||
剩下 `ci_low`/`ci_high` 的语义与 `monte_carlo_integral` 的 2 条新口径,见"下一步"。
|
剩下 `ci_low`/`ci_high` 的语义与 `monte_carlo_integral` 的 2 条新口径,见"下一步"。
|
||||||
## 任务清单
|
## 任务清单
|
||||||
@@ -292,13 +292,30 @@
|
|||||||
- ✅ **3 条待办已由用户第二轮办结**(同日):`message` 改成 `if result.success:` / `else:` 两支(失败语义不再被覆盖);
|
- ✅ **3 条待办已由用户第二轮办结**(同日):`message` 改成 `if result.success:` / `else:` 两支(失败语义不再被覆盖);
|
||||||
入口补 `isinstance(bounds, (list, tuple))` + `ndim == 2 and shape[1] == 2` 两道(`[0, 1]` 从裸 `IndexError` 变成可读 `ValueError`);
|
入口补 `isinstance(bounds, (list, tuple))` + `ndim == 2 and shape[1] == 2` 两道(`[0, 1]` 从裸 `IndexError` 变成可读 `ValueError`);
|
||||||
`n` 注解放宽为 `int | np.integer`。⏳ 尚有 2 条新口径(`bounds` 收不收 ndarray、`if`/`else` 三行重复)见"下一步"
|
`n` 注解放宽为 `int | np.integer`。⏳ 尚有 2 条新口径(`bounds` 收不收 ndarray、`if`/`else` 三行重复)见"下一步"
|
||||||
|
- [x] `solve/simulate.py` 的 `random_walk`(**L1,S 档,算法由用户本人写**,2026-10-05)
|
||||||
|
- 核心:`step_sampler(rng, size=(n_paths, n_steps))` 一次抽完整批增量 → `np.cumsum(..., axis=1)` →
|
||||||
|
首列填 `start`、其余列 `start + cumsum`。`data` 形状 `(n_paths, n_steps + 1)`
|
||||||
|
- **Codex 改了一处真 bug(决策 31,纯 bug)**:默认采样器写 `rng.integers(0, 2, size=size, dtype=float)`,
|
||||||
|
而 numpy 的 `integers` 不收 `dtype=float`(`TypeError: Unsupported dtype dtype('float64') for integers`)
|
||||||
|
—— **默认路径 100% 失败**(`success=False`、`data=None`),连 docstring 的 Examples 都跑不通。
|
||||||
|
改成 `rng.integers(0, 2, size=size) * 2 - 1`(后面的 `np.asarray(..., dtype=float)` 负责转 float)。
|
||||||
|
实测:`(5, 101)` 形状、`diff` 恒为 ±1、同种子可复现
|
||||||
|
- **Codex 只碰工程侧**:① ruff 两处 W293(只有空格的行);② docstring 补全(原只有 `step_sampler` 一节,
|
||||||
|
现补齐 `n_steps` / `n_paths` / `start` / `random_state` + `Returns` / `Raises` / `Notes`,
|
||||||
|
并写清 `step_sampler` 收的是**二维** `size=(n_paths, n_steps)`);③ Examples 增加一条"步长恒为 ±1"的断言
|
||||||
|
- **测试:新增 6 条**(此前 `random_walk` **一条测试都没有**)—— 形状与 `start` 列、默认步长恒 ±1、
|
||||||
|
同种子可复现、自定义高斯采样器(带漂移)、3 类非法入参、docstring Example 真能跑
|
||||||
|
- ⏳ **留给用户 4 条**(见"下一步"):非整数 `n_steps` 该抛 `TypeError` 而现抛 `ValueError`;
|
||||||
|
`steps` 形状没校验(`n_paths == n_steps` 时转置的采样器会**静默给出转置结果**);NaN 不判失败;
|
||||||
|
`params` 少了 `random_state`、`metrics` 只是把 `n_steps` / `n_paths` 抄了一遍
|
||||||
|
|
||||||
- [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**)
|
- [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**)
|
||||||
|
|
||||||
### 验证结果
|
### 验证结果
|
||||||
|
|
||||||
| 检查 | venv | conda `mcm-toolkit` |
|
| 检查 | venv | conda `mcm-toolkit` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pytest` | 见下(单列) | **168 passed, 31 skipped(0 xfail)** |
|
| `pytest` | 见下(单列) | **174 passed, 31 skipped(0 xfail)** |
|
||||||
| `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**(0 error) |
|
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
||||||
| `scripts/doctor.ps1` | 跑通 | — |
|
| `scripts/doctor.ps1` | 跑通 | — |
|
||||||
@@ -308,8 +325,8 @@
|
|||||||
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
||||||
`docs/04-environment.md` 坑 7)
|
`docs/04-environment.md` 坑 7)
|
||||||
- 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(与两套环境一致)
|
||||||
- **199 条用例:168 passed / 31 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
- **205 条用例:174 passed / 31 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||||
`test_core.py` 23、`test_solve.py` 32(本轮再 +2:非法 `bounds` 扩到 4 类 + 失败 message 语义 / `np.int64`)、`test_viz.py` 8、
|
`test_core.py` 23、`test_solve.py` 38(本轮 +6:`random_walk` 首次有测试)、`test_viz.py` 8、`test_decision.py` 7、
|
||||||
`test_stats.py` 53、`test_models.py` 5
|
`test_stats.py` 53、`test_models.py` 5
|
||||||
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
||||||
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
||||||
@@ -450,21 +467,44 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
|||||||
① `isinstance(bounds, (list, tuple))` 把 `np.array([[0, 1]])` 也拒了,与"尽量传 ndarray"的偏好冲突;
|
① `isinstance(bounds, (list, tuple))` 把 `np.array([[0, 1]])` 也拒了,与"尽量传 ndarray"的偏好冲突;
|
||||||
② `if` / `else` 两支里 `name` / `params["bounds"]` / `params["dimension"]` 各写了一遍(易错点 20)。
|
② `if` / `else` 两支里 `name` / `params["bounds"]` / `params["dimension"]` 各写了一遍(易错点 20)。
|
||||||
|
|
||||||
|
| 53 | `random_walk` 的默认步长采样器**不能用 `rng.integers(..., dtype=float)`**(numpy 直接抛 `TypeError`);`step_sampler` 收到的 `size` 是**二维** `(n_paths, n_steps)` |
|
||||||
|
用户 2026-10-05 写完第一版。默认采样器原写 `rng.integers(0, 2, size=size, dtype=float) * 2.0 - 1.0` ——
|
||||||
|
`Generator.integers` 不收浮点 dtype(`TypeError: Unsupported dtype dtype('float64') for integers`),
|
||||||
|
于是**整个默认路径 100% 失败**,连 docstring 的 Examples 都跑不通;Codex 按决策 31 改成 `rng.integers(0, 2, size=size) * 2 - 1`
|
||||||
|
(交给后面的 `np.asarray(..., dtype=float)` 转类型),实测形状 `(5, 101)`、步长恒 ±1、同种子可复现。
|
||||||
|
`step_sampler` 的 `size` 是 `(n_paths, n_steps)` 而不是 `monte_carlo` 那样的一个整数 —— 契约已写进 docstring,
|
||||||
|
**遗留 4 条待办**见"下一步"(`TypeError`/`ValueError` 分工、`steps` 形状校验、NaN 口径、`params` 缺 `random_state`)。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
### ⭐ 当前进度与待办(2026-10-05 第二轮)
|
### ⭐ 当前进度与待办(2026-10-05 第三轮)
|
||||||
|
|
||||||
**本轮:`monte_carlo_integral` 已由用户写完并审查通过**(L1,S 档)。Codex 只改了一处**纯 bug**
|
**本轮:`random_walk` 已由用户写完第一版并审查通过**(L1,S 档)。Codex 改了一处**真 bug**
|
||||||
(体积 `lows - highs` → `highs - lows`,见决策 51)—— **算法结构一行未动**。
|
(默认采样器 `rng.integers(..., dtype=float)` 是 numpy 不支持的写法,默认路径 100% 失败,见决策 53),
|
||||||
|
另补 ruff / docstring / **6 条从无到有的测试**。
|
||||||
|
|
||||||
**接下来三件事**
|
**接下来三件事**
|
||||||
1. `monte_carlo_integral` 的 3 条待办**已办结**;新增 2 条口径(下面一节)——都是你的活。
|
1. `random_walk` 有 **4 条待办**(下面一节)——都是你的活。
|
||||||
2. `ci_low` / `ci_high` 的语义确认一句(也是你的活),确认后 Codex 补 `docs/algorithms/monte_carlo.md` 卡片。
|
2. `ci_low` / `ci_high` 的语义确认一句(也是你的活),确认后 Codex 补 `docs/algorithms/monte_carlo.md` 卡片。
|
||||||
3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
|
3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
|
||||||
|
|
||||||
**你下一个要写的(S 档,仍在 `solve/simulate.py`)**:`random_walk`。
|
**你下一个要写的(S 档,仍在 `solve/simulate.py`)**:`markov_simulate` → `markov_steady_state`。
|
||||||
它和刚写完的两个函数是**同一套骨架的第三次复用**(造批 → 一次算 → 汇总),新东西只有两处:
|
两者是同一件事的两面:前者按转移矩阵**逐代抽状态**(`rng.choice` 的 `p=` 版),后者用**迭代**求平稳分布。
|
||||||
`np.cumsum` 把增量累成轨迹;`data` 的 shape 从 `(n,)` 变成 `(n_paths, n_steps + 1)` 的**二维**。
|
`markov_steady_state` 的 docstring 已写明口径:用迭代而不用特征分解,因为"收敛快不快"本身就是论文里要写的性质。
|
||||||
|
|
||||||
|
### ⏳ random_walk 的 4 条待办(2026-10-05,你的活)
|
||||||
|
|
||||||
|
1. **非整数 `n_steps` 抛的是 `ValueError`,按 §4.7 该是 `TypeError`**:现在 `n_steps=2.5` / `n_steps=True` 都走
|
||||||
|
`ValueError("必须是正整数")`。同模块的 `monte_carlo` / `monte_carlo_probability` 是"类型不对 → `TypeError`、
|
||||||
|
取值不对(`<= 0`)→ `ValueError`",这里要拆成两条。
|
||||||
|
2. **`steps` 没有形状校验,会静默出错**:实测 `n_paths == n_steps` 时,一个返回**转置**形状的采样器
|
||||||
|
能拿到 `success=True` 和形状"正确"的 `data` —— 数字却是转置过来的(`n_paths != n_steps` 时才报错)。
|
||||||
|
方向:`steps.shape != (n_paths, n_steps)` 就判失败,和 `monte_carlo` 的 `len(data) != n` 一个套路。
|
||||||
|
3. **NaN 不判失败**:`step_sampler` 返回全 NaN 时现在照样 `success=True`(决策 48 里 `monte_carlo` 是判失败的)——
|
||||||
|
两个函数的 NaN 口径要不要拉齐?
|
||||||
|
4. **`params` 少了 `random_state`**(另两个函数都记了,`Result.params` 的用途就是"能复现");
|
||||||
|
另外 `metrics={"n_steps", "n_paths"}` 只是把 `params` 里的两个数抄了一遍 —— 要么换成真正的指标
|
||||||
|
(终点的均值 / 标准差、理论 `√t` 对比),要么直接去掉。
|
||||||
|
|
||||||
### ✅ monte_carlo_probability 的 6 条待办(2026-10-05 全部办结)
|
### ✅ monte_carlo_probability 的 6 条待办(2026-10-05 全部办结)
|
||||||
|
|
||||||
@@ -488,13 +528,15 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
|||||||
`bounds=[0, 1]` 现在报"bounds 必须形如 [(lo, hi), ...],当前形状为 (2,),请检查是否漏了括号"。
|
`bounds=[0, 1]` 现在报"bounds 必须形如 [(lo, hi), ...],当前形状为 (2,),请检查是否漏了括号"。
|
||||||
- ~~③ `n: int`~~ → 已改 `int | np.integer`。
|
- ~~③ `n: int`~~ → 已改 `int | np.integer`。
|
||||||
|
|
||||||
### ⏳ monte_carlo_integral 还有 2 条口径(新增,你的活)
|
### ✅ monte_carlo_integral 的 2 条新口径(2026-10-05 已办 1 条、剩 1 条)
|
||||||
|
|
||||||
1. **`bounds` 把 ndarray 也拒了**:`isinstance(bounds, (list, tuple))` 使 `np.array([[0.0, 1.0]])` 直接 `ValueError`。
|
- ~~① `bounds` 把 ndarray 拒了~~ → 已加 `isinstance(bounds, np.ndarray)` 分支,`np.array([[0.0, 1.0]])` 现在能收。
|
||||||
这和你自己定的口径("以后我也尽量不传 list 和 tuple,尽量用 `pd.DataFrame` / `pd.Series` / `np.array`")冲突 ——
|
- ~~② `if` / `else` 里那三行重复~~ → 已把 `name` / `params["bounds"]` / `params["dimension"]` 提到 `if` 之前,只剩 `message` 分两支。
|
||||||
要不要放成"list / tuple / ndarray 都收"?(`np.asarray` 本来就能接住三者)
|
|
||||||
2. **`if` / `else` 两个分支里那三行写了两遍**:`result.name`、`params["bounds"]`、`params["dimension"]` 各写一次。
|
**还剩 1 条(你的活)**
|
||||||
要改的话很简单:这三行提到 `if` 之前,只让 `message` 分两支 —— 正是易错点 20 说的"同一个信息写两遍"。
|
1. **第一道 `isinstance` 分支现在是冗余的**:`np.asarray` 本来就同时收 list / tuple / ndarray,
|
||||||
|
而 `[]`、`"abc"`、标量这些都会被下面那句形状校验抓住(报"当前形状为 (0,)"等)。
|
||||||
|
整段可以塌成两行:`bounds_arr = np.asarray(bounds)` + 形状校验 —— 顺带 `bounds.copy()` 也是多余的一次拷贝。
|
||||||
|
|
||||||
### ⏳ monte_carlo 只剩 1 个口径要你确认
|
### ⏳ monte_carlo 只剩 1 个口径要你确认
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
| 21 | 公式里的 `^` 不是乘方,而且被 `except` 吞成 success=False | 2026-10-05 |
|
| 21 | 公式里的 `^` 不是乘方,而且被 `except` 吞成 success=False | 2026-10-05 |
|
||||||
| 22 | 失败结果的 `message` 被包装层无条件覆盖 —— 对外永远报"成功" | 2026-10-05 |
|
| 22 | 失败结果的 `message` 被包装层无条件覆盖 —— 对外永远报"成功" | 2026-10-05 |
|
||||||
| 23 | 体积/宽度这种"看着总为正"的量也会减反,维度奇偶还会把符号错误藏住 | 2026-10-05 |
|
| 23 | 体积/宽度这种"看着总为正"的量也会减反,维度奇偶还会把符号错误藏住 | 2026-10-05 |
|
||||||
|
| 24 | 默认参数路径从没被执行过 —— 默认采样器用了库不接受的参数 | 2026-10-05 |
|
||||||
|
| 25 | 两个维度相等时,转置的形状检查会静默通过 | 2026-10-05 |
|
||||||
|
| 22 | 失败结果的 `message` 被包装层无条件覆盖 —— 对外永远报"成功" | 2026-10-05 |
|
||||||
|
| 23 | 体积/宽度这种"看着总为正"的量也会减反,维度奇偶还会把符号错误藏住 | 2026-10-05 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -335,7 +339,9 @@
|
|||||||
(int + np.integer / bool 排在 int 前 / float + np.floating / 数组先 `np.asarray`),
|
(int + np.integer / bool 排在 int 前 / float + np.floating / 数组先 `np.asarray`),
|
||||||
再用三个探针各打一枪:`f(x=np.int64(1))`、`f(x=True)`、`f(x=1.0)`。
|
再用三个探针各打一枪:`f(x=np.int64(1))`、`f(x=True)`、`f(x=1.0)`。
|
||||||
**这一步是本人的活**(决策 49):要不要放宽、报什么错、算不算合法,都是取舍,不是机械活。
|
**这一步是本人的活**(决策 49):要不要放宽、报什么错、算不算合法,都是取舍,不是机械活。
|
||||||
- 首次:2026-10-04|复发:0
|
- 复发(2026-10-05):`random_walk` 的 `n_steps = 2.5` / `n_steps = True` 又都走 `ValueError("必须是正整数")` ——
|
||||||
|
同模块的 `monte_carlo` 早就是"类型不对 `TypeError`、取值不对 `ValueError`"。表就在那儿,还是没翻。
|
||||||
|
- 首次:2026-10-04|复发:**1(2026-10-05)**
|
||||||
|
|
||||||
### 19. 只想着主路径上的那种值,忘了同一位置还会出现别的类型 / 值
|
### 19. 只想着主路径上的那种值,忘了同一位置还会出现别的类型 / 值
|
||||||
|
|
||||||
@@ -404,3 +410,28 @@
|
|||||||
拿到 `mean` 之后先跟解析值比**符号**,再看数值大小。
|
拿到 `mean` 之后先跟解析值比**符号**,再看数值大小。
|
||||||
- 首次:2026-10-05|复发:0
|
- 首次:2026-10-05|复发:0
|
||||||
|
|
||||||
|
### 24. 默认参数路径从没被执行过 —— 默认采样器用了库不接受的参数
|
||||||
|
|
||||||
|
- 现象:`random_walk` 的默认步长采样器写 `rng.integers(0, 2, size=size, dtype=float)`。numpy 的 `Generator.integers`
|
||||||
|
**不收浮点 dtype**(`TypeError: Unsupported dtype dtype('float64') for integers`),于是 `random_walk(100, 5)`
|
||||||
|
这种最普通的调用 100% 返回 `success=False`、`data=None`;连 docstring 里那条 `>>> r.data.shape (5, 101)` 都跑不通。
|
||||||
|
- 为什么会错:写的时候验的是**自定义采样器**那条分支,默认分支(`step_sampler is None`)一次都没执行过;
|
||||||
|
更巧的是 `try/except Exception` 把它压成了一句 message,看起来像"输入不合法"而不是"代码写错了"。
|
||||||
|
与第 21 条同源:**错误现场被 `except` 藏起来**,静态检查(pyright)也看不见。
|
||||||
|
- 以后怎么防:新函数写完,**第一个跑的就是"什么参数都不填的默认调用"**,`success` 必须是 `True`;
|
||||||
|
再把 docstring 的 Examples 真执行一遍(它们是最便宜的一组冒烟测试)。
|
||||||
|
凡是用到"库函数 + 一串可选参数"的地方,别凭记忆写,跑一次看它收不收。
|
||||||
|
- 首次:2026-10-05|复发:0
|
||||||
|
|
||||||
|
### 25. 两个维度恰好相等时,转置的形状检查会静默通过
|
||||||
|
|
||||||
|
- 现象:`random_walk` 把 `size=(n_paths, n_steps)` 交给 `step_sampler`,但没校验它返回的形状。
|
||||||
|
实测一个返回**转置**形状 `(n_steps, n_paths)` 的采样器:`n_paths=6, n_steps=4` 时报错(好事),
|
||||||
|
而 `n_paths=n_steps=5` 时 `success=True`、`data` 形状 `(5, 6)` 也"对"—— 里面的路径却是转置过来的,**静默错**。
|
||||||
|
- 为什么会错:形状检查依赖"两个维度不相等"这个前提才生效;`cumsum(axis=1)` + 赋值广播在方阵上完全合法。
|
||||||
|
验算时只挑了一组 `n_paths == n_steps` 的参数,正好落在唯一发现不了的那个点上。
|
||||||
|
与第 23 条同一个思路的不同侧面:**对称/相等会把错误藏起来**。
|
||||||
|
- 以后怎么防:验算参数**把两个维度取成不同值**(`n_paths=3, n_steps=100` 这种),别用正方形;
|
||||||
|
对外部传进来的批量数组显式校验 `shape == (n, d)`,别指望后面的运算替你把关。
|
||||||
|
- 首次:2026-10-05|复发:0
|
||||||
|
|
||||||
@@ -286,10 +286,13 @@ def monte_carlo_integral(
|
|||||||
>>> abs(r.metrics["mean"] - 1 / 3) < 0.01 # ∫₀¹ x² dx = 1/3
|
>>> abs(r.metrics["mean"] - 1 / 3) < 0.01 # ∫₀¹ x² dx = 1/3
|
||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(bounds, np.ndarray):
|
||||||
if not isinstance(bounds, (list, tuple)) or len(bounds) == 0:
|
if not isinstance(bounds, (list, tuple)) or len(bounds) == 0:
|
||||||
raise ValueError("bounds 必须是非空的列表或元组")
|
raise ValueError("bounds 必须是非空的列表或元组")
|
||||||
|
|
||||||
bounds_arr = np.asarray(bounds)
|
bounds_arr = np.asarray(bounds)
|
||||||
|
else:
|
||||||
|
bounds_arr = bounds.copy()
|
||||||
|
|
||||||
if bounds_arr.ndim != 2 or bounds_arr.shape[1] != 2:
|
if bounds_arr.ndim != 2 or bounds_arr.shape[1] != 2:
|
||||||
raise ValueError(f"bounds 必须形如 [(lo, hi), ...],当前形状为 {bounds_arr.shape},请检查是否漏了括号")
|
raise ValueError(f"bounds 必须形如 [(lo, hi), ...],当前形状为 {bounds_arr.shape},请检查是否漏了括号")
|
||||||
@@ -320,15 +323,12 @@ def monte_carlo_integral(
|
|||||||
random_state=random_state,
|
random_state=random_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.success:
|
|
||||||
result.name = "monte_carlo_integral"
|
result.name = "monte_carlo_integral"
|
||||||
result.params["bounds"] = bounds
|
result.params["bounds"] = bounds
|
||||||
result.params["dimension"] = d
|
result.params["dimension"] = d
|
||||||
|
if result.success:
|
||||||
result.message = f"蒙特卡洛多重积分成功:维度 {d},体积 {volume:.4f},模拟 {n} 次。"
|
result.message = f"蒙特卡洛多重积分成功:维度 {d},体积 {volume:.4f},模拟 {n} 次。"
|
||||||
else:
|
else:
|
||||||
result.name = "monte_carlo_integral"
|
|
||||||
result.params["bounds"] = bounds
|
|
||||||
result.params["dimension"] = d
|
|
||||||
result.message = f"蒙特卡洛多重积分失败:维度 {d},体积 {volume:.4f},模拟 {n} 次。{result.message}"
|
result.message = f"蒙特卡洛多重积分失败:维度 {d},体积 {volume:.4f},模拟 {n} 次。{result.message}"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -341,25 +341,91 @@ def random_walk(
|
|||||||
start: float = 0.0,
|
start: float = 0.0,
|
||||||
random_state: int | None = None,
|
random_state: int | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""随机游走轨迹。
|
"""随机游走轨迹:一次抽完整批增量,再 `cumsum` 成路径。
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
n_steps:
|
||||||
|
每条路径的步数,正整数。`data` 的列数是 `n_steps + 1`(多出的那一列是起点)。
|
||||||
|
n_paths:
|
||||||
|
路径条数,正整数,默认 1 条。
|
||||||
step_sampler:
|
step_sampler:
|
||||||
`f(rng, size) -> 增量`,默认为 ±1 等概率(对称随机游走)。
|
`f(rng, size) -> 一批增量`。批量大小由本函数以 `size=(n_paths, n_steps)` 传入,
|
||||||
|
必须返回形状 `(n_paths, n_steps)` 的数组(契约同 `monte_carlo` 的 `sampler`,只是这里是二维)。
|
||||||
|
默认 ±1 等概率(对称随机游走)。
|
||||||
|
start:
|
||||||
|
起点,写进每条路径的第 0 列。
|
||||||
|
random_state:
|
||||||
|
随机种子。`None` 时跟随 `core.set_seed` 的全局流,见 `core.random.get_rng`。
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
`Result`:`data` 形状 `(n_paths, n_steps + 1)`。
|
`Result`:`data` 形状 `(n_paths, n_steps + 1)`,第 0 列恒为 `start`,之后是逐列累加出来的位置;
|
||||||
|
`metrics={"n_steps", "n_paths"}`。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError:
|
||||||
|
`n_steps` / `n_paths` 不是正整数。
|
||||||
|
TypeError:
|
||||||
|
`start` 不是数值。
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
随机游走是"平方根扩散"的原型:位置的标准差随步数按 `√t` 增长,所以典型图形是 `±√t` 的喇叭口。
|
||||||
|
本函数只负责 `cumsum`;**建模差异全在 `step_sampler` 里** —— 换成 `rng.normal(0, 1, size)` 就是高斯游走,
|
||||||
|
换成带正漂移的增量就是有趋势的游走。
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
>>> import numpy as np
|
||||||
>>> r = random_walk(n_steps=100, n_paths=5, random_state=0)
|
>>> r = random_walk(n_steps=100, n_paths=5, random_state=0)
|
||||||
>>> r.data.shape
|
>>> r.data.shape
|
||||||
(5, 101)
|
(5, 101)
|
||||||
|
>>> bool(np.all(np.abs(np.diff(r.data, axis=1)) == 1)) # 默认步长恒为 1
|
||||||
|
True
|
||||||
"""
|
"""
|
||||||
# TODO(L1)
|
if isinstance(n_steps, bool) or not isinstance(n_steps, (int, np.integer)) or n_steps <= 0:
|
||||||
raise NotImplementedError
|
raise ValueError(f"n_steps 必须是正整数,你传入了 {n_steps}")
|
||||||
|
if isinstance(n_paths, bool) or not isinstance(n_paths, (int, np.integer)) or n_paths <= 0:
|
||||||
|
raise ValueError(f"n_paths 必须是正整数,你传入了 {n_paths}")
|
||||||
|
if not isinstance(start, (int, float, np.number)):
|
||||||
|
raise TypeError(f"start 必须是数值,你传入了 {type(start).__name__}")
|
||||||
|
|
||||||
|
rng = get_rng(random_state)
|
||||||
|
|
||||||
|
if step_sampler is None:
|
||||||
|
def _default_step_sampler(rng, size):
|
||||||
|
return rng.integers(0, 2, size=size) * 2 - 1
|
||||||
|
|
||||||
|
step_sampler = _default_step_sampler
|
||||||
|
|
||||||
|
try:
|
||||||
|
steps = np.asarray(step_sampler(rng, size=(n_paths, n_steps)), dtype=float)
|
||||||
|
|
||||||
|
cumsum = np.cumsum(steps, axis=1)
|
||||||
|
|
||||||
|
paths = np.zeros((n_paths, n_steps + 1), dtype=float)
|
||||||
|
paths[:, 0] = start
|
||||||
|
paths[:, 1:] = start + cumsum
|
||||||
|
|
||||||
|
return Result(
|
||||||
|
name="random_walk",
|
||||||
|
params={"n_steps": n_steps, "n_paths": n_paths, "start": start},
|
||||||
|
metrics={"n_steps": n_steps, "n_paths": n_paths},
|
||||||
|
data=paths,
|
||||||
|
success=True,
|
||||||
|
message=f"随机游走模拟成功:生成了 {n_paths} 条路径,每条路径 {n_steps} 步。"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return Result(
|
||||||
|
name="random_walk",
|
||||||
|
params={"n_steps": n_steps, "n_paths": n_paths, "start": start},
|
||||||
|
metrics={},
|
||||||
|
data=None,
|
||||||
|
success=False,
|
||||||
|
message=f"随机游走模拟失败:{type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def markov_simulate(
|
def markov_simulate(
|
||||||
|
|||||||
@@ -276,6 +276,66 @@ def test_monte_carlo_probability_rejects_bad_confidence():
|
|||||||
monte_carlo_probability(lambda z: z > 0, _normal_batch, n=100, confidence=bad)
|
monte_carlo_probability(lambda z: z > 0, _normal_batch, n=100, confidence=bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_walk_shape_and_start():
|
||||||
|
"""`data` 形状 (n_paths, n_steps + 1),第 0 列是起点。"""
|
||||||
|
from mcm_toolkit.solve import random_walk
|
||||||
|
|
||||||
|
r = random_walk(n_steps=100, n_paths=5, start=2.5, random_state=0)
|
||||||
|
assert r.success
|
||||||
|
assert r.data.shape == (5, 101)
|
||||||
|
assert np.all(r.data[:, 0] == 2.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_walk_default_steps_are_plus_minus_one():
|
||||||
|
"""默认是对称随机游走:每一步都恰好是 ±1。"""
|
||||||
|
from mcm_toolkit.solve import random_walk
|
||||||
|
|
||||||
|
r = random_walk(n_steps=200, n_paths=3, random_state=0)
|
||||||
|
assert set(np.unique(np.diff(r.data, axis=1))) == {-1.0, 1.0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_walk_is_reproducible():
|
||||||
|
from mcm_toolkit.solve import random_walk
|
||||||
|
|
||||||
|
a = random_walk(n_steps=50, n_paths=4, random_state=7)
|
||||||
|
b = random_walk(n_steps=50, n_paths=4, random_state=7)
|
||||||
|
assert np.array_equal(a.data, b.data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_walk_accepts_custom_step_sampler():
|
||||||
|
"""换成高斯增量就是带漂移的高斯游走 —— 建模差异全在 step_sampler 里。"""
|
||||||
|
from mcm_toolkit.solve import random_walk
|
||||||
|
|
||||||
|
r = random_walk(
|
||||||
|
n_steps=100,
|
||||||
|
n_paths=3,
|
||||||
|
step_sampler=lambda rng, size: rng.normal(0.5, 1.0, size),
|
||||||
|
random_state=0,
|
||||||
|
)
|
||||||
|
assert r.success
|
||||||
|
assert r.data.shape == (3, 101)
|
||||||
|
assert abs(r.data[:, -1].mean() - 50.0) < 20.0 # 100 步 × 漂移 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_walk_rejects_bad_arguments():
|
||||||
|
from mcm_toolkit.solve import random_walk
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
random_walk(n_steps=0)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
random_walk(n_steps=10, n_paths=0)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
random_walk(n_steps=10, start="a") # type: ignore[arg-type] # 就是要传错
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_walk_docstring_example_runs():
|
||||||
|
from mcm_toolkit.solve import random_walk
|
||||||
|
|
||||||
|
r = random_walk(n_steps=100, n_paths=5, random_state=0)
|
||||||
|
assert r.data.shape == (5, 101)
|
||||||
|
assert np.all(np.abs(np.diff(r.data, axis=1)) == 1)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
def test_markov_steady_state_two_state_chain():
|
def test_markov_steady_state_two_state_chain():
|
||||||
"""P=[[0.9,0.1],[0.2,0.8]] 的平稳分布是 [2/3, 1/3]。"""
|
"""P=[[0.9,0.1],[0.2,0.8]] 的平稳分布是 [2/3, 1/3]。"""
|
||||||
|
|||||||
Reference in new issue
Block a user