Collision code generation#

GRiD generates CUDA routines for self- and environment-collision checks from URDF collision geometry. This is collision checking, not a contact-dynamics simulator. The collision interface is generated CUDA C++, not a promise that every collision operation is exposed through the NumPy, JAX, or PyTorch handles.

Generate collision routines#

After the installation, run from the repository root with a URDF whose collision assets resolve on your machine:

grid-generate path/to/robot.urdf --collision --collision-res 0.10,0.05

This enables collision code generation and a coarse-to-fine covering-sphere representation. The generated grid_collision::config_free device routine checks a configuration against the configured self-collision pairs and obstacle environment. It is called from your CUDA code, with device model/environment data and the required scratch storage.

--collision-native selects a representation using native sphere/capsule rows where supported; remaining geometry uses covering spheres. It is a different approximation and must be labeled separately in a comparison.

The repository example examples/codegen/generate_collision.py shows the programmatic two-tier flow. See grid_codegen/cli.py for the current CLI and grid_codegen/collision/grid_collision_geometry.cuh for geometry types. The integration notes in grid_codegen/collision/HANDOFF.md describe scratch layouts and device calls; older module-name examples there predate the current grid-generate entry point.

Device API surfaces#

Two device entry points share the same baked sphere batch (NUM_COLLISION_SPHERES spheres, sphere_anchor / sphere_offset / sphere_radius in namespace grid_collision, _broad-suffixed twins for the broad tier of a two-tier model):

  • grid_collision::config_free<T>(s_q, d_robotModel, env, ...): the block-cooperative check from joint positions: it runs the batched sphere extractor, then checks self pairs and the environment thread-per-range / thread-per-sphere and returns one block-uniform verdict. With a two-tier model the broad tier runs first and the fine tier only for the links it flags; the verdict equals a fine-only check.

  • grid_collision::warp::config_free<T>(s_Xworld, env, w_scratch) and grid_collision::warp::collision_distance<T>(s_Xworld, env, s_dist, s_normal, w_scratch): the same verdict / per-sphere clearance for a caller that already holds the joint world transforms (column-major 4x4 per movable joint, e.g. from ee_pose_inner_warp), evaluated by one warp with lanes striding the spheres (w_scratch = grid_collision::warp::W_SCRATCH_FLOATS floats per warp). This is the entry point for multi-warp solvers that need a verdict inside one warp of a block (one IK candidate per warp); sphere positions are formed in float even for T = double. The two surfaces are gated against each other on random configurations and environments (test/cuda_equivalents/test_cuda_collision_warp.py).

Both checkers take two trailing optional arguments. margin (metres, default 0) makes the verdict a hit as soon as any raw clearance is below the margin: robot spheres are tested at r + margin against the environment and the margin is added to one side of every self pair, so exact contact at the margin is free and the default reproduces the zero-margin rule exactly. collision_distance and the cost derivatives are untouched by it; they keep returning the geometric signed clearance, and collision_cost applies its own margin. check_self (default true) set to false skips the self-pair ranges so the verdict reflects the environment alone; collision_distance is environment-only already. On a two-tier model the margin inflates both tiers, which keeps the broad-to-fine covering argument intact. The gate runs the warp and block paths against the distance surfaces at 0, 5 mm and 2 cm, with and without the self pairs, and sweeps each configuration’s own minimum clearance from just below to just above the threshold.

The obstacle set itself comes from plain arrays. grid_collision::make_host_environment takes one row per obstacle (spheres as x y z r, capsules as the two endpoints and a radius, cuboids as centre, unit quaternion w x y z and full dimensions, planes as a normal and offset), validates every row and returns a HostEnvironment whose view() feeds the host instantiations of the primitives. DeviceEnvironment copies those lists to the current device, records it, hands out the Environment kernels take by value, frees on destruction and can be moved but not copied; make_device_environment does both in one call. The gate compares the owned view with a hand upload of the same lists (identical verdicts and clearances), the host primitives with the device ones, and the free device memory before construction and after destruction (test/cuda_equivalents/test_cuda_collision_environment.py).

