贡献指南#

您可以贡献缺陷修复、新功能、文档和教程。

贡献代码#

注意

请参阅 Google Style Guide 以了解编码风格,然后再为代码库做贡献。在编码风格部分,我们概述了代码库中遵循的与风格指南的具体偏差。

我们使用 GitHub 进行代码托管。请按照以下步骤贡献代码:

  1. 在 issue tracker 中创建一个问题,讨论您希望进行的更改或添加内容。这有助于我们避免重复工作,并确保这些更改与项目的路线图保持一致。

  2. Fork 仓库。

  3. 创建一个新分支用于您的更改。

  4. 进行更改并提交它们。

  5. 将您的更改推送到您的 fork。

  6. 向 main 分支 提交拉取请求。

  7. 确保对拉取请求模板执行所有检查。

在发送拉取请求之后,维护者将审查你的代码并提供反馈。

确保代码格式正确、文档完整,并按 单元测试 和 工具 所述,运行相关测试和必需的 CI 检查。

小技巧

每个拉取请求专注于一个变更,不相关的修改应分别提交,便于维护者独立审查和测试。

关于代码风格和测试的更多细节可以在 代码风格 和 单元测试 部分找到。

智能体开发#

阅读与当前任务相关的贡献章节和技能。已在对话中载入且仍然有效的指导应复用;相关文件变化或必要上下文丢失时再重读。原生技能发现已提供说明,无需加载整个目录或全部链接参考。准备最终变更时,再加载 PR 准备指南。

编辑前,先确定行为归属、最接近的可复用实现,以及解决问题所需的最小改动。涉及多个包或增加高频执行路径工作量时,简要说明受影响边界与运行成本。区分用户要求的范围调整和无关改进,后者记录为后续事项。

限制搜索范围,阅读相关函数与调用者,不要输出无关文件或完整工具清单。批量执行独立读取,并使总输出量便于检查。只有用户要求委派或适用工作流需要时,才为各智能体分配明确的问题和文件边界;共享基础设施的编辑应串行执行。

工作树与环境#

当前检出中存在无关变更时,使用独立工作树,在其中运行命令并使用独立 uv 环境,uv 可复用缓存中的下载包。必须复用已有环境时,验证前需核对解释器和导入源码路径。CLI 根据导入的包而非 shell 工作目录解析仓库根目录,可用以下命令检查:

uv run python -c "import sys; from isaaclab.paths import ISAACLAB_ROOT; print(sys.executable); print(ISAACLAB_ROOT)"

同时核对变更涉及的包实际从何处导入。避免在同一环境或输出目录中并发同步环境或构建文档。

验证与长时间任务#

编辑期间先运行范围最小的相关检查,再执行必需的最终检查。在任务记录中保存被测修订版或相关差异、命令和结果。在相关源码、依赖、配置或测试变化使结果失效前,复用成功结果。重试前先诊断失败,区分环境问题与本次变更引入的回归。

长时间检查只启动一次,保留进程或 CI 运行 ID 及日志。继续独立工作时,使用本地监测命令或较长轮询间隔,报告有意义的状态变化,不反复读取未变日志。若任务仅要求启动 CI,确认已启动后提供运行链接;若结果是完成任务的必要条件,则等待结束。

贡献文档#

为文档贡献的方式与为代码库贡献一样简单。所有文档的源文件都位于 IsaacLab/docs 目录中。文档使用 reStructuredText 格式编写。

我们使用 Sphinx 和 Book Theme 来维护文档。

API 的 [source] 链接在 GitHub 打开实现,使用已发布文档对应的分支或标签,当前检出构建默认为 main。不生成本地 Python 源码页面,查看实现需要联网。

发送文档的拉取请求与发送代码库的拉取请求相同。请按照 贡献代码 部分中提到的步骤进行操作。

文档媒体中,只有较小的 .jpg 文件应直接提交到 Isaac Lab 仓库。较大的图片、视频、动画等媒体,应上传到外部托管位置,再从文档链接或嵌入。

浏览器演示资源包和导出器源码通过永久的 interactive-demos 发布页 分发。复用 interactive-releases 发布分支,将以提交标识命名的归档上传到同一发布页。保留旧资源,并在 .github/actions/_lib/stage-browser-demos.sh 中同时更新已审查归档的 URL 和 SHA-256,无需新建发布标签或编号图库。文档工作流下载并校验归档,再将其中的 site/ 目录放入 docs/_build/demos/ 后发布 Pages。保留现有旧版目录,它们提供共享资源并维持旧链接。开发源码树包含资源暂存脚本、文档链接和小缩略图。从仓库根目录执行暂存步骤:

bash .github/actions/_lib/stage-browser-demos.sh

该步骤还会按各文档版本的位置修正演示的相对链接。本地预览完整网站时,请通过 HTTP 提供 docs/_build 目录:

uv run --no-project python -m http.server 8000 --directory docs/_build

文档打开 http://localhost:8000/current/index.html,演示打开 http://localhost:8000/demos/interactive/。下载的 docs-html 产物包含 PREVIEW.txt,给出从解压根目录运行的等效命令。演示必须通过 HTTP 加载,浏览器才能获取 WebAssembly 模块和数据。

小心

构建文档前,先安装 uv 。构建命令将包含文档依赖的 dev 可选依赖组同步到仓库的 .venv 。

根据修改影响的行为,选择文档验证方式:

  • 渲染文档不受影响时,跳过 Sphinx,包括单独修改 AGENTS.md 或 skills/ 的情况,改为运行相关代码或技能检查。

  • 编辑文档页时,使用增量 HTML 预览。

  • 修改 API 签名、文档字符串、Sphinx 配置或扩展,以及主题时,使用全新构建;Sphinx 缓存不能可靠检测 Python 源码变更。

  • 提交 PR 前,对最终影响文档的修改执行一次全新构建,并确保没有警告。只有后续又有相关修改或构建失败时,才需要重复。CI 也会执行全新构建。

增量 HTML 预览在 Linux 和 Windows 上均从仓库根目录运行:

uv run --extra dev --directory docs python -m sphinx -W --keep-going -j auto . _build/incremental

有 Make 的系统可使用等价命令:

uv run --extra dev make -C docs incremental-docs

打开 docs/_build/incremental/index.html 查看预览。后续运行复用缓存,将新警告视为错误,但可能遗漏旧警告或保留已删除页面。最终验证应使用全新构建;不要向同一输出目录并发构建。

最终全新构建从仓库根目录运行以下命令。它安装文档包,并构建当前版本:

uv run --extra dev isaaclab --docs

文档生成于 docs/_build/current 。在浏览器打开 docs/_build/current/index.html 查看。每次构建都会清空当前 HTML 输出和 Sphinx 缓存,避免保留已删除页面或缓存中的警告。

贡献资产#

大型资产文件不应直接加入 Isaac Lab 仓库,应托管在独立仓库,再从相关文档链接过去。

请查看 Isaac Sim 资产 以了解当前可用资源的更多信息。

注意

我们目前正在研究一种更好的方式来贡献资产。一旦我们有了解决方案,我们将更新此部分。在此期间,请按照下面提到的步骤进行操作。

要托管您自己的资源,当前的解决方案是:

  1. 创建一个单独的存储库用于资产,并将其添加到那里

  2. 确保资产已获得使用和分发的许可

  3. 在仓库的 README 文件中包含资产的图片

  4. 发送一个包含仓库链接的拉取请求

我们随后会验证资产及其许可,并确定集成方式。如有疑问,请在仓库提交 issue。

维护包的变更日志与版本#

每个纳入发布管理的包,在 docs/CHANGELOG.rst 中维护变更日志,在 pyproject.toml 中维护版本。

变更日志按时间顺序整理各包版本的重要变更。

备注

CI 每晚根据各 PR 的 towncrier 片段文件 ,生成 CHANGELOG.rst 和 pyproject.toml 中的包版本;贡献者不直接编辑二者。对于 PR 修改的每个 source/<pkg>/ 包(不计 changelog.d/ ),在 source/<pkg>/changelog.d/ 下添加片段:

  • <slug>.<type>.rst :每种条目类型一个文件, <type> 为 added 、 changed 、 deprecated 、 removed 或 fixed ;文件包含对应章节的列表条目。

  • <slug>.minor 或 <slug>.major :空文件,将版本升级级别从补丁级提高到次版本级(新增公共 API)或主版本级(不兼容变更)。

  • <slug>.skip :空文件,不生成条目也不升级版本,适用于只修改 CI、文档或测试的 PR。

<slug> 可使用任意简短、唯一的名称;建议默认采用分支名,并将 / 替换为 - 。同一批次中,该包采用最高升级级别。CI 按 语义化版本 升级 pyproject.toml 中的包版本。

变更日志使用 reStructuredText 格式,帮助用户和贡献者准确了解各包版本之间的重要变更。每个纳入发布管理的包都 必须 提供变更日志。

编写各片段时,遵循以下指南:

  • 各片段的 <type> 指定其条目所属的变更日志章节。

    • added :新增功能。

    • changed :已有功能的变更。

    • deprecated :即将移除的功能。

    • removed :已移除的功能。

    • fixed :错误修复。

  • 每项变更使用 * 列表项描述,续行缩进。

  • 不兼容变更以 Breaking: 开头;对于调用方需要适配的弃用、变更或移除行为,提供迁移说明。

  • 项目要点采用 过去时 编写。

    • 这意味着这个变化被描述为已经发生过。

    • 项目要简明扼要,直奔主题。不应冗长。

    • 项目符号应当包括变更的原因,如果适用。

