Hi all --
I've been dusting off some work from a few years ago on trig-pi functions for Numerics, and would appreciate any feedback you might have on these API to be added as requirements on the RealFunctions protocol:
/// The arccosine (inverse cosine) of `x`, scaled by 1/π.
///
/// If `x.magnitude <= 1`, the result is the angle (in half-turns) formed
/// between the positive real axis and the vector `(x, √(1-x²))`. This is
/// the value `y` in `-0.5...0.5` such that ``cos(piTimes: y)`` is `x` up
/// to floating-point rounding.
///
/// **Exact values and edge cases:**
/// - If x is not in `-1...1`, then `acosOverPi(x)` is NaN.
/// - If x is -1, then `acosOverPi(x)` is `1.0`.
/// - If x is ±0, then `acosOverPi(x)` is `0.5`.
/// - If x is +1, then `acosOverPi(x)` is `0.0`.
static func acosOverPi(_ x: Self) -> Self
/// The arcsine (inverse sine) of `x`, scaled by 1/π.
///
/// If `x.magnitude <= 1`, the result is the angle (in half-turns) formed
/// between the positive real axis and the vector `(√(1-x²), x)`. This is
/// the value `y` in `-0.5...0.5` such that ``sin(piTimes: y)`` is `x` up
/// to floating-point rounding.
///
/// **Symmetry:**
/// arcsine is an odd function. Thus `asinOverPi(-x)` is the same as
/// `-asinOverPi(x)`.
///
/// **Exact values and edge cases:**
/// - If x is not in `-1...1`, then `asinOverPi(x)` is NaN.
/// - If x is ±1, then `asinOverPi(x)` is `±0.5`.
/// - If x is ±0, then `asinOverPi(x)` is x.
static func asinOverPi(_ x: Self) -> Self
/// `atan(y/x)/π`, with representative selected based on the quadrant
/// of `(x,y)`.
///
/// The atan2OverPi function computes the angle (in half-turns,
/// in the range [-1, 1]) formed between the positive real axis and the
/// point `(x,y)`. The sign of the result always matches the sign of y.
///
/// > Warning:
/// Note the parameter ordering of this function; the `y` parameter
/// comes *before* the `x` parameter. This is a historical curiosity
/// going back to early FORTRAN math libraries (the ordering makes
/// some sense because atan2(y, x) computes atan(y / x) in the first
/// and fourth quadrants). In order to minimize opportunities for
/// confusion and subtle bugs, we require explicit parameter labels
/// with this function.
static func atan2OverPi(y: Self, x: Self) -> Self
/// The arctangent (inverse tangent) of `x`, scaled by 1/π.
///
/// The angle (in half-turns) formed between the positive real axis
/// and the point `(1, x)`. This is the value `y` in `-0.5...0.5`
/// such that ``tan(piTimes: y)`` is `x` up to floating-point rounding.
///
/// **Symmetry:**
/// arctangent is an odd function. Thus `atanOverPi(-x)` is the same as
/// `-atanOverPi(x)`.
///
/// **Exact values and edge cases:**
/// - If x is NaN, then `atanOverPi(x)` is NaN.
/// - If x is ±infinty, then `atanOverPi(x)` is `±0.5`.
/// - If x is ±1, then `atanOverPi(x)` is `±0.25`.
/// - If x is ±0, then `atanOverPi(x)` is x.
static func atanOverPi(_ x: Self) -> Self
/// The cosine of π times `x`.
///
/// Because π is not representable in any `FloatingPoint` type, for large
/// `x`, `.cos(.pi * x)` can have arbitrarily large relative error;
/// `.cos(piTimes: x)` always provides a result with small relative error.
///
/// This is observable even for modest arguments; consider `0.5`:
/// ```swift
/// Float.cos(.pi * 0.5) // 7.54979e-08
/// Float.cos(piTimes: 0.5) // 0.0
/// ```
/// It's important to be clear that there is no bug in the example
/// given above. Every step of both computations is producing the most
/// accurate possible result.
///
/// **Symmetry:**
/// cosine is an even function. Thus for every finite `x`,
/// ```swift
/// .cos(piTimes: -x) == .cos(piTimes: x)
/// ```
///
/// **Exact values and edge cases:**
/// - If x is not finite, `cos(piTimes: x)` is NaN.
/// - If x is an even integer, `cos(piTimes: x)` is 1.
/// > Note:
/// _All_ finite values with magnitude greater than or equal to
/// `(radix/ulpOfOne)` are even integers.
/// E.g. `Double.radix` is 2, and `Double.ulpOfOne` is 2⁻⁵², so
/// for every `Double` x with `x.magnitude` >= 2⁵³, `cos(piTimes: x)`
/// is 1.
/// - If x is an odd integer, `cos(piTimes: x)` is -1.
/// - If x is a half-integer (i.e. x = n + ½ for some integer n),
/// then `cos(piTimes: x)` is +0.
static func cos(piTimes x: Self) -> Self
/// The sine of π times `x`.
///
/// Because π is not representable in any `FloatingPoint` type, for large
/// `x`, `.sin(.pi * x)` can have arbitrarily large relative error;
/// `.sin(piTimes: x)` always provides a result with small relative error.
///
/// This is observable even for modest arguments; consider `10`:
/// ```swift
/// Float.sin(.pi * 10) // -2.4636322e-06
/// Float.sin(piTimes: 10) // 0.0
/// ```
/// It's important to be clear that there is no bug in the example
/// given above. Every step of both computations is producing the most
/// accurate possible result.
///
/// **Symmetry:**
/// sine is an odd function. Thus for every finite `x`,
/// ```swift
/// .sin(piTimes: -x) == -.sin(piTimes: x)
/// ```
///
/// **Exact values and edge cases:**
/// - If x is not finite, `sin(piTimes: x)` is NaN.
/// - If x is an integer, `sin(piTimes: x)` is zero, with the same sign
/// as x (so that the function is odd).
/// > Note:
/// _All_ finite values with magnitude greater than or equal to
/// `(1/.ulpOfOne)` are integers.
/// E.g. `Double.ulpOfOne` is 2⁻⁵², so for every `Double` x with
/// `x.magnitude` >= 2⁵², `sin(piTimes: x)` is zero.
/// - If x is a half-integer (i.e. x = n + ½ for some integer n),
/// then `sin(piTimes: x)` is 1 if n is even and -1 if n is odd.
static func sin(piTimes x: Self) -> Self
/// The tangent of π times `x`.
///
/// Because π is not representable in any `FloatingPoint` type, for
/// large `x`, `.tan(.pi * x)` can have arbitrarily large relative
/// error; `.tan(piTimes: x)` always provides a result with small
/// relative error.
///
/// This is observable even for modest arguments; consider `0.5`:
/// ```swift
/// Float.tan(.pi * 0.5) // 13245402.0
/// Float.tan(piTimes: 0.5) // infinity
/// ```
/// It's important to be clear that there is no bug in either example
/// given above. Every step of both computations is producing the most
/// accurate possible result.
///
/// **Symmetry:**
/// tangent is an odd function. Thus for every finite `x`,
/// ```swift
/// .tan(piTimes: -x) == -.tan(piTimes: x)
/// ```
///
/// **Exact values and edge cases:**
/// - If x is not finite, `tan(piTimes: x)` is NaN.
/// - If x is an integer, `tan(piTimes: x)` is zero, with the sign given
/// by `sin(piTimes: x)/cos(piTimes: x)`.
/// - If x is a half-integer, `tan(piTimes: x)` is infinity, with the sign
/// given by `sin(piTimes: x)/cos(piTimes: x)`
///
/// Thus, if `n` is a positive even integral value and `n.ulp <= 0.5`:
/// - `tan(piTimes: n)` is +0/+1 = +0
/// - `tan(piTimes: n + 0.5)` is +1/+0 = +infinity
/// - `tan(piTimes: n + 1.0)` is +0/-1 = -0
/// - `tan(piTimes: n + 1.5)` is -1/+0 = -infinity
///
/// This means that `tan(piTimes:)` is 2-periodic, even though the
/// mathematical tangent function is π-periodic (not 2π).
///
/// > Note:
/// _All_ finite values with magnitude greater than or equal to
/// `(2/.ulpOfOne)` are even integers.
/// E.g. `Double.ulpOfOne` is 2⁻⁵², so for every `Double` x with
/// `x.magnitude` >= 2⁵³, `tan(piTimes: x)` is ±0.
static func tan(piTimes x: Self) -> Self
The basic rationale for these additions is that by making the scaling by π implicit, we get to work with exact values for many common angles, and reduction from arbitrary angles into the fundamental range [-1,1] becomes an simple and exact operation. These are mostly IEEE 754 recommend operations since the 2008 standard revision, and the C23 standard adds bindings for most of them as well (matching the edge cases specified here). I will, however, provide implementations for these on all platforms, both because C23 support is pretty spotty and because it's desirable for these operations to get the same result on all platforms.
My main question here is the naming of the inverse functions; sin(piTimes: x)
works really nicely, but asinOverPi(x)
isn't quite as neat, but I don't have any ideas that I like much more. If there are any compelling alternatives, I'm interested to hear them (C calls this asinpi
, fwiw).
Review of this proposal will run through June 1, 2024. A WIP implementation branch is available for experimentation (partially backed by placeholder implementations for now).