Set Up an Acoustic Sensor#
The Acoustic generic wpm model records sampled echo envelopes from ultrasonic
pulses. A scene needs an OmniAcoustic prim with
OmniSensorGenericAcousticWpmAPI and a RenderProduct requesting
SignalWays. This procedure uses the acoustic example to configure the sensor,
materials and renderer, then read the strongest echo. See Outputs
for the signal-way layout and validity rules.
Before you start. Use the acoustic example to run these steps. If you are new to sensor scenes, first read Your First Lidar Sensor.
Request the SignalWays output#
Target the sensor with the RenderProduct’s camera relationship and add
a RenderVar with sourceName = "SignalWays" to orderedVars.
See RenderProducts
and RenderVars.
This is the model’s only output type:
#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")
SignalWays is a composite output, so this one RenderVar carries every
channel and param of the frame; Outputs lists them and shows how to
read them.
Label the surfaces#
Surface response uses omni:simready:nonvisual:* labels on the bound
Material prim. The example’s asphalt ground and concrete wall use these
materials without visual shaders:
#usda 1.0
def Xform "Materials"
{
# These materials intentionally omit visual shaders. The sensor return
# behavior comes from the nonvisual attributes on the bound Material.
def Material "Asphalt"
{
token omni:simready:nonvisual:base = "asphalt"
token omni:simready:nonvisual:coating = "none"
token[] omni:simready:nonvisual:attributes = ["none"]
}
def Material "Concrete"
{
token omni:simready:nonvisual:base = "concrete"
token omni:simready:nonvisual:coating = "none"
token[] omni:simready:nonvisual:attributes = ["none"]
}
}
from pxr import Gf, Sdf
world = stage.DefinePrim("/World", "Xform")
materials = stage.DefinePrim("/World/Materials", "Xform")
asphalt = stage.DefinePrim("/World/Materials/Asphalt", "Material")
asphalt.CreateAttribute(
"omni:simready:nonvisual:base", Sdf.ValueTypeNames.Token, custom=False
).Set("asphalt")
asphalt.CreateAttribute(
"omni:simready:nonvisual:coating", Sdf.ValueTypeNames.Token, custom=False
).Set("none")
asphalt.CreateAttribute(
"omni:simready:nonvisual:attributes", Sdf.ValueTypeNames.TokenArray, custom=False
).Set(["none"])
concrete = stage.DefinePrim("/World/Materials/Concrete", "Material")
concrete.CreateAttribute(
"omni:simready:nonvisual:base", Sdf.ValueTypeNames.Token, custom=False
).Set("concrete")
concrete.CreateAttribute(
"omni:simready:nonvisual:coating", Sdf.ValueTypeNames.Token, custom=False
).Set("none")
concrete.CreateAttribute(
"omni:simready:nonvisual:attributes", Sdf.ValueTypeNames.TokenArray, custom=False
).Set(["none"])
Non-Visual Sensor Materials explains the label vocabulary and how to author it.
Create the renderer with the motion BVH enabled#
This model needs an active motion BVH (see Outputs). Select AUTO
or ENABLE when creating the renderer; this example uses ENABLE. The
snippet also creates the stage and attaches it to the renderer:
main.py, from the runnable acoustic example## Enable motion BVH: the acoustic model runs the multi-tick trace path, and
# every envelope comes back empty without it.
log_file_path = None
if args.log is not None:
args.log.parent.mkdir(parents=True, exist_ok=True)
log_file_path = str(args.log)
renderer = ovrtx.Renderer(
ovrtx.RendererConfig(
log_file_path=log_file_path,
log_level="info",
motion_bvh=ovrtx.MotionBvh.ENABLE,
)
)
stage = ovstage.Stage("ovrtx.example.acoustic")
renderer.attach_ovstage(stage)
Motion BVH describes the switch in full: the modes, the C call, its lifetime and GPU support.
Load the scene and warm up#
Open the USDA into the attached stage and wait until that write has landed, so the sensor and its product exist before the first step:
# The USDA scene defines the sensor, the nonvisual materials it returns from,
# the render product, and a concrete wall 3.0 m along the sensor boresight.
print(f"Loading acoustic scene from {args.scene}...")
ordinal = 1
ovstage.population.open_usd(stage, str(args.scene), ordinal=ordinal)
stage.advance_write_floor(ordinal, ovstage.Scope.ALL).wait()
The model’s first frames carry no echo data (Startup frames), so take a few steps before you read one. The example takes three; each step renders the acoustic product and advances time by 0.1 s, one frame period at the authored 10 Hz:
# Warm up the sensor before reading an envelope: the model publishes nothing
# on its first frame.
for _ in range(WARMUP_STEPS):
renderer.step(render_products={RENDER_PRODUCT}, delta_time=STEP_DT_SECONDS, ordinal=ordinal)
Note
Warming up the renderer counts this settling among the reasons to warm the renderer up, beside texture streaming and path-tracing convergence.
Render a frame and read the strongest echo#
One more step returns the product’s frame. The snippet reads the frame through
the example’s two helpers, which map the SignalWays output to the CPU,
reshape the envelopes to one row per signal way and convert a sample index to a
range; it then takes the strongest sample of each row and prints the median peak
range and amplitude over the signal ways that responded:
# Render one acoustic frame and report the strongest echo, which locates the
# wall the scene places 3.0 m ahead of the sensor.
products = renderer.step(
render_products={RENDER_PRODUCT}, delta_time=STEP_DT_SECONDS, ordinal=ordinal
)
frame = products[RENDER_PRODUCT].frames[0]
frame_data = read_acoustic_signal_ways(frame)
if frame_data["num_signal_ways"] == 0:
print("no signal ways reported")
else:
ranges_m = sample_ranges_m(frame_data)
peaks = frame_data["envelopes"].max(axis=1)
responding = peaks > 0.0
peak_sample = np.argmax(frame_data["envelopes"][responding], axis=1)
print(
f"signal ways={frame_data['num_signal_ways']}, "
f"responding={int(np.count_nonzero(responding))}, "
f"median peak range={float(np.median(ranges_m[peak_sample])):.3f} m, "
f"median peak amplitude={float(np.median(peaks[responding])):.4g}"
)
if rr is not None:
log_acoustic_signal_ways(rr, frame_data)
Both helpers are shown and explained on Outputs. The runnable acoustic example adds a plot of the envelopes and its command-line options.
Verification#
The example prints four values:
signal ways: the number of waveforms the frame is laid out for.responding: how many contain a response.median peak range: the median range, in meters, over responding waveforms.median peak amplitude: their median peak amplitude.
Expect one signal way, one response and a peak near the wall at 3.0 m. Noise causes the amplitude to vary slightly between runs. The default topology has one mount listening to itself on one channel id; see Layout.
The strongest sample locates the wall. At the default sample duration, each sample spans about 1.8 cm of range and 320 samples reach about 5.6 m. See Sample ranges.
A frame with no signal ways prints no signal ways reported. If you see that,
or only noise with no peak, read Potential issues.
Potential issues#
Each of these is a configuration you can correct; the Symptom Index collects them with the symptoms of the other models.
Every envelope is noise or zero, and no signal way carries the VALID bit. The renderer was created without the motion BVH, which this model needs enabled when the renderer is created; without it every envelope comes back empty. Refer to Missing output and to Motion BVH.
The frame carries no echo data although the renderer has the motion BVH enabled. It is one of the first frames after the sensor was created, which carry no echo data, and nothing in the params marks such a frame; step past them as Load the scene and warm up does. Refer to Startup frames and to Missing output.
The peak range is not the wall’s, or no echo is detected where the wall stands. The prim is not oriented along its firing axis: the lobe goes where the prim’s local
-Zpoints, and a transform that does not map that axis onto the wall aims it elsewhere. Refer to Incorrect coordinates or units and to Orient the prim.Every surface responds alike. The
Materialprims bound to them carry no non-visual material label, or the labels they carry are not being read. Refer to Values look plausible but are wrong and to Non-Visual Sensor Materials.