强化学习第一版

This commit is contained in:
2026-09-11 16:56:39 +08:00
parent 2bc4c7ed82
commit 4699b0d45f
78 changed files with 27978 additions and 706 deletions
+2 -1
View File
@@ -5,12 +5,13 @@ import math
import pytest
from mcm26b.config import ProblemConfig
from mcm26b.harness import LocalSimTransport, LocalSimulator
from mcm26b.planner import ClearCertificate, DirectionalExtension
from mcm26b.planner.bayes_loop import BayesLoop, Task
from mcm26b.planner.seven import validate_seven_coverage
from mcm26b.protocol.client import SimulatorClient
from mcm26b.protocol.rules import DIRECTION, NO_SIGNAL
from mcm26b.sim import LocalSimulator, LocalSimTransport, Source
from referee import Source
def setup(sources=(), cls=BayesLoop, config=ProblemConfig()):
+102
View File
@@ -0,0 +1,102 @@
"""RL 扩展验收:特征/动作布局、训练环境闭环与模型接口守卫。
RL 依赖属于可选组 [dependency-groups] rl;未安装 numpy/gymnasium 时
全部跳过(运行闭环不依赖它们)。
"""
import math
import unittest
from pathlib import Path
pytest = None
try:
import pytest
numpy = pytest.importorskip("numpy")
pytest.importorskip("gymnasium")
except Exception: # pragma: no cover - 无 RL 依赖环境
numpy = None
if numpy is not None:
from mcm26b.belief import CandidateRegion
from mcm26b.config import ProblemConfig
from mcm26b.rl._constants import GLOBAL_DIM, NUM_ACTIONS, OBS_DIM, PER_ACTION_DIM
from mcm26b.rl.env import LocalizationEnv
from mcm26b.rl.features import build_observation, candidate_actions
from mcm26b.rl.policy import default_model_path
CONFIG = ProblemConfig()
STATION = (0.0, 0.0)
BEARING = 30.0
def _region():
region = CandidateRegion.initial(STATION, CONFIG)
region.clip_wedge(STATION, BEARING, 1.005)
return region
@unittest.skipIf(numpy is None, "RL 依赖组未安装")
class FeatureLayoutTests(unittest.TestCase):
def test_action_candidates_have_stable_count_and_order(self):
region = _region()
first = candidate_actions(region, STATION, CONFIG, [])
self.assertEqual(len(first), NUM_ACTIONS)
second = candidate_actions(region, STATION, CONFIG, [])
self.assertEqual(first, second)
self.assertEqual(first[0], region.mec()[0]) # 0 号候选 = MEC 圆心
def test_observation_dimension_matches_model_contract(self):
region = _region()
obs = build_observation(region, STATION, 3, CONFIG, problem=3,
blocked=[], virtual_time_s=0.0)
self.assertEqual(obs.shape, (OBS_DIM,))
self.assertEqual(OBS_DIM, GLOBAL_DIM + NUM_ACTIONS * PER_ACTION_DIM)
def test_blocked_changes_observation_but_not_layout(self):
region = _region()
clean = build_observation(region, STATION, 3, CONFIG, problem=4,
blocked=[], virtual_time_s=0.0)
blocked = build_observation(region, STATION, 3, CONFIG, problem=4,
blocked=[(100.0, 0.0)], virtual_time_s=0.0)
self.assertEqual(clean.shape, blocked.shape)
self.assertFalse(numpy.allclose(clean, blocked))
@unittest.skipIf(numpy is None, "RL 依赖组未安装")
class TrainingEnvironmentTests(unittest.TestCase):
def test_q3_episode_reset_and_step_shapes(self):
env = LocalizationEnv(problem=3)
obs, info = env.reset(seed=1)
self.assertEqual(obs.shape, (OBS_DIM,))
self.assertIn("source", info)
obs, reward, terminated, truncated, info = env.step(0)
self.assertEqual(obs.shape, (OBS_DIM,))
self.assertIsInstance(reward, float)
self.assertFalse(terminated and truncated)
def test_q4_directional_episode_reports_blocked_or_fallback(self):
env = LocalizationEnv(problem=4, directional_probability=1.0)
obs, info = env.reset(seed=7)
self.assertTrue(info["source"]["directional"])
terminated = truncated = False
for _ in range(env.max_episode_steps + 2):
obs, reward, terminated, truncated, info = env.step(
env.action_space.sample())
if terminated or truncated:
break
self.assertTrue(terminated or truncated)
# 定向分支:要么清除,要么走过后备清除/截断语义。
self.assertIn("fallback_attempts", info)
def test_missing_model_raises_clear_error(self):
from mcm26b.rl.policy import PointSelectionPolicy
with self.assertRaises(RuntimeError):
PointSelectionPolicy("output/rl/does-not-exist.zip")
def test_default_model_paths(self):
self.assertEqual(default_model_path(3), Path("output/rl/q3_ppo.zip"))
self.assertEqual(default_model_path(4), Path("output/rl/q4_ppo.zip"))
if __name__ == "__main__":
unittest.main()