Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Adds support for array parameter declaration in fortran code #15457

Merged
merged 2 commits into from
Nov 23, 2023

Conversation

melissawm
Copy link
Member

@melissawm melissawm commented Jan 28, 2020

Hello,

This PR is supposed to add support for array parameter declaration in fortran code. Previously, this resulted in an error when trying to use array parameter values to define other variables in the same code (see bug #11612)

I also added some tests, including one which is supposed to fail right now because this fix does not include the special case of array parameters with 0-based indexing in fortran.

Closes #11612
Closes #8730

Copy link
Contributor

@pearu pearu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! I agree that the PR could be split into several smaller ones each fixing one issue at the time. Otherwise, PR-s may grow with no limit.

numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
@melissawm
Copy link
Member Author

Hi all, I just added a few comments to let you know that I'm planning on working on more delicate issues in separate PR's so we can do proper testing and review. The code above works for simple cases. I don't know if this process is the recommended one, let me know what you think. Thanks!

@mattip
Copy link
Member

mattip commented Mar 5, 2020

Just to be clear: you want to merge this as-is with the problematic indexing? If so, could you add an TODO: fix ... in the code?

@eric-wieser
Copy link
Member

eric-wieser commented Mar 5, 2020

My understanding was that #15706 was extracted from this, so should be merged first.

Edit: Looks like they were made orthogonal, so the order matters less

@melissawm
Copy link
Member Author

Sorry, this was a bit messy but I think I have a better version now. I also added two tests, one which is meant to record the fact that for now we can't parse multidimensional parameter arrays, since this would imply executing fortran code before the extension module is built (e.g. the intrinsic reshape function).

The code changed quite a bit because I tried incorporating all suggestions while also improving my own understanding of how crackfortran works. Thanks for reviewing and for the patience in waiting for the fix!

@melissawm
Copy link
Member Author

@pearu can you take a look when you have the time? Thanks!

# This is an array parameter.
# First, we parse the dimension information
a = [item[9:] for item in varsn['attrspec'] if item[:9] == 'dimension'][0]
dimrange = a.lstrip('(').rstrip(')')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably safer as

Suggested change
dimrange = a.lstrip('(').rstrip(')')
a = a.strip() # does fortran care about whitespace?
if a[0] != "(" or a[-1] != ")":
raise ValueError(...)
dimrange = a[1:-1]

Otherwise you could strip off unbalanced parentheses

# is often incompatible with the original fortran indexing)
# 2) allow the parameter array to be accessed in python as a dictionary with fortran
# indices as keys
# We are choosing 2 for now.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option here would be:

class FortranParamArray:
    def __init__(self, indices, values):
        self._indices = indices
        self._values = values
    def __getitem__(self, index):
        return self._values[self._indices.index(index)]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And pass the range object in as the indices argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't consider this because the f2py code does not use OOP and I think this might be a part of a larger refactor in the future.

Copy link
Contributor

@pearu pearu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @melissawm for developing param_eval!
My only concern is that it is too rigorous in reporting unsupported (read: notimplemented) features as failures while f2py may not need these features when creating the wrapper code.

The used naming convention in "crackfortran.py" has an actual purpose: be forgiving on things that do not matter :)

y = self.module.foo_array_any_index(x)
assert_equal(y, x.reshape((2,3), order='F'))

def test_constant_array_multidimensional(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this test is expected to fail? f2py should pick up only the following information from the code:

subroutine foo_multidimensional_array(x)
        integer, parameter:: dp = selected_real_kind(15)
        real(dp), intent(inout):: x(5, 3, 2)
        end subroutine

The parameter y is irrelevant here and all the issues with it should be ignored by f2py.

@@ -2873,6 +2906,84 @@ def analyzevars(block):
analyzeargs_re_1 = re.compile(r'\A[a-z]+[\w$]*\Z', re.I)


def param_eval(n, varsn, v, g_params, params):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

param_eval is a complicated function and deserves explicit testing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if the tests I added are enough.

d = str(params[d])
if d[:d.find("(")] in params:
Copy link
Contributor

@pearu pearu Jun 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit1: a white space before ( in d would break this code.
nit2: consider

>>> d='abc'
>>> d[:d.find('(')]
'ab'

Hence, d[:d.find("(")] in params is buggy. Suggest checking .find(..) return value against -1 before continuing.

except Exception as msg:
params[n] = v
outmess('get_parameters: got "%s" on %s\n' % (msg, repr(v)))
params[n] = param_eval(n, vars[n], v, g_params, params)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since param_eval does not implement the corresponding Fortran specification in full, and often it does not matter (see test_constant_array_multidimensional), the try-except block should remain.

dimrange = range(int(dimrange[0]), int(dimrange[1])+1)
else:
print(n)
raise Exception('param_eval: multidimensional array parameters not supported: %s\n' % repr(n))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just report this via outmess rather than raising an exception because the corresponding case is often irrelevant for the f2py task.

@melissawm melissawm marked this pull request as draft June 30, 2020 17:35
@melissawm melissawm marked this pull request as ready for review July 20, 2020 22:22
@melissawm
Copy link
Member Author

I think I addressed everything, feedback is appreciated. Thanks once again for the patience and for the time spent reviewing this!

' supported: %s\n' % repr(n))

# Parse parameter value
v = v.lstrip('(/').rstrip('/)').split(',')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strip is likely not suitable here, it maps (/(a), b/) to a), b.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the code to address this, but I'm not sure if another approach would be preferable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v = (v[2:-2] if v.startswith('(/') else v).split(',')


then `d = 2` and we return immediately, with

`param_parse(d, params) = '2'`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write this in the form of a doctest, with >>>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if this is what you had in mind. I could write a proper doctest but that would involve populating the entire param dictionary which I don't think is the idea. I tried looking for similar tests in the rest of the code but couldn't find any. Thanks again for your patience on this, I'm still learning! Feel free to keep commenting if you think anything can be improved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put a suggestion above - but yes, a minimally populated params dictionary is exactly what I'm looking for, since otherwise it's hard to tell what form that argument takes.

@melissawm
Copy link
Member Author

(Sorry for the mess in the history - I didn't squash yet because it might make it harder to review. I'll do that once this is ready to merge)

pearu
pearu previously requested changes Oct 25, 2021
Copy link
Contributor

@pearu pearu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one bug and have a few nits. Otherwise, looks good!

numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
numpy/f2py/crackfortran.py Outdated Show resolved Hide resolved
@HaoZeke
Copy link
Member

HaoZeke commented Dec 15, 2021

@melissawm could you squash this before I rebase it?

@melissawm
Copy link
Member Author

Done, thanks @HaoZeke

@HaoZeke
Copy link
Member

HaoZeke commented Dec 19, 2021

Thanks @melissawm. This actually rebases cleanly, the relevant branch is here. So:

# Grab the F2PY refactor PR
gh pr checkout 20481
# Add it to your fork, assumes origin to be the fork
git push origin HEAD:f2pyRefactor
# Goto your branch for this PR
git checkout f2py-param-fix
# Rebase onto the refactor
git rebase -i f2pyRefactor
# Push the updated branch
git push -f

In case it does not apply cleanly, the other option is to checkout the branch mentioned above and force push that to this PR. Let me know if this can be made clearer for the other PRs :)

Copy link
Member

@HaoZeke HaoZeke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a release note for param_eval would be nice, otherwise this is ready.

@charris
Copy link
Member

charris commented Jun 9, 2022

@melissawm Needs rebase. Seems this was about ready to go.

Copy link
Member

@HaoZeke HaoZeke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs some work after the changes introduced in the recent PRs, but the issue still exists.

@bilderbuchi
Copy link
Contributor

@melissawm what's the status of this PR? It seems it was ~finished/ready to go already?
I just hit #11612, and this PR got my hopes for a fix up :D

@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label Sep 26, 2022
@charris
Copy link
Member

charris commented Nov 15, 2022

@melissawm Is this still in progress?

@pearu @HaoZeke Have your requested changes been dealt with?

@charris
Copy link
Member

charris commented Nov 15, 2022

Needs rebase.

@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Dec 6, 2022
Copy link
Member

@HaoZeke HaoZeke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, and fixes the still present #11612 so it would be great to have this in :)

I think all the requested changes were made, and subsequent iterations can clean up any new issues.

@HaoZeke HaoZeke requested a review from pearu April 15, 2023 19:46
@mattip
Copy link
Member

mattip commented Apr 20, 2023

@pearu could you take a look?

@HaoZeke
Copy link
Member

HaoZeke commented Nov 20, 2023

Would be nice to have this in :) I think any additional changes can be done in followups if necessary.

@HaoZeke
Copy link
Member

HaoZeke commented Nov 22, 2023

@pearu could you take a look / remove the review changes requested note? Can't be merged otherwise

EDIT: Dismissing the review to get this in :)

@rgommers rgommers added this to the 2.0.0 release milestone Nov 23, 2023
@HaoZeke HaoZeke dismissed pearu’s stale review November 23, 2023 19:42

Stale review, changes are good to go.

@HaoZeke HaoZeke merged commit a820164 into numpy:main Nov 23, 2023
60 checks passed
f2py core automation moved this from In progress to Done Nov 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

Successfully merging this pull request may close these issues.

f2py cannot handle array PARAMETER Wrong pyf with declaration of parameters in modules
8 participants