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 (kinds first, main last); 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 test do almost everything; dependencies pinned to a tag for 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
$ 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 program unit where execution begins.
  • module map / source tree — the use dependency 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

  1. You navigate a large code by its architecture, not by reading it — the five roles + the module map, queried with grep and tags.
  2. 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.