273 lines
9.6 KiB
Python
273 lines
9.6 KiB
Python
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ==============================================================================
|
|
|
|
"""Evaluate trained Go1 policy with specific velocity commands.
|
|
|
|
Tests three command patterns:
|
|
1. Forward-Backward (前后往返): vx oscillates +1.0 <-> -1.0
|
|
2. Left-Right (左右往返): vy oscillates +1.0 <-> -1.0
|
|
3. Rotation (旋转): wz oscillates +1.0 <-> -1.0
|
|
|
|
For each pattern, the command toggles direction every N seconds.
|
|
Metrics (tracking errors, etc.) are logged to CSV files for analysis.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from absl import app, flags
|
|
|
|
from motrix_envs import registry as env_registry
|
|
from motrix_rl import registry, utils
|
|
from motrix_rl.skrl.jax import wrap_env
|
|
from motrix_rl.skrl.jax.train.ppo import Trainer as SkrlJaxTrainer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_ENV = flags.DEFINE_string("env", "go1-flat-terrain-walk", "The env to evaluate")
|
|
_POLICY = flags.DEFINE_string(
|
|
"policy",
|
|
None,
|
|
"Path to policy checkpoint. Auto-discovers the latest best_agent.pickle if not specified.",
|
|
)
|
|
_DURATION = flags.DEFINE_float("duration", 4.0, "Seconds per command direction before toggling")
|
|
_OUTDIR = flags.DEFINE_string("outdir", None, "Output directory for CSV logs")
|
|
_RENDER = flags.DEFINE_bool("render", False, "Enable rendering (may crash on headless)")
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _find_best_policy(env_name: str) -> Path:
|
|
"""Auto-discover the best SKRL policy for the given env."""
|
|
base = Path(f"runs/{env_name}/skrl")
|
|
if not base.exists():
|
|
raise FileNotFoundError(f"No training runs found at {base}")
|
|
|
|
runs = sorted([d for d in base.iterdir() if d.is_dir()], key=lambda d: d.stat().st_mtime, reverse=True)
|
|
if not runs:
|
|
raise FileNotFoundError(f"No training runs found at {base}")
|
|
|
|
ckpt_dir = runs[0] / "checkpoints"
|
|
best = list(ckpt_dir.glob("best_agent.*"))
|
|
if best:
|
|
return best[0]
|
|
|
|
# Fallback: highest timestep
|
|
ckpts = list(ckpt_dir.glob("agent_*.pickle"))
|
|
if not ckpts:
|
|
raise FileNotFoundError(f"No checkpoints found in {ckpt_dir}")
|
|
|
|
def _ts(p):
|
|
try:
|
|
return int(p.stem.split("_")[1])
|
|
except (IndexError, ValueError):
|
|
return 0
|
|
|
|
return max(ckpts, key=_ts)
|
|
|
|
|
|
def _save_log(outdir: Path, label: str, records: list[dict]) -> None:
|
|
"""Save records to a CSV file."""
|
|
outdir.mkdir(parents=True, exist_ok=True)
|
|
import csv
|
|
|
|
path = outdir / f"{label}.csv"
|
|
with open(path, "w", newline="") as f:
|
|
writer = csv.DictWriter(f, fieldnames=records[0].keys())
|
|
writer.writeheader()
|
|
writer.writerows(records)
|
|
logger.info(f"Saved {len(records)} records → {path}")
|
|
|
|
|
|
# ── Command generators ──────────────────────────────────────────────────
|
|
|
|
|
|
def _command_forward_backward(t: float, period: float):
|
|
"""vx oscillates +1.0 / -1.0, vy=0, wz=0."""
|
|
half = period / 2.0
|
|
phase = (t % period) / half # 0..1 forward, 1..2 backward
|
|
vx = 1.0 if phase < 1.0 else -1.0
|
|
return np.array([vx, 0.0, 0.0], dtype=np.float32)
|
|
|
|
|
|
def _command_left_right(t: float, period: float):
|
|
"""vy oscillates +1.0 / -1.0, vx=0, wz=0."""
|
|
half = period / 2.0
|
|
phase = (t % period) / half
|
|
vy = 1.0 if phase < 1.0 else -1.0
|
|
return np.array([0.0, vy, 0.0], dtype=np.float32)
|
|
|
|
|
|
def _command_rotation(t: float, period: float):
|
|
"""wz oscillates +1.0 / -1.0, vx=0, vy=0."""
|
|
half = period / 2.0
|
|
phase = (t % period) / half
|
|
wz = 1.0 if phase < 1.0 else -1.0
|
|
return np.array([0.0, 0.0, wz], dtype=np.float32)
|
|
|
|
|
|
# ── Main evaluation logic ───────────────────────────────────────────────
|
|
|
|
|
|
def _run_pattern(
|
|
trainer: SkrlJaxTrainer,
|
|
policy_path: str,
|
|
pattern_name: str,
|
|
command_fn,
|
|
period: float,
|
|
total_steps: int,
|
|
ctrl_dt: float,
|
|
) -> list[dict]:
|
|
"""Run one command pattern and return tracking records."""
|
|
logger.info(f"--- {pattern_name} ---")
|
|
|
|
rlcfg = trainer._rlcfg
|
|
env = env_registry.make(trainer._env_name, sim_backend=trainer._sim_backend, num_envs=1)
|
|
env = wrap_env(env, enable_render=False)
|
|
|
|
# Build fresh agent for this run
|
|
models = trainer._make_model(env, rlcfg)
|
|
ppo_cfg = rlcfg.runner.agent.to_dict()
|
|
from motrix_rl.skrl.jax.train.ppo import _add_runtime_config
|
|
_add_runtime_config(ppo_cfg, env)
|
|
agent = trainer._make_agent(models, env, ppo_cfg, rlcfg.runner.memory)
|
|
agent.load(policy_path)
|
|
|
|
obs, info = env.reset()
|
|
state = env._env.state
|
|
records = []
|
|
|
|
for step in range(total_steps):
|
|
t = step * ctrl_dt
|
|
|
|
# Override command in env state
|
|
cmd = command_fn(t, period)
|
|
state.info["commands"] = cmd.reshape(1, -1)
|
|
|
|
# Recompute observation with the new command
|
|
new_obs = env._env._get_obs(state.data, state.info)
|
|
state = state.replace(obs=new_obs)
|
|
obs = new_obs
|
|
|
|
# Agent inference
|
|
outputs = agent.act(obs, timestep=0, timesteps=0)
|
|
actions = outputs[-1].get("mean_actions", outputs[0])
|
|
obs, reward, terminated, truncated, info = env.step(actions)
|
|
state = env._env.state
|
|
|
|
# Collect tracking data
|
|
lin_vel = env._env.get_local_linvel(state.data)[0] # [vx, vy, vz] (body frame)
|
|
gyro = env._env.get_gyro(state.data)[0]
|
|
|
|
tracking_err_xy = np.linalg.norm(cmd[:2] - lin_vel[:2])
|
|
tracking_err_yaw = abs(cmd[2] - gyro[2])
|
|
|
|
records.append({
|
|
"step": step,
|
|
"time": round(t, 3),
|
|
"cmd_vx": round(float(cmd[0]), 4),
|
|
"cmd_vy": round(float(cmd[1]), 4),
|
|
"cmd_wz": round(float(cmd[2]), 4),
|
|
"actual_vx": round(float(lin_vel[0]), 4),
|
|
"actual_vy": round(float(lin_vel[1]), 4),
|
|
"actual_vz": round(float(lin_vel[2]), 4),
|
|
"actual_wz": round(float(gyro[2]), 4),
|
|
"tracking_err_xy": round(float(tracking_err_xy), 6),
|
|
"tracking_err_yaw": round(float(tracking_err_yaw), 6),
|
|
"reward": round(float(reward[0][0]), 6),
|
|
})
|
|
|
|
if step % 100 == 0:
|
|
logger.info(
|
|
f" [{pattern_name}] step {step:5d}/{total_steps} "
|
|
f"cmd=[{cmd[0]:+.1f},{cmd[1]:+.1f},{cmd[2]:+.1f}] "
|
|
f"actual_v=[{lin_vel[0]:+.3f},{lin_vel[1]:+.3f},{gyro[2]:+.3f}] "
|
|
f"track_err_xy={tracking_err_xy:.4f} "
|
|
f"reward={float(reward[0][0]):.4f}"
|
|
)
|
|
|
|
env.close()
|
|
return records
|
|
|
|
|
|
def main(argv):
|
|
env_name = _ENV.value
|
|
ctrl_dt = 0.01 # matches Go1WalkNpEnvCfg.ctrl_dt
|
|
period = _DURATION.value * 2.0 # full cycle: forward + backward
|
|
|
|
# Resolve policy path
|
|
if _POLICY.present:
|
|
policy_path = _POLICY.value
|
|
else:
|
|
policy_path = str(_find_best_policy(env_name))
|
|
logger.info(f"Policy: {policy_path}")
|
|
|
|
# Build trainer (used to construct models & agent)
|
|
rlcfg = registry.default_rl_cfg(env_name, "skrl", backend="jax")
|
|
trainer = SkrlJaxTrainer(env_name, sim_backend=None, enable_render=False)
|
|
trainer._rlcfg = rlcfg
|
|
|
|
# Total steps per pattern = enough full cycles
|
|
cycles = 3
|
|
total_steps = int(cycles * period / ctrl_dt)
|
|
|
|
# Output directory
|
|
if _OUTDIR.present:
|
|
outdir = Path(_OUTDIR.value)
|
|
else:
|
|
ts = time.strftime("%y-%m-%d_%H-%M-%S")
|
|
outdir = Path(f"runs/{env_name}/eval_{ts}")
|
|
logger.info(f"Output directory: {outdir}")
|
|
|
|
patterns = [
|
|
("forward_backward", _command_forward_backward),
|
|
("left_right", _command_left_right),
|
|
("rotation", _command_rotation),
|
|
]
|
|
|
|
summary = {}
|
|
for label, cmd_fn in patterns:
|
|
records = _run_pattern(
|
|
trainer, policy_path, label, cmd_fn,
|
|
period=period, total_steps=total_steps, ctrl_dt=ctrl_dt,
|
|
)
|
|
_save_log(outdir, label, records)
|
|
|
|
# Summary stats (steady-state, skip first 2s for stabilization)
|
|
warmup = int(2.0 / ctrl_dt)
|
|
steady = records[warmup:]
|
|
if steady:
|
|
avg_err = np.mean([r["tracking_err_xy"] for r in steady])
|
|
avg_reward = np.mean([r["reward"] for r in steady])
|
|
else:
|
|
avg_err, avg_reward = float("nan"), float("nan")
|
|
summary[label] = {"avg_tracking_err_xy": avg_err, "avg_reward": avg_reward}
|
|
logger.info(f" [{label}] steady-state avg tracking_err_xy = {avg_err:.4f}, avg_reward = {avg_reward:.4f}")
|
|
|
|
# Print summary
|
|
print("\n" + "=" * 70)
|
|
print("EVALUATION SUMMARY")
|
|
print("=" * 70)
|
|
for label, stats in summary.items():
|
|
print(f" {label:25s} track_err_xy={stats['avg_tracking_err_xy']:.4f} avg_reward={stats['avg_reward']:.4f}")
|
|
print(f"\nDetailed CSV logs saved to: {outdir}")
|
|
print("=" * 70)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(main)
|