Skip to content

Commit f895b11

Browse files
committed
Add expMinusOne to quaternions
1 parent 7cd7508 commit f895b11

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

Sources/QuaternionModule/ElementaryFunctions.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,32 @@ extension Quaternion/*: ElementaryFunctions */ {
4949
}
5050
return Quaternion(halfAngle: θ, unitAxis:).multiplied(by: .exp(q.real))
5151
}
52+
53+
@inlinable
54+
public static func expMinusOne(_ q: Quaternion) -> Quaternion {
55+
// Note that the imaginary part is just the usual exp(r) sin(θ);
56+
// the only trick is computing the real part, which allows us to borrow
57+
// the derivative of real part for this function from complex numbers.
58+
// See `expMinusOne` in the ComplexModule for implementation details.
59+
guard q.isFinite else { return q }
60+
// TODO: Replace q.imaginary == .zero with `q.isReal`
61+
let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v||
62+
let = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v||
63+
// If exp(q) is close to the overflow boundary, we don't need to
64+
// worry about the "MinusOne" part of this function; we're just
65+
// computing exp(q). (Even when q.y is near a multiple of π/2,
66+
// it can't be close enough to overcome the scaling from exp(q.x),
67+
// so the -1 term is _always_ negligable). So we simply handle
68+
// these cases exactly the same as exp(q).
69+
guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else {
70+
let halfScale = RealType.exp(q.real/2)
71+
let rotation = Quaternion(halfAngle: θ, unitAxis:)
5272
return rotation.multiplied(by: halfScale).multiplied(by: halfScale)
5373
}
5474
return Quaternion(
55-
halfAngle: phase,
56-
unitAxis: unitAxis
57-
).multiplied(by: .exp(q.real))
75+
real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)),
76+
imaginary:* .exp(q.real) * .sin(θ)
77+
)
78+
}
5879
}
5980
}

Tests/QuaternionTests/ElementaryFunctionTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,57 @@ final class ElementaryFunctionTests: XCTestCase {
7575
}
7676
}
7777

78+
func testExpMinusOne<T: Real & FixedWidthFloatingPoint & SIMDScalar>(_ type: T.Type) {
79+
// expMinusOne(0) = 0
80+
XCTAssertEqual(0, Quaternion<T>.expMinusOne(Quaternion(real: 0, imaginary: 0, 0, 0)))
81+
XCTAssertEqual(0, Quaternion<T>.expMinusOne(Quaternion(real:-0, imaginary: 0, 0, 0)))
82+
XCTAssertEqual(0, Quaternion<T>.expMinusOne(Quaternion(real:-0, imaginary:-0,-0,-0)))
83+
XCTAssertEqual(0, Quaternion<T>.expMinusOne(Quaternion(real: 0, imaginary:-0,-0,-0)))
84+
// In general, expMinusOne(Quaternion(r,0,0,0)) should be expMinusOne(r),
85+
// but that breaks down when r is infinity or NaN, because we want all non-
86+
// finite Quaternion values to be semantically a single point at infinity.
87+
// This is fine for most inputs, but expMinusOne(Quaternion(-.infinity,0,0,0))
88+
// would produce 0 if we evaluated it in the usual way.
89+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .infinity, imaginary: .zero)).isFinite)
90+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .infinity, imaginary: .infinity)).isFinite)
91+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: 0, imaginary: .infinity)).isFinite)
92+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite)
93+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: -.infinity, imaginary: .zero)).isFinite)
94+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite)
95+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: 0, imaginary: -.infinity)).isFinite)
96+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite)
97+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .nan, imaginary: .nan)).isFinite)
98+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .infinity, imaginary: .nan)).isFinite)
99+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .nan, imaginary: .infinity)).isFinite)
100+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: -.infinity, imaginary: .nan)).isFinite)
101+
XCTAssertFalse(Quaternion<T>.expMinusOne(Quaternion(real: .nan, imaginary: -.infinity)).isFinite)
102+
// Near-overflow test, same as exp() above.
103+
let x = T.log(.greatestFiniteMagnitude) + T.log(9/8)
104+
let huge = Quaternion<T>.expMinusOne(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0)))
105+
let mag = T.greatestFiniteMagnitude/T.sqrt(2) * (9/8)
106+
XCTAssert(huge.real.isApproximatelyEqual(to: mag))
107+
XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag))
108+
XCTAssertEqual(huge.imaginary.y, .zero)
109+
XCTAssertEqual(huge.imaginary.z, .zero)
110+
// For small values, expMinusOne should be approximately the identity.
111+
var g = SystemRandomNumberGenerator()
112+
let small = T.ulpOfOne
113+
for _ in 0 ..< 100 {
114+
let q = Quaternion<T>(
115+
real: T.random(in: -small ... small, using: &g),
116+
imaginary: SIMD3(repeating: T.random(in: -small ... small, using: &g))
117+
)
118+
XCTAssert(q.isApproximatelyEqual(to: Quaternion.expMinusOne(q), relativeTolerance: 16 * .ulpOfOne))
119+
}
120+
}
121+
78122
func testFloat() {
79123
testExp(Float32.self)
124+
testExpMinusOne(Float32.self)
80125
}
81126

82127
func testDouble() {
83128
testExp(Float64.self)
129+
testExpMinusOne(Float64.self)
84130
}
85131
}

0 commit comments

Comments
 (0)