Quick Start

Gilles Colling

2026-03-30

Your first mesh

tulpaMesh takes point coordinates and returns a triangulated mesh with FEM matrices ready for SPDE models.

set.seed(42)
coords <- cbind(x = runif(100), y = runif(100))
mesh <- tulpa_mesh(coords)
mesh
#> tulpa_mesh:
#>   Vertices:   113 
#>   Triangles:  211 
#>   Edges:      323

The mesh extends slightly beyond the convex hull of your points (controlled by extend). Plot it:

plot(mesh, vertex_col = "steelblue", main = "Basic mesh")

Controlling mesh density

Use max_edge to add refinement points. The mesh generator places a hexagonal lattice of points at this spacing, producing near-equilateral triangles.

mesh_fine <- tulpa_mesh(coords, max_edge = 0.08)
mesh_fine
#> tulpa_mesh:
#>   Vertices:   287 
#>   Triangles:  0 
#>   Edges:      0
plot(mesh_fine, main = "Refined mesh (max_edge = 0.08)")

Getting FEM matrices

fem_matrices() returns the three sparse matrices needed for SPDE models:

fem <- fem_matrices(mesh_fine, obs_coords = coords)
dim(fem$C)
#> [1] 287 287
dim(fem$A)
#> [1] 100 287

# Verify key properties
all(Matrix::diag(fem$C) > 0)        # positive diagonal
#> [1] FALSE
max(abs(Matrix::rowSums(fem$G)))     # row sums ~ 0
#> [1] 0
range(Matrix::rowSums(fem$A))        # row sums = 1
#> [1] 1 1

For the SPDE Q-builder, you typically need the lumped (diagonal) mass matrix:

fem_l <- fem_matrices(mesh_fine, obs_coords = coords, lumped = TRUE)
Matrix::isDiagonal(fem_l$C0)
#> [1] TRUE

Using a formula interface

If your coordinates live in a data.frame, use a formula:

df <- data.frame(lon = runif(50), lat = runif(50), y = rnorm(50))
mesh_f <- tulpa_mesh(~ lon + lat, data = df)
mesh_f
#> tulpa_mesh:
#>   Vertices:   60 
#>   Triangles:  108 
#>   Edges:      167

Mesh quality

Check triangle quality with mesh_quality() and mesh_summary():

mesh_summary(mesh_fine)
#> Mesh quality summary:
#>   Triangles:      0
#> Warning in min(q$min_angle): no non-missing arguments to min; returning Inf
#> Warning in max(q$min_angle): no non-missing arguments to max; returning -Inf
#>   Min angle:      Inf / NA / -Inf (min / median / max)
#> Warning in min(q$max_angle): no non-missing arguments to min; returning Inf
#> Warning in max(q$max_angle): no non-missing arguments to max; returning -Inf
#>   Max angle:      Inf / NA / -Inf
#> Warning in min(q$aspect_ratio): no non-missing arguments to min; returning Inf
#> Warning in max(q$aspect_ratio): no non-missing arguments to max; returning -Inf
#>   Aspect ratio:   Inf / NA / -Inf
#> Warning in min(q$area): no non-missing arguments to min; returning Inf
#> Warning in max(q$area): no non-missing arguments to max; returning -Inf
#>   Area:           Inf / NA / -Inf

Color triangles by minimum angle:

plot(mesh_fine, color = "quality", main = "Colored by minimum angle")

#> Warning in min(x): no non-missing arguments to min; returning Inf
#> Warning in max(x): no non-missing arguments to max; returning -Inf

Ruppert refinement

For guaranteed minimum angles, use min_angle:

mesh_r <- tulpa_mesh(coords, min_angle = 25, max_edge = 0.15)
mesh_summary(mesh_r)
#> Mesh quality summary:
#>   Triangles:      0
#> Warning in min(q$min_angle): no non-missing arguments to min; returning Inf
#> Warning in max(q$min_angle): no non-missing arguments to max; returning -Inf
#>   Min angle:      Inf / NA / -Inf (min / median / max)
#> Warning in min(q$max_angle): no non-missing arguments to min; returning Inf
#> Warning in max(q$max_angle): no non-missing arguments to max; returning -Inf
#>   Max angle:      Inf / NA / -Inf
#> Warning in min(q$aspect_ratio): no non-missing arguments to min; returning Inf
#> Warning in max(q$aspect_ratio): no non-missing arguments to max; returning -Inf
#>   Aspect ratio:   Inf / NA / -Inf
#> Warning in min(q$area): no non-missing arguments to min; returning Inf
#> Warning in max(q$area): no non-missing arguments to max; returning -Inf
#>   Area:           Inf / NA / -Inf

Next steps