L03 · 刚体物理与稳定仿真
课程状态: 讲义和可执行 notebook 均已提供。notebook 已在 CPU 和参考 AMD ROCm 平台上通过干净 kernel 验证;由于 CPU 仍是本讲的最低硬件要求,L03 状态为
cpu-verified。
本讲定位
L02 让一个方块经历了声明、构建、步进和观测的完整生命周期。当时所有物理配置都保持 不变,以便把注意力集中在仿真程序的结构上。L03 开始有意识地改变部分参数,并回答一个 更难的问题:
当刚体落到桌面或在桌面上滑动时,哪些现象来自物理模型,哪些差异来自数值离散, 我们又需要什么证据才能谨慎地判断仿真是否稳定?
一张看起来合理的最终画面并不足以回答这个问题。方块最终可能停在桌面附近,但此前 短暂穿入桌面、丢失接触、发生反弹或持续振荡。反过来,接触点数量发生变化也不一定 表示方块真的离开了桌面。因此,本讲把时间序列和定义清楚的指标作为主要证据。
开始前,你应当能够:
- 从干净 kernel 运行课程 notebook,并识别实际选择的 Genesis 后端;
- 按
gs.init → Scene → add_entity → build → step → state阅读代码; - 区分 Morph、Material 和 Surface;
- 读取未批处理刚体的位置和线速度。
本讲不需要机器人模型、逆运动学、数据集或学习策略。实验只使用 Genesis 内置 Box, 可以在 CPU 上完成;经过验证的 AMD GPU 可以加速同一实验,但不是学习本讲的前提。
学习目标
完成 L03 后,你应当能够:
- 将几何、密度、摩擦、
dt、substeps、采样规则和颜色分别归入物理、数值、观测或 视觉信息; - 根据一组 Genesis 配置计算仿真时长、内部时间步长、外层样本数和内部更新次数;
- 设计受控对照,只改变一个因素,同时保持场景、seed、初始状态和仿真时长一致;
- 结合高度、速度、接触、几何间隙、穿透代理值和沉降误差解释方块落体实验;
- 解释 Genesis 1.3.3 如何组合接触双方的摩擦系数,并用位置和速度轨迹比较停止距离;
- 区分实测现象、求解器内部量和普适物理结论,避免超出证据范围;
- 按明确顺序排查未发生接触、状态非有限、异常穿透、未能停止和 CPU/GPU 结果差异。
实验的四个层次
把输入和证据分成四层,可以让受控仿真实验更容易分析。
| 层次 | L03 中的例子 | 它回答的问题 |
|---|---|---|
| 物理 | 几何、rho、摩擦、重力、初始位姿和速度 | 我们让引擎模拟了怎样的物理系统? |
| 数值 | dt、substeps、内部时间步长 | 引擎以多细的时间尺度近似系统演化? |
| 观测 | 外层采样时刻、停止阈值、持续窗口、绘图范围 | 我们如何把离散记录转化成证据? |
| 视觉 | Surface 颜色、图例颜色、示意图样式 | 我们如何区分和呈现对象? |
前三层会影响结论本身或证据力度。视觉层只帮助读者辨认对象,不会改变物理行为。把 方块涂成蓝色并不能让它变得更滑。
rho 属于物理层。对几何固定的 primitive,它会影响质量和惯量;但在本场景中,不能 据此推断不同密度的物体具有不同的重力加速度。两个主实验都会保持密度不变,以免它 干扰我们对接触或摩擦的判断。
本讲统一使用下面这条证据链:
物理配置 + 数值配置
↓
固定 seed 和相同仿真时长
↓
采样状态 + 接触证据 + 几何证据
↓
单变量对照 + 有适用边界的结论如果两个 case 同时改变摩擦、时间步长和初始速度,即使结果不同,也无法判断究竟是哪项 变化造成了差异。
时间离散:三个量,三种职责
Genesis 通过 SimOptions 暴露外层仿真时间步长和内部子步数:
from robo_genesis.course_utils import to_numpy
scene = gs.Scene(
sim_options=gs.options.SimOptions(dt=0.01, substeps=2),
rigid_options=gs.options.RigidOptions(enable_collision=True),
show_viewer=False,
)这组配置对应:
外层 dt = 0.01 s
substeps = 2
内部 dt = dt / substeps = 0.005 s课程使用 substep_dt 表示这个内部时间步长。它是由前两个参数计算得到的描述量, 不是第三个独立输入。
dt:外层边界
每调用一次 scene.step(),仿真时间就前进 dt 秒。如果每次调用后读取一次状态,外层 采样间隔也就是 dt。到了 L04 的关节控制和 L05 的观测,在最直接的单步循环中, Python 更新控制目标或读取传感器的频率也由这条外层边界决定。
执行 n_steps 次外层步进时:
simulated_time = n_steps × dt
external_samples = n_steps # 每次 step 后记录一次时因此,不同时间配置必须在相同仿真时长下比较,不能只让它们执行相同 step 数。例如, dt=0.01 时运行 100 步代表 1 秒,而 dt=0.02 时运行 100 步已经代表 2 秒。
substeps:内部分辨率
在一个外层 step 内,Genesis 1.3.3 会执行配置数量的内部子步。内部时间步长和更新次数为:
substep_dt = dt / substeps
internal_updates = n_steps × substeps在 dt 不变时增加 substeps,会让求解器使用更细的内部时间分辨率,但不会改变 Python 侧的观测与命令更新频率。更多子步也意味着更多内部计算,因此它是一项数值取舍,不能 简单理解为“免费让所有稳定性指标都单调变好”。
内部时间步长相同,不等于实验接口相同
比较下面两组配置:
| Case | dt | substeps | substep_dt | 1.5 s 内的样本数 |
|---|---|---|---|---|
| N1 | 0.01 s | 1 | 0.010 s | 150 |
| N4 | 0.02 s | 2 | 0.010 s | 75 |
它们的内部时间步长相同,在 1.5 秒内执行的内部更新总数也相同,但外层采样频率不同。 N1 在 0.01 秒处的样本没有对应的 N4 样本;计算轨迹差异时,只能对齐 0.02、0.04、 0.06 秒这样的共同采样时刻。
当后续课程开始在外层 step 边界更新动作和读取传感器时,这个区别会更加重要。L03 先 建立时间概念,L04 再把它应用到控制。
接触是一种数值近似
两个刚体网格在真实物理意义上不应占据同一空间,但离散求解器不会连续观测和求解每个 瞬间。它以有限时间步推进、检测接触,并在内部时间分辨率上施加约束。因此,即使仿真 总体可用,也可能测到少量重叠。
这并不意味着可以忽略穿透,而是要求我们准确说明测量方法及其局限。
基于几何的穿透代理值
落体实验使用水平固定桌面和初始姿态与坐标轴对齐的方块。方块理想静止时的中心高度为:
expected_center_z
= table_center_z + table_height / 2 + cube_size / 2notebook 根据每个外层 step 后记录的状态计算:
penetration_proxy
= max(0, expected_center_z - minimum_sampled_center_z)这个值可以比较四组受控 case,但它不是求解器精确的连续时间穿透深度:
- 轨迹只在每个外层
dt后观测一次; - 两个样本之间更深的瞬时事件可能被遗漏;
- 公式假设方块大致保持实验预期的轴对齐姿态;
- 它由中心高度推导,而不是直接读取求解器内部接触数据。
因此,应称它为“穿透代理值”,而不是笼统地称为“穿透深度”。
Contact count 是辅助证据
runner 会读取动态方块与桌面之间的接触:
contacts = cube.get_contacts(with_entity=table)
contact_count = int(contacts["position"].shape[0])contact count 描述的是当前采样时刻所报告接触流形中的接触数量。随着参与接触的角点、 表面或求解器接触点发生变化,这个数量也可能改变。第一次接触之后出现一次零接触, 并不足以证明方块离开了桌面。
因此,实验还会计算方块底部与桌面顶部之间的间隙:
clearance = cube_center_z - cube_size / 2 - table_top_z只有当 contact count 为零,并且 clearance 大于事先声明的小容差时,该样本才计为观测 到的几何分离。即便如此,分离时长仍受到外层 dt 的量化限制。
用多项证据理解稳定性
L03 不用单一阈值定义“稳定仿真”,而是联合阅读下面这些信号:
| 证据 | 它测量什么 | 单独使用时不能证明什么 |
|---|---|---|
有限的 z 和 vz 轨迹 | 记录状态在数值上仍可使用 | 接触行为正确 |
| 穿透代理值 | 最低采样中心相对理想静止高度的偏差 | 精确的连续时间穿透 |
| 接触是否存在 | 外层采样时刻是否报告接触 | 是否真实分离或反弹 |
| 几何分离 | 无接触且底部间隙为正 | 持续时间短于 dt 的事件 |
| 向上速度 | 第一次接触后的向上运动 | 没有间隙证据时的真实分离 |
| 尾段沉降误差 | 声明窗口内平均最终高度的偏差 | 被均值掩盖的振荡不存在 |
| 完整轨迹 | 瞬时穿透、反弹和沉降过程 | 已模拟时长之外的行为 |
可信的结论应说明观察到了哪些证据,以及观测窗口有多长。“最终画面看起来没问题” 不是这样的结论。
Part A:2×2 接触实验
Part A 让同一个方块落到同一张固定桌面,并模拟 1.5 秒。几何、密度、摩擦、重力、 初始位姿、seed 和精度全部保持不变,只改变 dt 与 substeps:
| Case | dt | substeps | substep_dt | 用途 |
|---|---|---|---|---|
| N1 | 0.01 s | 1 | 0.010 s | 较细的外层节奏,基准内部分辨率 |
| N2 | 0.01 s | 2 | 0.005 s | 固定 dt,单独考察增加 substeps |
| N3 | 0.02 s | 1 | 0.020 s | 最粗的内部分辨率 |
| N4 | 0.02 s | 2 | 0.010 s | 单独考察增加 substeps,并与 N1 匹配内部步长 |
运行前先预测:
- 哪些 case 有 150 个外层样本,哪些只有 75 个?
- 固定
dt增加 substeps 时,你预计穿透代理值如何变化? - N1 与 N4 在共同采样时刻的状态是否应当接近?
substep_dt相同,是否代表两条完整数组长度相同?- 如果某个 case 的穿透代理值更小,却出现短暂几何分离,能否只凭一个数字判断它 “更稳定”?
相关 Genesis 操作
可复用 runner 负责场景构建和步进,其核心物理路径保持得很短:
scene = gs.Scene(
sim_options=gs.options.SimOptions(dt=dt, substeps=substeps),
rigid_options=gs.options.RigidOptions(enable_collision=True),
show_viewer=False,
)
table = scene.add_entity(
gs.morphs.Box(size=(0.9, 0.6, 0.05), pos=(0.35, 0.0, 0.70), fixed=True),
material=gs.materials.Rigid(friction=0.8),
)
cube = scene.add_entity(
gs.morphs.Box(size=(0.08, 0.08, 0.08), pos=(0.35, 0.0, 1.0)),
material=gs.materials.Rigid(rho=500.0, friction=0.5),
)
scene.build()
for sample_index in range(n_steps):
scene.step()
z[sample_index] = float(to_numpy(cube.get_pos()).reshape(-1)[2])
vz[sample_index] = float(to_numpy(cube.get_vel()).reshape(-1)[2])
contact_count[sample_index] = int(cube.get_contacts(
with_entity=table
)["position"].shape[0])notebook 会显式展示每个 case 的配置、子进程命令、保存字段、指标计算和绘图逻辑。 场景构建在 Python 包中只保留一份可复用实现,避免中英文 notebook 逐渐变成两套不同的 物理实验。
按正确顺序阅读对照结果
四组 case 全部结束后,按照下面的顺序分析:
- 核对每组实际 backend、
dt、substeps、substep_dt、仿真时长和样本数。 - 先检查数组 shape 和数值有限性,再解释任何图表。
- 比较 N1 与 N2、N3 与 N4;每一对都固定外层
dt,只改变内部分辨率。 - 只在共同采样时刻比较 N1 与 N4。相同
substep_dt用来检查无控制刚体轨迹是否 接近,同时保留两者外层采样不同这一事实。 - 联合阅读高度、速度、接触、间隙和尾段指标。
- 把结论限定在当前场景和锁定引擎版本内,不要把它写成“增加 substeps 一定能解决 所有接触问题”的定律。
摩擦属于接触对
gs.materials.Rigid(friction=...) 为实体的刚体几何指定摩擦系数。接触处的运动取决于 两侧几何,而不是只取决于运动中的那个物体。
在本课程锁定的刚体接触实现中,Genesis 1.3.3 会先应用运行时 friction ratio,再取 两侧滑动摩擦系数中的较大值。当前实验的默认 ratio 为一,因此规则可以写成:
effective_pair_friction = max(table_friction, cube_friction)这是 Genesis 1.3.3 的实现规则,不是所有物理引擎都遵守的普适定律。引擎版本变化后, 必须重新核对。L11 会使用运行时 friction ratio 做域随机化;同一接触对规则解释了为 什么只改变一个接触面,有时不会产生任何效果。
基准参数让这个行为可以直接验证:
| 通道 | 桌面摩擦 | 方块摩擦 | 接触对有效摩擦系数 |
|---|---|---|---|
| 低摩擦方块 | 0.50 | 0.10 | 0.50 |
| 高摩擦方块 | 0.50 | 0.80 | 0.80 |
如果只把桌面摩擦从 0.50 改为 0.30,两个方块保持不变:
| 通道 | 修改后的桌面摩擦 | 方块摩擦 | 接触对有效摩擦系数 |
|---|---|---|---|
| 低摩擦方块 | 0.30 | 0.10 | 0.30 |
| 高摩擦方块 | 0.30 | 0.80 | 0.80 |
因此,低摩擦通道应当发生变化,而高摩擦通道可以作为近似不变的对照。实验检查这个 方向性预测,但不要求所有支持平台都得到某个硬编码停止距离。
Surface 颜色不参与上述计算。橙色和蓝色只用于在图中区分两条通道;交换颜色不应交换 物理结果。
Part B:摩擦与持续停止距离
Part B 使用一张桌面和两个相同方块。几何、密度、位姿、dt=0.01、substeps=2、 初始水平速度和测量时长全部固定。基准实验中只有两个方块自身的摩擦系数不同。
测量开始前先建立稳定接触
方块一开始位于几何上的静止高度,但求解器仍需要建立接触流形。runner 会先推进一小段 沉降时间,再把此后的时刻定义为测量零点,然后为两个自由刚体设置相同水平速度:
for _ in range(settle_steps):
scene.step()
initial_velocity = np.array([2.0, 0.0, 0.0, 0.0, 0.0, 0.0])
cube_low.set_dofs_velocity(initial_velocity)
cube_high.set_dofs_velocity(initial_velocity)如果省略沉降阶段,初始接触瞬态就会混入滑动对照。
查看结果前先定义“停止”
一个速度样本接近零,可能只是过零、振荡或数值波动。实验必须在运行前声明速度阈值和 持续窗口。只有从某个样本开始,整个持续窗口都满足
abs(vx) < stop_speed才能把这个样本判为停止起点。如果测量窗口内始终没有满足条件,停止时间和停止距离应 保持为不可用,而不是用最后一个样本编造结果。
如果在索引 i 检测到持续停止:
stop_time = time[i]
stop_distance = x[i] - x[0]runner 还会记录角速度。摩擦作用在接触位置,自由方块可能把一部分平移运动转化为转动。 因此,停止时间和停止距离不一定按照直觉给两个 case 排出相同顺序。应当联合阅读完整的 x(t)、vx(t) 和角速度轨迹,并以本实验要求的停止距离作为主要比较量。
不要把某个教科书式质点停止公式反过来套用到这条轨迹,以此估计 Genesis 的摩擦系数。 这里模拟的是带转动动力学的自由刚体,接触由离散求解器处理。
只改变一个因素的桌面实验
完成基准实验后,动手对照只把桌面摩擦从 0.50 改为 0.30。运行前先预测:
- 哪条通道的接触对有效摩擦系数会变化;
- 哪个方块应当滑得更远;
- 哪条通道应当近似不变;
- 为什么只改变颜色不会影响任何结果。
只有在方块属性、数值设置、沉降时长、初始速度、seed 和测量时长全部不变时,这个对照 才有效。
配套 notebook 流程
配套 notebook 发布后,将按以下顺序执行:
- 报告版本、请求的 backend mode、渲染模式、子进程实际后端和输出目录;
- 展示 N1–N4 配置并记录运行前预测;
- 在隔离进程中分别运行四组接触 case;
- 校验并读取结构化
.npz结果; - 启用渲染时展示真实的 Genesis 初始/最终相机画面,否则展示明确标注的状态示意图, 并同时展示指标表以及对齐的高度、速度和接触曲线;
- 运行双通道摩擦基准实验;
- 比较真实场景画面、位移、线速度、角速度和持续停止结果;
- 只把桌面摩擦改为 0.30,再次比较数值结果和最终场景画面;
- 输出
L03 CHECK: PASSED或具体失败条件。
Genesis 初始化具有进程级状态。使用独立进程,可以让多个 dt/substeps 配置从同一本 notebook 运行,而不要求你在每个 case 之间重启 kernel。这只是执行细节,不是本讲主题; 学习重点是传入每个进程的配置,以及进程返回的证据。
仿真后端与渲染能力是两个独立选择。核心路径仍只使用状态数据,并可在 ROBO_GENESIS_RENDER=0 时运行;该分支会打印明确的渲染 SKIP,并根据实测位置绘制 初始/最终示意图。设置 ROBO_GENESIS_RENDER=1 后,每个隔离进程都会在 build() 前 加入相机并返回经过校验的初始/最终 RGB 画面;相机或渲染一旦失败就会停止运行,不会 静默退回示意图。两条路径都保留轨迹图作为定量证据,相机画面则用于展示 Genesis 场景 实际呈现的状态。
面向学习者的默认模式会在可用时选择已经验证的 AMD 后端,否则使用 CPU;显式 CPU 模式构成本讲的 cpu-ok 路径。如果已经选择 AMD 而运行失败,错误必须保持可见;静默 改用 CPU 会让后端验证结论失效。
如何判断实验成功
最终结果应当证明下面这些条件,但不依赖某一组完全固定的浮点数:
- 每个子进程都报告请求模式和实际后端,并成功退出;
- 每个子进程都报告请求的渲染模式;启用时返回非空且数值有限的 RGB 画面,关闭时返回 明确的空画面字段并打印渲染
SKIP; - 所有必需数组长度正确且数值有限;
- 每个接触 case 都覆盖 1.5 秒,所以样本数会随
dt改变; - 计算得到的
substep_dt和内部更新次数与 case 表一致; - 每个 case 中方块都接触桌面,并且没有穿过桌面;
- 在
dt固定时,更细的内部时间步长会降低当前锁定场景的穿透代理值; - N1 与 N4 在共同采样时刻的状态差异位于预先确定的容差内,同时外层样本数仍然不同;
- 解释联合使用轨迹、接触、间隙和沉降证据,而不是把某一个指标直接称为“稳定性”;
- 摩擦基准实验中,接触对有效摩擦系数较低的通道在持续停止前移动得更远;
- 降低桌面摩擦会按预测方向改变低摩擦通道,同时让高摩擦通道近似不变;
- 生成的
.npz和图像只留在配置的输出目录中,不进入版本控制。
CPU 与 AMD 结果可能存在细小差异。容差应描述事先认可的数值范围,不能在看到失败后 临时放宽,只为了让检查通过。调查后端差异前,应当保留原始配置和证据。
常见失败与诊断
不同 case 覆盖了不同仿真时长
检查 n_steps × dt,不要只检查 n_steps。根据同一个目标时长重新计算步数,并要求 乘积与目标时长一致。
一次修改了太多输入
打印完整 case 配置并逐字段比较。N1→N2 或 N3→N4 只能改变 substeps;桌面摩擦练习 只能改变桌面摩擦。
substep_dt 相同,数组长度却不同
这正是 N1 与 N4 的预期结果。它们的内部时间步长相同,但外层 dt 和样本数不同。 计算轨迹差异前,应先对齐共同时间戳。
没有报告接触
先检查桌面顶部、方块底部、初始高度、重力、总时长和 enable_collision=True,再查看 高度轨迹,确认方块是否真的到达桌面。不要一开始就放宽容差。
Contact count 短暂变为零
检查同一采样时刻的底部间隙和垂直速度。只有接触条件与几何条件同时支持时,才能把它 计为观测到的分离。
状态变为非有限值,或方块穿过桌面
找到第一个异常样本,记录对应 case 和 backend,再把它的 substep_dt 与成功 case 比较。保留子进程的 stdout 和 stderr,不要捕获失败后替换成一个成功 case。
方块始终没有满足停止条件
检查测量时长、速度阈值、持续窗口、接触状态和完整速度轨迹。应当报告“在测量窗口内 没有停止”,不能把最终位移伪装成停止距离。
改变一个摩擦值却没有效果
按 Genesis 1.3.3 的 max 规则计算两个接触对的有效摩擦系数。接触另一侧原本就可能 占主导。这就是实验把桌面摩擦从 0.50 降到 0.30 的原因:不能假设修改任意一侧都会 改变运动。
请求 AMD 运行却报告 CPU
检查 PyTorch 的 HIP 构建信息、设备可见性、请求模式和子进程打印的实际后端。不能把 CPU 输出当作 AMD 运行证据。还要记住,ROCm 设备通过 PyTorch 的 cuda 命名空间暴露, 只看命名空间无法判断硬件是不是 NVIDIA。
CPU 与 AMD 结果存在细小差异
确认 seed、精度、引擎版本、case 配置和时长一致。先比较方向性关系和预先确定的容差, 再比较打印出的十进制字符串。如果定性关系发生变化,应同时保留两套结果并调查原因, 而不是隐藏其中一套。
检查点与练习
概念检查
先独立回答,再回看前面的表格:
- 当
dt=0.02、substeps=2 时,substep_dt是多少?1.5 秒内有多少个外层样本和 内部更新? - 为什么不能让每组时间配置都运行相同数量的外层 step?
- 为什么 N1 与 N4 只能在共同采样时刻比较?
contact_count == 0能否单独证明方块发生反弹?- 桌面摩擦为 0.50、方块摩擦为 0.10 时,Genesis 1.3.3 在本实验中采用的接触对 有效摩擦系数是多少?
- 为什么把桌面降到 0.30 会影响低摩擦通道,却不会影响系数为 0.80 的高摩擦方块?
- 为什么交换橙色和蓝色 Surface 不会改变轨迹?
- 为什么一个低于阈值的速度样本不能证明已经持续停止?
动手练习
先运行摩擦基准实验,再只把桌面摩擦从 0.50 改为 0.30。第二次运行前,写下预期的 接触对有效摩擦系数和每条通道停止距离的变化方向。运行后:
- 从打印配置确认没有其他输入发生变化;
- 比较完整
vx(t)曲线,而不是只看最终值; - 报告每条通道的持续停止距离;如果测量窗口内没有停止,应明确说明;
- 解释每项预测是否得到证据支持;
- 说明结论适用的引擎版本和参数范围。
作为扩展,可以增加 dt=0.01、substeps=4 的第五组接触 case。保持 1.5 秒时长和所有 物理参数不变,先预测哪些指标可能改善、哪些没有保证,以及配置需要增加多少内部计算。 把结果视为新增的一个数据点,而不是普适的收敛证明。
小结与后续衔接
- 物理参数描述被模拟的系统,数值参数定义时间离散,观测规则定义证据,视觉参数只 影响呈现。
dt是外层仿真、命令和采样边界;substeps 细分边界内部的求解过程,substep_dt = dt / substeps。- 公平的数值对照使用相同仿真时长,并在共同采样时刻对齐轨迹。
- 接触稳定性需要多项信号。穿透代理值、contact count、间隙、速度、沉降误差和完整 轨迹都有各自局限。
- Genesis 1.3.3 在应用运行时 ratio 后,使用刚体接触对两侧较大的滑动摩擦系数;这是 特定版本行为。
- 持续停止需要事先声明速度阈值和持续窗口。自由方块可能转动,因此应联合阅读位置、 线速度和角速度。
- 结论应说明支持它的场景、引擎版本、后端、配置和观测窗口。
L04 会把外层时间步长连接到关节命令周期,并比较目标状态与机器人真实运动。L07 会把 接触对推理应用到桌面、物体和夹爪。L11 将通过运行时域随机化再次讨论摩擦,并说明为 什么必须考虑所有相关接触面。
资料来源
- Genesis World 文档 — 官方用户与 API 文档。
- PyPI 上的 Genesis World 1.3.3 — 本课程锁定的准确引擎版本。
- Genesis 1.3.3
SimOptions源码 — 本讲时间合同所使用的dt和 substeps 定义。 - Genesis 1.3.3 刚体实体摩擦 API — 接触双方取较大值这一规则的公开说明。
- Genesis 1.3.3 刚体接触实现 — 应用 friction ratio 并形成接触对摩擦值的版本固定实现。