Skip to content

Fix equality constraints handling on collapse fixed joints#1233

Merged
eric-heiden merged 7 commits into
newton-physics:mainfrom
jvonmuralt:collapse_fixed_joints
Dec 16, 2025
Merged

Fix equality constraints handling on collapse fixed joints#1233
eric-heiden merged 7 commits into
newton-physics:mainfrom
jvonmuralt:collapse_fixed_joints

Conversation

@jvonmuralt

@jvonmuralt jvonmuralt commented Dec 10, 2025

Copy link
Copy Markdown
Member

Description

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

  • Bug Fixes

    • Improved handling during fixed-joint collapse so equality constraints remain correctly preserved and remapped, preventing loss or misreference of constraints after optimization.
  • Tests

    • Added a test validating that equality constraints (connect/weld/joint types) keep correct body and joint references through collapse and finalization.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 10, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

collapse_fixed_joints now tracks bodies referenced by equality constraints, conditionally skips certain merges, remaps constraint body/joint indices and transforms after merges, disables constraints referencing removed joints, and returns additional merge-state fields (body_merged_parent, body_merged_transform, merged_body_data) alongside existing remaps.

Changes

Cohort / File(s) Summary
Core builder changes
newton/_src/sim/builder.py
Enhanced collapse_fixed_joints: track bodies used by equality constraints; add skip logic for merges when a constrained child would merge into world; remap equality-constraint body1/body2 and joint1/joint2; transform anchor/relpose when bodies were merged (CONNECT/WELD handling); disable constraints referencing removed joints; return expanded merge-state (body_remap, joint_remap, body_merged_parent, body_merged_transform, merged_body_data).
Tests
newton/tests/test_equality_constraints.py
Added test_collapse_fixed_joints_with_equality_constraints to validate proper remapping of bodies/joints and preservation/update of equality constraints (CONNECT, joint, WELD) across collapse and finalization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Verify coordinate transforms applied to anchor and relpose for CONNECT vs WELD.
  • Confirm skip-merge condition correctly detects constrained children and intended parent (world vs dynamic).
  • Ensure all equality constraint fields (bodies, joints, anchors/poses) are consistently remapped; check handling when joints are removed.
  • Review both occurrences of remapping logic for equivalence and duplication risk.
  • Check callers of collapse_fixed_joints tolerate the expanded return payload.

Possibly related PRs

Suggested reviewers

  • adenzler-nvidia

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 title accurately describes the main change: fixing how equality constraints are handled when collapsing fixed joints, which is the core focus of the implementation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent 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 2a75acc and 3c8b46d.

📒 Files selected for processing (2)
  • newton/_src/sim/builder.py (3 hunks)
  • newton/tests/test_equality_constraints.py (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-18T15:56:26.587Z
Learnt from: adenzler-nvidia
Repo: newton-physics/newton PR: 552
File: newton/_src/solvers/mujoco/solver_mujoco.py:0-0
Timestamp: 2025-08-18T15:56:26.587Z
Learning: In Newton's MuJoCo solver, when transforming joint axes from Newton's internal frame to MuJoCo's expected frame, use wp.quat_rotate(joint_rot, axis) not wp.quat_rotate_inv(joint_rot, axis). The joint_rot represents rotation from joint-local to body frame, so forward rotation is correct.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-09-22T21:08:31.901Z
Learnt from: dylanturpin
Repo: newton-physics/newton PR: 806
File: newton/examples/ik/example_ik_franka.py:121-123
Timestamp: 2025-09-22T21:08:31.901Z
Learning: In the newton physics framework, when creating warp arrays for IK solver joint variables using wp.array(self.model.joint_q, shape=(1, coord_count)), the resulting array acts as a reference/pointer to the original model's joint coordinates, so updates from the IK solver automatically reflect in the model's joint_q buffer used for rendering, despite the general warp documentation suggesting copies are made by default.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-08-12T18:04:06.577Z
Learnt from: nvlukasz
Repo: newton-physics/newton PR: 519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (2)
newton/_src/viewer/viewer.py (1)
  • add (593-601)
newton/_src/sim/joints.py (2)
  • JointType (20-44)
  • EqType (82-97)
🔇 Additional comments (3)
newton/tests/test_equality_constraints.py (1)

210-354: New regression test accurately covers equality remapping and WELD semantics

This test nicely exercises collapse_fixed_joints with CONNECT, JOINT, and WELD constraints: it builds a minimal chain, collapses the fixed link1–link2 joint, then checks body/joint remaps plus the transformed WELD relpose (merge_xform * original_relpose) while keeping the WELD anchor unchanged in body2’s (link3’s) frame. The assertions match the implemented merge logic and EqType semantics, and the final model-count checks ensure no constraints are accidentally dropped.

newton/_src/sim/builder.py (2)

2967-2976: Skipping merges of constraint bodies into world is well-scoped

Using bodies_in_constraints together with last_dynamic_body == -1 to set should_skip_merge ensures you only prevent collapsing fixed joints when a child body that participates in any equality constraint would otherwise be merged into the world. Fixed joints downstream of a dynamic body still collapse, and the later remap logic adjusts those constraints. This is a minimal, targeted guard that avoids breaking world‑relative constraints without over-constraining other merges.


3205-3258: Equality-constraint remap & transform logic aligns with CONNECT/WELD/JOINT semantics

The new post-collapse remap correctly handles all cases:

  • Bodies:

    • Prefer body_remap when a body is retained, fall back to body_merged_parent when it was collapsed, and track body*_was_merged accordingly.
    • For CONNECT, you only transform anchor when body1 merged, which is correct since the anchor is defined in body1’s frame.
    • For WELD, you:
      • Update relpose with merge_xform * relpose when body1 merged, giving the new pose of body2 relative to the merged body1.
      • Update both anchor and relpose with merge_xform / inverse(merge_xform) when body2 merged, moving the anchor into the new body2 frame and re-expressing the relative pose, which matches the “anchor in body2, relpose = body2 relative to body1” convention and fixes the prior body1‑anchor bug from earlier revisions.
  • Joints:

    • joint_remap is applied to joint1/joint2, and any constraint referencing a removed joint is cleanly disabled with an optional verbose warning.

This matches what the new test asserts and resolves the previous WELD-anchor frame issue without introducing new inconsistencies.


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.

@codecov

codecov Bot commented Dec 10, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.90909% with 16 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
newton/_src/sim/builder.py 70.90% 16 Missing ⚠️

📢 Thoughts on this report? Let us know!

@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

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

3205-3258: Equality-constraint remap and frame transforms are consistent; consider adding coverage for anchors/relpose

The post-collapse remap of equality constraints looks logically sound:

  • Bodies are mapped either directly via body_remap or via body_merged_parent (including merges into world via -1), so no body index is left dangling.
  • For weld/connect constraints, the anchor and relpose updates implement the expected frame change:
    • If body1 is merged: anchor' = T1 * anchor, relpose' = T1 * relpose.
    • If body2 is merged (weld): relpose' = relpose * inv(T2).
    • Together this matches relpose' = T1 * relpose * T2^{-1} when both sides are merged.
  • Joint indices are remapped via joint_remap, and constraints referencing removed joints are safely disabled via equality_constraint_enabled.

Given the complexity, it would be worthwhile to extend tests to cover non-zero anchors and non-identity weld relpose (e.g., rotated/translated bodies) to validate that the transformed anchor/relpose behave as expected after collapsing.

newton/tests/test_equality_constraints.py (1)

210-332: Good coverage of index remap; you may want to also validate anchor/relpose transforms

This test nicely exercises collapse_fixed_joints with connect, joint, and weld equality constraints, and verifies that body/joint indices and counts are remapped as expected both on the builder and the finalized model.

To fully cover the new remap logic in collapse_fixed_joints, consider extending this test (or adding a sibling test) to:

  • Use a non-zero weld anchor and a non-identity relpose between link2 and link3, and
  • Assert that the post-collapse equality_constraint_anchor and equality_constraint_relpose match the analytically expected transforms after link2 is merged into link1.

That would catch any future regressions in the frame-change math, not just index remapping.

📜 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 d66ef03 and 2a75acc.

📒 Files selected for processing (2)
  • newton/_src/sim/builder.py (3 hunks)
  • newton/tests/test_equality_constraints.py (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: adenzler-nvidia
Repo: newton-physics/newton PR: 1107
File: newton/_src/solvers/mujoco/kernels.py:973-974
Timestamp: 2025-11-24T08:05:21.390Z
Learning: In Newton's MuJoCo solver integration (newton/_src/solvers/mujoco/), the mapping between Newton joints and MuJoCo joints is not 1-to-1. Instead, each Newton DOF maps to a distinct MuJoCo joint. This means that for multi-DOF joints (like D6 with 6 DOFs), there will be 6 corresponding MuJoCo joints, each with its own properties (margin, solimp, solref, etc.). The mapping is done via dof_to_mjc_joint array, ensuring each DOF's properties are written to its own MuJoCo joint without overwriting.
📚 Learning: 2025-09-22T21:03:39.624Z
Learnt from: dylanturpin
Repo: newton-physics/newton PR: 806
File: newton/_src/sim/ik/ik_lbfgs_optimizer.py:739-752
Timestamp: 2025-09-22T21:03:39.624Z
Learning: The L-BFGS optimizer in newton/_src/sim/ik/ik_lbfgs_optimizer.py currently intentionally only supports additive updates (assuming n_coords == n_dofs). Velocity space integration for joints with mismatched coordinate/DOF dimensions (like free/ball joints) is planned for future work and should not be flagged as an issue in current reviews.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-11-24T08:05:21.390Z
Learnt from: adenzler-nvidia
Repo: newton-physics/newton PR: 1107
File: newton/_src/solvers/mujoco/kernels.py:973-974
Timestamp: 2025-11-24T08:05:21.390Z
Learning: In Newton's MuJoCo solver integration (newton/_src/solvers/mujoco/), the mapping between Newton joints and MuJoCo joints is not 1-to-1. Instead, each Newton DOF maps to a distinct MuJoCo joint. This means that for multi-DOF joints (like D6 with 6 DOFs), there will be 6 corresponding MuJoCo joints, each with its own properties (margin, solimp, solref, etc.). The mapping is done via dof_to_mjc_joint array, ensuring each DOF's properties are written to its own MuJoCo joint without overwriting.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 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 (2)
newton/tests/test_equality_constraints.py (1)
newton/_src/sim/builder.py (7)
  • key (395-397)
  • add_equality_constraint_connect (2655-2684)
  • add_equality_constraint_joint (2686-2715)
  • add_equality_constraint_weld (2717-2752)
  • body_count (939-943)
  • joint_count (946-950)
  • collapse_fixed_joints (2887-3266)
newton/_src/sim/builder.py (1)
newton/_src/sim/joints.py (2)
  • JointType (20-44)
  • EqType (82-97)
⏰ 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 / Run GPU Benchmarks on AWS EC2
🔇 Additional comments (1)
newton/_src/sim/builder.py (1)

2967-2976: Skip-merge guard for equality-constrained bodies into world looks correct

The bodies_in_constraints set plus should_skip_merge = child_body in bodies_in_constraints and last_dynamic_body == -1 correctly prevents bodies referenced by equality constraints from being collapsed into the world, while still allowing them to be merged into other dynamic parents (which are then handled via the remap logic below). The fixed-joint skip path falls through to the existing “retain joint” branch, preserving the fixed joint as intended.

No functional issues spotted here.

Also applies to: 2991-3005

@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

🧹 Nitpick comments (2)
newton/tests/test_equality_constraints.py (1)

210-331: New collapse/equality test is well-structured; consider tightening assertions and removing prints

The scenario (fixed–revolute–fixed–revolute chain plus connect/joint/weld constraints) is a good minimal repro for the new collapse logic and validates remapping of both body and joint indices, as well as post-finalize counts.

Two optional refinements:

  • Add explicit checks that the three constraints remain enabled after collapse, e.g. self.assertTrue(builder.equality_constraint_enabled[eq_connect]) (similarly for eq_joint and eq_weld) to guard against future changes that might silently disable them while still keeping the count at 3.
  • Drop the print(...) at lines 331 and nearby or convert them to comments; they add noise to test output without affecting coverage.
newton/_src/sim/builder.py (1)

2967-3005: Equality-referenced bodies blocking world merges looks correct; optional tweak re disabled constraints

The bodies_in_constraints set and should_skip_merge condition:

bodies_in_constraints = set()
for i in range(len(self.equality_constraint_body1)):
    ...
should_skip_merge = child_body in bodies_in_constraints and last_dynamic_body == -1

correctly implement “don’t collapse a fixed joint into world when its child body is mentioned by any equality constraint”, matching the PR intent and the new test.

One optional consideration: currently even constraints with equality_constraint_enabled[i] == False still prevent collapse into world, since you don’t filter on the enabled flag when populating bodies_in_constraints. That’s a safe, conservative choice; if the intent is that disabled constraints should be completely ignored for topology decisions, you could restrict the set to enabled constraints only.

📜 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 d66ef03 and 2a75acc.

📒 Files selected for processing (2)
  • newton/_src/sim/builder.py (3 hunks)
  • newton/tests/test_equality_constraints.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: adenzler-nvidia
Repo: newton-physics/newton PR: 1107
File: newton/_src/solvers/mujoco/kernels.py:973-974
Timestamp: 2025-11-24T08:05:21.390Z
Learning: In Newton's MuJoCo solver integration (newton/_src/solvers/mujoco/), the mapping between Newton joints and MuJoCo joints is not 1-to-1. Instead, each Newton DOF maps to a distinct MuJoCo joint. This means that for multi-DOF joints (like D6 with 6 DOFs), there will be 6 corresponding MuJoCo joints, each with its own properties (margin, solimp, solref, etc.). The mapping is done via dof_to_mjc_joint array, ensuring each DOF's properties are written to its own MuJoCo joint without overwriting.
Learnt from: dylanturpin
Repo: newton-physics/newton PR: 806
File: newton/_src/sim/ik/ik_lbfgs_optimizer.py:739-752
Timestamp: 2025-09-22T21:03:39.624Z
Learning: The L-BFGS optimizer in newton/_src/sim/ik/ik_lbfgs_optimizer.py currently intentionally only supports additive updates (assuming n_coords == n_dofs). Velocity space integration for joints with mismatched coordinate/DOF dimensions (like free/ball joints) is planned for future work and should not be flagged as an issue in current reviews.
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 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 (2)
newton/tests/test_equality_constraints.py (1)
newton/_src/sim/builder.py (8)
  • key (395-397)
  • add_articulation (1034-1137)
  • add_equality_constraint_connect (2655-2684)
  • add_equality_constraint_joint (2686-2715)
  • add_equality_constraint_weld (2717-2752)
  • body_count (939-943)
  • collapse_fixed_joints (2887-3266)
  • finalize (5403-5908)
newton/_src/sim/builder.py (1)
newton/_src/sim/joints.py (2)
  • JointType (20-44)
  • EqType (82-97)
🔇 Additional comments (1)
newton/_src/sim/builder.py (1)

3205-3258: Weld equality anchor remapping appears to use the wrong body frame

For WELD constraints, the anchor is documented as relative to body2, but the current remapping logic transforms it when body1_was_merged and never when body2_was_merged. This is inconsistent with the documented semantics.

The issue: weld anchors will be misplaced whenever the anchored body (body2) is merged, while anchors are incorrectly modified when only body1 is merged.

For CONNECT constraints (where the anchor is on body1), the current logic is correct.

The proposed fix specializes anchor remapping per constraint type:

  • CONNECT: Transform anchor only when body1_was_merged
  • WELD: Transform anchor only when body2_was_merged, while updating relpose when either body is merged

@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 (2)
newton/tests/test_equality_constraints.py (1)

210-331: Good targeted regression test; consider also validating anchors/relpose.

The scenario and assertions around body/joint remapping after collapse_fixed_joints() look solid and exercise the new equality-constraint handling. To fully cover the new logic, you might also assert that the CONNECT/WELD anchors and the WELD relpose are transformed consistently with the expected merge transforms (especially for the link2link1 weld), since that code is subtle and easy to regress.

newton/_src/sim/builder.py (1)

2967-3005: Skip‑merge heuristic for constrained bodies looks reasonable; consider its scope.

Using bodies_in_constraints to prevent collapsing a constrained child body directly into world (last_dynamic_body == -1) makes sense and matches the goal of preserving bodies that appear explicitly in equality constraints. A couple of points to keep in mind:

  • This logic currently ignores equality_constraint_enabled; even disabled constraints will block world merges. That’s conservative but may be more restrictive than needed if users programmatically disable constraints before collapsing.
  • The heuristic is intentionally narrow: constrained bodies can still be merged into another dynamic body, relying on the post‑pass remap to keep constraints consistent. That matches the new tests, but it’d be good to double‑check that this is the intended behavior for all constraint types (CONNECT/WELD/JOINT) and that no callers rely on constrained bodies staying as separate links in those cases.

If that behavior is intentional, a short comment near should_skip_merge clarifying “only protect from merging into world; merges into other dynamics are remapped later” would make the design intent explicit.

📜 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 d66ef03 and 2a75acc.

📒 Files selected for processing (2)
  • newton/_src/sim/builder.py (3 hunks)
  • newton/tests/test_equality_constraints.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: adenzler-nvidia
Repo: newton-physics/newton PR: 1107
File: newton/_src/solvers/mujoco/kernels.py:973-974
Timestamp: 2025-11-24T08:05:21.390Z
Learning: In Newton's MuJoCo solver integration (newton/_src/solvers/mujoco/), the mapping between Newton joints and MuJoCo joints is not 1-to-1. Instead, each Newton DOF maps to a distinct MuJoCo joint. This means that for multi-DOF joints (like D6 with 6 DOFs), there will be 6 corresponding MuJoCo joints, each with its own properties (margin, solimp, solref, etc.). The mapping is done via dof_to_mjc_joint array, ensuring each DOF's properties are written to its own MuJoCo joint without overwriting.
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (1)
newton/_src/sim/joints.py (2)
  • JointType (20-44)
  • EqType (82-97)

Comment thread newton/_src/sim/builder.py
@jvonmuralt jvonmuralt changed the title Fix contraints on collapse fixed joints Fix constraints on collapse fixed joints Dec 10, 2025
@jvonmuralt jvonmuralt changed the title Fix constraints on collapse fixed joints Fix equality constraints handling on collapse fixed joints Dec 10, 2025
@jvonmuralt jvonmuralt marked this pull request as ready for review December 15, 2025 21:35

@eric-heiden eric-heiden 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.

Thanks!

@eric-heiden eric-heiden added this pull request to the merge queue Dec 16, 2025
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Dec 16, 2025
@eric-heiden eric-heiden added this pull request to the merge queue Dec 16, 2025
Merged via the queue into newton-physics:main with commit cb1b993 Dec 16, 2025
20 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 25, 2025
4 tasks
eric-heiden pushed a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
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.

Fix equality constraints handling when using ModelBuilder.collapse_fixed_joints()

2 participants