# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                         #
#         Quaternion algebra package for Maple 8                          #
#         --------------------------------------                          #
#                                                                         #
#         Copyright: Dr S. J. Sangwine                                    #
#                    February 2003                                        #
#                                                                         #
#         Email:     S.Sangwine@IEEE.org or sjs@essex.ac.uk               #
#                                                                         #
#         Address:   Department of Electronic Systems Engineering         #
#                    University of Essex, Wivenhoe Park                   #
#                    Colchester CO4 3SQ, United Kingdom                   #
#                                                                         #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                         #
# Version: 0.1  4 April 2003  This is the first version to have a number. #
#                             Previous versions were experimental.        #
#          0.2  7 April 2003  First version with support for matrices of  #
#                             quaternions.                                #
#          0.3  8 April 2003  Version designed to be saved to repository. #
#          0.4 22 September 2003 Addition of Involute function.           #
#          0.5 22 February 2005 Changed several functions to be           #
#                               independent of the quaternion             #
#                               representation.                           #
#                                                                         #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                         #
#                           Description                                   #
#                           -----------                                   #
#                                                                         #
# This package extends Maple to handle quaternions (extensions of the     #
# complex numbers to 4 dimensions, discovered by Hamilton in 1843).       #
#                                                                         #
# The aim in writing this package has been to extend Maple in as natural  #
# a manner as possible, so that handling quaternions is almost as easy as #
# handling complex numbers. It was especially important to integrate this #
# package with existing Maple facilities such as expand/simplify/solve,   #
# so that manipulation of quaternion expressions fits naturally into      #
# Maple. Whether the aim has been achieved is for the user to judge!      #
#                                                                         #
# This package depends on the isomorphism between quaternions and certain #
# 4x4 matrices of reals. Quaternions (and vectors) are represented using  #
# these matrices so that Maple's existing ability to handle matrices can  #
# be exploited to handle quaternions (and vectors). A previous attempt to #
# develop a quaternion package based on representations as 4-tuples was   #
# not successful because Maple has no built-in abilities with 4-tuples.   #
#                                                                         #
# A package called quaterman, written by Yves Papegay at INRIA in France  #
# adopted the same approach, but was written for a much earlier version   #
# of Maple, and it suffered from similar problems.                        #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

module QuaternionAlgebra()

  export e0, e1, e2, e3, Zero,
         MakeQuaternion,
         MakePureQuaternion,
         S, V, X, Y, Z,
         Conjugate, Involute, Scalar,
         ScalarProduct, VectorProduct,
         QNorm, Modulus, Unit, Inverse, Argument, Axis, Par, Perp,
         Exponential;

  global      Quaternion,      Pure_Quaternion,      Full_Quaternion,
         Unit_Quaternion, Unit_Pure_Quaternion, Unit_Full_Quaternion,

         QuaternionMatrix,  MatrixofQuaternions,

         `convert/Qlist`,
         `convert/Quaternion`,

         `convert/QuaternionMatrix`,
         `convert/MatrixofQuaternions`;

  local  startup, cleanup;

  option package,

         load   = startup,
         unload = cleanup,

         `Copyright Dr Steve Sangwine (S.Sangwine@IEEE.org), 2003`;

  description "Quaternion algebra package";

  # This version of the package uses the LinearAlgebra package to provide
  # matrix operations, but to make the coding as independent as possible
  # the following are defined. Note particular the avoidance of the dot
  # operator [mul(x,y) is used instead of x.y], although the dot operator
  # may be used in code that uses the facilities of this package. Notice
  # that the notation :- is used here deliberately. The reasons are given
  # in the Maple 8 help files under "Programming with the LinearAlgebra Package".

  use transpose = LinearAlgebra:-Transpose,
          trace = LinearAlgebra:-Trace,
           smul = LinearAlgebra:-MatrixScalarMultiply,
            mul = LinearAlgebra:-MatrixMatrixMultiply
    in

    # Basis matrices.
    # ---------------

    # These four matrices represent the unit scalar, and the three quaternion operators.
    # (These could represent the three operators i, j and k if the standard basis is in
    # use, but they could also represent a more general basis.) The use of these four
    # basis matrices simplifies much of the rest of the package. Note that the matrices
    # used for e1, e2 and e3 are not unique. There are other valid possibilities which
    # could be chosen. The chosen matrices must be anti-symmetric, and each pair must
    # multiply to give the third, with signs dependent on ordering. e0 is an identity
    # matrix, of course.
    #
    #     w  x  y  z    This is the representation for a quaternion
    #    -x  w -z  y    w + ix + jy + kz given the four basis matrices
    #    -y  z  w -x    defined here.
    #    -z -y  x  w

    e0 := Matrix(1 .. 4, 1 .. 4, [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]);
    e1 := Matrix(1 .. 4, 1 .. 4, [[0,1,0,0],[-1,0,0,0],[0,0,0,-1],[0,0,1,0]]);
    e2 := Matrix(1 .. 4, 1 .. 4, [[0,0,1,0],[0,0,0,1],[-1,0,0,0],[0,-1,0,0]]);
    e3 := mul(e1, e2);

    Zero := Matrix(1 .. 4, 1 .. 4, [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]);

    startup := proc() # This procedure defines some actions that must be invoked
                      # when the module is read from a repository. These are to
                      # define types and the basis matrices.

      global      Quaternion,      Pure_Quaternion,      Full_Quaternion,
             Unit_Quaternion, Unit_Pure_Quaternion, Unit_Full_Quaternion,

             QuaternionMatrix,  MatrixofQuaternions;

      # Quaternion Types
      # ----------------
 
      # Determining whether an expression is a quaternion relies on testing whether it is a
      # matrix, followed by establishing that it is a valid matrix. This must be done without
      # using any of the functions of this module (e.g. Scalar), because these have specified
      # parameter types which are enforced by using these type definitions.

      TypeTools:-AddType(Quaternion,
                         q -> type(q, Matrix[1 .. 4, 1 .. 4])
                                and evalb(simplify(q = (smul(e0, q[1, 1]) +
                                                        smul(e1, q[1, 2]) +
                                                        smul(e2, q[1, 3]) +
                                                        smul(e3, q[1, 4])))));

      TypeTools:-AddType(Unit_Quaternion, q -> evalb(type(q, Quaternion) and evalb(combine(simplify(QNorm(q))) = 1)));

      TypeTools:-AddType(Pure_Quaternion, p -> evalb(type(p, Quaternion) and evalb(trace(p) =  0)));
      TypeTools:-AddType(Full_Quaternion, q -> evalb(type(q, Quaternion) and evalb(trace(q) <> 0)));

      TypeTools:-AddType(Unit_Pure_Quaternion, p -> evalb(type(p, Pure_Quaternion) and type(p, Unit_Quaternion)));
      TypeTools:-AddType(Unit_Full_Quaternion, q -> evalb(type(q, Full_Quaternion) and type(q, Unit_Quaternion)));

      # Matrices
      # --------

      # Here we define types to operate on matrices of quaternions.
      # Since we are representing quaternions as 4x4 matrices, a matrix of quaternions
      # can be represented as a block matrix in which each element is a 4x4 matrix.
      # However, Maple normally flattens such matrices, and can only operate on them
      # in the flat form (sometimes known as a partitioned matrix). Therefore we define
      # a type for each of the block and flat forms, and conversions between them.

      # MatrixofQuaternions: the block form in which each element of the matrix is a
      #                      4x4 matrix conforming to the type rules for Quaternion.
      #                      Maple is not able to do much with this form, but it is
      #                      convenient for constructing a matrix or extracting elements.

      TypeTools:-AddType(MatrixofQuaternions, m -> type(m, 'Matrix(Quaternion)'));

      # QuaternionMatrix:    the flat form, which must have numbers of rows and columns
      #                      which are a multiple of 4, and the layout within each 4x4
      #                      partition must be a valid quaternion matrix. Maple is able
      #                      to manipulate this form because the quaternionic nature is
      #                      hidden in the pattern of the elements and standard matrix
      #                      manipulations will work.

      TypeTools:-AddType(QuaternionMatrix,
        proc(m)

          local r, c;

          if type(m, Matrix)
            and evalb(map(`mod`, [op(1, m)], 4) = [0, 0]) # Dimensions divisible by 4?
            then

            # So far we know m is a Matrix with dimensions which are a multiple of 4.
            # We now check that each 4x4 block is a Quaternion.

            for r from 1 by 4 to op(1, [op(1, m)]) do
              for c from 1 by 4 to op(2, [op(1, m)]) do
                if not type(m[r .. r + 3, c .. c + 3], Quaternion) then
                  return false; # The current block is not a valid Quaternion.
                end if
              end do
            end do;
            return true # All the blocks were Quaternions.
          end if;
          return false # Either m is not a Matrix, or the dimensions are not multiples of 4.
        end proc);

      # Conversion functions
      # --------------------

      # The first of these converts from a quaternion (full or pure) into a
      # list of 4 elements. This is particularly intended for display, since
      # it lists each component of the quaternion once only, and will
      # therefore take up much less page/screen space than the matrix form.

      `convert/Qlist` := (q::Quaternion) -> [seq(q[1, i], i = 1 .. 4)];

      # The second function converts from a list of 3 or 4 elements to the matrix
      # form. It reverses the conversion performed by the first function, but of
      # course it may also be used to construct quaternions in the matrix form.

      `convert/Quaternion` := proc(l::list)
        if nops(l) = 4 then
          MakeQuaternion(seq(op(i, l), i = 1 .. 4));
        elif nops(l) = 3 then
          MakePureQuaternion(seq(op(i, l), i = 1 .. 3));
        else
          error "convert/Quaternion requires its argument to have 3 or 4 elements, given %1", nops(l);
        end if;
      end proc;

      # The two conversion procedures below convert between the two types of matrix which
      # differ in dimensions by a factor 4. The approach adopted is to create a local
      # Matrix of the correct size, then iterate to fill it. This may not be very elegant
      # Maple code, but it works.

      `convert/QuaternionMatrix` :=
        proc(m::MatrixofQuaternions)

          local n, r, c;

          n := Matrix(op(1, m) * 4);

          for r from 1 to op(1, [op(1, m)]) do
            for c from 1 to op(2, [op(1, m)]) do
              n[4 * (r - 1) + 1 .. 4 * (r - 1) + 4,
                4 * (c - 1) + 1 .. 4 * (c - 1) + 4] := m[r, c];
            end do
          end do;
          return n;
        end proc;

      `convert/MatrixofQuaternions` :=
        proc(n::QuaternionMatrix)

          local m, r, c;

          m := Matrix(op(1, n)/4);

          for r from 1 to op(1, [op(1, m)]) do
            for c from 1 to op(2, [op(1, m)]) do
              m[r, c] := n[4 * (r - 1) + 1 .. 4 * (r - 1) + 4,
                           4 * (c - 1) + 1 .. 4 * (c - 1) + 4]
            end do
          end do;
          return m;
        end proc;

      NULL
    end proc;

    # Constructor functions
    # ---------------------

    # These make a quaternion from 3 or 4 arguments, or, if given only one,
    # they make the quaternion by subscripting the given argument.

    MakeQuaternion := proc()
      if nargs = 4 then
        smul(e0, args[1]) + smul(e1, args[2]) + smul(e2, args[3]) + smul(e3, args[4])
      elif nargs = 1 then
        use t = args[1] in
          MakeQuaternion(t[w], t[x], t[y], t[z])
        end use;
      else
        error "MakeQuaternion expects 1 or 4 arguments, given: %1", nargs
      end if;
    end proc;

    MakePureQuaternion := proc()
      if nargs = 3 then
        MakeQuaternion(0, args[1], args[2], args[3])
      elif nargs = 1 then
        MakePureQuaternion(args[1][x], args[1][y], args[1][z])
      else
        error "MakePureQuaternion expects 1 or 3 arguments, given: %1", nargs
      end if;
    end proc;

    # The following four functions depend on the representation because they
    # return scalars and not quaternions. It is possible to define them in a
    # way that is independent of the representation, but they will then return
    # results in the same representation as a quaternion, in this case, matrices.

    Scalar := (q::Quaternion) -> q[1,1];
    X      := (q::Quaternion) -> q[1,2];
    Y      := (q::Quaternion) -> q[1,3];
    Z      := (q::Quaternion) -> q[1,4];

    # The Involute function is fundamental to the remaining definitions, since it permits
    # the definition of the conjugate independent of the representation.

    Involute  := (q::Quaternion, u::Unit_Pure_Quaternion) -> -mul(u, mul(q,u)); # -u.q.u;

    # Common quaternion operations
    # ----------------------------

    # The following definitions are independent of the definition of a quaternion.

    QNorm   := (q::Quaternion) -> Scalar(q)^2 + X(q)^2 + Y(q)^2 + Z(q)^2;
    Modulus := (q::Quaternion) -> sqrt(QNorm(q));

    Unit    := (q::Quaternion) -> q /Modulus(q);
    Axis    := (q::Quaternion) -> Unit(V(q));

    Argument  := (q::Quaternion) -> arctan(Modulus(V(q)), Scalar(q));

    Conjugate := (q::Quaternion) -> (Involute(q,e1) + Involute(q,e2) + Involute(q,e3) - q)/2;
    Inverse   := (q::Quaternion) -> Conjugate(q)/QNorm(q);

    S := (q::Quaternion) -> (q + Conjugate(q))/2;
    V := (q::Quaternion) -> (q - Conjugate(q))/2;

    ScalarProduct := (l::Quaternion, r::Quaternion) ->
      Scalar(l) * Scalar(r) + X(l) * X(r) + Y(l) * Y(r) + Z(l) * Z(r);

    VectorProduct := (l::Pure_Quaternion, r::Pure_Quaternion) ->
      MakePureQuaternion(Y(l) * Z(r) - Z(l) * Y(r),
                         Z(l) * X(r) - X(l) * Z(r),
                         X(l) * Y(r) - Y(l) * X(r));

    Exponential := (mu::Unit_Pure_Quaternion, theta::scalar) ->
      smul(e0, cos(theta)) + smul(mu, sin(theta));

    # Parallel/perpendicular decomposition
    # ------------------------------------
    #
    # These are based on formulae due to Todd A. Ell which depend on
    # reflecting the vector part of x in a plane normal to y.

    Par  := (x::Quaternion, y::Unit_Pure_Quaternion) -> (x + Involute(x, y))/2;
    Perp := (x::Quaternion, y::Unit_Pure_Quaternion) -> (x - Involute(x, y))/2;

  end use;

  # The Maple documentation is somewhat confused about the load/unload options to
  # a package. The Maple 8 Advanced Programming Guide gives some advice in Chapter
  # 3, which seems inconsistent with the help files.

  startup(); # Not sure why this is necessary, but it seems to do no harm.

  cleanup := proc() # Even less sure why this is needed, ditto.

    global      Quaternion,      Pure_Quaternion,      Full_Quaternion,
           Unit_Quaternion, Unit_Pure_Quaternion, Unit_Full_Quaternion,

           QuaternionMatrix,  MatrixofQuaternions;

    TypeTools:-RemoveType(Quaternion);
    TypeTools:-RemoveType(Unit_Quaternion);
    TypeTools:-RemoveType(Pure_Quaternion);
    TypeTools:-RemoveType(Full_Quaternion);
    TypeTools:-RemoveType(Unit_Pure_Quaternion);
    TypeTools:-RemoveType(Unit_Full_Quaternion);
    TypeTools:-RemoveType(MatrixofQuaternions);
    TypeTools:-RemoveType(QuaternionMatrix);

    `convert/Qlist`      := evaln(`convert/Qlist`);
    `convert/Quaternion` := evaln(`convert/Quaternion`);

    `convert/QuaternionMatrix`    := evaln(`convert/QuaternionMatrix`);
    `convert/MatrixofQuaternions` := evaln(`convert/MatrixofQuaternions`);

    NULL
  end proc;

end module:

