Outputs#

The Acoustic generic wpm model delivers a SignalWays composite output containing six channels and seven parameters. Each signal way holds the sampled echo envelope for one receiving mount on one channel id. The array topology determines the number and order of signal ways; USD Attributes describes how to configure it.

SignalWays has no Counts channel, no per-point VALID flag, no coordinates, no coordinate-type, frame-of-reference or pose params, and no material identifier: the frame’s extent comes from its params, and nothing in it is a position. Use the layout and validity rules below when reading it. Sensor Outputs describes the shared output container.

Requesting the output#

Request SignalWays with a single RenderVar whose sourceName is SignalWays. This composite output includes all six channels and the frame header. The model ignores the RenderVar’s channels list; individual channels cannot be selected.

Channel tensors are stored in device memory; parameters are stored on the host. Map the output to CPU memory to read it with NumPy, or pass the device tensors to a CUDA-aware consumer. See Sensor Outputs for both approaches and the C API and Python API for the container definitions.

Set Up an Acoustic Sensor shows the complete sensor setup. Output Configuration explains RenderProducts and RenderVars.

The acoustic example scene authors this request:

#usda 1.0
    # The acoustic model publishes one composite RenderVar, "SignalWays". It
    # carries every per-sample channel plus the frame header as parameters, so
    # the product requests a single var rather than one per channel.
    def "Render"
    {
        def "Products"
        {
            def RenderProduct "AcousticProduct"
            {
                rel camera = </World/Acoustic>
                rel orderedVars = [<../../Vars/SignalWays>]
            }
        }

        def "Vars"
        {
            def RenderVar "SignalWays"
            {
                uniform string sourceName = "SignalWays"
            }
        }
    }
from pxr import Gf, Sdf

world = stage.DefinePrim("/World", "Xform")

render = stage.DefinePrim("/World/Render", "")

products = stage.DefinePrim("/World/Render/Products", "")

acoustic_product = stage.DefinePrim("/World/Render/Products/AcousticProduct", "RenderProduct")
acoustic_product.CreateRelationship("camera", custom=False).SetTargets(["/World/Acoustic"])
acoustic_product.CreateRelationship("orderedVars", custom=False).SetTargets(
    ["/World/Render/Vars/SignalWays"]
)

vars = stage.DefinePrim("/World/Render/Vars", "")

signal_ways = stage.DefinePrim("/World/Render/Vars/SignalWays", "RenderVar")
signal_ways.CreateAttribute(
    "sourceName", Sdf.ValueTypeNames.String, custom=False, variability=Sdf.VariabilityUniform
).Set("SignalWays")

The model takes its frame rate from the shared omni:sensor:frameRate attribute, which the example scene sets to 10 Hz, and it declares no partial-output mode. How the renderer schedules captures against your simulation steps is on Sensor Scheduling. The model also needs the renderer’s motion BVH active. Select AUTO or ENABLE when creating the renderer; the switch and its modes are under Motion BVH.

Signal ways#

A signal way contains the envelope recorded by one receiving mount on one channel id, plus transmitter, receiver, channel and firing-time metadata. Each firing event transmits from one mount to a receiver group. Receivers that detect an echo populate their corresponding signal ways. For example:

A firing event with

Produces

Transmitter: sensor mount 0

Receiver group: g001 with receivers [0, 1, 2]

Channel id: 0

Three signal ways, one per receiver: 0 to 0, 0 to 1 and 0 to 2, all on channel id 0. Each holds the samples showing how the pulse reached that particular receiver.

Events in the same firing sequence share a signal way when they use the same receiver and channel id, even if their transmitters differ. Their echoes combine, while TransmitterIds and TimeOffsetNs can identify only one event. Assign separate channel ids when events must remain distinguishable; see USD Attributes and Lay Out a Transducer Array.

A channel id is a firing event’s integer channel label. A channel is a named tensor in the SignalWays output. See the glossary for other uses.

Signal-way layout#

Each frame reserves numSignalWays = mountCount * channelCount slots, one for every combination of mount and distinct channel id used across all firing sequences. Slots are allocated even for combinations that receive no echo.

The slot index is receiverIndex * channelCount + channelId:

  • receiverIndex is the mount’s zero-based position in instance order.

  • channelCount is the number of distinct channel ids across all firing sequences.

  • channelId is the firing event’s channel id. Channel ids must be contiguous from zero.

