Skip to content

Improve performance of ModelBuilder.add_builder method#772

Merged
eric-heiden merged 13 commits into
newton-physics:mainfrom
eric-heiden:improve-builder-perf
Sep 17, 2025
Merged

Improve performance of ModelBuilder.add_builder method#772
eric-heiden merged 13 commits into
newton-physics:mainfrom
eric-heiden:improve-builder-perf

Conversation

@eric-heiden

@eric-heiden eric-heiden commented Sep 16, 2025

Copy link
Copy Markdown
Member

Description

Improve performance of add_builder, replicate, and find_shape_contact_pairs methods in ModelBuilder.
Add unit tests for add_builder method.
Addresses #55.

Time Measurements

Environment (8192 envs) Function Before After
Allegro hand ModelBuilder.replicate 28.3 s 2.8 s
ModelBuilder.finalize 3.7 s 2.9 s
G1 humanoid ModelBuilder.replicate 47.6 s 5.1 s
ModelBuilder.finalize 4.2 s 5.1 s
Cartpole ModelBuilder.replicate 5.8 s 0.8 s
ModelBuilder.finalize 0.4 s 0.3 s

Newton Migration Guide

Please ensure the migration guide for warp.sim users is up-to-date with the changes made in this PR.

  • The migration guide in docs/migration.rst is up-to date

Before your PR is "Ready for review"

  • Necessary tests have been added and new examples are tested (see newton/tests/test_examples.py)
  • Documentation is up-to-date
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • Refactor

    • Collision-filter pairs now preserve insertion order and may include duplicates, changing collision-filtering behavior across imports and model composition.
    • Model composition and replication are more efficient—transform reuse and reduced allocations improve merge/replication performance and memory use.
  • Tests

    • Added test coverage validating composition of multiple builders and transform propagation.
  • Documentation

    • Example updated to reflect new collision-filter handling.

Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
@coderabbitai

coderabbitai Bot commented Sep 16, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Migrate ModelBuilder collision-filter storage from a set to a list; initialize static-body shapes entry (-1); refactor replicate/add_builder to reuse a single transform and reduce allocations; streamline transform/translation handling and joint/body merging; update importers and examples to append collision pairs; add tests for add_builder.

Changes

Cohort / File(s) Summary
Builder core: collision filters, transforms, replication, composition
newton/_src/sim/builder.py
shape_collision_filter_pairs changed from set()list[tuple[int,int]] (all .add().append()). body_shapes now initialized with {-1: []}. replicate/add_builder refactored to reuse a single wp.transform (mutate .p), use transform_mul buffers, consolidate start_* index remapping, and merge body/joint/shape data via direct appends and remapped indices. Joint previous-transform computation and shape-transform appends now use wp.transform(...)/transform_mul paths and direct index writes. find_shape_contact_pairs membership checks adjusted to avoid duplicates.
Importers: collision-filter updates
newton/_src/utils/import_mjcf.py, newton/_src/utils/import_urdf.py, newton/_src/utils/import_usd.py
Replace builder.shape_collision_filter_pairs.add((i,j))builder.shape_collision_filter_pairs.append((i,j)) at all insertion sites; semantics change from deduplicating set to ordered list (duplicates preserved).
Example alignment
newton/examples/robot/example_robot_allegro_hand.py
Switch accumulation of allegro_hand.shape_collision_filter_pairs from .add().append().
Tests: add_builder composition
newton/tests/test_model.py
Add test_add_builder to validate composition counts, groups, and transform propagation when composing builders (duplicate insertion present in patch).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Src as Sub-builder
  participant Dst as Main Builder
  participant X as Reused Transform
  participant F as FilterChecker

  rect rgb(240,248,255)
    note over Dst,X: add_builder / replicate (new flow)
    Dst->>X: allocate single identity wp.transform
    loop per copy
      Dst->>X: mutate X.p (translation offset)
      Src->>Dst: supply body/joint/shape arrays
      Dst->>Dst: append bodies/joints/shapes, compute start_* offsets
      Dst->>Dst: write body_q/body_qd/joint_X_p/joint_q at remapped indices
      Dst->>Dst: append shape transforms via transform_mul(X, Src.shape_transform[s])
      Dst->>Dst: append collision-filter pairs via .append((a,b))
    end
  end

  rect rgb(240,255,240)
    note over Dst,F: find_shape_contact_pairs (filter check)
    Dst->>F: iterate model.shape_collision_filter_pairs (list)
    loop each pair
      F-->>Dst: membership check / skip duplicate processing
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Kenny-Vilella
  • camevor
  • AntoineRichard

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title is a concise, single-sentence summary that clearly identifies the primary change—improving performance of ModelBuilder.add_builder—and is specific and readable for someone scanning project history. It accurately reflects the main intent of the changeset without extraneous detail.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Signed-off-by: Eric Heiden <eric-heiden@outlook.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
newton/_src/utils/import_mjcf.py (1)

291-295: Bug: incoming_xform applied twice for world geoms (link == -1).

tf = incoming_xform * tf is done at Lines 291–295 and again at Lines 310–312, double-shifting world-attached shapes. Fix by removing the second multiplication or guarding it to only apply for link != -1.

-            if incoming_xform is not None:
-                tf = incoming_xform * tf
+            # already applied for world geoms above; keep only for non-world links if ever used there
+            if incoming_xform is not None and link != -1:
+                tf = incoming_xform * tf

Also applies to: 310-312

🧹 Nitpick comments (12)
newton/_src/sim/builder.py (6)

364-364: List replaces set for collision filters: enforce canonical pairs to avoid bloat.

Now that this is a list, duplicates/non‑canonical ordering will accumulate. Please ensure all insertions use (min, max) ordering to keep find_shape_contact_pairs() set‑conversion cheap.


977-981: Type mixing in particle offset.

pos_offset = xform.p may be a Warp vec; adding it to np.array(builder.particle_q) can incur dtype coercions. Cast once.

Apply:

