diff --git a/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml index cc24e3a5ad6..7b3b218b3b0 100644 --- a/source/isaaclab/config/extension.toml +++ b/source/isaaclab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.53.0" +version = "0.53.1" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index f2172b6190c..3e54d9e4a59 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog --------- +0.53.1 (2026-01-08) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added function :func:`~isaaclab.sim.utils.prims.change_prim_property` to change attributes on a USD prim. + This replaces the previously used USD command ``ChangeProperty`` that depends on Omniverse Kit API. + +Changed +^^^^^^^ + +* Replaced occurrences of ``ChangeProperty`` USD command to :func:`~isaaclab.sim.utils.prims.change_prim_property`. + + 0.53.0 (2026-01-07) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab/isaaclab/markers/visualization_markers.py b/source/isaaclab/isaaclab/markers/visualization_markers.py index 86514d45a8c..e8537a252ad 100644 --- a/source/isaaclab/isaaclab/markers/visualization_markers.py +++ b/source/isaaclab/isaaclab/markers/visualization_markers.py @@ -24,7 +24,6 @@ import torch from dataclasses import MISSING -import omni.kit.commands import omni.physx.scripts.utils as physx_utils from pxr import Gf, PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, Vt @@ -396,18 +395,12 @@ def _process_prototype_prim(self, prim: Usd.Prim): child_prim.SetInstanceable(False) # check if prim is a mesh -> if so, make it invisible to secondary rays if child_prim.IsA(UsdGeom.Gprim): - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "ChangePropertyCommand" kit command - sim_utils.attach_stage_to_usd_context(attaching_early=True) - # invisible to secondary rays such as depth images - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(f"{child_prim.GetPrimPath().pathString}.primvars:invisibleToSecondaryRays"), + sim_utils.change_prim_property( + prop_path=f"{child_prim.GetPrimPath().pathString}.primvars:invisibleToSecondaryRays", value=True, - prev=None, + stage=prim.GetStage(), type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, - usd_context_name=prim.GetStage(), ) # add children to list all_prims += child_prim.GetChildren() diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 0319619fb7d..4a79a908bab 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -15,7 +15,7 @@ from isaaclab.sim.converters.asset_converter_base import AssetConverterBase from isaaclab.sim.converters.mesh_converter_cfg import MeshConverterCfg from isaaclab.sim.schemas import schemas -from isaaclab.sim.utils import export_prim_to_file +from isaaclab.sim.utils import delete_prim, export_prim_to_file # import logger logger = logging.getLogger(__name__) @@ -173,7 +173,7 @@ def _convert_asset(self, cfg: MeshConverterCfg): ) # Delete the original prim that will now be a reference geom_prim_path = geom_prim.GetPath().pathString - omni.kit.commands.execute("DeletePrims", paths=[geom_prim_path], stage=stage) + delete_prim(geom_prim_path, stage=stage) # Update references to exported Xform and make it instanceable geom_undef_prim = stage.DefinePrim(geom_prim_path) geom_undef_prim.GetReferences().AddReference(self.usd_instanceable_meshes_path, primPath=geom_prim_path) @@ -220,7 +220,6 @@ async def _convert_mesh_to_usd(in_file: str, out_file: str, load_materials: bool enable_extension("omni.kit.asset_converter") import omni.kit.asset_converter - import omni.usd # Create converter context converter_context = omni.kit.asset_converter.AssetConverterContext() diff --git a/source/isaaclab/isaaclab/sim/converters/urdf_converter.py b/source/isaaclab/isaaclab/sim/converters/urdf_converter.py index 8770e2c368b..eb5e00e1209 100644 --- a/source/isaaclab/isaaclab/sim/converters/urdf_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/urdf_converter.py @@ -10,7 +10,6 @@ from packaging.version import Version from typing import TYPE_CHECKING -import isaacsim import omni.kit.app import omni.kit.commands diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py index 76a4966616b..242829d777e 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py @@ -17,11 +17,11 @@ add_labels, bind_physics_material, bind_visual_material, + change_prim_property, clone, create_prim, get_current_stage, get_first_matching_child_prim, - is_current_stage_in_memory, select_usd_variants, set_prim_visibility, ) @@ -231,25 +231,13 @@ def spawn_ground_plane( # Change the color of the plane # Warning: This is specific to the default grid plane asset. if cfg.color is not None: - # avoiding this step if stage is in memory since the "ChangePropertyCommand" kit command - # is not supported in stage in memory - if is_current_stage_in_memory(): - logger.warning( - "Ground plane color modification is not supported while the stage is in memory. Skipping operation." - ) - - else: - prop_path = f"{prim_path}/Looks/theGrid/Shader.inputs:diffuse_tint" - - # change the color - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(prop_path), - value=Gf.Vec3f(*cfg.color), - prev=None, - type_to_create_if_not_exist=Sdf.ValueTypeNames.Color3f, - usd_context_name=stage, - ) + # change the color + change_prim_property( + prop_path=f"{prim_path}/Looks/theGrid/Shader.inputs:diffuse_tint", + value=Gf.Vec3f(*cfg.color), + stage=stage, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Color3f, + ) # Remove the light from the ground plane # It isn't bright enough and messes up with the user's lighting settings omni.kit.commands.execute("ToggleVisibilitySelectedPrims", selected_paths=[f"{prim_path}/SphereLight"], stage=stage) diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py b/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py index 25658786467..074d6ac0e43 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py @@ -8,10 +8,10 @@ import logging from typing import TYPE_CHECKING -import omni.kit.commands -from pxr import Usd +from omni.usd.commands import CreateMdlMaterialPrimCommand, CreateShaderPrimFromSdrCommand +from pxr import Usd, UsdShade -from isaaclab.sim.utils import attach_stage_to_usd_context, clone, safe_set_attribute_on_usd_prim +from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_prim from isaaclab.sim.utils.stage import get_current_stage from isaaclab.utils.assets import NVIDIA_NUCLEUS_DIR @@ -30,9 +30,9 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa both *specular* and *metallic* workflows. All color inputs are in linear color space (RGB). For more information, see the `documentation `__. - The function calls the USD command `CreatePreviewSurfaceMaterialPrim`_ to create the prim. + The function calls the USD command `CreateShaderPrimFromSdrCommand`_ to create the prim. - .. _CreatePreviewSurfaceMaterialPrim: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.CreatePreviewSurfaceMaterialPrimCommand.html + .. _CreateShaderPrimFromSdrCommand: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.CreateShaderPrimFromSdrCommand.html .. note:: This function is decorated with :func:`clone` that resolves prim path into list of paths @@ -55,18 +55,39 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa # spawn material if it doesn't exist. if not stage.GetPrimAtPath(prim_path).IsValid(): - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "CreatePreviewSurfaceMaterialPrim" kit command - attach_stage_to_usd_context(attaching_early=True) - - omni.kit.commands.execute("CreatePreviewSurfaceMaterialPrim", mtl_path=prim_path, select_new_prim=False) + # note: we don't use Omniverse's CreatePreviewSurfaceMaterialPrimCommand + # since it does not support USD stage as an argument. The created material + # in that case is always the one from USD Context which makes it difficult to + # handle scene creation on a custom stage. + material_prim = UsdShade.Material.Define(stage, prim_path) + if material_prim: + shader_prim = CreateShaderPrimFromSdrCommand( + parent_path=prim_path, + identifier="UsdPreviewSurface", + stage_or_context=stage, + name="Shader", + ).do() + # bind the shader graph to the material + if shader_prim: + surface_out = shader_prim.GetOutput("surface") + if surface_out: + material_prim.CreateSurfaceOutput().ConnectToSource(surface_out) + + displacement_out = shader_prim.GetOutput("displacement") + if displacement_out: + material_prim.CreateDisplacementOutput().ConnectToSource(displacement_out) + else: + raise ValueError(f"Failed to create preview surface shader at path: '{prim_path}'.") else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") # obtain prim prim = stage.GetPrimAtPath(f"{prim_path}/Shader") + # check prim is valid + if not prim.IsValid(): + raise ValueError(f"Failed to create preview surface material at path: '{prim_path}'.") # apply properties - cfg = cfg.to_dict() + cfg = cfg.to_dict() # type: ignore del cfg["func"] for attr_name, attr_value in cfg.items(): safe_set_attribute_on_usd_prim(prim, f"inputs:{attr_name}", attr_value, camel_case=True) @@ -75,7 +96,9 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa @clone -def spawn_from_mdl_file(prim_path: str, cfg: visual_materials_cfg.MdlMaterialCfg) -> Usd.Prim: +def spawn_from_mdl_file( + prim_path: str, cfg: visual_materials_cfg.MdlFileCfg | visual_materials_cfg.GlassMdlCfg +) -> Usd.Prim: """Load a material from its MDL file and override the settings with the given config. NVIDIA's `Material Definition Language (MDL) `__ @@ -108,25 +131,24 @@ def spawn_from_mdl_file(prim_path: str, cfg: visual_materials_cfg.MdlMaterialCfg # spawn material if it doesn't exist. if not stage.GetPrimAtPath(prim_path).IsValid(): - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "CreateMdlMaterialPrim" kit command - attach_stage_to_usd_context(attaching_early=True) - # extract material name from path material_name = cfg.mdl_path.split("/")[-1].split(".")[0] - omni.kit.commands.execute( - "CreateMdlMaterialPrim", + CreateMdlMaterialPrimCommand( mtl_url=cfg.mdl_path.format(NVIDIA_NUCLEUS_DIR=NVIDIA_NUCLEUS_DIR), mtl_name=material_name, mtl_path=prim_path, + stage=stage, select_new_prim=False, - ) + ).do() else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") # obtain prim prim = stage.GetPrimAtPath(f"{prim_path}/Shader") + # check prim is valid + if not prim.IsValid(): + raise ValueError(f"Failed to create MDL material at path: '{prim_path}'.") # apply properties - cfg = cfg.to_dict() + cfg = cfg.to_dict() # type: ignore del cfg["func"] del cfg["mdl_path"] for attr_name, attr_value in cfg.items(): diff --git a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py index 3e4d7635a45..6270447169e 100644 --- a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py +++ b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py @@ -8,10 +8,9 @@ import logging from typing import TYPE_CHECKING -import omni.kit.commands from pxr import Sdf, Usd -from isaaclab.sim.utils import attach_stage_to_usd_context, clone, create_prim, get_current_stage +from isaaclab.sim.utils import change_prim_property, clone, create_prim, get_current_stage from isaaclab.utils import to_camel_case if TYPE_CHECKING: @@ -94,17 +93,11 @@ def spawn_camera( # lock camera from viewport (this disables viewport movement for camera) if cfg.lock_camera: - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "ChangePropertyCommand" kit command - attach_stage_to_usd_context(attaching_early=True) - - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(f"{prim_path}.omni:kit:cameraLock"), + change_prim_property( + prop_path=f"{prim_path}.omni:kit:cameraLock", value=True, - prev=None, + stage=stage, type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, - usd_context_name=stage, ) # decide the custom attributes to add if cfg.projection_type == "pinhole": diff --git a/source/isaaclab/isaaclab/sim/utils/prims.py b/source/isaaclab/isaaclab/sim/utils/prims.py index a81ccd2232e..432bc17e131 100644 --- a/source/isaaclab/isaaclab/sim/utils/prims.py +++ b/source/isaaclab/isaaclab/sim/utils/prims.py @@ -15,12 +15,9 @@ from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any -import omni import omni.kit.commands import omni.usd -import usdrt # noqa: F401 from isaacsim.core.cloner import Cloner -from omni.usd.commands import DeletePrimsCommand, MovePrimCommand from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, UsdShade, UsdUtils from isaaclab.utils.string import to_camel_case @@ -28,7 +25,7 @@ from .queries import find_matching_prim_paths from .semantics import add_labels -from .stage import attach_stage_to_usd_context, get_current_stage, get_current_stage_id +from .stage import get_current_stage, get_current_stage_id from .transforms import convert_world_pose_to_local, standardize_xform_ops if TYPE_CHECKING: @@ -188,7 +185,7 @@ def create_prim( return prim -def delete_prim(prim_path: str | Sequence[str], stage: Usd.Stage | None = None) -> None: +def delete_prim(prim_path: str | Sequence[str], stage: Usd.Stage | None = None) -> bool: """Removes the USD Prim and its descendants from the scene if able. Args: @@ -196,6 +193,9 @@ def delete_prim(prim_path: str | Sequence[str], stage: Usd.Stage | None = None) the function will delete all the prims in the list. stage: The stage to delete the prim in. Defaults to None, in which case the current stage is used. + Returns: + True if the prim or prims were deleted successfully, False otherwise. + Example: >>> import isaaclab.sim as sim_utils >>> @@ -213,10 +213,15 @@ def delete_prim(prim_path: str | Sequence[str], stage: Usd.Stage | None = None) if stage_id < 0: stage_id = stage_cache.Insert(stage).ToLongInt() # delete prims - DeletePrimsCommand(prim_path, stage=stage).do() + success, _ = omni.kit.commands.execute( + "DeletePrimsCommand", + paths=prim_path, + stage=stage, + ) + return success -def move_prim(path_from: str, path_to: str, keep_world_transform: bool = True, stage: Usd.Stage | None = None) -> None: +def move_prim(path_from: str, path_to: str, keep_world_transform: bool = True, stage: Usd.Stage | None = None) -> bool: """Moves a prim from one path to another within a USD stage. This function moves the prim from the source path to the destination path. If the :attr:`keep_world_transform` @@ -234,6 +239,9 @@ def move_prim(path_from: str, path_to: str, keep_world_transform: bool = True, s keep_world_transform: Whether to keep the world transform of the prim. Defaults to True. stage: The stage to move the prim in. Defaults to None, in which case the current stage is used. + Returns: + True if the prim was moved successfully, False otherwise. + Example: >>> import isaaclab.sim as sim_utils >>> @@ -243,9 +251,14 @@ def move_prim(path_from: str, path_to: str, keep_world_transform: bool = True, s # get stage handle stage = get_current_stage() if stage is None else stage # move prim - MovePrimCommand( - path_from=path_from, path_to=path_to, keep_world_transform=keep_world_transform, stage_or_context=stage - ).do() + success, _ = omni.kit.commands.execute( + "MovePrimCommand", + path_from=path_from, + path_to=path_to, + keep_world_transform=keep_world_transform, + stage_or_context=stage, + ) + return success """ @@ -393,21 +406,105 @@ def safe_set_attribute_on_usd_prim(prim: Usd.Prim, attr_name: str, value: Any, c f"Cannot set attribute '{attr_name}' with value '{value}'. Please modify the code to support this type." ) - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "ChangePropertyCommand" kit command - attach_stage_to_usd_context(attaching_early=True) - - # change property - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(f"{prim.GetPath()}.{attr_name}"), + # change property using the change_prim_property function + change_prim_property( + prop_path=f"{prim.GetPath()}.{attr_name}", value=value, - prev=None, + stage=prim.GetStage(), type_to_create_if_not_exist=sdf_type, - usd_context_name=prim.GetStage(), ) +def change_prim_property( + prop_path: str | Sdf.Path, + value: Any, + stage: Usd.Stage | None = None, + type_to_create_if_not_exist: Sdf.ValueTypeNames | None = None, + is_custom: bool = False, +) -> bool: + """Change or create a property value on a USD prim. + + This is a simplified property setter that works with the current edit target. If you need + complex layer management, use :class:`omni.kit.commands.ChangePropertyCommand` instead. + + By default, this function changes the value of the property when it exists. If the property + doesn't exist, :attr:`type_to_create_if_not_exist` must be provided to create it. + + Note: + The attribute :attr:`value` must be the correct type for the property. + For example, if the property is a float, the value must be a float. + If it is supposed to be a RGB color, the value must be of type :class:`Gf.Vec3f`. + + Args: + prop_path: Property path in the format ``/World/Prim.propertyName``. + value: Value to set. If None, the attribute value goes to its default value. + If the attribute has no default value, it is a silent no-op. + stage: The USD stage. Defaults to None, in which case the current stage is used. + type_to_create_if_not_exist: If not None and property doesn't exist, a new property will + be created with the given type and value. Defaults to None. + is_custom: If the property is created, specify if it is a custom property (not part of + the schema). Defaults to False. + + Returns: + True if the property was successfully changed, False otherwise. + + Raises: + ValueError: If the prim does not exist at the specified path. + + Example: + >>> import isaaclab.sim as sim_utils + >>> from pxr import Sdf + >>> + >>> # Change an existing property + >>> sim_utils.change_prim_property( + ... prop_path="/World/Cube.size", + ... value=2.0 + ... ) + True + >>> + >>> # Create a new custom property + >>> sim_utils.change_prim_property( + ... prop_path="/World/Cube.customValue", + ... value=42, + ... type_to_create_if_not_exist=Sdf.ValueTypeNames.Int, + ... is_custom=True + ... ) + True + """ + # get stage handle + stage = get_current_stage() if stage is None else stage + + # convert to Sdf.Path if needed + prop_path = Sdf.Path(prop_path) if isinstance(prop_path, str) else prop_path + + # get the prim path + prim_path = prop_path.GetAbsoluteRootOrPrimPath() + prim = stage.GetPrimAtPath(prim_path) + if not prim or not prim.IsValid(): + raise ValueError(f"Prim does not exist at path: '{prim_path}'") + + # get or create the property + prop = stage.GetPropertyAtPath(prop_path) + + if not prop: + if type_to_create_if_not_exist is not None: + # create new attribute on the prim + prop = prim.CreateAttribute(prop_path.name, type_to_create_if_not_exist, is_custom) + else: + logger.error(f"Property {prop_path} does not exist and 'type_to_create_if_not_exist' was not provided.") + return False + + if not prop: + logger.error(f"Failed to get or create property at path: '{prop_path}'") + return False + + # set the value + if value is None: + return bool(prop.Clear()) + else: + return bool(prop.Set(value, Usd.TimeCode.Default())) + + """ Exporting. """ @@ -709,6 +806,7 @@ def bind_visual_material( raise ValueError(f"Visual material '{material_path}' does not exist.") # resolve token for weaker than descendants + # bind material command expects a string token if stronger_than_descendants: binding_strength = "strongerThanDescendants" else: @@ -862,19 +960,14 @@ def _add_reference_to_prim(prim: Usd.Prim) -> Usd.Prim: ret_val = get_metrics_assembler_interface().check_layers( stage.GetRootLayer().identifier, sdf_layer.identifier, stage_id ) + # log that metric assembler did not detect any issues if ret_val["ret_val"]: - try: - import omni.metrics.assembler.ui - - omni.kit.commands.execute( - "AddReference", stage=stage, prim_path=prim.GetPath(), reference=Sdf.Reference(usd_path) - ) - - return prim - except Exception: - return _add_reference_to_prim(prim) - else: - return _add_reference_to_prim(prim) + logger.info( + "Metric assembler detected no issues between the current stage and the referenced USD file at path:" + f" {usd_path}" + ) + # add reference to the prim + return _add_reference_to_prim(prim) def get_usd_references(prim_path: str, stage: Usd.Stage | None = None) -> list[str]: diff --git a/source/isaaclab/test/sim/test_utils_prims.py b/source/isaaclab/test/sim/test_utils_prims.py index 3b805ada017..50a412a8848 100644 --- a/source/isaaclab/test/sim/test_utils_prims.py +++ b/source/isaaclab/test/sim/test_utils_prims.py @@ -469,6 +469,177 @@ def test_select_usd_variants_in_usd_file(): # assert variant_set.GetVariantSelection() == "Robotiq_2f_140" +""" +Property Management. +""" + + +def test_change_prim_property_basic(): + """Test change_prim_property() with existing property.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a cube prim + prim = sim_utils.create_prim("/World/Cube", "Cube", stage=stage, attributes={"size": 1.0}) + + # check initial value + assert prim.GetAttribute("size").Get() == 1.0 + + # change the property + result = sim_utils.change_prim_property( + prop_path="/World/Cube.size", + value=2.0, + stage=stage, + ) + + # check that the change was successful + assert result is True + assert prim.GetAttribute("size").Get() == 2.0 + + +def test_change_prim_property_create_new(): + """Test change_prim_property() creates new property when it doesn't exist.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a prim + prim = sim_utils.create_prim("/World/Test", "Xform", stage=stage) + + # check that the property doesn't exist + assert prim.GetAttribute("customValue").Get() is None + + # create a new property + result = sim_utils.change_prim_property( + prop_path="/World/Test.customValue", + value=42, + stage=stage, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Int, + is_custom=True, + ) + + # check that the property was created successfully + assert result is True + assert prim.GetAttribute("customValue").Get() == 42 + + +def test_change_prim_property_clear_value(): + """Test change_prim_property() clears property value when value is None.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a cube with an attribute + prim = sim_utils.create_prim("/World/Cube", "Cube", stage=stage, attributes={"size": 1.0}) + + # check initial value + assert prim.GetAttribute("size").Get() == 1.0 + + # clear the property value + result = sim_utils.change_prim_property( + prop_path="/World/Cube.size", + value=None, + stage=stage, + ) + + # check that the value was cleared + assert result is True + # Note: After clearing, the attribute should go its default value + assert prim.GetAttribute("size").Get() == 2.0 + + +@pytest.mark.parametrize( + "attr_name,value,value_type,expected", + [ + ("floatValue", 3.14, Sdf.ValueTypeNames.Float, 3.14), + ("boolValue", True, Sdf.ValueTypeNames.Bool, True), + ("intValue", 42, Sdf.ValueTypeNames.Int, 42), + ("stringValue", "test", Sdf.ValueTypeNames.String, "test"), + ("vec3Value", Gf.Vec3f(1.0, 2.0, 3.0), Sdf.ValueTypeNames.Float3, Gf.Vec3f(1.0, 2.0, 3.0)), + ("colorValue", Gf.Vec3f(1.0, 0.0, 0.5), Sdf.ValueTypeNames.Color3f, Gf.Vec3f(1.0, 0.0, 0.5)), + ], + ids=["float", "bool", "int", "string", "vec3", "color"], +) +def test_change_prim_property_different_types(attr_name: str, value, value_type, expected): + """Test change_prim_property() with different value types.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a prim + prim = sim_utils.create_prim("/World/Test", "Xform", stage=stage) + + # change the property + result = sim_utils.change_prim_property( + prop_path=f"/World/Test.{attr_name}", + value=value, + stage=stage, + type_to_create_if_not_exist=value_type, + is_custom=True, + ) + + # check that the change was successful + assert result is True + actual_value = prim.GetAttribute(attr_name).Get() + + # handle float comparison separately for precision + if isinstance(expected, float): + assert math.isclose(actual_value, expected, abs_tol=1e-6) + else: + assert actual_value == expected + + +@pytest.mark.parametrize( + "prop_path_input", + ["/World/Cube.size", Sdf.Path("/World/Cube.size")], + ids=["str_path", "sdf_path"], +) +def test_change_prim_property_path_types(prop_path_input): + """Test change_prim_property() with different path input types.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a cube prim + prim = sim_utils.create_prim("/World/Cube", "Cube", stage=stage, attributes={"size": 1.0}) + + # change property using different path types + result = sim_utils.change_prim_property( + prop_path=prop_path_input, + value=3.0, + stage=stage, + ) + + # check that the change was successful + assert result is True + assert prim.GetAttribute("size").Get() == 3.0 + + +def test_change_prim_property_error_invalid_prim(): + """Test change_prim_property() raises error for invalid prim path.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # try to change property on non-existent prim + with pytest.raises(ValueError, match="Prim does not exist"): + sim_utils.change_prim_property( + prop_path="/World/NonExistent.property", + value=1.0, + stage=stage, + ) + + +def test_change_prim_property_error_missing_type(): + """Test change_prim_property() returns False when property doesn't exist and type not provided.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a prim + prim = sim_utils.create_prim("/World/Test", "Xform", stage=stage) + + # try to create property without providing type + result = sim_utils.change_prim_property( + prop_path="/World/Test.nonExistentProperty", + value=42, + stage=stage, + ) + + # should return False since type was not provided + assert result is False + # property should not have been created + assert prim.GetAttribute("nonExistentProperty").Get() is None + + """ Internal Helpers. """ diff --git a/source/isaaclab/test/sim/test_utils_transforms.py b/source/isaaclab/test/sim/test_utils_transforms.py index f5e23574b2e..46fbecd90b6 100644 --- a/source/isaaclab/test/sim/test_utils_transforms.py +++ b/source/isaaclab/test/sim/test_utils_transforms.py @@ -593,8 +593,8 @@ def test_standardize_xform_ops_with_partial_values(): assert_quat_close(Gf.Quatd(*quat_before), quat_after_world, eps=1e-5) -def test_standardize_xform_ops_non_xformable_prim(): - """Test standardize_xform_ops returns False for non-Xformable prims.""" +def test_standardize_xform_ops_non_xformable_prim(caplog): + """Test standardize_xform_ops returns False for non-Xformable prims and logs error.""" # obtain stage handle stage = sim_utils.get_current_stage() @@ -607,10 +607,21 @@ def test_standardize_xform_ops_non_xformable_prim(): assert material_prim.IsValid() assert not material_prim.IsA(UsdGeom.Xformable) - # Attempt to apply standardize_xform_ops - should return False - result = sim_utils.standardize_xform_ops(material_prim) + # Clear any previous logs + caplog.clear() + + # Attempt to apply standardize_xform_ops - should return False and log a error + with caplog.at_level("ERROR"): + result = sim_utils.standardize_xform_ops(material_prim) + assert result is False + # Verify that a error was logged + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + assert "not an Xformable" in caplog.records[0].message + assert "/World/TestMaterial" in caplog.records[0].message + def test_standardize_xform_ops_preserves_reset_xform_stack(): """Test that standardize_xform_ops preserves the resetXformStack attribute."""