Each frame contains echoes from one firing sequence. A slot’s Flags entry has the VALID bit (0x40) set when its receiver received an echo in that frame. numSignalWays describes the allocated layout, not the number of valid echoes.

Reshape Amplitudes to [numSignalWays, numSamplesPerSignalWay] to get one envelope per row. The five metadata channels describe those rows; their entry at index w describes signal way w. See Channels for tensor shapes and read bounds, and the attribute reference for configuration limits.

Grid of one SignalWays frame. Each row is one signal way, indexed by the receiving mount's index times the number of distinct channel ids plus the channel id, and holds 320 Amplitudes samples. Beside the grid, the five per-signal-way channels hold one entry per row, and the VALID bit of Flags marks the rows that received an echo. An axis under the columns converts the sample index to range in meters.

Three mounts and two channel ids allocate six signal ways, shown here with 320 samples per envelope. Metadata entries correspond to envelope rows; Flags identifies rows that received echoes. The range axis uses the default sample duration.#

Channels#

Every channel is a flat tensor of E entries, where E is the numElements param: signal ways times samples per signal way. Only Amplitudes uses all of them; the other five describe a signal way as a whole.

Channel

Type

Shape

Unit

Meaning

TransmitterIds

float32

[E]

unitless

Index of the transmitting mount, the firing event’s txSensorId, for the signal way at this index. One entry per signal way.

ReceiverIds

float32

[E]

unitless

Index of the receiving mount for the signal way at this index: a mount index, not a receiver-group index. One entry per signal way.

ChannelIds

float32

[E]

unitless

Channel id of the firing event that produced the signal way, as authored in the firing sequence’s channel array. One entry per signal way.

Amplitudes

float32

[E]

unitless

Echo amplitude of one envelope sample. The one genuinely per-sample channel: sample s of signal way w is entry w * numSamplesPerSignalWay + s.

TimeOffsetNs

int32

[E]

ns

Firing time of the signal way’s event within its firing sequence, the authored eventTimeNs, in whole nanoseconds. One entry per signal way.

Flags

uint8

[E]

unitless

The VALID bit (0x40) at a signal way’s own index when that signal way received an echo this frame; zero everywhere else. One entry per signal way, not per sample, in both operating modes. Refer to Validity.

In both operating modes, TransmitterIds, ReceiverIds, ChannelIds, TimeOffsetNs and Flags use one entry per signal way. Entry w describes row w of the reshaped Amplitudes tensor. Entries beyond numSignalWays are zero. A signal way with no echo also has zero metadata, so use Flags to distinguish it from a valid echo whose identifiers and time offset are all zero; see Validity.

TransmitterIds, ReceiverIds and ChannelIds are float32 tensors. Cast their values to integers before using them as indices, as shown in Reading an envelope.

Params#

Every frame includes the following seven CPU-resident parameters as tensors of shape [1], including frames that were not traced.

Param

Type

Meaning

frameId

uint64

The model’s own frame counter. It advances with every frame the renderer does not discard, traced or not, and a live reconfiguration does not reset it.

numSignalWays

uint32

Signal ways the frame is laid out for: one for every pairing of a mount, as receiver, with a channel id used anywhere in the firing sequences. It counts signal ways with no echo too; the VALID bit says which ones did.

numSamplesPerSignalWay

uint32

Envelope samples per signal way. A model constant, 320, not derived from pulseDuration or sampleDuration and not stated in the schema.

numElements

uint32

numSignalWays * numSamplesPerSignalWay: the length E of every channel.

cycleIndex

uint32

Zero-based index of the firing sequence this frame traced, in the instance-name order of USD Attributes, so seq001 is 0. The model advances it once per traced frame and wraps it after the last sequence.

centerFrequencyHz

float32

The authored centerFrequency, in Hz.

sampleDurationUs

float32

The authored sampleDuration, converted from seconds to microseconds. The value the conversion under Sample ranges uses.

Use numSignalWays and numSamplesPerSignalWay to reshape the numElements samples. Read these dimensions from the output params. Count valid signal ways from Flags; no parameter reports this count.

In basic mode, sampleDuration controls range spacing. Mounts and distinct firing-sequence channel ids determine numSignalWays; receiver groups determine which signal ways receive echoes. The 320-sample length and layout are fixed. See USD Attributes for these controls and the enableInterference mode selector, and Validity for the basic-mode noise bounds noiseMin and noiseMax.

