Fortran stdlib
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against stdlib 0.8.1 (released 2026-01-26, still the newest release on 2026-10-19) and Fortran 2023
(ISO/IEC 1539-1:2023, J3/23-007r1), and was checked on 2026-10-19. Every example was built with GNU Fortran 16.2 under
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all, and run, and then built and run again with LLVM Flang 23
against a Flang build of the same stdlib, on an aarch64 Linux container. stdlib itself was compiled from the v0.8.1 tag
(see below for how, because a plain fpm git dependency on that tag does not work). The output shown is real output; where the two
compilers differ the difference is shown. Examples that fail on purpose show the real diagnostic. Intel ifx, NAG and the other vendor
compilers were not available, so nothing here claims how stdlib behaves with them beyond what the stdlib README itself says.
The Fortran Standard Library, stdlib, is a community project of the fortran-lang organization. It is not part of the
language: the standard defines the intrinsic procedures and intrinsic modules (see
Intrinsic Procedures Overview and
Intrinsic Modules and iso_fortran_env), and stdlib is an ordinary
library, written in Fortran, that you add to a project the way you add any other dependency. Its goal, in the words of its README, is a
de facto standard library that fills the gaps the standard leaves: string handling, file-system and process access, sorting, hash maps,
bitsets, a logging facility, linear algebra with a modern interface, statistics and numerical integration. The running example,
heatflow, uses it for text input and output (loadtxt, savetxt), a linear solve for an implicit time step, logging, string
processing of configuration records and sparse operators; how heatflow itself is built is in
Fortran Package Manager (fpm) and
Building with Make, CMake and Meson, and the BLAS and LAPACK side of
stdlib_linalg is in
BLAS, LAPACK and Numerical Libraries.
What stdlib is, and how its modules mature
stdlib has a written process (its WORKFLOW.md): an idea is discussed in an issue, the API is discussed, a specification page is
written, an implementation is added, and every new public API starts in an experimental state. A specification is a document that
describes the procedure well enough that someone could implement it without reading the stdlib source; the stdlib code is the
reference implementation. Each spec page carries a Status heading for every procedure.
On the 0.8.1 tag the specification index (doc/specs/index.md) lists every one of its 28 entries under Experimental Features & Modules
and states "(None yet)" under Released/Stable Features & Modules. The individual spec pages are slightly more nuanced, and this is
checked against the tag rather than assumed: 19 procedures of stdlib_linalg are marked Stable in stdlib_linalg.md
(diag, eye, trace, solve, solve_lu, lstsq, solve_lstsq, lstsq_space, det, .det., eig, eigh, eigvals, eigvalsh, svd,
svdvals, .inv., invert and inv); every other documented procedure in the 37 spec pages is marked Experimental, and none is marked Deprecated
(stdlib_kinds.md has no status headings). In practice:
| Question | Answer for 0.8.1 |
|---|---|
What does "experimental" promise? |
the API may still change or be removed in a later release; the library is usable, and it is tested by its own test suite and compiled in the project’s CI, but you should pin the version |
How do you pin it? |
a git tag or the metapackage’s resolved release in fpm, |
Is there a compatibility promise across 0.x releases? |
no; read |
Is stdlib part of Fortran 2028? |
no; the standard and stdlib are independent, and nothing in the standard requires a compiler to ship stdlib |
License |
MIT |
Two things the outline of this section sometimes assumes are not in 0.8.1: there is no date-and-time module (datetime is not in the
source tree), and stdlib_strings has no split procedure (its public interface is to_string, to_c_char, strip, chomp,
starts_with, ends_with, slice, find, replace_all, padl, padr, count, zfill and join). The Fortran 2023 intrinsics
split and tokenize exist in the language, but as recorded in
Characters and Strings, GNU Fortran 16.2 accepts tokenize and then fails to link
it, so splitting a record by hand, as in the string example below, is still the portable choice.
Getting stdlib
stdlib is written with the fypp preprocessor (templates over kinds and ranks), so its repository is not directly compilable: the .fypp
sources have to be expanded to .f90 first, and which route you take decides who does it. There are four practical routes, and the
differences are worth knowing before the first failed build.
| Route | What it does | Checked |
|---|---|---|
fpm metapackage |
fpm downloads the latest release of stdlib as a git dependency (per the fpm documentation); no Python needed |
worked, reported 0.8.1 |
fpm git branch |
the repository branch that already contains the expanded |
worked, reported 0.8.1 |
fpm git tag |
the tag contains only |
fails, see below |
fpm path dependency on a locally expanded tree |
run |
worked; used for every example here |
CMake |
CMake runs fypp itself; needs the |
worked |
fpm
Add the metapackage to the manifest. The [fortran] table is the same strict setting every heatflow manifest uses:
name = "heatflow"
version = "0.1.0"
[build]
auto-executables = true
[dependencies]
stdlib = "*"
[fortran]
implicit-typing = false
implicit-external = false
Metapackages are listed in the fpm documentation as experimental, the stdlib README says fpm 0.9.0 and later provides stdlib as a metapackage, and the version cannot be
constrained (the "*" means any version; it resolves to the newest release when the dependency is fetched). The older spelling, still
shown in the stdlib README, is the git branch:
[dependencies]
stdlib = { git = "https://github.com/fortran-lang/stdlib", branch = "stdlib-fpm" }
Source: stdlib 0.8.1 README: requirements, building with CMake and fpm, using stdlib, BLAS and LAPACK
Both were built and run in the container with fpm 0.13.0 and printed 0.8.1. What does not work is the obvious-looking pin to the release
tag:
[dependencies]
stdlib = { git = "https://github.com/fortran-lang/stdlib", tag = "v0.8.1" }
<ERROR> *cmd_run* Targets error: Unable to find source for module dependency: "stdlib_kinds" used by "app/main.f90"
STOP 1
fpm fetched the tag and found no Fortran source for stdlib_kinds, because on the tag it is src/core/stdlib_kinds.fypp. This is the real
message from fpm 0.13.0. To get exactly 0.8.1 you expand the tree yourself, which is what the examples on this page do:
git clone --branch v0.8.1 https://github.com/fortran-lang/stdlib stdlib-src
pip install fypp joblib
cd stdlib-src
python3 config/fypp_deployment.py --deploy_stdlib_fpm # writes ./stdlib-fpm with .f90 files
Source: stdlib 0.8.1 README: requirements, building with CMake and fpm, using stdlib, BLAS and LAPACK
and then in the manifest of the project that uses it (the maximum array rank defaults to 4 in this route):
[dependencies]
stdlib = { path = "../stdlib-src/stdlib-fpm" }
fpm only compiles the modules that the program transitively uses, so a program that imports stdlib_kinds builds in about a second, while one
that imports stdlib_linalg also compiles the embedded BLAS and LAPACK. The first build of the full set of modules used by the examples
below took about 40 seconds in the container.
fpm with an external BLAS and LAPACK
stdlib ships its own BLAS and LAPACK in Fortran (stdlib_linalg_blas, stdlib_linalg_lapack), so stdlib_linalg works with nothing else
installed. To use an optimized library instead, fpm 0.12.0 and later connects the two metapackages:
[dependencies]
stdlib = "*"
blas = "*"
With both enabled fpm defines STDLIB_EXTERNAL_BLAS and STDLIB_EXTERNAL_LAPACK for stdlib, which switch off the internal routines. In the
container this resolved to OpenBLAS: the link line contained -lopenblas and a solve call returned the right answer.
CMake
The CMake build runs fypp for you. With the Python tools installed (pip install fypp cmake ninja), FetchContent can pin the tag:
cmake_minimum_required(VERSION 3.20)
project(heatflow_stdlib LANGUAGES Fortran)
include(FetchContent)
FetchContent_Declare(
fortran_stdlib
GIT_REPOSITORY https://github.com/fortran-lang/stdlib.git
GIT_TAG v0.8.1
)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(fortran_stdlib)
add_executable(version_check version_check.f90)
target_link_libraries(version_check PRIVATE fortran_stdlib::fortran_stdlib)
Source: stdlib 0.8.1 README: requirements, building with CMake and fpm, using stdlib, BLAS and LAPACK
This configured, built (588 build steps) and ran in the container; it found the container’s OpenBLAS and reported "Found external BLAS" and
"Found external LAPACK". The alternative is to build once and install (cmake -S . -B build, cmake --build build, cmake --install build
with -DCMAKE_INSTALL_PREFIX=...) and use find_package. That route has a trap that was reproduced:
stdlib records the BLAS and LAPACK it was configured with in its exported targets, so a consumer that only says find_package(fortran_stdlib)
fails at configure time:
CMake Error at /work/inst/lib/cmake/fortran_stdlib/fortran_stdlib-targets.cmake:84 (set_target_properties):
The link interface of target "fortran_stdlib::fortran_stdlib_blas"
contains:
BLAS::BLAS
but the target was not found.
The fix is to find them first:
cmake_minimum_required(VERSION 3.20)
project(heatflow_stdlib LANGUAGES Fortran)
# stdlib's installed package refers to BLAS::BLAS and LAPACK::LAPACK when it was
# configured against an external BLAS/LAPACK, so the consumer has to find them first
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)
find_package(fortran_stdlib REQUIRED)
add_executable(version_check version_check.f90)
target_link_libraries(version_check PRIVATE fortran_stdlib::fortran_stdlib)
Source: stdlib 0.8.1 README: requirements, building with CMake and fpm, using stdlib, BLAS and LAPACK
with -DCMAKE_PREFIX_PATH pointing at the install prefix. The build options that matter, from the README of the tag:
| Option | Effect |
|---|---|
|
largest array rank the generated procedures support; default 4, minimum 4, 15 at most (7 for compilers without full Fortran 2003 support); a rank of 15 needs several gigabytes of memory to compile |
|
link dynamically; default off |
|
skip stdlib’s own tests |
|
do not look for an external BLAS and LAPACK; the internal ones are used |
|
also build the 64-bit-integer BLAS and LAPACK interfaces |
|
generate quadruple and extended precision code; auto-detected |
|
leave out optional modules ( |
The build configuration is visible in your program. The same example, printing the kind constants from stdlib_kinds, gave qp = -1 with the fpm
route and qp = 16 with the CMake route on the same compiler and machine, because CMake auto-detected quadruple precision and the
fpm route did not enable it (the fypp deployment script has --with_qp for that). Code that uses qp or xdp has to be written
for the value -1, which means "not available" (the spec of stdlib_kinds says so), and the kind of the stdlib you linked is part of your
build, not of the stdlib version. Make-based projects use pkg-config --cflags --libs fortran_stdlib or -lfortran_stdlib with the -I and -L
paths of the install prefix, as the README describes (not run here).
A tour of the modules
The 0.8.1 source tree contains the modules below. The Status column is taken from the spec pages of the tag; the last column links the specification, and the web pages at https://stdlib.fortran-lang.org/page/specs/index.html track the development branch, which can be ahead of the release.
| Module | Purpose | Status | Spec |
|---|---|---|---|
|
kind constants |
experimental |
|
|
|
experimental |
|
|
mathematical and physical constants; all CODATA values as a derived type with value, uncertainty and unit |
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
run and monitor external processes ( |
experimental |
|
|
ANSI escape sequences for colours and styles, combined with |
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
FNV-1, FNV-1a, Fibonacci, universal multiplicative, NM, Water, Pengy and SpookyV2 hash functions |
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
19 stable, the rest experimental |
|
|
the full reference BLAS and LAPACK for all real and complex kinds (and optionally 64-bit integers), with optional external replacement |
experimental |
|
|
conjugate gradient, preconditioned CG and BiCGSTAB for dense or CSR systems |
experimental |
|
|
COO, CSR, CSC, ELLPACK and SELL-C matrices, conversions, |
experimental |
|
|
tridiagonal matrix types with |
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
|
experimental |
|
|
random variates ( |
experimental |
|
|
Legendre polynomials, neural-network activation functions and their derivatives, |
experimental |
|
|
|
experimental |
Every example below includes the version information it was built against, so each output can be reproduced. The Source line under
each example links the specification page of the tag, and the example sources in the stdlib repository (example/<module>/) are
worth reading as short, tested usage snippets.
Kinds, version, optval and constants
Almost every other module takes its kinds from stdlib_kinds, so it is the first import. get_stdlib_version and the
stdlib_version_string constant tell a program which release it was built against; the spec recommends using the getter at run time
because a dynamically linked library can differ from the constants of the headers you compiled with. optval replaces the three-line
if (present(x)) idiom for optional arguments:
program kinds_version_optval
use stdlib_kinds, only: sp, dp, qp, xdp, int32, int64
use stdlib_version, only: get_stdlib_version, stdlib_version_string, stdlib_version_compact
use stdlib_optval, only: optval
use stdlib_constants, only: pi => PI_dp, stefan_boltzmann
implicit none
integer :: major, minor, patch
character(len=:), allocatable :: version
call get_stdlib_version(major, minor, patch, version)
print '(a, i0, a, i0, a, i0, a, a)', 'stdlib ', major, '.', minor, '.', patch, ' = ', version
print '(a, a, a, i0)', 'compile-time string ', stdlib_version_string, ', compact ', stdlib_version_compact
print '(a, 4(i0, 1x))', 'sp dp qp xdp = ', sp, dp, qp, xdp
print '(a, 2(i0, 1x))', 'int32 int64 = ', int32, int64
print '(a, f8.6)', 'diffusion length, default scale ', diffusion_length(0.01_dp, 4.0_dp)
print '(a, f8.6)', 'diffusion length, scale = 2 ', diffusion_length(0.01_dp, 4.0_dp, 2.0_dp)
print '(a, es12.5)', 'Stefan-Boltzmann constant ', stefan_boltzmann
print '(a, f0.10)', 'PI_dp ', pi
contains
pure function diffusion_length(alpha, t, scale) result(l)
real(dp), intent(in) :: alpha, t
real(dp), intent(in), optional :: scale
real(dp) :: l
l = optval(scale, 1.0_dp) * sqrt(4.0_dp * alpha * t)
end function diffusion_length
end program kinds_version_optval
Output:
stdlib 0.8.1 = 0.8.1
compile-time string 0.8.1, compact 801
sp dp qp xdp = 4 8 -1 -1
int32 int64 = 4 8
diffusion length, default scale 0.400000
diffusion length, scale = 2 0.800000
Stefan-Boltzmann constant 5.67037E-08
PI_dp 3.1415926536
Flang 23 printed the same eight lines. qp and xdp are -1 here, which is stdlib’s way to say the kind was not generated in this build
(see the CMake discussion above). optval is generic over the intrinsic types; its second argument must have the same type and kind as
the first. stdlib_constants gives PI_sp, PI_dp, … and the main CODATA constants in double precision (c, h, k, N_A, sigma/stefan_boltzmann, …), and
stdlib_codata has every CODATA constant as a codata_constant_type that converts with to_real to the precision you ask for. Short constant names
such as c and e can collide with your own names, which is why the spec suggests renaming at the use, as in use stdlib_constants, only: clight => c.
Text input and output: stdlib_io
loadtxt and savetxt read and write a rank-2 array of real, complex or integer as a delimited text file, one array row per line.
Writing uses the default format constant of the kind, which for real(dp) is FMT_REAL_DP, (es24.16e3), as the file below shows. open takes a
Python-like mode string ('r', 'w', 'x', 'a', '+', 'b', 't') and returns the unit, get_line reads a whole line into a
deferred-length string, get_file reads a whole file, and save_npy/load_npy exchange arrays of any rank with NumPy (they live in stdlib_io_npy,
not in stdlib_io, in 0.8.1):
program stdlib_io_demo
use stdlib_kinds, only: dp
use stdlib_io, only: loadtxt, savetxt, open, get_line, get_file, FMT_REAL_DP
use stdlib_io_npy, only: load_npy, save_npy
implicit none
integer, parameter :: nx = 5, ny = 3
real(dp) :: u(nx, ny)
real(dp), allocatable :: v(:, :), w(:, :)
character(len=:), allocatable :: line, text
integer :: i, j, unit, stat
! a small temperature field: u(i, j) = 10*(i-1) + (j-1)
do j = 1, ny
do i = 1, nx
u(i, j) = 10.0_dp * real(i - 1, dp) + real(j - 1, dp)
end do
end do
! text round trip: one array row per line, columns separated by ';'
call savetxt('field.csv', u, delimiter=';')
call loadtxt('field.csv', v, delimiter=';')
print '(a, 2(1x, i0), a, es9.2)', 'loaded shape', shape(v), ', max |u - v| = ', maxval(abs(u - v))
! skiprows and max_rows select a block of rows
call loadtxt('field.csv', w, skiprows=1, max_rows=2, delimiter=';')
print '(a, 2(1x, i0))', 'rows 2..3 shape', shape(w)
! open() with a mode string returns a unit; get_line reads a whole line
unit = open('field.csv', 'r', iostat=stat)
print '(a, i0)', 'open iostat = ', stat
call get_line(unit, line, iostat=stat)
print '(a, a)', 'first line: ', line
close(unit)
! get_file reads the entire file into one allocatable string
call get_file('field.csv', text)
print '(a, i0, a)', 'file holds ', len(text), ' characters'
! .npy: binary and self-describing, any rank, readable by NumPy
call save_npy('field.npy', u)
call load_npy('field.npy', v)
print '(a, es9.2)', 'npy round trip, max |u - v| = ', maxval(abs(u - v))
! format constant that prints a real(dp) with full precision
print FMT_REAL_DP, 1.0_dp / 3.0_dp
! missing file with iostat, instead of the default error stop
unit = open('does-not-exist.csv', 'r', iostat=stat)
print '(a, l1)', 'missing file reported through iostat: ', stat /= 0
end program stdlib_io_demo
Output:
loaded shape 5 3, max |u - v| = 0.00E+00
rows 2..3 shape 2 3
open iostat = 0
first line: 0.0000000000000000E+000; 1.0000000000000000E+000; 2.0000000000000000E+000
file holds 375 characters
npy round trip, max |u - v| = 0.00E+00
3.3333333333333331E-001
missing file reported through iostat: T
The file field.csv that savetxt wrote is five lines of three columns, the first being
0.0000000000000000E+000; 1.0000000000000000E+000; 2.0000000000000000E+000
Flang 23 printed the same output. Two behaviours are worth knowing. skiprows and max_rows select rows, not bytes, so skiprows=1, max_rows=2 gives
rows 2 and 3, a 2 x 3 array. And a procedure that can fail takes an iostat argument; without it, a missing file stops the program, which is why the
last line of the example passes iostat. The array is transposed relative to the file only in the sense that
Fortran stores columns contiguously: savetxt writes u(i, :) as line i, the rows of the array, not the columns that are contiguous in memory.
Source: stdlib 0.8.1: stdlib_io
Linear algebra: stdlib_linalg
stdlib_linalg wraps LAPACK behind generic, allocation-friendly interfaces: you write x = solve(a, b) instead of declaring workspace and
calling dgesv. Every procedure has two forms. The plain form returns a value and stops the program with error stop if something goes wrong
(a singular matrix, a size mismatch); the expert form takes a final err argument of type linalg_state_type and never stops, it reports the
problem in that argument (the function is then no longer pure). The example does one implicit (backward Euler) step of one-dimensional heat
flow, (I + r K) u_new = u_old, with the second-difference matrix K = tridiag(-1, 2, -1) and r = 0.4, and then uses the other
decompositions on the same matrix:
program stdlib_linalg_demo
use stdlib_kinds, only: dp
use stdlib_linalg, only: solve, det, inv, eye, diag, trace, norm, eigvalsh, svdvals, cholesky, lstsq
use stdlib_linalg_state, only: linalg_state_type
implicit none
integer, parameter :: n = 5
real(dp), parameter :: r = 0.4_dp ! alpha * dt / dx**2
real(dp) :: a(n, n), u(n), l(n, n), c(n, n)
real(dp) :: lambda(n), sigma(n), xs(6), ys(6), design(6, 2)
real(dp), allocatable :: x(:), fit(:)
type(linalg_state_type) :: err
integer :: i
! One implicit (backward Euler) step of 1D heat flow: (I + r*K) u_new = u_old,
! where K = tridiag(-1, 2, -1) is the discrete second difference.
a = (2.0_dp * r + 1.0_dp) * eye(n)
do i = 1, n - 1
a(i, i + 1) = -r
a(i + 1, i) = -r
end do
u = [0.0_dp, 0.0_dp, 100.0_dp, 0.0_dp, 0.0_dp]
x = solve(a, u) ! LAPACK *GESV underneath
print '(a, 5f9.4)', 'u_new =', x
print '(a, es10.2)', '|A u_new - u|_2 = ', norm(matmul(a, x) - u, 2)
print '(a, f9.4)', 'heat kept =', sum(x)
print '(a, f12.6)', 'det(A) = ', det(a)
print '(a, f12.6)', 'trace(A) = ', trace(a)
print '(a, es10.2)', '|A * inv(A) - I|_max = ', maxval(abs(matmul(a, inv(a)) - eye(n)))
lambda = eigvalsh(a) ! symmetric: ascending eigenvalues
print '(a, 5f8.4)', 'eigenvalues =', lambda
sigma = svdvals(a) ! descending singular values
print '(a, 5f8.4)', 'cond_2(A) =', sigma(1) / sigma(n)
call cholesky(a, c, lower=.true.) ! A = L L^T
l = c
print '(a, es10.2)', '|L L^T - A|_max = ', maxval(abs(matmul(l, transpose(l)) - a))
print '(a, 5f8.4)', 'diag(L) =', diag(l)
! least squares through the same module: fit T(x) = p + q x to noisy samples
xs = [0.0_dp, 1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp]
ys = [1.1_dp, 2.9_dp, 5.2_dp, 7.1_dp, 8.8_dp, 11.2_dp]
design(:, 1) = 1.0_dp
design(:, 2) = xs
fit = lstsq(design, ys)
print '(a, 2f8.4)', 'fit (p, q) =', fit
! expert interface: a singular matrix reports an error instead of error stop
a = 0.0_dp
x = solve(a, u, err=err)
print '(a, l1)', 'singular matrix flagged: ', err%error()
print '(a)', err%print()
end program stdlib_linalg_demo
Output:
u_new = 3.2206 14.4928 61.9968 14.4928 3.2206
|A u_new - u|_2 = 3.09E-15
heat kept = 97.4235
det(A) = 15.301440
trace(A) = 9.000000
|A * inv(A) - I|_max = 2.22E-16
eigenvalues = 1.1072 1.4000 1.8000 2.2000 2.4928
cond_2(A) = 2.2515
|L L^T - A|_max = 2.22E-16
diag(L) = 1.3416 1.3081 1.3063 1.3062 1.3062
fit (p, q) = 1.0429 2.0029
singular matrix flagged: T
[solve] returned Algebra Error: singular matrix
The same output was produced by Flang 23. Details that matter in practice:
-
eigvalshreturns the eigenvalues of a symmetric matrix in ascending order andsvdvalsthe singular values in descending order, so the 2-norm condition number of a symmetric positive definite matrix issigma(1) / sigma(n), here 2.2515. -
cholesky(a, c, lower=.true.)returns the triangular factor incand leavesaalone. With a single argument,call cholesky(a, lower=.true.), the factor overwritesa. -
lstsq(design, ys)solves the least-squares problem through LAPACK, here fittingp + q xto six samples. -
The second
solvecall uses the expert interface on a zero matrix.err%error()is true,err%print()returns[solve] returned Algebra Error: singular matrix, and the program continues. The first form, with noerr, would have stopped with that message. -
eye(n)returns areal(real64)identity by default; its optionalmoldargument selects another type and kind. -
matmul(a, x)in the residual is the intrinsic; the modulestdlib_intrinsicshasstdlib_matmulandstdlib_dot_productvariants of the intrinsics for when their summation order matters.
The Cholesky factorization as a numerical concept, and when to prefer BLAS-level calls, are in BLAS, LAPACK and Numerical Libraries.
Logging: stdlib_logger
stdlib_logger provides a logger_type and a ready-made global_logger. A logger has a threshold (all_level, debug_level,
information_level, warning_level, error_level, io_error_level, text_error_level, none_level; the default is information_level), an optional time
stamp, optional indentation and maximum width, and a list of units to write to. While that list is empty the messages go to
output_unit. Messages below the threshold are ignored. The example sends the global logger to the terminal and a
local logger to a file:
program stdlib_logger_demo
use stdlib_logger, only: logger_type, global_logger, debug_level, warning_level
implicit none
type(logger_type) :: run_log
integer :: unit, stat, step
! The global logger writes to output_unit while its log_units list is empty.
call global_logger%configure(time_stamp=.false.)
call global_logger%log_information('heatflow started', module='demo', procedure='main')
! A local logger with its own file; messages below the level are ignored.
call run_log%configure(time_stamp=.false., level=warning_level)
call run_log%add_log_file('run.log', unit, stat=stat)
print '(a, i0, a, i0)', 'log files opened: ', run_log%log_units_assigned(), ', stat = ', stat
do step = 1, 3
call run_log%log_debug('step detail') ! dropped: below warning_level
call run_log%log_information('step finished') ! dropped: below warning_level
end do
call run_log%log_warning('residual still large', module='solver', procedure='run')
call run_log%log_error('did not converge in 3 steps', module='solver', procedure='run', stat=1, errmsg='max_steps reached')
! lower the threshold: everything is logged from now on
call run_log%configure(level=debug_level)
call run_log%log_debug('now visible')
call run_log%log_message('a bare message without a label')
call run_log%remove_log_unit(unit, close_unit=.true., stat=stat)
print '(a, i0)', 'units left: ', run_log%log_units_assigned()
! echo the file content to show what was recorded
call echo('run.log')
contains
subroutine echo(file)
character(len=*), intent(in) :: file
character(len=200) :: line
integer :: u, ios
open(newunit=u, file=file, status='old', action='read')
do
read(u, '(a)', iostat=ios) line
if (ios /= 0) exit
print '(2a)', '| ', trim(line)
end do
close(u)
end subroutine echo
end program stdlib_logger_demo
Output:
demo % main: INFO: heatflow started
log files opened: 1, stat = 0
units left: 0
| solver % run: WARN: residual still large
| solver % run: ERROR: did not converge in 3 steps
| With stat = 1
| With errmsg = "max_steps reached"
| DEBUG: now visible
| a bare message without a label
The lines starting with | are the content of run.log, printed by the echo routine. The first line came from the global logger (time stamps were turned off to make the output
reproducible; by default every message is preceded by yyyy-mm-dd hh:mm:ss.sss). The two log_debug/log_information calls inside the loop never reached the file
because the level was warning_level; after configure(level=debug_level) the debug message appears. log_error appends stat and errmsg when they are
given, log_io_error does the same with iostat and iomsg, and log_text_error marks a column in a line of text. Flang 23 printed the same output. For parallel programs the spec advises one logger file per process, and a guarding region (for example an OpenMP critical) around the calls for threads.
Source: stdlib 0.8.1: stdlib_logger
Strings and text
Fortran’s character(len=*) variables are fixed length, and the intrinsics (trim, adjustl, index, scan, verify, repeat) are low level; see
Characters and Strings. stdlib adds three layers on top. stdlib_strings has functions over default
characters (and string_type) that return allocatable results. stdlib_string_type has the derived type string_type, with the intrinsic procedures, relational
operators, // and formatted I/O (through user-defined derived-type I/O, written dt) overloaded for it. stdlib_stringlist_type is a list of them.
program stdlib_strings_demo
use stdlib_kinds, only: dp
use stdlib_strings, only: strip, chomp, starts_with, ends_with, slice, find, replace_all, &
count, join, to_string
use stdlib_string_type, only: string_type, assignment(=), operator(//), operator(==), &
len, to_upper, write(formatted)
implicit none
character(len=*), parameter :: line = ' output = "heat_0042.csv" '
character(len=:), allocatable :: name
type(string_type) :: s, t
integer :: step
! stdlib_strings: functions over default character and string_type
name = strip(line)
print '(3a)', '[', name, ']'
print '(3a)', '[', chomp('field.csv', '.csv'), ']'
print '(l1, 1x, l1)', starts_with(name, 'output'), ends_with(name, '.csv"')
print '(a)', slice('abcdefghij', 2, 6, 2)
print '(i0)', find('heat_0042_0042.csv', '0042', 2)
print '(i0)', count('heat_0042_0042.csv', '_')
print '(a)', replace_all('heat_0042.csv', 'heat', 'flux')
print '(a)', join(['nx', 'ny', 'lx'], ', ')
step = 42
print '(a)', 'file heat_' // to_string(step, '(i0.6)') // '.csv'
print '(a)', to_string(3.14159_dp, '(f8.3)')
print '(a)', to_string(.true.)
! string_type: a deferred-length string with the intrinsic string procedures overloaded
s = 'Heat'
t = s // ' flow'
print '(dt)', t
print '(i0)', len(t)
print '(l1)', t == 'Heat flow'
t = to_upper(t)
write (*, '(dt)') t
end program stdlib_strings_demo
Output:
[output = "heat_0042.csv"]
[field]
T T
bdf
11
2
flux_0042.csv
nx, ny, lx
file heat_000042.csv
3.142
T
Heat flow
9
T
HEAT FLOW
strip removes leading and trailing blanks, chomp removes trailing characters of a set or a trailing substring (trailing whitespace when none is given), slice(s, first, last, stride) extracts with a
stride (so slice('abcdefghij', 2, 6, 2) is bdf), find(s, pattern, occurrence) returns the position of the n-th occurrence (the second 0042 starts at
position 11), count counts overlapping occurrences unless consider_overlapping=.false., join concatenates with a separator, and to_string(value, format) formats an integer, real, complex or logical
without the character(len=32) and write dance of an internal file; a format that does not fit the type produces [*]. To pad a number with zeros the format '(i0.6)' does the job.
Flang 23 printed identical output.
padl, padr and zfill are the three procedures that pad a string to a width, with a fill character (padl('7', 4, '0')), blanks, or zeros. They work, but under
GNU Fortran 16.2 and -Wall the use statement that imports any of them prints a warning about stdlib’s own module, five times for this three-name import:
program stdlib_padding_demo
use stdlib_strings, only: padl, padr, zfill
implicit none
print '(3a)', '[', padl('7', 4, '0'), ']'
print '(3a)', '[', padr('ny', 6), ']'
print '(3a)', '[', zfill('42', 6), ']'
end program stdlib_padding_demo
05b_padding.f90:2:6:
2 | use stdlib_strings, only: padl, padr, zfill
| 1
Warning: Type specified for intrinsic function 'len' at (1) is ignored [-Wsurprising]
followed by the correct output
[0007]
[ny ]
[000042]
Source: stdlib 0.8.1: stdlib_strings
The warning comes from the interface of those procedures in the module file (they have a dummy named len), the program is correct, and Flang 23 prints no warning. With GNU Fortran you
can silence it with -Wno-surprising for those compilation units; it does not appear for the other stdlib_strings procedures.
stdlib_ascii classifies and converts single characters and strings, stdlib_str2num converts text to numbers whose type and kind are those of a mold argument, and stringlist_type keeps
fields. The next example splits a tab-separated configuration record by hand, since stdlib_strings has no split in this release, and parses the values:
program stdlib_text_demo
use stdlib_kinds, only: sp, dp, int32
use stdlib_ascii, only: is_alpha, is_digit, is_white, is_punctuation, to_upper, to_lower, to_title, &
reverse, TAB, digits
use stdlib_str2num, only: to_num
use stdlib_stringlist_type, only: stringlist_type, fidx, bidx
use stdlib_string_type, only: string_type, write(formatted)
implicit none
character(len=*), parameter :: record = 'nx=64'//TAB//'ny=32'//TAB//'alpha=1.0e-2'
type(stringlist_type) :: keys
type(string_type) :: first
integer :: i, nd, start
integer :: n(2)
real(dp) :: alpha
! character classification, one character at a time
nd = 0
do i = 1, len(record)
if (is_digit(record(i:i))) nd = nd + 1
end do
print '(a, i0, a, l1, l1, l1)', 'digits in record: ', nd, '; classes of "a", TAB, ",": ', &
is_alpha('a'), is_white(TAB), is_punctuation(',')
print '(2a)', 'digit set: ', digits
! case conversion on default characters
print '(a)', to_upper('heatflow')
print '(a)', to_lower('HEATFLOW')
print '(a)', to_title('heat flow solver')
print '(a)', reverse('abc')
! to_num converts text to the type and kind of the mold, without an internal read
n(1) = to_num('64', 1_int32)
n(2) = to_num('32', 1_int32)
alpha = to_num('1.0e-2', 1.0_dp)
print '(a, 2i4, a, es10.3)', 'parsed n =', n, ', alpha =', alpha
print '(a, f8.3)', 'single precision:', to_num('0.125', 1.0_sp)
! split a tab separated record by hand and keep the fields in a stringlist
start = 1
do i = 1, len(record) + 1
if (i > len(record)) then
call keys%insert_at(bidx(1), record(start:i - 1))
else if (record(i:i) == TAB) then
call keys%insert_at(bidx(1), record(start:i - 1))
start = i + 1
end if
end do
print '(a, i0)', 'fields: ', keys%len()
first = keys%get(fidx(1))
print '(dt)', first
first = keys%get(bidx(1))
print '(dt)', first
end program stdlib_text_demo
Output:
digits in record: 7; classes of "a", TAB, ",": TTT
digit set: 0123456789
HEATFLOW
heatflow
Heat Flow Solver
cba
parsed n = 64 32, alpha = 1.000E-02
single precision: 0.125
fields: 3
nx=64
alpha=1.0e-2
to_num is a hand-written converter, not an internal read; the spec notes the accuracy it reaches for each kind (an exact match for sp, up to epsilon(0.0_dp) for dp), so use a formatted read where the last bit matters. stringlist_type is indexed with fidx(i) (counted from the front, starting at 1) and bidx(i) (from the back, starting at 1); insert_at(bidx(1), x)
appends. get with an index outside the list returns an empty string instead of failing, as the spec describes. Flang 23 printed identical output.
Sorting and selection
stdlib_sorting has five generic subroutines over rank-1 arrays. sort, ord_sort, sort_index and sort_adjoint accept the integer kinds, real, character(*), string_type and the bitset types (see each spec for the exact list); radix_sort takes integers and reals only. sort is an introsort and is not stable.
ord_sort is a stable hybrid of merge and insertion sort that is fast on data that is partly ordered already. radix_sort is a least-significant-digit radix sort.
sort_index and sort_adjoint sort and also tell you the permutation or carry a second array along. The index kind is int_index, which is int64.
Every one takes reverse for descending order. stdlib_selection finds the k-th smallest value without sorting everything:
program stdlib_sorting_demo
use stdlib_kinds, only: dp, int32
use stdlib_sorting, only: sort, ord_sort, sort_index, radix_sort, sort_adjoint, int_index
use stdlib_selection, only: select, arg_select
implicit none
real(dp) :: t(8), w(8), median
integer(int_index) :: order(8)
integer(int32) :: id(8)
integer :: k, kth
integer :: idx(8)
t = [310.5_dp, 295.0_dp, 301.2_dp, 295.0_dp, 330.9_dp, 288.4_dp, 301.2_dp, 305.0_dp]
id = [1_int32, 2_int32, 3_int32, 4_int32, 5_int32, 6_int32, 7_int32, 8_int32]
w = t
call sort(w) ! introsort, not stable
print '(a, 8f7.1)', 'sort ', w
w = t
call sort(w, reverse=.true.)
print '(a, 8f7.1)', 'sort, reverse ', w
w = t
call ord_sort(w) ! stable merge sort, fast on partly ordered data
print '(a, 8f7.1)', 'ord_sort ', w
w = t
call sort_index(w, order) ! sorted values plus the permutation
print '(a, 8f7.1)', 'sort_index ', w
print '(a, 8i7)', 'permutation ', order
w = t
call sort_adjoint(w, id) ! carry a second array along
print '(a, 8i7)', 'ids sorted by T ', id
call radix_sort(w) ! integers and reals only
print '(a, l1)', 'radix_sort keeps sorted data sorted: ', all(w(1:7) <= w(2:8))
! selection: the 4th smallest value without sorting everything
w = t
call select(w, 4, median) ! select reorders its argument
print '(a, f6.1)', '4th smallest ', median
idx = [(k, k = 1, 8)]
call arg_select(t, idx, 4, kth) ! arg_select leaves the data alone
print '(a, i0, a, f6.1)', 'its index ', kth, ', value ', t(kth)
end program stdlib_sorting_demo
Output:
sort 288.4 295.0 295.0 301.2 301.2 305.0 310.5 330.9
sort, reverse 330.9 310.5 305.0 301.2 301.2 295.0 295.0 288.4
ord_sort 288.4 295.0 295.0 301.2 301.2 305.0 310.5 330.9
sort_index 288.4 295.0 295.0 301.2 301.2 305.0 310.5 330.9
permutation 6 2 4 3 7 8 1 5
ids sorted by T 6 2 4 3 7 8 1 5
radix_sort keeps sorted data sorted: T
4th smallest 301.2
its index 7, value 301.2
Flang 23 printed identical output. The permutation 6 2 4 3 7 8 1 5 shows the stable sort at work for the ties: 295.0 occurs at positions 2 and 4 and 301.2 at positions 3 and 7, and their original order is kept.
Be careful with select: it reorders its argument (the example works on the copy w), while arg_select leaves the data alone and partially sorts the index array idx, which
must contain a permutation of 1:size(array) of the same integer kind as k. With equal values, arg_select may return either index (here 7 rather than 3), because its partial sort is not stable.
Hash maps and bitsets
Fortran has no built-in dictionary. stdlib_hashmaps provides two implementations of one abstract hashmap_type: chaining_hashmap_type (separate chaining) and open_hashmap_type (open addressing). Keys are
a key_type, built from an integer vector, a character string or raw bytes (set(key, ...)), and the helper map_entry and get_other_data accept those arguments directly. The associated value is class(*), so
you test its dynamic type with select type. stdlib_bitsets provides bitset_64 and bitset_large, compact sets of flags whose positions start at 0:
program stdlib_containers_demo
use stdlib_kinds, only: dp
use stdlib_hashmaps, only: chaining_hashmap_type, open_hashmap_type
use stdlib_hashmap_wrappers, only: key_type, set, get
use stdlib_bitsets, only: bitset_large
implicit none
type(chaining_hashmap_type) :: registry
type(open_hashmap_type) :: seen
type(key_type), allocatable :: keys(:)
class(*), allocatable :: value
character(len=:), allocatable :: name
logical :: conflict, exists
type(bitset_large) :: fixed
integer :: i, nx
! a map from solver names to relaxation factors; values are class(*)
call registry%init(slots_bits=6)
call registry%map_entry('jacobi', 1.0_dp, conflict)
call registry%map_entry('ftcs', 0.25_dp, conflict)
call registry%map_entry('jacobi', 2.0_dp, conflict) ! duplicate key
print '(a, l1, a, i0)', 'duplicate rejected: ', conflict, ', entries = ', registry%entries()
call registry%get_other_data('ftcs', value, exists)
select type (value)
type is (real(dp))
print '(a, f5.2, a, l1)', 'ftcs -> ', value, ' found: ', exists
end select
call registry%set_other_data('ftcs', 0.20_dp)
call registry%get_all_keys(keys)
print '(a, i0)', 'keys stored: ', size(keys)
do i = 1, size(keys)
call get(keys(i), name)
print '(2a)', ' key ', name
end do
call registry%remove('jacobi', exists)
print '(a, l1, a, i0)', 'removed: ', exists, ', entries now ', registry%entries()
! the open-addressing variant has the same API
call seen%init()
call seen%map_entry([3, 4], conflict=conflict)
call seen%key_test([3, 4], exists)
print '(a, l1)', 'integer key present: ', exists
! a bitset marking the fixed (boundary) cells of a 12-cell row (bit positions start at 0)
nx = 12
call fixed%init(nx)
call fixed%set(0)
call fixed%set(nx - 1)
print '(a, i0, a, i0, a)', 'fixed cells: ', fixed%bit_count(), ' of ', fixed%bits(), ' bits'
print '(a, l1, l1)', 'bit 0 set, bit 4 set: ', fixed%test(0), fixed%test(4)
call fixed%not()
print '(a, i0)', 'interior cells after not: ', fixed%bit_count()
end program stdlib_containers_demo
Output:
duplicate rejected: T, entries = 2
ftcs -> 0.25 found: T
keys stored: 2
key jacobi
key ftcs
removed: T, entries now 1
integer key present: T
fixed cells: 2 of 12 bits
bit 0 set, bit 4 set: TF
interior cells after not: 10
Flang 23 printed identical output. map_entry with an existing key does not overwrite: it sets conflict to true and keeps the old value (the 1.0 for jacobi was not replaced by 2.0); use
set_other_data to change a value, remove to delete, and get_all_keys for the key list, whose order the spec does not define. slots_bits makes the initial
table 2**slots_bits slots, and loading(), entries() and num_slots() report its state. For a bitset, bit_count() is the number of set bits, bits() the size, not() flips every bit, and the spec says positions outside 0 to bits()-1 are
ignored by the procedures that take a position, with an index_invalid_error status for the ones that report one.
Mathematics, statistics, random numbers and quadrature
program stdlib_numerics_demo
use stdlib_kinds, only: dp, int32
use stdlib_math, only: linspace, arange, clip, is_close, all_close, diff, deg2rad
use stdlib_stats, only: mean, var, median, corr
use stdlib_random, only: random_seed
use stdlib_stats_distribution_normal, only: rvs_normal, pdf_normal, cdf_normal
use stdlib_quadrature, only: trapz, simps, gauss_legendre
use stdlib_specialfunctions_gamma, only: gamma, log_gamma
implicit none
integer, parameter :: n = 9
real(dp) :: x(n), y(n), noisy(1000), nodes(5), weights(5)
real(dp) :: pi_, pair(n, 2), c(2, 2)
integer :: previous
! grids and sequences
x = linspace(0.0_dp, 1.0_dp, n)
print '(a, 9f6.3)', 'linspace ', x
print '(a, 5i3)', 'arange(1, 10, 2)', arange(1, 10, 2)
print '(a, 3f6.2)', 'clip ', clip(-0.5_dp, 0.0_dp, 1.0_dp), clip(0.4_dp, 0.0_dp, 1.0_dp), clip(1.7_dp, 0.0_dp, 1.0_dp)
print '(a, l1, l1)', 'is_close, <=0 ', is_close(0.1_dp + 0.2_dp, 0.3_dp), abs(0.1_dp + 0.2_dp - 0.3_dp) <= 0.0_dp
print '(a, 4f6.3)', 'diff ', diff(x(1:5))
! a temperature profile and its statistics
y = 100.0_dp * sin(deg2rad(180.0_dp) * x)
print '(a, f8.3, a, f8.3, a, f8.3)', 'mean ', mean(y), ' var ', var(y), ' median ', median(y)
pair(:, 1) = x
pair(:, 2) = 100.0_dp * x**2
c = corr(pair, 1) ! 2 x 2 correlation matrix of the columns
print '(a, f8.4)', 'corr(x, 100 x^2)', c(1, 2)
! integrals of the profile: trapezoid, Simpson, Gauss-Legendre
print '(a, f10.5)', 'trapz ', trapz(y, x)
print '(a, f10.5)', 'simps ', simps(y, x)
print '(a, f10.5)', 'exact ', 200.0_dp / acos(-1.0_dp)
call gauss_legendre(nodes, weights, [0.0_dp, 1.0_dp])
pi_ = acos(-1.0_dp)
print '(a, f10.5)', 'gauss_legendre ', sum(100.0_dp * sin(pi_ * nodes) * weights)
! reproducible noise: fix the seed, then draw a normal sample
call random_seed(12345_int32, previous)
noisy = rvs_normal(10.0_dp, 2.0_dp, 1000)
print '(a, f8.3, a, f8.3)', 'sample mean ', mean(noisy), ', sample sd ', sqrt(var(noisy))
print '(a, f8.5, a, f8.5)', 'pdf at mean ', pdf_normal(10.0_dp, 10.0_dp, 2.0_dp), ', cdf at mean ', cdf_normal(10.0_dp, 10.0_dp, 2.0_dp)
! special functions
print '(a, f10.5, f10.5)', 'gamma(5), gamma(0.5)', gamma(5.0_dp), gamma(0.5_dp)
print '(a, f10.5)', 'log_gamma(10) ', log_gamma(10.0_dp)
print '(a, l1)', 'all_close ', all_close(x, linspace(0.0_dp, 1.0_dp, n))
end program stdlib_numerics_demo
Output:
linspace 0.000 0.125 0.250 0.375 0.500 0.625 0.750 0.875 1.000
arange(1, 10, 2) 1 3 5 7 9
clip 0.00 0.40 1.00
is_close, <=0 TF
diff 0.125 0.125 0.125 0.125
mean 55.859 var 1489.702 median 70.711
corr(x, 100 x^2) 0.9622
trapz 62.84174
simps 63.67055
exact 63.66198
gauss_legendre 63.66198
sample mean 9.950, sample sd 2.054
pdf at mean 0.19947, cdf at mean 0.50000
gamma(5), gamma(0.5) 24.00000 1.77245
log_gamma(10) 12.80183
all_close T
Flang 23 printed identical output, including the random sample: the generator is stdlib’s own (xoshiro256** in the source), seeded by random_seed(put, get) (put seeds the state, get returns a value of the seeded state), so a seed makes the sequence reproducible across compilers. A few points:
-
is_close(a, b)compares with a relative tolerance (defaultsqrt(epsilon)of the kind), so0.1 + 0.2and0.3are close even though0.1_dp + 0.2_dp - 0.3_dpis not zero;all_closeis the array form. Comparing reals with==is what-Wcompare-realswarns about. -
mean,var,median,corr,covandmomenttakedimandmask.vardivides byn - 1unlesscorrected=.false..corrtakes a rank-1 or rank-2 array and returns the correlation of its columns alongdim, a2 x 2matrix in the example; there is no two-argumentcorr(x, y). -
trapz(y, x)andsimps(y, x)integrate sampled data (62.84 and 63.67 against the exact 63.66 for the half-sine);gauss_legendre(nodes, weights, interval)returns nodes and weights for any number of points, double precision only, so five points reach the exact value to the five decimals shown. -
rvs_normal(loc, scale, n)returnsnvariates,pdf_normalandcdf_normalthe density and distribution function. The generator for the normal and exponential variates is limited to double precision, according to the spec. -
gammaandlog_gammaofstdlib_specialfunctions_gammaextend the intrinsic functions, which take real arguments, to integer and complex arguments (the incomplete gamma family is in the same module).
Sparse operators and iterative solvers
Large diffusion problems give matrices that are almost all zeros. stdlib_sparse stores them in COO, CSR, CSC, ELLPACK or SELL-C format (CSR_dp_type, COO_dp_type, …), builds them from coordinate triplets with from_ijv
or from a dense matrix with dense2coo and coo2csr, and multiplies with spmv. stdlib_linalg_iterative_solvers solves a system without factorizing it, and stdlib_specialmatrices stores a tridiagonal matrix in three vectors.
The example solves the steady problem -u'' = 1 with u = 0 at both ends on eight interior points:
program stdlib_sparse_demo
use stdlib_kinds, only: dp
use stdlib_array, only: trueloc
use stdlib_sparse, only: CSR_dp_type, from_ijv, spmv
use stdlib_specialmatrices, only: tridiagonal_dp_type, tridiagonal, dense, spmv
use stdlib_linalg_iterative_solvers, only: stdlib_solve_cg
implicit none
integer, parameter :: n = 8
real(dp), parameter :: h = 1.0_dp / real(n + 1, dp)
integer :: row(3 * n - 2), col(3 * n - 2), k, i
real(dp) :: val(3 * n - 2), b(n), x(n), y(n), res
type(CSR_dp_type) :: a
type(tridiagonal_dp_type) :: t
real(dp), allocatable :: hot(:)
! steady 1D heat flow with uniform source: -u'' = 1, u(0) = u(1) = 0
! finite differences give the tridiagonal matrix tridiag(-1, 2, -1) / h**2
k = 0
do i = 1, n
if (i > 1) then
k = k + 1; row(k) = i; col(k) = i - 1; val(k) = -1.0_dp / h**2
end if
k = k + 1; row(k) = i; col(k) = i; val(k) = 2.0_dp / h**2
if (i < n) then
k = k + 1; row(k) = i; col(k) = i + 1; val(k) = -1.0_dp / h**2
end if
end do
call from_ijv(a, row, col, val) ! coordinate triplets to compressed sparse row
print '(a, i0, a, i0, a, i0)', 'CSR: ', a%nrows, ' rows, ', a%nnz, ' stored entries instead of ', n * n
b = 1.0_dp
x = 0.0_dp
call stdlib_solve_cg(a, b, x, rtol=1.0e-12_dp, restart=.false.) ! conjugate gradients
print '(a, 8f7.4)', 'u (CG) =', x
y = 0.0_dp
call spmv(a, x, y) ! y = A x
res = maxval(abs(y - b))
print '(a, es9.2)', 'max |A u - b| =', res
! the exact solution u(x) = x (1 - x) / 2 at the grid points
print '(a, 8f7.4)', 'exact =', [(0.5_dp * real(i, dp) * h * (1.0_dp - real(i, dp) * h), i = 1, n)]
! the same operator as a tridiagonal special matrix: three vectors, no sparse structure needed
t = tridiagonal(spread(-1.0_dp / h**2, 1, n - 1), spread(2.0_dp / h**2, 1, n), spread(-1.0_dp / h**2, 1, n - 1))
y = 0.0_dp
call spmv(t, x, y)
print '(a, es9.2)', 'special matrix agrees: ', maxval(abs(y - b))
print '(a, i0, a, i0)', 'dense() shape ', size(dense(t), 1), ' x ', size(dense(t), 2)
! trueloc turns a logical mask into the index list of its .true. elements
hot = x(trueloc(x > 0.1_dp))
print '(a, i0, a, 4f7.4)', 'cells above 0.1: ', size(hot), ' ', hot
end program stdlib_sparse_demo
Output:
CSR: 8 rows, 22 stored entries instead of 64
u (CG) = 0.0494 0.0864 0.1111 0.1235 0.1235 0.1111 0.0864 0.0494
max |A u - b| = 0.00E+00
exact = 0.0494 0.0864 0.1111 0.1235 0.1235 0.1111 0.0864 0.0494
special matrix agrees: 0.00E+00
dense() shape 8 x 8
cells above 0.1: 4 0.1111 0.1235 0.1235 0.1111
Flang 23 printed the same eight lines of output. Under GNU Fortran the link step also prints a linker warning, which is not part of the program output:
/usr/bin/ld: warning: .._stdlib-src_stdlib-fpm_src_stdlib_linalg_iterative_solvers_cg.f90.o: requires executable stack (because the .note.GNU-stack section is executable)
It comes from stdlib’s conjugate-gradient object file. The source assigns an internal procedure to a procedure pointer (op%matvec => matvec, with matvec defined after a contains inside the solver), which GNU Fortran implements with a code trampoline on the stack, hence the note. The program ran, but a
hardened system that refuses executable stacks may not. The use statements import spmv from both stdlib_sparse and stdlib_specialmatrices: two modules can export a generic name and the
generic interfaces are combined (J3/23-007r1, 15.4.3.4.1), which is why one spmv call can take a CSR matrix and another a tridiagonal_dp_type. The stdlib_solve_cg argument restart=.false. is the setting used in the spec’s own example; rtol and atol define the stopping test.
trueloc(mask) turns a logical mask into the list of its true indices so that a vector subscript can select the elements.
Accurate sums and hashes
stdlib_intrinsics provides stdlib_sum, stdlib_sum_kahan, stdlib_dot_product, stdlib_dot_product_kahan and stdlib_matmul, which sum in chunks (the plain stdlib_sum) or with Kahan compensation. The hash modules provide non-cryptographic hash functions (FNV-1, FNV-1a, Fibonacci,
universal multiplicative, NM, Water, Pengy, SpookyV2) for integer vectors and byte arrays, usable on their own or as the hasher of a hash map:
program stdlib_intrinsics_hash
use stdlib_kinds, only: sp, dp
use stdlib_intrinsics, only: stdlib_sum, stdlib_sum_kahan
use stdlib_hash_32bit, only: fnv_1a_hash
use stdlib_hash_64bit, only: fnv_1a_hash_64 => fnv_1a_hash
implicit none
integer, parameter :: n = 2000000
real(sp), allocatable :: x(:)
real(dp) :: reference
integer :: i
! many small single-precision values: the naive running sum loses the small terms
allocate (x(n))
x = 1.0e-3_sp
x(1) = 1.0e4_sp
reference = 1.0e4_dp + real(n - 1, dp) * real(1.0e-3_sp, dp)
print '(a, f14.4)', 'reference (dp) ', reference
print '(a, f14.4)', 'sum ', sum(x)
print '(a, f14.4)', 'stdlib_sum ', stdlib_sum(x)
print '(a, f14.4)', 'stdlib_sum_kahan ', stdlib_sum_kahan(x)
! non-cryptographic hashes of integer vectors and bytes, e.g. for cache keys
print '(a, i0)', 'fnv_1a 32-bit ', fnv_1a_hash([(i, i = 1, 8)])
print '(a, i0)', 'fnv_1a 64-bit ', fnv_1a_hash_64([(i, i = 1, 8)])
end program stdlib_intrinsics_hash
Output with GNU Fortran 16.2:
reference (dp) 11999.9991
sum 11953.1240
stdlib_sum 11998.8975
stdlib_sum_kahan 11999.9990
fnv_1a 32-bit -1706177971
fnv_1a 64-bit 7244529863066876845
Output with LLVM Flang 23:
reference (dp) 11999.9991
sum 11999.9990
stdlib_sum 11998.8975
stdlib_sum_kahan 11999.9990
fnv_1a 32-bit -1706177971
fnv_1a 64-bit 7244529863066876845
Two million single-precision values are added; the exact answer is 11999.9991. The plain intrinsic sum loses the small terms under GNU Fortran (11953.1240) and does not under Flang (11999.9990): the standard lets the processor evaluate a
reduction in any mathematically equivalent way, so the accuracy of sum depends on the compiler and the optimization flags. stdlib_sum_kahan returns 11999.9990 on both, stdlib_sum is a chunked pairwise-style sum that is better than the naive sum but not exact, and
the hashes are the same on both compilers.
Files, processes, terminals and error states
stdlib_system is the part of stdlib that talks to the operating system: run starts a process and waits (runasync does not), want_stdout=.true. captures its output into process%stdout, and the file-system procedures
(make_directory_all, remove_directory, delete_file, is_directory, exists, get_cwd, set_cwd) and path procedures (join_path, the / operator, base_name, dir_name, split_path) work on both POSIX and Windows paths.
The module is implemented partly in C (stdlib_system.c, stdlib_system_subprocess.c in the fpm tree), so a project that uses it needs a C compiler too. stdlib_ansi produces terminal colour codes, and stdlib_error supplies the state_type that all of these use to report errors without stopping:
program stdlib_system_demo
use stdlib_error, only: check, state_type
use stdlib_system, only: make_directory_all, remove_directory, delete_file, exists, is_directory, &
join_path, base_name, dir_name, run, process_type, is_windows, operator(/)
use stdlib_ansi, only: fg_color_green, fg_color_red, style_bold, style_reset, ansi_code, &
operator(//), operator(+)
implicit none
type(state_type) :: err
type(process_type) :: p
character(len=:), allocatable :: out_dir, file
type(ansi_code) :: ok_style
integer :: u
out_dir = 'results' / 'run01'
call make_directory_all(out_dir, err)
print '(a, l1, a, l1)', 'created: ', err%ok(), ', is_directory: ', is_directory(out_dir)
file = join_path(out_dir, 'heat.csv')
open(newunit=u, file=file, status='replace', action='write')
write (u, '(a)') '0.0,1.0'
close(u)
print '(2a)', 'file : ', file
print '(2a)', 'base : ', base_name(file)
print '(2a)', 'dir : ', dir_name(file)
print '(a, l1)', 'exists : ', exists(file)
! run an external program and capture its standard output
if (.not. is_windows()) then
p = run('wc -l ' // file, want_stdout=.true.)
print '(a, i0, 2a)', 'wc exit code ', p%exit_code, ': ', trim(adjustl(p%stdout))
end if
! state-based error handling instead of error stop
call delete_file(file, err)
print '(a, l1)', 'first delete ok: ', err%ok()
call delete_file(file, err) ! the file is gone: reported as ok, not as an error
print '(a, l1, 2a)', 'second delete ok: ', err%ok(), ' -> ', err%print()
call delete_file(out_dir, err) ! a directory cannot be deleted as a file
print '(a, l1, 2a)', 'delete directory failed: ', err%error(), ' -> ', err%print()
call remove_directory(out_dir, err)
call remove_directory('results', err)
print '(a, l1)', 'cleaned up: ', err%ok()
! check(): a failed condition with warn=.true. prints and continues
call check(1 + 1 == 3, msg='arithmetic is broken', warn=.true.)
! ANSI escape sequences for coloured terminal output
ok_style = fg_color_green + style_bold
print '(a)', ok_style // 'PASS' // style_reset
print '(a)', fg_color_red // 'FAIL' // style_reset
end program stdlib_system_demo
Output with GNU Fortran 16.2 (ESC marks the escape character, byte 27, which is not printable; the line arithmetic is broken is written to standard error by check and appears here in the order the terminal received the two streams):
created: T, is_directory: T
file : results/run01/heat.csv
base : heat.csv
dir : results/run01
exists : T
wc exit code 0: 1 results/run01/heat.csv
first delete ok: T
second delete ok: T -> Success!
delete directory failed: T -> Error encountered: Filesystem Error: Cannot delete results/run01 - is a directory
arithmetic is broken
cleaned up: T
ESC[0;1;32mPASSESC[0m
ESC[0;31mFAILESC[0m
Flang 23 printed the same lines (the standard-error line first, because of buffering) once stdlib’s C files were compiled with -fPIE: with the default C flags fpm used, linking with Flang 23 failed with
relocation R_AARCH64_ADR_PREL_PG_HI21 against symbol 'stderr@@GLIBC_2.17' ... recompile with -fPIC, and fpm build --c-compiler gcc --c-flag -fPIE fixed it.
The error-state design is the important part. A state_type (and its subtype linalg_state_type) holds an integer flag (STDLIB_SUCCESS, STDLIB_VALUE_ERROR, STDLIB_LINALG_ERROR, STDLIB_INTERNAL_ERROR, STDLIB_IO_ERROR, STDLIB_FS_ERROR),
a fixed-length message and a location, with the type-bound procedures ok(), error(), print() and print_message() and the comparison operators against the integer flags. The storage is fixed-size on purpose: no allocation on the error path. Procedures that can fail take an optional
err; when it is absent they call error stop with the message; when present they set it and return. Note the behaviour of delete_file in 0.8.1 shown above: its spec says deleting a file that does not exist is an error, but the
source returns a success state with the message "not deleted: file does not exist", and the example output confirms the source (second delete ok: T), so test for existence yourself if you need to tell the cases apart. Deleting a directory is an
error (Cannot delete results/run01 - is a directory).
check, error_stop and exit codes
check(condition, msg, code, warn) stops the program with error stop when the condition is false, unless warn is true, in which case it only prints to standard error. The default of warn is false in the source of 0.8.1, so a bare check stops (the prose of the spec says the default is true, but its behaviour list and the code agree on stopping). error_stop(msg, code) writes the
message to standard error and stops. The standard says an integer stop code on error stop is recommended to become the process exit status (J3/23-007r1, 11.4), and the stdlib code argument is meant to do that, but the implementation file the fpm route compiles
(f08estop.f90) honours only the values 1, 2 and 77; any other code prints ERROR: code n was specified. and exits with status 1. (The CMake build selects f18estop.f90, which forwards any code, only when its configure-time test of error stop with a variable succeeds; on this
compiler the CMake build used the fallback as well.) A singular inv without an err argument ends the program like this:
program stdlib_check_stop
use stdlib_error, only: check, error_stop
use stdlib_linalg, only: inv
implicit none
real(kind(1.0d0)) :: a(2, 2), b(2, 2)
call check(2 > 1, 'never printed') ! true: nothing happens
a = 0.0d0
b = inv(a) ! no err argument: a singular matrix stops the program
print *, 'not reached'
call error_stop('also not reached', 7)
end program stdlib_check_stop
ERROR STOP [inverse] returned Algebra Error: singular matrix
Error termination. Backtrace:
Source: stdlib 0.8.1: stdlib_error, stdlib 0.8.1: f08estop.f90, the error_stop fallback and J3/23-007r1: 11.4 STOP and ERROR STOP statements
(more lines follow: the gfortran backtrace with addresses, then exit status 1; the exact text of the first line depends on the compiler, Flang 23 printed Fortran ERROR STOP: [inverse] returned Algebra Error: singular matrix.) And a check with a code that the fallback does not know (warn is not given, so it stops):
program stdlib_check_code
use stdlib_error, only: check
implicit none
integer, parameter :: nx = 2
call check(nx >= 3, msg='grid needs at least 3 points', code=3)
print *, 'not reached'
end program stdlib_check_code
grid needs at least 3 points
ERROR: code 3 was specified.
ERROR STOP
The exit status was 1 with both compilers (Flang 23 printed Fortran ERROR STOP: code 1); changing code=3 to code=77 made the exit status 77 with both. Use 1, 2 or 77 if you rely on the status in 0.8.1, or call error stop yourself with the code, as
described in Error Handling and Program Termination.
Library conventions
Several design choices are common to the modules and explain why they look the way they do:
-
Kinds come from
stdlib_kinds, and most procedures are generic oversp,dpand the integer kinds (and overxdpandqpwhen the build has them). The number of generic specifics is the product of kinds and ranks, which is whyMAXRANKexists and why a first build ofstdlib_linalgcompiles thousands of lines. -
Optional
errarguments select between fatal and recoverable error handling, as above. -
Pure when possible. Many functions are
purein their plain form and losepurewhen you passerr; a few, such asinner_productin the iterative solvers, lost it in 0.8.1. -
Allocatable results.
strip,join,solve,lstsqand friends return allocatable results; assign to an allocatable variable. -
Zero-based and one-based indexing are both used. Bitset positions start at 0;
stringlistindices start at 1 (fidx(1));truelocreturns indices of the lower bound you give. Read the spec of each module. -
Specs are the contract. The behaviour of
delete_fileabove is an example of why the source and the spec need to be read together; the spec index of the tag, not the web site’s current pages, is the document that matches the release.
Pitfalls
-
A git dependency on the release tag does not compile with fpm. Use the metapackage or the
stdlib-fpmbranch, or expand the tree withfypp_deployment.py(see above). -
stdlib = "*"and the branch are not pins. They follow the newest release or branch head; record the version your tests passed with, and readCHANGELOG.mdbefore upgrading. -
qp = -1andxdp = -1when the build did not generate those kinds. Areal(qp)declaration then fails to compile; guard it or use onlyspanddp. -
The CMake package needs
find_package(BLAS)andfind_package(LAPACK)first when stdlib was configured with an external BLAS. -
sumand the other intrinsic reductions are not exact summations. Usestdlib_sum_kahanwhere the accuracy of long sums matters. -
selectreorders its input,arg_selectdoes not. -
map_entrywith an existing key keeps the old value and reportsconflict. -
Bitset positions start at 0, and
bit_countcounts set bits. -
error_stop(msg, code)andcheck(..., code=n)honour only 1, 2 and 77 in the build the fpm route produces. -
corrandcovneed a rank-1 or rank-2 array and adim; a pair of vectors must be stored as two columns. -
GNU Fortran prints
-Wsurprisingwarnings forpadl,padrandzfill, and the linker warns about an executable stack for the conjugate-gradient solver; neither is a program error. -
Everything is experimental. Even the 19 stable
stdlib_linalgprocedures sit in a module that is listed as experimental on the index; pin the version. -
The web site tracks development.
stdlib.fortran-lang.orgdocuments the head of the repository; for a release, readdoc/specsat the tag (the Source links on this page do that).
Where the books are outdated
The two Fortran books consulted for this section were written before stdlib existed or while it was starting, so they teach the language with the intrinsics only. Where current practice differs:
| Area | Outdated in the books, current approach |
|---|---|
Library support |
the books present the language and its intrinsics as the whole toolbox, and the numerical examples build their own string, sorting, statistics and file helpers; today stdlib offers tested versions of those (an experimental API, so check the version) |
Strings |
variable-length text is presented through |
Linear algebra |
a hand-written LU or Cholesky routine, or direct calls to |
Package management |
building a program from library sources is left to the reader; fpm and CMake fetch stdlib as a dependency |
Error handling |
|
Standard version provenance and compiler support
| Item | Introduced | Checked on 2026-10-19 |
|---|---|---|
stdlib as a project |
fortran-lang organization, not part of any ISO standard |
release 0.8.1 of 2026-01-26 built from the tag |
Generic interfaces from several modules merging under one name |
rules in 15.4.3.4.1 of Fortran 2023; edition not confirmed here |
|
|
defined in 11.4 of Fortran 2023; the edition that introduced it is not confirmed from the standard text, stdlib’s own comment calls its fallback a "Fortran 2008 error stop" |
|
User-defined derived-type I/O ( |
defined in 12.6.4.8 of Fortran 2023; edition not confirmed here |
GNU Fortran 16.2 and LLVM Flang 23 print |
Kind constants |
library constants, not a language feature |
|
stdlib build compiler support (README) |
— |
GCC 10 to 15 and Intel oneAPI (classic 2021.10, LLVM 2024.1) are in the project’s CI; this page additionally built and ran stdlib 0.8.1 with GNU Fortran 16.2 and LLVM Flang 23 (aarch64 Linux) |
NAG, NVIDIA HPC and older Intel releases are listed as not working in the README of the tag (issues 107 and 108, and failures with Intel Parallel Studio XE 16 to 18); none of them was available here.
References
-
Fortran Standard Library: home page and API documentation (FORD)
-
stdlib 0.8.1 README: requirements, building with CMake and fpm, using stdlib, BLAS and LAPACK
-
stdlib 0.8.1 WORKFLOW: how new modules are proposed, specified and promoted
-
Fortran Package Manager: Built-in dependencies (metapackages)
-
GNU Fortran manual: Options to request or suppress errors and warnings