- 物理引擎
- 具身智能
- 机器人
- 人工智能
【免费下载链接】genesis-world
Simulation platform for general-purpose robotics & embodied AI learning.
导读:本文以仓库根目录的 CLAUDE.md(Genesis Development Guidelines)为绝对主体,系统讲解 Genesis 仿真平台面向协作者与 AI 编码助手的全套开发准则,覆盖基础编码纪律、Quadrants 内核编写、批量数据访问、公开 API 设计、代码风格与测试方法论。文章在完整继承规范全部要点的基础上,结合genesis/源码实现、tests/测试套件与pyproject.toml配置逐一佐证,帮助读者理解每条规范的底层动机,并能在实际开发、测试与代码评审中直接落地。
规范文档的定位与整体结构
CLAUDE.md是 Genesis 项目面向 AI 编码助手(如 Claude)和贡献者的权威开发准则,其姊妹文档 CODING_GUIDELINES.md 是面向人类贡献者的补充约定,二者共同维护仓库的代码质量基线。全文按主题分为 11 个板块:
| 板块 | 核心关注点 |
|---|---|
| Miscellaneous | 基础编码纪律:类型、命名、异常、遗留代码清理 |
| Priorities | 冲突时的取舍优先级 |
| Data Access | Quadrants 与 torch/numpy 之间的数据转换与零拷贝 |
| Kernels | @qd.kernel/qd.func的可微分写法与参数约定 |
| API Design | 公开 API 的自洽性与易用性约束 |
| Style | 文档字符串、注释、命名与排版 |
| Testing Guidelines | 测试组织、物理断言、步数与容差预算 |
| Git Worktree 测试 | 可编辑安装模式下"测错引擎"的陷阱 |
| 集群测试 | Slurm 包装器gs-srun下的测试实践 |
| Apple 软件渲染器 | 在本地复现 macOS CI 渲染失败 |
| Tooling & Contributing | ruff、pre-commit、PR 标题规范 |
从源码结构看,这些规范并非孤立的写作要求,而是直接对应仓库的真实架构:Quadrants 作为底层计算内核,提供可自动微分(autodiff)的张量/字段抽象与@qd.kernel装饰器(见 array_class.py);Genesis 在其上封装了面向用户的求解器、实体与场景 API。规范的核心矛盾因此始终是:内核层追求极致性能与可微分性,用户层追求清晰、稳定、可预测的公开 API。
基础编码纪律(Miscellaneous)
强类型数据:用 dataclass / NamedTuple 取代 dict
规范明令禁止用普通 dict 打包属性,要求使用强类型命名数据结构(dataclasses 或简单 NamedTuple)。唯一的例外是刚体资产解析器产生的中间信息:因为"格式声称的内容在解析完成前是不完整、无结构的",所以允许以普通 dict 形式在解析器与消费它的解析逻辑之间传递,但任何内置对象不得持有它。
这一约束在源码中有清晰体现:genesis/engine/solvers/rigid/rigid_solver.py中所有求解器状态字段都是声明式的 Quadrants 字段定义,例如qpos0=V(dtype=gs.qd_float, shape=(solver.n_qs_, _B))、n_awake_dofs=V(dtype=gs.qd_int, shape=(_B,))(见 array_class.py)。Quadrants 数据类的 dtype 定义集中在 array_class.py,而 genesis/init.py 统一导出了qd_float、qd_int等类型。
禁止 getattr / hasattr,用 None 初始化与 isinstance 判断
动态属性探测会破坏类型可推导性,规范要求一律改用None初始化配合isinstance(...)判断,且判断应直接写在if语句中,不引入临时变量。
领域对象命名的正确性语义
领域对象只有三个名词:entity、link、geom,禁止自创 "body"、"object"、"piece" 等新词。这不仅是风格问题,更是正确性提示:刚体单位是 link,因此必须按entity.links→link.geoms的层级迭代几何体,而不是按 entity 聚合。规范同时要求把has_any_rigid_coupling这类"属性型"实例方法转换为 property(见 rigid_solver.py 的使用方式)。
类型转换的"零容忍"原则
float()/int()/.astype()/np.asarray()/dtype=一律禁止,除非严格必要——即外部接口强制要求 dtype(如 igl 的 float64/int64、内核参数的原生 Python 类型),且不得重复转换已被后续步骤转换过的值。numpy 标量天然支持索引、比较与算术运算。torch 张量转 numpy 必须走 misc.py 的tensor_to_array(内部先tensor_to_cpu处理 GPU→CPU 迁移),禁止手写detach().cpu().numpy();需要指定 dtype 时通过tensor_to_array(x, dtype=...)参数传入,而不是链式.astype。
异常与 assert 的分界
"能由用户输入触发"的情况抛异常,"代码内部流程不变量、无公开 API 暴露"的情况用assert。破坏物理正确性的关键错误必须抛异常(必要时 re-raise 澄清错误信息),绝不允许用 warning 掩盖。NaN 必须通过现有的 errno 机制中止仿真(可定义新错误码),同样不接受 warning。
清理与去重
规范要求删除全部遗留代码(不保留 deprecation / legacy 兼容层)、删除未使用的辅助函数(如has_coupling_type)、不允许代码重复、禁止注释掉的 print(一律改为 logging debug trace)、禁止"按最大尺寸预分配"(必须精确分配所需内存)。
优先级:质量优先于一致性
当从 PR、外部项目或旧实现移植代码时,代码质量优先于源码一致性:Genesis 的约定永远优先于原代码风格,原逻辑只作为参考而非实现风格。在构建/初始化路径上,可维护性优先(只要性能合理即可),坚持使用公开 API;在运行时热路径上,效率优先,最小化 GPU-CPU 传输、使用批量操作。
数据访问规范(Data Access)
这是规范中技术含量最高、与底层运行机制绑定最紧的部分,核心对象是 Quadrants 字段。
统一转换入口:qd_to_numpy / qd_to_torch
禁止直接调用 Quadrants 数据实例的.to_numpy()/.to_torch()方法,必须使用genesis.utils.misc提供的qd_to_numpy/qd_to_torch。源码实现(见 misc.py)显示:
transpose=True时应始终传入,把批维移到最前([B, n, dim]),与公开 getter 约定对齐;copy=False仅在 CPU 后端且启用零拷贝时受支持,GPU 数据转 numpy 必然需要拷贝;- 转换应提升到循环之前,一次调用后索引本地数组,禁止在紧循环内反复调用
qd_to_numpy;需要子集时把切片作为qd_to_numpy的输入参数,而不是先转换再切片; - 不要给
qd_to_torch/qd_to_numpy传copy=参数(当返回值是全新算术结果、无内部别名外泄时),让方法自行决定零拷贝或拷贝;只有"裸字段视图可能逃逸并被修改"时才用copy=True。
零拷贝写入路径的规则
- 需要被写入的
qd_to_torch视图必须先绑定到具名局部变量(如flags_t = qd_to_torch(..., copy=False)后再flags_t[mask] = ...),禁止通过匿名链修改;只读场景则相反,可以放心内联链式操作(如qd_to_torch(f).T.reshape(...).gather(...))。 - 零拷贝访问器应原样透传选择(selection);写入属于拷贝语义的 setter(每环境一个向量/标志/一行)在
gs.use_zerocopy成立时走视图路径。视图路径上禁止使用Scene._sanitize_envs_idx(见 rigid_solver.py 等处的传统用法),而应把选择原样交给indices_to_mask(定义于 misc.py),再以broadcast_tensor(misc.py)对view[mask].shape广播值、以assign_indexed_tensor(misc.py)执行写入。indices_to_mask从末尾计数的负边界是其已知缺口,应在该函数内修复,而不是逐个访问器绕开。 - 零拷贝路径只依据
gs.use_zerocopy分支(见 genesis/init.py,由环境变量GS_ENABLE_ZEROCOPY控制),禁止 try/except 探测。
构建期与运行期的数据获取分工
- 构建/初始化期:通过 entity/link/geom 公开 API 迭代(
entity.links、link.geoms、geom.init_verts、link.get_pos()等),只有效率、简洁性或可维护性确实需要时才下沉到低层求解器字段。 - 运行期:批量转换用
qd_to_numpy/qd_to_torch,禁止逐字段索引field[i];优先用 torch 零拷贝更新其他求解器的内部状态(numpy 零拷贝仅 CPU 后端支持)。 - 构建期应把 Python 引用(entity、link 对象)存入 dict 供运行期查找,而不是每步从求解器字段重新推导。
- 派生多个原始求解器字段的量应归属为单个向量化求解器 getter,而不是由消费者自行组装或写专属逐元素
@qd.kernel再重索引——字段访问耦合与 batched/non-batched 处理应集中在单一经测试的位置。批量参数用envs_idx=env_idx if batched else None处理,而不是针对批处理分支;getter/setter 应对所有环境一次性处理(IPC 相关的可退化为 for 循环),禁止按 env idx 反复调用。
Metal 流同步
在一批 torch 零拷贝写入之后、下一个 Quadrants 内核读取缓冲区之前,调用者需调用一次torch.mps.synchronize()。执行此类写入的辅助函数(如qd_zero_grad)内部从不自行同步,这一责任在调用方。源码 misc.py 中确实可见 Metal 后端的同步处理注释与torch.mps.synchronize()调用。
内核参数的数值类型
禁止向内核参数传递 numpy 标量(如numpy.float64)——它会破坏 Quadrants fastcache 的弱引用机制。必须强转为原生 Python 类型(float()、int())。
Kernel 编写规范
可微分的结构性要求
每个内核都必须能被 Quadrants 的 autodiff 反向执行,因此结构性禁令非常严格:
- 顶层只能由
for循环组成,禁止continue、禁止while; - "门控"用静态条件下置位、再由
if测试的标志实现,不能用提前continue; - 遍历块时用
for覆盖 dof 范围并做块起始测试;条带步进用for i_chunk_ in range((n + BLOCK_DIM - 1) // BLOCK_DIM),再以i = i_chunk_ * BLOCK_DIM + tid配合if i < n门控; - 手工编写反向内核被强烈反对,除非有非常充分的理由(如反转 autodiff 无法展开的迭代算法——约束求解、逆运动学)。
间接索引的绑定约束
从字段读出的索引必须先绑定到临时变量再用于索引其他字段(如i_r = rigid_info.links_root_rank[i_l]然后rigid_info.roots_link_end[i_r])。a[b[i]]这类嵌套间接索引在内核与 func 中禁止。
规范参数顺序(Canonical Parameter Order)
所有 kernel 与 func 遵循固定参数顺序:先是各类索引(循环索引、envs_idx等索引张量、joint_idx等索引标量或范围起止),然后是动态原生 Python 标量与定长 Quadrants 向量(每调用值:位置、四元数、穿透量),接着是 kernel/func 专属张量,然后依次是全部 state 结构体、info 结构体、静态配置,再是"实践中实为常量"的参数(形状整数、eps、容差),最后是编译标志——先是静态的(qd.template()布尔量),再是运行时原生布尔量(如write_L: bool,保持动态以省去每种取值的一次编译)。errno单独跟在最后一位。state/info/config 各组内部的组件顺序固定(dyn、rigid、collider 含 mpr、gjk、support_field、sdf,然后是 constraint)。调用点按签名顺序传参。
静态配置与运行时信息的边界
静态配置只能容纳有界选择:布尔、枚举、小固定集合内的整数。而"可取任意值的整数"(环境数量、叶子/面数量、由此导出的位宽)属于运行时信息——应为内核中的张量形状、info 张量或普通整数参数。静态整数会按值编译内核,场景间一变值就破坏预编译(刚性求解器从不对环境数量做键控即是参照)。
类型标注与命名
- 每个 kernel 和 func 参数都必须有类型:标量用原生 Python 类型(
int/float/bool),定长向量/矩阵用qd.types.vector(n)/qd.types.matrix(n, m);类型多态辅助函数的参数可保持不标注。 - 定长向量注解中不得写 dtype:写
qd.types.vector(3),绝不写qd.types.vector(3, dtype=gs.qd_float)。 - 新代码一律用自由函数
@qd.kernel,不用@qd.data_oriented;类型多态参数使用 array_class.py 中的V_ANNOTATION。唯一例外是 FEM 求解器,它延续旧的@qd.data_oriented方法模式,新增内核必须与之保持一致。 - 内核/func 调用中,同名参数按位传递(
self-named),匿名常量(裸字面量、尾部静态标志)按关键字传递;结构体成员与父结构体同传的 func 调用只能关键字传参(因为 quadrants 的位置参数展开会复制成员)。
qd.func 内联与编译预算
每个内核只内联一次它所调用的每个qd.func实例:同一辅助函数被两个不同参数的分支调用会编译两次,模板参数取两个值也会让被调方编译两次,qd.static(range(R))下重函数体会编译 R 次。规范给出的对策:在运行时分支挑选参数只调用一次、以运行时计数循环项(重算廉价逐项值而非携带逐项寄存器)、需要提前退出时用单次迭代for加break(collider 的模式)、可微内核中用if门包住顶层循环体(break/continue被拒)。CI 基准测得的编译时间是裁决标准:低于 2% 无所谓,8% 及以上必须处理,中间地带酌情判断。此外,纯读写数据访问器在热路径上应优先走零拷贝视图(qd_to_torch(..., copy=False)/qd_to_numpy(..., copy=False)),内核仅作回退——只为搬运/改写字段写微型内核,用微秒级工作付出完整内核分发开销不值得。
API 设计
单一正确值的自动解析
如果某选项在给定上下文中只有一个有效值(如 IPCCoupler 下必须enable_collision=False),不要强迫用户设置:默认None,初始化时解析为正确值,用户显式给出冲突值时抛异常。解析逻辑可基于其他选项、场景内容甚至运行时状态,但必须充分文档化。
辅助函数的克制
不为一两行直白逻辑(更别说单表达式)包装辅助函数,无论有多少调用者:直接内联,需要名字时作为局部变量。单一用户场景的专属辅助函数(single-use helper)一律禁止,除非它完全独立且通用(数学函数、转换工具);避免私有实例方法。单值返回值不得包进具名结构体(一字段 NamedTuple/dataclass 是死样板),只有真正多字段返回才用具名结构。
数组输入的类型别名
索引/数组输入选项必须使用项目的 array-like 类型别名(IArrayType/OptionalIArrayType/FArrayType/Vec3FType等,定义于 typing.py),禁止裸tuple[int, ...]/list[...]。原因在源码中可见:Options是严格 Pydantic(ConfigDict(strict=True)),裸类型集合字段会拒绝用户自然传入的 list 与 numpy 数组(如dofs_idx_local=[0, 1]),而别名携带strict=False,可将 array-like 输入强制转换为 tuple。模仿同级选项字段的类型写法,如filter_link_idx: OptionalIArrayType。
风格规范(Style)
文档字符串
- 以一句不超过 200 字符、不点名参数的短句开头,更多内容空行后分段展开;参数在各段中说明。
- 描述现状而非历史:"该函数现在做什么",不写"曾做什么"或"为 X 添加了 Y";若行为是某 bug 修复所需,以当前不变量解释(如"必须处理 Z 情形以避免 Y")。
- 面向用户的选项文档必须陈述权衡(cost AND benefit,何时选何值),只讲"做什么"而不讲代价的文档是无用的;用 Genesis 术语说明,不引用外部引擎(如 MuJoCo)与内部实现机制(约束"行"、耦合、锥投影、内核、分解路径),那些属于开发代码注释。
- 函数级描述放 docstring,禁止在函数体顶部放成块的
#注释;getter/方法开头写一大段#散文是错的——那是它的 docstring。文档字符串不得声明代码未提供的契约(未保证的数组形状、内存布局)。
命名
- 布尔变量/字段/属性以
is_/has_前缀开头,现在时优先(is_fixed、is_convex、has_multi_island_structure);was_只用于真正的过去时标志(宁取is_cached_loaded而非was_cached)。裸名(hibernated)或did_(did_fuse)均非法。前缀问的是状态而非输入参数:函数/方法/内核的布尔参数命名"调用者请求什么"(refresh_position、scale_inertia、in_place),不加前缀。 - 变量名以名词开头命名其持有对象种类(
rejected_kinds、links_offset_pos),不写裸形容词/分词(rejected、left_out);例外是布尔(上述前缀)与索引/循环变量(i_<type>约定)。 - 容器避免泛化的
all_前后缀,宁可具体(joints_xanchor、links_inertia_i、geoms_pos、entities_quat、verts_idx);避免纯变量名赋值(mass_mat_env = mass_mat_all);可变容器优先"清空复用"(self.abd_data_by_link.clear())而非重新分配(self.abd_data_by_link = {})。 - 使用
flatten()、reshape((-1,))、ravel()代替 squashing 是不允许的,且优先[..., 0, :]而非squash(axis=-2)。 - ASCII only:源码(代码、注释、docstring)不得出现非 ASCII 字符——写
tau而非希腊字母、qddot而非带双点的 q、*而非中圆点、-/--而非 em dash、纯# ---而非制表符分隔线。缩写在每次 docstring/注释首次出现时全称拼出并括号标注缩写(如 "positive semi-definite (PSD)")。
注释
- 注释解释为什么:它保护的不变量、诱人替代方案的失败模式;保持通用、现在时,不锚定易腐细节(测量值、基准数字、特定单测/示例)。
- 每条事实只存在于一个注释中,其他站点交叉引用它并保留一行指向真源(如 "see nt_H in array_class.py")。
- 注释独立成行置于所注解代码上方,尽量不尾随行内注释;不写无用/循环注释(不重述名字、类型、控制流已传达的信息,不写同义反复)。
- 段落结尾不得以单字词单独成行;行填充至 120 字符宽。
- 已知上游 bug 记
# FIXME:并命名 issue,最多两行,含 tracker 引用(如# FIXME: quadrants#887 - ...)与 workaround 的代价。
其他硬性风格
- 写朴素陈述句(主语-动词-宾语),用标准仿真词汇(质量矩阵、Jacobian、逆权重、运动学树、子步),不自造动词。
- 不用分号连接散文,不用破折号当括号:写两个句子或用圆括号。
- 局部变量以其是什么命名,而非其被测属性(场景变量叫
scene_from_substeps,不叫shorthand)。 - 禁止用浮点
==/!=比较,即使主机端值也如此:用np.allclose(a, b, atol=gs.EPS)或显式界。 - 不写第三方对象的私有状态;自持记录在旁并经 property 暴露。
- 叙述"是什么"而非"不是什么":去掉 "not X"、"unlike Y",除非读者天然会做错误解读。
- 导入分组:标准库、通用第三方(numpy/torch/tqdm…)、其他第三方、自家库(quadrants…)、genesis 自身(先绝对后相对),组间空行、组内字母序;改动一个导入就必须重整整个模块的导入。禁止导入别名(
Cloth而非ClothMaterial)。 - 匿名字面量按关键字传递以在调用点显其含义(
_adaptive_params(verts, faces, aggressiveness=7)),模块局部 qd.func 调用中的数值常量可保持位置传参。 - 换行按嵌套层级全有或全无:能一行放下(≤120 字符)必须单行;溢出则整体收进单个续行;再溢出则每个参数一行;展开的外层调用中,能单行放下的嵌套调用保持单行。嵌套数据字面量(矩阵、坐标/边表)是例外:即便整体能放下也保持一行一行。
- 一个
if子句要么全是静态条件要么全是动态条件;共享内存数组(qd.simt.block.SharedArray)用sh_前缀。 - 禁止用快照相等比较检测状态变化:Genesis 有专用变更检测设施(solver 的 StateChange 订阅机制,见 base_solver.py 中的
StateChange枚举与mutates装饰器),缓存运行数据再轮询对比新副本的做法被禁止;设施缺失或粒度不足不是借口——扩展机制,绝不退回到快照比较(这是整个 PR 被拒的充分理由)。 - 非 solver 类(materials、couplers、entities)不得使用
@qd.data_oriented;目录内命名保持一致(如 IPC 示例统一ipc_*.py前缀)。
测试规范(Testing Guidelines)
组织与命名
- 测试按组件再按能力组织:
tests/下每组件一个目录(rigid/、deformable/、particles/、ipc/、coupling/、sensors/、rendering/、parsers/、core/、integration/、benchmarks/),每能力一个文件(如 test_collision.py、test_imu.py)。新测试进既有能力文件,只有真正的新能力才开新文件。 - 优先加强既有测试而非新写:扩展场景与断言,或用
@pytest.mark.parametrize加维度。测试以所验证的特性命名,而非被仿真场景:test_reject_offaxis_contact_on_authored_decomp而非test_stacking_tower_stability;名字不以模块/文件夹名作前缀(tests/sensors/test_temperature.py::test_grid_sensor_contact_and_reset,绝不写test_temperature_grid_...)。 - 单元测试不得有 docstring——好的测试名胜过短 docstring;代码注释仅在强动机下允许(解释测试体无法传达的内容)。绝不写钉死已知缺陷、验证 deprecation warning、或测试"不提供能力"的测试;但"拒绝无法处理的输入"是行为,应被断言(若继续会损坏用户所得)。
打包测试(Pack Tests):一次场景构建
一次测试只建一个场景,宁可牺牲可读性也必须避免额外构建(构建慢且计算重)。偏好顺序:一个综合场景(多样实体与选项)> 同场景内不同配置实体 > 必须互不干扰的布置在同一场景内远距摆放 > 通过n_envs扫配置 > 参照仿真并排折叠进同一场景。"注定被拒绝的场景"构建零成本、不占预算,一个测试里多个没问题。
断言物理,而非执行
"仿真无错跑完"不是测试。检查有物理意义的量:自由落体位移(z = z0 - 0.5*g*t^2)、无地面穿透(min_z > -d_hat)、静止时速度→0、接触阻止下落。无解析期望时可先跑一次取参考值硬编码,用宽松容差断言,并加FIXME注释请求日后替换为物理感知断言。用 assertions.py 的assert_allclose/assert_equal:精确比较优先assert_equal(等价于零 atol/rtol),tol=同时设置 atol 与 rtol 且适用于量级为 1 的量,量级固定远离 1 的(如0..255像素通道)用atol=。提交针对特定缺陷的测试前,先故意破坏实现并观察断言失败。
步数与容差预算
步数预算是硬性约束,按测得而非猜测的最少步数来定:浮点敏感量加 10% 或进位到 50(取更大者)。层级:<100步无碍、100-300灰色(需说明)、>300近乎禁止(全测试套件仅允许 4-5 个)。同时约束总 env 步数(steps * n_envs):500 步 × 16 env 的稳定期(8000)不可接受。批量测试用n_envs=[0, 2]参数化(多 env 是形状 bug 的藏身处),且不添加 conftest fixture 已控制的死参数维度(如backend)。
- 验证数学而非规模:一个约束足以证明理论(对 1 成立即对 1k 成立),正确性不需要大/高 DOF 场景;真实多体场景仅在验证单约束无法验证的东西(端到端稳定性)时才值得花步数。
- 优先浮点稳健检查:单次约束求解比较(从固定状态一步)稳健;跨数值不同代码路径的多步轨迹比较不稳健(fp32 误差累积、正确路径也会发散)。跨引擎(MuJoCo)一致性是现实检查,解析闭式是数学检查。回归测试是被动添加(bug 浮现时),不做推测性添加。
- 梯度-有限差分(FD)容差钉在测得地板:地板
T = max|ana - fd| / (1 + |fd|)(配置 eps 下,取 CPU 与两个 GPU 架构最坏值),容差取地板的 1.5x-5x,值只能取自 {1, 2, 5}e-X。不追的地板是 fp64 的 1e-10 与 fp32 的 5e-5;eps 按精度设置(fp32 大、fp64 小)。地板恰为 0 说明检查空洞——修 loss 而非容差;与 eps 无关的残差是解析 bug,绝非 FD 伪影。 - 向物理精确收紧:宽松容差掩盖真实行为差异;收紧导致测试失败时深挖根因而非放宽;用精确断言路径(全 horizon、所有阶段)验证修复,不用廉价代理。
- 精确解析动力学检查:强制
gs.integrator.Euler使有限差分qacc等于求解器结果,并同时计入刚体转动惯量与隐式阻尼一阶修正(effective_inertia = I + damping*dt,见test_position_control)。
场景构建细则
- 每个
scene.add_entity/scene.add_sensor/gs.morphs.*/gs.options.*调用每行一个选项(ruff 不强制,需手写);关键字参数遵循被调函数声明顺序(add_entity(morph=..., material=..., surface=...)),关键字不授权重排。 - 只设严格必需的选项,默认值不显式设置(除非计算读取该值:
gravity、dt必须显式设置以匹配断言)。 - 标量/list 直接传递(
control_dofs_force(TAU)、set_dofs_kp([...])、inverse_kinematics(pos=[...], quat=[...])),不包np.full/np.array/单元素[...];一次性目标向量内联而非命名。 - 自定义 MJCF/URDF 模型用
xml.etree.ElementTree在 fixture 中构建,返回ET.tostring(mjcf, encoding="unicode")直接传给 morph 的file=(FileMorph.file接受内联 XML 字符串,见 conftest.py),绝不写临时 XML 文件、绝不write_text/f-string 拼 XML。 - 需落盘的临时资产用 session 级
asset_tmp_pathfixture,tmp_path仅 per-test;无模块级测试常量/辅助/参数化场景列表,参数从建好的模型读回(get_dofs_armature、get_dofs_damping,见 rigid_solver.py)。 - 重复样板藏进 fixture,但无回报的间接层严格有害(默认有害,须以真实收益证明);场景搭建留在测试体(单测读起来像示例脚本),宁重复也保持显式自洽,不做过早分解。
- 每步断言不做
float(...)/.item()强转:设计场景使断言无条件成立(如预热循环把关节转起来),再对整张张量.all()断言。 - 每个调用
scene.step()的测试取show_viewerfixture 并设置带相机视角的ViewerOptions,便于启用 viewer 调试。 - 需要接触的场景必须有可动物体:双固定 geom 对在构建期被丢弃,把固定实体沉入表面不会产生接触,还会永久埋没其几何体渲染。
- FEM 实体位置:
entity.get_state().pos形状[B, n_verts, 3],用[..., 2]跨 env/顶点选 z;刚体实体:entity.get_pos()返回[B, 3]或[3],用np.atleast_1d(...)[..., 2]与.all()做多 env 检查。
测试输出的纪律
绝不内联过滤/截断测试输出(pytest ... | tail/| grep):过滤器夹在 pytest 与磁盘之间会毁掉失败名的唯一副本并掩盖退出码。重定向完整输出到日志文件再从文件提取。绝不删除或弱化既有断言/测量来压掉失败(本地或 CI)——连同数据上报并询问;只在单机成立的阈值是校准问题。Bug 修复 PR 必须带回归测试:在main上失败、带修复后通过,加入已覆盖被破坏能力的那个测试。
从 Git Worktree 运行测试:可编辑安装的"测错引擎"陷阱
包以可编辑模式指向主检出安装,因此 worktree 的genesis/默认不被导入——worktree 的tests/实际跑的是主检出的genesis/,引擎改动看似无效、所有结果作废。规范给出的识别症状:引擎改动无效果、worktree 中新增的内核print()不输出、引擎变体 A/B 每臂位级相同——先按此陷阱排查,再谈物理解释。
正确做法:
- 从 worktree 调 pytest 时始终传
PYTHONPATH=$PWD:cd <worktree> && PYTHONPATH=$PWD pytest -n 8 tests/rigid;集群同理PYTHONPATH=$WORKTREE pytest ...。 - 每会话验证一次而非假设:
PYTHONPATH=$PWD pytest -n 0 -s -k <any test>并print(genesis.__file__),确认打印路径以 worktree 开头。 - 裸
python script.py从 worktree 运行确实会选中 worktree(cwd 先于可编辑路径),因此同一目录下脚本与 pytest 可能跑不同引擎,二者结果不可比较。 - 并行运行时给 worktree 独立编译缓存:
QD_OFFLINE_CACHE_FILE_PATH=<scratch>/quadrants GS_CACHE_FILE_PATH=<scratch>/genesis。worktree 引擎不同于填充共享~/.cache的引擎,每个内核都是新的,xdist worker 竞争写同一缓存会损坏条目:表现为上百个明明在作用域内的QuadrantsNameError、散布在单跑通过的测试中、第二次运行因坏条目持久而更糟。重定向缓存而非删除共享缓存。
集群测试实践
基于真实集群环境(ssh genesis-coreweave,代码路径/mnt/home/duburcqa/workspace/src/genesis):
- Git 操作(fetch/checkout/pull)必须在登录节点完成,不能在
gs-srun内(计算节点容器无法访问 GitHub)。 - 始终用
gs-srun(Slurm 包装器)分配 GPU 节点,绝不在登录节点直接跑 pytest/python;绝不手动 source venv——容器镜像已带正确环境,bash -lc登录 shell 自动配好。 - 组合模式:
ssh genesis-coreweave 'bash -lc "cd /mnt/home/duburcqa/workspace/src/genesis && git pull && gs-srun --partition=rtx-high --nodes=1 --gpus=1 bash -ilc \"cd /mnt/home/duburcqa/workspace/src/genesis && pytest -n 10 tests/ipc -v --no-header 2>&1\""'。 - 示例测试需
-m examples覆盖 pyproject.toml 的默认标记过滤(默认-m not (benchmarks or examples))。 - 本地文件复制用
scp <local_path> genesis-coreweave:<remote_path>后gs-srun运行。 - 计算容器挂载
/mnt/home,不挂登录节点的/tmp:登录节点/tmp下的脚本/补丁/输出目录在gs-srun下不可见,一切暂存到/mnt/home/duburcqa。 - 交互登录 shell(
bash -ilc)设置noclobber:cmd > file在文件已存在时报 "cannot overwrite existing file" 且静默无输出;固定路径再生成日志时先rm -f file或用>| file。 - 用
bash -ilc "source script.sh"跑暂存脚本,绝不bash -l script.sh:容器 Python 环境只在交互登录 shell 初始化,非交互bash -l在gs-srun下无 venv(ModuleNotFoundError: genesis)。脚本暂存于/mnt/home后 source 之,同时避开超长内联命令的嵌套引号问题。
复现 Apple 软件渲染器失败
GitHub Apple Silicon macOS runner 都是虚拟机,虚拟化 GPU 无 OpenGL,渲染回退到 Apple Software Renderer(该渲染器半损坏)。macOS 专属渲染失败源于测试触及其故障模式,因此单元测试要谨慎依赖渲染特性。在本地任意 Mac 强制复现:在任何 GL 上下文创建前劫持 pyglet 无条件追加的两个像素格式属性:
# force_sw.py - import before any GL context is created, e.g. 'pytest -p force_sw' with PYTHONPATH set from pyglet.libs.darwin import cocoapy cocoapy.NSOpenGLPFAAllRenderers = 70 # NSOpenGLPFARendererID cocoapy.NSOpenGLPFAMaximumPolicy = 0x00020400 # kCGLRendererGenericFloatID- 这只影响 pyglet 创建的上下文,离屏渲染须以
PYOPENGL_PLATFORM=pyglet路由回 pyglet 平台(其 macOS 默认 CGL,pyglet 对此一无所知)。 - 用与 macOS CI 相同的标志运行:
PYTHONPATH=<dir-with-force_sw.py> PYOPENGL_PLATFORM=pyglet GS_TORCH_FORCE_CPU_DEVICE=1 pytest -p force_sw --dev --logical --backend cpu --forked <tests>(CI 另选-m 'required and not slow')。 - 生效验证:Genesis 日志输出 "Software rendering context detected",且
scene.visualizer.is_software为 True。 - 已知故障模式一:相机视锥外的顶点几何被错误光栅化,破坏像素比较——地面平面默认
plane_size实际无限(1km × 1km),须给有限尺寸并置于视图内。 - 已知故障模式二:软件渲染后端因性能强制禁用阴影映射,快照场景必须显式关阴影(rasterizer 的
shadow=False);硬件 GL 带阴影生成的快照永远无法匹配。
工具链与贡献流程
- Lint/format 用 ruff(check + format,行宽 120,见 pyproject.toml)经 pre-commit 执行:
pre-commit install后每次提交自动运行。 - PR 标题带括号标签:
[BUG FIX]、[FEATURE]、[MISC](默认会改变仿真物理:不同模型或不同默认参数;求解器内部重构属[MISC])、[CHANGING]、[BREAKING](API 破坏)。提交标题是纯单行句、无标签;PR 与提交标题都以句号结尾。 - PR 标题陈述对最终用户的收益而非实现,实现细节进 PR 描述。
- 贡献者须遵守 CODING_GUIDELINES.md 与
.github/contributing/下的参考文档(ARCHITECTURE、TESTING、CODING_CONVENTIONS、EXAMPLES、PULL_REQUESTS、USD_PARSER);冲突时先问。
结语
把CLAUDE.md的规范与genesis/源码对照阅读,可以清晰地看到一条贯穿始终的主线:所有约束都服务于"可自动微分的性能内核 + 稳定清晰的公开 API"这一对目标。内核侧通过参数顺序、静态/运行时信息切分、qd.func内联与零拷贝视图换取编译速度与 GPU-CPU 传输的最小化;用户侧通过强类型数据结构、类型别名、单一正确值自动解析与权衡式选项文档换取 API 的可预测性与自洽性;测试侧则用打包场景、物理断言、步数与容差预算保证 CI 成本可控且断言真实有效。对于希望为 Genesis 贡献代码(无论是人类开发者还是 AI 编码助手)的读者,这份指南既是一份可执行的检查清单,也是理解仓库设计哲学的入门地图。
- 物理引擎
- 具身智能
- 机器人
- 人工智能
【免费下载链接】genesis-world
Simulation platform for general-purpose robotics & embodied AI learning.
相关推荐
Genesis 编码规范实战指南:仿真引擎内核开发的命名、内核编写与测试评审约定
Genesis 编码规范实战指南:仿真引擎内核开发的命名、内核编写与测试评审约定 本文以开源机器人仿真平台 Genesis 官方编码规范文档 CODING_GU
物理引擎具身智能机器人人工智能Saleor 工程规范全景:为横向扩展与并发安全而写的 Headless Commerce 内核开发指南
Saleor 工程规范全景:为横向扩展与并发安全而写的 Headless Commerce 内核开发指南 本文以仓库根目录 AGENTS.md https://
后端电商Penpot monorepo 测试工程实践:从 TDD 纪律到跨模块测试执行规范
Penpot monorepo 测试工程实践:从 TDD 纪律到跨模块测试执行规范 Penpot(开源的设计协作与 UI/UX 平台)采用多模块 monorep
前端设计系统图形学协同办公
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考