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

Add inverse hyperbolic functions asinh(), acosh() & atanh() #7110

Closed
jynus opened this issue Jun 18, 2023 · 8 comments · Fixed by godotengine/godot#78404
Closed

Add inverse hyperbolic functions asinh(), acosh() & atanh() #7110

jynus opened this issue Jun 18, 2023 · 8 comments · Fixed by godotengine/godot#78404
Milestone

Comments

@jynus
Copy link

jynus commented Jun 18, 2023

Describe the project you are working on

I was documenting the _draw() low level functions at godotengine/godot-docs#7532 and, as part of teaching a real-world problem wanted to implement a catenary shape (a rope shape between 2 points, not by simulating it physically, but numerically), which requires the usage of a hyperbolic arc (or inverse) tangent function:

func catenary(x : float) -> float:
    """
    Given a value x, a point1 and a _point2 in screen coordinates,
    and a length, return the value y (also in screen coordinates)
    of the catenary function that passes through both point1 and
    _point2 that has dimension length.
    Method adapted from <https://math.stackexchange.com/questions/
    3557767/how-to-construct-a-catenary-of-a-specified-length-
    through-two-specified-points>.
    """
    const E = 2.718281828459045

    # inverting point1 for screen coordinates
    var point0 = Vector2(point1.x, 2 * _point2.y - point1.y)
    var dx = _point2.x - point0.x
    var xb = (_point2.x + point0.x) / 2
    var dy = _point2.y - point0.y
    var r = sqrt(length * length - dy * dy) / dx

    # solve r = sinh(A) / A for A, with Newton method
    var A = sqrt(6 * (r - 1))
    while (r - sinh(A) / A) > 0.00000001:
        A = A - (sinh(A) - r * A) / (cosh(A - r))

    var a = dx / (2 * A)
    var b = xb - a * atanh(dy / length)
    var c = point0.y - a * cosh((point0.x - b) / a)
    var y = a * cosh((x - b)/ a) + c
    return 2 * _point2.y - y  # return screen coordinates

Describe the problem or limitation you are having in your project

Sadly, the function doesn't exist on GDScript, despite being available on C++ & C# Math libaries, and even in GLSL since 2018 godotengine/godot#16794.

Having no other option, I had to implement a workaround in GDScript, like this:

func atanh(x : float) -> float:
    """
    Implement missing arc tangent hyperbolic aka inverse of the
    tangent hyperbolic function, missing from Godot
    """
    return (float(1)/2 * log(float(1 + x)/float(1 - x)))

However, this has many issues:

  • Godot engine, through C++, already has that function built-in, it only has to make it available on GDScript. Having it on C++ will allow a faster execution with very little loss (almost 0 extra code needed except the binding)
  • GDScript already has sin(), cos(), tan(), asin(), acos(), atan(), sinh(), cosh(), tanh(), but not the inverse of these last functions. A user of mathematics functions will expect those functions to be available as asinh(), acosh() atanh() if the direct hyperbolic functions already exist and will be confused why not. While I understand GDScript should not have all math functions and (specially given they are builtin and global scope) measure should be used, either it contains all hyperbolic functions or none of them, appearing quite inconsistent in design.
  • It makes the parity between using C# or GDScript inconsistent- while GDScript should not have all C# standard library, it makes it inconsistent with its math library
  • By adding this functions I don't have to keep documenting "Sadly, GDScript lacks this functionality, so here is a block of code to do the same" only for the GDSCript version, which looks bad.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Create and document the implementation of asinh, acosh and atanh in its Math (globalscope) library, so they are available to the users. Once it is done, docs can be updated to use the same implementation for both C# and GDScript, people are no longer confused by the lack of only some of the hyperbolic functions, and users can now use those to solve curves from differential equations, commonly seen on proyects and games in cartesian coordinates.

From Wikipedia:

They also occur in the solutions of many linear differential equations (such as the equation defining a catenary), cubic equations, and Laplace's equation in Cartesian coordinates. Laplace's equations are important in many areas of physics, including electromagnetic theory, heat transfer, fluid dynamics, and special relativity.

In fact, they are so common that they were added to the shader language to generate effects- those same effect are needed to do some geometry and drawing transformations.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Expose those 3 functions are they are implemented on C++. For the period in which they are not defined, clamp their value to the last valid value or INFINITY -INFINITY if the latest value tends to that. This last part is arguable and very open to discussion.

If this enhancement will not be used often, can it be worked around with a few lines of script?

Yes, I added the lines of script- but as I said before, the issue is not that they are not there, the issue is the inconsistency with only being partially implemented. I would be ok with removing all hyperbolic functions, but that would make no sense and would make GDScript inconsistent with C++, C# and its own shader language.

Is there a reason why this should be core and not an add-on in the asset library?

