restruct project

This commit is contained in:
xtkuang 2026-07-20 08:56:11 +08:00
parent 91347be23e
commit 83100f7fa4
135 changed files with 3173 additions and 1224 deletions

11
.gitignore vendored
View File

@ -1,7 +1,16 @@
.venv/ .venv/
*.vscode/ *.vscode/
build/ build/
dist/
*.egg-info/ *.egg-info/
*__pycache__/ __pycache__/
*.py[cod]
dataset/ dataset/
models/ models/
# 训练与回放产物不属于源码。已被 Git 跟踪的历史文件不会因本规则自动删除。
logs/
outputs/
*.pt
*.onnx
events.out.tfevents.*

View File

@ -1 +1,3 @@
recursive-include source/engineai_lab/assets * recursive-include source/engineai_lab/assets *
global-exclude *.pyc *.pyo
prune source/engineai_lab/assets/__pycache__

View File

@ -19,6 +19,9 @@ engineai-lab
└── engineai_lab └── engineai_lab
├── algorithms ├── algorithms
├── assets ├── assets
│ ├── gen2
│ │ ├── meshes
│ │ └── urdf
│ └── pm01 │ └── pm01
│ ├── meshes │ ├── meshes
│ └── urdf │ └── urdf
@ -26,6 +29,10 @@ engineai-lab
├── tasks ├── tasks
│ └── velocity │ └── velocity
│ ├── config │ ├── config
│ │ ├── common
│ │ ├── gen2
│ │ │ ├── agents
│ │ │ └── stages
│ │ └── pm01 │ │ └── pm01
│ └── mdp │ └── mdp
└── utils └── utils
@ -58,7 +65,7 @@ Create and activate a new environment with Python 3.11:
git clone https://github.com/isaac-sim/IsaacLab.git git clone https://github.com/isaac-sim/IsaacLab.git
cd IsaacLab cd IsaacLab
git checkout 4df6560e git checkout 4df6560e
./isaaclab -i rsl_rl # Install rsl-rl dependency ./isaaclab.sh -i rsl_rl # Install rsl-rl dependency
``` ```
We highly recommend using the main branch`(4df6560e)` of Isaac Lab, as it can support rsl-rl-lib >= 5.0 and Isaac Sim >= 5.0 . We highly recommend using the main branch`(4df6560e)` of Isaac Lab, as it can support rsl-rl-lib >= 5.0 and Isaac Sim >= 5.0 .
@ -76,6 +83,11 @@ pip install -e .
For a step-by-step Chinese guide to importing and training a new humanoid robot, see For a step-by-step Chinese guide to importing and training a new humanoid robot, see
[EngineAI Lab 新人形机器人行走训练攻略](docs/humanoid_locomotion_onboarding_guide.md). [EngineAI Lab 新人形机器人行走训练攻略](docs/humanoid_locomotion_onboarding_guide.md).
Gen2 的阶段划分、训练/续训/迁移命令和配置修改约定见
[Gen2 分阶段训练说明](source/engineai_lab/tasks/velocity/config/gen2/README.md)
URDF、mesh 与简化碰撞体约定见
[Gen2 资产说明](source/engineai_lab/assets/gen2/README.md)。
### Supported Robots ### Supported Robots
This repository currently supports the following environments from the EngineAI Robots family: This repository currently supports the following environments from the EngineAI Robots family:
@ -84,6 +96,12 @@ This repository currently supports the following environments from the EngineAI
|--------|--------|--------| |--------|--------|--------|
PM01|`Flat-PM01-v0`|Basic flat-terrain locomotion PM01|`Flat-PM01-v0`|Basic flat-terrain locomotion
PM01|`Flat-AMP-PM01-v0`|AMP-based motion imitation on flat terrain PM01|`Flat-AMP-PM01-v0`|AMP-based motion imitation on flat terrain
Gen2|`Flat-Gen2-v0`|基础低速行走,包含站立指令采样
Gen2|`Flat-Gen2-Speed-v0`|平地速度与稳定性巩固
Gen2|`Flat-Gen2-Natural-v0`|自然对侧摆臂
Gen2|`Flat-Gen2-Fast-v0`|课程提升至 1.6 m/s
Gen2|`Flat-Gen2-Sprint-v0`|课程提升至 3.0 m/s
Gen2|`Flat-Gen2-NaturalRun-v0`|3.0 m/s 自然跑姿微调
*More robots and environments are coming soon!* *More robots and environments are coming soon!*
@ -94,6 +112,39 @@ PM01|`Flat-AMP-PM01-v0`|AMP-based motion imitation on flat terrain
python scripts/play.py --task=Flat-PM01-v0 --num_envs 128 --load_run <name> python scripts/play.py --task=Flat-PM01-v0 --num_envs 128 --load_run <name>
``` ```
### Gen2 Staged Training
基础阶段从头训练:
```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \
--task Flat-Gen2-v0 \
--num_envs 4096 \
--max_iterations 1500 \
--run_name gen2_walk_v0 \
--device cuda:0 \
--rl_device cuda:0 \
--headless
```
跨阶段只迁移策略权重,例如 Fast -> Sprint
```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \
--task Flat-Gen2-Sprint-v0 \
--num_envs 4096 \
--resume True \
--load_mode finetune \
--load_run '<fast_run_directory>' \
--checkpoint model_<iteration>.pt \
--run_name gen2_sprint_v1 \
--device cuda:0 \
--rl_device cuda:0 \
--headless
```
同一 Task 中断续训才使用 `--load_mode resume`。所有阶段和对应 Play Task 的完整表格见上面的 Gen2 分阶段训练说明。
### Evaluating a Policy ### Evaluating a Policy
``` ```
@ -142,7 +193,7 @@ If you have any questions about using this repository, we're here to help!
## License ## License
EngineAI-Lab is released under [BSD-3 License](LICENSE). This checkout does not currently contain a `LICENSE` file, while its package metadata and the upstream README use different license labels. Add a license file and align the metadata before redistribution.
## Acknowledgement ## Acknowledgement

