Midterm Exam — Solutions
Model answers and grading guidance for midterm.md. Every "what does this print?" output
below was computed by hand, honoring the book's rule that no code is executed; if a student's compiled
output ever disagrees, recheck by hand — one of you has found something worth the class's attention. Field
widths follow the format edit descriptors exactly (there is no leading carriage-control blank on modern
formatted output, as throughout the book).
Grading philosophy (from rubrics/problem-set-rubric.md). For Part B, grade the exact output —
spacing included — but award most of the credit for the right values and the right ordering; a transposed
sign or a wrong field width is a minor deduction. For Part C, require both a correct diagnosis (what
and why) and a correct fix; a fix with no explanation is worth at most half. For Part D, grade correctness
first, then modern style (implicit none, intent on every argument, real(dp)/_dp, assumed-shape,
result clause); a warning-free build is part of the grade.
Point summary: A = 25, B = 25, C = 20, D = 30, total 100.
Part A — Concepts (25 points)
A1. (3 pts) The two Fortrans. The eras are FORTRAN 77 and earlier versus Fortran 90 and later.
FORTRAN 77 (written in all capitals) is the language of the caricature: fixed-form source (statements
confined to columns 7–72), COMMON blocks for global state, GOTO, and implicit typing (a variable's
type inferred from the first letter of its name). Fortran 90+ (title case) is a modern language:
free-form source, arrays as first-class objects, modules, derived types, dynamic (allocatable)
memory, and — by 2008 — built-in parallelism. This book teaches modern Fortran (2018 baseline); the
legacy language is read and modernized in Part IV, never written.
- Full credit: names both eras, one concrete distinguishing feature of each, and identifies modern Fortran as what the book teaches. Docking: "Fortran 77 vs Fortran 90" with no features is Proficient; conflating the two eras is Developing.
A2. (4 pts) Why Fortran is fast. (2 pts each.) (1) Arrays are first-class objects. The language
knows an array's shape and bounds, so a whole-array statement like a = b + c hands the compiler the entire
operation at once; it can vectorize or parallelize it directly instead of reverse-engineering the structure
out of a hand-written loop. (2) The no-aliasing rule. Procedure arguments are assumed not to overlap
in memory, so the compiler may freely reorder loads and stores (stream through memory predictably) — exactly
the optimization C could not perform until it added the restrict keyword to let the programmer promise
what Fortran guarantees by default.
- Full credit: both decisions named, each with a correct one-sentence "why." Docking: "it's compiled" or "it's low-level" is not an answer and earns nothing for that half — per the rubric's honesty standard.
A3. (3 pts) implicit none. It switches off Fortran's legacy implicit typing, so every variable must
be explicitly declared before use. That turns a misspelled variable name from a silent bug into a
compile-time error: without implicit none, writing temperatrue where you meant temperature silently
creates a brand-new (real) variable, initially undefined; with it, the typo fails to compile. It is required
in every program, module, and procedure in modern style.
- Full credit: names implicit typing and the typo-catching benefit with an example. Docking: "you must declare variables" without the why is Proficient.
A4. (3 pts) Kinds and dp. Plain real is single precision (~7 digits) on most compilers, and a
hardcoded kind number like real(8) is not portable (the "8" is a compiler-specific byte count). Instead you
describe the precision you need: selected_real_kind(15, 307) requests a kind with at least 15
significant decimal digits and a decimal exponent range to at least $10^{307}$ — IEEE double precision
on every mainstream system, portably. You then declare reals real(dp) and must write real literals with the
_dp suffix (1.0_dp, 0.5_dp) so the constant carries the right kind.
- Full credit: portability reason + what 15/307 mean + the
_dpliteral requirement.
A5. (3 pts) Column-major order. In column-major layout the first index varies fastest: a 2-D array is
stored one whole column at a time, so a(1,1), a(2,1), a(3,1), … occupy consecutive memory addresses. The
rule this implies: the inner (innermost) loop should run over the first index, so consecutive iterations
touch adjacent memory (cache-friendly). Getting it backwards strides across memory, wasting most of every
cache line, and can be up to roughly 10× slower for exactly the same answer. (C and NumPy default to the
opposite, row-major, order.)
- Full credit: "first index fastest / column by column" and the inner-loop-over-first-index rule. Mentioning the ~10× penalty is a plus but not required.
A6. (3 pts) Modules vs. COMMON. A COMMON block is a raw memory overlay: different routines can
map the same shared bytes under different names, types, or lengths, and nothing checks the mismatch — an
untyped, corruption-prone global. A module is a real namespace with an explicit, compiler-checked
interface: it can keep its state private and expose it only through procedures, cannot silently
mis-overlay memory, and can be reasoned about in isolation. Any one of these — the compiler checks types and
signatures across the boundary, or encapsulation behind private — is a correct "safer" reason.
- Full credit: names
COMMON's unchecked-overlay danger and one concrete module advantage the compiler provides.
A7. (3 pts) intent. The three intents: intent(in) — read-only (the procedure may read but not
write the argument); intent(out) — write-only (the argument arrives undefined and the procedure must
set it); intent(inout) — read and write (a meaningful value comes in and may be modified in place). The
free bug-catch: the compiler enforces the declaration — assigning to an intent(in) argument is a
compile-time error, so a whole class of accidental-write bugs is caught before the program ever runs. The
house rule is an intent on every dummy argument.
- Full credit: all three intents with correct meanings and "compiler-enforced." Noting that
intent(out)arrives undefined is a plus (and is the crux of C3).
A8. (3 pts) allocatable vs. pointer. Default to allocatable. Two reasons (any two): it is
automatically deallocated when it goes out of scope (no leaks); assignment b = a performs a deep copy
so storage is never shared by accident (value semantics); and because two allocatables cannot alias, the
compiler can optimize more aggressively. A pointer, by contrast, aliases (shallow copy → shared storage),
must be freed by hand (leak risk), and can dangle or be undefined. Reach for a pointer only when you
genuinely need aliasing, a self-referential/dynamic structure (linked list, tree), a polymorphic
container, a procedure-pointer callback, or C interoperability.
- Full credit: states the default, two valid reasons, and one legitimate use for a pointer.
Part B — Read the Code (25 points)
B1 — Arithmetic and division (5 pts)
x1 = 3.000
x2 = 3.500
p = 4
m = 1
q = 2
Reasoning: a / b is 7 / 2 in integer arithmetic — it truncates to 3 before the assignment to the
real x, so x1 is 3.000, not 3.500. real(a, dp) / b converts a to real first, so the whole
division is real: 3.500. a / b + 1 is 3 + 1 = 4 (integer). mod(7, 2) is 1. modulo(-7, 3) takes
the sign of the divisor (+3), giving 2 (whereas mod(-7, 3) would be -1). Field note: f6.3 right-
justifies 3.000 in six columns, so one leading space appears after the literal 'x1 = '.
- Common wrong answers:
x1 = 3.500(missing the integer-division truncation — the whole point) → deduct 2;q = -1(confusingmodulowithmod) → deduct 1.
B2 — Arrays, sections, and reductions (5 pts)
flat = 11 12 21 22 31 32
col2 = 21 22
total= 129
big = 2
Reasoning: the fill gives a(1,1)=11, a(2,1)=12, a(1,2)=21, a(2,2)=22, a(1,3)=31, a(2,3)=32. Printing the
whole array streams it in column-major (array-element) order — down column 1, then column 2, then column
3 — so the flat line is 11 12 21 22 31 32, not 11 21 31 12 22 32. The section a(:, 2) is column 2,
[21, 22]. sum(a) = 11+12+21+22+31+32 = 129. count(a > 25) counts 31 and 32 only, giving 2.
- Common wrong answer: a row-major flat line (
11 21 31 12 22 32) — the exact misconception the column- major rule targets. Deduct 2 and flag for review of §5.6.
B3 — select case (5 pts)
3 -> small
0 -> zero
-5 -> negative
12 -> large
Reasoning: 3 falls in case (1:9) → small; 0 matches case (0) → zero; -5 matches the open range
case (:-1) (everything ≤ −1) → negative; 12 matches none of the listed cases → case default →
large. The cases are disjoint, so exactly one block runs for each value, with no fall-through. trim
removes the trailing blanks from the character(len=8) variable, and i4 right-justifies each integer in
four columns (note the two spaces before -5).
- Common wrong answer: putting
12insmall(forgetting the range stops at 9) or mishandling thecase (:-1)open range.
B4 — A procedure with intent (5 pts)
x = 11.0 12.0 13.0 14.0
sum = 50.0
Reasoning: accumulate declares v as intent(inout) and does the whole-array add v = v + offset, so x
becomes [11, 12, 13, 14] in place. The pure function array_sum returns sum([11,12,13,14]) = 50.
Each value prints in an f6.1 field (two leading spaces before 11.0, etc.).
- Common wrong answer: leaving
xas[1, 2, 3, 4](missing thatintent(inout)writes back through the assumed-shape array) → deduct 2.
B5 — A derived type and whole-object assignment (5 pts)
a vol = 24.0
b vol = 80.0
a h = 3.0
Reasoning: a = box(2.0_dp, 3.0_dp, 4.0_dp) sets a%w, a%h, a%d = 2, 3, 4. Because box has only intrinsic
(non-pointer) components, b = a is a value copy — b gets its own independent components. Assigning
b%h = 10.0_dp therefore changes b alone. So a's volume is 2·3·4 = 24.0, b's is 2·10·4 = 80.0, and
crucially a%h is still 3.0, not 10.0. This is the value-semantics point: derived-type assignment
copies the whole object.
- Common wrong answer:
a h = 10.0(assumingb = aaliases, as it would in Python) → deduct 2; this is the discriminating line.
Part C — Find the Bug (20 points)
C1 — Integer division (7 pts)
Diagnosis (4 pts). sum(scores) is an integer (it sums an integer array to 353), and 4 is an
integer literal, so sum(scores) / 4 is integer division: 353 / 4 truncates toward zero to 88
before it is assigned to the real mean. The true mean is 353 / 4 = 88.25; the real variable on the left
cannot rescue a value already truncated on the right. Fix (3 pts): make the division real by converting
the numerator (or writing a real divisor):
mean = real(sum(scores), dp) / real(size(scores), dp) ! or: real(sum(scores), dp) / 4.0_dp
Now it prints mean = 88.25.
- Grading: full credit needs the words "integer division / truncates" (not just "add a decimal point"). A
bare fix with no diagnosis is 3/7. Accept
/ 4.0_dpas a fix, thoughreal(size(scores), dp)is the more robust modern answer.
C2 — Off-by-one and 1-based indexing (7 pts)
Diagnosis (4 pts). The loop runs k = 0, 4, a habit carried over from a 0-based language. Fortran arrays
are 1-based by default, so v has valid indices 1..5: (1) v(0) on the first iteration is out of
bounds — a memory error, caught at run time by -fcheck=all and otherwise silent corruption; and (2)
v(5) is never assigned, so it is left undefined. Fix (3 pts): loop over the array's real bounds and
index directly:
do k = 1, 5
v(k) = k * k
end do
(Recall Fortran's do bounds are inclusive on both ends, so do k = 1, 5 runs exactly five times.) This
gives v = [1, 4, 9, 16, 25], printed as 1 4 9 16 25.
- Grading: both faults must be named for full credit (out-of-bounds
v(0)and unsetv(5)); naming only one is 4–5/7. The corrected loop alone with no explanation is 3/7.
C3 — intent(out) on an accumulator (6 pts)
Diagnosis (4 pts). acc is declared intent(out), which means it arrives undefined — whatever
running total the caller passed in is discarded the instant the procedure begins. Reading acc on the
right-hand side of acc = acc + sum(x) therefore uses an undefined value, so the result is garbage.
intent(out) is only for a value computed from scratch; a procedure that needs the incoming value must
declare it intent(inout). Fix (2 pts): one word —
real(dp), intent(inout) :: acc
- Grading: full credit needs "
intent(out)arrives undefined / discards the incoming value." Answers that merely say "changeouttoinout" without the reason are 3/6.
Part D — Write It (30 points)
Model solutions; other correct, modern-style answers earn full marks. All compile clean with
gfortran -std=f2018 -Wall.
D1 — A pure function (10 pts)
program rms_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp) :: v(4) = [3.0_dp, 4.0_dp, 0.0_dp, 0.0_dp]
print '(a, f6.2)', 'rms = ', rms(v)
contains
pure function rms(v) result(r)
real(dp), intent(in) :: v(:) ! assumed-shape, read-only
real(dp) :: r
r = sqrt(sum(v**2) / real(size(v), dp))
end function rms
end program rms_demo
Printed value for v = [3.0_dp, 4.0_dp, 0.0_dp, 0.0_dp]:
rms = 2.50
Hand-check: $\sum v_i^2 = 9 + 16 + 0 + 0 = 25$, $n = 4$, so $\text{rms} = \sqrt{25/4} = \sqrt{6.25} = 2.5$.
- Grading (10):
pureprefix andintent(in)(2); assumed-shapev(:)(2);resultclause (1); correct formula withv**2,sum, andsizeand no explicit loop (3);real(size(v), dp)to avoid the integer-division trap inside the mean (2). Writingsum(v**2) / size(v)(integer division on the count) is the classic error — deduct those 2 points. Ado-loop implementation that is otherwise correct caps at 6 (the task asked for whole-array style).
D2 — A module with a derived type and a type-bound procedure (12 pts)
module geometry
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: rectangle
type :: rectangle
real(dp) :: width = 0.0_dp
real(dp) :: height = 0.0_dp
contains
procedure :: area => rectangle_area
end type rectangle
contains
pure function rectangle_area(self) result(a)
class(rectangle), intent(in) :: self ! class(...), NOT type(...)
real(dp) :: a
a = self%width * self%height
end function rectangle_area
end module geometry
program use_geometry
use geometry, only: rectangle
implicit none
type(rectangle) :: r
r = rectangle(3.0_dp, 4.0_dp)
print '(a, f6.1)', 'area = ', r%area()
end program use_geometry
Printed value:
area = 12.0
- Grading (12): module with
private+public :: rectangleso only the type is exported (2); type with tworeal(dp)components defaulting to0.0_dp(2);containsinside the type bindingarea => rectangle_area(2); passed-object dummy declaredclass(rectangle), nottype(rectangle)(3 — this is the single most common first error and is heavily weighted); correctwidth * heightbody with aresultclause (1); driver thatuses the module, constructs with the structure constructor, and callsr%area()(2). Declaring the passed objecttype(rectangle)is a compile error — cap at 9 and note it.nopass, or writingarea(r)as a free function instead of a method, does not meet the "type-bound" requirement — cap at 8.
D3 — A small array computation (8 pts)
program above_mean
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp) :: t(6) = [1.0_dp, 5.0_dp, 3.0_dp, 9.0_dp, 2.0_dp, 4.0_dp]
real(dp) :: mean
integer :: n_above
mean = sum(t) / real(size(t), dp) ! real divisor: no integer-division trap
n_above = count(t > mean) ! whole-array comparison as a mask
print '(a, f6.2)', 'mean = ', mean
print '(a, i0)', 'n_above = ', n_above
end program above_mean
Printed values:
mean = 4.00
n_above = 2
Hand-check: $\text{sum}(t) = 1+5+3+9+2+4 = 24$, $n = 6$, so mean $= 24/6 = 4.0$. The mask t > mean is true
for 5.0 and 9.0 only — note 4.0 is not strictly greater than 4.0 — so count returns 2.
- Grading (8): mean with a real divisor
real(size(t), dp)(2 —sum(t)/size(t)truncates and is the planted trap here too);count(t > mean)using a whole-array comparison, no explicit loop (3); both values printed (1); correct hand-stated results4.00and2(2). A student who reportsn_above = 3(wrongly counting the element equal to the mean) misread "strictly greater" — deduct 1.
End of solutions. Total: 100 points (A 25 · B 25 · C 20 · D 30).