Date: 2026-01-16 Module: linalg Purpose: Comprehensive guide to BELFEM's backend-agnostic linear algebra API Revision: 2026-01-16 - Initial version
Overview
The src/linalg module provides a backend-agnostic linear algebra API for BELFEM. It abstracts over two high-performance backends:
- Armadillo (default on Linux): LAPACK/BLAS-based, mature ecosystem
- Blaze: Expression template library, optimized for small-to-medium matrices
The abstraction allows switching backends at compile time without changing user code, enabling performance tuning and portability.
Key Features
- Backend Independence: Write once, compile with either Armadillo or Blaze
- Expression Templates: Lazy evaluation for complex expressions (e.g., A*B + C*D)
- Bounds Checking: Debug-mode assertions (zero overhead in release)
- Operator Overloading: Natural mathematical syntax (+, -, *, /)
- Direct Backend Access: Escape hatch via vector_data() / matrix_data()
Critical: Backend Selection Backend is chosen at compile time. The CMake cache options are USE_MATRIX_ARMADILLO and USE_MATRIX_BLAZE (CMakeLists.txt:81-87); the BELFEM_ARMADILLO / BELFEM_BLAZE names are the generated compile defines (config/linalg/config_matrix.cmake), so -DBELFEM_ARMADILLO=ON on the command line does nothing. The default is Armadillo everywhere except Apple, where it is Blaze. You cannot mix backends in a single build.
Common Pitfalls
1. Mixing Vector/Matrix with Raw Backend Types
Vector<real> v(3);
arma::vec armadillo_vec = v;
arma::vec armadillo_vec = v.vector_data();
Solution: Use .vector_data() or .matrix_data() for backend interop.
2. Element-wise vs. Matrix Multiplication
Matrix<real> A(3, 3), B(3, 3);
Matrix<real> C = A * B;
Solution: * is matrix multiplication. For element-wise, access backend directly.
3. Initializer List Ambiguity
Vector<real> v = {3};
Vector<real> w(3);
Vector<real> u = {1,2,3};
Solution: Use explicit syntax: Vector<real> v(3, 0.0) for size+fill.
4. Row/Column Lifetime
Matrix<real> A(3, 3);
auto row = A.row(0);
A.set_size(5, 5);
Solution: Don't hold views across resizing operations.
5. Debug vs. Release Behavior
Vector<real> v(5);
real x = v(10);
Solution: Test in both modes. Use static analysis for release builds.
Core Types
Vector<T> - Column Vector
Files: cl_Vector.hpp, backend implementations in armadillo/cl_AR_Vector.hpp or blaze/cl_BZ_Vector.hpp
Description
Template class wrapping backend column vector (stored as n×1 matrix in Armadillo).
Construction
Vector<real> v;
Vector<real> v(10);
Vector<real> v(10, 0.0);
Vector<real> v = {1.0, 2.0, 3.0};
Vector<int> v = {1, 2, 3, 4, 5};
Vector<real> v = trans(someMatrix.row(0));
Element Access
Vector<real> v(5, 1.0);
real x = v(0);
v(4) = 2.0;
real* ptr = v.data();
const real* cptr = v.data();
Size Operations
size_t n = v.length();
v.set_size(20);
v.set_size(20, 0.0);
v.fill(1.0);
Arithmetic Operations
Vector<real> a(3), b(3), c(3);
a = 1.0;
a = {1, 2, 3};
a = b;
a = std::move(b);
a += b;
a -= b;
a *= 2.0;
a /= 2.0;
a %= b;
c = a + b;
c = a - b;
c = a * 2.0;
c = 2.0 * a;
c = a / 2.0;
Iteration
Vector<real> v(5, 1.0);
for (real& x : v) {
x *= 2.0;
}
for (const real& x : v) {
std::cout << x << " ";
}
Backend Interoperability
Vector<real> v(3);
auto& backend_vec = v.vector_data();
#ifdef BELFEM_ARMADILLO
arma::Mat<real>& av = v.vector_data();
av.save("vector.dat", arma::raw_ascii);
#elif BELFEM_BLAZE
blaze::DynamicVector<real>& bv = v.vector_data();
#endif
Printing
Vector<real> v = {1.0, 2.0, 3.0};
v.print("MyVector");
When to Use
- Primary vector type for all BELFEM computations
- FEM shape function evaluations
- Nodal coordinates, DOF values
- Right-hand side vectors for linear systems
See: Vector class in cl_Vector.hpp and backend implementations
Matrix<T> - Dense Matrix
Files: cl_Matrix.hpp, backend implementations in armadillo/cl_AR_Matrix.hpp or blaze/cl_BZ_Matrix.hpp
Description
Template class wrapping backend dense matrix (column-major in both backends).
Construction
Matrix<real> A;
Matrix<real> A(3, 3);
Matrix<real> A(3, 3, 0.0);
Matrix<real> A = {{1, 2, 3},
{4, 5, 6},
{7, 8, 9}};
Matrix<real> A = inv(B);
Element Access
Matrix<real> A(3, 3);
real x = A(0, 0);
A(2, 2) = 1.0;
auto row0 = A.row(0);
auto col1 = A.col(1);
auto sub = A.submat(0, 0, 1, 1);
real* ptr = A.data();
Size Operations
size_t rows = A.n_rows();
size_t cols = A.n_cols();
size_t total = A.capacity();
A.set_size(5, 5);
A.set_size(5, 5, 0.0);
A.fill(1.0);
Do not size a raw transfer from capacity(). Under Blaze the columns are padded for SIMD alignment, so spacing() — the inter-column stride — can exceed n_rows() and capacity() is the padded total (cl_BZ_Matrix.hpp:254-269). For an MPI send of a whole matrix the payload length is spacing() * n_cols(). Element access always goes through A(i, j); never compute an offset from data().
Setting Rows/Columns
Matrix<real> A(3, 3);
Vector<real> v = {1, 2, 3};
A.set_row(0, v);
A.set_col(1, v);
Arithmetic Operations
Matrix<real> A(3, 3), B(3, 3), C(3, 3);
A = 1.0;
A = B;
A = std::move(B);
A += B;
A -= B;
A *= 2.0;
A /= 2.0;
A *= B;
C = A + B;
C = A - B;
C = A * B;
C = A * 2.0;
C = A / 2.0;
Matrix-Vector Multiplication
Matrix<real> A(3, 3);
Vector<real> x(3), b(3);
b = A * x;
Printing
Matrix<real> A = {{1, 2}, {3, 4}};
A.print("MyMatrix");
When to Use
- Jacobians, stiffness matrices (small, dense)
- Rotation matrices, transformation matrices
- Local element matrices before assembly
See: Matrix class in cl_Matrix.hpp and backend implementations
Free Functions
Linear Algebra Operations
dot(a, b) - Dot Product
Vector<real> a = {1, 2, 3};
Vector<real> b = {4, 5, 6};
real result = dot(a, b);
File: fn_dot.hpp
cross(a, b) - Cross Product
Vector<real> a = {1, 0, 0};
Vector<real> b = {0, 1, 0};
Vector<real> c = cross(a, b);
Requirements: Both vectors must have length 3.
File: fn_cross.hpp
crossmat(n, A, out) - Normal-Matrix Cross Product
Computes cross product of a normal vector with each column of a matrix.
Vector<real> n = {nx, ny};
Matrix<real> A(2, ncols);
Vector<real> nxA(ncols);
crossmat(n, A, nxA);
Vector<real> n3 = {nx, ny, nz};
Matrix<real> A3(3, ncols);
Matrix<real> nxA3(3, ncols);
crossmat(n3, A3, nxA3);
Signature:
- 2D: void crossmat(const Vector<real>& n, const Matrix<real>& A, Vector<real>& out)
- 3D: void crossmat(const Vector<real>& n, const Matrix<real>& A, Matrix<real>& out)
Use case: FEM flux computations, boundary integrals
File: fn_crossmat.hpp
norm(v) - Euclidean Norm
Vector<real> v = {3, 4};
real n = norm(v);
File: fn_norm.hpp
trans(A) - Transpose
Matrix<real> A = {{1, 2, 3},
{4, 5, 6}};
Matrix<real> AT = trans(A);
File: fn_trans.hpp
Matrix Decompositions and Solvers
inv(A) - Matrix Inverse
Matrix<real> A = {{4, 7}, {2, 6}};
Matrix<real> Ainv = inv(A);
Note: For 2×2 and 3×3, specialized fast implementations inv2() and inv3() exist.
Files: fn_inv.hpp, fn_inv2.hpp, fn_inv3.hpp
det(A) - Determinant
Matrix<real> A = {{1, 2}, {3, 4}};
real d = det(A);
File: fn_det.hpp
gesv(A, x, pivot) - General Linear System Solve
Solves Ax = b via LU decomposition. Overwrites A with LU factorization and x with solution.
Matrix<real> A = {{3, 2}, {1, 2}};
Vector<real> x = {5, 3};
Vector<int_t> pivot(2);
gesv(A, x, pivot);
Signature: int_t gesv(Matrix<T>& A, Vector<T>& x, Vector<int_t>& pivot, bool AbortOnError = true) — returns LAPACK info (see the LAPACK guide) Modifies: Both A (LU factorization) and x (RHS → solution) Requires: pivot.length() >= A.n_rows() File: fn_gesv.hpp
posv(A, x) - Positive Definite System Solve
Solves Ax = b assuming A is symmetric positive definite (uses Cholesky). Overwrites A with Cholesky factorization and x with solution.
Matrix<real> A = {{4, 2}, {2, 3}};
Vector<real> x = {6, 5};
posv(A, x);
Signature: int_t posv(Matrix<T>& A, Vector<T>& x, bool AbortOnError = true) Returns: LAPACK info — 0 on success; > 0 means the leading minor of that order is not positive definite. With AbortOnError = true (the default) a non-zero info raises BELFEM_ERROR instead — which throws in a debug build and aborts in release. Modifies: Both A (Cholesky factorization) and x (RHS → solution) Requires: A must be symmetric positive definite Faster than gesv() (~2× for SPD matrices)
File: src/linalg/lapack/fn_posv.hpp
eigen(A, values [, abortOnComplex]) / eigen_sym(A, values) - Eigenvalues
Eigenvalues only (no eigenvectors). eigen returns the number of complex eigenvalues (aborts on the first one by default); eigen_sym reads the upper triangle and returns ascending real eigenvalues.
Matrix<real> A = {{2, 1}, {1, 2}};
Vector<real> values;
eigen_sym(A, values);
File: fn_eigen.hpp
Utility Functions
linspace(start, end, n) - Linearly Spaced Vector
Vector<real> v = linspace(0.0, 1.0, 11);
File: fn_linspace.hpp
append(a, b) - Vector Concatenation (In-Place)
Vector<real> a = {1, 2};
Vector<real> b = {3, 4};
append(a, b);
Signature: void append(Vector<T>& a, Vector<T>& b) Modifies: First argument a in-place File: fn_append.hpp
combine(a, b, out) - Concatenate Vectors
Writes a followed by b into out (resized; must not alias an input). Overloads take three or four inputs. Unlike append, the inputs are untouched.
File: fn_combine.hpp
reverse(v) - Reverse Vector
Vector<real> v = {1, 2, 3};
Vector<real> r = reverse(v);
File: fn_reverse.hpp
sort(v) - Sort Vector (In-Place)
Vector<real> v = {3, 1, 2};
sort(v);
Signature: void sort(Vector<T>& v) Modifies: Argument in-place File: fn_sort.hpp
unique(v) - Unique Elements (In-Place)
Sorts vector and removes duplicates in-place.
Vector<real> v = {1, 2, 2, 3, 1};
unique(v);
Signature: void unique(Vector<T>& v) Modifies: Argument in-place (sorts then removes duplicates) File: fn_unique.hpp
Statistical/Aggregate Functions
sum(v) - Sum of Elements
Vector<real> v = {1, 2, 3, 4};
real s = sum(v);
File: fn_sum.hpp
max(v) - Maximum Element
Vector<real> v = {1, 5, 3};
real m = max(v);
File: fn_max.hpp
min(v) - Minimum Element
Vector<real> v = {1, 5, 3};
real m = min(v);
File: fn_min.hpp
Polynomial Operations
polyval(p, x) - Evaluate Polynomial
Evaluates polynomial using highest-power-first coefficient ordering via Horner's method.
Vector<real> p = {3, 2, 1};
real y = polyval(p, 2.0);
Important: Coefficients are highest-power-first, not lowest-power-first.
File: fn_polyval.hpp
dpolyval(p, x) - Polynomial Derivative
First derivative: dp/dx.
File: fn_dpolyval.hpp
ddpolyval(p, x) - Polynomial Second Derivative
Second derivative: d²p/dx².
File: fn_ddpolyval.hpp
polyfit(x, y, degree, coeffs) - Polynomial Fit
Least-squares polynomial fit of given degree. The coefficient vector is the last argument; it is resized to degree + 1, highest power first.
Vector<real> x = {0, 1, 2, 3};
Vector<real> y = {1, 3, 7, 13};
Vector<real> p;
polyfit(x, y, 2, p);
File: fn_polyfit.hpp
Specialized Functions
r2(approximated, exact) - Coefficient of Determination
Statistical measure of fit quality (R² score).
Vector<real> y_true = {1, 2, 3, 4};
Vector<real> y_pred = {1.1, 1.9, 3.2, 3.9};
real r2 = r2(y_pred, y_true);
File: fn_r2.hpp
Backend Comparison
| Feature | Armadillo | Blaze |
| Maturity | Mature (10+ years) | Mature (5+ years) |
| Dependencies | LAPACK/BLAS | Header-only |
| Expression Templates | Yes | Yes (advanced) |
| Sparse Matrices | Yes | Yes |
| Small Matrix Opt | Good | Excellent |
| Large Matrix Opt | Excellent (LAPACK) | Good |
| Default in BELFEM | Yes | No |
| Ease of Installation | Moderate (needs LAPACK) | Easy (header-only) |
When to Use Which Backend
Use Armadillo (Linux default) when:
- Need sparse matrix support
- Large dense matrices (> 100×100)
- Interfacing with existing LAPACK code
- Need mature, stable ecosystem
Use Blaze when:
- Small to medium matrices (< 50×50)
- Header-only build preferred
- Maximum performance for expression templates
- Modern C++ template metaprogramming
Recommendation: Stick with Armadillo unless you have specific performance requirements.
Backend Selection at Compile Time
CMake Configuration
# Armadillo (default on Linux; Blaze is the default on Apple)
cmake -DUSE_MATRIX_ARMADILLO=ON -DUSE_MATRIX_BLAZE=OFF ..
# Blaze
cmake -DUSE_MATRIX_BLAZE=ON -DUSE_MATRIX_ARMADILLO=OFF ..
Preprocessor Checks
#ifdef BELFEM_ARMADILLO
arma::mat A = ...;
#elif BELFEM_BLAZE
blaze::DynamicMatrix<real> A = ...;
#endif
Performance Tips
1. Use Expression Templates
Matrix<real> temp1 = A * B;
Matrix<real> temp2 = temp1 + C;
Matrix<real> result = temp2 * D;
Matrix<real> result = (A * B + C) * D;
2. Reserve Size When Known
Vector<real> v;
for (int i = 0; i < 1000; ++i) {
v.set_size(i+1);
}
Vector<real> v(1000);
for (int i = 0; i < 1000; ++i) {
v(i) = compute(i);
}
3. Prefer Specialized Inverses
Matrix<real> A(2, 2), Ainv(2, 2);
real detA = inv2(A, Ainv);
Matrix<real> B(3, 3), Binv(3, 3);
real detB = inv3(B, Binv);
4. Use SPD Solvers When Applicable
Vector<real> x = b;
posv(A, x);
5. Avoid Row/Column Copies
Vector<real> row = A.row(0);
auto row_view = A.row(0);
real sum = 0.0;
for (size_t i = 0; i < A.n_cols(); ++i) {
sum += row_view(i);
}
Common Patterns
Pattern 1: Solving Linear System
Matrix<real> K(n, n);
Vector<real> u(n);
Vector<int_t> pivot(n);
gesv(K, u, pivot);
Pattern 2: Rotation Matrix Construction
real theta = M_PI / 4;
Matrix<real> R(2, 2);
R(0, 0) = cos(theta);
R(0, 1) = -sin(theta);
R(1, 0) = sin(theta);
R(1, 1) = cos(theta);
Vector<real> v = {1, 0};
Vector<real> v_rot = R * v;
Pattern 3: Coordinate Transformation
Matrix<real> T = compute_transformation_matrix();
Vector<real> local_coords = {1, 2, 3};
Vector<real> global_coords = T * local_coords;
Pattern 4: Polynomial Interpolation
Vector<real> x = {0, 1, 2, 3, 4};
Vector<real> y = {1, 2.1, 3.9, 6.2, 8.8};
Vector<real> p;
polyfit(x, y, 2, p);
real y_interp = polyval(p, 2.5);
Pattern 5: Jacobian Computation
Matrix<real> jacobian(m, n);
Vector<real> x0(n);
real h = 1e-8;
for (size_t j = 0; j < n; ++j) {
Vector<real> x_plus = x0;
x_plus(j) += h;
Vector<real> f_plus = residual(x_plus);
Vector<real> x_minus = x0;
x_minus(j) -= h;
Vector<real> f_minus = residual(x_minus);
Vector<real> df = (f_plus - f_minus) / (2*h);
jacobian.set_col(j, df);
}
Thread Safety and MPI
Thread Safety: Like containers, Vector and Matrix are not thread-safe.
- Reading: Safe from multiple threads
- Writing: Requires external synchronization
MPI: Each rank has independent copies. Use comm module for synchronization.
Example with OpenMP:
Matrix<real> A(100, 100);
#pragma omp parallel for collapse(2)
for (int i = 0; i < 100; ++i) {
for (int j = 0; j < 100; ++j) {
A(i, j) = compute(i, j);
}
}
Sparse Matrices
BELFEM's linalg module focuses on dense matrices. For sparse matrices, use:
- src/sparse/ module: Wrappers for MUMPS, STRUMPACK, PETSc, etc.
- Direct backend access: arma::sp_mat (Armadillo) or blaze::CompressedMatrix (Blaze)
Related Modules
- containers: Cell<T> for dynamic arrays (complements Vector for non-numeric data)
- sparse: Sparse matrix solvers (MUMPS, STRUMPACK, PETSc, PARDISO)
- fem: Uses Vector/Matrix extensively for element matrices
- numerics: Polynomial evaluation, splines, integration (uses linalg)
See Also
Revision History:
- 2026-01-16: Initial version
- 2026-01-16: Corrected factual errors based on external review (in-place function signatures, polynomial conventions, storage order claims, crossmat semantics)