Exercises: Linear Algebra and LAPACK

These exercises live or die on getting the calling convention exactly right, so most of them want you at a keyboard with the LAPACK reference page open beside you. The habit to build is the one this chapter is really about: state the problem in standard form, hand it to the library, and check the answer — a residual you computed and confirmed is a result you own. Several problems ask you to link -llapack -lblas; if the link fails, reread §21.5 before you touch the code.

Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†) and odd-numbered problems are in appendices/answers-to-selected.md; the computational ones also appear as compilable code in code/exercise-solutions.f90. Try every problem before you look. Compile with gfortran -std=f2018 -Wall (add -llapack -lblas for anything that calls LAPACK).


Part A — Matrix Storage and the Intrinsics ⭐

21.1 † Predict the exact printout, then compile to confirm. Pay attention to the fill order:

integer :: m(2, 3)
m = reshape([1, 2, 3, 4, 5, 6], [2, 3])     ! no order= argument
print '(3i4)', m(1, :)                        ! row 1
print '(3i4)', m(2, :)                        ! row 2

Write the 2×3 matrix m represents, and explain why it is not the matrix whose rows read 1 2 3 and 4 5 6.

21.2 By hand, compute matmul(a, b) for a = [[2, 1], [0, 3]] and b = [[1, 4], [2, 1]] (each written row by row). Show all four entries. Then compute the elementwise a * b and confirm it differs.

21.3 † For a square matrix a, the product matmul(transpose(a), a) is always symmetric. Explain why in one sentence, and verify it for a = [[1, 2], [3, 4]] by computing both matmul(transpose(a), a) and its transpose and showing they match.

21.4 dot_product(x, y) returns a scalar; matmul(m, x) returns a vector when m is a matrix and x a vector. For m of shape (4, 4) and x of length 4, what is the shape of matmul(m, x)? And what is dot_product([1.0_dp, -1.0_dp, 2.0_dp], [3.0_dp, 3.0_dp, 1.0_dp])?


Part B — LAPACK Naming and the Calling Convention ⭐⭐

21.5 † Decode each LAPACK name into precision, matrix type, and computation: (a) dgesv; (b) sgetrf; (c) dsyev; (d) zheev; (e) dgels. (One computes an LU factorization; one finds eigenvalues of a complex Hermitian matrix; one solves a least-squares problem.)

21.6 Write out the full dgesv argument list in order, and for each argument state whether it is input, output, or input/output. Which two arrays does dgesv overwrite, and with what?

21.7 † You declare real(dp) :: a(100, 100) but only fill and solve the top-left 5×5 block. What value must you pass as lda to dgesv, and why is it not 5? What goes wrong if you pass 5?

21.8 A dgesv call returns info = 3. Then a second call, on a different system, returns info = -7. Interpret each value: what happened, and in which case is the bug likely in your code rather than in the data?


Part C — Port It ⭐⭐

Translate the snippet to modern Fortran that calls LAPACK, and note what the library makes you do by hand that the high-level tool did for you.

21.9 † Port this NumPy to Fortran with dgesv, and print the solution:

import numpy as np
A = np.array([[3.0, 2.0], [1.0, 2.0]])
b = np.array([7.0, 5.0])
x = np.linalg.solve(A, b)

What is x? Name three things NumPy did that your Fortran must now do explicitly.

21.10 Port w = np.linalg.eigvalsh(A) (eigenvalues of a real symmetric matrix, ascending) to a dsyev call, including the workspace query. What does eigvalsh assume about A that tells you to use dsyev rather than the general dgeev?

21.11 † Port the MATLAB one-liner x = A \ b (solve the system) to Fortran. Beyond the syntax, name the two behavioral differences you must account for: what happens to A, and what memory layout does your matrix already have that MATLAB's does not.


Part D — Find the Bug ⭐⭐

Each snippet is wrong. Say what happens — a link error, a silent wrong answer, or a crash — and fix it.

21.12 †

real(dp) :: a(3,3), b(3)
integer  :: ipiv(3), info
! ... fill a and b ...
call dgesv(3, 1, a, 3, b, ipiv, 3, info)     ! solve A x = b

21.13 A programmer solves, then checks the residual with the same a:

call dgesv(n, 1, a, n, ipiv, b, n, info)
resid = matmul(a, b) - rhs_saved             ! expecting ~0

The residual comes back large and nonsensical even though info == 0. Why?

21.14 † This is meant to solve 2x + 3y = 8, x + 4y = 9 (intended matrix [[2,3],[1,4]]), whose solution is (1, 2) — but it prints the wrong answer:

a = reshape([2.0_dp, 3.0_dp, 1.0_dp, 4.0_dp], [2, 2])   ! meant to be [[2,3],[1,4]]
b = [8.0_dp, 9.0_dp]
call dgesv(2, 1, a, 2, ipiv, b, 2, info)

What system did it actually solve (write the matrix), and what is the one-token fix?

