<!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: BSD-3-Clause -->

# Contact Binding: Reading Contact Forces

Contact bindings let you read contact forces between **sensor** bodies and
**filter** bodies. A sensor is a rigid body (or a set of bodies matched by a
physics-object path pattern) whose contacts you want to measure. A filter is a
second set of bodies whose contacts with each sensor you want to isolate.

Contact reporting is opt-in: every authored USD prim matched by `sensor_patterns`
must have `PhysxContactReportAPI` applied. A matched prim without it is dropped
from the binding — the runtime logs `Failed to find contact report API at
'<path>'` — and if that leaves no sensors at all, `create_contact_binding()`
fails. Filter prims need no extra schema, and runtime-only clones (which have no
USD prim) inherit contact reporting from the source actor.

## Prerequisites

- Complete the [Tensor Bindings](tensor_bindings.md) tutorial.
- Your USD scene has rigid body prims in contact (or that will come into contact
  during simulation).
- Every authored USD prim you name in `sensor_patterns` has
  `PhysxContactReportAPI` applied:

  ```usda
  def Mesh "box" (
      prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI", "PhysxContactReportAPI"]
  )
  {
      float physxContactReport:threshold = 0
  }
  ```

  The optional `physxContactReport:threshold` is the force below which contacts are
  not reported; the bundled sample scene uses `0` so every contact is reported.

  The schema must sit on the prim you actually name as the sensor. When the rigid
  body and its collider are separate prims, applying it to the body but naming the
  collider (or the reverse) matches nothing — the two must agree.
