Simulation-ready geometry

A finite-element model needs more than an outline. Its regions must share their interfaces, every boundary that gets a condition needs a name, periodic sides must be identified point by point, and the mesh must be fine where the solution changes fast. planex2d sets all of this on the geometry and passes it to netgen in one call, so that the mesh carries the materials, the boundary names and the identified pairs that NGSolve uses. The faces can come from a planar BREP or STEP file, a DXF or SVG drawing, a sketch from a .CATPart file, a circuit board or a Shapely polygon. The first cell is the complete workflow, ready to copy and run, and the section Step by step runs it in parts, with the plots one looks at along the way.

The example is one cell of a finned heat sink, as wide as the fin pitch of 10 mm. An aluminum base 4 mm thick carries a fin 2 mm thick and 20 mm high, and a copper spreader of 6 × 1 mm under the fin takes the heat of the component. The two faces come without names in the planar BREP file heat-sink-cell.brep, as geometry from another tool often does.

[2]:
import ngsolve as ngs
from ngsolve_webgpu.jupyter import Draw
import planex as px

aluminum, copper = sorted(px.FromBrep("heat-sink-cell.brep"), key=lambda f: -f.mass)
aluminum.name, copper.name = "aluminum", "copper"
cell = px.Glue([aluminum, copper])

cell.edges.name = "air"
cell.edges[px.y <= 0].name = "insulated"
cell.faces(name="copper")[0].edges.Max(px.y).name = "interface"
cell.edges.Min(px.y).name = "source"
cell.edges.Min(px.x).name = "left"
cell.edges.Max(px.x).name = "right"
cell.Identify("left", "right")
cell.edges(name="source").maxh = 0.0002

mesh = ngs.Mesh(px.OCCGeometry(cell).GenerateMesh(maxh=0.0005))
print(mesh.ne, "elements |", mesh.GetMaterials(), "|", sorted(set(mesh.GetBoundaries())))
print(len(mesh.ngmesh.GetIdentifications()), "identified point pairs")
plain, periodic = ngs.H1(mesh, order=2), ngs.Periodic(ngs.H1(mesh, order=2))
print("free degrees of freedom", sum(plain.FreeDofs()), "plain |", sum(periodic.FreeDofs()), "periodic")
Draw(mesh)
782 elements | ('aluminum', 'copper') | ['air', 'insulated', 'interface', 'left', 'right', 'source']
9 identified point pairs
free degrees of freedom 1725 plain | 1708 periodic
3D view
▶ Click to interact

Step by step

Reading and gluing

px.FromBrep reads the two planar faces as a compound, in meters, and sorting by area tells the aluminum from the copper. Their names become the materials of the mesh. px.Glue cuts the two faces against each other into one shape, so that the top of the copper and the part of the base above it become one edge that both faces share, and the mesh then has matching nodes on both sides of the interface (see Faces, shapes and compounds and STEP and BREP).

[3]:
import ngsolve as ngs
from ngsolve_webgpu.jupyter import Draw
import planex as px

aluminum, copper = sorted(px.FromBrep("heat-sink-cell.brep"), key=lambda f: -f.mass)
aluminum.name, copper.name = "aluminum", "copper"
cell = px.Glue([aluminum, copper])
print(len(cell.faces), "faces,", len(cell.edges), "edges")
2 faces, 13 edges

Naming the boundaries

cell.edges.name names every edge at once, here air for the convection to the surrounding air. Selections then rename the others by where they lie, with no coordinate taken from the drawing. The origin is the bottom left corner of the base, so edges[px.y <= 0] takes the edges whose middle lies at or below the base’s bottom, the bottom of the base beside the copper and the copper’s edges, as insulated. The highest of the copper face’s edges, Max(px.y), is the interface that both faces share. Of all edges, the lowest is the bottom of the copper, the heat source, and the leftmost and rightmost are the two sides of the cell, left and right (Figure 1). See Picking what to measure for these selections and Naming and marking for the names in the mesh.

[4]:
cell.edges.name = "air"
cell.edges[px.y <= 0].name = "insulated"
cell.faces(name="copper")[0].edges.Max(px.y).name = "interface"
cell.edges.Min(px.y).name = "source"
cell.edges.Min(px.x).name = "left"
cell.edges.Max(px.x).name = "right"
print(sorted({e.name for e in cell.edges}))
# px.Plot(cell)
['air', 'insulated', 'interface', 'left', 'right', 'source']
The cell with its boundary names, the periodic sides in cyan
Figure 1: One cell of the heat sink, glued from aluminum and copper

Periodic sides and mesh sizes

The heat sink repeats every 10 mm, so the cell’s left and right sides carry the same temperature. Identify pairs the two sides, and planex2d finds the shift between them, so netgen places the nodes of both sides in pairs (see Identifications). A maxh on an edge refines the mesh there, here 0.2 mm at the heat source, against 0.5 mm elsewhere (see Meshing a face).

[6]:
cell.Identify("left", "right")
cell.edges(name="source").maxh = 0.0002

Meshing and checking

netgen meshes the shape with both materials and all the names, and the identification gives node pairs, each shifted by exactly one fin pitch. NGSolve’s Periodic space uses the pairs and has fewer free degrees of freedom than the plain one, since each pair carries one value. gmsh takes the same shape through WriteGeo, with the edge names as physical groups (see gmsh and FEniCSx).

[7]:
mesh = ngs.Mesh(px.OCCGeometry(cell).GenerateMesh(maxh=0.0005))
print(mesh.ne, "elements |", mesh.GetMaterials(), "|", sorted(set(mesh.GetBoundaries())))
print(len(mesh.ngmesh.GetIdentifications()), "identified point pairs")
plain, periodic = ngs.H1(mesh, order=2), ngs.Periodic(ngs.H1(mesh, order=2))
print("free degrees of freedom", sum(plain.FreeDofs()), "plain |", sum(periodic.FreeDofs()), "periodic")
Draw(mesh)
782 elements | ('aluminum', 'copper') | ['air', 'insulated', 'interface', 'left', 'right', 'source']
9 identified point pairs
free degrees of freedom 1725 plain | 1708 periodic
3D view
▶ Click to interact

Other formats

Format

Reader

Names it brings

See

BREP (planar)

px.FromBrep(path)

None

STEP and BREP

STEP (planar)

px.FromStep(path)

Names, colors, mesh sizes

STEP and BREP

DXF

px.FromDXF(path)

Layers as edge names

Files

SVG

px.FromSVG(path)

Groups as edge names

SVG files

KiCad, Altium, Eagle and other boards

px.FromKiCad(path)

Nets as face names

KiCad, Altium, Eagle and more

.CATPart sketches

px.FromCATPart(path)

Element names as ids

CATIA V5

Pitfalls

  • Unglued regions. Faces that only touch, without px.Glue, are meshed separately, with nodes that do not match at the interface.

  • Names after gluing. Gluing cuts edges, so boundaries are named on the glued shape, not on the faces before.

  • Units. The geometry is in meters, so every maxh is in meters too.

  • Matching sides. Identified sides must be congruent, here two straight sides of equal length. A side cut into pieces on one end and not on the other cannot be paired.