feat(solve): 审查 markov_simulate:工程侧收口 + 9 条测试
用户实现的第一版 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
This commit is contained in:
1 parent
9368dd91cb
commit
dd9bd638b8
5 files changed
+323
-23
No files matched your search
@@ -407,6 +407,108 @@ def test_random_walk_docstring_example_runs():
|
||||
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():
|
||||
|
||||
Reference in new issue
Block a user