小技巧

有疑问时,请检查现有的 changelog 文件中的样式,并遵循相同的样式。

例如, source/isaaclab/changelog.d/fix-partial-reset.fixed.rst :

* Fixed contact sensor reset behavior when only a subset of environments was reset.

针对 PR 的基线验证,无需创建临时远程跟踪引用:

uv run python tools/changelog/cli.py check upstream/main --include-worktree

检查器接受带远程名的引用、完整引用和提交 SHA。main 等分支简写仍优先使用已有的 origin/main;若需显式选择本地分支,使用 refs/heads/main。pre-commit 钩子优先使用已设置的 ISAACLAB_CHANGELOG_BASE_REF,否则使用 main。验证前请获取目标基线。

编码风格#

我们遵循 Google Style Guides 作为代码库的规范。对于 Python 代码,遵循 PEP 指南。最重要的包括 PEP-8 用于代码注释和布局, PEP-484 和 PEP-585 用于类型提示。

对于文档,我们采用 Google Style Guide 来编写 docstring。我们使用 Sphinx 来生成文档。请确保您的代码有良好的文档说明并遵循指南。

重构与 API 设计#

采用能解决问题的最小修改。修改接口前,阅读周边代码、调用方、测试和文档。新增代码或整理现有实现时,遵循以下规则:

  • 除非修改明确要求,否则保留可观察行为。除返回值外,还要检查顺序、形状、dtype、设备、输入修改和失败行为。移除或重命名公共 API,需要弃用过程和迁移方式。

  • 无状态的计算和转换优先使用函数式方式:以明确的输入、输出编写普通函数,并将副作用限制在清晰边界。类应承担有意义的状态或资源管理、在生命周期中维护不变量,或实现架构所需接口。避免仅用于归组静态方法、包装单个操作或向其他对象转发调用的类。简化设计时保留既有公共约定。CLI 帮助应使用 argparse 的解析和格式化能力,不要另建私有 argv 扫描器或帮助构建器类。

  • 添加新文件前,先将共享操作放到已有的、负责其约定的模块中。引入辅助函数、配置选项或抽象前,优先复用现有机制。只有约定相同时才提取共享逻辑;调用者不需要公共 API 时,辅助函数保持私有。能够减少不必要嵌套时,优先采用直接控制流和提前返回。

  • 对于预期内的不兼容,例如过滤可用相机通道,应使用判定条件或可选查找结果,不要通过抛出并捕获异常处理常规选择或能力检查。

  • 缺少可选依赖时,保留 Python 原始的 ModuleNotFoundError,不要仅为增加安装提示或将其重新标记为配置错误而包装导入失败。

  • 简单表达式和操作应直接内联,若辅助函数只是增加一层跳转,就不要提取。不要仅为给明显操作换个名字而创建单行辅助函数。只有能消除有意义的重复,或为连贯且不简单的操作提供有用名称时,才引入辅助函数,其收益应超过理解调用者时跳转到另一个定义的成本。停用工作流时,删除不再使用的辅助调用链,以及仅用于保留这些辅助函数的测试。若命名复杂判定条件能使分支更易读,可以命名,但操作本身保持内联。避免 cfg = visualizer.cfg 这类仅缩短属性访问的别名。

  • 优先直接读取和赋值属性,即 obj.value 和 obj.value = value。只有确实需要动态属性访问时,例如属性名在运行时确定,才使用 getattr 和 setattr。不要对已知属性使用它们,也不要以默认值掩盖缺失的必需属性;可选字段应在接口中明确表达。已知为空的选择使用具体默认值。None 留给独立的策略或生命周期状态,例如选择环境帧率或等待客户端连接。

  • 每份状态和每项校验应只有一个负责方。使用者遵循负责方的约定,不应自行修补结果或维护重复状态。只有生命周期和失效规则明确时才缓存派生值,不要暴露可变缓存结果以免调用者误改。共享资源应在自身生命周期边界完成初始化;注册首个使用者不应触发共享资源初始化或转移句柄。选择只在初始化时解析一次,后端直接使用最终选择,不再二次过滤或缓存。当前选择与可用选项应各保留单一表示。生产者关闭时解除借用回调,Python 管理的数组不需要单独的后端销毁步骤。启动过程负责可视化器选择,包括为录制保留的图像生成器;配置扫描返回最终列表,不应另存第二份生成器列表等待后续解析。图像处理归于 utils.images,不得依赖 envs.utils。场景引用在仿真初始化时绑定,核心仿真不得依赖环境相机适配器。为单个使用者添加参数记录和准备辅助函数前,先检查配置或数组元数据中已有的值。剩余设置应留在负责方,只缓存需要复用的缓冲区或计算结果。

  • 从组装入口将场景依赖传给使用者。调用者已持有依赖时,不要通过仿真单例再次查找。初始化时解析引用并保留解析后的对象,不要在配置字段间复制路径。给使用者传入已解析的资源,不要传入仅用于查找资源的更大构建计划;可视化器接收已绑定的相机选项,克隆和渲染器场景准备则保留 ClonePlan。

  • 通用配置放在共同负责方,明确记录各后端能力。集合使用复数命名。在预告的移除版本中删除空钩子和过期兼容别名,不要继续维护未使用的扩展点。

  • 后端选择应保留在共享分发边界。使用既有类型、配置与能力约定,不要从类名字符串推测行为。通过配置实例定制已有预设备选项,不要仅为修改后端设置而添加预设子类。预设声明和选择属于 isaaclab_tasks 与脚本,应在运行时启动前完成。核心仿真和后端包不得导入、声明或解析预设。

  • 保持物理与渲染职责分离,在最终确定配置前解析构造需求。共享资源负责方重建资源前,应先释放借用者;物理管理器不得导入渲染器实现或操作其附加状态。相机和 ISP 设置应在相机构造期间、场景克隆或原生导入前写入。渲染器初始化使用已准备配置,不要为特定功能向 BaseRenderer 添加场景创建钩子。几何所有权见 场景数据提供器,后端集成见 添加物理后端。

  • 不生成 prim 的传感器路径应视为对已有场景 prim 的引用,而不是独立管理的克隆源。先导入所属资产,再从其复制出的 prim 中解析传感器跟踪对象。

  • 嵌套物理刚体应随其所属源对象一起导入。仅当父对象的源到目标映射尚未覆盖第二次声明时,该声明才需要单独的原生副本。

  • 优先使用项目已有依赖和标准库。不要为假设中的未来用途添加依赖或兼容层。

