Files
Mujoco_WASM/training_server/tests/test_obstacle_env.py
T
chenlin 438e56bcc8
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
feat(training): release V0.9.1 避障训练与基础策略迁移
2026-09-08 10:50:13 +08:00

224 lines
9.9 KiB
Python

"""Optional installed-mjlab checks; GPU smoke is opt-in, never a full training run."""
import importlib.util
import os
import sys
import unittest
from pathlib import Path
from types import SimpleNamespace
SERVICE_ROOT = Path(__file__).resolve().parents[1]
for root in (SERVICE_ROOT, SERVICE_ROOT / "rl"):
sys.path.insert(0, str(root))
HAS_MJLAB = importlib.util.find_spec("mjlab") is not None
@unittest.skipUnless(HAS_MJLAB, "mjlab is not installed in this Python")
class ObstacleContractTest(unittest.TestCase):
def test_training_config_file_is_revalidated(self):
import json
import tempfile
from scripts.train import _load_task_config
from task_config import OBSTACLE_TASK, TaskConfigError, validate_task_config
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "training_config.json"
custom = validate_task_config(OBSTACLE_TASK, {"sensorCfg": {"fov": 60}}, 42)
source.write_text(json.dumps(custom))
self.assertEqual(_load_task_config(OBSTACLE_TASK, str(source), 42), custom)
with self.assertRaises(ValueError):
_load_task_config(OBSTACLE_TASK, str(source), 43)
custom["sensorCfg"]["maxDistance"] = -1
source.write_text(json.dumps(custom))
with self.assertRaises(TaskConfigError):
_load_task_config(OBSTACLE_TASK, str(source), 42)
source.write_text("x" * (128 * 1024 + 1))
with self.assertRaises(ValueError):
_load_task_config(OBSTACLE_TASK, str(source), 42)
def test_pattern_order_normalization_and_body_offsets(self):
import torch
from src.tasks.obstacle_avoidance.mdp import ForwardFanPatternCfg, forward_depth
offsets, directions = ForwardFanPatternCfg(fov=90).generate_rays(None, "cpu")
self.assertEqual(tuple(directions.shape), (32, 3))
torch.testing.assert_close(offsets, torch.tensor([0.3, 0, 0.05]).repeat(32, 1))
torch.testing.assert_close(directions.norm(dim=1), torch.ones(32))
self.assertAlmostEqual(directions[0, 1].item(), -(0.5**0.5), places=6)
self.assertAlmostEqual(directions[-1, 1].item(), 0.5**0.5, places=6)
scene = {
"forward_scan": SimpleNamespace(
data=SimpleNamespace(
distances=torch.tensor([[-1, 0, 1, 4, 8]], dtype=torch.float32)
)
)
}
result = forward_depth(SimpleNamespace(scene=scene), max_distance=4)
torch.testing.assert_close(result, torch.tensor([[1, 0, 0.25, 1, 1]]))
def test_navigation_matches_body_heading_and_arrival_stop(self):
import torch
from src.tasks.obstacle_avoidance.mdp import NavigationCommandCfg
data = SimpleNamespace(
root_link_pos_w=torch.tensor([[-5.0, 0, 0.32]]),
root_link_quat_w=torch.tensor([[1.0, 0, 0, 0]]),
)
class Scene(dict):
env_origins = torch.tensor([[-5.0, 0, 0]])
env = SimpleNamespace(
num_envs=1, device="cpu", scene=Scene(robot=SimpleNamespace(data=data))
)
command = NavigationCommandCfg(resampling_time_range=(1e9, 1e9)).build(env)
torch.testing.assert_close(command.command, torch.tensor([[0.6, 0, 0]]))
self.assertAlmostEqual(command.errors()[1].item(), 10)
data.root_link_quat_w[:] = torch.tensor([[0.5**0.5, 0, 0, 0.5**0.5]])
self.assertAlmostEqual(command.command[0, 2].item(), -1)
data.root_link_pos_w[0, 0] = 4.8
torch.testing.assert_close(command.command, torch.zeros((1, 3)))
def test_navigation_reset_randomizes_safe_distant_pairs_and_heading(self):
import torch
from src.tasks.obstacle_avoidance.mdp import NavigationCommandCfg
class Robot:
def __init__(self, count):
self.data = SimpleNamespace(
default_root_state=torch.tensor([[0, 0, 0.32] + [0] * 10] * count),
root_link_pos_w=torch.zeros((count, 3)),
root_link_quat_w=torch.zeros((count, 4)),
)
def write_root_link_pose_to_sim(self, pose, env_ids):
self.data.root_link_pos_w[env_ids] = pose[:, :3]
self.data.root_link_quat_w[env_ids] = pose[:, 3:]
def write_root_link_velocity_to_sim(self, velocity, env_ids):
self.velocity = velocity
class Scene(dict):
env_origins = torch.zeros((64, 3))
robot = Robot(64)
env = SimpleNamespace(num_envs=64, device="cpu", scene=Scene(robot=robot))
command = NavigationCommandCfg(
resampling_time_range=(1e9, 1e9),
navigation_points=((-2, 0), (-1, 0), (0, 0), (1, 0), (2, 0)),
component_starts=(0,),
component_counts=(5,),
fallback_pairs=(((-2, 0), (2, 0)),),
min_goal_distance=2,
).build(env)
torch.manual_seed(7)
command.sample_episode(torch.arange(64))
self.assertTrue(
((command.goals_w - robot.data.root_link_pos_w[:, :2]).norm(dim=1) >= 2).all()
)
self.assertGreater(torch.unique(robot.data.root_link_pos_w[:, :2], dim=0).shape[0], 1)
self.assertGreater(torch.unique(command.goals_w, dim=0).shape[0], 1)
torch.testing.assert_close(robot.data.root_link_quat_w.norm(dim=1), torch.ones(64))
self.assertTrue((robot.velocity == 0).all())
def test_all_terrain_presets_compile_at_exported_world_coordinates(self):
import mujoco
import numpy as np
from mjlab.terrains import TerrainGenerator
from src.tasks.obstacle_avoidance.env_cfg import unitree_go2_obstacle_env_cfg
from src.tasks.obstacle_avoidance.terrain import apply_terrain_configuration
from task_config import (
OBSTACLE_TASK,
TERRAIN_PRESETS,
build_terrain_layout,
validate_task_config,
)
for preset in (p for p in TERRAIN_PRESETS if p != "custom_boxes"):
with self.subTest(preset=preset):
custom = validate_task_config(OBSTACLE_TASK, {"terrainPreset": preset}, 7)
cfg = unitree_go2_obstacle_env_cfg()
apply_terrain_configuration(cfg, custom)
generator = TerrainGenerator(cfg.scene.terrain.terrain_generator)
spec = mujoco.MjSpec()
generator.compile(spec)
model = spec.compile()
layout = build_terrain_layout(custom)
np.testing.assert_allclose(model.geom_pos, [box["pos"] for box in layout["boxes"]])
np.testing.assert_allclose(
model.geom_size, [box["size"] for box in layout["boxes"]]
)
np.testing.assert_allclose(generator.terrain_origins[0, 0], [-5, 0, 0])
np.testing.assert_allclose(model.geom_friction[:, 0], layout["friction"])
self.assertTrue((model.geom_group == 0).all())
@unittest.skipUnless(os.environ.get("GO2_RUN_MJLAB_SMOKE") == "1", "opt-in GPU smoke")
def test_real_environment_81_observation_and_mujoco_raycast_parity(self):
import mujoco
import numpy as np
import torch
import warp as wp
from mjlab.envs import ManagerBasedRlEnv
from src.tasks.obstacle_avoidance.env_cfg import (
apply_obstacle_configuration,
unitree_go2_obstacle_env_cfg,
)
from src.tasks.obstacle_avoidance.mdp import ForwardFanPatternCfg
from task_config import JOINT_NAMES, OBSTACLE_TASK, validate_task_config
from warp._src import context
if not torch.cuda.is_available():
self.skipTest("CUDA is unavailable")
if not hasattr(wp, "context"):
wp.context = context
cfg = unitree_go2_obstacle_env_cfg()
apply_obstacle_configuration(
cfg, validate_task_config(OBSTACLE_TASK, {}, 42), randomize_navigation=False
)
cfg.scene.num_envs = 2
env = ManagerBasedRlEnv(cfg, device="cuda:0")
try:
obs, _ = env.reset()
self.assertEqual(tuple(obs["actor"].shape), (2, 81))
self.assertEqual(list(env.scene["robot"].joint_names), JOINT_NAMES)
np.testing.assert_allclose(env.scene.env_origins.cpu(), [[-5, 0, 0]] * 2)
np.testing.assert_allclose(
env.scene["robot"].data.root_link_pos_w.cpu(), [[-5, 0, 0.32]] * 2, atol=1e-6
)
for _ in range(3):
obs, reward, terminated, truncated, _ = env.step(
torch.zeros((2, 12), device=env.device)
)
self.assertTrue(torch.isfinite(obs["actor"]).all())
self.assertTrue(torch.isfinite(reward).all())
self.assertFalse(terminated.any() or truncated.any())
model = env.sim.mj_model
data = mujoco.MjData(model)
data.qpos[:] = env.sim.data.qpos[0].cpu().numpy()
mujoco.mj_forward(model, data)
body = model.body("robot/base_link").id
rotation = data.xmat[body].reshape(3, 3)
offsets, directions = ForwardFanPatternCfg().generate_rays(None, "cpu")
expected = []
for offset, direction in zip(offsets.numpy(), directions.numpy(), strict=True):
distance = mujoco.mj_ray(
model,
data,
data.xpos[body] + rotation @ offset,
rotation @ direction,
np.array([1, 0, 0, 0, 0, 0], dtype=np.uint8),
1,
-1,
np.array([-1], dtype=np.int32),
)
expected.append(1 if distance < 0 else min(1, distance / 4))
np.testing.assert_allclose(obs["actor"][0, 47:79].cpu(), expected, atol=2e-5)
finally:
env.close()
if __name__ == "__main__":
unittest.main()