Thanks to visit codestin.com
Credit goes to github.com

Skip to content

isclose behavior on non-finite complex values seems incorrect #15959

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

Open
mruberry opened this issue Apr 12, 2020 · 19 comments
Open

isclose behavior on non-finite complex values seems incorrect #15959

mruberry opened this issue Apr 12, 2020 · 19 comments

Comments

@mruberry
Copy link

For example:

np.isclose(complex(float('inf'), 0), complex(float('inf'), 0))
: False

np.isclose(complex(1, float('inf')), complex(1, float('nan')), equal_nan=True)
: True

In the first case it seems like the same number should be close to itself, and in the latter case I think the numbers should not be close since NaN is not close to anything.

Python's cmath.isclose behavior appears to demand that complex numbers containing -inf or inf must be identical to be close. It does not support equal_nan so complex numbers containing NaN are never close to other complex numbers. If equal_nan is True I would expect NaNs to act like infinities and require the numbers be identical to be "close."

@rgommers
Copy link
Member

Thanks @mruberry, that's a clear bug. np.isclose doesn't have any complex tests, and the RuntimeWarning the above code snippet triggers hints that the implementation wasn't done with complex values in mind:

/home/rgommers/anaconda3/lib/python3.7/site-packages/numpy/core/numeric.py:2267: RuntimeWarning: invalid value encountered in multiply
  x = x * ones_like(cond)
/home/rgommers/anaconda3/lib/python3.7/site-packages/numpy/core/numeric.py:2268: RuntimeWarning: invalid value encountered in multiply
  y = y * ones_like(cond)

Python's cmath.isclose behavior appears to demand that complex numbers containing -inf or inf must be identical to be close. It does not support equal_nan so complex numbers containing NaN are never close to other complex numbers. If equal_nan is True I would expect NaNs to act like infinities and require the numbers be identical to be "close."

That sounds right.

@Qiyu8
Copy link
Member

Qiyu8 commented Apr 13, 2020

as far as I know, Complex Numbers cannot be compared. which means that it's unreasonable to judging whether 1+2i is close to 2+1i.

@mruberry
Copy link
Author

as far as I know, Complex Numbers cannot be compared. which means that it's unreasonable to judging whether 1+2i is close to 2+1i.

While the complex numbers aren't part of any ordered field, which means there's not a great mathematical concept of saying that a < b, there is a well-defined and widely accepted notion of saying "a is close to b" even when both a and b are complex. See Python's cmath.isclose, for example: https://docs.python.org/3/library/cmath.html.

@seberg
Copy link
Member

seberg commented Apr 13, 2020

It might be a bit more complicated? Since np.isnan actually True or False, but our complex numbers can have only one of the components being NaN. That is a slightly weird number, but means that np.nan and 1j*np.nan can easily be considered "identical" by accident. I am curious if we can rephrase isclose (to avoid np.isnan, np.isfinite, etc.) or make isclose a ufunc itself.

@mruberry
Copy link
Author

The implementation we're proposing for PyTorch would never consider (NaN, 0) close to (0, NaN). That said, while isfinite(Complex) seems OK to define (true if neither part is -inf, inf, or NaN), isnan(Complex) may not be definable.

@seberg
Copy link
Member

seberg commented Apr 13, 2020

The thing is, if we make it a ufunc, or implement with something like subtract_allow_nonfinite, or even distance and distance_nonfinite could make sense (I do not like that really, so isclose as a ufunc is probably most reasonble in the long run). The thing is that complex is just one issue. In theory types like quaternions can exist and have similar subtleties.

@rgommers
Copy link
Member

The thing is, if we make it a ufunc,

I suggest to keep this focused on behavior. We can't make it a ufunc anyway (it has non-ufunc keywords), but either way it's not really relevant to the discussion.

It might be a bit more complicated? Since np.isnan actually True or False, but our complex numbers can have only one of the components being NaN. That is a slightly weird number, but means that np.nan and 1j*np.nan can easily be considered "identical" by accident.

This may make sense, given that a complex number really is a single number, so the whole complex number should be "not a number". (nan + 0j) and (0 + nanj) can be constructed, but I'd say isnan is correct here in just saying they're both NaN.

np.nan and 1j*np.nan can easily be considered "identical" by accident.

I'd say that's fine. We're talking about "identical" for semantic purposes here, not numerical behavior.

@eric-wieser
Copy link
Member

Just a reminder that matching the behavior of cmath.isclose exactly isn't really an option, for the same reason that today isclose(float) does not match math.isclose(float) (gh-10161).

@seberg
Copy link
Member

seberg commented Apr 14, 2020

@rgommers right, you can say that 1+np.nan*1j is just one well to spell complex NaN. For most code maybe that is even a good way to define it, although things may get confusing if you then do arr.real and they are completely different.

I may have been thinking a bit more of testing, where this is not what you want probably. However, testing is not as important in the sense that I do not worry about generalizing it. The reason why I mention ufuncs is that a ufunc is the best way we have find a solution without putting in complex special paths everywhere which do not generalize.

