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 _dp literal 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 (confusing modulo with mod) → 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 defaultlarge. 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 12 in small (forgetting the range stops at 9) or mishandling the case (:-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 x as [1, 2, 3, 4] (missing that intent(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 copyb 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 (assuming b = a aliases, 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_dp as a fix, though real(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 unset v(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 "change out to inout" 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): pure prefix and intent(in) (2); assumed-shape v(:) (2); result clause (1); correct formula with v**2, sum, and size and no explicit loop (3); real(size(v), dp) to avoid the integer-division trap inside the mean (2). Writing sum(v**2) / size(v) (integer division on the count) is the classic error — deduct those 2 points. A do-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 :: rectangle so only the type is exported (2); type with two real(dp) components defaulting to 0.0_dp (2); contains inside the type binding area => rectangle_area (2); passed-object dummy declared class(rectangle), not type(rectangle) (3 — this is the single most common first error and is heavily weighted); correct width * height body with a result clause (1); driver that uses the module, constructs with the structure constructor, and calls r%area() (2). Declaring the passed object type(rectangle) is a compile error — cap at 9 and note it. nopass, or writing area(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 results 4.00 and 2 (2). A student who reports n_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).