fix(solve): 审查 mm1_metrics + 第二批入口改动(工程侧收口 + 6 条测试)

用户本轮改了 markov_simulate / markov_steady_state 的入口校验,并写完 mm1_metrics,
三处算法本体都正确(mm1 的 5 个公式经探针与闭式解逐位对拍通过,含 W = Wq + 1/mu)。
Codex 只动工程侧:

Fixed
- mm1_metrics 的稳定性分界 "<" 应为 "<="(docstring 与既有测试的口径都是 lambda >= mu 不稳定):
  lambda == mu 原先掉进 try、rho == 1.0 触发 ZeroDivisionError,被 except 吞成 success=False。
  两个分支的可观测输出完全相同(success=False / metrics={} / data=None),只有 message 不同,
  故按决策 31 直接修并加回归测试,未改动任何口径。
- 三处报错文案的参数名抄错:markov_steady_state 的 max_iter <= 0 写成 "tol 必须是正数",
  mm1_metrics 的 service_rate 两条报错都写成 "tol";半角逗号统一为全角。

Changed
- mm1_metrics 的 docstring 补 Raises 与 data 说明;call_params = {...} 补空格;
  ruff 6 条(W293 x5 / F541 f-string 无占位符 / W292 文件末尾无换行)。
- markov_steady_state 的 try 前 3 个空行 -> 1;"异常: " -> "异常:"。
- tests/test_solve.py:摘掉 2 条 mm1_metrics 的 skip,新增 4 条(lambda == mu 的回归、
  非负校验回归、max_iter=3 未收敛判 success=False)。
- STATUS.md 决策 56 / 57 更新办结项 + 新增决策 58(mm1 的 4 条待拍板);CHANGELOG;易错点 29。

校验:pytest 202 passed / 28 skipped(230 收集);ruff 0;pyright 0
This commit is contained in:
guzhujushi committed 2026-10-09 23:52:53 +08:00
1 parent 0955890010
commit 953e988983
5 files changed
+311 -72

No files matched your search

+87 -14
View File
@@ -521,6 +521,8 @@ def markov_simulate(
arr, _ = as_array(transition)
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
if np.any(arr < 0):
raise ValueError("transition 矩阵的元素必须是非负的(概率不能为负)")
if not np.allclose(arr.sum(axis=1), 1.0):
raise ValueError("transition 每一行的和必须是 1.0")
s = arr.shape[0]
@@ -638,19 +640,30 @@ def markov_steady_state(transition: Any, max_iter: int | np.integer = 10000, tol
arr, _ = as_array(transition)
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
if np.any(arr < 0):
raise ValueError("transition 矩阵的元素必须是非负的(概率不能为负)")
if not np.allclose(arr.sum(axis=1), 1.0):
raise ValueError("transition 每一行的和必须是 1.0")
if isinstance(max_iter, bool) or not isinstance(max_iter, (int, np.integer)):
raise ValueError(f"max_iter 必须是正整数,你传入了 {type(max_iter).__name__}")
if max_iter <= 0:
raise ValueError(f"max_iter 必须是正整数,你传入了 {max_iter}")
if not isinstance(tol, (int, float)) or isinstance(tol, bool):
raise TypeError(f"tol 必须是浮点数,你传入了 {type(tol).__name__}")
if tol <= 0:
raise ValueError(f"tol 必须是正数,你传入了 {tol}")
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}")
call_params = {
"transition": arr.tolist(),
"max_iter": max_iter,
"tol": tol,
}
try:
for _ in range(max_iter):
@@ -670,21 +683,20 @@ def markov_steady_state(transition: Any, max_iter: int | np.integer = 10000, tol
return Result(
name="markov_steady_state",
params={"max_iter": max_iter, "tol": tol},
params=call_params,
metrics=metrics,
data=v,
success=True,
message=f"平稳分布求解{'成功' if converged else '未完全收敛'},迭代 {iterations} 次。"
success=converged,
message=f"平稳分布求解{'成功' if converged else '失败:未在最大迭代次数内收敛'},迭代 {iterations} 次。"
)
except Exception as e:
return Result(
name="markov_steady_state",
params={"max_iter": max_iter, "tol": tol},
params=call_params,
metrics={},
data=None,
success=False,
message=f"平稳分布求解失败:{type(e).__name__} - {str(e)}"
message=f"平稳分布求解异常:{type(e).__name__} - {str(e)}"
)
@@ -700,7 +712,15 @@ def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
Returns
-------
`Result`:`metrics={"rho", "L", "Lq", "W", "Wq"}`
(系统利用率、系统内平均顾客数、队列长度、平均逗留时间、平均等待时间)。
(系统利用率、系统内平均顾客数、队列长度、平均逗留时间、平均等待时间);
`data` 与 `metrics` 同值(闭式解没有样本数组)。
Raises
------
TypeError:
`arrival_rate` / `service_rate` 不是数值(`bool` 也判非法)。
ValueError:
`arrival_rate` / `service_rate` 不是正数。
Notes
-----
@@ -714,5 +734,58 @@ def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
>>> round(r.metrics["rho"], 4)
0.6667
"""
# TODO(L1)
raise NotImplementedError
if not isinstance(arrival_rate, (int, float)) or isinstance(arrival_rate, bool):
raise TypeError(f"arrival_rate 必须是浮点数,你传入了 {type(arrival_rate).__name__}")
if arrival_rate <= 0:
raise ValueError(f"arrival_rate 必须是正数,你传入了 {arrival_rate}")
if not isinstance(service_rate, (int, float)) or isinstance(service_rate, bool):
raise TypeError(f"service_rate 必须是浮点数,你传入了 {type(service_rate).__name__}")
if service_rate <= 0:
raise ValueError(f"service_rate 必须是正数,你传入了 {service_rate}")
call_params = {"arrival_rate": arrival_rate, "service_rate": service_rate}
if service_rate <= arrival_rate:
return Result(
name="mm1_metrics",
params=call_params,
metrics={},
data=None,
success=False,
message="系统不稳定:到达率 λ 大于等于服务率 μ,队列长度无界,无法计算闭式解。"
)
try:
rho = arrival_rate / service_rate
L = rho / (1.0 - rho) # 系统内平均顾客数
Lq = rho ** 2 / (1.0 - rho) # 队列中平均等待顾客数
W = 1.0 / (service_rate - arrival_rate) # 平均逗留时间
Wq = W - 1.0 / service_rate # 平均等待时间
# 4. 封装返回
metrics = {
"rho": float(rho),
"L": float(L),
"Lq": float(Lq),
"W": float(W),
"Wq": float(Wq)
}
return Result(
name="mm1_metrics",
params=call_params,
metrics=metrics,
data=metrics,
success=True,
message="M/M/1 排队系统指标计算成功。"
)
except Exception as e:
return Result(
name="mm1_metrics",
params=call_params,
metrics={},
data=None,
success=False,
message=f"计算失败:{type(e).__name__} - {str(e)}"
)