fix(solve): 同步第三轮入口口径(docstring + 测试 + lint)
用户第三轮改了 5 处口径:非负校验带容差、initial 收 np.integer、paths 赋值挪进 try、
metrics 换成 terminal_distribution、mm1 放开 lambda=0 并收 numpy 标量。三处算法本体仍正确
(探针复测:终点分布 {0: 0.6644, 1: 0.3356}、lambda=0 给 L=Lq=Wq=0 且 W=1/mu、data is metrics 已为 False)。
Changed(Codex 只做同步,未改口径)
- docstring:markov_simulate 的 Returns 与"遗留"段、markov_steady_state 的 Raises
(TypeError / ValueError 分工)、mm1_metrics 的 Parameters / Returns / Raises。
- tests/test_solve.py:同步 5 条受影响的测试,新增 2 条(terminal_distribution 只列出现过的状态、
lambda=0 的空系统)。test_solve.py 64 条。
- ruff:B905(zip 缺 strict)+ 2 条空白 -> 0;rng 之后的双空行 -> 1。
- STATUS.md:决策 56 / 57 / 58 更新到"只剩 5 条小口径";「下一步」第八轮;
新增「第八轮备忘:models 与 decision 各是干什么的」(函数清单 / 使用顺序 / 建议开工顺序)。
- CHANGELOG 与 docs/dev/personal-pitfalls.md 易错点 30(口径一改,docstring / 测试 / STATUS 同时过期)。
校验:pytest 204 passed / 28 skipped(232 收集);ruff 0;pyright 0
This commit is contained in:
1 parent
953e988983
commit
9cb295a362
5 files changed
+190
-107
No files matched your search
@@ -487,7 +487,8 @@ def markov_simulate(
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是形状 `(n_paths, steps + 1)` 的整数状态序列,第 0 列是初始状态;
|
||||
`metrics={"mean_terminal", "std_terminal"}`(终点状态的均值与标准差,`std` 用 `ddof=0`)。
|
||||
`metrics={"mean_terminal", "terminal_distribution"}` —— 前者是终点状态的均值,
|
||||
后者是 `{状态: 出现频率}` 的 dict(按终点统计,只含**实际出现过**的状态,没出现的状态没有 key)。
|
||||
|
||||
Raises
|
||||
------
|
||||
@@ -504,9 +505,10 @@ def markov_simulate(
|
||||
`cum_probs[:, -1] = 1.0` 是为了压掉浮点累加误差,避免随机数落在 `[1 - 1e-16, 1)` 时索引越界。
|
||||
终点分布随步数收敛到平稳分布:这既是本函数的自检手段,也是论文里值得写一句的性质。
|
||||
|
||||
遗留(决策 56 待定):`initial` 现在靠 `isinstance(initial, int)` 区分"标量状态 / 状态数组",
|
||||
`np.int64`、`bool` 会掉进数组分支,"按状态数校验长度"与"按路径数写入"也互相矛盾。
|
||||
口径定下来后,上面 `initial` 这一节与本段随之改写。
|
||||
遗留(决策 56):`initial` 靠 `isinstance(initial, (int, np.integer))` 区分"标量状态 / 状态数组",
|
||||
`np.int64` 已收,但 `bool` 仍会被当成状态 1(`isinstance(True, int)` 为真);
|
||||
且数组分支"按**状态数**校验长度"与真正写进去时"要按**路径数**"仍互相矛盾 —— 只有 `n_paths == s` 才跑得通。
|
||||
状态越界(如 `initial=5`、`s=2`)也要等第一轮索引才炸。口径定了这段随之改写。
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -521,8 +523,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 np.any(arr < -1e-12):
|
||||
raise ValueError("transition 矩阵的元素必须是非负的")
|
||||
if not np.allclose(arr.sum(axis=1), 1.0):
|
||||
raise ValueError("transition 每一行的和必须是 1.0")
|
||||
s = arr.shape[0]
|
||||
@@ -538,24 +540,25 @@ def markov_simulate(
|
||||
|
||||
rng = get_rng(random_state)
|
||||
|
||||
if isinstance(initial, int):
|
||||
if isinstance(initial, (int,np.integer)):
|
||||
current_states = np.full(n_paths, initial)
|
||||
initial_param = int(initial)
|
||||
else:
|
||||
current_states, _ = as_array(initial, ndim=1)
|
||||
if len(current_states) != s:
|
||||
raise TypeError(f"initial 的长度必须是 {s},而你传入的是 {len(current_states)}")
|
||||
|
||||
paths = np.zeros((n_paths, steps + 1), dtype=int)
|
||||
paths[:, 0] = current_states
|
||||
initial_param = current_states.tolist()
|
||||
|
||||
call_params = {
|
||||
"n_steps": steps,
|
||||
"steps": steps,
|
||||
"n_paths": n_paths,
|
||||
"initial": initial,
|
||||
"random_state": random_state,
|
||||
"initial": initial_param,
|
||||
"random_state": random_state
|
||||
}
|
||||
|
||||
try:
|
||||
paths = np.zeros((n_paths, steps + 1), dtype=int)
|
||||
paths[:, 0] = current_states
|
||||
for t in range(steps):
|
||||
probs = arr[current_states]
|
||||
|
||||
@@ -569,9 +572,12 @@ def markov_simulate(
|
||||
current_states = next_states
|
||||
|
||||
terminal_positions = paths[:, -1]
|
||||
unique, counts = np.unique(terminal_positions, return_counts=True)
|
||||
terminal_dist = {int(state): float(count) / n_paths for state, count in zip(unique, counts, strict=True)}
|
||||
|
||||
metrics = {
|
||||
"mean_terminal": float(np.mean(terminal_positions)),
|
||||
"std_terminal": float(np.std(terminal_positions)),
|
||||
"terminal_distribution": terminal_dist
|
||||
}
|
||||
return Result(
|
||||
name="markov_simulate",
|
||||
@@ -612,9 +618,11 @@ def markov_steady_state(transition: Any, max_iter: int | np.integer = 10000, tol
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError:
|
||||
`transition` 不是二维方阵;`max_iter` 不是整数;`tol` 不是数值。
|
||||
ValueError:
|
||||
`transition` 不是二维方阵、或某一行和不等于 1.0;
|
||||
`max_iter` 不是正整数;`tol` 不是正数。
|
||||
`transition` 含负元素、或某一行和不等于 1.0;
|
||||
`max_iter` 不是正整数;`tol` 不是正数(NaN 也算非法)。
|
||||
|
||||
Notes
|
||||
-----
|
||||
@@ -639,19 +647,19 @@ 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 TypeError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
|
||||
if np.any(arr < -1e-12):
|
||||
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__}")
|
||||
raise TypeError(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:
|
||||
if tol <= 0 or np.isnan(tol):
|
||||
raise ValueError(f"tol 必须是正数,你传入了 {tol}")
|
||||
|
||||
s = arr.shape[0]
|
||||
@@ -706,21 +714,23 @@ def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
|
||||
Parameters
|
||||
----------
|
||||
arrival_rate, service_rate:
|
||||
到达率 λ 与服务率 μ。`λ >= μ` 时系统不稳定,队列长度无界——
|
||||
这种情况下函数返回 `success=False` 并说明原因,而不是返回 `inf`。
|
||||
到达率 λ 与服务率 μ,都收 `int` / `float` / numpy 标量。
|
||||
`λ` 允许为 0(空系统:`L = Lq = Wq = 0`,`W` 取极限值 `1/μ`);
|
||||
`λ >= μ` 时系统不稳定、队列长度无界 —— 这种情况返回 `success=False` 并说明原因,而不是返回 `inf`。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`metrics={"rho", "L", "Lq", "W", "Wq"}`
|
||||
(系统利用率、系统内平均顾客数、队列长度、平均逗留时间、平均等待时间);
|
||||
`data` 与 `metrics` 同值(闭式解没有样本数组)。
|
||||
`data` 与 `metrics` 同值(闭式解没有样本数组),但**不是同一个对象**。
|
||||
不稳定时 `success=False`、`metrics={{"rho"}}`(只报利用率)、`data=None`。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError:
|
||||
`arrival_rate` / `service_rate` 不是数值(`bool` 也判非法)。
|
||||
ValueError:
|
||||
`arrival_rate` / `service_rate` 不是正数。
|
||||
`arrival_rate` 是负数;`service_rate` 不是正数。
|
||||
|
||||
Notes
|
||||
-----
|
||||
@@ -734,27 +744,27 @@ def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
|
||||
>>> round(r.metrics["rho"], 4)
|
||||
0.6667
|
||||
"""
|
||||
if not isinstance(arrival_rate, (int, float)) or isinstance(arrival_rate, bool):
|
||||
if not isinstance(arrival_rate, (int, float, np.number)) 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):
|
||||
if arrival_rate < 0:
|
||||
raise ValueError(f"arrival_rate 必须是非负数,你传入了 {arrival_rate}")
|
||||
if not isinstance(service_rate, (int, float, np.number)) 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}
|
||||
call_params = {"arrival_rate": float(arrival_rate), "service_rate": float(service_rate)}
|
||||
|
||||
if service_rate <= arrival_rate:
|
||||
if arrival_rate >= service_rate:
|
||||
rho = arrival_rate / service_rate
|
||||
return Result(
|
||||
name="mm1_metrics",
|
||||
params=call_params,
|
||||
metrics={},
|
||||
metrics={"rho": float(rho)},
|
||||
data=None,
|
||||
success=False,
|
||||
message="系统不稳定:到达率 λ 大于等于服务率 μ,队列长度无界,无法计算闭式解。"
|
||||
message=f"系统不稳定:λ >= μ,队列无界。当前利用率 rho={rho:.4f}"
|
||||
)
|
||||
|
||||
try:
|
||||
rho = arrival_rate / service_rate
|
||||
L = rho / (1.0 - rho) # 系统内平均顾客数
|
||||
@@ -775,7 +785,7 @@ def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
|
||||
name="mm1_metrics",
|
||||
params=call_params,
|
||||
metrics=metrics,
|
||||
data=metrics,
|
||||
data=metrics.copy(),
|
||||
success=True,
|
||||
message="M/M/1 排队系统指标计算成功。"
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user