More complex mathematical functions should be on its own library- we should not convert Godot's Math into numpy, but I think these 3 functions SPECIALLY due to the lightness of its implementation and maintenance, should have its place on globalscope. I will myself add all documentation changes needed.

The only issue I can see is that, because non-virtual builtin methods cannot be shadowed, those projects that have implemented those functions on their own to overcome the issue will see an error on their projects. However, while this could mean the the deployment should happen on a major release, I think it is one of those regressions that is "worth it".

@theraot
Copy link

theraot commented Jun 20, 2023

I tested the pull request (godotengine/godot#78404), it works. I also had a look at the code, and found it to be a very straightforward implementation.

Now, when atanh goes out of range, do you really want INFINITY? If you want parity with other languages (which is a point in this proposal), you want NAN.

Be aware that on -1.0 and 1.0 the function atanh is undefined, with one-sided limits approaching negative and positive infinity respectively... But beyond that range the limit does not exist (assuming we are working on the reals, which is the standard. Otherwise, we would be working on the complex, in which case atanh is not infinite beyond that range either).

Perhaps you have an argument for infinity that I have not thought of?

Godot has is_finite which checks against both infinity and nan, so checking won't be an issue.

@AThousandShips
Copy link
Member

AThousandShips commented Jun 20, 2023

This is pretty much equivalent to doing atanh(CLAMP(x,-1,1)) while silencing math errors, which is the approach of save asin/acos, so I think it's in line with what we do currently

@jynus
Copy link
Author

jynus commented Jun 20, 2023

Sorry, I just saw this comment before the PR comment- I wasn't sure about what exactly to do (as I mention on the proposal).

I thought of NaN, but like @AThousandShips perfectly says I ended up using infinity/-infinity as to mimic the solution on the other functions "out of range", in which it was decided to "not break" calculations (send NaN).

I also saw that gdscript has no issue working with INF (as it is called there) values -but I am not like 100% sold on that, I am open to other suggestions if there are other alternatives, as long as those are documented. INF will not be issue-free, just will behave "better" than NaN.

@kleonc
Copy link
Member

kleonc commented Jun 20, 2023

Now, when atanh goes out of range, do you really want INFINITY?

I think -INF/INF makes sense here, given the argument is assumed to be clamped to the function's domain. And

  • atanh(x) is continous,
  • $\displaystyle\lim_{x \to -1^+} atanh(x) = -\infty$,
  • $\displaystyle\lim_{x \to 1^-} atanh(x) = \infty$.

This is pretty much equivalent to doing atanh(CLAMP(x,-1,1)) while silencing math errors, which is the approach of save asin/acos, so I think it's in line with what we do currently

What's worth explicitly noting though (in the docs) is that for atanh(x) the x argument is conceptually being clamped to the open interval $(-1, 1)$, not to the closed one $[-1, 1]$ (as mentioned both endpoints are not in the domain).

@jynus
Copy link
Author

jynus commented Jun 20, 2023

What's worth explicitly noting though (in the docs)

Yes, I will try to make this clear on the docs with an amend, as it is a less straightforward decision than on the other cases.

@theraot
Copy link

theraot commented Jun 20, 2023

From what @kleonc says it seems i should have asked if clamping as desired, not if INFINITY was desired (yes, I'm aware of the limits, I mentioned them). Notice I wasn't asking about atanh(-1.0) and atanh(1.0) but beyond that, where other languages have libraries that return NAN instead.

What @lawnjelly says here godotengine/godot#78404 (comment) makes sense to me (NAN is worse than -INF and INF), although we can check for all of them with is_finite, the issue is getting it by surprise (i.e. the developer didn't know they had to check).

Either way I agree the documentation must be clear on what it does.

@jynus
Copy link
Author

jynus commented Jun 21, 2023

See godotengine/godot#78404#issuecomment-1600729386 for the built documentation of acosh and atanh of the latest push. Given it is a reference manual, I wanted to be precise but not extend too much about why, as it should be fast to read.

@akien-mga akien-mga added this to the 4.2 milestone Sep 1, 2023
@jynus
Copy link
Author

jynus commented Sep 1, 2023

Thank you for the review and merge! Excited to be able to use this for my catenaries on Godot! 💖

mandryskowski pushed a commit to mandryskowski/godot that referenced this issue Oct 11, 2023
GDScript has the following built-in trigonometry functions:

- `sin()`
- `cos()`
- `tan()`
- `asin()`
- `acos()`
- `atan()`
- `atan()`
- `sinh()`
- `cosh()`
- `tanh()`

However, it lacks the hyperbolic arc (also known as inverse
hyperbolic) functions:

- `asinh()`
- `acosh()`
- `atanh()`

Implement them by just exposing the C++ Math library, but clamping
its values to the closest real defined value.
For the cosine, clamp input values lower than 1 to 1.
In the case of the tangent, where the limit value is infinite,
clamp it to -inf or +inf.

References godotengine#78377
Fixes godotengine/godot-proposals#7110
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants