Content from Connecting to ARCHER2 and transferring data


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What can I expect from this course?
  • How will the course work and how will I get help?
  • How can I give feedback to improve the course?
  • How can I access ARCHER2 interactively?

Objectives

  • Understand how this course works, how I can get help and how I can give feedback.
  • Understand how to connect to ARCHER2.

Purpose


Attendees of this course will get access to the ARCHER2 HPC facility. You will have the ability to request an account and to log in to ARCHER2 before the course begins. If you are not able to log in, you can come to this pre-session where the instructors will help make sure you can login to ARCHER2.

Connecting using SSH


The ARCHER2 login address is

BASH

login.archer2.ac.uk

Access to ARCHER2 is via SSH using both a time-based one time password (TOTP) and a passphrase-protected SSH key pair.

Passwords and password policy


When you first get an ARCHER2 account, you will get a single-use password from the SAFE which you will be asked to change to a password of your choice. Your chosen password must have the required complexity as specified in the ARCHER2 Password Policy.

The password policy has been chosen to allow users to use both complex, shorter passwords and long, but comparatively simple passwords. For example, passwords in the style of both LA10!£lsty and horsebatterystaple would be supported.

SSH keys


As well as password access, users are required to add the public part of an SSH key pair to access ARCHER2. The public part of the key pair is associated with your account using the SAFE web interface. See the ARCHER2 User and Best Practice Guide for information on how to create SSH key pairs and associate them with your account:

TOTP/MFA


ARCHER2 accounts are now required to use timed one-time passwords (TOTP), as part of a multi-factor authorisation (MFA) system. Instructions on how to add MFA authentication to a machine account on SAFE can be found [here][safe-machine-mfa].

Data transfer services: scp, rsync, Globus Online


ARCHER2 supports a number of different data transfer mechanisms. The one you choose depends on the amount and structure of the data you want to transfer and where you want to transfer the data to. The two main options are:

  • scp: The standard way to transfer small to medium amounts of data off ARCHER2 to any other location.
  • rsync: Used if you need to keep small to medium datasets synchronised between two different locations

More information on data transfer mechanisms can be found in the ARCHER2 User and Best Practice Guide:

  • [Data management and transfer][archer2-data].

Installation


For details of how to log into an ARCHER2 account, see https://docs.archer2.ac.uk/quick-start/quickstart-users/

Check out the git repository to your laptop or ARCHER2 account.

BASH

$ git clone https://github.com/EPCCed/2026-02-18-MO-Fortran-intro.git
$ cd 2026-02-18-MO-Fortran-intro

Within the repository, the code we will use is located in the episodes/files/exercises directory. The default Fortran compiler on ARCHER2 is the Cray Fortran compiler invoked using ftn. For example,

BASH

$ cd episodes/files/exercises/01-hello-world
$ ftn example1.f90

should generate an executable with the default name a.out. If you are working on another machine, you should invoke the correct compiler e.g. gfortran.

Each section of the course is associated with a different directory, each of which contains a number of example programs and exercise templates. Answers to exercises generally re-appear as templates to later exercises. Miscellaneous solutions to some of the larger problems also appear in solutions subdirectories.

Not all the examples compile. Some have deliberate errors which will be discussed as part of the course.

Course structure and method


Rather than having separate lectures and practical sessions, this course is taught following The Carpentries methodology, where we all work together through material learning key skills and information throughout the course. Typically, this follows the method of the instructor demonstrating and then the attendees doing along with the instructor. There are some larger exercises we will start working on at the end of day one.

There are many helpers available to assist you and to answer any questions you may have as we work through the material together. You should also feel free to ask questions of the instructor whenever you like. The instructor will also provide many opportunities to pause and ask questions.

Key Points
  • We should all understand and follow the ARCHER2 Code of Conduct to ensure this course is conducted in the best teaching environment.
  • ARCHER2’s login address is login.archer2.ac.uk.
  • You have to change the default text password the first time you log in
  • MFA is mandatory in ARCHER2

Content from Hello World


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What is the structure of a Fortran program?
  • How do I print output from the program to the terminal?

Objectives

  • Write, compile and run a simple Fortran program.
  • Understand how to use the print and write statements.`
  • Load the iso_fortran_env module with a use statement.

First example


A very simple program might be:

FORTRAN

program example1

  ! An example program prints "Hello World" to the screen

  print * , "Hello World"

end program example1

Formally, a Fortran program consists of one or more lines made up of Fortran statements. Line breaks are significant (e.g., there are no semi-colons ; required here).

Comments are introduced with an exclamation mark !, and may trail other statements.

The program statement is roughly doing the equivalent job of main() in C/C++. However, note there is not (and must not be) a return statement.

Exercise (1 minute)

Challenge

Compile your first program

Check now you can compile and run the first example program example1.f90. What output do you get when you run it?

Compile and run the program on ARCHER2 as follows:

BASH

ftn example1.f90
./a.out

which will give the following output:

OUTPUT

 Hello world

Formal description

FORTRAN

  [ program [program-name] ]
     [ specification-part ]
     [ executable-part ]
  [ contains
     internal-subprogram-part ]
  end [program-name]

Optional components are represented with square brackets [...]. It follows that the shortest standard-conforming program will be (see example2.f90):

FORTRAN

end

If the program-name is present, it must be at both the beginning and the end, and must be the same in both places.

We will return to the contains statement in the context of modules.


In general

FORTRAN

  print format [ , output-item-list ]

where the format is a format specifier (discussed later) and the output-item-list is a comma-separated list of values/variables to be printed to the standard output.

If the format is a * (a so-called free-format) the implementation is allowed to apply a default format for a given type of item.

Alternative


Consider the following program (available as example3.f90):

FORTRAN

program example3

  use iso_fortran_env, only : output_unit

  write (output_unit, *) "Hello ", "world"

end program example3

This example shows a more general way to provide some output. Here we are also going to employ the use statement to import a symbol from the (intrinsic) module iso_fortran_env. The symbol is output_unit which identifies the default standard output (cf. stdout).

use statement

Formally,

FORTRAN

  use [[ , module-nature] ::] module-name [ , only : [only-list]]

If module-nature is present, it must be either intrinsic or non_intrinsic. The implementation must provide certain intrinsic modules such iso_fortran_env. You can write modules of your own, as we will see later on.

There is no formal namespace mechanism in Fortran (cf. C++), so restrictions on which symbols are visible can be made via an optional only-list. If there is no only-list then all the public symbols from module-name will be visible.

write statement

Formally,

FORTRAN

  write (io-control-spec-list) [output-item-list]

where the output-item-list is a comma separated list of items to be output. The io-control-spec-list has a large number of potential arguments (again comma separated). For formatted output, these must include at least a unit number and a format:

FORTRAN

  write ([unit = ] io-unit, [fmt = ] format) [output-item-list]

where the io-unit is a valid integer unit number, and the format is a format-specifier (as for print).

Examples are

FORTRAN

  write (unit = output_unit, fmt = *)
  write (output_unit, *)
  write (*, *)

C programmers looking for a new-line like symbol will notice that none has appeared so far. The default situation is that both print and write generate a new-line automatically. The * symbol in the context of io-unit is a default output unit (usually the screen).

We will return to the write statement and format-specifiers in more detail in the context of i/o to external files.

Some comments on style


Modern Fortran is not case sensitive. Older versions required capitals, a style which has persisted to the present day in some places. So you may see things such as

FORTRAN

PROGRAM example1

  PRINT *, "Hello World"

END PROGRAM example1

As modern etiquette tends to regard capitals as shouting, this can cause some tension.

The compiler will accept mixed case. An additional tool would be required to enforce a particular style.

This course will prefer an all lower-case style.

Exercise (2 minutes)

Challenge

Check symbol values

Write a program which prints out the actual values of the symbols output_unit, error_unit, and input_unit (all from iso_fortran_env) to the screen.

If you haven’t used the only clause in your use iso_fortran_env, add it now. What happens to the results if you miss out one of the symbols referenced from the only clause? This behaviour will be explained in the following section.

Bonus: check the values using both the Cray and GCC compilers. On ARCHER2 both are invoked with the ftn compiler wrapper. Which compiler is actually used depends on the PrgEnv- module loaded.

Make sure to use the module, then use print * statements to write the values of the three symbols to the screen. Sample solution code is available in exercise1.f90.

Running with the default Cray compiler gives the output:

OUTPUT

 output_unit is:  101
 error_unit is:   102
 input_unit is:   100

while using gfortran from GCC gives:

OUTPUT

 output_unit is:            6
 error_unit is:             0
 input_unit is:             5
Key Points
  • A Fortran program begins with a program statement and ends with an end program statement.
  • A print statement is a simple way to write output to the terminal.
  • A write statement provides more control including ways to write to file.
  • Modules can be loaded with the use statement.
  • The iso_fortran_env module provides symbols which can be used to help read from and write to the terminal.

Content from Break


Last updated on 2026-02-21 | Edit this page

Comfort break.

Content from Variables


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What are some basic types of numeric variables in Fortran?
  • How are variables declared and defined?
  • How do I create constants?

Objectives

  • Understand how to create variables and assign values to them.
  • Specify the representation in memory of variables independently of the implementation.
  • Store constant values.

Numeric variables of intrinsic type


The following program declares a variable with each of the three intrinsic numeric types, and provides an initial value in each case.

FORTRAN

program example1

   implicit none

   integer :: i = 1            ! A default integer kind
   real    :: a = 2.0          ! A default floating point kind
   complex :: z = (0.0, 1.0)   ! A complex kind with (real-part, imag-part)

end program example1

Initial values are optional. If a declaration does not specify an initial value, the variable is said to be undefined.

Variable names

The valid Fortran character set for names is a-z, A-Z, 0-9 and the underscore _. Valid names must begin with a character. The maximum length of a name is 63 characters (F2003) with no spaces allowed. This includes names for programs, and names for variables.

Other special characters recognised by Fortran are:

Character Name Character Name
Blank ; Semi-colon
= Equals ! Exclamation mark
+ Plus " Double quote
- Minus % Percent
* Asterick & Ampersand
/ Slash ~ Tilde
\ Backslash < Less than
( Left parenthesis > Greater than
) Right parenthesis ? Question mark
[ Left square bracket ' Apostrophe
] Right square bracket ` Grave accent
{ Left curly brace ^ Circumflex
} Right curly brace | Pipe
, Comma $ Dollar sign
. Decimal point # Hash
: Colon @ At

Other characters may appear in comments.

implicit statement

The implicit statement defines a type for variable names not explicitly declared. So, the default situation can be represented by

FORTRAN

  implicit integer (i-n), real (a-h, o-z)

that is, variables with names beginning with letters i-n are implicitly of type integer, while anything else is of type real (unless explicitly declared otherwise).

By modern standards this is tantamount to recklessness. The general solution to prevent errors involving undeclared variables (usually arising from typos) is to declare that no names have implicit type via

FORTRAN

implicit none

In this case, all variable names must be declared explicitly before they are referenced.

However, it is still common to see the idiom that variables beginning with i-n are integers and so on, albeit declared explicitly.

Callout

Using implicit none

To reiterate – all modern Fortran code really should use implicit none. Failing to do so will almost certainly lead to bugs and hence time spent debugging!

Exercise (1 minute)

Challenge

The importance of being explicit

Compile and run the accompanying program exercise1.f90. What’s the problem and how should we avoid it? Check the compiler can trap the problem.

The issue is that the variable we declare is initialise is ll, but the variable we print is l1. Look closely at these names: the latter mistakenly uses a digit one 1 where it should be the letter l. When we print the hitherto unmentioned l1, the compiler implicitly creates it as a new integer.

Add implicit none to the top of the code

FORTRAN

program exercise1

  implicit none

  integer :: ll = 1

  print *, "The value of ll is: ", l1

end program exercise1

which then produces an error at compile time, e.g. with the Cray compiler:

OUTPUT


  print *, "The value of ll is: ", l1
                                   ^
ftn-113 ftn: ERROR EXERCISE1, File = exercise1.f90, Line = 7, Column = 36
  IMPLICIT NONE is specified in the local scope, therefore an explicit type must be specified for data object "L1".

Cray Fortran : Version 15.0.0 (20221026200610_324a8e7de6a18594c06a0ee5d8c0eda2109c6ac6)
Cray Fortran : Compile time:  0.0024 seconds
Cray Fortran : 9 source lines
Cray Fortran : 1 errors, 0 warnings, 0 other messages, 0 ansi
Cray Fortran : "explain ftn-message number" gives more information about each message.

kind of type

The declarations above give us variables of some (implementation-defined) default type (typically 4-byte integers, 4-byte reals). A mechanism to control the exact kind, or representation, is provided. For example (see example2.f90),

FORTRAN

  use iso_fortran_env, only : int64, real64
  implicit none

  integer (kind = int64)   :: i = 100
  real    (kind = real64)  :: a = 1.0
  complex (kind = real64)  :: z = (0.0, 1.0)

Here we use kind type parameters int64 and real64 to specify that we require 64-bit integers and 64-bit floating point storage, respectively.

A standard conforming Fortran implementation must provide at least one integer precision with a decimal exponent range of at least 18 (experience suggests all provide at least two kinds). Note that Fortran does not have any equivalent of C/C++ unsigned integer types.

An implementation must provide at least two real precisions, one default precision, and one extended precision (sometimes referenced as double precision).

Formally, the numeric types are introduced

FORTRAN

  numeric-type-spec [kind-selector] ...

where the numeric-type-spec is one of integer, real, or complex. The optional kind selector is

FORTRAN

  ([kind = ] scalar-integer-initialisation-expr)

The upshot of this is that the syntax of declarations is quite elastic, and you may see a number of different styles. A reasonable form of concise declaration with an explicit kind type parameter is:

FORTRAN

  integer (int32)  :: i
  real    (real32) :: a
  complex (real32) :: z

Example (2 minutes)

Take a moment to compare the output of the programs example1.f90 and example2.f90, which declare and initialise a variable of each type and print out their values to the screen.

Numeric literal constants

One may (optionally) specify the kind of an integer literal by appending the kind type parameter with an underscore _, e.g.:

FORTRAN

123
+123
-123
12345678910_int64

Floating point literal constants can take a number of forms. Examples are:

FORTRAN

-3.14
.314
1.0e0             ! default precision
1.0d0             ! extended (double) precision
3.14_real128      ! may be available
3.14e-1           ! Scientific notation
3.14d+1           ! Scientific notation extended precision

Complex literals are constructed with real and imaginary parts, with each part real.

FORTRAN

(0.0, 1.0)        ! square root of -1

Parameters

Suppose we did not want to hardwire our kind type parameters throughout the code. Consider:

FORTRAN

program example3

  implicit none

  integer, parameter :: my_e_k = kind(1.e0)
  integer, parameter :: my_d_k = kind(1.d0)

  real (my_e_k) :: a
  real (my_d_k) :: b

  ! ...

end program example3

Here we have introduced an integer with the parameter attribute. This is an instruction to the compiler that the associated name should be a constant (similar to a const declaration in C). Any subsequent attempt to assign a value to a parameter will result in a compile time error.

The value specified in the parameter declaration must be a constant expression (which the compiler may be able to evaluate at compile time). Here we have made use of the intrinsic function kind(). The kind() function returns an integer which is the kind type parameter of the argument. In this context a constant expression is one in which all parts are intrinsic.

Note we have not included anything or use’d anything to make this kind() function available. It is an intrinsic function and part of the language itself.

Using a parameter provides a degree of abstraction for the real data type. Other examples might include:

FORTRAN

  integer, parameter :: nmax = 32              ! A constant
  real,    parameter :: pi = 4.0*atan(1.e0)    ! A well-known constant
  complex, parameter :: zi = (0.0, 1.0)        ! square root of -1

The intrinsic function atan() returns the inverse tangent (as a value in radians) of the argument.

Exercise (2 minutes)

Discussion

Challenge

Consider further the accompanying example3.f90, where we have introduced another intrinsic function storage_size() (roughly equivalent to C’s sizeof() although it returns a size in bits rather than bytes). Run the program and check the actual values of the kind type parameters and associated storage sizes. Speculate on the portability of a program declaring, e.g.:

FORTRAN

  integer (4) :: i32
  integer (8) :: i64

Exercise (2 minutes)

Challenge

Parameters

Consider the accompanying exercise2.f90. Check the compiler error emitted and remove the offending line.