Validity#

Read the first numSignalWays entries of Flags. Signal way w received an echo when Flags[w] & 0x40 is nonzero. In both operating modes, the model sets only this VALID bit. It applies to the whole envelope, not individual samples. Bound amplitude reads by numElements. See Sensor Outputs for validity-bit handling.

Basic mode adds uniform noise even to envelopes with no echo. Nonzero amplitude therefore does not establish validity; use Flags. noiseMin and noiseMax set the noise bounds.

Startup frames#

The first frames after sensor startup contain no echo data and have no identifying parameter. Discard a few frames before reading envelopes; the example takes three warm-up steps (Set Up an Acoustic Sensor). Renderer warm-up for texture streaming and path-tracing convergence is described in Output Configuration. If no frame contains echo data, see the Symptom Index.

Reading an envelope#

Map SignalWays to CPU memory. Reshape Amplitudes using numSignalWays and numSamplesPerSignalWay, then read the first numSignalWays identity entries and cast them to integers. This Python example handles frames with no signal ways before reshaping. The acoustic example has no C counterpart:

def read_acoustic_signal_ways(frame) -> dict:
    """Map the SignalWays composite tensor to CPU and return one frame of echoes."""
    with frame.render_vars[SIGNAL_WAYS_RENDER_VAR].map(device=ovrtx.Device.CPU) as signal_ways:

        def param(name: str) -> float:
            return float(np.from_dlpack(signal_ways.params[name])[0])

        num_signal_ways = int(param("numSignalWays"))
        num_samples = int(param("numSamplesPerSignalWay"))

        if num_signal_ways == 0 or num_samples == 0:
            return {"num_signal_ways": 0, "num_samples": 0}

        num_elements = num_signal_ways * num_samples

        def channel(name: str) -> np.ndarray:
            return np.from_dlpack(signal_ways[name])[:num_elements]

        return {
            "num_signal_ways": num_signal_ways,
            "num_samples": num_samples,
            "sample_duration_s": param("sampleDurationUs") * 1.0e-6,
            "center_frequency_hz": param("centerFrequencyHz"),
            "cycle_index": int(param("cycleIndex")),
            "envelopes": channel("Amplitudes").reshape(num_signal_ways, num_samples).copy(),
            # The identifier channels carry one entry per signal way rather than one per
            # element, so only the first num_signal_ways entries are meaningful.
            "transmitter_ids": channel("TransmitterIds")[:num_signal_ways].astype(int),
            "receiver_ids": channel("ReceiverIds")[:num_signal_ways].astype(int),
            "channel_ids": channel("ChannelIds")[:num_signal_ways].astype(int),
        }


The function also carries sampleDurationUs out of the params, converted to seconds, because the next step needs it. Mapping, synchronization and the lifetime of a mapped output are the same for every composite output and are explained on Sensor Outputs.

Sample ranges#

In basic mode (enableInterference false, the default), bin s covers round-trip times from (s + 1) Δt up to (s + 2) Δt, excluding the upper bound. Here Δt is sampleDurationUs converted to seconds. Multiply either time bound by 343.21 m/s and divide by two to obtain its range bound. Read the duration from the frame parameters because it can change at runtime.

The example plots each sample at the lower bound of its range bin:

import numpy as np


def sample_ranges_m(frame_data: dict) -> np.ndarray:
    """Return the lower range bound, in meters, of each envelope bin."""
    sample_index = np.arange(frame_data["num_samples"], dtype=np.float64)
    # Bin s holds round-trip times [(s + 1) dt, (s + 2) dt).
    return (sample_index + 1) * frame_data["sample_duration_s"] * 343.21 / 2.0


At the default sampleDuration of 102.4 µs, samples are about 1.8 cm apart in range, and the 320-sample envelope covers about 5.6 m. Selecting the peak sample gives a range estimate at that spacing.

Increasing sampleDuration extends the range covered by the envelope but increases the spacing between samples. Decreasing it gives finer spacing over a shorter range. Coverage remains subject to the model’s fixed propagation limits.

Multiple propagation paths can produce several peaks from one pulse. Ground or secondary-surface reflections add longer paths; different points on an extended reflector also have different path lengths. The strongest sample can therefore lie beyond the nearest surface. When estimating one range from the peak, account for this ambiguity and the sample spacing. In the example, the strongest sample identifies the concrete wall 3.0 m ahead of the sensor.