Files
JuliaFEM.jl/docs/book/gmsh_tutorial.md
T
Jukka Aho 46b0cd3927 docs(book): Add comprehensive Gmsh to physics tutorial
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
2025-11-09 23:24:15 +02:00

13 KiB
Raw Blame History

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
Tutorial
Getting Started
gmsh
heat-equation
academic-usage
method-of-lines
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 (.geo files)
  • 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:

  • lc controls mesh density (smaller = finer mesh)
  • Physical groups (Physical Line, Physical Surface) label regions for boundary conditions
  • ElementOrder = 1 gives 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:

  1. Loop over elements: For each element e:

  2. 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
    
  3. Scatter to global:

    global_dofs = get_dofs(element)
    K[global_dofs, global_dofs] += Kₑ
    M[global_dofs, global_dofs] += Mₑ
    
  4. 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:

  1. DifferentialEquations.jl - Advanced ODE solvers (Rosenbrock, IMEX, etc.)
  2. Krylov.jl - Iterative linear solvers for large systems
  3. Custom time integration - Research on novel time-stepping schemes
  4. Sensitivity analysis - Automatic differentiation through solvers
  5. 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):

  1. Use iterative solvers (Krylov.jl)
  2. Enable threading
  3. 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

  1. Gmsh Documentation: https://gmsh.info/doc/texinfo/gmsh.html
  2. JuliaFEM Documentation: https://juliafem.github.io/JuliaFEM.jl
  3. DifferentialEquations.jl: https://docs.sciml.ai/DiffEqDocs/
  4. 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:

  1. ✓ Mesh generation with Gmsh (.geo.msh)
  2. ✓ Weak formulation and Galerkin method
  3. ✓ FEM assembly (element → global matrices)
  4. ✓ Extracting K, M, f for external solvers
  5. ✓ Solving with DifferentialEquations.jl
  6. ✓ 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.