Study Projects
Project 1 — Weather Station Statistics
Every engineer writes this program once: read records from a CSV, keep them in derived types, compute statistics, write a report. It exercises files, derived types, array reductions and formatted I/O together. The twist that makes it interesting: the number of stations is unknown before reading, so the container must grow — the demo uses a fixed array for simplicity, and your challenge is to upgrade it to allocatable.
program weather_stats
use iso_fortran_env, only: real64
implicit none
type :: station ! one record per line of the CSV
character(len=24) :: name
real(real64) :: temp
integer :: altitude
end type station
type(station) :: st(4) ! the fixed-size version
integer :: unit, ios, n, i, imax
character(len=128) :: msg
open (newunit=unit, file="stations.csv", status="old", action="read", &
iostat=ios, iomsg=msg)
if (ios /= 0) then
print '(a)', trim(msg)
error stop 1
end if
n = 0
do
read (unit, '(a24,1x,f8.2,1x,i6)', iostat=ios) &
st(n+1)%name, st(n+1)%temp, st(n+1)%altitude
if (ios < 0) exit ! end of file
if (ios > 0) cycle ! skip malformed lines
n = n + 1
end do
close (unit)
print '(a,i0,a)', 'stations read: ', n
print '(a,f8.2)', 'mean temperature: ', sum(st(1:n)%temp) / n
print '(a,f8.2)', 'max temperature: ', maxval(st(1:n)%temp)
imax = maxloc(st(1:n)%temp, dim=1)
print '(a,a,a)', 'hottest: ', trim(st(imax)%name), ' (report line below)'
do i = 1, n
write (*, '(a24,1x,f8.2,1x,i6)') st(i)%name, st(i)%temp, st(i)%altitude
end do
! Challenge: replace the fixed st(4) with st(:) allocatable —
! read once to count, allocate, reposition, read again.
end program weather_stats
Project 2 — Object-Oriented Shapes
A polymorphic calculator: an abstract shape type, two concrete extensions, and one area call that dispatches correctly no matter which concrete type is passed. This is the oop lesson compressed into a single standalone module-and-program, and the natural next step is adding a third shape (triangle, …) and a list container of class(shape) entries.
module shapes
implicit none
private
public :: shape, circle, rectangle, describe
type, abstract :: shape
contains
procedure(area_iface), deferred :: area
end type shape
abstract interface
real function area_iface(self)
import :: shape
class(shape), intent(in) :: self
end function area_iface
end interface
type, extends(shape) :: circle
real :: r = 1.0
contains
procedure :: area => circle_area
end type circle
type, extends(shape) :: rectangle
real :: w = 1.0, h = 1.0
contains
procedure :: area => rect_area
end type rectangle
contains
real function circle_area(self)
class(circle), intent(in) :: self
circle_area = 3.1415926535 * self%r**2
end function circle_area
real function rect_area(self)
class(rectangle), intent(in) :: self
rect_area = self%w * self%h
end function rect_area
subroutine describe(s)
class(shape), intent(in) :: s
select type (s) ! pattern-match on the concrete type
type is (circle)
print '(a,f8.3)', 'circle, area = ', s%area()
type is (rectangle)
print '(a,f8.3)', 'rectangle, area = ', s%area()
class default
print '(a)', 'unknown shape'
end select
end subroutine describe
end module shapes
program shape_app
use shapes
implicit none
type(circle) :: c
type(rectangle) :: r
c = circle(r=2.0)
r = rectangle(w=3.0, h=4.0)
call describe(c) ! dispatches to circle_area
call describe(r) ! dispatches to rect_area
end program shape_app
Project 3 — Heat Diffusion in Three Parallel Styles
A 1-D heat diffusion step — u_new(i) = u(i) + dt*(u(i-1) - 2*u(i) + u(i+1)) — is the smallest realistic stencil. This project shows the same loop written three ways so you can compare them side by side: a plain serial loop, a do concurrent version, and an OpenMP directive version. Both parallel versions must produce the same answer, which is your correctness test. The study-project challenge extends it: replace the fixed 1000-cell line with a 2-D grid and decompose it with MPI halos, exactly as the mpi lesson described.
program heat_diffusion
use iso_fortran_env, only: real64
implicit none
integer, parameter :: n = 1000, steps = 200
real(real64) :: u(0:n+1), unew(0:n+1)
integer :: it, i
u = 0.0_real64
u(1) = 1.0_real64 ! a point heat source in the middle
do it = 1, steps
! serial version — the one every other version is checked against
do i = 1, n
unew(i) = u(i) + 0.2_real64 * (u(i-1) - 2.0_real64*u(i) + u(i+1))
end do
! parallel version 1: standard do concurrent
do concurrent (i = 1:n)
unew(i) = u(i) + 0.2_real64 * (u(i-1) - 2.0_real64*u(i) + u(i+1))
end do
! parallel version 2: OpenMP (compile with -fopenmp)
!$omp parallel do
do i = 1, n
unew(i) = u(i) + 0.2_real64 * (u(i-1) - 2.0_real64*u(i) + u(i+1))
end do
!$omp end parallel do
! advance the state once for all three flavours: same input, same output
unew(0) = unew(1); unew(n+1) = unew(n) ! insulated ends
u = unew
end do
print '(a,f10.6)', 'center after 200 steps: ', u(n/2)
end program heat_diffusion
Three answers must agree to machine precision — if they do not, one of the versions lost the no-alias guarantee or the reduction contract, and hunting the difference is the exercise. When they agree, add timing (the concurrent_sum demo's system_clock pattern) and compare serial vs OpenMP on your machine. That measured comparison is the entire HPC phase in miniature.