Skip to content

Fortran 101

Fortran 101 - control structure, where parameters are defined, some keywords e.g. private, intent, submodule, use module etc

Presenter: @edoyango (Edward Yang).

Date: 14/05/2026

Note

Today we'll be building a simple Fortran example program that uses some of the concepts that MOM6 is built with. Next week, we'll apply these ideas to some real MOM6 code. Our program today, takes a 3d array (first two indices represent the lateral domain, and the last represents the columns), and performs either a sum or max operation along the column, reducing the result to a 2d array that represents the lateral domain only. If you are comfortable with Python and would like to see today's end goal, the final version of the code is given in Python below.

Contributing to MOM6 can be extra daunting if you're not used to programming in Fortran. These notes aim to introduce Fortran to someone who might already be familiar with Python. And thankfully, most of the Fortran features exercised in MOM6 have a Python equivalent. Here, we won't be looking at MOM6 code directly, because the code itself is quite long -- even if the language features used aren't too complicated.

Python equivalent of the example program to be built
"""
Python code that emulates the example Fortran program.
It uses Python functions and classes to represent the equivalent Fortran
subroutines and derived types.
"""

import numpy as np

class Grid:
    def __init__(self, is_: int, js: int, ie: int, je: int, nz: int):
        self.is_ = is_
        self.js = js
        self.ie = ie
        self.je = je
        self.nz = nz

class ControlStructure:
    def __init__(self, initialized: bool = False, which_op: int = 1):
        self.initialized = initialized
        self.which_op = which_op

def do_something_along_column(cs: ControlStructure, g: Grid, arr1: np.ndarray) -> np.ndarray:
    if not cs.initialized:
        raise RuntimeError("Control structure not initialized!")

    if cs.which_op == 1:
        return _sum_along_column(g, arr1)
    if cs.which_op == 2:
        return _max_along_column(g, arr1)
    raise ValueError("Invalid operation provided! must be either 1 or 2")

def _sum_along_column(g: Grid, arr1: np.ndarray) -> np.ndarray:
    arr2 = arr1[:, :, 0].copy()
    for j in range(g.je - g.js + 1):
        for k in range(1, g.nz):
            for i in range(g.ie - g.is_ + 1):
                arr2[i, j] += arr1[i, j, k]
    return arr2

def _max_along_column(g: Grid, arr1: np.ndarray) -> np.ndarray:
    arr2 = arr1[:, :, 0].copy()
    for j in range(g.je - g.js + 1):
        for k in range(1, g.nz):
            for i in range(g.ie - g.is_ + 1):
                arr2[i, j] = max(arr2[i, j], arr1[i, j, k])
    return arr2

import numpy as np

from my_module import ControlStructure, Grid, do_something_along_column

if __name__ == "__main__":
    g = Grid(is_=1, js=2, ie=3, je=4, nz=5)
    cs = ControlStructure(initialized=True, which_op=1)

    input_array = np.ones((g.ie - g.is_ + 1, g.je - g.js + 1, g.nz))
    output_array = do_something_along_column(cs, g, input_array)

    print(np.sum(output_array))

Programs and modules

Most of the code in MOM6 is organised into "modules" which usually relate to a certain area of the ocean physics. For example, MOM_barotropic.F90 contains the barotropic solver code, and MOM_tracer_advect.F90 contains the tracer advection code and so on. Modules contains code that can be reused in other modules or "programs". Modules cannot be directly compiled and run, and so modules' code must be "used" from a program. The skeleton of this arrangement can look like:

module my_module ! my_module is the name of the module
  implicit none
end module my_module

program my_program ! my_program is the name of the program
  use my_module
  implicit none
end program my_program

