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

deltaangle between identical vectors return nan #463

Open
simonthor opened this issue May 9, 2024 · 4 comments · May be fixed by #465
Open

deltaangle between identical vectors return nan #463

simonthor opened this issue May 9, 2024 · 4 comments · May be fixed by #465
Assignees
Labels
bug The problem described is something that must be fixed

Comments

@simonthor
Copy link

Vector Version

1.3.1

Python Version

3.11.4

OS / Environment

Kubuntu Linux 22.04

Describe the bug

When taking two identical vectors and computing the angle between them, I expect the result to be 0. Instead, I get NaN:

> v = vector.obj(x=1, y=1, z=1)
> v.deltaangle(v)
nan

This is also true when using vector.array. I have not tried other backends.

Any additional but relevant log output

No response

@simonthor simonthor added the bug (unverified) The problem described would be a bug, but needs to be triaged label May 9, 2024
@jpivarski
Copy link
Member

Guidance from ROOT: this should return zero, rather than nan.

ROOT's TVector3 has an Angle method that returns zero for the same vector:

>>> import ROOT
>>> a = ROOT.TVector3(1.1, 2.2, 3.3)
>>> a.Angle(a)
0.0

I wasn't able to find a generic "angle" method on XYZVector.

The issue is that in

def xy_z_xy_z(lib, x1, y1, z1, x2, y2, z2):
v1m = mag.xy_z(lib, x1, y1, z1)
v2m = mag.xy_z(lib, x2, y2, z2)
return lib.arccos(dot.xy_z_xy_z(lib, x1, y1, z1, x2, y2, z2) / v1m / v2m)

the argument of arccos is supposed to be 1, but it's 1.0000000000000002. This is just an unfortunate round-off error: vector.obj(x=3, y=4, z=0) has a v.dot(v) / v.mag**2 of precisely 1.0, and vector.obj(x=3, y=4, z=5) has 0.9999999999999999, for instance.

I think a good solution would be to clamp the argument of arccos to be within -1 and 1, since $(\vec{x} \cdot \vec{y}) / (|\vec{x}| |\vec{y}|)$ is provably within this range (barring round-off error). That is, instead of

lib.arccos(ARG)

do

lib.arccos(lib.maximum(-1, lib.minimum(1, ARG)))

(with minimum and maximum being identity functions for SymPy).

@Saransh-cpp Saransh-cpp self-assigned this May 13, 2024
@Saransh-cpp
Copy link
Collaborator

Thanks for the explanation! Just to confirm - should minimum and maximum be the identity functions or sympy.Min and sympy.Max aliases for the SymPy? (lib.maximum points to sympy.Max at the moment)

@jpivarski
Copy link
Member

For SymPy, these functions should be the identity (no-op).

Maybe we need a clearer distinction among the functions on lib: they're used in two ways.

  1. To actually perform the main calculation that we want.
  2. To clean up corner cases and numerical error.

lib.nan_to_num and this particular use of lib.minimum/lib.maximum are for reason number 2. Mathematically, it's always true that

$$-1 \le \frac{\vec{x} \cdot \vec{y}}{|\vec{x}| |\vec{y}|} \le +1$$

but because floating point numbers are not exact, (as well as functions on them, like the addition and multiplication in $\cdot$), sometimes the computed value is about $10^{-16}$ outside of this range. That's enough to make a function like lib.arccos return NaN, but we really wanted lib.arccos(-1) (π) or lib.arccos(1) (0). Passing lib.arccos's argument through lib.minimum and lib.maximum adjusts for the $10^{-16}$ error.

But SymPy has no error because it's not numerical. Passing the argument through minimum/maximum would be superfluous at best, but it's worse than that because it complicates the mathematical expression in a way that would prevent simplification. (That is, unless SymPy is smart enough to recognize that the above inequality holds, and therefore minimum/maximum with ‒1 and 1 can be dropped from the expression, but I doubt SymPy is that smart. That's asking a lot from a CAS.)

It might happen at some later time that we want to use lib.minimum and lib.maximum in a mathematically important way—reason number 1, above—and then we'd need some way to distinguish between that case, which should use sympy.Min and sympy.Max, and the numerical clean-up case, which would treat this function as an identity. At that point, we'd have to extend the way we use lib to indicate that distinction. But we don't need it yet...

@Saransh-cpp Saransh-cpp linked a pull request May 13, 2024 that will close this issue
7 tasks
@Saransh-cpp Saransh-cpp added bug The problem described is something that must be fixed and removed bug (unverified) The problem described would be a bug, but needs to be triaged labels May 13, 2024
@Saransh-cpp
Copy link
Collaborator

Thanks for the detailed explanation! I've updated the definitions for lib.maximum and lib.minimum in the same PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug The problem described is something that must be fixed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants