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:

  • pointer is a valid, writable CUDA-accessible pointer on device;

  • the allocation remains alive until the synchronous call returns;

  • capacity is 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

PositionsToDevice

float3

Global position

VelocitiesToDevice

float3

Global linear velocity

AngularVelocitiesLocalToDevice

float3

Local angular velocity

AngularVelocitiesGlobalToDevice

float3

Global angular velocity

OrientationQuaternionsToDevice

float4

Quaternion (x, y, z, w)

FamiliesToDevice

uint32

Family number

MassesToDevice

float32

Mass

MOIsToDevice

float3

Principal moments of inertia

ContactAccelerationsToDevice

float3

Global contact acceleration

ContactAngularAccelerationsLocalToDevice

float3

Local contact angular acceleration

ContactAngularAccelerationsGlobalToDevice

float3

Global contact angular acceleration

OwnerWildcardValuesToDevice

float32

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.