fix(solve): random_walk 的 start 校验恢复后收尾(文案/空格/测试)
两条口径由用户第二轮改定,Codex 顺手补工程侧。 - 用户:恢复 start 的入口类型校验;n_steps/n_paths 注解放宽为 int | np.integer - Codex:start 报错文案"必须是整数" -> "必须是数值"(这条校验本来就收 float,原文案自相矛盾), 类型名统一 type(x).__name__;n_paths 默认值补空格;docstring 的 start 说明与 Raises 同步 - tests:start="a" 恢复断言 TypeError;补回 np.int64 的 n_steps/n_paths;solve 段 42 条 - 验证:209 收集 / 178 passed / 31 skipped / 0 xfail,ruff 0,pyright 0 - docs:STATUS 决策 55(方阵歧义两条路)+ 验证计数;CHANGELOG
This commit is contained in:
1 parent
6fca82e3fc
commit
cd6f613568
4 files changed
+54
-21
No files matched your search
@@ -484,6 +484,17 @@
|
||||
- `STATUS.md`:新增**决策 54** 与 `random_walk` 的 3 条新口径;4 条待办标记办结;验证计数更新为 177 / 31。
|
||||
- `docs/dev/personal-pitfalls.md`:新增第 **26** 条(`except` 用到的变量在 `try` 里才赋值 —— 每条失败路径都炸)。
|
||||
|
||||
### Changed
|
||||
- `random_walk` 收口 2 条口径(用户第二轮改动):恢复 `start` 的入口类型校验(`isinstance(start, (int, float, np.number))`);
|
||||
签名注解放宽为 `n_steps: int | np.integer` / `n_paths: int | np.integer`(与 §4.7 及 `monte_carlo_integral` 的 `n` 对齐)。
|
||||
- `random_walk` 的 `start` 报错文案(Codex 顺手):原文"start必须是整数"与校验本身矛盾 —— 这条校验**收 float**,
|
||||
改成"start 必须是数值";类型名统一 `{type(start).__name__}`(全模块口径);`n_paths: int | np.integer= 1` 补空格。
|
||||
- `random_walk` 的 docstring 同步:`start` 的说明去掉"入口不校验类型",`Raises` 补回 `start` 的 `TypeError`。
|
||||
- `tests/test_solve.py`:`start="a"` 恢复断言 `TypeError`;补回 `np.int64` 的 `n_steps` / `n_paths`;
|
||||
`solve` 段 41 → **42** 条,全量 **209 收集 / 178 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
|
||||
- `STATUS.md`:新增**决策 55**(`n_paths == n_steps` 的"方阵歧义"留作已知边界,A 接受 / B 换契约两条路);
|
||||
3 条口径办结 2 条,验证计数更新为 178 / 31。
|
||||
|
||||
## [0.0.1] - 2026-10-01
|
||||
|
||||
### Added
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
**5 种失败输入 5 个全炸** `UnboundLocalError`(本该返回 `success=False` 的 `Result`,变成把异常抛给调用方)。
|
||||
按原意把 `call_params` 移到 `try` 之前(不动任何口径),实测失败路径全部恢复正常;新增一条"必然失败"的回归测试(易错点 26)
|
||||
- **测试:本轮再 +3 → 共 9 条**(新增"失败也必须返回 Result 且带 params"、NaN 判失败、`√t` 扩散);
|
||||
`start="a"` 那条改为断言 `success is False`(因为用户删掉了 `start` 的入口类型校验,口径待定)
|
||||
`start="a"` 那条恢复成 `pytest.raises(TypeError)`(用户恢复了入口校验,见下)
|
||||
- ⏳ **留给用户 3 条**(见"下一步"):`start` 的类型校验要不要恢复;`n_steps` / `n_paths` 签名注解仍是 `int` 却收 `np.int64`;
|
||||
`n_paths == n_steps` 时转置采样器仍无法被形状校验发现
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
|
||||
| 检查 | venv | conda `mcm-toolkit` |
|
||||
|---|---|---|
|
||||
| `pytest` | 见下(单列) | **177 passed, 31 skipped(0 xfail)** |
|
||||
| `pytest` | 见下(单列) | **178 passed, 31 skipped(0 xfail)** |
|
||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
||||
| `scripts/doctor.ps1` | 跑通 | — |
|
||||
@@ -332,8 +332,8 @@
|
||||
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
||||
`docs/04-environment.md` 坑 7)
|
||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||
- **208 条用例:177 passed / 31 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||
`test_core.py` 23、`test_solve.py` 41(本轮 +3:失败路径 / NaN / `√t` 扩散)、`test_viz.py` 8、`test_decision.py` 7、
|
||||
- **209 条用例:178 passed / 31 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||
`test_core.py` 23、`test_solve.py` 42(本轮 +1:`np.int64` 的 `n_steps` / `n_paths`)、`test_viz.py` 8、`test_decision.py` 7、
|
||||
`test_stats.py` 53、`test_models.py` 5
|
||||
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
||||
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
||||
@@ -489,6 +489,13 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
||||
② 签名 `n_steps: int` / `n_paths: int` 与运行期收 `np.int64` 不一致(§4.7;`monte_carlo_integral` 的 `n` 已放宽);
|
||||
③ `n_paths == n_steps` 时,返回转置形状的采样器仍会静默通过(形状校验天生发现不了,见易错点 25)。
|
||||
|
||||
| 55 | `step_sampler` 的"方阵歧义"留作**已知边界**:`n_paths == n_steps` 时,返回转置形状的采样器无法被形状校验发现 |
|
||||
形状校验(决策 54)对"方阵"天生失效 —— 转置 `(n_steps, n_paths)` 正好等于目标 `(n_paths, n_steps)`。
|
||||
实测确认:`n_paths != n_steps` 能拦住,相等时静默通过(易错点 25)。两条路(**用户拍板**):
|
||||
**A 接受**:docstring 写明"方阵时转置的采样器察觉不到";**B 换契约**:`size` 传**整数** `n_paths * n_steps`、要求返回**一维**数组,
|
||||
内部 `reshape(n_paths, n_steps)` —— 二维返回一律判失败(探针实测:转置二维与方阵二维都被拦下),
|
||||
而且与 `monte_carlo` 的 `sampler` 契约(决策 48 的 `size=n`)统一,"monte_carlo 风格的采样器可直接复用"。
|
||||
|
||||
## 下一步
|
||||
|
||||
### ⭐ 当前进度与待办(2026-10-05 第四轮)
|
||||
@@ -498,7 +505,7 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
||||
另补 docstring、+3 条测试(共 9 条)。
|
||||
|
||||
**接下来三件事**
|
||||
1. `random_walk` 的 4 条待办已办结,新增 **3 条**(下面一节)——都是你的活。
|
||||
1. `random_walk` 的 4 条待办已办结,第二轮新增的 3 条又办结 2 条(下面一节)——只剩 1 条。
|
||||
2. `ci_low` / `ci_high` 的语义确认一句(也是你的活),确认后 Codex 补 `docs/algorithms/monte_carlo.md` 卡片。
|
||||
3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
|
||||
|
||||
@@ -514,17 +521,19 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
||||
- ~~④ `params` 少 `random_state` / `metrics` 抄了一遍~~ → 已补 `random_state`;`metrics` 换成 `mean_terminal` / `std_terminal`
|
||||
(终点位置的均值与标准差,`ddof=0`)—— 这才是"游走"该看的指标,测试里还顺手验了 `std ≈ √t`。
|
||||
|
||||
### ⏳ random_walk 还有 3 条口径(新增,你的活)
|
||||
### ✅ random_walk 新增的 3 条口径(2026-10-05 办结 2 条、剩 1 条)
|
||||
|
||||
1. **`start` 的入口类型校验被删了**:`start="a"` 现在走 `success=False`("could not convert string to float"),
|
||||
不再是入口的 `TypeError`。是有意改成"交给失败分支"(那 docstring 里我已如实写明"入口不校验类型"),还是顺手删的?
|
||||
要恢复的话,原来那行是 `if not isinstance(start, (int, float, np.number)): raise TypeError(...)`。
|
||||
2. **签名注解比行为窄**:`n_steps: int` / `n_paths: int`,但入口校验明明收 `np.integer`(实测 `np.int64` 能过)。
|
||||
按 §4.7 应与 `monte_carlo_integral` 的 `n` 一样写成 `int | np.integer` —— 现在 pyright 会对老老实实按注解传参的
|
||||
调用方报 `reportArgumentType`(我写测试时就被拦了一次,所以那条测试暂时撤了)。
|
||||
3. **`n_paths == n_steps` 时形状校验天生失效**:转置的采样器返回 `(n_steps, n_paths)`,正好等于 `(n_paths, n_steps)`,
|
||||
校验放行、结果静默转置(易错点 25)。要么接受这个边界,要么让 `step_sampler` 契约改成"返回 `(n_paths, n_steps)` 之外的形状即失败"
|
||||
之外的更强约束(比如显式要求 `size` 是 tuple 并校验每个维度)—— 你定。
|
||||
- ~~① `start` 的入口类型校验被删~~ → 已恢复(`isinstance(start, (int, float, np.number))`)。
|
||||
我顺手把文案里的"必须是整数"改成"必须是数值"(这条校验本来就收 float,原文案自相矛盾),
|
||||
并把 `{type(start)}` 统一成 `{type(start).__name__}`(全模块的口径)。测试改回 `pytest.raises(TypeError)`。
|
||||
- ~~② 签名注解比行为窄~~ → 已改成 `n_steps: int | np.integer` / `n_paths: int | np.integer`,
|
||||
与 §4.7 及 `monte_carlo_integral` 的 `n` 对齐;`np.int64` 的测试也补回来了。
|
||||
|
||||
**还剩 1 条(你的活)**
|
||||
1. **`n_paths == n_steps` 时转置形状仍会静默通过**:形状校验对"方阵"天生失效(转置正好等于目标形状)。
|
||||
两个方向,任选(见"决策 55"):**A** 接受这个边界,在 docstring 写明"返回转置形状的采样器在方阵时无法察觉";
|
||||
**B** 把 `step_sampler` 的契约改成"`size` 是**整数** `n_paths * n_steps`、必须返回**一维**数组",
|
||||
内部再 `reshape(n_paths, n_steps)` —— 二维返回一律判失败,转置问题从根上消失,且与 `monte_carlo` 的 `sampler` 契约统一。
|
||||
|
||||
### ✅ monte_carlo_probability 的 6 条待办(2026-10-05 全部办结)
|
||||
|
||||
|
||||
@@ -335,8 +335,8 @@ def monte_carlo_integral(
|
||||
|
||||
|
||||
def random_walk(
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
n_steps: int | np.integer,
|
||||
n_paths: int | np.integer = 1,
|
||||
step_sampler: Callable[..., Any] | None = None,
|
||||
start: float = 0.0,
|
||||
random_state: int | None = None,
|
||||
@@ -354,7 +354,7 @@ def random_walk(
|
||||
必须返回形状 `(n_paths, n_steps)` 的数组(契约同 `monte_carlo` 的 `sampler`,只是这里是二维)。
|
||||
默认 ±1 等概率(对称随机游走)。
|
||||
start:
|
||||
起点,写进每条路径的第 0 列(**入口不校验类型**,传非数值会落到失败分支)。
|
||||
起点,写进每条路径的第 0 列。
|
||||
random_state:
|
||||
随机种子。`None` 时跟随 `core.set_seed` 的全局流,见 `core.random.get_rng`。
|
||||
|
||||
@@ -367,6 +367,7 @@ def random_walk(
|
||||
------
|
||||
TypeError:
|
||||
`n_steps` / `n_paths` 不是整数。
|
||||
`start` 不是数值。
|
||||
ValueError:
|
||||
`n_steps` / `n_paths` 不是正整数。
|
||||
|
||||
@@ -397,6 +398,9 @@ def random_walk(
|
||||
if 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:
|
||||
@@ -425,7 +429,7 @@ def random_walk(
|
||||
paths[:, 0] = start
|
||||
paths[:, 1:] = start + cumsum
|
||||
|
||||
terminal_positions = paths[:, -1] # 取最后一列,得到 n_paths 个终点位置
|
||||
terminal_positions = paths[:, -1]
|
||||
metrics = {
|
||||
"mean_terminal": float(np.mean(terminal_positions)),
|
||||
"std_terminal": float(np.std(terminal_positions))
|
||||
|
||||
+11
-2
@@ -331,8 +331,8 @@ def test_random_walk_rejects_bad_arguments():
|
||||
random_walk(n_steps=0)
|
||||
with pytest.raises(ValueError):
|
||||
random_walk(n_steps=10, n_paths=0)
|
||||
r = random_walk(n_steps=10, start="a") # type: ignore[arg-type] # 就是要传错
|
||||
assert r.success is False # 口径待定:入口抛 TypeError 也可以,见 STATUS"下一步"
|
||||
with pytest.raises(TypeError):
|
||||
random_walk(n_steps=10, start="a") # type: ignore[arg-type] # 就是要传错
|
||||
|
||||
|
||||
def test_random_walk_failure_returns_result_not_exception():
|
||||
@@ -363,6 +363,15 @@ def test_random_walk_rejects_nan_steps():
|
||||
assert "NaN" in r.message
|
||||
|
||||
|
||||
def test_random_walk_accepts_numpy_integer():
|
||||
"""§4.7:`np.int64` 不是 `int` 的子类,注解与运行期都要收(易错点 1)。"""
|
||||
from mcm_toolkit.solve import random_walk
|
||||
|
||||
r = random_walk(n_steps=np.int64(20), n_paths=np.int64(3), random_state=0)
|
||||
assert r.success
|
||||
assert r.data.shape == (3, 21)
|
||||
|
||||
|
||||
def test_random_walk_metrics_are_terminal_positions():
|
||||
"""终点位置的标准差按 √t 扩散(t = n_steps)—— 随机游走的核心性质。"""
|
||||
from mcm_toolkit.solve import random_walk
|
||||
|
||||
Reference in new issue
Block a user