Handling Diffraction#
Like reflection, diffraction changes the direction of rays that interact with the edges of an object. An edge can be a line segment or any rough transition between two adjacent surfaces. The angle of deflection depends on the angle between the incoming ray and the local direction of the edge at the point of incidence.
Unlike reflection, a ray is diffracted into a continuum of rays that form a cone-like structure around the edge.
In DiffeRT, a TriangleMesh automatically identifies its diffraction edges (wedges) and computes their associated wedge parameters.
import equinox as eqx
import jax.numpy as jnp
from differt.geometry import TriangleMesh, assemble_paths
from differt.plotting import draw_paths, set_backend
from differt.rt import fermat_path_on_linear_objects
from differt.scene import TriangleScene
Visualizing Diffraction Edges and Wedges#
We can load a mesh and plot both all of its triangle edges and the subset of those edges that qualify as diffraction edges (wedges) where the face normal changes.
set_backend("plotly")
corner = TriangleMesh.box()[0:4]
# Automatically extract diffraction edges computed by the triangle mesh
diff_edges = corner.diffraction_edges
# Since there are two directed half-edges for the same physical edge,
# we take the first one.
edge_origins = diff_edges[0:1, 0, :]
edge_vectors = (diff_edges[0:1, 1, :] - diff_edges[0:1, 0, :])[:, None, :]
print("Diffraction edges:", diff_edges)
print("Wedge parameters:", corner.wedge_parameters)
# Plot all triangle edges vs diffraction edges (wedges)
fig_edges = corner.plot(
show_triangle_edges=True,
triangle_edges_kwargs={"name": "All triangle edges", "line_color": "gray"},
show_diffraction_edges=True,
diffraction_edges_kwargs={
"name": "Diffraction edges (wedges)",
"line_color": "red",
"line_width": 5,
},
backend="plotly",
)
fig_edges
Diffraction edges: [[[-0.5 0.5 0.5]
[-0.5 0.5 -0.5]]]
Wedge parameters: [1.5]
Computing Diffraction Paths#
Using the automatically extracted edge coordinates, we find the Fermat paths from the transmitter (BS) to the receivers (UEs) via diffraction on the wedge.
h = jnp.linspace(-0.5, 0.5, 5)
receivers = jnp.stack((jnp.zeros_like(h), jnp.ones_like(h), h), axis=-1)
scene = TriangleScene(
transmitters=jnp.array([-1.0, -0.5, 0.0]), receivers=receivers, mesh=corner
)
fig = scene.plot(
tx_kwargs={"labels": "BS", "name": "BS"},
rx_kwargs={
"labels": "UE",
"marker_size": 5,
"name": "UEs",
"textfont_size": 10,
},
)
paths = fermat_path_on_linear_objects(
scene.transmitters, scene.receivers, edge_origins, edge_vectors
)
paths = assemble_paths(
scene.transmitters,
paths,
scene.receivers,
)
fig = draw_paths(paths, figure=fig, name="Diffraction paths")
fig
Example on Simple Street Canyon#
For a more complex and realistic scenario, we can load the 'simple_street_canyon' scene. In this mesh, many adjacent triangles are coplanar (e.g., on the same building walls) and are therefore filtered out, leaving only the actual building corners/wedges as diffraction edges.
from differt.scene import download_sionna_scenes, get_sionna_scene
download_sionna_scenes()
file = get_sionna_scene("simple_street_canyon")
canyon_mesh = TriangleScene.load_xml(file).mesh
# Plot all triangle edges vs diffraction edges (wedges)
fig_canyon = canyon_mesh.plot(
show_triangle_edges=True,
triangle_edges_kwargs={"name": "All triangle edges", "line_color": "gray"},
show_diffraction_edges=True,
diffraction_edges_kwargs={
"name": "Diffraction edges (wedges)",
"line_color": "red",
"line_width": 3,
},
backend="plotly",
)
fig_canyon
Example on Bruxelles Mesh (Non-Convex Polyhedra)#
Finally, we can load a more complex mesh like the Bruxelles city model. This mesh is particularly interesting because it contains non-convex polyhedra, allowing us to verify if both convex (\(n > 1.0\)) and concave (\(n < 1.0\)) wedges are correctly identified and parameterized.
# Load the Bruxelles mesh file
bruxelles_mesh = TriangleMesh.load_obj("bruxelles.obj")
# Extract the wedge parameters n
n_params = bruxelles_mesh.wedge_parameters
# Print statistics for verification
print(f"Total diffraction edges: {n_params.shape[0]}")
print(f"Convex wedges (n > 1.0): {jnp.sum(n_params > 1.0)}")
print(f"Concave wedges (n < 1.0): {jnp.sum(n_params < 1.0)}")
# Plot all triangle edges vs diffraction edges (wedges)
fig_bruxelles = bruxelles_mesh.plot(
show_triangle_edges=True,
triangle_edges_kwargs={"name": "All triangle edges", "line_color": "gray"},
show_diffraction_edges=True,
diffraction_edges_kwargs={
"name": "Diffraction edges (wedges)",
"line_color": "red",
"line_width": 2,
},
backend="plotly",
)
fig_bruxelles
Total diffraction edges: 11206
Convex wedges (n > 1.0): 9167
Concave wedges (n < 1.0): 1179
/home/docs/checkouts/readthedocs.org/user_builds/differt/envs/latest/lib/python3.12/site-packages/jax/_src/debugging.py:471: UserWarning:
The mesh contains non-manifold edges (edges shared by more than two triangles). These edges will be excluded from diffraction calculations.
The Importance of Vertex Deduplication#
When computing diffraction edges, DiffeRT relies on analyzing adjacency between triangles. However, if a mesh contains duplicate vertices (i.e., multiple vertices sharing identical coordinates), the topology checks will fail because the shared edges are not recognized as being common to adjacent triangles.
By default, TriangleMesh has assume_unique_vertices=False, meaning that it will automatically and transparently deduplicate vertices prior to running diffraction edge detection. Let’s see this in action by rebuilding a box from its individual planes (which duplicates shared vertices at the boundaries):
# Rebuild a box by appending individual planes (causing duplicate vertices at boundary edges)
mesh = TriangleMesh.empty()
for plane in TriangleMesh.box().iter_objects():
mesh += plane
# By default, DiffeRT will automatically deduplicate and successfully find the 8 unique diffraction edges
print("Default diffraction edges count:", mesh.diffraction_edges.shape[0])
# If we manually set assume_unique_vertices to True when the vertices are NOT unique,
# DiffeRT skips deduplication, failing to find the diffraction edges
mesh_no_dedup = eqx.tree_at(lambda m: m.assume_unique_vertices, mesh, True)
print(
"Diffraction edges count without deduplication:",
mesh_no_dedup.diffraction_edges.shape[0],
)
Default diffraction edges count: 8
Diffraction edges count without deduplication: 0