Spaces:
Sleeping
Sleeping
Train on real Himalayan (Khumbu SRTM) terrain: per-env heightfield crops
#6
by arminfg - opened
- README.md +12 -3
- app.py +44 -12
- dem_khumbu.npy +3 -0
- himalaya_terrain.py +111 -0
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: G1
|
| 3 |
emoji: 🦿
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: green
|
|
@@ -8,10 +8,19 @@ sdk_version: 6.26.0
|
|
| 8 |
python_version: '3.11'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
-
short_description: GPU training for Unitree G1
|
| 12 |
---
|
| 13 |
|
| 14 |
-
# Unitree G1 —
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
Trains `G1JoystickRoughTerrain` from [MuJoCo Playground](https://playground.mujoco.org/)
|
| 17 |
with Brax PPO on the Space's GPU. Thousands of MJX environments step in parallel,
|
|
|
|
| 1 |
---
|
| 2 |
+
title: G1 Himalaya Terrain Training
|
| 3 |
emoji: 🦿
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: green
|
|
|
|
| 8 |
python_version: '3.11'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
+
short_description: GPU training for Unitree G1 on real Himalayan terrain
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Unitree G1 — Himalayan terrain locomotion training
|
| 15 |
+
|
| 16 |
+
Trains on **real elevation data**: `dem_khumbu.npy` is SRTM-derived terrain
|
| 17 |
+
(AWS Terrain Tiles, 4.2 m/px, 3.2 km square) of the Khumbu glacier valley below
|
| 18 |
+
Everest Base Camp. `himalaya_terrain.py` cuts random 600 m crops, squeezes them
|
| 19 |
+
onto Playground's 20 m arena with 0.3 m of relief plus scree detail, and gives
|
| 20 |
+
every environment its own crop through the domain randomizer. Set `TERRAIN=playground`
|
| 21 |
+
to fall back to the stock 5 cm noise field; `HIMALAYA_RELIEF`, `HIMALAYA_PATCH`,
|
| 22 |
+
`NUM_TERRAINS` tune the terrain.
|
| 23 |
+
|
| 24 |
|
| 25 |
Trains `G1JoystickRoughTerrain` from [MuJoCo Playground](https://playground.mujoco.org/)
|
| 26 |
with Brax PPO on the Space's GPU. Thousands of MJX environments step in parallel,
|
app.py
CHANGED
|
@@ -3,6 +3,11 @@
|
|
| 3 |
Training runs in a background thread so the Gradio server stays responsive; the
|
| 4 |
UI is a log tail plus start/stop. Brax PPO cannot be interrupted from outside,
|
| 5 |
so the stop button sets a flag that the progress callback checks and raises on.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
|
@@ -24,6 +29,14 @@ HF_REPO = os.environ.get("HF_REPO", "").strip()
|
|
| 24 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() or None
|
| 25 |
AUTO_START = os.environ.get("AUTO_START", "1") == "1"
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# /data exists only when persistent storage is attached; fall back to /tmp.
|
| 28 |
OUT = Path("/data/ckpt") if Path("/data").is_dir() else Path("/tmp/ckpt")
|
| 29 |
|
|
@@ -58,7 +71,8 @@ def init_upload() -> None:
|
|
| 58 |
who = api.whoami().get("name")
|
| 59 |
api.create_repo(HF_REPO, repo_type="model", exist_ok=True)
|
| 60 |
UPLOAD["enabled"] = True
|
| 61 |
-
|
|
|
|
| 62 |
except Exception as e:
|
| 63 |
log(f"cannot write to HF_REPO={HF_REPO}: {type(e).__name__}: "
|
| 64 |
f"{str(e).splitlines()[0][:200]}")
|
|
@@ -72,8 +86,9 @@ def _upload(path: Path) -> None:
|
|
| 72 |
return
|
| 73 |
try:
|
| 74 |
from huggingface_hub import HfApi
|
|
|
|
| 75 |
HfApi(token=HF_TOKEN).upload_file(
|
| 76 |
-
path_or_fileobj=str(path), path_in_repo=path.name,
|
| 77 |
repo_id=HF_REPO, repo_type="model")
|
| 78 |
log(f"uploaded {path.name}")
|
| 79 |
UPLOAD["fails"] = 0
|
|
@@ -153,6 +168,20 @@ def train_worker() -> None:
|
|
| 153 |
log(f"loading {ENV_NAME} ...")
|
| 154 |
env = registry.load(ENV_NAME)
|
| 155 |
env_cfg = registry.get_default_config(ENV_NAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
ppo_params = locomotion_params.brax_ppo_config(ENV_NAME)
|
| 157 |
ppo_params.num_timesteps = int(STATE["target"])
|
| 158 |
log(f"num_envs={ppo_params.get('num_envs')} "
|
|
@@ -196,9 +225,9 @@ def train_worker() -> None:
|
|
| 196 |
_, params, _ = ppo.train(
|
| 197 |
**ppo_kwargs,
|
| 198 |
environment=env,
|
| 199 |
-
eval_env=
|
| 200 |
wrap_env_fn=wrapper.wrap_for_brax_training,
|
| 201 |
-
randomization_fn=
|
| 202 |
progress_fn=progress,
|
| 203 |
policy_params_fn=policy_params_fn,
|
| 204 |
seed=SEED,
|
|
@@ -225,8 +254,11 @@ def start(timesteps=None):
|
|
| 225 |
t = STATE["thread"]
|
| 226 |
if t is not None and t.is_alive():
|
| 227 |
return status_md()
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
| 230 |
STATE["step"] = 0
|
| 231 |
STATE["stop"] = False
|
| 232 |
STATE["thread"] = threading.Thread(target=train_worker, daemon=True)
|
|
@@ -244,7 +276,7 @@ def status_md() -> str:
|
|
| 244 |
alive = STATE["thread"] is not None and STATE["thread"].is_alive()
|
| 245 |
target = int(STATE["target"])
|
| 246 |
pct = 100.0 * STATE["step"] / max(target, 1)
|
| 247 |
-
return (f"**{ENV_NAME}** — status: `{STATE['status']}`"
|
| 248 |
f"{' (running)' if alive else ''} \n"
|
| 249 |
f"step {STATE['step']:,} / {target:,} ({pct:.1f}%) \n"
|
| 250 |
f"checkpoints: `{OUT}`"
|
|
@@ -257,13 +289,13 @@ def logs() -> str:
|
|
| 257 |
|
| 258 |
|
| 259 |
with gr.Blocks(title="G1 rough-terrain training") as demo:
|
| 260 |
-
gr.Markdown("# Unitree G1 —
|
| 261 |
st = gr.Markdown(status_md())
|
| 262 |
with gr.Row():
|
| 263 |
-
steps_in = gr.Number(value=NUM_TIMESTEPS, precision=0, label="timesteps"
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
gr.Button("Stop", variant="stop").click(stop, outputs=st)
|
| 267 |
out = gr.Textbox(label="training log", lines=26, max_lines=26,
|
| 268 |
autoscroll=True, value=logs())
|
| 269 |
timer = gr.Timer(3.0)
|
|
|
|
| 3 |
Training runs in a background thread so the Gradio server stays responsive; the
|
| 4 |
UI is a log tail plus start/stop. Brax PPO cannot be interrupted from outside,
|
| 5 |
so the stop button sets a flag that the progress callback checks and raises on.
|
| 6 |
+
|
| 7 |
+
Start/stop are deliberately NOT exposed as named API endpoints (api_name=False).
|
| 8 |
+
On a public Space, auto-named endpoints get probed -- a caller sent the string
|
| 9 |
+
"ping" to /start and then hit /stop, which killed a run mid-flight. The read-only
|
| 10 |
+
log and status endpoints stay exposed so the run can be monitored remotely.
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
|
|
|
| 29 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() or None
|
| 30 |
AUTO_START = os.environ.get("AUTO_START", "1") == "1"
|
| 31 |
|
| 32 |
+
# Terrain: "himalaya" swaps Playground's 5 cm noise heightfield for crops of real
|
| 33 |
+
# SRTM elevation of the Khumbu valley (see himalaya_terrain.py); "playground"
|
| 34 |
+
# keeps the stock terrain. Every env trains on its own crop.
|
| 35 |
+
TERRAIN = os.environ.get("TERRAIN", "himalaya")
|
| 36 |
+
NUM_TERRAINS = int(os.environ.get("NUM_TERRAINS", 64))
|
| 37 |
+
HIMALAYA_RELIEF = float(os.environ.get("HIMALAYA_RELIEF", 0.3)) # metres over the 20 m arena
|
| 38 |
+
HIMALAYA_PATCH = float(os.environ.get("HIMALAYA_PATCH", 600.0)) # metres of real ground per arena
|
| 39 |
+
|
| 40 |
# /data exists only when persistent storage is attached; fall back to /tmp.
|
| 41 |
OUT = Path("/data/ckpt") if Path("/data").is_dir() else Path("/tmp/ckpt")
|
| 42 |
|
|
|
|
| 71 |
who = api.whoami().get("name")
|
| 72 |
api.create_repo(HF_REPO, repo_type="model", exist_ok=True)
|
| 73 |
UPLOAD["enabled"] = True
|
| 74 |
+
sub = f"/tree/main/{TERRAIN}" if TERRAIN != "playground" else ""
|
| 75 |
+
log(f"checkpoints -> https://huggingface.co/{HF_REPO}{sub} (as {who})")
|
| 76 |
except Exception as e:
|
| 77 |
log(f"cannot write to HF_REPO={HF_REPO}: {type(e).__name__}: "
|
| 78 |
f"{str(e).splitlines()[0][:200]}")
|
|
|
|
| 86 |
return
|
| 87 |
try:
|
| 88 |
from huggingface_hub import HfApi
|
| 89 |
+
prefix = f"{TERRAIN}/" if TERRAIN != "playground" else ""
|
| 90 |
HfApi(token=HF_TOKEN).upload_file(
|
| 91 |
+
path_or_fileobj=str(path), path_in_repo=prefix + path.name,
|
| 92 |
repo_id=HF_REPO, repo_type="model")
|
| 93 |
log(f"uploaded {path.name}")
|
| 94 |
UPLOAD["fails"] = 0
|
|
|
|
| 168 |
log(f"loading {ENV_NAME} ...")
|
| 169 |
env = registry.load(ENV_NAME)
|
| 170 |
env_cfg = registry.get_default_config(ENV_NAME)
|
| 171 |
+
eval_env = registry.load(ENV_NAME, config=env_cfg)
|
| 172 |
+
randomization_fn = registry.get_domain_randomizer(ENV_NAME)
|
| 173 |
+
if TERRAIN == "himalaya":
|
| 174 |
+
from himalaya_terrain import apply_terrain, make_terrains, randomizer
|
| 175 |
+
log(f"building {NUM_TERRAINS} Himalaya crops: {HIMALAYA_PATCH:.0f} m of Khumbu "
|
| 176 |
+
f"-> 20 m arena, relief {HIMALAYA_RELIEF} m ...")
|
| 177 |
+
grids = make_terrains(NUM_TERRAINS, seed=SEED, patch_m=HIMALAYA_PATCH,
|
| 178 |
+
relief=HIMALAYA_RELIEF)
|
| 179 |
+
apply_terrain(env, grids, HIMALAYA_RELIEF)
|
| 180 |
+
apply_terrain(eval_env, grids, HIMALAYA_RELIEF)
|
| 181 |
+
randomization_fn = randomizer(randomization_fn, grids)
|
| 182 |
+
log("terrain: himalaya (per-env crops via domain randomization)")
|
| 183 |
+
else:
|
| 184 |
+
log("terrain: playground stock rough terrain")
|
| 185 |
ppo_params = locomotion_params.brax_ppo_config(ENV_NAME)
|
| 186 |
ppo_params.num_timesteps = int(STATE["target"])
|
| 187 |
log(f"num_envs={ppo_params.get('num_envs')} "
|
|
|
|
| 225 |
_, params, _ = ppo.train(
|
| 226 |
**ppo_kwargs,
|
| 227 |
environment=env,
|
| 228 |
+
eval_env=eval_env,
|
| 229 |
wrap_env_fn=wrapper.wrap_for_brax_training,
|
| 230 |
+
randomization_fn=randomization_fn,
|
| 231 |
progress_fn=progress,
|
| 232 |
policy_params_fn=policy_params_fn,
|
| 233 |
seed=SEED,
|
|
|
|
| 254 |
t = STATE["thread"]
|
| 255 |
if t is not None and t.is_alive():
|
| 256 |
return status_md()
|
| 257 |
+
try:
|
| 258 |
+
if timesteps is not None:
|
| 259 |
+
STATE["target"] = max(100_000, int(float(timesteps)))
|
| 260 |
+
except (TypeError, ValueError):
|
| 261 |
+
log(f"ignoring non-numeric timesteps input: {timesteps!r}")
|
| 262 |
STATE["step"] = 0
|
| 263 |
STATE["stop"] = False
|
| 264 |
STATE["thread"] = threading.Thread(target=train_worker, daemon=True)
|
|
|
|
| 276 |
alive = STATE["thread"] is not None and STATE["thread"].is_alive()
|
| 277 |
target = int(STATE["target"])
|
| 278 |
pct = 100.0 * STATE["step"] / max(target, 1)
|
| 279 |
+
return (f"**{ENV_NAME}** · terrain `{TERRAIN}` — status: `{STATE['status']}`"
|
| 280 |
f"{' (running)' if alive else ''} \n"
|
| 281 |
f"step {STATE['step']:,} / {target:,} ({pct:.1f}%) \n"
|
| 282 |
f"checkpoints: `{OUT}`"
|
|
|
|
| 289 |
|
| 290 |
|
| 291 |
with gr.Blocks(title="G1 rough-terrain training") as demo:
|
| 292 |
+
gr.Markdown("# Unitree G1 — Himalayan terrain locomotion training")
|
| 293 |
st = gr.Markdown(status_md())
|
| 294 |
with gr.Row():
|
| 295 |
+
steps_in = gr.Number(value=NUM_TIMESTEPS, precision=0, label="timesteps")
|
| 296 |
+
gr.Button("Start", variant="primary").click(
|
| 297 |
+
start, inputs=steps_in, outputs=st, api_name=False)
|
| 298 |
+
gr.Button("Stop", variant="stop").click(stop, outputs=st, api_name=False)
|
| 299 |
out = gr.Textbox(label="training log", lines=26, max_lines=26,
|
| 300 |
autoscroll=True, value=logs())
|
| 301 |
timer = gr.Timer(3.0)
|
dem_khumbu.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5a9f73e91d7ee40949349f7ea4b6a4e035ebc4a97d257a70921e498bd5136548
|
| 3 |
+
size 2359424
|
himalaya_terrain.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Himalayan heightfields for MuJoCo Playground's G1JoystickRoughTerrain.
|
| 2 |
+
|
| 3 |
+
`dem_khumbu.npy` is real SRTM-derived elevation (AWS Terrain Tiles, zoom 15,
|
| 4 |
+
4.2 m/px, 3.2 km square) of the Khumbu glacier valley below Everest Base Camp,
|
| 5 |
+
generated by examples/g1_himalaya.py in the mujoco-mcp-server repo.
|
| 6 |
+
|
| 7 |
+
Playground's rough-terrain scene is a 256x256 heightfield over a 20 m arena.
|
| 8 |
+
A real valley is not walkable at 1:1 by a humanoid, so `patch_m` metres of real
|
| 9 |
+
ground are squeezed onto the arena and the relief normalized to `relief` metres.
|
| 10 |
+
Each crop is centred so the ground under the spawn sits at data value 0.5 --
|
| 11 |
+
the floor geom is lowered by relief/2 to put that at z = 0 -- and a 1 m disc
|
| 12 |
+
around the spawn is levelled (Playground jitters the spawn by +-0.5 m).
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
DEM_PATH = Path(__file__).with_name("dem_khumbu.npy")
|
| 22 |
+
DEM_METRES_PER_PX = 4.2
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_dem() -> np.ndarray:
|
| 26 |
+
return np.load(DEM_PATH).astype(np.float64)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def sample_patch(dem, mpp, rng, patch_m, res, arena, relief,
|
| 30 |
+
detail_m=0.06, detail_width_m=0.15):
|
| 31 |
+
"""One res x res crop of `patch_m` metres of the DEM at a random position and
|
| 32 |
+
heading, in [0, 1]. A slope-weighted fine layer adds scree (SRTM has no
|
| 33 |
+
rock-scale detail)."""
|
| 34 |
+
from scipy.ndimage import gaussian_filter, map_coordinates
|
| 35 |
+
|
| 36 |
+
n = dem.shape[0]
|
| 37 |
+
px = patch_m / mpp
|
| 38 |
+
margin = px * 0.75
|
| 39 |
+
ci, cj = rng.uniform(margin, n - margin, size=2)
|
| 40 |
+
theta = rng.uniform(0, 2 * np.pi)
|
| 41 |
+
|
| 42 |
+
u = (np.arange(res) - (res - 1) / 2) / (res - 1) * px
|
| 43 |
+
U, V = np.meshgrid(u, u)
|
| 44 |
+
c, s = np.cos(theta), np.sin(theta)
|
| 45 |
+
h = map_coordinates(dem, [ci - s * U + c * V, cj + c * U + s * V], order=3, mode="nearest")
|
| 46 |
+
h = (h - h.min()) / (h.max() - h.min() + 1e-9)
|
| 47 |
+
|
| 48 |
+
if detail_m > 0:
|
| 49 |
+
gy, gx = np.gradient(h)
|
| 50 |
+
slope = np.hypot(gx, gy)
|
| 51 |
+
slope = slope / (np.percentile(slope, 90) + 1e-9)
|
| 52 |
+
fine = gaussian_filter(rng.normal(size=(res, res)),
|
| 53 |
+
sigma=max(0.6, detail_width_m / (arena / res)))
|
| 54 |
+
fine /= np.abs(fine).max() + 1e-9
|
| 55 |
+
h = h + (detail_m / relief) * np.clip(slope, 0.15, 1.0) * fine
|
| 56 |
+
h = (h - h.min()) / (h.max() - h.min() + 1e-9)
|
| 57 |
+
return h
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def make_terrains(n_terrains: int, seed: int = 0, res: int = 256, arena: float = 20.0,
|
| 61 |
+
patch_m: float = 600.0, relief: float = 0.3,
|
| 62 |
+
spawn_clear_r: float = 1.0) -> np.ndarray:
|
| 63 |
+
"""(n_terrains, res, res) heightfield grids in [0, 1], spawn-centred at 0.5."""
|
| 64 |
+
dem = load_dem()
|
| 65 |
+
rng = np.random.default_rng(seed)
|
| 66 |
+
u = np.linspace(-arena / 2, arena / 2, res)
|
| 67 |
+
X, Y = np.meshgrid(u, u)
|
| 68 |
+
blend = np.clip((np.hypot(X, Y) - spawn_clear_r) / 0.8, 0.0, 1.0)
|
| 69 |
+
|
| 70 |
+
out = np.empty((n_terrains, res, res), dtype=np.float32)
|
| 71 |
+
for k in range(n_terrains):
|
| 72 |
+
h = sample_patch(dem, DEM_METRES_PER_PX, rng, patch_m, res, arena, relief)
|
| 73 |
+
h0 = float(h[res // 2, res // 2])
|
| 74 |
+
h = h * blend + h0 * (1.0 - blend)
|
| 75 |
+
# centre the spawn height at 0.5 without leaving [0, 1]
|
| 76 |
+
h = 0.5 + 0.5 * (h - h0) / max(h0, 1.0 - h0, 1e-6)
|
| 77 |
+
out[k] = np.clip(h, 0.0, 1.0)
|
| 78 |
+
return out
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def apply_terrain(env, grids: np.ndarray, relief: float) -> None:
|
| 82 |
+
"""Bake grids[0] into a Playground env's model and rebuild its MJX model.
|
| 83 |
+
Later per-env variation goes through `randomizer`."""
|
| 84 |
+
from mujoco import mjx
|
| 85 |
+
|
| 86 |
+
mj = env._mj_model
|
| 87 |
+
hf = mj.hfield("hfield").id
|
| 88 |
+
floor = mj.geom("floor").id
|
| 89 |
+
mj.hfield_size[hf, 2] = relief # vertical range
|
| 90 |
+
mj.geom_pos[floor, 2] = -0.5 * relief # data 0.5 -> z = 0 under the spawn
|
| 91 |
+
mj.hfield_data[:] = grids[0].ravel()
|
| 92 |
+
env._mjx_model = mjx.put_model(mj, impl=env._config.impl)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def randomizer(base_fn, grids: np.ndarray):
|
| 96 |
+
"""Wrap Playground's domain randomizer so every env also gets its own crop
|
| 97 |
+
of the Himalaya (env i uses grids[i % len(grids)])."""
|
| 98 |
+
import jax
|
| 99 |
+
import jax.numpy as jnp
|
| 100 |
+
|
| 101 |
+
flat = jnp.asarray(grids.reshape(len(grids), -1))
|
| 102 |
+
|
| 103 |
+
def fn(model, rng):
|
| 104 |
+
model, in_axes = base_fn(model, rng)
|
| 105 |
+
n = rng.shape[0]
|
| 106 |
+
idx = jnp.arange(n) % len(grids)
|
| 107 |
+
model = model.tree_replace({"hfield_data": flat[idx]})
|
| 108 |
+
in_axes = in_axes.tree_replace({"hfield_data": 0})
|
| 109 |
+
return model, in_axes
|
| 110 |
+
|
| 111 |
+
return fn
|
requirements.txt
CHANGED
|
@@ -4,3 +4,4 @@ mujoco-mjx>=3.2.7
|
|
| 4 |
playground==0.2.0
|
| 5 |
brax>=0.12.1
|
| 6 |
huggingface_hub>=0.35
|
|
|
|
|
|
| 4 |
playground==0.2.0
|
| 5 |
brax>=0.12.1
|
| 6 |
huggingface_hub>=0.35
|
| 7 |
+
scipy>=1.10
|