feat(solve): 审查 markov_steady_state:工程侧收口 + 8 条测试

用户实现的 markov_steady_state 经探针实测算法本体正确(3 状态与特征分解逐位一致、
200x200 一致到 1.9e-12 且只用 6 次迭代 / 0.10s),本次只动工程侧,未改任何算法行:

- ruff 5 条 W293 全清;mm1_metrics 前 4 个空行 -> 2;失败文案改成全角冒号
- docstring 补 max_iter / tol 两个参数与 Raises,写清 tol 是相邻差口径
  (实测慢混合链 tol=1e-10 -> 实际误差 4.6e-9,被 1/(1-|lambda2|) 放大约 50 倍)
- Notes 修正"周期链会不收敛"的说法:可约链的实情是平稳分布不唯一,
  迭代法给的是离初值最近的那个(均匀初值对双随机矩阵本身就是不动点)
- tests/test_solve.py:摘掉 1 条 skip + 新增 7 条(该函数共 8 条实跑)
- STATUS.md 新增决策 57(6 条待拍板口径)与第六轮「下一步」;CHANGELOG;易错点 28

校验:pytest 196 passed / 30 skipped(226 收集);ruff 0;pyright 0
This commit is contained in:
guzhujushi committed 2026-10-09 23:20:13 +08:00
1 parent dd9bd638b8
commit 0955890010
5 files changed
+263 -27

No files matched your search

+75 -7
View File
@@ -590,23 +590,42 @@ def markov_simulate(
)
def markov_steady_state(transition: Any, max_iter: int = 10000, tol: float = 1e-10) -> Any:
"""平稳分布。
def markov_steady_state(transition: Any, max_iter: int | np.integer = 10000, tol: float = 1e-10) -> Any:
"""迭代法求马尔可夫链的平稳分布(`v @ P` 反复推进,直到相邻两次几乎不变)。
Parameters
----------
transition:
行随机矩阵(每行和为 1)。
行随机矩阵(每行和为 1)的二维方阵。
max_iter:
迭代上限,正整数,默认 10000。
tol:
收敛阈值,正数,默认 1e-10。判据是**相邻两次迭代的最大分量差**
`max|v_k+1 - v_k| < tol`,不是"到不动点的距离",见 Notes。
Returns
-------
`Result`:`data` 是平稳分布向量,`metrics={"converged", "iterations"}`。
`Result`:`data` 是一维平稳分布向量(和为 1);
`metrics={"converged", "iterations"}`(是否在 `max_iter` 内向容、实际迭代次数)。
Raises
------
ValueError:
`transition` 不是二维方阵、或某一行和不等于 1.0;
`max_iter` 不是正整数;`tol` 不是正数。
Notes
-----
用迭代法而不是特征分解:迭代法能顺便告诉你"收敛快不快",
而收敛速度本身就是论文里值得写一句的性质。
**不可约 + 非周期**才有唯一平稳分布,周期链会不收敛,要检查。
初值是**均匀分布** `1/s`;**不可约 + 非周期**才有唯一平稳分布。
可约链(如单位阵 `eye(2)`)的平稳分布不唯一,迭代法给出的是"离初值最近的那个"
—— 均匀初值对双随机矩阵本身就是不动点,所以 `converged=True, iterations=1` 只说明"初值恰是不动点",
不代表这条链有唯一平稳分布;周期链从一般初值出发会一直振荡、`converged=False`。
`tol` 是**相邻差**口径,实际误差约被 `1 / (1 - |λ2|)` 放大:实测双状态慢混合链
`[[0.999, 0.001], [0.02, 0.98]]`(`|λ2| = 0.979`)用默认 `tol=1e-10` 停在第 867 次,
与特征向量相差 `4.6e-9`(约 50 倍)。慢混合链要更准,把 `tol` 压小即可。
Examples
--------
@@ -616,8 +635,57 @@ def markov_steady_state(transition: Any, max_iter: int = 10000, tol: float = 1e-
>>> bool(np.allclose(r.data, [2 / 3, 1 / 3], atol=1e-6))
True
"""
# TODO(L1)
raise NotImplementedError
arr, _ = as_array(transition)
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
if not np.allclose(arr.sum(axis=1), 1.0):
raise ValueError("transition 每一行的和必须是 1.0")
s = arr.shape[0]
v = np.full(s, 1.0 / s)
iterations = 0
converged = False
if isinstance(max_iter, bool) or not isinstance(max_iter, (int, np.integer)) or max_iter <= 0:
raise ValueError(f"max_iter 必须是正整数,你传入了 {max_iter}")
if not isinstance(tol, (int, float)) or tol <= 0:
raise ValueError(f"tol 必须是正数,你传入了 {tol}")
try:
for _ in range(max_iter):
v_next = v @ arr
error = np.max(np.abs(v_next - v))
v = v_next
iterations += 1
if error < tol:
converged = True
break
metrics = {
"converged": converged,
"iterations": iterations
}
return Result(
name="markov_steady_state",
params={"max_iter": max_iter, "tol": tol},
metrics=metrics,
data=v,
success=True,
message=f"平稳分布求解{'成功' if converged else '未完全收敛'},迭代 {iterations} 次。"
)
except Exception as e:
return Result(
name="markov_steady_state",
params={"max_iter": max_iter, "tol": tol},
metrics={},
data=None,
success=False,
message=f"平稳分布求解失败:{type(e).__name__} - {str(e)}"
)
def mm1_metrics(arrival_rate: float, service_rate: float) -> Any: