Skip to content

Commit d5f890e

Browse files
markuswntrstephentyrone
authored andcommitted
Add transformation tests to quaternion
1 parent d196252 commit d5f890e

File tree

1 file changed

+144
-16
lines changed

1 file changed

+144
-16
lines changed

Tests/QuaternionTests/TransformationTests.swift

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//===--- PolarTests.swift -------------------------------------*- swift -*-===//
1+
//===--- TransformationTests.swift ----------------------------*- swift -*-===//
22
//
33
// This source file is part of the Swift Numerics open source project
44
//
@@ -18,7 +18,7 @@ final class TransformationTests: XCTestCase {
1818

1919
// MARK: Angle/Axis
2020

21-
func testAngleAxisSpin<T: Real & SIMDScalar>(_ type: T.Type) {
21+
func testAngleAxis<T: Real & SIMDScalar>(_ type: T.Type) {
2222
let xAxis = SIMD3<T>(1,0,0)
2323
// Positive angle, positive axis
2424
XCTAssertEqual(Quaternion<T>(angle: .pi, axis: xAxis).angle, .pi)
@@ -34,34 +34,30 @@ final class TransformationTests: XCTestCase {
3434
XCTAssertEqual(Quaternion<T>.init(angle: -.pi, axis: -xAxis).axis, xAxis)
3535
}
3636

37-
func testAngleAxisSpin() {
38-
testAngleAxisSpin(Float32.self)
39-
testAngleAxisSpin(Float64.self)
37+
func testAngleAxis() {
38+
testAngleAxis(Float32.self)
39+
testAngleAxis(Float64.self)
4040
}
4141

42-
func testAngleMultipleOfPi<T: Real & SIMDScalar>(_ type: T.Type) {
42+
func testAngleAxisMultipleOfPi<T: Real & SIMDScalar>(_ type: T.Type) {
4343
let xAxis = SIMD3<T>(1,0,0)
4444
// 2π
4545
let pi2 = Quaternion<T>(angle: .pi * 2, axis: xAxis)
4646
XCTAssertEqual(pi2.angle, .pi * 2)
4747
XCTAssertEqual(pi2.axis, xAxis)
4848
// 3π - axis inverted
4949
let pi3 = Quaternion<T>(angle: .pi * 3, axis: xAxis)
50-
XCTAssertEqual(pi3.angle, .pi, accuracy: .ulpOfOne * 2)
50+
XCTAssertTrue(closeEnough(pi3.angle, .pi, ulps: 1))
5151
XCTAssertEqual(pi3.axis, -xAxis)
52-
// 4π - axis inverted
53-
let pi4 = Quaternion<T>(angle: .pi * 4, axis: xAxis)
54-
XCTAssertEqual(pi4.angle, .zero, accuracy: .ulpOfOne * 6)
55-
XCTAssertEqual(pi4.axis, -xAxis)
5652
// 5π - axis restored
5753
let pi5 = Quaternion<T>(angle: .pi * 5, axis: xAxis)
58-
XCTAssertEqual(pi5.angle, .pi, accuracy: .ulpOfOne * 10)
54+
XCTAssertTrue(closeEnough(pi5.angle, .pi, ulps: 5))
5955
XCTAssertEqual(pi5.axis, xAxis)
6056
}
6157

62-
func testAngleMultipleOfPi() {
63-
testAngleMultipleOfPi(Float32.self)
64-
testAngleMultipleOfPi(Float64.self)
58+
func testAngleAxisMultipleOfPi() {
59+
testAngleAxisMultipleOfPi(Float32.self)
60+
testAngleAxisMultipleOfPi(Float64.self)
6561
}
6662

6763
func testAngleAxisEdgeCases<T: Real & SIMDScalar>(_ type: T.Type) {
@@ -185,11 +181,143 @@ final class TransformationTests: XCTestCase {
185181
testPolarDecompositionEdgeCases(Float32.self)
186182
testPolarDecompositionEdgeCases(Float64.self)
187183
}
184+
185+
// MARK: Act on Vector
186+
187+
func testActOnVector<T: Real & SIMDScalar>(_ type: T.Type) {
188+
let vector = SIMD3<T>(1,1,1)
189+
let xAxis = SIMD3<T>(1,0,0)
190+
191+
let piHalf = Quaternion<T>(angle: .pi/2, axis: xAxis)
192+
XCTAssertTrue(closeEnough(piHalf.act(on: vector).x, 1, ulps: 0))
193+
XCTAssertTrue(closeEnough(piHalf.act(on: vector).y, -1, ulps: 1))
194+
XCTAssertTrue(closeEnough(piHalf.act(on: vector).z, 1, ulps: 1))
195+
196+
let pi = Quaternion<T>(angle: .pi, axis: xAxis)
197+
XCTAssertTrue(closeEnough(pi.act(on: vector).x, 1, ulps: 0))
198+
XCTAssertTrue(closeEnough(pi.act(on: vector).y, -1, ulps: 2))
199+
XCTAssertTrue(closeEnough(pi.act(on: vector).z, -1, ulps: 2))
200+
201+
let twoPi = Quaternion<T>(angle: .pi * 2, axis: xAxis)
202+
XCTAssertTrue(closeEnough(twoPi.act(on: vector).x, 1, ulps: 0))
203+
XCTAssertTrue(closeEnough(twoPi.act(on: vector).y, 1, ulps: 3))
204+
XCTAssertTrue(closeEnough(twoPi.act(on: vector).z, 1, ulps: 3))
205+
}
206+
207+
func testActOnVector() {
208+
testActOnVector(Float32.self)
209+
testActOnVector(Float64.self)
210+
}
211+
212+
func testActOnVectorRandom<T>(_ type: T.Type)
213+
where T: Real, T: BinaryFloatingPoint, T: SIMDScalar,
214+
T.Exponent: FixedWidthInteger, T.RawSignificand: FixedWidthInteger
215+
{
216+
// Generate random angles, axis and vector to test rotation properties
217+
// - angle are selected from range -π to π
218+
// - axis values are selected from -1 to 1; axis length is unity
219+
// - vector values are selected from 10 to 10000
220+
let inputs: [(angle: T, axis: SIMD3<T>, vector: SIMD3<T>)] = (0..<100).map { _ in
221+
let angle = T.random(in: -.pi ... .pi)
222+
var axis = SIMD3<T>.random(in: -1 ... 1)
223+
axis /= .sqrt((axis * axis).sum()) // Normalize
224+
var vector = SIMD3<T>.random(in: -1 ... 1)
225+
vector /= .sqrt((vector * vector).sum()) // Normalize
226+
vector *= T.random(in: 10 ... 10000) // Scale
227+
return (angle, axis, vector)
228+
}
229+
230+
for (angle, axis, vector) in inputs {
231+
let q = Quaternion(angle: angle, axis: axis)
232+
// The following equation in the form of v' = qvq⁻¹ is the mathmatical
233+
// definition for how a quaternion rotates a vector (by promoting it to
234+
// a quaternion) and goes "the full and long way" to calculate the
235+
// rotation of vector by a quaternion. The result is used to test the
236+
// rotation properties of "act(on:)"
237+
let vrot = (q // q
238+
* Quaternion(imaginary: vector) // v (pure quaternion)
239+
* q.conjugate // q⁻¹ (as q is of unit length, q⁻¹ == q*)
240+
).imaginary // the result is pure quaternion with v' == imaginary
241+
242+
XCTAssertTrue(q.act(on: vector).x.isFinite)
243+
XCTAssertTrue(q.act(on: vector).y.isFinite)
244+
XCTAssertTrue(q.act(on: vector).z.isFinite)
245+
// Test for sign equality on the components to see if the vector rotated
246+
// to the correct quadrant and if the vector is of equal in length,
247+
// instead of testing component equality – as they are hard to compare
248+
// with proper tolerance
249+
XCTAssertEqual(q.act(on: vector).x.sign, vrot.x.sign)
250+
XCTAssertEqual(q.act(on: vector).y.sign, vrot.y.sign)
251+
XCTAssertEqual(q.act(on: vector).z.sign, vrot.z.sign)
252+
XCTAssertTrue(closeEnough(q.act(on: vector).lengthSquared, vrot.lengthSquared, ulps: 16))
253+
}
254+
}
255+
256+
func testActOnVectorRandom() {
257+
testActOnVectorRandom(Float32.self)
258+
testActOnVectorRandom(Float64.self)
259+
}
260+
261+
func testActOnVectorEdgeCase<T: Real & ExpressibleByFloatLiteral & SIMDScalar>(_ type: T.Type) {
262+
263+
/// Test for zero, infinity
264+
let q = Quaternion(angle: .pi, axis: SIMD3(1,0,0))
265+
XCTAssertEqual(q.act(on: .zero), .zero)
266+
XCTAssertEqual(q.act(on: -.zero), .zero)
267+
XCTAssertEqual(q.act(on: .infinity), SIMD3(repeating: .infinity))
268+
XCTAssertEqual(q.act(on: -.infinity), SIMD3(repeating: .infinity))
269+
270+
// Rotate a vector with a value close to greatestFiniteMagnitude
271+
// in all lanes.
272+
// A vector this close to the bounds should not hit infinity when it
273+
// is rotate by a perpendicular axis with an angle that is a multiple of π
274+
275+
// An axis perpendicular to the vector, so all lanes are changing equally
276+
let axis = SIMD3<T>(1/2,0,-1/2)
277+
// Create a value close (somewhat) close to .greatestFiniteMagnitude
278+
let scalar = T(
279+
sign: .plus, exponent: T.greatestFiniteMagnitude.exponent,
280+
significand: 1.999999
281+
)
282+
283+
let closeToBounds = SIMD3<T>(repeating: scalar)
284+
285+
// Perform a 180° rotation on all components
286+
let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToBounds)
287+
// Must be finite after the rotation
288+
XCTAssertTrue(pi.x.isFinite)
289+
XCTAssertTrue(pi.y.isFinite)
290+
XCTAssertTrue(pi.z.isFinite)
291+
XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 4))
292+
XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 4))
293+
XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 4))
294+
295+
// Perform a 360° rotation on all components
296+
let twoPi = Quaternion(angle: 2 * .pi, axis: axis).act(on: closeToBounds)
297+
// Must still be finite after the process
298+
XCTAssertTrue(twoPi.x.isFinite)
299+
XCTAssertTrue(twoPi.y.isFinite)
300+
XCTAssertTrue(twoPi.z.isFinite)
301+
XCTAssertTrue(closeEnough(twoPi.x, scalar, ulps: 8))
302+
XCTAssertTrue(closeEnough(twoPi.y, scalar, ulps: 8))
303+
XCTAssertTrue(closeEnough(twoPi.z, scalar, ulps: 8))
304+
}
305+
306+
func testActOnVectorEdgeCase() {
307+
testActOnVectorEdgeCase(Float32.self)
308+
testActOnVectorEdgeCase(Float64.self)
309+
}
188310
}
189311

190-
// Helper
312+
// MARK: - Helper
191313
extension SIMD3 where Scalar: FloatingPoint {
192314
fileprivate static var infinity: Self { SIMD3(.infinity,0,0) }
193315
fileprivate static var nan: Self { SIMD3(.nan,0,0) }
194316
fileprivate var isNaN: Bool { x.isNaN && y.isNaN && z.isNaN }
195317
}
318+
319+
// TODO: replace with approximately equals
320+
func closeEnough<T: Real>(_ a: T, _ b: T, ulps allowed: T) -> Bool {
321+
let scale = max(a.magnitude, b.magnitude, T.leastNormalMagnitude).ulp
322+
return (a - b).magnitude <= allowed * scale
323+
}

0 commit comments

Comments
 (0)