The problem is that nmax is created with the parameter attribute and given a value of 32, but that later on an attempt is made to assign a new value of 33.

FORTRAN

nmax = 33

to resolve the issue.

Arithmetic operations on numeric types

For all intrinsic numeric types, standard arithmetic operations, addition (+), subtraction (-), multplication (*) and division (/) are defined, along with exponents (**) and negation (-).

In order of increasing precedence these are -, +, /, *, and ** (otherwise left-to-right). In particular

FORTRAN

   a = b*c**2    ! is evaluated as b*(c**2)
   a = b*c*d     ! evaluated left-to-right (b*c)*d

Use parentheses to avoid ambiguity if necessary.

A large number of intrinsic functions exist for basic mathematical operations and are relevant for all numeric types. A simple example is sqrt(). Some are specific to particular types, e.g., conjg() for complex conjugate.

Broadly, assignments featuring different data types will cause promotion to a “wider” type or cause truncation to a “narrower” type. If one wants to be explicit, the equivalent of the cast mechanism in C is via intrinsic functions, e.g.,

FORTRAN

   integer          :: i = 1
   complex (real64) :: z = (1.0, 1.0)
   real    (real64) :: a, b

   a = real(i, real64)          ! a should be 1.d0
   a = real(z, real64)          ! imaginary part is ignored

   b = real(i)                  ! return default real kind

   z = cmplx(a, b)              ! (sic) Form complex number from two reals

The second argument of the real() function is optional, and specifies the kind type parameter of the desired result. If the optional argument is not present, then a real value of the default kind is returned.

Note here the token real has been used in two different contexts: as a statement in the declaration of variables a and b, and as a function. There is not quite the same concept of “reserved words” (cf C/C++); lines are split into tokens based on spaces, and tokens are parsed in context.

Complex real and imaginary parts

The real and imaginary parts of a complex variable may be accessed

FORTRAN

  complex :: z

  z%re = 0.0     ! real part
  z%im = 1.0     ! imaginary part

where the % symbol is referred to as the component selector. The real and imaginary parts are also available via the real() and aimag() intrinsic functions, respectively.

Exercise (2 minutes)

Challenge

Using sqrt()

By using variables of complex type, check that you can use the intrinsic function srqt() to confirm that the square root of -1 is i. What happens if you try to try to take the square root of a negative value stored as a real variable?

If you call sqrt() on a complex variable z%re = -1.0 and z%im = 0.0 then a result will be returned with a real part of 0 and an imaginary part of 1.

If you call sqrt() on a real number -1 then the compiler will exit with an error and a message explaining why this is not valid. If sqrt() is called on a variable with the value -1 it will return NaN.

Exercise (5 minutes)

Challenge

Calculating pi

The accompanying template exercise3.f90 provides instructions for an exercise which involves the approximation to the constant pi computed via a Gauss-Legendre algorithm. Some background can be found at https://en.wikipedia.org/wiki/Gauss-Legendre_algorithm.

Use the instructions in the template to calculate an estimate of pi by creating an appropriate parameter to store a kind, declaring the appropriate variables, then performing the calculation and printing the values of pi.

A solution to this problem appears as the template for the first exercise in the episode on loops.

Exercise (5 minutes)

Challenge

Calculating the conductance in a narrow channel

A second exercise in a similar vein looks at an approximation to the conductance of a rectangular channel subject to a constant flow. Instructions are in exercise4.f90.

As with the previous exercise, create a kind, the variables, then perform the necessary arithmetic to calculate C_1.

A solution to this problem appears as the template for the second exercise in the episode on loops.

Key Points
  • Fortran provides the intrinsic numeric types of integer, real and complex.
  • Without a kind, these types are implementation-defined. Use kind to specify the representation of variables.
  • The iso_fortran_env intrinsic module provides standard kinds such as real32 and real64.
  • Always use implicit none to prevent the accidental implicit declaration of new variables.
  • Use the parameter attribute to make the value associated with the name constant.

Content from Logical and conditionals, character variables


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What other types of variable does Fortran provide?
  • How can I direct a program’s flow down different paths?
  • Is it possible to store characters in a variable?

Objectives

  • Understand how to use logical variables.
  • Be able to control the flow of a program with if statements.
  • Write conditional expressions using logical and relational operators.
  • Be able to use a case construct.
  • Learn how to declare character variables.

Logical variables


Fortran has a logical type (cf. Boolean type in C); there are two relevant literal values, illustrated here:

FORTRAN

  logical :: switch0 = .false.
  logical :: switch1 = .true.

It is possible to specify a kind type parameter for logical kinds, but you don’t see it very often. The default logical kind has kind type parameter kind(.false.).

Logical operators and expressions

Standard logical operators .or., .and. and .not. are available. The precedence is illustrated by, e.g.,

FORTRAN

  q = i .or. j .and. .not. k    ! evaluated as i .or. (j .and. (.not. k))

Again, use parentheses to avoid ambiguity, or to add clarity.

Remember that .or. evaluates to .true. if either or both its operands are .true., while .and. requires both to be .true. in order to evaluate to .true.. The .not. operator inverts the value of its operand.

Relational operators

To form logical expressions from numeric or other expressions, we require relational operators. The are two forms in Fortran:

Relation Operator Older form For
Less than < .lt. integer real
Less than or equal to <= .le. integer real
Greater than > .gt. integer real
Greater than or equal to >= .ge. integer real
Equal to == .eq. integer real complex
Not equal to /= .neq. integer real complex

If you are coming from C/C++ don’t be tempted by != for inequality.

Logical equivalence

Equivalence between two logical expressions or variables is established via the logical operators .eqv. and .neqv.; do not be tempted to use == for logical equality, although some compilers may let you get away with it.

Logic and flow of control


if construct

Conditional statements are provided by the if construct, formally:

FORTRAN

[if-name:]  if (logical-expression) then
              block
            [ else if (logical-expression) then
              block ] ...
            [ else
              block ]
            end if [if-name]

There may be zero or more else if blocks, but at most one else block. At most one block is executed. For example:

FORTRAN

  if (i < j) then
    print *, "The smaller is: i ", i
  else if (i > j) then
    print *, "The larger is:  i ", i
  else
    print *, "i,j are the same: ", i
  end if

A single clause if statement is also available, for example:

FORTRAN

  if (a >= 0.0) b = sqrt(a)

Example 1 (3 minutes)

Challenge

Using relational operators

The file example1.f90 contains a version of the code above. Check it works as expected. Can you replace the relational operators to use the older form given in the table above?

You should be able to compile the code and run it to obtain the output:

OUTPUT

 The smaller is: i  1

To use the older forms of the operators > and <, change them respectively to .gt. and .lt.:

FORTRAN

  condition1 = (i .lt. j)
  condition2 = (i .gt. j)

Construct names

A number of control constructs in Fortran include the option to specify names. This can be useful when highly nested structures are present and one needs to refer unambiguously to one or other. A construct name follows the same rules as for variable names.

A if construct with a name must have the matching name with the end if.

For example

FORTRAN

highly_nested_if_construct: if (a < b) then
                              ! ... structured block ...
                            end if highly_nested_if_construct

As a matter of style, a leading name can be obtrusive, so one can put it on a separate line using the continuation character &, e.g.,

FORTRAN

outer_if: &
  if (a < b) then
     ! ... structured block ...
  endif outer_if

The standard maximum line length in Fortran is 132 characters. Longer lines can be broken with the continuation character & to a maximum of 255 continuation lines (F2003).

Finally, note the use of endif without a space is not a typo; both forms with and without a space are acceptable (this is true of a number of Fortran statements). It’s probably preferable to stick to “end if” (at least be consistent).

case construct

This is an analogue of the C switch facility, and can be useful for actions conditional on a range or set of distinct values. Formally,

FORTRAN

[case-name:]  select case (case-expression)
                [ case (case-value-range-list)
		    block ] ...
		[ case default
		    block ]
              end select [case-name]

The case-expression must be a scalar integer, logical, or character expression. The case-value-range-list is a comma-separated list of either individual values, or ranges.

For example:

FORTRAN

   integer :: mycard = 1         ! Playing cards 1--13

   select case (mycard)
     case (1)
       ! Action for ace ...
     case (2:10)
       ! Action for other card ...
     case (11, 12, 13)
       ! Court card ...
     case default
       ! error...?
   end select

A range may be open-ended (e.g., 2: or :10). Note there is no break-like statement as in the C switch; only the relevant case block is executed.

Character variables


Character variables hold zero or more characters. Some examples are:

FORTRAN

program example2

  implicit none
  character (len = *), parameter :: string1 = "don't"   ! 5 characters
  character (len = 5)            :: string2 = "Don""t"  ! 5 characters
  character (len = 6)            :: string3 = 'don''t'  ! 5 characters + blank

end program example2

The implementation must provide at least one type of character storage, with the default kind being kind('A'). In practice, kind type parameters are not often specified. However, there should be a len specifier.

There is, again, a certain elasticity in the form of the declaration, so you may see slightly different things.

Strings may be concatenated with the string concatenation operator //; a single character, or a subset of characters, can be extracted via use of an array-index like notation e.g., string(1:2).

We will return to character variables in more detail when we consider strings in a later section.

Exercise (1 minute)


Challenge

Character len

Compile and check the output of example2.f90 to see the result of the examples above. What happens if you change one of the len specifications to be too short?

Without modifications, the code should produce the following output:

OUTPUT

 string1: don't
 string2: Don"t
 string3: don't
 Catenated:       don't Don"tdon't
 Substring:       Do
 kind('A'):        1

If we make string3 too short, e.g.:

FORTRAN

  character (len = 2)            :: string3 = 'don''t'

then the variable will truncate at that length:

OUTPUT

 string1: don't
 string2: Don"t
 string3: do
 Catenated:       doDon"tdon't
 Substring:       Do
 kind('A'):        1

Exercise (5 minutes)


Challenge

Solving a quadratic equation

Write a program which uses real data types to compute the two solutions to the quadratic equation:

\(ax^2 + bx + c = 0\)

for given values of a, b, and c. See Wikipedia’s page for some background. A template exercise1.f90 provides some instructions.

A solution to this exercise appears as a template for the first exercise in the episode on array expressions

Key Points
  • Fortran provides two non-numeric intrinsic data types: logical and character.
  • A program’s flow can be directed using the results of logical operations used in conjunction with if and case constructs.

Content from Lunch


Last updated on 2026-02-21 | Edit this page

Break for lunch.

Content from Loops and loop control


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How can I make the program do work repeatedly?
  • How can I have the program only do as much work as is required?

Objectives

  • Learn how to use a do construct to execute a single block of code many times.
  • See how to have a do construct skip a loop with cycle or end early with exit.
  • Learn how to use loop control variables with do constructs.

Uncontrolled do construct


A simple iteration is provided by the do statement. For example, …

FORTRAN

  do
    ! ... go around for ever ...
  end do

A slightly more useful version requires some control (see example1.f90):

FORTRAN

   integer :: i = 0
   do
     i = i + 1
     if (mod(i, 2) == 0) cycle    ! go to the next iteration
     if (i >= 10) exit            ! exit loop completely
     ! ... some computation ...
   end do

   ! ... control continues here after exit ...

Loop constructs may be nested, and may also be named (see example2.f90):

FORTRAN

  some_outer_loop: &
  do
    some_inner_loop: &
    do
      if (i >= 10) exit some_outer_loop  ! exit belongs to outer loop
      ! ... continues ...
    end do some_inner_loop
  end do some_outer_loop

If the control statements cycle or exit do not have a label, they belong to the innermost construct in which they appear.

Loop control


More typically, one encounters controlled iterations with an integer loop control variable. E.g.,

FORTRAN

  integer :: i
  do i = 1, 10, 2
    ! ... perform some computation ...
  end do

Formally, we have

FORTRAN

[do-name:] do [loop-control]
             do-block
	   end do [do-name]

with loop-control of the form:

FORTRAN

  do-variable = int-expr-lower, int-expr-upper [, int-expr-stride]

and where the number of iterations will be

FORTRAN

  max(0, (int-expr-upper - int-expr-lower + int-expr-stride)/int-expr-stride)

in the absence of any subsequent control. The number of iterations may be zero. Any of the expressions may be negative. If the stride is not present, it will be 1 (unity); if the stride is present, it may not be zero.

Exercise (2 minutes)

Challenge

How many iterations make a do loop?

What is the number of iterations in the following cases?

FORTRAN

   do i = 1, 10
     print *, "i is  ", i
   end do

   do i = 1, 10, -2
     print *, "i is ", i
   end do

   do i = 10, 1, -2
     print *, "i is ", i
   end do

You can confirm your answers by running example3.f90. Note there is no way (cf. C for) to limit the scope of the loop variable to the loop construct itself; the variable will then have a final value after exit from the loop.

The following output is produced:

OUTPUT

 First loop: 1, 10
 i is  1
 i is  2
 i is  3
 i is  4
 i is  5
 i is  6
 i is  7
 i is  8
 i is  9
 i is  10
 Second loop: 1, 10, -2
 Third loop: 10, 1, -2
 i is  10
 i is  8
 i is  6
 i is  4
 i is  2

The first loop has ten iterations with i running from 0 to 10 in steps of 1.

The second loop runs zero times as, with a stride of -2, there is no way to iterate from 1 to 10.

The third loop has five iterations, from 10 to 2 in steps of -2. The stated end value of 1 can’t be reached, and 0 would go too far, so it finishes at 2.

Exercise (5 minutes)

Challenge

Calculating pi with loops

We return to the exercises discussed in the earlier episode on variables. You can use your own solutions, or the new template here.

For exercise1.f90 which computes an approximation to the constant pi using the Gauss-Legendre algorithm, introduce an iteration to compute a fixed number of successive improvements. How many iterations are required to converge when using kind(1.d0)? Would you be able to adjust the program to halt the iteration if the approximation is within a given tolerance of the true answer?

Sample code implementing loops with this problem is used as a template to the exercise in the episode on arrays.

You should be able to observe that with kind(1.d0) the value of pi converges after only three iterations. You also can have the code exit the loop and halt iteration by storing the previous estimate of pi if the difference between the old and new values is less than the desired tolerance.

Exercise (5 minutes)

Challenge

Conductance of a channel with loops

exercise2.f90 returns to the calculation of the conductance in the narrow channel and will need similar work. Use a loop to compute a fixed number of terms in the sum over index k (use real type real64 for the sum). Here you should find convergence is much slower (you may need about 1000 terms); check by printing out the current value every so many terms (say 20).

Expert question: What happens if you accumulate the sum in the reverse order in this case? Can you think why this happens?

Answer to the expert question: floating point numbers of a given format (such as real64) all have the same relative error, no matter how big or small they are. The absolute error on the other hand is the produce of the number’s value and the relative error; thus, a small real has a small absolute error, and a large real has a large absolute error. If the large numbers are summed first, the sum’s absolute error quickly becomes large, and the smaller numbers are subsumed into it. If the small numbers are summed first, the sum’s absolute error remains small enough that the small numbers are able to contribute. Consider further how in floating point maths it is true that (x + y) + z /= x + (y + z), if you are interested look up Kahan’s algorithm.

Key Points
  • Iteration in Fortran is based around the do construct (somewhat analogous to C for construct). There is no equivalent of the C++ iterator class.
  • Without any control, a do loop will execute forever.
  • A loop iteration can be skipped with a cycle statement.
  • A loop can be ended if an exit statement is encountered.
  • It is very common to control the execution of a loop with an integer variable.

Content from Array declarations


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How do I create arrays of different sizes and numbers of dimensions?
  • Can I create the arrays at runtime if I don’t yet know how big they need to be?

Objectives

  • Understand how to create arrays of one, two and more dimensions.
  • Recognise and use array terminology.
  • Be able to create and use allocatable arrays.
  • Get help from the compiler if things go wrong.

A one-dimensional array


Arrays may be declared with addition of the dimension attribute, e.g.,

FORTRAN

program example1

  implicit none

  real, dimension(3) :: a    ! declare a with elements a(1), a(2), a(3)

end program example1

The size of the array is the total number of elements (here 3). The default lower bound is 1 and the upper bound is 3.

In a slightly older style you may also see arrays declared by placing the size of the array directly after the name, e.g. and equivalently,

FORTRAN

  real :: a(3)               ! declare a with elements a(1), a(2), a(3)
