Understanding the Paths Object

Sionna RT uses aPathSolverto compute propagationPaths between the antennas oftransmitters and receivers in a scene. The goal of this developer guide is toexplain the properties of thePaths object in detail. Let’s start with ashort code snippet that computes propagation paths between a transmitter and receiver:

# Importsimportsionna.rtfromsionna.rtimportload_scene,PlanarArray,Transmitter,Receiver, \PathSolver# Load scenescene=load_scene(sionna.rt.scene.munich,merge_shapes=False)# Configure TX/RX antenna arrayscene.tx_array=PlanarArray(num_rows=2,num_cols=1,pattern="iso",polarization="V")scene.rx_array=scene.tx_array# Create TX/RXscene.add(Transmitter(name="tx",position=[8.5,21,27]))scene.add(Receiver(name="rx",position=[44,95,1.5]))# Compute propagation pathsp_solver=PathSolver()# without a synthetic arraypaths=p_solver(scene=scene,max_depth=3,synthetic_array=False)# with a synthetic arraypaths_syn=p_solver(scene=scene,max_depth=3,synthetic_array=True)

Depending on the boolean argumentsynthetic_array used during the call of thepath solver above, a source/target is either a transmit/receive antenna or a pointlocated at the center of an antenna array. In our example, there are two sourcesand targets (one for each antenna of the array) ifsynthetic_array isFalse. There is a single source and target (one for each radio device) ifsynthetic_array isTrue. This can be seen from the properties of the paths objects as shown below:

# Show sources/targetsprint("Source coordinates:\n",paths.sources)print("Target coordinates:\n",paths.targets)# Show sources/targets with `synthetic_array`print("Source coordinates (synthetic array):\n",paths_syn.sources)print("Target coordinates (synthetic array):\n",paths_syn.targets)
Sourcecoordinates:[[8.5,21,27.0214],[8.5,21,26.9786]]Targetcoordinates:[[44,95,1.52141],[44,95,1.47859]]Sourcecoordinates(syntheticarray):[[8.5,21,27]]Targetcoordinates(syntheticarray):[[44,95,1.5]]

Apart from the paths coefficientspaths.a and delayspaths.tau, the paths instance stores a lot ofside information about the propagation paths, such as angles of arrival anddeparture, Doppler shifts, interaction types, ids of intersected objects, andcoordinates of intersection points (i.e., vertices).

# Let us inspect a specific path in detailpath_idx=4# Try out other values in the range [0, 14]# For a detailed overview of the dimensions of all properties, have a look at the API documentationprint(f"\n--- Detailed results for path{path_idx} ---")print(f"Channel coefficient:{paths.a[0].numpy()[0,0,0,0,path_idx]+1j*paths.a[1].numpy()[0,0,0,0,path_idx]}")print(f"Propagation delay:{paths.tau[0,0,0,0,path_idx].numpy()*1e6:.5f} us")print(f"Zenith angle of departure:{paths.theta_t.numpy()[0,0,0,0,path_idx]:.4f} rad")print(f"Azimuth angle of departure:{paths.phi_t.numpy()[0,0,0,0,path_idx]:.4f} rad")print(f"Zenith angle of arrival:{paths.theta_r.numpy()[0,0,0,0,path_idx]:.4f} rad")print(f"Azimuth angle of arrival:{paths.phi_r.numpy()[0,0,0,0,path_idx]:.4f} rad")print(f"Doppler shift:{paths.doppler.numpy()[0,0,0,0,path_idx]:.4f} Hz")
---Detailedresultsforpath4---Channelcoefficient:(-1.0185684914176818e-05-9.316545401816256e-06j)Propagationdelay:0.60660usZenithangleofdeparture:1.7115radAzimuthangleofdeparture:0.1612radZenithangleofarrival:1.4110radAzimuthangleofarrival:-0.9712radDopplershift:0.0000Hz
# Show the interactions undergone by all paths:# 0 - No interaction, 1 - Specular reflection, 2 - Diffuse reflection, 4 - Refraction# Note that diffuse reflections are turned off by default.# Shape [max_depth, num_rx, num_rx_ant, num_tx, num_tx_ant, num_paths]print("Interactions:\n",paths.interactions.numpy()[:,0,0,0,0,:])print("Number of paths: ",paths.interactions.shape[-1])
Interactions:[[410111114411111][100101101101101][400100004400100]]Numberofpaths:15

We can see that there are in total 15 propagation paths (number of columns) with a maximum number of three interactions (number of rows). There is for example a paths consisting of a refraction (4), followed by a specular reflection (1), and another refraction (4). The line-of-sight (LoS) path has no interactions with the scene, i.e., [0,0,0].

The coordinates for every interaction as well as the corresponding object idscan be extracted in the following way:

# Object ids for the selected pathprint("Object IDs:\n",paths.objects.numpy()[:,0,0,0,0,path_idx])# Coordinates of interaction points of the selected pathprint("Vertices:\n",paths.vertices.numpy()[:,0,0,0,0,path_idx])
ObjectIDs:[236442949672954294967295]Vertices:[[42.10770891.0555340.][0.0.0.][0.0.0.]

Note that the second and third object ids equal 4294967295, indicating aninvalid shape.This happens because the path under consideration has only a depth of one,consisting of a single specular reflection before the receiver is reached.

We can recover aSceneObject from its id in the following way:

forobjinscene.objects.values():ifobj.object_id==paths.objects.numpy()[0,0,0,0,0,0]:break

A scene object enriches aMitsuba shape with additional properties.However, all of the currently implemented algorithms assume that a scene objectis constructed from aMitsuba mesh(which inherits from Mitsuba shape). A mesh is defined by a set of triangles(also called faces or primitives), which is not the case for a shape, which could be, e.g., defined as a sphere of acertain radius.

We can access the shape of a scene object via the propertySceneObject.mi_shape:

print(obj.mi_shape)
PLYMesh[name="element_231-itu_marble.ply",bbox=BoundingBox3f[min=[41.2902,113.299,0],max=[109.21,145.203,17.7352]],vertex_count=30,vertices=[360Bofvertexdata],face_count=30,faces=[360Boffacedata],face_normals=1]

ThePaths.primitives property provides the ids of the faces of themi_shapeof scene object that paths intersect. One can recover the normal vectors of theprimitives in the following way:

obj.mi_shape.face_normal(paths.primitives.numpy()[:,0,0,0,0,path_idx])
[[-0.316442,-0.948612,0],[nan,nan,nan],[nan,nan,nan]]

In our example, the path only intersects a single object and there is hence alsoonly a single normal vector of interest.

In some cases, there is a different number of valid paths between differentpairs of sources and targets.An example would be a link between two antenna arrays that is partially occluded,so that only some antennas have a line-of-sight connection.Which paths are valid can be seen from the property`Paths.valid`:

paths.valid
[[[[[True,True,True,True,True,True,True,True,True,True,True,True,True,True,True],[True,True,True,True,True,True,True,True,True,True,True,True,True,True,True]]],[[[True,True,True,True,True,True,True,True,True,True,True,True,True,True,True],[True,True,True,True,True,True,True,True,True,True,True,True,True,True,True]]]]]

This tensor can be useful to mask invalid paths in computations.

The path instance can be used to compute the channel frequency responsePaths.cfr(), the baseband equivalent responsePaths.cir(), or the discrete complex-basebandequivalent responsePaths.taps(). How these functions are used is describedin detail in the tutorialIntroduction to Sionna RT.