-                pos_offset = xform.p
+                pos_offset = np.asarray(xform.p, dtype=np.float32)

1060-1062: Use captured start index for clarity and robustness.

Using start_shape_idx avoids relying on current shape_count invariants.

-        shape_count_offset = self.shape_count
-        self.shape_collision_filter_pairs.extend(
-            [(i + shape_count_offset, j + shape_count_offset) for i, j in builder.shape_collision_filter_pairs]
-        )
+        self.shape_collision_filter_pairs.extend(
+            [(i + start_shape_idx, j + start_shape_idx) for i, j in builder.shape_collision_filter_pairs]
+        )

2416-2417: Canonicalize same‑body shape filter pairs.

Ensure (a, b) is ordered to match find_shape_contact_pairs() checks and prevent duplicates.

-                self.shape_collision_filter_pairs.append((same_body_shape, shape))
+                a, b = (same_body_shape, shape)
+                if a > b:
+                    a, b = b, a
+                self.shape_collision_filter_pairs.append((a, b))

2441-2442: Canonicalize parent–child filter pairs added from add_shape.

Same rationale as above.

-                        self.shape_collision_filter_pairs.append((parent_shape, shape))
+                        a, b = (parent_shape, shape)
+                        if a > b:
+                            a, b = b, a
+                        self.shape_collision_filter_pairs.append((a, b))

4431-4465: Set conversion on filters: good safeguard; consider pruning on insert.

Converting to a set at runtime avoids duplicates, but the list can still grow large. With canonicalization fixes above, consider deduping opportunistically if perf/memory becomes a concern.

newton/_src/utils/import_mjcf.py (2)

923-924: Filter pairs for visuals: canonicalize and skip self‑pairs.

Avoid (i, i) and enforce (min, max).

-            builder.shape_collision_filter_pairs.append((i, j))
+            if i != j:
+                a, b = (i, j) if i < j else (j, i)
+                builder.shape_collision_filter_pairs.append((a, b))

926-929: Self‑collision disable loop: canonicalize pair order.

Same rationale as above.

-                builder.shape_collision_filter_pairs.append((i, j))
+                a, b = (i, j) if i < j else (j, i)
+                builder.shape_collision_filter_pairs.append((a, b))
newton/examples/robot/example_robot_allegro_hand.py (1)

111-112: Canonicalize appended filter pairs.

Keep (min, max) ordering to match filtering semantics elsewhere.

-                    allegro_hand.shape_collision_filter_pairs.append((shape1, shape2))
+                    a, b = (shape1, shape2) if shape1 < shape2 else (shape2, shape1)
+                    allegro_hand.shape_collision_filter_pairs.append((a, b))
newton/_src/utils/import_usd.py (1)

1253-1271: Canonicalize and skip world–world pairs to avoid duplicates with list-backed filters

Now that shape_collision_filter_pairs is a list, appending unordered pairs introduces duplicates and unnecessary entries (especially (a,b) vs (b,a)). Also, per our previous learning, world-attached shapes (-1) already don’t collide; skip those pairs to keep the list compact.

Apply this diff in the three places where pairs are appended:

@@
-        builder.shape_collision_filter_pairs.append((shape1, shape2))
+        if not (builder.shape_body[shape1] == -1 and builder.shape_body[shape2] == -1):
+            a, b = sorted((shape1, shape2))
+            builder.shape_collision_filter_pairs.append((a, b))
@@
-            if other_shape_id != shape_id:
-                builder.shape_collision_filter_pairs.append((shape_id, other_shape_id))
+            if other_shape_id != shape_id:
+                if not (builder.shape_body[shape_id] == -1 and builder.shape_body[other_shape_id] == -1):
+                    a, b = sorted((shape_id, other_shape_id))
+                    builder.shape_collision_filter_pairs.append((a, b))
@@
-                        builder.shape_collision_filter_pairs.append((shape1, shape2))
+                        if not (builder.shape_body[shape1] == -1 and builder.shape_body[shape2] == -1):
+                            a, b = sorted((shape1, shape2))
+                            builder.shape_collision_filter_pairs.append((a, b))

(Background: collisions between body == -1 shapes are auto-skipped; no need to list them.)

newton/tests/test_model.py (1)

570-571: Use tolerant comparison for transforms to avoid float-equality flakiness

Direct equality on warp transforms can be brittle due to FP rounding. Use the helper already imported.

-        self.assertEqual(builder.body_q[0], offset_xform * orig_xform)
-        self.assertEqual(builder.body_q[1], offset_xform * orig_xform)
+        assert_np_equal(np.array(builder.body_q[0]), np.array(offset_xform * orig_xform), tol=1.0e-6)
+        assert_np_equal(np.array(builder.body_q[1]), np.array(offset_xform * orig_xform), tol=1.0e-6)
newton/_src/utils/import_urdf.py (1)

533-541: Canonicalize filter pairs (and guard i==j) to reduce duplicates with list-backed storage

Small cleanup to keep the list minimal.

-    for i in range(start_shape_count, end_shape_count):
-        for j in visual_shapes:
-            builder.shape_collision_filter_pairs.append((i, j))
+    for i in range(start_shape_count, end_shape_count):
+        for j in visual_shapes:
+            if i != j:
+                a, b = sorted((i, j))
+                builder.shape_collision_filter_pairs.append((a, b))
@@
-        for i in range(start_shape_count, end_shape_count):
-            for j in range(i + 1, end_shape_count):
-                builder.shape_collision_filter_pairs.append((i, j))
+        for i in range(start_shape_count, end_shape_count):
+            for j in range(i + 1, end_shape_count):
+                a, b = sorted((i, j))
+                builder.shape_collision_filter_pairs.append((a, b))
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1cb4a and 70ffba3.

