Obstruction Detection

Obstruction detection determines whether the line of sight between two quadrature (or sample) points is blocked by a third surface, setting H_ij = 0 in the view factor integral for blocked pairs.

Enabling obstruction

Pass the physical group tags of all surfaces that may block rays:

result = compute_view_factors(mesh; nquad=4, obstruction_groups=[3, 4])

The source and destination groups are automatically excluded from the obstruction geometry for each element pair — a surface never blocks rays that originate from or terminate on itself. You may safely include all group tags in obstruction_groups without worrying about self-blocking:

all_tags = collect(keys(mesh.group_tags))
result   = compute_view_factors(mesh; nquad=4, obstruction_groups=all_tags)

Obstruction geometry is built from element corner nodes only — 2 triangles per quadrilateral, 1 per triangle, 1 segment per curve element — so a curved 2nd-order blocker is treated as flat for visibility.

Radiating only some groups

Assembly is dense over every radiating element, so a mesh that carries many shadowing bodies costs O(N_total²) even if you only want one pair of surfaces — and most of that work computes view factors between the shadowing bodies. radiating_groups restricts the radiating set while every group still obstructs:

all_tags = collect(keys(mesh.group_tags))
result = compute_view_factors(mesh; nquad=4,
                              radiating_groups   = [wall_tag, pin_tag],
                              obstruction_groups = all_tags)

The obstruction geometry is carried over whole, so occlusion is unchanged; the pair count falls with the square of the element-count ratio (on a reactor fuel-assembly slice, FA_wall → Pin_11 with 19 shadowing pins loaded drops from 18400 radiating elements to 3040, a 37× reduction in pairs).

With verbose=true the run lists its obstruction groups; blockers outside the radiating set are shown by tag ("tag 3"), since their names are dropped along with their elements.

The returned F_group covers only the radiating groups. The enclosure is deliberately open, so row sums no longer approach 1 and check_closure is not meaningful — use check_reciprocity, which is unaffected. To write such a result out with write_nekrs_view_factors, pass restrict_to_radiating(mesh, tags) as the mesh so the rows line up. See restrict_to_radiating.

How it works

CPU path: for each unique set of active obstruction groups (after excluding the source and destination groups), a BVH is built once from the merged triangle (3D) or line-segment (2D) soups of those groups and reused for all element pairs sharing that set. Ray–triangle intersection uses the Möller–Trumbore algorithm; ray–segment intersection uses 2D Cramer's rule.

GPU path: the BVH is flattened to typed device arrays with miss-link pointers for stackless traversal. Each GPU thread traverses the BVH independently with no thread-local stack, eliminating register pressure from MVector storage. Per-triangle group tags stored in the BVH allow each thread to skip triangles belonging to the emitter or receiver group without host-side pre-filtering.

Skipping non-facing pairs: before integrating an element pair, compute_view_factors (with facing_cull=true, the default) checks conservative bounds on each element's points and normals and skips any pair whose kernel is provably zero at every point pair — in O(1), rather than discovering that one point pair (and one BVH ray cast) at a time. Results are unchanged. Set facing_cull=false to disable it. It does not apply to raytrace=true, which has no pair loop.

Ray-shooting Monte Carlo is different

raytrace=true builds one scene BVH from every radiating element plus obstruction_groups, so radiating elements obstruct each other automatically without being listed in obstruction_groups — that argument only adds extra non-radiating blocker geometry for this method. See Integration Methods.

Compatibility

Obstruction detection works with all four integration methods, on CPU and GPU:

# Quadrature + obstruction
result = compute_view_factors(mesh; nquad=4, obstruction_groups=[3])

# Monte Carlo + obstruction
result = compute_view_factors(mesh; monte_carlo=true, n_samples=5000,
                               obstruction_groups=[3])

# Duffy + obstruction
result = compute_view_factors(mesh; nquad=6, use_duffy=true,
                               obstruction_groups=[3])

# Ray-shooting: group 3 is an extra blocker on top of the radiating mesh
result = compute_view_factors(mesh; raytrace=true, n_rays=10000,
                               obstruction_groups=[3])

# GPU + obstruction
using CUDA
result = compute_view_factors(mesh; nquad=4, backend=CUDABackend(),
                               obstruction_groups=[3])

The Monte Carlo near-pair Duffy patch (see Integration Methods) honours obstruction_groups too.

Performance

The BVH query is O(log N_tris) per ray. For meshes with many obstruction triangles and high nquad, the BVH cost can dominate the integration cost.

Pair-area Monte Carlo does not win here, despite the intuition that it should: both it and quadrature ray-cast once per kernel-positive point pair, and Monte Carlo simply evaluates more point pairs (n_samples versus nquad⁴). For large obstructed 3D meshes, prefer raytrace=true, and use radiating_groups and facing_cull to avoid work on pairs nobody asked for. See Integration Methods and Performance Guide.