Clase 14: Interfaces con otros lenguajes

A diferencia de Python, tanto Fortran como C y C++ son lenguajes compilados: el código fuente tiene que se convertido a un lenguaje ejecutable para poder ser utilizado. Como mencionamos anteriormente, esto suele ser muy conveniente en el flujo de programación usual pero puede haber casos en que el código en Python es notablemente más lento, o casos en que ya existen rutinas en otros lenguajes y convertirlas a un nuevo lenguaje sea mucho trabajo. En esos casos, una solución posible y conveniente es utilizar un lenguaje compilado para rutinas específicas y Python para todo el resto del trabajo (entrada/salida, manejo de datos, graficación, etc).

Ejemplo 1: Rotación de vectores

Supongamos que queremos resolver el problema de la rotación de vectores en el espacio usando los tres ángulos de Euler.

Dado un array con los tres ángulos de Euler, calculamos la matriz de rotación (de 3 x 3), y la aplicamos (esto es, multiplicamos) a un arreglo de N vectores.

En una primera versión, podemos hacerlo en Python directamente en el notebook:

import numpy as np

def matrix_rotation(angles):
  cx, cy, cz = np.cos(angles)
  sx, sy, sz = np.sin(angles)
  R = np.zeros((3, 3))
  R[0, 0] = cx * cz - sx * cy * sz
  R[0, 1] = cx * sz + sx * cy * cz
  R[0, 2] = sx * sy

  R[1, 0] = -sx * cz - cx * cy * sz
  R[1, 1] = -sx * sz + cx * cy * cz
  R[1, 2] = cx * sy

  R[2, 0] = sy * sz
  R[2, 1] = -sy * cz
  R[2, 2] = cy
  return R


def rotate(angles, v):
  return np.dot(matrix_rotation(angles), v)
N = 100
# Ángulos de Euler
angle = np.random.random(3)
# Definimos N vectores tridimensionales
v = np.random.random((3, N))

Si ya tenemos un módulo donde están programadas las funciones necesarias

# %load rotacion_p.py
#! /usr/bin/ipython3
import numpy as np


def matrix_rotation(angles):
  cx, cy, cz = np.cos(angles)
  sx, sy, sz = np.sin(angles)
  R = np.zeros((3, 3))
  R[0, 0] = cx * cz - sx * cy * sz
  R[0, 1] = cx * sz + sx * cy * cz
  R[0, 2] = sx * sy

  R[1, 0] = -sx * cz - cx * cy * sz
  R[1, 1] = -sx * sz + cx * cy * cz
  R[1, 2] = cx * sy

  R[2, 0] = sy * sz
  R[2, 1] = -sy * cz
  R[2, 2] = cy
  return R


def rotate(angles, v):
  return np.dot(matrix_rotation(angles), v)

es fácil utilizarlas:

N = 100
# Ángulos de Euler
angle = np.random.random(3)
# Definimos N vectores tridimensionales
v = np.random.random((3, N))
y = rotate(angle,v)
print(angle)
print(y[:,0:5].T)
[0.06193349 0.99877501 0.79127934]
[[ 0.90717864  0.68082735 -0.01987772]
 [ 0.43798584  0.78786433  0.54493442]
 [ 0.75536562  0.51102298  0.41541194]
 [ 0.17681573  0.1682188  -0.02342615]
 [ 0.20673471  0.38302061  0.17507386]]

Importando desde un módulo en un archivo

También podemos tener las funciones para la rotación en un módulo rotaciones.py. Recordemos cómo se importarían en este caso

pwd
'/home/fiol/Clases/IntPython/clases-python/clases'
cd ../scripts/interfacing_python
/home/fiol/Clases/IntPython/clases-python/scripts/interfacing_python
ls
__pycache__/  rotaciones.py
import rotaciones as rotp
help(rotp)
Help on module rotaciones:

NAME
    rotaciones

FUNCTIONS
    matrix_rotation(angles)

    rotate(angles, v)

FILE
    /home/fiol/Clases/IntPython/clases-python/scripts/interfacing_python/rotaciones.py
yp = rotp.rotate(angle,v)
print(yp[:,0:5].T)
np.allclose(y,yp)
[[ 0.90717864  0.68082735 -0.01987772]
 [ 0.43798584  0.78786433  0.54493442]
 [ 0.75536562  0.51102298  0.41541194]
 [ 0.17681573  0.1682188  -0.02342615]
 [ 0.20673471  0.38302061  0.17507386]]
True

Interfaces con Fortran

Veamos cómo trabajar si tenemos el código para realizar las rotaciones en Fortran

Primer ejemplo: Nuestro código

El código en Fortran que tenemos es:

function rotate(theta, v, N) result(y)
  implicit none
  integer :: N
  real(8), intent(IN) :: theta(3)
  real(8), intent(IN) :: v(3,N)
  real(8) :: y(3,N)
  real(8) :: R(3,3)
  real(8) :: cx, cy, cz, sx, sy, sz

  ! Senos y Cosenos de los tres ángulos de Euler
  cx = cos(theta(1)); cy = cos(theta(2)); cz = cos(theta(3))
  sx = sin(theta(1)); sy = sin(theta(2)); sz = sin(theta(3))

  ! Matriz de rotación
  R(1,1) = cx*cz - sx*cy*sz
  R(1,2) = cx*sz + sx*cy*cz
  R(1,3) = sx*sy

  R(2,1) = -sx*cz - cx*cy*sz
  R(2,2) = -sx*sz + cx*cy*cz
  R(2,3) = cx*sy

  R(3,1) = sy*sz
  R(3,2) = -sy*cz
  R(3,3) = cy

  ! Aplicamos la rotación
  y = matmul(R, v)
end function rotate
cd ../interfacing_F
/home/fiol/Clases/IntPython/clases-python/scripts/interfacing_F

F2PY

F2PY -Fortran to Python interface generator- es una utilidad que permite generar una interface para utilizar funciones y datos definidos en Fortran desde Python.

Información sobre la interfaz entre Fotran y Python, y en particular sobre F2PY, puede encontrarse en:

El primer paso es utilizar esta utilidad:

$ f2py3 -c rotacion.f90 -m rotacion_f
!f2py3 -c rotacion.f90 -m rotacion_f
running build
running config_cc
INFO: unifing config_cc, config, build_clib, build_ext, build commands --compiler options
running config_fc
INFO: unifing config_fc, config, build_clib, build_ext, build commands --fcompiler options
running build_src
INFO: build_src
INFO: building extension "rotacion_f" sources
INFO: f2py options: []
INFO: f2py:> /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_fmodule.c
creating /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11
Reading fortran codes...
    Reading file 'rotacion.f90' (format:free)
Post-processing...
    Block: rotacion_f
                    Block: rotaciones
                            Block: matrix_rotation
appenddecl: "dimension" not implemented.
                            Block: rotate
appenddecl: "dimension" not implemented.
                    Block: test_rotation
Applying post-processing hooks...
  character_backward_compatibility_hook
Post-processing (stage 2)...
    Block: rotacion_f
            Block: unknown_interface
                    Block: rotaciones
                            Block: matrix_rotation
                            Block: rotate
                    Block: test_rotation
Building modules...
    Building module "rotacion_f"...
            Constructing F90 module support for "rotaciones"...
            Creating wrapper for Fortran function "matrix_rotation"("matrix_rotation")...
            Constructing wrapper function "rotaciones.matrix_rotation"...
              r = matrix_rotation(angles)
            Creating wrapper for Fortran function "rotate"("rotate")...
            Constructing wrapper function "rotaciones.rotate"...
              y = rotate(angles,v,[n])
    Wrote C/API module "rotacion_f" to file "/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_fmodule.c"
    Fortran 90 wrappers are saved to "/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_f-f2pywrappers2.f90"
INFO:   adding '/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/fortranobject.c' to sources.
INFO:   adding '/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11' to include_dirs.
copying /usr/lib64/python3.11/site-packages/numpy/f2py/src/fortranobject.c -> /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11
copying /usr/lib64/python3.11/site-packages/numpy/f2py/src/fortranobject.h -> /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11
INFO:   adding '/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_f-f2pywrappers2.f90' to sources.
INFO: build_src: building npy-pkg config files
/home/fiol/.local/lib/python3.11/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
!!

        ****************************************************************************
        Please avoid running setup.py directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.
        ****************************************************************************

!!
  self.initialize_options()
running build_ext
INFO: customize UnixCCompiler
INFO: customize UnixCCompiler using build_ext
INFO: get_default_fcompiler: matching types: '['arm', 'gnu95', 'intel', 'lahey', 'pg', 'nv', 'absoft', 'nag', 'vast', 'compaq', 'intele', 'intelem', 'gnu', 'g95', 'pathf95', 'nagfor', 'fujitsu']'
INFO: customize ArmFlangCompiler
WARN: Could not locate executable armflang
INFO: customize Gnu95FCompiler
INFO: Found executable /usr/bin/gfortran
INFO: customize Gnu95FCompiler
INFO: customize Gnu95FCompiler using build_ext
INFO: building 'rotacion_f' extension
INFO: compiling C sources
INFO: C compiler: gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC

creating /tmp/tmp8o0oz7vs/tmp
creating /tmp/tmp8o0oz7vs/tmp/tmp8o0oz7vs
creating /tmp/tmp8o0oz7vs/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11
INFO: compile options: '-DNPY_DISABLE_OPTIMIZATION=1 -I/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11 -I/usr/lib64/python3.11/site-packages/numpy/core/include -I/usr/include/python3.11 -c'
INFO: gcc: /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_fmodule.c
INFO: gcc: /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/fortranobject.c
INFO: compiling Fortran 90 module sources
INFO: Fortran f77 compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran f90 compiler: /usr/bin/gfortran -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran fix compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
INFO: compile options: '-I/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11 -I/usr/lib64/python3.11/site-packages/numpy/core/include -I/usr/include/python3.11 -c'
extra options: '-J/tmp/tmp8o0oz7vs/ -I/tmp/tmp8o0oz7vs/'
INFO: gfortran:f90: rotacion.f90
rotacion.f90:56:24:

   56 |   integer :: n, Nloops,i,j
      |                        1
Warning: Unused variable ‘i’ declared at (1) []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wunused-variable-Wunused-variable]8;;]
rotacion.f90:56:26:

   56 |   integer :: n, Nloops,i,j
      |                          1
Warning: Unused variable ‘j’ declared at (1) []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wunused-variable-Wunused-variable]8;;]
rotacion.f90:59:31:

   59 |   real(8), dimension(3, 3) :: R
      |                               1
Warning: Unused variable ‘r’ declared at (1) []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wunused-variable-Wunused-variable]8;;]
rotacion.f90:44:42:

   44 |     y = matmul(matrix_rotation(angles), v)
      |                                          ^
Warning: ‘__var_1_mma.offset’ is used uninitialized []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wuninitialized-Wuninitialized]8;;]
rotacion.f90:47:21:

   47 | end module rotaciones
      |                     ^
note: ‘__var_1_mma’ declared here
rotacion.f90:44:42:

   44 |     y = matmul(matrix_rotation(angles), v)
      |                                          ^
Warning: ‘__var_1_mma.dim[0].lbound’ is used uninitialized []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wuninitialized-Wuninitialized]8;;]
rotacion.f90:47:21:

   47 | end module rotaciones
      |                     ^
note: ‘__var_1_mma’ declared here
rotacion.f90:44:42:

   44 |     y = matmul(matrix_rotation(angles), v)
      |                                          ^
Warning: ‘__var_1_mma.dim[0].ubound’ is used uninitialized []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wuninitialized-Wuninitialized]8;;]
rotacion.f90:47:21:

   47 | end module rotaciones
      |                     ^
note: ‘__var_1_mma’ declared here
rotacion.f90:44:42:

   44 |     y = matmul(matrix_rotation(angles), v)
      |                                          ^
Warning: ‘__var_1_mma.dim[1].lbound’ is used uninitialized []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wuninitialized-Wuninitialized]8;;]
rotacion.f90:47:21:

   47 | end module rotaciones
      |                     ^
note: ‘__var_1_mma’ declared here
rotacion.f90:44:42:

   44 |     y = matmul(matrix_rotation(angles), v)
      |                                          ^
Warning: ‘__var_1_mma.dim[1].ubound’ is used uninitialized []8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wuninitialized-Wuninitialized]8;;]
rotacion.f90:47:21:

   47 | end module rotaciones
      |                     ^
