Skip to content

Commit 187f6be

Browse files
markuswntrstephentyrone
authored andcommitted
Account for underflow in quaternion rotation
1 parent 847ec44 commit 187f6be

File tree

2 files changed

+60
-30
lines changed

2 files changed

+60
-30
lines changed

Sources/QuaternionModule/Transformation.swift

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -352,12 +352,12 @@ extension Quaternion {
352352
///
353353
/// p' = qpq⁻¹
354354
///
355-
/// where `p` is a *pure* quaternion (`real == .zero`) with imaginary part equal
356-
/// to vector `v`, and where `p'` is another pure quaternion with imaginary
357-
/// part equal to the transformed vector `v'`. The implementation uses this
358-
/// formular but boils down to a simpler and faster implementation as `p` is
359-
/// known to be pure and `q` is assumed to have unit length – which allows
360-
/// simplification.
355+
/// where `p` is a *pure* quaternion (`real == .zero`) with imaginary part
356+
/// equal to vector `v`, and where `p'` is another pure quaternion with
357+
/// imaginary part equal to the transformed vector `v'`. The implementation
358+
/// uses this formular but boils down to a simpler and faster implementation
359+
/// as `p` is known to be pure and `q` is assumed to have unit length – which
360+
/// allows for simplification.
361361
///
362362
/// - Note: This method assumes this quaternion is of unit length.
363363
///
@@ -369,6 +369,11 @@ extension Quaternion {
369369
/// ```
370370
/// SIMD3(-.infinity,0,0) * q == SIMD3(.infinity,.infinity,.infinity)
371371
/// ```
372+
/// - For any quaternion `q`, even `.zero` or `.infinity`, if `vector` is
373+
/// `.zero`, the returning vector is also `.zero`.
374+
/// ```
375+
/// SIMD3(0,0,0) * q == .zero
376+
/// ```
372377
///
373378
/// - Parameter vector: A vector to rotate by this quaternion
374379
/// - Returns: The vector rotated by this quaternion
@@ -377,17 +382,20 @@ extension Quaternion {
377382
@inlinable
378383
public func act(on vector: SIMD3<RealType>) -> SIMD3<RealType> {
379384
guard vector.isFinite else { return SIMD3(repeating: .infinity) }
385+
guard vector != .zero else { return .zero }
380386

381387
// The following expression have been split up so the type-checker
382388
// can resolve them in a reasonable time.
383389
let p1 = vector * (real*real - imaginary.lengthSquared)
384390
let p2 = 2 * imaginary * imaginary.dot(vector)
385391
let p3 = 2 * real * imaginary.cross(vector)
386392
let rotatedVector = p1 + p2 + p3
387-
if rotatedVector.isFinite { return rotatedVector }
388393

389-
// If the vector is no longer finite after it is rotated, scale it down,
390-
// rotate it again and then scale it back-up after the rotation operation
394+
// If the rotation computes without over/underflow, everything is fine
395+
// and the result is correct. If not, we have to do the computation
396+
// carefully and first unscale the vector, rotate it again and then
397+
// rescale the vector
398+
if rotatedVector.isNormal { return rotatedVector }
391399
let scale = max(abs(vector.max()), abs(vector.min()))
392400
return act(on: vector/scale) * scale
393401
}
@@ -451,6 +459,12 @@ extension SIMD3 where Scalar: FloatingPoint {
451459
x.isFinite && y.isFinite && z.isFinite
452460
}
453461

462+
/// True if all values of this instance are finite
463+
@usableFromInline @inline(__always)
464+
internal var isNormal: Bool {
465+
x.isNormal && y.isNormal && z.isNormal
466+
}
467+
454468
/// Returns the squared length of this instance.
455469
@usableFromInline @inline(__always)
456470
internal var lengthSquared: Scalar {

Tests/QuaternionTests/TransformationTests.swift

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -291,30 +291,35 @@ final class TransformationTests: XCTestCase {
291291
testActOnVectorRandom(Float64.self)
292292
}
293293

294-
func testActOnVectorEdgeCase<T: Real & ExpressibleByFloatLiteral & SIMDScalar>(_ type: T.Type) {
294+
func testActOnVectorEdgeCase<T: Real & SIMDScalar>(_ type: T.Type) {
295295

296296
/// Test for zero, infinity
297297
let q = Quaternion(angle: .pi, axis: SIMD3(1,0,0))
298298
XCTAssertEqual(q.act(on: .zero), .zero)
299299
XCTAssertEqual(q.act(on: -.zero), .zero)
300+
XCTAssertEqual(q.act(on: .nan ), SIMD3(repeating: .infinity))
300301
XCTAssertEqual(q.act(on: .infinity), SIMD3(repeating: .infinity))
301302
XCTAssertEqual(q.act(on: -.infinity), SIMD3(repeating: .infinity))
303+
}
302304

303-
// Rotate a vector with a value close to greatestFiniteMagnitude
304-
// in all lanes.
305-
// A vector this close to the bounds should not hit infinity when it
306-
// is rotate by a perpendicular axis with an angle that is a multiple of π
305+
func testActOnVectorEdgeCase() {
306+
testActOnVectorEdgeCase(Float32.self)
307+
testActOnVectorEdgeCase(Float64.self)
308+
}
307309

308-
// An axis perpendicular to the vector, so all lanes are changing equally
309-
let axis = SIMD3<T>(1,0,-1) / .sqrt(2)
310-
// Create a value (somewhat) close to .greatestFiniteMagnitude
310+
func testActOnVectorOverflow<T: Real & ExpressibleByFloatLiteral & SIMDScalar>(_ type: T.Type) {
311+
// Create a vector (somewhat) close to greatestFiniteMagnitude on all lanes.
312+
// We can not use greatestFiniteMagnitude here to test the careful rotation
313+
// path, as we lose some precision in the process and it will overflow after
314+
// rescaling the vector.
311315
let scalar = T(
312316
sign: .plus, exponent: T.greatestFiniteMagnitude.exponent,
313317
significand: 1.999999
314318
)
315-
316319
let closeToBounds = SIMD3<T>(repeating: scalar)
317320

321+
// An axis perpendicular to the vector, so all lanes change equally
322+
let axis = SIMD3<T>(1,0,-1) / .sqrt(2)
318323
// Perform a 180° rotation on all components
319324
let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToBounds)
320325
// Must be finite after the rotation
@@ -324,21 +329,32 @@ final class TransformationTests: XCTestCase {
324329
XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 4))
325330
XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 4))
326331
XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 4))
332+
}
327333

