New file: docs/book/gmsh_tutorial.md (544 lines) Complete educational resource addressing Issue #183: Step 1: Mesh Generation with Gmsh - Why Gmsh (features, academic adoption) - .geo file syntax and concepts - Mesh generation commands - Understanding .msh format Step 2: Weak Formulation (Theory) - Strong form → weak form derivation - Galerkin approximation - M du/dt + K u = f system Step 3: FEM Assembly in JuliaFEM - Loading meshes - Creating problems and elements - Boundary conditions (Dirichlet, Neumann) - Assembly process internals Step 4: Extracting Matrices (Issue #183 core answer) - How to get K, M, f after assembly - Why extract (5 use cases) - Integration with DifferentialEquations.jl - Complete working example Step 5: Method of Lines - PDE → ODE spatial discretization strategy - Separation of space/time concerns - Modularity benefits Plus: Comparison (built-in vs external), Extensions (nonlinear, 3D, parallel, GPU), Troubleshooting, References Demonstrates 'laboratory not fortress' philosophy
13 KiB
title, author, date, categories, tags, issue, description
| title | author | date | categories | tags | issue | description | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| From Gmsh to Physics: Complete Heat Equation Tutorial | Jukka Aho | 2025-11-09 |
|
|
183 | Complete workflow from mesh generation to FEM assembly, addressing academic usage without built-in physics |
From Gmsh to Physics: Complete Heat Equation Tutorial
Purpose: Demonstrate complete FEM workflow from mesh generation through physics, addressing Issue #183 - using JuliaFEM for academic/research purposes without built-in physics.
What You'll Learn:
- Generate meshes with Gmsh
- Load meshes into JuliaFEM
- Assemble stiffness and mass matrices
- Extract matrices for external solvers (DifferentialEquations.jl, etc.)
- Solve the heat equation using method of lines
Problem Statement
We'll solve the transient heat equation on a unit square Ω = [0,1] × [0,1]:
Governing equation:
∂u/∂t = α∇²u + f(x,y,t) in Ω
Boundary conditions:
- Dirichlet: u = 0 on left edge (x = 0)
- Neumann: ∂u/∂n = 0 on other edges (natural BC)
Initial condition:
u(x,y,0) = sin(πx)sin(πy)
Parameters:
- α = 1.0 (thermal diffusivity)
- f = 0 (no heat source)
Step 1: Mesh Generation with Gmsh
Why Gmsh?
Gmsh is a free, open-source mesh generator that:
- Supports complex geometries (2D and 3D)
- Generates quality meshes with various element types
- Has scripting capabilities (
.geofiles) - Widely used in academic and industrial FEM
Creating the Geometry File
Create unit_square.geo:
// Gmsh geometry file: Unit square mesh
// Generate with: gmsh -2 unit_square.geo -o unit_square.msh
// Mesh element size
lc = 0.1;
// Corner points
Point(1) = {0, 0, 0, lc};
Point(2) = {1, 0, 0, lc};
Point(3) = {1, 1, 0, lc};
Point(4) = {0, 1, 0, lc};
// Edges
Line(1) = {1, 2}; // Bottom
Line(2) = {2, 3}; // Right
Line(3) = {3, 4}; // Top
Line(4) = {4, 1}; // Left
// Surface
Line Loop(1) = {1, 2, 3, 4};
Plane Surface(1) = {1};
// Physical groups for boundary conditions
Physical Line("bottom") = {1};
Physical Line("right") = {2};
Physical Line("top") = {3};
Physical Line("left") = {4};
Physical Surface("body") = {1};
// Use triangular elements
Mesh.ElementOrder = 1; // Linear elements (Tri3)
Mesh.Algorithm = 6; // Frontal-Delaunay for 2D
Key concepts:
lccontrols mesh density (smaller = finer mesh)- Physical groups (
Physical Line,Physical Surface) label regions for boundary conditions ElementOrder = 1gives linear triangular elements (Tri3)
Generating the Mesh
gmsh -2 unit_square.geo -o unit_square.msh
Options:
-2: Generate 2D mesh-o: Output file name
Result: unit_square.msh contains nodes and element connectivity.
Understanding the Mesh Format
Gmsh .msh format (ASCII version 2.2):
$MeshFormat
2.2 0 8
$EndMeshFormat
$Nodes
121 # Number of nodes
1 0.0 0.0 0.0 # node_id x y z
2 0.1 0.0 0.0
...
$EndNodes
$Elements
240 # Number of elements
1 1 2 4 1 1 2 # elem_id type num_tags tags... node_ids...
2 2 2 1 1 1 2 13 # type=2 is Tri3
...
$EndElements
Element types:
- Type 1: 2-node line (Seg2)
- Type 2: 3-node triangle (Tri3)
- Type 3: 4-node quadrilateral (Quad4)
Step 2: Weak Formulation (Theory)
Strong Form
The strong form (classical PDE) is:
∂u/∂t - α∇²u = f in Ω
u = g on Γ_D (Dirichlet boundary)
∂u/∂n = h on Γ_N (Neumann boundary)
Weak Form
Multiply by test function v and integrate by parts:
∫_Ω v(∂u/∂t) dΩ + α∫_Ω ∇v·∇u dΩ = ∫_Ω vf dΩ + ∫_{Γ_N} vh dΓ
Galerkin Approximation
Approximate u and v with finite element basis functions:
u(x,y,t) ≈ Σᵢ uᵢ(t) Nᵢ(x,y)
v(x,y) ≈ Σⱼ vⱼ Nⱼ(x,y)
Substitute and collect terms:
M du/dt + K u = f
Where:
- M = mass matrix:
Mᵢⱼ = ∫_Ω Nᵢ Nⱼ dΩ - K = stiffness matrix:
Kᵢⱼ = α∫_Ω ∇Nᵢ·∇Nⱼ dΩ - f = force vector:
fᵢ = ∫_Ω Nᵢ f dΩ + ∫_{Γ_N} Nᵢ h dΓ
This is a system of ODEs - the spatial discretization is complete, leaving only time dependence.
Step 3: FEM Assembly in JuliaFEM
Loading the Mesh
using JuliaFEM
# Read Gmsh mesh
mesh = abaqus_read_mesh("unit_square.msh") # Or gmsh reader when available
# Inspect mesh
println("Nodes: ", length(mesh.nodes))
println("Elements: ", length(mesh.elements))
println("Sets: ", keys(mesh.element_sets))
Creating Problem and Elements
# Heat transfer problem (domain)
body = Problem(Heat, "heat_body", 2) # 2D problem
body_elements = create_elements(mesh, "body")
# Add material properties
thermal_conductivity = 1.0 # α
for element in body_elements
update!(element, "thermal conductivity", thermal_conductivity)
end
add_elements!(body, body_elements)
Key: update! sets element properties. JuliaFEM stores these in element fields (now immutable NamedTuples for 130x speedup!).
Boundary Conditions
# Dirichlet BC: u = 0 on left edge
bc = Problem(Dirichlet, "fixed_temp", 2, "temperature")
bc_elements = create_elements(mesh, "left")
for element in bc_elements
update!(element, "temperature", 0.0)
end
add_elements!(bc, bc_elements)
Neumann BC: Natural boundary conditions (∂u/∂n = 0) require no explicit code - they're automatically satisfied by the weak form.
Assembly Process
# Assemble at time t = 0
time = 0.0
assemble!(body, time)
assemble!(bc, time)
What happens internally:
-
Loop over elements: For each element e:
-
Compute element matrices:
# Get element nodes and geometry X = [node.position for node in element.nodes] # Numerical integration over element for (ξ, w) in gauss_points(element) # Shape functions and derivatives N = basis(element, ξ) dN = grad_basis(element, ξ) # Jacobian (parametric → physical) J = dN' * X detJ = det(J) # Physical derivatives dN_dx = J \ dN # Local matrices Kₑ += w * detJ * α * (dN_dx * dN_dx') Mₑ += w * detJ * (N * N') end -
Scatter to global:
global_dofs = get_dofs(element) K[global_dofs, global_dofs] += Kₑ M[global_dofs, global_dofs] += Mₑ -
Apply Dirichlet BCs: Zero out rows/columns for constrained DOFs.
Step 4: Extracting Matrices (Issue #183)
This is what Chris Rackauckas asked for!
After assembly, extract matrices for use with external solvers:
# Get assembled system
K = body.assembly.K # SparseMatrixCSC{Float64}
M = body.assembly.M # SparseMatrixCSC{Float64}
f = body.assembly.f # Vector{Float64}
# System is now: M * du/dt = -K * u + f
Why Extract Matrices?
Use cases:
- DifferentialEquations.jl - Advanced ODE solvers (Rosenbrock, IMEX, etc.)
- Krylov.jl - Iterative linear solvers for large systems
- Custom time integration - Research on novel time-stepping schemes
- Sensitivity analysis - Automatic differentiation through solvers
- Optimal control - Adjoint methods, PDE-constrained optimization
Example: Solve with DifferentialEquations.jl
using OrdinaryDiffEq
# Initial condition: u₀ = sin(πx)sin(πy)
u0 = zeros(length(mesh.nodes))
for (i, node) in enumerate(mesh.nodes)
x, y = node.position[1:2]
u0[i] = sin(π*x) * sin(π*y)
end
# Apply boundary conditions to u0
apply_bc!(u0, bc)
# Define ODE: M * du/dt = -K * u + f
# Rearrange: du/dt = M \ (-K * u + f)
function heat_ode!(du, u, p, t)
K, M, f = p
du .= M \ (-K * u .+ f)
end
# Create ODE problem
tspan = (0.0, 1.0)
prob = ODEProblem(heat_ode!, u0, tspan, (K, M, f))
# Solve with adaptive Rosenbrock method
sol = solve(prob, Rosenbrock23())
# Solution is now in sol.u (array of states at different times)
Advantages:
- Adaptive time-stepping
- Stiff ODE solvers
- Event handling
- Sensitivity analysis
- GPU acceleration (CuArrays)
Step 5: Complete Working Example
See examples/gmsh_heat_equation/gmsh_heat_equation.jl for full runnable code.
Run it:
cd examples/gmsh_heat_equation
gmsh -2 unit_square.geo -o unit_square.msh
julia --project=. gmsh_heat_equation.jl
Output:
- Mesh statistics
- Assembly information
- Extracted matrix sizes
- Instructions for next steps
Method of Lines: Spatial Discretization Strategy
Key insight: FEM performs spatial discretization only, converting PDE → ODE system.
Before FEM (PDE):
∂u(x,y,t)/∂t = α∇²u(x,y,t) + f(x,y,t)
- Infinite-dimensional: u depends on continuous (x,y)
- Cannot solve directly on computer
After FEM (ODE):
M du(t)/dt = -K u(t) + f(t)
- Finite-dimensional: u is a vector of n nodal values
- Can solve with ODE integrators
This separation is powerful:
- Space: FEM handles complex geometry, boundary conditions
- Time: ODE solvers handle stiff systems, adaptivity, stability
- Modularity: Swap spatial/temporal methods independently
Comparison: Built-in vs External Solvers
Option 1: JuliaFEM Built-in (Easiest)
solver = Solver(Linear)
push!(solver, body, bc)
solver() # Uses direct solver
results = solver.results
Pros: Simple, integrated, handles BCs automatically Cons: Less control, fixed time-stepping, direct solver only
Option 2: Extract Matrices (Flexible)
K, M, f = extract_matrices(body, bc)
prob = ODEProblem(heat_ode!, u0, tspan, (K, M, f))
sol = solve(prob, Tsit5())
Pros: Full control, adaptive methods, iterative solvers, research flexibility Cons: More code, manual BC handling, need to understand ODE interface
Recommendation: Start with Option 1 for learning, move to Option 2 for research.
Extensions and Next Steps
1. Nonlinear Problems
If α = α(u) (temperature-dependent conductivity):
function nonlinear_heat!(du, u, p, t)
K, M, f = p
# Reassemble K with current u
K_nonlinear = assemble_stiffness(u)
du .= M \ (-K_nonlinear * u .+ f)
end
2. Time-Dependent BCs
function bc_time(t)
return sin(2π*t) # Oscillating temperature
end
# Update f vector in ODE function
f_bc = apply_bc_vector(bc, t)
3. 3D Problems
Same workflow, just change:
- Geometry file to 3D (Tet4, Tet10 elements)
- Problem dimension:
Problem(Heat, "body", 3)
4. Parallel Assembly
For large meshes (>1M elements):
using Threads
@threads for element in elements
assemble_local!(element)
end
5. GPU Acceleration
using CUDA
K_gpu = CuSparseMatrixCSC(K)
u_gpu = CuArray(u0)
# Solve on GPU
Troubleshooting
Gmsh not found
Error: gmsh: command not found
Solution: Install Gmsh:
- Ubuntu/Debian:
sudo apt install gmsh - macOS:
brew install gmsh - Windows: Download from https://gmsh.info
Mesh too coarse/fine
Adjust lc in .geo file:
lc = 0.05→ finer mesh (more elements, slower)lc = 0.2→ coarser mesh (fewer elements, faster)
Assembly takes forever
For large meshes (>100K elements):
- Use iterative solvers (Krylov.jl)
- Enable threading
- Profile assembly (
@time,@profview)
Oscillations in solution
- Mesh too coarse → refine
- Time step too large → reduce or use adaptive solver
- Boundary conditions wrong → check Gmsh physical groups
References
- Gmsh Documentation: https://gmsh.info/doc/texinfo/gmsh.html
- JuliaFEM Documentation: https://juliafem.github.io/JuliaFEM.jl
- DifferentialEquations.jl: https://docs.sciml.ai/DiffEqDocs/
- Issue #183: https://github.com/JuliaFEM/JuliaFEM.jl/issues/183
Books:
- Hughes, "The Finite Element Method" (theory)
- Zienkiewicz & Taylor, "The Finite Element Method" (comprehensive)
- Quarteroni et al., "Numerical Models for Differential Problems" (modern methods)
Summary
What we covered:
- ✓ Mesh generation with Gmsh (
.geo→.msh) - ✓ Weak formulation and Galerkin method
- ✓ FEM assembly (element → global matrices)
- ✓ Extracting K, M, f for external solvers
- ✓ Solving with DifferentialEquations.jl
- ✓ Method of lines (PDE → ODE)
Key takeaway: JuliaFEM provides the spatial discretization machinery. You bring the physics and time integration. Perfect for academic/research flexibility.
Next: Try the example, modify the geometry, experiment with different BCs, explore nonlinear problems!
This tutorial addresses Issue #183 and demonstrates JuliaFEM's "laboratory, not fortress" philosophy - educational, transparent, and flexible for research use.