Unverified Commit 8ba18efa authored by Mayank Mittal's avatar Mayank Mittal Committed by GitHub

Fixes the issue with using callbacks inside python classes (#181)

# Description

In Python, memory management is based on reference counts, where an
object isn't "truly" deleted if it has variables referring to the memory
address. More details:
https://www.pythontutorial.net/advanced-python/python-references/

In Orbit code, we provide "self" to various callbacks, which increments
the reference counts since they "refer" to the object. This means that
when we do:
```python
robot = Articulation(cfg=ANYMAL_C_CFG)
```
The reference count is 3: one for the `robot` variable and two for the
callbacks we launched on separate threads.

Now, if we try to delete the object by doing `del robot`, the reference
count becomes 2. These correspond to the callbacks that were launched.
However, the destructor `__del__()` isn't called yet because it is only
done when all the reference counts are 0.

As a fix, this MR uses a proxy weak reference to "self" when passing it
to underlying callbacks or other classes. This does not increment the
reference count for the object; thereby, when the main object is
deleted, all ts proxy references become invalid as well.

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## 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
parent 9a61666c
......@@ -134,3 +134,73 @@ In the above case, the actual error is:
File "source/standalone/workflows/robomimic/tools/collect_demonstrations.py", line 57, in pre_process_actions
return torch.concat([delta_pose, gripper_vel], dim=1)
TypeError: expected Tensor as element 1 in argument 0, but got int
Preventing memory leaks in the simulator
----------------------------------------
Memory leaks in the Isaac Sim simulator can occur when C++ callbacks are registered with Python objects.
This happens when callback functions within classes maintain references to the Python objects they are
associated with. As a result, Python's garbage collection is unable to reclaim memory associated with
these objects, preventing the corresponding C++ objects from being destroyed. Over time, this can lead
to memory leaks and increased resource usage.
To prevent memory leaks in the Isaac Sim simulator, it is essential to use weak references when registering
callbacks with the simulator. This ensures that Python objects can be garbage collected when they are no
longer needed, thereby avoiding memory leaks. The `weakref <https://docs.python.org/3/library/weakref.html>`_
module from the Python standard library can be employed for this purpose.
For example, consider a class with a callback function ``on_event_callback`` that needs to be registered
with the simulator. If you use a strong reference to the ``MyClass`` object when passing the callback,
the reference count of the ``MyClass`` object will be incremented. This prevents the ``MyClass`` object
from being garbage collected when it is no longer needed, i.e., the ``__del__`` destructor will not be
called.
.. code:: python
import omni.kit
class MyClass:
def __init__(self):
app_interface = omni.kit.app.get_app_interface()
self._handle = app_interface.get_post_update_event_stream().create_subscription_to_pop(
self.on_event_callback
)
def __del__(self):
self._handle.unsubscribe()
self._handle = None
def on_event_callback(self, event):
# do something with the message
To fix this issue, it's crucial to employ weak references when registering the callback. While this approach
adds some verbosity to the code, it ensures that the ``MyClass`` object can be garbage collected when no longer
in use. Here's the modified code:
.. code:: python
import omni.kit
import weakref
class MyClass:
def __init__(self):
app_interface = omni.kit.app.get_app_interface()
self._handle = app_interface.get_post_update_event_stream().create_subscription_to_pop(
lambda event, obj=weakref.proxy(self): obj.on_event_callback(event)
)
def __del__(self):
self._handle.unsubscribe()
self._handle = None
def on_event_callback(self, event):
# do something with the message
In this revised code, the weak reference ``weakref.proxy(self)`` is used when registering the callback,
allowing the ``MyClass`` object to be properly garbage collected.
By following this pattern, you can prevent memory leaks and maintain a more efficient and stable simulation.
[package]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.9.7"
version = "0.9.8"
# Description
title = "ORBIT framework for Robot Learning"
......
Changelog
---------
0.9.8 (2023-09-30)
~~~~~~~~~~~~~~~~~~
Fixed
^^^^^
* Fixed the boundedness of class objects that register callbacks into the simulator.
These include devices, :class:`AssetBase`, :class:`SensorBase` and :class:`CommandGenerator`.
The fix ensures that object gets deleted when the user deletes the object.
0.9.7 (2023-09-26)
~~~~~~~~~~~~~~~~~~
......
......@@ -6,6 +6,7 @@
from __future__ import annotations
import re
import weakref
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Sequence
......@@ -53,31 +54,41 @@ class AssetBase(ABC):
if len(matching_prim_paths) == 0:
raise RuntimeError(f"Could not find prim with path {self.cfg.prim_path}.")
# note: Use weakref on all callbacks to ensure that this object can be deleted when its destructor is called.
# add callbacks for stage play/stop
physx_interface = omni.physx.acquire_physx_interface()
self._initialize_handle = physx_interface.get_simulation_event_stream_v2().create_subscription_to_pop_by_type(
int(omni.physx.bindings._physx.SimulationEvent.RESUMED), self._initialize_callback
int(omni.physx.bindings._physx.SimulationEvent.RESUMED),
lambda event, obj=weakref.proxy(self): obj._initialize_callback(event),
)
self._invalidate_initialize_handle = (
physx_interface.get_simulation_event_stream_v2().create_subscription_to_pop_by_type(
int(omni.physx.bindings._physx.SimulationEvent.STOPPED), self._invalidate_initialize_callback
int(omni.physx.bindings._physx.SimulationEvent.STOPPED),
lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event),
)
)
# add callback for debug visualization
if self.cfg.debug_vis:
app_interface = omni.kit.app.get_app_interface()
self._debug_visualization_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop(
self._debug_vis_callback
lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event),
)
else:
self._debug_visualization_handle = None
def __del__(self):
"""Unsubscribe from the callbacks."""
self._initialize_handle.unsubscribe()
self._invalidate_initialize_handle.unsubscribe()
if self._debug_visualization_handle is not None:
# clear physics events handles
if self._initialize_handle:
self._initialize_handle.unsubscribe()
self._initialize_handle = None
if self._invalidate_initialize_handle:
self._invalidate_initialize_handle.unsubscribe()
self._invalidate_initialize_handle = None
# clear debug visualization
if self._debug_visualization_handle:
self._debug_visualization_handle.unsubscribe()
self._debug_visualization_handle = None
"""
Properties
......
......@@ -13,6 +13,7 @@ methods.
from __future__ import annotations
import torch
import weakref
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Sequence
......@@ -58,8 +59,9 @@ class CommandGeneratorBase(ABC):
# add callback for debug visualization
if self.cfg.debug_vis:
app_interface = omni.kit.app.get_app_interface()
# NOTE: Use weakref on callback to ensure that this object can be deleted when its destructor is called.
self._debug_visualization_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop(
self._debug_vis_callback
lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event),
)
else:
self._debug_visualization_handle = None
......@@ -68,6 +70,7 @@ class CommandGeneratorBase(ABC):
"""Unsubscribe from the callbacks."""
if self._debug_visualization_handle is not None:
self._debug_visualization_handle.unsubscribe()
self._debug_visualization_handle = None
"""
Properties
......
......@@ -8,6 +8,7 @@
from __future__ import annotations
import numpy as np
import weakref
from typing import Callable
import carb
......@@ -68,7 +69,11 @@ class Se2Gamepad(DeviceBase):
self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface()
self._gamepad = self._appwindow.get_gamepad(0)
self._gamepad_sub = self._input.subscribe_to_gamepad_events(self._gamepad, self._on_gamepad_event)
# note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called
self._gamepad_sub = self._input.subscribe_to_gamepad_events(
self._gamepad,
lambda event, *args, obj=weakref.proxy(self): obj._on_gamepad_event(event, *args),
)
# bindings for gamepad to command
self._create_key_bindings()
# command buffers
......
......@@ -7,6 +7,7 @@
import numpy as np
import weakref
from scipy.spatial.transform.rotation import Rotation
from typing import Callable, Tuple
......@@ -67,7 +68,11 @@ class Se3Gamepad(DeviceBase):
self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface()
self._gamepad = self._appwindow.get_gamepad(0)
self._gamepad_sub = self._input.subscribe_to_gamepad_events(self._gamepad, self._on_gamepad_event)
# note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called
self._gamepad_sub = self._input.subscribe_to_gamepad_events(
self._gamepad,
lambda event, *args, obj=weakref.proxy(self): obj._on_gamepad_event(event, *args),
)
# bindings for gamepad to command
self._create_key_bindings()
# command buffers
......
......@@ -8,6 +8,7 @@
from __future__ import annotations
import numpy as np
import weakref
from typing import Callable
import carb
......@@ -56,7 +57,11 @@ class Se2Keyboard(DeviceBase):
self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface()
self._keyboard = self._appwindow.get_keyboard()
self._keyboard_sub = self._input.subscribe_to_keyboard_events(self._keyboard, self._on_keyboard_event)
# note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called
self._keyboard_sub = self._input.subscribe_to_keyboard_events(
self._keyboard,
lambda event, *args, obj=weakref.proxy(self): obj._on_keyboard_event(event, *args),
)
# bindings for keyboard to command
self._create_key_bindings()
# command buffers
......
......@@ -7,6 +7,7 @@
import numpy as np
import weakref
from scipy.spatial.transform.rotation import Rotation
from typing import Callable, Tuple
......@@ -61,7 +62,11 @@ class Se3Keyboard(DeviceBase):
self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface()
self._keyboard = self._appwindow.get_keyboard()
self._keyboard_sub = self._input.subscribe_to_keyboard_events(self._keyboard, self._on_keyboard_event)
# note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called.
self._keyboard_sub = self._input.subscribe_to_keyboard_events(
self._keyboard,
lambda event, *args, obj=weakref.proxy(self): obj._on_keyboard_event(event, *args),
)
# bindings for keyboard to command
self._create_key_bindings()
# command buffers
......
......@@ -159,5 +159,8 @@ class BaseEnv:
def close(self):
"""Cleanup for the environment."""
if not self._is_closed:
# clear callbacks and instance
self.sim.clear_all_callbacks()
self.sim.clear_instance()
# update closing status
self._is_closed = True
......@@ -295,7 +295,7 @@ class RLEnv(BaseEnv, gym.Env):
self._orbit_window.visible = False
self._orbit_window.destroy()
# update closing status
self._is_closed = True
super().close()
"""
Implementation specifics.
......
......@@ -12,6 +12,7 @@ Each sensor class should inherit from this class and implement the abstract meth
from __future__ import annotations
import torch
import weakref
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Sequence
......@@ -50,31 +51,41 @@ class SensorBase(ABC):
# flag for whether the sensor is initialized
self._is_initialized = False
# note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called.
# add callbacks for stage play/stop
physx_interface = omni.physx.acquire_physx_interface()
self._initialize_handle = physx_interface.get_simulation_event_stream_v2().create_subscription_to_pop_by_type(
int(omni.physx.bindings._physx.SimulationEvent.RESUMED), self._initialize_callback
int(omni.physx.bindings._physx.SimulationEvent.RESUMED),
lambda event, obj=weakref.proxy(self): obj._initialize_callback(event),
)
self._invalidate_initialize_handle = (
physx_interface.get_simulation_event_stream_v2().create_subscription_to_pop_by_type(
int(omni.physx.bindings._physx.SimulationEvent.STOPPED), self._invalidate_initialize_callback
int(omni.physx.bindings._physx.SimulationEvent.STOPPED),
lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event),
)
)
# add callback for debug visualization
if self.cfg.debug_vis:
app_interface = omni.kit.app.get_app_interface()
self._debug_visualization_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop(
self._debug_vis_callback
lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event),
)
else:
self._debug_visualization_handle = None
def __del__(self):
"""Unsubscribe from the callbacks."""
self._initialize_handle.unsubscribe()
self._invalidate_initialize_handle.unsubscribe()
if self._debug_visualization_handle is not None:
# clear physics events handles
if self._initialize_handle:
self._initialize_handle.unsubscribe()
self._initialize_handle = None
if self._invalidate_initialize_handle:
self._invalidate_initialize_handle.unsubscribe()
self._invalidate_initialize_handle = None
# clear debug visualization
if self._debug_visualization_handle:
self._debug_visualization_handle.unsubscribe()
self._debug_visualization_handle = None
"""
Properties
......
......@@ -5,9 +5,10 @@
from __future__ import annotations
import builtins
import enum
import gc
import numpy as np
import weakref
from typing import Any
import carb
......@@ -318,7 +319,10 @@ class SimulationContext(_SimulationContext):
# we do this because physics views go invalid once we stop the simulation. So there is
# no point in keeping the simulation running.
if self.cfg.shutdown_app_on_stop and not self.timeline_callback_exists("shutdown_app_on_stop"):
self.add_timeline_callback("shutdown_app_on_stop", self._shutdown_app_on_stop_callback)
self.add_timeline_callback(
"shutdown_app_on_stop",
lambda *args, obj=weakref.proxy(self): obj._shutdown_app_on_stop_callback(*args),
)
def step(self, render: bool = True):
"""Steps the physics simulation with the pre-defined time-step.
......@@ -500,14 +504,28 @@ class SimulationContext(_SimulationContext):
"""
# check if the simulation is stopped
if event.type == int(omni.timeline.TimelineEventType.STOP):
# make sure that any replicator workflows finish rendering/writing
if not builtins.ISAAC_LAUNCHED_FROM_TERMINAL:
try:
import omni.replicator.core as rep
rep_status = rep.orchestrator.get_status()
if rep_status not in [rep.orchestrator.Status.STOPPED, rep.orchestrator.Status.STOPPING]:
rep.orchestrator.stop()
rep.orchestrator.wait_until_complete()
except Exception:
pass
# clear the instance and all callbacks
# note: clearing callbacks is important to prevent memory leaks
self.clear_all_callbacks()
# workaround for exit issues, clean the stage first:
if omni.usd.get_context().can_close_stage():
omni.usd.get_context().close_stage()
# print logging information
self.app.print_and_log("Simulation is stopped. Shutting down the app.")
# clear the stage
stage_utils.close_stage()
# shutdown the simulator
self.app.shutdown()
# disabled on linux to avoid a crash
carb.get_framework().unload_all_plugins()
# garbage collect
gc.collect()
# exit the application
print("Exiting the application complete.")
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES, ETH Zurich, and University of Toronto
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# ignore private usage of variables warning
# pyright: reportPrivateUsage=none
from __future__ import annotations
"""Launch Isaac Sim Simulator first."""
from omni.isaac.orbit.app import AppLauncher
# launch omniverse app
app_launcher = AppLauncher(headless=True)
simulation_app = app_launcher.app
"""Rest everything follows."""
import ctypes
import traceback
import unittest
import carb
import omni.isaac.core.utils.stage as stage_utils
import omni.isaac.orbit.sim as sim_utils
from omni.isaac.orbit.assets import Articulation
from omni.isaac.orbit.assets.config import ANYMAL_C_CFG, FRANKA_PANDA_ARM_WITH_PANDA_HAND_CFG
class TestArticulation(unittest.TestCase):
"""Test for articulation class."""
def setUp(self):
"""Create a blank new stage for each test."""
# Create a new stage
stage_utils.create_new_stage()
# Simulation time-step
self.dt = 0.01
# Load kit helper
sim_cfg = sim_utils.SimulationCfg(dt=self.dt, device="cuda:0", shutdown_app_on_stop=False)
self.sim = sim_utils.SimulationContext(sim_cfg)
def tearDown(self):
"""Stops simulator after each test."""
# stop simulation
self.sim.stop()
# clear the stage
self.sim.clear()
self.sim.clear_instance()
"""
Tests
"""
def test_initialization_floating_base(self):
"""Test articulation initialization for a floating-base."""
# Create articulation
robot = Articulation(cfg=ANYMAL_C_CFG.replace(prim_path="/World/Robot"))
# Check that boundedness of articulation is correct
self.assertEqual(ctypes.c_long.from_address(id(robot)).value, 1)
# Play sim
self.sim.reset()
# Check if robot is initialized
self.assertTrue(robot._is_initialized)
# Check buffers that exists and have correct shapes
self.assertTrue(robot.data.root_pos_w.shape == (1, 3))
self.assertTrue(robot.data.root_quat_w.shape == (1, 4))
self.assertTrue(robot.data.joint_pos.shape == (1, 12))
# Simulate physics
for _ in range(10):
# perform rendering
self.sim.step()
# update robot
robot.update(self.dt)
# Delete articulation
del robot
def test_initialization_fixed_base(self):
"""Test articulation initialization for fixed base."""
# Create articulation
robot = Articulation(cfg=FRANKA_PANDA_ARM_WITH_PANDA_HAND_CFG.replace(prim_path="/World/Robot"))
# Check that boundedness of articulation is correct
self.assertEqual(ctypes.c_long.from_address(id(robot)).value, 1)
# Play sim
self.sim.reset()
# Check if robot is initialized
self.assertTrue(robot._is_initialized)
# Check buffers that exists and have correct shapes
self.assertTrue(robot.data.root_pos_w.shape == (1, 3))
self.assertTrue(robot.data.root_quat_w.shape == (1, 4))
self.assertTrue(robot.data.joint_pos.shape == (1, 9))
# Simulate physics
for _ in range(10):
# perform rendering
self.sim.step()
# update robot
robot.update(self.dt)
# Delete articulation
del robot
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()
......@@ -22,6 +22,8 @@ simulation_app = SimulationApp({"headless": False})
"""Rest everything follows."""
import ctypes
from omni.isaac.core.simulation_context import SimulationContext
from omni.isaac.orbit.devices import Se3Keyboard
......@@ -51,6 +53,10 @@ def main():
print("Press 'L' to print a message. Press 'ESC' to quit.")
# Check that boundedness of articulation is correct
if ctypes.c_long.from_address(id(teleop_interface)).value != 1:
raise RuntimeError("Teleoperation interface is not bounded to a single instance.")
# Reset interface internals
teleop_interface.reset()
......
......@@ -49,17 +49,13 @@ QUAT_WORLD = [-0.3647052, -0.27984815, -0.1159169, 0.88047623]
class TestCamera(unittest.TestCase):
"""Test for orbit camera sensor"""
"""
Test Setup and Teardown
"""
"""Test for USD Camera sensor."""
def setUp(self):
"""Create a blank new stage for each test."""
self.camera_cfg = CameraCfg(
height=24,
width=32,
height=128,
width=128,
prim_path="/World/Camera",
update_period=0,
data_types=["distance_to_image_plane"],
......@@ -127,9 +123,6 @@ class TestCamera(unittest.TestCase):
# check image data
for im_data in camera.data.output.to_dict().values():
self.assertTrue(im_data.shape == (1, self.camera_cfg.height, self.camera_cfg.width))
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera.__del__()
def test_camera_resolution(self):
"""Test camera resolution is correctly set."""
......@@ -146,9 +139,6 @@ class TestCamera(unittest.TestCase):
# access image data and compare shapes
for im_data in camera.data.output.to_dict().values():
self.assertTrue(im_data.shape == (1, self.camera_cfg.height, self.camera_cfg.width))
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera.__del__()
def test_camera_init_offset(self):
"""Test camera initialization with offset using different conventions."""
......@@ -219,12 +209,6 @@ class TestCamera(unittest.TestCase):
np.testing.assert_allclose(camera_ros.data.quat_w_opengl[0], QUAT_OPENGL, rtol=1e-5)
np.testing.assert_allclose(camera_ros.data.quat_w_world[0], QUAT_WORLD, rtol=1e-5)
# delete all cameras
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera_ros.__del__()
camera_opengl.__del__()
camera_world.__del__()
def test_multi_camera_init(self):
"""Test multi-camera initialization."""
# create two cameras with different prim paths
......@@ -256,11 +240,6 @@ class TestCamera(unittest.TestCase):
for im_data in cam.data.output.to_dict().values():
self.assertTrue(im_data.shape == (1, self.camera_cfg.height, self.camera_cfg.width))
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
cam_1.__del__()
cam_2.__del__()
def test_camera_set_world_poses(self):
"""Test camera function to set specific world pose."""
camera = Camera(self.camera_cfg)
......@@ -270,9 +249,6 @@ class TestCamera(unittest.TestCase):
camera.set_world_poses([POSITION], [QUAT_WORLD], convention="world")
np.testing.assert_allclose(camera.data.pos_w, [POSITION], rtol=1e-5)
np.testing.assert_allclose(camera.data.quat_w_world, [QUAT_WORLD], rtol=1e-5)
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera.__del__()
def test_camera_set_world_poses_from_view(self):
"""Test camera function to set specific world pose from view."""
......@@ -283,9 +259,6 @@ class TestCamera(unittest.TestCase):
camera.set_world_poses_from_view([POSITION], [[0.0, 0.0, 0.0]])
np.testing.assert_allclose(camera.data.pos_w, [POSITION], rtol=1e-5)
np.testing.assert_allclose(camera.data.quat_w_ros, [QUAT_ROS], rtol=1e-5)
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera.__del__()
def test_intrinsic_matrix(self):
"""Checks that the camera's set and retrieve methods work for intrinsic matrix."""
......@@ -318,9 +291,6 @@ class TestCamera(unittest.TestCase):
# This is a bug in the simulator.
self.assertAlmostEqual(rs_intrinsic_matrix[0, 0], K[0, 0], 4)
# self.assertAlmostEqual(rs_intrinsic_matrix[1, 1], K[1, 1], 4)
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera.__del__()
def test_throughput(self):
"""Checks that the single camera gets created properly with a rig."""
......@@ -368,9 +338,6 @@ class TestCamera(unittest.TestCase):
# Check image data
for im_data in camera.data.output.values():
self.assertTrue(im_data.shape == (1, camera_cfg.height, camera_cfg.width))
# delete camera
# TODO: Why do need to delete camera manually. Shouldn't it be deleted automatically?
camera.__del__()
"""
Helper functions.
......
......@@ -14,6 +14,7 @@ simulation_app = AppLauncher(headless=True).app
"""Rest everything follows."""
import ctypes
import numpy as np
import traceback
import unittest
......@@ -89,13 +90,41 @@ class TestSimulationContext(unittest.TestCase):
sim.set_setting("/myExt/using_omniverse_version", sim.get_version())
self.assertSequenceEqual(sim.get_setting("/myExt/using_omniverse_version"), sim.get_version())
def test_render_modes(self):
"""Test that you can change render modes."""
def test_headless_mode(self):
"""Test that render mode is headless since we are running in headless mode."""
sim = SimulationContext()
# check default render mode
self.assertEqual(sim.render_mode, sim.RenderMode.HEADLESS)
def test_boundedness(self):
"""Test that the boundedness of the simulation context remains constant.
Note: This test fails right now because Isaac Sim does not handle boundedness correctly. On creation,
it is registering itself to various callbacks and hence the boundedness is more than 1. This may not be
critical for the simulation context since we usually call various clear functions before deleting the
simulation context.
"""
sim = SimulationContext()
# manually set the boundedness to 1? -- this is not possible because of Isaac Sim.
sim.clear_all_callbacks()
sim._stage_open_callback = None
sim._physics_timer_callback = None
sim._event_timer_callback = None
# check that boundedness of simulation context is correct
sim_ref_count = ctypes.c_long.from_address(id(sim)).value
# reset the simulation
sim.reset()
self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count)
# step the simulation
for _ in range(10):
sim.step()
self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count)
# clear the simulation
sim.clear_instance()
self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count - 1)
if __name__ == "__main__":
try:
......
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