328-
// Perform a 360° rotation on all components
329-
let twoPi = Quaternion(angle: 2 * .pi, axis: axis).act(on: closeToBounds)
330-
// Must still be finite after the process
331-
XCTAssertTrue(twoPi.x.isFinite)
332-
XCTAssertTrue(twoPi.y.isFinite)
333-
XCTAssertTrue(twoPi.z.isFinite)
334-
XCTAssertTrue(closeEnough(twoPi.x, scalar, ulps: 8))
335-
XCTAssertTrue(closeEnough(twoPi.y, scalar, ulps: 8))
336-
XCTAssertTrue(closeEnough(twoPi.z, scalar, ulps: 8))
334+
func testActOnVectorOverflow() {
335+
testActOnVectorOverflow(Float32.self)
336+
testActOnVectorOverflow(Float64.self)
337337
}
338338

339-
func testActOnVectorEdgeCase() {
340-
testActOnVectorEdgeCase(Float32.self)
341-
testActOnVectorEdgeCase(Float64.self)
339+
func testActOnVectorUnderflow<T: Real & ExpressibleByFloatLiteral & SIMDScalar>(_ type: T.Type) {
340+
let scalar = T.leastNormalMagnitude
341+
let closeToZero = SIMD3<T>(repeating: scalar)
342+
// An axis perpendicular to the vector, so all lanes change equally
343+
let axis = SIMD3<T>(1,0,-1) / .sqrt(2)
344+
// Perform a 180° rotation on all components
345+
let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToZero)
346+
// Must be finite after the rotation
347+
XCTAssertTrue(pi.x.isFinite)
348+
XCTAssertTrue(pi.y.isFinite)
349+
XCTAssertTrue(pi.z.isFinite)
350+
XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 2))
351+
XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 2))
352+
XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 2))
353+
}
354+
355+
func testActOnVectorUnderflow() {
356+
testActOnVectorUnderflow(Float32.self)
357+
testActOnVectorUnderflow(Float64.self)
342358
}
343359
}
344360

0 commit comments

Comments
 (0)