The module and program's boundaries in the code are deliniated by program/module and end program/module couples. The program/module's name must follow the first program/module and matching end program/module (including the program/module name is optional, but it's a requirement to include the name in modules in).

implicit none is a "quirk" of Fortran. It says that all variables' type must be declared. Otherwise, the compiler can make an "educated guess" as to what the type it is, which can result in unexpected behaviour. Hence, it is best practice to always include implicit none in every module and program.

Subroutines and declaring variables

Programs can have runnable code. But as your codebase gets larger, it's likely that you will:

  1. want to organise the code in some way to make it easier to understand and maintain (e.g. group code related to certain physics together), and

  2. store code that is reused in multiple places. Subroutines help facilitate this. Subroutines are similar to Python functions except that subroutines don't return anything, and instead, they modify its arguments instead. Fortran does have functions also, but are used less frequently in MOM6.

Like programs and modules, subroutines are bounded by subroutine <name> and end subroutine <name>. The subroutine's arguments follow the name, followed by the type declaration of the arguments and local variables. Unlike Python, the types of all variables must be declared and cannot change. In the example below, the variable attributes commonly used in MOM6 are shown (type, dimension, and intent). Variables can be declared in any order, but a common convention (that MOM6 follows) is to declare the arguments first, followed by local variables.

The example above is trivial as both the program or module have no code. So let's create our first subroutine (Fortran comments are prefixed with !):

module my_module

  implicit none

! says that subroutines/functions are declared after
contains

  ! this is declaring the subroutine's signature
  subroutine sum_along_column(is, ie, js, je, nz, arr1, arr2)

    ! each argument's type must be declared. Here we have:
    ! * type (integer/real)
    !   * real is equivalent to np.float32. However, MOM6 opts to control the precision at compile
    !     time.
    ! * dimension aka shape. No dimension means that variable is scalar. dimension(...) means the
    !   variable is an array.
    !   * dimension(a:b) means that for the given index, only indices a to b are defined.
    !     This a unique feature of Fortran that is utilised heavily in MOM6.
    ! * intent
    !   * `in`: the variable will only be read
    !   * `out`: the variable will be written to
    !   * `inout`: not shown here, but means that the variable may be read and/or written to.
    ! once an argument has been declared, it can also be used to declare others
    integer,                           intent(in)  :: is, ie, js, je, nz
    real, dimension(is:ie, js:je, nz), intent(in)  :: arr1
    real, dimension(is:ie, js:je),     intent(out) :: arr2
    ! all local variables must also be declared
    integer :: i, j, k

    do j=js,je
      ! copies can be done using array slicing
      arr2(:,j) = arr1(:,j,1) ! initialize the sum along columns
      ! MOM6 keeps nested loops on a single line, dilineated by colons.
      do k=2,nz ; do i=is,ie
        arr2(i,j) = arr2(i,j) + arr1(i,j,k) ! do the sum along columns
      enddo ; enddo
    enddo

  end subroutine sum_along_column

end module my_module

Note that subroutines are invoked with call <subroutine>(...)

Array copies

Like with Python NumPy, arrays' contents can be copied between each other. If the arrays are of the same shape, they can be copied with specifying array indices (a = b), or you may specify which slices to copy e.g. a(1:10) = b(1:10), or a(:, 1) = b(:) etc. Noting that Fortran accesses array elements/slices using round brackets () instead of square brackets [] common in other languages. It is also worth noting that array assignments/copies copies the array's values (different from Python where a = b means something different from a[:] = b[:]).

Loops

Loops are dilineated by do variable=start,end,step and enddo - which is like for variable in range(start,end+1,step): in Python. The main difference between fortran loop ranges and Python ranges is that the end is included in the range.

You may have also noticed that the loop ordering is a bit strange - where the outer loop iterates of the middle index, with the outer index in the middle, and the inner-most loop iterating over the first. This is quite common in MOM6!

Derived types

Derived types are similar to Python classes in that they can be instantiated and group related information. Like Python classes, Fortran derived types has a name and attributes (or members). One of the key derived types in MOM6 is the ocean_grid_type which describe the grid extents (including the computational and halo extents). It also stores other grid information like lateral dimensions of the columns, masking etc. We can create a simple version of the grid type and use it in our subroutine:

module my_module

  implicit none

  ! grid type to hold grid bounds                     Python analogue:
  type grid_type                                      ! class gridType:
    integer :: is, js   ! starting indices            !     def __init__(self, is, js, ie, je, nz):
    integer :: ie, je   ! ending indices              !         self.is = is ; self.js = js
    integer :: nz       ! number of vertical layers   !         self.ie = ie ; self.je = je
  end type grid_type                                  !         self.nz = nz

contains

  ! we can replace our grid indices with a grid_type
  subroutine sum_along_column(g, arr1, arr2)
    ! We can still use the grid_type's members to define subsequent variables
    ! below, `g` is an instance of the `grid_type`. This `g`'s members are being accessed with `%`,
    ! which in python would be `g.is`. `g`'s members are then used to size the input/output arrays.
    type(grid_type),                             intent(in)  :: g    ! g is an instance of grid_type
    real, dimension(g%is:g%ie, g%js:g%je, g%nz), intent(in)  :: arr1
    real, dimension(g%is:g%ie, g%js:g%je),       intent(out) :: arr2
    integer :: i, j, k

    do j=g%js,g%je
      arr2(:,j) = arr1(:,j,1) ! initialize the sum along columns
      do k=2,g%nz ; do i=g%is,g%ie
        arr2(i,j) = arr2(i,j) + arr1(i,j,k) ! do the sum along columns
      enddo ; enddo
    enddo

  end subroutine sum_along_column

end module my_module

Let's also introduce another pattern used in MOM6: the "control structure". Each module will have its own control structure that mostly stores information to control MOM6's behaviour e.g. which algorithm to use or whether a certain physics is turned on or not. In our simple example, the control structure will simply control whether to do a sum or max along columns.

module my_module

  implicit none

  private ! this says that by default, contents of this module aren't accessible.

  !< grid type to hold grid bounds
  type grid_type
    integer :: is, js   !< starting indices
    integer :: ie, je   !< ending indices
    integer :: nz       !< number of vertical layers
  end type grid_type

  !< control structure
  type control_structure_type
    logical :: initialized = .false. !< whether the control structure is initialized - defaults to .false.
    integer :: which_op = 1          !< which operation to do - default is 1 (sum)
  end type control_structure_type

  ! Explicitly say which things can be used - do_sum/max_along_column cannot be directly used
  public :: grid_type, control_structure_type, do_something_along_column

contains

  !< this subroutine either does a max or sum along columns
  subroutine do_something_along_column(cs, g, arr1, arr2)
    type(control_structure_type),                intent(in)  :: cs   !< control structure
    type(grid_type),                             intent(in)  :: g    !< grid type
    real, dimension(g%is:g%ie, g%js:g%je, g%nz), intent(in)  :: arr1 !< input array
    real, dimension(g%is:g%ie, g%js:g%je),       intent(out) :: arr2 !< output array

    if (.not.CS%initialized) error stop "Control structure not initialized!"

    if (CS%which_op == 1) then
      call sum_along_column(g, arr1, arr2)
    elseif (CS%which_op == 2) then
      call max_along_column(g, arr1, arr2)
    else
      error stop "Invalid operation provided! must be either 1 or 2"
    endif

  end subroutine do_something_along_column

  !< performs a sum reduction along columns
  subroutine sum_along_column(g, arr1, arr2)
    type(grid_type),                             intent(in)  :: g    !< grid type
    real, dimension(g%is:g%ie, g%js:g%je, g%nz), intent(in)  :: arr1 !< input array
    real, dimension(g%is:g%ie, g%js:g%je),       intent(out) :: arr2 !< output array
    integer :: i, j, k ! loop indices

    do j=g%js,g%je
      arr2(:,j) = arr1(:,j,1) ! initialize the sum along columns
      do k=2,g%nz ; do i=g%is,g%ie
        arr2(i,j) = arr2(i,j) + arr1(i,j,k) ! do the sum along columns
      enddo ; enddo
    enddo

  end subroutine sum_along_column

  !< performs a max reduction along columns
  subroutine max_along_column(g, arr1, arr2)
    type(grid_type),                             intent(in)  :: g    !< grid type
    real, dimension(g%is:g%ie, g%js:g%je, g%nz), intent(in)  :: arr1 !< input array
    real, dimension(g%is:g%ie, g%js:g%je),       intent(out) :: arr2 !< output array
    integer :: i, j, k ! loop indices

    do j=g%js,g%je
      arr2(:,j) = arr1(:,j,1) ! initialize the sum along columns
      do k=2,g%nz ; do i=g%is,g%ie
        arr2(i,j) = max(arr2(i,j), arr1(i,j,k)) ! do the max along columns
      enddo ; enddo
    enddo

  end subroutine max_along_column

end module my_module

The module is much larger now - the control structure type has been added and two subroutines have also been added. max_along_column is almost identical to sum_along_column except that it does a max operation instead of +.

If statements

In the module, we've also added a 3-branch if statement. If statements look quite similar to Python's except that the evaluation must be put it into brackets and is followe by then: i.e. if (statement) then. Otherwise the semantics are identical.

Module public and private

To hide details, MOM6 likes to leverage "public" and "private" statements in modules. MOM6 modules will declare everything as private by default (by having an unqualified private clause), and then explicitly list the objects that should be accessible outside of the module with public :: list, of, objects, and, procedures. Private things are visible to other things within the module, but not outside.

Documenting comments

MOM6 uses Doxygen comments that automatically generate documentation for the code. These type of comments are sentineled with !< (as opposed to only !). Procedures (functions and subroutines), types and members, and arguments must be documented.

Using modules

Let's finish our program and use the module code! The program will be very simple -- uses hardcoded values to initialize everything.

program my_program
  ! specify which things we want from the module
  ! Python analogue: from my_module import grid_type, control_structure_type, do_something_along_column
  ! note that because of the privacy in the module, `sum_along_column` is not included in the
  ! `public :: ...` list. This means that importing or calling `sum_along_column` from this
  ! program would fail. Adding `sum_along_column` to the `public :: ...` list would make it
  ! available here.
  ! Note that excluding `only` would lead to everything public being available. However, MOM6
  ! ensures to make imports explicit. This is, in general, good practice as it makes it easier to
  ! identify where something used inside the program/module comes from.
  use my_module, only: grid_type, control_structure_type, do_something_along_column
  implicit none
  ! variable declaration
  type(grid_type) :: g
  type(control_structure_type) :: cs
  ! use allocatable arrays for dynamic array sizes in the program
  real, allocatable :: input_array(:, :, :), output_array(:, :)

  ! derived type instances don't have to be constructed (the construction is implied in the declaration)
  ! but they can be initialized with a default type constructor, or simply setting the members.
  g = grid_type(is=1, js=2, ie=3, je=4, nz=5)
  cs%initialized = .true.
  cs%which_op = 1

  allocate( &
    input_array(g%is:g%ie, g%js:g%je, g%nz), &
    output_array(g%is:g%ie, g%is:g%ie) &
  )

  input_array = 1.0

  call do_something_along_column(cs, g, input_array, output_array)

  ! print the sum of the array to the terminal
  write(*, *) sum(output_array)

end program my_program

A program itself looks quite similar the subroutines above, except there aren't any arguments. Near the top we use the module and select which things we want. This convention isn't mandatory, but is closely followed by MOM6.

Allocatable arrays

Unlike subroutines, programs cannot get array sizes by simply passing them in. So, often arrays are made given the "allocatable" attribute, which lets the program "allocate" the array based on some user input or similar. Here, we allocate the arrays' based on hardcoded values, but that could be changed to use CLI arguments or something.

Compiling and running the example

To compile the program, you can put the above module and program into the same file, say example.f90, and compile it with gfortran example.f90 -o example.x. You can then execute it with ./example.x. You should get 45 with many zeros printed!

MOM6 is a much more complex codebase with many more dependencies and consequently more complex build system.