Compute edge diffraction

Describe diffracting wedges as DfrStates, accumulate diffracted power and field on a grid, and export explicit diffraction paths with RayD.

RayD's diffraction operations take a list of diffracting wedge states that you supply and evaluate their contribution at receivers. You decide which edges diffract and from which source; RayD samples the edges, checks visibility, evaluates the diffraction coefficient, and sums the result.

OperationWhat it computesResult
accum_dfr_directMonte Carlo diffracted power and field onto a grid, first orderDfrAccum
accum_dfrthe same, up to max_order 2 or 3 through recursive statesDfrAccum
accum_dfr_coherent_directexact first-order coherent field onto a grid, one diffraction point per edge and cellDfrCoherentAccum
trace_dfr_pathsexplicit diffraction paths between transmitters and receiversDfrPaths

Describe the wedges#

A DfrStates record holds one row per wedge and source:

FieldMeaning
countnumber of states (Dr.Jit; Torch derives it from the tensors)
edge_indexthe edge's id, for bookkeeping
edge_pos, edge_dir, edge_t_min, edge_t_maxthe edge segment edge_pos + t * edge_dir for t in [edge_t_min, edge_t_max]
n0, n1the normals of the two wedge faces
prim0, prim1the global primitive ids of the two faces, or -1
exterior_anglethe wedge's exterior angle, in radians
src, src_powerthe source position and power that illuminates the edge
wi, d0incident and initial directions (Dr.Jit fields; optional in Torch)
prefix_depthnumber of reflections before the edge (Dr.Jit)

You can fill the edge fields from Scene.edge_info() (start, vector, normals) and edge_topology() (adjacent faces). accumulate_reflections with collect_wedges=True also records WedgeEvents that tell you which edges reflected rays reached.

Accumulate diffraction on a grid#

The example has one triangle far away at z = 10 so it does not block anything, and one wedge state on the x axis lit from z = 1. The receiver grid is the plane z = -1.

import math
import drjit as dr
import rayd.drjit as rd
from drjit.cuda import Array3f, Array3i, Float, Int, Bool
 
scene = rd.Scene()
scene.add_mesh(rd.Mesh(Array3f([-1.0, 1.0, -1.0], [-1.0, -1.0, 1.0], [10.0, 10.0, 10.0]),
                       Array3i([0], [1], [2])))
scene.build()
 
st = rd.DfrStates()
st.count = 1
st.edge_index = Int([0])
st.edge_pos = Array3f([0.0], [0.0], [0.0])
st.edge_dir = Array3f([1.0], [0.0], [0.0])
st.edge_t_min, st.edge_t_max = Float([-0.5]), Float([0.5])
st.n0 = Array3f([0.0], [1.0], [0.0])
st.n1 = Array3f([0.0], [-1.0], [0.0])
st.prim0, st.prim1 = Int([-1]), Int([-1])
st.exterior_angle = Float([1.5 * math.pi])
st.src = Array3f([0.0], [0.0], [1.0])
st.src_power = Float([2.0])
st.wi = Array3f([0.0], [0.0], [-1.0])
st.d0 = Array3f([0.0], [0.0], [-1.0])
st.prefix_depth = Int([0])
 
grid = rd.DfrGrid()
grid.axis, grid.position = 2, -1.0
grid.coord0_min, grid.coord0_max = -1.0, 1.0
grid.coord1_min, grid.coord1_max = -1.0, 1.0
grid.resolution0 = grid.resolution1 = 1
grid.cell_area = 4.0
 
mat = rd.DfrMaterial()
mat.eta_r, mat.sigma = Float([4.0]), Float([0.0])
mat.mu_r, mat.gain, mat.valid = Float([1.0]), Float([1.0]), Bool([True])

Monte Carlo accumulation#

accum_dfr_direct samples points on each edge. DfrOptions sets the wavelength, the wavenumber k, and the sample budget per strategy: direct_samples, keller_samples (Keller-cone sampling), and suffix_samples (a reflection after the edge). strategy_mask enables strategies with rd.RAYD_DFR_DIRECT, RAYD_DFR_KELLER, and RAYD_DFR_SUFFIX_REFL; the default 3 is direct plus Keller.