Callout

Fortran array indexing

Very importantly, Fortran arrays by default begin their indexing at 1. This is in direct contrast to other languages such as C, C++ and Python which begin at 0. Both styles have advantages and disadvantages; the Fortran standard has simply settled on a style that more closely resembles the indexing of mathematical matrices, while starting from 0 makes the offset in the memory more visible.

Lower and upper bounds

If necessary or useful, you can however choose yourself the bounding indices of your arrays:

FORTRAN

  real, dimension(-2:1) :: b ! elements b(-2), b(-1), b(0), b(1)

Here we specify, explicitly, the lower and upper bounds. The size of this array is 4.

Array constructor

One may specify array values as a constructor

FORTRAN

  integer, dimension(3), parameter :: s = (/ -1, 0, +1 /)   ! F2003 or
  integer, dimension(3), parameter :: t = [  -1, 0, +1 ]    ! F2008

A two-dimensional array


FORTRAN

  real, dimension(2,3) :: a   ! elements a(1,1), a(1,2), a(1,3)
                              !          a(2,1), a(2,2), a(2,3)

This two-dimensional array (said to have rank 2) has two elements in the first dimension (or extent 2), and 3 elements in the second dimension (extent 3). It is said to have shape (2,3), which is the sequence of extents in each dimension. Its size is 6 elements.

There is an array element order which in which we expect the implementation to store contiguously in memory. In Fortran this has to be the left-most dimension counting fastest. For array a we expect the order in memory to be

FORTRAN

a(1,1), a(2,1), a(1,2), a(2,2), a(1,3), a(2,3)

that is, the opposite the convention in C.

Callout

Looping through multi-dimensional arrays

In you write a loop which moves through an array, remember that you will typically move most quickly through the first index. That means that generally the innermost loop’s control variable should be used for the first index, the next outer loop should use the second control variable, and so on, e.g.,

FORTRAN

do j = 1, 10
  do i = 1, 20
    a(i,j) = ...   ! Do work to calculate the (i,j)th element of an array.
  end do
end do

The principles of rank-2 arrays can be extended to higher ranks. The standard requires support for a minimum of rank-15 arrays, but individual compilers may allow for even more.

reshape

A constructor for an array of rank 2 or above might be used, e.g.,

FORTRAN

  integer, dimension(2,3) :: m = reshape([1,2,3,4,5,6], shape = [2,3])

where we have used the intrinsic function reshape().

reshape() can be used wherever you might need to alter an array’s shape.

Exercise (2 minutes)

Discussion

Array sizes and shapes

Check the accompanying example1.f90 to see examples of intrinsic functions available to interrogate array size and shape at run time.

Allocatable arrays


If we wish to establish storage with shape determined at run time, the allocatable attribute can be used. The rank must be specified but the value of the extent in each dimension is deferred using the : symbol:

FORTRAN

  real, dimension(:, :), allocatable :: a

  ! ... establish shape required, say (imax, jmax) ...

  allocate(a(imax, jmax))

  ! ... use and then release storage ...

  deallocate(a)

Again, this array will take on the default lower bound of 1 in each dimension.

Formally,

FORTRAN

  allocate(allocate-list [, source = array-expr] [ , stat = scalar-int-var])

The optional source argument may be used to provide a template for the newly allocated object (values will be copied). We will return to this in more detail in the context of dynamic type.

A successful allocation with the optional stat argument will assign a value of zero to the argument.

Allocation status

An array declared with the allocatable attribute is initially in an unallocated state. When allocated, this status will change; this status can be interrogated via the intrinsic function allocated().

FORTRAN

  integer, dimension(:), allocatable :: m
  ...
  if (allocated(m)) then
    ! ... we can do something ...
  end if

An attempt to deallocate a variable which is not allocated is an error.

Exercise (5 minutes)

Discussion

Computing pi with arrays

Return again to the program to compute the approximation of pi via the Gauss-Legendre expansion (last seen in the previous episode on do loops). You may use your own version or the new template provided in this directory (see exercise1.f90).

Introduce array storage for the quantites a, b and t. Use a fixed number of terms. Assign appropriate values in a first loop. In a second loop, compute the approximation of pi at each iteration.

What might you do if you wanted to store only the number of terms taken to reach a converged answer?

Help is available!

As arrays are self-describing in Fortran, it is relatively easy for the compiler to analyse whether array accesses are valid, or within bounds. This can help debugging. Most compilers will have an option that instructs the compiler to inject additional code which checks bounds at run time. For the Cray Fortran compiler, this is -hbounds; for the GNU compiler, this is -fcheck=bounds.

E.g.,

BASH

$ ftn -hbounds exercise1.f90

Some compilers may also be able to check certain bounds at compile time, and issue a compile-time message. However, in general, errors may not appear until run time. If your program crashes, or produces unexpected results, this compiler option can help to track down problems with invalid array accesses.

Key Points
  • Unlike C, which often uses pointers to handle array data, Fortran has arrays which are an intrinsic feature of the language.
  • The number of dimensions in an array is its rank.
  • The number of elements in one of an array’s dimensions is its extent.
  • The ordered sequence of extents in an array is its shape.
  • The total number of elements in an array, equal to the product of its shape, is its size.
  • In Fortran array indices begin at 1 by default, but this can be changed if required.
  • New arrays can be created by using the intrinsic reshape() function with an existing one.
  • An array with an unknown size can be allocated at runtime.
  • Compilers are typically able to help you debug issues with arrays if you ask them to.

Content from Array expressions and assignments


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How do I assign values to Fortran arrays?
  • Can I work at once with subsets of an array?
  • How can I reduce or extract information from an array?

Objectives

  • Learn the syntax used to work with array sections.
  • Learn about array reductions.
  • Understand how to use arrays in logical expressions.

Array sections


A general subset of array elements, an array section, may be constructed from a triplet with a start and end subscript and a stride:

FORTRAN

  [subscript] : [subscript] [: stride]

Given a rank 1 array a(:), some valid array sub-objects are:

FORTRAN

  a         ! the whole array
  a(:)      ! the whole array again
  a(1:4)    ! array section [ a(1), a(2), a(3), a(4) ]
  a(:4)     ! all elements up to and including a(4)
  a(2::2)   ! elements a(2), a(4), a(6), ...

If an array subscript is present, it must be valid; if the stride is present it may not be zero.

Array expressions and assignments


Intrinsic operations can be used to construct expressions which are arrays. For example:

FORTRAN

  integer, dimension(4, 8) :: a1, a2
  integer, dimension(4)    :: b1

  a1 = 0                      ! initialise all elements to zero
  a2 = a1 + 1                 ! all elements of a2(:,:)
  a1 = 2*a1                   ! multiplied element-wise

  b1 = a1(:, 1)               ! b1 set to first column of a1

Such expressions and assignments must take place between entities with the same shape (they are said to conform). A scalar conforms with an array of any shape.

Given the above declarations, the following would not make sense:

FORTRAN

  b1 = a1

Exercise (2 minutes)

Challenge

Array appearance

A caution. How should we interpret the following assignments?

FORTRAN

  d = 1.0
  e(:) = 1.0

Compile the accompanying program example1.f90, check the compilation errors and improve the program.

The first assignment given above is ambiguous. Is d a scalar variable, or an entire array? The second assignment is clearly on every element of the rank-1 array e.

The compiler errors you get when compiling example1.f90 may spell out exactly what is going wrong. At line 13,

FORTRAN

b1 = a1

b1 is a rank-1 array; the rank-2 array a1 cannot be used to assign values to it. Change it to

FORTRAN

b1(:) = a1(:,1)

Then, the second issue is the assignment

FORTRAN

b1(1:4) = a2(1, 4:4)

as the section a2(1, 4:4) is an array of the incorrect size (note that a2(1, 4) would work as it becomes a scalar, but wouldn’t set b2 to the first half of the first row of a2). To correct the error, fix the start index:

FORTRAN

b1(1:4) = a2(1, 1:4)
Callout

Array style

Following this exercise, you hopefullly agree it may be a good idea to prefer use of a(:) over a when referencing the whole array. The former has the appearance of an array, while the latter may incorrectly appear to be a scalar to an unfamiliar reader of the code (or you yourself if it’s been a while since you last read it).

Elemental intrinsic functions


Many intrinsic functions are elemental: that is, a call with a scalar argument will return a scalar, while a call with an array argument will return an array with the result of the function for each individual element of the argument. For example,

FORTRAN

   real, dimension(4) :: a, b
   ...
   b(1:4) = cos(a(1:4))

will fill each element of b with the cosine of the corresponding element in a.

Reductions


Other intrinsic functions can be used to perform reduction operations on arrays, and usually return a scalar. Common reductions are

FORTRAN

   a = minval( [1.0, 2.0, 3.0] )  ! the minimum value from the array
   b = maxval( [1.0, 2.0, 3.0] )  ! the maximum value from the array
   c = sum(array(:))              ! the sum of all values in the array

Logical expressions and masks


There is an array equivalent of the if construct called where, e.g.,:

FORTRAN

  real, dimension(20) :: a
  ...
  where (a(:) >= 0.0)
    a(:) = sqrt(a(:))
  end where

which performs the appropriate operations element-wise. Formally,

FORTRAN

  where (array-logical-expr)
    array-assignments
  end where

in which all the array-logical-expr and array-assignments must have the same shape.

Logical functions any(), all(), and others may be used to reduce logical arrays or array expressions. These return a logical value so can be used in an if statement:

FORTRAN

   if (any(a(:) < 0.0)) then
     ! do something if at least one element of a(:) is negative
   end if
   if (all(a(:) < 0.0)) then
     ! do something if every element of a(:) is negative
   end if

Some intrinsic functions have an optional mask argument which can be used to restrict the operations to certain elements, e.g.,

FORTRAN

  b = min(array(:), mask = (array(:) > 0.0))     ! minimum of positive value
  n = count(array(:), mask = (array(:) > 0.0))   ! count the number of positive values

These may be useful in certain situations.

Another caution

There may be a temptation to start to construct array expressions of baroque complexity, perhaps in the search for brevity. This temptation is probably best avoided:

  1. Such expressions can become very hard to read and interpret for correctness;
  2. Performance: compilers may struggle to generate the best code if expressions are very complex. Array expressions and constructs may not work in parallel: explicit loops may provide better opportunities.

If array expressions are used, simple ones are best.

Exercise (5 minutes)

Discussion

Quadratic equation

The template exercise1.f90 re-visits the quadratic equation exercise. Check you can replace the scalars where appropriate. See the template for further instructions.

Challenge

Sieve of Eratosthenes

Here’s an additional exercise: write a program to implement the Sieve of Eratosthenes algorithm which performs a search for prime numbers. See exercise2.f90 for a template with some further instructions. How much array syntax can you reasonably introduce?

A sample solution is provided in solution.f90.

Key Points
  • Fortran allows flexible operations on arrays or subsets of array elements and provides numerous intrinsic functions for such operations.
  • Some of these, such as all() and any() can be useful in directing the logical flow of a program.

Content from Break


Last updated on 2026-02-21 | Edit this page

Comfort break.

Content from Mini exercise: a choice of two


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What are some real problems I can try out?

Objectives

  • Work on one of two larger exercises, putting your new skills to use.

A choice…


We have two exercises you can choose to work on now. Both will allow you to make use of the Fortran that you have learnt today. The former exercise is the solution of a tri-diagonal system of equations, while the latter will allow you program an implementation of Conway’s Game of Life.

Whichever you choose (or if you choose both), we have time this afternoon and tomorrow for you to work on your solutions.

Challenge

Solve a tri-diagonal system (30 minutes)

This exercise will solve a tri-diagonal system of equations. A linear system of equations can be represented by the problem

\(Ax = b\)

where \(A\) is a known matrix, \(b\) is a known vector, and \(x\) is an unknown vector we wish to solve for. \(A\) is tri-diagonal, meaning that it is 0 everywhere except for a stripe of non-zeroes three elements wide moving down the diagonal – except the very top and bottom, where it is two elements wide.

This means that we can represent an \(n\)-by-\(n\) tri-diagonal matrix using three rank-1 arrays, one of size n representing the diagonal and two of size n-1 representing the upper and lower diagonals. Through two operations across the arrays, the first moving forwards and the second backwards, as described at Wikipedia, the solution x can be determined.

The accompanying template tri-diagonal.f90 has some further instructions, and some suggestions on how to test the result.

A solution to the problem appears as a template to the exercise in the later episode on dummy arguments.

Challenge

Game of Life (60 minutes)

In this exercise you will create a simple implementation of Conway’s Game of Life. The Game of Life is a cellular automata model where an array of cells is updated based on some simple rules which give rise to complex patterns.

This model uses a ‘board’ of cells, each cell having a value of 0 (“dead”) or 1 (“alive”). The board’s initial state is chosen and then stepped forwards. In a new step, each cell calculates the sum of the surrounding eight cells. Depending on the value of that sum, the cell’s state may change or stay the same. The rules governing the update are summarised in the following table:

Cell state Sum of neighbouring 8 cells New Cell state
0 0,1,2,4,5,6,7,8 0
0 3 1
1 0,1 0
1 4,5,6,7,8 0
1 2,3 1

A template life.f90 is provided with some hints to get you started.

Reference

Your program should produce the following output for the initial board and the first update:

$ ./a.out
 Initial Board Set-up:
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 ........#.#.#........
 ........#...#........
 ........#...#........
 ........#...#........
 ........#.#.#........
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................

 First iteration:
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .........#.#.........
 .......##...##.......
 .......###.###.......
 .......##...##.......
 .........#.#.........
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................
 .....................

Two reference answers are given in the solutions directory. The first only computes the first update, while the second includes the time stepping loop.

Key Points
  • With a fairly small amount of the Fortran language, you can already solve some real problems.

Content from Modules and compilation of modules


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How can I collect parameters and sub-programs of my own in a module?
  • Can I keep some parts of the module internal to it and hidden?
  • Am I able to use scoping to introduce temporary variables in a larger program unit?

Objectives

  • Learn how to write a non-intrinsic module of your own, and understand what is and isn’t appropriate to place within them..
  • Understand how to use public and private within a module to control what components are visible externally.
  • Use the block construct to control the scope of names within larger program structures.

Module structure


We have already used one intrinsic module (iso_fortran_env); we can also write our own, e.g.,

FORTRAN

module module1

  implicit none
  integer, parameter :: mykind = kind(1.d0)

contains

  function pi_mykind() result(pi)   ! Return the value of a well-known constant

    real (kind = mykind) :: pi
    pi = 4.0*atan(1.0_mykind)

  end function pi_mykind

end module module1

We may now use the new module in other program units (main program or other modules). For example:

FORTRAN

program example1

  use module1
  implicit none

  real (kind = mykind) :: value
  value = pi_mykind()

end program example1

Here both the parameter mykind and the function pi_mykind() are said to be available by use association. Note that any use statements must come before the implicit statement.

Formally, the structure of a module is:

FORTRAN

  module module-name
    [specification-statements]
  [ contains
    module-subprograms ]
  end [ module [ module-name ]]

The contains statement separates the specification statements from module sub-programs. Sub-programs, or procedures, consist of functions and/or subroutines which will cover in the next episode .

Digression: compilation of modules, and programs

One would typically expect modules and a main program to be in separate files, e.g.,:

FORTRAN

$ ls
module1.f90     program1.f90

It is often convenient to use the same name for the both module and the corresponding file (with extension .f90). You can do differently, but it can become confusing. Likewise for the main program.

We can compile the module, e.g.,

BASH

$ ftn -c module1.f90

where the -c option to the Fortran compiler ftn requests compilation only (no link). This should give us two new files:

BASH

$ ls
module1.f90     module1.mod     module1.o       program1.f90

The first is a compiler-specific module file (usually with a .mod extension). This plays the role roughly analogous to a header (.h) file in C, in that it contains the relevant public information about what the module provides. The other file is the object file (.o extension) which can be linked with the run time to form an executable.

We can now compile both the main program and the module to give an executable.

BASH

$ ftn module1.o program1.f90

Again, by analogy with C header files, we do not include the .mod file in the compilation command; there is a search path which the compiler uses to look for module files (which includes the current working directory).

Callout

.mod files

