[train] 更新新MJCF与第一版完整训练框架

This commit is contained in:
2026-07-27 12:31:21 +08:00
parent dfd646cf47
commit b08956aec7
116 changed files with 7649 additions and 6760 deletions
@@ -215,6 +215,27 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
border-color: var(--accent);
color: white;
}}
.range-selector {{
display: flex;
gap: 0.4rem;
margin-bottom: 1rem;
}}
.range-btn {{
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.3rem 0.75rem;
cursor: pointer;
color: var(--text);
font-size: 0.8rem;
font-weight: 500;
}}
.range-btn:hover {{ border-color: var(--accent); }}
.range-btn.active {{
background: var(--accent);
border-color: var(--accent);
color: white;
}}
.tab-content {{ display: none; }}
.tab-content.active {{ display: block; }}
.tab-description {{
@@ -303,12 +324,24 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
<div id="tracking" class="tab-content active">
<p class="tab-description">Nightly motion imitation training and evaluation on Unitree G1 (1024 trials per run).</p>
<div class="range-selector" id="range-selector">
<button class="range-btn" data-days="30">30d</button>
<button class="range-btn active" data-days="90">90d</button>
<button class="range-btn" data-days="180">180d</button>
<button class="range-btn" data-days="0">All</button>
</div>
<div class="charts" id="charts"></div>
</div>
<div id="throughput" class="tab-content">
<p class="tab-description">Physics simulation throughput across tasks (4096 parallel envs, NVIDIA RTX 5090).</p>
<div class="task-grid" id="task-grid"></div>
<div class="range-selector" id="range-selector-tp">
<button class="range-btn" data-days="30">30d</button>
<button class="range-btn active" data-days="90">90d</button>
<button class="range-btn" data-days="180">180d</button>
<button class="range-btn" data-days="0">All</button>
</div>
<div id="task-chart-panels"></div>
</div>
@@ -393,6 +426,8 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
}};
let charts = [];
let trackingCharts = [];
let throughputCharts = [];
function updateChartColors() {{
const style = getComputedStyle(root);
@@ -449,7 +484,7 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
`;
chartsContainer.appendChild(card);
charts.push(new Chart(card.querySelector('canvas'), {{
const chart = new Chart(card.querySelector('canvas'), {{
type: 'line',
data: {{
datasets: [
@@ -459,7 +494,8 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
borderColor: color,
backgroundColor: color + '20',
borderWidth: 2,
pointRadius: 4,
pointRadius: 2,
pointHoverRadius: 5,
tension: 0.1,
fill: true
}},
@@ -531,7 +567,9 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
}}
}}
}}
}}));
}});
charts.push(chart);
trackingCharts.push(chart);
}});
// Tab switching
@@ -621,7 +659,8 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
borderColor: '#58a6ff',
backgroundColor: '#58a6ff20',
borderWidth: 2,
pointRadius: 4,
pointRadius: 2,
pointHoverRadius: 5,
tension: 0.1,
fill: true
}},
@@ -631,7 +670,8 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
borderColor: '#3fb950',
backgroundColor: '#3fb95020',
borderWidth: 2,
pointRadius: 4,
pointRadius: 2,
pointHoverRadius: 5,
tension: 0.1,
fill: true
}}
@@ -697,6 +737,7 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
}}
}});
charts.push(chart);
throughputCharts.push(chart);
throughputChartInstances[task] = {{ chart, panelId: `task-panel-${{i}}` }};
// Card click handler
@@ -712,6 +753,23 @@ def generate_dashboard_html(runs: list[dict], throughput_data: list[dict]) -> st
}} else {{
taskGrid.innerHTML = '<p style="color: var(--text-dim)">No throughput data available. Run measure_throughput.py to generate data.</p>';
}}
// Date-range windowing across both tracking and throughput charts.
// Setting min and clearing max also resets any zoom/pan.
function setRange(days) {{
const min = days > 0 ? Date.now() - days * 86400000 : undefined;
[...trackingCharts, ...throughputCharts].forEach(c => {{
c.options.scales.x.min = min;
c.options.scales.x.max = undefined;
c.update();
}});
document.querySelectorAll('.range-btn').forEach(b =>
b.classList.toggle('active', parseInt(b.dataset.days) === days));
}}
document.querySelectorAll('.range-btn').forEach(btn => {{
btn.addEventListener('click', () => setRange(parseInt(btn.dataset.days)));
}});
setRange(90);
</script>
</body>
</html>
@@ -764,7 +822,11 @@ def main(
if run_id in eval_results_by_id:
print(f"Using cached result for {run_id}")
else:
result = evaluate_run(run_path, num_envs)
try:
result = evaluate_run(run_path, num_envs)
except RuntimeError as e:
print(f"Skipping {run_path}: {e}")
continue
eval_results_by_id[run_id] = result
new_evals += 1
else:
@@ -783,7 +845,11 @@ def main(
print(f"Reached eval limit ({eval_limit}), skipping remaining new runs")
break
run_path = f"{entity}/{project}/{run.id}"
result = evaluate_run(run_path, num_envs)
try:
result = evaluate_run(run_path, num_envs)
except RuntimeError as e:
print(f"Skipping {run.name} ({run.id}): {e}")
continue
eval_results_by_id[run.id] = result
new_evals += 1
@@ -0,0 +1,115 @@
"""Interactive single-patch terrain explorer (Viser + MuJoCo MjSpec).
Run with:
uv run python scripts/tools/terrain_explorer.py
uv run python scripts/tools/terrain_explorer.py --port 8081
Then open the printed URL (default http://localhost:8080).
"""
from __future__ import annotations
import argparse
import time
import mujoco
import numpy as np
import viser
from mjviser.conversions import merge_geoms
from mjlab.terrains.config import ALL_TERRAIN_PRESETS
from mjlab.terrains.terrain_generator import TerrainGenerator, TerrainGeneratorCfg
PATCH_SIZE = (8.0, 8.0)
# Per-preset overrides applied when building in the explorer (e.g. to surface
# difficulty-driven behavior that is off by default).
_PRESET_OVERRIDES: dict[str, dict] = {
"random_rough": {"scale_with_difficulty": True},
}
def _build_terrain_mesh(preset_name: str, difficulty: float, seed: int):
"""Generate a single terrain patch and return a merged trimesh (or raise)."""
preset_fn = ALL_TERRAIN_PRESETS[preset_name]
overrides = _PRESET_OVERRIDES.get(preset_name, {})
generator_cfg = TerrainGeneratorCfg(
seed=seed,
size=PATCH_SIZE,
num_rows=1,
num_cols=1,
border_width=0.0,
curriculum=False,
# A degenerate range pins the single patch to exactly this difficulty.
difficulty_range=(difficulty, difficulty),
color_scheme="height",
sub_terrains={preset_name: preset_fn(proportion=1.0, **overrides)},
)
generator = TerrainGenerator(generator_cfg)
spec = mujoco.MjSpec()
generator.compile(spec)
model = spec.compile()
terrain_body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "terrain")
geom_ids = [i for i in range(model.ngeom) if model.geom_bodyid[i] == terrain_body_id]
return merge_geoms(model, geom_ids)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--port", type=int, default=8080, help="Port for the viser server."
)
args = parser.parse_args()
server = viser.ViserServer(port=args.port)
preset_names = sorted(ALL_TERRAIN_PRESETS)
terrain_dropdown = server.gui.add_dropdown(
"Terrain", options=preset_names, initial_value=preset_names[0]
)
difficulty_slider = server.gui.add_slider(
"Difficulty", min=0.0, max=1.0, step=0.01, initial_value=0.0
)
seed_input = server.gui.add_number("Seed", initial_value=42, step=1)
status = server.gui.add_markdown("**Status:** ready")
handle: viser.SceneNodeHandle | None = None
def update() -> None:
nonlocal handle
name = terrain_dropdown.value
difficulty = float(difficulty_slider.value)
seed = int(seed_input.value)
status.content = f"**Status:** building `{name}` at difficulty {difficulty:.2f}..."
try:
mesh = _build_terrain_mesh(name, difficulty, seed)
except Exception as e: # noqa: BLE001 - surface any generation failure in the UI.
status.content = f"**Error:** {type(e).__name__}: {e}"
print(f"Failed to build {name} at difficulty {difficulty}: {e}")
return
if handle is not None:
handle.remove()
handle = server.scene.add_mesh_trimesh("/terrain", mesh)
status.content = (
f"**Loaded** `{name}` at difficulty {difficulty:.2f} ({len(mesh.faces):,} faces)"
)
terrain_dropdown.on_update(lambda _: update())
difficulty_slider.on_update(lambda _: update())
seed_input.on_update(lambda _: update())
# Top-down-ish initial camera.
@server.on_client_connect
def _(client: viser.ClientHandle) -> None:
client.camera.position = np.array([10.0, 10.0, 8.0])
client.camera.look_at = np.array([0.0, 0.0, 0.0])
update()
while True:
time.sleep(1.0)
if __name__ == "__main__":
main()