Skip to content

Commit 847ec44

Browse files
markuswntrstephentyrone
authored andcommitted
Account for over/underflow on halfAngle and axis calculations
1 parent ac5421a commit 847ec44

File tree

2 files changed

+60
-15
lines changed

2 files changed

+60
-15
lines changed

Sources/QuaternionModule/Transformation.swift

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,14 @@ extension Quaternion {
5858
/// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation
5959
@inlinable
6060
public var axis: SIMD3<RealType> {
61-
guard isFinite, imaginary != .zero, real != .zero else {
62-
return SIMD3(repeating: .nan)
63-
}
64-
return imaginary / .sqrt(imaginary.lengthSquared)
61+
guard isFinite, imaginary != .zero else { return SIMD3(repeating: .nan) }
62+
63+
// If lengthSquared computes without over/underflow, everything is fine
64+
// and the result is correct. If not, we have to do the computation
65+
// carefully and unscale the quaternion first.
66+
let lenSq = imaginary.lengthSquared
67+
guard lenSq.isNormal else { return divided(by: magnitude).axis }
68+
return imaginary / .sqrt(lenSq)
6569
}
6670

6771
/// The [Angle-Axis][wiki] representation.
@@ -267,7 +271,7 @@ extension Quaternion {
267271
@inlinable
268272
public init(rotation vector: SIMD3<RealType>) {
269273
let angle: RealType = .sqrt(vector.lengthSquared)
270-
if !angle.isZero && angle.isFinite {
274+
if !angle.isZero, angle.isFinite {
271275
self = Quaternion(halfAngle: angle/2, unitAxis: vector/angle)
272276
} else {
273277
self = Quaternion(angle)
@@ -404,8 +408,19 @@ extension Quaternion {
404408
/// If the quaternion is zero or non-finite, halfAngle is `nan`.
405409
@usableFromInline @inline(__always)
406410
internal var halfAngle: RealType {
407-
guard !isZero && isFinite else { return .nan }
408-
return .atan2(y: .sqrt(imaginary.lengthSquared), x: real)
411+
guard isFinite else { return .nan }
412+
guard imaginary != .zero else {
413+
// A zero quaternion does not encode transformation properties.
414+
// If imaginary is zero, real must be non-zero or nan is returned.
415+
return real.isZero ? .nan : .zero
416+
}
417+
418+
// If lengthSquared computes without over/underflow, everything is fine
419+
// and the result is correct. If not, we have to do the computation
420+
// carefully and unscale the quaternion first.
421+
let lenSq = imaginary.lengthSquared
422+
guard lenSq.isNormal else { return divided(by: magnitude).halfAngle }
423+
return .atan2(y: .sqrt(lenSq), x: real)
409424
}
410425

411426
/// Creates a new quaternion from given half rotation angle about given
@@ -430,18 +445,18 @@ extension Quaternion {
430445
// and *(x,y,z)* axis representations internally to the module.
431446
extension SIMD3 where Scalar: FloatingPoint {
432447

433-
/// Returns the squared length of this instance.
434-
@usableFromInline @inline(__always)
435-
internal var lengthSquared: Scalar {
436-
dot(self)
437-
}
438-
439448
/// True if all values of this instance are finite
440449
@usableFromInline @inline(__always)
441450
internal var isFinite: Bool {
442451
x.isFinite && y.isFinite && z.isFinite
443452
}
444453

454+
/// Returns the squared length of this instance.
455+
@usableFromInline @inline(__always)
456+
internal var lengthSquared: Scalar {
457+
dot(self)
458+
}
459+
445460
/// Returns the scalar/dot product of this vector with `other`.
446461
@usableFromInline @inline(__always)
447462
internal func dot(_ other: SIMD3<Scalar>) -> Scalar {

Tests/QuaternionTests/TransformationTests.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,36 @@ final class TransformationTests: XCTestCase {
116116
testAngleAxisEdgeCases(Float64.self)
117117
}
118118

119+
func testHalfAngleAndAxisOverflow<T: Real & SIMDScalar>(_ type: T.Type) {
120+
let unscaled = Quaternion<T>(1, SIMD3(repeating: 1))
121+
let scaled = Quaternion<T>(
122+
.greatestFiniteMagnitude,
123+
SIMD3(repeating: .greatestFiniteMagnitude)
124+
)
125+
XCTAssertEqual(scaled.angle, unscaled.angle)
126+
XCTAssertEqual(scaled.axis, unscaled.axis)
127+
}
128+
129+
func testHalfAngleAndAxisOverflow() {
130+
testHalfAngleAndAxisOverflow(Float32.self)
131+
testHalfAngleAndAxisOverflow(Float64.self)
132+
}
133+
134+
func testHalfAngleAndAxisUnderflow<T: Real & SIMDScalar>(_ type: T.Type) {
135+
let unscaled = Quaternion<T>(1, SIMD3(repeating: 1))
136+
let scaled = Quaternion<T>(
137+
.leastNormalMagnitude,
138+
SIMD3(repeating: .leastNormalMagnitude)
139+
)
140+
XCTAssertEqual(scaled.angle, unscaled.angle)
141+
XCTAssertEqual(scaled.axis, unscaled.axis)
142+
}
143+
144+
func testHalfAngleAndAxisUnderflow() {
145+
testHalfAngleAndAxisUnderflow(Float32.self)
146+
testHalfAngleAndAxisUnderflow(Float64.self)
147+
}
148+
119149
// MARK: Rotation Vector
120150

121151
func testRotationVector<T: Real & SIMDScalar>(_ type: T.Type) {
@@ -170,11 +200,11 @@ final class TransformationTests: XCTestCase {
170200
XCTAssertEqual(Quaternion<T>(length: .zero, phase: .infinity, axis: .infinity), .zero)
171201
XCTAssertEqual(Quaternion<T>(length: .zero, phase:-.infinity, axis: -.infinity), .zero)
172202
XCTAssertEqual(Quaternion<T>(length: .zero, phase: .nan , axis: .nan ), .zero)
173-
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .zero , axis: .zero ), .zero)
203+
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .zero , axis: .zero ), .infinity)
174204
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .infinity, axis: .infinity), .infinity)
175205
XCTAssertEqual(Quaternion<T>(length: .infinity, phase:-.infinity, axis: -.infinity), .infinity)
176206
XCTAssertEqual(Quaternion<T>(length: .infinity, phase: .nan , axis: .infinity), .infinity)
177-
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .zero , axis: .zero ), .zero)
207+
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .zero , axis: .zero ), .infinity)
178208
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .infinity, axis: .infinity), .infinity)
179209
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase:-.infinity, axis: -.infinity), .infinity)
180210
XCTAssertEqual(Quaternion<T>(length:-.infinity, phase: .nan , axis: .infinity), .infinity)

0 commit comments

Comments
 (0)