Symmetry Rendering#

When solving on a reduced domain with symmetry boundary conditions, the Symmetry class renders the full solution by applying mirror transformations on the GPU.

Mirror Symmetry#

Render a quarter-domain solution as a full domain:

[1]:
from ngsolve import *
from ngsolve_webgpu import MeshData, FunctionData, CFRenderer, Symmetry
from webgpu.jupyter import Draw

# Solve on a quarter of the domain
mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))
cf = sin(3.14159 * x) * sin(3.14159 * y)

md = MeshData(mesh)
fd = FunctionData(md, cf, order=3)

sym = Symmetry()
sym.mirror_x()  # reflect across x=0 -> 2 copies
sym.mirror_y()  # reflect across y=0 -> 4 copies (group closure)

cfr = CFRenderer(fd, symmetry=sym)
Draw(cfr)
[1]:

Each call to mirror_x/y/z doubles the number of rendered copies by computing the group closure of all generator combinations. The transforms are applied in the vertex shader — no mesh duplication needed.

Antisymmetric Fields (Parity)#

For fields that change sign under a mirror operation:

[2]:
from ngsolve import *
from ngsolve_webgpu import MeshData, FunctionData, CFRenderer, Symmetry, Colorbar
from webgpu.jupyter import Draw

mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))
cf = x * sin(3.14159 * y)  # odd in x

md = MeshData(mesh)
fd = FunctionData(md, cf, order=3)

sym = Symmetry()
sym.mirror_x()
sym_odd = sym.with_parity([-1])  # field is antisymmetric under mirror_x
cfr = CFRenderer(fd, symmetry=sym_odd)
cfr.colormap.set_min(-1)
colorbar = Colorbar(cfr.colormap)
Draw([cfr, colorbar])
[2]:

with_parity([-1]) tells the renderer to negate function values for copies involving that mirror generator. The parity list has one entry per generator in the order they were added.

Vector Symmetry#

Arrow glyphs (SurfaceVectors, ClippingVectors) support the same symmetry system. Positions and directions are expanded on the GPU via a compute shader — no CPU overhead.

[3]:
from ngsolve import *
from ngsolve_webgpu import MeshData, FunctionData, SurfaceVectors, Symmetry
from webgpu.jupyter import Draw

mesh = Mesh(unit_cube.GenerateMesh(maxh=0.3))
cf = CF((x, y, 0))

md = MeshData(mesh)
fd = FunctionData(md, cf, order=1)

sym = Symmetry()
sym.mirror_x()
sym.mirror_y()

vecs = SurfaceVectors(fd, symmetry=sym)
Draw(vecs)
Warning: failed to init renderer SurfaceVectors: 'directions_imag'
[3]:

Electromagnetic Symmetry (Polar vs Axial Vectors)#

For electromagnetic simulations, vectors transform differently depending on whether they are polar (E-field, displacement) or axial/pseudovectors (B-field, H-field):

vector_symmetry

Transform

Use case

"polar" (default)

d' = M · d

E-field with PMC wall, B-field with PEC wall

"axial"

d' = det(M) · M · d

E-field with PEC wall, B-field with PMC wall

# B-field mirrored at a PMC boundary:
vecs_B = SurfaceVectors(fd_B, symmetry=sym, vector_symmetry="axial")

API Reference#

Symmetry()

  • .mirror_x(), .mirror_y(), .mirror_z() — add mirror generators (chainable)

  • .with_parity(list) — returns new Symmetry with per-generator sign flips for scalar fields

  • .n_copies — total number of symmetry copies (read-only)

Scalar renderers (CFRenderer, ClippingCF, MeshElements2d, MeshElements3d, IsoSurfaceRenderer, NegativeSurfaceRenderer, NegativeClippingRenderer, GeometryRenderer):

  • Pass symmetry=sym to constructor

Vector renderers (SurfaceVectors, ClippingVectors):

  • Pass symmetry=sym to constructor

  • Pass vector_symmetry="axial" for pseudovectors (B-field at PMC, E-field at PEC)