facebook-github-bot pushed a commit to pytorch/pytorch that referenced this issue Apr 22, 2020
Summary:
Previously torch.isclose would RuntimeError when called on complex tensors. This update updates torch.isclose to run on complex tensors and be consistent with [NumPy](https://numpy.org/doc/1.18/reference/generated/numpy.isclose.html). However, NumPy's handling of NaN, -inf, and inf values is odd, so I adopted  Python's [cmath.isclose](https://docs.python.org/3/library/cmath.html) behavior when dealing with them. See numpy/numpy#15959 for more on NumPy's behavior.

While implementing complex isclose I also simplified the isclose algorithm to:

- A is close to B if A and B are equal, if equal_nan is true then NaN is equal to NaN
- If A and B are finite, then A is close to B if `abs(a - b) <= (atol + abs(rtol * b))`

This PR also documents torch.isclose, since it was undocumented, and adds multiple tests for its behavior to test_torch.py since it had no dedicated tests.

The PR leaves equal_nan=True with complex inputs an error for now, pending the outcome of numpy/numpy#15959.
Pull Request resolved: #36456

Differential Revision: D21159853

Pulled By: mruberry

fbshipit-source-id: fb18fa7048e6104cc24f5ce308fdfb0ba5e4bb30
@mruberry
Copy link
Author

Follow-up from PyTorch: complex isclose was implemented in pytorch/pytorch#36456. We punted on supporting equal_nan=True, however, pending resolution of this discussion. Seems like the questions to be resolved are:

  • Should equal_nan=True be supported for complex isclose at all?
  • If it is, are numbers like NaN + 5j "close" to numbers like 0 + NaNj when equal_nan=True? Both of these are considered NaN by np.isnan, as @rgommers points out.

@rgommers
Copy link
Member

I'd lean towards yes, support equal_nan=True (unless the implementation turns out to be messy, but it looks straightforward). And being consistent with isnan.

@mruberry
Copy link
Author

@rgommers I agree with you. The implementation is simple snd the concept consistent.

@seberg
Copy link
Member

seberg commented Apr 30, 2020

I think I am fine with defining it this way. Would be good to briefly mention it in the documentation though that this employs isnan and (unlike np.testing.assert_arrays_equal) does not distinguish between different NaN values, which is interesting e.g. for complex. (But potentially also for quaternions and other possible objects with a whole valid dimension of NaNs).

There are some other subtleties, which are maybe not immediately vital... If we assume that np.isnan(NaT) (not a time) may return True in the future, arguably isclose(NaT, NaN) should still be a type error, but is currently not.

@seberg
Copy link
Member

seberg commented Jun 4, 2020

@vrakesh and @anirudh2290 pointed out that we should make sure to stay aligned with Python (not sure if that means updating us or them).
One thing, I noticed is that Python currently defines:

cmath.isclose(np.inf, complex(np.inf, 1), rel_tol=20, abs_tol=1e100)

as False. I.e. if one of the entries is inf, even the other must match exactly. From a mathematical point, I would expect this to honor absolute tolerance. As for relative tolerance, arguably, unless it is 0 it is close. If it is 0, returning False with a warning (as we have) is maybe the best we can do?

@anirudh2290
Copy link
Member

So to summarize, we should consider the current behavior

>>> np.isclose(np.array([1 + 2.j]), np.array([2 + 1.j]))
array([False])

as a bug and fix it.

For inf, to be aligned with python, if there is an inf in any component then both components have to exactly match otherwise its false. It doesnt look like it honors absolute in python, so we shouldnt support it either.

>>> cmath.isclose(complex(np.inf, 1), complex(np.inf, 1.1), abs_tol=1)
False

For rel_tol, also it seems to not consider rel_tol

For equal_nan, it seems fine to say that if isnan is true then it is nan and if equal_nan is True for isclose then isclose should return true. We can add the isnan bit to the documentation.

Also, the PEP https://www.python.org/dev/peps/pep-0485/ indicates that complex tolerances can be passed in though that doesn't seem to be really true.

>>> cmath.isclose(complex(np.inf, 1), complex(np.inf, 1.1), rel_tol=complex(0, 1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float

@rgommers
Copy link
Member

rgommers commented Jun 4, 2020

np.isclose(np.array([1 + 2.j]), np.array([2 + 1.j]))

I assume you forgot to replace some number with inf here? The issue is about inf/nan, and I don't see anything wrong here with the given code snippet.

@rgommers
Copy link
Member

rgommers commented Jun 4, 2020

@vrakesh and @anirudh2290 pointed out that we should make sure to stay aligned with Python (not sure if that means updating us or them).

To repeat what I've said a few times before: that's great if it's possible, but not a good enough reason to break backwards compatibility in case NumPy and Python both made reasonable but different choices (didn't check in detail if that's the case here). That's especially true if Python came way later with its implementation.

@seberg
Copy link
Member

seberg commented Jun 4, 2020

Oh, I had forgotten after all this discussion that the original issue post already mentioned cmath and this behaviour. We need to decide what we think is best, and then open a bpo and maybe document the differences. I personally think the python behaviour with partial complex inf may just be an oversight (although python is in slightly different situations about giving warnings)?

@anirudh2290
Copy link
Member

np.isclose(np.array([1 + 2.j]), np.array([2 + 1.j]))

I assume you forgot to replace some number with inf here? The issue is about inf/nan, and I don't see anything wrong here with the given code snippet.

😄 sorry, my bad.

yes for inf it looks like it will be a breaking change, though i find the following behavior inconsistent in numpy:

>>> np.isclose(np.inf, np.inf)
True
>>> np.isclose(np.inf + 1.j, np.inf + 1.j)
False

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

No branches or pull requests

6 participants