' '

Difficulty

3

Reading material

math/misc/quaternions

1. Implementation

Implement the class Quaternion. Following operations will be required:

  • A constructor Quaternion(double a, double b, double c, double d) creating the quaternion \(a + bi + cj + dk\).

  • A static factory rotation(Angle theta, const Vector3D& axis) creating a quaternion representing the rotation around the given axis by the given angle.

  • A member function Point3D rotate(const Point3D& p) which rotates p.

  • The member function conjugate.

  • The operators

    • Quaternion + Quaternion

    • Quaternion - Quaternion

    • double * Quaternion

    • Quaternion * double

    • Quaternion * Quaternion

    • Quaternion / double

    • Quaternion += Quaternion

    • Quaternion -= Quaternion

    • Quaternion *= double

    • Quaternion /= double

    • Quaternion == Quaternion

    • Quaternion != Quaternion

2. Writing Tests

The only way to check the correctness of your implementation is to write tests.

When writing down test cases, it is crucial to only mention that information that is necessary to fully specify the test. For example,

TEST_CASE("(0,0,0,0) + (0,0,0,0) == (0,0,0,0)")
{
    Quaternion quat1(0,0,0,0);
    Quaternion quat2(0,0,0,0);
    Quaternion expected(0,0,0,0);
    Quaternion actual = quat1 + quat2;
    CHECK(actual == expected);
}

TEST_CASE("(1,0,0,0) + (0,0,0,0) == (1,0,0,0)")
{
    Quaternion quat1(1,0,0,0);
    Quaternion quat2(0,0,0,0);
    Quaternion expected(1,0,0,0);
    Quaternion actual = quat1 + quat2;
    CHECK(actual == expected);
}

are badly written tests, as there is way too much duplication. Were the way quaternions are modelled to change, we’d have to rewrite each test.

It is therefore important to rely on helper functions or macros in order to distill the essence of each test. What is actually tested is that

\[(0 + 0i + 0j + 0k) + (0 + 0i + 0j + 0k) = (0 + 0i + 0j + 0k)\]

and

\[(1 + 0i + 0j + 0k) + (0 + 0i + 0j + 0k) = (1 + 0i + 0j + 0k)]\]

Only this information should be mentioned in the tests:

TEST_ADDITION(Q(0,0,0,0), Q(0,0,0,0), Q(0,0,0,0))
TEST_ADDITION(Q(1,0,0,0), Q(0,0,0,0), Q(1,0,0,0))

This is a much more compact notation and expresses exactly what we want to test without mentioning any implementation details. Of course, we still need to define TEST_ADDITION:

#define Q(a, b, c, d)       (a, b, c, d)

