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:
guzhujushi committed 2026-10-05 17:04:52 +08:00
1 parent dcda080be4
commit 8ec08ea9ce
5 files changed
+254 -32

No files matched your search

+23
View File
@@ -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
+59 -17
View File
@@ -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 个口径要你确认
+32 -1
View File
@@ -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
+80 -14
View File
@@ -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, (list, tuple)) or len(bounds) == 0: if not isinstance(bounds, np.ndarray):
raise ValueError("bounds 必须是非空的列表或元组") if not isinstance(bounds, (list, tuple)) or len(bounds) == 0:
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,
) )
result.name = "monte_carlo_integral"
result.params["bounds"] = bounds
result.params["dimension"] = d
if result.success: if result.success:
result.name = "monte_carlo_integral"
result.params["bounds"] = bounds
result.params["dimension"] = d
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(
+60
View File
@@ -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]。"""