diff --git a/docker/test/test_dockerfile_nonroot.py b/docker/test/test_dockerfile_nonroot.py index 98b2c7d7495f..7cc54ab3289c 100644 --- a/docker/test/test_dockerfile_nonroot.py +++ b/docker/test/test_dockerfile_nonroot.py @@ -169,6 +169,15 @@ def test_kitless_dockerfile_installs_newton_rl_ov_and_visualizers_without_isaac_ ) +def test_container_test_runner_only_links_an_actual_isaac_sim_runtime(): + """Cache mount points under /isaac-sim must not masquerade as an Isaac Sim installation.""" + runner_text = (REPO_ROOT / ".github/actions/run-tests/run_tests.sh").read_text(encoding="utf-8") + guarded_link = re.compile(r"if \[ -x /isaac-sim/python\.sh \]; then\s+ln -s /isaac-sim _isaac_sim;?\s+fi") + + assert guarded_link.search(runner_text) + assert runner_text.count("ln -s /isaac-sim _isaac_sim") == 1 + + # --------------------------------------------------------------------------- # # Volume mount-point writability # diff --git a/pyproject.toml b/pyproject.toml index bce9bc4d9b2c..90d151b6560b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,9 +85,8 @@ dependencies = [ "rsl-rl-lib==5.4.1", # default RL framework "onnxscript>=0.5", # ----- newton (default physics engine) ----- - # Loose bound so the wheel co-resolves with isaacsim's newton[sim]==1.2.0 pin; the - # exact PyPI release is forced via [tool.uv].override-dependencies (uv sync only). - "newton[sim]>=1.2.0", + # Pin the Newton build used by both workspace and wheel installs until the 1.6 release. + "newton[sim] @ git+https://github.com/newton-physics/newton.git@24bd863528d6b91137408930d0fbe8fa216ad962", # Import and mesh-processing packages used by Newton, including the ones that honoring # USD-authored ``physics:approximation`` requires. Keep these explicit instead of selecting # newton[importers], whose standalone USD dependency would overlap with usd-exchange. @@ -403,7 +402,7 @@ override-dependencies = [ "numpy>=2", "mujoco~=3.11.0", "mujoco-warp~=3.11.0", - "newton[sim]==1.5.1", + "newton[sim] @ git+https://github.com/newton-physics/newton.git@24bd863528d6b91137408930d0fbe8fa216ad962", # Force the Newton-matched schemas over isaacsim's ==0.2.0 pin. "newton-usd-schemas>=0.4.1", "torch==2.11.0", diff --git a/source/isaaclab/changelog.d/pr-7453-expanded-prebundles.rst b/source/isaaclab/changelog.d/pr-7453-expanded-prebundles.rst new file mode 100644 index 000000000000..0d21d986a7ad --- /dev/null +++ b/source/isaaclab/changelog.d/pr-7453-expanded-prebundles.rst @@ -0,0 +1,4 @@ +Fixed +^^^^^ + +* Fixed Docker installs leaving dangling package links in expanded Isaac Sim prebundles. diff --git a/source/isaaclab/changelog.d/replication-names-copies.skip b/source/isaaclab/changelog.d/replication-names-copies.skip new file mode 100644 index 000000000000..29811b819395 --- /dev/null +++ b/source/isaaclab/changelog.d/replication-names-copies.skip @@ -0,0 +1 @@ +Dependency pin coverage is documented by the Newton package fragment. diff --git a/source/isaaclab/isaaclab/cli/commands/install.py b/source/isaaclab/isaaclab/cli/commands/install.py index 457f7b62cc04..b30bcfeaec5a 100644 --- a/source/isaaclab/isaaclab/cli/commands/install.py +++ b/source/isaaclab/isaaclab/cli/commands/install.py @@ -1062,10 +1062,14 @@ def _repoint_prebundle_packages() -> None: print_debug("No pip_prebundle directories found under Isaac Sim.") return + # Extras are expanded as wheel trees nested below pip_prebundle. + package_roots = prebundle_dirs | { + path for prebundle_dir in prebundle_dirs for path in prebundle_dir.glob("*[[]*[]]/*") if path.is_dir() + } repointed = 0 - for prebundle_dir in prebundle_dirs: + for package_root in package_roots: for pkg_name in _PREBUNDLE_REPOINT_PACKAGES: - prebundled = prebundle_dir / pkg_name + prebundled = package_root / pkg_name venv_pkg = site_packages / pkg_name if not venv_pkg.exists(): @@ -1118,9 +1122,9 @@ def _repoint_prebundle_packages() -> None: # env package into the prebundle, which is a real directory by design. if use_symlinks and (site_packages / "torch").exists(): shadowing = [ - prebundle_dir / "torch" - for prebundle_dir in prebundle_dirs - if (prebundle_dir / "torch").is_dir() and not (prebundle_dir / "torch").is_symlink() + package_root / "torch" + for package_root in package_roots + if (package_root / "torch").is_dir() and not (package_root / "torch").is_symlink() ] if shadowing: raise RuntimeError( diff --git a/source/isaaclab/test/cli/test_install_commands.py b/source/isaaclab/test/cli/test_install_commands.py index 43581295240c..c0c0d09b7403 100644 --- a/source/isaaclab/test/cli/test_install_commands.py +++ b/source/isaaclab/test/cli/test_install_commands.py @@ -930,6 +930,25 @@ def test_repoints_across_multiple_prebundle_dirs(self, tmp_path): for pb in (pb1, pb2): assert (pb / "torch").is_symlink(), f"torch in {pb} should be repointed" + def test_repoints_package_inside_expanded_extra_bundle(self, tmp_path): + """Expanded extras bundles must not retain file links into a replaced package.""" + isaacsim_path, prebundle = self._sim_with_prebundle(tmp_path / "sim", ["newton"]) + shared_init = prebundle / "newton" / "legacy" / "__init__.py" + shared_init.parent.mkdir() + shared_init.write_text("") + bundled_newton = prebundle / "newton[sim]" / "newton-wheel" / "newton" + bundled_init = bundled_newton / "legacy" / "__init__.py" + bundled_init.parent.mkdir(parents=True) + bundled_init.symlink_to(shared_init) + site_pkgs = _make_site_packages(tmp_path / "env", ["newton"]) + + with self._patch(isaacsim_path, site_pkgs, str(tmp_path / "env" / "bin" / "python")): + _repoint_prebundle_packages() + + assert (prebundle / "newton").resolve() == (site_pkgs / "newton").resolve() + assert bundled_newton.is_symlink() + assert bundled_newton.resolve() == (site_pkgs / "newton").resolve() + # ---- Windows: copy instead of symlink ----------------------------------- def test_copies_package_on_windows_instead_of_symlinking(self, tmp_path): diff --git a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py index 973014079b00..bc50847f67dd 100644 --- a/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py +++ b/source/isaaclab/test/install_ci/uv_pip/test_uv_pip_install_isaaclab_all_trains_cartpole.py @@ -15,8 +15,8 @@ Reinstall AFTER the wheel install: unsafe-best-match re-resolves torch from PyPI to CPU.) - (aarch64 only) export LD_PRELOAD=/lib/aarch64-linux-gnu/libgomp.so.1 Tests: - - python -c "import importlib.metadata as m; assert m.version('newton') == '1.5.1'" - -> verify the wheel resolves Newton 1.5 + - python -c "import importlib.metadata as m; assert m.version('newton') == '1.6.0.dev0'" + -> verify the wheel resolves the pinned Newton 1.6 development build - uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole-Direct --num_envs 16 presets=newton_mjwarp --max_iterations 5; uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole-Camera-Direct --num_envs 16 presets=newton_mjwarp,newton_renderer --max_iterations 2 @@ -57,11 +57,11 @@ def test_uv_pip_install_isaaclab_all_trains_cartpole(self, isaaclab_root, wheel, assert result.returncode == 0, f"uv pip install {wheel}[all] failed:\n{result.stdout}\n{result.stderr}" result = self.run_in_uv_env( - ["python", "-c", "import importlib.metadata as m; assert m.version('newton') == '1.5.1'"], + ["python", "-c", "import importlib.metadata as m; assert m.version('newton') == '1.6.0.dev0'"], cwd=isaaclab_root, ) assert result.returncode == 0, ( - f"isaaclab[all] did not resolve Newton 1.5:\n{result.stdout}\n{result.stderr}" + f"isaaclab[all] did not resolve the pinned Newton build:\n{result.stdout}\n{result.stderr}" ) # Restore the CUDA build selected for this architecture. diff --git a/source/isaaclab/test/install_ci/uv_pip/uv-overrides.txt b/source/isaaclab/test/install_ci/uv_pip/uv-overrides.txt index 95a0eec25afa..bbf4df976ab0 100644 --- a/source/isaaclab/test/install_ci/uv_pip/uv-overrides.txt +++ b/source/isaaclab/test/install_ci/uv_pip/uv-overrides.txt @@ -1,7 +1,7 @@ numpy>=2 mujoco~=3.11.0 mujoco-warp~=3.11.0 -newton[sim]==1.5.1 +newton[sim] @ git+https://github.com/newton-physics/newton.git@24bd863528d6b91137408930d0fbe8fa216ad962 newton-usd-schemas>=0.4.1 torch==2.11.0 torchvision==0.26.0 diff --git a/source/isaaclab/test/sim/test_newton_manager_visualization_state.py b/source/isaaclab/test/sim/test_newton_manager_visualization_state.py index 823afc3e222e..407d4e639818 100644 --- a/source/isaaclab/test/sim/test_newton_manager_visualization_state.py +++ b/source/isaaclab/test/sim/test_newton_manager_visualization_state.py @@ -836,8 +836,7 @@ def test_clone_visualization_builder_ignores_non_env_deformables_on_world_import monkeypatch.setattr(vb, "_restore_visible_colliders_without_visual_shapes", lambda *args, **kwargs: None) monkeypatch.setattr(vb, "import_builder_visual_material_paths", lambda *args, **kwargs: None) monkeypatch.setattr(vb, "build_source_builders", lambda *args, **kwargs: {}) - monkeypatch.setattr(vb, "replicate_builder_mapping", lambda *args, **kwargs: None) - monkeypatch.setattr(vb, "rename_builder_labels", lambda *args, **kwargs: None) + monkeypatch.setattr(vb, "replicate_builder_mapping", lambda *args, **kwargs: ({}, [], [])) _builder, (shadow_entities, registry_groups) = vb.build_visualization_builder_from_stage_envs( stage, diff --git a/source/isaaclab_newton/changelog.d/replication-names-copies.rst b/source/isaaclab_newton/changelog.d/replication-names-copies.rst new file mode 100644 index 000000000000..4d409dd5d33b --- /dev/null +++ b/source/isaaclab_newton/changelog.d/replication-names-copies.rst @@ -0,0 +1,4 @@ +Changed +^^^^^^^ + +* Changed homogeneous Newton cloning to assign labels during replication, avoiding a second full-model pass. diff --git a/source/isaaclab_newton/changelog.d/root-joint-names.rst b/source/isaaclab_newton/changelog.d/root-joint-names.rst new file mode 100644 index 000000000000..5f532697b71c --- /dev/null +++ b/source/isaaclab_newton/changelog.d/root-joint-names.rst @@ -0,0 +1,5 @@ +Changed +^^^^^^^ + +* Changed importer-generated floating-base root joints to use ``{body}_free_joint`` instead of + generated ``joint_`` labels, giving replicated environments stable body-derived joint paths. diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py index a6b233f4a874..d43177e828e0 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/newton_clone_utils.py @@ -11,7 +11,7 @@ import numpy as np import torch import warp as wp -from newton import GeoType, ModelBuilder, ShapeFlags +from newton import GeoType, JointType, ModelBuilder, ShapeFlags from pxr import Usd, UsdGeom, UsdPhysics @@ -148,9 +148,22 @@ def _build_source_builder( replace_newton_builder_shape_colors(builder, stage) if load_visual_shapes: import_builder_visual_material_paths(builder, stage) + _name_root_joints_after_their_body(builder) return builder +def _name_root_joints_after_their_body(builder: ModelBuilder) -> None: + """Name importer-generated free root joints after their child bodies, in place.""" + for index, label in enumerate(builder.joint_label): + if not isinstance(label, str) or not label.startswith("joint_") or not label[6:].isdigit(): + continue + if builder.joint_type[index] != JointType.FREE or builder.joint_parent[index] != -1: + continue + body_label = builder.body_label[builder.joint_child[index]] + if isinstance(body_label, str) and body_label.startswith("/"): + builder.joint_label[index] = f"{body_label}_free_joint" + + def _quat_multiply(a: np.ndarray, b: np.ndarray) -> np.ndarray: """Hamilton product of xyzw quaternion arrays, broadcast over the leading axes.""" ax, ay, az, aw = a[..., 0], a[..., 1], a[..., 2], a[..., 3] @@ -186,6 +199,37 @@ def _invert_xform(xform: Sequence[float] | np.ndarray) -> np.ndarray: return np.concatenate([-_quat_rotate(quat_inv, xform[:3]), quat_inv]) +def _label_groups(builder: ModelBuilder) -> dict[str, list]: + """Return every entity-label container owned by a Newton builder.""" + groups = { + name: value for name, value in vars(builder).items() if name.endswith("_label") and isinstance(value, list) + } + groups["mujoco:equality_constraint_label"] = builder.custom_attributes["mujoco:equality_constraint_label"].values + return groups + + +def _rebase_labels(builder: ModelBuilder, source: str, destination: str) -> str: + """Make entity labels relative to the nearest templated destination ancestor.""" + source = source.rstrip("/") or "/" + destination = destination.rstrip("/") or "/" + prefix, _, destination_name = destination.rpartition("/") + if "{}" in destination_name: + prefix, destination_name = destination, "" + for labels in _label_groups(builder).values(): + for index, label in enumerate(labels): + if not isinstance(label, str) or not label or not label.startswith("/"): + continue + suffix = clone_path.relative_to(label, source) + if suffix is None: + suffix = label[len(source) :] if label.startswith(source + "_") else None + if suffix is None: + raise ValueError(f"Newton label {label!r} is outside clone source {source!r}.") + labels[index] = (destination_name + suffix).lstrip("/") + if not labels[index]: + raise ValueError(f"Newton label {label!r} cannot be prefixed by destination {destination!r}.") + return prefix + + def replicate_builder_mapping( builder: ModelBuilder, sources: Sequence[str], @@ -193,12 +237,14 @@ def replicate_builder_mapping( positions: torch.Tensor, quaternions: torch.Tensor, source_builders: dict[str, ModelBuilder], + destinations: Sequence[str] | None = None, + env_ids: torch.Tensor | None = None, *, source_site_indices: dict[int, dict[str, list[int]]] | None = None, env_root_sites: dict[str, wp.transform] | None = None, per_world_builder_hooks: Sequence[Callable[[ModelBuilder, int, list[float], list[float]], None]] = (), -) -> tuple[dict[str, list[list[int]]], list[wp.transform]]: - """Replicate source builders into per-env Newton worlds.""" +) -> tuple[dict[str, list[list[int]]], list[wp.transform], list[tuple[str, int]]]: + """Replicate source builders, naming homogeneous copies at their destinations.""" source_site_indices = source_site_indices or {} env_root_sites = env_root_sites or {} num_worlds = mapping.size(1) @@ -215,6 +261,8 @@ def replicate_builder_mapping( and num_worlds > 0 and bool(mapping.all().item()) and not per_world_builder_hooks + and bool(destinations) + and env_ids is not None ) if can_batch: source_builder = source_builders[sources[0]] @@ -234,14 +282,24 @@ def replicate_builder_mapping( stride = source_builder.shape_count source_xform_inv = _invert_xform(xforms_np[0]) xforms = _compose_world_xforms(positions_np, quaternions_np, source_xform_inv) - builder.replicate(source_builder, num_worlds, xforms=xforms) + + label_groups = _label_groups(source_builder) + original_labels = {name: list(labels) for name, labels in label_groups.items()} + try: + prefix = _rebase_labels(source_builder, sources[0], destinations[0]) + prefixes = [prefix.format(env_id) for env_id in env_ids.tolist()] + builder.replicate(source_builder, num_worlds, xforms=xforms, label_prefixes=prefixes) + finally: + for name, labels in original_labels.items(): + label_groups[name][:] = labels for label, local_indices in site_local_indices.items(): local_site_map[label] = [ [base_shape + world * stride + local for local in local_indices] for world in range(num_worlds) ] - return local_site_map, world_xforms + bindings = rename_builder_labels(builder, sources, destinations, env_ids, mapping, skip_entity_labels=True) + return local_site_map, world_xforms, bindings source_world_indices = mapping.to(dtype=torch.int64).argmax(dim=1).tolist() @@ -273,35 +331,22 @@ def replicate_builder_mapping( for col in range(num_worlds): builder.begin_world() - for label, world_site_xforms in root_site_xforms.items(): site_idx = builder.add_site(body=-1, xform=world_site_xforms[col], label=label) local_site_map.setdefault(label, [[] for _ in range(num_worlds)])[col].append(site_idx) - for row in rows_per_world[col]: source_builder = source_builders[sources[row]] offset = builder.shape_count builder.add_builder(source_builder, xform=source_xforms[row, col]) - for label, source_shape_indices in source_site_indices.get(id(source_builder), {}).items(): local_indices = local_site_map.setdefault(label, [[] for _ in range(num_worlds)])[col] local_indices.extend(offset + shape_idx for shape_idx in source_shape_indices) - for hook in per_world_builder_hooks: hook(builder, col, xform_rows[col][:3], xform_rows[col][3:]) builder.end_world() - return local_site_map, world_xforms - - -_BUILTIN_LABEL_TYPES: tuple[str, ...] = ( - "body", - "joint", - "shape", - "articulation", - "constraint_mimic", - "equality_constraint", -) + bindings = rename_builder_labels(builder, sources, destinations, env_ids, mapping) if destinations else [] + return local_site_map, world_xforms, bindings def rename_builder_labels( @@ -310,6 +355,8 @@ def rename_builder_labels( destinations: Sequence[str], env_ids: torch.Tensor, mapping: torch.Tensor, + *, + skip_entity_labels: bool = False, ) -> list[tuple[str, int]]: """Rewrite source-root labels to per-env destination roots and return Fabric body bindings.""" fabric_body_bindings: list[tuple[str, int]] = [] @@ -321,10 +368,7 @@ def rename_builder_labels( world_cols = torch.nonzero(mapping[source_index], as_tuple=True)[0].tolist() # Pre-normalize the destination roots destination = destinations[source_index] - world_roots = { - env_id: (destination.format(env_id).rstrip("/") or "/") - for env_id in (env_ids_list[col] for col in world_cols) - } + world_roots = {col: (destination.format(env_ids_list[col]).rstrip("/") or "/") for col in world_cols} def _rename_pair(values, worlds, src_root=source_root, roots=world_roots, *, collect_body_bindings=False): rows = ( @@ -346,14 +390,11 @@ def _rename_pair(values, worlds, src_root=source_root, roots=world_roots, *, col fabric_body_bindings.append((renamed_value, index)) bound_body_indices.add(index) - for labels, worlds, collect_body_bindings in ( - (builder.body_label, builder.body_world, True), - (builder.joint_label, builder.joint_world, False), - (builder.shape_label, builder.shape_world, False), - (builder.articulation_label, builder.articulation_world, False), - (builder.constraint_mimic_label, builder.constraint_mimic_world, False), - ): - _rename_pair(labels, worlds, collect_body_bindings=collect_body_bindings) + if not skip_entity_labels: + for name, labels in vars(builder).items(): + worlds = getattr(builder, f"{name[:-6]}_world", None) if name.endswith("_label") else None + if isinstance(labels, list) and worlds is not None: + _rename_pair(labels, worlds, collect_body_bindings=name == "body_label") custom_attrs = builder.custom_attributes.values() worlds_by_freq = {attr.frequency: attr.values for attr in custom_attrs if attr.references == "world"} diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index 8d0f201ffe38..5ed9cecd7ce0 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -24,7 +24,6 @@ from isaaclab_newton.cloner.newton_clone_utils import ( _restore_visible_colliders_without_visual_shapes, build_source_builders, - rename_builder_labels, replicate_builder_mapping, ) from isaaclab_newton.physics import NewtonManager @@ -106,13 +105,8 @@ def _build_newton_builder_from_mapping( up_axis: str = "Z", load_visual_shapes: bool = True, global_paths: tuple[str, ...] = (), -) -> tuple[ModelBuilder, object, dict, list, dict[str, ModelBuilder]]: - """Build a Newton model builder from clone mapping inputs. - - Also returns the per-source builders (``{source_path: ModelBuilder}``) so the - committing path can retain them for single-model consumers such as the - batched Newton IK action. - """ +) -> tuple[ModelBuilder, object, dict, list, dict[str, ModelBuilder], list[tuple[str, int]]]: + """Build a Newton model builder from clone mapping inputs and retain its source builders.""" if positions is None: positions = torch.zeros((mapping.size(1), 3), device=mapping.device, dtype=torch.float32) if quaternions is None: @@ -170,8 +164,10 @@ def _build_newton_builder_from_mapping( global_sites, source_sites, root_sites = NewtonManager._cl_inject_sites(builder, source_builders) replicate_args = (builder, sources, mapping, positions, quaternions, source_builders) - local_site_map, world_xforms = replicate_builder_mapping( + local_site_map, world_xforms, fabric_body_bindings = replicate_builder_mapping( *replicate_args, + destinations, + env_ids, source_site_indices=source_sites, env_root_sites=root_sites, per_world_builder_hooks=NewtonManager._per_world_builder_hooks, @@ -179,7 +175,7 @@ def _build_newton_builder_from_mapping( site_index_map = {label: (idx, None) for label, idx in global_sites.items()} site_index_map.update((label, (None, per_world)) for label, per_world in local_site_map.items()) - return builder, stage_info, site_index_map, world_xforms, source_builders + return builder, stage_info, site_index_map, world_xforms, source_builders, fabric_body_bindings def _renderer_wants_visual_shapes() -> bool: @@ -309,19 +305,20 @@ def _merged_mapping(self) -> _MappingBatch: def replicate(self) -> tuple[ModelBuilder, object, dict]: """Build the Newton model builder from queued mappings and optionally publish it.""" sources, destinations, env_ids, mapping, positions, quaternions = self._merged_mapping() - builder, stage_info, site_index_map, world_xforms, source_builders = _build_newton_builder_from_mapping( - stage=self.stage, - sources=sources, - destinations=destinations, - env_ids=env_ids, - mapping=mapping, - positions=positions, - quaternions=quaternions, - up_axis=self.up_axis, - load_visual_shapes=self.load_visual_shapes, - global_paths=self._global_paths, + builder, stage_info, site_index_map, world_xforms, source_builders, fabric_body_bindings = ( + _build_newton_builder_from_mapping( + stage=self.stage, + sources=sources, + destinations=destinations, + env_ids=env_ids, + mapping=mapping, + positions=positions, + quaternions=quaternions, + up_axis=self.up_axis, + load_visual_shapes=self.load_visual_shapes, + global_paths=self._global_paths, + ) ) - fabric_body_bindings = rename_builder_labels(builder, sources, destinations, env_ids, mapping) if self.commit_to_manager: NewtonManager._cl_site_index_map = site_index_map NewtonManager._cl_fabric_body_bindings = fabric_body_bindings diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 0025e065b0ff..15d71604f219 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -2006,7 +2006,7 @@ def instantiate_builder_from_stage(cls): quaternions = torch.tensor([quat for _, quat in poses], dtype=torch.float32) mapping = torch.ones((1, len(env_paths)), dtype=torch.bool) replicate_args = (builder, (proto_path,), mapping, positions, quaternions, source_builders) - local_site_map, world_xforms = replicate_builder_mapping( + local_site_map, world_xforms, _ = replicate_builder_mapping( *replicate_args, source_site_indices=source_site_indices, env_root_sites=env_root_sites, diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index cef64e4309d7..aa518cffe5ae 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -20,7 +20,6 @@ from isaaclab_newton.cloner.newton_clone_utils import ( _restore_visible_colliders_without_visual_shapes, build_source_builders, - rename_builder_labels, replicate_builder_mapping, ) from isaaclab_newton.physics.visualization_deformables import add_shadow_deformables_to_builder @@ -148,8 +147,7 @@ def build_visualization_builder_from_stage_envs( schema_resolvers, ignore_paths=source_deformable_ignore_paths or None, ) - replicate_builder_mapping(builder, sources, mapping, positions, quaternions, source_builders) - rename_builder_labels(builder, sources, destinations, env_ids, mapping) + replicate_builder_mapping(builder, sources, mapping, positions, quaternions, source_builders, destinations, env_ids) shadow_entities, registry_groups = add_shadow_deformables_to_builder( builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan ) diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index ee81d39e83b7..c06a761f24b9 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -12,14 +12,9 @@ import torch import warp as wp from isaaclab_newton.cloner import newton_clone_utils as newton_clone_utils_module -from isaaclab_newton.cloner.newton_clone_utils import ( - _BUILTIN_LABEL_TYPES, - rename_builder_labels, - replicate_builder_mapping, -) +from isaaclab_newton.cloner.newton_clone_utils import rename_builder_labels, replicate_builder_mapping from isaaclab_newton.physics import visualization_builder as visualization_builder_module from isaaclab_newton.physics import visualization_deformables as visualization_deformables_module -from newton.solvers import SolverMuJoCo from pxr import Usd, UsdGeom @@ -38,9 +33,8 @@ _VIS_BUILTIN_LABEL_ATTRS = tuple(attr for attr in _VIS_LABEL_SUFFIXES if attr != "equality_constraint_label") _VIS_EQ_FREQ = "mujoco:equality_constraint" -_TENDON_FREQ = "mujoco:tendon" -_SRC = "/Sources/protoA" -_DST = "/World/envs/env_{}" +_SRC = "/World/envs/env_0/protoA" +_DST = "/World/envs/env_{}/protoA" class _FakeVisualizationModelBuilder: @@ -125,35 +119,10 @@ def _record_world_slice(self, label_start, label_end, geometry_start, geometry_e self.world_slices[self._current_world].append((label_start, label_end, geometry_start, geometry_end)) -def _inject_builtins(builder: newton.ModelBuilder, types: tuple[str, ...], src_path: str, worlds: list[int]) -> None: - for kind in types: - for world in worlds: - if kind == "equality_constraint": - builder.add_custom_values( - **{ - "mujoco:equality_constraint_label": f"{src_path}/{kind}_{world}", - "mujoco:equality_constraint_world": world, - } - ) - else: - getattr(builder, f"{kind}_label").append(f"{src_path}/{kind}_{world}") - getattr(builder, f"{kind}_world").append(world) - - -def _inject_tendons(builder: newton.ModelBuilder, src_path: str, worlds: list[int]) -> None: - labels = builder.custom_attributes["mujoco:tendon_label"].values = [] - world_ids = builder.custom_attributes["mujoco:tendon_world"].values = [] - for world in worlds: - labels.append(f"{src_path}/Tendon_{world}") - world_ids.append(world) - builder._custom_frequency_counts[_TENDON_FREQ] = len(worlds) - - def _make_builder(worlds: list[int]) -> newton.ModelBuilder: builder = newton.ModelBuilder() - SolverMuJoCo.register_custom_attributes(builder) - _inject_builtins(builder, _BUILTIN_LABEL_TYPES, _SRC, worlds) - _inject_tendons(builder, _SRC, worlds) + builder.shape_label.extend(f"{_SRC}/shape_{world}" for world in worlds) + builder.shape_world.extend(worlds) return builder @@ -178,73 +147,10 @@ def _populate_custom_frequency(builder, freq_name, string_columns, worlds): builder._custom_frequency_counts[f"syn:{freq_name}"] = len(worlds) -class TestRenameBuilderLabels(unittest.TestCase): - def setUp(self): - self.worlds = [0, 1, 2] - self.env_ids = torch.tensor(self.worlds, dtype=torch.int32) - self.mapping = torch.ones(1, len(self.worlds), dtype=torch.bool) - - def _rename(self, builder): - rename_builder_labels(builder, [_SRC], [_DST], self.env_ids, self.mapping) - - def _assert_builtins(self, builder, types=_BUILTIN_LABEL_TYPES): - for kind in types: - if kind == "equality_constraint": - labels = builder.custom_attributes["mujoco:equality_constraint_label"].values - worlds = builder.custom_attributes["mujoco:equality_constraint_world"].values - else: - labels = getattr(builder, f"{kind}_label") - worlds = getattr(builder, f"{kind}_world") - self.assertEqual( - labels, - [f"{_DST.format(int(w))}/{kind}_{int(w)}" for w in worlds], - ) - - def test_builtin_and_tendon_labels_rewritten_per_world(self): - builder = _make_builder(self.worlds) - self._rename(builder) - self._assert_builtins(builder) - tendon_worlds = builder.custom_attributes["mujoco:tendon_world"].values - self.assertEqual( - builder.custom_attributes["mujoco:tendon_label"].values, - [f"{_DST.format(int(w))}/Tendon_{int(w)}" for w in tendon_worlds], - ) - - def test_source_root_boundary_cases(self): - builder = _make_builder(self.worlds) - builder.body_label.append(_SRC) - builder.body_world.append(self.worlds[0]) - self._rename(builder) - self.assertEqual(builder.body_label[-1], _DST.format(self.worlds[0])) - - builder = _make_builder(self.worlds) - rename_builder_labels(builder, [f"{_SRC}/"], [_DST], self.env_ids, self.mapping) - self._assert_builtins(builder) - - def test_unmatched_rows_left_untouched(self): - builder = _make_builder(self.worlds) - builder.body_label.append(f"{_SRC}/body_99") - builder.body_world.append(99) - builder.custom_attributes["mujoco:tendon_label"].values.append("named_tendon") - builder.custom_attributes["mujoco:tendon_world"].values.append(self.worlds[0]) - self._rename(builder) - self.assertEqual(builder.body_label[-1], f"{_SRC}/body_99") - self.assertEqual(builder.custom_attributes["mujoco:tendon_label"].values[-1], "named_tendon") - - def test_sparse_env_ids(self): - for worlds in ([10, 20, 30], [0, 1_000_000, 2_147_000_000]): - builder = newton.ModelBuilder() - SolverMuJoCo.register_custom_attributes(builder) - _inject_builtins(builder, ("body",), _SRC, worlds) - env_ids = torch.tensor(worlds, dtype=torch.int32) - rename_builder_labels(builder, [_SRC], [_DST], env_ids, torch.ones(1, len(worlds), dtype=torch.bool)) - self._assert_builtins(builder, ("body",)) - - class TestRenameCustomAttributes(unittest.TestCase): def setUp(self): self.worlds = [0, 1] - self.env_ids = torch.tensor(self.worlds, dtype=torch.int32) + self.env_ids = torch.tensor([10, 20], dtype=torch.int32) self.mapping = torch.ones(1, len(self.worlds), dtype=torch.bool) def test_custom_string_columns_follow_frequency_worlds(self): @@ -260,16 +166,9 @@ def test_custom_string_columns_follow_frequency_worlds(self): for column in columns: self.assertEqual( builder.custom_attributes[f"syn:{column}"].values, - [f"{_DST.format(int(w))}/{column}_{int(w)}" for w in worlds], + [f"{_DST.format(int(self.env_ids[w]))}/{column}_{int(w)}" for w in worlds], ) - def test_empty_custom_string_column_passes_through(self): - builder = newton.ModelBuilder() - _add_custom_frequency(builder, "freqA", ["freqA_label"]) - rename_builder_labels(builder, [_SRC], [_DST], self.env_ids, self.mapping) - _populate_custom_frequency(builder, "freqA", ["freqA_label"], self.worlds) - self.assertEqual(len(builder.custom_attributes["syn:freqA_label"].values), len(self.worlds)) - def test_custom_string_columns_ignore_unset_world_rows(self): builder = newton.ModelBuilder() _add_custom_frequency(builder, "freqA", ["freqA_label"]) @@ -281,7 +180,7 @@ def test_custom_string_columns_ignore_unset_world_rows(self): self.assertEqual( builder.custom_attributes["syn:freqA_label"].values, - ["unassigned", f"{_DST.format(self.worlds[0])}/freqA_label_{self.worlds[0]}"], + ["unassigned", f"{_DST.format(int(self.env_ids[0]))}/freqA_label_{self.worlds[0]}"], ) def test_shape_material_paths_follow_shape_worlds(self): @@ -300,7 +199,9 @@ def test_shape_material_paths_follow_shape_worlds(self): rename_builder_labels(builder, [_SRC], [_DST], self.env_ids, self.mapping) - self.assertEqual(paths, {index: f"{_DST.format(index)}/Looks/material" for index in range(len(self.worlds))}) + self.assertEqual( + paths, {index: f"{_DST.format(int(self.env_ids[index]))}/Looks/material" for index in self.worlds} + ) def test_other_shape_attributes_without_world_references_pass_through(self): builder = _make_builder(self.worlds) @@ -321,26 +222,6 @@ def test_other_shape_attributes_without_world_references_pass_through(self): self.assertEqual(notes, {index: f"{_SRC}/note" for index in range(len(self.worlds))}) -class TestRenameMultiSource(unittest.TestCase): - def test_prefix_overlap_does_not_cross_contaminate(self): - sources = ["/Sources/protoA", "/Sources/protoAB"] - builder = newton.ModelBuilder() - SolverMuJoCo.register_custom_attributes(builder) - builder.body_label.extend([f"{sources[0]}/body", f"{sources[1]}/body"] * 2) - builder.body_world.extend([0, 0, 1, 1]) - rename_builder_labels( - builder, - sources, - ["/World/envs/env_{}", "/World/envs/env_{}"], - torch.tensor([0, 1], dtype=torch.int32), - torch.tensor([[1, 1], [1, 1]], dtype=torch.bool), - ) - self.assertEqual( - builder.body_label, - ["/World/envs/env_0/body", "/World/envs/env_0/body", "/World/envs/env_1/body", "/World/envs/env_1/body"], - ) - - class TestReplicateBuilderMapping(unittest.TestCase): @staticmethod def _source_builder(root_path: str): @@ -349,6 +230,7 @@ def _source_builder(root_path: str): return builder def test_source_local_sites_batched_with_correct_indices(self): + source_path, destination = "/World/envs/env_0", "/World/envs/env_{}" source = newton.ModelBuilder() source.add_body(xform=wp.transform((2.0, 0.0, 0.0), wp.quat_identity())) site_idx = source.add_site(body=0, xform=wp.transform(), label="ee") @@ -364,13 +246,15 @@ def test_source_local_sites_batched_with_correct_indices(self): quaternions = torch.tensor([[0.0, 0.0, 0.0, 1.0]] * 3) with mock.patch.object(builder, "replicate", wraps=builder.replicate) as replicate: - local_site_map, _ = replicate_builder_mapping( + local_site_map, _, _ = replicate_builder_mapping( builder, - (_SRC,), + (source_path,), torch.ones((1, 3), dtype=torch.bool), positions, quaternions, - {_SRC: source}, + {source_path: source}, + destinations=(destination,), + env_ids=torch.arange(3), source_site_indices={id(source): {"ee": [site_idx]}}, ) @@ -379,10 +263,11 @@ def test_source_local_sites_batched_with_correct_indices(self): local_site_map["ee"], [[base_shape + world * stride + site_idx] for world in range(3)], ) - for world_indices in local_site_map["ee"]: - self.assertEqual(builder.shape_label[world_indices[0]], "ee") + for world, world_indices in enumerate(local_site_map["ee"]): + self.assertEqual(builder.shape_label[world_indices[0]], f"/World/envs/env_{world}/ee") def test_env_root_sites_batched_at_correct_world_positions(self): + source_path, destination = "/World/envs/env_0", "/World/envs/env_{}" source = newton.ModelBuilder() source.add_body(xform=wp.transform((2.0, 0.0, 0.0), wp.quat_identity())) @@ -393,13 +278,15 @@ def test_env_root_sites_batched_at_correct_world_positions(self): env_root_offset = wp.transform((0.1, 0.0, 0.0), wp.quat_identity()) with mock.patch.object(builder, "replicate", wraps=builder.replicate) as replicate: - local_site_map, _ = replicate_builder_mapping( + local_site_map, _, _ = replicate_builder_mapping( builder, - (_SRC,), + (source_path,), torch.ones((1, 3), dtype=torch.bool), positions, quaternions, - {_SRC: source}, + {source_path: source}, + destinations=(destination,), + env_ids=torch.arange(3, dtype=torch.long), env_root_sites={"origin": env_root_offset}, ) @@ -414,11 +301,12 @@ def test_env_root_sites_batched_at_correct_world_positions(self): site_pos = builder.shape_transform[world_indices[0]].p self.assertAlmostEqual(float(site_pos[0]), float(positions[world][0]) + 0.1, places=5) self.assertAlmostEqual(float(site_pos[1]), 0.0, places=5) - self.assertEqual(builder.shape_label[world_indices[0]], "origin") + self.assertEqual(builder.shape_label[world_indices[0]], f"/World/envs/env_{world}/origin") def test_inactive_source_rows_are_ignored(self): - sources = ("/Sources/inactive", "/Sources/active") + sources = ("/World/envs/env_0/inactive", "/World/envs/env_0/active") source_builders = {source: self._source_builder(source) for source in sources} + source_builders[sources[0]].body_label.append("/outside/the/plan") builder = _FakeVisualizationModelBuilder() replicate_builder_mapping( @@ -428,9 +316,11 @@ def test_inactive_source_rows_are_ignored(self): torch.tensor([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]), torch.tensor([[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]]), source_builders, + destinations=("/World/envs/env_{}/inactive", "/World/envs/env_{}/active"), + env_ids=torch.arange(2), ) - self.assertEqual(builder.geometry_sources_for_world(0), ["/Sources/active"]) + self.assertEqual(builder.geometry_sources_for_world(0), ["/World/envs/env_0/active"]) self.assertEqual(builder.geometry_sources_for_world(1), []) @@ -555,5 +445,110 @@ def test_visualization_builder_uses_clone_plan_sources_and_rewrites_labels(self) ) +class TestReplicationNamesItsCopies(unittest.TestCase): + _SRC = "/World/envs/env_0/Robot" + _ENV = "/World/envs/env_{}" + + def test_batched_prefixes_name_each_world_and_preserve_the_prototype(self): + source = newton.ModelBuilder() + body = source.add_body(xform=wp.transform(), label=self._SRC) + source.add_shape_box(body=body, label=f"{self._SRC}/shape") + child = source.add_link(xform=wp.transform(), label=f"{self._SRC}/link") + source.add_joint_revolute(parent=body, child=child, axis=(0.0, 0.0, 1.0), label=f"{self._SRC}/hinge") + original = { + name: list(getattr(source, name)) + for name in ("body_label", "joint_label", "shape_label", "articulation_label") + } + builder = newton.ModelBuilder() + env_ids = torch.tensor([10, 20], dtype=torch.long) + mapping = torch.ones(1, len(env_ids), dtype=torch.bool) + positions = torch.zeros((len(env_ids), 3), dtype=torch.float32) + quaternions = torch.zeros((len(env_ids), 4), dtype=torch.float32) + quaternions[:, 3] = 1.0 + replicate_builder_mapping( + builder, + [self._SRC], + mapping, + positions, + quaternions, + {self._SRC: source}, + destinations=["/World/envs/env_{}/Robot"], + env_ids=env_ids, + ) + for name, source_labels in original.items(): + expected = [ + label.replace(self._SRC, f"{self._ENV.format(i)}/Robot", 1) for i in env_ids for label in source_labels + ] + self.assertEqual(getattr(builder, name), expected) + self.assertEqual(getattr(source, name), source_labels) + + def test_hook_labels_are_rewritten_after_the_slow_path(self): + source = newton.ModelBuilder() + source.add_body(label=f"{self._SRC}/base") + builder = newton.ModelBuilder() + env_ids = torch.tensor([10, 20]) + mapping = torch.ones((1, 2), dtype=torch.bool) + + def hook(builder, *_): + builder.add_body(label=f"{self._SRC}/hook") + + replicate_builder_mapping( + builder, + (self._SRC,), + mapping, + torch.zeros((2, 3)), + torch.tensor([[0.0, 0.0, 0.0, 1.0]] * 2), + {self._SRC: source}, + destinations=("/World/envs/env_{}/Robot",), + env_ids=env_ids, + per_world_builder_hooks=(hook,), + ) + self.assertEqual( + builder.body_label, + [f"/World/envs/env_{env_id}/Robot/{label}" for env_id in env_ids for label in ("base", "hook")], + ) + + +class TestRootJointNaming(unittest.TestCase): + """The importer leaves a floating base's root joint unnamed; every other entity is named.""" + + _SOURCE = "/World/envs/env_0/Robot" + _BODY = "/World/envs/env_0/Robot/pelvis" + + @staticmethod + def _builder_with_free_root(body_label: str) -> newton.ModelBuilder: + builder = newton.ModelBuilder() + body = builder.add_link(xform=wp.transform(), label=body_label) + builder.add_joint_free(child=body) + return builder + + def test_a_generated_root_joint_name_becomes_its_body_path(self): + builder = self._builder_with_free_root(self._BODY) + self.assertFalse(builder.joint_label[0].startswith("/")) + + newton_clone_utils_module._name_root_joints_after_their_body(builder) + + self.assertEqual(builder.joint_label[0], f"{self._BODY}_free_joint") + + def test_other_joint_labels_are_left_alone(self): + named = self._builder_with_free_root(self._BODY) + named.joint_label[0] = "authored" + + non_free = newton.ModelBuilder() + parent = non_free.add_link(xform=wp.transform(), label=self._BODY) + child = non_free.add_link(xform=wp.transform(), label=f"{self._BODY}/link") + non_free.add_joint_revolute(parent=parent, child=child, axis=(0.0, 0.0, 1.0)) + + non_root = newton.ModelBuilder() + parent = non_root.add_link(xform=wp.transform(), label=self._BODY) + child = non_root.add_link(xform=wp.transform(), label=f"{self._BODY}/link") + non_root.add_joint_free(parent=parent, child=child) + + for builder in (named, non_free, non_root): + original = list(builder.joint_label) + newton_clone_utils_module._name_root_joints_after_their_body(builder) + self.assertEqual(builder.joint_label, original) + + if __name__ == "__main__": unittest.main() diff --git a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py index 2523cd405a65..a42c14634bf4 100644 --- a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py +++ b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py @@ -522,7 +522,8 @@ def test_mpm_solver_cfg_forwards_every_solver_field(field_name, value): @pytest.mark.parametrize("field_name, value", _KAMINO_PADMM_FIELD_VALUES) def test_kamino_solver_cfg_forwards_padmm_fields(field_name, value): """Every tunable P-ADMM cfg field round-trips into ``PADMMSolverConfig``.""" - solver_cfg = KaminoPADMMSolverCfg(dynamics_solver_cfg=KaminoPADMMCfg(**{field_name: value})) + sparse_kwargs = {"sparse_jacobian": True, "sparse_dynamics": True} if field_name == "penalty_update_method" else {} + solver_cfg = KaminoPADMMSolverCfg(**sparse_kwargs, dynamics_solver_cfg=KaminoPADMMCfg(**{field_name: value})) newton_cfg = solver_cfg.to_solver_config() assert hasattr(newton_cfg.padmm, field_name), ( f"{field_name!r} disappeared from PADMMSolverConfig — KaminoPADMMCfg needs to drop or rename it." @@ -530,6 +531,13 @@ def test_kamino_solver_cfg_forwards_padmm_fields(field_name, value): assert getattr(newton_cfg.padmm, field_name) == value +def test_kamino_padmm_rejects_adaptive_penalties_with_dense_dynamics(): + """Kamino's adaptive P-ADMM penalties require the sparse solver path.""" + solver_cfg = KaminoPADMMSolverCfg(dynamics_solver_cfg=KaminoPADMMCfg(penalty_update_method="balanced")) + with pytest.raises(ValueError, match="sparse_dynamics=True"): + solver_cfg.to_solver_config() + + @pytest.mark.parametrize("field_name, value", _KAMINO_DVI_FIELD_VALUES) def test_kamino_solver_cfg_forwards_dvi_fields(field_name, value): """Every tunable DVI cfg field round-trips into ``DVISolverConfig``.""" diff --git a/source/isaaclab_newton/test/physics/test_vbd_core.py b/source/isaaclab_newton/test/physics/test_vbd_core.py index 3604fa2d8fc7..4e0db788a4d4 100644 --- a/source/isaaclab_newton/test/physics/test_vbd_core.py +++ b/source/isaaclab_newton/test/physics/test_vbd_core.py @@ -132,7 +132,7 @@ def create_builder(cls, *, up_axis): def replicate(*args, **kwargs): replicate_calls.append(kwargs) - return {}, [object() for _ in env_paths] + return {}, [object() for _ in env_paths], [] monkeypatch.setattr(newton_module, "get_current_stage", lambda: stage) monkeypatch.setattr(pxr, "UsdGeom", usd_geom) diff --git a/tools/wheel_builder/uv-overrides.txt b/tools/wheel_builder/uv-overrides.txt index 95a0eec25afa..bbf4df976ab0 100644 --- a/tools/wheel_builder/uv-overrides.txt +++ b/tools/wheel_builder/uv-overrides.txt @@ -1,7 +1,7 @@ numpy>=2 mujoco~=3.11.0 mujoco-warp~=3.11.0 -newton[sim]==1.5.1 +newton[sim] @ git+https://github.com/newton-physics/newton.git@24bd863528d6b91137408930d0fbe8fa216ad962 newton-usd-schemas>=0.4.1 torch==2.11.0 torchvision==0.26.0 diff --git a/uv.lock b/uv.lock index d8053b9e0cdf..6b45cc44b090 100644 --- a/uv.lock +++ b/uv.lock @@ -20,7 +20,7 @@ overrides = [ { name = "coverage", specifier = ">=7.6.1" }, { name = "mujoco", specifier = "~=3.11.0" }, { name = "mujoco-warp", specifier = "~=3.11.0" }, - { name = "newton", extras = ["sim"], specifier = "==1.5.1" }, + { name = "newton", extras = ["sim"], git = "https://github.com/newton-physics/newton.git?rev=24bd863528d6b91137408930d0fbe8fa216ad962" }, { name = "newton-usd-schemas", specifier = ">=0.4.1" }, { name = "numpy", specifier = ">=2" }, { name = "packaging", specifier = ">=20,<27" }, @@ -2048,7 +2048,7 @@ requires-dist = [ { name = "meshio", specifier = ">=5.3.5" }, { name = "moviepy", marker = "extra == 'video'", specifier = ">=1.0.3,<2.0.0.dev0" }, { name = "myst-parser", marker = "extra == 'dev'" }, - { name = "newton", extras = ["sim"], specifier = ">=1.2.0" }, + { name = "newton", extras = ["sim"], git = "https://github.com/newton-physics/newton.git?rev=24bd863528d6b91137408930d0fbe8fa216ad962" }, { name = "newton-usd-schemas", specifier = ">=0.2.0" }, { name = "numba", specifier = ">=0.63.1" }, { name = "numpy", specifier = ">=2" }, @@ -3358,14 +3358,11 @@ wheels = [ [[package]] name = "newton" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } +version = "1.6.0.dev0" +source = { git = "https://github.com/newton-physics/newton.git?rev=24bd863528d6b91137408930d0fbe8fa216ad962#24bd863528d6b91137408930d0fbe8fa216ad962" } dependencies = [ { name = "warp-lang" }, ] -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/1d/d6a3e799c25cf04b5ab35b9903cafd09731df123a0c91f4d27caed28d431/newton-1.5.1-py3-none-any.whl", hash = "sha256:a757269fe03ca2e50edb9636b3c7eb91c2d1e359b6e9c992b33dc6d9058b5285", size = 5564220, upload-time = "2026-08-27T20:33:23.381Z" }, -] [package.optional-dependencies] sim = [