fix(solve): random_walk 失败路径的 UnboundLocalError;第二轮口径收口

口径由用户第二轮改定;Codex 修一处真 bug + 工程侧。

- 真 bug:call_params 写在 try 里、却在 except 被引用,5 种失败输入 5 个全炸 UnboundLocalError
  (本该返回 success=False 的 Result,变成把异常抛给调用方)。按原意前移到 try 之前,未改口径;
  失败时照样带上 n_steps / n_paths / start / random_state
- 用户口径:n_steps/n_paths 拆 TypeError/ValueError;加 steps 形状与 NaN 两条校验;
  默认步长改 rng.choice([-1.0, 1.0]);params 补 random_state;metrics 改 mean_terminal/std_terminal
- 工程:docstring 同步(Returns 的 metrics、Raises 的分工、去掉已删的 start 校验);+3 条测试
  (失败也必须返回带 params 的 Result、NaN 判失败、终点 std ~= sqrt(t)),solve 段 41 条
- 验证:208 收集 / 177 passed / 31 skipped / 0 xfail,ruff 0,pyright 0
- docs:STATUS 决策 54 + 3 条新口径;CHANGELOG;易错点 26
This commit is contained in:
guzhujushi committed 2026-10-05 17:24:23 +08:00
1 parent 8ec08ea9ce
commit 6fca82e3fc
5 files changed
+160 -37

No files matched your search

+47 -2
View File
@@ -318,14 +318,59 @@ def test_random_walk_accepts_custom_step_sampler():
def test_random_walk_rejects_bad_arguments():
"""§4.7 的分工:类型不对抛 TypeError,取值不对(<= 0)抛 ValueError。"""
from mcm_toolkit.solve import random_walk
with pytest.raises(TypeError):
random_walk(n_steps=2.5) # type: ignore[arg-type] # 就是要传错
with pytest.raises(TypeError):
random_walk(n_steps=True)
with pytest.raises(TypeError):
random_walk(n_steps=10, n_paths=1.5) # type: ignore[arg-type] # 就是要传错
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] # 就是要传错
r = random_walk(n_steps=10, start="a") # type: ignore[arg-type] # 就是要传错
assert r.success is False # 口径待定:入口抛 TypeError 也可以,见 STATUS"下一步"
def test_random_walk_failure_returns_result_not_exception():
"""失败也要返回带 params 的 Result,不能把异常抛给调用方 —— 包装层引用未赋值变量那次就靠它拦住。"""
from mcm_toolkit.solve import random_walk
def transposed(rng, size):
return rng.normal(0, 1, (size[1], size[0]))
r = random_walk(n_steps=4, n_paths=6, step_sampler=transposed, random_state=0)
assert r.success is False
assert "形状" in r.message
assert r.params["n_paths"] == 6 # 失败也要留下追溯信息
assert r.data is None
def test_random_walk_rejects_nan_steps():
"""增量含 NaN 判失败 —— 与 monte_carlo 的 NaN 口径(决策 48)一致。"""
from mcm_toolkit.solve import random_walk
r = random_walk(
n_steps=4,
n_paths=2,
step_sampler=lambda rng, size: np.full(size, np.nan),
random_state=0,
)
assert r.success is False
assert "NaN" in r.message
def test_random_walk_metrics_are_terminal_positions():
"""终点位置的标准差按 √t 扩散(t = n_steps)—— 随机游走的核心性质。"""
from mcm_toolkit.solve import random_walk
n_steps = 100
r = random_walk(n_steps=n_steps, n_paths=2000, start=5.0, random_state=0)
assert abs(r.metrics["mean_terminal"] - 5.0) < 5.0 # 对称游走,期望还在起点附近
assert abs(r.metrics["std_terminal"] - np.sqrt(n_steps)) < 5.0 # √t 扩散
def test_random_walk_docstring_example_runs():