22 KiB
Go2 前视射线避障部署契约 v1
API
默认任务白名单:Unitree-Go2-Flat、Unitree-Go2-Rough、Unitree-Go2-ObstacleAvoidance。
GET /api/training/health 的 tasks 保持字符串列表,新增 taskMetadata:每项含 id/name/browserCompatible/terrainPresets/terrainParameters/sensorTypes/sensorParameters/mapSyncScope。参数元数据含 min/max/default,计数含 integer:true。
POST /api/training/jobs 保留原参数,并接受:
{
"taskId": "Unitree-Go2-ObstacleAvoidance",
"numEnvs": 1024,
"maxIterations": 1000,
"seed": 42,
"runName": "obstacle-navigation",
"device": "gpu",
"gpuIds": [0],
"wandbMode": "offline",
"terrainPreset": "discrete_obstacles",
"terrainParams": {
"size": 12,
"obstacle_count": 24,
"obstacle_height_min": 0.2,
"obstacle_height_max": 0.6,
"spacing": 1.2,
"friction": 0.8
},
"sensorType": "raycast",
"sensorCfg": { "fov": 90, "maxDistance": 4, "safetyDistance": 0.5, "avoidanceWeight": 2 }
}
只实现 raycast,不是 RGB 或真实深度相机;camera_depth 明确拒绝。sensorCfg.type 可选但只能是 raycast。默认观测数量32;显式multi模式固定48,不接受任意数量(见文末多层契约)。预设与字段严格白名单校验,布尔、非有限、超范围值拒绝。地形 size 8–24m、障碍物数1–100,实际数量受 spacing 容量限制。全部可选参数与范围见 task_config.py。普通训练的奖励 preset 仍仅允许 Flat;Obstacle专属自调参见文末。
创建和查询 job 返回 deployment,即该作业的确定性部署配置;列表中的 job 也携带此字段。不新增端点;ONNX 下载仍为 /api/training/jobs/{id}/artifacts/policy.onnx。自定义配置由服务写入作业目录 training_config.json,以参数数组 --task-config <server-owned-path> 传入训练器,绝不使用 shell。训练器再次校验配置与seed。
policy.onnx 的 platform_deployment metadata 字符串是同一对象的JSON编码,旁边同时导出 deployment.json。ONNX中包含Actor观测归一化,不要在前端再套一层运行均值/方差。其他标准 mjlab metadata 保留。旧 Rough 实际 Actor 为234维(47+17×11 downward height scan),browserCompatible:false;前端必须禁用一键加载,不能按 Flat 处理。没有新配置的旧 Flat/tuning 保持原环境行为。
地图:精确碰撞布局,不是同名编辑器高度场
deployment.terrain 为 boxes-v1:size/friction/boxes/spawn/spawnQuaternion/target/actualObstacleCount/approximation。
- 单块正方形地图,世界原点在地图中心,+X前、+Y左、+Z上,米/弧度。每个 box 的
pos是世界中心,size为半尺寸,yaw=0。底板占z=[-0.2,0]。无额外无限地面/边墙。 - 直接用导出的boxes构造碰撞几何,不要重新运行浏览器的同名地形生成器。
rough/wave为16×16有界box离散化,pyramid_stairs为四层box;approximation:true必须在UI标识训练专用近似布局。 - 障碍物坐标由
random.Random(seed)对确定性格点洗牌;宽深均0.4m、高度按范围随机,实际数量受spacing与地图容量限制。两端保留平坦出生/目标条带。无需在JS复刻Python RNG,布局本身是权威结果。最多257个boxes。 spawn=[-size/2+1,0,0.32]、四元数wxyz=[1,0,0,0]、目标[size/2-1,0]是部署及固定评估的参考起终点。训练时每个episode在同一连通自由区域内重新采样起终点(障碍/边界净空>0.55m、间距>=2m)并随机化初始yaw,初始速度为0;不会跨不可通行墙体抽取目标。MuJoCo地形原点仍为参考spawn,随机复位直接写入世界坐标。- mjlab采用1×1 patch,同一张地图用于多个互相独立的Warp环境世界;env origin就是出生点而非地图中心。目标在每个env中用
env_origin.xy+[size-2,0],不是错误地再加地图中心。 - terrain摩擦为
[friction,0.005,0.0001],priority=1、condim=3;足端startup滑动摩擦固定为相同值。浏览器应保持匹配的摩擦及足端接触配置。 - “同步当前场景地图”导出已应用场景的
custom_boxes权威布局,细则见下节;预设生成模式仍保持原行为。
已应用场景 → custom_boxes
请求使用 terrainPreset: "custom_boxes"、customTerrainBoxes: <完整boxes-v1对象>;terrainParams 可省略,若提供则只能是与布局完全一致的 {size, friction}。服务和训练入口均重新验证,不读取客户端路径或XML,不运行地形RNG、不降级预设。作业目录task JSON、deployment JSON及ONNX metadata中的terrain使用同一布局。health的terrainPresets公布能力。
- 浏览器从成功编译的全部sceneMaps静态碰撞
geom_xpos/geom_xmat/geom_size提取,包括工程地图路径、重复实例和嵌套body变换;不使用Three装饰包围盒、编辑器RNG或不完整XML。动态机器人、无碰撞装饰排除;未声明地图身份的其他静态碰撞报错,不能漏障碍。 - box世界半尺寸为
abs(R)*halfsize;sphere、capsule、ellipsoid、cylinder使用解析精确世界AABB。旋转box与非box转换会扩大碰撞占据区域。mesh/hfield明确拒绝;项目地图原有include导入限制不变,已经编译模型的include变换不重新解析。 - 固定floor为中心
[0,0,-.1]、半尺寸[size/2,size/2,.1]。仅明确命名floor/ground/flat的朝上水平z=0 plane,以及顶面z≈0的水平支撑box归并。地下/坑底/非零高度或倾斜plane拒绝,不能静默填坑。其他障碍原坐标保留。底板覆盖整个正方形范围,可能填补支撑面间空白;UI明确说明标准化,不保证与原场景几何同构。 - 最多256障碍+1底板,超量报错不截断;世界原点不变,尺寸8–24m,取覆盖应用地图/几何的正方形,不clamp或平移几何。XY必须在floor内,Z界限[-.2,12],所有半尺寸严格>0(包括拒绝负零),有限数值。摩擦必须统一为
[f,.005,.0001]且f为.2–2,混合值报错要求用户先显式统一,不能静默覆盖。 approximation对custom必须为true(AABB及标准底板转换);actualObstacleCount必须等于boxes数减一。字段严格白名单,不信任声明值。floor必须标准。出生z固定.32,默认XY/四元数取成功加载时真实初始浮动基座姿态,不取跑动后的pose;目标建议为初始朝向前方3m,不自动寻找安全点。面板提供最小出生/目标XY配置,修改后需重新同步。- 出生及目标中心必须距边界>=.5m;与每个非底板障碍XY AABB的最近距离必须严格>.5m(圆形安全区,包括切触拒绝),不能删除或移动障碍以修复。四元数必须有限且归一化。后端env origin、初始化姿态、目标偏移和越界中心按自定义spawn计算。
- 同步/提交均拒绝未应用草稿、训练部署替换后的场景、加载中或已过时场景;提交前再次比较当前编译布局。HTTP
MAX_REQUEST_BYTES与训练JSON上限均128KiB,足够257个double-precision boxes且有界;ONNX部署metadata上限仍100KB。
81维观测/12维动作
Actor顺序(float32):
| slice | 内容 |
|---|---|
| 0:3 | robot/imu_ang_vel,本体坐标角速度rad/s |
| 3:6 | 本体坐标重力单位向量(静止 [0,0,-1]) |
| 6:9 | 下述目标导航速度命令 [vx,0,wz] |
| 9:11 | sin/cos(2π×episodeTime/0.6);命令范数<0.1时均为0 |
| 11:23 | 相对默认关节位置 |
| 23:35 | 关节速度,无缩放 |
| 35:47 | 上一原始12维动作,不是力矩或位置目标 |
| 47:79 | 32条前向测距,miss→1,否则clamp(distance/maxDistance,0,1) |
| 79:81 | [headingError/π, clamp(goalDistance/size,0,1)] |
关节顺序FL/FR/RL/RR,每腿hip/thigh/calf;默认角 [-.1,.9,-1.8, .1,.9,-1.8, -.1,.9,-1.8, .1,.9,-1.8]。
位置目标 defaultJointPosition+0.25*rawAction;kp每腿 [20,20,40],kd [1,1,2],力矩限幅 [23.5,23.5,45]。后端物理dt=.005,decimation=4,策略50Hz;浏览器仍可更小物理dt并采用single-in-flight/held-action。Go2-W轮式模型不是此任务的同构训练机器人,不应声称动力学严格等同。
射线
安装在 base_link。对 i=0..31:
angle=(-fov/2+i*fov/31)*π/180,局部direction=[cos(angle),sin(angle),0]。- 局部offset=
[.3,0,.05],origin=baseWorldPosition+Rbase*offset,directionWorld=Rbase*direction。采用完整body姿态,不可只用yaw。采样从右到左且包括两端;无恰好0°的中间射线。 - 后端地形group0、机器人visual group2/collision group3,include_geom_groups=(0,)排除整个机器人。浏览器必须语义等价地仅射向地形(如其group2),不能照搬group0或只排除base_link。包含地面;倾斜时地面命中也构成感知数据。miss或超过range归一化为1。
- 前向扇形只能在一个高度切片感知;矮障碍物/跌落/盲区并不保证可检测,不能称安全导航保证。
导航及episode
delta=target.xy-basePosition.xy;distance=hypot(delta);yaw由base wxyz计算;headingError=atan2(sin(atan2(dy,dx)-yaw),cos(...))。未到达时 vx=navigation.speed*max(0,cos(headingError))(缺省.6,调参可.3–1.2)、vy=0、wz=clamp(headingError,-1,1)。distance<.5m到达后command全0、headingError=0,distance observation仍保留实际距离;不重采样新目标,不因到达施加失败惩罚。移动机器人离开半径后自然恢复导航。
训练episode20s,超时重置;跌倒、非足端接触force>10N、机器人中心越过地图边界内0.3m终止并重置。训练复位随机选择同一连通自由区域的起终点和初始yaw,恢复初始关节姿态及零速度,并清phase/上一动作;固定评估仍使用deployment声明的参考起终点以保持版本间可比。浏览器episode若不自动重置必须明确自己的评测行为;到达至少要停命令而非无限推进。奖励保留速度跟踪/平滑/姿态/非法接触终止惩罚,并增加朝目标世界速度投影及归一化近障平方惩罚。
验证
.venv/bin/python -m unittest discover -s training_server/tests
.venv/bin/python -m ruff check training_server
GO2_RUN_MJLAB_SMOKE=1 MUJOCO_GL=egl .venv/bin/python -m unittest discover -s training_server/tests -p test_obstacle_env.py
WANDB_MODE=disabled .venv/bin/python training_server/rl/scripts/train.py Unitree-Go2-ObstacleAvoidance --env.scene.num-envs=4 --agent.max-iterations=1 --gpu-ids '[0]' --output-dir /tmp/go2-obstacle-train-smoke
GPU smoke对比真实mjlab/Warp与MuJoCo mj_ray距离、检查多env出生点/关节顺序/81维有限obs。1iteration只验训练/导出管线,不证明策略已学会避障或Sim2Sim行为收敛。
Obstacle DeepSeek 自调参(obstacle-v1)
POST /api/tuning/sessions 新支持 taskId=Unitree-Go2-ObstacleAvoidance。
沿用 automatic/approval、运行时双向切换、稀疏patch、最多4项/单轮0.5–2倍、工程护栏与revision CAS;Flat完整schema、奖励应用与相对基线六维评分不变。
可携带与训练相同的顶层terrain/sensor字段,或单独taskConfig对象(不能混用);场景/seed仅创建时确定。所有字段/范围、NaN/Inf、跨任务patch/护栏拒绝;旧revision返回409且不修改配置/审计/待审批项。
| JSON路径 | 范围;默认 | 真实环境映射 |
|---|---|---|
weights.avoidance_weight |
.5–5;2(创建时可取sensorCfg值) | obstacle_proximity.weight=-value |
params.target_velocity |
.3–1.2;.6 m/s | NavigationCommandCfg.speed,不是奖励权重 |
weights.collision_penalty |
-10–-.5;-5 | obstacle_collision:同illegal_contact函数/参数,非足端地形接触>10N指示量 |
weights.action_smoothness |
-.05–-.001;-.05 | action_rate_l2.weight |
保留原is_terminated等非白名单项,collision_penalty额外独立惩罚非法接触,不用它替代客观碰撞统计。服务写每trial独立reward/task JSON,以--reward-config/--task-config argv传入真实训练器和评估器;训练入口先应用场景再应用调参。导出ONNX/deployment的navigation.speed与sensorCfg.avoidanceWeight反映实际配置,浏览器仍81→12且使用配套速度。旧无调参模型保持.6。奖励Preset在普通训练面板仍仅Flat可选;Obstacle可从调参工作台直接导入最佳带metadata策略,不把Obstacle preset混入Flat训练。
固定客观评估与安全门槛
- 固定seed 101/202/303、每seed 1000策略步×.02s=20s;每env仅统计reset后的第一个episode。提前终止后仍运行完整horizon,但不将自动reset后新episode混入首episode;剩余步数不能带来平滑/净距奖励。各seed先按env等权均值,再三seed等权。evalNumEnvs创建时固定(所有trial相同),Agent不能改seed、horizon、权重、场景、传感器、起终点。
- 固定评估按三个seed分别构造布局,非随机预设不保证三张不同地图;评估关闭训练期随机起终点。custom严格保持上传的同一地图及验证过的参考spawn/target,标记
fixed-custom-map,不是三个随机地形。保留任务原有reset_joint/startup随机化并用seed复现;若轨迹相同仅是确定性重复试验,不能当作泛化证据。 - 读取真实checkpoint Actor网络及其
obs_normalizer统计,strict加载;没有零动作/假策略fallback,不读取训练reward作为指标。不同地图CUDA编译隔离:三个seed顺序启动同一Python解释器子进程(非fork/非并行),共享不可变checkpoint快照并核对SHA256;父进程使用独立临时目录,worker继承父进程组,现有session取消会终止worker,每seed超时1200s即整轮失败。任意seed失败、缺失、错身份、样本不足、非有限或协议不一致整轮fail closed,不用部分结果凑均值。 - hook在termination manager计算后、
_reset_idx之前捕获包括首terminal的物理量。与mjlab终止判断一致,派生pose最多滞后一个.005s物理子步;接触force history覆盖全部4子步。每步读取目标水平距离、前视射线命中比例、非底板障碍净距、实际action差分、接触/跌倒标志。前视ray_hit可包含地板,仅作为感知诊断,不直接评分。 - 擦碰/碰撞:任一机器人(含足端)与非底板障碍的实际接触力幅值>1N,或非足端与任意地形(含地面)>10N。使用编译terrain_1…几何的独立contact sensor(maxforce+4子步history),排除terrain_0标准底板;不是用ray命中推测碰撞。1N以下触碰不计,不能称零接触安全保证。
- 跌倒:base高度<.12m或姿态倾角>70°(projected_gravity.z > -cos70°)。到达:水平距离<.5m。成功:episode内到达,且整段首episode无碰撞、无跌倒;先到达后擦碰/跌倒也失败。未成功时间项必为0,不因提前跌倒取短时间高分。
固定五项0–1分量:success为成功指示;time成功时1-firstArrivalStep/1000,否则0;clearance每步clamp((baseXY到非底板box XY AABB距离-.3m)/.5m,0,1),按1000步求均值(终止后补0,任何跌倒整项0,无障碍时每有效步1);smooth每步1-clamp(mean((action_t-action_t-1)^2),0,1),初始action=0,按1000步求均值(终止后补0);no_fall为无跌倒指示。净距是保守圆形机身足迹代理,并非全机身mesh最近距离,地板不作为障碍、躺地不当安全。
绝对总分=.4*success+.2*time+.2*clearance+.1*smooth+.1*no_fall。例如两步简化fixture,第二步无碰撞到达,净距均.25m、action差分MSE均.5,得分.4+0+.1+.05+.1=.65。固定1000步实际协议不接受fixture horizon。
相对初始baseline的硬门槛:fall_rate<=baseline+.02且success>=baseline-.02(边界包含;1e-12浮点容差)。不通过eligible=false且score=-1,不能用其他高分绕过。baseline也用完整客观绝对评分,不默认指标为0。result记录完整协议/场景布局、seed结果、样本数、checkpoint SHA256、各项均值,candidate必须与baseline协议完全一致。
显式多层射线(阶段5;不是局部高程图)
sensorCfg.sensorMode 是唯一模式字段:缺省/single_ring_raycast 保持旧32ray/81obs及旧proximity奖励行为;显式 multi_ring_raycast 为3×16=48ray/97obs。sensorType/type仍为raycast。health新增sensorModes,UI可选择;不是任意自定义扫描图案。没有局部高程图网格、用途或输入定义,本阶段未实现高程图。
任务JSON、deployment JSON及ONNX metadata规范化导出以下字段(FOV仍叫fov):
{
"sensorMode": "multi_ring_raycast",
"rayCount": 48,
"pitchAngles": [0, -20, -45],
"yawCount": 16,
"yawAngles": [-45, -39, -33, -27, -21, -15, -9, -3, 3, 9, 15, 21, 27, 33, 39, 45],
"fov": 90,
"angleUnit": "deg",
"rayOrder": "layer-major"
}
旧single规范化为pitch [0]、yawCount/rayCount=32。所有yaw为-fov/2+i*fov/(yawCount-1)含两端,从右到左,无0度中心ray。只允许两种固定组合;显式矛盾字段/未知字段/不支持的模式拒绝,不静默覆盖。旧metadata缺新增字段可规范化后与新job语义匹配;旧81 graph绝不冒充97。
局部direction=[cos(pitch)*cos(yaw),cos(pitch)*sin(yaw),sin(pitch)],先一层16yaw再下一pitch层;origin仍为basePosition+Rbase*[.3,0,.05],direction乘完整base姿态。后端实际RayCastSensorCfg使用同一pattern;浏览器预计算局部direction并缓存每个已编译静态box世界变换,解析slab求交;只对精确单位旋转用轴对齐快速路径,无变换近似。
97维slice:基础0:47完全不变,真实测距47:95,目标误差95:97仍为原有符号的heading/π与距离归一化。miss/超range→1,地板命中保留真实距离;调参target_velocity继续传入真实command与导出navigation.speed。原single-in-flight、held-action、事务导入和资源释放协议不变。三seed评估仍是顺序独立解释器;advisor按实际32/48模式说明盲区。
地板与奖励
仅multi的obstacle_proximity排除标准底板顶面:首次计算用CPU编译模型验证唯一与首box相符的静态world-weld/group0 box(世界中心[0,0,-.1]、半尺寸[size/2,size/2,.1]、单位世界旋转),再核实生成器名称terrain_0,失败关闭;不是只根据名称。真实编译terrain为固定body而非bodyid=0,使用weld身份。
当前mjlab RayCastData没有hit geom ID,因此是几何分类过滤:必须finite实际命中,hit世界XY在floor范围(容差1e-5m),abs(hit.z)<=1e-5m,normal与+Z逐分量误差<=1e-5。每个Warp环境是独立同坐标地图,hit不额外加env_origin。容差为float32的10微米,不把5cm低障碍抹除。低box顶面、侧面不滤;全floor/miss惩罚0且finite,其余仍用最近有效距离的原平方归一化。观测数据不原地修改。与底板顶面共面的几何无法凭此分类区分,不能声称等价命中geom ID;台阶/非零高度表面不是标准地板。
下倾层改善部分低障碍/有界地板边缘感知,但层间、侧后方、遮挡和有限range仍有盲区;标准boxes-v1底板本身会填补内部空洞,不能声称已解决坑探测或安全导航。
阶段5复现
.venv/bin/python training_server/tests/generate_multi_ring_golden.py
GO2_RUN_MULTI_SMOKE=1 MUJOCO_GL=egl .venv/bin/python -m unittest discover -s training_server/tests -p test_multi_ring.py
WANDB_MODE=disabled MUJOCO_GL=egl .venv/bin/python training_server/rl/scripts/train.py Unitree-Go2-ObstacleAvoidance --env.scene.num-envs=4 --agent.max-iterations=1 --gpu-ids '[0]' --task-config /tmp/go2-multi-ring-stage5/task.json --output-dir /tmp/go2-multi-ring-stage5/train
真实4env/1iteration新97策略仅验训练、归一化Actor导出和浏览器ORT链路,不是旧81 checkpoint改metadata,不证明导航成功。CLI子进程--help回归分别验证Flat/Rough/Obstacle的任务注册;修复前直接CLI仅注册mjlab内建任务,原错误日志保留。
事务重导入回归确认97→97、97→81→97合法graph/metadata成功;伪97metadata+81graph与缺失ORT算子均在预期阶段报具体错误并保留旧会话。曾看到“模型编译失败”是App只转抛diagnostic.summary遮蔽了原detail,并非实际MJCF编译失败;已改为detail优先,未改事务资源/rollback。
复审修复:奖励preset任务归属
公共preset仍可保存Obstacle调参结果,但save/get/list均从来源session的config.taskId恢复权威任务归属并验证完整reward schema;列表和单项返回taskId。历史session仅缺taskId时按既有Flat默认解释,且必须通过完整Flat schema。来源缺失/损坏、显式未知或空taskId、损坏preset均失败关闭(列表不返回部分可信结果);不相信客户端声明,也不凭奖励字段猜任务。
普通本地训练的rewardPresetId入口仍仅支持Flat;resolver核对来源任务与请求任务,服务再次验证完整schema,跨任务/损坏preset返回400且不创建job。UI仅展示服务明确标记Flat的preset,旧服务无身份字段时不展示,而不是默认为Flat。Obstacle preset UI未扩展;Obstacle调参最佳策略配套导入、Approval/Automatic均保持原协议。