feat(solve): monte_carlo_integral 落地,修掉体积符号真 bug

实现由用户本人写;Codex 只碰工程侧 + 一处纯 bug(决策 31)。

- 真 bug:体积写成 lows - highs,奇数维为负(1D x^2 于 [0,1] 得 -0.3324、3D 得 -0.5005);
  2D 因两个负号相乘"看着对"(+0.2499),把 bug 藏住。改成 widths = highs - lows 后
  复测 0.3324 / 0.5005 / 1.9971([0,2])全部正确
- docstring:补 func / n / random_state 与 Raises;写清 metrics["mean"] 才是积分估计值、
  data 是 n 个样本各自的贡献(决策 51);修掉跑不通的 Examples(data 是数组,bool(数组) 会抛)
- tests/test_solve.py:摘 1 条 skip + 新增 4 条(3D 奇数维 / [0,2] 宽度 / 2D+params / 非法 bounds)
- 验证:197 收集 / 166 passed / 31 skipped / 0 xfail,ruff 0,pyright 0
- docs:STATUS 决策 51 + 3 条待办 + 下一站 random_walk;CHANGELOG;易错点 22/23,第 3 条复发 +1
This commit is contained in:
guzhujushi committed 2026-10-05 15:55:12 +08:00
1 parent b0c4b4a413
commit ed1f9f0c53
5 files changed
+207 -37

No files matched your search

