Python class linked with fortran module#

%load_ext fortranmagic
%%fortran
module particles_f90

    implicit none
    
    real(8), dimension(:), allocatable :: positions
    real(8), dimension(:), allocatable :: velocities
            
contains
    subroutine init_particles( n )
    
        integer, intent(in) :: n
                
        integer :: i
        
        if (.not. allocated(positions)) then
            allocate(positions(n))
        end if
        positions = [(i, i = 1, n, 1)]
        if (.not. allocated(velocities)) then
            allocate(velocities(n))
        end if
        velocities = 1.0

    end subroutine init_particles
 
    subroutine push_particles( dt )
        
        real(8), intent(in) :: dt
    
        positions = positions + dt * velocities
        
    end subroutine push_particles
end module particles_f90
Traceback (most recent call last):
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/f2py/__main__.py", line 5, in <module>
    main()
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/f2py/f2py2e.py", line 770, in main
    run_compile()
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/f2py/f2py2e.py", line 594, in run_compile
    build_backend = f2py_build_generator(backend_key)
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/f2py/_backends/__init__.py", line 6, in f2py_build_generator
    from ._distutils import DistutilsBackend
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/f2py/_backends/_distutils.py", line 3, in <module>
    from numpy.distutils.core import setup, Extension
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/distutils/core.py", line 24, in <module>
    from numpy.distutils.command import config, config_compiler, \
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/distutils/command/config.py", line 19, in <module>
    from numpy.distutils.mingw32ccompiler import generate_manifest
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/distutils/mingw32ccompiler.py", line 27, in <module>
    from distutils.msvccompiler import get_build_version as get_build_msvc_version
ModuleNotFoundError: No module named 'distutils.msvccompiler'
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[2], line 1
----> 1 get_ipython().run_cell_magic('fortran', '', 'module particles_f90\n\n    implicit none\n    \n    real(8), dimension(:), allocatable :: positions\n    real(8), dimension(:), allocatable :: velocities\n            \ncontains\n    subroutine init_particles( n )\n    \n        integer, intent(in) :: n\n                \n        integer :: i\n        \n        if (.not. allocated(positions)) then\n            allocate(positions(n))\n        end if\n        positions = [(i, i = 1, n, 1)]\n        if (.not. allocated(velocities)) then\n            allocate(velocities(n))\n        end if\n        velocities = 1.0\n\n    end subroutine init_particles\n \n    subroutine push_particles( dt )\n        \n        real(8), intent(in) :: dt\n    \n        positions = positions + dt * velocities\n        \n    end subroutine push_particles\nend module particles_f90\n')

File /usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/IPython/core/interactiveshell.py:2517, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2515 with self.builtin_trap:
   2516     args = (magic_arg_s, cell)
-> 2517     result = fn(*args, **kwargs)
   2519 # The code below prevents the output from being displayed
   2520 # when using magics with decorator @output_can_be_silenced
   2521 # when the last Python token in the expression is a ';'.
   2522 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/fortranmagic.py:516, in FortranMagics.fortran(self, line, cell)
    511 res = self._run_f2py(f2py_args + ['-m', module_name, '-c',
    512                                   f_f90_file],
    513                      verbosity=args.verbosity,
    514                      fflags=fflags)
    515 if res != 0:
--> 516     raise RuntimeError("f2py failed, see output")
    518 self._code_cache[key] = module_name
    519 module = _imp_load_dynamic(module_name, module_path)

RuntimeError: f2py failed, see output

The Python class#

class Particles(object):
    
    def __init__(self, n):
        self.index       = 0
        self.numberof    = n
        particles_f90.init_particles( n)
        self.positions  = particles_f90.positions
        self.velocities = particles_f90.velocities
        
    @property 
    def position(self):      
        return self.positions[self.index]
    
    @property 
    def velocity(self):      
        return self.velocities[self.index]

Access to Fortran data from Python#

particles = Particles(10)
particles.velocities 
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
particles.positions
array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])
particles.index = 0
particles.position
np.float64(1.0)
particles.index = 1
particles.position
np.float64(2.0)

Create an Iterator class#

