Skip to content

BUG: incompatibility between 1.23 and 1.24 for f2py generated interface to string arrays #23356

Closed

Description

@bobeldering

Describe the issue:

Since NumPy version 1.24, f2py generates for Fortran arrays of strings (e.g. CHARACTER WORD(3) * 8) a NumPy array object with dtype |S1, where the string length is added as a last dimension of the array (so the shape for the example in 1.24 is (3, 8)). In prior versions the array would have a dtype with the size of the Fortran string and the same dimensions as the Fortran type.

Reproduce the code example:

Fortran code (test.f):

      PROGRAM TEST
      
      CHARACTER        WORD*8
      CHARACTER        WORDARR(3)*8
      CHARACTER        WORDARR2(3, 4)*8
      
      COMMON  /BLOCK/  WORD, WORDARR, WORDARR2
      END

Command to generate Python module:

python -m numpy.f2py -c test.f -m test

Python test script:

import test
for attr in ('word', 'wordarr', 'wordarr2'):
    var = getattr(test.block, attr)
    print(attr, var.shape, var.dtype)

Error message:

Version 1.23.5 output:

word () |S8
wordarr (3,) |S8
wordarr2 (3, 4) |S8

Version 1.24.2 output:

word (8,) |S8
wordarr (3, 8) |S1
wordarr2 (3, 4, 8) |S1

Runtime information:

>>> import sys, numpy; print(numpy.__version__); print(sys.version); print(numpy.show_runtime())

1.24.2
3.10.4 (main, Mar 31 2022, 08:41:55) [GCC 7.5.0]
[{'simd_extensions': {'baseline': ['SSE', 'SSE2', 'SSE3'],
'found': ['SSSE3',
'SSE41',
'POPCNT',
'SSE42',
'AVX',
'F16C',
'FMA3',
'AVX2'],
'not_found': ['AVX512F',
'AVX512CD',
'AVX512_KNL',
'AVX512_KNM',
'AVX512_SKX',
'AVX512_CLX',
'AVX512_CNL',
'AVX512_ICL']}},
{'architecture': 'Haswell',
'filepath': '/jop91_0/eldering/anaconda3/envs/test/lib/python3.10/site-packages/numpy.libs/libopenblas64_p-r0-15028c96.3.21.so',
'internal_api': 'openblas',
'num_threads': 8,
'prefix': 'libopenblas',
'threading_layer': 'pthreads',
'user_api': 'blas',
'version': '0.3.21'}]
None

Context for the issue:

The issue of WORD having both a shape of (8,) and a dtype of |S8 is solved by #23194. But the differences between version 1.23 and 1.24 for WORDARR and WORDARR2 remain.

The issue of equivalence between string length and array dimensions is mentioned here #19388. But I didn't get from that discussion that the way version 1.24 treats string arrays is an intended feature. Also, this difference isn't mentioned in the release notes. So I assume it is a bug. Please correct me if I'm mistaken.

Activity

  1. charris commented on Mar 7, 2023

    @charris
    Member

    @HaoZeke Thoughts?

  2. bobeldering commented on Mar 13, 2023

    @bobeldering
    ContributorAuthor

    I tried to understand what changed with version 1.24. The most relevant change to this problem seems to be the removal of the special case handling of strings around line 145 of fortranobject.c.
    This special case previously used the last dimension of the array as the element size and then decreased the rank.

    Another bit of code that might be relevant (and gets me quite confused) is in crackfortran.py
    Here the reverse of the above special case is executed: adding the element size to the dimensions of an array.

  3. seberg commented on Mar 16, 2023

    @seberg
    Member

    Maybe @2sn you have a quick idea, just wondering if the fix may be to extend the older PR with an array path?

  4. 2sn commented on Mar 16, 2023

    @2sn
    Contributor

    @seberg I am not quite sure I understand the example (I never wrote fortran code that uses the type declarations for strings in that way). I can confirm that in some old version of f2py it did not like the len= parameter, and sometimes it used to produce an array of char 1 of length of the string. I had written some wrappers for that to get the strings in python I needed. The new version seemed to fix that except the was an actual bug in f2py that it it took the sting length and also generated an array length of that value of that length of string - obviously beyond memory boundaries. The PR I contributed just fixes that for scalar strings and may also address this oversight in the 1.24.2 NumPy version. I think it may also fix some of the cases shown above.

    word (8,) |S8
    

    That now should definitely return

    word () |S8
    

    This was my PR.

    I thought these two cases

    wordarr (3, 8) |S1
    wordarr2 (3, 4, 8) |S1
    

    would (certainly should) still do the same as listed above for 1.23.5. But I do not recall whether I tested it.

    When do we expect a release of NumPy 1.24.3?

  5. seberg commented on Mar 16, 2023

    @seberg
    Member

    Hmmm, not sure we have a concrete plan for 1.24.3 yet, so if we have some follow-ups to fix this, we could probably include it. Or are you just hoping for 1.24.3 soon?
    There may be a few issues to mop-up, but I suspect we could do a release pretty soon.

  6. 2sn commented on Mar 16, 2023

    @2sn
    Contributor

    I just tried out the current main branch. Using the definitions above within a module for a current project (for the sake of a quick test) I get

    In [2]: multistar._interface.data.word.dtype
    Out[2]: dtype('S8')
    
    In [3]: multistar._interface.data.word.shape
    Out[3]: ()
    
    In [5]: multistar._interface.data.wordarr.dtype
    Out[5]: dtype('S1')
    
    In [6]: multistar._interface.data.wordarr.shape
    Out[6]: (3, 8)
    
    In [7]: multistar._interface.data.wordarr2.dtype
    Out[7]: dtype('S1')
    
    In [8]: multistar._interface.data.wordarr2.shape
    Out[8]: (3, 4, 8)
    

    So, my PR is included and fixes the scalar case, as it should, but I have included a test that checks the array cases work as well. So I was confused what happened here, whether there have been other changes to f2py since, and whether the test was ignored or was changed as well. Or I whether messed it up. The last two cases should not have had the last dimension by should have had dtype |S8 - at least that is what I would find desirable.

    You can see these tests in
    numpy/f2py/tests/src/string/scalar_string.f90
    and
    numpy/f2py/tests/test_character.py
    of my PR, testing exactly that. So I was not sure what happened to these test in the current main branch. The tests seem there. How can it not work?

    So I gave it another try, and change definition to modern fortran:

    module data
    
      CHARACTER(len=8) :: WORD
      CHARACTER(len=8), dimension(3) :: WORDARR
      CHARACTER(len=8), dimension(3,4) :: WORDARR2
    
      save
    
    end module data
    

    Now this gives the expected results, that are also in the test case of my PR:

    In [2]: multistar._interface.data.word.dtype
    Out[2]: dtype('S8')
    
    In [3]: multistar._interface.data.word.shape
    Out[3]: ()
    
    In [4]: multistar._interface.data.wordarr.dtype
    Out[4]: dtype('S8')
    
    In [5]: multistar._interface.data.wordarr.shape
    Out[5]: (3,)
    
    In [6]: multistar._interface.data.wordarr2.dtype
    Out[6]: dtype('S8')
    
    In [7]: multistar._interface.data.word.shape
    Out[7]: ()
    

    The issue must then be that current crackfortran does not properly interpret the construct

          CHARACTER        WORDARR(3)*8
    

    and takes the second number as a dimension as well, whereas it interprets modern format specification correctly.

    So, unless someone knows crackfortran well enough to debug this dated fortran code case, maybe it is an option for @bobeldering to modify the type specification in the quoted Fortran code to a more modern format that is implemented in f2py correctly?

  7. bobeldering commented on Mar 22, 2023

    @bobeldering
    ContributorAuthor

    With the knowledge that the parsing works for Fortran 90 style string arrays, I had another look at the Fortran parsing

    As I understand it, the pair of functions cracktypespec0 and cracktypespec parse the type definition as far as it is in front of the variable name. The part after the variable name is parsed in updatevars.

    In updatevars, the dict from the regular expression match parsing the type definition after the variable name, is updated for a few cases. One of those cases adds the length to the dimensions and removes the length (if the parsed type has both a length and dimensions). Another transforms the length to a charselector or kindselector, but since this case is handled after the previous one, it isn't evaluated for string arrays.

    I made a patch, which fixes the problem for this test case, by switching the order in which these checks are handled. The patch passes all the f2py test. But as I mentioned before, I'm quite confused about the purpose of the case where the length is added to the dimensions. It clearly used to produce the correct results with the special case handling of strings in fortranobject.c (that was removed in 1.24). I can make a PR for this patch, but I'm curious if anybody knows what the purpose of these lines of code is (was?).

  8. seberg commented on Mar 24, 2023

    @seberg
    Member

    @bobeldering do you want to make a PR for the patch? It might be easier to get someone to comment on that. Is the if 'len' in d1 and 'array' in d1: line maybe relevant for numeric arrays running into the same thing?

  9. bobeldering commented on Mar 27, 2023

    @bobeldering
    ContributorAuthor

    I extended the test case with some types from the first Fortran77 documentation I could find, to check how it handles number types:

          PROGRAM TEST
          
          CHARACTER        WORD*8
          CHARACTER        WORDARR(3)*8
          CHARACTER        WORDARR2(3,5)*8
          INTEGER          NUMBER
          INTEGER          NUMARR(3)
          INTEGER * 8      LONGNUM
          INTEGER * 8      LNUMARR(3)
          REAL             FLT
          REAL             FLTARR(3)
          REAL * 8         LONGFLT
          REAL * 8         LFLTARR(3)
    
          CHARACTER        LETTER
          
          COMMON  /BLOCK/  WORD, WORDARR, WORDARR2,
         1     NUMBER, NUMARR, LONGNUM, LNUMARR,
         2     FLT, FLTARR, LONGFLT, LFLTARR,
         3     LETTER
          END
    
    

    Printing the shape and dtype gave some interesting results:

    word () |S8
    wordarr (3,) |S8
    wordarr2 (3, 5) |S8
    number () int32
    numarr (3,) int32
    longnum () int32
    lnumarr () int32
    flt () float32
    longflt () int32
    lfltarr () int32
    letter () |S1
    

    So both the dtype and shape for the * 8 types seems off to me. However, versions 1.23.5, 1.24.2 and the patched version all produce the same results for the non-character based types. So I consider this a possibly related, but separate bug and made a PR.

  10. 2sn commented on Mar 27, 2023

    @2sn
    Contributor

    @bobeldering I would say the character types look OK with dtype |S, however, the lnumar seems broken, all long and double types a are just the single and real counterparts, which seems very wrong. Even long floats end up as int.

    Could you try what happens if you use implicit none (as all your variables that star l and i and up as single ints)?
    Are there any type-overwrites enabled in the fortran compiler flags?

  11. bobeldering commented on Mar 27, 2023

    @bobeldering
    ContributorAuthor

    @2sn I think you are correct that the types are implicitly derived from the variable name. Changing the L to a Z gives:

    zongnum () float32
    znumarr () float32
    zongflt () float32
    zfltarr () float32
    

    Adding implicit none gives a strong hint that the parsing failed, f2py generates extra warning messages:

    Line #9 in test.f:"      INTEGER * 8      LONGNUM"
    	updatevars: no name pattern found for entity='*8longnum'. Skipping.
    Line #10 in test.f:"      INTEGER * 8      ZNUMARR(3)"
    	updatevars: no name pattern found for entity='*8znumarr(3)'. Skipping.
    Line #13 in test.f:"      REAL * 8         ZONGFLT"
    	updatevars: no name pattern found for entity='*8zongflt'. Skipping.
    Line #15 in test.f:"      REAL * 8         ZFLTARR(3)"
    	updatevars: no name pattern found for entity='*8zfltarr(3)'. Skipping.
    

    So it looks like the * 8s are not parsed in the pre variable name type definition, but instead prepended to the variable name. And possibly float32 is just the default type?

  12. 2sn commented on Mar 27, 2023

    @2sn
    Contributor

    @bobeldering I still recommend to try putting in an implicit none statement. Apparently long and double type spects are still ignored. Which is really strange. This should have worked traditionally. Hence I also wonder whether there are any type overwrites in the compile flags. Something line -i8-i4 or so.

  13. bobeldering commented on Mar 28, 2023

    @bobeldering
    ContributorAuthor

    The warnings messages occur with the implicit none statement added. I forgot to mention that this attempt eventually failed to build. The complete output is:

    $ python -m numpy.f2py -c test.f -m test 
    running build
    running config_cc
    INFO: unifing config_cc, config, build_clib, build_ext, build commands --compiler options
    running config_fc
    INFO: unifing config_fc, config, build_clib, build_ext, build commands --fcompiler options
    running build_src
    INFO: build_src
    INFO: building extension "test" sources
    INFO: f2py options: []
    INFO: f2py:> /tmp/tmpps93dx32/src.linux-x86_64-3.10/testmodule.c
    creating /tmp/tmpps93dx32/src.linux-x86_64-3.10
    Reading fortran codes...
    	Reading file 'test.f' (format:fix,strict)
    Line #1 in test.f:"      implicit none"
    	analyzeline: no group yet. Creating program group with name "test".
    updatevars: "character wordarr(3)*8" is mapped to "character wordarr(3,8)"
    updatevars: "character wordarr2(3,5)*8" is mapped to "character wordarr2(3,5,8)"
    Line #9 in test.f:"      INTEGER * 8      LONGNUM"
    	updatevars: no name pattern found for entity='*8longnum'. Skipping.
    Line #10 in test.f:"      INTEGER * 8      LNUMARR(3)"
    	updatevars: no name pattern found for entity='*8lnumarr(3)'. Skipping.
    Line #13 in test.f:"      REAL * 8         LONGFLT"
    	updatevars: no name pattern found for entity='*8longflt'. Skipping.
    Line #15 in test.f:"      REAL * 8         LFLTARR(3)"
    	updatevars: no name pattern found for entity='*8lfltarr(3)'. Skipping.
    crackline: groupcounter=3 groupname={0: '', 1: 'program', 2: 'module', 3: 'interface', 4: 'program'}
    crackline: Mismatch of blocks encountered. Trying to fix it by assuming "end" statement.
    Post-processing...
    Applying post-processing hooks...
      character_backward_compatibility_hook
    Post-processing (stage 2)...
    Building modules...
    error: f2py target file '/tmp/tmpps93dx32/src.linux-x86_64-3.10/testmodule.c' not generated
    
  14. 2sn commented on Mar 28, 2023

    @2sn
    Contributor

    @bobeldering Yes, I can reproduce your findings. W/o the implicit none I get from

    for attr in  dir(_test.block):
        if attr.startswith("__"):
            continue
        var = getattr(_test.block, attr)
        print(attr, var.shape, var.dtype) 
    

    the output

    flt () float32
    fltarr (3,) float32
    letter () |S1
    lfltarr () int32
    lnumarr () int32
    longflt () int32
    longnum () int32
    numarr (3,) int32
    number () int32
    word () |S8
    wordarr (3,) |S8
    wordarr2 (3, 5) |S8
    

    but when I add it I also get the error message

    running build
    running config_cc
    INFO: unifing config_cc, config, build_clib, build_ext, build commands --compiler options
    running config_fc
    INFO: unifing config_fc, config, build_clib, build_ext, build commands --fcompiler options
    running build_src
    INFO: build_src
    INFO: building extension "_test" sources
    INFO: f2py options: []
    INFO: f2py:> /tmp/tmpcbhadm69/src.linux-x86_64-3.11/_testmodule.c
    creating /tmp/tmpcbhadm69/src.linux-x86_64-3.11
    Reading fortran codes...
            Reading file 'test25.f' (format:fix,strict)
    updatevars: "character wordarr(3)*8" is mapped to "character wordarr(3,8)"
    updatevars: "character wordarr2(3,5)*8" is mapped to "character wordarr2(3,5,8)"
    Line #10 in test25.f:"      INTEGER * 8      LONGNUM"
            updatevars: no name pattern found for entity='*8longnum'. Skipping.
    Line #11 in test25.f:"      INTEGER * 8      LNUMARR(3)"
            updatevars: no name pattern found for entity='*8lnumarr(3)'. Skipping.
    Line #14 in test25.f:"      REAL * 8         LONGFLT"
            updatevars: no name pattern found for entity='*8longflt'. Skipping.
    Line #16 in test25.f:"      REAL * 8         LFLTARR(3)"
            updatevars: no name pattern found for entity='*8lfltarr(3)'. Skipping.
    Post-processing...
            Block: _test
                            Block: test
    etc. ...
    

    gfortran itself, however, compiles it just fine, so the issue must lay with updatevars. Apparently, it does not know how to handle this pattern. May I ask, is there any specific reason not to update to modern f90-style declarations? If you have lots of code, I presume an AI could handle that for you.

  15. 2sn commented on Mar 28, 2023

    @2sn
    Contributor
    please reformat the following code to use f95-style type declarations
    
          PROGRAM TEST
    
          CHARACTER        WORD*8
          CHARACTER        WORDARR(3)*8
          CHARACTER        WORDARR2(3,5)*8
          INTEGER          NUMBER
          INTEGER          NUMARR(3)
          INTEGER * 8      LONGNUM
          INTEGER * 8      LNUMARR(3)
          REAL             FLT
          REAL             FLTARR(3)
          REAL * 8         LONGFLT
          REAL * 8         LFLTARR(3)
    
          CHARACTER        LETTER
    
          COMMON  /BLOCK/  WORD, WORDARR, WORDARR2,
         1     NUMBER, NUMARR, LONGNUM, LNUMARR,
         2     FLT, FLTARR, LONGFLT, LFLTARR,
         3     LETTER
          END
    

    result

    PROGRAM TEST
    
      CHARACTER(8)          :: WORD
      CHARACTER(8), DIMENSION(3) :: WORDARR
      CHARACTER(8), DIMENSION(3,5) :: WORDARR2
      INTEGER               :: NUMBER
      INTEGER, DIMENSION(3) :: NUMARR
      INTEGER*8             :: LONGNUM
      INTEGER*8, DIMENSION(3) :: LNUMARR
      REAL                  :: FLT
      REAL, DIMENSION(3)    :: FLTARR
      REAL*8                :: LONGFLT
      REAL*8, DIMENSION(3)  :: LFLTARR
    
      CHARACTER             :: LETTER
    
      COMMON /BLOCK/ WORD, WORDARR, WORDARR2, &
                     NUMBER, NUMARR, LONGNUM, LNUMARR, &
                     FLT, FLTARR, LONGFLT, LFLTARR, &
                     LETTER
    END PROGRAM TEST
    

    This compiles and gives the result

    flt () float32
    fltarr (3,) float32
    letter () |S1
    lfltarr (3,) float64
    lnumarr (3,) int64
    longflt () float64
    longnum () int64
    numarr (3,) int32
    number () int32
    word () |S8
    wordarr () |S3
    wordarr2 (3,) |S5
    

    as desired.

  16. seberg commented on Mar 28, 2023

    @seberg
    Member

    (Didn't quite follow the discussion) As long as we are confident that the suggest fix doesn't break the numeric things which used to work, we could just apply and backport it, though? Even if the better fix is to use f95-style declarations in the mid-term, probably.

    That is, if you two are pretty confident about that being the case?

  17. 2sn commented on Mar 28, 2023

    @2sn
    Contributor

    I did not review the fix. So I can't advise. My apologies.

  18. 2sn commented on Mar 28, 2023

    @2sn
    Contributor

    @seberg I think that most likely a small addition or adjustment of a pattern in updatevars might fix it, but I do not have time at the present to look into it. But in general, I think it would indeed be preferable for f2py being able to compile f77-style codes as well. Although I no longer use f77-style code (all re-written during lock-downs).

  19. bobeldering commented on Mar 28, 2023

    @bobeldering
    ContributorAuthor

    The Fortran 77 code I'm using f2py for is large-ish; about 80k lines. The general idea is to slowly change the code base to Python (when changes to the functionality are required) and use f2py to generate the wrappers for the places where the Python code still calls the old Fortran code. I prefer to touch the Fortran code as little as possible, reasons for that include that I'm not very familiar with Fortran and it would complicate incorporating patches to the Fortran code (I don't expect many, but it might still happen).

    The numeric types I added to the example above were just for testing whether the patch affects them. The Fortran code I'm working with doesn't use those numeric types, neither does is use implicit typing. The only problem I'm running into with NumPy 1.24 is the handling of string arrays declared like CHARACTER WORDARR(3)*8.

  20. 2sn commented on Mar 28, 2023

    @2sn
    Contributor

    @bobeldering My advice would be to

    • keep as much code as possible in Fortran as it is a lot faster. In particular calls and explicit loops. In my experience, one call through f2py is worth about thousand lines of fortran code in terms of time cost.
    • learn fortran 77. It is very simple, can be learnt in an afternoon. ;-)
  21. seberg commented on Apr 25, 2023

    @seberg
    Member

    This should have been fixed by gh-23637 (and the original PR).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions