#!/usr/bin/env python3 """Generate standard linear stairs for MuJoCo sim2sim testing. Each cell: flat 2m approach -> N linear steps -> flat 2m platform -> N steps down -> flat edge. Treads are horizontal, rises are vertical (1px = 0.1m wide, acceptable for hfield). Usage: uv run scripts/gen_stairs_test.py --step-height 0.07 --step-depth 0.31 """ import numpy as np, os, argparse from PIL import Image HS = 0.1 # horizontal scale [m/px] VS = 0.005 # vertical scale [m/unit] CELL_M = 8.0 PLATFORM_M = 4.0 # bigger flat platform -> fewer steps CELL_PX = int(CELL_M / HS) # 80 PLATFORM_PX = int(PLATFORM_M / HS) # 40 BORDER_M = 2.0 BORDER_PX = int(BORDER_M / HS) # 20 NUM_CELLS = 2 TOT_PX = NUM_CELLS * CELL_PX + 2 * BORDER_PX TOTAL_M = TOT_PX * HS np.random.seed(42) def make_linear_stairs(step_height_m, step_depth_m=0.31): """Linear stairs: flat approach -> N steps up -> flat platform -> edge. Each step has a flat horizontal tread and (essentially) vertical rise.""" t = np.zeros((CELL_PX, CELL_PX), dtype=np.int16) sd = int(step_depth_m / HS) # tread depth in px sh = int(step_height_m / VS) # rise height in pixel units # How many steps fit on each side of the platform? avail = (CELL_PX - PLATFORM_PX) // 2 n_steps = avail // max(sd, 1) if n_steps < 1: n_steps = 1 edge = (CELL_PX - PLATFORM_PX - n_steps * sd) // 2 # remaining flat on each side # Draw steps going UP from left (in +x direction) # Each step: flat tread at current height, then rise to next height x = edge h = 0 for i in range(n_steps): x_next = x + sd t[:, x:x_next] = h # tread at current height x = x_next h += sh # Platform (flat at max height) plat_start = x plat_end = plat_start + PLATFORM_PX t[:, plat_start:plat_end] = h # Continue stairs going DOWN on the right (optional: mirror) x = plat_end for i in range(n_steps): h -= sh x_next = x + sd t[:, x:x_next] = h x = x_next return t def main(): p = argparse.ArgumentParser() p.add_argument("--step-height", type=float, default=0.15, help="step rise height in metres (default 0.15)") p.add_argument("--step-depth", type=float, default=0.50, help="step tread depth in metres (default 0.50)") args = p.parse_args() print(f"Linear stairs: step_h={args.step_height:.2f}m tread={args.step_depth:.2f}m platform={PLATFORM_M:.0f}m") hf_raw = np.zeros((TOT_PX, TOT_PX), dtype=np.int16) for i in range(NUM_CELLS): for j in range(NUM_CELLS): cell = make_linear_stairs(args.step_height, args.step_depth) y0 = BORDER_PX + i * CELL_PX x0 = BORDER_PX + j * CELL_PX hf_raw[y0:y0 + CELL_PX, x0:x0 + CELL_PX] = cell hf_m = hf_raw.astype(np.float32) * VS z_min = float(hf_m.min()) z_max = float(hf_m.max()) z_range = max(z_max - z_min, 0.001) print(f" height: [{z_min:.3f}, {z_max:.3f}]m z_scale={z_range:.3f} max={z_max*100:.0f}cm") png = ((hf_m - z_min) / z_range * 65535.0).astype(np.uint16) out_dir = os.path.join(os.path.dirname(__file__), "..", "motrix_envs", "src", "motrix_envs", "locomotion", "go1", "xmls", "assets") os.makedirs(out_dir, exist_ok=True) out_path = os.path.join(out_dir, "stairs_test.png") Image.fromarray(png).save(out_path) print(f" saved: {out_path}") sbase = max(z_min, 0.001) print(f" XML: size=\"{TOTAL_M/2:.1f} {TOTAL_M/2:.1f} {z_range:.3f} {sbase:.3f}\"") if __name__ == "__main__": main()