View File

@ -29,10 +29,11 @@
| 模块 | 典型路径 | 职责 | | 模块 | 典型路径 | 职责 |
|---|---|---| |---|---|---|
| 机器人资产 | `source/gen2_lab/assets/` | URDF、mesh、简化碰撞体和审计结果 | | 机器人资产 | `source/engineai_lab/assets/gen2/` | URDF、mesh 和简化碰撞体;审计结果写入 `outputs/` |
| 机器人配置 | `source/engineai_lab/robots/gen2.py` | q0、初始高度、关节顺序、执行器参数和资产加载 | | 机器人配置 | `source/engineai_lab/robots/gen2.py` | q0、初始高度、关节顺序、执行器参数和资产加载 |
| 环境配置 | `source/engineai_lab/tasks/velocity/config/gen2/flat_env_cfg.py` | scene、action、observation、command、reward、reset、termination、curriculum | | 环境共享配置 | `source/engineai_lab/tasks/velocity/config/gen2/common_env_cfg.py` | scene、action、observation、reset、termination 和 Play 公共设置 |
| PPO 配置 | `source/engineai_lab/tasks/velocity/config/gen2/agents/rsl_rl_ppo_cfg.py` | 网络、熵系数、迭代数和实验目录 | | 分阶段环境 | `source/engineai_lab/tasks/velocity/config/gen2/stages/` | 每阶段独立的 reward、command、curriculum 和覆盖参数 |
| 分阶段 PPO | `source/engineai_lab/tasks/velocity/config/gen2/agents/` | 每阶段独立的迭代数、熵系数和学习率 |
| MDP 函数 | `source/engineai_lab/tasks/velocity/mdp/` | 自定义命令、观测、奖励和终止逻辑 | | MDP 函数 | `source/engineai_lab/tasks/velocity/mdp/` | 自定义命令、观测、奖励和终止逻辑 |
| 任务注册 | `source/engineai_lab/tasks/velocity/config/gen2/__init__.py` | Gym task ID 到环境/PPO 配置的映射 | | 任务注册 | `source/engineai_lab/tasks/velocity/config/gen2/__init__.py` | Gym task ID 到环境/PPO 配置的映射 |
| 训练入口 | `scripts/train.py` | 创建环境、加载 checkpoint、运行 RSL-RL | | 训练入口 | `scripts/train.py` | 创建环境、加载 checkpoint、运行 RSL-RL |
@ -45,10 +46,17 @@
source/engineai_lab/robots/<robot>.py source/engineai_lab/robots/<robot>.py
source/engineai_lab/tasks/velocity/config/<robot>/ source/engineai_lab/tasks/velocity/config/<robot>/
├── __init__.py ├── __init__.py
├── flat_env_cfg.py ├── registry.py
├── common_env_cfg.py
├── stages/
│ ├── walk.py
│ ├── speed.py
│ └── natural.py
└── agents/ └── agents/
├── __init__.py ├── __init__.py
└── rsl_rl_ppo_cfg.py ├── walk_ppo_cfg.py
├── speed_ppo_cfg.py
└── natural_ppo_cfg.py
``` ```
## 3. 总体推进路线和阶段门 ## 3. 总体推进路线和阶段门
@ -151,13 +159,13 @@ Gen2 实例中PM01 参考腿长约 `0.82 m`Gen2 有效腿长约 `0.786 m`
可以把 Gen2 的静态检查脚本复制并参数化: 可以把 Gen2 的静态检查脚本复制并参数化:
```bash ```bash
cd /home/xtkuang/Projects/cmvr/RL/engineai_amp cd /home/xtkuang/Projects/cmvr/RL/cmvr_ai_lab
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python \ /home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python \
scripts/gen2_check_rl_readiness.py scripts/gen2_check_rl_readiness.py
``` ```
该脚本检查质量、惯量、关节限位、碰撞重叠、脚底高度和 reset z。用于新机器人时必须修改其中的 URDF、foot link、robot cfg 和高度常量路径。当前脚本仍按旧名称 `GEN2_BASE_HEIGHT_TARGET` 搜索高度;复制时应改为新机器人的 `ROBOT_PELVIS_HEIGHT_TARGET`/`ROBOT_TORSO_HEIGHT_TARGET`,任何 `UNKNOWN`不能当作通过。 该脚本检查质量、惯量、关节限位、碰撞重叠、脚底高度、reset z以及 pelvis/torso 高度目标。用于新机器人时,必须修改其中的 URDF、foot link、robot cfg 和高度常量路径;任何 `unknown` 都会使检查失败,不能当作通过。
### 5.2 视觉 mesh 不适合直接作为训练碰撞体 ### 5.2 视觉 mesh 不适合直接作为训练碰撞体
@ -530,10 +538,9 @@ gym.register(
### 10.1 静态检查 ### 10.1 静态检查
```bash ```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python -m py_compile \ /home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python -m compileall \
source/engineai_lab/robots/<robot>.py \ source/engineai_lab/robots/<robot>.py \
source/engineai_lab/tasks/velocity/config/<robot>/flat_env_cfg.py \ source/engineai_lab/tasks/velocity/config/<robot>/
source/engineai_lab/tasks/velocity/config/<robot>/agents/rsl_rl_ppo_cfg.py
``` ```
### 10.2 环境加载检查 ### 10.2 环境加载检查
@ -761,7 +768,7 @@ Gen2 在这一步才把 `0.4 m/s` 固定命令下的实际速度从约 `0.23`
下列 `<...>` 是需要替换的占位符,不能原样执行。 下列 `<...>` 是需要替换的占位符,不能原样执行。
```bash ```bash
cd /home/xtkuang/Projects/cmvr/RL/engineai_amp cd /home/xtkuang/Projects/cmvr/RL/cmvr_ai_lab
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \ /home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \
--task Flat-<Robot>-v0 \ --task Flat-<Robot>-v0 \
@ -774,7 +781,9 @@ cd /home/xtkuang/Projects/cmvr/RL/engineai_amp
--headless --headless
``` ```
### 14.2 从 checkpoint 续训 ### 14.2 同一 Task 从 checkpoint 完整续训
同一任务中断后继续时使用 `resume`,它会恢复 optimizer、iteration 和 curriculum
```bash ```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \ /home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \
@ -783,6 +792,7 @@ cd /home/xtkuang/Projects/cmvr/RL/engineai_amp
--seed 42 \ --seed 42 \
--max_iterations <additional_iterations> \ --max_iterations <additional_iterations> \
--resume True \ --resume True \
--load_mode resume \
--load_run <previous_run_directory> \ --load_run <previous_run_directory> \
--checkpoint model_<iteration>.pt \ --checkpoint model_<iteration>.pt \
--run_name <new_run_name> \ --run_name <new_run_name> \
@ -793,7 +803,29 @@ cd /home/xtkuang/Projects/cmvr/RL/engineai_amp
当前 CLI 的 `--resume` 使用布尔值参数,应写成 `--resume True`,不能只写一个裸 `--resume` 当前 CLI 的 `--resume` 使用布尔值参数,应写成 `--resume True`,不能只写一个裸 `--resume`
### 14.3 TensorBoard ### 14.3 跨阶段迁移 checkpoint
从一个 Task 进入下一阶段时必须使用 `finetune`。它只加载 actor/critic使用新阶段的 optimizer 和 curriculum
```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python scripts/train.py \
--task Flat-<Robot>-<NextStage>-v0 \
--num_envs 4096 \
--seed 42 \
--max_iterations <iterations> \
--resume True \
--load_mode finetune \
--load_run <previous_stage_run_directory> \
--checkpoint model_<iteration>.pt \
--run_name <next_stage_run_name> \
--device cuda:0 \
--rl_device cuda:0 \
--headless
```
checkpoint 会记录原 Task ID跨 Task 使用完整 `resume` 会被训练入口拒绝,这是为了防止错误恢复旧 curriculum。
### 14.4 TensorBoard
```bash ```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/tensorboard \ /home/xtkuang/App/anaconda3/envs/engineai_lab/bin/tensorboard \
@ -1136,8 +1168,10 @@ fixed-speed benchmark
当前仓库中可以直接参考: 当前仓库中可以直接参考:
- [`robots/gen2.py`](../source/engineai_lab/robots/gen2.py) - [`robots/gen2.py`](../source/engineai_lab/robots/gen2.py)
- [`config/gen2/flat_env_cfg.py`](../source/engineai_lab/tasks/velocity/config/gen2/flat_env_cfg.py) - [`config/gen2/README.md`](../source/engineai_lab/tasks/velocity/config/gen2/README.md)
- [`config/gen2/agents/rsl_rl_ppo_cfg.py`](../source/engineai_lab/tasks/velocity/config/gen2/agents/rsl_rl_ppo_cfg.py) - [`config/gen2/common_env_cfg.py`](../source/engineai_lab/tasks/velocity/config/gen2/common_env_cfg.py)
- [`config/gen2/stages/`](../source/engineai_lab/tasks/velocity/config/gen2/stages)
- [`config/gen2/agents/`](../source/engineai_lab/tasks/velocity/config/gen2/agents)
- [`mdp/commands.py`](../source/engineai_lab/tasks/velocity/mdp/commands.py) - [`mdp/commands.py`](../source/engineai_lab/tasks/velocity/mdp/commands.py)
- [`mdp/rewards.py`](../source/engineai_lab/tasks/velocity/mdp/rewards.py) - [`mdp/rewards.py`](../source/engineai_lab/tasks/velocity/mdp/rewards.py)
- [`scripts/train.py`](../scripts/train.py) - [`scripts/train.py`](../scripts/train.py)

View File

@ -24,6 +24,15 @@ def add_rsl_rl_args(parser: argparse.ArgumentParser):
arg_group.add_argument("--resume", type=bool, default=None, help="Whether to resume from a checkpoint.") arg_group.add_argument("--resume", type=bool, default=None, help="Whether to resume from a checkpoint.")
arg_group.add_argument("--load_run", type=str, default=None, help="Name of the run folder to resume from.") arg_group.add_argument("--load_run", type=str, default=None, help="Name of the run folder to resume from.")
arg_group.add_argument("--checkpoint", type=str, default=None, help="Checkpoint file to resume from.") arg_group.add_argument("--checkpoint", type=str, default=None, help="Checkpoint file to resume from.")
arg_group.add_argument(
"--load_mode",
choices=("resume", "finetune"),
default="resume",
help=(
"resume restores optimizer, iteration, and curriculum state; "
"finetune loads actor/critic weights into a fresh optimizer and curriculum."
),
)
# -- logger arguments # -- logger arguments
arg_group.add_argument( arg_group.add_argument(
"--logger", type=str, default=None, choices={"wandb", "tensorboard", "neptune"}, help="Logger module to use." "--logger", type=str, default=None, choices={"wandb", "tensorboard", "neptune"}, help="Logger module to use."

View File

@ -14,9 +14,26 @@ import numpy as np
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
URDF_PATH = REPO_ROOT / "source" / "gen2_lab" / "assets" / "robot_simplified_collision.urdf" URDF_PATH = (
REPO_ROOT
/ "source"
/ "engineai_lab"
/ "assets"
/ "gen2"
/ "urdf"
/ "robot_simplified_collision.urdf"
)
GEN2_ROBOT_CFG = REPO_ROOT / "source" / "engineai_lab" / "robots" / "gen2.py" GEN2_ROBOT_CFG = REPO_ROOT / "source" / "engineai_lab" / "robots" / "gen2.py"
GEN2_ENV_CFG = REPO_ROOT / "source" / "engineai_lab" / "tasks" / "velocity" / "config" / "gen2" / "flat_env_cfg.py" GEN2_ENV_CFG = (
REPO_ROOT
/ "source"
/ "engineai_lab"
/ "tasks"
/ "velocity"
/ "config"
/ "gen2"
/ "common_env_cfg.py"
)
FOOT_LINKS = ("left_leg_link_6", "right_leg_link_6") FOOT_LINKS = ("left_leg_link_6", "right_leg_link_6")
@ -91,14 +108,16 @@ def _load_base_init_z() -> float | None:
return float(match.group(1).split(",")[2].strip()) return float(match.group(1).split(",")[2].strip())
def _load_base_height_target() -> float | None: def _load_height_targets() -> dict[str, float]:
if not GEN2_ENV_CFG.exists(): if not GEN2_ENV_CFG.exists():
return None return {}
text = GEN2_ENV_CFG.read_text(encoding="utf-8") text = GEN2_ENV_CFG.read_text(encoding="utf-8")
match = re.search(r"GEN2_BASE_HEIGHT_TARGET\s*=\s*([0-9.]+)", text) targets = {}
if match is None: for name in ("GEN2_PELVIS_HEIGHT_TARGET", "GEN2_TORSO_HEIGHT_TARGET"):
return None match = re.search(rf"{name}\s*=\s*([0-9.]+)", text)
return float(match.group(1)) if match is not None:
targets[name] = float(match.group(1))
return targets
def _link_tree(root: ET.Element): def _link_tree(root: ET.Element):
@ -282,7 +301,7 @@ def main() -> None:
non_adjacent_overlaps.sort(reverse=True) non_adjacent_overlaps.sort(reverse=True)
base_z = _load_base_init_z() base_z = _load_base_init_z()
base_target = _load_base_height_target() height_targets = _load_height_targets()
foot_min_z = {box.name: float(box.vertices[:, 2].min()) for box in boxes if box.name in FOOT_LINKS} foot_min_z = {box.name: float(box.vertices[:, 2].min()) for box in boxes if box.name in FOOT_LINKS}
lowest_foot_z = min(foot_min_z.values()) lowest_foot_z = min(foot_min_z.values())
suggested_base_z = -lowest_foot_z + 0.01 suggested_base_z = -lowest_foot_z + 0.01
@ -307,10 +326,21 @@ def main() -> None:
world_min = min_z + base_z if base_z is not None else float("nan") world_min = min_z + base_z if base_z is not None else float("nan")
print(f" {name}: relative={min_z:.6f} world_at_init={world_min:.6f}") print(f" {name}: relative={min_z:.6f} world_at_init={world_min:.6f}")
print(f"base_init_z={base_z:.6f}" if base_z is not None else "base_init_z=unknown") print(f"base_init_z={base_z:.6f}" if base_z is not None else "base_init_z=unknown")
print(f"base_height_target={base_target:.6f}" if base_target is not None else "base_height_target=unknown") for target_name in ("GEN2_PELVIS_HEIGHT_TARGET", "GEN2_TORSO_HEIGHT_TARGET"):
target = height_targets.get(target_name)
print(f"{target_name.lower()}={target:.6f}" if target is not None else f"{target_name.lower()}=unknown")
print(f"suggested_base_z_for_1cm_foot_clearance={suggested_base_z:.6f}") print(f"suggested_base_z_for_1cm_foot_clearance={suggested_base_z:.6f}")
hard_fail = bool(missing_inertial or bad_mass or bad_inertia or missing_limits or bad_limits or non_adjacent_overlaps) missing_height_targets = len(height_targets) != 2
hard_fail = bool(
missing_inertial
or bad_mass
or bad_inertia
or missing_limits
or bad_limits
or non_adjacent_overlaps
or missing_height_targets
)
ground_penetration = base_z is not None and lowest_foot_z + base_z < -0.005 ground_penetration = base_z is not None and lowest_foot_z + base_z < -0.005
if hard_fail: if hard_fail:
print("RESULT=FAIL") print("RESULT=FAIL")

View File

@ -3,10 +3,10 @@
The script does not modify the original robot.urdf. It writes: The script does not modify the original robot.urdf. It writes:
- source/gen2_lab/assets/collision_simplified/*.stl - source/engineai_lab/assets/gen2/meshes/collision_simplified/*.stl
- source/gen2_lab/assets/robot_simplified_collision.urdf - source/engineai_lab/assets/gen2/urdf/robot_simplified_collision.urdf
- source/gen2_lab/assets/robot_simplified_collision_mesh.urdf - source/engineai_lab/assets/gen2/urdf/robot_simplified_collision_mesh.urdf
- source/gen2_lab/assets/collision_simplified_audit.csv - outputs/gen2_asset_audit/collision_simplified_audit.csv
The recommended URDF is robot_simplified_collision.urdf. It uses URDF box The recommended URDF is robot_simplified_collision.urdf. It uses URDF box
primitives for collision. The generated STL files and mesh URDF are useful for primitives for collision. The generated STL files and mesh URDF are useful for
@ -24,12 +24,14 @@ from xml.dom import minidom
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
ASSET_DIR = REPO_ROOT / "source" / "gen2_lab" / "assets" ASSET_DIR = REPO_ROOT / "source" / "engineai_lab" / "assets" / "gen2"
INPUT_URDF = ASSET_DIR / "robot.urdf" URDF_DIR = ASSET_DIR / "urdf"
OUTPUT_DIR = ASSET_DIR / "collision_simplified" MESH_DIR = ASSET_DIR / "meshes"
OUTPUT_PRIMITIVE_URDF = ASSET_DIR / "robot_simplified_collision.urdf" INPUT_URDF = URDF_DIR / "robot.urdf"
OUTPUT_MESH_URDF = ASSET_DIR / "robot_simplified_collision_mesh.urdf" OUTPUT_DIR = MESH_DIR / "collision_simplified"
AUDIT_CSV = ASSET_DIR / "collision_simplified_audit.csv" OUTPUT_PRIMITIVE_URDF = URDF_DIR / "robot_simplified_collision.urdf"
OUTPUT_MESH_URDF = URDF_DIR / "robot_simplified_collision_mesh.urdf"
AUDIT_CSV = REPO_ROOT / "outputs" / "gen2_asset_audit" / "collision_simplified_audit.csv"
def _parse_floats(text: str | None, default: tuple[float, float, float]) -> tuple[float, float, float]: def _parse_floats(text: str | None, default: tuple[float, float, float]) -> tuple[float, float, float]:
@ -235,7 +237,9 @@ def main() -> None:
continue continue
mesh_file = mesh.attrib["filename"] mesh_file = mesh.attrib["filename"]
extent, center, tri_count = _binary_stl_bbox(ASSET_DIR / mesh_file) # URDF 中只允许相对路径;从 URDF 所在目录解析,避免依赖当前工作目录。
source_mesh_path = (INPUT_URDF.parent / mesh_file).resolve()
extent, center, tri_count = _binary_stl_bbox(source_mesh_path)
proxy_center, size, scale = _proxy_box(name, extent, center) proxy_center, size, scale = _proxy_box(name, extent, center)
original_xyz = _parse_floats(origin.attrib.get("xyz"), (0.0, 0.0, 0.0)) original_xyz = _parse_floats(origin.attrib.get("xyz"), (0.0, 0.0, 0.0))
@ -244,7 +248,7 @@ def main() -> None:
proxy_origin_xyz = tuple(original_xyz[i] + rotated_center[i] for i in range(3)) proxy_origin_xyz = tuple(original_xyz[i] + rotated_center[i] for i in range(3))
stl_name = f"{name}_box_collision.stl" stl_name = f"{name}_box_collision.stl"
stl_rel_path = f"collision_simplified/{stl_name}" stl_rel_path = f"../meshes/collision_simplified/{stl_name}"
_write_binary_stl(OUTPUT_DIR / stl_name, _box_triangles(proxy_center, size)) _write_binary_stl(OUTPUT_DIR / stl_name, _box_triangles(proxy_center, size))
_replace_collision_with_box(collision, proxy_origin_xyz, original_rpy, size) _replace_collision_with_box(collision, proxy_origin_xyz, original_rpy, size)
@ -277,6 +281,7 @@ def main() -> None:
_write_pretty_xml(primitive_tree, OUTPUT_PRIMITIVE_URDF) _write_pretty_xml(primitive_tree, OUTPUT_PRIMITIVE_URDF)
_write_pretty_xml(mesh_tree, OUTPUT_MESH_URDF) _write_pretty_xml(mesh_tree, OUTPUT_MESH_URDF)
AUDIT_CSV.parent.mkdir(parents=True, exist_ok=True)
with AUDIT_CSV.open("w", newline="", encoding="utf-8") as file: with AUDIT_CSV.open("w", newline="", encoding="utf-8") as file:
writer = csv.DictWriter(file, fieldnames=list(audit_rows[0].keys())) writer = csv.DictWriter(file, fieldnames=list(audit_rows[0].keys()))
writer.writeheader() writer.writeheader()

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""启动 Isaac Sim 后验证全部 Gen2 Task 注册和 Train/Play 契约。"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = REPO_ROOT / "source"
if str(SOURCE_ROOT) not in sys.path:
sys.path.insert(0, str(SOURCE_ROOT))
from isaaclab.app import AppLauncher
# Isaac Lab 配置模块依赖 pxr必须先启动 App再导入 engineai_lab.tasks。
app_launcher = AppLauncher(headless=True)
simulation_app = app_launcher.app
def _load_class(entry_point: str):
module_name, class_name = entry_point.rsplit(":", 1)
return getattr(importlib.import_module(module_name), class_name)
def main() -> None:
import gymnasium as gym
import engineai_lab.tasks # noqa: F401
from engineai_lab.tasks.velocity.config.gen2.agents.rsl_rl_ppo_cfg import (
Gen2FastPPORunnerCfg as LegacyFastRunnerCfg,
)
from engineai_lab.tasks.velocity.config.gen2.agents.fast_ppo_cfg import Gen2FastPPORunnerCfg
from engineai_lab.tasks.velocity.config.gen2.flat_env_cfg import Gen2FastEnvCfg as LegacyFastEnvCfg
from engineai_lab.tasks.velocity.config.gen2.registry import GEN2_TASK_SPECS
from engineai_lab.tasks.velocity.config.gen2.stages.fast import Gen2FastEnvCfg
# 旧模块现在是兼容层;类对象必须与新阶段模块完全相同。
assert LegacyFastEnvCfg is Gen2FastEnvCfg
assert LegacyFastRunnerCfg is Gen2FastPPORunnerCfg
configs = {}
for task_id, _, _ in GEN2_TASK_SPECS:
spec = gym.spec(task_id)
env_cfg_cls = _load_class(spec.kwargs["env_cfg_entry_point"])
runner_cfg_cls = _load_class(spec.kwargs["rsl_rl_cfg_entry_point"])
env_cfg = env_cfg_cls()
runner_cfg = runner_cfg_cls()
configs[task_id] = env_cfg
assert runner_cfg.experiment_name == "velocity_flat_terrain_gen2"
assert len(env_cfg.actions.joint_pos.joint_names) == 28
assert env_cfg.observations.policy.joint_pos.history_length == 15
print(f"OK {task_id}: {env_cfg_cls.__module__}.{env_cfg_cls.__name__}", flush=True)
train_play_pairs = (
("Flat-Gen2-v0", "Flat-Gen2-Play-v0"),
("Flat-Gen2-Speed-v0", "Flat-Gen2-Speed-Play-v0"),
("Flat-Gen2-Natural-v0", "Flat-Gen2-Natural-Play-v0"),
("Flat-Gen2-Fast-v0", "Flat-Gen2-Fast-Play-v0"),
("Flat-Gen2-Sprint-v0", "Flat-Gen2-Sprint-Play-v0"),
("Flat-Gen2-NaturalRun-v0", "Flat-Gen2-NaturalRun-Play-v0"),
)
for train_id, play_id in train_play_pairs:
train_cfg = configs[train_id]
play_cfg = configs[play_id]
assert train_cfg.actions.joint_pos.joint_names == play_cfg.actions.joint_pos.joint_names
assert train_cfg.actions.joint_pos.scale == play_cfg.actions.joint_pos.scale
print(f"RESULT=PASS tasks={len(configs)} train_play_pairs={len(train_play_pairs)}", flush=True)
if __name__ == "__main__":
main()

View File

@ -19,8 +19,17 @@ from mpl_toolkits.mplot3d.art3d import Poly3DCollection
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_URDF = REPO_ROOT / "source" / "gen2_lab" / "assets" / "robot_simplified_collision.urdf" DEFAULT_URDF = (
DEFAULT_OUTPUT = REPO_ROOT / "source" / "gen2_lab" / "assets" / "gen2_collision_visualization.png" REPO_ROOT
/ "source"
/ "engineai_lab"
/ "assets"
/ "gen2"
/ "urdf"
/ "robot_simplified_collision.urdf"
)
# 生成图属于检查产物,不写回运行时资产包。
DEFAULT_OUTPUT = REPO_ROOT / "outputs" / "gen2_asset_audit" / "gen2_collision_visualization.png"
GEN2_CFG = REPO_ROOT / "source" / "engineai_lab" / "robots" / "gen2.py" GEN2_CFG = REPO_ROOT / "source" / "engineai_lab" / "robots" / "gen2.py"

View File

@ -11,10 +11,11 @@ from isaaclab.app import AppLauncher
# local imports # local imports
import cli_args # isort: skip import cli_args # isort: skip
# ensure repository root is on Python path for Hydra registry imports # Prefer this checkout over any other editable ``engineai_lab`` installation.
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path: SOURCE_ROOT = REPO_ROOT / "source"
sys.path.append(str(REPO_ROOT)) if str(SOURCE_ROOT) not in sys.path:
sys.path.insert(0, str(SOURCE_ROOT))
# add argparse arguments # add argparse arguments
parser = argparse.ArgumentParser(description="Play an RSL-RL policy checkpoint.") parser = argparse.ArgumentParser(description="Play an RSL-RL policy checkpoint.")
@ -171,6 +172,13 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
# set the log directory for the environment (works for all environment types) # set the log directory for the environment (works for all environment types)
env_cfg.log_dir = log_dir env_cfg.log_dir = log_dir
# Fixed/keyboard replay already applies its own step or ramp controller.
# Disable the training-side ramp to avoid applying acceleration limits twice.
if args_cli.command_source != "random" and hasattr(
env_cfg.commands.base_velocity, "command_ramp_rates"
):
env_cfg.commands.base_velocity.command_ramp_rates = None
# create isaac environment # create isaac environment
env = gym.make(args_cli.task, cfg=env_cfg, render_mode=None) env = gym.make(args_cli.task, cfg=env_cfg, render_mode=None)

View File

@ -11,10 +11,11 @@ from isaaclab.app import AppLauncher
# local imports # local imports
import cli_args # isort: skip import cli_args # isort: skip
# ensure repository root is on the Python path for Hydra registry imports # Prefer this checkout over any other editable ``engineai_lab`` installation.
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path: SOURCE_ROOT = REPO_ROOT / "source"
sys.path.append(str(REPO_ROOT)) if str(SOURCE_ROOT) not in sys.path:
sys.path.insert(0, str(SOURCE_ROOT))
# add argparse arguments # add argparse arguments
parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.") parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.")
@ -70,6 +71,49 @@ torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = False torch.backends.cudnn.benchmark = False
def _restore_training_progress(env, runner, agent_cfg, checkpoint_infos):
"""Restore environment-side curriculum state after a full RSL-RL resume."""
base_env = env.unwrapped
engineai_info = checkpoint_infos.get("engineai_lab", {}) if isinstance(checkpoint_infos, dict) else {}
saved_task = engineai_info.get("task")
if saved_task is not None and saved_task != args_cli.task:
raise ValueError(
f"Checkpoint belongs to task {saved_task!r}, not {args_cli.task!r}. "
"Use --load_mode finetune for cross-task weight transfer."
)
if "common_step_counter" in engineai_info:
common_step_counter = int(engineai_info["common_step_counter"])
state_source = "checkpoint metadata"
else:
common_step_counter = (runner.current_learning_iteration + 1) * agent_cfg.num_steps_per_env
state_source = "legacy checkpoint iteration estimate"
base_env.common_step_counter = common_step_counter
base_env.curriculum_manager.compute(env_ids=None)
base_env.command_manager.reset(env_ids=None)
print(f"[INFO] Restored common_step_counter={common_step_counter} from {state_source}.")
command_term = base_env.command_manager.get_term("base_velocity")
if hasattr(command_term.cfg.ranges, "lin_vel_x"):
print(f"[INFO] Restored forward command range: {command_term.cfg.ranges.lin_vel_x}")
def _attach_training_state_to_checkpoints(env, runner):
"""Make RSL-RL's periodic saves include the environment curriculum counter."""
base_env = env.unwrapped
original_save = runner.save
def save_with_training_state(path, infos=None):
checkpoint_infos = dict(infos) if isinstance(infos, dict) else {}
checkpoint_infos["engineai_lab"] = {
"task": args_cli.task,
"common_step_counter": int(base_env.common_step_counter),
}
original_save(path, infos=checkpoint_infos)
runner.save = save_with_training_state
@hydra_task_config(args_cli.task, "rsl_rl_cfg_entry_point") @hydra_task_config(args_cli.task, "rsl_rl_cfg_entry_point")
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlOnPolicyRunnerCfg): def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlOnPolicyRunnerCfg):
"""Train with RSL-RL agent.""" """Train with RSL-RL agent."""
@ -133,8 +177,25 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
# get path to previous checkpoint # get path to previous checkpoint
resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint) resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
print(f"[INFO]: Loading model checkpoint from: {resume_path}") print(f"[INFO]: Loading model checkpoint from: {resume_path}")
# load previously trained model if args_cli.load_mode == "finetune":
runner.load(resume_path) runner.load(
resume_path,
load_cfg={
"actor": True,
"critic": True,
"optimizer": False,
"iteration": False,
"rnd": False,
},
)
print("[INFO] Loaded actor/critic weights with a fresh optimizer and curriculum.")
else:
checkpoint_infos = runner.load(resume_path)
_restore_training_progress(env, runner, agent_cfg, checkpoint_infos)
print(f"[INFO] Optimizer learning rate after load: {runner.alg.learning_rate:.6g}")
_attach_training_state_to_checkpoints(env, runner)
# dump the configuration into log-directory # dump the configuration into log-directory
dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg) dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg)

View File

@ -30,7 +30,18 @@ setup(
keywords=EXTENSION_TOML_DATA["package"]["keywords"], keywords=EXTENSION_TOML_DATA["package"]["keywords"],
install_requires=INSTALL_REQUIRES, install_requires=INSTALL_REQUIRES,
license="MIT", license="MIT",
include_package_data=True, # 运行时资产显式列出,避免 setuptools 把 mesh/urdf 目录误判为 namespace package。
include_package_data=False,
package_data={
"engineai_lab.assets": [
"pm01/meshes/*",
"pm01/urdf/*",
"gen2/README.md",
"gen2/urdf/*",
"gen2/meshes/*",
"gen2/meshes/collision_simplified/*",
]
},
python_requires=">=3.10", python_requires=">=3.10",
classifiers=[ classifiers=[
"Natural Language :: English", "Natural Language :: English",

View File

@ -1,4 +1,28 @@
import os """运行时机器人资产路径。
# Conveniences to other module directories via relative paths 所有会被 Python 包加载的资产都必须位于 ``engineai_lab/assets``
ASSET_DIR = os.path.abspath(os.path.dirname(__file__)) 这样 editable 安装和 wheel 安装使用相同的路径规则
"""
from pathlib import Path
ASSET_ROOT = Path(__file__).resolve().parent
# 保留 PM01 现有的字符串拼接接口。
ASSET_DIR = str(ASSET_ROOT)
GEN2_ASSET_DIR = ASSET_ROOT / "gen2"
GEN2_URDF_DIR = GEN2_ASSET_DIR / "urdf"
GEN2_ORIGINAL_URDF_PATH = GEN2_URDF_DIR / "robot.urdf"
GEN2_SIMPLIFIED_COLLISION_URDF_PATH = GEN2_URDF_DIR / "robot_simplified_collision.urdf"
GEN2_SIMPLIFIED_COLLISION_MESH_URDF_PATH = GEN2_URDF_DIR / "robot_simplified_collision_mesh.urdf"
__all__ = [
"ASSET_DIR",
"ASSET_ROOT",
"GEN2_ASSET_DIR",
"GEN2_URDF_DIR",
"GEN2_ORIGINAL_URDF_PATH",
"GEN2_SIMPLIFIED_COLLISION_URDF_PATH",
"GEN2_SIMPLIFIED_COLLISION_MESH_URDF_PATH",
]

View File

@ -0,0 +1,70 @@
# Gen2 运行时资产
本目录是 `engineai_lab` Python 包内唯一的 Gen2 运行时资产位置。这样 editable 安装、wheel 安装和从其他工作目录启动时都会得到同一份 URDF 与 mesh。
```text
gen2/
├── README.md
├── urdf/
│ ├── robot.urdf
│ ├── robot_simplified_collision.urdf
│ └── robot_simplified_collision_mesh.urdf
└── meshes/
├── *_visual.stl
├── *_collision.stl
└── collision_simplified/
└── *_box_collision.stl
```
## 三份 URDF 的职责
| 文件 | 用途 | 是否用于训练 |
|---|---|---|
| `robot.urdf` | 原始高精度 visual/collision作为生成源和人工核对基准 | 否 |
| `robot_simplified_collision.urdf` | visual mesh + URDF box collision接触更稳定、计算更轻 | **是,唯一默认训练资产** |
| `robot_simplified_collision_mesh.urdf` | visual mesh + 简化 box STL便于外部工具叠加检查 | 否 |
`source/engineai_lab/robots/gen2.py` 明确加载 `robot_simplified_collision.urdf`。文件缺失时会直接报错,不会静默回退到原始高精度碰撞体,避免不同安装环境得到不同训练物理。
URDF 内 mesh 使用相对路径:
```text
../meshes/<link>_visual.stl
../meshes/<link>_collision.stl
../meshes/collision_simplified/<link>_box_collision.stl
```
不要写机器相关的绝对路径或旧的 `source/gen2_lab` 路径。
## 重新生成简化碰撞体
生成脚本不会覆盖 `robot.urdf`
```bash
cd /home/xtkuang/Projects/cmvr/RL/cmvr_ai_lab
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python \
scripts/gen2_generate_simplified_collisions.py
```
它会更新两份 simplified URDF 和 `meshes/collision_simplified/`,审计 CSV 写入 `outputs/gen2_asset_audit/`
生成后必须执行:
```bash
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python \
scripts/gen2_check_rl_readiness.py
/home/xtkuang/App/anaconda3/envs/engineai_lab/bin/python \
scripts/gen2_visualize_collisions.py
```
静态检查期望 `RESULT=PASS`,且 pelvis/torso 高度目标不能显示 `unknown`。可视化 PNG 写入 `outputs/gen2_asset_audit/`,不写回资产包。
## 修改约束
1. `robot.urdf` 是生成源,先在这里确认 link、joint、limit、mass、inertia 和 mesh。
2. 不要直接手改生成的 simplified URDF 后忘记同步生成脚本。
3. 训练前检查 29 个 link、28 个 joint、全部 mesh 引用和 q0 脚底高度。
4. 历史 `logs/**/params/env.yaml` 中的旧绝对路径只是训练快照,不要批量改写。
5. 供应商原始压缩包放在仓库 `third_party/gen2/`,不进入运行时 wheel。

Some files were not shown because too many files have changed in this diff Show More