数组操作与运行开销#

检查每步、每次重置或每个环境执行的代码中的分配、复制、同步和重复计算。单个环境中很小的开销,在大批次中可能占主要部分。

  • 支持切片的 API 应保留切片,只有使用方要求具体索引时才生成索引数组,并尽量复用缓存的设备索引。保留选择器的顺序和设备约定。

  • 直接按所需值、dtype 和设备分配数组。优先使用 torch.full 、 wp.full ,而不是通过 Python 列表填充、对临时数组做算术,或经由另一库来回转换。

  • 对 Warp 持有数组的操作应留在 Warp 中。使用者需要多种数组接口时采用 ProxyArray,不要仅为修改 Warp 缓冲区而转换到 Torch 再转回。

  • 每步控制流应直接明了,每个生命周期操作只调用一次。可选工作仅为当前启用的使用者执行,并保留配置的更新节奏。

  • 只有检查布局与归属要求后,才移除多余复制和 contiguous() 调用。除非 API 明确承诺原地操作,否则不要修改调用方所有的输入。

  • 支持时使用批量操作。在热点路径中,避免通过 Python 循环遍历环境,以及不必要的主机与设备传输,或会同步设备的标量读取。

  • 生命周期允许时,开销较大的可选缓冲区可在首次使用时分配。还要考虑计算图捕获:如果分配器有此要求,必需分配必须在捕获前完成。

命名与文档#

  • 函数、方法和命令行参数使用 snake_case 。通过一致的前缀使相关公共符号易于发现,并沿用已有 API 词汇。

  • 可行时使用具体类型、 list[str] 等内置集合注解,以及表示可选值的 X | None 。参数和返回类型放在签名中,不在文档字符串重复。

  • 公共 API 使用 Google 风格文档字符串。物理单位直接写在描述中,例如 Particle positions [m], shape [N, 3] 。混合关节量使用 [m or rad, depending on joint type] 。适用时说明坐标系和数组形状;索引、计数和标志不需要物理单位。

  • 注释保持简短、有选择性。解释代码本身无法说明的意图、不明显约束或边界情况,不要逐步叙述实现,也不要重复表达式作用。欢迎有助于组织文件的可读性注释、章节标记和标题。

  • 代码注释描述当前约定。迁移说明和旧行为描述应放在公共迁移文档或变更日志中,不要在实现内留下重构历史。只有历史背景能解释仍影响正确性的约束时,才保留它。

  • API 改变时同步更新公共文档,并根据实现核实技术说明。

代码结构#

我们遵循特定的代码库结构。这有助于维护代码库并使其更易于理解。

在配置的长度限制内,短表达式写在一行;长表达式在有意义的边界换行。循环头只关注迭代,复杂嵌套记录在循环体内解包。优先使用描述性名称,避免新缩写。通过 * / ** 复用匹配的序列或映射,不要解包后重建;也不要仅为缩短代码而引入打包容器或反射赋值。