#define TEST_ADDITION(q1, q2, qe)                                \
    TEST_CASE("Addition of "#q1 " and " #q2 " should give " #qe) \
    {                                                            \
       Quaternion quat1 q1;                                      \
       Quaternion quat2 q2;                                      \
       Quaternion expected qe;                                   \
       Quaternion actual = quat1 + quat2;                        \
       CHECK(actual == expected);                                \
    }

If we were to modify the way we designed quaternions, we’d only have to update this macro.

3. Evaluation

Important

For people who already made this extension: you also need to define operators == and =! so as to be able to test the Quaternion class. The task above has been updated.

Write tests that check quaternion addition (both + and +=):

  • \((0 + 0i + 0j + 0k) + (0 + 0i + 0j + 0k) = (0 + 0i + 0j + 0k)\)

  • \((1 + 0i + 0j + 0k) + (0 + 0i + 0j + 0k) = (1 + 0i + 0j + 0k)\)

  • \((0 + 1i + 0j + 0k) + (0 + 0i + 0j + 0k) = (0 + 1i + 0j + 0k)\)

  • \((0 + 0i + 1j + 0k) + (0 + 0i + 0j + 0k) = (0 + 0i + 1j + 0k)\)

  • \((0 + 0i + 0j + 1k) + (0 + 0i + 0j + 0k) = (0 + 0i + 0j + 1k)\)

  • \((3 + 5i + 2j + 1k) + (7 + 4i + 2j + 6k) = (10 + 9i + 4j + 7k)\)

#define Q(a, b, c, d)       (a, b, c, d)

#define TEST_ADDITION(q1, q2, qe)                                \
    TEST_CASE("Addition of "#q1 " and " #q2 " should give " #qe) \
    {                                                            \
       Quaternion quat1 q1;                                      \
       Quaternion quat2 q2;                                      \
       Quaternion expected qe;                                   \
       SECTION("Using operator +")                               \
       {                                                         \
           Quaternion actual = quat1 + quat2;                    \
           CHECK(actual == expected);                            \
       }                                                         \
       SECTION("Using operator +=")                              \
       {                                                         \
           quat1 += quat2;                                       \
           CHECK(quat1 == expected);                             \
       }                                                         \
    }

// Test cases
TEST_ADDITION(Q(0,0,0,0), Q(0,0,0,0), Q(0,0,0,0))
TEST_ADDITION(Q(1,0,0,0), Q(0,0,0,0), Q(1,0,0,0))
TEST_ADDITION(Q(0,0,0,0), Q(1,0,0,0), Q(1,0,0,0))
TEST_ADDITION(Q(1,0,0,0), Q(1,0,0,0), Q(2,0,0,0))
Important

Test sections (represent by SECTION in the code above) are a bit peculiar: each section can be seen as a separate test, where the part outside the section is shared. For example,

TEST_ADDITION(Q(0,0,0,0), Q(0,0,0,0), Q(0,0,0,0))

generates the following tests:

// Test 1
Quaternion quat1(0,0,0,0);
Quaternion quat2(0,0,0,0);
Quaternion expected(0,0,0,0);
Quaternion actual = quat1 + quat2;
CHECK(actual == expected);

// Test 2
Quaternion quat1(0,0,0,0);
Quaternion quat2(0,0,0,0);
Quaternion expected(0,0,0,0);
quat1 += quat2;
CHECK(quat1 == expected);

Here, we have used sections to test both + and += in one go: the same data can be used for both.

Write tests that check quaternion subtraction (both - and -=):

  • \((0 + 0i + 0j + 0k) - (0 + 0i + 0j + 0k) = (0 + 0i + 0j + 0k)\)

  • \((0 + 0i + 0j + 0k) - (1 + 0i + 0j + 0k) = (-1 + 0i + 0j + 0k)\)

  • \((0 + 0i + 0j + 0k) - (0 + 1i + 0j + 0k) = (0 -1i + 0j + 0k)\)

  • \((0 + 0i + 0j + 0k) - (0 + 0i + 1j + 0k) = (0 + 0i -1j + 0k)\)

  • \((0 + 0i + 0j + 0k) - (0 + 0i + 0j + 1k) = (0 + 0i + 0j -1k)\)

  • \((3 + 5i + 2j + 1k) - (7 + 4i + 2j + 6k) = (-4 + i + 0j -5k)\)

Note

[21/12] These tests contained a few bugs which have been fixed. Since these fix were introduced after 1 Dec, you are free to ignore them, but it would be a bit weird if your implementation passed the buggy tests…​

Write tests that check multiplication between quaternions and reals (both * and *=):

  • \((0 + 0i + 0j + 0k) \cdot 5 = 5 \cdot (0 + 0i + 0j + 0k) = (0 + 0i + 0j + 0k)\)

  • \((1 + 0i + 0j + 0k) \cdot 5 = 5 \cdot (1 + 0i + 0j + 0k) = (5 + 0i + 0j + 0k)\)

  • \((0 + 1i + 0j + 0k) \cdot 3 = 3 \cdot (0 + 1i + 0j + 0k) = (0 + 3i + 0j + 0k)\)

  • \((0 + 0i + 1j + 0k) \cdot 7 = 7 \cdot (0 + 0i + 1j + 0k) = (0 + 0i + 7j + 0k)\)

  • \((0 + 0i + 0j + 1k) \cdot 4 = 4 \cdot (0 + 0i + 0j + 1k) = (0 + 0i + 0j + 4k)\)

  • \((1 + 2i + 3j + 4k) \cdot 2 = 2 \cdot (1 + 2i + 3j + 4k) = (2 + 4i + 6j + 8k)\)

Write tests that check multiplication between quaternions (only *). Test every combination between

  • \((1 + 0i + 0j + 0k)\)

  • \((0 + 1i + 0j + 0k)\)

  • \((0 + 0i + 1j + 0k)\)

  • \((0 + 0i + 0j + 1k)\)

As a reminder, here’s

\[ \begin{array}{r|ccc} \times & i & j & k \\ \hline i & -1 & k & -j \\ j & -k & -1 & i \\ k & j & -i & -1 \\ \end{array}\]

A few example combinations are

TEST_QQ_MULTIPLICATION(Q(1, 0, 0, 0), Q(1, 0, 0, 0), Q(1, 0, 0, 0))
TEST_QQ_MULTIPLICATION(Q(1, 0, 0, 0), Q(0, 1, 0, 0), Q(0, 1, 0, 0))
TEST_QQ_MULTIPLICATION(Q(1, 0, 0, 0), Q(0, 0, 1, 0), Q(0, 0, 1, 0))
TEST_QQ_MULTIPLICATION(Q(1, 0, 0, 0), Q(0, 0, 0, 1), Q(0, 0, 0, 1))
TEST_QQ_MULTIPLICATION(Q(0, 1, 0, 0), Q(0, 1, 0, 0), Q(-1, 0, 0, 0))
TEST_QQ_MULTIPLICATION(Q(0, 1, 0, 0), Q(0, 0, 1, 0), Q(0, 0, 0, 1))
TEST_QQ_MULTIPLICATION(Q(0, 1, 0, 0), Q(0, 0, 0, 1), Q(0, 0, -1, 0))
Note

Think about you could simplify the tests even more compared to what’s shown above.

Write tests that check rotation. Test all the following combinations:

  • Rotate around axes \((1, 0, 0)\), \((0, 1, 0)\) and \((0, 0, 1)\).

  • Rotate by 90 degrees, 180 degrees and 270 degrees.

  • Rotate points \((1, 0, 0)\), \((0, 1, 0)\) and \((0, 0, 1)\).

This should total 27 tests. You can rely on the following macros, together with some test cases:

#define AXIS(x, y, z)     (x, y, z)
#define POINT(x, y, z)    (x, y, z)

#define TEST_ROTATION(axis_, angle_, point_, expected_)                        \
    TEST_CASE("Rotation of " #point_ " by " #angle_ " degrees around " #axis_) \
    {                                                                          \
      Vector3D axis axis_;                                                     \
      Angle angle = Angle::degrees(angle_);                                    \
      Point3D point point_;                                                    \
      Point3D expected expected_;                                              \
      Quaternion quaternion = Quaternion::rotation(angle, axis);               \
      Point3D actual = quaternion.rotate(point);                               \
      CHECK(actual == approx(expected));                                       \
    }

// Some cases
TEST_ROTATION(AXIS(1, 0, 0),  90, POINT(1, 0, 0), POINT(1, 0, 0))
TEST_ROTATION(AXIS(1, 0, 0), 180, POINT(1, 0, 0), POINT(1, 0, 0))
TEST_ROTATION(AXIS(1, 0, 0), 270, POINT(1, 0, 0), POINT(1, 0, 0))

TEST_ROTATION(AXIS(0, 1, 0),  90, POINT(1, 0, 0), POINT(0, 0, -1))
TEST_ROTATION(AXIS(0, 1, 0), 180, POINT(1, 0, 0), POINT(-1, 0, 0))
TEST_ROTATION(AXIS(0, 1, 0), 270, POINT(1, 0, 0), POINT(0, 0, 1))

TEST_ROTATION(AXIS(0, 0, 1),  90, POINT(1, 0, 0), POINT(0, 1, 0))
TEST_ROTATION(AXIS(0, 0, 1), 270, POINT(1, 0, 0), POINT(0, -1, 0))
TEST_ROTATION(AXIS(0, 0, 1), 180, POINT(1, 0, 0), POINT(-1, 0, 0))