Chapter 36 — Key Takeaways (Anatomy of a Real Scientific Code)
A one-page reference for finding your way around, and organizing, a large Fortran code. Keep it beside you the first time you open someone else's hundred thousand lines.
The five architectural roles
| Role | Job | In our solver | Depends on |
|---|---|---|---|
| Driver (program) | Orchestrate: setup, main loop, output. No science itself. | program heat |
everything below |
| Solver module | The numerical engine: stepping/iteration scheme | heat_solver (step, stable_dt) |
core, physics, utils |
| Physics module | One physical process, ideally pure functions |
the laplacian (diffusion operator) |
utils |
| I/O module | All contact with the outside world (parse, write) | heat_io (read_config, write_field) |
core, utils |
| Utility module | Foundational, general support | kinds, constants, timers |
nothing (the bottom) |
The source tree mirrors the dependency graph
src/
├── util/ kinds, constants, logging ← bottom: everyone uses, uses nothing
├── core/ grid/field types, solver engine
├── physics/ diffusion, advection, ... ← one module per process
├── io/ config parsing, output writers
└── driver/ run ← top: uses almost everything
app/ main.f90 (the program → the executable)
test/ ... (unit + regression tests)
- Read it bottom-up for compile order (
kindsfirst,mainlast); top-down for what the code does. - One module per file, module named after the file — the convention that makes a code navigable from a file listing alone.
Build systems in the wild
| Tool | You write | Compile order | Best for |
|---|---|---|---|
| Make | a Makefile (targets + deps + commands) |
by hand | universal; inherited codes |
| CMake | CMakeLists.txt (describe → generate build) |
tool-derived | big cross-platform, multi-language |
| Meson | meson.build |
tool-derived | modern, clean, fast |
| fpm | a short fpm.toml |
scans use, automatic |
new Fortran projects |
- fpm layout rule: programs →
app/, library modules →src/, tests →test/. fpm build·fpm run·fpm testdo almost everything; dependencies pinned to atagfor repro.
Build configuration = how source becomes a program
Compiler + version + flags (-O2/-O0 -g -fcheck=all) + enabled options/macros + library
versions. Two builds of one source can give different numbers — so record the build configuration or
the result is not reproducible (Ch. 37). Capture compile-time choices in parameter constants:
logical, parameter :: debug = .true. ! flip to .false. → optimizer deletes if(debug) branches
Navigating 100,000 lines — the moves
$ grep -rin "^\s*program " app/ src/ # 1. find the ENTRY POINT (the one `program`)
$ grep -rin "^\s*module " src/ # 2. list every module (nodes of the map)
$ grep -rin "^\s*use " src/ # edges of the MODULE MAP (who uses whom)
$ ctags -R src/ app/ # 3. build a tags index: jump to any definition
$ grep -rin "subroutine step" src/ # where is `step` DEFINED?
$ grep -rin "call step" src/ app/# who CALLS `step`?
- Navigate by architecture, don't read front to back. Hold the map; query the rest.
- grep finds text; tags/LSP (
fortls) find symbols — needed because Fortran is case-insensitive and names get renamed on import (use m, only: s => step).
Reading someone else's code: where state lives
| Where state lives | Readability | How to reason about it |
|---|---|---|
Arguments + intent |
easiest | read the signature — inputs/outputs are visible |
A derived type threaded through (field_t) |
easy | one visible argument carries the state |
| Module variable (global) | hard | hidden input — grep every assignment to know its value |
COMMON block (legacy) |
hardest | grep the block name across routines (Ch. 17) |
- To understand a routine, find what it reads and writes first, then follow the data.
- Follow the data, not the control flow — in numerical code the data flow is the algorithm.
- Where the time goes: almost always the inner stencil loop (80/20). Hypothesize from structure, confirm with a profiler (Ch. 28) before optimizing (Ch. 29). Navigate → measure → change → re-measure.
Terms introduced
- driver program — top-level unit that orchestrates a run and performs no science itself.
- solver / physics / utility module — the numerical engine / one physical process / foundational support.
- build configuration — the full set of choices (compiler, flags, options, library versions) that turn source into a program.
- entry point — the single
programunit where execution begins. - module map / source tree — the
usedependency graph / the directory hierarchy that (well) mirrors it.
Project piece added this chapter
The solver is reorganized into a package: src/{kinds,constants,timers,heat_types,heat_solver,heat_io}.f90,
app/main.f90 (the driver), test/, and an fpm.toml (fpm derives the compile order) — with a Makefile
dependency section as the hand-maintained alternative. Same physics (the Chapter 24 result); real
architecture. This is the shape the Chapter 38 capstone
presents as research.
The two things to remember
- You navigate a large code by its architecture, not by reading it — the five roles + the module map, queried with grep and tags.
- To understand a routine, find where its state lives and follow the data — argument-passed state reads locally; global state makes you search the whole module.