View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.numbers.quaternion;
18  
19  import java.util.Random;
20  import java.util.SplittableRandom;
21  import org.junit.jupiter.api.Assertions;
22  import org.junit.jupiter.api.Test;
23  
24  class QuaternionTest {
25      /** Epsilon for double comparison. */
26      private static final double EPS = Math.ulp(1d);
27      /** Epsilon for double comparison. */
28      private static final double COMPARISON_EPS = 1e-14;
29  
30      @Test
31      void testZeroQuaternion() {
32          Assertions.assertEquals(0, Quaternion.ZERO.norm());
33      }
34  
35      @Test
36      void testUnitQuaternions() {
37          Assertions.assertEquals(1, Quaternion.ONE.norm());
38          Assertions.assertSame(Quaternion.ONE, Quaternion.ONE.normalize());
39  
40          Assertions.assertEquals(1, Quaternion.I.norm());
41          Assertions.assertSame(Quaternion.I, Quaternion.I.normalize());
42  
43          Assertions.assertEquals(1, Quaternion.J.norm());
44          Assertions.assertSame(Quaternion.J, Quaternion.J.normalize());
45  
46          Assertions.assertEquals(1, Quaternion.K.norm());
47          Assertions.assertSame(Quaternion.K, Quaternion.K.normalize());
48      }
49  
50      @Test
51      final void testAccessors1() {
52          final double q0 = 2;
53          final double q1 = 5.4;
54          final double q2 = 17;
55          final double q3 = 0.0005;
56          final Quaternion q = Quaternion.of(q0, q1, q2, q3);
57  
58          Assertions.assertEquals(q0, q.getW());
59          Assertions.assertEquals(q1, q.getX());
60          Assertions.assertEquals(q2, q.getY());
61          Assertions.assertEquals(q3, q.getZ());
62      }
63  
64      @Test
65      final void testAccessors2() {
66          final double q0 = 2;
67          final double q1 = 5.4;
68          final double q2 = 17;
69          final double q3 = 0.0005;
70          final Quaternion q = Quaternion.of(q0, q1, q2, q3);
71  
72          final double sP = q.getScalarPart();
73          final double[] vP = q.getVectorPart();
74  
75          Assertions.assertEquals(q0, sP);
76          Assertions.assertEquals(q1, vP[0]);
77          Assertions.assertEquals(q2, vP[1]);
78          Assertions.assertEquals(q3, vP[2]);
79      }
80  
81      @Test
82      final void testAccessors3() {
83          final double q0 = 2;
84          final double q1 = 5.4;
85          final double q2 = 17;
86          final double q3 = 0.0005;
87          final Quaternion q = Quaternion.of(q0, new double[] {q1, q2, q3});
88  
89          final double sP = q.getScalarPart();
90          final double[] vP = q.getVectorPart();
91  
92          Assertions.assertEquals(q0, sP);
93          Assertions.assertEquals(q1, vP[0]);
94          Assertions.assertEquals(q2, vP[1]);
95          Assertions.assertEquals(q3, vP[2]);
96      }
97  
98      @Test
99      void testWrongDimension() {
100         Assertions.assertThrows(IllegalArgumentException.class,
101             () -> Quaternion.of(new double[] {1, 2})
102         );
103     }
104 
105     @Test
106     final void testConjugate() {
107         final double q0 = 2;
108         final double q1 = 5.4;
109         final double q2 = 17;
110         final double q3 = 0.0005;
111         final Quaternion q = Quaternion.of(q0, q1, q2, q3);
112 
113         final Quaternion qConjugate = q.conjugate();
114 
115         Assertions.assertEquals(q0, qConjugate.getW());
116         Assertions.assertEquals(-q1, qConjugate.getX());
117         Assertions.assertEquals(-q2, qConjugate.getY());
118         Assertions.assertEquals(-q3, qConjugate.getZ());
119     }
120 
121     @Test
122     final void testMultiplyQuaternionQuaternion() {
123 
124         // Case : analytic test case
125 
126         final Quaternion qA = Quaternion.of(1, 0.5, -3, 4);
127         final Quaternion qB = Quaternion.of(6, 2, 1, -9);
128         final Quaternion qResult = Quaternion.multiply(qA, qB);
129 
130         Assertions.assertEquals(44, qResult.getW(), EPS);
131         Assertions.assertEquals(28, qResult.getX(), EPS);
132         Assertions.assertEquals(-4.5, qResult.getY(), EPS);
133         Assertions.assertEquals(21.5, qResult.getZ(), EPS);
134 
135         // Conjugate of the product of two quaternions and product of their conjugates :
136         // Conj(qA * qB) = Conj(qB) * Conj(qA)
137 
138         final Quaternion productOfConjugate = qB.conjugate().multiply(qA.conjugate());
139         final Quaternion conjugateOfProduct = (qA.multiply(qB)).conjugate();
140 
141         Assertions.assertEquals(productOfConjugate.getW(), conjugateOfProduct.getW(), EPS);
142         Assertions.assertEquals(productOfConjugate.getX(), conjugateOfProduct.getX(), EPS);
143         Assertions.assertEquals(productOfConjugate.getY(), conjugateOfProduct.getY(), EPS);
144         Assertions.assertEquals(productOfConjugate.getZ(), conjugateOfProduct.getZ(), EPS);
145     }
146 
147     @Test
148     final void testMultiplyQuaternionVector() {
149 
150         // Case : Product between a vector and a quaternion : QxV
151 
152         final Quaternion quaternion = Quaternion.of(4, 7, -1, 2);
153         final double[] vector = {2.0, 1.0, 3.0};
154         final Quaternion qResultQxV = Quaternion.multiply(quaternion, Quaternion.of(vector));
155 
156         Assertions.assertEquals(-19, qResultQxV.getW(), EPS);
157         Assertions.assertEquals(3, qResultQxV.getX(), EPS);
158         Assertions.assertEquals(-13, qResultQxV.getY(), EPS);
159         Assertions.assertEquals(21, qResultQxV.getZ(), EPS);
160 
161         // Case : Product between a vector and a quaternion : VxQ
162 
163         final Quaternion qResultVxQ = Quaternion.multiply(Quaternion.of(vector), quaternion);
164 
165         Assertions.assertEquals(-19, qResultVxQ.getW(), EPS);
166         Assertions.assertEquals(13, qResultVxQ.getX(), EPS);
167         Assertions.assertEquals(21, qResultVxQ.getY(), EPS);
168         Assertions.assertEquals(3, qResultVxQ.getZ(), EPS);
169     }
170 
171     @Test
172     final void testDotProductQuaternionQuaternion() {
173         // expected output
174         final double expected = -6.;
175         // inputs
176         final Quaternion q1 = Quaternion.of(1, 2, 2, 1);
177         final Quaternion q2 = Quaternion.of(3, -2, -1, -3);
178 
179         final double actual1 = Quaternion.dot(q1, q2);
180         final double actual2 = q1.dot(q2);
181 
182         Assertions.assertEquals(expected, actual1, EPS);
183         Assertions.assertEquals(expected, actual2, EPS);
184     }
185 
186     @Test
187     final void testScalarMultiplyDouble() {
188         // expected outputs
189         final double w = 1.6;
190         final double x = -4.8;
191         final double y = 11.20;
192         final double z = 2.56;
193         // inputs
194         final Quaternion q1 = Quaternion.of(0.5, -1.5, 3.5, 0.8);
195         final double a = 3.2;
196 
197         final Quaternion q = q1.multiply(a);
198 
199         Assertions.assertEquals(w, q.getW(), COMPARISON_EPS);
200         Assertions.assertEquals(x, q.getX(), COMPARISON_EPS);
201         Assertions.assertEquals(y, q.getY(), COMPARISON_EPS);
202         Assertions.assertEquals(z, q.getZ(), COMPARISON_EPS);
203     }
204 
205     @Test
206     final void testAddQuaternionQuaternion() {
207         // expected outputs
208         final double w = 4;
209         final double x = -1;
210         final double y = 2;
211         final double z = -4;
212         // inputs
213         final Quaternion q1 = Quaternion.of(1., 2., -2., -1.);
214         final Quaternion q2 = Quaternion.of(3., -3., 4., -3.);
215 
216         final Quaternion qa = Quaternion.add(q1, q2);
217         final Quaternion qb = q1.add(q2);
218 
219         Assertions.assertEquals(w, qa.getW(), EPS);
220         Assertions.assertEquals(x, qa.getX(), EPS);
221         Assertions.assertEquals(y, qa.getY(), EPS);
222         Assertions.assertEquals(z, qa.getZ(), EPS);
223 
224         Assertions.assertEquals(w, qb.getW(), EPS);
225         Assertions.assertEquals(x, qb.getX(), EPS);
226         Assertions.assertEquals(y, qb.getY(), EPS);
227         Assertions.assertEquals(z, qb.getZ(), EPS);
228     }
229 
230     @Test
231     final void testSubtractQuaternionQuaternion() {
232         // expected outputs
233         final double w = -2.;
234         final double x = 5.;
235         final double y = -6.;
236         final double z = 2.;
237         // inputs
238         final Quaternion q1 = Quaternion.of(1., 2., -2., -1.);
239         final Quaternion q2 = Quaternion.of(3., -3., 4., -3.);
240 
241         final Quaternion qa = Quaternion.subtract(q1, q2);
242         final Quaternion qb = q1.subtract(q2);
243 
244         Assertions.assertEquals(w, qa.getW(), EPS);
245         Assertions.assertEquals(x, qa.getX(), EPS);
246         Assertions.assertEquals(y, qa.getY(), EPS);
247         Assertions.assertEquals(z, qa.getZ(), EPS);
248 
249         Assertions.assertEquals(w, qb.getW(), EPS);
250         Assertions.assertEquals(x, qb.getX(), EPS);
251         Assertions.assertEquals(y, qb.getY(), EPS);
252         Assertions.assertEquals(z, qb.getZ(), EPS);
253     }
254 
255     @Test
256     final void testNorm() {
257 
258         final double q0 = 2;
259         final double q1 = 1;
260         final double q2 = -4;
261         final double q3 = 3;
262         final Quaternion q = Quaternion.of(q0, q1, q2, q3);
263 
264         final double norm = q.norm();
265 
266         Assertions.assertEquals(Math.sqrt(30), norm);
267 
268         final double normSquareRef = Quaternion.multiply(q, q.conjugate()).getScalarPart();
269         Assertions.assertEquals(Math.sqrt(normSquareRef), norm);
270     }
271 
272     @Test
273     final void testNormalize() {
274 
275         final Quaternion q = Quaternion.of(2, 1, -4, -2);
276 
277         final Quaternion versor = q.normalize();
278 
279         Assertions.assertEquals(2.0 / 5.0, versor.getW());
280         Assertions.assertEquals(1.0 / 5.0, versor.getX());
281         Assertions.assertEquals(-4.0 / 5.0, versor.getY());
282         Assertions.assertEquals(-2.0 / 5.0, versor.getZ());
283 
284         Assertions.assertEquals(1, versor.norm());
285 
286         Assertions.assertSame(versor.normalize(), versor);
287     }
288 
289     @Test
290     final void testNormalizeFail_zero() {
291         final Quaternion q = Quaternion.of(0, 0, 0, 0);
292         Assertions.assertThrows(IllegalStateException.class,
293                 q::normalize
294         );
295     }
296 
297     @Test
298     final void testNormalizeFail_nan() {
299         final Quaternion q = Quaternion.of(0, 0, 0, Double.NaN);
300         Assertions.assertThrows(IllegalStateException.class,
301                 q::normalize
302         );
303 
304     }
305 
306     @Test
307     final void testNormalizeFail_positiveInfinity() {
308         final Quaternion q = Quaternion.of(0, 0, Double.POSITIVE_INFINITY, 0);
309         Assertions.assertThrows(IllegalStateException.class,
310                 q::normalize
311         );
312     }
313 
314     @Test
315     final void testNormalizeFail_negativeInfinity() {
316         final Quaternion q = Quaternion.of(0, Double.NEGATIVE_INFINITY, 0, 0);
317         Assertions.assertThrows(IllegalStateException.class,
318                 q::normalize
319         );
320     }
321 
322     @Test
323     final void testObjectEquals() {
324         final double one = 1;
325         final Quaternion q1 = Quaternion.of(one, one, one, one);
326         Assertions.assertEquals(q1, q1);
327 
328         final Quaternion q2 = Quaternion.of(one, one, one, one);
329         Assertions.assertEquals(q2, q1);
330 
331         final Quaternion q3 = Quaternion.of(one, Math.nextUp(one), one, one);
332         Assertions.assertNotEquals(q3, q1);
333 
334         Assertions.assertNotEquals(q3, "bar");
335     }
336 
337     @Test
338     void testHashCode() {
339         Quaternion x = Quaternion.of(0.0, 0.0, 0.0, 0.0);
340         Quaternion y = Quaternion.of(0.0, 0.0 + Double.MIN_VALUE, 0.0, 0.0);
341         Assertions.assertNotEquals(x.hashCode(), y.hashCode());
342         y = Quaternion.of(0.0 + Double.MIN_VALUE, 0.0, 0.0, 0.0);
343         Assertions.assertNotEquals(x.hashCode(), y.hashCode());
344 
345         // "equals" and "hashCode" must be compatible: if two objects have
346         // different hash codes, "equals" must return false.
347         final String msg = "'equals' not compatible with 'hashCode'";
348 
349         x = Quaternion.of(0.0, 0.0, 0.0, 0.0);
350         y = Quaternion.of(-0.0, 0.0, 0.0, 0.0);
351         Assertions.assertNotEquals(x.hashCode(), y.hashCode());
352         Assertions.assertNotEquals(x, y, msg);
353 
354         x = Quaternion.of(0.0, 0.0, 0.0, 0.0);
355         y = Quaternion.of(0.0, -0.0, 0.0, 0.0);
356         Assertions.assertNotEquals(x.hashCode(), y.hashCode());
357         Assertions.assertNotEquals(x, y, msg);
358 
359         x = Quaternion.of(0.0, 0.0, 0.0, 0.0);
360         y = Quaternion.of(0.0, 0.0, -0.0, 0.0);
361         Assertions.assertNotEquals(x.hashCode(), y.hashCode());
362         Assertions.assertNotEquals(x, y, msg);
363 
364         x = Quaternion.of(0.0, 0.0, 0.0, 0.0);
365         y = Quaternion.of(0.0, 0.0, 0.0, -0.0);
366         Assertions.assertNotEquals(x.hashCode(), y.hashCode());
367         Assertions.assertNotEquals(x, y, msg);
368     }
369 
370     @Test
371     final void testQuaternionEquals() {
372         final double inc = 1e-5;
373         final Quaternion q1 = Quaternion.of(2, 1, -4, -2);
374         final Quaternion q2 = Quaternion.of(q1.getW() + inc, q1.getX(), q1.getY(), q1.getZ());
375         final Quaternion q3 = Quaternion.of(q1.getW(), q1.getX() + inc, q1.getY(), q1.getZ());
376         final Quaternion q4 = Quaternion.of(q1.getW(), q1.getX(), q1.getY() + inc, q1.getZ());
377         final Quaternion q5 = Quaternion.of(q1.getW(), q1.getX(), q1.getY(), q1.getZ() + inc);
378 
379         Assertions.assertFalse(q1.equals(q2, 0.9 * inc));
380         Assertions.assertFalse(q1.equals(q3, 0.9 * inc));
381         Assertions.assertFalse(q1.equals(q4, 0.9 * inc));
382         Assertions.assertFalse(q1.equals(q5, 0.9 * inc));
383 
384         Assertions.assertTrue(q1.equals(q2, 1.1 * inc));
385         Assertions.assertTrue(q1.equals(q3, 1.1 * inc));
386         Assertions.assertTrue(q1.equals(q4, 1.1 * inc));
387         Assertions.assertTrue(q1.equals(q5, 1.1 * inc));
388     }
389 
390     @Test
391     final void testQuaternionEquals2() {
392         final Quaternion q1 = Quaternion.of(1, 4, 2, 3);
393         final double gap = 1e-5;
394         final Quaternion q2 = Quaternion.of(1 + gap, 4 + gap, 2 + gap, 3 + gap);
395 
396         Assertions.assertTrue(q1.equals(q2, 10 * gap));
397         Assertions.assertFalse(q1.equals(q2, gap));
398         Assertions.assertFalse(q1.equals(q2, gap / 10));
399     }
400 
401     @Test
402     final void testIsUnit() {
403         final Random r = new Random(48);
404         final int numberOfTrials = 1000;
405         for (int i = 0; i < numberOfTrials; i++) {
406             final Quaternion q1 = Quaternion.of(r.nextDouble(), r.nextDouble(), r.nextDouble(), r.nextDouble());
407             final Quaternion q2 = q1.normalize();
408             Assertions.assertTrue(q2.isUnit(COMPARISON_EPS));
409         }
410 
411         final Quaternion q = Quaternion.of(1, 1, 1, 1);
412         Assertions.assertFalse(q.isUnit(COMPARISON_EPS));
413     }
414 
415     @Test
416     final void testIsPure() {
417         final Quaternion q1 = Quaternion.of(0, 5, 4, 8);
418         Assertions.assertTrue(q1.isPure(EPS));
419 
420         final Quaternion q2 = Quaternion.of(0 - EPS, 5, 4, 8);
421         Assertions.assertTrue(q2.isPure(EPS));
422 
423         final Quaternion q3 = Quaternion.of(0 - 1.1 * EPS, 5, 4, 8);
424         Assertions.assertFalse(q3.isPure(EPS));
425 
426         final Random r = new Random(48);
427         final double[] v = {r.nextDouble(), r.nextDouble(), r.nextDouble()};
428         final Quaternion q4 = Quaternion.of(v);
429         Assertions.assertTrue(q4.isPure(0));
430 
431         final Quaternion q5 = Quaternion.of(0, v);
432         Assertions.assertTrue(q5.isPure(0));
433     }
434 
435     @Test
436     final void testPositivePolarFormWhenScalarPositive() {
437         Quaternion q = Quaternion.of(3, -3, -3, 3).positivePolarForm();
438         Quaternion expected = Quaternion.of(0.5, -0.5, -0.5, 0.5);
439         assertEquals(q, expected, EPS);
440 
441         Assertions.assertSame(q.positivePolarForm(), q);
442     }
443 
444     @Test
445     final void testPositivePolarFormWhenScalarNegative() {
446         Quaternion q = Quaternion.of(-3, 3, -3, 3).positivePolarForm();
447         Quaternion expected = Quaternion.of(0.5, -0.5, 0.5, -0.5);
448         assertEquals(q, expected, EPS);
449 
450         Assertions.assertSame(q.positivePolarForm(), q);
451     }
452 
453     @Test
454     final void testPositivePolarFormWhenScalarPositiveAndNormalized() {
455         Quaternion q = Quaternion.of(123, 45, 67, 89).normalize().positivePolarForm();
456 
457         Assertions.assertTrue(q.getW() >= 0);
458         Assertions.assertSame(q.positivePolarForm(), q);
459     }
460 
461     @Test
462     final void testPositivePolarFormWhenScalarNegativeAndNormalized() {
463         Quaternion q = Quaternion.of(123, 45, 67, 89).normalize().negate().positivePolarForm();
464 
465         Assertions.assertTrue(q.getW() >= 0);
466         Assertions.assertSame(q.positivePolarForm(), q);
467     }
468 
469     @Test
470     void testNegate() {
471         final double a = -1;
472         final double b = 2;
473         final double c = -3;
474         final double d = 4;
475         final Quaternion q = Quaternion.of(a, b, c, d);
476         final Quaternion qNeg = q.negate();
477         Assertions.assertEquals(-a, qNeg.getW());
478         Assertions.assertEquals(-b, qNeg.getX());
479         Assertions.assertEquals(-c, qNeg.getY());
480         Assertions.assertEquals(-d, qNeg.getZ());
481 
482         Assertions.assertTrue(q.equals(qNeg.negate(), 0d));
483     }
484 
485     @Test
486     void testNegateNormalized() {
487         final double a = -1;
488         final double b = 2;
489         final double c = -3;
490         final double d = 4;
491         final Quaternion q = Quaternion.of(a, b, c, d).normalize();
492         final Quaternion qNeg = q.negate();
493         Assertions.assertTrue(q.equals(qNeg.negate(), 0d));
494     }
495 
496     @Test
497     void testNegatePositivePolarForm() {
498         final double a = -1;
499         final double b = 2;
500         final double c = -3;
501         final double d = 4;
502         final Quaternion q = Quaternion.of(a, b, c, d).positivePolarForm();
503         final Quaternion qNeg = q.negate();
504         Assertions.assertTrue(q.equals(qNeg.negate(), 0d));
505     }
506 
507     @Test
508     final void testPolarForm() {
509         final SplittableRandom r = new SplittableRandom(48);
510         final int numberOfTrials = 1000;
511         for (int i = 0; i < numberOfTrials; i++) {
512             final Quaternion q = Quaternion.of(2 * (r.nextDouble() - 0.5), 2 * (r.nextDouble() - 0.5),
513                                                2 * (r.nextDouble() - 0.5), 2 * (r.nextDouble() - 0.5));
514             final Quaternion qP = q.positivePolarForm();
515 
516             Assertions.assertTrue(qP.isUnit(COMPARISON_EPS), "polar form is not unit length");
517             Assertions.assertTrue(qP.getW() >= 0, "scalar part is not positive");
518         }
519     }
520 
521     @Test
522     final void testInverse() {
523         final Quaternion q = Quaternion.of(1.5, 4, 2, -2.5);
524 
525         final Quaternion inverseQ = q.inverse();
526         Assertions.assertEquals(1.5 / 28.5, inverseQ.getW());
527         Assertions.assertEquals(-4.0 / 28.5, inverseQ.getX());
528         Assertions.assertEquals(-2.0 / 28.5, inverseQ.getY());
529         Assertions.assertEquals(2.5 / 28.5, inverseQ.getZ());
530 
531         final Quaternion product = Quaternion.multiply(inverseQ, q);
532         Assertions.assertEquals(1, product.getW(), EPS);
533         Assertions.assertEquals(0, product.getX(), EPS);
534         Assertions.assertEquals(0, product.getY(), EPS);
535         Assertions.assertEquals(0, product.getZ(), EPS);
536 
537         final Quaternion qNul = Quaternion.of(0, 0, 0, 0);
538         try {
539             final Quaternion inverseQNul = qNul.inverse();
540             Assertions.fail("expecting ZeroException but got : " + inverseQNul);
541         } catch (IllegalStateException ex) {
542             // expected
543         }
544     }
545 
546     @Test
547     void testInverse_zeroNorm() {
548         Quaternion q = Quaternion.of(0, 0, 0, 0);
549         Assertions.assertThrows(IllegalStateException.class,
550                 q::inverse
551         );
552     }
553 
554     @Test
555     void testInverse_nanNorm() {
556         Quaternion q = Quaternion.of(Double.NaN, 0, 0, 0);
557         Assertions.assertThrows(IllegalStateException.class,
558                 q::inverse
559         );
560     }
561 
562     @Test
563     void testInverse_positiveInfinityNorm() {
564         Quaternion q = Quaternion.of(0, Double.POSITIVE_INFINITY, 0, 0);
565         Assertions.assertThrows(IllegalStateException.class,
566                 q::inverse
567         );
568     }
569 
570     @Test
571     void testInverse_negativeInfinityNorm() {
572         Quaternion q = Quaternion.of(0, 0, Double.NEGATIVE_INFINITY, 0);
573         Assertions.assertThrows(IllegalStateException.class,
574                 q::inverse
575         );
576     }
577 
578     @Test
579     void testInverseNormalized() {
580         final Quaternion invQ = Quaternion.of(-1.2, 3.4, -5.6, -7.8).normalize().inverse();
581         final Quaternion q = invQ.inverse();
582         final Quaternion result = q.multiply(invQ);
583         Assertions.assertTrue(Quaternion.ONE.equals(result, EPS), result.toString());
584     }
585 
586     @Test
587     void testInversePositivePolarForm() {
588         final Quaternion invQ = Quaternion.of(1.2, -3.4, 5.6, -7.8).positivePolarForm().inverse();
589         final Quaternion q = invQ.inverse();
590         final Quaternion result = q.multiply(invQ);
591         Assertions.assertTrue(Quaternion.ONE.equals(result, EPS), result.toString());
592     }
593 
594     @Test
595     final void testMultiply() {
596         final Quaternion q1 = Quaternion.of(1, 2, 3, 4);
597         final Quaternion q2 = Quaternion.of(4, 3, 2, 1);
598         final Quaternion actual = q1.multiply(q2);
599         final double w = 1 * 4 - 2 * 3 - 3 * 2 - 4 * 1;
600         final double x = 1 * 3 + 2 * 4 + 3 * 1 - 4 * 2;
601         final double y = 1 * 2 - 2 * 1 + 3 * 4 + 4 * 3;
602         final double z = 1 * 1 + 2 * 2 - 3 * 3 + 4 * 4;
603         final Quaternion expected = Quaternion.of(w, x, y, z);
604         assertEquals(actual, expected, EPS);
605     }
606 
607     @Test
608     final void testParseFromToString() {
609         final Quaternion q = Quaternion.of(1.1, 2.2, 3.3, 4.4);
610         Quaternion parsed = Quaternion.parse(q.toString());
611         assertEquals(parsed, q, EPS);
612     }
613 
614     @Test
615     final void testParseSpecials() {
616         Quaternion parsed = Quaternion.parse("[1e-5 Infinity NaN -0xa.cp0]");
617         Assertions.assertEquals(1e-5, parsed.getW(), EPS);
618         Assertions.assertTrue(Double.isInfinite(parsed.getX()));
619         Assertions.assertTrue(Double.isNaN(parsed.getY()));
620         Assertions.assertEquals(-0xa.cp0, parsed.getZ(), EPS);
621     }
622 
623     @Test
624     final void testParseMissingStart() {
625         Assertions.assertThrows(IllegalArgumentException.class,
626             () -> Quaternion.parse("1.0 2.0 3.0 4.0]")
627         );
628     }
629 
630     @Test
631     final void testParseMissingEnd() {
632         Assertions.assertThrows(IllegalArgumentException.class,
633             () -> Quaternion.parse("[1.0 2.0 3.0 4.0")
634         );
635     }
636 
637     @Test
638     final void testParseMissingPart() {
639         Assertions.assertThrows(IllegalArgumentException.class,
640             () -> Quaternion.parse("[1.0 2.0 3.0 ]")
641         );
642     }
643 
644     @Test
645     final void testParseInvalidScalar() {
646         Assertions.assertThrows(IllegalArgumentException.class,
647             () -> Quaternion.parse("[1.x 2.0 3.0 4.0]")
648         );
649     }
650 
651     @Test
652     final void testParseInvalidI() {
653         Assertions.assertThrows(IllegalArgumentException.class,
654             () -> Quaternion.parse("[1.0 2.0x 3.0 4.0]")
655         );
656     }
657 
658     @Test
659     final void testParseInvalidJ() {
660         Assertions.assertThrows(IllegalArgumentException.class,
661             () -> Quaternion.parse("[1.0 2.0 3.0x 4.0]")
662         );
663     }
664 
665     @Test
666     final void testParseInvalidK() {
667         Assertions.assertThrows(IllegalArgumentException.class,
668             () -> Quaternion.parse("[1.0 2.0 3.0 4.0x]")
669         );
670     }
671 
672     @Test
673     final void testToString() {
674         final Quaternion q = Quaternion.of(1, 2, 3, 4);
675         Assertions.assertEquals("[1.0 2.0 3.0 4.0]", q.toString());
676     }
677 
678     /**
679      * Assert that two quaternions are equal within tolerance
680      * @param actual
681      * @param expected
682      * @param tolerance
683      */
684     private void assertEquals(Quaternion actual, Quaternion expected, double tolerance) {
685         Assertions.assertTrue(actual.equals(expected, tolerance), "expecting " + expected + " but got " + actual);
686     }
687 
688 }