As stated, the .mod files are compiler specific. That means that you shouldn’t expect that a file created by one compiler will be usable by a different compiler. This can be can cause compile time errors if you try to use a .mod from one compiler with another; perhaps the most common scenario for this is when testing builds with different compilers without first cleaning up the .mod files. Sometimes compilers also use different naming schemes for the .mod, such as capitalising them. This particular behaviour can usually be changed with compiler flags.

Exercise (2 minutes)

Challenge

Compiling modules

If you haven’t already done so, compile the accompanying module1.f90 and program1.f90. Check the errors which occur if you: (1) try to compile the program without the module file via, e.g.,

BASH

$ ftn program1.f90

and (2), if you try to compile and link the module file alone:

BASH

$ ftn module1.f90

Compiling the program alone produces (with the Cray compiler) the following error:

OUTPUT


  use module1
      ^
ftn-292 ftn: ERROR PROGRAM1, File = program1.f90, Line = 3, Column = 7
  "MODULE1" is specified as the module name on a USE statement, but the compiler cannot find it.

  real (kind = mykind) :: a
               ^
ftn-113 ftn: ERROR PROGRAM1, File = program1.f90, Line = 6, Column = 16
  IMPLICIT NONE is specified in the local scope, therefore an explicit type must be specified for data object "MYKIND".
               ^
ftn-868 ftn: ERROR PROGRAM1, File = program1.f90, Line = 6, Column = 16
  "MYKIND" is used in a constant expression, therefore it must be a constant.

Cray Fortran : Version 15.0.0 (20221026200610_324a8e7de6a18594c06a0ee5d8c0eda2109c6ac6)
Cray Fortran : Compile time:  0.0472 seconds
Cray Fortran : 13 source lines
Cray Fortran : 3 errors, 0 warnings, 0 other messages, 0 ansi
Cray Fortran : "explain ftn-message number" gives more information about each message.

This tells us precisely that the compiler doesn’t know what module we’re talking about when we use module1, and then that mykind (which we have been expecting to get from the module) doesn’t exist either.

Compiling module1 alone gives the following output from the Cray compiler:

OUTPUT

