Skip to content

Commit 4dbe2e8

Browse files
committed
Add exponential function to quaternions
1 parent 0b4571e commit 4dbe2e8

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//===--- ElementaryFunctions.swift ----------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2019-2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import RealModule
14+
15+
extension Quaternion/*: ElementaryFunctions */ {
16+
17+
// MARK: - exp-like functions
18+
19+
/// The quaternion exponential function e^q whose base `e` is the base of the
20+
/// natural logarithm.
21+
///
22+
/// Mathematically, this operation can be expanded in terms of the `Real`
23+
/// operations `exp`, `cos` and `sin` as follows:
24+
/// ```
25+
/// exp(r + xi + yj + zk) = exp(r + v) = exp(r) exp(v)
26+
/// = exp(r) (cos(θ) + (v/θ) sin(θ)) where θ = ||v||
27+
/// ```
28+
/// Note that naive evaluation of this expression in floating-point would be
29+
/// prone to premature overflow, since `cos` and `sin` both have magnitude
30+
/// less than 1 for most inputs (i.e. `exp(r)` may be infinity when
31+
/// `exp(r) cos(θ)` would not be).
32+
public static func exp(_ q: Quaternion<RealType>) -> Quaternion<RealType> {
33+
guard q.isFinite else { return q }
34+
// Firstly evaluate θ and v/θ where θ = ||v|| (as discussed above)
35+
// There are 2 special cases for ||v|| that we need to take care of:
36+
// The value of ||v|| may be invalid due to an overflow in `.lengthSquared`.
37+
// As the internal `SIMD3.length` helper functions deals with overflow and
38+
// underflow of `.lengthSquared`, we can safely ignore this case here.
39+
// However, we still have to check for ||v|| = 0 before evaluating v/θ
40+
// as it would incorrectly yield a division by zero.
41+
let phase = q.imaginary.length
42+
let unitAxis = !phase.isZero ? (q.imaginary / phase) : .zero
43+
// If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow.
44+
// To protect ourselves against sketchy log or exp implementations in
45+
// an unknown host library, or slight rounding disagreements between
46+
// the two, subtract one from the bound for a little safety margin.
47+
guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else {
48+
let halfScale = RealType.exp(q.real/2)
49+
let rotation = Quaternion(
50+
halfAngle: phase,
51+
unitAxis: unitAxis
52+
)
53+
return rotation.multiplied(by: halfScale).multiplied(by: halfScale)
54+
}
55+
return Quaternion(
56+
halfAngle: phase,
57+
unitAxis: unitAxis
58+
).multiplied(by: .exp(q.real))
59+
}
60+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===--- ElementaryFunctionTests.swift ------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Numerics open source project
4+
//
5+
// Copyright (c) 2019 - 2021 Apple Inc. and the Swift Numerics project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
import RealModule
14+
import _TestSupport
15+
16+
@testable import QuaternionModule
17+
18+
final class ElementaryFunctionTests: XCTestCase {
19+
20+
func testExp<T: Real & FixedWidthFloatingPoint & SIMDScalar>(_ type: T.Type) {
21+
// exp(0) = 1
22+
XCTAssertEqual(1, Quaternion<T>.exp(Quaternion(real: 0, imaginary: 0, 0, 0)))
23+
XCTAssertEqual(1, Quaternion<T>.exp(Quaternion(real:-0, imaginary: 0, 0, 0)))
24+
XCTAssertEqual(1, Quaternion<T>.exp(Quaternion(real:-0, imaginary:-0,-0,-0)))
25+
XCTAssertEqual(1, Quaternion<T>.exp(Quaternion(real: 0, imaginary:-0,-0,-0)))
26+
// In general, exp(Quaternion(r, 0, 0, 0)) should be exp(r), but that breaks
27+
// down when r is infinity or NaN, because we want all non-finite
28+
// quaternions to be semantically a single point at infinity. This is fine
29+
// for most inputs, but exp(Quaternion(-.infinity, 0, 0, 0)) would produce
30+
// 0 if we evaluated it in the usual way.
31+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .infinity, imaginary: .zero)).isFinite)
32+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .infinity, imaginary: .infinity)).isFinite)
33+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: 0, imaginary: .infinity)).isFinite)
34+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite)
35+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: -.infinity, imaginary: .zero)).isFinite)
36+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite)
37+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: 0, imaginary: -.infinity)).isFinite)
38+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite)
39+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .nan, imaginary: .nan)).isFinite)
40+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .infinity, imaginary: .nan)).isFinite)
41+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .nan, imaginary: .infinity)).isFinite)
42+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: -.infinity, imaginary: .nan)).isFinite)
43+
XCTAssertFalse(Quaternion<T>.exp(Quaternion(real: .nan, imaginary: -.infinity)).isFinite)
44+
// Find a value of x such that exp(x) just overflows. Then exp((x, π/4))
45+
// should not overflow, but will do so if it is not computed carefully.
46+
// The correct value is:
47+
//
48+
// exp((log(gfm) + log(9/8), π/4) = exp((log(gfm*9/8), π/4))
49+
// = gfm*9/8 * (1/sqrt(2), 1/(sqrt(2))
50+
let x = T.log(.greatestFiniteMagnitude) + T.log(9/8)
51+
let huge = Quaternion<T>.exp(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0)))
52+
let mag = T.greatestFiniteMagnitude/T.sqrt(2) * (9/8)
53+
XCTAssert(huge.real.isApproximatelyEqual(to: mag))
54+
XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag))
55+
XCTAssertEqual(huge.imaginary.y, .zero)
56+
XCTAssertEqual(huge.imaginary.z, .zero)
57+
// For randomly-chosen well-scaled finite values, we expect to have the
58+
// usual identities:
59+
//
60+
// exp(z + w) = exp(z) * exp(w)
61+
// exp(z - w) = exp(z) / exp(w)
62+
var g = SystemRandomNumberGenerator()
63+
let values: [Quaternion<T>] = (0..<100).map { _ in
64+
Quaternion(
65+
real: T.random(in: -1 ... 1, using: &g),
66+
imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3))
67+
}
68+
for z in values {
69+
for w in values {
70+
let p = Quaternion.exp(z) * Quaternion.exp(w)
71+
let q = Quaternion.exp(z) / Quaternion.exp(w)
72+
XCTAssert(Quaternion.exp(z + w).isApproximatelyEqual(to: p))
73+
XCTAssert(Quaternion.exp(z - w).isApproximatelyEqual(to: q))
74+
}
75+
}
76+
}
77+
78+
func testFloat() {
79+
testExp(Float32.self)
80+
}
81+
82+
func testDouble() {
83+
testExp(Float64.self)
84+
}
85+
}

0 commit comments

Comments
 (0)