This commit is contained in:
2026-09-11 17:24:07 +08:00
parent 4699b0d45f
commit 30df22e8b2
240 changed files with 3391 additions and 64 deletions
+95
View File
@@ -0,0 +1,95 @@
"""训练/部署观测和动作的一致性,以及RL改进的目标口径。"""
import importlib.util
import math
from pathlib import Path
import pytest
np = pytest.importorskip('numpy')
pytest.importorskip('gymnasium')
from mcm26b.rl.env import LocalizationEnv
from mcm26b.rl.features import action_mask, build_observation, candidate_actions
def test_training_observation_matches_runtime_channel_belief():
env = LocalizationEnv(problem=3, handoff_probability=1)
obs, _ = env.reset(seed=1)
belief = env._belief.channels[env._channel]
expected = build_observation(belief.region, env._client.state.position, env._channel,
env.config, env.problem, env._blocked, belief,
virtual_time_s=env._client.state.virtual_time_s,
receiver_channel=env._client.state.channel)
np.testing.assert_array_equal(obs, expected)
assert obs[7] == np.float32(1/12)
assert tuple(obs[14:17]) == (0,1,0)
assert env._client.state.channel != env._channel
env.close()
def test_mask_rejects_measured_and_duplicate_candidates():
env = LocalizationEnv(problem=3)
env.reset(seed=17)
belief = env._belief.channels[env._channel]
region = belief.region
position = env._client.state.position
candidates = candidate_actions(region, position, env.config)
belief.measurements.append(dict(position=candidates[0], result='no_signal', svd_deg=None))
mask = env.action_masks()
assert not mask[0]
keys = [(round(p[0]*2),round(p[1]*2)) for p,v in zip(candidates,mask) if v]
assert len(keys) == len(set(keys))
assert any(mask)
assert all(math.dist(p,region.mec()[0]) <= 1000-region.mec()[1]+1e-6
for p,v in zip(candidates,mask) if v)
env.close()
def test_q4_blocked_sequence_resets_on_direction():
env = LocalizationEnv(problem=4,directional_probability=1,handoff_probability=0)
env.reset(seed=7)
for expected in ('no_signal','direction'):
region = env._belief.candidate_set(env._channel)
candidates = candidate_actions(region,env._client.state.position,env.config,env._blocked)
# 只有测试端可用真值选择一个指定分支,策略从不接触该接口。
index = next(i for i,p in enumerate(candidates)
if env._simulator._observe(p,env._channel)[0] == expected)
_, _, terminated, truncated, _ = env.step(index)
assert len(env._blocked) == (1 if expected=='no_signal' else 0)
if expected=='no_signal':
assert not terminated and not truncated
env.close()
def test_success_reward_is_constant_minus_actual_virtual_cost():
env = LocalizationEnv(problem=3,handoff_probability=0)
env.reset(seed=3)
total = 0
for _ in range(16):
legal = np.flatnonzero(env.action_masks())
action = int(0 if 0 in legal else legal[0])
_, reward, terminated, truncated, info = env.step(action)
total += reward
if terminated or truncated:
break
assert info['cleared']
assert total == pytest.approx(20-info['virtual_cost_s']/100)
assert env._client.state.exited
def test_bc_and_ppo_forward_mapping_agree():
torch = pytest.importorskip('torch')
from stable_baselines3 import PPO
spec = importlib.util.spec_from_file_location('pretrain',Path('scripts/pretrain_rl_expert.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
torch.set_num_threads(1)
env = LocalizationEnv()
ppo = PPO('MlpPolicy',env,policy_kwargs={'net_arch':[128,128]},device='cpu',n_steps=16,batch_size=8)
bc = module.BCMlp()
state = bc.net.state_dict()
ppo.policy.mlp_extractor.policy_net.load_state_dict({k:v for k,v in state.items() if not k.startswith('4.')})
ppo.policy.action_net.load_state_dict({'weight':state['4.weight'],'bias':state['4.bias']})
x = torch.randn(10,401)
torch.testing.assert_close(bc(x),ppo.policy.action_net(ppo.policy.mlp_extractor.policy_net(x)))
env.close()