Some checks failed
nightly / Test against latest dependencies (py3.10) (push) Has been cancelled
nightly / Test against latest dependencies (py3.13) (push) Has been cancelled
tests / tests (3.13, locked) (push) Has been cancelled
tests / tests (3.13, unlocked) (push) Has been cancelled
tests / pyright (3.10) (push) Has been cancelled
tests / lint-format (push) Has been cancelled
tests / tests (3.10, locked) (push) Has been cancelled
tests / tests (3.11, locked) (push) Has been cancelled
tests / tests (3.12, locked) (push) Has been cancelled
tests / pyright (3.11) (push) Has been cancelled
tests / pyright (3.12) (push) Has been cancelled
tests / pyright (3.13) (push) Has been cancelled
tests / ty-check (3.10) (push) Has been cancelled
tests / ty-check (3.11) (push) Has been cancelled
tests / ty-check (3.12) (push) Has been cancelled
tests / ty-check (3.13) (push) Has been cancelled
tests / stubs (push) Has been cancelled
tests / smoke-test (push) Has been cancelled
Docker / check_paths (push) Has been cancelled
docs / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Upstream: https://github.com/michaelgillett/mjlab Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047 Upstream-Branch: main
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
"""Tests for flat patch sampling on heightfield terrains."""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
from conftest import get_test_device
|
|
|
|
from mjlab.terrains.heightfield_terrains import HfRandomUniformTerrainCfg
|
|
from mjlab.terrains.terrain_entity import TerrainEntity, TerrainEntityCfg
|
|
from mjlab.terrains.terrain_generator import (
|
|
FlatPatchSamplingCfg,
|
|
TerrainGeneratorCfg,
|
|
)
|
|
from mjlab.terrains.utils import find_flat_patches_from_heightfield
|
|
|
|
|
|
@pytest.fixture
|
|
def rng() -> np.random.Generator:
|
|
return np.random.default_rng(42)
|
|
|
|
|
|
def test_patches_avoid_step_boundary(rng: np.random.Generator):
|
|
"""Patches must not land near a sharp height discontinuity."""
|
|
heights = np.zeros((80, 80), dtype=np.float64)
|
|
# Step along column axis → appears on MuJoCo x-axis.
|
|
heights[:, 40:] = 1.0
|
|
patch_radius = 0.3
|
|
cfg = FlatPatchSamplingCfg(
|
|
num_patches=50, patch_radius=patch_radius, max_height_diff=0.05
|
|
)
|
|
patches = find_flat_patches_from_heightfield(
|
|
heights=heights, horizontal_scale=0.1, z_offset=0.0, cfg=cfg, rng=rng
|
|
)
|
|
assert patches.shape == (50, 3)
|
|
for i in range(len(patches)):
|
|
x, z = patches[i, 0], patches[i, 2]
|
|
# Step is at x=4.0; no patch center should be within patch_radius of it.
|
|
assert x < 3.7 or x >= 4.3, f"Patch {i} at x={x:.2f} too close to step"
|
|
# z must be consistent with the side the patch landed on.
|
|
if x < 3.7:
|
|
assert abs(z) < 0.1, f"Low-x patch should have z~0, got {z}"
|
|
else:
|
|
assert abs(z - 1.0) < 0.1, f"High-x patch should have z~1, got {z}"
|
|
|
|
|
|
def test_range_constraints(rng: np.random.Generator):
|
|
"""x/y range filters restrict where patches can land."""
|
|
heights = np.zeros((80, 80), dtype=np.float64)
|
|
cfg = FlatPatchSamplingCfg(
|
|
num_patches=10,
|
|
patch_radius=0.3,
|
|
max_height_diff=0.1,
|
|
x_range=(2.0, 6.0),
|
|
y_range=(2.0, 6.0),
|
|
)
|
|
patches = find_flat_patches_from_heightfield(
|
|
heights=heights, horizontal_scale=0.1, z_offset=0.0, cfg=cfg, rng=rng
|
|
)
|
|
assert np.all(patches[:, 0] >= 2.0) and np.all(patches[:, 0] <= 6.0)
|
|
assert np.all(patches[:, 1] >= 2.0) and np.all(patches[:, 1] <= 6.0)
|
|
|
|
|
|
def test_fallback_when_no_valid_patches(rng: np.random.Generator):
|
|
"""When nothing is flat, all patches fall back to the terrain center."""
|
|
# Linear ramp along rows — every footprint spans a large height range.
|
|
heights = np.arange(80).reshape(80, 1).repeat(80, axis=1).astype(np.float64)
|
|
cfg = FlatPatchSamplingCfg(num_patches=5, patch_radius=0.3, max_height_diff=0.001)
|
|
patches = find_flat_patches_from_heightfield(
|
|
heights=heights, horizontal_scale=0.1, z_offset=0.0, cfg=cfg, rng=rng
|
|
)
|
|
center_x = 80 * 0.1 / 2.0
|
|
center_y = 80 * 0.1 / 2.0
|
|
np.testing.assert_allclose(patches[:, 0], center_x)
|
|
np.testing.assert_allclose(patches[:, 1], center_y)
|
|
|
|
|
|
def test_patches_respect_edge_margin(rng: np.random.Generator):
|
|
"""No patch center should be within patch_radius of the heightfield edge."""
|
|
heights = np.zeros((80, 80), dtype=np.float64)
|
|
patch_radius = 0.5
|
|
h_scale = 0.1
|
|
cfg = FlatPatchSamplingCfg(
|
|
num_patches=200, patch_radius=patch_radius, max_height_diff=0.1
|
|
)
|
|
patches = find_flat_patches_from_heightfield(
|
|
heights=heights, horizontal_scale=h_scale, z_offset=0.0, cfg=cfg, rng=rng
|
|
)
|
|
terrain_x = 80 * h_scale
|
|
terrain_y = 80 * h_scale
|
|
margin = patch_radius
|
|
# Every patch center must be at least patch_radius from each edge.
|
|
assert np.all(patches[:, 0] >= margin - h_scale)
|
|
assert np.all(patches[:, 0] <= terrain_x - margin + h_scale)
|
|
assert np.all(patches[:, 1] >= margin - h_scale)
|
|
assert np.all(patches[:, 1] <= terrain_y - margin + h_scale)
|
|
|
|
|
|
def test_grid_resolution(rng: np.random.Generator):
|
|
"""Finer grid_resolution still produces correct flat patches on a step."""
|
|
heights = np.zeros((80, 80), dtype=np.float64)
|
|
heights[:, 40:] = 1.0
|
|
cfg = FlatPatchSamplingCfg(
|
|
num_patches=30,
|
|
patch_radius=0.3,
|
|
max_height_diff=0.05,
|
|
grid_resolution=0.025,
|
|
)
|
|
patches = find_flat_patches_from_heightfield(
|
|
heights=heights, horizontal_scale=0.1, z_offset=0.0, cfg=cfg, rng=rng
|
|
)
|
|
assert patches.shape == (30, 3)
|
|
for i in range(len(patches)):
|
|
x, z = patches[i, 0], patches[i, 2]
|
|
assert x < 3.7 or x >= 4.3, f"Patch {i} at x={x:.2f} too close to step"
|
|
if x < 3.7:
|
|
assert abs(z) < 0.1
|
|
else:
|
|
assert abs(z - 1.0) < 0.1
|
|
|
|
|
|
def test_terrain_importer_end_to_end():
|
|
"""Flat patches flow through generator → importer with correct shape and bounds."""
|
|
device = get_test_device()
|
|
terrain_size = (4.0, 4.0)
|
|
num_patches = 5
|
|
patch_cfg = FlatPatchSamplingCfg(
|
|
num_patches=num_patches, patch_radius=0.3, max_height_diff=0.1
|
|
)
|
|
gen_cfg = TerrainGeneratorCfg(
|
|
seed=123,
|
|
size=terrain_size,
|
|
num_rows=2,
|
|
num_cols=2,
|
|
sub_terrains={
|
|
"rough": HfRandomUniformTerrainCfg(
|
|
proportion=1.0,
|
|
noise_range=(0.02, 0.08),
|
|
noise_step=0.01,
|
|
flat_patch_sampling={"spawn": patch_cfg},
|
|
),
|
|
},
|
|
)
|
|
importer_cfg = TerrainEntityCfg(
|
|
terrain_type="generator", terrain_generator=gen_cfg, num_envs=4
|
|
)
|
|
importer = TerrainEntity(importer_cfg, device=device)
|
|
|
|
assert "spawn" in importer.flat_patches
|
|
patches = importer.flat_patches["spawn"]
|
|
assert patches.shape == (2, 2, num_patches, 3)
|
|
assert patches.dtype == torch.float
|
|
|
|
# Every patch should be within the world-space extent of the full terrain grid.
|
|
# Grid is centered at origin → spans [-total/2, +total/2].
|
|
half_x = gen_cfg.num_rows * terrain_size[0] / 2
|
|
half_y = gen_cfg.num_cols * terrain_size[1] / 2
|
|
pts = patches.cpu().numpy().reshape(-1, 3)
|
|
assert np.all(pts[:, 0] >= -half_x) and np.all(pts[:, 0] <= half_x)
|
|
assert np.all(pts[:, 1] >= -half_y) and np.all(pts[:, 1] <= half_y)
|