Coming soon: trig-pi functions

I've put together a branch that introduces three new functions for the Real module:

protocol Real {
...
+ static func cos(piTimes x: Self) -> Self
+ static func sin(piTimes x: Self) -> Self
+ static func tan(piTimes x: Self) -> Self
}

These functions compute, respectively, cos(πx), sin(πx), and tan(πx) in a manner that is much more accurate than is possible with the "obvious" expressions like .cos(.pi * x), because π cannot be exactly represented as a FloatingPoint type, so .pi * x always introduces a small relative error, which can become a large relative error in the face of further operations. This makes getting many common cases exactly right significantly easier. E.g.:

[ 1> import Real
[ 2> var a = Double.tan(.pi * 1.5)
a: Double = 5443746451065123 // huh?
[ 3> var b = Double.tan(piTimes: 1.5)
b: Double = -Inf // yay!
[ 4> var c = Float.sin(.pi * 1000000) 
c: Float = -0.152986646 // what the ...
[ 5> var d = Float.sin(piTimes: 1000000)
d: Float = 0 // yay!

I have some more to do, and need to commit the tests, but they're basically working and you can play with them now if you grab the branch.

The functionality provided by these operations is something that I'm quite set on adding, but I am less committed to this particular spelling, and am interested in alternatives. Some possibilities:

let a = Double.cospi(x)    // C-family style
let b = cos(πx: y) -> Self // too-clever by half
... your suggestion here!

Especially worth keeping in mind is a pattern that provides names for inverse trig-pi functions, which have an implicit scale of π on their result rather than their argument.

24 Likes

I think you’ve got it right with cos(piTimes: x)

For the inverse, I’d say acosOverPi(x)

1 Like

I think we discussed this last year when adding the Swift overlay to vForce. FWIW, we ended up with, for example:

public static func cosPi<U>(_ vector: U) -> [Float]

vForce doesn't have the inverse trig-pi functions.

simon

I’m so in love with this. I hadn’t thought about it before but I’m realizing I should just store “rotation” in my app as a real from 0..<2, which will be way more human-readable and also less squirmy for common (90º) rotations.

The only thing I’d love to have added is a (tauTimes:) variant because to me τ is even more readable — mmm, 0..<1 rotation value * τ.

2 Likes

Have you considered using a wrapper type, which wraps a Real and represents "pi times" that amount?

There could be a protocol that describes it, that would let you write overloads for cos that use these precise expressions. E.g. cos(PiTimes(1.5)). This would let you also allow you to generalize over degrees, radians (by a factor of π or τ, or none at all), and even gradians

1 Like

You could do something like this:

struct MultipleOfPi<T: BinaryFloatingPoint> {
  var n: T
  fileprivate init(n: T) { self.n = n }
}
struct PiSymbol {}
let pi = PiSymbol()

extension BinaryFloatingPoint {
  static func * (lhs: Self, rhs: PiSymbol) -> MultipleOfPi<Self> {
    return MultipleOfPi(n: lhs)
  }
  static func * (lhs: Self, rhs: MultipleOfPi<Self>) -> MultipleOfPi<Self> {
    return MultipleOfPi(n: lhs * rhs.n)
  }
  static func * (lhs: MultipleOfPi<Self>, rhs: Self) -> MultipleOfPi<Self> {
    return MultipleOfPi(n: lhs.n * rhs)
  }
}

func cos<T>(_ mPi: MultipleOfPi<T>) -> T where T:BinaryFloatingPoint {
  fatalError()
}

let x: Float80
let _ = cos(x * pi) // result is Float80
let _ = cos(1.123 * pi) // result is Double
let _ = cos(2 * pi * 1.132) // result is Double

While this looks pretty cool, it may lead to subtle bugs: the visual difference between cos(x * .pi) and cos(x * pi) is not easy to spot, but the first one involves actually multiplying by the imprecise constant (Float/Double/etc).pi and the second one wouldn't.

Yes please! @scanon, would including this in the module have any benefit, or can those of us who prefer tau simply define it in terms of (piTimes:) without any loss of accuracy?

2 has an exact representation in the floating point types involved, so there’s not much benefit to baking tau in.

4 Likes

Right, there's never any loss of accuracy when scaling by 0.5 or 2.0 in a BinaryFloatingPoint type (unless underflow or overflow occurs, but in practice that "never" happens when working with angles, and if it does something has already gone wrong).

1 Like

I have.

In order to implement that type well, you need to have bindings for the trig-pi operations (and make them available on all platforms). So we may very well end up exposing something like that, but making these functions available is the first step toward doing so.

It also adds another * operator to the overload set, which may cause "expression too complex" issues for some code that would otherwise want to use it in the short-term.