- For CUDA output tensors, enable DirectGPU TensorAPI before creating the
  `PhysX` instance; `physxScene:enableGPUDynamics=true` alone only selects GPU
  dynamics. Refer to
  [GPU Warmup and Determinism](../developer_guide.md#gpu-warmup-and-determinism).

## Key Concepts

- **Create the binding before the first step** whose contacts you want to observe.
  The binding registers an internal contact-report callback. No contact data exists
  until at least one `step()`, `step_sync()`, or `step_n_sync()` call has
  completed.
- Create contact bindings once outside simulation loops and reuse them. In Python,
  use the context-manager form or call `cb.destroy()` when finished; otherwise
  garbage collection emits `ResourceWarning` when it eventually releases the
  native binding.
- Reading before the first step returns all-zeros tensors.
- `dt` for the impulse-to-force conversion (`force = impulse / dt`) is taken
  automatically from the last successful `step()`, `step_sync()`, or
  `step_n_sync()` call. You do not pass it manually.
- Result tensor shapes:
  - Net forces: `[S, 3]` -- one 3-D force vector per matched sensor object.
  - Force matrix: `[S, F, 3]` — force vectors per (sensor, filter) pair.
  - Detailed contact data: contact forces and separations use `[C, 1]`;
    positions and normals use `[C, 3]`; all are indexed by `[S, F]`
    count/start-index tensors.
  - Detailed friction data: friction forces and points use `[C, 3]` buffers
    indexed by `[S, F]` count/start-index tensors.

## Python

### Full Binding + Destroy

```python
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause

# NOTE: This file is included verbatim in documentation via literalinclude.
"""
ContactBinding sample: reading contact forces between sensor and filter bodies.

This sample demonstrates:
1. Creating a contact binding before the first simulation step
2. Reading per-sensor net contact forces  [S, 3]
3. Reading a sensor x filter force matrix [S, F, 3]
4. Using the context-manager form to ensure proper cleanup
"""

import numpy as np
from pathlib import Path

from ovphysx import PhysX


def attach_scene(physx, usd_path, stage_name):
    import ovstage

    if not ovstage.population.available():
        raise RuntimeError("ovstage population bridge is unavailable")

    stage = ovstage.Stage(stage_name)
    ordinal = 1
    try:
        ovstage.population.open_usd(stage, str(usd_path), ordinal=ordinal, domains=ovstage.PopulationDomain.PHYSICS)
        # Population does not seal: the caller owns ordinal lifecycle, and
        # attach_ovstage() reads at a sealed ordinal.
        stage.advance_write_floor(ordinal=ordinal).wait()
        physx.attach_ovstage(stage, read_ordinal=ordinal)
        return stage
    except Exception:
        stage.destroy()
        raise


def main():
    # --- 1. Initialize SDK and load scene ---
    PhysX.set_cpu_mode(True)
    physx = PhysX()
    stage = None
    data_dir = Path(__file__).resolve().parent.parent / "data"

    try:
        stage = attach_scene(physx, data_dir / "boxes_falling_on_groundplane.usda", "ovphysx-contact-sample")
        physx.wait_all()

        # --- 2. Create a contact binding BEFORE the first step ---
        # sensor_patterns: bodies whose contact forces you want to read.
        # filter_patterns: bodies to measure contacts against (one per sensor).
        # The binding must be created before any step() call whose contacts you
        # want to observe.
        cb = physx.create_contact_binding(
            sensor_patterns=["/World/Cube1"],
            filter_patterns=["/World/GroundPlane/CollisionMesh"],
            filters_per_sensor=1,
            max_contact_data_count=256,
        )

        sensor_count = cb.sensor_count   # number of matched sensor prims
        filter_count = cb.filter_count   # number of filter prims per sensor

        print(f"Sensors: {sensor_count}, filters per sensor: {filter_count}")

        # --- 3. Simulate until boxes land ---
        for _ in range(120):
            physx.step(1.0 / 60.0)
        physx.wait_all()

        # --- 4. Read net contact forces: shape [S, 3] ---
        # dt is taken automatically from the last successful stepping call.
        net_forces = np.zeros((sensor_count, 3), dtype=np.float32)
        cb.read_net_forces(net_forces)
        print("Net contact forces [S, 3]:", net_forces)

        # --- 5. Read contact force matrix: shape [S, F, 3] ---
        force_matrix = np.zeros((sensor_count, filter_count, 3), dtype=np.float32)
        cb.read_force_matrix(force_matrix)
        print("Contact force matrix [S, F, 3]:", force_matrix)

        # --- 6. Clean up first demo ---
        cb.destroy()

        # Context-manager usage (alternative to manual destroy):
        # Reset the stage so we can reuse the same PhysX instance.
        physx.reset_stage()
        physx.wait_all()
        physx.detach_ovstage()
        stage.destroy()
        stage = None
        stage = attach_scene(physx, data_dir / "boxes_falling_on_groundplane.usda", "ovphysx-contact-sample-reload")
        physx.wait_all()

        with physx.create_contact_binding(sensor_patterns=["/World/Cube1"]) as cb2:
            for _ in range(60):
                physx.step(1.0 / 60.0)
            physx.wait_all()
            out = np.zeros((cb2.sensor_count, 3), dtype=np.float32)
            cb2.read_net_forces(out)
            print("Net forces (context manager):", out)
        # cb2 is automatically destroyed here

        print("Contact binding sample completed successfully")
    finally:
        if stage is not None:
            physx.detach_ovstage()
            stage.destroy()
        physx.release()
        print("Cleanup complete")


if __name__ == "__main__":
    main()
```

### Context-Manager Form (Recommended)

The complete sample above also demonstrates context-manager cleanup after
reloading the stage.

## C

```c
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause

// NOTE: This file is included verbatim in documentation via literalinclude.

#include <ovphysx/ovphysx.h>
#include <ovphysx/ovphysx_types.h>
#include "ovstage_sample.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef __cplusplus
#error "This file must be compiled as C, not C++"
#endif

static int check_result(ovphysx_result_t r, const char* ctx)
{
    if (r.status != OVPHYSX_API_SUCCESS) {
        fprintf(stderr, "ERROR in %s: ", ctx);
        ovphysx_string_t err = ovphysx_get_last_error();
        if (err.ptr && err.length > 0)
            fprintf(stderr, "%.*s\n", (int)err.length, err.ptr);
        else
            fprintf(stderr, "status=%d\n", (int)r.status);
        return 0;
    }
    return 1;
}

static int check_enqueue(ovphysx_enqueue_result_t r, const char* ctx)
{
    if (r.status != OVPHYSX_API_SUCCESS) {
        fprintf(stderr, "ERROR in %s: ", ctx);
        ovphysx_string_t err = ovphysx_get_last_error();
        if (err.ptr && err.length > 0)
            fprintf(stderr, "%.*s\n", (int)err.length, err.ptr);
        else
            fprintf(stderr, "status=%d\n", (int)r.status);
        return 0;
    }
    return 1;
}

static int wait_op(ovphysx_handle_t handle, ovphysx_op_index_t op_index, const char* ctx)
{
    ovphysx_op_wait_result_t wait_result = {0};
    ovphysx_result_t r = ovphysx_wait_op(handle, op_index, UINT64_MAX, &wait_result);
    int has_errors = (wait_result.num_errors > 0);
    ovphysx_destroy_wait_result(&wait_result);
    if (has_errors) {
        fprintf(stderr, "ERROR in %s: async operation failed\n", ctx);
        return 0;
    }
    if (r.status != OVPHYSX_API_SUCCESS) {
        fprintf(stderr, "ERROR in %s: wait failed (status=%d)\n", ctx, (int)r.status);
        return 0;
    }
    return 1;
}

/* Allocate a 2-D float32 DLTensor on the CPU. Caller frees data and shape. */
static DLTensor make_tensor_f32_2d(size_t rows, size_t cols, float** out_data, int64_t** out_shape)
{
    DLTensor t;
    memset(&t, 0, sizeof(DLTensor));
    *out_data  = (float*)calloc(rows * cols, sizeof(float));
    *out_shape = (int64_t*)malloc(2 * sizeof(int64_t));
    (*out_shape)[0] = (int64_t)rows;
    (*out_shape)[1] = (int64_t)cols;
    t.data         = *out_data;
    t.ndim         = 2;
    t.shape        = *out_shape;
    t.strides      = NULL;
    t.byte_offset  = 0;
    t.dtype.code   = kDLFloat;
    t.dtype.bits   = 32;
    t.dtype.lanes  = 1;
    t.device.device_type = kDLCPU;
    t.device.device_id   = 0;
    return t;
}

/* Allocate a 3-D float32 DLTensor on the CPU. Caller frees data and shape. */
static DLTensor make_tensor_f32_3d(size_t d0, size_t d1, size_t d2,
                                   float** out_data, int64_t** out_shape)
{
    DLTensor t;
    memset(&t, 0, sizeof(DLTensor));
    *out_data  = (float*)calloc(d0 * d1 * d2, sizeof(float));
    *out_shape = (int64_t*)malloc(3 * sizeof(int64_t));
    (*out_shape)[0] = (int64_t)d0;
    (*out_shape)[1] = (int64_t)d1;
    (*out_shape)[2] = (int64_t)d2;
    t.data         = *out_data;
    t.ndim         = 3;
    t.shape        = *out_shape;
    t.strides      = NULL;
    t.byte_offset  = 0;
    t.dtype.code   = kDLFloat;
    t.dtype.bits   = 32;
    t.dtype.lanes  = 1;
    t.device.device_type = kDLCPU;
    t.device.device_id   = 0;
    return t;
}

static int run(void)
{
    ovphysx_result_t r;
    ovphysx_enqueue_result_t er;

    /* 1. Initialize SDK */
    r = ovphysx_initialize();
    if (!check_result(r, "ovphysx_initialize")) return 1;

    ovphysx_create_args args = OVPHYSX_CREATE_ARGS_DEFAULT;

    ovphysx_handle_t handle = 0;
    r = ovphysx_create_instance(&args, &handle);
    if (!check_result(r, "ovphysx_create_instance")) { ovphysx_shutdown(); return 1; }

    /* 2. Populate ovstage from USD and attach it */
    ovphysx_sample_stage_attachment_t stage_attachment = {0};
    if (!ovphysx_sample_attach_usd_with_ovstage(
            handle, OVPHYSX_TEST_DATA "/boxes_falling_on_groundplane.usda", &stage_attachment)) {
        ovphysx_destroy_instance(handle); ovphysx_shutdown(); return 1;
    }

    /* 3. Create contact binding BEFORE the first step.
     *    sensor: the falling box.  filter: the ground plane. */
    ovphysx_string_t sensors[1];
    sensors[0] = ovphysx_cstr("/World/Cube1");

    ovphysx_string_t filters[1];
    filters[0] = ovphysx_cstr("/World/GroundPlane/CollisionMesh");

    ovphysx_contact_binding_handle_t cb = 0;
    r = ovphysx_create_contact_binding(handle, sensors, 1, /* 1 sensor pattern */
                                       filters, 1, /* 1 filter pattern per sensor */
                                       256, /* flat contact-data capacity */
                                       &cb);
    if (!check_result(r, "ovphysx_create_contact_binding")) {
        ovphysx_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle); ovphysx_shutdown(); return 1;
    }

    /* 4. Query matched sensor / filter counts */
    int32_t sensor_count = 0, filter_count = 0;
    r = ovphysx_get_contact_binding_spec(handle, cb, &sensor_count, &filter_count);
    if (!check_result(r, "ovphysx_get_contact_binding_spec")) {
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        return 1;
    }
    printf("Sensors: %d  Filters per sensor: %d\n", sensor_count, filter_count);

    /* 5. Simulate until the box lands */
    for (int i = 0; i < 120; i++) {
        er = ovphysx_step(handle, 1.0f / 60.0f);
        if (!check_enqueue(er, "ovphysx_step")) {
            ovphysx_destroy_contact_binding(handle, cb);
            ovphysx_sample_destroy_stage(handle, &stage_attachment);
            ovphysx_destroy_instance(handle);
            ovphysx_shutdown();
            return 1;
        }
    }
    if (!wait_op(handle, er.op_index, "step")) {
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        return 1;
    }

    /* 6. Read net contact forces: shape [S, 3].
     *    dt is taken automatically from the last successful stepping call. */
    float* net_data   = NULL;
    int64_t* net_shp  = NULL;
    DLTensor net_tensor = make_tensor_f32_2d(
        (size_t)sensor_count, 3, &net_data, &net_shp);

    r = ovphysx_read_contact_net_forces(handle, cb, &net_tensor);
    if (!check_result(r, "ovphysx_read_contact_net_forces")) {
        free(net_data); free(net_shp);
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        return 1;
    }
    printf("Net contact forces [%d, 3]:\n", sensor_count);
    for (int s = 0; s < sensor_count; s++) {
        printf("  sensor %d: fx=%.3f  fy=%.3f  fz=%.3f\n",
               s,
               net_data[s * 3 + 0],
               net_data[s * 3 + 1],
               net_data[s * 3 + 2]);
    }
    free(net_data); free(net_shp);

    /* 7. Read contact force matrix: shape [S, F, 3]. */
    float* mat_data   = NULL;
    int64_t* mat_shp  = NULL;
    DLTensor mat_tensor = make_tensor_f32_3d(
        (size_t)sensor_count, (size_t)filter_count, 3,
        &mat_data, &mat_shp);

    r = ovphysx_read_contact_force_matrix(handle, cb, &mat_tensor);
    if (!check_result(r, "ovphysx_read_contact_force_matrix")) {
        free(mat_data); free(mat_shp);
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        return 1;
    }
    printf("Contact force matrix [%d, %d, 3]:\n", sensor_count, filter_count);
    for (int s = 0; s < sensor_count; s++) {
        for (int f = 0; f < filter_count; f++) {
            int base = (s * filter_count + f) * 3;
            printf("  [%d][%d]: fx=%.3f  fy=%.3f  fz=%.3f\n",
                   s, f,
                   mat_data[base + 0],
                   mat_data[base + 1],
                   mat_data[base + 2]);
        }
    }
    free(mat_data); free(mat_shp);

    printf("Contact binding sample completed successfully\n");

    /* 8. Destroy contact binding */
    ovphysx_destroy_contact_binding(handle, cb);

    ovphysx_sample_destroy_stage(handle, &stage_attachment);
    ovphysx_destroy_instance(handle);
    ovphysx_shutdown();
    printf("Cleanup complete\n");

    return 0;
}

int main(void) {
    int rc = run();
    return rc;
}
```

## Unfiltered Contacts

Pass `filter_patterns=None` and `filters_per_sensor=0` to collect contacts with
all bodies:

```python
def create_unfiltered_binding(physx):
    return physx.create_contact_binding(
        sensor_patterns=["/World/robot/ee"],
        max_contact_data_count=512,
    )
```

In C:

```c
#include <ovphysx/ovphysx.h>

static ovphysx_result_t create_unfiltered_binding(
    ovphysx_handle_t handle,
    ovphysx_contact_binding_handle_t* out_binding)
{
    ovphysx_string_t sensors[] = { ovphysx_cstr("/World/robot/ee") };
    return ovphysx_create_contact_binding(
        handle, sensors, 1, NULL, 0, 512, out_binding);
}
```

## Multiple Sensors and Filters

The `filter_patterns` array is **flat** and must have length
`len(sensor_patterns) * filters_per_sensor`. Each block of `filters_per_sensor`
entries corresponds to one sensor:

```python
def create_filtered_binding(physx):
    # 2 sensors, 2 filters each -> 4 filter entries total
    return physx.create_contact_binding(
        sensor_patterns=["/World/robot_0/ee", "/World/robot_1/ee"],
        filter_patterns=[
            "/World/obstacle_A", "/World/obstacle_B",  # robot_0/ee filters
            "/World/obstacle_A", "/World/obstacle_B",  # robot_1/ee filters
        ],
        filters_per_sensor=2,
    )
```

## Detailed Contact and Friction Data

Use `cb.max_contact_data_count` to allocate reusable flat buffers. For each
sensor/filter pair, `counts[s, f]` and `start_indices[s, f]` identify the valid
slice inside the flat buffers.

`cb.sensor_paths` returns the resolved sensor paths in row order.
`cb.filter_paths` returns a nested `[sensor][filter]` list in column order.

Create the binding with `filter_patterns`, `filters_per_sensor > 0`, and
`max_contact_data_count > 0` before calling `read_contact_data()` or
`read_friction_data()`. The aggregate `read_net_forces()` and
`read_force_matrix()` calls do not require this detailed-contact capacity.
`counts` and `start_indices` may be `int32` or `uint32`; NumPy's default integer
dtype is usually `int64`, so allocate these arrays with an explicit dtype.

If the flat buffers are too small, the read raises `RuntimeError` instead of
returning incomplete data. Do not use the payload arrays after that error.
`counts` and `start_indices` still contain the full required layout. Use
`int(np.max(start_indices.astype(np.int64) + counts.astype(np.int64)))` as
`max_contact_data_count` when
recreating the binding for subsequent simulation steps. Recreating a binding
does not recover the overflowing step's payload.

```python
import numpy as np

def read_detailed_data(binding):
    capacity = binding.max_contact_data_count
    contact_forces = np.zeros((capacity, 1), dtype=np.float32)
    positions = np.zeros((capacity, 3), dtype=np.float32)
    normals = np.zeros((capacity, 3), dtype=np.float32)
    separations = np.zeros((capacity, 1), dtype=np.float32)
    shape = (binding.sensor_count, binding.filter_count)
    counts = np.zeros(shape, dtype=np.int32)
    starts = np.zeros(shape, dtype=np.int32)

    binding.read_contact_data(
        contact_forces,
        positions,
        normals,
        separations,
        counts,
        starts,
    )

    sensor = 0
    contact_filter = 0
    start = starts[sensor, contact_filter]
    stop = start + counts[sensor, contact_filter]
    sensor_filter_positions = positions[start:stop]

    friction_forces = np.zeros((capacity, 3), dtype=np.float32)
    friction_points = np.zeros((capacity, 3), dtype=np.float32)
    friction_counts = np.zeros(shape, dtype=np.int32)
    friction_starts = np.zeros(shape, dtype=np.int32)

    binding.read_friction_data(
        friction_forces,
        friction_points,
        friction_counts,
        friction_starts,
    )
    return sensor_filter_positions, friction_forces, friction_points
```

Friction data is per friction anchor, not a pre-summed `[S, F, 3]` pair
force. To build a pair-level friction force tensor, sum
`friction_forces[start:stop]` for each `(sensor, filter)` pair using the
matching `friction_counts` and `friction_starts` entries.

This flat representation matches the underlying PhysX tensor API and avoids a
fixed per-pair contact-point dimension. Build a padded `[S, F, K, D]` view in
application code only if that layout is useful for a specific algorithm.
