Search code examples
pythonnumpyfortranf2py

Pass boolean array without copying with `f2py`?


How should Fortran variables be typed if I intend to pass them a boolean, NumPy array with f2py? I have tried both integer*1 and logical*1, but these both indicate that the array is copied.

For example, if I compile the file, foo.f95, containing:

subroutine foo(x, n)
    logical*1 x(n)
    !f2py intent(in) x
    !f2py intent(hide), depend(x) :: n=shape(x,0)
    ...
end subroutine

with f2py -c -m foo foo.f90 -DF2PY_REPORT_ON_ARRAY_COPY=1 and run something like:

import numpy as np
import foo
x = np.random.randn(100) < 0
foo.foo(x)

it prints

copied an array: size=100, elsize=1

I get the same result if I change logical*1 to integer*1. What is the proper type for the boolean array in the Fortran file, so that the array is not copied?

Note that this is not a problem of memory contiguity since the arrays are 1D -- foo.foo(np.asfortranarray(x)) prints the same copy message.


Solution

  • From some experiments(*), it seems that Python/f2py treats np.int8 as compatible with logical*1, while np.bool or np.bool8 not for some reason. After inserting print *, "(fort) x = ", x into foo.f90, we get:

    >>> foo.foo( np.array( [ True, False, False ], dtype=np.bool ) )
    copied an array: size=3, elsize=1
     (fort) x =  T F F
    >>> foo.foo( np.array( [ False, True, False ], dtype=np.bool8 ) )
    copied an array: size=3, elsize=1
     (fort) x =  F T F
    >>> foo.foo( np.array( [ False, False, True ], dtype=np.int8 ) ) # no copy
     (fort) x =  F F T
    

    Because True and False are simply mapped to 1 and 0, using an int8 array on the Python side may be convenient.


    (*) Some experiments

    Here, I changed the f2py intent comment to inout to see whether we can modify the array from the Fortran side.

    foo.f90:

    subroutine foo(x, n)
        use iso_c_binding
        implicit none
        integer n
        logical*1 x( n )
        ! logical x( n )
        ! logical(c_bool) x( n )
    
        !f2py intent(inout) x
        !f2py intent(hide), depend(x) :: n=shape(x,0)
    
        print *, "(fort) x = ", x
        print *, "(fort) sizeof(x(1)) = ", sizeof(x(1))
        print *, "(fort) resetting x(:) to true"
        x(:) = .true.
    end subroutine
    

    test.py:

    import numpy as np
    import foo
    
    for T in [ np.bool, np.bool8,
               np.int,  np.int8,  np.int32,  np.int64,
               np.uint, np.uint8, np.uint32, np.uint64,
               np.dtype('b'), np.dtype('int8'), np.dtype('int32') ]:
    
        print( "-------------------------" )
        print( "dtype =", T )
    
        x = np.array( [ True, False, True ], dtype=T )
        print( "input x =", x )
    
        try:
            foo.foo( x )
            print( "output x =", x )
        except:
            print( "failed" )
    

    Results with logical*1:

    -------------------------
    dtype = <class 'bool'>
    input x = [ True False  True]
    failed
    -------------------------
    dtype = <class 'numpy.bool_'>
    input x = [ True False  True]
    failed
    -------------------------
    dtype = <class 'int'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.int8'>
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [1 1 1]
    -------------------------
    dtype = <class 'numpy.int32'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.int64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint8'>
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [1 1 1]
    -------------------------
    dtype = <class 'numpy.uint32'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int8
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [1 1 1]
    -------------------------
    dtype = int8
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [1 1 1]
    -------------------------
    dtype = int32
    input x = [1 0 1]
    failed
    

    Results with logical (default kind):

    -------------------------
    dtype = <class 'bool'>
    input x = [ True False  True]
    failed
    -------------------------
    dtype = <class 'numpy.bool_'>
    input x = [ True False  True]
    failed
    -------------------------
    dtype = <class 'int'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.int8'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.int32'>
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     4
     (fort) resetting x(:) to true
    output x = [1 1 1]
    -------------------------
    dtype = <class 'numpy.int64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint8'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint32'>
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     4
     (fort) resetting x(:) to true
    output x = [1 1 1]
    -------------------------
    dtype = <class 'numpy.uint64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int8
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int8
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int32
    input x = [1 0 1]
     (fort) x =  T F T
     (fort) sizeof(x(1)) =                     4
     (fort) resetting x(:) to true
    output x = [1 1 1]
    

    Results with logical(c_bool) (via iso_c_binding):

    -------------------------
    dtype = <class 'bool'>
    input x = [ True False  True]
    failed
    -------------------------
    dtype = <class 'numpy.bool_'>
    input x = [ True False  True]
    failed
    -------------------------
    dtype = <class 'int'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.int8'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.int32'>
    input x = [1 0 1]
     (fort) x =  T F F
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [65793     0     1]
    -------------------------
    dtype = <class 'numpy.int64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint8'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = <class 'numpy.uint32'>
    input x = [1 0 1]
     (fort) x =  T F F
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [65793     0     1]
    -------------------------
    dtype = <class 'numpy.uint64'>
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int8
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int8
    input x = [1 0 1]
    failed
    -------------------------
    dtype = int32
    input x = [1 0 1]
     (fort) x =  T F F
     (fort) sizeof(x(1)) =                     1
     (fort) resetting x(:) to true
    output x = [65793     0     1]
    

    For some reason, this last logical(c_bool) does not work with the above usage... (f2py seems to consider logical(c_bool) as 4 bytes, while gfortran treats it as 1 byte, so something is inconsistent...)