We mainly consider here wrapping codes in C and Fortran.
We classically wrap already existing codes to access them via Python.
Depending on the language to wrap, the tools are different.
The documentation gives several ways to wrap Fortran codes but it all boils down to the same thing:
f2py allows to wrap the Fortran code in a Python module that can be then imported
Given this simple Fortran (F90) snippet, that computes the sum of squares of the element of an array:
pyfiles/f2py/file_to_wrap.f90
subroutine sum_squares(A, res)
implicit none
real, dimension(:) :: A
real :: res
integer :: i, N
N = size(A)
res = 0.
do i=1, N
res = res + A(i)*A(i)
end do
end subroutine
The Fortran code can then be wrapped in one command:
# Syntax: python3 -m numpy.f2py -c <Fortran_files> -m <module_name>
python3 -m numpy.f2py -c "../pyfiles/f2py/file_to_wrap.f90" -m wrap_f90
This command calls the module f2py
of numpy
to compile (-c
) file_to_wrap.f90 into a Python module (-m
) named wrap_f90. The module can then be imported in Python:
import numpy as np
import wrap_f90
A = np.ones(10)
result = 0.
wrap_f90.sum_squares(A, result)
print(result)
In Fortran, it is considered best practice to put intents to subroutine arguments. This also helps f2py
to wrap efficiently the code but also changes the subroutine a bit.
Let's wrap the code updated with intents:
pyfiles/f2py/file_to_wrap2.f90
subroutine sum_squares(A, res)
implicit none
real, dimension(:), intent(in) :: A
real, intent(out) :: res
integer :: i, N
N = size(A)
res = 0.
do i=1, N
res = res + A(i)*A(i)
end do
end subroutine
Again, we wrap...
python3 -m numpy.f2py -c "../pyfiles/f2py/file_to_wrap2.f90" -m wrap_f90
And we import...
import numpy as np
import wrap_f90
A = np.ones(10)
result = wrap_f90.sum_squares(A)
print(result)
This time, f2py recognized that result
was a outgoing arg. As a consequence, the subroutine was wrapped smartly and made to return the arg.
Note that using a function
(in the Fortran sense of the term) leads to the same result (see the other example in pyfiles/f2py/file_to_wrap2.f90).
In Fortran, it is also considered best practice to organize the subroutines in modules. These are highly similar to Python modules and are in fact, intepreted as such by f2py !
Consider the following code that implements the dtw and cort computations in Fortran:
pyfiles/dtw_cort_dist/V9_fortran/dtw_cort.f90
module dtw_cort
implicit none
contains
subroutine dtwdistance(s1, s2, dtw_result)
! Computes the dtw between s1 and s2 with distance the absolute distance
doubleprecision, intent(in) :: s1(:), s2(:)
doubleprecision, intent(out) :: dtw_result
integer :: i, j
integer :: len_s1, len_s2
doubleprecision :: dist
doubleprecision, allocatable :: dtw_mat(:, :)
len_s1 = size(s1)
len_s2 = size(s1)
allocate(dtw_mat(len_s1, len_s2))
dtw_mat(1, 1) = dabs(s1(1) - s2(1))
do j = 2, len_s2
dist = dabs(s1(1) - s2(j))
dtw_mat(1, j) = dist + dtw_mat(1, j-1)
end do
do i = 2, len_s1
dist = dabs(s1(i) - s2(1))
dtw_mat(i, 1) = dist + dtw_mat(i-1, 1)
end do
! Fill the dtw_matrix
do i = 2, len_s1
do j = 2, len_s2
dist = dabs(s1(i) - s2(j))
dtw_mat(i, j) = dist + dmin1(dtw_mat(i - 1, j), &
dtw_mat(i, j - 1), &
dtw_mat(i - 1, j - 1))
end do
end do
dtw_result = dtw_mat(len_s1, len_s2)
end subroutine dtwdistance
doubleprecision function cort(s1, s2)
! Computes the cort between s1 and s2 (assuming they have the same length)
doubleprecision, intent(in) :: s1(:), s2(:)
integer :: len_s1, t
doubleprecision :: slope_1, slope_2
doubleprecision :: num, sum_square_x, sum_square_y
len_s1 = size(s1)
num = 0
sum_square_x = 0
sum_square_y = 0
do t=1, len_s1 - 1
slope_1 = s1(t + 1) - s1(t)
slope_2 = s2(t + 1) - s2(t)
num = num + slope_1 * slope_2
sum_square_x = sum_square_x + slope_1 * slope_1
sum_square_y = sum_square_y + slope_2 * slope_2
end do
cort = num / (dsqrt(sum_square_x*sum_square_y))
end function cort
end module dtw_cort
The subroutines dtwdistance
and cort
are part of the dtw_cort
module. The file can be wrapped as before
python3 -m numpy.f2py -c "../pyfiles/dtw_cort_dist/V9_fortran/dtw_cort.f90" -m distances_fort
But the import slighlty changes as dtw_cort
is now a module of distances_fort
:
import numpy as np
from distances_fort import dtw_cort
cort_result = dtw_cort.cort(s1, s2)
print(cort_result)
Note that the wrapping integrates the documentation of the function (if written...) !
from distances_fort import dtw_cort
print(dtw_cort.cort.__doc__)
Running the command python3 -m numpy.f2py
(without arguments) gives a lot of information on the supported arguments for further uses of f2py
. Know that you can this way:
The documentation of f2py (https://docs.scipy.org/doc/numpy/f2py/) can also help, covering notably:
Cf2py
directives to overcome F77 limitations (e.g. intents)f2py
inside Python scriptsThey are different ways of wrapping C code. We present CFFI.
The workflow is the following:
1. Get your C code working (with a .c and a .h)
Ok, supposed to be done
2. Set up your packaging to compile your code as a module
We give the compilation instructions in the file setup.py:
from setuptools import setup, Extension
version = "0.1"
module_distance = Extension(
name="cdtw",
sources=["cdtw.c"],
)
setup(
name="dtw_cort_dist_mat",
version=version,
description="data scientist tool for time series",
long_description="data scientist tool for time series",
classifiers=[],
author="Robert Bidochon",
author_email="robert@bidochon.fr",
license="GPL",
include_package_data=True,
install_requires=["cffi", "numpy", "setuptools"],
entry_points="",
ext_modules=[module_distance],
)
3. Compile your code
in a terminal, type:
python3 setup.py build_ext
4. In the python code, declare the function you will be using
from cffi import FFI
ffi = FFI()
ffi.cdef("double square(double x, double y);")
5. In the python code, open/load the compiled module
6. Use your functions
sq = dllib.square(2.0, 3.0)
Ctypes (standard library)
An historical tool is Swig (http://swig.org/). It allows to access C/C++ code from a variety of languages. It requires the writing of an intermediate file that describes the C API.
Cython