Feature/netgen periodic meshes - #5217
Conversation
When a Netgen mesh carries periodic identifications, ngsPETSc now produces a vertex-merged (periodic) DMPlex. This finishes the job on the Firedrake side: the Mesh() netgen branch detects periodicity, builds the intermediate topology without reordering, and attaches a discontinuous DG1 coordinate field (MeshGeometry._periodic_coordinates) carrying the un-wrapped geometry via _postprocess_periodic_mesh, exactly as Firedrake's own periodic meshes. Each Firedrake cell is matched to its Netgen element by geometry (hashing the wrapped per-cell coordinates), which is robust to reordering. The DG coordinate space uses the equispaced variant required by _set_dg_coordinates. High-order curving of periodic meshes raises NotImplementedError for now. Adds regression tests for a periodic square (one and two directions), a periodic cylinder, and the too-coarse error path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Periodic Meshes" section to the Netgen integration demo showing how
to declare OCC periodic identifications and import the resulting mesh into
Firedrake. The worked example is the periodic-cylinder ("screw pinch")
reduced model of a tokamak: a cylinder whose two circular ends are
identified, making the axial (toroidal) direction periodic. It labels the
lateral wall, checks the geometry survives the vertex merge (volume 2*pi^2),
and solves a manufactured Helmholtz problem with Dirichlet data on the wall
and periodicity on the ends, demonstrating seam continuity. Also documents
the too-coarse ValueError and the degree==1 limitation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| topology_reorder = False if netgen_periodic else reorder | ||
| topology = MeshTopology(plex, name=plex.getName(), reorder=topology_reorder, |
There was a problem hiding this comment.
| topology_reorder = False if netgen_periodic else reorder | |
| topology = MeshTopology(plex, name=plex.getName(), reorder=topology_reorder, | |
| reorder = False if netgen_periodic else reorder | |
| topology = MeshTopology(plex, name=plex.getName(), reorder=reorder, |
| @PETSc.Log.EventDecorator() | ||
| def _periodic_coordinates(self, permutation_tol=1e-8): |
| # is finished off with a discontinuous coordinate field below. That field | ||
| # is built on the un-reordered topology (as for Firedrake's own periodic | ||
| # meshes), so suppress reordering here and reapply it in postprocessing. | ||
| netgen_periodic = len(netgen_firedrake_mesh.meshMap.ngMesh.GetIdentifications()) > 0 |
There was a problem hiding this comment.
Do as the comment says and supress reordering here. Remove the comment, we don't like AI slop
| # discontinuous coordinate field carrying the un-wrapped geometry. | ||
| if degree != 1: | ||
| raise NotImplementedError( | ||
| "High-order curving of periodic netgen meshes is not supported yet." |
There was a problem hiding this comment.
Support this, I support you
| from firedrake.utility_meshes import _postprocess_periodic_mesh | ||
| permutation_tol = netgen_flags.get("permutation_tol", 1e-8) | ||
| coordinates = mesh._periodic_coordinates(permutation_tol=permutation_tol) | ||
| temp = _postprocess_periodic_mesh(coordinates, | ||
| mesh.comm, | ||
| distribution_parameters, | ||
| reorder, | ||
| name, | ||
| kwargs.get("distribution_name"), | ||
| kwargs.get("permutation_name")) | ||
| temp.netgen_mesh = mesh.netgen_mesh | ||
| temp.netgen_flags = mesh.netgen_flags | ||
| mesh = temp |
There was a problem hiding this comment.
create a helper function in netgen.py doing everything here
…ry tutorials. This takes some of the demo from netgen_meshes to make it less lengthy.
|
|
||
| from netgen.occ import Cylinder, OCCGeometry, Pnt, Z, gp_Trsf, gp_Vec | ||
| from netgen.meshing import IdentificationType | ||
| from math import pi as PI |
There was a problem hiding this comment.
| from math import pi as PI |
|
|
||
| @PETSc.Log.EventDecorator() | ||
| def _periodic_coordinates(self): | ||
| '''Return a discontinuous coordinate field for a periodic netgen mesh. |
There was a problem hiding this comment.
We should not add a netgen-only function to this file.
| # overwriting, so the per-cell matching always sees the interpolated values. | ||
| wrapped_fd = data[cell_nodes].real.copy() | ||
|
|
||
| for i in range(cell_nodes.shape[0]): |
There was a problem hiding this comment.
This is an antipattern (see AGENTS.md)
|
This work should be properly re-implemented on top of #5277 and NGSolve/ngsPETSc#125 |
…lem. I have rewritten verbose sentences to make the demo clearer and easier to use
There was a problem hiding this comment.
@ThighamOxford any chance you would like to take a hit also at tackling the other comments Pablo left ? I'll be thinking about/working on high order :)
|
|
||
| - \Delta u = \sin(x), \quad u(0) = u(2 \pi), | ||
|
|
||
| on :math:`\Omega = [0, 2 \pi]`. This problem has a trivial nullspace of constants; if we fix the constant to be zero then this problem has an analytical solution of :math:`u(x) = \sin(x)`. |
There was a problem hiding this comment.
| on :math:`\Omega = [0, 2 \pi]`. This problem has a trivial nullspace of constants; if we fix the constant to be zero then this problem has an analytical solution of :math:`u(x) = \sin(x)`. | |
| on :math:`\Omega = [0, 2 \pi]`. This problem has a trivial nullspace of constants, thus if we fix the constant to be zero then this problem has an analytical solution of :math:`u(x) = \sin(x)`. |
|
|
||
| - \Delta u = (1 + \pi^2) \sin(x) \sin(\pi y), \quad u(0,y) = u(2 \pi, y), \quad u(x, 0) = u(x, 1) = 0, | ||
|
|
||
| on :math:`\Omega = [0, 2 \pi] \times [0,1]`. The nullspace is fixed for this problem. The exact solution is :math:`u(x,y) = \sin(x)\sin(\pi y)`. |
There was a problem hiding this comment.
What do you mean when you say that the nullspace is fixed ? Does the problem have a nullspace ?
There was a problem hiding this comment.
The 1D periodic poisson problem with periodic data at the boundary has a nullspace of constants. I then clarify that for this 2D periodic poisson problem the nullspace is fixed due to additional dirichlet boundary conditions on the top and bottom of the box.
| :alt: Finite element solution to 2D periodic Poisson problem. | ||
|
|
||
| Finite element solution to 2D periodic Poisson problem. | ||
|
|
There was a problem hiding this comment.
If I remember correctly there are a bunch of features that do not work on extruded meshes, it would be nice to point to this issue, no ? Probably more detail can be found in the documentation for extruded meshes ?
|
|
||
| where ``transformation`` is the rigid motion (typically a translation) that maps ``shape_a`` onto ``shape_b``. | ||
| Netgen then meshes the two boundaries compatibly and records the vertex pairs; Firedrake consumes them | ||
| automatically -- no extra flag on the ``Mesh`` constructor is required. |
There was a problem hiding this comment.
| automatically -- no extra flag on the ``Mesh`` constructor is required. | |
| automatically: no extra flag on the ``Mesh`` constructor is required. |
| .. warning:: | ||
|
|
||
| The mesh must contain at least a handful of cells along each periodic direction. If a single cell spans a | ||
| whole period, its two ends are identified and the cell collapses; Firedrake then raises a ``ValueError`` |
There was a problem hiding this comment.
| whole period, its two ends are identified and the cell collapses; Firedrake then raises a ``ValueError`` | |
| whole period, its two ends are identified and the cell collapses. Firedrake then raises a ``ValueError`` |
| whole period, its two ends are identified and the cell collapses; Firedrake then raises a ``ValueError`` | ||
| asking you to refine along the periodic direction. Here the axis has length :math:`2\pi` and ``maxh=0.4`` | ||
| gives roughly sixteen cells along it, which is ample. Only ``degree == 1`` periodic meshes are supported | ||
| for now. |
There was a problem hiding this comment.
I need to work on fixing this :)
| Because the two end caps have been identified, no boundary markers survive on them: the seam has become an | ||
| *interior* set of facets, and the only labelled boundary that remains is the lateral wall. This is what makes | ||
| a continuous field wrap around continuously in the ``z``-direction. We can verify the geometry survived the | ||
| merge intact --- the volume of the cylinder is :math:`\pi r^2 h = 2\pi^2`: :: |
There was a problem hiding this comment.
| merge intact --- the volume of the cylinder is :math:`\pi r^2 h = 2\pi^2`: :: | |
| merge intact: the volume of the cylinder is :math:`\pi r^2 h = 2\pi^2`: :: |
|
|
||
| To construct a periodic cylinder of length :math:`2\pi` we identify the two end caps of the cylinder by a translation of :math:`2\pi` along ``z``. :: | ||
|
|
||
| from netgen.occ import Cylinder, OCCGeometry, Pnt, Z, gp_Trsf, gp_Vec |
There was a problem hiding this comment.
I believe there is no need to use gp_Vec tuple should be accepted now (bad Claude...).
Also Translation should be importable without gp_Trsf.
| cyl.faces.Max(Z).name = "top" | ||
| # Identify the bottom cap with the top cap: a translation of 2*pi along Z | ||
| # maps one onto the other, making the axial direction periodic. | ||
| cyl.faces.Min(Z).Identify(cyl.faces.Max(Z), "toroidal", |
Co-authored-by: Umberto Zerbinati <umberto.zerbinati@maths.ox.ac.uk>
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
ngsPETSc PR: NGSolve/ngsPETSc#124