Boilerplate reduction and templating#
Using FYPP for binding generic interfaces#
f2py
doesn’t currently support binding interface blocks. However, there are
workarounds in use. Perhaps the best known is the usage of tempita
for using
.pyf.src
files as is done in the bindings which are part of scipy. tempita support has been removed and
is no longer recommended in any case.
Note
The reason interfaces cannot be supported within f2py
itself is because
they don’t correspond to exported symbols in compiled libraries.
❯ nm gen.o
0000000000000078 T __add_mod_MOD_add_complex
0000000000000000 T __add_mod_MOD_add_complex_dp
0000000000000150 T __add_mod_MOD_add_integer
0000000000000124 T __add_mod_MOD_add_real
00000000000000ee T __add_mod_MOD_add_real_dp
Here we will discuss a few techniques to leverage f2py
in conjunction with
fypp to emulate generic interfaces and to ease the binding of multiple
(similar) functions.
Basic example: Addition module#
Let us build on the example (from the user guide, F2PY examples) of a subroutine which takes in two arrays and returns its sum.
C
SUBROUTINE ZADD(A,B,C,N)
C
DOUBLE COMPLEX A(*)
DOUBLE COMPLEX B(*)
DOUBLE COMPLEX C(*)
INTEGER N
DO 20 J = 1, N
C(J) = A(J)+B(J)
20 CONTINUE
END
We will recast this into modern fortran:
module adder
implicit none
contains
subroutine zadd(a, b, c, n)
integer, intent(in) :: n
double complex, intent(in) :: a(n), b(n)
double complex, intent(out) :: c(n)
integer :: j
do j = 1, n
c(j) = a(j) + b(j)
end do
end subroutine zadd
end module adder
We could go on as in the original example, adding intents by hand among other things, however in production often there are other concerns. For one, we can template via FYPP the construction of similar functions:
module adder
implicit none
contains
#:def add_subroutine(dtype_prefix, dtype)
subroutine ${dtype_prefix}$add(a, b, c, n)
integer, intent(in) :: n
${dtype}$, intent(in) :: a(n), b(n)
${dtype}$ :: c(n)
integer :: j
do j = 1, n
c(j) = a(j) + b(j)
end do
end subroutine ${dtype_prefix}$add
#:enddef
#:for dtype_prefix, dtype in [('i', 'integer'), ('s', 'real'), ('d', 'real(kind=8)'), ('c', 'complex'), ('z', 'double complex')]
@:add_subroutine(${dtype_prefix}$, ${dtype}$)
#:endfor
end module adder
This can be pre-processed to generate the full fortran code:
❯ fypp gen_adder.f90.fypp > adder.f90
As to be expected, this can be wrapped by f2py
subsequently.
Now we will consider maintaining the bindings in a separate file. Note the
following basic .pyf
which can be generated for a single subroutine via f2py -m adder adder_base.f90 -h adder.pyf
:
! -*- f90 -*-
! Note: the context of this file is case sensitive.
python module adder ! in
interface ! in :adder
module adder ! in :adder:adder_base.f90
subroutine zadd(a,b,c,n) ! in :adder:adder_base.f90:adder
double complex dimension(n),intent(in) :: a
double complex dimension(n),intent(in),depend(n) :: b
double complex dimension(n),intent(out),depend(n) :: c
integer, optional,intent(in),check(shape(a, 0) == n),depend(a) :: n=shape(a, 0)
end subroutine zadd
end module adder
end interface
end python module adder
! This file was auto-generated with f2py (version:2.0.0.dev0+git20240101.bab7280).
! See:
! https://web.archive.org/web/20140822061353/http://cens.ioc.ee/projects/f2py2e
With the docstring:
c = zadd(a,b,[n])
Wrapper for ``zadd``.
Parameters
----------
a : input rank-1 array('D') with bounds (n)
b : input rank-1 array('D') with bounds (n)
Other Parameters
----------------
n : input int, optional
Default: shape(a, 0)
Returns
-------
c : rank-1 array('D') with bounds (n)
Which is already pretty good. However, n
should never be passed in the first
place so we will make some minor adjustments.
! -*- f90 -*-
! Note: the context of this file is case sensitive.
python module adder ! in
interface ! in :adder
module adder ! in :adder:adder_base.f90
subroutine zadd(a,b,c,n) ! in :adder:adder_base.f90:adder
integer intent(hide),depend(a) :: n=len(a)
double complex dimension(n),intent(in) :: a
double complex dimension(n),intent(in),depend(n) :: b
double complex dimension(n),intent(out),depend(n) :: c
end subroutine zadd
end module adder
end interface
end python module adder
! This file was auto-generated with f2py (version:2.0.0.dev0+git20240101.bab7280).
! See:
! https://numpy.org/doc/stable/f2py/
Which corresponds to:
In [3]: ?adder.adder.zadd
Call signature: adder.adder.zadd(*args, **kwargs)
Type: fortran
String form: <fortran function zadd>
Docstring:
c = zadd(a,b)
Wrapper for ``zadd``.
Parameters
----------
a : input rank-1 array('D') with bounds (n)
b : input rank-1 array('D') with bounds (n)
Returns
-------
c : rank-1 array('D') with bounds (n)
Finally, we can template over this in a similar manner, to attain the original
goal of having bindings which make use of f2py
directives and have minimal
spurious repetition.
! -*- f90 -*-
! Note: the context of this file is case sensitive.
python module adder ! in
interface ! in :adder
module adder ! in :adder:adder_base.f90
#:def add_subroutine(dtype_prefix, dtype)
subroutine ${dtype_prefix}$add(a,b,c,n) ! in :adder:adder_base.f90:adder
integer intent(hide),depend(a) :: n=len(a)
${dtype}$ dimension(n),intent(in) :: a
${dtype}$ dimension(n),intent(in),depend(n) :: b
${dtype}$ dimension(n),intent(out),depend(n) :: c
end subroutine ${dtype_prefix}$add
#:enddef
#:for dtype_prefix, dtype in [('i', 'integer'), ('s', 'real'), ('d', 'real(kind=8)'), ('c', 'complex'), ('z', 'complex(kind=8)')]
@:add_subroutine(${dtype_prefix}$, ${dtype}$)
#:endfor
end module adder
end interface
end python module adder
! This file was auto-generated with f2py (version:2.0.0.dev0+git20240101.bab7280).
! See:
! https://numpy.org/doc/stable/f2py/
Usage boils down to:
fypp gen_adder.f90.fypp > adder.f90
fypp adder.pyf.fypp > adder.pyf
f2py -m adder -c adder.pyf adder.f90 --backend meson