用户实现的第一版 markov_simulate 算法本体经探针实测正确(终点频率 [0.657, 0.343] 对照平稳分布 [2/3, 1/3],同种子可复现),本次只动工程侧,未改任何算法行: - ruff 8 条全清(I001 导入排序 + 5 处 W293 / 2 处 W291) - docstring 从一行补全为 Parameters / Returns / Raises / Notes / Examples - 文案:马尔科夫→马尔可夫(3 处)、initail→initial、半角逗号→全角、paths[:,0]→paths[:, 0] - arr, kind = as_array(...) 的 kind 全程未用 → arr, _ - tests/test_solve.py 新增 9 条 markov_simulate 测试(此前 0 条) - STATUS.md 新增决策 56(initial 语义待用户拍板,A/B/C 三选项)与 5 条待办 - CHANGELOG 与 docs/dev/personal-pitfalls.md 易错点 27(入口分流后两条分支口径矛盾) 校验:pytest 188 passed / 31 skipped(219 收集);ruff 0;pyright 0
564 lines
21 KiB
Python
564 lines
21 KiB
Python
"""solve 包测试。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
_NOT_IMPLEMENTED = "未实现:见 docs/03-roadmap.md L0/L1"
|
||
|
||
|
||
def _normal_batch(rng, size, **kwargs):
|
||
"""`monte_carlo` 的 sampler 契约:收下实现传来的 `size`,一次抽够。"""
|
||
return rng.normal(0, 1, size)
|
||
|
||
|
||
def test_monte_carlo_integral_converges_to_analytic_value():
|
||
"""∫₀¹ x² dx = 1/3。n=2e5 时误差应 < 0.01。"""
|
||
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)
|
||
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():
|
||
"""四类非法 bounds 都要在入口抛 ValueError:区间反了 / 漏括号 / 空 / 参差不齐。"""
|
||
from mcm_toolkit.solve import monte_carlo_integral
|
||
|
||
for bad in ([(1, 0)], [0, 1], [], [(0, 1), (0, 2, 3)]):
|
||
with pytest.raises(ValueError):
|
||
monte_carlo_integral(lambda X: X[:, 0], bounds=bad, n=100, random_state=0)
|
||
|
||
|
||
def test_monte_carlo_integral_failure_message_stays_failure():
|
||
"""失败时 message 不能被包装层改写成"成功"(易错点 22)。"""
|
||
from mcm_toolkit.solve import monte_carlo_integral
|
||
|
||
r = monte_carlo_integral(lambda X: 1.0, bounds=[(0, 1)], n=100, random_state=0)
|
||
assert r.success is False
|
||
assert "成功" not in r.message
|
||
assert "失败" in r.message
|
||
assert r.params["dimension"] == 1 # 失败也要留下追溯信息
|
||
|
||
|
||
def test_monte_carlo_integral_accepts_numpy_integer_n():
|
||
"""§4.7:`np.int64` 不是 `int` 的子类,注解与运行期都要收(易错点 1)。"""
|
||
from mcm_toolkit.solve import monte_carlo_integral
|
||
|
||
r = monte_carlo_integral(lambda X: X[:, 0], bounds=[(0, 1)], n=np.int64(1000), random_state=0)
|
||
assert r.success
|
||
assert len(r.data) == 1000
|
||
|
||
|
||
def test_monte_carlo_probability_of_standard_normal():
|
||
"""P(Z > 0) = 0.5,n=5000 时误差应 < 0.05。"""
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=5000, random_state=0)
|
||
assert r.success
|
||
assert abs(r.metrics["probability"] - 0.5) < 0.05
|
||
|
||
|
||
def test_monte_carlo_is_reproducible():
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
a = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=1000, random_state=9)
|
||
b = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=1000, random_state=9)
|
||
assert a.metrics["probability"] == b.metrics["probability"]
|
||
|
||
|
||
def test_monte_carlo_returns_n_results_and_documented_metrics():
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
r = monte_carlo(_normal_batch, lambda x: x**2, n=2000, random_state=0)
|
||
assert r.success
|
||
assert r.data.shape == (2000,)
|
||
assert set(r.metrics) == {"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}
|
||
|
||
|
||
def test_monte_carlo_calls_simulator_only_once():
|
||
"""`simulator` 只被调用一次 —— 这是"必须向量化"的可执行版本。"""
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
counter = {"calls": 0}
|
||
|
||
def simulator(params):
|
||
counter["calls"] += 1
|
||
return params
|
||
|
||
r = monte_carlo(_normal_batch, simulator, n=1000, random_state=0)
|
||
assert r.success
|
||
assert counter["calls"] == 1
|
||
|
||
|
||
def test_monte_carlo_data_is_reproducible_with_same_seed():
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
a = monte_carlo(_normal_batch, lambda x: x, n=2000, random_state=11)
|
||
b = monte_carlo(_normal_batch, lambda x: x, n=2000, random_state=11)
|
||
assert np.array_equal(a.data, b.data)
|
||
|
||
|
||
def test_monte_carlo_follows_set_seed_when_random_state_omitted():
|
||
from mcm_toolkit.core import set_seed
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
set_seed(5)
|
||
a = monte_carlo(_normal_batch, lambda x: x, n=500)
|
||
set_seed(5)
|
||
b = monte_carlo(_normal_batch, lambda x: x, n=500)
|
||
assert np.array_equal(a.data, b.data)
|
||
|
||
|
||
def test_monte_carlo_estimates_known_expectation():
|
||
"""E[X²] = 1(X ~ N(0, 1));n = 2e5 时误差应 < 0.02。"""
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
r = monte_carlo(_normal_batch, lambda x: x**2, n=200_000, random_state=0)
|
||
assert abs(r.metrics["mean"] - 1.0) < 0.02
|
||
|
||
|
||
def test_monte_carlo_docstring_example_runs():
|
||
"""docstring 的 Examples 必须真的能跑通(此前那一版是坏的)。"""
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
draw = lambda rng, size, **k: rng.normal(0, 1, size) # noqa: E731
|
||
r = monte_carlo(draw, lambda x: x**2, n=10000, random_state=0)
|
||
assert r.success
|
||
assert abs(r.metrics["mean"] - 1.0) < 0.1
|
||
|
||
|
||
def test_monte_carlo_accepts_numpy_integer_n():
|
||
"""§4.7:`np.int64` 不是 `int` 的子类,不能漏。"""
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
r = monte_carlo(_normal_batch, lambda x: x, n=np.int64(100), random_state=0)
|
||
assert r.success
|
||
assert r.data.shape == (100,)
|
||
|
||
|
||
def test_monte_carlo_counts_boolean_outcomes():
|
||
"""布尔结果按 0/1 统计 —— 事件概率是 MC 最常见的用法。"""
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
r = monte_carlo(
|
||
lambda rng, size, **k: rng.normal(0, 1, size) > 0,
|
||
lambda x: x,
|
||
n=5000,
|
||
random_state=0,
|
||
)
|
||
assert r.success
|
||
assert r.data.dtype == float
|
||
assert abs(r.metrics["mean"] - 0.5) < 0.05
|
||
|
||
|
||
def test_monte_carlo_rejects_nan_results():
|
||
"""结果里出现 NaN 就判失败,不静默给出一列 nan 指标。"""
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
r = monte_carlo(
|
||
lambda rng, size, **k: np.r_[np.zeros(size - 1), np.nan],
|
||
lambda x: x,
|
||
n=10,
|
||
random_state=0,
|
||
)
|
||
assert not r.success
|
||
assert "NaN" in r.message
|
||
|
||
|
||
def test_monte_carlo_rejects_bad_arguments():
|
||
from mcm_toolkit.solve import monte_carlo
|
||
|
||
with pytest.raises(TypeError):
|
||
monte_carlo(123, lambda x: x) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(TypeError):
|
||
monte_carlo(_normal_batch, 123) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(TypeError):
|
||
monte_carlo(_normal_batch, lambda x: x, n=True)
|
||
with pytest.raises(TypeError):
|
||
monte_carlo(_normal_batch, lambda x: x, n=10.0) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(ValueError):
|
||
monte_carlo(_normal_batch, lambda x: x, n=0)
|
||
|
||
|
||
def test_monte_carlo_probability_returns_documented_metrics():
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=20000, random_state=0)
|
||
assert r.success
|
||
assert set(r.metrics) == {"probability", "ci_low", "ci_high", "std_error"}
|
||
assert r.data.shape == (20000,)
|
||
assert 0.0 <= r.metrics["ci_low"] <= r.metrics["probability"]
|
||
assert r.metrics["probability"] <= r.metrics["ci_high"] <= 1.0
|
||
|
||
|
||
def test_monte_carlo_probability_std_error_matches_binomial_formula():
|
||
"""`std_error` 就是比例的标准误 √(p(1-p)/n)。"""
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
n = 20000
|
||
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=n, random_state=0)
|
||
p = r.metrics["probability"]
|
||
assert np.isclose(r.metrics["std_error"], np.sqrt(p * (1 - p) / n))
|
||
|
||
|
||
def test_monte_carlo_probability_ci_narrows_with_n():
|
||
"""区间宽度随 n 增大而收窄(O(1/√n))。"""
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
def width(n):
|
||
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=n, random_state=0)
|
||
return r.metrics["ci_high"] - r.metrics["ci_low"]
|
||
|
||
assert width(100_000) < width(1_000)
|
||
|
||
|
||
def test_monte_carlo_probability_higher_confidence_is_wider():
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
def width(confidence):
|
||
r = monte_carlo_probability(
|
||
lambda z: z > 0, _normal_batch, n=5000, random_state=0, confidence=confidence
|
||
)
|
||
return r.metrics["ci_high"] - r.metrics["ci_low"]
|
||
|
||
assert width(0.99) > width(0.90)
|
||
|
||
|
||
def test_monte_carlo_probability_ci_matches_statsmodels_wilson():
|
||
"""区间必须对得上独立实现(statsmodels 的 wilson)—— `^` 写成异或那次就靠它拦住。"""
|
||
from statsmodels.stats.proportion import proportion_confint
|
||
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
n = 5000
|
||
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=n, random_state=0)
|
||
count = int(r.data.sum())
|
||
lo, hi = proportion_confint(count, n, alpha=0.05, method="wilson")
|
||
assert np.isclose(r.metrics["ci_low"], lo)
|
||
assert np.isclose(r.metrics["ci_high"], hi)
|
||
|
||
|
||
def test_monte_carlo_probability_rejects_bad_confidence():
|
||
from mcm_toolkit.solve import monte_carlo_probability
|
||
|
||
for bad in (0.0, 1.0, -0.5, 1.5):
|
||
with pytest.raises(ValueError):
|
||
monte_carlo_probability(lambda z: z > 0, _normal_batch, n=100, confidence=bad)
|
||
|
||
|
||
def test_random_walk_shape_and_start():
|
||
"""`data` 形状 (n_paths, n_steps + 1),第 0 列是起点。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
r = random_walk(n_steps=100, n_paths=5, start=2.5, random_state=0)
|
||
assert r.success
|
||
assert r.data.shape == (5, 101)
|
||
assert np.all(r.data[:, 0] == 2.5)
|
||
|
||
|
||
def test_random_walk_default_steps_are_plus_minus_one():
|
||
"""默认是对称随机游走:每一步都恰好是 ±1。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
r = random_walk(n_steps=200, n_paths=3, random_state=0)
|
||
assert set(np.unique(np.diff(r.data, axis=1))) == {-1.0, 1.0}
|
||
|
||
|
||
def test_random_walk_is_reproducible():
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
a = random_walk(n_steps=50, n_paths=4, random_state=7)
|
||
b = random_walk(n_steps=50, n_paths=4, random_state=7)
|
||
assert np.array_equal(a.data, b.data)
|
||
|
||
|
||
def test_random_walk_accepts_custom_step_sampler():
|
||
"""换成高斯增量就是带漂移的高斯游走 —— 建模差异全在 step_sampler 里。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
r = random_walk(
|
||
n_steps=100,
|
||
n_paths=3,
|
||
step_sampler=lambda rng, size: rng.normal(0.5, 1.0, size),
|
||
random_state=0,
|
||
)
|
||
assert r.success
|
||
assert r.data.shape == (3, 101)
|
||
assert abs(r.data[:, -1].mean() - 50.0) < 20.0 # 100 步 × 漂移 0.5
|
||
|
||
|
||
def test_random_walk_rejects_bad_arguments():
|
||
"""§4.7 的分工:类型不对抛 TypeError,取值不对(<= 0)抛 ValueError。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
with pytest.raises(TypeError):
|
||
random_walk(n_steps=2.5) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(TypeError):
|
||
random_walk(n_steps=True)
|
||
with pytest.raises(TypeError):
|
||
random_walk(n_steps=10, n_paths=1.5) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(ValueError):
|
||
random_walk(n_steps=0)
|
||
with pytest.raises(ValueError):
|
||
random_walk(n_steps=10, n_paths=0)
|
||
with pytest.raises(TypeError):
|
||
random_walk(n_steps=10, start="a") # type: ignore[arg-type] # 就是要传错
|
||
|
||
|
||
def test_random_walk_rejects_2d_step_sampler():
|
||
"""`step_sampler` 必须返回**一维** —— 二维返回(含方阵)一律判失败(易错点 25 的根治)。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
def transposed(rng, size):
|
||
return rng.normal(0, 1, (4, 3)) # 无论要多少,一律给二维
|
||
|
||
r = random_walk(n_steps=4, n_paths=3, step_sampler=transposed, random_state=0)
|
||
assert r.success is False # 失败也要返回 Result,不能抛给调用方
|
||
assert "长度" in r.message
|
||
assert r.params["n_paths"] == 3 # 失败也要留下追溯信息
|
||
assert r.data is None
|
||
|
||
# 旧方案在 n_paths == n_steps 时对"转置"完全无感;现在同样拦住
|
||
assert random_walk(n_steps=3, n_paths=3, step_sampler=transposed, random_state=0).success is False
|
||
|
||
|
||
def test_random_walk_step_sampler_gets_flat_size_and_is_row_major():
|
||
"""契约:`size` 是整数 `n_paths * n_steps`,一维增量按**行主序**切成 n_paths 行。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
seen = {}
|
||
|
||
def sampler(rng, size):
|
||
seen["size"] = size
|
||
return np.arange(1, size + 1, dtype=float) # 1, 2, 3, 4, 5, 6
|
||
|
||
r = random_walk(n_steps=3, n_paths=2, step_sampler=sampler, random_state=0)
|
||
assert seen["size"] == 6 # 整数,不是 (n_paths, n_steps)
|
||
assert r.data.tolist() == [[0, 1, 3, 6], [0, 4, 9, 15]] # cumsum(1,2,3) / cumsum(4,5,6)
|
||
|
||
|
||
def test_random_walk_rejects_nan_steps():
|
||
"""增量含 NaN 判失败 —— 与 monte_carlo 的 NaN 口径(决策 48)一致。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
r = random_walk(
|
||
n_steps=4,
|
||
n_paths=2,
|
||
step_sampler=lambda rng, size: np.full(size, np.nan),
|
||
random_state=0,
|
||
)
|
||
assert r.success is False
|
||
assert "NaN" in r.message
|
||
|
||
|
||
def test_random_walk_accepts_numpy_integer():
|
||
"""§4.7:`np.int64` 不是 `int` 的子类,注解与运行期都要收(易错点 1)。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
r = random_walk(n_steps=np.int64(20), n_paths=np.int64(3), random_state=0)
|
||
assert r.success
|
||
assert r.data.shape == (3, 21)
|
||
|
||
|
||
def test_random_walk_metrics_are_terminal_positions():
|
||
"""终点位置的标准差按 √t 扩散(t = n_steps)—— 随机游走的核心性质。"""
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
n_steps = 100
|
||
r = random_walk(n_steps=n_steps, n_paths=2000, start=5.0, random_state=0)
|
||
assert abs(r.metrics["mean_terminal"] - 5.0) < 5.0 # 对称游走,期望还在起点附近
|
||
assert abs(r.metrics["std_terminal"] - np.sqrt(n_steps)) < 5.0 # √t 扩散
|
||
|
||
|
||
def test_random_walk_docstring_example_runs():
|
||
from mcm_toolkit.solve import random_walk
|
||
|
||
r = random_walk(n_steps=100, n_paths=5, random_state=0)
|
||
assert r.data.shape == (5, 101)
|
||
assert np.all(np.abs(np.diff(r.data, axis=1)) == 1)
|
||
|
||
def test_markov_simulate_shape_start_and_states():
|
||
"""`data` 形状 (n_paths, steps + 1),第 0 列是初始状态,取值只能是合法状态编号。"""
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||
r = markov_simulate(P, initial=0, steps=100, n_paths=5, random_state=0)
|
||
assert r.success
|
||
assert r.data.shape == (5, 101)
|
||
assert np.all(r.data[:, 0] == 0)
|
||
assert set(np.unique(r.data)) <= {0, 1}
|
||
assert np.issubdtype(r.data.dtype, np.integer)
|
||
|
||
|
||
def test_markov_simulate_absorbing_states_never_move():
|
||
"""单位转移矩阵:状态自锁,整条路径恒等于初始状态(确定性自检)。"""
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
r = markov_simulate(np.eye(2), initial=1, steps=30, n_paths=4, random_state=0)
|
||
assert r.success
|
||
assert np.all(r.data == 1)
|
||
assert r.metrics["mean_terminal"] == 1.0
|
||
assert r.metrics["std_terminal"] == 0.0
|
||
|
||
|
||
def test_markov_simulate_is_reproducible():
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.7, 0.3], [0.4, 0.6]])
|
||
a = markov_simulate(P, initial=0, steps=40, n_paths=6, random_state=11)
|
||
b = markov_simulate(P, initial=0, steps=40, n_paths=6, random_state=11)
|
||
c = markov_simulate(P, initial=0, steps=40, n_paths=6, random_state=12)
|
||
assert np.array_equal(a.data, b.data)
|
||
assert not np.array_equal(a.data, c.data)
|
||
|
||
|
||
def test_markov_simulate_terminal_frequency_matches_stationary():
|
||
"""终点频率收敛到平稳分布 [2/3, 1/3] —— 马尔可夫链模拟的核心性质。"""
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||
r = markov_simulate(P, initial=0, steps=200, n_paths=20000, random_state=0)
|
||
freq = np.bincount(r.data[:, -1], minlength=2) / r.data.shape[0]
|
||
assert np.allclose(freq, [2 / 3, 1 / 3], atol=0.02)
|
||
|
||
|
||
def test_markov_simulate_metrics_are_terminal_states():
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||
r = markov_simulate(P, initial=0, steps=50, n_paths=500, random_state=3)
|
||
assert r.metrics["mean_terminal"] == float(np.mean(r.data[:, -1]))
|
||
assert r.metrics["std_terminal"] == float(np.std(r.data[:, -1]))
|
||
|
||
|
||
def test_markov_simulate_rejects_bad_transition():
|
||
"""非方阵、行和不为 1 都要在入口直接报错,不能悄悄模拟。"""
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
with pytest.raises(ValueError):
|
||
markov_simulate(np.array([[0.5, 0.5, 0.0], [0.2, 0.8, 0.0]]), initial=0)
|
||
with pytest.raises(ValueError):
|
||
markov_simulate(np.array([[0.5, 0.4], [0.2, 0.8]]), initial=0)
|
||
|
||
|
||
def test_markov_simulate_rejects_bad_arguments():
|
||
"""§4.7:步数 / 路径数的类型与正负都要卡住(bool 先判,易错点 1)。"""
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||
with pytest.raises(ValueError):
|
||
markov_simulate(P, initial=0, steps=0)
|
||
with pytest.raises(ValueError):
|
||
markov_simulate(P, initial=0, n_paths=0)
|
||
with pytest.raises(TypeError):
|
||
markov_simulate(P, initial=0, steps=1.5) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(TypeError):
|
||
markov_simulate(P, initial=0, n_paths=1.5) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(TypeError):
|
||
markov_simulate(P, initial=0, steps=True) # type: ignore[arg-type] # 就是要传错
|
||
with pytest.raises(TypeError):
|
||
markov_simulate(P, initial=0, n_paths=True) # type: ignore[arg-type] # 就是要传错
|
||
|
||
|
||
def test_markov_simulate_accepts_numpy_integer():
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||
r = markov_simulate(P, initial=0, steps=np.int64(10), n_paths=np.int64(2), random_state=0)
|
||
assert r.success
|
||
assert r.data.shape == (2, 11)
|
||
|
||
|
||
def test_markov_simulate_docstring_example_runs():
|
||
from mcm_toolkit.solve import markov_simulate
|
||
|
||
P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||
r = markov_simulate(P, initial=0, steps=50, n_paths=4, random_state=0)
|
||
assert r.data.shape == (4, 51)
|
||
assert np.all(r.data[:, 0] == 0)
|
||
|
||
|
||
|
||
|
||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||
def test_markov_steady_state_two_state_chain():
|
||
"""P=[[0.9,0.1],[0.2,0.8]] 的平稳分布是 [2/3, 1/3]。"""
|
||
from mcm_toolkit.solve import markov_steady_state
|
||
|
||
r = markov_steady_state(np.array([[0.9, 0.1], [0.2, 0.8]]))
|
||
assert np.allclose(r.data, [2 / 3, 1 / 3], atol=1e-6)
|
||
|
||
|
||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||
def test_knapsack_dp_known_optimum():
|
||
from mcm_toolkit.solve import knapsack_dp
|
||
|
||
r = knapsack_dp(weights=[1, 3, 4, 5], values=[1, 4, 5, 7], capacity=7)
|
||
assert np.isclose(r.metrics["total_value"], 9.0)
|
||
assert np.isclose(r.metrics["total_weight"], 7.0)
|
||
|
||
|
||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||
def test_linear_program_basic():
|
||
from mcm_toolkit.solve import linear_program
|
||
|
||
r = linear_program(c=[1, 2], A_ub=[[1, 1]], b_ub=[4], bounds=[(0, None)] * 2)
|
||
assert r.success
|
||
assert np.isclose(r.metrics["objective"], 4.0)
|
||
|
||
|
||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||
def test_mm1_metrics_closed_form():
|
||
from mcm_toolkit.solve import mm1_metrics
|
||
|
||
r = mm1_metrics(arrival_rate=2, service_rate=3)
|
||
assert np.isclose(r.metrics["rho"], 2 / 3, rtol=1e-6)
|
||
assert np.isclose(r.metrics["L"], 2.0, rtol=1e-6)
|
||
|
||
|
||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||
def test_mm1_rejects_unstable_system():
|
||
"""λ >= μ 时系统不稳定,应 success=False 而不是返回 inf。"""
|
||
from mcm_toolkit.solve import mm1_metrics
|
||
|
||
assert mm1_metrics(arrival_rate=3, service_rate=3).success is False
|
||
|
||
|
||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||
def test_simulated_annealing_finds_quadratic_minimum():
|
||
from mcm_toolkit.solve import simulated_annealing
|
||
|
||
f = lambda v: (v[0] - 3.0) ** 2 + (v[1] + 1.0) ** 2 # noqa: E731
|
||
r = simulated_annealing(f, x0=[0.0, 0.0], n_iter=5000, random_state=0)
|
||
assert np.allclose(r.params["x"], [3.0, -1.0], atol=0.1)
|