在一个 Python 文件中,我们遵循以下结构:

# Imports: These are sorted by the pre-commit hooks.
# Constants
# Functions (public)
# Classes (public)
# _Functions (private)
# _Classes (private)

通过 uv run isaaclab -f ,由 Ruff 排序导入。分组依次为 __future__ 、标准库、第三方包、Omniverse 运行时包、Isaac Lab 包和本地相对导入;具体包分组在 pyproject.toml 中配置,由格式化器应用顺序。同一包内、目标最多需要三个前导点时,使用相对导入,例如 from ...utils import math as math_utils 。更深目标、其他包,以及包含 if __name__ == "__main__": 并作为脚本运行的模块,使用绝对导入。

优先在模块层导入。局部导入适用于将可选后端或仿真器依赖推迟到所选运行路径确实需要时。配置导入应能在仿真器启动前使用,不要重复包已负责的运行时初始化。循环导入可使用 typing.TYPE_CHECKING 处理,详情见 循环导入 。

__init__.py 中的公共导出模块是例外:使用 lazy_export() ,而非传统导入。详情见 延迟加载与模块导出 。

直接向 Warp 内核传入 ProxyArray。每个持有数组只保留一个代理,不并列维护 _ta、_warp 或 _torch 属性;带时间戳的数组缓存可在 data 中持有代理。只有接收方 API 要求时才显式访问原生数组。若内核操作在启动前已确定,使用专用内核或静态 Warp 分支进行特化,不要在运行时切换模式。共享索引只保留一份实现,并将生成内核与原路径进行基准比较。

Python 没有私有和公共类和函数的概念。然而,我们遵循用下划线作为私有函数和类前缀的约定。公共函数和类是供用户使用的函数和类。私有函数和类是旨在在该文件内部使用的函数和类。无论函数和类是公共还是私有的,我们都遵循代码的风格指南,并确保代码和文档一致。

确实需要类时,按以下顺序组织成员:

# Constants
# Class variables (public or private): Must have the type hint ClassVar[type]
# Dunder methods, when needed: __init__, __del__
# Representation: __repr__, __str__
# Properties: @property
# Instance methods (public)
# Class methods (public)
# Static methods (public)
# _Instance methods (private)
# _Class methods (private)
# _Static methods (private)

经验法则是类中的函数按照用户期望使用它们的方式排序。例如,如果类包含方法 initialize(), reset(), update(), 和 close(), 然后它们应该按照使用顺序列出。私有函数在类中也适用相同的规则。他们的顺序基于在类内部调用的顺序。

只包含类所需的成员;这一顺序不是要求实现所有方法的清单。持有资源的类,应通过 close() 或上下文管理器提供显式清理。

最小函数示例
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause


def normalize_weights(weights: list[float]) -> list[float]:
    """Scale non-negative weights to sum to one without modifying the input.

    Args:
        weights: Finite, non-negative weights with a positive total.

    Returns:
        Normalized weights in the input order.

    Raises:
        ValueError: If a weight is negative or the total is not positive.
    """
    total = sum(weights)
    if total <= 0 or any(weight < 0 for weight in weights):
        raise ValueError("Weights must be non-negative with a positive total.")
    return [weight / total for weight in weights]

循环导入#

循环导入发生在两个模块相互导入时,这是 Python 中常见的问题。您可以通过遵循在此 StackOverflow post 中概述的最佳实践来防止循环导入。

一般来说,避免循环导入是至关重要的,因为它们可能导致不可预测的行为。

然而,在我们的代码库中,我们在子包级别遇到了循环导入。这种情况是由于我们特定的代码结构所导致的。我们将类或函数及其相应的配置对象组织到不同的文件中。这种分离增强了代码的可读性和可维护性。然而,它可能导致循环导入,因为在许多配置对象中,我们使用属性 class_type 和 func 分别指定类或函数作为默认值。

为解决这一问题,采用两种互补方式:

  1. 可解析字符串 :将 class_type 和 func 存为基于 {DIR} 的字符串,例如 "{DIR}.sensor:Sensor" ,避免构建配置时导入实现模块。调用或访问需要实现的属性时,通过 ResolvableString 将字符串解析为实际类。触发解析前,先初始化所需运行时。

  2. TYPE_CHECKING 保护 :在 typing.TYPE_CHECKING 条件下导入实现类,使 IDE 和类型检查器可根据类型注解补全,而不触发运行时导入。

两种方式的完整示例见 可解析字符串 和 延迟加载与模块导出 。

配置操作#

配置类应保持声明式。跨配置默认值合并和运行时连接应放在启动准备或构造代码中,不应成为配置对象的方法。校验钩子可检查配置自身字段。改变所有权时使用显式声明,不要为保留旧路由而添加已弃用选择器的转换,或查找未选中的场景实体。

