3

After some digging online and trial-error, I am still wondering how to passing arrays of strings from Python to Fortran by f2py.

I have the Fortran subroutine in string.f90 as:

  SUBROUTINE FOO(A)
  CHARACTER*5,dimension(10),intent(inout):: A
  PRINT*, "A=",A
  END

Then I run f2py -m mystring -c string.f90. The compiling was successful.

The python session is in test.py:

import mystring
import numpy as np
nobstot=10
xstring=np.empty(nobstot,dtype='S5')
xstring[0]="ABCDE"
mystring.foo(xstring)

Run python test.py, I have the error message:

1-th dimension must be 5 but got 0 (not defined).
Traceback (most recent call last) :
File "test.py", line 6, in <module>
mystring.foo(xstring)
mystring.error: failed in converting 1st argument `a' of mystring.foo to C/Fortran array

In the f2py compiling step, the gfortran and gcc compiler were invoked.

After >>> print mystring.foo.__doc__, there was:

foo(a)
Wrapper for ``foo``.
Parameters
---------
a : in/output rank-2 array('S') with bounds (10,5)

So, I tried test.py as:

import mystring
import numpy as np
nobstot=10
xstring=np.empty((nobstot,5),dtype='S1')
print xstring.shape
xstring[0]="ABCDE"
mystring.foo(xstring)

Then run python test.py, the error message was:

Traceback (most recent call last):
File "test.py", line 7, in <module>
mystring.foo(xstring)
ValueError: failed to initialize intent(inout) array -- input 'S' not compatible to 'c'
Ting Lei
  • 91
  • 9

4 Answers4

3

First, to pass array of strings to Fortran, in Python, you must create an array of chars with shape (<number of strings>, <string length>), fill its content, and then pass the char array to f2py generated function. Using your example:

xstring = np.empty((nobstot, 5), dtype='c')
xstring[0] = "ABCDE"
xstring[1] = "FGHIJ"
mystring.foo(xstring)

In order this to work, you'll need to change your Fortran code too:

subroutine foo(A)
character*5, dimension(10), intent(in) :: A
print*, "A(1)=",A(1)
print*, "A(2)=",A(2)
end

Notice that intent(inout) is replaced with intent(in). This is because strings in Python, as well as strings in numpy arrays of strings, are immutable but in Fortran they may not be. As a result, the memory layout of Python strings cannot simply passed to Fortran functions and the user must reorganize the string data as explained above.

Second, if your Fortran code changes the strings, as usage of intent(inout) suggests, you'll need to declare such string arguments as intent(in, out), for instance, by using f2py directive. Here follows a complete example:

subroutine foo(A)
character*5, dimension(10), intent(inout) :: A
!f2py intent(in, out) A
print*, "A(1)=",A(1)
print*, "A(2)=",A(2)
A(1)="QWERT"
end

F2py call:

f2py -m mystring -c string.f90

Python test script:

import mystring
import numpy as np
nobstot = 10
xstring = np.empty((nobstot, 5), dtype='c')
xstring[0] = "ABCDE"
xstring[1] = "FGHIJ"
xstring = mystring.foo(xstring)
print("xstring[0]=",string[0].tostring())
print("xstring[1]=",string[1].tostring())

Console output:

 A(1)=ABCDE
 A(2)=FGHIJ
xstring[0]= QWERT
xstring[1]= FGHIJ
Pearu
  • 46
  • 3
  • I don't understand why the Fortran code should be changed the way you show. (Leaving aside the syntax-error typo.) – Vladimir F Героям слава Jan 27 '17 at 10:19
  • Normally, the `!f2py intent(in, out) A` is not needed, because the Fortran `intent(inout)` is used instead automatically. What is the reason you say it is needed here? – Vladimir F Героям слава Jan 27 '17 at 10:20
  • The true reason why `!f2py intent(in, out)` is needed, is that f2py support for string arrays is incomplete. numpy string arrays were introduced after the active development of f2py. When completing the string array support, usage of numpy string arrays would resolve this issue for good. Till then one can still use f2py, but with above mentioned restrictions (use numpy char arrays instead of numpy string array). – Pearu Jan 27 '17 at 12:11
  • @pearu, Yes, it works using your method. (just typo like in "print "....",string .." while it should be "print "..", xstring..." ) . Your help including explanation are so appreciated! – Ting Lei Jan 27 '17 at 15:02
  • Well, OK, I understand you need a numpy array of characters, but why the Fortran `intent(inout)` does not suffice when it does suffice for all other arrays or scalars? And 2) I still can't see the reason for *"you'll need to change your Fortran code too"* in your first Fortran snippet. In fact I can't spot there any significant change at all. – Vladimir F Героям слава Jan 27 '17 at 15:27
  • May be you see I am just being PITA, but I do think your answer is good, the advice to use array of characters is crucial. I just don't see any reason for the other points. – Vladimir F Героям слава Jan 27 '17 at 15:33
  • 1) `intent(inout)` is not sufficient for strings because f2py does not fully implement support for string arrays. However, for other scalars, f2py has full support. 2) In the first solution, `intent(inout)` is replaced with `intent(in)`. Without this change the example does not work. So, to me it is a significant change. Re PITA: I appreciate it. However note that the given issue has several solutions depending on what is allowed to be changed. I presented only two of them (there's also signature file approach). I'd prefer the second solution till f2py has better support for string arrays. – Pearu Jan 28 '17 at 11:26
0

Communicating string to and from fortran is a little tricky.

With this line you establisted practical a two-dimension character-array 10 * 5

CHARACTER*5,dimension(10),intent(inout):: A

try change it to

CHARACTER*10,intent(inout):: A

that makes it a one-dimensional array of 10 characters. If it works, but the output is garbage, check if both are the same character format (ascii/multibyte or unicode).

Alex
  • 779
  • 7
  • 15
  • Thanks a lot. Yes, when it is declared as a scalar of string, the python script finished ok. And, fortunately, the output seems ok as expected. For being now, I will stick to your suggestion for my purpose. If I have any new info or more straightforward solution easier to be used in real cases, I will update it here. For being now, I will wait for some time to see if there are other suggestions before I choose yours as the answer. – Ting Lei Jan 27 '17 at 03:17
  • An update: I found in the Fortran subroutine, it could be declared as the character of adjustable length, like: CHARACTER(*),intent(inout):: A. This will make this scheme more powerful than I thought. – Ting Lei Jan 27 '17 at 04:31
  • 10 times 5 = 50 , so why CHARACTER*10 and not CHARACTER*50? – Vladimir F Героям слава Jan 27 '17 at 10:17
  • @Vladimir F, I guess CHARACTER*10 is a typo too here (plz correct me if i'm wrong, @Alex). Though, for Fortran, the passing of argument is by the position, so the case when the dummy argument use less memory spaces than the actual argument is safe. and as I mentioned, a more flexible solution following Alex's is to use the character(*). So many thanks to all of you. – Ting Lei Jan 27 '17 at 15:08
  • Well safe, but you will see garbage. Be careful, `character(*)` behaves differently and has a different ABI. A separate hidden argument about the string length is passed. But where will f2py get the correct value for the hidden argument if you pass an array of chars? If it uses the last dimension of the numpy char array, then fair enough. – Vladimir F Героям слава Jan 27 '17 at 15:32
  • @Vladimir F, When I used the character(*), from the print ..__doc__, there was "A , in/output rank-0 array(string(len=-1),'c')". And it worked as expected in the test case. Does this mean it is reliable even when the numpy string arrays are used? Alex also mentioned possible garbage output from the character format, So I will first stick to Pearu's method to avoid any uncertainties. – Ting Lei Jan 27 '17 at 15:56
0

Will this work @Pearu ? convert ASCII -> int with ord and directly send a numpy array

xstring = np.zeros((10, 5)).astype(int)
strings = ["ABCDE","FGHIJ"]
for istring,string in enumerate(strings):
   for ichar,char in enumerate(string):
      xstring[istring,ichar] = ord(char)
 
mystring.foo(xstring)
ansri
  • 37
  • 6
0

following the comments here I tried to reproduce this approach inside my Fortran/python code, but I don't have any success. In my code, I need to pass an array of strings with unknown sizes, and the string with unknown lengths. For example, inside my Fortran code I have:

   character(len=*), dimension(:), intent(in)  :: string

And following this topic, this doesn't work for me.

So I did some modifications and found an approach that will be useful for me and for sharing here. I know that this is an old topic, but I believe that other people can use it.

When I pass an array of strings from python to Fortran, f2py transform an array of string in some like string(:,:), where the 1st dimension is the string length and the 2nd dimension is the array length. With this in my mind I modify the Fortran in this way:

subroutine foo(A)
   character, dimension(:,:), intent(in) :: A
   character(len=25), allocatable :: B(:)
   integer :: strLen
   integer :: arrLen

   strLen = size(A,1)
   arrLen = size(A,2)
   allocate(B(arrLen))
   do i = 1, arrLen
      B(i) = transfer(A(:,i),B(i)(1:strLen))
   enddo
   
   do i=1, arrLen
      print*,trim(B(i))
   enddo

end

Python test script, with different strings and array length:

import mystring
import numpy as np

print('strings for test 1:')
xstring = np.array(['ABCDE','FGHIJ'],dtype='c').T
mystring.foo(xstring)

print('strings for test 2:')
isisList = ['amsua_n15    ', 'amsua_n18    ', 'amsua_n19    ', 'amsua_metop-a']
xstring = np.array(isisList,dtype='c').T
mystring.foo(xstring)

Console result:

strings for test 1:
 ABCDE
 FGHIJ
strings for test 2:
 amsua_n15
 amsua_n18
 amsua_n19
 amsua_metop-a