Unverified Commit b124a55d authored by Mayank Mittal's avatar Mayank Mittal Committed by GitHub

Adds methods to set physics-based schemas (#110)

# Description

This MR introduces wrappers around different [USD
Physics](https://openusd.org/dev/api/usd_physics_page_front.html) and
[PhysX
solver](https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/index.html)
schemas. The functions allow modifying the properties on an asset prim
using configuration objects.

The schemas supersede the current `omni.isaac.orbit.utils.kit.py` which
did the same job but had duplication in the implementations.

## Type of change

- New feature (non-breaking change which adds functionality)
- This change requires a documentation update

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./orbit.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file

---------
Signed-off-by: 's avatarMayank Mittal <12863862+Mayankm96@users.noreply.github.com>
parent c438fe97
[package]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.8.7"
version = "0.8.8"
# Description
title = "ORBIT framework for Robot Learning"
......
Changelog
---------
0.8.8 (2023-08-09)
~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added configuration classes and functions for setting different physics-based schemas in the
:mod:`omni.isaac.orbit.sim.schemas` module. These allow modifying properties of the physics solver
on the asset using configuration objects.
0.8.7 (2023-08-03)
~~~~~~~~~~~~~~~~~~
......
......@@ -3,13 +3,17 @@
#
# SPDX-License-Identifier: BSD-3-Clause
"""This sub-module provides the ``SimulationContext`` class.
"""This sub-module contains utilities for simulation.
To make it convenient to use the module, we recommend importing the module as follows:
.. code-block:: python
import omni.isaac.orbit.sim as sim_utils
The :class:`SimulationContext` inherits from the :class:`omni.isaac.core.simulation_context.SimulationContext` class
to provide additional functionality for the Orbit extension. This includes configuring the simulation through
the configuration class :class:`SimulationCfg` and providing a context manager for the simulation.
"""
from .schemas import * # noqa: F401, F403
from .simulation_cfg import PhysicsMaterialCfg, PhysxCfg, SimulationCfg
from .simulation_context import SimulationContext
......
# Copyright [2023] Boston Dynamics AI Institute, Inc.
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES, ETH Zurich, and University of Toronto
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Sub-module containing utilities for schemas used in Omniverse.
We wrap the USD schemas for PhysX and USD Physics in a more convenient API for setting the parameters from
Python. This is done so that configuration objects can define the schema properties to set and make it easier
to tune the physics parameters without requiring to open Omniverse Kit and manually set the parameters into
the respective USD attributes.
.. caution::
Schema properties cannot be applied on prims that are prototypes as they are read-only prims. This
particularly affects instanced assets where some of the prims (usually the visual and collision meshes)
are prototypes so that the instancing can be done efficiently.
In such cases, it is assumed that the prototypes have sim-ready properties on them that don't need to be modified.
Trying to set properties into prototypes will throw a warning saying that the prim is a prototype and the
properties cannot be set.
The schemas are defined in the following links:
* `UsdPhysics schema <https://openusd.org/dev/api/usd_physics_page_front.html>`_
* `PhysxSchema schema <https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/index.html>`_
Locally, the schemas are defined in the following files:
* ``_isaac_sim/kit/extsPhysics/omni.usd.schema.physics/plugins/UsdPhysics/resources/UsdPhysics/schema.usda``
* ``_isaac_sim/kit/extsPhysics/omni.usd.schema.physx/plugins/PhysxSchema/resources/PhysxSchema/schema.usda``
"""
from .schemas import (
set_articulation_root_properties,
set_collision_properties,
set_mass_properties,
set_rigid_body_properties,
)
from .schemas_cfg import (
ArticulationRootPropertiesCfg,
CollisionPropertiesCfg,
MassPropertiesCfg,
RigidBodyPropertiesCfg,
)
__all__ = [
# articulation root
"ArticulationRootPropertiesCfg",
"set_articulation_root_properties",
# rigid bodies
"RigidBodyPropertiesCfg",
"set_rigid_body_properties",
# colliders
"CollisionPropertiesCfg",
"set_collision_properties",
# mass
"MassPropertiesCfg",
"set_mass_properties",
]
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES, ETH Zurich, and University of Toronto
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import omni.isaac.core.utils.stage as stage_utils
from pxr import PhysxSchema, Usd, UsdPhysics
from ..utils import apply_nested, safe_set_attribute_on_usd_schema
from . import schemas_cfg
@apply_nested
def set_articulation_root_properties(
prim_path: str, cfg: schemas_cfg.ArticulationRootPropertiesCfg, stage: Usd.Stage = None
):
"""Set PhysX parameters for an articulation root prim.
The `articulation root`_ marks the root of an articulation tree. For floating articulations, this should be on
the root body. For fixed articulations, this API can be on a direct or indirect parent of the root joint
which is fixed to the world.
The schema comprises of attributes that belong to the `ArticulationRootAPI`_ and `PhysxArticulationAPI`_.
schemas. The latter contains the PhysX parameters for the articulation root.
The properties are applied to the articulation root prim. The common properties (such as solver position
and velocity iteration counts, sleep threshold, stabilization threshold) take precedence over those specified
in the rigid body schemas for all the rigid bodies in the articulation.
.. note::
This function is decorated with :func:`apply_nested` that set the properties to all the prims
(that have the schema applied on them) under the input prim path.
.. _articulation root: https://nvidia-omniverse.github.io/PhysX/physx/5.2.1/docs/Articulations.html
.. _ArticulationRootAPI: https://openusd.org/dev/api/class_usd_physics_articulation_root_a_p_i.html
.. _PhysxArticulationAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_articulation_a_p_i.html
Args:
prim_path (str): The prim path to the articulation root.
cfg (schemas_cfg.ArticulationRootPropertiesCfg): The configuration for the articulation root.
stage (Usd.Stage, optional): The stage where to find the prim. Defaults to None, in which case the
current stage is used.
"""
# obtain stage
if stage is None:
stage = stage_utils.get_current_stage()
# get articulation USD prim
articulation_prim = stage.GetPrimAtPath(prim_path)
# check if prim has articulation applied on it
if not UsdPhysics.ArticulationRootAPI(articulation_prim):
return False
# retrieve the articulation api
physx_articulation_api = PhysxSchema.PhysxArticulationAPI(articulation_prim)
if not physx_articulation_api:
physx_articulation_api = PhysxSchema.PhysxArticulationAPI.Apply(articulation_prim)
# convert to dict
cfg = cfg.to_dict()
# set into physx api
for attr_name, value in cfg.items():
safe_set_attribute_on_usd_schema(physx_articulation_api, attr_name, value)
# success
return True
@apply_nested
def set_rigid_body_properties(prim_path: str, cfg: schemas_cfg.RigidBodyPropertiesCfg, stage: Usd.Stage = None):
"""Set PhysX parameters for a rigid body prim.
A `rigid body`_ is a single body that can be simulated by PhysX. It can be either dynamic or kinematic.
A dynamic body responds to forces and collisions. A `kinematic body`_ can be moved by the user, but does not
respond to forces. They are similar to having static bodies that can be moved around.
The schema comprises of attributes that belong to the `RigidBodyAPI`_ and `PhysxRigidBodyAPI`_.
schemas. The latter contains the PhysX parameters for the rigid body.
.. note::
This function is decorated with :func:`apply_nested` that sets the properties to all the prims
(that have the schema applied on them) under the input prim path.
.. _rigid body: https://nvidia-omniverse.github.io/PhysX/physx/5.2.1/docs/RigidBodyOverview.html
.. _kinematic body: https://openusd.org/release/wp_rigid_body_physics.html#kinematic-bodies
.. _RigidBodyAPI: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html
.. _PhysxRigidBodyAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_rigid_body_a_p_i.html
Args:
prim_path (str): The prim path to the rigid body.
cfg (schemas_cfg.RigidBodyPropertiesCfg): The configuration for the rigid body.
stage (Usd.Stage, optional): The stage where to find the prim. Defaults to None, in which case the
current stage is used.
"""
# obtain stage
if stage is None:
stage = stage_utils.get_current_stage()
# get rigid-body USD prim
rigid_body_prim = stage.GetPrimAtPath(prim_path)
# check if prim has rigid-body applied on it
if not UsdPhysics.RigidBodyAPI(rigid_body_prim):
return False
# retrieve the USD rigid-body api
usd_rigid_body_api = UsdPhysics.RigidBodyAPI(rigid_body_prim)
# retrieve the physx rigid-body api
physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI(rigid_body_prim)
if not physx_rigid_body_api:
physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI.Apply(rigid_body_prim)
# convert to dict
cfg = cfg.to_dict()
# set into USD API
for attr_name in ["rigid_body_enabled", "kinematic_enabled"]:
value = cfg.pop(attr_name, None)
safe_set_attribute_on_usd_schema(usd_rigid_body_api, attr_name, value)
# set into PhysX API
for attr_name, value in cfg.items():
safe_set_attribute_on_usd_schema(physx_rigid_body_api, attr_name, value)
# success
return True
@apply_nested
def set_collision_properties(prim_path: str, cfg: schemas_cfg.CollisionPropertiesCfg, stage: Usd.Stage = None):
"""Set PhysX properties of collider prim.
These properties are based on the `UsdPhysics.CollisionAPI` and `PhysxSchema.PhysxCollisionAPI`_ schemas.
For more information on the properties, please refer to the official documentation.
Tuning these parameters influence the contact behavior of the rigid body. For more information on
tune them and their effect on the simulation, please refer to the
`PhysX documentation <https://nvidia-omniverse.github.io/PhysX/physx/5.2.1/docs/AdvancedCollisionDetection.html>`_.
.. note::
This function is decorated with :func:`apply_nested` that sets the properties to all the prims
(that have the schema applied on them) under the input prim path.
.. UsdPhysics.CollisionAPI: https://openusd.org/dev/api/class_usd_physics_collision_a_p_i.html
.. PhysxSchema.PhysxCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_collision_a_p_i.html
Args:
prim_path (str): The prim path of parent.
cfg (schemas_cfg.CollisionPropertiesCfg): The configuration for the collider.
stage (Usd.Stage, optional): The stage where to find the prim. Defaults to None, in which case the
current stage is used.
"""
# obtain stage
if stage is None:
stage = stage_utils.get_current_stage()
# get USD prim
collider_prim = stage.GetPrimAtPath(prim_path)
# check if prim has collision applied on it
if not UsdPhysics.CollisionAPI(collider_prim):
return False
# retrieve the USD rigid-body api
usd_collision_api = UsdPhysics.CollisionAPI(collider_prim)
# retrieve the collision api
physx_collision_api = PhysxSchema.PhysxCollisionAPI(collider_prim)
if not physx_collision_api:
physx_collision_api = PhysxSchema.PhysxCollisionAPI.Apply(collider_prim)
# convert to dict
cfg = cfg.to_dict()
# set into USD API
for attr_name in ["collision_enabled"]:
value = cfg.pop(attr_name, None)
safe_set_attribute_on_usd_schema(usd_collision_api, attr_name, value)
# set into PhysX API
for attr_name, value in cfg.items():
safe_set_attribute_on_usd_schema(physx_collision_api, attr_name, value)
# success
return True
@apply_nested
def set_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, stage: Usd.Stage = None):
"""Set properties for the mass of a rigid body prim.
These properties are based on the `UsdPhysics.MassAPI` schema. If the mass is not defined, the density is used
to compute the mass. However, in that case, a collision approximation of the rigid body is used to
compute the density. For more information on the properties, please refer to the
`documentation <https://openusd.org/release/wp_rigid_body_physics.html#body-mass-properties>`_.
.. caution::
The mass of an object can be specified in multiple ways and have several conflicting settings
that are resolved based on precedence. Please make sure to understand the precedence rules
before using this property.
.. note::
This function is decorated with :func:`apply_nested` that sets the properties to all the prims
(that have the schema applied on them) under the input prim path.
.. UsdPhysics.MassAPI: https://openusd.org/dev/api/class_usd_physics_mass_a_p_i.html
Args:
prim_path (str): The prim path of the rigid body.
cfg (schemas_cfg.MassPropertiesCfg): The configuration for the mass properties.
stage (Usd.Stage, optional): The stage where to find the prim. Defaults to None, in which case the
current stage is used.
"""
# obtain stage
if stage is None:
stage = stage_utils.get_current_stage()
# get USD prim
rigid_prim = stage.GetPrimAtPath(prim_path)
# check if prim has collision applied on it
if not UsdPhysics.MassAPI(rigid_prim):
return False
# retrieve the USD rigid-body api
usd_physics_mass_api = UsdPhysics.MassAPI(rigid_prim)
# convert to dict
cfg = cfg.to_dict()
# set into USD API
for attr_name in ["mass", "density"]:
value = cfg.pop(attr_name, None)
safe_set_attribute_on_usd_schema(usd_physics_mass_api, attr_name, value)
# success
return True
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES, ETH Zurich, and University of Toronto
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
from omni.isaac.orbit.utils import configclass
@configclass
class ArticulationRootPropertiesCfg:
"""Properties to apply to the root of an articulation.
See :meth:`set_articulation_root_properties` for more information.
.. note::
If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of
the properties and leave the rest as-is.
"""
articulation_enabled: bool | None = None
"""Whether to enable or disable articulation."""
enabled_self_collisions: bool | None = None
"""Whether to enable or disable self-collisions."""
solver_position_iteration_count: int | None = None
"""Solver position iteration counts for the body."""
solver_velocity_iteration_count: int | None = None
"""Solver position iteration counts for the body."""
sleep_threshold: float | None = None
"""Mass-normalized kinetic energy threshold below which an actor may go to sleep."""
stabilization_threshold: float | None = None
"""The mass-normalized kinetic energy threshold below which an articulation may participate in stabilization."""
@configclass
class RigidBodyPropertiesCfg:
"""Properties to apply to a rigid body.
See :meth:`set_rigid_body_properties` for more information.
.. note::
If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of
the properties and leave the rest as-is.
"""
rigid_body_enabled: bool | None = None
"""Whether to enable or disable the rigid body."""
kinematic_enabled: bool | None = None
"""Determines whether the body is kinematic or not.
A kinematic body is a body that is moved through animated poses or through user defined poses. The simulation
still derives velocities for the kinematic body based on the external motion.
For more information on kinematic bodies, please refer to the `documentation <https://openusd.org/release/wp_rigid_body_physics.html#kinematic-bodies>`_.
"""
disable_gravity: bool | None = False
"""Disable gravity for the actor. Defaults to False."""
linear_damping: float | None = None
"""Linear damping for the body."""
angular_damping: float | None = None
"""Angular damping for the body."""
max_linear_velocity: float | None = None
"""Maximum linear velocity for rigid bodies (in m/s)."""
max_angular_velocity: float | None = None
"""Maximum angular velocity for rigid bodies (in rad/s)."""
max_depenetration_velocity: float | None = None
"""Maximum depenetration velocity permitted to be introduced by the solver (in m/s)."""
max_contact_impulse: float | None = None
"""The limit on the impulse that may be applied at a contact."""
enable_gyroscopic_forces: bool | None = None
"""Enables computation of gyroscopic forces on the rigid body."""
retain_accelerations: bool | None = None
"""Carries over forces/accelerations over sub-steps."""
solver_position_iteration_count: int | None = None
"""Solver position iteration counts for the body."""
solver_velocity_iteration_count: int | None = None
"""Solver position iteration counts for the body."""
sleep_threshold: float | None = None
"""Mass-normalized kinetic energy threshold below which an actor may go to sleep."""
stabilization_threshold: float | None = None
"""The mass-normalized kinetic energy threshold below which an actor may participate in stabilization."""
@configclass
class CollisionPropertiesCfg:
"""Properties to apply to colliders in a rigid body.
See :meth:`set_collision_properties` for more information.
.. note::
If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of
the properties and leave the rest as-is.
"""
collision_enabled: bool | None = None
"""Whether to enable or disable collisions."""
contact_offset: float | None = None
"""Contact offset for the collision shape (in m).
The collision detector generates contact points as soon as two shapes get closer than the sum of their
contact offsets. This quantity should be non-negative which means that contact generation can potentially start
before the shapes actually penetrate.
"""
rest_offset: float | None = None
"""Rest offset for the collision shape (in m).
The rest offset quantifies how close a shape gets to others at rest, At rest, the distance between two
vertically stacked objects is the sum of their rest offsets. If a pair of shapes have a positive rest
offset, the shapes will be separated at rest by an air gap.
"""
torsional_patch_radius: float | None = None
"""Radius of the contact patch for applying torsional friction (in m).
It is used to approximate rotational friction introduced by the compression of contacting surfaces.
If the radius is zero, no torsional friction is applied.
"""
min_torsional_patch_radius: float | None = None
"""Minimum radius of the contact patch for applying torsional friction (in m)."""
@configclass
class MassPropertiesCfg:
"""Properties to define explicit mass properties of a rigid body.
See :meth:`set_mass_properties` for more information.
.. note::
If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of
the properties and leave the rest as-is.
"""
mass: float | None = None
"""The mass of the rigid body (in kg).
Note:
If non-zero, the mass is ignored and the density is used to compute the mass.
"""
density: float | None = None
"""The density of the rigid body (in kg/m^3).
The density indirectly defines the mass of the rigid body. It is generally computed using the collision
approximation of the body.
"""
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES, ETH Zurich, and University of Toronto
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import functools
from typing import Any, Callable
import carb
import omni.isaac.core.utils.prims as prim_utils
import omni.kit.commands
from pxr import Sdf, Usd
from omni.isaac.orbit.utils.string import to_camel_case
def safe_set_attribute_on_usd_schema(schema_api: Usd.APISchemaBase, name: str, value: Any, camel_case: bool = True):
"""Set the value of an attribute on its USD schema if it exists.
A USD API schema serves as an interface or API for authoring and extracting a set of attributes.
They typically derive from the :class:`pxr.Usd.SchemaBase` class. This function checks if the
attribute exists on the schema and sets the value of the attribute if it exists.
Args:
schema_api (Usd.APISchemaBase): The USD schema to set the attribute on.
name (str): The name of the attribute.
value (Any): The value to set the attribute to.
camel_case (bool, optional): Whether to convert the attribute name to camel case. Defaults to True.
Raises:
TypeError: When the input attribute name does not exist on the provided schema API.
"""
# if value is None, do nothing
if value is None:
return
# convert attribute name to camel case
if camel_case:
attr_name = to_camel_case(name, to="CC")
else:
attr_name = name
# retrieve the attribute
# reference: https://openusd.org/dev/api/_usd__page__common_idioms.html#Usd_Create_Or_Get_Property
attr = getattr(schema_api, f"Create{attr_name}Attr", None)
# check if attribute exists
if attr is not None:
attr().Set(value)
else:
# think: do we ever need to create the attribute if it doesn't exist?
# currently, we are not doing this since the schemas are already created with some defaults.
carb.log_error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.")
raise TypeError(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.")
def safe_set_attribute_on_usd_prim(prim: Usd.Prim, attr_name: str, value: Any, camel_case: bool = True):
"""Set the value of a attribute on its USD prim.
The function creates a new attribute if it does not exist on the prim. This is because in some cases (such
as with shaders), their attributes are not exposed as USD prim properties that can be altered. This function
allows us to set the value of the attributes in these cases.
Args:
prim (Usd.Prim): The USD prim to set the attribute on.
attr_name (str): The name of the attribute.
value (Any): The value to set the attribute to.
camel_case (bool, optional): Whether to convert the attribute name to camel case. Defaults to True.
"""
# if value is None, do nothing
if value is None:
return
# convert attribute name to camel case
if camel_case:
attr_name = to_camel_case(attr_name, to="cC")
# resolve sdf type based on value
if isinstance(value, bool):
sdf_type = Sdf.ValueTypeNames.Bool
elif isinstance(value, int):
sdf_type = Sdf.ValueTypeNames.Int
elif isinstance(value, float):
sdf_type = Sdf.ValueTypeNames.Float
elif isinstance(value, (tuple, list)) and len(value) == 3 and any(isinstance(v, float) for v in value):
sdf_type = Sdf.ValueTypeNames.Float3
elif isinstance(value, (tuple, list)) and len(value) == 2 and any(isinstance(v, float) for v in value):
sdf_type = Sdf.ValueTypeNames.Float2
else:
raise NotImplementedError(
f"Cannot set attribute '{attr_name}' with value '{value}'. Please modify the code to support this type."
)
# change property
omni.kit.commands.execute(
"ChangePropertyCommand",
prop_path=Sdf.Path(f"{prim.GetPath()}.{attr_name}"),
value=value,
prev=None,
type_to_create_if_not_exist=sdf_type,
usd_context_name=prim.GetStage(),
)
def apply_nested(func: Callable) -> Callable:
"""Decorator to apply a function to all prims under a specified prim-path.
The function iterates over the provided prim path and all its children to apply input function
to all prims under the specified prim path.
Note:
If the function succeeds to apply to a prim, it will not look at the children of that prim.
This is based on the physics behavior that nested schemas are not allowed. For example, a parent prim
and its child prim cannot both have a rigid-body schema applied on them, or it is not possible to
have nested articulations.
Args:
func (Callable): The function to apply to all prims under a specified prim-path. The function
must take the prim-path as the first argument and the configuration as the second argument.
Returns:
Callable: The wrapped function that applies the function to all prims under a specified prim-path.
"""
@functools.wraps(func)
def wrapper(prim_path: str, cfg: object):
# get USD prim
prim = prim_utils.get_prim_at_path(prim_path)
# check if prim is valid
if not prim.IsValid():
raise ValueError(f"Prim at path '{prim_path}' is not valid.")
# iterate over all prims under prim-path
all_prims = [prim]
while len(all_prims) > 0:
# get current prim
child_prim = all_prims.pop(0)
child_prim_path = prim_utils.get_prim_path(child_prim)
# check if prim is a prototype
# note: we prefer throwing a warning instead of ignoring the prim since the user may
# have intended to set properties on the prototype prim.
if child_prim.IsInstance():
carb.log_warn(f"Cannot perform '{func.__name__}' on instanced prim: '{child_prim_path}'")
continue
# set properties
success = func(child_prim_path, cfg)
# if successful, do not look at children
# this is based on the physics behavior that nested schemas are not allowed
if not success:
all_prims += child_prim.GetChildren()
return wrapper
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES, ETH Zurich, and University of Toronto
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Launch Isaac Sim Simulator first."""
from __future__ import annotations
from omni.isaac.orbit.app import AppLauncher
# launch omniverse app
simulation_app = AppLauncher(headless=True).app
"""Rest everything follows."""
import traceback
import unittest
import carb
import omni.isaac.core.utils.prims as prim_utils
import omni.isaac.core.utils.stage as stage_utils
from omni.isaac.core.simulation_context import SimulationContext
from pxr import UsdPhysics
import omni.isaac.orbit.sim.schemas as schemas
from omni.isaac.orbit.utils.assets import ISAAC_NUCLEUS_DIR
from omni.isaac.orbit.utils.string import to_camel_case
class TestPhysicsSchema(unittest.TestCase):
"""Test fixture for checking schemas modifications through Orbit."""
def setUp(self) -> None:
"""Create a blank new stage for each test."""
# Simulation time-step
self.dt = 0.1
# Load kit helper
self.sim = SimulationContext(physics_dt=self.dt, rendering_dt=self.dt, backend="numpy")
# Set some default values for test
self.arti_cfg = schemas.ArticulationRootPropertiesCfg(
enabled_self_collisions=False,
articulation_enabled=True,
solver_position_iteration_count=4,
solver_velocity_iteration_count=1,
sleep_threshold=0.0,
stabilization_threshold=0.0,
)
self.rigid_cfg = schemas.RigidBodyPropertiesCfg(
rigid_body_enabled=True,
kinematic_enabled=False,
disable_gravity=False,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=1000.0,
max_depenetration_velocity=10.0,
max_contact_impulse=10.0,
enable_gyroscopic_forces=True,
retain_accelerations=True,
solver_position_iteration_count=8,
solver_velocity_iteration_count=1,
sleep_threshold=0.0,
stabilization_threshold=0.0,
)
self.collision_cfg = schemas.CollisionPropertiesCfg(
collision_enabled=True,
contact_offset=0.05,
rest_offset=0.001,
min_torsional_patch_radius=0.1,
torsional_patch_radius=1.0,
)
self.mass_cfg = schemas.MassPropertiesCfg(mass=1.0, density=100.0)
def tearDown(self) -> None:
"""Stops simulator after each test."""
# stop simulation
self.sim.stop()
self.sim.clear()
def test_valid_properties_cfg(self):
"""Test that all the config instances have non-None values.
This is to ensure that we check that all the properties of the schema are set.
"""
for cfg in [self.arti_cfg, self.rigid_cfg, self.collision_cfg, self.mass_cfg]:
# check nothing is none
for k, v in cfg.__dict__.items():
self.assertIsNotNone(v, f"{cfg.__class__.__name__}:{k} is None. Please make sure schemas are valid.")
def test_set_properties_on_invalid_prim(self):
"""Test setting properties on a prim that does not exist."""
# set properties
with self.assertRaises(ValueError):
schemas.set_rigid_body_properties("/World/asset_xyz", self.rigid_cfg)
def test_set_properties_on_articulation_instanced_usd(self):
"""Test setting properties on articulation instanced usd.
In this case, setting collision properties on the articulation instanced usd will fail.
"""
# spawn asset to the stage
asset_usd_file = f"{ISAAC_NUCLEUS_DIR}/Robots/ANYbotics/anymal_instanceable.usd"
prim_utils.create_prim("/World/asset_instanced", usd_path=asset_usd_file, translation=(0.0, 0.0, 0.62))
# set properties on the asset and check all properties are set
schemas.set_articulation_root_properties("/World/asset_instanced", self.arti_cfg)
schemas.set_rigid_body_properties("/World/asset_instanced", self.rigid_cfg)
schemas.set_mass_properties("/World/asset_instanced", self.mass_cfg)
# validate the properties
self._validate_properties_on_prim(
"/World/asset_instanced", ["PhysxArticulationRootAPI", "PhysxRigidBodyAPI", "PhysicsMassAPI"]
)
def test_set_properties_on_articulation_usd(self):
"""Test setting properties on articulation usd."""
# spawn asset to the stage
asset_usd_file = f"{ISAAC_NUCLEUS_DIR}/Robots/Franka/franka.usd"
prim_utils.create_prim("/World/asset", usd_path=asset_usd_file, translation=(0.0, 0.0, 0.62))
# set properties on the asset and check all properties are set
schemas.set_articulation_root_properties("/World/asset", self.arti_cfg)
schemas.set_rigid_body_properties("/World/asset", self.rigid_cfg)
schemas.set_collision_properties("/World/asset", self.collision_cfg)
schemas.set_mass_properties("/World/asset", self.mass_cfg)
# validate the properties
self._validate_properties_on_prim(
"/World/asset", ["PhysxArticulationAPI", "PhysxRigidBodyAPI", "PhysxCollisionAPI", "PhysicsMassAPI"]
)
"""
Helper functions.
"""
def _validate_properties_on_prim(self, prim_path: str, schema_names: list[str], verbose: bool = False):
"""Validate the properties on the prim.
Note:
Right now this function exploits the hierarchy in the asset to check the properties. This is not a
fool-proof way of checking the properties. We should ideally check the properties on the prim itself
and all its children.
Args:
prim_path (str): The prim name.
schema_names (list[str]): The list of schema names to validate.
verbose (bool, optional): Whether to print verbose logs. Defaults to False.
"""
# check articulation properties are set correctly
if "PhysxArticulationRootAPI" in schema_names:
root_prim = prim_utils.get_prim_at_path(prim_path)
for attr_name, attr_value in self.arti_cfg.__dict__.items():
# skip names we know are not present
if attr_name in ["func"]:
continue
# convert attribute name in prim to cfg name
prim_prop_name = f"physxArticulation:{to_camel_case(attr_name, to='cC')}"
# validate the values
self.assertEqual(root_prim.GetAttribute(prim_prop_name).Get(), attr_value)
# check rigid body properties are set correctly
if "PhysxRigidBodyAPI" in schema_names:
root_prim = prim_utils.get_prim_at_path(prim_path)
for link_prim in root_prim.GetChildren():
if UsdPhysics.RigidBodyAPI(link_prim):
for attr_name, attr_value in self.rigid_cfg.__dict__.items():
# skip names we know are not present
if attr_name in ["func", "rigid_body_enabled", "kinematic_enabled"]:
continue
# convert attribute name in prim to cfg name
prim_prop_name = f"physxRigidBody:{to_camel_case(attr_name, to='cC')}"
# validate the values
self.assertEqual(link_prim.GetAttribute(prim_prop_name).Get(), attr_value)
elif verbose:
print(f"Skipping prim {link_prim.GetPrimPath()} as it is not a rigid body.")
# check collision properties are set correctly
# note: we exploit the hierarchy in the asset to check
if "PhysxCollisionAPI" in schema_names:
root_prim = prim_utils.get_prim_at_path(prim_path)
for link_prim in root_prim.GetChildren():
for mesh_prim in link_prim.GetChildren():
if UsdPhysics.CollisionAPI(mesh_prim):
for attr_name, attr_value in self.collision_cfg.__dict__.items():
# skip names we know are not present
if attr_name in ["func", "collision_enabled"]:
continue
# convert attribute name in prim to cfg name
prim_prop_name = f"physxCollision:{to_camel_case(attr_name, to='cC')}"
# validate the values
self.assertEqual(mesh_prim.GetAttribute(prim_prop_name).Get(), attr_value)
elif verbose:
print(f"Skipping prim {mesh_prim.GetPrimPath()} as it is not a collision mesh.")
# check rigid body mass properties are set correctly
# note: we exploit the hierarchy in the asset to check
if "PhysicsMassAPI" in schema_names:
root_prim = prim_utils.get_prim_at_path(prim_path)
for link_prim in root_prim.GetChildren():
if UsdPhysics.MassAPI(link_prim):
for attr_name, attr_value in self.mass_cfg.__dict__.items():
# skip names we know are not present
if attr_name in ["func"]:
continue
# print(link_prim.GetProperties())
prim_prop_name = f"physics:{to_camel_case(attr_name, to='cC')}"
# validate the values
self.assertEqual(link_prim.GetAttribute(prim_prop_name).Get(), attr_value)
elif verbose:
print(f"Skipping prim {link_prim.GetPrimPath()} as it is not a mass api.")
if __name__ == "__main__":
try:
unittest.main()
except Exception as err:
carb.log_error(err)
carb.log_error(traceback.format_exc())
raise
finally:
# close sim app
simulation_app.close()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment