Reputation: 364
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:
passing the array with <int*> array_variable.data
This is discouraged by Cython (https://github.com/cython/cython/wiki/tutorials-NumpyPointerToC)
Creating variable as Fortran contiguous MemoryView int[::1,:] array_variable = np.ones((10,15), dtype=np.int32, order='F')
.
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
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