使用 instantiate() 构建配置所选择的实现:

from isaaclab.utils import clone, instantiate, replace, to_dict, update_from_dict, validate

robot_cfg = replace(ROBOT_CFG, prim_path="{ENV_REGEX_NS}/Robot")
other_cfg = clone(robot_cfg)
update_from_dict(other_cfg, {"init_state": {"pos": (1.0, 0.0, 0.0)}})
validate(other_cfg)
settings = to_dict(other_cfg)
robot = instantiate(robot_cfg)
action = instantiate(action_cfg, env)

instantiate 将配置作为构造函数第一个参数,后接额外参数。它不复制配置、不构建嵌套配置,也不缓存实例。资源共享仍由 SimulationContext.get_or_create_backend 负责。

clone 和 replace 返回新配置,保留显式标记为借用的字段; update_from_dict 原地更新现有配置; validate 检查必需字段,并运行嵌套的 validate_config 钩子。新代码优先使用这些函数。现有 cfg.copy() 、 cfg.replace(...) 、 cfg.validate() 、 cfg.to_dict() 、 cfg.from_dict(...) 和 cfg.class_type(cfg, ...) 调用仍受支持,未弃用。较长名称 class_to_dict 和 update_class_from_dict 也仍受支持。

延迟加载与模块导出#

公共包导出使用 延迟加载 ,使导入顶层包(例如 import isaaclab.sensors )不会立即引入 pxr 、 omni 或 scipy 等大型依赖。这很关键,因为配置类必须能在 SimulationApp 启动 之前 构建。

我们遵循 SPEC 1:子模块与函数的延迟加载 ,结合 .pyi 类型存根使用 lazy_loader 库(获 NumPy、SciPy、scikit-image、scikit-learn、NetworkX 推荐)。存根是 IDE 自动补全和运行时延迟加载的 唯一权威定义 。

公共导出模块的标准模式:

# mypackage/__init__.py
from isaaclab.utils.module import lazy_export

lazy_export()

旁边放置对应的类型存根:

# mypackage/__init__.pyi
__all__ = ["MyClass", "MyOtherClass", "my_function"]

from .my_module import MyClass, MyOtherClass
from .my_other_module import my_function

.pyi 存根的主要规则:

  • 顶部 __all__ 列表将名称标为公共重新导出项,符合 PEP 484 。

  • 来自同一子模块的导入放在一行;超过 100 字符时,用括号进行多行导入。

  • 本地子模块符号使用 相对导入 ( from .something import ... )。绝对通配导入( from pkg import * )只用于跨包回退,见下文。

  • 包含标准 Isaac Lab 许可文件头。

跨包回退 :对于重新导出其他包名称的模块,例如委托到 isaaclab.envs.mdp 的任务 MDP 模块,在 .pyi 存根中添加外部包的通配导入:

# isaaclab_tasks/.../mdp/__init__.pyi
__all__ = ["MyReward", "MyObservation"]

from .rewards import MyReward
from .observations import MyObservation

from isaaclab.envs.mdp import *

__init__.py 与标准模式相同,只调用无参数的 lazy_export() :

# isaaclab_tasks/.../mdp/__init__.py
from isaaclab.utils.module import lazy_export

lazy_export()

运行时, lazy_export 解析 .pyi 存根,并将绝对通配导入 from isaaclab.envs.mdp import * 作为回退:本地子模块中找不到的名称,会到指定包中查找。这也使类型检查器和 IDE 能完整识别重新导出的符号。

相对通配重新导出 :存根还可使用 from .submodule import * ,立即导出本地子模块的全部公共名称。这在导入时解析,并非延迟加载。仅 API 规模较大或频繁变化,并不足以成为立即导入的理由。

备注

相对通配重新导出绕过延迟加载,在包初始化时立即导入子模块的全部公共名称。一般不建议使用,除非确有必要。优先在存根中列出明确的具名导入,使公共 API 清晰、便于审阅,并保持延迟加载。

# isaaclab_tasks/.../mdp/__init__.pyi
from .rewards import *
from .observations import *

from isaaclab.envs.mdp import *

确保分发 .pyi 存根

在包的 pyproject.toml 中声明存根文件,将它们包含在分发包中:

[tool.setuptools.package-data]
"*" = ["*.pyi"]

提供延迟导出存根的包应保留此配置。pre-commit 的 insert-license 钩子已配置为自动给 .pyi 文件添加许可头( \.(pyi?|ya?ml)$ )。

可解析字符串#

配置字段需要引用依赖仿真器运行时的类或可调用对象时,存为 ResolvableString ,而非直接引用。这样可避免构建配置时立即导入 omni 、 pxr 等大型模块。调用或访问需要实现的属性时会触发解析,调用方必须先初始化所需运行时。解析本身不会等待 SimulationApp ,不依赖 Kit 的工作流也无需启动它。

