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

+34 -12
View File
@@ -354,27 +354,29 @@ def random_walk(
必须返回形状 `(n_paths, n_steps)` 的数组(契约同 `monte_carlo` 的 `sampler`,只是这里是二维)。
默认 ±1 等概率(对称随机游走)。
start:
起点,写进每条路径的第 0 列。
起点,写进每条路径的第 0 列(**入口不校验类型**,传非数值会落到失败分支)。
random_state:
随机种子。`None` 时跟随 `core.set_seed` 的全局流,见 `core.random.get_rng`。
Returns
-------
`Result`:`data` 形状 `(n_paths, n_steps + 1)`,第 0 列恒为 `start`,之后是逐列累加出来的位置;
`metrics={"n_steps", "n_paths"}`。
`metrics={"mean_terminal", "std_terminal"}`(终点的均值与标准差,`std` 用 `ddof=0`)。
Raises
------
TypeError:
`n_steps` / `n_paths` 不是整数。
ValueError:
`n_steps` / `n_paths` 不是正整数。
TypeError:
`start` 不是数值。
Notes
-----
随机游走是"平方根扩散"的原型:位置的标准差随步数按 `√t` 增长,所以典型图形是 `±√t` 的喇叭口。
本函数只负责 `cumsum`;**建模差异全在 `step_sampler` 里** —— 换成 `rng.normal(0, 1, size)` 就是高斯游走,
换成带正漂移的增量就是有趋势的游走。
`step_sampler` 返回的形状不等于 `(n_paths, n_steps)`、或增量里出现 NaN,都判 `success=False`
(NaN 口径与 `monte_carlo` 一致);其余异常同样统一走失败分支。
Examples
--------
@@ -385,23 +387,37 @@ def random_walk(
>>> bool(np.all(np.abs(np.diff(r.data, axis=1)) == 1)) # 默认步长恒为 1
True
"""
if isinstance(n_steps, bool) or not isinstance(n_steps, (int, np.integer)) or n_steps <= 0:
if isinstance(n_steps, bool) or not isinstance(n_steps, (int, np.integer)):
raise TypeError(f"n_steps 必须是整数,你传入了 {type(n_steps).__name__}")
if n_steps <= 0:
raise ValueError(f"n_steps 必须是正整数,你传入了 {n_steps}")
if isinstance(n_paths, bool) or not isinstance(n_paths, (int, np.integer)) or n_paths <= 0:
if isinstance(n_paths, bool) or not isinstance(n_paths, (int, np.integer)):
raise TypeError(f"n_paths 必须是整数,你传入了 {type(n_paths).__name__}")
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:
def _default_step_sampler(rng, size):
return rng.integers(0, 2, size=size) * 2 - 1
return rng.choice([-1.0, 1.0], size=size)
step_sampler = _default_step_sampler
call_params = {
"n_steps": n_steps,
"n_paths": n_paths,
"start": start,
"random_state": random_state,
}
try:
steps = np.asarray(step_sampler(rng, size=(n_paths, n_steps)), dtype=float)
if steps.shape != (n_paths, n_steps):
raise ValueError(f"step_sampler 返回形状必须为 ({n_paths}, {n_steps}),实际得到了 {steps.shape}")
if np.isnan(steps).any():
raise ValueError("step_sampler 生成的增量数据中包含 NaN,请检查采样逻辑")
cumsum = np.cumsum(steps, axis=1)
@@ -409,10 +425,16 @@ def random_walk(
paths[:, 0] = start
paths[:, 1:] = start + cumsum
terminal_positions = paths[:, -1] # 取最后一列,得到 n_paths 个终点位置
metrics = {
"mean_terminal": float(np.mean(terminal_positions)),
"std_terminal": float(np.std(terminal_positions))
}
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},
params=call_params,
metrics=metrics,
data=paths,
success=True,
message=f"随机游走模拟成功:生成了 {n_paths} 条路径,每条路径 {n_steps} 步。"
@@ -420,7 +442,7 @@ def random_walk(
except Exception as e:
return Result(
name="random_walk",
params={"n_steps": n_steps, "n_paths": n_paths, "start": start},
params=call_params,
metrics={},
data=None,
success=False,