+26
View File
@@ -399,6 +399,32 @@
- `docs/dev/personal-pitfalls.md`:新增第 **21** 条(公式里的 `^` 不是乘方,且被 `except` 吞成 `success=False`),索引补到 21。 - `docs/dev/personal-pitfalls.md`:新增第 **21** 条(公式里的 `^` 不是乘方,且被 `except` 吞成 `success=False`),索引补到 21。
- docstring 的 "Wald 区间" 同步改为 "Wilson score 区间"。 - docstring 的 "Wald 区间" 同步改为 "Wilson score 区间"。
### Added
- `solve.monte_carlo_integral` 落地(**L1,S 档**,实现由用户本人写):`bounds` → 自造 `(n, d)` 均匀样本 → 逐点求值 →
`mean(func(X) × 体积)`;内部自造 `sampler` / `simulator` 再复用 `monte_carlo` —— "造批 → 一次算 → 汇总"的第二次复用。
`params` 额外记 `bounds` / `dimension`;配套 `tests/test_solve.py` 新增 4 条 + 摘 1 条 skip。
### Fixed
- **`monte_carlo_integral` 的体积符号写反**(真 bug,按决策 31 修):写成 `wights = lows - highs`,奇数维体积为负 ——
1D `x²` 于 `[0,1]` 实测 **-0.3324**(应 +1/3)、3D `x` 得 **-0.5005**(应 +1/2);**2D `x·y` 却得 +0.2499 "看着对"**
(两个负号相乘抵消,把 bug 藏住)。改成 `widths = highs - lows` 后复测 **0.3324 / 0.5005 / 1.9971(`[0,2]`)** 全部正确。
- `monte_carlo_integral` 的 docstring `Examples` 跑不通:断言 `abs(r.data - 1 / 3) < 0.01`,而 `data` 是 n 个样本的贡献数组,
`bool(数组)` 会抛 `ValueError: truth value ... is ambiguous`;改成断言 `metrics["mean"]`。
- 格式:`simulate.py` 里 `def random_walk` 前只留了 1 个空行;`tests/test_solve.py` 有 3 处留了 3 个空行。
### Changed
- `monte_carlo_integral` 的 docstring 补齐 `func` / `n` / `random_state` 三节 `Parameters` 与 `Raises`(原文只有 `bounds` +
Notes + Examples);`Returns` 写清"**`metrics["mean"]` 才是积分估计值**,`data` 是 n 个样本各自的贡献"(**决策 51**)。
- `tests/test_solve.py`:摘掉 `test_monte_carlo_integral_converges_to_analytic_value` 的 skip,新增 3 条"抓符号"用例
(3D 奇数维、`[0,2]` 宽度、2D + `name`/`dimension`/`bounds` 三个 params)与 1 条非法 `bounds` 抛 `ValueError`;
`solve` 段 25 → **30** 条,全量 **197 收集 / 166 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:L1 清单加入 `monte_carlo_integral`;新增**决策 51** 与「`monte_carlo_integral` 的 3 条待办」;
「下一步」改成从 `random_walk` 接着写;验证计数更新为 166 / 31;`monte_carlo_probability` 的 6 条待办收窄到 1 条。
- `docs/dev/personal-pitfalls.md`:新增第 **22 / 23** 条(失败结果的 `message` 被包装层无条件覆盖;体积/宽度也会减反、
维度奇偶会把符号错误藏住),第 3 条(注解与 docstring 不一致)**复发 +1**,索引补到 23。
- `tmp/type_demo.py`(gitignored):用 pyright 证明 `Callable[..., bool]` 会**拒绝**一个注解良好的向量化 `event`
(`ndarray` 与 `bool` 不兼容),`Callable[..., Any]` 才收 —— 这是决策 50 那条遗留的实际代价。
## [0.0.1] - 2026-10-01 ## [0.0.1] - 2026-10-01
### Added ### Added
+64 -27
View File
@@ -1,6 +1,6 @@
# STATUS — 当前进度 # STATUS — 当前进度
> 更新日期:2026-10-03 > 更新日期:2026-10-05
## 本次任务 ## 本次任务
@@ -21,8 +21,9 @@
当前 **L0 11/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、 当前 **L0 11/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`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`(**用户写**,2026-10-05)。剩余 1 项:`save_figure`(Codex 写)。 `monte_carlo_probability`、`monte_carlo_integral`(**用户写**,2026-10-05)。剩余 1 项:`save_figure`(Codex 写)。
`monte_carlo` 的口径已拍板(决策 48),`ci_low`/`ci_high` 的语义与 `monte_carlo_probability` 的 6 条待办见"下一步"。 `monte_carlo` 的口径已拍板(决策 48);`ci_low`/`ci_high` 的语义、`monte_carlo_probability` 剩下的
1 条待办、`monte_carlo_integral` 的 3 条待办,都见"下一步"。
## 任务清单 ## 任务清单
@@ -274,13 +275,30 @@
④ 文案:NaN 提示里的 "simulator" 改成 "event"(从 `monte_carlo` 抄来没改) ④ 文案:NaN 提示里的 "simulator" 改成 "event"(从 `monte_carlo` 抄来没改)
- **测试:摘掉 2 条 skip**(`test_monte_carlo_probability_of_standard_normal`、`test_monte_carlo_is_reproducible` - **测试:摘掉 2 条 skip**(`test_monte_carlo_probability_of_standard_normal`、`test_monte_carlo_is_reproducible`
—— 实现写完还挂着 skip,正是易错点 9)并新增 5 条,`solve` 段共 24 条 —— 实现写完还挂着 skip,正是易错点 9)并新增 5 条,`solve` 段共 24 条
- [x] `solve/simulate.py` 的 `monte_carlo_integral`(**L1,S 档,算法由用户本人写**,2026-10-05)
- 核心:`bounds` → 自己造 `(n, d)` 均匀样本 → 逐点求值 → `mean(func(X) × 体积)`;
自造 `sampler` / `simulator` 再复用 `monte_carlo` —— "造批 → 一次算 → 汇总"这套骨架的第二次复用
- **Codex 只改了一处真 bug(决策 31,纯 bug)**:体积写成 `wights = lows - highs`(名字打错 + 减反)。
奇数维体积为负:1D `x²` 于 `[0,1]` 实测 **-0.3324**(应 +1/3)、3D `x` 得 **-0.5005**(应 +1/2);
而 **2D `x·y` 得 +0.2499 "看着对"** —— 两个负号相乘抵消,把 bug 藏住了。
改成 `widths = highs - lows` 后复测 **0.3324 / 0.5005 / 1.9971(`[0,2]`)** 全部正确
- **Codex 只碰工程侧**:① docstring 补 `func` / `n` / `random_state` 三节 `Parameters` 与 `Raises`;
② `Returns` 写清"**`metrics["mean"]` 才是积分估计值**,`data` 是 `n` 个样本各自的贡献";
③ **修掉跑不通的 Examples**(原版断言 `abs(r.data - 1 / 3) < 0.01` —— `data` 是数组,
`bool(数组)` 会抛 `ValueError: truth value ... is ambiguous`),改成断言 `metrics["mean"]`;
④ 格式:`bounds_arr[:,0]` → `[:, 0]`、函数间空行(`def random_walk` 前只留了 1 个空行)
- **测试:新增 4 条 + 摘掉 1 条 skip**(`solve` 段共 30 条)—— 1D 收敛到 1/3(n=2e5)、
**3D 抓体积符号**(奇数维)、**`[0,2]` 抓宽度**(偶数维藏不住)、2D + `name`/`dimension`/`bounds` 三个 params、
`(1, 0)` 抛 `ValueError`。三条"抓符号"用例就是照着上面那个"2D 掩盖效应"设计的
- ⏳ **留给用户 3 条**(见"下一步"):`result.message` 无条件覆盖(失败被写成"成功")、
`bounds` 形状没校验(`[0, 1]` 抛裸 `IndexError`)、`n: int` 与全模块 `int | np.integer` 不一致
- [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**) - [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**)
### 验证结果 ### 验证结果
| 检查 | venv | conda `mcm-toolkit` | | 检查 | venv | conda `mcm-toolkit` |
|---|---|---| |---|---|---|
| `pytest` | 见下(单列) | **161 passed, 32 skipped(0 xfail)** | | `pytest` | 见下(单列) | **166 passed, 31 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` | 跑通 | — |
@@ -290,15 +308,14 @@
(修配置之前是 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(与两套环境一致)
- **193 条用例:161 passed / 32 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、 - **197 条用例:166 passed / 31 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
`test_core.py` 23、`test_solve.py` 25(+16,含摘掉 2 条 skip)、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 53、 `test_core.py` 23、`test_solve.py` 30(上一轮 25,本轮摘 1 条 skip + 新增 4 条)、`test_viz.py` 8、`test_decision.py` 7、
`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`
- 32 条 skip 是等实现的算法(`train_test_split`、`monte_carlo_integral`、`anova`、`fit_distribution` 等); - 31 条 skip 是等实现的算法(`train_test_split`、`random_walk`、`markov_*`、`anova`、`fit_distribution` 等);
`monte_carlo_probability` 已落地,摘掉 2 条 skip(本轮净 +3 条用例); `monte_carlo_probability` / `monte_carlo_integral` 已落地,分别摘掉 2 条与 1 条 skip;
`bootstrap_ci` 已落地摘掉 3 条 skip(本轮新增 6 条用例,净 +3);`test_solve.py` 9 条按决策 26 改为逐条 skip;
`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 复现 + 测试真红。
@@ -313,9 +330,10 @@
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。 - [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。 note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
- [ ] L0 完成 **11/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、 - [ ] L0 完成 **11/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`), `detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
另 L1 的 `confidence_interval` / `t_test` 已提前落地; `monte_carlo`(用户写,2026-10-04));另 L1 的 `confidence_interval` / `t_test` /
**只剩 `monte_carlo`(用户写)与 `save_figure`(Codex 写)**,详情见 E 节与"下一步" `monte_carlo_probability` / `monte_carlo_integral` 已提前落地;
**L0 只剩 `save_figure`(Codex 写)**,详情见 E 节与"下一步"
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用** - [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
(`helper_unknown_error: setup refresh had errors`),只有提权 (`helper_unknown_error: setup refresh had errors`),只有提权
(`require_escalated`)模式可用。沙箱模式只允许写 `D:\MCM&ICM_Toolkit`。 (`require_escalated`)模式可用。沙箱模式只允许写 `D:\MCM&ICM_Toolkit`。
@@ -415,26 +433,33 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
用户 2026-10-05 按"6 条待办"自行改定。Wilson 在 p 贴近 0 / 1 时不会退化成零宽:实测 p=1、n=1000 从 `(1.0, 1.0)` 用户 2026-10-05 按"6 条待办"自行改定。Wilson 在 p 贴近 0 / 1 时不会退化成零宽:实测 p=1、n=1000 从 `(1.0, 1.0)`
变成 `(0.99617, 1.0)`;p=0 从"判失败"变成 `(0.0, 0.00383)`(原 Wald 是 0)。区间口径与 变成 `(0.99617, 1.0)`;p=0 从"判失败"变成 `(0.0, 0.00383)`(原 Wald 是 0)。区间口径与
`statsmodels.proportion_confint(method="wilson")` 对齐,已加成对照测试。 `statsmodels.proportion_confint(method="wilson")` 对齐,已加成对照测试。
**遗留两条**:`event: Callable[..., bool]` 注解与"吃整批、返回数组"不符;警告文案把 p≈1 也说成"小概率事件"。 **遗留**(2026-10-05 第二轮复核):`event: Callable[..., bool]` 注解与"吃整批、返回数组"不符(**待改**);
另:`z_score^2` 那处是 Codex 修的真 bug(`^` 是异或,见易错点 21)——按决策 31 属纯 bug,未改任何语义。 警告文案把 p≈1 也说成"小概率事件" —— **用户已于同日改掉**(现在两半都覆盖,见 CHANGELOG 本轮)。
| 51 | `monte_carlo_integral` 的 `data` = **n 个样本各自的贡献**(`func(X_i) × 体积`);积分估计值看 `metrics["mean"]` |
用户 2026-10-05 写完实现。Codex 审查只改了一处**纯 bug**(决策 31):体积 `lows - highs` → `highs - lows` ——
奇数维体积为负(1D `x²` 于 `[0,1]` 实测 **-0.3324**、3D `x` 得 **-0.5005**),而 **2D 因两个负号相乘"看着对"**
(+0.2499),把 bug 藏住了;改后实测 0.3324 / 0.5005 / 1.9971 全部正确。docstring 与测试都按上述口径写死。
**留给用户 3 条**:① `result.message` 无条件覆盖(失败被写成"成功");② `bounds` 形状没校验(`[0, 1]` 抛裸 `IndexError`);
③ `n: int` 与全模块的 `int | np.integer` 不一致。
## 下一步 ## 下一步
### ⭐ 当前进度与待办(2026-10-05) ### ⭐ 当前进度与待办(2026-10-05 第二轮)
**本轮:`monte_carlo_probability` 已由用户写完并改到第二版**(L1,S 档)。Codex 两轮都只做工程侧, **本轮:`monte_carlo_integral` 已由用户写完并审查通过**(L1,S 档)。Codex 只改了一处**纯 bug**
**算法逻辑一行未动**(唯一例外是一个纯 typo:`z_score^2` → `z_score**2`,见易错点 21 / 决策 50)。 (体积 `lows - highs` → `highs - lows`,见决策 51)—— **算法结构一行未动**。
**接下来三件事** **接下来三件事**
1. `monte_carlo_probability` 还剩 **2 条待办**(下面一节)——都是你的活。 1. `monte_carlo_integral` 有 **3 条待办**(下面一节)——都是你的活。
2. `ci_low` / `ci_high` 的语义确认一句(也是你的活),确认后 Codex 补 `docs/algorithms/monte_carlo.md` 卡片。 2. `ci_low` / `ci_high` 的语义确认一句(也是你的活),确认后 Codex 补 `docs/algorithms/monte_carlo.md` 卡片。
3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。 3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
**你下一个要写的(S 档,仍在 `solve/simulate.py`)**:`monte_carlo_integral` → `random_walk`。 **你下一个要写的(S 档,仍在 `solve/simulate.py`)**:`random_walk`。
`monte_carlo_integral` 其实就是 `monte_carlo` 的特例:自己造 `n×d` 的均匀样本 → 逐点求值 → 它和刚写完的两个函数是**同一套骨架的第三次复用**(造批 → 一次算 → 汇总),新东西只有两处:
`mean(f) × 体积`,正好把那套"造批 → 一次算 → 汇总"的写法复用第二次。 `np.cumsum` 把增量累成轨迹;`data` 的 shape 从 `(n,)` 变成 `(n_paths, n_steps + 1)` 的**二维**。
### ✅ monte_carlo_probability 的 6 条待办(2026-10-05 已办 5 条) ### ✅ monte_carlo_probability 的 6 条待办(2026-10-05 已办 5 条、剩 1 条)
- ~~① `p <= 1e-3` 判 `success=False`~~ → 改成 `warning_message`,仍返回区间(实测 p=0 → `(0.0, 0.00383)`)。 - ~~① `p <= 1e-3` 判 `success=False`~~ → 改成 `warning_message`,仍返回区间(实测 p=0 → `(0.0, 0.00383)`)。
- ~~② Wald 在 p 靠近 1 时退化~~ → 换 **Wilson**(p=1、n=1000:`(1.0, 1.0)` → `(0.99617, 1.0)`)。 - ~~② Wald 在 p 靠近 1 时退化~~ → 换 **Wilson**(p=1、n=1000:`(1.0, 1.0)` → `(0.99617, 1.0)`)。
@@ -443,10 +468,22 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
- ~~⑤ `metrics` 混 `np.float64`~~ → `float()` 统一(实测 4 个 key 全是 `float`)。 - ~~⑤ `metrics` 混 `np.float64`~~ → `float()` 统一(实测 4 个 key 全是 `float`)。
- ~~⑥ `n: int`~~ → 已改 `int | np.integer`。 - ~~⑥ `n: int`~~ → 已改 `int | np.integer`。
**还剩 2 条(你的活)** **还剩 1 条(你的活)**
1. `event: Callable[..., bool]` 的注解与行为不符:实际必须吃**整批**参数、返回长度 `n` 的数组。 1. `event: Callable[..., bool]` 的注解与行为不符:实际必须吃**整批**参数、返回长度 `n` 的数组。
2. 警告文案把 **p ≈ 1** 也说成"小概率事件 P < 1e-3 不建议使用"——那一条建议对 p→1 是错的 改法(一行):`Callable[..., bool]` → `Callable[..., Any]`,与同模块的 `sampler` / `simulator` 对齐。
(临界情形该说的是"改用 1−p 或换方法")。条件是你写的 `p_value <= 1e-3 or p_value >= 1 - 1e-3`,文案只覆盖了前一半。 pyright 实测:`Callable[..., bool]` 会**拒绝**一个注解良好的向量化 `event`(返回 `ndarray` 与 `bool` 不兼容),
`Callable[..., Any]` 才收得下 —— 这正是"注解与行为不符"的实际代价。
### ⏳ monte_carlo_integral 的 3 条待办(2026-10-05,你的活)
1. **`result.message` 被无条件覆盖**:拿到 `result` 后直接 `result.message = "…成功…"`,内层 `success=False` 时也照写。
实测 `func` 返回标量时,对外是 `success=False` + **"蒙特卡洛多重积分成功"** —— 两个字段自相矛盾。
方向(你定):用 `if result.success:` 包住,或 message 只**追加**、不覆盖。
2. **`bounds` 形状没校验**:`bounds=[0, 1]`(1-D 扁平)在 `bounds_arr[:, 0]` 抛 `IndexError: too many indices`
—— 是裸异常,没包进 `Result`。方向:要求"每个元素都是二元组",入口给可读的 `ValueError`;
顺手也能和 `(1, 0)` 那条校验合并到一处写。
3. **`n: int = 10000` 与全模块不一致**:别的函数都写 `int | np.integer`(§4.7)。
运行期其实收 `np.int64`(`monte_carlo` 里放行了),只是注解更窄 —— 调用方按注解老实写,pyright 会对 `np.int64` 报错。
### ⏳ monte_carlo 只剩 1 个口径要你确认 ### ⏳ monte_carlo 只剩 1 个口径要你确认
@@ -461,7 +498,7 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
- B:改成均值 CI → 写"估计值 1.0024 ± 0.0043(95% CI)"(证明 n 够大) - B:改成均值 CI → 写"估计值 1.0024 ± 0.0043(95% CI)"(证明 n 够大)
- C:两者都要 → 指标要改名(如 `p025`/`p975` + `mean_ci_low`/`mean_ci_high`) - C:两者都要 → 指标要改名(如 `p025`/`p975` + `mean_ci_low`/`mean_ci_high`)
注意:`monte_carlo_probability` 里的 `ci_*` 已经是**估计量的 CI**(Wald 区间),所以这两个函数的 `ci_*` 注意:`monte_carlo_probability` 里的 `ci_*` 已经是**估计量的 CI**(Wilson 区间,决策 50),所以这两个函数的 `ci_*`
目前**含义不同**——这正是要给它们定口径的原因之一。 目前**含义不同**——这正是要给它们定口径的原因之一。
### ✅ 已拍板(决策 48) ### ✅ 已拍板(决策 48)
+26 -1
View File
@@ -43,6 +43,8 @@
| 19 | 只想着主路径那种值,忘了布尔 / NaN / 长度不对 | 2026-10-04 | | 19 | 只想着主路径那种值,忘了布尔 / NaN / 长度不对 | 2026-10-04 |
| 20 | "不简单"的信号:同一个信息要在调用方写两遍 | 2026-10-04 | | 20 | "不简单"的信号:同一个信息要在调用方写两遍 | 2026-10-04 |
| 21 | 公式里的 `^` 不是乘方,而且被 `except` 吞成 success=False | 2026-10-05 | | 21 | 公式里的 `^` 不是乘方,而且被 `except` 吞成 success=False | 2026-10-05 |
| 22 | 失败结果的 `message` 被包装层无条件覆盖 —— 对外永远报"成功" | 2026-10-05 |
| 23 | 体积/宽度这种"看着总为正"的量也会减反,维度奇偶还会把符号错误藏住 | 2026-10-05 |
--- ---
@@ -75,7 +77,10 @@
两处打架时合法用法会被 IDE 标红,真正的错误反而看不出来。 两处打架时合法用法会被 IDE 标红,真正的错误反而看不出来。
- 以后怎么防:改签名和改 docstring 必须同一次做完。 - 以后怎么防:改签名和改 docstring 必须同一次做完。
本项目所有 `random_state` 的注解统一写 `int | np.integer | np.random.Generator | None`。 本项目所有 `random_state` 的注解统一写 `int | np.integer | np.random.Generator | None`。
- 首次:2026-10-01|复发:1 - 复发(2026-10-05):`monte_carlo_probability` 写 `event: Callable[..., bool]`,docstring 却说"返回长度为 `n` 的**布尔数组**"
—— 注解只描述"返回一个 bool"。pyright 实测:这样写会**拒绝**任何注解良好的向量化事件函数(`ndarray` 与 `bool` 不兼容),
而运行期它一直都收得下。同一个坑从"收什么"换到了"返回什么":**注解是契约,不是备注**。
- 首次:2026-10-01|复发:1 → **2(2026-10-05)**
### 4. 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局 ### 4. 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局
@@ -377,5 +382,25 @@
这类"公式抄错"会被直接钉死;④ 记住 `except Exception` 越宽,错误越难看见。 这类"公式抄错"会被直接钉死;④ 记住 `except Exception` 越宽,错误越难看见。
- 首次:2026-10-05|复发:0 - 首次:2026-10-05|复发:0
### 22. 失败结果的 `message` 被包装层无条件覆盖 —— 对外永远报"成功"
- 现象:`monte_carlo_integral` 里内层 `monte_carlo` 已经返回 `success=False`(`func` 返回了标量),
但包装层接着无条件执行 `result.message = "蒙特卡洛多重积分成功:…"`。对外就是 `success=False` + 一句"成功"文案。
- 为什么会错:把 `result` 当成"自己的返回值"直接美化,忘了它是**可能失败**的通道。
`success` 和 `message` 是同一份状态的两个视图,分开写就一定有机会不一致 —— 与第 12 条(后面的 `if` 静默覆盖前面的)同源。
- 以后怎么防:包装函数里只改"成功才会有意义"的字段(`name` / `params` / `metrics`),改 `message` 一律包在
`if result.success:` 里;或者只**追加**、不覆盖。写完先跑一次**注定失败**的输入,看对外那两个字段是否自洽。
- 首次:2026-10-05|复发:0
### 23. 体积/宽度这种"看着总为正"的量也会减反,维度奇偶还会把符号错误藏住
- 现象:多重积分里体积写成 `wights = lows - highs`(名字打错 + 减反)。奇数维(1D、3D)估计值全为负、
而 **2D 看起来完全正常**(`∫∫ x·y` 得 +0.2499,正确值 0.25)—— 两个负号相乘抵消,把错误藏住了。
- 为什么会错:① "上界减下界"被顺手写成了"参数顺序相减",而变量名 `wights` 又不像 `lows` / `highs` 那样自带方向提示;
② 验算时只挑了一个二维例子(乘法的对称性恰好掩盖了符号),**维度是奇是偶没进脑子**。
同源教训见第 19 条(只想着主路径那种值)。
- 以后怎么防:长度/宽度/体积一律写 `high - low`,并在心里念一遍"大的减小的";
验算**至少各挑一个奇数维和一个偶数维**(1D + 3D 都能一秒现形);
拿到 `mean` 之后先跟解析值比**符号**,再看数值大小。
- 首次:2026-10-05|复发:0
+54 -5
View File
@@ -213,7 +213,7 @@ def monte_carlo_probability(
warning_message = "" warning_message = ""
if p_value <= 1e-3 or p_value >= 1 - 1e-3: if p_value <= 1e-3 or p_value >= 1 - 1e-3:
warning_message = "小概率事件 P < 1e-3 不建议使用monte_carlo_probability" warning_message = "小概率事件 P < 1e-3 或者高概率事件 P > 1 - 1e-3,不建议使用monte_carlo_probability"
metrics = { metrics = {
"probability": p_value, "probability": p_value,
@@ -253,8 +253,26 @@ def monte_carlo_integral(
Parameters Parameters
---------- ----------
func:
`f(X) -> 一批函数值`。`X` 是形状 `(n, d)` 的样本矩阵(一行一个样本),
返回长度 `n` 的数组(写成 `X[:, 0] ** 2` 这种逐行写法,不要写 Python 循环)。
bounds: bounds:
`[(lo, hi), ...]`,维度即积分重数。 `[(lo, hi), ...]`,维度即积分重数,要求每个 `lo < hi`。
n:
模拟次数,正整数。
random_state:
随机种子。`None` 时跟随 `core.set_seed` 的全局流,见 `core.random.get_rng`。
Returns
-------
`Result`:**`metrics["mean"]` 才是积分估计值** ≈ `∫f dV`;
`data` 是 `n` 个样本各自的贡献(`func(X_i) × 体积`),可拿去画随 `n` 的收敛图。
`params` 里额外记了 `bounds` 与 `dimension`。
Raises
------
ValueError:
`bounds` 里出现 `lo >= hi`。
Notes Notes
----- -----
@@ -265,11 +283,42 @@ def monte_carlo_integral(
-------- --------
>>> import numpy as np >>> import numpy as np
>>> r = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200000, random_state=0) >>> r = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200000, random_state=0)
>>> abs(r.data - 1 / 3) < 0.01 >>> abs(r.metrics["mean"] - 1 / 3) < 0.01 # ∫₀¹ x² dx = 1/3
True True
""" """
# TODO(L1) ★ S 档:配完整函数卡片 bounds_arr = np.asarray(bounds)
raise NotImplementedError
lows = bounds_arr[:, 0]
highs = bounds_arr[:, 1]
d = len(lows)
if np.any(lows >= highs):
raise ValueError("积分边界非法:下界必须严格小于上界")
widths = highs - lows
volume = np.prod(widths)
def sampler(rng, size=n):
return rng.uniform(low=lows, high=highs, size=(size, d))
def simulator(X):
vals = func(X)
if len(vals) != n:
raise ValueError(f"func 返回的结果数量({len(vals)})必须与模拟次数 n({n})一致")
return np.asarray(vals, dtype=float).ravel() * volume
result = monte_carlo(
sampler=sampler,
simulator=simulator,
n=n,
random_state=random_state,
)
result.name = "monte_carlo_integral"
result.params["bounds"] = bounds
result.params["dimension"] = d
result.message = f"蒙特卡洛多重积分成功:维度 {d},体积 {volume:.4f},模拟 {n} 次。"
return result
def random_walk( def random_walk(
+37 -4
View File
@@ -13,13 +13,48 @@ def _normal_batch(rng, size, **kwargs):
return rng.normal(0, 1, size) return rng.normal(0, 1, size)
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
def test_monte_carlo_integral_converges_to_analytic_value(): def test_monte_carlo_integral_converges_to_analytic_value():
"""∫₀¹ x² dx = 1/3。n=2e5 时误差应 < 0.01。""" """∫₀¹ x² dx = 1/3。n=2e5 时误差应 < 0.01。"""
from mcm_toolkit.solve import monte_carlo_integral from mcm_toolkit.solve import monte_carlo_integral
r = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200_000, random_state=0) r = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200_000, random_state=0)
assert abs(r.data - 1 / 3) < 0.01 assert r.success
assert abs(r.metrics["mean"] - 1 / 3) < 0.01 # 体积必须是正的
assert len(r.data) == 200_000 # data 是 n 个样本各自的贡献
def test_monte_carlo_integral_odd_dimension_sign():
"""∫∫∫ x dV over [0,1]³ = 1/2 —— 奇数维最容易暴露"体积符号写反"。"""
from mcm_toolkit.solve import monte_carlo_integral
r = monte_carlo_integral(lambda X: X[:, 0], bounds=[(0, 1)] * 3, n=200_000, random_state=0)
assert abs(r.metrics["mean"] - 0.5) < 0.01
def test_monte_carlo_integral_signed_bounds():
"""∫₀² x dx = 2 —— 边界带宽度时,体积算错和符号写反都会现形。"""
from mcm_toolkit.solve import monte_carlo_integral
r = monte_carlo_integral(lambda X: X[:, 0], bounds=[(0, 2)], n=200_000, random_state=0)
assert abs(r.metrics["mean"] - 2.0) < 0.02
def test_monte_carlo_integral_2d_and_params():
from mcm_toolkit.solve import monte_carlo_integral
bounds = [(0, 1), (0, 1)]
r = monte_carlo_integral(lambda X: X[:, 0] * X[:, 1], bounds=bounds, n=200_000, random_state=0)
assert abs(r.metrics["mean"] - 0.25) < 0.01
assert r.name == "monte_carlo_integral"
assert r.params["dimension"] == 2
assert r.params["bounds"] == bounds
def test_monte_carlo_integral_rejects_bad_bounds():
from mcm_toolkit.solve import monte_carlo_integral
with pytest.raises(ValueError):
monte_carlo_integral(lambda X: X[:, 0], bounds=[(1, 0)], n=100, random_state=0)
def test_monte_carlo_probability_of_standard_normal(): def test_monte_carlo_probability_of_standard_normal():
@@ -153,7 +188,6 @@ def test_monte_carlo_rejects_bad_arguments():
monte_carlo(_normal_batch, lambda x: x, n=0) monte_carlo(_normal_batch, lambda x: x, n=0)
def test_monte_carlo_probability_returns_documented_metrics(): def test_monte_carlo_probability_returns_documented_metrics():
from mcm_toolkit.solve import monte_carlo_probability from mcm_toolkit.solve import monte_carlo_probability
@@ -198,7 +232,6 @@ def test_monte_carlo_probability_higher_confidence_is_wider():
assert width(0.99) > width(0.90) assert width(0.99) > width(0.90)
def test_monte_carlo_probability_ci_matches_statsmodels_wilson(): def test_monte_carlo_probability_ci_matches_statsmodels_wilson():
"""区间必须对得上独立实现(statsmodels 的 wilson)—— `^` 写成异或那次就靠它拦住。""" """区间必须对得上独立实现(statsmodels 的 wilson)—— `^` 写成异或那次就靠它拦住。"""
from statsmodels.stats.proportion import proportion_confint from statsmodels.stats.proportion import proportion_confint