可使用 {DIR} 简写或完整限定模块路径:

# Good — {DIR} shorthand (resolved to the current package at runtime)
class_type: type[Sensor] | str = "{DIR}.sensor:Sensor"

# Good — fully-qualified path (useful for cross-package references)
class_type: type[Sensor] | str = "isaaclab.sensors.my_sensor.sensor:Sensor"

# Bad — eagerly imports the implementation module
from .sensor import Sensor
class_type: type = Sensor

配置机制将 {DIR} 展开为定义该字段的类所在的包,例如 isaaclab.sensors.my_sensor ,而不导入被引用实现。同一包内的引用优先使用 {DIR} ,因为重命名和移动后仍能保持正确。

类型注解 type[Sensor] 所需类,应在 TYPE_CHECKING 条件下导入,使 IDE 仍能补全,而不触发运行时导入:

from __future__ import annotations
import typing

if typing.TYPE_CHECKING:
    from .sensor import Sensor

分开配置与实现文件#

具有配置类的组件,应将配置与运行时实现放在不同文件,使导入配置不会加载大型运行时依赖。这一模式并不要求为无状态函数引入类或配置对象:

my_sensor/
├── __init__.py          # lazy_export()
├── __init__.pyi         # re-exports: SensorCfg, Sensor
├── sensor_cfg.py        # pure data — no runtime deps
└── sensor.py            # implementation — may import omni, pxr, etc.

__init__.py :通过 lazy_export() 延迟加载存根中的名称:

# my_sensor/__init__.py
from isaaclab.utils.module import lazy_export

lazy_export()

__init__.pyi :声明供 IDE 补全和延迟加载使用的公共 API:

# my_sensor/__init__.pyi
__all__ = ["SensorCfg", "Sensor"]

from .sensor_cfg import SensorCfg
from .sensor import Sensor

sensor_cfg.py :纯数据,通过可解析字符串引用实现类,避免导入:

# my_sensor/sensor_cfg.py
from __future__ import annotations
import typing

from isaaclab.utils import configclass

if typing.TYPE_CHECKING:
    from .sensor import Sensor

@configclass
class SensorCfg:
    class_type: type[Sensor] | str = "{DIR}.sensor:Sensor"

sensor.py :实现部分,只在需要时导入运行时依赖:

# my_sensor/sensor.py
from pxr import Usd

from .sensor_cfg import SensorCfg

class Sensor:
    def __init__(self, cfg: SensorCfg, stage: Usd.Stage) -> None:
        self.cfg = cfg
        self.stage = stage

类型提示#

函数签名和类属性使用具体类型提示。文档字符串描述含义、单位、形状和约束,不重复已注解类型:

def add(a: int, b: int) -> int:
    """Add two integers.

    Args:
        a: The first operand.
        b: The second operand.

    Returns:
        The sum of the operands.
    """
    return a + b
  • 优先使用 list[str] 、 dict[str, int] 等内置集合类型。

  • 可选值使用 X | None 。

  • 类型需要仅运行时可用的导入时,使用 TYPE_CHECKING 和延迟注解。

  • 无返回值函数标注 -> None ,文档字符串省略不必要的 Returns: 章节。

警告与日志#

根据谁需要对消息采取行动,参照 Python 的 日志指南 选择机制:

情形

机械结构

调用者应修改代码或配置:使用弃用 API 或参数、设置被忽略或冲突,或使用方式有误。

warnings.warn(message, <Category>, stacklevel=...)

调用者无法通过修改代码避免的 运行时事件:使用了回退方案、可选依赖或功能不可用,或性能下降。

logger.warning(message),配合模块级 logger = logging.getLogger(__name__)

脚本和命令行工具面向用户的通知。

logger.warning(message),同上

库代码与入口的 进度和状态:解析后的任务配置、日志目录、环境与管理器摘要。

logger.info(message)

命令行工具 退出前报告的失败。

logger.error(message)

  • 始终为 warnings.warn 显式传入类别:

    • 调用者从自己的代码中使用已弃用 Python API 时,使用 DeprecationWarning。

    • 用户通过 Isaac Lab 入口使用已弃用配置值、预设或命令行选项时,使用 FutureWarning。即使从库代码发出,Python 也默认显示此类警告。

    • 使用方式有误或设置被忽略时,使用 UserWarning。

  • 设置 stacklevel,让警告指向调用者代码行,而不是 Isaac Lab 内部。

  • 不要使用 print 输出警告或状态,也不要添加 [WARNING]、[WARN]、[INFO] 或 [ERROR] 前缀。打印消息不受 --verbose / --info 和日志处理器控制,测试也无法可靠捕获。日志记录本身已携带级别。程序的实际输出仍适合使用 print,例如命令结果、--dry_run 命令行,或教程中逐步讲解的表格。

  • Isaac Lab 入口和 launch_simulation() 调用 isaaclab.app.logging_utils.configure_console_logging,将 isaaclab* 日志器的 INFO 记录以 [INFO]: <message> 格式输出到 stdout,警告输出到 stderr。新命令行入口应先调用它,使仿真运行时启动前记录的消息也能显示。

  • 测试中,warnings.warn 使用 pytest.warns 断言,logger.warning 使用 caplog。

