Files
chenshijue 4240bb889a 初始提交:关节模组仿真平台
- 三接口契约:自包含 MJCF / 配置 schema / 报告计算规范
- Python 流水线:urdf_to_mjcf → generate_schema → simulate_report(validate_module 一键编排)
- 输入案例 urdf + 生成产物 output(自包含 MJCF/schema/报告/网格副本)
- 详细架构说明 docs/architecture.md
2026-08-28 11:37:47 +08:00

189 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
关节模组 schema 生成器(接口 2 的后端实现)
====================================================================
从一份关节模组 URDF 生成前端要的配置 JSONschema),即 [schema.md](schema.md)
定义的观测契约:输入/输出关节名、减速比、限位、默认仿真参数。
输入/输出关节由用户**显式指定**--input / --output)。因为自动识别在真实 URDF 上
不可靠(见 docs/architecture.md 说明:多级级联有多个正 multiplier 的 mimic,且 fixed 关节也没有
mimic,程序分不清哪级是末端输出)。
减速比来源优先级(同 schema.md 第 5 节):
1. mimic —— 输出关节 <mimic multiplier> 的倒数(默认)
2. manual —— --ratio 手动指定(URDF 无 mimic 时)
用法(在 scripts/ 目录下运行):
python3 generate_schema.py \
--urdf ../urdf/planetary_joint_split_motor_demo.urdf \
--input sun_input_joint --output carrier_output_joint \
--model planetary_joint_split_motor_demo.xml \
--out planetary_joint_split_motor_demo.json
依赖:仅标准库(xml.etree / json / argparse),不需要 trimesh / mujoco。
"""
import argparse
import json
import math
import os
import xml.etree.ElementTree as ET
# 默认仿真参数(与 schema.md / report_spec.md 保持一致)
DEFAULTS = {
"timestep": 0.001,
"duration": 4.0,
"kp": 20.0,
"kd": 0.3,
"amplitude": 2.0 * math.pi,
"frequency": 0.25,
"load_torque": -3.0,
"damping": 0.01,
}
# 输入关节无 <limit>continuous)时的位置限位占位:±6 圈 = ±12π
DEFAULT_POSITION_LIMIT = 12.0 * math.pi
# 输入力矩限位占位(应填真实电机额定值,见 schema.md limits.torque
DEFAULT_TORQUE_LIMIT = 10.0
def _float(attr):
try:
return float(attr)
except (TypeError, ValueError):
return None
def parse_joints(root):
"""返回 {关节名: <joint> 元素}。"""
return {j.get("name"): j for j in root.findall("joint")}
def get_mimic(joint_el):
"""返回 (joint, multiplier, offset) 或 None。"""
m = joint_el.find("mimic")
if m is None:
return None
return {
"joint": m.get("joint"),
"multiplier": _float(m.get("multiplier")),
"offset": _float(m.get("offset", 0.0)),
}
def get_limit(joint_el):
"""返回 (lower, upper, velocity) 或 None。关节无 <limit>continuous)返回 None。"""
lim = joint_el.find("limit")
if lim is None:
return None
return {
"lower": _float(lim.get("lower")),
"upper": _float(lim.get("upper")),
"velocity": _float(lim.get("velocity")),
}
def main():
ap = argparse.ArgumentParser(description="关节模组 URDF → schema JSON")
ap.add_argument("--urdf", required=True, help="输入 URDF 路径")
ap.add_argument("--input", required=True, help="输入关节名(电机端,显式指定)")
ap.add_argument("--output", required=True, help="输出关节名(模组末端,显式指定)")
ap.add_argument("--model", default=None,
help="要引用进 schema.model 的 MJCF 文件名(默认 <robot名>.xml")
ap.add_argument("--out", default=None, help="schema 输出 JSON 路径(默认 <robot名>.json")
ap.add_argument("--module-id", default=None, help="module_id(默认 robot 名)")
ap.add_argument("--name", default=None, help="显示名(默认 robot 名)")
ap.add_argument("--ratio", type=float, default=None,
help="手动指定减速比(URDF 无 mimic 时用,gear_ratio_source=manual")
ap.add_argument("--torque-limit", type=float, default=DEFAULT_TORQUE_LIMIT,
help="输入力矩限位 [N·m],默认 ±10(占位)")
ap.add_argument("--position-limit", type=float, default=DEFAULT_POSITION_LIMIT,
help="输入关节无 <limit> 时的位置限位 ±rad,默认 ±12π")
for k, v in DEFAULTS.items():
ap.add_argument(f"--{k.replace('_', '-')}", type=float, default=v,
help=f"仿真参数 {k}(默认 {v}")
ap.add_argument("--mode", choices=["normal", "overload"], default="normal",
help="仿真模式(normal / overload,默认 normal")
args = ap.parse_args()
tree = ET.parse(args.urdf)
robot = tree.getroot()
robot_name = robot.get("name")
joints = parse_joints(robot)
if args.input not in joints:
ap.error(f"输入关节 '{args.input}' 在 URDF 中不存在;可用关节:{sorted(joints)}")
if args.output not in joints:
ap.error(f"输出关节 '{args.output}' 在 URDF 中不存在;可用关节:{sorted(joints)}")
# ---- 减速比 ----
mimic = get_mimic(joints[args.output])
gear_ratio = None
gear_ratio_source = None
if mimic is not None and mimic["multiplier"] not in (None, 0.0):
gear_ratio = 1.0 / abs(mimic["multiplier"])
gear_ratio_source = "mimic"
elif args.ratio is not None:
gear_ratio = args.ratio
gear_ratio_source = "manual"
else:
ap.error("输出关节没有 <mimic>,请用 --ratio 手动指定减速比")
# ---- 限位 ----
in_limit = get_limit(joints[args.input])
if in_limit is not None and in_limit["lower"] is not None and in_limit["upper"] is not None:
pos_lim = [in_limit["lower"], in_limit["upper"]]
else:
pos_lim = [-args.position_limit, args.position_limit]
velocity = in_limit["velocity"] if in_limit is not None else None
module_id = args.module_id or robot_name
model = args.model or (module_id + ".xml")
out_path = args.out or (module_id + ".json")
schema = {
"module_id": module_id,
"name": args.name or robot_name,
"model": model,
"input_joint": args.input,
"output_joint": args.output,
"gear_ratio": gear_ratio,
"gear_ratio_source": gear_ratio_source,
"limits": {
"position": pos_lim,
"torque": [-args.torque_limit, args.torque_limit],
},
"simulation": {
"timestep": args.timestep,
"duration": args.duration,
"kp": args.kp,
"kd": args.kd,
"amplitude": args.amplitude,
"frequency": args.frequency,
"load_torque": args.load_torque,
"damping": args.damping,
"mode": args.mode,
},
}
if velocity is not None:
schema["limits"]["velocity"] = [-velocity, velocity]
with open(out_path, "w", encoding="utf-8") as f:
json.dump(schema, f, ensure_ascii=False, indent=2)
f.write("\n")
print(f"已生成 schema: {os.path.abspath(out_path)}")
print(f" module_id : {module_id}")
print(f" input_joint : {args.input}")
print(f" output_joint: {args.output}")
print(f" gear_ratio : {gear_ratio:.6f} (source={gear_ratio_source})")
print(f" position_lim: {pos_lim[0]:.4f} ~ {pos_lim[1]:.4f} rad")
print(f" torque_lim : ±{args.torque_limit} N·m")
print(f" mode : {args.mode}")
print(f" load_torque : {args.load_torque} N·m")
print(f" damping : {args.damping}")
if __name__ == "__main__":
main()