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:
1 parent
0955890010
commit
953e988983
5 files changed
+311
-72
No files matched your search
@@ -555,6 +555,39 @@
|
|||||||
`tol=np.nan` 穿过校验白跑迭代、`converged=False` 仍 `success=True`、
|
`tol=np.nan` 穿过校验白跑迭代、`converged=False` 仍 `success=True`、
|
||||||
类型错也抛 `ValueError`(与决策 54 分工不一致)、`params` 没记转移矩阵。
|
类型错也抛 `ValueError`(与决策 54 分工不一致)、`params` 没记转移矩阵。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `solve.mm1_metrics` 落地(**L1,S 档**,实现由用户本人写):闭式解 `ρ = λ/μ`、`L = ρ/(1-ρ)`、
|
||||||
|
`Lq = ρ²/(1-ρ)`、`W = 1/(μ-λ)`、`Wq = W - 1/μ`;`λ >= μ` 走"系统不稳定"分支返回 `success=False`。
|
||||||
|
Codex 审查结论:**公式全对**(探针实测 3 组参数 `(2,3)` / `(1,4)` / `(0.5,1.0)` 与闭式公式逐位一致,
|
||||||
|
`W = Wq + 1/μ` 恒等式成立)。至此 `solve` 层的 L1 清单**全部落地**。
|
||||||
|
- `markov_simulate` / `markov_steady_state` 增加**非负校验**(用户 2026-10-09 改定):含负元素的行随机矩阵
|
||||||
|
现在入口即报错,不再静默给出错结果(前者会"全落 0",后者会溢出成 `nan` 却报成功)。
|
||||||
|
- `markov_steady_state` 的 `params` 补 `transition`(`arr.tolist()`);`success=converged`
|
||||||
|
(未收敛判失败,口径 B);`tol` 拆成 `TypeError` / `ValueError` 且先判 `bool`。
|
||||||
|
- `tests/test_solve.py`:摘掉 2 条 `mm1_metrics` 的 skip,新增 4 条(含 `λ == μ` 的回归测试);本轮累计 +6 条。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`mm1_metrics` 的稳定性分界写错**(真 bug,按决策 31 修):`service_rate < arrival_rate` 与 docstring / 既有测试的
|
||||||
|
`λ >= μ` 口径不符 —— `λ == μ` 会掉进 `try`、`rho == 1.0` → `ZeroDivisionError`,被吞成 `success=False`
|
||||||
|
(结果碰巧对,但 message 成了"计算失败:ZeroDivisionError - float division by zero")。改成 `<=` 并加回归测试;
|
||||||
|
**两个分支的可观测输出完全相同(`success=False` / `metrics={}` / `data=None`),故未动任何口径**。
|
||||||
|
- **三处报错文案的参数名抄错**:`markov_steady_state` 的 `max_iter <= 0` 写成"tol 必须是正数";
|
||||||
|
`mm1_metrics` 的 `service_rate` 两条报错都写成 "tol"。顺带把半角逗号统一成全角。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `mm1_metrics` 的 docstring 补 `Raises` 与 `data` 说明;`call_params = {{...}}` 补空格;
|
||||||
|
ruff 6 条(5 处 `W293` + `F541` f-string 无占位符 + `W292` 文件末尾无换行)。
|
||||||
|
- `markov_steady_state` 的 `try` 前 3 个空行 → 1;`异常: ` → `异常:`。
|
||||||
|
- `STATUS.md`:决策 56 / 57 更新办结项,新增**决策 58**(`mm1_metrics` 的 4 条待拍板);「下一步」推进到第七轮;
|
||||||
|
验证计数更新为 **202 passed / 28 skipped(230 收集)**。
|
||||||
|
- `docs/dev/personal-pitfalls.md`:新增易错点 29(同一个校验里的两把尺子)。
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- **待用户拍板的口径**(Codex 只报不改):`markov_simulate` 4 条(`initial` 语义 A/B/C、`paths[:, 0]` 赋值位置、
|
||||||
|
状态越界、`std_terminal` 对分类量)、`markov_steady_state` 3+2 条(`tol` 的 NaN、`max_iter` 类型错、
|
||||||
|
非负校验的**容差**、未收敛却带 `data`)、`mm1_metrics` 4 条(`λ = 0`、numpy 标量、`data=metrics`、
|
||||||
|
失败分支丢 `rho`)。详见 `STATUS.md` 决策 56 / 57 / 58。
|
||||||
|
|
||||||
## [0.0.1] - 2026-10-01
|
## [0.0.1] - 2026-10-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
||||||
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
||||||
`monte_carlo_probability`、`monte_carlo_integral`、`random_walk`、`markov_simulate`、
|
`monte_carlo_probability`、`monte_carlo_integral`、`random_walk`、`markov_simulate`、
|
||||||
`markov_steady_state`(**均为用户写**)。剩余 1 项:`save_figure`(Codex 写)。
|
`markov_steady_state`、`mm1_metrics`(**均为用户写**)—— `solve` 层的 L1 清单**已全部落地**。
|
||||||
|
剩余 1 项:`save_figure`(Codex 写)。
|
||||||
`monte_carlo` 的口径已拍板(决策 48);`monte_carlo_probability` / `random_walk` 的待办**均已办结**;
|
`monte_carlo` 的口径已拍板(决策 48);`monte_carlo_probability` / `random_walk` 的待办**均已办结**;
|
||||||
`markov_simulate`(决策 56,5 条)与 `markov_steady_state`(决策 57,6 条)的**入口口径待你拍板**。
|
`markov_simulate`(决策 56)/ `markov_steady_state`(决策 57)/ `mm1_metrics`(决策 58)
|
||||||
|
**仍有一批入口口径待你拍板**(本轮各办结了一批,详见"下一步")。
|
||||||
另 3 个小口径仍悬着:`ci_low`/`ci_high` 的语义、`monte_carlo_integral` 的第一道 `isinstance` 分支可塌成两行、
|
另 3 个小口径仍悬着:`ci_low`/`ci_high` 的语义、`monte_carlo_integral` 的第一道 `isinstance` 分支可塌成两行、
|
||||||
`random_state` 注解与 `std` 的 `ddof` 是否全项目对齐。
|
`random_state` 注解与 `std` 的 `ddof` 是否全项目对齐。
|
||||||
|
|
||||||
@@ -333,7 +335,9 @@
|
|||||||
单位矩阵吸收态自锁、可复现(同种子相同、异种子不同)、终点频率收敛到 `[2/3, 1/3]`、
|
单位矩阵吸收态自锁、可复现(同种子相同、异种子不同)、终点频率收敛到 `[2/3, 1/3]`、
|
||||||
metrics = 终点状态、非法转移矩阵(非方阵 / 行和不为 1)、非法 `steps` / `n_paths`(0 / float / bool)、
|
metrics = 终点状态、非法转移矩阵(非方阵 / 行和不为 1)、非法 `steps` / `n_paths`(0 / float / bool)、
|
||||||
`np.int64` 可收、docstring Example 真能跑
|
`np.int64` 可收、docstring Example 真能跑
|
||||||
- ⏳ **留给你的 5 条**见"下一步 → markov_simulate 待你拍板"(全是入口口径与少考虑情况,决策 49)
|
- **第二轮(2026-10-09)**:用户补了 `np.any(arr < 0)` 非负校验 → 待办 ② 办结;
|
||||||
|
Codex 补一条"行和是 1 但含负元素"的回归测试
|
||||||
|
- ⏳ **余下 4 条**见"下一步 → markov_simulate 待你拍板"(全是入口口径与少考虑情况,决策 49)
|
||||||
|
|
||||||
- [x] `solve/simulate.py` 的 `markov_steady_state`(**L1,S 档,算法由用户本人写**,2026-10-09)
|
- [x] `solve/simulate.py` 的 `markov_steady_state`(**L1,S 档,算法由用户本人写**,2026-10-09)
|
||||||
- 核心:`v` 从均匀分布出发,`v_next = v @ arr` 反复推进(行随机矩阵按**行向量右乘**),
|
- 核心:`v` 从均匀分布出发,`v_next = v @ arr` 反复推进(行随机矩阵按**行向量右乘**),
|
||||||
@@ -349,7 +353,28 @@
|
|||||||
- **测试:+8 条、摘掉 1 条 skip**(此前该函数只有 1 条挂着的 skip)—— 2 状态 `[2/3, 1/3]` + `converged` / `iterations`、
|
- **测试:+8 条、摘掉 1 条 skip**(此前该函数只有 1 条挂着的 skip)—— 2 状态 `[2/3, 1/3]` + `converged` / `iterations`、
|
||||||
3 状态与特征分解对拍、`data` 是分布(和为 1 / 非负 / `float64`)、慢混合链迭代次数 > 100、
|
3 状态与特征分解对拍、`data` 是分布(和为 1 / 非负 / `float64`)、慢混合链迭代次数 > 100、
|
||||||
`np.int64` 的 `max_iter`、非法转移矩阵、`max_iter=0` / `tol=0`、docstring Example 真能跑
|
`np.int64` 的 `max_iter`、非法转移矩阵、`max_iter=0` / `tol=0`、docstring Example 真能跑
|
||||||
- ⏳ **留给你的 6 条口径**见"下一步 → markov_steady_state 待你拍板"(决策 57)
|
- **第二轮(2026-10-09,用户改定)**:补非负校验(① 办结)、`params` 记 `transition`(⑥ 办结)、
|
||||||
|
`success=converged`(选口径 B,④ 办结)、`tol` 拆成 `TypeError` / `ValueError` 且先判 `bool`(② 办结);
|
||||||
|
Codex 顺手修文案(`max_iter <= 0` 那条写成 "tol 必须是正数")与 `异常: ` → `异常:`,
|
||||||
|
并补 2 条测试(负元素矩阵、`max_iter=3` 未收敛走 `success=False`)
|
||||||
|
- ⏳ **余下 3 条 + 1 个新口子**见"下一步 → markov_steady_state 待你拍板"(决策 57)
|
||||||
|
|
||||||
|
- [x] `solve/simulate.py` 的 `mm1_metrics`(**L1,S 档,算法由用户本人写**,2026-10-09)
|
||||||
|
- 核心:闭式解 `ρ = λ/μ`、`L = ρ/(1-ρ)`、`Lq = ρ²/(1-ρ)`、`W = 1/(μ-λ)`、`Wq = W - 1/μ`;
|
||||||
|
`λ >= μ` 走"系统不稳定"分支返回 `success=False`
|
||||||
|
- **Codex 审查结论:公式全对**(探针实测 3 组参数 `(2,3)` / `(1,4)` / `(0.5,1.0)` 与闭式公式逐位一致,
|
||||||
|
`W = Wq + 1/μ` 恒等式也成立)
|
||||||
|
- **Codex 修了一处边界 bug(决策 31,纯 bug)**:稳定性判断写成 `service_rate < arrival_rate`,
|
||||||
|
而 docstring 与既有测试的口径都是 `λ >= μ` —— 于是 `λ == μ` 掉进 `try`、`rho == 1.0` →
|
||||||
|
`ZeroDivisionError` 被 `except` 吞成 `success=False`(结果碰巧对,message 却是"计算失败:ZeroDivisionError")。
|
||||||
|
两支的可观测输出完全相同(`success=False` / `metrics={}` / `data=None`),改 `<=` 只换 message,故直接修并加回归测试
|
||||||
|
- **Codex 只碰工程侧**:① ruff 6 条(5 处 `W293` + `F541` f-string 无占位符 + `W292` 文件末尾无换行);
|
||||||
|
② 文案:`service_rate` 的报错写成 "tol"、`max_iter` 的也写成 "tol"(同一轮里 3 处参数名抄错)、半角逗号 → 全角;
|
||||||
|
③ `call_params={...}` → `call_params = {...}`;④ docstring 补 `Raises` + 说明 `data` 与 `metrics` 同值
|
||||||
|
- **测试:+6 条、摘掉 2 条 skip**(此前只有 2 条挂着的 skip)—— 闭式公式逐项对拍(含 `W = Wq + 1/μ`)、
|
||||||
|
`λ == μ` 与 `λ > μ` 都判失败且带"不稳定"文案、入口(0 / 负数 / `bool` / 非数值)、
|
||||||
|
`λ == μ` 不能靠 `ZeroDivisionError` 落到失败分支、docstring Example
|
||||||
|
- ⏳ **留给你的 4 条**见"下一步 → mm1_metrics 待你拍板"(决策 58)
|
||||||
|
|
||||||
- [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**)
|
- [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**)
|
||||||
|
|
||||||
@@ -357,7 +382,7 @@
|
|||||||
|
|
||||||
| 检查 | venv | conda `mcm-toolkit` |
|
| 检查 | venv | conda `mcm-toolkit` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pytest` | 见下(单列) | **196 passed, 30 skipped(0 xfail)** |
|
| `pytest` | 见下(单列) | **202 passed, 28 skipped(0 xfail)** |
|
||||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||||
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
||||||
| `scripts/doctor.ps1` | 跑通 | — |
|
| `scripts/doctor.ps1` | 跑通 | — |
|
||||||
@@ -367,15 +392,15 @@
|
|||||||
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
||||||
`docs/04-environment.md` 坑 7)
|
`docs/04-environment.md` 坑 7)
|
||||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||||
- **226 条用例:196 passed / 30 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
- **230 条用例:202 passed / 28 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||||
`test_core.py` 23、`test_solve.py` 58(本轮 +8、摘掉 1 条 skip)、`test_viz.py` 8、`test_decision.py` 7、
|
`test_core.py` 23、`test_solve.py` 62(本轮 +4、再摘掉 2 条 skip)、`test_viz.py` 8、`test_decision.py` 7、
|
||||||
`test_stats.py` 53、`test_models.py` 5
|
`test_stats.py` 53、`test_models.py` 5
|
||||||
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
||||||
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
||||||
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
||||||
- 30 条 skip 是等实现的算法(`train_test_split`、`mm1_metrics`、`anova`、`fit_distribution`、
|
- 28 条 skip 是等实现的算法(`train_test_split`、`anova`、`fit_distribution`、`knapsack_dp`、
|
||||||
`knapsack_dp`、`linear_program`、`simulated_annealing` 等);
|
`linear_program`、`simulated_annealing` 等);
|
||||||
`monte_carlo_probability` / `monte_carlo_integral` / `markov_steady_state` 已落地,分别摘掉 2 / 1 / 1 条 skip;`markov_simulate` 新增的 9 条全部实跑;
|
`monte_carlo_probability` / `monte_carlo_integral` / `markov_steady_state` / `mm1_metrics` 已落地,分别摘掉 2 / 1 / 1 / 2 条 skip;`markov_simulate` 新增的 9 条全部实跑;
|
||||||
`TestValidate` 的 4 条仍挂着 —— `check_*` 还是 `NotImplementedError`,但它们实现时应当直接复用 `as_array`
|
`TestValidate` 的 4 条仍挂着 —— `check_*` 还是 `NotImplementedError`,但它们实现时应当直接复用 `as_array`
|
||||||
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
||||||
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
|
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
|
||||||
@@ -541,34 +566,47 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
|||||||
长度不匹配时**裸抛** `ValueError`(本该 `success=False`,同易错点 26 一族)。另有三个口子:
|
长度不匹配时**裸抛** `ValueError`(本该 `success=False`,同易错点 26 一族)。另有三个口子:
|
||||||
`np.int64(0)` 掉进数组分支、被 `as_array` 拒(与 `steps`/`n_paths` 收 `np.integer` 不一致,易错点 1 复发);
|
`np.int64(0)` 掉进数组分支、被 `as_array` 拒(与 `steps`/`n_paths` 收 `np.integer` 不一致,易错点 1 复发);
|
||||||
`True` 因 `isinstance(True, int)` 为真被当状态 1 **静默放行**;越界状态(如 `initial=5`、`s=2`)只在
|
`True` 因 `isinstance(True, int)` 为真被当状态 1 **静默放行**;越界状态(如 `initial=5`、`s=2`)只在
|
||||||
第一轮索引时才炸、被 `except` 吞成 `success=False`。可选口径见下一步的 A / B / C。
|
第一轮索引时才炸、被 `except` 吞成 `success=False`。
|
||||||
|
**2026-10-09 更新**:② 已办结(用户补了 `np.any(arr < 0)`,含负元素的行随机矩阵现在入口直接报错);
|
||||||
|
① ③ ④ ⑤ 仍待拍板(A / B / C 三个选项见"下一步")。
|
||||||
|
|
||||||
| 57 | `markov_steady_state` 的 6 条口径**待用户拍板**(Codex 只报选项,未代改,决策 49) |
|
| 57 | `markov_steady_state` 的口径:**4 条已办、3 条 + 2 个新口子仍待拍板**(Codex 只报选项,未代改,决策 49) |
|
||||||
探针实测出的口子:① **含负元素**的"行和为 1"矩阵(`[[1.5, -0.5], [0, 1]]`)会溢出成 `nan`,而
|
**2026-10-09 用户第二轮办结**:① 补 `np.any(arr < 0)`(含负元素的行随机矩阵现在入口即报错,原先会溢出成 `nan`
|
||||||
`data=[nan, nan]` 却 `success=True` —— 违反决策 48 的 NaN 口径(根因:没查 `arr >= 0`);附带的
|
却报 `success=True`);② `tol` 拆成 `TypeError`(类型 + `bool`)与 `ValueError`(取值);
|
||||||
`RuntimeWarning: overflow encountered in matmul` 因为 pyproject 设了 `filterwarnings = ["error::RuntimeWarning"]`,
|
④ **选口径 B**:`converged=False` → `success=False`;⑥ `params` 补 `transition`(`arr.tolist()`,可 JSON 化)。
|
||||||
**能让测试直接报错**,所以不能只靠测试兜;② `tol=True` 因 `isinstance(True, int)` 通过校验 → `tol=1` →
|
**仍待拍板**:③ `tol=np.nan`(`nan <= 0` 为假,穿过校验白跑 §max_iter 次迭代);
|
||||||
1 次就"收敛",给出 `[0.55, 0.45]` 的假答案(`max_iter` 查了 `bool`、`tol` 没查,易错点 1 复发);
|
⑤ `max_iter` 的类型错仍抛 `ValueError`(与决策 54 的"类型错 `TypeError`"不一致);
|
||||||
③ `tol=np.nan` 因 `nan <= 0` 为假穿过校验 → 白跑 10000 次迭代;④ `converged=False` 仍 `success=True`
|
⑦ **两把尺子宽严不一**——非负校验用精确 `arr < 0`,同一个函数里的行和校验却用宽容的 `np.allclose`,
|
||||||
且 `data` 是未收敛的迭代值(`max_iter=5` → `[0.6387, 0.3613]`);⑤ 类型错也抛 `ValueError`
|
实测 `[[1.0, -1e-16], [0.5, 0.5]]`(行和都是 1)会被判非法,建议改成 `< -1e-12` 这类容差;
|
||||||
(`max_iter=100.0`),与决策 54 的"类型错 `TypeError`"分工不一致;⑥ `params` 只记 `max_iter` / `tol`,
|
⑧ 未收敛时 `success=False` 但 `data` 仍是未收敛向量(文件内唯一"失败却带 `data`"的分支)—— 保持还是 `data=None`?
|
||||||
没记转移矩阵(`monte_carlo_integral` 记了 `bounds`),事后复现不了。可选口径见"下一步"。
|
|
||||||
|
| 58 | `mm1_metrics` 的稳定性分界由 Codex 按**决策 31** 直接修(`<` → `<=`),另有 4 条口径待用户拍板 |
|
||||||
|
docstring 与既有测试写的都是 `λ >= μ` 不稳定,代码却写 `service_rate < arrival_rate` —— 于是 `λ == μ`
|
||||||
|
掉进 `try`、`rho == 1.0` → `ZeroDivisionError`,被 `except` 吞成 `success=False`
|
||||||
|
(碰巧对,但 message 成了"计算失败:ZeroDivisionError - float division by zero")。两支的可观测输出完全相同
|
||||||
|
(`success=False` / `metrics={}` / `data=None`),改 `<=` 只换 message,故直接修 + 加回归测试。
|
||||||
|
**待拍板**:① `λ = 0` 现在被 `ValueError` 拒(数学上是合法退化解:空系统 `L = W = 0`);
|
||||||
|
② `np.int64` / `np.float32` 全被 `TypeError` 拒(文件内 `random_walk` 的 `start` 收 `np.number`、
|
||||||
|
整数参数收 `np.integer`,§4.7 有共识);③ `data=metrics` —— **同一个 dict 对象**(`r.data is r.metrics`),
|
||||||
|
且成功分支给 dict、失败分支给 `None`,与文件内"`data` 是数组"的习惯不同;
|
||||||
|
④ 失败分支的 `metrics={}` 会丢掉 `rho` 等已算出的量(可选保留)。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
### ⭐ 当前进度与待办(2026-10-09 第六轮)
|
### ⭐ 当前进度与待办(2026-10-09 第七轮)
|
||||||
|
|
||||||
**本轮:`markov_steady_state`(你写的)已审查通过** —— 算法本体正确,一行没动(3 状态与特征分解逐位一致、
|
**本轮:你改了 `markov_simulate` / `markov_steady_state` 的入口,并写完 `mm1_metrics`** —— 三处算法本体都正确。
|
||||||
200×200 只用 6 次迭代 / 0.10s);Codex 只清工程侧(ruff 5 条、docstring 补 `max_iter`/`tol`/`Raises`、
|
Codex 只做工程侧:`mm1_metrics` 修了一处**边界 bug**(`<` → `<=`,见决策 58)、ruff 6 条、4 处文案参数名抄错、
|
||||||
修正"周期链"表述、全角冒号),并 **+8 条测试、摘掉 1 条 skip**。
|
docstring 补 `Raises`、**+6 条测试 + 摘掉 2 条 skip**。
|
||||||
三连:`pytest` **196 passed / 30 skipped**、`ruff` 0、`pyright` 0。
|
三连:`pytest` **202 passed / 28 skipped**、`ruff` 0、`pyright` 0。`solve` 层 L1 清单**已全部落地**。
|
||||||
|
|
||||||
**接下来三件事**
|
**接下来三件事**
|
||||||
1. **`markov_steady_state` 的 6 条口径待你拍板**(下节,决策 57)。
|
1. **三个函数的口径待你拍板**:`markov_simulate` 4 条(决策 56)、`markov_steady_state` 3+2 条(决策 57)、
|
||||||
2. **`markov_simulate` 的 5 条口径**仍在挂着(决策 56 的 A / B / C),与 `solve` 的收尾一起定。
|
`mm1_metrics` 4 条(决策 58)—— 全在下面三节。
|
||||||
3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
|
2. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
|
||||||
|
3. 之后就往外层走:L1 只剩 `decision` / `models` / `stats` 的几条。
|
||||||
|
|
||||||
### ⏳ markov_simulate 待你拍板(5 条,Codex 只报不改)
|
### ⏳ markov_simulate 待你拍板(4 条)
|
||||||
|
|
||||||
**① `initial` 的语义(最大的一条,三个选项)**
|
**① `initial` 的语义(最大的一条,三个选项)**
|
||||||
现在标量分支按**路径数**铺满、数组分支按**状态数**校验长度,两条口径互相矛盾(详见决策 56)。
|
现在标量分支按**路径数**铺满、数组分支按**状态数**校验长度,两条口径互相矛盾(详见决策 56)。
|
||||||
@@ -576,39 +614,57 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
|||||||
- **B**:只收长度 `n_paths` 的数组(标量走 `np.full` 铺满),`data` 首列就是给定的初始状态序列(与 `paths[:, 0]` 赋值天然一致)
|
- **B**:只收长度 `n_paths` 的数组(标量走 `np.full` 铺满),`data` 首列就是给定的初始状态序列(与 `paths[:, 0]` 赋值天然一致)
|
||||||
- **C**:沿用现状,但把类型收成 `int | np.integer` + 先判 `bool`,把 `paths[:, 0] = ...` 挪进 `try`,docstring 写清"数组按路径数"
|
- **C**:沿用现状,但把类型收成 `int | np.integer` + 先判 `bool`,把 `paths[:, 0] = ...` 挪进 `try`,docstring 写清"数组按路径数"
|
||||||
|
|
||||||
**② 转移矩阵没查非负**:`[[1.5, -0.5], [0, 1]]` 行和恰为 1.0 → 通过校验,但 `cumsum` 非单调 →
|
~~**② 转移矩阵没查非负**~~ → **已办(2026-10-09)**:入口加了 `np.any(arr < 0)` → `ValueError`,
|
||||||
落格结果**全是 0**(静默错)。建议入口加一条 `np.any(arr < 0)` → `ValueError`。
|
测试里钉了"行和是 1 但含负元素"这条。余下的一小问:这条校验是**精确**判负,见决策 57 的 ⑦(容差)。
|
||||||
|
|
||||||
**③ `paths[:, 0]` 赋值在 `try` 之外**:长度不匹配时裸抛 `ValueError`,与"出错统一 `success=False`"(决策 48)不一致。
|
**③ `paths[:, 0]` 赋值在 `try` 之外**:长度不匹配时裸抛 `ValueError`,与"出错统一 `success=False`"(决策 48)不一致。
|
||||||
|
|
||||||
**④ 状态越界没在入口拦**:`initial=5`(`s=2`)第一轮才 `IndexError`,被吞成 `success=False`;
|
**④ 状态越界没在入口拦**:`initial=5`(`s=2`)第一轮才 `IndexError`,被吞成 `success=False`;
|
||||||
建议入口加 `0 <= initial < s`。
|
建议入口加 `0 <= initial < s`。
|
||||||
**⑤ `std_terminal` 对**分类**状态的意义**:状态编号是标称量,均值和标准差只对"有序状态"(如排队人数)成立 ——
|
|
||||||
|
**⑤ `std_terminal` 对分类状态的意义**:状态编号是标称量,均值和标准差只对"有序状态"(如排队人数)成立 ——
|
||||||
要不要把 `metrics` 换成状态频率(`terminal_freq`)?你定。
|
要不要把 `metrics` 换成状态频率(`terminal_freq`)?你定。
|
||||||
|
|
||||||
### ⏳ markov_steady_state 待你拍板(6 条,Codex 只报不改)
|
### ⏳ markov_steady_state 待你拍板(3 条 + 2 个新口子)
|
||||||
|
|
||||||
**① 转移矩阵没查非负**(与 `markov_simulate` 的待办 ② 是同一块):`[[1.5, -0.5], [0, 1]]` 行和恰为 1.0 →
|
- ~~**① 转移矩阵没查非负**~~ → **已办(2026-10-09)**
|
||||||
通过校验 → `v` 指数发散 → `data=[nan, nan]` **但 `success=True`**(违反决策 48 的 NaN 口径)。
|
- ~~**② `tol` 没查 `bool`**~~ → **已办**:`tol` 现在拆成 `TypeError`(类型 / `bool`)与 `ValueError`(取值)
|
||||||
实测还会先抛 `RuntimeWarning: overflow encountered in matmul`,而 pyproject 设了
|
- ~~**④ `converged=False` 仍 `success=True`**~~ → **已办**:选了**口径 B**,`success=converged`
|
||||||
`filterwarnings = ["error::RuntimeWarning"]` —— 这条**能让测试直接报错**,所以得在校验层拦,不能只靠测试兜。
|
- ~~**⑥ `params` 没记转移矩阵**~~ → **已办**:补了 `"transition": arr.tolist()`
|
||||||
|
|
||||||
**② `tol` 没查 `bool`**:`tol=True` → `isinstance(True, int)` 为真 → `tol=1` →
|
**③ `tol` 没查 NaN**:`np.nan <= 0` 为假 → 穿过校验 → 白跑满 10000 次迭代才返回(结果本身对,纯浪费)。
|
||||||
1 次就"收敛",给出 `[0.55, 0.45]` 的**假答案**(`success=True, converged=True, iterations=1`)。
|
建议与 `max_iter` 一起走"类型 → 有限性 → 取值"三件套(易错点 28)。
|
||||||
`max_iter` 查了 `bool`,`tol` 漏了(易错点 1 复发)。
|
|
||||||
|
|
||||||
**③ `tol` 没查 NaN**:`np.nan <= 0` 为假 → 穿过校验 → 白跑 10000 次迭代后返回(结果本身是对的,纯浪费)。
|
**⑤ `max_iter` 的类型错仍抛 `ValueError`**(`max_iter=100.0` / `max_iter=True`),
|
||||||
|
与文件内其它函数(决策 54:类型错 `TypeError`、取值错 `ValueError`)不一致 —— 要不要对齐?
|
||||||
|
|
||||||
**④ `converged=False` 仍 `success=True`**,且 `data` 是未收敛的迭代值(`max_iter=5` → `[0.6387, 0.3613]`)。
|
**⑦【新】非负校验的两把尺子**:`arr < 0` 是**精确**判断,而同一个函数里的行和校验用的是宽容的 `np.allclose`。
|
||||||
选项:**A** 保持现状(`converged` 自己看);**B** 不收敛判 `success=False`;**C** 不收敛时 `data=None`、只留 `metrics`。
|
实测 `[[1.0, -1e-16], [0.5, 0.5]]`(行和都是 1.0,只是浮点归一化的残渣)会被判"含负元素"而报错。
|
||||||
|
建模时转移矩阵常常是从数据归一化出来的,这种 `-1e-17` 级残渣很常见 —— 建议改成 `np.any(arr < -1e-12)`
|
||||||
|
(或 `arr.min() < -1e-12`)这样的容差写法。`markov_simulate` 的同名校验也适用。
|
||||||
|
|
||||||
**⑤ 类型错也抛 `ValueError`**(`max_iter=100.0`),与文件内其它函数(决策 54:类型错 `TypeError`、取值错 `ValueError`)
|
**⑧【新】未收敛时 `success=False` 但 `data` 仍是未收敛向量**:这是文件里唯一"失败却带 `data`"的分支
|
||||||
不一致 —— 要不要对齐?`tol` 同理。
|
(其它失败分支一律 `data=None`)。调用方若只看 `data is not None` 就会拿到没收敛的结果 —— 保持(B)还是改 `data=None`(C)?
|
||||||
|
|
||||||
**⑥ `params` 没记转移矩阵**:`{"max_iter": 10000, "tol": 1e-10}` 复现不了"算的是哪个矩阵"
|
### ⏳ mm1_metrics 待你拍板(4 条)
|
||||||
(`monte_carlo_integral` 记了 `bounds`)—— 要不要补?
|
|
||||||
|
|
||||||
**你下一个要写的(S 档,仍在 `solve/simulate.py`)**:`mm1_metrics`(M/M/1 闭式指标,10 行)。
|
**① `λ = 0` 被拒**:现在 `arrival_rate <= 0` → `ValueError`。数学上 `λ = 0` 是合法退化解
|
||||||
docstring 已写明口径:`λ >= μ` 时系统不稳定、队列长度无界 —— 这种情况返回 `success=False` 并说明原因,
|
(没有顾客到达,`ρ = L = Lq = W = Wq = 0`)—— 要收还是继续拒?
|
||||||
而不是返回 `inf`。写完 `solve` 这一层就齐了,L1 只剩 `decision` / `models` 那几条。
|
|
||||||
|
**② numpy 标量全被拒**:`np.int64(2)` / `np.float32(2.0)` → `TypeError`。
|
||||||
|
文件内其它函数已按 §4.7 放宽(`random_walk` 的 `start` 收 `np.number`、计数参数收 `np.integer`),
|
||||||
|
这里还是 `(int, float)` —— 要不要对齐?(`np.float64` 本来就是 `float` 的子类,不受影响)
|
||||||
|
|
||||||
|
**③ `data=metrics`**:`data` 和 `metrics` 是**同一个 dict 对象**(实测 `r.data is r.metrics` 为 `True`),
|
||||||
|
而且成功分支给 dict、失败分支给 `None`,与文件内"`data` 是样本数组"的习惯不同。选项:**A** 保持;
|
||||||
|
**B** `data=None`(闭式解没有样本数组);**C** 只留 `metrics`、`data` 给一个 5 元素数组。
|
||||||
|
|
||||||
|
**④ 失败分支丢掉了已算出的量**:`λ >= μ` 时 `metrics={}`,但 `ρ = λ/μ` 其实算得出来
|
||||||
|
(`ρ >= 1` 本身就是"不稳定"的量化说明,论文里常要写)—— 要不要在失败分支里保留 `rho`?
|
||||||
|
|
||||||
|
**你下一个要写的**:`solve` 这层已经齐了(`mm1_metrics` 落地后 L1 清单走完)。
|
||||||
|
建议下一步挑 **`decision` / `models` 里你感兴趣的 L1 函数**开刀(E 节有清单),
|
||||||
|
或者先把上面三节的口径一次性拍完、Codex 批量收口 —— 两条路都行,你定。
|
||||||
|
|
||||||
### ✅ random_walk 的 4 条待办(2026-10-05 已办结)
|
### ✅ random_walk 的 4 条待办(2026-10-05 已办结)
|
||||||
|
|
||||||
|
|||||||
@@ -487,3 +487,22 @@
|
|||||||
有限性(`np.isfinite`,挡 NaN / inf)、取值区间**;② 同一函数里同类参数(`max_iter` / `tol` / `n` / `tol`)
|
有限性(`np.isfinite`,挡 NaN / inf)、取值区间**;② 同一函数里同类参数(`max_iter` / `tol` / `n` / `tol`)
|
||||||
用**同一套模板**写,别一个查 `bool` 一个不查;③ 校验"概率/权重"这类输入时,除了和为 1,还要问一句"有没有可能是负数/NaN"。
|
用**同一套模板**写,别一个查 `bool` 一个不查;③ 校验"概率/权重"这类输入时,除了和为 1,还要问一句"有没有可能是负数/NaN"。
|
||||||
- 首次:2026-10-09|复发:0
|
- 首次:2026-10-09|复发:0
|
||||||
|
|
||||||
|
### 29. 同一个校验里的两把尺子:一个宽容、一个精确
|
||||||
|
|
||||||
|
- 现象:`markov_simulate` / `markov_steady_state` 校验转移矩阵时有两条:
|
||||||
|
`if not np.allclose(arr.sum(axis=1), 1.0)`(**宽容**,默认 `atol=1e-8`)和 `if np.any(arr < 0)`(**精确**)。
|
||||||
|
实测 `[[1.0, -1e-16], [0.5, 0.5]]`:行和都是 1.0,但左边那行有个 `-1e-16` 的归一化残渣 →
|
||||||
|
被判"含负元素"直接报错。**这个矩阵在数学上没问题,是校验太严**。
|
||||||
|
- 为什么会错:两条校验是分两次、照着不同念头写的 —— 行和那条想着"浮点比较不能写 `==`",
|
||||||
|
非负那条想着"概率怎么能为负"。各自都对,**放在一起就成了一严一宽**。建模时矩阵常常是
|
||||||
|
`df / df.sum()` 归一化出来的,这类 `-1e-17` 残渣非常常见,比赛现场会直接卡住。
|
||||||
|
- 同族(同一轮里一起犯的):① 边界比较与文档口径不一致 —— `mm1_metrics` 写 `service_rate < arrival_rate`,
|
||||||
|
而 docstring 与测试都写 `λ >= μ` 不稳定,于是 `λ == μ` 掉进正常计算分支、`rho = 1` →
|
||||||
|
`ZeroDivisionError`(结果碰巧对,message 却是"计算失败:ZeroDivisionError");
|
||||||
|
② 一批报错文案是复制粘贴的,**参数名忘了改** —— 同一轮里 `max_iter` 的文案写 "tol"、
|
||||||
|
`service_rate` 的两条文案也都写 "tol"。两条都是"看着像对的、跑起来也行、但口径/文案是错的"。
|
||||||
|
- 以后怎么防:① 同一个量的多条校验,**先把容差定下来再写**(要么都用 `np.allclose` / 小 `eps`,要么都精确),
|
||||||
|
写完回头对一眼"这几条的严格程度一致吗";② 阈值类比较(`<` / `<=`)**对着 docstring 读一遍**,
|
||||||
|
特别是 == 落在哪边;③ 复制粘贴报错文案时,逐个把参数名念出来。
|
||||||
|
- 首次:2026-10-09|复发:0
|
||||||
@@ -521,6 +521,8 @@ def markov_simulate(
|
|||||||
arr, _ = as_array(transition)
|
arr, _ = as_array(transition)
|
||||||
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
||||||
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
|
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
|
||||||
|
if np.any(arr < 0):
|
||||||
|
raise ValueError("transition 矩阵的元素必须是非负的(概率不能为负)")
|
||||||
if not np.allclose(arr.sum(axis=1), 1.0):
|
if not np.allclose(arr.sum(axis=1), 1.0):
|
||||||
raise ValueError("transition 每一行的和必须是 1.0")
|
raise ValueError("transition 每一行的和必须是 1.0")
|
||||||
s = arr.shape[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)
|
arr, _ = as_array(transition)
|
||||||
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
||||||
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
|
raise ValueError(f"transition 必须是二维方阵,你传入的形状是 {arr.shape}")
|
||||||
|
if np.any(arr < 0):
|
||||||
|
raise ValueError("transition 矩阵的元素必须是非负的(概率不能为负)")
|
||||||
if not np.allclose(arr.sum(axis=1), 1.0):
|
if not np.allclose(arr.sum(axis=1), 1.0):
|
||||||
raise ValueError("transition 每一行的和必须是 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]
|
s = arr.shape[0]
|
||||||
v = np.full(s, 1.0 / s)
|
v = np.full(s, 1.0 / s)
|
||||||
|
|
||||||
iterations = 0
|
iterations = 0
|
||||||
converged = False
|
converged = False
|
||||||
|
call_params = {
|
||||||
if isinstance(max_iter, bool) or not isinstance(max_iter, (int, np.integer)) or max_iter <= 0:
|
"transition": arr.tolist(),
|
||||||
raise ValueError(f"max_iter 必须是正整数,你传入了 {max_iter}")
|
"max_iter": max_iter,
|
||||||
if not isinstance(tol, (int, float)) or tol <= 0:
|
"tol": tol,
|
||||||
raise ValueError(f"tol 必须是正数,你传入了 {tol}")
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for _ in range(max_iter):
|
for _ in range(max_iter):
|
||||||
@@ -670,21 +683,20 @@ def markov_steady_state(transition: Any, max_iter: int | np.integer = 10000, tol
|
|||||||
|
|
||||||
return Result(
|
return Result(
|
||||||
name="markov_steady_state",
|
name="markov_steady_state",
|
||||||
params={"max_iter": max_iter, "tol": tol},
|
params=call_params,
|
||||||
metrics=metrics,
|
metrics=metrics,
|
||||||
data=v,
|
data=v,
|
||||||
success=True,
|
success=converged,
|
||||||
message=f"平稳分布求解{'成功' if converged else '未完全收敛'},迭代 {iterations} 次。"
|
message=f"平稳分布求解{'成功' if converged else '失败:未在最大迭代次数内收敛'},迭代 {iterations} 次。"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Result(
|
return Result(
|
||||||
name="markov_steady_state",
|
name="markov_steady_state",
|
||||||
params={"max_iter": max_iter, "tol": tol},
|
params=call_params,
|
||||||
metrics={},
|
metrics={},
|
||||||
data=None,
|
data=None,
|
||||||
success=False,
|
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
|
Returns
|
||||||
-------
|
-------
|
||||||
`Result`:`metrics={"rho", "L", "Lq", "W", "Wq"}`
|
`Result`:`metrics={"rho", "L", "Lq", "W", "Wq"}`
|
||||||
(系统利用率、系统内平均顾客数、队列长度、平均逗留时间、平均等待时间)。
|
(系统利用率、系统内平均顾客数、队列长度、平均逗留时间、平均等待时间);
|
||||||
|
`data` 与 `metrics` 同值(闭式解没有样本数组)。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError:
|
||||||
|
`arrival_rate` / `service_rate` 不是数值(`bool` 也判非法)。
|
||||||
|
ValueError:
|
||||||
|
`arrival_rate` / `service_rate` 不是正数。
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
@@ -714,5 +734,58 @@ def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
|
|||||||
>>> round(r.metrics["rho"], 4)
|
>>> round(r.metrics["rho"], 4)
|
||||||
0.6667
|
0.6667
|
||||||
"""
|
"""
|
||||||
# TODO(L1)
|
if not isinstance(arrival_rate, (int, float)) or isinstance(arrival_rate, bool):
|
||||||
raise NotImplementedError
|
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)}"
|
||||||
|
)
|
||||||
+65
-7
@@ -469,6 +469,8 @@ def test_markov_simulate_rejects_bad_transition():
|
|||||||
markov_simulate(np.array([[0.5, 0.5, 0.0], [0.2, 0.8, 0.0]]), initial=0)
|
markov_simulate(np.array([[0.5, 0.5, 0.0], [0.2, 0.8, 0.0]]), initial=0)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
markov_simulate(np.array([[0.5, 0.4], [0.2, 0.8]]), initial=0)
|
markov_simulate(np.array([[0.5, 0.4], [0.2, 0.8]]), initial=0)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
markov_simulate(np.array([[1.5, -0.5], [0.0, 1.0]]), initial=0) # 行和是 1,但含负元素
|
||||||
|
|
||||||
|
|
||||||
def test_markov_simulate_rejects_bad_arguments():
|
def test_markov_simulate_rejects_bad_arguments():
|
||||||
@@ -570,6 +572,20 @@ def test_markov_steady_state_rejects_bad_transition():
|
|||||||
markov_steady_state(np.ones((2, 3)) / 3)
|
markov_steady_state(np.ones((2, 3)) / 3)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
markov_steady_state(np.array([[0.5, 0.4], [0.2, 0.8]]))
|
markov_steady_state(np.array([[0.5, 0.4], [0.2, 0.8]]))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
markov_steady_state(np.array([[1.5, -0.5], [0.0, 1.0]])) # 行和是 1,但含负元素
|
||||||
|
|
||||||
|
|
||||||
|
def test_markov_steady_state_non_convergence_is_success_false():
|
||||||
|
"""`max_iter` 不够时按"未收敛"处理:`success=False` 且 `converged=False`(口径 B)。"""
|
||||||
|
from mcm_toolkit.solve import markov_steady_state
|
||||||
|
|
||||||
|
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||||||
|
r = markov_steady_state(P, max_iter=3)
|
||||||
|
assert r.success is False
|
||||||
|
assert r.metrics["converged"] is False
|
||||||
|
assert r.metrics["iterations"] == 3
|
||||||
|
assert "未在最大迭代次数内收敛" in r.message
|
||||||
|
|
||||||
|
|
||||||
def test_markov_steady_state_rejects_bad_arguments():
|
def test_markov_steady_state_rejects_bad_arguments():
|
||||||
@@ -610,21 +626,63 @@ def test_linear_program_basic():
|
|||||||
assert np.isclose(r.metrics["objective"], 4.0)
|
assert np.isclose(r.metrics["objective"], 4.0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
|
||||||
def test_mm1_metrics_closed_form():
|
def test_mm1_metrics_closed_form():
|
||||||
|
"""与闭式公式逐项对拍(ρ = λ/μ,L = ρ/(1-ρ),Wq = ρ/(μ-λ),W = Wq + 1/μ)。"""
|
||||||
|
from mcm_toolkit.solve import mm1_metrics
|
||||||
|
|
||||||
|
arrival_rate, service_rate = 2, 3
|
||||||
|
rho = arrival_rate / service_rate
|
||||||
|
r = mm1_metrics(arrival_rate=arrival_rate, service_rate=service_rate)
|
||||||
|
|
||||||
|
assert r.success
|
||||||
|
assert np.isclose(r.metrics["rho"], rho, rtol=1e-12)
|
||||||
|
assert np.isclose(r.metrics["L"], rho / (1 - rho), rtol=1e-12)
|
||||||
|
assert np.isclose(r.metrics["Lq"], rho**2 / (1 - rho), rtol=1e-12)
|
||||||
|
assert np.isclose(r.metrics["W"], 1 / (service_rate - arrival_rate), rtol=1e-12)
|
||||||
|
assert np.isclose(r.metrics["Wq"], rho / (service_rate - arrival_rate), rtol=1e-12)
|
||||||
|
assert np.isclose(r.metrics["W"], r.metrics["Wq"] + 1 / service_rate, rtol=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mm1_rejects_unstable_system():
|
||||||
|
"""λ >= μ 时系统不稳定,应 success=False 而不是返回 inf(λ == μ 也要走这一支)。"""
|
||||||
|
from mcm_toolkit.solve import mm1_metrics
|
||||||
|
|
||||||
|
equal = mm1_metrics(arrival_rate=3, service_rate=3)
|
||||||
|
assert equal.success is False
|
||||||
|
assert "不稳定" in equal.message
|
||||||
|
assert equal.metrics == {}
|
||||||
|
|
||||||
|
assert mm1_metrics(arrival_rate=3, service_rate=2).success is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_mm1_rejects_bad_arguments():
|
||||||
|
"""λ / μ 必须是正数:0、负数、bool、非数值都要在入口拦下。"""
|
||||||
|
from mcm_toolkit.solve import mm1_metrics
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
mm1_metrics(arrival_rate=0, service_rate=3)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
mm1_metrics(arrival_rate=-1, service_rate=3)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
mm1_metrics(arrival_rate=True, service_rate=3)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
mm1_metrics(arrival_rate=2, service_rate="3") # type: ignore[arg-type] # 就是要传错
|
||||||
|
|
||||||
|
|
||||||
|
def test_mm1_docstring_example_runs():
|
||||||
from mcm_toolkit.solve import mm1_metrics
|
from mcm_toolkit.solve import mm1_metrics
|
||||||
|
|
||||||
r = mm1_metrics(arrival_rate=2, service_rate=3)
|
r = mm1_metrics(arrival_rate=2, service_rate=3)
|
||||||
assert np.isclose(r.metrics["rho"], 2 / 3, rtol=1e-6)
|
assert round(r.metrics["rho"], 4) == 0.6667
|
||||||
assert np.isclose(r.metrics["L"], 2.0, rtol=1e-6)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
def test_mm1_stability_boundary_is_success_not_exception():
|
||||||
def test_mm1_rejects_unstable_system():
|
"""λ == μ 不能靠 ZeroDivisionError 落到失败分支:必须是显式的"不稳定"口径。"""
|
||||||
"""λ >= μ 时系统不稳定,应 success=False 而不是返回 inf。"""
|
|
||||||
from mcm_toolkit.solve import mm1_metrics
|
from mcm_toolkit.solve import mm1_metrics
|
||||||
|
|
||||||
assert mm1_metrics(arrival_rate=3, service_rate=3).success is False
|
r = mm1_metrics(arrival_rate=5, service_rate=5)
|
||||||
|
assert r.success is False
|
||||||
|
assert "ZeroDivisionError" not in r.message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
|
|||||||
Reference in new issue
Block a user