class ParticleArray(object):
    
    def __init__(self, particles):
        self.particles = particles
        self.numberof = particles.numberof
        
    def __getitem__(self, index): 
        self.particles.index = index 
        return self.particles
    
    def __len__(self): 
        return self.numberof
    
    def __iter__(self): 
        for i in range(self.numberof):
            self.particles.index = i
            yield self.particles
particle_array = ParticleArray(particles)
particle_array[0].position
np.float64(1.0)
for p in particle_array:
    print(p.position)
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0

Fortran derived type#

%%fortran
module mesh

implicit none
type :: geometry
    real(8) :: xmin, xmax, dx            ! coordinates of origin and grid size
    integer :: nx                        ! number of grid points
    real(8), dimension(:), pointer :: x  ! coordinates of points
end type geometry

contains

subroutine create(geom, xmin, xmax, nx)

    !f2py integer(8), intent(out) :: geom
    type(geometry), pointer :: geom
    real(8), intent(in) :: xmin, xmax
    integer, intent(in) :: nx
            
    real(8) :: dx
            
    integer :: i
            
    allocate(geom)
    geom%xmin = xmin
    geom%xmax = xmax
    geom%dx = ( xmax - xmin ) / (nx-1) 
    geom%nx = nx
    allocate(geom%x(nx))
    do i=1,nx
        geom%x(i)=geom%xmin+(i-1)*geom%dx
    end do

end subroutine create

subroutine view(geom)
    !f2py integer(8), intent(in) :: geom
    type(geometry), pointer :: geom
    print*, 'nx = ', geom%nx
    print*, geom%xmin, geom%xmax
    print*, geom%x(:)
end subroutine view

subroutine get_size(geom, nx)

    !f2py integer(8), intent(in) :: geom
    type(geometry), pointer :: geom
    integer, intent(out) :: nx
    
    nx = geom%nx
    
end subroutine get_size


end module mesh
geom = mesh.create(0.0, 1.0, 10)
mesh.get_size(geom)
10
type(geom)
int

f2py with C code#

  • Signature file is mandatory

  • intent(c) must be used for all variables and can be set globally.

  • Function name is declared with intent(c)

%rm -rf cfuncts*
%%file cfuncts.c

void push_particles(double* positions, double* velocities, double dt, int n){
    for (int i=0; i<n; i++){
       positions[i] = positions[i] + dt * velocities[i];
        
    }
} 
Writing cfuncts.c
%%file cfuncts.pyf

python module cfuncts 
    interface
        subroutine push_particles(positions, velocities, dt, n) 
            intent(c):: push_particles
            intent(c)
            integer, optional, depend(velocities) :: n = len(velocities)
            real(8), dimension(n),  intent(inplace)  :: positions 
            real(8), dimension(n),  intent(in) :: velocities
            real(8), intent(in) :: dt
        end subroutine push_particles
    end interface
end python module cfuncts
Writing cfuncts.pyf
import sys
!{sys.executable} -m numpy.f2py --quiet -c cfuncts.c cfuncts.pyf -m cfuncts
/home/pnavaro/.julia/conda/3/x86_64/envs/f2py/lib/python3.11/site-packages/numpy/f2py/f2py2e.py:723: VisibleDeprecationWarning: 
distutils has been deprecated since NumPy 1.26.x
Use the Meson backend instead, or generate wrappers without -c and use a custom build script
  builder = build_backend(
WARN: Could not locate executable armflang
Removing build directory /tmp/tmpksoj2o88
import numpy as np
import cfuncts
print(cfuncts.push_particles.__doc__)
push_particles(positions,velocities,dt,[n])

Wrapper for ``push_particles``.

Parameters
----------
positions :  rank-1 array('d') with bounds (n)
velocities : input rank-1 array('d') with bounds (n)
dt : input float

Other Parameters
----------------
n : input int, optional
    Default: len(velocities)
n = 10
dt = 0.1
x = np.arange(n, dtype="d")
v = np.ones(n, dtype="d")
cfuncts.push_particles( x, v, dt)
x
array([0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 8.1, 9.1])

References#