note: ‘__var_1_mma’ declared here
INFO: compiling Fortran sources
INFO: Fortran f77 compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran f90 compiler: /usr/bin/gfortran -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran fix compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
INFO: compile options: '-I/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11 -I/usr/lib64/python3.11/site-packages/numpy/core/include -I/usr/include/python3.11 -c'
extra options: '-J/tmp/tmp8o0oz7vs/ -I/tmp/tmp8o0oz7vs/'
INFO: gfortran:f90: /tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_f-f2pywrappers2.f90
/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_f-f2pywrappers2.f90:5:52:

    5 |       subroutine f2pywrap_rotaciones_matrix_rotation (matrix_rotationf2p&
      |                                                    1
......
   24 |       subroutine f2pywrap_rotaciones_matrix_rotation (matrix_rotationf2p&
      |                                                    2
Warning: Rank mismatch in argument 'matrix_rotation' (2/1) between (1) and (2)
/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_f-f2pywrappers2.f90:12:43:

   12 |       subroutine f2pywrap_rotaciones_rotate (rotatef2pywrap, angles, v, &
      |                                           1
......
   30 |       subroutine f2pywrap_rotaciones_rotate (rotatef2pywrap, rotate, ang&
      |                                           2
Warning: Rank mismatch in argument 'rotate' (2/1) between (1) and (2)
INFO: /usr/bin/gfortran -Wall -g -Wall -g -shared /tmp/tmp8o0oz7vs/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_fmodule.o /tmp/tmp8o0oz7vs/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/fortranobject.o /tmp/tmp8o0oz7vs/rotacion.o /tmp/tmp8o0oz7vs/tmp/tmp8o0oz7vs/src.linux-x86_64-3.11/rotacion_f-f2pywrappers2.o -L/usr/lib/gcc/x86_64-redhat-linux/13 -L/usr/lib/gcc/x86_64-redhat-linux/13 -L/usr/lib64 -lgfortran -o ./rotacion_f.cpython-311-x86_64-linux-gnu.so
Removing build directory /tmp/tmp8o0oz7vs
from rotacion_f import rotaciones as rotf
yf = rotf.rotate(angle, v)
print(y[:,0:5].T)
print(yf[:,0:5].T)
np.allclose(yf,y)
[[ 0.90717864  0.68082735 -0.01987772]
 [ 0.43798584  0.78786433  0.54493442]
 [ 0.75536562  0.51102298  0.41541194]
 [ 0.17681573  0.1682188  -0.02342615]
 [ 0.20673471  0.38302061  0.17507386]]
[[ 0.90717864  0.68082735 -0.01987772]
 [ 0.43798584  0.78786433  0.54493442]
 [ 0.75536562  0.51102298  0.41541194]
 [ 0.17681573  0.1682188  -0.02342615]
 [ 0.20673471  0.38302061  0.17507386]]
True
%timeit rotate(angle,v)
6.42 µs ± 107 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%timeit rotf.rotate(angle, v)
2.48 µs ± 24.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

Veamos qué es exactamente lo que importamos:

np.info(rotf.rotate)
y = rotate(angles,v,[n])

Wrapper for rotate.

Parameters
----------
angles : input rank-1 array('d') with bounds (3)
v : input rank-2 array('d') with bounds (3,n)

Other Parameters
----------------
n : input int, optional
    Default: shape(v, 1)

Returns
-------
y : rank-2 array('d') with bounds (3,n) and rotate storage

Como vemos, estamos usando la función rotate definida en Fortran. Notar que: * Tiene tres argumentos. * Dos argumentos requeridos: angles y v * Un argumento, correspondiente a la dimensión n que F2PY automáticamente detecta como opcional.

Segundo Ejemplo: Código heredado

La conversión que realizamos con f2py3 la podríamos haber realizado en dos pasos:

$ f2py3 rotacion.f90 -m rotacion_f -h rotacion.pyf
$ f2py3 -c rotacion.pyf -m rotacion_f

En el primer paso se crea un archivo signature que después se utiliza para crear el módulo que llamaremos desde Python. Haciéndolo en dos pasos nos permite modificar el texto del archivo .pyf antes de ejecutar el segundo comando.

Esto es útil cuando el código original no es lo suficientemente “moderno”, no tiene toda la información necesaria sobre los argumentos o es un código que uno no quiere o puede editar. Veamos que forma tienen con un ejemplo más simple (tomado de la guía de usuario):

      SUBROUTINE FIB(A,N)
C
C     CALCULATE FIRST N FIBONACCI NUMBERS
C
      INTEGER N
      REAL*8 A(N)
      DO I=1,N
         IF (I.EQ.1) THEN
            A(I) = 0.0D0
         ELSEIF (I.EQ.2) THEN
            A(I) = 1.0D0
         ELSE
            A(I) = A(I-1) + A(I-2)
         ENDIF
      ENDDO
      END
!f2py3 --overwrite-signature fib1.f -m fib1 -h fib1.pyf
Reading fortran codes...
    Reading file 'fib1.f' (format:fix,strict)
Post-processing...
    Block: fib1
                    Block: fib
Applying post-processing hooks...
  character_backward_compatibility_hook
Post-processing (stage 2)...
Saving signatures to file "./fib1.pyf"

El contenido del archivo fib1.pyf es:

python module fib1 ! in
    interface  ! in :fib1
        subroutine fib(a,n) ! in :fib1:fib1.f
            real*8 dimension(n) :: a
            integer, optional,check(len(a)>=n),depend(a) :: n=len(a)
        end subroutine fib
    end interface
end python module fib1

Este código indica que tenemos una subrutina que toma dos argumentos: - a es un array - n es un entero opcional, que tiene que ser mayor que len(a)

!f2py3  -c fib1.pyf fib1.f
running build
running config_cc
INFO: unifing config_cc, config, build_clib, build_ext, build commands --compiler options
running config_fc
INFO: unifing config_fc, config, build_clib, build_ext, build commands --fcompiler options
running build_src
INFO: build_src
INFO: building extension "fib1" sources
creating /tmp/tmpvzr316vv/src.linux-x86_64-3.11
INFO: f2py options: []
INFO: f2py: fib1.pyf
Reading fortran codes...
    Reading file 'fib1.pyf' (format:free)
Post-processing...
    Block: fib1
                    Block: fib
Applying post-processing hooks...
  character_backward_compatibility_hook
Post-processing (stage 2)...
Building modules...
    Building module "fib1"...
    Generating possibly empty wrappers"
    Maybe empty "fib1-f2pywrappers.f"
        Constructing wrapper function "fib"...
          fib(a,[n])
    Wrote C/API module "fib1" to file "/tmp/tmpvzr316vv/src.linux-x86_64-3.11/fib1module.c"
INFO:   adding '/tmp/tmpvzr316vv/src.linux-x86_64-3.11/fortranobject.c' to sources.
INFO:   adding '/tmp/tmpvzr316vv/src.linux-x86_64-3.11' to include_dirs.
copying /usr/lib64/python3.11/site-packages/numpy/f2py/src/fortranobject.c -> /tmp/tmpvzr316vv/src.linux-x86_64-3.11
copying /usr/lib64/python3.11/site-packages/numpy/f2py/src/fortranobject.h -> /tmp/tmpvzr316vv/src.linux-x86_64-3.11
INFO:   adding '/tmp/tmpvzr316vv/src.linux-x86_64-3.11/fib1-f2pywrappers.f' to sources.
INFO: build_src: building npy-pkg config files
/home/fiol/.local/lib/python3.11/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
!!

        ****************************************************************************
        Please avoid running setup.py directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.
        ****************************************************************************

!!
  self.initialize_options()
running build_ext
INFO: customize UnixCCompiler
INFO: customize UnixCCompiler using build_ext
INFO: get_default_fcompiler: matching types: '['arm', 'gnu95', 'intel', 'lahey', 'pg', 'nv', 'absoft', 'nag', 'vast', 'compaq', 'intele', 'intelem', 'gnu', 'g95', 'pathf95', 'nagfor', 'fujitsu']'
INFO: customize ArmFlangCompiler
WARN: Could not locate executable armflang
INFO: customize Gnu95FCompiler
INFO: Found executable /usr/bin/gfortran
INFO: customize Gnu95FCompiler
INFO: customize Gnu95FCompiler using build_ext
INFO: building 'fib1' extension
INFO: compiling C sources
INFO: C compiler: gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC

creating /tmp/tmpvzr316vv/tmp
creating /tmp/tmpvzr316vv/tmp/tmpvzr316vv
creating /tmp/tmpvzr316vv/tmp/tmpvzr316vv/src.linux-x86_64-3.11
INFO: compile options: '-DNPY_DISABLE_OPTIMIZATION=1 -I/tmp/tmpvzr316vv/src.linux-x86_64-3.11 -I/usr/lib64/python3.11/site-packages/numpy/core/include -I/usr/include/python3.11 -c'
INFO: gcc: /tmp/tmpvzr316vv/src.linux-x86_64-3.11/fib1module.c
INFO: gcc: /tmp/tmpvzr316vv/src.linux-x86_64-3.11/fortranobject.c
INFO: compiling Fortran sources
INFO: Fortran f77 compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran f90 compiler: /usr/bin/gfortran -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran fix compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
INFO: compile options: '-I/tmp/tmpvzr316vv/src.linux-x86_64-3.11 -I/usr/lib64/python3.11/site-packages/numpy/core/include -I/usr/include/python3.11 -c'
INFO: gfortran:f77: fib1.f
INFO: gfortran:f77: /tmp/tmpvzr316vv/src.linux-x86_64-3.11/fib1-f2pywrappers.f
INFO: /usr/bin/gfortran -Wall -g -Wall -g -shared /tmp/tmpvzr316vv/tmp/tmpvzr316vv/src.linux-x86_64-3.11/fib1module.o /tmp/tmpvzr316vv/tmp/tmpvzr316vv/src.linux-x86_64-3.11/fortranobject.o /tmp/tmpvzr316vv/fib1.o /tmp/tmpvzr316vv/tmp/tmpvzr316vv/src.linux-x86_64-3.11/fib1-f2pywrappers.o -L/usr/lib/gcc/x86_64-redhat-linux/13 -L/usr/lib/gcc/x86_64-redhat-linux/13 -L/usr/lib64 -lgfortran -o ./fib1.cpython-311-x86_64-linux-gnu.so
Removing build directory /tmp/tmpvzr316vv
ls -ltr
total 780
drwxr-xr-x. 1 fiol fiol     16 mar 27  2023 fib2.cpython-39-darwin.so.dSYM/
drwxr-xr-x. 1 fiol fiol     16 mar 27  2023 fib3.cpython-39-darwin.so.dSYM/
-rwxr-xr-x. 1 fiol fiol  55448 mar 27  2023 fib3.cpython-39-darwin.so*
-rw-r--r--. 1 fiol fiol    403 mar 27  2023 fib2.pyf
-rwxr-xr-x. 1 fiol fiol  55448 mar 27  2023 fib2.cpython-39-darwin.so*
-rw-r--r--. 1 fiol fiol    347 mar 27  2023 fib1.f
-rwxr-xr-x. 1 fiol fiol  55512 mar 27  2023 fib1.cpython-39-darwin.so*
-rw-r--r--. 1 fiol fiol   1007 mar 27  2023 rotacion.pyf
-rwxr-xr-x. 1 fiol fiol  58480 mar 27  2023 rotacion_f.cpython-39-darwin.so*
-rw-r--r--. 1 fiol fiol   2202 mar 27  2023 rotacion.f90
-rw-r--r--. 1 fiol fiol    558 mar 27  2023 rotaciones.mod
-rw-r--r--. 1 fiol fiol    372 mar 27  2023 fib3.f
-rwxr-xr-x. 1 fiol fiol 124216 mar 22 13:27 fib2.cpython-311-x86_64-linux-gnu.so*
-rwxr-xr-x. 1 fiol fiol 124216 mar 22 13:28 fib3.cpython-311-x86_64-linux-gnu.so*
-rwxr-xr-x. 1 fiol fiol 151944 mar 22 14:28 rotacion_f.cpython-311-x86_64-linux-gnu.so*
-rw-r--r--. 1 fiol fiol    501 mar 22 14:41 fib1.pyf
-rwxr-xr-x. 1 fiol fiol 125040 mar 22 14:43 fib1.cpython-311-x86_64-linux-gnu.so*
import fib1
print(fib1.fib.__doc__)
fib(a,[n])

Wrapper for fib.

Parameters
----------
a : input rank-1 array('d') with bounds (n)

Other Parameters
----------------
n : input int, optional
    Default: shape(a, 0)
a = np.zeros(12)
fib1.fib(a)
print(a)
[ 0.  1.  1.  2.  3.  5.  8. 13. 21. 34. 55. 89.]

Podemos darle además a la función fib el argumento opcional n. Solo que debe cumplir la condición especificada en el archivo fib1.pyf

a = np.zeros(12)
fib1.fib(a,12)
print(a)
[ 0.  1.  1.  2.  3.  5.  8. 13. 21. 34. 55. 89.]
a = np.zeros(12)
fib1.fib(a,8)
print(a)
---------------------------------------------------------------------------

error                                     Traceback (most recent call last)

Cell In[29], line 2
      1 a = np.zeros(12)
----> 2 fib1.fib(a,8)
      3 print(a)


error: (shape(a, 0) == n) failed for 1st keyword n: fib:n=8
a = np.zeros(12)
fib1.fib(a,18)
print(a)
---------------------------------------------------------------------------

error                                     Traceback (most recent call last)

Cell In[30], line 2
      1 a = np.zeros(12)
----> 2 fib1.fib(a,18)
      3 print(a)


error: (shape(a, 0) == n) failed for 1st keyword n: fib:n=18

Esta es una de las características de F2PY: hace un chequeo básico de los argumentos. Hay otro error que no llega a atrapar: Si le pasamos un array que no es del tipo indicado, falla (sin avisar). Éste claramente no es el comportamiento deseado:

a = np.zeros(12, dtype=int)
fib1.fib(a)
print(a)
[0 0 0 0 0 0 0 0 0 0 0 0]

Vamos a modificar el archivo de signature para enseñarle dos cosas:

  • El entero es un argumento de entrada (requerido)

  • El array a es un archivo de salida exclusivamente. Entonces no debemos dárselo. La parte dimension(n) y depend(n) indica que debe crear un vector de ese tamaño.

!f2py3  -c fib2.pyf fib1.f 1&2> /dev/null
import fib2
print(fib2.fib.__doc__)
a = fib(n)

Wrapper for fib.

Parameters
----------
n : input int

Returns
-------
a : rank-1 array('d') with bounds (n)
fib2.fib(9)
array([ 0.,  1.,  1.,  2.,  3.,  5.,  8., 13., 21.])
print(fib2.fib(14))
[  0.   1.   1.   2.   3.   5.   8.  13.  21.  34.  55.  89. 144. 233.]

La segunda manera de arreglar este problema, en lugar de modificar el archivo de signature podría haber sido modificar el código (o hacer una rutina intermedia). Agregando comentarios de la forma Cf2py no influimos en la compilación Fortran pero F2PY los reconoce. En este caso le damos la información sobre la intención de los argumentos en el código:

      SUBROUTINE FIB(A,N)
C
C     CALCULATE FIRST N FIBONACCI NUMBERS
C
      INTEGER N
      REAL*8 A(N)
Cf2py intent(in) n
Cf2py intent(out) a
Cf2py depend(n) a
      DO I=1,N
         IF (I.EQ.1) THEN
            A(I) = 0.0D0
         ELSEIF (I.EQ.2) THEN
            A(I) = 1.0D0
         ELSE
            A(I) = A(I-1) + A(I-2)
         ENDIF
      ENDDO
      END
!f2py3  -c fib3.f -m fib3 1&2> /dev/null
import fib3
print(fib3.fib.__doc__)
a = fib(n)

Wrapper for fib.

Parameters
----------
n : input int

Returns
-------
a : rank-1 array('d') with bounds (n)
print(fib2.fib.__doc__)
a = fib(n)

Wrapper for fib.

Parameters
----------
n : input int

Returns
-------
a : rank-1 array('d') with bounds (n)

como vemos, son exactamente iguales.

F2PY para código en C

Es posible usar F2PY para código escrito en C, pero en ese caso debemos escribir el signature file a mano.

Para código en C es conveniente utilizar Cython. Cython es un lenguaje de programación pensado para hacer más fácil escribir extensiones a Python en C. Uno escribe el código en Cython, y luego es traducido a C, con optimizaciones.

Cython también puede utilizarse con Fortran de una manera similar a cómo se usa con C. Para más información ver la documentación oficial

Interfaces con otros lenguajes: C

Existen varias formas de utilizar bibliotecas o códigos hechos en C desde Python. Nosotros veremos el uso de Ctypes, sin embargo existen otras alternativas como Cython, CFFI, pybind11 y Boost.Python.

Seguimos con el ejemplo de la rotación de vectores. Por completitud, agregamos la funciones en Python para comparar

import numpy as np
pwd
'/home/fiol/Clases/IntPython/clases-python/clases'

Si ya tenemos un módulo donde están programadas las funciones necesarias

import numpy as np

def matrix_rotation(angles):
  cx, cy, cz = np.cos(angles)
  sx, sy, sz = np.sin(angles)
  R = np.zeros((3, 3))
  R[0, 0] = cx * cz - sx * cy * sz
  R[0, 1] = cx * sz + sx * cy * cz
  R[0, 2] = sx * sy

  R[1, 0] = -sx * cz - cx * cy * sz
  R[1, 1] = -sx * sz + cx * cy * cz
  R[1, 2] = cx * sy

  R[2, 0] = sy * sz
  R[2, 1] = -sy * cz
  R[2, 2] = cy
  return R


def rotate(angles, v):
  return np.dot(matrix_rotation(angles), v)
N = 100
# Ángulos de Euler
angle = np.random.random(3)
# Definimos N vectores tridimensionales
v = np.random.random((3, N))
# y= rotp.rotate(angle, v)
y = rotate(angle,v)
print(angle)
print(y[:,0:5].T)
[0.58546738 0.89120506 0.36725088]
[[ 0.57382166  0.36547642  0.42691154]
 [ 1.20276102  0.38198882  0.35784136]
 [ 0.3032189   0.17018825 -0.15835914]
 [ 0.69480807  0.1054089  -0.15883335]
 [ 1.36627837  0.1736621   0.17036402]]

Veamos cómo trabajar si tenemos el código para realizar las rotaciones en C.

Primer ejemplo: Nuestro código

El código en C que tenemos es:

 typedef struct {
      float m[3][3];
    } m3x3;

    typedef struct {
      float a[3];
    } v3;

...

float * rotate(float angles[3], float *v, int N){

        m3x3 R = matrix_rotation(angles);

        float* y = (float*)malloc(3*N*sizeof(float));
        v3 p;

        printf("%p\n",y);
        for(int i=0; i<N; i++){
            // p = &y[i*3];
            p = matmul3(R,&v[i*3]);
            y[i*3+0] = p.a[0];
            y[i*3+1] = p.a[1];
            y[i*3+2] = p.a[2];
            // printf("%6.3f %6.3f %6.3f \n",y[i*3+0],y[i*3+1],y[i*3+2]);
        }
        return y;


  }
cd ../scripts/interfacing_C
/home/fiol/Clases/IntPython/clases-python/scripts/interfacing_C

CTypes

No vamos a usar directamente Ctypes, sino a través de NumPy, que provee algunas funciones convenientes para acceder al código C.

El primer paso es compilar nuestro código y generar una biblioteca:

$ gcc -fpic -Wall -shared rotacion.c -o librotacion.so

Si uno trabaja en Windows con MS C++, generará una dll

cl.exe -c rotacion.c
link.exe /DLL /OUT:rotacion.dll

Si en cambio instaló gcc, puede usar el comando previo.

!gcc -fpic -Wall -shared rotacion.c -o librotacion.so
!ls
librotacion.so      rotacion.c

En segundo lugar, importamos el módulo ctypeslib

import numpy.ctypeslib as ctl

Este módulo nos provee de la función load_library para importar la biblioteca

help(ctl.load_library)
Help on function load_library in module numpy.ctypeslib:

load_library(libname, loader_path)
    It is possible to load a library using

    >>> lib = ctypes.cdll[<full_path_name>] # doctest: +SKIP

    But there are cross-platform considerations, such as library file extensions,
    plus the fact Windows will just load the first library it finds with that name.
    NumPy supplies the load_library function as a convenience.

    .. versionchanged:: 1.20.0
        Allow libname and loader_path to take any
        python:path-like object.

    Parameters
    ----------
    libname : path-like
        Name of the library, which can have 'lib' as a prefix,
        but without an extension.
    loader_path : path-like
        Where the library can be found.

    Returns
    -------
    ctypes.cdll[libpath] : library object
       A ctypes library object

    Raises
    ------
    OSError
        If there is no library with the expected extension, or the
        library is defective and cannot be loaded.
rotc = ctl.load_library('librotacion.so','.')

Una vez cargada la biblioteca, tenemos que definir adecuadamente cómo pasar los argumentos a la función rotate de C:

float * rotate(float angles[3], float *v, int N)

Para eso se utiliza la función argtypes que recibe una lista de tipos. Notemos que los dos primeros argumentos son arreglos de C (o sea, punteros), mientras que el último es un entero.

npflags = ['C_CONTIGUOUS']   # Require a C contiguous array in memory

float_1d_type = ctl.ndpointer(dtype=np.float32, ndim=1, flags=npflags) # Puntero a float, 1D
float_2d_type = ctl.ndpointer(dtype=np.float32, ndim=2, flags=npflags) # Puntero a float, 2D
type(float_1d_type)
_ctypes.PyCSimpleType

Con estos tipos de datos, defino los tipos de argumentos, que son tres en total. El último es un dato de tipo entero, para lo cual se usa directamente c_intp. Para definir el tipo de argumentos de entrada a la función rotc.rotate usamos el método argtypes:

rotc.rotate.argtypes =  [float_1d_type, float_2d_type, ctl.c_intp]
help(rotc.rotate)
Help on _FuncPtr in module ctypes object:

rotate = class _FuncPtr(_ctypes.CFuncPtr)
 |  Method resolution order:
 |      _FuncPtr
 |      _ctypes.CFuncPtr
 |      _ctypes._CData
 |      builtins.object
 |
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)
 |
 |  ----------------------------------------------------------------------
 |  Methods inherited from _ctypes.CFuncPtr:
 |
 |  __bool__(self, /)
 |      True if self else False
 |
 |  __call__(self, /, *args, **kwargs)
 |      Call self as a function.
 |
 |  __repr__(self, /)
 |      Return repr(self).
 |
 |  ----------------------------------------------------------------------
 |  Static methods inherited from _ctypes.CFuncPtr:
 |
 |  __new__(*args, **kwargs) from _ctypes.PyCFuncPtrType
 |      Create and return a new object.  See help(type) for accurate signature.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from _ctypes.CFuncPtr:
 |
 |  argtypes
 |      specify the argument types
 |
 |  errcheck
 |      a function to check for errors
 |
 |  restype
 |      specify the result type
 |
 |  ----------------------------------------------------------------------
 |  Methods inherited from _ctypes._CData:
 |
 |  __ctypes_from_outparam__(...)
 |
 |  __hash__(self, /)
 |      Return hash(self).
 |
 |  __reduce__(...)
 |      Helper for pickle.
 |
 |  __setstate__(...)

Hagamos un ejemplo sencillo con N=2

N = 2
# Ángulos de Euler
angle = np.random.random(3).astype(np.float32)
# Definimos N vectores tridimensionales
v = np.random.random((3, N)).astype(np.float32)

Las funciones que dispongo en C reciben tipos float, es decir que me tengo que asegurar esto a través del método astype.

Ahora tenemos que definir el tipo de dato de salida, que retorna C a través de un puntero a float, float*. Para esto usamos el método restype. Como a priori no sé qué tipo de rango tiene mi arreglo de salida, tengo que definirlo explícitamente.

rotc.rotate.restype = ctl.ndpointer(dtype=np.float32, shape=(N,3))

Hay que tener precaución con el manejo de arreglos, que es muy distinto en C y Python. En Python son objetos, de los cuales yo puedo tener distintas vistas, slices, etc. Hay que recordar que en principio estas son formas de acceder al mismo objeto, pero no se pueden traducir directamente a C, que necesita un arreglo contiguo de datos.

v = np.array([[1,0], [0,1], [0,0]]).astype(np.float32)
vt = v.T.copy()

print(v)
print(vt)
print(np.shape(v))
print(np.shape(v.T))
[[1. 0.]
 [0. 1.]
 [0. 0.]]
[[1. 0. 0.]
 [0. 1. 0.]]
(3, 2)
(2, 3)

Veamos, v es un arreglo de 3 filas y 2 columnas, que contiene dos vectores de tres dimensiones que se desean rotar, organizados como columnas. Esto no es lo que necesita mi arreglo en C, que es tiene los vectores organizados contiguamente en un solo arreglo unidimensional. Entonces, tengo que transformarlo. Para eso usamos el .T. Ojo que además, hay que crear un objeto nuevo con copy(), sino es una vista del mismo objeto v.

angle90 = np.array([0,0,np.pi/2],dtype = np.float32)
print(angle90)
[0.        0.        1.5707964]
yf = rotc.rotate(angle90,
                      vt,
                      N)
y = rotate(angle90,v)
np.set_printoptions(suppress=True) # suppress controla cómo print va a escribir los números de punto flotante.

print(y)
print(yf.T)
np.allclose(y,yf.T)
[[-0.00000004  1.        ]
 [-1.         -0.00000004]
 [ 0.          0.        ]]
[[-0.00000004  1.        ]
 [-1.         -0.00000004]
 [ 0.          0.        ]]
True

Interfaces con clases en C++

El ejemplo original está acá que sigue este ejemplo:

El código en C++ que tenemos es:

class Test{
     private:
        int n;
     public:
        Test(int k){
            n=k;
        }
        void setInt(int k){
            n = k;
        }
        int getInt(){
            return n;
        }
};
cd ../scripts/interfacing_Cpp
/home/fiol/Clases/IntPython/clases-python/scripts/interfacing_Cpp

La implementación de Python que estamos usando está escrita en C, de modo tal que tenemos que exportar las funciones de la clase Test en C++ en el código fuente de la siguiente manera:

extern "C"
{
    // include below each method you want to make visible outside
    Test* init(int k) {return new Test(k);}
    void setInt(Test *self, int k) {self->setInt(k);}
    int getInt(Test *self) {return self->getInt();}

    // Add the declaration '__declspec(dllexport)' before each function in Windows
}

La declaración extern "C" indican al compilador de C++ que genere código compatible con C de todas las funciones incluídas en el bloque.

CTypes

Vamos ahora a usar directamente Ctypes. Como antes, el primer paso es compilar nuestro código y generar una biblioteca:

$ g++ -fpic -shared test.cpp -o libtest.so

Si uno trabaja en Windows, generará una dll

cl.exe -c test.cpp
link.exe /DLL /OUT:test.dll
# !gcc -fpic -Wall -shared rotacion.c -o librotacion.so
!g++ -fpic -shared test.cpp -o libtest.so
!ls
libtest.so  test.cpp

En segundo lugar, importamos el módulo ctypes

import ctypes

Este módulo nos provee de la función CDLL para importar la biblioteca

lib = ctypes.CDLL('./libtest.so')

Ahora vamos a crear una clase en Python equivalente a la que teníamos en C++. Al igual que en el caso de C, tenemos que establecer los tipos de datos de entrada (via el método argtypes) y salida (vía el método restype) para cada función de la clase.

class Test():
    def __init__(self, val: int):
        # Declare input and output types for each method you intend to use
        lib.init.argtypes = [ctypes.c_int]
        lib.init.restype = ctypes.c_void_p

        lib.setInt.argtypes = [ctypes.c_void_p, ctypes.c_int]
        lib.setInt.restype = ctypes.c_void_p

        lib.getInt.argtypes = [ctypes.c_void_p]
        lib.getInt.restype = ctypes.c_int

        # use the C++ constructor to build the instance
        self.q = lib.init(val)

    def setInt(self, n):
        lib.setInt(self.q, n)

    def getInt(self):
        return lib.getInt(self.q)
T1 = Test(12)
print(T1.getInt())
T1.setInt(32)
print(T1.getInt())
12
32
type(T1.q)
int