/opt/cray/pe/cce/15.0.0/binutils/x86_64/x86_64-pc-linux-gnu/bin/ld: /usr/lib64//crt1.o: in function `_start':
/home/abuild/rpmbuild/BUILD/glibc-2.31/csu/../sysdeps/x86_64/start.S:104: undefined reference to `main'

This is less immediately meaningful. The second line tells us that there’s no function called main available. This is because what we’ve compiled has no program ... end program. There’s no program in there to start, were we able to invoke it on the command line. In other words, a module alone doesn’t make a program we can run.

Scope


Entities declared in a module are, by default, available by use association, that is, they are visible in program units which use the module. One can make this scope explicit via the public and private statements.

FORTRAN

module module1

  implicit none
  public

  integer, parameter :: mykind = kind(1.d0)

contains

  function pi_mykind() result(pi)

    real (kind = mykind) :: pi
    ...
  end function pi_mykind()

end module module1

Note that the parameter mykind is available throughout the module via host association (always).

An alternative would be to switch the default to private, and explicitly add public attributes:

FORTRAN

module module1

  implicit none
  private

  integer, parameter, public :: mykind = kind(1.d0)    ! visible via `use`
  integer, parameter         :: mypriv = 2             ! not visible via `use`

  public :: pi_mykind

contains
  function pi_mykind() result(pi)                      ! public
    ! ... may call my_private() ...
  end function pi_mykind

  subroutine my_private()                              ! private
    ...
  end subroutine
end module module1

Note that scope of the implicit statement also covers the whole module, including sub-programs.

Exercise (1 minute)

Challenge

Private modules

Edit the accompanying module1.f90 to add a private statement and check the error if you try to compile program1.f90.

Making the module private hides the mykind parameter it provide from the program. On compilation, mykind can’t be found in the scope of the program, and the compiler will complain that no such name exists.

Module data and other horrors


It is possible to establish non-parameter data in the specification section of a module. E.g.,

FORTRAN

module module2

  implicit none
  integer, dimension(:), allocatable :: iarray
...

This course will argue that you should not do so. There are a number of reasons.

  1. Any such data takes on the character of a global mutable object. Global objects are generally frowned upon in modern software development.
  2. Operations in module procedures on such data run the risk of being neither thread safe nor re-entrant.

Even worse, variables declared with an initialisation in a module sub-program, e.g.,

FORTRAN

  integer :: i = 1

implicitly take on the Fortran save attribute. This means the variable is placed in heap memory and retains its value between calls. (This is analogous to a static declaration in C.) This is certainly neither thread-safe nor re-entrant. Uninitialised variables appear on the stack (and disappear) as expected.

For this reason it is the rule, rather than the exception, that variables are not initialised at the point of declaration in Fortran.

We will look at alternative ways of establishing and moving data as we go along.

Scope again: block


It is not possible in Fortran to intermingle declarations and executable statements. Specification statements must appear at the start of scope before any executable statements. This can lead to rather lengthy list of declarations at the start of large routines.

It is possible to introduce a local scope which follows executable statements using the block construct. Schematically:

FORTRAN

   ... some computation ...
   block
     integer :: itmp                  ! in scope within the block only
     ... some more computation ...
   end block
   ... some more computation ...

This can be useful for introducing temporary variables which are only required for the duration of a short part of a longer procedure. In this way, it acts like { .. } in C.

Exercise (5 minutes)

Challenge

Return to Gauss-Legendre

Return to your code for the approximation to pi via the Gauss-Legendre iteration (or use the template exercise1.f90 from the earlier episode on arrays). Using the examples above as a template, write a module to contain a function which returns the value so computed. Check you can use the new function from a main program.

An example solution is provided in solution_program.f90 and solution_module.f90.

Challenge

Module dependencies

Food for thought: can we have the following situation?

FORTRAN

module a

  use b
  implicit none
  ! content a ...
end module a

and

FORTRAN

module b

  use a
  implicit none
  ! content b ...
end module b

If not, why not?

This is a circular dependency: a depends on b depends on a. There is no solvable dependency tree, and this is not allowed.

Key Points
  • Modules in Fortran provide the way to structure collections of related definitions and operations, and make them available elsewhere.

Content from Functions and subroutines


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How can I factor code out into different program procedures?
  • How do I control the flow of information into and out of procedures?

Objectives

  • See how to create and call both functions and subroutines.
  • Use the intent attribute with dummy arguments to change their read and write permissions within the procedure.
  • Understand the meanings of pure and recursive procedures.

What’s the difference?


The difference between functions and subroutines is to a degree one of context. A function returns a result and is generally used where it is best invoked as part of an expression or assignment, schematically:

FORTRAN

  value = my_function(arg1, arg2, ...)

Unlike C, it is not possible simply to discard a function result. A subroutine, by contrast, does not return a result (it may be thought of as a void function in C terms), but it is also invoked differently:

FORTRAN

  call my_subroutine(arg1, arg2, ...)

Subroutines are generally used to express more lengthy algorithms.

Note that in the calling context, the arguments arg1, arg2, etc are referred to as the actual arguments.

Dummy arguments and intent attribute


Procedures may have zero or more arguments (referred to as the dummy arguments at the point at which the procedure is defined). Three different cases can be identified:

  1. read-only arguments whose values are not updated by the procedure;
  2. read-write arguments whose values are expected to be defined on entry, and may also be updated by the procedure;
  3. write-only arguments whose values are only defined on exit.

These three cases may be encoded in the declarations of the dummy arguments of a procedure via the intent attribute . For example:

FORTRAN

  subroutine print_x(x)

    real, intent(in) :: x

    print *, "The value of x is: ", x

  end subroutine print_x

Here, dummy argument x has intent(in) which tells the compiler that any attempt to modify the value is erroneous (a compiler error will result). This is different from C, where a change to an argument passed by value is merely not reflected in the caller.

Callout

Passing by reference

If you are used to C/C++, you should remain aware here that the Fortran standard requires actual arguments to be passed to functions and subroutines in such a way that they ‘appear’ to have been passed by reference. How to do so is left to the compiler (which may use copies or actually pass by reference) but any changes made to a dummy argument inside a procedure will be reflected in the actual arguments passed to it, unless prevented by having intent(in).

If one wants to alter the existing value of the argument, intent(inout) is appropriate:

FORTRAN

  subroutine increment_x(x)

    real, intent(inout) :: x

    x = x + 1.0

  end subroutine increment_x

If the dummy argument is undefined on entry, or has a value which is simply to be overwritten, use intent(out), e.g.:

FORTRAN

  subroutine assign_x(x)

    real, intent(out) :: x

    x = 1.0

  end subroutine assign_x

The intent attribute, as well as allowing the compiler to detect inadvertent errors, provides some useful documentation for the reader.

Local variables do not have intent and are declared as usual.

Exercise (2 minutes)

Challenge

Exercise name

Attempt to compile the accompanying module1.f90 and associated main program program1.f90. Check the error message emitted, and sort out the intent of the dummy arguments in module1.f90.

assign_x() should use intent(out) for x as it doesn’t matter what it was previously; we want to set it to 1 and return it.

print_x() is correct to use intent(in) for x as it needs to know its value in order to print it, and it shouldn’t change it.

increment_x() should have intent(inout) for x: the subroutine needs to know its previous value in order to add to it (hence in) and it needs to be able to pass the new value back out (hence out).

Functions


A function may be defined as:

FORTRAN

function my_mapping(value) result(a)

  real, intent(in) :: value
  real             :: a

  a = ...

end function my_mapping

Formally,

FORTRAN

[prefix] function function-name (dummy-arg-list) [suffix]
  [ specification-part ]
  [ executable-part ]
end [ function [function-name] ]

As ever, there is some elasticity in the exact form of the declarations you may see. In particular, older versions did not have the result() suffix, and the function-name was used as the variable to which the return value was assigned. E.g.,

FORTRAN

real function length(area)
  real area
  length = sqrt(area)
end function length

The modern form should be preferred; the result() part allows the two names to be decoupled.

pure procedures

Procedures which have no side effects may be declared with the pure prefix; this may provide useful information to the compiler in some circumstances. E.g.,

pure function special_function(x) result(y)
  real, intent(in) :: x
  ! ...
end function special_function

There are a number of conditions which must be met to qualify for pure status:

  1. For a function, any dummy arguments must be intent(in);
  2. No variables accessed by host association can be updated (and no variables with save attribute);
  3. there must be no operations on external files;
  4. there must be no stop statement.

recursive procedures

If recursion is required, a procedure must be declared with the recursive prefix. E.g.,

FORTRAN

recursive function fibonacci(n) result(nf)
  ! ... implementation...
  nf = fibonacci(n-1) + fibonacci(n-2)
end function fibonacci

Such a declaration must be included for both types of recursion: direct (a procedure calling itself) and indirect (a procedure calling another which in turn calls the first).

Subroutines


Subroutines follow the same rules as functions, except that there is no result() suffix specification:

FORTRAN

[prefix] subroutine subroutine-name (dummy-arg-list)
  [ specification-part ]
  [ executable-part ]
end [ subroutine [subroutine-name] ]

return statement

One may have a return statement to indicate that control should be returned to the caller, but it is not necessary (the end statement does the same job). We will use return when we consider error handling later.

Placing procedures in modules


It is very often a good idea to include your procedures within modules beneath the contains statement. Aside from leading to good organisation of code, this has an extra advantage in that the compiler will then automatically generate the contract block (forward declaration or prototype) for the procedure as part of the module. This can make it easier when using certain types of procedures (such as those with allocatable dummy arguments) or in certain contexts (such as passing the procedure as an argument).

Exercise (5 minutes)


Challenge

Gauss and Legendre met Fibonacci

  1. Can your function for the evaluation of pi from the previous episodes safely be declared pure? You can also use the accompanying template exercise_module1.f90 and exercise_program1.f90 to check.
  2. Add a new version of this calculation: a subroutine which takes the number of terms as an argument, and also returns the computed value in the argument list.
  3. Add to the module a recursive function to compute the nth Fibonacci number, and test it in the main code. See, for example the page at Wikipedia.

Solutions are available in solution_program1.f90 and solution_module1.f90.

Key Points
  • Functions and subroutines are referred to collectively as _procedures_.
  • Using intent for dummy variables allows control over whether updates to their values are permitted.
  • Procedures can be modified with prefixes such as pure and recursive which ensure respectively that the procedure has no side-effects and that it is able to directly or indirectly call itself.
  • Procedures may be defined as module sub-programs for which the compiler will automatically generate the contract block as part of the module file.

Content from More on array dummy arguments


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What do I need to take into consideration when passing an array as an argument?
  • Can I pass arrays which haven’t yet been allocated?
  • How do I implement optional dummy arguments?

Objectives

  • Understand the meaning of an assumed shape array and where care needs to be taken with its bounds.
  • Be able to use the intrinsic lbound() and ubound() functions.
  • Understand the conditions around the usage of allocatable arrays as dummy arguments.
  • Make some arguments optional and be able to provide them using keyword arguments.

Array dummy arguments


When a dummy argument is an array, we need to think about how we want the procedure to know about its shape.

Explicit shape

One is entitled to make explicit the shape of an array in a procedure definition, e.g.,

FORTRAN

  subroutine array_action1(nmax, a)
    integer, intent(in)                    :: nmax
    real, dimension(1:nmax), intent(inout) :: a
    ...
  end subroutine array_action1

and similarly for arrays of higher rank. As before, if the lower bound of the array is unspecified, it takes on the default value of 1.

Assumed shape

However, it may be more desirable to leave the exact shape implicit in the array itself, e.g.,

FORTRAN

  subroutine array_action2(a, b)
    real, dimension(:,:), intent(in) :: a
    real, dimension(:,:), intent(inout) :: b
    ...
  end subroutine array_action2

Note that the rank is always explicit.

There are pros and cons to this: it is slightly more concise and general, but some care may need to be exercised with the distinction between extents and bounds. It is the shape that is passed along with the actual arguments, and not the bounds.

Callout

Bounds, extents and shapes

Remember from the earlier lesson on arrays that the number of elements an array has in a given dimension is that dimension’s extent. The ordered set of extents is the array’s shape. The bounds are the minimum and maximum indices used in each dimension.

lbound() and ubound() again

You will have seen the lbound() and ubound() functions being used during the example in the earlier episode on arrays. These functions return a rank one array which is the relevant bound in each dimension. The optional argument dim can be used to obtain the bound in the corresponding rank or dimension e.g.,

FORTRAN

  real, dimension(:,:), intent(in) :: a
  integer :: lb1, lb2

  lb1 = lbound(a, dim = 1)  ! lower bound in first dimension
  lb2 = ubound(a, dim = 2)  ! upper bound in second dimension

Exercise (4 minutes)

Discussion

Checking array bounds

Consider the accompanying example in program1.f90 and module1.f90. Is the code correct? Add calls to lbound() and ubound() in the subroutine to check.

What do you think was the intention of the programmer? What remedies are available?

Automatic arrays

One is allowed to bring into existence ‘automatic’ arrays on the stack. These are usually related to temporary workspace, e.g.,

FORTRAN

subroutine array_swap1(a, b)
  integer, dimension(:), intent(inout) :: a, b
  integer, dimension(size(a))          :: tmp

  tmp(:) = a(:)
  a(:)   = b(:)
  b(:)   = tmp(:)
end subroutine array_swap1

In this example, the same effect could have been achieved using a loop and a temporary scalar.

Allocatable dummy arguments

It may occasionally be appropriate to have a dummy argument with both an intent and allocatable attribute.

FORTRAN

  subroutine my_storage(lb, ub, a)

    integer, intent(in) :: lb, ub
    integer, dimension(:), allocatable, intent(out) :: a

    allocate(a(lb:ub))

  end subroutine my_storage

There are a number of conditions to such usage.

  1. The corresponding actual argument must also be allocatable (and have the same type and rank);
  2. If the intent is intent(in) the allocation status cannot be changed;
  3. If the intent is intent(out) and the array is allocated on entry, the array is automatically deallocated.

The intent applies to both the allocation status and to the array itself. Some care may be required.

A function may have an allocatable result which is an array; this might be thought of as returning a temporary array which is automatically deallocated when the relevant calling expression has been evaluated.

Optional arguments


We have encountered a number of intrinsic procedures with optional dummy arguments. Such procedures may be constructed with the optional attribute for a dummy argument, e.g.:

FORTRAN

  subroutine do_something(a, flag, ierr)
    integer, intent(in)            :: a
    logical, intent(in),  optional :: flag
    integer, intent(out), optional :: ierr

  end subroutine do_something

Any operations on such optional arguments should guard against the possibility that the corresponding actual argument was not present using the intrinsic function present(). E.g.,

FORTRAN

  local_flag = some_default_value
  if (present(flag)) local_flag = flag

Any attempt to reference a missing optional argument will generate an error.

The one exception is that an optional argument may be passed directly as an actual argument to another procedure where the corresponding dummy argument is also optional.

Positional and keyword arguments

Procedures will often have a combination of a number of mandatory (non-optional) dummy arguments, and optional arguments. These may be mixed via the use of keywords, which are the dummy argument name. E.g., using the subroutine defined above:,

FORTRAN

  call do_something(a, ierr = my_error_var)

Here, a is appearing as a conventional positional argument, while a keyword argument is used to select the appropriate optional argument ierr. The rules are:

  1. no further positional arguments can appear after the first keyword argument;
  2. positional arguments must appear exactly once, keyword arguments at most once.

Exercise (10 minutes)


Challenge

Tri-diagonal modules

Consider again the problem of the tri-diagonal matrix.

Refactor your existing stand-alone program (or use the template exercise.f90) to provide a module subroutine such as

FORTRAN

  subroutine tridiagonal_solve(b, a, c, rhs, x)

where b, a, and c are arrays of the relevant extent to represent the diagonal elements, the lower diagonal elements and the upper diagonal elements respectively. Use the size() intrinsic to determine the current size of the matrix in the subroutine. The extent of the off-diagonal arrays should be one less element. The rhs is the vector of right-hand side elements, and x should hold the solution on exit. Assume, in the first instance, that all the arrays are of the correct extent on entry.

Check your result by calling the subroutine from a main program with some test values. (You may wish to take two copies of the template exercise.f90 and use one as the basis of a module, and the other as the basis of the main program.)

What do you need to do if the diagonal b and the right-hand side d arrays should be declared intent(in)?

What checks would be required in a robust implementation of such a routine?

In the original implementation, the diagonal and right-hand side are destroyed during the calculation. To use intent(in) with the dummy arguments, we have to leave them untouched. That means making copies in local automatic arrays and using those instead.

A robust implementation would need to include checks on the bounds of the dummy arrays to make sure that they correctly conform to one another.

Sample solutions are available in the later episode on derived types in exercise_program.f90 and exercise_module.f90.

Key Points
  • There are some additional considerations to think about when dummy arguments are arrays.
  • You may wish to pass the shape of the array explicitly, at the cost of providing more dummy arguments.
  • Arrays may have an assumed shape, but remember that they only receive the shape and not the original bounds of its dimensions.
  • Allocatable arrays can also be used as dummy arguments.
  • Dummy arguments can have the optional attribute. The corresponding actual arguments can be positional or provided as a keyword argument.

Content from Break


Last updated on 2026-02-21 | Edit this page

Comfort break.

Content from More on characters and strings


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How are character variables compared in Fortran?
  • How can I create and use flexible strings?

Objectives

  • Understand relational operators and some of the intrinsic functions that can be used with character variables.
  • Be able to create and use deferred length character variables.
  • Work with arrays of character variables.
  • Be able to use characters as procedure dummy arguments and results.

Useful character operations


Relational operators

The meaning of relational operators == and so on in the context of characters is largely what one would expect from the standard ASCII sequence. E.g.,

FORTRAN

   "A" <  "b"      ! true
   "A" == "a"      ! false
   "A" /= "]"      ! true

Note the ordinal position of a single character in the ASCII sequence can be found via an intrinsic function, e.g.:

FORTRAN

   integer             :: i
   character (len = 1) :: c
   i = iachar("A")           ! i = 65
   c = achar(i)              ! c = "A"; requires 1 <= i <= 127

Remember that in ASCII the upper case characters A to Z have integer values 65 to 90, while the lower case characters a to z have integer values 97 to 122.

If character variables are compared, then each letter is compared left-to-right until a difference is found (or not). If the variables have different lengths, then the shorter is padded with blanks.

Some other intrinsic functions

The length of a character variable is provided by the len() intrinsic function.

The length with trailing blanks removed is len_trim(). To actually remove the trailing blanks, use trim(). This is often seen when concatenating fixed-length character strings using the // operator:

FORTRAN

  print *, "File name: ", trim(file_stub)//"."//trim(extension)

It’s also useful if you want to perform an operation on each individual character, e.g.,

FORTRAN

  do n = 1, len_trim(string)
    if (string(n:n) == 'a') counta = counta + 1
  end do

Note that the colon is mandatory in a sub-string reference, so a single character must be referenced, e.g.,

FORTRAN

   print *, "Character at position i: ", string(i:i)

Various other intrinsic functions exist for lexical comparisons, justification, sub-string searches, and so on.

Deferred length strings


The easiest way to provide a string which can be manipulated on a flexible basis is the deferred length character:

FORTRAN

  character (len = :), allocatable :: string

  string = "ABCD"         ! Allocate and assign string
  string = "ABCDEFG"      ! Reallocate and assign again

  string(:) = ''          ! Keep same allocation, but set all blanks

This may be initialised and updated in a general way, and the run time will keep track of book-keeping. This also (usually) reduces occurrences of trailing blanks.

If an allocation is required for which only the length is known (e.g., there is no literal string involved), the following form of allocation is required:

FORTRAN

  integer :: mylen
  character (len = :), allocatable :: string

  ! ... establish value of mylen ...

  allocate(character(len = mylen) :: string)

Allocatable strings will automatically be deallocated when they go out of scope and are no longer required. One can also be explicit:

FORTRAN

  deallocate(string)

if wanted.

Arrays of strings


We have seen that we can define a fixed length parameter, e.g.,:

FORTRAN

  character (len = *), parameter :: day = "Sunday"

Suppose we wanted an array of strings for “Sunday”, “Monday”, “Tuesday”, and so on. One might be tempted to try something of the form:

FORTRAN

  character (len = *), dimension(7), parameter :: days = [ ... ]

Exercise (2 minutes)

Check the result of the compilation of example3.f90.

The are a number of solutions to this issue. One could try to pad the lengths of each array element to be the same length. A better way is to use a constructor with a type specification:

FORTRAN

[character (len = 9) :: "Sunday", "Monday", "Tuesday", "Wednesday", ...]

Here the type specification is used to avoid ambiguity in how the list is to be interpreted.

Check you can make this adjustment to example3.f90.

Strings as dummy arguments, or results


If a character variable has intent in in the context of a procedure, it typically may be declared:

FORTRAN

  subroutine some_character_operation(carg1, ...)

     character (len = *), intent(in) :: arg1
     ...

This is also appropriate for character variables of intent inout where the length does not change.

For all other cases, use of deferred length allocatable characters is recommended. E.g.,

FORTRAN

  function build_filename(stub, extension) result(filename)

    character (len = *), intent(in)  :: stub
    character (len = *), intent(in)  :: extension
    character (len = :), allocatable :: filenane
    ...

A matching declaration in the caller is required.

Exercise (10 minutes)


Challenge

Exercise name

Write a subroutine which takes an existing string, and adjusts it to make sure it is all lower case. That is, any character between “A” and “Z” is replaced by the corresponding character between “a” and “z”.

Write an additional function to return a new string which is all lower case, leaving the original unchanged.

You can use the accompanying templates exercise_module1.f90 and exercise_program1.f90.

A solution to the exercise can be found in solution_program1.f90 and solution_module1.f90.

Key Points
  • String-handling can be problematic if there is no mechanism to keep track of the length of the string.
  • Remember that Fortran provides intrinsic functions such as trim() and operators such as concatenation // which can help with string manipulation.
  • Pay attention when using strings as dummy arguments or procedure results.

Content from Formats and edit descriptors


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How do I read input from the keyboard to my program?
  • How can I control the format of input and output of various types?

Objectives

  • Use the read statement analogously to write to obtain input from the keyboard.
  • Learn how to combine edit descriptors to create a format specifier.
  • Use format specifiers to control the appearance of output of the various types we have encountered.

Input and output


So far we have seen only output (via either print or write). Input is via the read statement.

FORTRAN

  use :: iso_fortran_env
  ...
  read (unit = input_unit, fmt = *) var-io-list

In very many respects, read looks much like write, in that it takes the same arguments, including unit number and format specifier. However, whereas write may have an I/O list involving expressions, read must have only the variables which will store the incoming values.

The standard input unit (input_unit from iso_fortran_env in the above) is typically the keyboard (or “screen”) for interactive use.

Format specifier


In addition to the list-directed or free-format * specifier, a format may be a string literal, a fixed character parameter, or a string constructed at run time. Some examples are:

FORTRAN

   read (*, *) var             ! read using list-directed, or free format
   print '(a)', "A string"     ! character literal '(a)'
   write (*, myformat) var     ! character variable (or parameter)
   write(unit = myunit) var    ! unformatted (no format)

Output with a format specifier, either string or *, is referred to as formatted I/O. This is to distinguish it from unformatted I/O; this is essentially a direct dump of the binary internal representation. For unformatted I/O the format specifier is simply omitted.

Binary output can have its uses, but more portable and flexible methods of “fast” output are probably to be preferred (e.g., via a library such as HDF5). We will not consider unformatted output any further in this course.

Edit descriptors


A format specifier consists of a comma-separated list of edit descriptors enclosed in parentheses. The edit descriptors determine how the internal representation of data is translated or converted to characters which can be displayed.

The formal and exhaustive statement of exactly what is allowed in an edit descriptor is rather, well, exhausting. It is easier to consider some common examples of the three different types of descriptor.

Data edit descriptors

Data edit descriptors feature a single character describing the type data format wanted, and a literal integer part indicating the total field width, and number of decimals. These include

FORTRAN

  iw        ! integer width w characters
  fw.d      ! floating point number total width w and d decimal characters
  ew.d      ! scientific notation with total width w and d decimal characters
  aw        ! character string total width w

The width is the total width of the field, and must be wide enough to accommodate any leading signs, decimal points, or exponents. Where there is a w.d the d indicates the number of digits to appear after the decimal point.

Data edit descriptors should correspond to the relevant item in the io-list, based on position. Some simple examples are:

FORTRAN

  integer :: ivar = 40
  real    :: avar = 40.0
  print "(  i10)", ivar           ! produces "40" with 8 leading spaces
  print "(f10.3)", avar           ! produces "40.000" with 4 leading spaces
  print "(e10.3)", avar           ! produces "0.400e+02" with 1 leading space

For scientific notation, the exponent will appear either with two digits (“E+dd” or “E-dd”), or three digits for exponents greater than 99 (“+ddd” or “-ddd”).

If a first significant digit is required before the decimal point in scientific notation (instead of a zero) the es edit descriptor can be used instead of e; otherwise it’s the same.

An engineering edit descriptor en is available. It also acts like a standard e descriptor, but exponents are limited to a subset …,-3,0,+3,+6,…, corresponding to standard SI prefixes milli-, kilo-, mega-, and so on. There will be an appropriate movement in the decimal point.

If an exponent greater than 999 is required, an optional e descriptor for the exponent itself is available:

FORTRAN

  print "(e14.3e4)", avar         ! Use at least 4 digits in exponent

Note that an integer descriptor is allowed to be of the form iw.d, in which case leading zeros are added to pad to the width w, if required.

Character string edit descriptors

One can use a string literal as part of the edit descriptor itself, e.g.,

FORTRAN

  real :: avar = 4.0
  print "('This is the result: ', e14.7)", avar

Control edit descriptors

There are a number of these. They do not correspond to an item in the io-list, but alter the appearance of the output. The most common is x which introduces a space (or blank). E.g.,

FORTRAN

   print "(i12,x,e12.6)", ivar, avar

This will ensure at least one space between the two items in the list.

If a leading plus sign is wanted, use the sp control, e.g.:

FORTRAN

  print "(sp,e12.6)", abs(avar)

Exercise (2 minutes)

Challenge

Exercise name

Compile and run the accompanying example1.f90 program, which provides some specimen formats. Some of the format specifiers have not been allowed a large enough width. What’s the result? What’s the solution?

Using gfortran produces the following output:

OUTPUT

Format i10:            40
Format i10.10: 0000000040
Format l10:             F
Format f10.3:      40.000
Format e10.3:   0.400E+02
Format en10.3: 40.000E+00
Format a10:    Hello
Format i1:     *
Format f10.3:  **********
Format e10.3:   0.180+309
Others:
Format sp,e12.3:   +0.180+309
Format e12.3e4:   0.180E+0309

Where the format specifiers are too narrow, the output is reduced to asterisks * in the space that has been provided. To fix this, the specifiers need to be widened. For example, the integer specifier on line 25 is i1, clearly not enough to print -40. Making it i3 allows it to output.

The next failed output uses f10.3. It’s trying to print a huge() number, where it was used to store the largest possible number of the same type as the original avar. As a real64, that’s approximately 1.80E+308. The format specifier provides a width of 10 characters to print a number with 309 digits (although we only have precision in the first 15 or so). The f format specifier is very definitely unsuitable; one of the e types such as es24.17 is much more appropriate.

Repeat counts


For a larger number of items of the same type, it can be useful to specify a repeat count. Simply place the items to be repeated within parentheses and prefix a literal integer count to the format, e.g.:

FORTRAN

   real, dimension(4) :: a = [ 1.0, 2.0, 3.0, 4.0]

   print "(4(2x,e14.7))", a(1:4)

Multiple subformats are comma-separated, much like the individual items, and subformats can also be nested. In this way, more complex formats can be constructed.

Complex variables

Variables of complex type are treated as two real values for the purposes of a format specifier. The real and imaginary part do not have to have the same edit descriptor.

Logical variables

A “safe” (portable) minimum width for an l (logical) edit descriptor is 7 characters to accommodate “.false.”, although some implementations may only produce an F.

New lines


If no new line at all is wanted, one can use the advance = no argument to write(), e.g.:

FORTRAN

  write (*, "('Input an integer: ')", advance = 'no')
  read (*, *) ivar

Non-advancing output must not use list-directed I/O.

Statement labels


Formally, a format specifier may also be a statement label. For example,

FORTRAN

   write (unit = myunit, fmt = 100) ia, ib, c

100 format(i3, i3, f14.7)

Here, the statement at the line with label 100 is the format keyword, which may be used to define a format specifier. This is very common in older forms of Fortran. However, as the reference and the statement can become widely separated, this can be troublesome. However, statement labels are useful in other contexts, as we will see when we come to error handling.

Exercise (5 minutes)


Challenge

Experimenting with formats

Using the example1.f90 program, check what happens if there is a mismatch between the format specifier and the data type in the I/O list. Choose an example, and run the program.

What happens if the number of items in the io-list is larger than the number of edit descriptors?

Additional exercise. The b edit descriptor can be used to convert integer values to their binary representation. For a 32-bit integer type, the recommended descriptor would be b32.32 to ensure the leading zeros are visible. You may wish to try this out.

Using the wrong type variable with a format specifier should still allow the code to compile, but you will receive a runtime error when it is encountered.

Any extra items in the io-list above what the format provides will not be printed.

Key Points
  • Control over the appearance of output is via the format specifier, which is a string.
  • This similar in spirit to the format string provided to printf() in C.
  • The use of a format allows predictable tabulation of output for human readers.

Content from Lunch


Last updated on 2026-02-21 | Edit this page

Break for lunch.

Content from Operations on external files


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How do I open and close files for input and output?
  • How do I handle any errors that might occur during I/O?

Objectives

  • Learn how to open and close files for input and output using a unit number.
  • Be able to read and write to an open file and handle errors that might arise.
  • Move around an open file using read to move forwards and backspace() and rewind() to move back.

Operations on external files


Writing data to, or reading data from, an external file is an essential part of a useful application.

Writing a file


File handles, or unit numbers are obtained using open(), and are used to direct the output of write() to the relevant file.

FORTRAN

  integer :: myunit

  open(newunit = myunit, file = 'filename.dat', form = 'formatted', &
       action = 'write', status = 'new')

  write(unit = myunit, *) data1    ! write data first record
  write(unit = myunit, *) data2    ! second record ... and so on

  close(unit = myunit, status = 'keep')

To read back the same data, we might use:

FORTRAN

   open(newunit = myunit, file = 'filename.dat', form = 'formatted', &
        action = "read", status = "old")

   read(unit = myunit, *) data1
   read(unit = myunit, *) data2

   close (unit = myunit, status = 'keep')

Unit numbers

A valid unit number can be assigned by using the newunit option to open() – this has been available since Fortran 2008. There is no need to choose your own (and it can be error-prone to do so).

Note that a given file can only be connected to one unit number at any given time.

open()

The arguments seen above include:

  • file: literal string or character variable or expression with the name of the file in the file system;
  • form: formatted or unformatted;
  • action: read, write or readwrite. An error may occur if inappropriate actions are performed;
  • status: one of old, new, replace, scratch, or unknown.

A file with status old is expected to exist, while an attempt to create a new file when one already exists with the same name will result in an error. If replace is specified, any existing file will be overwritten by a new file. If scratch is specified, a temporary file is created which will be deleted when close() is executed (or at the end of the program). The system will automatically choose a name for a scratch file if no file argument is present. The default status is unknown, which means the status is system dependent.

Formally, only the unit number is mandatory (and must appear first), but there is no reason not to provide as much information as possible.

close()

The unit number is again mandatory. The status is either keep or delete. If the status is omitted, the default is keep, except for scratch files.

inquire()


The inquire statement offers a way to obtain information on the current state of either unit numbers or files. There are a large number of optional arguments. One common usage is to check whether a file exists:

FORTRAN

  logical :: exists
  inquire( file = 'filename.dat', exist = exists)

Internal files


In some situations, it may be convenient to use a formatted read to generate a new string in memory (in the same way as sprintf in C). Fortran uses a so-called internal file, which is usually a character string. E.g.,

FORTRAN

   character (len = 10) :: buffer
   integer              :: ival
   ...
   read(buffer, fmt = "i10") ival

Here buffer takes the place of the input unit. This can be used, e.g., to create format strings at run time.

Recovery from errors


Operations on external files can be error-prone. While there is no formal exception mechanism in Fortran, some ability to recover is available.

Consider the following schematic example:

FORTRAN

subroutine read_my_file_format(myunit, ..., ierr)

  integer, intent(in)   :: myunit
  integer, intent(out)  :: ierr          ! error code

  character (len = 256) :: msg

  read (myunit, ..., err = 999, iomsg = msg) ...

  ! Everything was ok
  ierr = 0
  return

999 continue
  ierr = -1
  print *, "Error reading file: ", trim(msg)

  return
end subroutine real_my_file_format

We assume the relevant file has been opened successfully, and is connected to unit number myunit. If an error occurs at the point of the read statement, the err argument directs control to be transferred to the statement will label 999. In this case the system should provide a meaningful message describing the error.

If the read is successful, the routine completes at the first return statement with intent out ierr = 0.

Error handling for open() and close()

Both open and close statements provide optional arguments for error handling, illustrated schematically here with open():

FORTRAN

  integer :: ierr
  character (len = 128) :: msg

  open( newunit = myunit, ..., err = 900, iostat = ierr, iomsg = msg)
  • err: is a label in the same scope to which control is transferred on error;
  • iostat: an integer variable which is zero on success but positive if an error has occurred;
  • iomsg: if an error occurs, a system-dependent message will be assigned to msg.

If neither err nor iostat arguments are present, then an error may result in immediate termination of the program.

The variable msg should be a scalar character variable; the message will be truncated or padded appropriately.

Error handling for read() and write()

Error handling facilities for read() and write() are similar, and are illustrated here:

FORTRAN

  write (myunit, myformat, end = 999, err = 998, iostat = ierr, iomsg = msg) ..
  • end: label in same scope to which control is transferred on end-of-file;
  • err: label in same scope to which control is transferred on any other error; the label may be the same as end;
  • iostat: the integer error code is negative if end-of-record or end-of-file, or positive if another error (e.g., bad format conversion) or zero on success;
  • iomsg: should return a useful message on error.

The two negative iostat cases can be distinguished via calls to the intrinsic functions

FORTRAN

  is_iostat_end(ierr)
  is_iostat_eor(ierr)

stop statement


If one really can’t continue, then execution can be terminated immediately via the stop statement. This has an optional message string argument.

FORTRAN

  stop "Cannot continue"

In general one should try to recover by returning control to the caller, so stop is a last resort.

Moving around an open file


It is sometimes useful to be able to reposition oneself in an open file so that a given record can be read (or written) more than once. This can be done using backspace() with the relevant unit number. Formally

FORTRAN

  backspace([unit = ] unit-number [, iostat = iostat] [, err = label])

This steps back one record.

It is also possible to reposition to the beginning of the file, again with the relevant connected unit number using rewind().

FORTRAN

  rewind([unit = ] unit-number [, iostat = iostat] [, err = label])

Exercise (20 minutes)


Challenge

Writing a simple image

Portable bit map (PBM, PGM, PPM) is a very simple image format which can be expressed as an ASCII file. See the description at Wikipedia.

In the template program1.f90, we establish a two-dimensional logical array of a fixed size, and we want to write out a file that can be viewed in “P1” format (file extension .pbm ) by an image viewer.

The data is initialised with a suitable pattern to check that the image is correct (it is the right way up and is not a mirror). A little care is required in the rows and columns (look closely at the example).

Provide a module with the subroutine write_pbm() with three arguments as in the template program. Note the template program expects a module solution_module.

Additional exercises: repeat for integer data (to “P2” format .pgm) and floating point data (to “P3” format .ppm).

You can check any image you produce by either downloading it to your machine with scp or rsync, or you can view it directly on ARCHER2 by logging in with X11 forwarding enabled, running module load imagemagick and then display test49.pbm.

A solution to this problem appears in the later episode on interfaces.

You should be able to produce an output PBM that looks like the example shown below.

Sample PBM output.
Key Points
  • Writing data to, or reading data from, an external file is an essential part of a useful application.
  • Unit numbers are integers which are used as handles to open files.
  • Errors from I/O can be handled with labels and by examining the returned message.

Content from Using namelists


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What is a namelist?
  • How can I use a namelist to read and write grouped data?

Objectives

  • Understand what a namelist is and how to create declare one.
  • Be able to read and write namelists from file.
  • Understand how namelists store structured data such as arrays and derived types.

Fortran namelists


Fortran supports a special form of file I/O called namelists which enable the grouping of variables for reading and writing.

The namelist specification


The namelist associates a name with a list of variables. The namelist is declared using a namelist construct which takes the form

FORTRAN

  namelist /namelist-group-name/ variable-name-list

where the namelist-group-name is surrounded by forward slashes / and the variable-name-list is a comma separated list of variables, e.g.:

FORTRAN

  integer :: a, b, c
  namelist /ints/ a, b, c

Namelists in a file


A namelist file is plain text and may contain one or more namelists. Each of these is begun by &namelist-group-name and terminated by /. Variable-value assignments occur in the body of the namelist, e.g. the file may contain

FORTRAN

&ints
a = 1
b = 2
c = 3
/

The ordering of variables in the namelist does not matter, so the previous example could also have been written as

FORTRAN

&ints
c = 3
a = 1
b = 2
/

Reading a namelist


A common usecase for namelists is to specify parameters for a program at runtime. For example a simulation code might read its configuration from a namelist called run as follows:

FORTRAN

&run
name = "TGV" ! Case name
nsteps = 100 ! Number of timesteps
dt = 0.1     ! Timestep
/

As you can see, an extra nice feature of namelists is their support for comments using the same ! character as in Fortran code.

To read the namelist from a file, the namelist-group-name is passed as the argument to read(), so in order to read this run namelist we might do the following:

FORTRAN

  integer :: myunit
  character(len=:) :: name
  integer :: nsteps
  real :: dt

  namelist /run/ name, nsteps, dt

  open(newunit = myunit, file = 'config', action = 'read', status = 'old')
  read(myunit, run)
  close(myunit)

Note that any variables not set in the namelist file will retain their initial values.

Exercise (5 minutes)

Challenge

Reading a namelist from file

Using namelist-read.f90, confirm that the runtime values can be specified by changing the contents of the namelist in config.nml. Modify the program to specify a default value for dt and confirm that dt retains its value unless otherwise set in the namelist.

What happens if the value set is not of the same type as the variable specification in the program?

What happens if you add an unexpected value to the namelist?

If the value in the namelist is not of the same type as the program’s variable, you will receive an error stating that it is mismatched.

Any unexpected values will also trigger an error stating that the namelist object cannot be matched to a variable.

Reading multiple namelists

As stated earlier, we can have multiple namelists within a single file. Continuing with our simulation example we might additionally want to specify some numerical schemes for the simulation, e.g. adding the schemes namelist:

FORTRAN

&run
name = "TGV" ! Case name
nsteps = 100 ! Number of timesteps
dt = 0.1     ! Timestep
/

&schemes
advection = "upwind"  ! Advection scheme
diffusion = "central" ! Diffusion scheme
transient = "RK3"     ! Timestepping scheme
/

We can read the values for each of these namelists as outlined previously, but note, however that the order of the namelists themselves is important. After reading a namelist the file reader will be positioned at its end, and trying to read a namelist that occurred previously in the file will fail. Either the reading subroutine must match the order of the namelists which occur in the file or file motion calls such as rewind() should be used to ensure that reading is independent of order.

Exercise (5 mins)

Challenge

Reading multiple namelists

Extend the example program namelist-read.f90 to read the numerical schemes namelist from config-full.nml.

(Optional) Ensure that your program can read the run configuration and the numerical schemes regardless of the order in which they appear.

Add some character variables to store the items from the schemes namelist, declare the namelist itself with those variables, then, after reading the run namelist, read schemes. To make sure it can read the two schemes in any order, do a rewind(nmlunit) in between them. This way, you read through to the first namelist, reset position to the start, then read through to the second.

If they are in the wrong order and you don’t do this, you will encounter an end of file error.

Writing a namelist


Writing a namelist works similarly to reading one. With an open file, or other output device, passing the namelist as the argument to write() will write the namelist to the file.

FORTRAN

  integer :: myunit
  integer :: a, b, c
  namelist /ints/ a, b, c

  a = 1
  b = 2
  c = 3

  open(newunit = myunit, file = 'output.nml', action = 'write', status = 'new')
  write(myunit, ints)
  close(myunit)

will output the following to the new file output.nml:

FORTRAN

&INTS
 A=1          ,
 B=2          ,
 C=3          ,
 /

Exercise (10 minutes)

Challenge

Writing namelists to file

Write a program to write the simulation parameters as a namelist so that it can be read by the previous example program.

(Optional) If you completed the extension to read the namelists in any order, confirm that this works with your new program when writing in any order.

A solution writing the run namelist can be found in namelist-write.f90.

Handling complex data


So far we have only considered simple scalars and strings, but namelists also support I/O of more complex data structures such as arrays or user-defined types. These are handled similarly to the code we have seen so far.

Exercise (10 minutes)

Discussion

Writing complex data to namelists

An example program namelist-complex.f90 is provided to write more complex data structures to a namelist. Try running this program and inspecting the output to see how these data structures are represented.

Modify the program to read the namelist back into data and confirm that the data is correct, for example by testing array elements match their expected values.

Key Points
  • Namelists are declared using the namelist /namelist-group-name/ variable-name-list construct.
  • Read and write namelists by passing them directly to the read and write statements.
  • Remember that namelists cannot be read out of order from a file without first moving backwards through the file.

Content from Break


Last updated on 2026-02-21 | Edit this page

Comfort break.

Content from Exercises


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What are some problems I can work on?

Objectives

  • Continue to work on exercises from taught material and the larger two problems from yesterday.

We will use the rest of today to work on any of the taught exercises you would like to spend more time on, as well as the tri-diagonal and Game of Life exercises from yesterday afternoon’s session .

Key Points
  • Gain more experience putting your new knowledge to use.

Content from Structures: derived types


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How can I group together variables to make more complex structures?
  • How are these derived types defined and then initialised?

Objectives

  • Understand what a derived type is.
  • Learn how to define a derived type.
  • Learn the different ways of initialising a derived type variable.
  • Get a glimpse of more advanced object oriented methodology.

Derived types


We have seen intrinsic types, such as integer and character. However, in many cases it can be useful to create structured types combining multiple types together in some way of our choosing. We call these derived types.

Type definitions


A derived type with two components would be declared, e.g.,

FORTRAN

  type :: my_type
    integer                         :: nmax
    real, dimension(:), allocatable :: data
  end type my_type

Components may be intrinsic data types (all declared in the usual way), or derived types.

A variable of this type is declared

FORTRAN

  type (my_type) :: var

and individual components are referenced with the component selector %, e.g.,

FORTRAN

  var%nmax = 10
  ...
  print *, "Values are ", var%data(1:3)

The component selector is the same as we have seen earlier for the complex intrinsic type – recall that the real and imaginary components of a complex variable z are accessed with z%re and z%im respectively.

An array of types is defined in the usual way, and the component selector is applied to individual elements, e.g.,

FORTRAN

  type (my_type), dimension(10) :: var
  ...
  var(1)%nmax = 100

Dummy arguments to procedures are declared in the same way as for intrinsic types with the appropriate attribute list, including intent.

Put type definitions in a module

If a type definition is placed in the specification part of a module, it can be made available consistently elsewhere via use association.

Some derived type features require that the definition be in a module.

Scope of components

Formally, we have

FORTRAN

  type [ [, attribute-list] :: ] type-name
    [private]
    component-part
  [ contains
    procedure-part ]
  end type [ type-name ]

The default situation is for both the type and its components to be public. This may be made explicit by

FORTRAN

  type, public :: my_type
    ...
  end type my_type

If one wants a public type with private components (an opaque type), use

FORTRAN

  type, public :: my_opaque_type
    private
    ...
  end type my_opaque_type

Externally, other program units will be able to reference this opaque type, but will not be allowed to access the components (a compiler error).

If a type is only for use within the module in which it is defined, then it can be declared private in the attribute list.

Type constructors

For types with public components, it is possible to use a structure constructor to provide initialisation, e.g.:

FORTRAN

  type, public :: my_type
     integer :: ia
     real    :: b
     complex :: z
  end type my_type

  ...
  type (my_type) :: a

  a = my_type(3, 2.0, (0.0, 1.0))

Values or expressions can be used, but must appear in the order specified in the definition of the components. An allocatable or pointer (next episode) component must appear as null() in a constructor expression list.

Default initialisation

A type may be defined with default initial values. One notable exception is that allocatable components do not have an initialisation. E.g.:

FORTRAN

  type :: my_type
    integer                            :: nmax = 10
    real                               :: a0 = 1.0
    integer, dimension(:), allocatable :: ndata
  end type

A default initialisation can be applied by using an empty constructor:

FORTRAN

  type (my_type) :: a

  a = my_type()

For an allocatable component, the result is a component with a not allocated status.

Warning: some compilers can’t manage an empty constructor for allocatable components. The appropriate expression in the constructor is null().

Exercise (5 minutes)

Challenge

A type to store a random number generator

The accompanying example (module1.f90 and program1.f90) provides an implementation of a very simple pseudo-random number generator. This is a so-called linear congruential generator.

The module provides a derived type to aggregate the multiplier a, the seed or state s, the increment c and the modulus m. These have some default values. Practical implementations often choose c = 0.

Compile the program, and check the first few numbers returned in the sequence. The key to obtaining acceptable statistics is to identify some appropriate values of a and m (e.g., those given in the default).

Check you can introduce some new values of a and m using the structure constructor (a spectacularly bad choice is suggested in the code).

What happens if you make the components of the type private? What would you then have to provide to allow initialisation?

Running the code as provided gives the following output:

OUTPUT

 Step  1,  45991
 Step  2,  2115172081
 Step  3,  17451818
 Step  4,  1615161307
 Step  5,  1424320507
 Step  6,  1230752996

Changing to use the suggested bad RNG values means doing

FORTRAN

type (my_rng) :: rng = my_rng(1, 1, 0, 2147483647)

and this produces the following output (note Cray Fortran being helpful with the first line)

OUTPUT

 Step  2*1
 Step  2,  1
 Step  3,  1
 Step  4,  1
 Step  5,  1
 Step  6,  1

Making the RNG type’s components private means doing:

FORTRAN

  type, public :: my_rng
    private
    integer (int64) :: a = 45991
    integer (int32) :: s = 1
    integer (int64) :: c = 0
    integer (int64) :: m = 2147483647
  end type my_rng

Trying to compile the program while setting the a etc. values will cause a compiler error; those components are no longer public, and the compiler knows it shouldn’t touch them. If we wanted to change those values while my_rng is opaque, we’d need to write another module procedure to do so.

Default input/output for derived types


List-directed output for derived types can be used to provide a default output in which each component appears in order, schematically:

FORTRAN

  type (my_type) :: a
  ...
  write (*, fmt = *) a
  write (*, fmt = *) a%component1, a%component2, ...

or one can apply a specific format to correspond to the known type components.

Exercise (15 minutes)

Challenge

A tri-diagonal structure

In the earlier material on using arrays as dummy arguments we implemented the tri-diagonal solver as a module procedure. Implement a derived type to hold the relevant data for the tri-diagonal matrix, ie., at least the three diagonals.

Define a function which returns a fully initialised matrix type based on arrays holding the three diagonals. Refactor the solver routine to use the new matrix type.

Additional exercise: A very simple tridiagonal matrix may have all diagonal elements the same, and all off-diagonal elements the same. Write an additional function to initialise such a matrix from two scalar values.

A template for the exercise can be found in exercise_program.f90 and exercise_module.f90; or you can use your own version that you have been developing to this point.

Your new tri-diagonal matrix type should look something like this:

FORTRAN

  type, public :: tri_matrix
    integer :: nmax
    real (mykind), dimension(:), allocatable :: a     ! lower (2:nmax)
    real (mykind), dimension(:), allocatable :: b     ! diag (1:nmax)
    real (mykind), dimension(:), allocatable :: c     ! upper (1:nmax-1)
  end type tri_matrix

Implementations of the solution are available in solution_program.f90 and solution_module.f90.

Key Points
  • The ability to aggregate related data in a structure is important.
  • Fortran offers the _derived type_ in addition to intrinsic types.
  • In its simplest form, one may think of this as the analogue to a C struct.
  • Derived types also form the basis of aggregation of data and related operations or procedures (viz. object-oriented programming); however, this introductory course will only touch on this feature.

Content from Pointers and targets


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How do I use a pointer to reference another variable?
  • Can I alias variable names?
  • Can I change the sizes of allocatable arrays?

Objectives

  • Understand how to create pointers, targets, and how to associate the two to create references.
  • Learn how to use an associate block to provide aliases to variables.
  • Learn how to efficiently reallocate storage for an allocatable array if its size needs to be changed.

Pointer attribute


A pointer may be declared by adding the pointer attribute to the relevant data type, e.g.,

FORTRAN

integer, pointer :: p => null()

In this case we declare a pointer to integer p, which is initialised to the special value null(). The pointer is said to be unassociated. This is a different state to undefined (i.e., without initialisation).

Note that pointer assignment uses => and not =. A common cause of errors is to forget the > if pointer assignment is intended.

It is important to be able to check that a given pointer is not null(). This is done with the associated() intrinsic; schematically,

FORTRAN

   integer, pointer :: p => null()
   ...
   if (associated(p)) ... do something

If a pointer is not initialised as pointing to a target variable, it should be initialised with null() to avoid undefined behaviour when testing with associated(). Pointers may otherwise be used in expressions and assignments in the usual way.

If one wishes to have a pointer to an array, the rank of the pointer should be the same as the target:

FORTRAN

  real, dimension(:),   pointer :: p1
  real, dimension(:,:), pointer :: p2

and so on.

Targets


A pointer may be associated with another variable of the appropriate type (which is not itself a pointer) by using the target attribute:

FORTRAN

  integer, target  :: datum
  integer, pointer :: p => null()

  ...

  p => datum

The pointer is now said to be associated with the target. We can now perform operations on datum vicariously through p. E.g., a standard assignment would be

FORTRAN

  integer, target  :: datum = 1
  integer, pointer :: p => null()

  ...

  p => datum     ! pointer assignment
  p = 2          ! normal assignment

leaves us with datum = 2. There is no dereferencing in the C fashion; the data is moved as the result of the normal assignment.

The target attribute is there to provide information to the compiler about which variables may be, and which variables may not be, associated with a pointer. This somewhat in the same spirit as the C restrict qualifier.

Note that there is an optional target argument to the associated() intrinsic, which allows the programmer to inquire whether a pointer is associated with a specific target, e.g.,

FORTRAN

   associated(p, target = datum)   ! .true. if p => datum

Exercise (2 minutes)

Challenge

Associating a pointer

Try to compile the accompanying example1.f90, which is an erroneous version of the code above. See what compiler message you get (if any). Fix the problem.

Check you can use the associated() function to print out the status of p before and after the pointer assignment.

The p pointer can’t be associated with datum as the latter doesn’t have the target attribute; the compilation should fail with an error. If you add the attribute, it should compile and run:

FORTRAN

  integer, target  :: datum = 1

Check the associated() status of p along these lines:

FORTRAN

  print *, "p associated?", associated(p)

Pointers as aliases

One common use of pointers is to provide a temporary alias to another variable (where no copying takes place). As a convenience, one can use the associate construct, e.g.:

FORTRAN

  real :: improbably_or_tediously_long_variable_name
  ...
  associate(p => improbably_or_tediously_long_variable_name)
     ! ... lots of operations involving p ...
  end associate

Note that there is no requirement here to have the target attribute in the original declaration (and there’s no explicit declaration of p). Any update to p in the associate block will be reflected in the target on exit.

Multiple associations can be made in the same block; simply provide a comma separated list in the parentheses. Also of note is that the selector on the right hand side of the association can be either a variable or an expression.

Exercise (2 minutes)

Challenge

Using an associate construct

Compile, and check the output of the accompanying code in example2.f90.

You should see that the association is made to a strided section of the array r1:

FORTRAN

associate(p => r1(2::2))

Within the associate block p acts to point to the second, fourth and sixth elements of r1.

Pointers to establish storage


One common use of pointers is for linked data structures. For example, an entry in a linked list might be represented by the type

FORTRAN

  type :: my_node
    integer                 :: datum
    type (my_node), pointer :: next
  end type my_node

This sort of dynamic data structure requires that we establish or destroy storage as entries are added to the list, or removed from the list.

FORTRAN

  subroutine my_list_add_node(head, datum)

    ! Insert new datum at head of list

    type (my_node), pointer, intent(inout) :: head
    integer,                 intent(in)    :: datum

    type (my_node), pointer :: pnode

    allocate(pnode)       ! assume no error
    pnode%datum = datum
    pnode%next => head
    head => pnode

  end subroutine my_list_add_node

In the subroutine, we can see the pointer pnode is allocated. This dynamically creates a my_node variable to contain the new datum. At the end of the procedure, the pnode pointer is itself used as a target for the head pointer.

Pointer or allocatable array?

The question may now arise: should you use pointers or allocatable arrays? If you just want to establish storage for arrays, the answer is almost certainly that you should use allocatable. An allocatable array will almost certainly be held contiguously in memory, whereas pointers are a more general data structure which may have to accommodate a stride.

If an allocatable array is not appropriate, then a pointer may be required.

If you just require a temporary alias, the associate construct is recommended.

Reallocating array storage


If one needs to increase (or decrease) the size of an existing allocatable array, the move_alloc() intrinsic is useful. E.g., if we have an integer rank one array

FORTRAN

  integer, dimension(:), allocatable :: iorig

and establish storage of a given size, and some relevant initialisations, we may then wish to increase the size of it.

FORTRAN

  integer :: nold
  integer, dimension(:), allocatable :: itmp

  nold = size(iorig)
  allocate(itmp(2*nold))           ! double size; assume no error
  itmp(1:nold) = iorig(1:nold)     ! copy existing contents explicitly
  call move_alloc(itmp, iorig)
  ! itmp deallocated, iorig now refers to the memory that had been itmp

This minimises the number of copies involved in re-assigning the original storage.

Challenge

move_alloc() efficiency

A thought exercise. How many copies would be required if move_alloc() was not available when enlarging the size of an existing allocatable array?

If move_alloc() were not available, we would need to make two copies rather than one. Analogously to the above example, we would have to perform the following to double the storage in iorig:

FORTRAN

  integer :: nold
  integer, dimension(:), allocatable :: itmp

  nold = size(iorig)
  allocate(itmp(nold))             ! allocate temporary storage for original data
  itmp(:) = iorig(:)               ! first copy from original storage into the temporary
  deallocate(iorig)                ! deallocate the original array
  allocate(iorig(2*ndold))         ! and then reallocate at twice the size
  iorig(1:nold) = itmp(:)          ! second copy from the temporary into the new storage
  deallocate(itmp)                 ! deallocate the temporary

Arrays of pointers

A small trick is required to arrange an array of pointers. Recall that

FORTRAN

  real, dimension(:), pointer :: a

is a pointer to a rank one array, and not a rank one array of pointers. If one did want an array of such objects, it can be achieved by wrapping it in a type:

FORTRAN

  type :: pointer_rr1
    real, dimension(:), pointer :: p => null()
  end type pointer_rr1

  type (pointer_rr1), dimension(10) :: a

  a(1)%p => null()

So a is a rank one array of the new type, each element having the component p which is a pointer to a real rank one type.

Key Points
  • Pointers are extremely useful for certain types of operations: they provide a way to refer indirectly to other variables or names (they act as an _alias_), or can be used for establishing dynamic data structures.
  • Remember to always initialise a pointer, to null() if necessary.
  • If a pointer is allocated, memory for its type is allocated and the pointer becomes associated.
  • The move_alloc() intrinsic function moves a memory allocation from one allocatable variable to another.

Content from Procedures again: interfaces


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What is an interface and why might I need to write one?
  • Can I use a function or a subroutine as an argument to another procedure?
  • How can I write a procedure that will accept arguments of different types?
  • Can I overload arithmetic and relational operators to work with my derived types?
  • Can I write a function that will accept both scalar and array arguments?

Objectives

  • Understand the need for interfaces and recognise when they are necessary.
  • Be able to write an interface to allow the passing of a procedure as an actual argument.
  • Understand and be able to implement overloading and polymorphism.
  • Understand what an elemental function is and be able to write one.

So far, we have demanded that all procedures be defined as part of a module. This has the important advantage that it makes the interface to the function or subroutine explicit to the compiler, and to other program units via the use statement. The compiler can then check the order and type of the actual arguments.

Other situations arise where we may need to give the compiler additional information about the interface.

An external function


Consider a program unit (which may or may not be a separate file) which contains a function declaration outside a module scope. E.g.,

FORTRAN

  function my_mapping(i) result(imap)
    integer, intent(in) :: i
    integer             :: imap
    ...
  end function my_mapping

If we compile a program which includes a reference to this function, an error may result. We have not given the compiler any information about how the function is meant to be called.

It is possible to provide the compiler with some limited information about the return value of the function with the external attribute (also available as a statement). E.g.,

FORTRAN

  program example

    implicit none
    integer, external :: my_mapping
    ...

However, we have still not given the compiler full information about the interface. The interface is said to remain implicit. To make it explicit, the interface construct is available:

FORTRAN

  program example

    implicit none
    interface
      function my_mapping(i) result(imap)
        integer, intent(in) :: i
        integer             :: imap
      end function my_mapping
    end interface

    ...

This has provided a full, explicit, statement about the interface of the function my_mappping(). (Note that the dummy argument names are not significant, but the function name and the argument types and intents are.)

The compiler should now be able to check the arguments are used correctly in the calling program (as if we had declared the function in a module).

Interface blocks are necessary in other contexts.

Exercise (5 minutes)

Challenge

Externals and interfaces

To illustrate the points made above, a very simple external function is defined in the file external.f90, and an accompanying program example1.f90 calls the function therein.

Try to compile the two files, e.g.:

FORTRAN

$ ftn external.f90 example1.f90

What happens?

Try adding the appropriate external declaration

FORTRAN

  integer, external :: array_size

What happens if you try to compile the program now? (Note at this point that there are no modules involved, so no .mod files will appear).

Then try running the program. Is the output what you expect?

Finally, remove the external declaration and try to introduce the correct interface block. What happens now?

Without either the external declaration of the function or an interface, the compilation will simply fail. gfortran produces the following:

OUTPUT

example1.f90:24:33:

   24 |   print *, "The array size is: ", array_size(a), size(a)
      |                                 1
Error: Function 'array_size' at (1) has no IMPLICIT type

Adding the external declaration allows compilation to succeed, but the following output is produced on running the program:

OUTPUT

 The array size is:            0           6

The function call returns an incorrect value of 0 where the (3,2) array has an actual size of 6.

Removing the external declaration and adding an interface matching the function in external.f90 as follows will help (and you may have already spotted the issue):

FORTRAN

  interface
    function array_size(a) result(isize)
      real, dimension(:), intent(in) :: a
      integer                        :: isize
    end function array_size
  end interface

Compiling now produces an error again, as reported here by gfortran:

OUTPUT

example1.f90:23:45:

   23 |   print *, "The array size is: ", array_size(a), size(a)
      |                                             1
Error: Rank mismatch in argument 'a' at (1) (rank-1 and rank-2)

In the main program we were attempting call array_size() on a rank two array, while in external.f90 we implemented it for rank one arrays. Now that we have an interface, the compiler can see that the way we’re calling the function is incorrect, so it produces the error and fails compilation.

This is preferable to the external declaration by which we essentially asked the compiler to trust that it can call array_size() however we tell it to do so, even though it was in this case incorrect.

Passing functions or subroutines as arguments


Sometimes it is convenient to provide a procedure as an argument to another function or subroutine. Good examples of this are for numerical optimisation or numerical integration methods where a user-defined function needs to be evaluated for a series of different arguments which cannot be prescribed in advance.

This can be done if an interface block is provided which describes to the calling procedure the function that is the dummy argument. E.g.,

FORTRAN

  subroutine my_integral(a, b, afunc, result)
    real, intent(in) :: a
    real, intent(in) :: b
    interface
      function afunc(x) result(y)
        real, intent(in) :: x
        real             :: y
      end function afunc
    end interface
    ...

The function dummy argument has no intent. One cannot in general use intrinsic procedures as actual arguments.

Limited polymorphism


We have seen a number of intrinsic functions which take arguments of different types, such as mod(). Such a situation where the arguments can take a different form is sometimes referred to as limited polymorphism (or overloading).

However, it is not possible in Fortran to define two procedures of the same name, but different arguments (at least in the same scope). We need different names; suppose we have two module sub-programs, schematically:

FORTRAN

   subroutine my_specific_int(ia)
     integer, intent(inout) :: ia
     ... integer implementation ...
   end subroutine my_specific_int

   subroutine my_specfic_real(ra)
     real, intent(inout) :: ra
     ... real implementation ...
   end subroutine my_specific_real

A mechanism exists to allow the compiler to identify the correct routine based on the actual argument when used with a generic name. This is:

FORTRAN

  interface my_generic_name
    module procedure my_specific_int
    module procedure my_specific_real
    ...
  end interface my_generic_name

This should appear in the specification (upper) part of the relevant module. The two specific implementations must be distinguishable by the compiler, that is, at least one non-optional dummy argument must be different.

Exercise

Challenge

An interface for polymorphic PBMs

In the earlier episode on I/O, we wrote a module to produce a .pbm image file. The accompanying module pbm_image.f90 provides two implementations of such a routine: one for a logical array, and another for an integer array.

Check you can add the appropriate interface block with the generic name write_pbm to allow the program example3.f90 to be compiled correctly.

We need to add an interface to the module specification for write_pbm which allows use of both the write_logical_pbm and write_integer_pbm subroutines. You should be able to follow the description above to do so. The only minor complication is that the module uses private by default; unless we specify public for the new generic interface, it will remain hidden from the main program.

Putting this together, the following should allow you to compile and run the program with no errors:

FORTRAN

  public :: write_pbm
  interface write_pbm
    module procedure write_logical_pbm
    module procedure write_integer_pbm
  end interface

With this, the internal workings of the module are hidden away and its use is entirely via write_pbm.

Operator overloading


For simple derived types it may be meaningful to define relational and arithmetic operators. For example, if we had a date type such as

FORTRAN

  type :: my_date
    integer :: day
    integer :: month
    integer :: year
  end type my_date

it may be meaningful to ask whether two dates are equal and so on (it would not really be meaningful to add one date to another).

One can write a function to do this:

FORTRAN

  function my_dates_equal(date1, date2) result(equal)
    type (my_date), intent(in) :: date1
    type (my_date), intent(in) :: date2
    logical                       equal
    ! ...
  end function my_dates_equal

As a syntactic convenience, it might be useful to use == in a logical expression using dates. This can be arranged via

FORTRAN

  interface operator(==)
    module procedure my_dates_equal
  end interface

Again this should appear in the relevant specification part of the relevant module. Such overloading is possible for relational operators ==, /=, >=, <=, > and <. If appropriate, overloading is also available for arithmetic operators +, -, *, and /.

It is also possible to overload assignment =.

Elemental functions


Again, we have seen that some intrinsic functions allow either scalar or array actual arguments. The same effect can be achieved for a user-defined function by declaring it to be elemental. The procedure is declared in terms of a scalar dummy argument, but then may be applied to an array actual argument element by element.

Such a procedure should be declared:

FORTRAN

  elemental function my_function(a) result(b)
    integer, intent(in) :: a
    integer             :: b
    ! ...
  end function my_function

An invocation should be, e.g.:

FORTRAN

   iresult(1:4) = my_function(ival(1:4))

All arguments (and function results) must conform. An elemental routine usually must also be pure.

Exercise (5 minutes)

Challenge

Elemental conversion from logical to PBM

In the pbm_image.f90 module there is a utility function logical_to_pbm() which is used in write_logical_pbm() to translate the logical array to an integer array. Refactor this part of the code to use an elemental function.

The function logical_to_pbm() is currently pure and takes in a scalar logical lvar to return the scalar integer ivar. The function is currently pure. Everything is already in place to convert the function to elemental:

FORTRAN

  elemental function logical_to_pbm(lvar) result (ivar)

    ! Utility to return 0 or 1 for .false. and .true.

    logical, intent(in) :: lvar
    integer             :: ivar

    ivar = 0
    if (lvar) ivar = 1

  end function logical_to_pb

All that remains is to have write_logical_pbm() call this new elemental version. Further down in that function you will see the old nested loop to move through the map array and from it use logical_to_pbm() to fill the imap array:

FORTRAN

    do j = 1, size(map, dim = 2)
       do i = 1, size(map, dim = 1)
          imap(i,j) = logical_to_pbm(map(i,j))
       end do
    end do

With the new elemental version of logical_to_pbm(), we can replace this entire structure with the single line:

FORTRAN

imap(:,:) = logical_to_pbm(map(:,:))

Exercise (20 minutes)


Challenge

Integrating a function

Write a module/program to perform a very simple numerical integration of a simple one-dimensional function f(x). We can use a trapezoidal rule: for lower and upper limits a and b, the integral can be approximated by

  (b - a)*(f(a) + f(b))/2.0

We can go further and split the interval between a and b into small sections of size h = (b - a)/n. In a similar manner, approximating the integral of each small section with a trapezium allows us to estimate the total integral to be

  h*(f(a) + sum + f(b))/2.0

with the sum

  sum = 0.0
  do k = 1, n-1
    sum = sum + 2.0*f(a+k*h)
  end do

Write a procedure that will take the limits a and b, the integer number of steps n, and the function, and returns a result.

To check, you can evaluate the function cos(x) sin(x) between a = 0 and b = pi/2 (the answer should be 1/2). Check your answer gets better for value of n = 10, 100, 1000.

A sample solution is provided in integral_program.f90 and integral_module.f90.

Key Points
  • An interface provides important information about a procedure to the compiler. Without this information, the compiler may not be able to use it.
  • When using modules, interfaces are generated automatically and provided to the compiler.
  • If not in a module, a function can be made visible to the compiler by declaring it with the external attribute, but the compiler can go further by checking the argument types if you write a full, explicit interface block.
  • Interfaces can also be used to implement limited polymorphism and operator overloading for derived types.
  • An elemental function is one which can be applied to a scalar actual argument or element-by-element to an array actual argument.

Content from Break


Last updated on 2026-02-21 | Edit this page

Comfort break.

Content from Miscellaneous


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How can I retrieve any command line arguments I want to give my program?
  • How can I examine environment variables?
  • Can I have my Fortran code execute OS commands on the terminal?
  • Can I get the time and date?

Objectives

  • Learn several useful procedures that allow your programs to interact with the outside shell and environment.
  • Learn how to obtain the time and date and be able to time sections of code.

There a a number of features which have not been covered so far, but are useful in certain contexts.

Interacting with the environment


Command line arguments

If you wish to write a program which makes use of command line arguments, or simply wish to know the executable name at run time, the command line can be retrieved.

The number of command line arguments is returned by the function

FORTRAN

command_argument_count()

This returns an integer: zero if the information is not available, or the number of arguments not including the executable name itself.

The entire command line can be retrieved as a string via

FORTRAN

  call get_command(command = cmd, length = len, status = stat)

This returns the command line as a string (truncated or padded with spaces as appropriate), the integer length of the command line string, and an integer status which will be non-zero if the information is not available. All the arguments are optional.

Individual command line arguments based on their position can be retrieved using the subroutine

FORTRAN

  subroutine get_command_argument(position, value, length, status)
    integer,                       intent(in)  :: position
    character (len = *), optional, intent(out) :: value
    integer,             optional, intent(out) :: length
    integer,             optional, intent(out) :: status

Here, position is zero for the (executable) command itself and positive for others. The remaining arguments are: value is the command; length is the length of the command, and istat returns 0 on success, -ve if the arg string is too short, or +ve if the information is not available.

Environment variables

A similar routine exists for inquiry about environment variables:

FORTRAN

subroutine get_environment_variable(name, value, length, status, trim_name)
  character (len = *),           intent(in)  :: name
  character (len = *), optional, intent(out) :: value
  integer,             optional, intent(out) :: length
  integer,             optional, intent(out) :: status
  logical,             optional, intent(in)  :: trim_name

This will return the value associated with the given name if it exists. Various non-zero status error conditions can occur, including a value of 1 if the variable does not exist, or -1 if the value is present, but is too long to fit in the string provided.

System commands

It is sometimes useful to pass control of execution back to the operating system so that some other command can be used.

FORTRAN

subroutine execute_command_line(command, wait, iexit, icmd, cmdmsg)

  character (len = *),           intent(in)    :: command
  logical,             optional, intent(in)    :: wait
  integer,             optional, intent(inout) :: iexit
  integer,             optional, intent(out)   :: icmd
  character (len = *), optional, intent(inout) :: cmdmsg

The command should be a string. The wait argument tells the routine to return only when the command has finishing executing. The iexit argument gives the return value of the command. The icmd argument returns a positive value if the command fails to execute (e.g., the command was not found), or negative if execution is not supported, or zero on successful execution. An informative message should be returned in cmdmsg if icmd is positive.

It is recommended to use wait = .true. always. Portable programs should use system commands with extreme caution, or not at all.

Time and date from date_and_time()

Use, e.g.,

FORTRAN

  character (len = 8)   :: date        ! "yyyymmdd"
  character (len = 10)  :: time        ! "hhmmss.sss"
  character (len = 5)   :: zone        ! "shhmm"
  integer, dimension(8) :: ivalues     ! see below

  call date_and_time(date = date, time = time, zone = zone, values = ivalues)

All the arguments are optional. On return the date holds the year, month, and day in a string of the form “yyyymmdd”; time holds the time in hours, minutes, seconds, and milliseconds; the time zone is encoded with a sign (+ or -) and the time difference from Coordinated Universal Time (UTC) in hours and minutes.

ivalues returns the same information as the other three arguments in the form of an array of integers. These are, in order: year, month (1-12), day (1-31), time difference in minutes between local and UTC, hour (0-23), minute (0-59), seconds (0-59), and milliseconds (0-999).

Timing pieces of code


If you want to record the time taken to execute a particular section of code, the cpu_time() function can be used. This returns a real positive value which is some system-dependent time in seconds. Subtracting two consecutive values will give an elapsed time:

FORTRAN

   real :: t0, t1

   call cpu_time(t0)
   ! ... code to be timed here ...
   call cpu_time(t1)

   print *, "That piece of code took ", t1-t0, " seconds"

In the unlikely event that there is no clock available, a negative value may be returned from cpu_time().

Key Points
  • Fortran provides other utility procedures that can help with your program’s functionality.
  • These allow you to retrieve information from the calling command line and environment.
  • You can pass commands back to the operating system, but you should be very careful that your code remains portable.
  • Timing your code with cpu_time() can help you profile your code.

Content from Lunch


Last updated on 2026-02-21 | Edit this page

Break for lunch.

Content from Other things you may see


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • What have we not covered that I should be aware of?

Objectives

  • Learn about some other Fortran features which may be of interest.

Not covered in this course


The Fortran standard covers a large and increasing number of features. Some of these we have only touched on, and others not mentioned at all.

  • Object-oriented features: type extension, abstract classes/interfaces, …
  • Interoperability with C
  • Support for IEEE floating point arithmetic
  • Coarrays

Things you may see


Old stuff

Don’t panic! Consult a reference to see what is happening.

May include:

  • old-style declarations real*4, real*8, character*10 etc.
  • GO TO statements
  • DATA blocks

Some features are formally obsolescent, in which case the compiler should give a warning. There should be a more modern (“better”) way doing the same thing. Obsolescent features will be deleted at some point in the future.

Preprocessor

The processing of Fortran is not standardised in the same way as the C-preprocessor is part of the C standard.

However, it is very common to see preprocessor directives for conditional compilation, and other preprocessor features. E.g.,

FORTRAN

#ifdef HAVE_SOME_FEATURE
  ...
#endif

As the Fortran preprocessor is not standardised, some care may be required to ensure portability. For example, stringification of macro arguments can be problematic.

An additional compiler flag may be required to switch on the preprocessor explicitly. Alternatively, it is common that compilers will automatically run the preprocessor if the file extension has a capital letter e.g., .F90, .F03, and so on.

Parallelism


where, forall

Fortran has introduced a number of constructs which are intended to allow the expression of parallelism.

While the standard allows these, and all compilers should inplement them correctly, few compilers actually introduce any parallelism.

Loops are a perfectly good solution.

Message passing interface

The message passing interface provides a standard interface for distributed memory computing.

MPI makes use of a number of data types, macro definitions, and library subroutines. A modern program might introduce the information required via

FORTRAN

  use mpi_f08

which uses derived types for data types (which are often opaque).

Earlier versions might use

FORTRAN

  use mpi

where the opaque types are integer handles.

Older codes may even use

FORTRAN

#include 'mpif.h'

to make the necessary handles and macros available.

MPI library calls

MPI is at base a C interface which accommodates Fortran. The C routines often have prototypes of the form:

FORTRAN

  int MPI_Send(const void * buf, int ount, MPI_Datatype dt, int dest, int tag,
               MPI_Comm comm);

The data to be sent is identified here using void * buf. A return code provides an error status.

The lack of void * in Fortran means that the Fortran API, formally, has been on a rather shaky foundation for a long time. This is being addressed in the most recent standards.

In particular, the use of array sections as the buf argument might prove problematic and should probably be avoided. It is also preferable for the application to marshal data into a contiguous buffer for performance reasons.

In modern versions, the error return code in Fortran are optional integer arguments.

OpenMP

OpenMP is a standard way to introduce thread-level parallelism (typically at the level of loops). A program should

FORTRAN

  use omp_lib

to provide OpenMP functions and kind type parameters.

OpenMP is largely based around compiler directives, which are switched on via a compiler option, usually -fopenmp.

In Fortran, the directives are introduced by the sentinels:

FORTRAN

  !$omp ...
  ...
  !$omp end ...

which is ignored as a comment if no OpenMP is required (cf. #pragma omp ... { } in C).

The OpenMP standard documents provide a useful stub implementation which can be used in place of the real implementation when compiling without OpenMP. This prevents a profileration of conditional compilation directives.

GPU programming

The GPU programming model of choice for Fortran has probably been OpenACC to the present time.

NVIDIA support a Fortran extension CUDA Fortran, which is not portable.

The OpenMP standard also has support for GPU offload, but the status of compiler implementations is in flux.

Testing


Testing is an important consideration in modern software engineering. There are a number of unit test frameworks such as pFUnit.

Some others are mentioned at the Fortran wiki.

Resources


The standard reference is “Modern Fortran Explained” by Metcalf, Reid, Cohen and Bader (Oxford University Press). The latest version is “Modern Fortran Explained: Incorporating Fortran 2023” (2023).

The Fortran wiki also has a lot of useful material.

Key Points
  • You may come across Fortran following an older standard; some will be understandable on sight, but you may need to consult a reference.
  • MPI and OpenMP allow you to write Fortran code that will execute in parallel.
  • NVIDIA supports CUDA in Fortran for execution on GPUs.
  • Writing tests is an important step in safe code development.
  • Several books and the Fortran Wiki are invaluable resources.

Content from Exercises: conjugate gradient and matrices


Last updated on 2026-02-21 | Edit this page

Overview

Questions

  • How can I really push what I’ve learnt so far?

Objectives

  • Write a conjugate gradient solver.
  • Read a Matrix Market Exchange file and plot a downloaded matrix.

Exercises


To finish things off, here are two fairly substantial exercises you can tackle which should put everything you’ve learnt to the test.

Challenge

Conjugate gradient solver

Contrasting with the very specific tri-diagonal solver we’ve been working on, the conjugate gradient method provides a slightly more general method for the solution of linear systems

\(Ax = b\)

where the matrix \(A\) is symmetric positive definite. The algorithm is explained in detail on Wikipedia, and if you are very interested you might like to read An Introduction to the Conjugate Gradient Method Without the Agonizing Pain.

However, it is not necessary to understand the details, as here we are just interested in implementing the algorithm.

There are two main steps involved. The first is to perform a matrix-vector multiplication. This can be done using the Fortran intrinsic function matmul(). (Or you can have a go at implementing your own version - it’s not too difficult.)

The second step is to compute a scalar residual from a vector residual. If we have an array (vector) r(1:n) this can be done with:

FORTRAN

  residual = sum(r(:)*r(:))

All the operations in the algorithm can be composed of these, as well as vector additions and multiplications by a scalar.

A template program cg_test.f90 is provided with a small matrix to use as a test. You need to implement a module cgradient which supplies a function cg_test() taking the arguments set out in the template.

Remember that you can always check your answer by multiplying out \(Ax\) to recover the original right-hand side \(b\).

You can check a suggested solution with cg_test.f90 and cgradient_solution.f90.

Challenge

Storage of large sparse matrices

The Matrix Market provides a number of archived matrices of different types. It also defines a simple ASCII format for the storage of sparse matrices.

The Matrix Market Exchange format .mtx files are structured as follows.

OUTPUT

%% Exactly one header line starting %%
% Zero or more comment lines starting %
nrows ncols nnonzero
i1 j1 a(i1,j1)
i2 j2 a(i2,j2)
...

where nrows and ncols are the number of rows and columns in the matrix, respectively. nnonzero is the number of non-zero entries in the matrix. For each non-zero entry there then follows a single line which contains the row index, the column index, and the value of the matrix element itself.

Download an example, e.g.,

BASH

$ wget https://math.nist.gov/pub/MatrixMarket2/Harwell-Boeing/laplace/gr_30_30.mtx.gz
$ gunzip gr_30_30.mtx

If you look at the first few line of this example, you should see

OUTPUT

%%MatrixMarket matrix coordinate real symmetric
900 900 4322
1 1  8.0000000000000e+00
2 1 -1.0000000000000e+00
31 1 -1.0000000000000e+00
32 1 -1.0000000000000e+00
2 2  8.0000000000000e+00
  • Define a type that can hold the sparse representation and provide a procedure which initialises such a type from a file.

  • Provide a procedure which brings into existence a dense matrix (just a two-dimensional array) initialised with the correct non-zero elements.

  • Use the .pbm file generator to produce an image of the non-zero elements to provide a check the file has been read correctly.

  • Try some other matrices from the Matrix Market.

A suggested solution in provided in mmtest.f90 and mmarket.f90.

Key Points
  • Fortran performance and array handling make it ideal for the solution of intense problems.

Content from Break


Last updated on 2026-02-21 | Edit this page

Comfort break.

Content from Exercises


Last updated on 2026-02-21 | Edit this page

Overview

Questions

Objectives

We will use the rest of today to work on any exercises from the previous lessons you still wish to go over.

Key Points