Study Projects

Three programs that pull the whole track together, written with the standard library only (project 3 optionally with OpenMP/coarray flags). Type each one yourself — do not copy and paste — then break it deliberately and repair it. Each is complete as shown and takes about an hour to write and understand; the challenge at the end of each is the version you would meet in real work.

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.