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.
| Operation | What it computes | Result |
|---|---|---|
accum_dfr_direct | Monte Carlo diffracted power and field onto a grid, first order | DfrAccum |
accum_dfr | the same, up to max_order 2 or 3 through recursive states | DfrAccum |
accum_dfr_coherent_direct | exact first-order coherent field onto a grid, one diffraction point per edge and cell | DfrCoherentAccum |
trace_dfr_paths | explicit diffraction paths between transmitters and receivers | DfrPaths |
Describe the wedges#
A DfrStates record holds one row per wedge and source:
| Field | Meaning |
|---|---|
count | number of states (Dr.Jit; Torch derives it from the tensors) |
edge_index | the edge's id, for bookkeeping |
edge_pos, edge_dir, edge_t_min, edge_t_max | the edge segment edge_pos + t * edge_dir for t in [edge_t_min, edge_t_max] |
n0, n1 | the normals of the two wedge faces |
prim0, prim1 | the global primitive ids of the two faces, or -1 |
exterior_angle | the wedge's exterior angle, in radians |
src, src_power | the source position and power that illuminates the edge |
wi, d0 | incident and initial directions (Dr.Jit fields; optional in Torch) |
prefix_depth | number 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.