1
- //===--- PolarTests .swift --------- ----------------------------*- swift -*-===//
1
+ //===--- TransformationTests .swift ----------------------------*- swift -*-===//
2
2
//
3
3
// This source file is part of the Swift Numerics open source project
4
4
//
@@ -18,7 +18,7 @@ final class TransformationTests: XCTestCase {
18
18
19
19
// MARK: Angle/Axis
20
20
21
- func testAngleAxisSpin < T: Real & SIMDScalar > ( _ type: T . Type ) {
21
+ func testAngleAxis < T: Real & SIMDScalar > ( _ type: T . Type ) {
22
22
let xAxis = SIMD3 < T > ( 1 , 0 , 0 )
23
23
// Positive angle, positive axis
24
24
XCTAssertEqual ( Quaternion < T > ( angle: . pi, axis: xAxis) . angle, . pi)
@@ -34,34 +34,30 @@ final class TransformationTests: XCTestCase {
34
34
XCTAssertEqual ( Quaternion < T > . init ( angle: - . pi, axis: - xAxis) . axis, xAxis)
35
35
}
36
36
37
- func testAngleAxisSpin ( ) {
38
- testAngleAxisSpin ( Float32 . self)
39
- testAngleAxisSpin ( Float64 . self)
37
+ func testAngleAxis ( ) {
38
+ testAngleAxis ( Float32 . self)
39
+ testAngleAxis ( Float64 . self)
40
40
}
41
41
42
- func testAngleMultipleOfPi < T: Real & SIMDScalar > ( _ type: T . Type ) {
42
+ func testAngleAxisMultipleOfPi < T: Real & SIMDScalar > ( _ type: T . Type ) {
43
43
let xAxis = SIMD3 < T > ( 1 , 0 , 0 )
44
44
// 2π
45
45
let pi2 = Quaternion < T > ( angle: . pi * 2 , axis: xAxis)
46
46
XCTAssertEqual ( pi2. angle, . pi * 2 )
47
47
XCTAssertEqual ( pi2. axis, xAxis)
48
48
// 3π - axis inverted
49
49
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 ) )
51
51
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)
56
52
// 5π - axis restored
57
53
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 ) )
59
55
XCTAssertEqual ( pi5. axis, xAxis)
60
56
}
61
57
62
- func testAngleMultipleOfPi ( ) {
63
- testAngleMultipleOfPi ( Float32 . self)
64
- testAngleMultipleOfPi ( Float64 . self)
58
+ func testAngleAxisMultipleOfPi ( ) {
59
+ testAngleAxisMultipleOfPi ( Float32 . self)
60
+ testAngleAxisMultipleOfPi ( Float64 . self)
65
61
}
66
62
67
63
func testAngleAxisEdgeCases< T: Real & SIMDScalar > ( _ type: T . Type ) {
@@ -185,11 +181,143 @@ final class TransformationTests: XCTestCase {
185
181
testPolarDecompositionEdgeCases ( Float32 . self)
186
182
testPolarDecompositionEdgeCases ( Float64 . self)
187
183
}
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
+ }
188
310
}
189
311
190
- // Helper
312
+ // MARK: - Helper
191
313
extension SIMD3 where Scalar: FloatingPoint {
192
314
fileprivate static var infinity : Self { SIMD3 ( . infinity, 0 , 0 ) }
193
315
fileprivate static var nan : Self { SIMD3 ( . nan, 0 , 0 ) }
194
316
fileprivate var isNaN : Bool { x. isNaN && y. isNaN && z. isNaN }
195
317
}
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