E1 · 状态、Context 与时间#
先读建模与坐标。本页继续使用 Drake 1.57.0 固定源码;所有例子未执行,仅做源码/语法核对。目标是回答“这份数值属于哪个系统、哪个 Context、哪个时刻、哪个采样阶段”。
1. Context 是状态容器,也是依赖图的一部分#
一个 Diagram 的 Context 树与它的子系统树一一对应。根 Context 包含整个图的子 Context;Plant API 通常要求 Plant 自己的 Context,不能把根 Context 或另一棵不兼容系统的 Context 直接传进去。一一对应关系
# 阅读片段;没有在本阶段执行。diagram 和 plant 来自同一次构建。
def locate_plant_context(diagram, plant):
root_context = diagram.CreateDefaultContext()
plant_context = plant.GetMyMutableContextFromRoot(root_context)
return root_context, plant_context
同一 Context 树的时间必须一致,SetTime 只能在根 Context 上调用。时间 setter 合同
root_context 应一直存活;plant_context 是其中的子 Context。不要对图中的 Plant 单独 CreateDefaultContext(),再误以为它会保留原 Diagram 的 SceneGraph/控制器连接:新独立 Context 不具备原根 Context 中的连接求值环境。只做独立 Plant 运算时可按其输入要求创建单独 Context;两种用途应显式区分。子 Context 访问
Context 不只有一个浮点状态数组。它保存时间、连续/离散/abstract state、参数、accuracy、fixed input values 和缓存依赖。离散 Plant 默认还可能有动力学输出的抽样状态,因此 GetPositionsAndVelocities 返回的 [q;v] 并不是整个 Diagram 的完整状态。控制器积分器、滤波器和数据保持器也各有自己的 Context。Context 值、抽样状态
Simulator 的常用 C++ 构造入口持有 System 引用、拥有兼容 Context;System 生命周期应长于 Simulator。Python 绑定有自己的生命周期管理,仍建议保留 diagram、plant、simulator 的明确变量,不把 System、Context、Simulator 当成一个“环境对象”。Simulator 所有权
2. q、v、q̇ 和索引映射#
对光滑连续多体系统,配置和速度之间是
这里 q∈R^{nq} 是配置参数,实际受配置约束;v∈R^{nv} 是广义速度,广义力 τ 与它配对,功率为 τᵀv。N(q) 是 nq×nv 的运动学映射;M(q) 是整个模型的广义质量矩阵,不是单刚体的 6×6 空间惯量。该表达只是连续动力学结构,不能当成离散接触更新算法;完整力项、约束和求解过程在 E3。原生方程
关节/模型假设 |
nq |
nv |
位置与速度的含义 |
|---|---|---|---|
weld |
0 |
0 |
固定相对位姿,不分配运动自由度 |
单转动关节 |
1 |
1 |
角度 rad、角速度 rad/s |
单移动关节 |
1 |
1 |
位移 m、速度 m/s |
quaternion floating joint |
7 |
6 |
q 为 wxyz + 平移;v 为角速度 + 线速度 |
浮动基座 + k 个独立单自由度关节 |
7+k |
6+k |
此计数假设无额外坐标、未改变基座参数化/拓扑 |
Quaternion floating joint 的 v 是 parent joint frame F 中测量、也在 F 中表达的 [ω_FM_F; v_FMo_F]。默认浮动基座的 F 对应 world,此时两部分都在 world 中表达;这不表示任意自由关节的 parent 都是 world。旋转的四元数有 4 项,但角速度只有 3 项,因此通常不能用 (q_next-q)/h 直接代替 v。请用 MapVelocityToQDot / MapQDotToVelocity 并尊重有效配置和维度。浮动关节布局与 frame、速度表达系、速度映射
2.1 关节编号不是状态切片起点#
先从 GetJointByName(name, model_instance) 找到关节,再在 Finalize 后查询 position_start()/num_positions() 与 velocity_start()/num_velocities()。两种 start 分别位于全 Plant 的 q和全 Plant 的 v,不是 [q;v] 中同一个偏移,也不是按实例抽取数组中的偏移。JointIndex 是身份索引,不能替代它们。关节的状态偏移
# 只演示地址映射;返回副本以便保存。尚未执行。
def read_joint_state(plant, plant_context, joint):
q = plant.GetPositions(plant_context)
v = plant.GetVelocities(plant_context)
iq, iv = joint.position_start(), joint.velocity_start()
qj = q[iq:iq + joint.num_positions()].copy()
vj = v[iv:iv + joint.num_velocities()].copy()
return qj, vj
若只关注某个机器人,用 GetPositions(context, model_instance)、GetVelocities(...) 获取该实例的紧凑数组,或用 GetPositionsFromArray 等进行原生映射;不要假定一组模型实例在全局数组中总是你猜想的连续切片。多实例共享 Plant 不等于批量隔离的多个世界。实例访问
2.2 Python getter 的别名不等于快照#
固定绑定源码特意区分:GetPositions(context) / GetVelocities(context) 对 POD 标量路径返回现有数据引用;带 model_instance 的重载返回拷贝。AutoDiff/symbolic 等标量的转换策略不能由 double 路径推断。作为日志或前后对照数据,统一显式 .copy(),避免后来修改 Context 时“旧读数”跟着改变。绑定中的引用与拷贝
修改状态优先使用 SetPositions、SetVelocities、SetPositionsAndVelocities 或关节自己的 setter。对借出的可变存储长期持有引用、绕过 setter 再写入,会使缓存依赖的失效通知难以保证。修改也不代表施加了力:把 q 从 A 设到 B 是初值/状态编辑,不是机器人走过该轨迹。Context 变更与缓存通知
2.3 不能把 SetFreeBodyPose 一律叫作设置世界位姿#
1.57.0 的 SetFreeBodyPose(context, body, X_JpJc) 对任意 free body 设置 child joint frame 相对 parent joint frame 的位姿。只有浮动基座的相应 frame 就是 world/body 时,它才是 X_WB。child frame 还可能偏离 body frame;仅改变量名字不会完成变换。同样,SetFreeBodySpatialVelocity 的输入也有对应 frame/点语义。公开 API 合同
若确实要设置浮动基座的世界位姿,用名称更明确的 SetFloatingBaseBodyPoseInWorldFrame,并先核实 HasUniqueFloatingBaseBody 等条件。对一般关节,按 parent/child frame 关系换算相对位姿。内部实现仍有名为 X_WB 的局部参数,但会把它的旋转/平移直接传给该 body 的 mobilizer;不能仅凭内部变量名覆盖公开 API 的合同。世界位姿入口、实现追踪
3. 重置与快照:先列出要恢复的东西#
操作 |
实际范围 |
不应推断的效果 |
|---|---|---|
|
当前 Plant Context 的 q/v |
不重置控制器、抽样输出、时钟或全部参数 |
|
改变系统之后分配/重置状态所用的默认 q |
不立即修改既有 Context |
|
按该系统默认规则设置 State |
不等于重置 time、parameters、fixed inputs |
|
本版本实现先重置 parameters,再重置 State |
不重置时间、accuracy 或 fixed inputs |
|
为该系统分配并初始化新的 Context |
单独 Plant 的新 Context 不含原 Diagram 环境 |
根 |
克隆整棵 Context 及其中按存储类型定义复制的值 |
不克隆 System、Simulator 或外部进程 |
|
只复制 State |
不包含时间、参数、固定输入和全套缓存/依赖 |
|
复制状态/参数,可用于子 Context;通知依赖变化 |
不复制时间/accuracy/fixed inputs |
根 |
另加时间/accuracy,要求根 Context |
本版本仍不复制 fixed inputs |
直接查看 System::SetDefaultContext 实现,避免按函数名想象“全部复位”。默认 q 的后续作用见 SetDefaultPositions;复制 API 的排除项见 Context 复制合同和 Clone 合同。
对课程中的纯内存 Diagram,最清晰的独立初值副本是根 snapshot = root_context.Clone(),然后从 snapshot 重新取 Plant 子 Context。不要对已有子 Context 调用 Clone 来幻想得到整个图;该方法要求根 Context。若把快照值复制回现有根 Context,另行恢复 fixed inputs,检查初始化事件,再处理 Simulator 的运行状态。
“Context 深拷贝”也不是操作系统级 checkpoint:自定义 abstract value 按其类型的复制/Clone 语义复制;自定义类型内部的共享外部资源不会自动变成独立设备、网络连接或文件。Drake 的 Value<T> 要求 T 可复制或可克隆,但不替作者设计 T 的资源独立性。外部 Python 控制器变量、随机数发生器、日志游标、渲染器/硬件状态须另行纳入恢复协议。Value 复制规则、存储策略
Simulator 自身的积分器配置、统计与数值过程不是 Context.Clone 的对象。未来恢复运行时,应明确建立新 Simulator 还是复用旧对象,并记录配置;不能仅因 q/v 一样就承诺逐位相同的后续轨迹。改了时间后必须重新 Initialize(),这样才会重建时间事件安排;Initialize 默认还会触发初始化更新和发布,可能改变初始状态或产生外部输出。它不是无副作用的“清缓存”。需要抑制初始化事件时,调用者承担初始状态正确性的责任。Initialize 行为
4. 缓存:何时重算,何时根本不属于缓存问题#
Drake 通过依赖关系判断 Eval 的结果是否仍有效。改变 q 可能使位置运动学以及依赖它的量过期;安全 setter 发出通知,后续 Eval 按需更新。无需把“每次读取前手工刷新全部缓存”作为常规用法,也不能把曾经返回的借用引用当成永远固定的记录。失效传播机制
但是离散 Plant 默认的 dynamics 输出保存的是抽样 State,它不是“该失效但未失效的普通缓存”。设置 q 后 body pose 会改变,而 contact-results output 仍可能反映上次离散更新。盲目关闭 cache 无法把这样的语义改成实时输出。应先确认端口是否 sampled,再确认安全写入、输入来源和读取的 Context;最后才诊断缓存依赖是否有问题。下一节展开这个区别。
5. 四种时间尺度不要合并成 dt#
量 |
控制什么 |
不控制什么 |
|---|---|---|
|
h=0 选连续模型;h>0 选周期更新的离散模型 |
不是渲染帧率或 wall-clock sleep |
Simulator 积分器的最大步长/accuracy |
连续状态的数值积分限制与误差控制 |
不改离散 Plant 的 h 或 SAP 配置 |
|
推进到不超过给定的绝对仿真时刻,可能因事件提前结束 |
参数不是“再走这么多秒”或“只走一步” |
控制/观测/发布周期 |
由相应系统事件、输入信号或外部调用协议决定 |
不会因改了 viewer 刷新率自动同步 |
本版本 C++ 和 Python 的 MultibodyPlant 构造器都要求显式 time_step;MultibodyPlantConfig.time_step 才有默认 0.001 s。不要把旧示例中的缺省行为写成当前构造规则。例子始终显式给出 time_step=0.001,它只是教学配置,不代表推荐值或 DexLab 实测配置。C++ 构造器、Python 绑定、Config 默认
对连续 Plant,Simulator 的数值积分器计算连续状态变化;对离散 Plant,Plant 的事件更新自身状态,Simulator 调度这些事件。如果 Diagram 还含连续控制器,即使 Plant 离散,整个图仍是混合系统,积分器仍会处理那部分连续状态。因此“换 Simulator 积分器”不等于“换掉离散接触算法”。模型与积分关系、混合系统推进
积分器 target accuracy 服务于其支持的连续局部误差估计,不是最终位置误差的统一上界,也不是接触求解器残差阈值。set_fixed_step_mode(True) 也可能因事件边界缩短一步;模型离散步长、积分步长、内部 solver 迭代次数是三种不同概念。减小 h 不自动证明稳定、接触准确或参数合理;完整数值与接触假设在 E3 按算法展开。accuracy 条件、fixed-step 与事件
一个具体能力边界:本版本连续 Plant 的仿真忽略关节限位并给出警告,不能因 URDF 写了 lower/upper 就声称连续仿真会执行限位。控制/优化使用的限位与动力学执行规则还需分别核对。连续模式限位警告
6. 采样时刻:当前 pose 和输出 force 可能不是同一阶段#
离散 Plant 默认保存上次更新时计算的动力学样本 s。按源码的抽象记号:
这表明更新后的 Context 可同时含新的运动状态和用于该更新的旧阶段动力学样本。抽样状态初始没有有效样本;别把初始化前读取的动力学端口当成已经计算好的接触观测。端口合同的细节与后续力解释在 E3 继续核实。离散状态与输出方程
读取方式 |
默认离散 Plant 的语义 |
|---|---|
|
live:当前 Context 中的 q/v |
|
sampled:最近一步记录的动力学样本 |
Plant 直接 |
按当前时间、状态、输入计算;不使用上述输出采样状态 |
连续 Plant 的 dynamics 输出 |
live,随当前状态/输入改变 |
可在 Finalize 前对离散 Plant SetUseSampledOutputPorts(False),使 dynamics 输出也按当前输入/状态重算,代价是可能重复计算、改变 direct-feedthrough 关系并移除抽样状态。另一个入口 ExecuteForcedEvents() 会强制动力学更新,连位置也可能改变;它不是只读刷新函数。本阶段没有执行这些操作。采样选项与 API 区别、Finalize 前置条件
还要区分事件的左右极限。同一个标量时刻 t 可以有更新前 x⁻(t) 和更新后 x⁺(t)。Simulator 的一步先处理起点更新,再积分到下个事件/目标边界并发布;在 AdvanceTo(t) 的终点可能还有该时刻待处理的离散/abstract update。AdvancePendingEvents() 在时间不前进的情况下处理这些 pending updates。它会改变状态,并可能触发发布/monitor;不要作为每次读取后的习惯调用。事件顺序、边界时刻与 pending events
未来做日志时,每项记录至少标明:仿真时间、模型/配置版本、状态所在 Context、更新前/后阶段、端口 sampled/live、坐标/参考点/单位。一个共享 timestamp 不能单独证明 pose 与 force 同步。本阶段只定义语义,实验与跨引擎对照后续引用 DexLab 的原协议。
7. 阅读练习与源码追踪#
一个默认四元数浮动基座带 6 个单转动关节,nq/nv 各是多少?在
[q;v]中,关节的velocity_start()应怎样使用?old_q = plant.GetPositions(context)后修改 q,为什么 old_q 不一定保存旧值?如何写成明确快照?对 t=3 s 的 Context 调用
SetDefaultContext,是否会回到 t=0?fixed input 是否复位?从实现逐项回答。克隆根 Context 后,Python 函数闭包里的随机数发生器是否自然成为独立副本?积分器参数呢?
离散 Plant 的 q 刚被 setter 改过,body pose 改了、contact-results output 没改。能否据此断定缓存出错?
AdvanceTo(0.1)之后AdvanceTo(0.1)是否必然什么都不发生?说明 pending events 的影响。h=1 ms 的离散 Plant 连接连续控制器;将 Simulator 积分器最大步长改为 0.2 ms,是否改变了 Plant 的接触更新周期?
为一般 free body 调用
SetFreeBodyPose(..., X_WB),为什么即使维度匹配也可能错误?指出应查的两个 joint frame。
答案与推理
nq=13、nv=12。velocity_start 是 v 的偏移;在
[q;v]中应加 nq。更直接地先用 GetVelocities 拿 v,再按 velocity_start 切片;勿把相同偏移用于按实例压缩过的 v。固定绑定的 double 全 Plant getter 是引用路径。用
old_q = plant.GetPositions(context).copy();此副本仍只是 q,不是完整 Context 快照。不会。该版本实现重置参数与 State,没有 SetTime,也没有重建 fixed inputs。需要显式的时间和输入恢复策略;若将修改后的 Context 用于已有 Simulator,改时间之后必须 Initialize。
不会。闭包的外部资源和 Simulator 都不在 Context 树里;自定义 abstract value 的资源独立性取决于它的复制合同。Context 克隆不提供完整进程回放保证。
不能。默认 dynamics 输出读取抽样 State,运动学输出读取当前 q/v。先核实 sampled/live,再检查 Context 与输入;清 cache 不能改变抽样语义。
不必然。相同时间可处理上一次终点留待处理的更新事件,也可能发布和调用 monitor;时间不变不代表状态或外部输出不变。
不改变。Plant 仍按 h 周期离散更新;更小积分步长用于连续部分和事件调度,不是替换 Plant 的离散算法。
公开合同是
X_JpJc;Jp 不一定等于 world,Jc 不一定等于 body。需要从建模 frame 关系换算,或在确为 floating base 时使用显式世界位姿入口。
建议的源码追踪顺序:Parser → Finalize → joint.position_start/velocity_start → Plant 状态 setter → Context 失效通知 → Simulator::Initialize/AdvanceTo → Plant 的 sampled output 说明。此链解释建模/状态/时间;完整碰撞、约束装配、SAP 求解和力回写见已交付的E3。返回课程目录或验证记录。