Exercises: Anatomy of a Real Scientific Code
Architecture is a skill you build by reading and reorganizing code, not by memorizing definitions. These exercises make you classify modules by role, read directory trees and build files, write the grep and tags commands that navigate an unfamiliar code, reason about where state lives, and — the heart of the set — reorganize the heat solver into a real package. Several ask you to open a large open-source Fortran code and find your way around it; do those at a real terminal, because the whole point is that the techniques work on code no one could read in a sitting.
Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†)
and odd-numbered problems are in appendices/answers-to-selected.md; the compilable ones (36.10, 36.15,
36.19, 36.21, 36.25) are worked in full as code/exercise-solutions.f90. Every program compiles with
gfortran -std=f2018 -Wall. Try each problem before you look.
Part A — Roles and Layout ⭐
36.1 † Classify each of these modules by its architectural role (driver, solver, physics, I/O, or
utility): (a) a module that defines dp and a handful of physical constants; (b) a module holding the
five-point Laplacian as a pure function; (c) program run, which reads a config, loops calling step,
and writes output; (d) a module that opens and parses a namelist input deck; (e) a module holding the
time-stepping engine step and stable_dt.
36.2 In two or three sentences, say what a driver program does and, just as importantly, what it does not do. Why is the driver the first file to read when you want the thirty-second summary of a code?
36.3 † A well-organized src/ tree has util/ at the bottom and driver/ at the top. Explain why this
directory ordering is the module dependency graph "made visible on disk," and give the compile order it
implies.
36.4 For a code laid out as src/{util,core,physics,io,driver}/, name the district each of these files
belongs in: kinds.f90, advection.f90, netcdf_writer.f90, time_integrator.f90, logging.f90.
Part B — Reading the Layout ⭐⭐
36.5 † You are handed this (composite) tree and asked "where would I add a routine that writes results in
a new HDF5 format?" Answer, and give the module that would use your new module.
src/
├── util/ kinds.f90, constants.f90, logging.f90
├── core/ grid_types.f90, solver.f90
├── physics/ diffusion.f90, advection.f90
├── io/ config_io.f90, field_io.f90
└── driver/ run.f90
36.6 (Find the bug — in the architecture.) A teammate puts the five-point Laplacian in src/io/ and
has src/physics/diffusion.f90 use the driver module to read a global timestep. Name the two design
principles from §36.1–§36.2 this violates, and describe the symptom each will eventually cause.
36.7 † State the "one module per file, module named after the file" convention, and explain concretely how it makes a 100,000-line code navigable with nothing but a file listing. What do you lose when a code breaks it?
Part C — Build Systems ⭐⭐
36.8 † (Read a Makefile.) Given the Makefile from §36.3, (a) what determines the compile order of
the modules, and (b) exactly what must you edit if you add a new module boundary.f90 that heat_solver
uses? What error appears at build time if you forget?
36.9 Compare fpm and a hand-written Makefile for a new Fortran project on three axes: how the compile order is determined, what you must maintain by hand, and when you would nonetheless prefer CMake over both.
36.10 † (Type, compile, and run — predict first.) Read code/example-03-build-config.f90. Write down
its exact output. Then predict the output after changing debug to .false. and recompiling, and explain,
in terms of the optimizer, what happens to the guarded print line.
36.11 Define build configuration in one sentence, then give a concrete example of two build configurations of the same source that could produce different numerical results, and say what a reproducible project must therefore record.
Part D — Navigating a Codebase ⭐⭐
36.12 † (Write the commands.) You have cloned an unfamiliar code into ./src and ./app. Write the
shell commands (grep, and a ctags invocation) that (a) find the program's entry point, (b) list every
module the code defines, (c) find where subroutine step is defined, and (d) find every caller of step.
36.13 Fortran is case-insensitive, and codes rename on import (use m, only: s => step). Give one
concrete way each of these facts can make a plain grep for a name mislead you, and say what tool resolves
each and why.
36.14 † (Legacy.) A FORTRAN 77 code has no modules, so there is no use graph to grep. Describe how you
would build the equivalent of a "module map" for it — what do you grep for instead, and what plays the role
that use plays in modern code? (Cross-reference
Chapter 17.)
Part E — Where State Lives and Where Time Goes ⭐⭐⭐
36.15 † (Type, compile, and run — predict first.) Read code/example-02-where-state-lives.f90. Predict
its three output lines exactly. Then explain, in one sentence each, why via_global() returns two different
values from identical-looking calls and why via_arg cannot.
36.16 You are reading a routine that uses a value dt_global which appears nowhere in its argument list.
Where is dt_global almost certainly defined, and what is the one search you must perform to know what
value it holds when this routine runs? Why does this make the routine slower to understand than one that
takes dt as an argument?
36.17 † (Follow the data.) For the heat solver, trace the life of the temperature field as a sequence of four moves: where it is born, where it is read and written, and where it leaves the program. Name the module and procedure for each. Why is "follow the data" a better reading strategy than "follow the control flow" in a numerical code?
36.18 You are asked to speed up an unfamiliar explicit finite-difference solver. Form a hypothesis about where the time goes from the code's structure alone, then state exactly how you would confirm it before changing anything (name the tool and the Chapter it comes from).
Part F — Design It: Reorganize the Solver ⭐⭐⭐
36.19 † (Design it — compilable.) Split the diffusion physics out of heat_solver. Write a diffusion
module exporting pure function laplacian(u, dx, dy), and a heat_solver whose step uses it. The
physics result must be unchanged (verify on the 5×5, two-step case that u(2,3) = 32.00 and the maximum
stays $100$). Draw how the module map changes versus the pre-split version.
36.20 (Design it.) Write the fpm.toml and the src/ + app/ + test/ directory tree for the
reorganized six-file solver (kinds, timers, heat_types, heat_solver, heat_io, and the heat
driver). State which file goes in app/ and why the rest go in src/.
36.21 † (Design it — compilable.) Add a constants utility module exporting pi and two_pi as
real(dp) parameters (taking dp from kinds). Say where it sits in the layer diagram, which modules may
use it, and whether adding it forces heat_io to recompile. Write a short program that prints both
constants.
36.22 Write the module-dependency section of a hand-written Makefile (the foo.o : bar.o lines) for
the six-file solver of 36.20, and explain why getting one of these lines wrong produces the "Cannot open
module file" error rather than a link error.
Part G — Back of the Envelope and Interleaved ⭐⭐⭐
36.23 † (Back of the envelope.) A production code is 100,000 lines. To fix your assigned bug you read the 150-line driver, three modules averaging 350 lines, and the 400-line solver. What fraction of the code did you read? What does the answer say about the "navigate, don't read" thesis of §36.4?
36.24 (Interleaved — Chapter 8.) A proposed layout has heat_solver use heat_io (to log mid-step)
and heat_io use heat_solver (to re-normalize before writing). Explain why this will not compile, name
the principle it violates, and give a concrete fix that keeps both capabilities.
36.25 † (Interleaved — Chapter 13 — compilable.) Here is a fragile field-allocation routine. Rewrite
it to (a) validate its precondition and (b) guard the allocation, returning a success flag instead of
aborting. Show it accepting n = 4 and refusing n = -1.
subroutine make_field(u, n)
real(dp), allocatable, intent(out) :: u(:,:)
integer, intent(in) :: n
allocate(u(n, n)) ! no validation, no stat= : aborts on a bad n
end subroutine make_field
36.26 (Interleaved — Chapter 28.) Connect this chapter's architecture skills to profiling: explain why "a code you can navigate is a code you can profile, and a code you can profile is a code you can make fast," using the heat solver's hot loop as the example.
36.27 † (Port it.) A Python project is laid out as main.py, solver/engine.py, physics/diffusion.py,
and utils/io.py. Map each file to a Fortran module and one of the five architectural roles, and note the
one guarantee the Fortran layering has that the Python layering does not.
36.28 (Interleaved — Chapter 9.) Our solver bundles nx, ny, dx, dy, u into one field_t rather than
passing five loose arguments. Explain how this single change improves data flow and therefore readability —
what does a reader now learn from a signature subroutine step(field, alpha, dt) that step(u, nx, ny, dx,
dy, alpha, dt) obscured?
Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the
compilable ones (36.10, 36.15, 36.19, 36.21, 36.25) are worked in full as code/exercise-solutions.f90.
For every design or reorganization problem, state the module map and the compile order — they are part of
the answer.