📒 Files selected for processing (6)
  • newton/_src/sim/builder.py (12 hunks)
  • newton/_src/utils/import_mjcf.py (1 hunks)
  • newton/_src/utils/import_urdf.py (1 hunks)
  • newton/_src/utils/import_usd.py (1 hunks)
  • newton/examples/robot/example_robot_allegro_hand.py (1 hunks)
  • newton/tests/test_model.py (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/examples/robot/example_robot_allegro_hand.py
  • newton/_src/utils/import_mjcf.py
  • newton/_src/utils/import_usd.py
  • newton/_src/utils/import_urdf.py
  • newton/_src/sim/builder.py
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/_src/utils/import_usd.py
📚 Learning: 2025-08-20T03:30:16.037Z
Learnt from: eric-heiden
PR: newton-physics/newton#584
File: newton/_src/utils/import_urdf.py:159-167
Timestamp: 2025-08-20T03:30:16.037Z
Learning: In the URDF importer (newton/_src/utils/import_urdf.py), cylinders are intentionally created using add_shape_capsule() instead of add_shape_cylinder() because cylinder collisions are not fully supported yet. This is a deliberate workaround until proper cylinder collision support is implemented.

Applied to files:

  • newton/_src/utils/import_usd.py
  • newton/_src/utils/import_urdf.py
🧬 Code graph analysis (3)
newton/_src/utils/import_usd.py (1)
newton/_src/sim/builder.py (1)
  • shape_count (508-512)
newton/tests/test_model.py (3)
newton/_src/sim/builder.py (10)
  • ModelBuilder (67-4464)
  • add_body (1184-1246)
  • add_joint_revolute (1367-1449)
  • add_shape_sphere (2524-2555)
  • add_joint_free (1610-1661)
  • add_builder (900-1182)
  • body_count (515-519)
  • joint_count (522-526)
  • articulation_count (571-575)
  • shape_count (508-512)
newton/_src/sim/joints.py (1)
  • JointType (20-44)
newton/tests/unittest_utils.py (1)
  • assert_np_equal (239-245)
newton/_src/sim/builder.py (2)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
newton/_src/sim/state.py (2)
  • particle_count (94-98)
  • body_count (87-91)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
🔇 Additional comments (6)
newton/_src/sim/builder.py (6)

639-642: Reusing a single Transform instance across iterations: confirm no aliasing.

xform is mutated in-place then passed to add_builder(). Verify add_builder() never stores xform by reference; otherwise subsequent mutations could affect prior copies. If unsure, instantiate per-iteration.


968-971: Direct call into Warp core + from_buffer: verify ABI stability and perf.

wp.context.runtime.core.wp_builtin_mul_transformf_transformf is internal; from_buffer(np.empty(7)) allocates every call. Confirm against the Warp version targeted in CI and benchmark vs. plain a * b to justify the indirection.


1025-1041: Root transforms: looks correct; add a targeted test for FREE joints.

Good handling of FREE joint q and world-parent X_p. Please add/keep a unit test that asserts both joint_q and joint_X_p update with xform (position + quaternion).


1360-1364: LGTM: parent–child filter pairs now canonicalized and appended.


2228-2231: Collapse path: direct re-append of transforms looks fine.

Reusing stored q/qd is OK here.


2310-2311: Collapse path: preserving joint frames OK.

Keeping parent_xform/child_xform directly preserves original frames.

Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
newton/_src/sim/builder.py (4)

364-365: List-based collision-filter storage: OK; consider dedup before use.

Switching from a set to a list is fine for amortized appends. To keep memory bounded across many add_builder/replicate calls, dedup the list at a choke point (e.g., before assigning to the Model).

You can do this near finalize:

unique_pairs = sorted(set(self.shape_collision_filter_pairs))
m.shape_collision_filter_pairs = unique_pairs

639-646: Avoid item-assignment on wp.transform; set .p directly.

Index-based writes on wp.transform may not be supported and could fail on some Warp versions. Set the translation via .p (or construct a new transform).

-        xform = wp.transform_identity()
+        xform = wp.transform_identity()
         for i in range(num_copies):
             offset = offsets[i]
-            # assign translation
-            for j in range(3):
-                xform[j] = offset[j]
+            # assign translation
+            xform.p = wp.vec3(offset[0], offset[1], offset[2])
             self.add_builder(builder, xform=xform)

980-983: Normalize pos_offset to NumPy for vectorized add.

xform.p may be a Warp vec3; converting once avoids dtype surprises and object arrays during np math.

-            if xform is not None:
-                pos_offset = xform.p
+            if xform is not None:
+                pos_offset = np.array(xform.p, dtype=np.float32)
             else:
                 pos_offset = np.zeros(3)

1061-1063: Offsetting imported filter pairs: OK; consider canonicalization + dedup.

Pairs are appended with offsets. To prevent accidental duplicates from different code paths, canonicalize and dedup once (see earlier comment).

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70ffba3 and eef948f.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (12 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (2)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
newton/_src/sim/joints.py (1)
  • JointType (20-44)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (8)
newton/_src/sim/builder.py (8)

408-409: Static body bucket always present (-1): LGTM.

This removes repeated get(-1, []) boilerplate and simplifies callers.


1015-1017: Static shape transform offset: LGTM.

Correctly applies xform only to world-attached shapes; body-attached shapes remain in body frame.


1026-1041: Joint relocation under xform: LGTM; verify FREE joint quaternion ordering.

FREE joints’ q are updated and parent-frame X_p is offset when parent == -1. Please confirm builder.joint_q stores orientation as xyzw (the code assumes 7-tuple [px,py,pz,qx,qy,qz,qw]).


1050-1055: Body pose offset under xform: LGTM.

Applying xform to all body_q is the right global relocation; FREE joint fix-ups above keep consistency.


1361-1365: Parent/child collision filtering on joint add: LGTM.

Canonical ordering and append semantics match the new list storage.


2217-2221: Fixed-joint collapse: static-shape restoration and direct re-append: LGTM.

Keeping -1 bucket through clear/restore and reusing stored q/qd avoids extra allocations.

Also applies to: 2226-2228, 2307-2309


2414-2415: Same-body and parent-body collision filtering on shape add: LGTM.

New shapes are larger-index; pairs are naturally in canonical order.

Also applies to: 2439-2440


4429-4463: Building filter set for contact search: LGTM.

Converting the list to a set here provides O(1) lookups; matches the list-backed storage change.

Comment thread newton/_src/sim/builder.py
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
@eric-heiden eric-heiden marked this pull request as ready for review September 17, 2025 05:15
@eric-heiden eric-heiden requested a review from camevor September 17, 2025 05:17
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
newton/_src/sim/builder.py (1)

967-974: Unsafe from_buffer and reliance on private Warp core API.

Creating a transform via from_buffer over a temporary NumPy array risks lifetime/aliasing; also wp.context.runtime.core.* is private and brittle. Allocate a proper wp.transform and add a safe fallback to Python-level multiply.

Apply this diff:

-        transform_mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf
-
-        # dispatches two transform multiplies to the native implementation
-        def transform_mul(a, b):
-            out = wp.transform.from_buffer(np.empty(7, dtype=np.float32))
-            transform_mul_cfunc(a, b, ctypes.byref(out))
-            return out
+        # best-effort: use native cfunc if present; otherwise fall back to Python operator *
+        try:
+            _mul_tf_tf = wp.context.runtime.core.wp_builtin_mul_transformf_transformf  # type: ignore[attr-defined]
+        except Exception:
+            _mul_tf_tf = None
+
+        def transform_mul(a, b):
+            if _mul_tf_tf is not None:
+                out = wp.transform()  # owns its memory
+                try:
+                    _mul_tf_tf(a, b, ctypes.byref(out))
+                    return out
+                except Exception:
+                    pass
+            return a * b
🧹 Nitpick comments (1)
newton/_src/sim/builder.py (1)

978-983: Ensure NumPy addition with pos_offset uses an array.

xform.p is a Warp vec3; add explicit conversion to avoid dtype surprises.

-            if xform is not None:
-                pos_offset = xform.p
+            if xform is not None:
+                pos_offset = np.array(xform.p, dtype=np.float32)
             else:
                 pos_offset = np.zeros(3)
-            self.particle_q.extend((np.array(builder.particle_q) + pos_offset).tolist())
+            self.particle_q.extend((np.array(builder.particle_q, dtype=np.float32) + pos_offset).tolist())
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eef948f and 798f737.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (13 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (2)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
newton/_src/sim/joints.py (1)
  • JointType (20-44)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
🔇 Additional comments (12)
newton/_src/sim/builder.py (12)

364-365: Switching collision-filter storage to list looks fine.

Using a list simplifies append sites; you already convert to a set when needed. Keep an eye on duplicates/memory, but functionally LGTM.


408-409: Good default for static/global shapes.

Initializing body_shapes with {-1: []} removes conditional checks downstream. LGTM.


1014-1015: Correctly offsetting world-attached shape transforms.

Applying xform only for shapes with body == -1 is correct; local body-attached shapes already move with body_q. LGTM.


1025-1041: Joint offset handling looks right; minor sanity check.

Updating FREE joint q from the prior 7-tuple and premultiplying X_p when parent == -1 is correct. Please verify quaternion normalization stays intact after transform_mul.


1049-1054: Body pose offsetting LGTM.

Premultiplying body_q with xform only when provided is correct and avoids extra allocs.


1060-1062: Offsetting copied filter pairs is correct.

Order is preserved since a constant offset is added to both indices. LGTM.


1363-1364: Collision filter insertion uses canonical order.

Append with (a,b) after swap ensures stable lookup later. LGTM.


2216-2220: Static shape preservation across collapse is correct.

Saving/restoring body_shapes[-1] around clear() avoids KeyError and keeps globals intact. LGTM.


2306-2308: Rebuilding joint frames uses stored transforms directly.

Appending parent/child transforms from retained joints is fine. LGTM.


2412-2414: Same-body shape filter pairs OK.

Adding (same_body_shape, shape) prevents intra-body contacts; index monotonicity makes the pair canonical. LGTM.


2435-2439: Parent-child collision filtering OK.

Appending parent/child shape pairs when requested is consistent with the new list semantics. LGTM.


4428-4429: Set conversion for filters is appropriate.

Building a set once for membership checks amortizes the list choice. LGTM.

Comment thread newton/_src/sim/builder.py
Comment thread newton/_src/sim/builder.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
newton/_src/sim/builder.py (1)

4454-4462: Bug: mutating s1 corrupts subsequent iterations; also handle empty result array.

  • Don’t swap s1 in-place; use local a,b to keep outer-loop state intact.
  • When no pairs are found, np.array(contact_pairs) yields shape (0,), which breaks wp.vec2i construction.

Apply this diff:

-                # Ensure canonical order (smaller_element, larger_element)
-                if s1 > s2:
-                    s1, s2 = s2, s1
-
-                if (s1, s2) not in filters:
-                    contact_pairs.append((s1, s2))
+                # Ensure canonical order (smaller, larger) without mutating loop vars
+                a, b = (s1, s2) if s1 < s2 else (s2, s1)
+                if (a, b) not in filters:
+                    contact_pairs.append((a, b))
 
-        model.shape_contact_pairs = wp.array(np.array(contact_pairs), dtype=wp.vec2i, device=model.device)
+        if contact_pairs:
+            pairs_np = np.asarray(contact_pairs, dtype=np.int32).reshape(-1, 2)
+        else:
+            pairs_np = np.empty((0, 2), dtype=np.int32)
+        model.shape_contact_pairs = wp.array(pairs_np, dtype=wp.vec2i, device=model.device)
         model.shape_contact_pair_count = len(contact_pairs)
♻️ Duplicate comments (1)
newton/_src/sim/builder.py (1)

967-974: Stop using private Warp cfunc + from_buffer; allocate a proper transform with a safe fallback.

Accessing wp.context.runtime.core is private and out may alias temporary memory. Use wp.transform() and fall back to public operator overload if the cfunc isn’t available. This was flagged previously and remains unresolved.

Apply this diff:

-        # explicitly resolve the transform multiplication function to avoid
-        # repeatedly resolving builtin overloads during shape transformation
-        transform_mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf
-
-        # dispatches two transform multiplies to the native implementation
-        def transform_mul(a, b):
-            out = wp.transform.from_buffer(np.empty(7, dtype=np.float32))
-            transform_mul_cfunc(a, b, ctypes.byref(out))
-            return out
+        # Try to use the native transform mul; fall back to the public overload
+        try:
+            _mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf  # private; may not exist
+        except Exception:
+            _mul_cfunc = None
+
+        def transform_mul(a, b):
+            if _mul_cfunc is not None:
+                out = wp.transform()
+                _mul_cfunc(a, b, ctypes.byref(out))
+                return out
+            return a * b
🧹 Nitpick comments (2)
newton/_src/sim/builder.py (2)

1025-1040: Joint offsetting logic mostly fine; consider slice assign for FREE joints.

Minor micro-opt: replace the 7‑element loop with a slice assign to reduce Python overhead.

-                        qi += start_q
-                        for j in range(7):
-                            self.joint_q[qi + j] = tf[j]
+                        qi += start_q
+                        self.joint_q[qi : qi + 7] = list(tf)

2217-2220: Robustness: guard static_shapes retrieval when clearing.

If invariant breaks in future, this can KeyError. Cheap to harden.

-        static_shapes = self.body_shapes[-1]
+        static_shapes = self.body_shapes.get(-1, [])
         self.body_shapes.clear()
-        # restore static shapes
-        self.body_shapes[-1] = static_shapes
+        # restore static shapes
+        self.body_shapes[-1] = static_shapes
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eef948f and 798f737.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (13 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (1)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
🔇 Additional comments (11)
newton/_src/sim/builder.py (11)

364-365: API switch to list for collision filters looks good.

Using a list is fine; downstream code converts to a set when needed.


409-409: Initialize body_shapes with -1 entry — good default.

Aligns with the “global/world body = -1” convention and simplifies lookups.


975-983: Particle offset path reads xform.p — LGTM.

Vectorized add with NumPy is appropriate here.


1014-1016: Efficient root-shape transform application — LGTM.

Using transform_mul only when xform is provided avoids extra allocations.


1049-1054: Body pose offset via transform_mul — LGTM.


1060-1062: Filter-pair remap uses current shape_count as offset — verify this equals start_shape_idx.

Given the ordering here, self.shape_count still reflects pre-copy counts (len(self.shape_type)), so offset==start_shape_idx. Please confirm no earlier code appends to shape_type before this block.


1360-1364: Canonicalize parent/child filter pairs — LGTM.

Ensures stable lookup keys; good.


2307-2308: Re-appending parent/child joint transforms — LGTM.


2411-2415: Same-body collision filter insertion — LGTM.

Pairs are inserted in canonical order since existing indices < new shape.


2434-2439: Parent–child collision filtering — LGTM.

New child shape index ≥ existing parent shape indices, so pairs remain canonical.


4428-4429: Converting filters list to a set — LGTM.

Dedupes at query time; acceptable given low frequency.

Comment thread newton/_src/sim/builder.py
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
newton/_src/sim/builder.py (2)

639-645: Don’t mutate wp.transform via indexing; assign translation via .p.

Index assignment on Warp transforms is not supported. Set xform.p explicitly.

Apply this diff:

-        xform = wp.transform_identity()
+        xform = wp.transform()
         for i in range(num_copies):
             offset = offsets[i]
-            # assign translation
-            for j in range(3):
-                xform[j] = offset[j]
+            # assign translation
+            xform.p = wp.vec3(offset[0], offset[1], offset[2])
             self.add_builder(builder, xform=xform)

969-974: Unsafe from_buffer + private C symbol; use proper transform and add fallback.

Avoid from_buffer on a temp NumPy array and reliance on private runtime symbols without guard.

Apply this diff:

-        def transform_mul(a, b):
-            out = wp.transform.from_buffer(np.empty(7, dtype=np.float32))
-            transform_mul_cfunc(a, b, ctypes.byref(out))
-            return out
+        def transform_mul(a, b):
+            out = wp.transform()
+            try:
+                transform_mul_cfunc(a, b, ctypes.byref(out))
+                return out
+            except Exception:
+                # safe public fallback
+                return a * b
🧹 Nitpick comments (1)
newton/_src/sim/builder.py (1)

364-364: List-based collision filters: enforce invariants or dedup to bound growth.

Switching to list is fine for determinism, but duplicates can accumulate. Guarantee (a <= b) ordering at all append sites (looks mostly true) and consider dedup once (e.g., in finalize) if lists get large.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 798f737 and e3aa589.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (13 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (2)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
newton/_src/sim/joints.py (1)
  • JointType (20-44)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (12)
newton/_src/sim/builder.py (12)

408-408: Good default for static shapes bucket.

Initializing body_shapes with {-1: []} avoids KeyErrors and simplifies callers.


1013-1016: Applying xform only for world-attached shapes is correct.

Keeping body-attached shape transforms local avoids double-transforming.


1025-1039: Joint rebase logic: confirm constructor and indexing expectations.

Please confirm that wp.transform(*builder.joint_q[qi:qi+7]) is a supported constructor call and that start_q/start_X_p offsets align with joint_q_start/joint_X_p lengths for all joint types.


1049-1054: body_q transform application looks good.

Using transform_mul when xform is provided and raw extend otherwise is correct.


1359-1364: Canonical pair ordering on joint-based filters: LGTM.

This avoids (a,b)/(b,a) duplicates.


2216-2220: Static-shapes preservation across rebuild: LGTM.

Capturing and restoring body_shapes[-1] is the right fix.


2225-2227: Retaining body q/qd directly is fine.

Keeps transforms and velocities intact after collapse.


2306-2308: Re-append joint frames directly: LGTM.

Consistent with earlier stored transforms.


2411-2417: Same-body collision filters on add_shape: LGTM.

New shape index is always greater than existing, so canonical order holds.


2435-2440: Parent–child collision filters on add_shape: LGTM.

Parent shapes precede child shapes; order is canonical.


4428-4428: Converting filters to a set for lookup is appropriate.

This compensates for list storage and removes duplicates at query time.


4454-4458: Stable pair construction without mutating loop vars: LGTM.

Fixes the prior s1/s2 mutation issue.

Comment thread newton/_src/sim/builder.py
camevor
camevor previously approved these changes Sep 17, 2025

@camevor camevor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added one point, otherwise lgtm!

Comment thread newton/_src/sim/builder.py
Comment thread newton/_src/sim/builder.py Outdated
Comment thread newton/_src/sim/builder.py Outdated
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
newton/_src/sim/builder.py (5)

1059-1061: Fix validated: use start_shape_idx for filter-pair offset.

This resolves the over-shift bug when copying filter pairs.


4162-4163: Model expects a set — good cast at finalize.

Matches solver expectations noted in prior review.


4427-4463: Contact-pair generation is now set-aware and canonical; minor optional skip for world–world.

  • Using model.shape_collision_filter_pairs (set) is right.
  • Canonicalization avoids s1/s2 mutation bugs from earlier versions.
  • Optional: skip pairs where both shapes are world-attached (body == -1); collision is auto-skipped, so this trims work.

Apply:

         for i1 in range(len(sorted_indices)):
             s1 = sorted_indices[i1]
+            b1 = self.shape_body[s1]
             env1 = self.shape_group[s1]
             collision_group1 = self.shape_collision_group[s1]
             for i2 in range(i1 + 1, len(sorted_indices)):
                 s2 = sorted_indices[i2]
+                b2 = self.shape_body[s2]
                 env2 = self.shape_group[s2]
+                # skip world–world pairs (auto-ignored by collision)
+                if b1 == -1 and b2 == -1:
+                    continue

Also safe to place the world–world check after the env check to retain the early-break behavior.


639-646: Don’t mutate wp.transform via indexing; set translation via .p.

Index assignment on wp.transform isn’t supported; assign the position vector directly.

Apply:

-        xform = wp.transform_identity()
+        xform = wp.transform_identity()
         for i in range(num_copies):
             offset = offsets[i]
-            # assign translation
-            for j in range(3):
-                xform[j] = offset[j]
+            # assign translation safely
+            xform.p = wp.vec3(offset[0], offset[1], offset[2])
             self.add_builder(builder, xform=xform)

969-974: Unsafe transform buffer + private Warp API; allocate a real transform and add fallback.

Avoid from_buffer on a temp NumPy array and hard reliance on wp.context.runtime.core.*.

Apply:

-        transform_mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf
+        try:
+            transform_mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf
+        except AttributeError:
+            transform_mul_cfunc = None
@@
-        def transform_mul(a, b):
-            out = wp.transform.from_buffer(np.empty(7, dtype=np.float32))
-            transform_mul_cfunc(a, b, ctypes.byref(out))
-            return out
+        def transform_mul(a, b):
+            if transform_mul_cfunc is None:
+                return a * b
+            out = wp.transform()  # proper Warp-owned storage
+            try:
+                transform_mul_cfunc(a, b, ctypes.byref(out))
+                return out
+            except Exception:
+                return a * b
🧹 Nitpick comments (2)
newton/_src/sim/builder.py (2)

979-983: Ensure pos_offset is NumPy-compatible.

xform.p is a wp.vec3; make the dtype explicit before vectorized add.

Apply:

-                pos_offset = xform.p
+                pos_offset = np.array([xform.p[0], xform.p[1], xform.p[2]], dtype=np.float32)

940-949: Docs/migration note needed for users of Shape filter API change.

Public API now stores filter pairs as list in the builder; document the cast to set at finalize and that callers should not rely on set semantics pre-finalize.

I can draft a short migration snippet for docs/migration.rst if helpful.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3aa589 and 761285b.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (14 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (2)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
newton/_src/sim/joints.py (1)
  • JointType (20-44)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (14)
newton/_src/sim/builder.py (14)

408-409: Initialize body_shapes with -1 entry.

Good default to avoid KeyErrors for world/static shapes.


1014-1016: OK: shape transform offset uses fast mul.

Correctly applies xform only for world-attached shapes.


1025-1040: Free-joint pose and parent-frame offsets look correct.

Offsets joint_q for FREE and X_p for world-anchored parents only; indices adjusted via start_q/start_X_p.

Please confirm unit coverage hits:

  • FREE joint child under xform
  • Non-FREE with parent == -1

1359-1363: Canonical order for parent/child filter pairs.

Consistent tuple ordering; fine with list storage.


2412-2413: Same-body shape filters are canonical by construction.

New shape index is always larger; appending (older, newer) is correct and duplicate-safe once cast to set.


2434-2438: Parent–child collision filters: order and timing are correct.

Parent shapes exist before child; tuples are canonical and offset-free here.


1170-1179: Env-count logic LGTM.

Globals don’t bump num_envs; explicit environment ensures monotonic num_envs.


1090-1166: Bulk attribute extension is coherent.

Order aligns with earlier pre-extensions (shape_body/transform first, rest later).

Run tests that assert all per-shape arrays have equal length after add_builder.


2215-2219: Static-shape preservation during collapse is correct.

Saving/restoring body_shapes[-1] avoids losing world-attached shapes.


2305-2307: Reusing stored joint transforms is fine.

No re-expand/pack overhead; consistent with earlier storage.


955-964: Assigning current_env_group per add_builder instance is sound.

Group override semantics are clear; matches the docstring.


1063-1089: Group overrides for particles/bodies/shapes/joints/articulations are consistent.

Flat override is expected with add_builder(environment=...).


4122-4152: Shape arrays to Model: consistent dtypes; geo_source de-dup is good.

No concerns.


364-364: Switching collision filters to list is fine; cast to set at finalize.

No .add calls found — all mutations use append/extend; finalize casts to set (newton/_src/sim/builder.py:4162); model still stores a set (newton/_src/sim/model.py:140).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
newton/_src/sim/builder.py (1)

975-983: Potential type/broadcast bug: mixing wp.vec3 with NumPy array math.

xform.p is a wp.vec3; adding it to np.array(builder.particle_q) can yield dtype=object or raise. Convert to a NumPy vector and stack particle positions robustly.

Apply:

-            if xform is not None:
-                pos_offset = xform.p
-            else:
-                pos_offset = np.zeros(3)
-            self.particle_q.extend((np.array(builder.particle_q) + pos_offset).tolist())
+            if xform is not None:
+                pos_offset = np.array([xform.p[0], xform.p[1], xform.p[2]], dtype=np.float32)
+                # robust stacking regardless of wp.vec3 or lists/tuples
+                pos_np = np.array([[p[0], p[1], p[2]] for p in builder.particle_q], dtype=np.float32)
+                self.particle_q.extend((pos_np + pos_offset).tolist())
+            else:
+                # fast path: no offset
+                self.particle_q.extend(builder.particle_q)
♻️ Duplicate comments (2)
newton/_src/sim/builder.py (2)

639-646: Don’t mutate wp.transform via indexing; set translation via .p.

Index-based mutation on wp.transform is brittle and was previously flagged. Assign the position once via xform.p.

Apply:

-        xform = wp.transform_identity()
+        xform = wp.transform_identity()
         for i in range(num_copies):
             offset = offsets[i]
-            # assign translation
-            for j in range(3):
-                xform[j] = offset[j]
+            # assign translation
+            xform.p = wp.vec3(offset[0], offset[1], offset[2])
             self.add_builder(builder, xform=xform)

967-974: Unsafe from_buffer + private API usage; allocate a real transform and add a safe fallback.

Creating a transform from a temporary NumPy buffer risks lifetime issues, and accessing wp.context.runtime.core is private/fragile. Use a proper wp.transform for the out param and fall back to Python-level multiply if the cfunc is unavailable.

Apply:

-        transform_mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf
+        # Try to resolve native mul; fall back to Python operator if unavailable
+        transform_mul_cfunc = getattr(getattr(getattr(wp.context.runtime, "core", object()),
+                                              "wp_builtin_mul_transformf_transformf", None),
+                                      "__call__", None)

         # dispatches two transform multiplies to the native implementation
         def transform_mul(a, b):
-            out = wp.transform.from_buffer(np.empty(7, dtype=np.float32))
-            transform_mul_cfunc(a, b, ctypes.byref(out))
-            return out
+            # allocate a proper Warp transform; C kernel will fill it
+            out = wp.transform()
+            if transform_mul_cfunc is not None:
+                try:
+                    transform_mul_cfunc(a, b, ctypes.byref(out))
+                    return out
+                except Exception:
+                    pass
+            # safe fallback using public operator
+            return a * b
🧹 Nitpick comments (5)
newton/_src/sim/builder.py (5)

364-364: LGTM: list-backed collision-filter store with set materialization at finalize.

Storing as a list here and converting to a set in finalize is fine. Keep an eye on growth if many duplicates are appended in hot import paths.

If duplicates become an issue, we can keep a small shadow set for O(1) membership checks at append sites while still storing the list for order/debuggability.


1025-1040: FREE-joint rebase logic looks correct; minor robustness nit.

The loop writes 7 scalars via tf[j]. If Warp ever changes transform indexing, consider slicing with .p/.q to be explicit.

Would you like a follow-up patch to assign self.joint_q[qi:qi+3] = tf.p and self.joint_q[qi+3:qi+7] = tf.q?


2411-2415: Skip adding filter pairs for world (-1) shapes; canonicalize order.

Per our learnings, collisions between shapes with body index -1 are already skipped by the collision system; adding pairs here bloats the list. Also make the order explicit.

Apply:

         if body in self.body_shapes:
             # no contacts between shapes of the same body
-            for same_body_shape in self.body_shapes[body]:
-                self.shape_collision_filter_pairs.append((same_body_shape, shape))
+            if body != -1:
+                for same_body_shape in self.body_shapes[body]:
+                    a, b = (same_body_shape, shape) if same_body_shape < shape else (shape, same_body_shape)
+                    self.shape_collision_filter_pairs.append((a, b))
             self.body_shapes[body].append(shape)
         else:
             self.body_shapes[body] = [shape]

Context: “collisions between shapes with body index -1 ... are automatically skipped,” so we can avoid emitting those pairs. [Used retrieved learnings]


2434-2438: Canonicalize parent/child shape filter pairs for consistency.

Guarantees (min, max) ordering like other sites.

Apply:

-                    for parent_shape in self.body_shapes[parent_body]:
-                        self.shape_collision_filter_pairs.append((parent_shape, shape))
+                    for parent_shape in self.body_shapes[parent_body]:
+                        a, b = (parent_shape, shape) if parent_shape < shape else (shape, parent_shape)
+                        self.shape_collision_filter_pairs.append((a, b))

4427-4460: LGTM: contact pair generation with env and group gating; stable ordering fixed.

The canonical pair formation without mutating loop vars is correct. Minor doc nit: the comment says “Copy the set …” but we only alias it.

Change the comment to “Alias the set of filtered-out shape pairs …” or actually copy via filters = set(model.shape_collision_filter_pairs) if you plan to mutate.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3aa589 and 761285b.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (14 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (10)
newton/_src/sim/builder.py (10)

409-409: LGTM: initialize body_shapes with static (-1) bucket.

This avoids repeated get(-1, []) patterns and simplifies merges.


1014-1016: LGTM: apply xform to world-attached shape transforms on import.

Using transform_mul here maintains correctness for root shapes.


1049-1054: LGTM: body_q offset via transform_mul.

This is the right spot to apply the root transform to bodies.


1059-1061: LGTM: correct offset for copied collision-filter pairs (uses start_shape_idx).

This fixes the prior over-shift bug.


2215-2219: LGTM: preserve and restore static (-1) shapes across collapse.

Keeps the static bucket intact while rebuilding.


2224-2225: LGTM: carry over body_q/body_qd verbatim during rebuild.

Matches the retained bodies without extra conversions.


2305-2306: LGTM: store parent/child joint frames directly from retained data.

Keeps transforms consistent with the merged topology.


4162-4162: LGTM: finalize converts list→set for filters (aligns with Model expectations).

Matches downstream consumers that rely on set semantics.


1362-1362: LGTM: canonicalize joint parent/child filter pair and append.

Matches the new list-based store and avoids duplicate (a,b)/(b,a).


965-974: Audit private wp.context.runtime.core usage

Findings: direct uses located at newton/_src/sim/builder.py:967, newton/_src/sim/graph_coloring.py:233,241, and tests newton/tests/test_coloring.py:142,155,170,176,190,196.

Action: replace direct wp.context.runtime.core calls with public Warp APIs or centralize them behind a single compatibility shim with runtime feature-detection and documentation.

Signed-off-by: Eric Heiden <eric-heiden@outlook.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
newton/_src/sim/builder.py (2)

639-643: Don’t mutate Warp transforms via slicing; assign translation through .p.

Index assignment on wp.transform is unreliable. Set the position explicitly while reusing the same object.

Apply:

-        xform = wp.transform_identity()
+        xform = wp.transform()
         for i in range(num_copies):
-            xform[:3] = offsets[i]
+            xform.p = wp.vec3(offsets[i][0], offsets[i][1], offsets[i][2])
             self.add_builder(builder, xform=xform)

964-971: Critical: from_buffer() out-param risks dangling memory; avoid private cfunc hard dependency.

Creating a transform from a temporary NumPy buffer can leave out pointing at freed memory; also wp.context.runtime.core is private and may change.

Apply:

-        transform_mul_cfunc = wp.context.runtime.core.wp_builtin_mul_transformf_transformf
+        transform_mul_cfunc = getattr(getattr(wp.context.runtime, "core", None), "wp_builtin_mul_transformf_transformf", None)
...
-        def transform_mul(a, b):
-            out = wp.transform.from_buffer(np.empty(7, dtype=np.float32))
-            transform_mul_cfunc(a, b, ctypes.byref(out))
-            return out
+        def transform_mul(a, b):
+            if transform_mul_cfunc:
+                out = wp.transform()  # proper Warp-owned storage
+                transform_mul_cfunc(a, b, ctypes.byref(out))
+                return out
+            # safe public fallback
+            return a * b
🧹 Nitpick comments (3)
newton/_src/sim/builder.py (3)

972-980: Normalize xform input before use.

If callers pass a tuple/list instead of a Warp transform, xform.p will fail.

Apply:

-            self.particle_max_velocity = builder.particle_max_velocity
-            if xform is not None:
-                pos_offset = xform.p
+            self.particle_max_velocity = builder.particle_max_velocity
+            if xform is not None:
+                if not hasattr(xform, "p"):
+                    xform = wp.transform(*xform)
+                pos_offset = xform.p

2405-2411: Skip generating same-body filter pairs for world (-1) to reduce pair count.

Per your prior guidance, world-attached shapes are auto-excluded; explicit (-1, -1) pairs are redundant and can bloat the list for large scenes.
[retrieved_learnings used]

Apply:

-        if body in self.body_shapes:
+        if body in self.body_shapes:
             # no contacts between shapes of the same body
-            for same_body_shape in self.body_shapes[body]:
-                self.shape_collision_filter_pairs.append((same_body_shape, shape))
+            if body != -1:
+                for same_body_shape in self.body_shapes[body]:
+                    self.shape_collision_filter_pairs.append((same_body_shape, shape))

4423-4455: Edge case: ensure empty contact pair array has shape (0,2).

When no pairs exist, np.array([]) yields shape (0,), which can trip Warp dtype/shape checks.

Apply:

-        model.shape_contact_pairs = wp.array(np.array(contact_pairs), dtype=wp.vec2i, device=model.device)
+        if contact_pairs:
+            arr = np.asarray(contact_pairs, dtype=np.int32)
+        else:
+            arr = np.empty((0, 2), dtype=np.int32)
+        model.shape_contact_pairs = wp.array(arr, dtype=wp.vec2i, device=model.device)
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 761285b and d746154.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py (14 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
PR: newton-physics/newton#587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (3)
newton/_src/sim/style3d/builder_style3d.py (1)
  • add_builder (110-140)
newton/_src/sim/state.py (2)
  • particle_count (94-98)
  • body_count (87-91)
newton/_src/sim/joints.py (1)
  • JointType (20-44)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (7)
newton/_src/sim/builder.py (7)

364-364: Builder stores list, Model converts to set: good choice.

List storage reduces builder‑time overhead; set conversion happens in finalize. No changes requested.


408-409: Always-initialized static (-1) body bucket is sensible.

This prevents KeyErrors and makes downstream logic simpler.


1003-1014: Root-shape transform application looks correct.

Transforming only world-attached shapes when xform is provided is the right behavior.


1045-1050: Body pose offset on add_builder is correct.

Multiplying xform * body_q for incoming bodies preserves orientation and translation as intended.


1054-1058: Correct offset for copied collision-filter pairs.

Using start_shape_idx avoids over-shifting. Thanks for fixing this.


2211-2215: Static shapes preserved across collapse: good.

Saving and restoring self.body_shapes[-1] after clearing avoids losing world-attached shapes.


4158-4160: Model now holds a set of filter pairs: correct type for fast membership.

Matches usage in find_shape_contact_pairs.

Comment thread newton/_src/sim/builder.py
@eric-heiden eric-heiden merged commit 8a14e45 into newton-physics:main Sep 17, 2025
13 of 15 checks passed
eric-heiden added a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
…ics#772)

Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jan 28, 2026
4 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Feb 6, 2026
4 tasks
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
…ics#772)

Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants