8000 ENH: add `norm=forward,backward` to numpy.fft functions by cval26 · Pull Request #16476 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

ENH: add norm=forward,backward to numpy.fft functions #16476

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

Merged
merged 16 commits into from
Jul 12, 2020
Merged

ENH: add norm=forward,backward to numpy.fft functions #16476

merged 16 commits into from
Jul 12, 2020

Conversation

cval26
Copy link
Contributor
@cval26 cval26 commented Jun 1, 2020

ENH: add new kwarg value norm=inverse to numpy.fft

The kwarg option norm=inverse leads to scaling of the transforms
inverse (opposite) to that of the default option norm=None; i.e. the
forward transform is normalized with 1/n whereas the backward one
with 1. The fft routines and their tests have been modified to
reflect the changes; all tests have been passed successfully.

Closes #16126

cc @leofang @anirudh2290 @charris @mattvend

cvav-git added 5 commits June 1, 2020 15:25
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch fft-adding-norm-kwarg
# Changes to be committed:
#	modified:   _pocketfft.py
#
ENH: add new kwarg value `norm=inverse` to numpy.fft

The kwarg option `norm=inverse` leads to scaling of the transforms
inverse (opposite) to that of the default option `norm=None`; i.e. the
forward transform is normalized with `1/n` whereas the backward one
with `1`. The fft routines and their tests have been modified to
reflect the changes; all tests have been passed successfully.

Closes issue #16126
@cval26 cval26 changed the title ENH: add new kwarg value norm=inverse to numpy.fft (#16126) ENH: add new kwarg value norm=inverse to numpy.fft Jun 1, 2020
@leofang
Copy link
Contributor
leofang commented Jun 2, 2020

Thanks for quickly putting a PR together @cvav-git! I am not sure if I have enough time this week to help review, but it's on my plate 🙂

cc: @peterbell10 @grlee77 from SciPy/pyFFTW and @asi1024 from CuPy

@cval26
Copy link
Contributor Author
cval26 commented Jun 2, 2020

Absolutely @leofang, feel free to review it whenever it suits you best! I was just familiar with that part of the code and thought I'd put the PR together; it's one of my first contributions so I'm still learning a lot about the process, thank you for your help and support :)

In the meantime I'll see if I can address some of the failing automated checks.

EDIT: there are now 2 failing automated checks; one is on circleci:build and seems to have to do with docstrings of fft/__init__.py that I haven't modified so I'm not sure how to resolve that; the second is concerned with the documentation, I've modified the docstrings of the functions I've changed to reflect the addition of the new parameter choice norm=inverse, but I'm not sure if and what else do I need to change. Full disclosure: that's my first numpy PR so I apologize for any naive questions, I'm still getting familiar with the procedures!

@rossbar rossbar added the triage review Issue/PR to be discussed at the next triage meeting label Jun 3, 2020
@seberg seberg added the 62 - Python API Changes or additions to the Python API. Mailing list should usually be notified. label Jun 3, 2020
@rossbar
Copy link
Contributor
rossbar commented Jun 3, 2020

This PR led to some detailed discussions on normalization schemes in the NumPy community meeting; it would probably be good if this proposal hit the NumPy mailing list.

@rossbar rossbar added triaged Issue/PR that was discussed in a triage meeting and removed triage review Issue/PR to be discussed at the next triage meeting labels Jun 3, 2020
@leofang
Copy link
Contributor
leofang commented Jun 13, 2020

Just wanna catch up: if there's already a discussion in the mailing list, can someone post a link to the thread head here please?

@cval26
Copy link
Contributor Author
cval26 commented Jun 13, 2020

You can follow the mailing list discussion here

Copy link
Member
@seberg seberg left a comment

Choose a reason for hiding this comment

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

Looks pretty good to me, the unitary function would be nice to refactor a bit to adapt to the new option I think. If you want, you can update this with forward with the slight risk of more discussion coming up with a better name.

Thanks, lets make sure to merge this pretty soon, please ping me if I forget!

@seberg
Copy link
Member
seberg commented Jun 27, 2020

I have not carefully checked the tests yet, but they seemed very straight forward and fine.

Copy link
Contributor Author
@cval26 cval26 left a comment

Choose a reason for hiding this comment

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

made all suggested corrections and will recommit shortly; I'm using the tentative name norm=forward for the new kwarg option, which seems to be the most popular choice

EDIT: I've added a new commit with the proposed changes and the norm=forward name

Copy link
Contributor
@leofang leofang 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, @cvav-git! LGTM except for a few nitpicks.

I find NumPy's FFT tests have inconsistent styles (ex: some test if the original array can be obtained after a round trip, while others simply test the normalization factors). It could be that I'm finishing this on my phone so it's harder for me to see the full picture...Anyway I think this PR is fine.

Chris Vavaliaris and others added 2 commits June 28, 2020 15:52
Co-authored-by: Leo Fang <leofang@bnl.gov>
Co-authored-by: Leo Fang <leofang@bnl.gov>
@cval26
Copy link
Contributor Author
cval26 commented Jun 28, 2020

thanks for the review suggestions @leofang; I've added a commit with the suggestions.

@seberg it looks like the commits do not trigger the CircleCI doc checks; do you know why is that, or if they can be triggered manually smh?

@cval26
Copy link
Contributor Author
cval26 commented Jun 29, 2020

following @peterbell10 comments on the NumPy mailing list, I've added norm=backward as an alias for the default norm=None kwarg value; if we choose to move forward with this implementation, I'll have to modify more docstrings to comply, but for now I've modified the code and the numpy.fft.fft docstrings so that we can work on how they should look like. The norm=None is still the default option internally so no compatibility problems are present, it's just that norm=backward would now appear as the preferred choice in the API.

@seberg @leofang

@seberg seberg closed this Jun 29, 2020
@seberg seberg reopened this Jun 29, 2020
@cval26
Copy link
Contributor Author
cval26 commented Jun 30, 2020

following @seberg's earlier review suggestions, I've added a commit where I streamline the norm checks that were done by the private function _unitary(norm). The function has been replaced by the two private functions _go_forw(n, norm) and _go_back(n ,norm), which depending on the norm keyword value and the transform direction sets the inv_norm variable used for the normalization f 628C actor fct=1/inv_norm. The equivalent options norm=None and norm=backward are treated as one case to make sure they produce identical results (they only differ on the API level).

Since norm indicates the normalization scheme based on the transform's direction, the variable inv_norm depends on the direction, which is why two functions are introduced (could also be one function with an additional higher level if clause, but I think separating it into two functions makes for cleaner & faster code). All norm checks are now done within those two functions, whereas the fft functions only make calls with the appropriate arguments. Overall, I think the code should now be much more readable; all fft tests pass just fine as before too.

@leofang has informed us previously that no fft private functions are used by the CuPy team, so these changes should introduce no difficulties downstream.

@cval26
Copy link
Contributor Author
cval26 commented Jul 2, 2020

following @peterbell10's review suggestions I've modified the hermitian FFTs to normalize the data only once; thanks to @peterbell10 for providing the new private function _swap_direction(norm) which makes this change possible very efficiently.

I've modified all docstrings to reflect the API changes and added a release document signaling the new feature. I've also modified the tests to explicitly test the norm=backward argument value too; it's equivalent to None but just to be on the safe side. Unless someone has more suggestions or there's changes required in this commit, I think we can move on with the PR's review and merge when everyone's happy.

In terms of the mailing list discussion, I think many ppl appreciate the naming consistence with respect to round trip equality; the ppl that don't find this naming scheme straightforward also seemed to understand its usefulness, and I believe there's been a couple of days with no new messages on this matter.

@seberg @leofang

Copy link
Contributor
@leofang leofang left a comment

Choose a reason for hiding this comment

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

Sorry I dropped the ball. Overall LGTM! I left a few nitpicks, but feel free to ignore them.

Comment on lines 108 to 109
return {"backward": "forward", None: "forward",
"ortho": "ortho", "forward": "backward"}[norm]
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this is micro-optimization, but I'd prefer a plain if-else over dict for simple cases like this...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

apart from how compact it is, I think this is superior to a plain if-else clause bcs it handles the KeyError exception very well; if it's alright with you I'd suggest we go ahead with the try clause

Copy F438 link
Contributor

Choose a reason for hiding this comment

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

What's wrong with this?

if ...:
    # do this
elif ...:
    # do that
else:
    raise ValueError(...)

Copy link
Contributor

Choose a reason for hiding this comment

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

If you move the dictionary into a module level constant, dict lookup can actually be faster on average than a series of if-else chains:

_SWAP_DIRECTION_MAP = {...}

def _swap_direction(norm):
  try:
    return _SWAP_DIRECTION_MAP[norm]
  except KeyError:
    raise ValueError("")

There's not much in it though, a few 10s of nanoseconds at most I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you, Peter! Is it because this way we save time for the dict construction?

Like I said this is micro optimization, that I was just curious why dict was preferred is all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in my reply I didn't mean that using an if clause cannot handle the exception, I just meant that using a try clause makes it (IMO) cleaner and easier to read; other than that I'm happy with an if clause too;

initially I was thinking of writing it with an if clause, but @peterbell10 suggested the dict solution which I found much more streamlined compared to what I would have come up with

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i've added a commit moving the dictionary into a module level constant to accelerate the lookup; it looks like our two options are: (1) use a try clause with the dict lookup with the module level constant, (2) revert to an if clause for the direction swap instead;

let me know what you think; from the comments I understand that these are comparable in terms of performance @peterbell10 @leofang

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it because this way we save time for the dict construction?

Yes, if it's inside the function then the dict is reconstructed for every function call whereas the module level variable is constructed only once at import time.

As for dict vs if-else in a more general sense. I have no strong feeling either way, both are not that much code and both can be quite readable.

Co-authored-by: Leo Fang <leofang@bnl.gov>
@cval26
Copy link
Contributor Author
cval26 commented Jul 9, 2020

I've added a commit with the review suggestions; the only thing I'd argue we keep as is is the try clause in the hermitian FFTs; I think the way it handles the exception is much cleaner than an if clause would; let me know what you think

@seberg @leofang @peterbell10

Copy link
Contributor
@leofang leofang left a comment

Choose a reason for hiding this comment

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

Thank you @cvav-git for patiently incorporating feedbacks from so many people! LGTM.

@grlee77
Copy link
Contributor
grlee77 commented Jul 9, 2020

@cvav-git, Can you please update the PR title to reflect the new option name (norm='forward')? (Or to something generic like "add new forward normalization option to numpy.fft")

@mattip mattip changed the title ENH: add new kwarg value norm=inverse to numpy.fft ENH: add kwarg values norm=forward,backward to numpy.fft functions Jul 9, 2020
@seberg seberg changed the title ENH: add kwarg values norm=forward,backward to numpy.fft functions ENH: add norm=forward,backward to numpy.fft functions Jul 9, 2020
Copy link
Member
@seberg seberg left a comment

Choose a reason for hiding this comment

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

Thanks, LGTM, lets finish this off. The error message seems malformed, I would just avoid the \ line continuation entirely. It would be great to have some very short reminder in the norm kwarg docs, I am not quite sure what that should be, maybe anyone else here has an idea?

Comment on lines 112 to 113
raise ValueError(f'Invalid norm value {norm}; should be "backward",\
"ortho" or "forward".')
F438 Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise ValueError(f'Invalid norm value {norm}; should be "backward",\
"ortho" or "forward".')
raise ValueError(f'Invalid norm value {norm}; should be "backward", '
'"ortho" or "forward".') from None

Copy link
Member

Choose a reason for hiding this comment

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

(The line continuation change also above)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

.. versionadded:: 1.10.0

Normalization mode (see `numpy.fft`). Default is None.
Normalization mode (see `numpy.fft`). Default is "backward".
Copy link
Member

Choose a reason for hiding this comment

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

It would be great to have 1-2 sentences here pointing out that the normalization signals where the normalization occurs in the fft-ifft pair. And/or just making this slightly longer to provide some of the info from the general docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

return True
raise ValueError("Invalid norm value %s, should be None or \"ortho\"."
% norm)
def _go_forw(n, norm):
Copy link
Member

Choose a reason for hiding this comment

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

Maybe rename to _get_forward_normalization? I guess these functions are so short that any docstring is not helpful :).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

transforms by setting the keyword argument ``norm`` to ``"ortho"`` (default is
`None`) so that both direct and inverse transforms will be scaled by
:math:`1/\\sqrt{n}`.
The default normalization has the direct (forward) transforms unscaled and the
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add "backward" here. Maybe also an initial sentence just about the fact that the argument refers to the pair/which direction is transformed. It reads a bit more difficult than feels necessary to me, but I think its fine to leave for later improvements. Its not easy to write easy to read docs...

Copy link
Contributor Author
@cval26 cval26 Jul 12, 2020

Choose a reason for hiding this comment

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

ok; added a commit with all review suggestions @seberg

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, will merge once the tests run through (maybe minus the azure tests if they are acting up again).

@seberg
Copy link
Member
seberg commented Jul 12, 2020

Ok, the s390x job is hanging and azure is acting up right now (due to a setuptools update). But everything was passing, and all other checks pass and it is a python only small update. So putting it in, thanks again @cvav-git.

@seberg seberg merged commit 0862be9 into numpy:master Jul 12, 2020
@cval26
Copy link
Contributor Author
cval26 commented Jul 12, 2020

great, glad I could help! thanks for taking care of this PR :)

@leofang
Copy link
Contributor
leofang commented Jul 12, 2020

Thank you, @cvav-git and all!

@cval26 cval26 deleted the fft-adding-norm-kwarg branch July 13, 2020 02:41
@charris charris mentioned this pull request Oct 10, 2020
20 tasks
leftaroundabout added a commit to leftaroundabout/odl that referenced this pull request Dec 7, 2024
In older versions of PyFFT, the `normalise_idft` argument was only
relevant for the inverse direction, else ignored. ODLs `pyfftw_call` wrapper
was modelled around this behaviour.

Starting with PyFFT-0.13, the new NumPy normalisation arguments `norm='forward'`
and `norm='backward'` (numpy/numpy#16476) were adopted
for the high-level binders (pyFFTW/pyFFTW#308, merged in
pyFFTW/pyFFTW#330).
They implemented this by pre-matching the `norm` argument and setting
`normalise_idft` accordingly.
In case of the 'forward' normalisation this involves using `normalise_idft=False`
to signal that the normalisation should instead happen in the forward direction,
which was previously not even possible. In other words, in PyFFT<0.13
`normalise_idft=False` meant no normalisation at all was used (neither for fft
nor ifft, with the consequence that they weren't inverses in this case), and that
is the behaviour ODL expected.

Explicitly setting `normalise_idft=True` in this specific case restores the
desired behaviour, and is backwards-compatible because PyFFT<0.13 ignores
the `normalise_idft` argument in the forward direction.
leftaroundabout added a commit to leftaroundabout/odl that referenced this pull request Dec 7, 2024
In older versions of PyFFT, the `normalise_idft` argument was only
relevant for the inverse direction, else ignored. ODLs `pyfftw_call` wrapper
was modelled around this behaviour.

Starting with PyFFT-0.13, the new NumPy normalisation arguments `norm='forward'`
and `norm='backward'` (numpy/numpy#16476) were adopted
for the high-level binders (pyFFTW/pyFFTW#308, merged in
pyFFTW/pyFFTW#330).
They implemented this by pre-matching the `norm` argument and setting
`normalise_idft` accordingly.
In case of the 'forward' normalisation this involves using `normalise_idft=False`
to signal that the normalisation should instead happen in the forward direction,
which was previously not even possible. In other words, in PyFFT<0.13
`normalise_idft=False` meant no normalisation at all was used (neither for fft
nor ifft, with the consequence that they weren't inverses in this case), and that
is the behaviour ODL expected.

Explicitly setting `normalise_idft=True` in this specific case restores the
desired behaviour, and is backwards-compatible because PyFFT<0.13 ignores
the `normalise_idft` argument in the forward direction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
62 - Python API Changes or additions to the Python API. Mailing list should usually be notified. component: numpy.fft triaged Issue/PR that was discussed in a triage meeting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support norm="unnormalized" for FFT functions?
6 participants
0