Exercises: Fortran-Python Interoperability

These exercises make you do the two-language workflow, not just read about it. Several ask you to build a real extension module with f2py and call it from Python; a few ask you to measure a speedup on your own machine. You will need NumPy installed (f2py ships with it) and a working gfortran — the same compiler you have used all book.

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 are in code/exercise-solutions.f90. Try every problem before you look. Because benchmark numbers depend on your hardware, the timing exercises give a model result and ask for your number.


Part A — Warm-ups ⭐

15.1 † Explain, flag by flag, what f2py -c -m mymod kernel.f90 does. Which part names the thing you will import in Python?

15.2 What is an extension module? Name two extension modules you almost certainly already use without having thought of them that way.

15.3 † For a Fortran dummy argument, state how f2py presents each of these to Python: intent(in), intent(out), intent(inout).

15.4 Give the matching NumPy dtype for each: real(real64), real(real32), integer(int32), complex(real64). Why is np.array([1, 2, 3]) a trap when the Fortran expects real(dp)?


Part B — Wrap It ⭐⭐

Type, build, and run. Predict the Python signature before you build.

15.5 † Here is a Fortran function that returns the Euclidean norm of a vector:

module veclib
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
contains
  function norm2_kernel(x, n) result(r)
    integer,  intent(in) :: n
    real(dp), intent(in) :: x(n)
    real(dp) :: r
    r = sqrt(sum(x**2))
  end function norm2_kernel
end module veclib

Add the one !f2py directive that hides n, write the f2py build command naming the module veclib, and write the two-line Python driver that calls it on [3.0, 4.0]. What does it print, and why?

15.6 A subroutine has three arguments: intent(in) :: a(n), intent(out) :: total, and intent(out) :: mean, with n hidden. After wrapping, what does the Python call look like, and what does it return? (Hint: two intent(out) arguments.)

15.7 † Wrap the smooth kernel from code/example-03-smooth.f90 and write a Python driver that smooths [0, 0, 0, 12, 0, 0, 0]. Predict the output array by hand before running.


Part C — The order='F' Issue ⭐⭐

15.8 † A colleague's f2py-accelerated code is slower than their pure-NumPy version. The relevant lines are field = np.zeros((4000, 4000)) and a loop that calls step(field, ...) 20,000 times. Diagnose the problem in one sentence and give the one-keyword fix.

15.9 True or false, and explain: "Because Fortran is column-major and NumPy is row-major, f2py silently transposes your array, so a[i, j] in Python is u(j, i) in Fortran."

15.10 † You pass a default np.zeros((100, 100)) array to an f2py routine whose argument is intent(inout). What happens, and why is it different from passing the same array to an intent(in) argument?

15.11 Given a 2-D array a, write the one line that tells you whether it is F-contiguous, and the one line that returns an F-contiguous version (copying only if necessary).


Part D — Port It ⭐⭐

Translate the Python to Fortran, wrap it, and compare speed. Show your reasoning; exact times will differ.

15.12 † Port this pure-Python running-difference loop to a wrapped Fortran kernel, then benchmark both on an array of five million elements:

def diffs_py(x):
    n = len(x)
    d = np.empty(n)
    d[0] = 0.0
    for i in range(1, n):
        d[i] = x[i] - x[i-1]
    return d

Write the Fortran, the build command, and the benchmark harness (with an assert np.allclose check). Report the speedup you measure and explain where the gap comes from.

15.13 Port a 2-D four-neighbor averaging smoother (out(i,j) = average of the four edge-neighbors) to Fortran and wrap it. What must the Python driver do to the input array to make every call zero-copy?

15.14 † Back of the envelope. A kernel is called 100,000 times on a 2000 × 2000 float64 field. If the field is C-contiguous, f2py copies it in and (for the output) back on every call. Estimate the total bytes moved by copying alone. How does order='F' change that number?


Part E — Alternatives: ctypes and cffi ⭐⭐⭐

15.15 † Explain why ctypes cannot reliably call a plain Fortran subroutine that lacks bind(c). What symbol name would gfortran typically export for a module-less subroutine foo, and why does that defeat ctypes?

15.16 Given the bind(c) function in code/example-04-cfuncs.f90, write the ctypes restype and argtypes declarations and the call. Why must the length argument be passed with ctypes.byref?

15.17 † Give two concrete situations in which you would choose ctypes (or cffi) over f2py, and one in which f2py is clearly the better choice. Justify each.


Part F — Design It (extend the solver) ⭐⭐⭐

15.18 † Modify the heat kernel so a single Python call advances the field by k steps inside Fortran (loop k times internally, ping-ponging between two buffers), returning only the final field. Why does this amortize the boundary-crossing cost, and when does it matter most?

15.19 Write an intent(inout) in-place variant of step (update u directly, no u_new). What two properties must the Python caller guarantee about the array it passes, and what does f2py do if they are not met?

15.20 † Design (in prose + skeleton code) a Python driver that runs the solver and saves a PNG frame every 50 steps, so the frames can be assembled into an animation. Which part is Python's job and which is Fortran's? Tie your answer to the two-language workflow of §15.1.


Part G — Back of the Envelope & Interleaved ⭐⭐⭐

15.21 † (looks ahead to Chapter 31) Your kernel is 95% of the program's runtime. You make just the kernel 50× faster by moving it to Fortran; the other 5% (Python orchestration) is unchanged. What is the speedup of the whole program? (This is Amdahl's Law in disguise — set it up and compute it.)

15.22 Estimate the memory footprint of one C-to-F copy of a 5000 × 5000 float64 array. If you make that copy on every one of 10,000 steps, how much total copying have you done, and what is the fix?

15.23 † (interleaved Chapter 5) In a Fortran double loop over a 2-D array, why should the inner loop run over the first index? Connect your answer to why a NumPy array bound for Fortran should be created order='F'.

15.24 (interleaved Chapter 14) Which iso_c_binding kind matches a NumPy float64? Write the C prototype for a bind(c) function double dot(double *x, double *y, int *n) and the matching Fortran interface.

15.25 † (interleaved Chapter 6) You wrote intent on every dummy argument since Chapter 6 as a safety habit. Explain how that same habit now, in Chapter 15, does double duty by shaping the Python interface f2py generates.


Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the compilable Fortran solutions (15.5, 15.7, 15.12, 15.13, 15.19, 15.24) are in code/exercise-solutions.f90. The timing exercises (15.12, 15.14, 15.22) give a model result — your measured numbers depend on your machine, and finding them is the point.