Standalone Python Script with particles and CPU context

Hi there,

As a few people have noticed, there seems to be some problem when using particles in a standalone python script, especially when the simulation context is using “cuda”. Specifically, the particles “freeze” at the initial positions and does not move. I have found the solution to be using:

world = World(backend="torch", device="cpu")
physx_interface = acquire_physx_interface()
physx_interface.overwrite_gpu_setting(1)

I read somewhere on this forum that this is a GPU rendering issue. Through some testing, i believe this is the case, i.e. place a ball below many particles, and play the simulation, the ball moves while the particles seem to be frozen in the GUI. So potentially that physx is simulating correctly but somehow the USD stage is not updated properly. This would be acceptable if there’s a way to access the states of the particles, but the only way right now is to use UsdGeom.Points.GetPointsAttr() in the code below, which reads the USD prim’s attributes (that doesn’t get updated with GPU context).

So I have two questions:

  1. Does using CPU context introduce performance overheads? Since the particle dynamics are handled by the GPU, does this mean performance will potentially be bottlenecked by the GPU-CPU data transfer?

  2. What’s the best way to get the position of all the particles, and to release them? So far I’m using UsdGeom.Points.GetPointsAttr() for this purpose, but i noticed that it’s quite slow when there are multiple particle sets present. What’s the recommended method for this? (i.e. something similar to ArticulationView in omni/physics/tensors/impl/api, maybe?)

For reference, i’m using the following script:

#launch Isaac Sim before any other imports
#default first two lines in any standalone application
from omni.isaac.kit import SimulationApp
simulation_app = SimulationApp({"headless": False}) # we can also run as headless.

from omni.isaac.core import World
from omni.isaac.core.objects import DynamicCuboid
import numpy as np
from pxr import Usd, UsdLux, UsdGeom, Sdf, Gf, Vt, UsdPhysics, PhysxSchema
import omni.kit.commands
from omni.physx.scripts import particleUtils
import omni.timeline
from omni.isaac.kit.utils import set_carb_setting
from omni.physx import acquire_physx_interface
from pxr import UsdGeom, Vt

world = World(backend="torch", device="cpu")
physx_interface = acquire_physx_interface()
physx_interface.overwrite_gpu_setting(1)

world.scene.add_default_ground_plane()
fancy_cube =  world.scene.add(
    DynamicCuboid(
        prim_path="/World/random_cube",
        name="fancy_cube",
        position=np.array([0, 0, 1.0]),
        scale=np.array([0.5015, 0.5015, 0.5015]),
        color=np.array([0, 0, 1.0]),
    ))

# spawn particle stuff here

scene = world.scene
_stage = scene.stage
_defaultPrimPath = Sdf.Path("/World")

numParticlesZ = 35
_rng_seed = 42
_rng = np.random.default_rng(_rng_seed)

# TODO solve type check? TGS
_solverPositionIterations = 4
# physxAPI = PhysxSchema.PhysxSceneAPI.Apply(scene.GetPrim())
# hysxAPI.CreateSolverTypeAttr("TGS")

# Particle system
particleSystemPath = _defaultPrimPath.AppendChild("particleSystem0")

# particle params
particleSpacing = 0.05
restOffset = particleSpacing * 0.9
fluidRestOffset = restOffset * 0.6
particleContactOffset = restOffset + 0.001
particle_system = particleUtils.add_physx_particle_system(
    stage=_stage,
    particle_system_path=particleSystemPath,
    simulation_owner="physicsScene",
    contact_offset=restOffset * 1.5 + 0.01,
    rest_offset=restOffset * 1.5,
    particle_contact_offset=particleContactOffset,
    solid_rest_offset=0.0,
    fluid_rest_offset=fluidRestOffset,
    solver_position_iterations=_solverPositionIterations,
)

particle_system.CreateMaxVelocityAttr().Set(200)

particlesPath = _defaultPrimPath.AppendChild("particles")

_stage.SetInterpolationType(Usd.InterpolationTypeHeld)

lower = Gf.Vec3f(0, 0, 2.5)
positions, velocities = particleUtils.create_particles_grid(
    lower, particleSpacing + 0.01, 110, 65, numParticlesZ
)

uniform_range = particleSpacing * 0.2

for i in range(len(positions)):
    positions[i][0] += _rng.uniform(-uniform_range, uniform_range)
    positions[i][1] += _rng.uniform(-uniform_range, uniform_range)
    positions[i][2] += _rng.uniform(-uniform_range, uniform_range)

widths = [particleSpacing] * len(positions)
particles = particleUtils.add_physx_particleset_points(
    stage=_stage,
    path=particlesPath,
    positions_list=Vt.Vec3fArray(positions),
    velocities_list=Vt.Vec3fArray(velocities),
    widths_list=widths,
    particle_system_path=particleSystemPath,
    self_collision=True,
    fluid=False,
    particle_group=0,
    particle_mass=0.001,
    density=0.0,
)

world.reset()
pset_prim = _stage.GetPrimAtPath("/World/particles")
geom_points = UsdGeom.Points.Get(pset_prim)

while simulation_app.is_running():
    position, orientation = fancy_cube.get_world_pose()
    linear_velocity = fancy_cube.get_linear_velocity()
    particle_poss = geom_points.GetPointsAttr().Get()
    world.step(render=True) # execute one physics step and one rendering step

simulation_app.close() # close Isaac Sim```

Ok, I found out that casting Vt arrays into numpy arrays is much faster than torch tensors. That solves the second problem. The first question remains. If the simulation context is on CPU, does that mean the position and velocity attributes of the UsdGeom.Points prim needs to be copied from GPU at every timestep? And is there native support to solve this, or some api that allows access to physx particle states, or solve the rendering issue with GPU, at least when using a standalone Python script?

Hello, I’m learning to use the isaac-sim. I have a question that has been bothering me for a long time. I need to make a project to simulate a water pump, but I don’t know how to change the particles’ velocity or apply velocity deltas on the particles. Do you know how to do it?