do = rd.DfrOptions()
do.wavelength = 0.125
do.k = 2 * math.pi / 0.125
do.direct_samples = 256
do.keller_samples = 256
 
acc = scene.accum_dfr_direct(st, grid, mat, do)
print(acc.power, acc.direct_count, acc.keller_count)   # [0.000706677] [256] [61]

DfrAccum has power and complex field_x, field_y, field_z per cell, plus counters (direct_count, keller_count, suffix_count) and rejection counts (vis_rejects, edge_vis_rejects, utd_rejects). For second and third order, call accum_dfr(initial_states, recursive_states, grid, material, options) with options.max_order set.

Coherent accumulation#

accum_dfr_coherent_direct evaluates one diffraction point per edge and cell (chosen on the edge when select_diffraction_point=True) and sums the complex field coherently. It uses no random samples and does not support gradients. prefilter_visibility (default True) turns on a visibility prefilter before the field is evaluated.

co = rd.DfrCoherentOptions()
co.wavelength = 0.125
co.k = 2 * math.pi / 0.125
 
coh = scene.accum_dfr_coherent_direct(st, grid, mat, co)
print(coh.direct_count)    # [1]
print(dr.abs(coh.direct_field_x))   # [0.0243655]

build_dfr_coherent_tx_states(edges, tx_position, material, options) and build_dfr_coherent_higher_candidates(prev_states, edges, global_to_local_edge_index, options) prepare coherent states from a DfrCoherentEdge table, for transmitter-lit and higher-order chains.

Export explicit paths#

trace_dfr_paths(tx_positions, rx_positions, states, material, options) returns individual diffraction paths instead of a grid, for channel models that need per-path delay and field. DfrPathOptions.max_paths and max_rx size the output buffer; paths.capacity reports the resulting row count.

po = rd.DfrPathOptions()
po.wavelength = 0.125
po.k = 2 * math.pi / 0.125
po.max_paths = 4
po.max_rx = 1
 
paths = scene.trace_dfr_paths(Array3f([0.0], [0.0], [1.0]), Array3f([0.3], [0.0], [-1.0]), st, mat, po)
print(paths.count, paths.valid, paths.edge0)   # [1] [True] [0]
print(paths.delay)                              # [6.74592e-09]

Each path row records its transmitter tx_id, receiver rx_id, order, the edges it uses (edge0 to edge2), its delay, and its field. Convert rows to backend-neutral records with path exchange.

PyTorch#

The Torch backend takes the same records as dataclasses and the options as keyword arguments:

import math, torch
import rayd.torch as rt
 
dev = torch.device("cuda")
vec3 = lambda rows: torch.tensor(rows, device=dev, dtype=torch.float32)
scene = rt.Scene()
scene.add_mesh(rt.Mesh(vec3([[-1, -1, 0], [1, -1, 0], [-1, 1, 0]]),
                       torch.tensor([[0, 1, 2]], device=dev, dtype=torch.int32)))
scene.build()
 
states = rt.DfrStates(
    edge_index=torch.tensor([0], device=dev, dtype=torch.int32),
    edge_pos=vec3([[0.0, 0.0, 0.0]]), edge_dir=vec3([[1.0, 0.0, 0.0]]),
    edge_t_min=vec3([-1.0]), edge_t_max=vec3([1.0]),
    n0=vec3([[0.0, 0.0, 1.0]]), n1=vec3([[0.0, 0.0, -1.0]]),
    prim0=torch.tensor([0], device=dev, dtype=torch.int32),
    prim1=torch.tensor([0], device=dev, dtype=torch.int32),
    exterior_angle=vec3([math.pi]), src=vec3([[0.0, -1.0, 0.25]]), src_power=vec3([1.0]))
grid = rt.DfrGrid(axis=2, position=0.0, resolution0=4, resolution1=4)
acc = scene.accum_dfr_direct(states=states, grid=grid, material=rt.DfrMaterial.default(1, device=dev),
                             wavelength=1.0, direct_samples=4096, seed=1)
print(acc.power.shape)   # torch.Size([4, 4])

lane_offset and lane_count run a window of the Monte Carlo sample space, so several processes can each run a disjoint window and add their grids to get the single-run result. accum_dfr_direct and accum_dfr support gradients for geometry and material in both backends; trace_dfr_paths is forward-only in Torch.