Case Study 26.1: The Inherited Diffusion Code — From a Home-Grown Dump to ParaView
"The first thing you do with an inherited code is not run it. It is read what it writes." — a maxim from more than one national-lab onboarding
Executive Summary
You have inherited a small, working 2-D diffusion code, diffuse.f90, from a departed colleague. It runs,
it is numerically fine, and it produces a file called field.out in a format the colleague invented, which
a brittle plot.gnu script turns into a single static image. Your advisor wants an animation of the run
for a group meeting on Friday, and ParaView cannot read field.out at all. This case study is the analysis
and the fix: you will read the home-grown format, verify it against a field you know, diagnose exactly why
it neither animates nor opens in ParaView, and port the output to standard legacy VTK so the whole run
becomes a scrubbable time series — without touching one line of the (correct) physics.
Skills applied
- Reading an undocumented ASCII output format and recovering its layout (§26.1, §26.2).
- Verifying a writer against a hand-known field before trusting it (§26.2).
- Diagnosing why a single-file, custom dump cannot become a time series (§26.3).
- Porting output to a standard VTK STRUCTURED_POINTS writer with the exact header (§26.2).
- Loading the resulting frame sequence as an animation in ParaView (§26.3).
Background
The inherited writer looks like this — modern enough to compile, but home-grown in format:
subroutine dump(u, nx, ny, fname)
use, intrinsic :: iso_fortran_env, only: dp => real64
integer, intent(in) :: nx, ny
real(dp), intent(in) :: u(nx, ny)
character(len=*), intent(in) :: fname
integer :: iu, i, j
open(newunit=iu, file=fname, status='replace', action='write')
write(iu, '(2i6)') nx, ny ! a header of two integers
do i = 1, nx
write(iu, '(*(es13.5))') (u(i, j), j = 1, ny) ! one line per i, all j across
end do
close(iu)
end subroutine dump
It writes a two-integer header, then one line per i with all the j values across the row. It is not
wrong — it round-trips fine into the colleague's gnuplot script — but it is nobody's standard, and no
visualization tool knows how to read it. Your job is to understand it well enough to replace it safely.
Phase 1 — Read the format against a field you know
Never reverse-engineer a format from a big run; use the smallest field whose every value you chose. Drive
the inherited dump with a 3×2 field:
real(dp) :: u(3, 2)
u(:, 1) = [0.0_dp, 1.0_dp, 2.0_dp]
u(:, 2) = [3.0_dp, 4.0_dp, 5.0_dp]
call dump(u, 3, 2, 'field.out')
Trace the writer by hand. The header line is nx ny in 2i6, so 3 2. Then the loop runs over
i (the outer loop) and writes all j on each line in es13.5:
3 2
0.00000E+00 3.00000E+00
1.00000E+00 4.00000E+00
2.00000E+00 5.00000E+00
Now you know the format exactly: a line per i, columns are j, values in scientific notation. That
is a transposed, header-tagged layout — the opposite of the row-major matrix a plotting tool expects, and
the reason the gnuplot script needed a transpose fudge you would have to reproduce. This is the moment the
custom format reveals its cost: every consumer must know its private conventions.
Sanity check. The value
u(2,1) = 1.0must appear on the line fori = 2, first column (j = 1): the second data line is1.00000E+00 4.00000E+00, whose first entry is indeed1.00000E+00. The format is understood.
Phase 2 — Diagnose: why it can't animate, and why ParaView refuses it
Two independent problems block Friday's animation, and it is worth separating them.
Problem 1 — it is one file, not a series. The run calls dump once, at the end, overwriting field.out
each time. There is no record of intermediate states, so there is nothing to animate — the time history was
never saved. No format change fixes this alone; the time loop must write a frame per interval.
Problem 2 — the format is not VTK. Even the final snapshot cannot open in ParaView, because field.out
has none of the structure a reader needs: no magic string, no declared dataset type, no grid geometry, no
POINT_DATA count. ParaView has no way to know it is a 3×2 uniform grid of doubles.
| Requirement for a ParaView animation | field.out provides it? |
|---|---|
| A magic string identifying the format | ✗ (starts with 3 2) |
| Declared grid type + geometry (origin, spacing, dims) | ✗ (only nx ny) |
| A point/cell data count | ✗ |
| One file per timestep, ordered | ✗ (single overwritten file) |
The fix addresses both: write standard VTK, and write it once per interval with an ordered name.
Phase 3 — Recover the geometry the format threw away
VTK needs geometry the custom format never stored: the origin and the spacing. The physics code has them —
dx and dy are parameters of the run — they simply were not written to field.out. You confirm the run
uses dx = dy = 0.01 on the 3×2 test and a hot top edge. This is the general lesson of §26.1: a custom
dump often omits the grid because "everyone here knows it," which is exactly what makes it unportable. VTK
forces you to write the geometry down, and that is a feature.
Phase 4 — Port the output to standard legacy VTK
Now replace dump with the canonical write_vtk, giving it the geometry via the field_t type. The physics
is untouched; only the output routine changes.
subroutine write_vtk(field, filename, step)
type(field_t), intent(in) :: field
character(len=*), intent(in) :: filename
integer, intent(in) :: step
integer :: iu, i, j
open(newunit=iu, file=filename, status='replace', action='write')
write(iu, '(a)') '# vtk DataFile Version 3.0'
write(iu, '(a, i0)') 'ported from field.out, step ', step
write(iu, '(a)') 'ASCII'
write(iu, '(a)') 'DATASET STRUCTURED_POINTS'
write(iu, '(a, 3(1x, i0))') 'DIMENSIONS', field%nx, field%ny, 1
write(iu, '(a, 3(1x, f0.6))')'ORIGIN', 0.0_dp, 0.0_dp, 0.0_dp
write(iu, '(a, 3(1x, f0.6))')'SPACING', field%dx, field%dy, 1.0_dp
write(iu, '(a, 1x, i0)') 'POINT_DATA', field%nx * field%ny
write(iu, '(a)') 'SCALARS temperature double 1'
write(iu, '(a)') 'LOOKUP_TABLE default'
do j = 1, field%ny ! j outer, i inner: VTK x-fastest order
do i = 1, field%nx
write(iu, '(f0.6)') field%u(i, j)
end do
end do
close(iu)
end subroutine write_vtk
Notice the loop nest inverted relative to the inherited dump: the old writer looped i outer and
wrote j across; VTK wants j outer, i inner, one value per line. Getting this backwards is exactly the
transpose bug of Exercise 10 — the values would all be present but the plate would render sideways. Drive
the new writer with the same 3×2 test field (dx = dy = 0.01) and it produces:
# vtk DataFile Version 3.0
ported from field.out, step 0
ASCII
DATASET STRUCTURED_POINTS
DIMENSIONS 3 2 1
ORIGIN 0.000000 0.000000 0.000000
SPACING 0.010000 0.010000 1.000000
POINT_DATA 6
SCALARS temperature double 1
LOOKUP_TABLE default
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
Sanity check. Six values follow
POINT_DATA 6, in VTK orderu(1,1),u(2,1),u(3,1),u(1,2),u(2,2), u(3,2) = 0,1,2,3,4,5. Compare with the custom dump from Phase 1: the same six numbers, now in a standard, self-describing container ParaView can read.
Phase 5 — Save a series and open the animation
The last change is in the driver's time loop: call write_vtk every save_every steps with a zero-padded
name (using frame_name from Chapter 12).
do step = 0, n_steps
if (mod(step, save_every) == 0) call write_vtk(field, frame_name(step), step)
call step_field(field, alpha, dt)
end do
Run it, and the output folder fills with heat_000000.vtk, heat_000100.vtk, … . In ParaView,
File ▸ Open collapses them into one heat_..vtk source; Apply, color by temperature, and the play
button appears. Friday's animation exists — and the physics code was never touched.
| Before | After |
|---|---|
field.out, custom transposed ASCII |
standard legacy .vtk STRUCTURED_POINTS |
| single overwritten file | zero-padded per-step series |
| opens only via a private gnuplot script | opens in ParaView and VisIt |
geometry implicit ("everyone knows dx") |
geometry written explicitly (origin/spacing) |
Discussion Questions
- The inherited
dumpwas not wrong — it worked for its author. What, precisely, made it a liability the moment someone else (or a new tool) needed the data? Relate your answer to the "self-describing" idea from Chapter 25. - Porting the writer inverted the loop nest (
iouter →jouter). Why is that not just a style choice but a correctness requirement? What would the picture look like if you kept the old order? - The geometry (
dx,dy) existed in the code but not infield.out. Why does VTK's insistence that you write it down make the data more valuable, not just more verbose? - You changed only the output routine, not the physics. Why is that separation (I/O in one module, the solver in another — Chapter 8) what made this a one-afternoon job instead of a rewrite?
Your Turn: Extensions
- Option A (analyze). Take the Phase 1 output and write a Fortran reader that parses
field.outback into afield_t(read the2i6header, then the transposed values into the rightu(i,j)). Confirm it round-trips by writing VTK from the reconstructed field and diffing against the Phase 4 file. - Option B (port). The old
plot.gnuscript drew a single heat map. Port its intent to a matplotlib script (§26.5) that reads the ported VTK's values and renders the same map with a perceptually uniform colormap — the modern replacement for the brittle gnuplot script. - Option C (extend). Add a
.pvdcollection (§26.3) so the ported series carries physical time (t = step*dt). Present the animation with a real-seconds slider at the group meeting.
Key Takeaways
- Read what a code writes before you trust or replace it — recover the format against a field whose every value you chose, then verify one known entry.
- A custom dump's convenience is a portability debt: no magic string, no geometry, no count means no tool but its author's can read it.
- Porting to VTK is mostly bookkeeping done exactly: the five header parts, the geometry the old format
omitted, and the
j-outer/i-inner value order. - Keep I/O separate from physics and a format change touches one routine — the analysis here changed how the run is seen, not what it computes.