Working with Meshes#
This guide covers creating, manipulating, and analyzing finite element meshes.
Mesh Fundamentals#
An IWFM mesh consists of three main components:
Nodes: Points with x, y coordinates
Elements: Triangular (3 vertices) or quadrilateral (4 vertices) cells
Faces: Edges shared between elements
Node Class#
from pyiwfm.core.mesh import Node
# Create a node
node = Node(
id=1,
x=100.0,
y=200.0,
is_boundary=True, # Node on domain boundary
)
# Access properties
print(f"Node {node.id} at ({node.x}, {node.y})")
print(f"Is boundary: {node.is_boundary}")
Element Class#
from pyiwfm.core.mesh import Element
# Create a quadrilateral element
quad = Element(
id=1,
vertices=(1, 2, 5, 4), # Node IDs, counter-clockwise
subregion=1,
)
print(f"Element {quad.id} is a quad: {quad.is_quad}")
print(f"Number of vertices: {quad.n_vertices}")
# Create a triangular element
tri = Element(
id=2,
vertices=(2, 3, 5), # 3 vertices for triangle
subregion=1,
)
print(f"Element {tri.id} is a triangle: {tri.is_triangle}")
Creating a Mesh Manually#
Create a mesh by defining nodes and elements:
from pyiwfm.core.mesh import AppGrid, Node, Element
# Define a 3x3 grid of nodes (9 nodes total)
nodes = {}
node_id = 1
for j in range(3):
for i in range(3):
is_boundary = (i == 0 or i == 2 or j == 0 or j == 2)
nodes[node_id] = Node(
id=node_id,
x=float(i * 100),
y=float(j * 100),
is_boundary=is_boundary,
)
node_id += 1
# Define 4 quadrilateral elements
elements = {
1: Element(id=1, vertices=(1, 2, 5, 4), subregion=1),
2: Element(id=2, vertices=(2, 3, 6, 5), subregion=1),
3: Element(id=3, vertices=(4, 5, 8, 7), subregion=2),
4: Element(id=4, vertices=(5, 6, 9, 8), subregion=2),
}
# Create the grid
grid = AppGrid(nodes=nodes, elements=elements)
# IMPORTANT: Compute connectivity after creating the grid
grid.compute_connectivity()
# Mesh statistics
print(f"Nodes: {grid.n_nodes}")
print(f"Elements: {grid.n_elements}")
print(f"Boundary nodes: {grid.n_boundary_nodes}")
Mesh Connectivity#
After calling compute_connectivity(), the mesh has additional information:
# Node connectivity
for node in grid.iter_nodes():
print(f"Node {node.id}:")
print(f" Connected nodes: {node.connected_nodes}")
print(f" Surrounding elements: {node.surrounding_elements}")
# Element neighbors
for elem in grid.iter_elements():
print(f"Element {elem.id}:")
print(f" Vertices: {elem.vertices}")
print(f" Subregion: {elem.subregion}")
Iterating Over Mesh Components#
# Iterate over nodes
for node in grid.iter_nodes():
print(f"Node {node.id}: ({node.x}, {node.y})")
# Iterate over elements
for elem in grid.iter_elements():
print(f"Element {elem.id}: {elem.vertices}")
# Iterate over boundary nodes only
for node in grid.iter_boundary_nodes():
print(f"Boundary node {node.id}")
# Get specific node or element
node = grid.nodes[5]
elem = grid.elements[1]
Generating Meshes with Triangle#
For more complex geometries, use the Triangle mesh generator:
import numpy as np
from pyiwfm.mesh_generation import TriangleMeshGenerator
from pyiwfm.mesh_generation.constraints import (
BoundaryConstraint,
LineConstraint,
PointConstraint,
RefinementZone,
)
# Define boundary (L-shaped domain)
boundary_coords = np.array([
[0.0, 0.0],
[1000.0, 0.0],
[1000.0, 500.0],
[500.0, 500.0],
[500.0, 1000.0],
[0.0, 1000.0],
])
boundary = BoundaryConstraint(coordinates=boundary_coords)
# Add a stream constraint (forces mesh edges along stream)
stream_coords = np.array([
[100.0, 0.0],
[100.0, 500.0],
[300.0, 800.0],
])
stream = LineConstraint(coordinates=stream_coords)
# Add point constraints (forces nodes at specific locations)
wells = [
PointConstraint(x=200.0, y=200.0),
PointConstraint(x=400.0, y=300.0),
]
# Add refinement zone (smaller elements in this area)
refine_zone = RefinementZone(
coordinates=np.array([
[0.0, 0.0],
[300.0, 0.0],
[300.0, 300.0],
[0.0, 300.0],
]),
max_area=1000.0, # Smaller elements in this zone
)
# Generate mesh
generator = TriangleMeshGenerator()
result = generator.generate(
boundary=boundary,
line_constraints=[stream],
point_constraints=wells,
refinement_zones=[refine_zone],
max_area=5000.0, # Maximum element area
min_angle=25.0, # Minimum angle (improves quality)
)
print(f"Generated {result.n_nodes} nodes, {result.n_elements} elements")
# Convert to AppGrid
grid = generator.to_appgrid(result)
Generating Meshes with Gmsh#
Gmsh supports quad and mixed element meshes:
from pyiwfm.mesh_generation import GmshMeshGenerator
# Create generator for quad elements
generator = GmshMeshGenerator(element_type="quad")
# Generate mesh
result = generator.generate(
boundary=boundary,
line_constraints=[stream],
max_area=5000.0,
)
# Convert to AppGrid
grid = generator.to_appgrid(result)
# Check element types
n_quads = sum(1 for e in grid.iter_elements() if e.is_quad)
n_tris = sum(1 for e in grid.iter_elements() if e.is_triangle)
print(f"Quads: {n_quads}, Triangles: {n_tris}")
Mesh Quality#
Check mesh quality metrics:
import numpy as np
# Calculate element areas
areas = []
for elem in grid.iter_elements():
# Get vertex coordinates
coords = np.array([
[grid.nodes[v].x, grid.nodes[v].y]
for v in elem.vertices
])
# Calculate area using shoelace formula
n = len(coords)
area = 0.5 * abs(sum(
coords[i, 0] * coords[(i + 1) % n, 1] -
coords[(i + 1) % n, 0] * coords[i, 1]
for i in range(n)
))
areas.append(area)
areas = np.array(areas)
print(f"Min area: {areas.min():.2f}")
print(f"Max area: {areas.max():.2f}")
print(f"Mean area: {areas.mean():.2f}")
print(f"Area ratio (max/min): {areas.max() / areas.min():.2f}")
Adding Holes to Meshes#
Create meshes with internal holes (e.g., for islands or exclusion zones):
# Define hole boundary
hole_coords = np.array([
[400.0, 400.0],
[600.0, 400.0],
[600.0, 600.0],
[400.0, 600.0],
])
# Generate mesh with hole
generator = TriangleMeshGenerator()
result = generator.generate(
boundary=boundary,
holes=[hole_coords],
max_area=5000.0,
)