Retrieving simulation data¶
Trackers expose both ordinary Python-returning methods and zero-host-copy-style methods that fill caller-owned CUDA memory.
Host access¶
Use host methods for inspection, logging, and small result sets:
solver.DoDynamicsThenSync(0.01)
position = tracker.Pos() # first tracked owner
positions = tracker.Positions() # every owner in this tracker
velocities = tracker.Velocities()
Most single-object methods accept an optional owner offset. Bulk methods return
all owners covered by the tracker. Quaternion values use public ordering
(x, y, z, w).
Direct CUDA access¶
The *ToDevice methods synchronously fill a raw CUDA pointer represented by
a Python integer. This avoids first materializing the result as a Python list.
The allocation remains entirely owned by the caller.
This CuPy example retrieves all tracked positions:
1"""Retrieve tracked DEME positions directly into a CuPy allocation."""
2
3import cupy as cp
4import deme
5
6
7solver = deme.DEMSolver([0])
8material = solver.LoadMaterial(
9 {"E": 1.0e7, "nu": 0.3, "CoR": 0.5, "mu": 0.4, "Crr": 0.0}
10)
11solver.InstructBoxDomainDimension(1.0, 1.0, 1.0)
12sphere_type = solver.LoadSphereType(0.01, 0.025, material)
13initial_positions = [[0.0, 0.0, 0.2], [0.0, 0.0, 0.4]]
14sphere_batch = solver.AddClumps(sphere_type, initial_positions)
15tracker = solver.Track(sphere_batch)
16solver.SetInitTimeStep(1.0e-5)
17solver.Initialize()
18solver.DoDynamicsThenSync(0.001)
19
20destination_device = 0
21owner_count = len(initial_positions)
22with cp.cuda.Device(destination_device):
23 # A float3 result requires a C-contiguous (owner_count, 3) float32 array.
24 positions = cp.empty((owner_count, 3), dtype=cp.float32)
25 tracker.PositionsToDevice(
26 positions.data.ptr,
27 owner_count, # Capacity is float3 elements, not bytes or scalar floats.
28 destination_device,
29 )
30 print(positions)
The direct interface is intentionally low level. Every call must satisfy all of these requirements:
pointeris a valid, writable CUDA-accessible pointer ondevice;the allocation remains alive until the synchronous call returns;
capacityis a number of result elements, not bytes;dtype and row layout match the method;
the current simulation state has been synchronized before it is read.
Supplying the wrong pointer, device, dtype, layout, or capacity can cause a native CUDA error or memory corruption. Prefer host access unless eliminating the host transfer materially helps the application.
Why the pointer argument is an integer¶
The Python bindings represent a native CUDA address with C++
std::uintptr_t. Pybind11 accepts an ordinary Python int for that
argument, so callers pass the numeric address exported by their array library:
tracker.PositionsToDevice(
int(cuda_array_pointer),
owner_count,
cuda_device_id,
)
The integer is only an address; passing it does not transfer ownership, retain the allocation, infer its size or dtype, or validate which CUDA device owns it. The Python array that owns the address must remain alive through the call. A Python object such as a NumPy array, Warp array, CuPy array, or PyTorch tensor must not itself be supplied where the integer pointer is expected.
Common pointer accessors are warp_array.ptr, cupy_array.data.ptr, and
torch_tensor.data_ptr(). Only CUDA-accessible allocations are valid for the
device-retrieval methods; an integer derived from ordinary NumPy host storage
is not valid.
Tracked-owner methods¶
The following table gives the destination representation. A float3 result
maps to a C-contiguous (capacity, 3) array of float32; float4 maps
to (capacity, 4).
Method |
Destination element |
Meaning |
|---|---|---|
|
|
Global position |
|
|
Global linear velocity |
|
|
Local angular velocity |
|
|
Global angular velocity |
|
|
Quaternion |
|
|
Family number |
|
|
Mass |
|
|
Principal moments of inertia |
|
|
Global contact acceleration |
|
|
Local contact angular acceleration |
|
|
Global contact angular acceleration |
|
|
Named owner wildcard |
The required owner capacity is the number of owners covered by the tracker. In the common case where a tracker was created from a clump batch, this is the number of clumps added in that batch.
Contact-pair methods¶
Python exposes three all-owner compacting methods:
GetContactForcesForAllToDevice(points, forces, capacity, device)GetContactForcesAndLocalTorqueForAllToDevice(points, forces, torques, capacity, device)GetContactForcesAndGlobalTorqueForAllToDevice(points, forces, torques, capacity, device)
Every destination is a C-contiguous (capacity, 3) float32 allocation.
The methods return the number of valid compacted contact pairs; only rows
before that count contain results. Capacity must cover the simulation’s total
recorded contact count, not merely the final number involving this tracker.
Contact retrieval also requires force recording to remain enabled.
Destination device¶
device names the logical CUDA device that owns the destination allocation;
it does not have to be inferred from the pointer. With CuPy, enter
with cp.cuda.Device(device): before allocating, pass array.data.ptr,
and keep the array in scope through the call. DEME handles retrieval when the
destination differs from a worker device, subject to the CUDA capabilities of
the system.
NVIDIA Warp¶
A Warp CUDA array exposes its base allocation address through the integer
array.ptr property. Use packed Warp dtypes that match the native CUDA
element type: wp.vec3 for float3, wp.vec4 for float4,
wp.float32 for float, and wp.uint32 for unsigned int.
1"""Retrieve DEME positions into a caller-owned NVIDIA Warp array."""
2
3import warp as wp
4
5
6def retrieve_positions(tracker, owner_count: int, device_id: int = 0) -> wp.array:
7 """Return tracked positions as a contiguous ``wp.vec3`` CUDA array."""
8 device = f"cuda:{device_id}"
9 positions = wp.empty(owner_count, dtype=wp.vec3, device=device)
10
11 # Warp may use stream-ordered allocation. Make the allocation visible
12 # before DEME accesses it from DEME's own CUDA stream.
13 wp.synchronize_device(device)
14
15 tracker.PositionsToDevice(
16 int(positions.ptr), # pybind11 converts this Python int to std::uintptr_t.
17 owner_count, # Capacity is float3/wp.vec3 elements, not bytes.
18 device_id,
19 )
20 return positions
Pass the CUDA logical ordinal used in the Warp device string as the DEME
device argument. The array must be contiguous and must remain alive until
the synchronous DEME call returns. Synchronizing the Warp device immediately
after allocation is the conservative interoperability choice because Warp may
allocate from a stream-ordered CUDA memory pool while DEME performs the copy on
its own stream.