第 6 章 执行器、验证与求解器¶
结论可以概括为:同一个关节上并挂两套执行器属于常态而非异常;按名字建"关节→执行器"表会静默选中力矩槽位,症状是"命令有值、关节不动、ctrl 读数正常"。
1. 出力构成与执行器建表¶
1.1 执行器的出力构成¶
一条执行器最终出的力只由一个公式决定,而三个类型开关(gain / bias / dyn)决定它长成 motor、position 还是肌肉。
公式是 scalar_force = gain_term * (act or ctrl) + bias_term(XML reference 的 actuator 一节):增益项乘上"内部激活值或命令",再加偏置项。三个类型开关各自取值:dyntype 有 none / integrator / filter / filterexact / muscle / user(默认 none),其中 filter 的方程是 act_dot = (ctrl - act) / dynprm[0];gaintype 有 fixed / affine / muscle / so3 / user(默认 fixed);biastype 有 none / affine / muscle / so3 / user(默认 none)。钳位则分两处:ctrllimited 把命令钳进 ctrlrange,forcerange 钳的是输出力。作用顺序:
flowchart LR
c["ctrl"] --> ct["ctrllimited<br/>钳进 ctrlrange"]
ct --> g["增益项<br/>gaintype × gainprm"]
d["dyntype<br/>有内部状态时先更新 act"] --> g
b["偏置项<br/>biastype × biasprm"] --> s(("scalar_force"))
g --> s
s --> fl["forcelimited<br/>钳进 forcerange"] --> gear["gear 传动比"] --> out["关节空间广义力<br/>qfrc_actuator"]
三个开关的三元组决定元素到底是 motor、position 还是肌肉,钳位两处各管一端。
3.11 可用的快捷元素:general、motor、position、orientation、velocity、intvelocity、damper、cylinder、muscle、adhesion、dcmotor、plugin。crank/follow 在 3.11 已不存在(引用它们的老示例要改成 cranksite/slidersite 传动写法)。文档里也没有 filtershape 这个属性。
"纯力矩"的形式定义就藏在这一行里:<motor> 是"直接驱动执行器"的快捷写法,内部映射成 dyntype=none、gaintype=fixed、gainprm=1、biastype=none,于是公式退化成立力 = ctrl。
⇒ 判据是三元组 (gaintype, biastype, dyntype),与名字无关。<position> 的差别就在 biastype=mjBIAS_AFFINE(验证见下表)。
1.2 同关节多执行器与建表方式¶
模型:arm_joint1 上同时挂 position_idx61_arm_r_joint1(kp 可设)与 motor_idx61_arm_r_joint1,另有一台只挂 motor 的 arm_joint2。编译后的三台执行器长这样:
| 槽位 | 名字 | 挂在 | gaintype | biastype | dyntype | gainprm[0] |
|---|---|---|---|---|---|---|
| 0 | position_idx61_arm_r_joint1 |
arm_joint1 |
FIXED | AFFINE | NONE | 100 |
| 1 | motor_idx61_arm_r_joint1 |
arm_joint1 |
FIXED | NONE | NONE | 1 |
| 2 | motor_idx61_arm_r_joint2 |
arm_joint2 |
FIXED | NONE | NONE | 1 |
两种建表方式从同一张模型里得到不同的结果:按名字(取最后一个)选到的是两个 motor_* 槽位,而按 (gaintype, biastype, dyntype) 判类型,arm_joint1 上选到的是 position_,arm_joint2 则被归为"全是纯力矩"。
把 IK 解出的"1.4 rad"当命令灌进去(跑 1500 步 = 3.000 s 仿真时间,autoreset 关掉以排除干扰):
| 位置环路 kp | 写入槽位 | 结果 qpos |
若目标是 1.4 rad 的误差 |
|---|---|---|---|
| 100 | 按名字(motor 槽) | 0.014000 rad | 1.386 rad |
| 100 | 按类型(position 槽) | 1.399994 rad | 6e-6 rad |
| 10 000 | 按名字(motor 槽) | 0.000140 rad | 1.39986 rad |
| 10 000 | 按类型(position 槽) | 1.400010 rad | 1.0e-5 rad |
这个经典症状被完整复现出来,并且给出了它的放大机制:
- 1.4 被当成 1.4 N·m 灌进力矩槽;
- 与此同时位置那套以
ctrl=0、kp=10⁴把关节按在零位; - kp 越大,关节越接近零——0.000140 rad。"执行器增益调得更狠"会让"不动"更彻底,这是最容易误诊成"电机没使能"的方向。
ctrl读数全程正常(1.400),因为写进去的值本身就是合法的。读 ctrl 不能证明你写对了槽位。
想确认自己的建表函数经得住更乱的现场:在这个模型上再挂第三个执行器(velocity 型)到 arm_joint1,看三种类型下选出的还是不是同一个;再把 position 的 kp 扫到 1e5/1e6,观察写进 motor 槽后 qpos 进一步趋近 0——这条趋势就是“增益越大越像电机没使能”的证据;最后故意把 ctrllimited 关掉、ctrlrange 留着,确认命令不再被钳位,理解两个属性的分工。
坑:按名字建表会静默选中力矩槽
同一关节挂 position 与 motor 两台执行器时,按名字取“最后一个”拿到的是电机槽:命令被当成 N·m,关节反而被位置环按在零位——ctrl 读数全程正常,只有关节不动。
2. 雅可比对照与验证方法论¶
结论是:"扳动一个关节而 TCP 未动"很可能正是正确行为。在做任何"执行面通不通"的验证之前,先量一遍每个关节对目标点的位移贡献。
mj_jac 计算末端执行器的运动学雅可比,一次给出平移块 jacp 与旋转块 jacr,每块都是 3×nv:每一行是"指定点的对应坐标相对各自由度的偏导数"(APIfunctions)。mj_jacBody / mj_jacBodyCom / mj_jacGeom / mj_jacSite 只是内部调用它的快捷方式,mj_jacPointAxis 则把点与轴分开给。它的前置依赖是运动学(mj_kinematics,随后 mj_comPos),时间导数版本是 mj_jacDot。约束侧的雅可比不在这个结构里——所有标量约束违规量的雅可比都集中在 mjData.efc_J。
调用时注意 3.11 的 Python 签名多了一个必填的 site 索引:
## 两个输出都要预先分配好 (3, nv) 且内存连续;site_id 用 mujoco.mj_name2id 取
mujoco.mj_jacSite(m, d, jacp, jacr, site_id)
按旧文档的四参数写法调用会当场失败,异常文本把支持的签名整个列了出来——签名不匹配不会静默,比第 5 章那种静默错位友好得多。
2.1 雅可比与 TCP 位移对照¶
模型:三段链,j1_roll(hinge,轴 = 局部 z,TCP 落在这根轴上)、j2_pitch(hinge,轴 = 局部 y,力臂 0.35 m)、j3_roll(hinge,轴 = 局部 z,同样穿过 TCP)。静止时 TCP 在世界坐标 [0.0, 0.0, 0.65] 米。每个关节单独转 0.5 rad,量到的位移与雅可比列范数放在一起:
| 关节 | qpos_adr / dof_adr |
实际位移 | \|\partial p/\partial q\| |
力臂 |
|---|---|---|---|---|
j1_roll |
0 / 0 | 0.0000 mm | 0.000000 | 轴穿过 TCP |
j2_pitch |
1 / 1 | 173.1828 mm | 0.350000 | 0.35 m |
j3_roll |
2 / 2 | 0.0000 mm | 0.000000 | 轴穿过 TCP |
三条互相独立的路径对上了同一个结论:
| 路径 | 读数 | 说明 |
|---|---|---|
正向运动学对照(mj_forward 后取 site_xpos) |
0 / 173.1828 / 0 mm | 引擎真搬动了什么 |
雅可比列范数(mj_jacSite) |
0 / 0.350000 / 0 | 瞬时灵敏度(单位弧度对应的米) |
解析式 2r·sin(θ/2),r=0.35、θ=0.5 |
173.18 mm | 弦长,不是 r·θ=175 mm |
注意第三行的 173.18 vs 175:小角度近似在这里差 1%,量 0.5 rad 必须用弦长公式;拿 r·θ 当"位移"会在 1 rad 时给出 6% 的系统偏差。
⇒ 与一组真机数字(沿臂轴的滚转轴转 0.5 rad → 0 mm;能搬动 TCP 的是 394 / 252 / 96 mm)完全同构。选验证关节时要用右边那两列都非零的关节。
把 j2_pitch 的力臂改成 0.65 m,先用弦长公式预测位移,再执行一次对照(不可使用 r·θ);再用 mj_jacBodyCom 与 mj_jacSite 对同一 body 求列范数,差异来自质心与 site 位置不同;最后增加一个 mj_jacDot 读数,在 qvel≠0 时与 Δq 的有限差分对照,检查一致到多少位。
2.2 验证方法论¶
这一节是真正的方法论内容。做“发布面通不通”的探针时:
- 先离线量一遍逐关节对目标点的位移表(就是「雅可比与 TCP 位移对照」那张表),挑一个搬得动它的关节;
- 表本身要打印出来当证据(“我转了 j2_pitch 0.5 rad,TCP 从…到…,位移 173.18 mm”);
- 如果某关节的列范数为 0,那它从没被这套验证覆盖过——某列全 0 说明验证没量到它,而不是它工作正常。
同一类问题在时序上还有一个变体:延迟指标的起点要与体感同定义(人感觉到的延迟是"停手 → 画面送达"),而「引擎侧的三个对照工具」里"传感器在积分之前算"(第 5 章)意味着读数是本步开始时的状态。两者都要求:先定义谁来计时,再取数。
改朝向、换机位时不能只信代码路径:拿一个已知真实位置的点(例如地面标记),从它的像素/机体读数反算世界坐标,误差应该在毫米级。这与第 4 章的"转身不变量"是同一族判据——用外部真值,而不是用自己的函数。
2.3 转移导数与逆导数¶
mjd_transitionFD 给出一步状态转移的线性化:A = ∂(qpos₊, qvel₊)/∂(qpos, qvel)(2nv×2nv)、B = ∂/∂ctrl,C/D 是传感器侧。单 hinge + position 执行器(kp=20、kv=2、dt=0.002):单次 102 µs,A = [[0.994725, 0.001341], [−2.637363, 0.67033]]、B = [0.005275, 2.637363];eig(A) = (0.9834, 0.6816) 都在单位圆内——第二个特征值的强收缩来自 implicitfast 对阻尼的隐式处理,这是把离散 A 当稳定性诊断的入口。flg_centered 在这个光滑线性系统上单侧与中心差分只差 5.6e-11——eps 与差分格式的调优真正要紧的位置是接触等不连续处(computation 章 Derivatives)。mjd_inverseFD 是逆动力学侧的对应物,输出对 q/qvel/act 与 ctrl 的导数。
3. 约束模型与求解器默认值¶
3.1 约束模型:正则化与阻抗¶
这一章把第 4 章"range 被顶穿"从症状升级为可整定的量。核心事实:MuJoCo 的约束是软的,range/solref/solimp 描述的是"多软",而不是"硬不硬"。
约束为什么"软",computation 章给了三层解释:正则化项 R 是一个对角阵,R > 0 就是在让约束变软;受约束的加速度是"无约束加速度"与"参考加速度"的插值,插值权重叫阻抗 d,它是长度 nc、逐元素满足 0 < d < 1 的向量;每条标量约束的行为近似一个有阻尼的二阶系统——时间常数与阻尼比由 solref 设置,强度(也就是 d)由 solimp 设置。d 还会被引擎夹进 [mjMINIMP, mjMAXIMP]。
常量取值:mjMINIMP = 0.0001、mjMAXIMP = 0.9999、mjMINVAL = 1e-15、mjNGROUP = 6。
(mjMINVAL 就是第 3 章那条质量下限的阈值——运动 body 的质量与惯量必须大于它,取值 1e-15;即引擎只拦截真正的零与负值,不拦截“小到没有意义”的惯量。)
阻抗计算用的约束空间惯量矩阵对角元,是编译期从初始位姿 qpos0 近似出来的;大各向异性惯量、复杂链或远离 qpos0 运行的模型可能因此出现无解释的求解器发散(BADQACC)、过度穿透或收敛差。诊断入口是 fwdinv flag(看正逆动力学差异有多大);怀疑近似不足就开 diagexact 让运行期算精确对角(computation 章)。
3.2 求解器选型与 solref/solimp 默认值¶
Newton 是默认求解器;solref 定时间常数、solimp 定约束强度上限,二者共同决定越界与穿透幅度。
三档求解器的分工(computation 章):Newton 是默认,残差、代价下降、投影梯度三个量任一下降到容差以下就提前终止;PGS 用对偶公式,在 Newton 之前是默认算法;CG 是共轭梯度,没有设置开销。第四个名字 NoSlip 不是求解器,它是一个后处理步骤,靠把 noslip_iterations 设为正值启用——所以调它不会改变"解得多快",只改变"摩擦够不够"。
<option> 的相关默认值:solver = Newton、iterations 100、tolerance 1e-8、ls_iterations 50、ls_tolerance 0.01、noslip_iterations 0、noslip_tolerance 1e-6、cone = pyramidal、jacobian = auto(nv < 60 解析为 dense,nv >= 60 解析为 sparse)、timestep 0.002(XML reference)。
性能方面的几条结论需要记录:典型机器人模型上 PGS 迭代 10 次得到的解在实用意义上已与全局最优无异;Newton 收敛很快,通常 2–3 次迭代;timestep 是用户最该关心的单个参数——太宽会不稳定,太小则浪费 CPU,而较软的约束可以用较大的时间步长;推荐的积分器是 implicitfast。还有一条实时性判据:时间步长必须大于每步的 CPU 时间,用 RK4 时要大于 4 倍(computation 章)。
<option> 的积分器取值是 [Euler, RK4, implicit, implicitfast], 'Euler'——没有 semisplit(这个关键字无法识别;文档对隐式欧拉的称呼是“半隐式欧拉法”)。同理 mpr_iterations 在 3.11 不存在。
solref : real(2) 默认 "0.02 1":第一项是时间常数、第二项是阻尼比。文档给的硬约束是时间常数至少要是 timestep 的两倍,否则约束一步收不完、仿真会不稳;阻尼比一般取 1(临界阻尼),偏小是欠阻尼(约束有弹性、会回弹),偏大是过阻尼(迟钝)。两项都取负则换成"直接"格式,即直接给 (-stiffness, -damping)。solimp : real(5) 默认 "0.9 0.95 0.001 0.5 2",五个数是 d0, dwidth, width, midpoint, power,它们参数化了阻抗曲线 d(r),也就是"约束有多硬"(XML reference)。
同类族还有 solreflimit/solimplimit(限位)、solreffriction/solimpfriction(摩擦)。注意:3.11 没有 sollimit、也没有 solfriction 这两个属性名,旧写法会被 schema 直接拒下,不会静默降级成别的参数。
modeling 章 Tips 的性能调参清单与上表直接衔接的还有:jacobian 在 dense/sparse 两条独立代码路径之间试选,auto 启发式不总选对;碰撞开销占大头时用 contype/conaffinity 减少检测对、把 mesh-mesh 换成 primitive-primitive(mesh 可用 trimesh/Blender/MeshLab/CoACD 减面),SDF 几何的碰撞最贵;椭圆锥比 pyramidal 更准也更贵,不关心精确摩擦可换回锥;自编译可切 32 位浮点(大模型省内存带宽,典型模型收益难测)。加上前文的 timestep 与积分器选择,timestep 仍是单个参数里的第一位。
solref 的两种格式对应软接触模型的两种参数化:正值 (timeconst, dampratio) 经阻抗换算,负值直接给 (−stiffness, −damping)。完全弹性接触只能用后者(「防滑、背隙与恢复系数」的弹跳对照);想要“更硬的接触”优先调时间常数,而不是先动 solimp 初值——下节对照。
从约束表到加速度的求解流程,NoSlip 是最后一层后处理:
flowchart LR
build["构建约束:efc_J / 残差 / 岛"] --> pick{"选求解器"}
pick -->|"Newton(默认)"| nw["残差 / 代价 / 投影梯度<br/>达容差即停,典型 2–3 次"]
pick -->|"PGS"| pg["对偶公式<br/>迭代 10 次已近最优"]
pick -->|"CG"| cg["共轭梯度,无设置开销"]
nw --> acc["qacc"]
pg --> acc
cg --> acc
acc --> noslip["NoSlip 后处理<br/>noslip_iterations > 0 才启用<br/>只改摩擦,不改求解速度"]
4. 约束柔度与越界幅度¶
场景(第 4 章「软约束与 range 越界」的模型):weld 从远处把零件拉到 mount,途中同一个夹爪的 slide 关节上限是 0.046 m。越界量由约束柔度决定,几条结论可以直接从参数语义推出,也与该场景的走势一致:
- 首次越界发生在 weld 的瞬态,不是稳态泄漏;越界幅度是约束柔度的函数,不随求解器
iterations变化——把 20 提到 200 结果不动,所谓“越界幅度是残差的函数”因此不成立。 - 限位变硬有效但收益递减:
solreflimit的时间常数从默认 0.02 缩到 0.005,越界明显下降;再缩到 0.002 收益很小,而且已经越过文档下限“时间常数至少为timestep的两倍”,只能当趋势指示,不能抄成推荐值。 - 阻抗放软(
solimplimit="0 0.1 …")越界增大,与 d 小即约束弱的直觉一致。 - 负值直接刚度格式不保证单调:(−2000,−20) 比 (−20000,−200) 更糟,不能按比例缩放,要单独整定。
- 换
implicitfast不改变结果,瓶颈在约束柔度而非积分器;这与「求解器选型与solref/solimp默认值」的 timestep 建议不冲突。
⇒ range 不是硬限位。 想让它表现得像硬限位,代价是更硬的限位约束 + 更小的 timestep,而且第 4 章的正解(t=0 对齐、零 DOF 嵌套)依然是首选。
接触侧与限位同源:球从高处落到平面,最大穿透由 solref 时间常数主控;solimp 的 d0 从 0.9 加硬到 0.99 几乎不改结果,因为接触深处已接近阻抗上限;直接刚度格式给出与硬时间常数同量级的穿透。
求解器开销依工况而变:稳态接触下 CG 略快于 Newton,PGS 因迭代慢数倍且解略糙;落地瞬态则 Newton 最快。文档“Newton 通常最快、大模型 CG 可能更快”要按负载理解——重负载下 Newton 的迭代优势显现,轻负载下 CG 的无设置开销更划算。
jacobian 的 auto 在 nv=60 与 nv=61 两组都紧贴 sparse、dense 最慢,与源码 jacobian == AUTO && nv >= 60 一致;XML reference 的“up to 60 dense”是文档表述,实际 60 已归 sparse。
cone 与 impratio:纯静摩擦推力场景(推力低于 μmg)长时间漂移可忽略,所谓“缓慢滑移”属于持续滑动接触,不是静摩擦判据的反例。
5. 接触筛选与 margin/gap¶
接触先要过 contype/conaffinity 与分组两道筛选;margin 决定何时被检测到,gap 决定何时开始出力。
兼容性判据是位与运算:(contype1 & conaffinity2) || (contype2 & conaffinity1),即一个的 contype 与另一个的 conaffinity 必须有公共位为 1,两者默认都是 1;分组还要同时落在 order/collide 的启用范围内(computation 章)。margin 与 gap 则是把 geom 表面外扩:距离小于 margin + gap 就检测到接触,小于 margin 才产生接触力。中间那段(margin < 距离 ≤ margin + gap)是"未激活接触"——它已经在 mjData.contact 里占了一格,但 efc_address = -1。
efc_address = -1 是一个有效的观测点:ncon 中包含"已检出但不产生接触力"的接触,将 ncon 作为"是否发生接触"的判据会偏乐观,需过滤 efc_address >= 0。
摩擦的量纲:geom friction : real(3), '1 0.005 0.0001'(滑动 / 扭转 / 滚动),pair friction : real(5), '1 1 0.005 0.0001 0.0001',且“使前两个系数不同会导致各向异性的切向摩擦”。给 geom 的 friction 写 5 个值会被直接拒下——引擎不会帮你截断多余的项。
6. 防滑、背隙与恢复系数¶
modeling 章的 tips 部分把常见建模难题的解法分成三组,这里配运行数据展开。
防滑(Preventing slip)。夹取滑移分三种症状,各有对应旋钮:
- 防滑接触力在摩擦锥外——物理上就防不住。检查顺序:夹持力 × 滑动摩擦系数是否显著大于物重;滑动摩擦系数是否够大;绕法向的旋转是否需要扭转摩擦(
condim=4)或滚动摩擦(condim=6)。曲面或带凸起的接触面靠法向几何防滑;平面接触则确认multiccd与nativeccd两个 flag 未关(默认都开)。 - 高频低幅振动——高增益控制器或 stick-slip 与机构固有模态共振。降
timestep或给相关 joint 加armature;若来自显式阻尼的反馈,换 implicit/implicitfast 积分器。 - 缓慢滑移——软接触模型的设计属性,没有它逆动力学无定义。缓解:
impratio调大(只在椭圆锥下好用),或noslip_iterations设 1–3 启用 NoSlip 后处理(彻底防滑,代价是逆动力学无定义与额外计算)。
背隙(Backlash)。齿轮间隙的建模写法是同位姿双 hinge:J1 带执行器与 armature,J2 带 ±1° 限位,总转角 = J1+J2。两处运行证据:无 armature 时编译照常通过,但第一步即 BADQACC、qpos 全 NaN(关节空间惯量矩阵奇异,引擎只在运行期暴露);带 armature=0.01 后恒力矩 0.3 跑 2 s,J2 停在 −0.001017 rad(贴限位方向的小偏移),力矩反向则 J2 反号(+0.001017)——背隙的“先空程后带动”就由这一小段限位行程呈现。J2 的限位也可换成 equality(J2=0 的软约束,阻抗在 0 附近弱、偏离变强),过渡更软且约束力全程可读。
恢复系数(Restitution)。solref 两项全负时解释为 (−stiffness, −damping)。要完全弹性,给大刚度零阻尼:solref="-1000 0"。同一颗球从 1 m 落地的弹跳峰值:该设置下前六峰 1.0 / 0.9991 / 0.9993 / 1.0 / 0.9989 / 0.9995 m,首末差 0.0005 m——能量在初值附近波动而不漂移(软接触逐步吸收形变,不严格守恒,但宏观峰值不掉);默认 "0.02 1" 是临界阻尼,第一次落地后峰值即停在半径高度 0.1 m 附近,不再弹起。
7. 要点¶
- 环路类型只按
gaintype/biastype/dyntype认,不按名字认;名字前缀(position_/motor_)只是约定,随时会被上游改掉。 - 建表用
dict.setdefault(joint, actuator)+ 过滤纯力矩与底盘steer/wheel,避免"取最后一个"式覆盖。 - 把这张表打印出来:"关节 X → 执行器 Y(gain=…, bias=…, dyn=…)"。交付时它是验收项,出问题时它是第一手证据;只说"我建好映射了"不够。
ctrllimited/ctrlrange与forcerange是两种不同的钳位:前者钳命令、后者钳输出力。做 NaN 防御时不要以为forcerange能兜住非法值——第 5 章已述,forcerange有无,8 组数字完全一样。-
双挂本身往往是有意的(位置环 + 力矩前馈)。此时要显式规定"哪个槽位接受命令",而不是靠遍历顺序;两套执行器的
ctrlrange也应当一个用弧度、一个用 N·m 并在注释里写明量纲。 -
出力公式
scalar_force = gain_term * (act or ctrl) + bias_term;纯力矩 =(fixed, none, none)。 - 按名字建表在双挂模型上会静默选错,误差以 rad vs N·m 计;高 kp 的位置环会把关节按得更死。
ctrl读数正常 ≠ 写对槽位;把映射表打印出来当验收项。- 3.11 没有
crank/follow元素、没有filtershape属性。 mj_jacSite在 3.11 需要 5 个参数(含 site id),jacp/jacr是 (3, nv) 连续数组。- 转轴穿过目标点的关节,位移与雅可比列范数同时精确为 0,这是物理不是 bug。
- 0.5 rad 的位移用弦长算(173.18 mm),
r·θ会给出 175 mm 的系统偏差。 - 验证动作要先证明它能伤得到被测对象;逐关节位移表是交付物的一部分。
- 约束是软的:
solref定时常数是多少,solimp定多强,二者共同决定越界/穿透幅度。 - 限位被"视为无摩擦接触处理";迭代数与积分器不改变瞬态越界,柔度改变。
- 3.11 的属性名:
solreflimit/solimplimit、solreffriction/solimpfriction;sollimit/solfriction会直接报错。 ncon包含未激活接触(efc_address = -1),判断是否接触需过滤。- 常数可查:
mjMINIMP=0.0001、mjMAXIMP=0.9999、mjMINVAL=1e-15。 - 防滑三症状各对一组旋钮:锥外→法向力/摩擦/
condim;高频振动→timestep/armature/隐式积分器;慢滑移→impratio/noslip_iterations。 - 背隙双关节:无
armature编译不拦但第一步BADQACC;J2偏移量级 ≈1 mrad、随力矩反向。 solref全负 = 直接刚度格式;"-1000 0"峰值不漂,"0.02 1"一次落地即不弹。- 阻抗对角是
qpos0的编译期近似;诊断用fwdinv,修正用diagexact。