Geometry coverage and correctness#

  • Sphere spacing controls geometry resolution and work. Coarse-to-fine checks are intended to match the fine-only verdict for the same representation.

  • Covering geometry is an approximation of the original robot. Agreement with a fine-tier sphere check is not proof of exact mesh collision detection.

  • Missing or unusable meshes fail generation. A failed voxel fill may use an explicitly warned bounding-box cover, but geometry is never dropped to produce a partial model. Self-collision pair exclusions must also match your application.

  • Correctness tests live under test/cuda_equivalents/test_cuda_collision_*; they cover geometry, pairs, tiering, costs, and native representations.

The release measurements do not include collision timings. When benchmarking collisions, report latency alongside geometry coverage and fine-tier verdict agreement.

Sphere models and fidelity reports#

Generate an interchangeable sphere URDF without compiling CUDA:

python -m grid_codegen.spherize robot.urdf --out spheres.urdf --mesh-mode bounded-bulge --report
grid-generate robot.urdf --spherized-urdf spheres.urdf

grid-spherize is the installed entry point for the same module. The bounded-bulge mode uses one fixed mesh policy: 2.5 mm voxels and a 10 mm bulge budget relative to the voxel approximation. Coarse candidate centres fall back to the full lattice for thin geometry. Primitive geometry retains its analytic sphere cover. The input is the URDF’s collision geometry, not its visual meshes. Unresolved assets and incomplete fits are errors.

For package:// meshes, the importer checks paths relative to the URDF and explicit ROS_PACKAGE_PATH roots (either a package directory or its parent). It never downloads assets. Drake’s collision meshes for iiwa14 links 6 and 7 are bundled under grid_assets/robots/drake with their upstream licenses, revision and hashes; the bundled URDF resolves them without environment setup. The historical iiwa14 sphere asset omits those meshes and is not a complete replacement for the source collision model.

The complete-model CUDA gate checks the supplied-model CLI, every transformed sphere against CPU forward kinematics, and collision verdicts at 1, 32 and 100 threads (test_cuda_collision_complete.py).

To evaluate an existing model against the original collision geometry:

python -m grid_codegen.spherize robot.urdf --report spheres.urdf

The per-link report gives sphere count, voxel-estimated maximum/p99 bulge, and the uncovered fraction of 2,000 seeded mesh-surface samples. Bulge uses 400 samples per sphere. These are reproducible diagnostics, not a proof of continuous mesh coverage or a certified bulge bound. Primitive meshes used for this report are tessellated. Inspect the report alongside the model. --geometry visual measures against the <visual> shapes instead of the collision geometry, for comparison with fidelity tables published against visual meshes (on some robots the collision hulls sit well outside the visible links); the header line names the geometry source, and a visual mesh that does not resolve to a local file is named and its link skipped. Generation always reads the collision elements.

A sphere model built on another URDF of the same robot (foam’s Panda model places the finger joints differently from the kinematic URDF most consumers use) does not pass the frame check. --rebind-to re-attaches it instead of loosening the check:

python -m grid_codegen.spherize spheres.urdf --rebind-to kinematic.urdf --out rebound.urdf --report

Each sphere keeps its offset in its parent link’s frame and is attached to the target’s link of the same name, so its placement follows the target’s kinematics (the source’s world placements are not preserved). The result is validated strictly against the target and opens with a provenance comment: the source and target digests, the fidelity against the target’s collision geometry when its meshes resolve, and the content hash of the body that follows. A link that carries spheres in the source but is absent from the target is an error.

--spherized-urdf rejects missing collision links, non-sphere geometry, invalid spheres, and mismatched joint frames. The one tolerance is the base chain: a link with no movable ancestor may be absent from the sphere model or carry no collision element, with a warning naming it, because spheres on such a link never move and the generator drops them at bake time. It cannot prove that an arbitrary supplied sphere model covers its source mesh. The sole named lookup is foam; it requires an optional local external/foam/assets/panda/smaller_panda_spherized.urdf. GRiD does not download that asset or substitute another model when absent. Pass an explicit path to a model from your own checkout instead. No cuRobo or fairness-study assets are bundled by this mechanism.