Skip to content

Add parsing fixed tendons from MJCF#1406

Merged
gyeomannvidia merged 53 commits into
newton-physics:mainfrom
gyeomannvidia:dev/fixedTendons/replication
Jan 22, 2026
Merged

Add parsing fixed tendons from MJCF#1406
gyeomannvidia merged 53 commits into
newton-physics:mainfrom
gyeomannvidia:dev/fixedTendons/replication

Conversation

@gyeomannvidia

@gyeomannvidia gyeomannvidia commented Jan 19, 2026

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"

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

Summary by CodeRabbit

  • New Features

    • Full fixed-tendon support: per-tendon and per-joint properties, multi-world replication, and runtime tendon property updates.
  • Importer

    • MJCF importer now parses fixed-tendon sections, joint associations, tendon metadata, and computes/handles springlength defaults.
  • Tests

    • Added comprehensive tests for parsing, conversion, length/limit behavior, and runtime property propagation.

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

gyeomannvidia and others added 21 commits January 14, 2026 11:36
…ain and Miguel

Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
…er to follow.

Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
…mmit run -a'

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
…or runtime updates of fixed tendon properties. Enhance tests to validate conversion and updates across multiple worlds.

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
…ication

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
…in MJCF import. Ensure expected coefficients match parsed values in wrap_prm.

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
…nsure expected values match those calculated by MuJoCo based on the mass matrix and tendon geometry.

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds fixed-tendon support: new SolverNotifyFlags.TENDON_PROPERTIES, MJCF tendon parsing, MuJoCo tendon initialization with per-world mapping, a Warp kernel to propagate tendon properties at runtime, and unit tests for parsing, conversion, and runtime updates.

Changes

Cohort / File(s) Summary
Notification Flag
newton/_src/solvers/flags.py
Added TENDON_PROPERTIES = 1 << 7 to SolverNotifyFlags and included it in the ALL mask.
Warp Kernel
newton/_src/solvers/mujoco/kernels.py
Added update_tendon_properties_kernel to copy Newton tendon attributes into MuJoCo arrays; note: the kernel is defined twice (duplicate identical definitions).
MuJoCo Solver (core)
newton/_src/solvers/mujoco/solver_mujoco.py
Added mjc_tendon_to_newton_tendon field, _validate_tendon_attributes(), _init_tendons(), and update_tendon_properties(). Wired tendon init/replication into conversion/expand/post-conversion flows and made notify_model_changed() respond to TENDON_PROPERTIES. Expanded model-field replication for tendon fields.
MJCF Importer
newton/_src/utils/import_mjcf.py
Added fixed-<tendon> parsing: collects per-tendon and per-joint attributes, validates joints, registers tendon-joint associations, and emits tendon metadata into builder custom attributes.
Tests — fixed tendon behavior
newton/tests/test_fixed_tendon.py
New TestMujocoFixedTendon suite testing tendon length behavior and limit enforcement across initial conditions and breach scenarios.
Tests — MJCF parsing
newton/tests/test_import_mjcf.py
Added tests for fixed-tendon MJCF parsing, defaults, limit parsing, and automatic spring-length computation.
Tests — solver integration
newton/tests/test_mujoco_solver.py
Added test validating multi-world tendon conversion and runtime updates via notify_model_changed(TENDON_PROPERTIES).

Sequence Diagram(s)

sequenceDiagram
    participant MJCF as MJCF Import
    participant Model as Newton Model
    participant Solver as SolverMuJoCo
    participant Kernel as Warp Kernel
    participant MJC as MuJoCo Runtime

    MJCF->>Model: Parse fixed-tendon sections & attributes
    Model->>Solver: Provide tendon metadata (world, adr, num, joint mappings)
    Solver->>Solver: _validate_tendon_attributes()
    Solver->>Solver: _init_tendons() -> create Newton tendons and per-world replication
    Solver->>Solver: Build mjc_tendon_to_newton_tendon mapping

    Note over Model,Solver: Runtime property update path
    Model->>Solver: notify_model_changed(TENDON_PROPERTIES)
    Solver->>Solver: update_tendon_properties()
    Solver->>Kernel: launch update_tendon_properties_kernel(inputs...)
    Kernel->>MJC: write updated tendon arrays (stiffness, damping, frictionloss, range, margin, solref/solimp, armature, actfrcrange)
    MJC->>MJC: subsequent simulation steps use updated tendon properties
Loading
sequenceDiagram
    participant Build as Model Build
    participant Conv as MJCF->MJC Conversion
    participant Init as Tendon Initialization
    participant Map as Mapping Array Creation

    Build->>Conv: Start conversion
    Conv->>Init: _init_tendons(model, spec, joint_mapping, template_world)
    Init->>Init: validate lengths, parse per-tendon & per-joint attrs
    Init->>Init: create MuJoCo tendon entries and per-world replication
    Init->>Map: return tendon indices
    Map->>Conv: populate mjc_tendon_to_newton_tendon[world, tendon_id]
    Conv->>Build: conversion complete with tendon mapping
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • eric-heiden
  • adenzler-nvidia
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly summarizes the main feature addition: parsing fixed tendons from MJCF into Newton, which aligns with the core changes across multiple files implementing this functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

@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

🤖 Fix all issues with AI agents
In `@newton/_src/solvers/mujoco/solver_mujoco.py`:
- Around line 944-945: The condition guarding assignment to t.armature is wrong:
it checks tendon_margin instead of tendon_armature; update the conditional so
that the assignment t.armature = float(tendon_armature.numpy()[i]) only runs
when tendon_armature is not None (i.e., change the if from `if tendon_margin is
not None:` to `if tendon_armature is not None:`) to ensure you reference the
correct tensor before indexing.

In `@newton/tests/test_fixed_tendon.py`:
- Around line 105-110: The assertion message in test_fixed_tendon.py incorrectly
says "Expected stiffness value" while the test checks tendon length; update the
assertAlmostEqual call's msg to reference tendon length (use
expected_tendon_length and measured_tendon_length) so the message correctly
states something like "Expected tendon length: {expected_tendon_length},
Measured value: {measured_tendon_length}" in the existing assertAlmostEqual
invocation that compares expected_tendon_length and measured_tendon_length.
🧹 Nitpick comments (4)
newton/_src/utils/import_mjcf.py (1)

1335-1338: Minor: Missing blank line before the next section comment.

For consistency with the rest of the file (e.g., line 1254-1255 has a blank line before # parse contact pairs), add a blank line after the tendon parsing block.

📝 Suggested fix
                 if verbose:
                     joint_names_str = ", ".join(f"{builder.joint_key[j]}*{c}" for j, c in joint_entries)
                     print(f"Parsed fixed tendon: {tendon_name} ({joint_names_str})")
+
     # -----------------
     # parse actuators
newton/_src/solvers/flags.py (1)

51-52: Consider expanding the docstring for consistency with other flags.

Other flag docstrings enumerate the specific properties (e.g., line 34 lists "joint_target_ke, joint_target_kd, joint_effort_limit..."). The current docstring "eg stiffness" is less precise.

📝 Suggested improvement
     FIXED_TENDON_PROPERTIES = 1 << 7
-    """Indicates fixed tendon properties: eg stiffness."""
+    """Indicates fixed tendon property updates: tendon_stiffness, tendon_damping, tendon_frictionloss, tendon_range, tendon_margin, tendon_solref, tendon_solimp, tendon_armature, tendon_actfrcrange."""
newton/tests/test_import_mjcf.py (1)

994-1007: Potential brittleness: hard‑coded tendon_invweight0 values.
Line 994‑1007 compares exact MuJoCo‑computed values; these can vary across MuJoCo versions or platforms. Please verify stability against the supported MuJoCo build, and consider loosening or guarding this check if it proves version‑sensitive.

newton/tests/test_fixed_tendon.py (1)

177-177: Remove unused variable.

start_tendon_length is computed but never used. As per static analysis hint, this should be removed or used for additional assertions if intended.

♻️ Proposed fix
-        start_tendon_length = joint_start_positions[0] * coeff0 + joint_start_positions[1] * coeff1
-
         builder = newton.ModelBuilder()

Comment thread newton/_src/solvers/mujoco/solver_mujoco.py Outdated
Comment thread newton/tests/test_fixed_tendon.py
…ry crashes when there are 0 tendons and removing them

Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.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

🤖 Fix all issues with AI agents
In `@newton/tests/test_fixed_tendon.py`:
- Around line 92-94: The comment above joint_start_positions is misleading:
replace the incorrect exponent notation and math with a clear linear expression
using multiplication (pos*coef) and the correct arithmetic result; e.g., state
that tendon length = pos*coef0 + pos1*coef1 = 0.5*1 + 0.0*1 = 0.5 and that the
target length is 0.0 (per MJCF), ensuring the comment references
joint_start_positions and the two coefficients consistently.
🧹 Nitpick comments (1)
newton/tests/test_fixed_tendon.py (1)

177-177: Remove unused variable.

start_tendon_length is computed but never used. Either remove it or use it in an assertion/logging for clarity.

♻️ Proposed fix
-        start_tendon_length = joint_start_positions[0] * coeff0 + joint_start_positions[1] * coeff1
-
         builder = newton.ModelBuilder()

Comment thread newton/tests/test_fixed_tendon.py Outdated
…have identical behaviour.

Signed-off-by: Gordon Yeoman <gyeoman@nvidia.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

🤖 Fix all issues with AI agents
In `@newton/tests/test_fixed_tendon.py`:
- Around line 124-129: The assertion message in tests/test_fixed_tendon.py
incorrectly refers to "tendon length" while comparing joint_q[3] to q1; update
the msg in the assertAlmostEqual for q3 to clearly state it's comparing
joint_q[3] (e.g., "Expected joint_q[3]" or "Measured joint_q[3]") and include
both expected (q1) and actual (q3) values so the failure message accurately
reflects the checked variable.
- Around line 234-238: The test's limit check uses strict >/< on
measured_tendon_length which can fail at exact boundaries or due to
floating-point noise; update the logic in the test (the has_legal_length
calculation that references measured_tendon_length, lower_limit, upper_limit) to
be inclusive and tolerant — either change to using >= and <=
(measured_tendon_length >= lower_limit and measured_tendon_length <=
upper_limit) or, better, use math.isclose to allow a small tolerance around the
bounds when comparing to lower_limit and upper_limit.
♻️ Duplicate comments (1)
newton/tests/test_fixed_tendon.py (1)

95-96: Fix the misleading tendon-length comment.
The comment uses exponent notation and incorrect arithmetic; it should reflect the linear combination and the correct value.

📝 Proposed fix
-        # Length of tendon at start is: pos**coef0 + pos1*coef1 = 2*0.5 + 0*0.0 = 1.0
+        # Length of tendon at start is: pos0*coef0 + pos1*coef1 = 0.5*1 + 0.0*1 = 0.5
🧹 Nitpick comments (2)
newton/tests/test_fixed_tendon.py (2)

79-80: Rename unused loop variables to satisfy linting.
Ruff flags i as unused in both loops; prefer _i.

♻️ Proposed fix
-        for i in range(0, 2):
+        for _i in range(0, 2):
             builder.add_world(individual_builder)
@@
-        for i in range(0, 2):
+        for _i in range(0, 2):
             builder.add_world(individual_builder)

Also applies to: 209-210


203-203: Remove unused start_tendon_length.
It’s assigned but never used (Ruff F841).

♻️ Proposed fix
-        start_tendon_length = joint_start_positions[0] * coeff0 + joint_start_positions[1] * coeff1

Comment thread newton/tests/test_fixed_tendon.py Outdated
Comment thread newton/tests/test_fixed_tendon.py
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
@adenzler-nvidia

Copy link
Copy Markdown
Member

looking better and better. Could you go over the coderabbit/copilot issues and address them/resolve if there is no action needed?

Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Comments resolved.

@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

🤖 Fix all issues with AI agents
In `@newton/tests/test_import_mjcf.py`:
- Around line 1375-1384: Update the misleading inline comment "Check
solimplimit" to accurately reflect the assertion: change it to "Check
solimpfriction" so it matches the variables being compared
(expected_solimpfriction and solver.mjw_model.tendon_solimp_fri). Locate the
block where the loop over k performs assertAlmostEqual on measured vs expected
and replace the old comment text with the new one to keep the comment consistent
with the checked symbols.
♻️ Duplicate comments (5)
newton/tests/test_import_mjcf.py (2)

27-27: Tests need conditional import or skip decorators for optional MuJoCo dependency.

The module-level import from newton.solvers import SolverMuJoCo will cause the entire test file to fail importing in environments where the optional mujoco or mujoco_warp packages are not installed.

🔧 Suggested fix using pytest.importorskip
-from newton.solvers import SolverMuJoCo
+# Guard against missing optional mujoco dependencies
+try:
+    from newton.solvers import SolverMuJoCo
+    HAS_MUJOCO = True
+except ImportError:
+    HAS_MUJOCO = False
+    SolverMuJoCo = None

Then decorate the test methods using SolverMuJoCo:

`@unittest.skipUnless`(HAS_MUJOCO, "mujoco not available")
def test_single_mujoco_fixed_tendon_parsing(self):
    ...

1027-1047: Comments don't match the code being tested.

Lines 1027 and 1038 both say "Check solver solimplimit" but the code is actually checking solimpfriction values (tendon_solimp_fri and tendon_solimp_friction).

newton/_src/solvers/mujoco/solver_mujoco.py (3)

991-998: Missing bounds validation for tendon joint ranges.

The code doesn't validate that joint_start + joint_num <= joint_entry_count before iterating. If the tendon attributes specify an invalid range, this could cause an out-of-bounds array access on tendon_joint or tendon_coef.

Additionally, the None check for tendon_joint/tendon_coef at lines 996-997 is inside the loop rather than before entering it when joint_num > 0.

🐛 Proposed fix
             # Add joints for this fixed tendon's linear combination
             joint_start = int(tendon_joint_adr[i])
             joint_num = int(tendon_joint_num[i])
+
+            # Validate range before iterating
+            if joint_num > 0:
+                if tendon_joint is None or tendon_coef is None:
+                    warnings.warn(
+                        f"Skipping tendon {i}: tendon_joint/tendon_coef must be provided when tendon_joint_num > 0.",
+                        stacklevel=2,
+                    )
+                    continue
+                if joint_start < 0 or joint_start + joint_num > joint_entry_count:
+                    warnings.warn(
+                        f"Skipping tendon {i}: invalid joint range (start={joint_start}, num={joint_num}, max={joint_entry_count}).",
+                        stacklevel=2,
+                    )
+                    continue

             for j in range(joint_start, joint_start + joint_num):
-                if tendon_joint is None or tendon_coef is None:
-                    break
-
                 newton_joint = int(tendon_joint[j])

2746-2763: Bug: Tendon mapping breaks when global tendons (world < 0) are present.

The calculation tendons_per_world = total_tendons // model.num_worlds assumes all tendons are world-specific. If any tendon has world < 0 (global), this division will be incorrect, causing the modulo-based template mapping to alias global tendons to wrong Newton indices.

For example, with 1 global tendon and 2 tendons per world across 2 worlds (5 total), tendons_per_world becomes 2, causing incorrect remapping.

🐛 Suggested fix
                 mujoco_attrs = getattr(model, "mujoco", None)
                 tendon_world = getattr(mujoco_attrs, "tendon_world", None) if mujoco_attrs else None
-                if tendon_world is not None:
-                    total_tendons = len(tendon_world)
-                    tendons_per_world = total_tendons // model.num_worlds if model.num_worlds > 0 else total_tendons
-                else:
-                    tendons_per_world = ntendon
+                tendon_world_np = tendon_world.numpy() if tendon_world is not None else None
+                if tendon_world_np is not None and model.num_worlds > 0:
+                    # Count tendons from the first non-global world only
+                    tendons_per_world = int(np.sum(tendon_world_np == first_world))
+                else:
+                    tendons_per_world = ntendon
                 mjc_tendon_to_newton_tendon_np = np.full((nworld, ntendon), -1, dtype=np.int32)
                 for mjc_tendon, newton_tendon in enumerate(selected_tendons):
+                    # Handle global tendons specially
+                    if tendon_world_np is not None and tendon_world_np[newton_tendon] < 0:
+                        for w in range(nworld):
+                            mjc_tendon_to_newton_tendon_np[w, mjc_tendon] = newton_tendon
+                        continue
                     template_tendon = newton_tendon % tendons_per_world if tendons_per_world > 0 else newton_tendon
                     for w in range(nworld):
                         mjc_tendon_to_newton_tendon_np[w, mjc_tendon] = w * tendons_per_world + template_tendon

3244-3249: Docstring incorrectly mentions springlength.

The docstring lists "springlength" among the updated properties, but the implementation explicitly skips it (as noted in the comment at lines 3265-3266). The docstring should be updated to match the actual behavior.

✏️ Suggested fix
     def update_tendon_properties(self):
         """Update fixed tendon properties in the MuJoCo model.

-        Updates tendon stiffness, damping, frictionloss, range, margin, solref, solimp,
-        armature, and actfrcrange from Newton custom attributes.
+        Updates tendon stiffness, damping, frictionloss, range, margin, solref_limit, solimp_limit,
+        solref_friction, solimp_friction, armature, and actfrcrange from Newton custom attributes.
+
+        Note: springlength is intentionally not updated at runtime (MuJoCo auto-computes when set to -1).
         """
🧹 Nitpick comments (1)
newton/_src/solvers/mujoco/solver_mujoco.py (1)

2874-2888: Consider removing commented-out tendon fields.

Per a previous discussion, commented-out fields in this list were intended to be removed once implementation is complete, rather than left as comments. The commented fields tendon_length0 and tendon_invweight0 (lines 2887-2888) could be removed, with the explanation moved to a separate note if needed.

This is a minor style consideration and not blocking.

Comment thread newton/tests/test_import_mjcf.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: 2

🤖 Fix all issues with AI agents
In `@newton/_src/utils/import_mjcf.py`:
- Around line 1440-1456: The float(coef_str) conversion can raise on malformed
MJCF and abort parsing; update the tendon joint parsing block in import_mjcf.py
(the code that looks up joint_name via builder.joint_key and appends to
joint_entries) to catch ValueError (and TypeError) around converting coef_str to
float, emit a verbose warning referencing tendon_name and joint_name when
conversion fails, and continue without appending that joint entry so import
remains resilient.

In `@newton/tests/test_import_mjcf.py`:
- Around line 1375-1384: The inline comment "Check solimplimit" is incorrect for
the loop that asserts tendon_solimp_fri values; update the comment to "Check
solimpfriction" (or, if the intent was to test solimplimit, change the accessed
arrays/attributes from expected_solimpfriction and
solver.mjw_model.tendon_solimp_fri to the corresponding expected_solimplimit and
solver.mjw_model.<solimplimit_attribute>). Locate the loop using
expected_solimpfriction and solver.mjw_model.tendon_solimp_fri and either rename
the comment to match those symbols or swap in the correct expected_* and model
attribute names to match the original intent.
♻️ Duplicate comments (4)
newton/tests/test_import_mjcf.py (2)

1027-1047: Comments don't match the code being tested.

Lines 1027 and 1038 say "Check solver solimplimit" / "Check model solimplimit" but the code is checking solimpfriction values (tendon_solimp_fri and tendon_solimp_friction).

🔧 Suggested fix
-                # Check solver solimplimit
+                # Check solver solimpfriction
                 for k in range(0, 5):
                     expected = expected_solimpfriction[0][j][k]
                     measured = solver.mjw_model.tendon_solimp_fri.numpy()[i][j][k]
                     ...

-                # Check model solimpfriction
+                # Check model solimpfriction
                 for k in range(0, 5):

27-27: Module-level import will break tests when MuJoCo is not installed.

SolverMuJoCo requires optional dependencies (mujoco, mujoco_warp). This import at module level causes all tests in this file to fail when these dependencies are unavailable, even tests that don't use SolverMuJoCo.

🛠️ Suggested fix: Use conditional import with skip decorator
 import newton
 import newton.examples
 from newton._src.geometry.types import GeoType
 from newton._src.sim.builder import ShapeFlags
-from newton.solvers import SolverMuJoCo
+
+try:
+    from newton.solvers import SolverMuJoCo
+    HAS_MUJOCO = True
+except ImportError:
+    HAS_MUJOCO = False

Then decorate MuJoCo-dependent tests:

`@unittest.skipUnless`(HAS_MUJOCO, "MuJoCo dependencies not available")
def test_single_mujoco_fixed_tendon_parsing(self):
    ...
newton/_src/solvers/mujoco/solver_mujoco.py (2)

2746-2763: Fix tendon mapping when global tendons are present.

The current mapping logic uses tendons_per_world = total_tendons // model.num_worlds which doesn't correctly handle global tendons (tendon_world < 0). Global tendons are counted in total_tendons but should not be divided across worlds. This will cause incorrect Newton tendon indices to be looked up for global tendons and may alias mappings incorrectly.

🐛 Suggested fix from past review
-                if tendon_world is not None:
-                    total_tendons = len(tendon_world)
-                    tendons_per_world = total_tendons // model.num_worlds if model.num_worlds > 0 else total_tendons
-                else:
-                    tendons_per_world = ntendon
+                tendon_world_np = tendon_world.numpy() if tendon_world is not None else None
+                if tendon_world_np is not None and model.num_worlds > 0:
+                    tendons_per_world = int(np.sum(tendon_world_np == first_world))
+                else:
+                    tendons_per_world = ntendon
                 mjc_tendon_to_newton_tendon_np = np.full((nworld, ntendon), -1, dtype=np.int32)
                 for mjc_tendon, newton_tendon in enumerate(selected_tendons):
+                    if tendon_world_np is not None and tendon_world_np[newton_tendon] < 0:
+                        # Global tendon: map to the same Newton index for all worlds
+                        for w in range(nworld):
+                            mjc_tendon_to_newton_tendon_np[w, mjc_tendon] = newton_tendon
+                        continue
                     template_tendon = newton_tendon % tendons_per_world if tendons_per_world > 0 else newton_tendon
                     for w in range(nworld):
                         mjc_tendon_to_newton_tendon_np[w, mjc_tendon] = w * tendons_per_world + template_tendon

991-998: Add bounds validation for tendon joint ranges.

The loop iterates from joint_start to joint_start + joint_num without validating that this range is within bounds of tendon_joint and tendon_coef arrays. If tendon_joint_adr or tendon_joint_num contains invalid values, this will cause an IndexError.

🐛 Proposed fix
             # Add joints for this fixed tendon's linear combination
             joint_start = int(tendon_joint_adr[i])
             joint_num = int(tendon_joint_num[i])
 
+            # Validate joint range bounds
+            if joint_start < 0 or joint_num < 0:
+                warnings.warn(
+                    f"Skipping tendon {i}: invalid joint range (start={joint_start}, num={joint_num}).",
+                    stacklevel=2,
+                )
+                continue
+            if tendon_joint is not None and joint_start + joint_num > len(tendon_joint):
+                warnings.warn(
+                    f"Skipping tendon {i}: joint range exceeds array bounds "
+                    f"(start={joint_start}, num={joint_num}, max={len(tendon_joint)}).",
+                    stacklevel=2,
+                )
+                continue
+
             for j in range(joint_start, joint_start + joint_num):

Comment thread newton/_src/utils/import_mjcf.py
Comment thread newton/tests/test_import_mjcf.py
@gyeomannvidia

Copy link
Copy Markdown
Member Author

@CodeRabbit resolve

@coderabbitai

coderabbitai Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Comments resolved.

@gyeomannvidia gyeomannvidia added this pull request to the merge queue Jan 22, 2026
Merged via the queue into newton-physics:main with commit b046666 Jan 22, 2026
22 checks passed
jnskkmhr pushed a commit to jnskkmhr/newton that referenced this pull request Jan 22, 2026
Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Co-authored-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Co-authored-by: Viktor Reutskyy <33062116+vreutskyy@users.noreply.github.com>
eric-heiden pushed a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Co-authored-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Co-authored-by: Viktor Reutskyy <33062116+vreutskyy@users.noreply.github.com>
@gyeomannvidia gyeomannvidia deleted the dev/fixedTendons/replication branch April 1, 2026 08:13
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Co-authored-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Co-authored-by: Viktor Reutskyy <33062116+vreutskyy@users.noreply.github.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.

4 participants