Types and construction
From AbstractQuaternion{T} we define three subtypes:
Quaternion{T}, which is an element of the general algebra of quaternions over anyT<:Number. Most commonly,Tis a real floating-point type, but integers, symbolic types, and complex numbers are also supported. Complex components are used to represent Lorentz transformations.Rotor{T}, which is an element of the multiplicative group of unit quaternions, and is interpreted as mapping to a rotation. The magnitude is assumed to be 1 (though, for efficiency, this is not generally confirmed), and the sign may be freely changed in certain cases.QuatVec{T}, which is an element of the additive group of quaternions with 0 scalar part; a "pure vector" quaternion.
For simplicity, almost every function in this package is defined for general Quaternions, so you may not need any other type. However, it can frequently be more accurate and more efficient to use the other subtypes where relevant.
Constructors, constants, and conversions
At the most basic level, Quaternion{T} mimics Complex{T} as closely as possible, including the behavior of most functions in Base. The Rotor{T} and QuatVec{T} subtypes behave very similarly, except that most of their constructors automatically impose the constraints that the norm is 1 and the scalar component is 0, respectively. Also note that when a certain operation is not defined for either of those subtypes, the functions will usually convert to a general Quaternion automatically.
To create new Quaternions interactively, it is typically most convenient to use the constants imx, imy, and imz — or equivalently 𝐢, 𝐣, and 𝐤 — multiplied by appropriate factors and added together. For programmatic work, it is more common to use the quaternion function — which takes all four components, the three vector components, or just the one scalar component, and creates a new Quaternion of the type implied by the arguments. The rotor and quatvec functions do the same for the other subtypes. You can also specify the type, as in Quaternion{Float64}(...). Type conversions with promote, widen, float, etc., work as expected.
Quaternionic.AbstractQuaternion — Type
AbstractQuaternion{T<:Number} <: NumberAbstract supertype of the quaternion types Quaternion, Rotor, and QuatVec, with elements of type T, which may be retrieved with basetype. Every subtype stores its four components (w, x, y, z) as an SVector{4, T}, which may be retrieved with components.
Quaternionic.Quaternion — Type
Quaternion{T<:Number} <: AbstractQuaternion{T}Quaternionic number type with elements of type T.
QuaternionF16, QuaternionF32 and QuaternionF64 are aliases for Quaternion{Float16}, Quaternion{Float32} and Quaternion{Float64} respectively. See also Rotor and QuatVec.
The functions
quaternion(w, x, y, z)
quaternion(x, y, z)
quaternion(w)create a new quaternion with the given components. The argument w is the scalar component, and x, y, and z are the corresponding "vector" components. If any of these arguments is missing, it will be set to zero. The type of the returned quaternion will be inferred from the input arguments, or can be specified, by passing the type parameter T as above.
Note that the constants imx, imy, and imz can also be used like the complex im to create new Quaternion object.
Examples
julia> quaternion(1, 2, 3, 4)
1 + 2𝐢 + 3𝐣 + 4𝐤
julia> Quaternion{Float64}(1, 2, 3, 4)
1.0 + 2.0𝐢 + 3.0𝐣 + 4.0𝐤
julia> quaternion(1.0, 2.0, 3.0, 4.0)
1.0 + 2.0𝐢 + 3.0𝐣 + 4.0𝐤
julia> quaternion(2, 3, 4)
0 + 2𝐢 + 3𝐣 + 4𝐤
julia> quaternion(1)
1 + 0𝐢 + 0𝐣 + 0𝐤Quaternionic.Rotor — Type
Rotor{T<:Number} <: AbstractQuaternion{T}Quaternion of unit magnitude with elements of type T. These objects can be significantly faster and more accurate in certain operations representing rotations.
A rotor is typically considered to be an element of the group $\mathrm{Spin}(3) ≃ \mathrm{SU}(2)$, which can be thought of as the subgroup of quaternions with norm 1. They are particularly useful as representations of rotations because a rotor $R$ acts on a vector $\vec{v}$ by "conjugation" as
\[\vec{v}' = R\, \vec{v}\, R^{-1}.\]
(which can be represented in code as R * v / R or, more efficiently, as R(v)). This operation preserves the inner product between any two vectors conjugated in this way, and so is a rotation. Note that, because there are two factors of $R$ here, the sign of $R$ does not affect the result. Therefore, $\mathrm{Spin}(3)$ forms a double cover of the rotation group $\mathrm{SO}(3)$. For this reason, it will occasionally be useful to disregard or arbitrarily change the sign of a Rotor (as in distance functions) — though this is not generally the default, and may cause problems if the input rotors change sign when the corresponding rotations are not so different (cf. unflip).
RotorF16, RotorF32 and RotorF64 are aliases for Rotor{Float16}, Rotor{Float32} and Rotor{Float64} respectively. See also Quaternion and QuatVec.
The functions
rotor(w, x, y, z)
rotor(x, y, z)
rotor(w)
rotor(v)
rotor(q)create a new rotor with the given components (where the components are as described in Quaternion), automatically normalizing them on input. Here, v is a vector of length 1, 3, or 4, and q is any quaternion. The element type of the result is a floating-point type, even for integer input. The type constructors Rotor(...), without a type parameter, are identical to rotor(...) and also normalize. So does convert(Rotor{T}, x) when x is a number or a quaternion other than a Rotor, and therefore so do the implicit conversions made by push!, setindex!, and typed array literals. Converting a Rotor to a Rotor{T} with a different element type T only converts the components, without normalizing them again.
If you would like to bypass normalization, you can call the type constructor with an explicit type parameter:
Rotor{T}(w, x, y, z)
Rotor{T}(x, y, z)
Rotor{T}(w)
Rotor{T}(v)
Rotor{T}(q)These simply convert the given components to type T and store them; here, v is any AbstractVector that can be converted to an SVector{4, T}. If you want to handle the normalization step yourself, you can use LinearAlgebra.normalize.
However, once a Rotor is created, its norm will often be assumed to be precisely 1. So if its true norm is significantly different, you will likely see weird results — including vectors with very different lengths after "rotation" by a non-unit Rotor. For the same reason, abs and abs2 of a Rotor return exactly 1, and mixing a Rotor with any number or quaternion that is not a Rotor, in arithmetic or in promote, gives a Quaternion. The functions zero(Rotor{T}) also returns a Quaternion, and round of a Rotor returns a Quaternion, because neither result is a unit quaternion. However, zeros(Rotor{T}, dims...) returns an array with element type Rotor{T}, filled with zero rotors that are not normalized, so that it can be used to preallocate an array of rotors.
The product or quotient of two Rotors is a Rotor. For real element types, that result is renormalized to remove the accumulated rounding error. For complex element types (as for Lorentz transformations), it is renormalized only when the sum of the squared magnitudes of its components is less than 16. The complex spinor norm of a boost with rapidity $η$ is computed as $\cosh^2(η/2) - \sinh^2(η/2)$, which cancels catastrophically for large $η$, so renormalizing such a product would add error rather than remove it.
For complex element types, normalization divides by the principal square root of the spinor norm $w^2 + x^2 + y^2 + z^2$, which is itself complex. The overall sign of the normalized result therefore changes discontinuously as the spinor norm crosses the negative real axis.
Note that simply creating a Quaternion that happens to have norm 1 does not make it a Rotor. However, you can pass such a Quaternion to the rotor function and get the desired result.
Examples
julia> rotor(1, 2, 3, 4)
rotor(0.18257418583505536 + 0.3651483716701107𝐢 + 0.5477225575051661𝐣 + 0.7302967433402214𝐤)
julia> rotor(quaternion(1, 2, 3, 4))
rotor(0.18257418583505536 + 0.3651483716701107𝐢 + 0.5477225575051661𝐣 + 0.7302967433402214𝐤)
julia> Rotor{Float16}(1, 2, 3, 4)
rotor(1.0 + 2.0𝐢 + 3.0𝐣 + 4.0𝐤)
julia> rotor(Rotor{Float16}(1, 2, 3, 4))
rotor(0.1826 + 0.3652𝐢 + 0.548𝐣 + 0.7305𝐤)
julia> rotor(1.0)
rotor(1.0 + 0.0𝐢 + 0.0𝐣 + 0.0𝐤)Quaternionic.QuatVec — Type
QuatVec{T<:Number} <: AbstractQuaternion{T}Pure-vector quaternion with elements of type T. These objects can be significantly faster and more accurate in certain operations than general Quaternions.
QuatVecF16, QuatVecF32 and QuatVecF64 are aliases for QuatVec{Float16}, QuatVec{Float32} and QuatVec{Float64} respectively. See also Quaternion and Rotor.
The functions
quatvec(w, x, y, z)
quatvec(x, y, z)
quatvec(w)create a new QuatVec with the given components (where the components are as described in Quaternion), except that the scalar argument w is always set to 0. The same is true of the type constructors QuatVec(...) and QuatVec{T}(...), and therefore also of convert, push!, and other implicit conversions: any scalar part given to them is discarded, so that a QuatVec never stores a nonzero scalar part. For example, QuatVec{T}(w, x, y, z) and convert(QuatVec{T}, q) ignore w and the scalar part of q, respectively, and QuatVec{T}(w) is the zero vector.
A QuatVec is equal (by == and isequal) to another quaternion only if the other quaternion has zero scalar part and the same vector part, and it is equal to a number only if both are zero. Because the multiplicative identity is not a pure vector, one(QuatVec{T}) and oneunit(QuatVec{T}) return Quaternion values. On the other hand, ones(QuatVec{T}, dims...) returns an array with element type QuatVec{T}, as requested, so the scalar part of each one(QuatVec{T}) is discarded when it is stored, and every element is the zero vector.
Examples
julia> quatvec(1, 2, 3, 4)
+ 2𝐢 + 3𝐣 + 4𝐤
julia> quatvec(quaternion(1, 2, 3, 4))
+ 2𝐢 + 3𝐣 + 4𝐤
julia> quatvec(2, 3, 4)
+ 2𝐢 + 3𝐣 + 4𝐤
julia> quatvec(1)
+ 0𝐢 + 0𝐣 + 0𝐤Quaternionic.quaternion — Function
quaternion(w, x, y, z)
quaternion(x, y, z)
quaternion(w)
quaternion(v)
quaternion(q)Create a Quaternion with the given components, whose element type is inferred from the arguments. Any missing components are set to zero. Here, v is a vector of length 1, 3, or 4, which is interpreted as the corresponding list of arguments, and q is any quaternion, whose components are copied.
Quaternionic.rotor — Function
rotor(w, x, y, z)
rotor(x, y, z)
rotor(w)
rotor(v)
rotor(q)Create a Rotor from the given components, normalizing them so that the result has norm 1. Missing components are set to zero, v is a vector of length 1, 3, or 4, which is interpreted as the corresponding list of arguments, and q is any quaternion. The element type of the result is a floating-point type, even for integer input. To store components without normalizing them, use the type constructor Rotor{T}(...) with an explicit type parameter.
Quaternionic.quatvec — Function
quatvec(w, x, y, z)
quatvec(x, y, z)
quatvec(w)
quatvec(v)
quatvec(q)Create a QuatVec with the given vector components, whose element type is inferred from the arguments. The scalar argument w, or the scalar part of the quaternion q, is discarded, so quatvec(w) is the zero vector. Here, v is a vector of length 1, 3, or 4, which is interpreted as the corresponding list of arguments.
Quaternionic.imx — Constant
imx
𝐢The quaternionic unit associated with rotation about the x axis. Can also be entered as Unicode bold 𝐢 (which can be input as \bfi<tab>).
Note that — just as im is a Complex{Bool} — imx is a QuatVec{Bool}, and as soon as you multiply by a scalar of any other number type (e.g., a Float64) it will be promoted to a QuatVec of that number type, and once you add a scalar it will be promoted to a Quaternion.
Examples
julia> imx * imx
-1 + 0𝐢 + 0𝐣 + 0𝐤
julia> 1.2imx
+ 1.2𝐢 + 0.0𝐣 + 0.0𝐤
julia> 1.2 + 3.4imx
1.2 + 3.4𝐢 + 0.0𝐣 + 0.0𝐤
julia> 1.2 + 3.4𝐢
1.2 + 3.4𝐢 + 0.0𝐣 + 0.0𝐤Quaternionic.imy — Constant
imy
𝐣The quaternionic unit associated with rotation about the y axis. Can also be entered as Unicode bold 𝐣 (which can be input as \bfj<tab>).
Note that — just as im is a Complex{Bool} — imy is a QuatVec{Bool}, and as soon as you multiply by a scalar of any other number type (e.g., a Float64) it will be promoted to a QuatVec of that number type, and once you add a scalar it will be promoted to a Quaternion.
Examples
julia> imy * imy
-1 + 0𝐢 + 0𝐣 + 0𝐤
julia> 1.2imy
+ 0.0𝐢 + 1.2𝐣 + 0.0𝐤
julia> 1.2 + 3.4imy
1.2 + 0.0𝐢 + 3.4𝐣 + 0.0𝐤
julia> 1.2 + 3.4𝐣
1.2 + 0.0𝐢 + 3.4𝐣 + 0.0𝐤Quaternionic.imz — Constant
imz
𝐤The quaternionic unit associated with rotation about the z axis. Can also be entered as Unicode bold 𝐤 (which can be input as \bfk<tab>).
Note that — just as im is a Complex{Bool} — imz is a QuatVec{Bool}, and as soon as you multiply by a scalar of any other number type (e.g., a Float64) it will be promoted to a QuatVec of that number type, and once you add a scalar it will be promoted to a Quaternion.
Examples
julia> imz * imz
-1 + 0𝐢 + 0𝐣 + 0𝐤
julia> 1.2imz
+ 0.0𝐢 + 0.0𝐣 + 1.2𝐤
julia> 1.2 + 3.4imz
1.2 + 0.0𝐢 + 0.0𝐣 + 3.4𝐤
julia> 1.2 + 3.4𝐤
1.2 + 0.0𝐢 + 0.0𝐣 + 3.4𝐤Quaternionic.𝐢 — Constant
𝐢Alias for imx, the quaternionic unit QuatVec{Bool} along the x axis.
Quaternionic.𝐣 — Constant
𝐣Alias for imy, the quaternionic unit QuatVec{Bool} along the y axis.
Quaternionic.𝐤 — Constant
𝐤Alias for imz, the quaternionic unit QuatVec{Bool} along the z axis.
Quaternionic.components — Function
components(q::AbstractQuaternion{T})Return the components of q as stored in the struct — as an SVector{4, T}. The components are ordered as (w, x, y, z), where w is the scalar part and x, y, and z are the vector parts.
Quaternionic.basetype — Function
basetype(q)
basetype(QT)Return the element type T of the quaternion q, or of the quaternion type QT, where q isa AbstractQuaternion{T} or QT <: AbstractQuaternion{T}. This is the type of each component, as eltype is for an array. For example, basetype(QuaternionF64) is Float64.
Number functions from Base
The standard Number functions that work for Complex, such as isfinite, iszero, etc., should work analogously for Quaternion. The hash, read, and write functions are also implemented. Because quaternions are Numbers, broadcasting treats each quaternion as a single scalar: abs.(q) is the same as abs(q), for example. To apply a function to each component, apply it to components(q) and construct a new quaternion from the result.
Random quaternions
It is frequently convenient to construct random Quaternion objects, which can be done just as with other types by passing the desired output type to the randn function. The rand function is not overloaded, because there would be no geometric significance to such a Quaternion; randn results are independent of the orientation of the basis used to define the quaternions. Note that it is possible to get random rotors and vectors by passing the appropriate types to the randn function.
Base.randn — Method
randn([rng=default_rng()], QT, [dims...])Generate a normally distributed random quaternion of type QT, or an array of such quaternions with dimensions dims. The type QT must specify the element type, which can be any AbstractFloat, as in QuaternionF64, Rotor{Float32}, or QuatVec{BigFloat}. (There is no default type; randn() with no type argument still returns a Float64.)
If QT is a Quaternion type, the values are drawn from the spherically symmetric quaternionic normal distribution with mean 0 and variance 1, so that the expected value of abs2(q) is 1. This corresponds to each of the four components having an independent normal distribution with mean 0 and variance 1/4.
If QT is a Rotor type, the result is normalized. Because the distribution is spherically symmetric, the result is a uniformly distributed random rotation.
If QT is a QuatVec type, the result has zero scalar part, and its vector part has mean 0 and variance 1, corresponding to each of the three vector components having an independent normal distribution with mean 0 and variance 1/3.
Examples
julia> randn(QuaternionF64)
0.4336736009756228 - 0.45087190792840853𝐢 - 0.24723937675211696𝐣 - 0.4514571469326208𝐤
julia> randn(QuaternionF16, 2, 2)
2×2 Matrix{QuaternionF16}:
0.4321 + 1.105𝐢 + 0.2664𝐣 - 0.1359𝐤 0.064 + 0.9263𝐢 - 0.4138𝐣 + 0.05505𝐤
0.2512 - 0.2585𝐢 - 0.2803𝐣 - 0.00964𝐤 -0.1256 + 0.1848𝐢 + 0.03607𝐣 - 0.752𝐤Symbolic components and LaTeX display
Quaternions with Num components from Symbolics.jl support the basic algebra and most of the mathematical functions. A few functions, such as sqrt, choose an algorithm by comparing the values of components, and raise an error ("non-boolean (Num) used in boolean context") for symbolic components. To simplify each component of a symbolic quaternion, use quaternion(simplify.(components(q))...).
When Latexify.jl is loaded, latexify(q) returns the LaTeX form of a quaternion, and quaternions are displayed as LaTeX in environments that support the text/latex MIME type, such as Jupyter notebooks.