import logging
import warnings

logger = logging.getLogger(__name__)


def set_gains(stiffness: float, damping: float | None = None, kd: float | None = None) -> None:
    if kd is not None:
        warnings.warn("'kd' is deprecated. Use 'damping' instead.", DeprecationWarning, stacklevel=2)
        damping = kd
    ...


def capture_graph() -> None:
    try:
        ...
    except RuntimeError as exc:
        logger.warning(f"CUDA graph capture failed; falling back to eager launches. Reason: {exc}")

编写代码文档#

文档应使调用方无需阅读实现,即可使用 API。

  • 先说明行为,再解释约束、默认值和相关失败情况。

  • 说明单位、坐标系、形状,以及输入和返回数据的归属或修改行为。

  • 在相关代码附近,用简短注释解释不明显的设计约束。

  • 小示例有助于说明用法时,可提供示例。避免重复签名或逐项叙述操作。

  • 使用平实语言和主动表达,删除重复及含糊限定词。

  • 行为变化时更新文档,并根据当前实现检查示例。

单元测试#

我们使用 pytest 进行单元测试。在最能体现行为的边界,为每个约定指定一个主要测试负责方,保持覆盖精简、快速。先查看该边界现有覆盖;如果能明确覆盖修改后的行为,优先扩展现有测试。

新增、修改、审阅或精简测试时,采用 test-audit 技能 中的编写门槛和保留标准。它维护判断测试收益是否值得其成本的详细条件。

  • 只有现有覆盖未检验的独立行为、回归、边界或失败情况,才添加测试。格式调整和机械性整理不会自动要求新测试。添加前,明确保护的约定、可能发生的回归,以及现有覆盖为何发现不了。不要只为支持测试而添加生产导出、标志、包装器或注入钩子,应检验实际边界。

  • 测试可观察行为和公共约定。避免依赖私有实现细节的断言,或通过重复生产算法计算期望值。避免无断言的冒烟测试、自我比较,以及由模拟对象或测试夹具直接提供测试声称要验证的行为。

  • 确认回归测试在没有修复时因预期原因失败,修复后通过。在错误所属边界覆盖它;在其他层或后端重复测试,需要有独立风险作为理由。

  • 参数矩阵和仿真夹具应聚焦不同执行路径。整合多余覆盖,不要添加重叠用例或无必要地重建同一场景。如果设备、形状和环境数等维度没有检验不同路径,避免构造它们的笛卡尔积。复用已验证该约定的夹具。

  • 移除或合并覆盖前,明确剩余的验证依据,并证明约定被破坏时它会失败;适用时,对负责该行为的生产实现进行针对性变异。保留独立边界情况,以及独立的 API、物理、后端一致性和打包约定。测试慢或断言看起来相似,本身不能证明重复。

  • 先运行范围最小的相关测试。如果缺少可选依赖,找到对应项目依赖组,用 uv run --extra <extra> python -m pytest ... 重试。以缩短测试时间为目标的修改,应在同一机器测量前后耗时,并将核函数编译与执行时间分开。

Linux 和 Windows 使用同样命令:

# Run a particular test
uv run python -m pytest source/isaaclab/test/utils/test_circular_buffer.py::test_reset

# Run all tests in a particular file
uv run python -m pytest source/isaaclab/test/utils/test_circular_buffer.py

# Run source-package tests through the repository test runner
uv run python tools/run_all_tests.py

# Run tooling tests under tools/
uv run isaaclab --test

这些命令在测试失败时均以非零状态退出,因此调用它们的 shell 或 CI 步骤也会失败。

工具#

我们使用以下工具来维护代码质量:

  • pre-commit: 在代码库上运行格式化工具和代码检查工具的列表。

  • ruff :一个极其快速的 Python 代码检查和格式化工具。

在 Linux 或 Windows 上,从 uv 管理的环境运行仓库格式化和 lint 检查:

uv run isaaclab --format

编辑期间,传入仓库相对文件路径,可限制按文件执行的钩子范围:

uv run isaaclab --format source/isaaclab/isaaclab/cli/commands/format.py

更新日志检查等仓库级钩子仍会运行。该命令只执行一次 pre-commit 并返回失败状态,钩子修改文件时也视为失败。应检查修改、解决失败后再重跑,不会自动重放所有钩子。提交前,对最终变更运行完整命令。