heavelock
heavelock

Reputation: 364

Passing np.ndarray to Fortran with Cython

I am working on wrapping a Fortran module in Python. I chose to do it with use of Cython. My problem is passing a np.ndarray to Fortran. I am able to receive an np.ndarray from Fortran, but all my attempts to passing to Fortran didn't work.

I figured out, that the problem lays directly on the Cython - Fortran interface, as my Fotran subroutine is working properly (as much as it can work without data). The Cython side seems to be working properly too, I can manipulate variables over there.

My minimum working example:

PATTERN_wrap.f90

module PATTERN_wrap
    use iso_c_binding, only: c_float, c_double, c_short, c_int
    implicit none

CONTAINS
    subroutine c_pattern(scalar_variable, array_variable, return_array) bind(c)
        implicit NONE

        INTEGER(c_int), intent(in) :: scalar_variable
        INTEGER(c_int), intent(in), DIMENSION(10, 15) :: array_variable

        REAL(c_float), INTENT(OUT), DIMENSION(10) :: return_array

        write(*,*) "start fortran"
        write(*,*) "scalar_variable"
        write(*,*) scalar_variable
        write(*,*) "array_variable"
        write(*,*) array_variable

        return_array = 3
        write(*,*) "end fortran"


!        call DO_PATTERN(&
!                scalar_variable=scalar_variable, &
!                array_variable=array_variable, &
!                return_array=return_array)
!
    end subroutine

end module PATTERN_wrap

Note: The call to subroutine DO_PATTERN that actually does something is commented out because it's not relevant at this moment. I just wanted to point out that code above is a wrapper.

pattern.pyx

#cython: language_level=3
import cython
import numpy as np
cimport numpy as np

cdef extern:
    void c_pattern(
            int *scalar_variable,
            int *array_variable,
            float *return_array
    )

def run_pattern(
        int scalar_variable,
):
    cdef:
        np.ndarray[int, ndim=2, mode="fortran"] array_variable = np.ones((10,15), dtype=np.int32, order='F')
        np.ndarray[float, ndim=1, mode="fortran"] return_array = np.zeros(10, dtype=np.float32, order='F')

    c_pattern(
        &scalar_variable,
        &array_variable[0,0],
        &return_array[0],
    )

    print('Cython side')
    print(return_array)

    return return_array

setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
import numpy
npy_include_dir = numpy.get_include()

ext_modules = [Extension("pattern", ["pattern.pyx"],
                         include_dirs = [npy_include_dir],
                         libraries = ['gfortran', 'fftw3'], # need to include gfortran as a library
                         extra_link_args=[
                             "PATTERN_wrap.o"
                         ])]

setup(name = 'pattern',
      cmdclass = {'build_ext': build_ext},
      ext_modules = ext_modules)

I am compiling my fortran code with

gfortran -Wall -fbounds-check -lm -g -fbacktrace  -fcheck=all -Wall -ffpe-trap=zero,invalid,overflow -fPIC -L/usr/lib/ -lfftw3 -L/usr/lib/ -lfftw3 -c PATTERN_wrap.f90

and compiling the Cython code with either python -m pip install . or python setup.py build_ext --inplace. That does not seem to have any difference.

I test the package:

$ python -c "import pattern; pattern.run_pattern(2);"
 start fortran
 scalar_variable
           2
 array_variable

 end fortran
Cython side
[3. 3. 3. 3. 3. 3. 3. 3. 3. 3.]

As you can see, scalar is being passed to fortran properly, returning array is also passed back to Cython properly. The only thing not working is passing arrays from Cython to Fortran. In short, there should be an 2D array of ones printed after array_variable.

Apart of the MWE above, I tried different approaches:

All my attempts failed in the same way as MWE.

I tried also using a header file, doesn't make a difference. Header file was used for example here: Fortran - Cython Workflow This question itself does not contain answer for my question - only scalars are passed to Fortran there.

I would also like to note that the same wrapper plus all underlying files are working properly when I compile a package with f2py. The subroutine also works inside of the original Fortran program.

EDIT:

My development environment is running in docker. The baseimage is continuumio/miniconda3:4.8.2 which on the other hand is based on Debian Buster. I tested there gfortran-8 and gfortran-9 as well as a hdf5 compiler with enabled fortran. The result was all the time the same.

I decided to run my tests on my host system, Ubuntu 18.04 with gcc/gfortran 7.50. It did work properly. So I went to try different gcc versions.

I tested images:

running them with:

docker run --rm -v ~/minimum_working_example:/mwe -it gcc:7  /bin/bash

and then

apt update && apt install python3-pip -yy && cd /mwe && python3 -m pip install cython numpy && make && python3 setup.py build_ext --inplace && python3 -c "import pattern; pattern.run_pattern(2);" && rm -rf build/ *.so *.c *.mod *.o

On all of those images my code is working properly.

EDIT2:

I just ran test on bare continuumio/miniconda3:4.8.2, with the same test command (with added apt install gfortran since there is no fortran by default) and the code works.

I rebuilt my image and tested in the same way. It doesn't work...

Upvotes: 2

Views: 1039

Answers (1)

heavelock
heavelock

Reputation: 364

I managed to find the solution. The code is okay. The problem was my configuration.

As I described above, I tested different configurations of gcc/gfortran to see if that was influencing the Cythonizing. It was not. Thus I proceeded to disassemble my Dockerfile in order to find a step that was causing the code to break. It turned out, that it was installation of numpy by conda.

All my test above with ggc images I did with use of pip:

$ python -m pip install numpy
Collecting numpy
  Downloading numpy-1.18.4-cp38-cp38-manylinux1_x86_64.whl (20.7 MB)
     |████████████████████████████████| 20.7 MB 18.9 MB/s
Installing collected packages: numpy
Successfully installed numpy-1.18.4

One package, one wheel, quick and easy. However, I was using conda in my 'production' image.

If you install numpy by conda:

$ conda install numpy
Collecting package metadata (current_repodata.json): done
Solving environment: done

## Package Plan ##

  environment location: /opt/conda

  added / updated specs:
    - numpy


The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    blas-1.0                   |              mkl           6 KB
    intel-openmp-2020.1        |              217         780 KB
    libgfortran-ng-7.3.0       |       hdf63c60_0        1006 KB
    mkl-2020.1                 |              217       129.0 MB
    mkl-service-2.3.0          |   py38he904b0f_0          62 KB
    mkl_fft-1.0.15             |   py38ha843d7b_0         159 KB
    mkl_random-1.1.1           |   py38h0573a6f_0         341 KB
    numpy-1.18.1               |   py38h4f9e942_0           5 KB
    numpy-base-1.18.1          |   py38hde5b4d6_1         4.2 MB
    ------------------------------------------------------------
                                           Total:       135.5 MB

...

Important thing to note here is that conda, apart of numpy, is also installing libgfortran-ng-7.3.0. In the image I am working on, there is installed gcc/gfortran 8.5.0.

Why this is important? When you run cython compilation:

$ python setup.py build_ext --inplace
running build_ext
cythoning pattern.pyx to pattern.c
building 'pattern' extension
creating build
creating build/temp.linux-x86_64-3.8
gcc -pthread -B /opt/conda/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/opt/conda/lib/python3.8/site-packages/numpy/core/include -I/opt/conda/include/python3.8 -c pattern.c -o build/temp.linux-x86_64-3.8/pattern.o
In file included from /opt/conda/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1832,
                 from /opt/conda/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,
                 from /opt/conda/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:4,
                 from pattern.c:599:
/opt/conda/lib/python3.8/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning "Using deprecated NumPy API, disable it with " "#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
 #warning "Using deprecated NumPy API, disable it with " \
  ^~~~~~~
gcc -pthread -shared -B /opt/conda/compiler_compat -L/opt/conda/lib -Wl,-rpath=/opt/conda/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.8/pattern.o -lgfortran -o /mwe/pattern.cpython-38-x86_64-linux-gnu.so PATTERN_wrap.o

As you can see in the list line, among includes that are passed to gcc is /opt/conda/lib.

$ ls /opt/conda/lib | grep "fortran"
libgfortran.so
libgfortran.so.4
libgfortran.so.4.0.0          

Here it is, libgfortran, in different version that I compiled originally my code with.

The solution was:

$ conda install -c conda-forge libgfortran-ng==8.2.0

Note: using conda-forge channel is necessary, in my case conda was not able to resolve dependencies with packages from base channel only. What is more, this version of libgfortran-ng also required changing libblas from openblas version to mkl, if that concerns you.

In this way, I installed in conda a libgfortran that has the same major version as one I was using in my system. After reruning compilation of Cythonized package, everything worked properly.

Anyway, beware of conda.

PS: Thanks @DawidW for your feedback and testing my code.

Upvotes: 2

Related Questions