21.15 This links and runs but sometimes prints garbage:

call dgesv(n, 1, a, n, ipiv, b, n, info)
print '(f10.4)', b        ! the solution

What critical step is missing, and what should the code do when it is present?


Part E — Design It (Extend the Solver) ⭐⭐⭐

21.16 † (Design it — solver.) Extend the Project Checkpoint from two interior nodes to n. Assemble the tridiagonal matrix a(n,n) with 1+2r on the diagonal and -r off it, build the right-hand side for a hot left edge (u_L = 1) and cold right edge (u_R = 0) starting from u^n = 0, and solve with dgesv for n = 3, r = 1. What are the three interior temperatures, and is the profile physically sensible?

21.17 (Design it — solver.) Rewrite 21.16 to call the tridiagonal solver dgtsv(n, nrhs, dl, d, du, b, ldb, info) instead of dgesv. What three short arrays replace the full a(n,n), and why is this the honest choice for a 1-D implicit step (think about the operation count as n grows)?

21.18 † (Design it.) The two-dimensional implicit heat step couples each grid point to its four neighbors. Explain why, for an nx × ny grid, the matrix A becomes (nx·ny) × (nx·ny), and why it is sparse (mostly zeros). For nx = ny = 1000, how many rows does A have, and why does this rule out dgesv?


Part F — Back of the Envelope ⭐⭐⭐

21.19 † (Cost of a solve.) LU factorization of a dense n × n matrix costs about $\tfrac{2}{3}n^3$ floating-point operations. For n = 1000, estimate the operation count. At a sustained $10^{10}$ flop/s, how long does the dgesv take? Compare to the $2n^3$ of a single matmul at the same n.

21.20 (Memory.) A dense n × n matrix of real(dp) needs $8n^2$ bytes. For n = 10{,}000, how many GiB is that ($1\ \text{GiB} = 1024^3$ bytes)? Now suppose the matrix has only 5 nonzeros per row; roughly how much memory would a sparse (CSR) representation need instead, and what factor have you saved?

21.21 † (Arithmetic intensity.) A Level-1 BLAS daxpy on length-n vectors does about 2n flops touching about 3n numbers; a Level-3 dgemm on n × n matrices does about 2n³ flops touching about 3n² numbers. Compute the flops-per-number ratio for each as a function of n, and explain in one sentence why only Level 3 can approach the processor's peak speed.


Part G — Interleaved (Chapters 5, 16, and 20) ⭐⭐

21.22 † (Ch. 5.) Fortran stores a(n, n) column-major. Write the doubly-nested loop that fills it with the memory grain, and state the one-sentence reason this same layout is why a Fortran matrix needs no transposition before you hand it to dgesv.

21.23 (Ch. 16.) In the ecosystem chapter, LAPACK and BLAS were named but not called. What does LAPACK depend on, in what order must you list them to the linker, and how would you declare the dependency in an fpm project instead of typing flags?

21.24 † (Ch. 20.) A matrix A has condition number $\kappa(A) \approx 10^{8}$. You solve A x = b in double precision, whose machine epsilon is about $10^{-16}$. Roughly how many correct significant decimal digits can you expect in x, and why does a small residual not by itself guarantee an accurate solution?

21.25 (Ch. 20.) Distinguish two failure modes of a linear solve: dgesv returning info > 0 (an exactly singular matrix) versus a matrix that is merely ill-conditioned. Which one does info catch, which one does it not, and what LAPACK routine estimates the second?

21.26 † (Synthesis.) Write a complete program that solves $A = \begin{bmatrix} 2 & 1 & 1 \\ 1 & 3 & 2 \\ 1 & 0 & 4 \end{bmatrix}$, $\mathbf{b} = (3, 2, 9)$ with dgesv, prints the solution, and confirms maxval(abs(matmul(a_orig, x) - b_orig)) < 10^{-10}. What is x, and why must you save a_orig and b_orig before the call?

21.27 (Ch. 5.) Explain why you should almost never form the inverse A⁻¹ explicitly (say, to compute x = matmul(A_inv, b)) when all you want is x. What does dgesv do instead, and which LAPACK routine would you use if you genuinely needed the inverse?

21.28 † (Design + Ch. 20.) Fit a straight line $y = c_1 + c_2 x$ to the points $(0,1), (1,3), (2,5), (3,8)$ by forming the normal equations $A^{\mathsf{T}}A\,\mathbf{c} = A^{\mathsf{T}}\mathbf{y}$ (with transpose and matmul) and solving the 2×2 system with dgesv. What are c_1 and c_2? Then say, citing conditioning (Ch. 20), why the normal equations are numerically inferior to solving the least-squares problem directly with dgels or the SVD.


Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the four computational ones (21.9, 21.16, 21.26, 21.28) are worked as runnable, LAPACK-linked code in code/exercise-solutions.f90. Design problems 21.16–21.18 admit variations — as long as your matrix is assembled correctly, your info is checked, and your residual is small, you are right.