{ "cells": [ { "cell_type": "markdown", "id": "a1b2c3d4", "metadata": {}, "source": [ "# Symmetry Rendering\n", "\n", "When solving on a reduced domain with symmetry boundary conditions, the `Symmetry` class renders the full solution by applying mirror transformations on the GPU." ] }, { "cell_type": "markdown", "id": "b2c3d4e5", "metadata": {}, "source": [ "## Mirror Symmetry\n", "\n", "Render a quarter-domain solution as a full domain:" ] }, { "cell_type": "code", "execution_count": null, "id": "c3d4e5f6", "metadata": {}, "outputs": [], "source": [ "from ngsolve import *\n", "from ngsolve_webgpu import MeshData, FunctionData, CFRenderer, Symmetry\n", "from webgpu.jupyter import Draw\n", "\n", "# Solve on a quarter of the domain\n", "mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))\n", "cf = sin(3.14159 * x) * sin(3.14159 * y)\n", "\n", "md = MeshData(mesh)\n", "fd = FunctionData(md, cf, order=3)\n", "\n", "sym = Symmetry()\n", "sym.mirror_x() # reflect across x=0 -> 2 copies\n", "sym.mirror_y() # reflect across y=0 -> 4 copies (group closure)\n", "\n", "cfr = CFRenderer(fd, symmetry=sym)\n", "Draw(cfr)" ] }, { "cell_type": "markdown", "id": "d4e5f6a7", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "id": "e5f6a7b8", "metadata": {}, "source": [ "## Antisymmetric Fields (Parity)\n", "\n", "For fields that change sign under a mirror operation:" ] }, { "cell_type": "code", "execution_count": null, "id": "f6a7b8c9", "metadata": {}, "outputs": [], "source": [ "from ngsolve import *\n", "from ngsolve_webgpu import MeshData, FunctionData, CFRenderer, Symmetry, Colorbar\n", "from webgpu.jupyter import Draw\n", "\n", "mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))\n", "cf = x * sin(3.14159 * y) # odd in x\n", "\n", "md = MeshData(mesh)\n", "fd = FunctionData(md, cf, order=3)\n", "\n", "sym = Symmetry()\n", "sym.mirror_x()\n", "sym_odd = sym.with_parity([-1]) # field is antisymmetric under mirror_x\n", "cfr = CFRenderer(fd, symmetry=sym_odd)\n", "cfr.colormap.set_min(-1)\n", "colorbar = Colorbar(cfr.colormap)\n", "Draw([cfr, colorbar])" ] }, { "cell_type": "markdown", "id": "a7b8c9d0", "metadata": {}, "source": [ "`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." ] }, { "cell_type": "markdown", "id": "vec1", "metadata": {}, "source": [ "## Vector Symmetry\n", "\n", "Arrow glyphs (`SurfaceVectors`, `ClippingVectors`) support the same symmetry system. Positions and directions are expanded on the GPU via a compute shader — no CPU overhead." ] }, { "cell_type": "code", "execution_count": null, "id": "vec2", "metadata": {}, "outputs": [], "source": [ "from ngsolve import *\n", "from ngsolve_webgpu import MeshData, FunctionData, SurfaceVectors, Symmetry\n", "from webgpu.jupyter import Draw\n", "\n", "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.3))\n", "cf = CF((x, y, 0))\n", "\n", "md = MeshData(mesh)\n", "fd = FunctionData(md, cf, order=1)\n", "\n", "sym = Symmetry()\n", "sym.mirror_x()\n", "sym.mirror_y()\n", "\n", "vecs = SurfaceVectors(fd, symmetry=sym)\n", "Draw(vecs)" ] }, { "cell_type": "markdown", "id": "vec3", "metadata": {}, "source": [ "## Electromagnetic Symmetry (Polar vs Axial Vectors)\n", "\n", "For electromagnetic simulations, vectors transform differently depending on whether they are **polar** (E-field, displacement) or **axial/pseudovectors** (B-field, H-field):\n", "\n", "| `vector_symmetry` | Transform | Use case |\n", "|---|---|---|\n", "| `\"polar\"` (default) | d' = M · d | E-field with PMC wall, B-field with PEC wall |\n", "| `\"axial\"` | d' = det(M) · M · d | E-field with PEC wall, B-field with PMC wall |\n", "\n", "```python\n", "# B-field mirrored at a PMC boundary:\n", "vecs_B = SurfaceVectors(fd_B, symmetry=sym, vector_symmetry=\"axial\")\n", "```" ] }, { "cell_type": "markdown", "id": "vec4", "metadata": {}, "source": [ "## API Reference\n", "\n", "**Symmetry()**\n", "- `.mirror_x()`, `.mirror_y()`, `.mirror_z()` — add mirror generators (chainable)\n", "- `.with_parity(list)` — returns new Symmetry with per-generator sign flips for scalar fields\n", "- `.n_copies` — total number of symmetry copies (read-only)\n", "\n", "**Scalar renderers** (`CFRenderer`, `ClippingCF`, `MeshElements2d`, `MeshElements3d`, `IsoSurfaceRenderer`, `NegativeSurfaceRenderer`, `NegativeClippingRenderer`, `GeometryRenderer`):\n", "- Pass `symmetry=sym` to constructor\n", "\n", "**Vector renderers** (`SurfaceVectors`, `ClippingVectors`):\n", "- Pass `symmetry=sym` to constructor\n", "- Pass `vector_symmetry=\"axial\"` for pseudovectors (B-field at PMC, E-field at PEC)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.5" } }, "nbformat": 4, "nbformat_minor": 5 }