102D Boundary Conditions: Neumann Edges and Dirichlet Lifting
This notebook extends the 1D boundary condition concepts to 2D triangular meshes, showing how Neumann conditions are applied on edge boundaries and how Dirichlet conditions are enforced across a full problem.
import numpy as npimport sympy as spfrom IPython.display import Math, displayfrom symbolic_fem_workbench import ( ReferenceTriangleP1, AffineTriangleMap2D, integrate_reference_triangle_exact, neumann_load_vector_triangle_edge,)from symbolic_fem_workbench.assembly import ( assemble_dense_matrix, assemble_dense_vector, apply_dirichlet_by_row_substitution, apply_dirichlet_by_lifting, expand_reduced_solution,)print("Imports successful. Ready for 2D FEM with boundary conditions.")
Imports successful. Ready for 2D FEM with boundary conditions.
10.1 Neumann BCs on Triangle Edges
In 2D, the boundary integral becomes a line integral along each edge:
\[\int_{\Gamma_N} t^* v \, ds\]
For a straight edge of length |e| in a P1 triangle, if the flux t-star is constant, the contribution to each endpoint node is:
\[f_i^{\text{edge}} = t^* \cdot \frac{|e|}{2}\]
This comes from integrating the constant flux against the linear shape function over the edge.
# Define reference coordinates firstxi, eta = sp.symbols('xi eta', real=True, positive=True)# Create a reference P1 triangleref_tri = ReferenceTriangleP1(xi, eta)print(f"Reference triangle shape functions:")for idx, N inenumerate(ref_tri.shape_functions):print(f" N_{idx}: {N}")# Define a constant Neumann flux and edge lengtht_star = sp.Symbol('t_star', real=True) # Prescribed fluxedge_len = sp.Symbol('edge_len', positive=True)# Compute Neumann load contribution for edge 0 (between nodes 0 and 1)# Nodes 0 and 1 are at (0,0) and (1,0) on the reference trianglef_edge = neumann_load_vector_triangle_edge( ref_tri, edge_local_nodes=(0, 1), prescribed_flux=t_star, edge_length=edge_len, xi=xi, eta=eta,)print(f"\nNeumann load vector for edge 0-1:")print(f"f_edge = {f_edge.T}")print()print(f"Each endpoint gets t_star times edge_len divided by 2")print(f"The opposite node (node 2) gets 0")
Reference triangle shape functions:
N_0: -eta - xi + 1
N_1: xi
N_2: eta
Neumann load vector for edge 0-1:
f_edge = Matrix([[edge_len*t_star/2, edge_len*t_star/2, 0]])
Each endpoint gets t_star times edge_len divided by 2
The opposite node (node 2) gets 0
10.2 2D Problem Setup: 4-Triangle Mesh with Mixed BCs
Consider a simple domain made of 4 triangles in a 1x1 square:
Mesh: 6 nodes, 4 elements
Node coordinates:
Node 0: (0.0, 0.0)
Node 1: (0.5, 0.0)
Node 2: (1.0, 0.0)
Node 3: (0.0, 1.0)
Node 4: (0.5, 1.0)
Node 5: (1.0, 1.0)
Element connectivity:
Element 0: nodes 0, 1, 3
Element 1: nodes 1, 4, 3
Element 2: nodes 1, 2, 4
Element 3: nodes 2, 5, 4
# For a teaching example, we use a uniform Poisson problem:# -nabla u = 0 in Omega# u = 0 on Gamma_D (bottom)# du/dn = t-star on Gamma_N (top)# Local stiffness matrix for P1 triangle (standard Poisson)# For a uniform reference triangle with area 0.5:K_ref = np.array([ [1, -1/2, -1/2], [-1/2, 1, -1/2], [-1/2, -1/2, 1],]) /2# Assemble global systemK_global = np.zeros((n_nodes, n_nodes))F_global = np.zeros(n_nodes)# Assemble element matricesfor e, (a, b, c) inenumerate(connectivity):# Get physical coordinates x_a, y_a = nodes[a] x_b, y_b = nodes[b] x_c, y_c = nodes[c]# Compute physical element area (0.5 times |det(J)|) J = np.array([ [x_b - x_a, x_c - x_a], [y_b - y_a, y_c - y_a], ]) area =0.5*abs(np.linalg.det(J))# Scale local stiffness matrix by area K_e = K_ref / area# Assemblefor i, global_i inenumerate([a, b, c]):for j, global_j inenumerate([a, b, c]): K_global[global_i, global_j] += K_e[i, j]print(f"Assembled global K ({n_nodes}x{n_nodes}):")print(np.round(K_global, 3))print(f"\nF_global (before boundary conditions): {F_global}")
The top boundary consists of edges between nodes (3,4) and (4,5). Each has length 0.5.
# Apply Neumann BC: t-star = 1.0 on top edgest_star_val =1.0edge_len_val =0.5# Length of each top edge# Top edge 1: nodes 3 and 4f_edge_34 = t_star_val * edge_len_val /2F_global[3] += f_edge_34F_global[4] += f_edge_34# Top edge 2: nodes 4 and 5f_edge_45 = t_star_val * edge_len_val /2F_global[4] += f_edge_45 # Node 4 appears in both edges!F_global[5] += f_edge_45print(f"After adding Neumann flux (t-star={t_star_val}) on top edges:")print(f"F_global: {np.round(F_global, 4)}")print()print(f"Note: Node 4 (top-center) receives contributions from both edges.")
After adding Neumann flux (t-star=1.0) on top edges:
F_global: [0. 0. 0. 0.25 0.5 0.25]
Note: Node 4 (top-center) receives contributions from both edges.
10.4 Enforce Dirichlet BC on Bottom (Row Substitution)