Add parsing fixed tendons from MJCF#1406
Conversation
…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>
…mannvidia/newton into dev/gyeoman/mjcFixedTendons
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
…mannvidia/newton into dev/gyeoman/mjcFixedTendons
…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>
|
Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughWalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 actuatorsnewton/_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‑codedtendon_invweight0values.
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_lengthis 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()
…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>
There was a problem hiding this comment.
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_lengthis 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()
…have identical behaviour. Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
There was a problem hiding this comment.
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 flagsias 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 unusedstart_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
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
Signed-off-by: Gordon Yeoman <gyeoman@nvidia.com>
uv run -m newton.tests
|
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>
✅ Actions performedComments resolved. |
There was a problem hiding this comment.
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 SolverMuJoCowill cause the entire test file to fail importing in environments where the optionalmujocoormujoco_warppackages 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 = NoneThen 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
solimpfrictionvalues (tendon_solimp_friandtendon_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_countbefore iterating. If the tendon attributes specify an invalid range, this could cause an out-of-bounds array access ontendon_jointortendon_coef.Additionally, the
Nonecheck fortendon_joint/tendon_coefat lines 996-997 is inside the loop rather than before entering it whenjoint_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_worldsassumes all tendons are world-specific. If any tendon hasworld < 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_worldbecomes 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_length0andtendon_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.
There was a problem hiding this comment.
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
solimpfrictionvalues (tendon_solimp_friandtendon_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.
SolverMuJoCorequires 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 useSolverMuJoCo.🛠️ 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 = FalseThen 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_worldswhich doesn't correctly handle global tendons (tendon_world < 0). Global tendons are counted intotal_tendonsbut 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_starttojoint_start + joint_numwithout validating that this range is within bounds oftendon_jointandtendon_coefarrays. Iftendon_joint_adrortendon_joint_numcontains invalid values, this will cause anIndexError.🐛 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):
|
@CodeRabbit resolve |
✅ Actions performedComments resolved. |
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>
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>
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>
Description
Newton Migration Guide
Please ensure the migration guide for warp.sim users is up-to-date with the changes made in this PR.
docs/migration.rstis up-to dateBefore your PR is "Ready for review"
newton/tests/test_examples.py)pre-commit run -aSummary by CodeRabbit
New Features
Importer
Tests
✏️ Tip: You can customize this high-level summary in your review settings.