GithubHelp home page GithubHelp logo

juliageometry / quaternions.jl Goto Github PK

View Code? Open in Web Editor NEW
116.0 31.0 37.0 947 KB

A Julia implementation of quaternions

Home Page: https://juliageometry.github.io/Quaternions.jl

License: MIT License

Julia 100.00%
julia quaternion

quaternions.jl's Introduction

Quaternions.jl

A Julia implementation of quaternions.

Stable Dev Build Status codecov Aqua QA

Quaternions are best known for their suitability as representations of 3D rotational orientation. They can also be viewed as an extension of complex numbers.

First example

julia> using Quaternions

julia> k = quat(0, 0, 0, 1)
Quaternion{Int64}(0, 0, 0, 1)

julia> j = quat(0, 0, 1, 0)
Quaternion{Int64}(0, 0, 1, 0)

julia> i = j*k
Quaternion{Int64}(0, 1, 0, 0)

julia> i^2 == j^2 == k^2 == i*j*k == -1 # Similar to `im^2`.
true

julia> 1 + i + k + j  # Compatible with arithmetic operations as a `Number`.
Quaternion{Int64}(1, 1, 1, 1)

Check out the docs for further instructions.

In JuliaGeometry organization, there is also Octonions.jl package.

quaternions.jl's People

Contributors

adambrewster avatar andreasnoack avatar bzinberg avatar dependabot[bot] avatar dkarrasch avatar fredrikekre avatar hyrodium avatar jw3126 avatar keesvp avatar kristofferc avatar mikmoore avatar ranocha avatar rdeits avatar sethaxen avatar simondanisch avatar stevengj avatar stuinzuri avatar tianrluo avatar timholy avatar westleyargentum avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

quaternions.jl's Issues

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

If you'd like for me to do this for you, comment TagBot fix on this issue.
I'll open a PR within a few hours, please be patient!

`Quaternions.imag` is slow with `Vector`

It seems better to have the return value with type Tuple instead of Vector.

julia> using Quaternions, BenchmarkTools

julia> q = Quaternion(1,2,3,4)
Quaternion{Int64}(1, 2, 3, 4, false)

julia> function new_imag(q::Quaternion)
           (q.v1,q.v2,q.v3)
       end
new_imag (generic function with 1 method)

julia> Quaternions.imag(q)
3-element Vector{Int64}:
 2
 3
 4

julia> new_imag(q)
(2, 3, 4)

julia> @benchmark Quaternions.imag($q)

BenchmarkTools.Trial: 10000 samples with 997 evaluations.
 Range (min  max):  20.596 ns   1.775 μs  ┊ GC (min  max): 0.00%  98.12%
 Time  (median):     22.907 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   26.725 ns ± 65.403 ns  ┊ GC (mean ± σ):  9.71% ±  3.92%

    ▂▆██▇▅▃▂▂▁                             ▁▁▁▁               ▂
  ▄▆██████████▇███▇▇▇▆▆▅▅▇▆▄▆▇▇▆▆▆▅▅▄▅▅▅▅▆██████▇▇▆▅▅▅▅▄▅▄▆▇▇ █
  20.6 ns      Histogram: log(frequency) by time      44.4 ns <

 Memory estimate: 80 bytes, allocs estimate: 1.

julia> @benchmark new_imag($q)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min  max):  0.977 ns  10.895 ns  ┊ GC (min  max): 0.00%  0.00%
 Time  (median):     2.375 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   2.397 ns ±  0.232 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

                                            █                 
  ▂▁▁▁▁▁▁▁▁▁▂▁▂▁▁▂▁▁▁▁▁▁▁▁▁▂▁▂▁▁▃▁▁▁▁▁▁▁▂▁▅▁█▁▁▇▁▁▁▁▁▁▁▂▁▃▁▃ ▂
  0.977 ns       Histogram: frequency by time        2.86 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

Note that this makes a breaking change.

Issues around conversion DCM to Quaternion

A conversion from DCM (direction cosine matrix) to Quaternion is defined here.

https://github.com/JuliaGeometry/Quaternions.jl/blob/v0.4.8/src/Quaternion.jl#L236-L256

The function qrotation has the following problems:

  • It only accepts Matrix. This should be replaced with AbstractMatrix.
  • The returned Quaternion doesn't have the norm flag true.
  • The returned quaternion might have rotation angle larger than π.

The last problem is same as JuliaGeometry/Rotations.jl#97.

Here's a script to check the returned value with MRP.

using Quaternions
using Rotations
using Plots
using GeometryBasics
plotly()

rs = rand(RotMatrix{3},10000)

qs = Quaternions.qrotation.(Matrix.(rs))

ps = QuatRotation{Float64}[]
for q in qs
    push!(ps, QuatRotation{Float64}(Quaternion(q.s,q.v1,q.v2,q.v3,true)))
end

points = Point.(Rotations.params.(MRP.(ps)))
scatter(points)
Peek.2022-02-22.17-18.mp4

Add quaternion SpecialFunctions?

With #56, we would be able to extend a number of special functions to the quaternions. Currently, via DualNumbers, this package already depends on SpecialFunctions, so it wouldn't increase the weight of this package at all. The question is, would this actually be useful?

adding conversion functionality

I am wondering if it would be appropriate to add some conversion functionality to this package, for instance converting from Quaternions to Euler angles and vice versa. For now, maybe we can just import the functionality from C or python modules such as these

Type instability in `axis`

Return type is always Vector{Float64} when angle(q) is zero or NaN, leading to a type instability.

julia> code_warntype(axis,(QuaternionF32,))
MethodInstance for Quaternions.axis(::QuaternionF32)
  from axis(q::Quaternion) in Quaternions at /home/mike/.julia/packages/Quaternions/GVtxM/src/Quaternion.jl:205
Arguments
  #self#::Core.Const(Quaternions.axis)
  q@_2::QuaternionF32
Locals
  s::Float32
  q@_4::QuaternionF32
Body::Union{Vector{Float32}, Vector{Float64}}
1 ─       (q@_4 = q@_2)
│         (q@_4 = Quaternions.normalize(q@_4))
│   %3  = Quaternions.angle(q@_4)::Float32%4  = (%3 / 2)::Float32
│         (s = Quaternions.sin(%4))
│   %6  = Quaternions.abs(s)::Float32%7  = (%6 > 0)::Bool
└──       goto #3 if not %7
2%9  = Base.getproperty(q@_4, :v1)::Float32%10 = Base.getproperty(q@_4, :v2)::Float32%11 = Base.getproperty(q@_4, :v3)::Float32%12 = Base.vect(%9, %10, %11)::Vector{Float32}%13 = (%12 / s)::Vector{Float32}
└──       return %13
3%15 = Base.vect(1.0, 0.0, 0.0)::Vector{Float64}
└──       return %15

I'd have made a PR but am not at a machine where I can do so right now. I think something like

function axis(q::Quaternion)
    s = sin(angle(q) / 2) * abs(q)
    iszero(s) ?
        [one(q.v1/s), zero(q.v2/s), zero(q.v3/s)] : 
        [q.v1/s, q.v2/s, q.v3/s]
end

will fix the instability and also allow NaN propagation. This still doesn't fix the case where only one imaginary component isinf, where we could give a reasonable answer (assuming we care to have axis defined for infinite quaternions), but that was broken under the old version too.

EDIT: Alternatively,

function axis(q::Quaternion)
    s = abs_imag(q)
    iszero(s) ?
        [one(q.v1/s), zero(q.v2/s), zero(q.v3/s)] : 
        [q.v1/s, q.v2/s, q.v3/s]
end

would be more accurate, propagate NaN on only the imaginary part, and would return a well-defined vector for the zero quaternion. If we don't want zero (or any not-convincingly unit quaternion?) to be a valid input, then I'd suggest a DomainError.

The documentation doesn't sufficiently clarify when to use Quaternion or quat

I find the documentation confusing because it has some examples that create a quaternion with Quaternion and others with quat, and does not clearly explain what the differences between them are.

After trial and error, I discovered that one difference between Quaternion and quat is that Quaternions([1,2,3]) gives an error, and quat([1,2,3]) does not.

I would like the documentation to explain more clearly when Quaternion and quat are interchangeable and when they are not, and which method is preferable.

`slerp` breaks flag for quaternion normalization

julia> q1 = Quaternion(1.0, 0.0, 0.0, 0.0, true)
Quaternion{Float64}(1.0, 0.0, 0.0, 0.0, true)

julia> q2 = Quaternion(0.0, 1.0, 0.0, 0.0, true)
Quaternion{Float64}(0.0, 1.0, 0.0, 0.0, true)

julia> slerp(q1,q2,0.3)
Quaternion{Float64}(0.8910065241883678, 0.45399049973954675, 0.0, 0.0, false)

The flag false should be true here because slerp keeps norm of quaternion if the input quaternions are unit quaternions.

inv not working on Quaternion{Num}

The inv function doesn't work for quaternions of Symbolics.Num type:

julia> using Quaternions, Symbolics

julia> q = quat(Num.([0,0,0,1])...)
Quaternion{Num}(0, 0, 0, 1)

julia> inv(q)
ERROR: MethodError: /(::Quaternion{Num}, ::Num) is ambiguous. Candidates:
  /(a::Number, b::Num) in Symbolics at /home/j/.julia/packages/SymbolicUtils/qulQp/src/methods.jl:71
  /(q::Quaternion, x::Real) in Quaternions at /home/j/.julia/packages/Quaternions/kqEsP/src/Quaternion.jl:123
Possible fix, define
  /(::Quaternion, ::Num)
Stacktrace:
 [1] inv(q::Quaternion{Num})
   @ Quaternions ~/.julia/packages/Quaternions/kqEsP/src/Quaternion.jl:143
 [2] top-level scope
   @ REPL[3]:1

Should `Complex` be convertible to `Quaternion`?

We already have constructor and convert method from Complex to Quaternion.

julia> Quaternion(Complex(1,2))
Quaternion{Int64}(1, 2, 0, 0, false)

julia> convert(Quaternion, Complex(1,2))
Quaternion{Int64}(1, 2, 0, 0, false)

However, we still missing the following methods + and *.

julia> Quaternion(1,2,3,4) + Complex(1,2)
ERROR: MethodError: no method matching imag(::Complex{Int64})
You may have intended to import Base.imag
Closest candidates are:
  imag(::Quaternion) at ~/.julia/dev/Quaternions/src/Quaternion.jl:44
  imag(::Octonion) at ~/.julia/dev/Quaternions/src/Octonion.jl:48
Stacktrace:
 [1] convert(#unused#::Type{Quaternion{Int64}}, z::Complex{Int64})
   @ Quaternions ~/.julia/dev/Quaternions/src/Quaternion.jl:21
 [2] _promote
   @ ./promotion.jl:327 [inlined]
 [3] promote
   @ ./promotion.jl:350 [inlined]
 [4] +(x::Quaternion{Int64}, y::Complex{Int64})
   @ Base ./promotion.jl:379
 [5] top-level scope
   @ REPL[23]:1

julia> Quaternion(1,2,3,4) * Complex(1,2)
ERROR: MethodError: no method matching imag(::Complex{Int64})
You may have intended to import Base.imag
Closest candidates are:
  imag(::Quaternion) at ~/.julia/dev/Quaternions/src/Quaternion.jl:44
  imag(::Octonion) at ~/.julia/dev/Quaternions/src/Octonion.jl:48
Stacktrace:
 [1] convert(#unused#::Type{Quaternion{Int64}}, z::Complex{Int64})
   @ Quaternions ~/.julia/dev/Quaternions/src/Quaternion.jl:21
 [2] _promote
   @ ./promotion.jl:327 [inlined]
 [3] promote
   @ ./promotion.jl:350 [inlined]
 [4] *(x::Quaternion{Int64}, y::Complex{Int64})
   @ Base ./promotion.jl:380
 [5] top-level scope
   @ REPL[24]:1

I think we have the following choices to solve this problem.

  • Remove the constructor and convert method
    • There is no natural embedding ℂ ⊆ ℍ because it depends on the choice of the basis.
    • This will make a breaking change.
  • Add type promotions
    • This will not make a breaking change.

I don't know the practical benefit of converting from Complex to Quaternion, so I prefer the first.

Related comment: #29 (comment)

Extend all analytic functions in base for quaternions

All complex analytic functions f in Base can be extended to the quaternions with the following function:

using Quaternions

function quat_analytic(f, q::Quaternion)
    a = Quaternions.abs_imag(q)
    z = complex(q.s, a)
    fz = f(z)
    wr, wi = reim(fz)
    scale = wi / a
    if a > 0
        return Quaternion(wr, scale * q.v1, scale * q.v2, scale * q.v3)
    else  # quaternion may be real or complex
        return Quaternion(wr, oftype(scale, wi), zero(scale), zero(scale))
    end
end

The function is adapted from Theorem 5 of doi.org/10.1017/S0305004100055638. It can also be derived a completely different way by mapping a quaternion to a 2x2 skew-Hermitian matrix whose product preserves the Hamilton product, extending the analytic function to a matrix function, and then mapping back to the equivalent quaternions.

This function gives equivalent results to the current functions implementations here, but it's faster than all but log:

julia> using BenchmarkTools

julia> q = quatrand()
Quaternion{Float64}(-1.76527498999994, -0.9985320755133996, -0.606774830599748, 1.0149711723420227, false)

julia> @btime exp($q)
  31.384 ns (0 allocations: 0 bytes)
Quaternion{Float64}(0.003950512836446494, -0.11038429890473422, -0.0670768780605819, 0.11220158472113788, false)

julia> @btime $quat_analytic(exp, $q)
  28.991 ns (0 allocations: 0 bytes)
Quaternion{Float64}(0.003950512836446494, -0.11038429890473421, -0.06707687806058188, 0.11220158472113786, false)

julia> @btime log($q)
  74.247 ns (0 allocations: 0 bytes)
Quaternion{Float64}(0.8534278686043146, -1.5624466956559513, -0.9494470456448084, 1.5881696675557215, false)

julia> @btime $quat_analytic(log, $q)
  81.745 ns (0 allocations: 0 bytes)
Quaternion{Float64}(0.8534278686043146, -1.5624466956559513, -0.9494470456448083, 1.5881696675557215, false)

julia> @btime sqrt($q)
  131.847 ns (0 allocations: 0 bytes)
Quaternion{Float64}(0.539632108658829, -0.9251970550780371, -0.5622115704973448, 0.9404288181299799, false)

julia> @btime $quat_analytic(sqrt, $q)
  30.934 ns (0 allocations: 0 bytes)
Quaternion{Float64}(0.5396321086588289, -0.9251970550780371, -0.5622115704973447, 0.9404288181299798, false)

julia> @btime sin($q)
  158.434 ns (0 allocations: 0 bytes)
Quaternion{Float64}(-2.410402900676647, 0.2797836189575866, 0.1700152275131357, -0.28438977044324726, false)

julia> @btime $quat_analytic(sin, $q)
  48.684 ns (0 allocations: 0 bytes)
Quaternion{Float64}(-2.410402900676648, 0.2797836189575863, 0.17001522751313533, -0.2843897704432474, false)

julia> quat_analytic(tan, q)  sin(q) / cos(q)  cos(q) \ sin(q)
true

julia> quat_analytic(sin, quat_analytic(asin, q))  q
true

julia> quat_analytic(cosh, quat_analytic(acosh, q))  q
true

julia> quat_analytic(exp10, q)  10^q
true

julia> quat_analytic(inv, q)  inv(q)
true

Drop inefficient functions for rotation?

The following functions return Array which is less efficient than StaticArrays.StaticArray.

  • rotationmatrix
  • rotationmatrix_normalized
  • axis

These can be replaced with efficient functions in Rotations.jl package. Adding warning messages to replace these functions with Rotations.jl package, and removing them in the next breaking release would be better, I guess.

Both Quaternions.jl and Rotations.jl packages are maintained under JuliaGeometry organization, so I think this change will not be problematic.

julia> using Quaternions, Rotations

julia> q = nquatrand()
QuaternionF64(0.09045921662676236, 0.9292679982955513, -0.0983890390919598, 0.34438018882858595, true)

julia> axis(q)
3-element Vector{Float64}:
  0.9330935318067537
 -0.09879407893716233
  0.34579790466015914

julia> rotation_axis(QuatRotation(q))
3-element StaticArrays.SVector{3, Float64} with indices SOneTo(3):
  0.9330935318067537
 -0.09879407893716233
  0.3457979046601592

julia> rotationmatrix(q)
3×3 Matrix{Float64}:
  0.743444  -0.245164   0.622243
 -0.120555  -0.964273  -0.235888
  0.657843   0.100355  -0.746439

julia> QuatRotation(q)
3×3 QuatRotation{Float64} with indices SOneTo(3)×SOneTo(3)(QuaternionF64(0.0904592, 0.929268, -0.098389, 0.34438, true)):
  0.743444  -0.245164   0.622243
 -0.120555  -0.964273  -0.235888
  0.657843   0.100355  -0.746439

Drop normalize for quaternions

LinearAlgebra only implements 2 normalize methods:

julia> using LinearAlgebra

julia> methods(normalize)
# 2 methods for generic function "normalize":
[1] normalize(a::AbstractArray) in LinearAlgebra at /Applications/Julia-1.7.app/Contents/Resources/julia/share/julia/stdlib/v1.7/LinearAlgebra/src/generic.jl:1788
[2] normalize(a::AbstractArray, p::Real) in LinearAlgebra at /Applications/Julia-1.7.app/Contents/Resources/julia/share/julia/stdlib/v1.7/LinearAlgebra/src/generic.jl:1788

Notably, there is no normalize(::Real) or normalize(::Complex). For good reason, as for numbers, this is the role of the sign function, which is defined generically for all numbers:
https://github.com/JuliaLang/julia/blob/bb2d8630f3aeb99c38c659a35ee9aa57bb71012a/base/number.jl#L161
We should deprecate normalize then and refer users to the sign function.

A difference between our normalize and sign functions, however, is how they treat zero quaternions:

julia> normalize(q)
QuaternionF64(0.1015814137657451, 0.8417214120379644, 0.5262271589168154, 0.0653548629598791, true)

julia> sign(q)
QuaternionF64(0.1015814137657451, 0.8417214120379644, 0.5262271589168154, 0.0653548629598791, false)

julia> normalize(zero(q))
QuaternionF64(NaN, NaN, NaN, NaN, true)

julia> sign(zero(q))
QuaternionF64(0.0, 0.0, 0.0, 0.0, false)

There are at least 4 different ways one could handle normalize for zero quaternions:

  • return a 0 quaternion (sign)
  • return NaNs (current behavior)
  • return a random unit quaternion
  • return a deterministic unit quaternion (e.g. Quaternion(1))

I don't like the last 2, as they are arbitrary. The question is whether the current behavior is a bug or the desired behavior. If a bug, then we are free to define Base.@deprecate normalize(x) sign(x). If desired, then we should define a different function for normalizing instead of overriding LinearAlgebra.normalize and deprecate normalize.

Release Real type constraint on Quaternion

There are several variations on quaternions that uses non-real numbers with the quaternion multiplication operations, for which most/all of the current defaults can be kept. The two I'm thinking of are biquaternions and dual quaternions.

Currently we implement dual quaternions as 2 quaternions, one being the "primal" and one being corresponding "infinitesimal", but equivalently, it could be implemented as 4 Dual numbers, tying together the individual primals with infinitesimals. If this latter choice was made, DualQuaternion{T} could just be an alias for Quaternion{Dual{T}}, and we may we able to eliminate any special functionality for dual quaternions.

The steps would be:

  1. Remove the Real type constraint for the T in Quaternion{T}.
  2. Make any necessary changes to make the Quaternion implementations more general
  3. Replace DualQuaternion's struct with a const, keeping the same tests.

These are breaking changes. Note that biquaternions would probably not work so long as we embed the complex numbers in the quaternions #62.

Relates #16 and #8

[PackageEvaluator.jl] Your package Quaternions may have a testing issue.

This issue is being filed by a script, but if you reply, I will see it.

PackageEvaluator.jl is a script that runs nightly. It attempts to load all Julia packages and run their test (if available) on both the stable version of Julia (0.2) and the nightly build of the unstable version (0.3).

The results of this script are used to generate a package listing enhanced with testing results.

The status of this package, Quaternions, on...

  • Julia 0.2 is 'No tests, but package loads.' PackageEvaluator.jl
  • Julia 0.3 is 'No tests, but package loads.' PackageEvaluator.jl

'No tests, but package loads.' can be due to their being no tests (you should write some if you can!) but can also be due to PackageEvaluator not being able to find your tests. Consider adding a test/runtests.jl file.

'Package doesn't load.' is the worst-case scenario. Sometimes this arises because your package doesn't have BinDeps support, or needs something that can't be installed with BinDeps. If this is the case for your package, please file an issue and an exception can be made so your package will not be tested.

This automatically filed issue is a one-off message. Starting soon, issues will only be filed when the testing status of your package changes in a negative direction (gets worse). If you'd like to opt-out of these status-change messages, reply to this message.

Add ChainRules rules for certain functions?

It's becoming common in the ecosystem to add ChainRules rules for functions that use features that are problematic or slow for certain AD packages, like mutation, loops, or control flow. Maybe we should do this to, at least to add projections and such. JuliaDiff/ChainRules.jl#540 should probably be finished first.

We already have ChainRulesCore as an indirect dependency via DualNumbers and then SpecialFunctions, so this would not make this package heavier.

Bug in `log()` for negative reals?

I think there is a bug in Base.log(q::Quaternion) if q is a negative real.

For example:

julia> q = quat(-1.0)
QuaternionF64(-1.0, 0.0, 0.0, 0.0, true)

julia> exp(log(q)) == q
false

note that this is not due to floating point (im-)precision:

julia> exp(log(q))
QuaternionF64(1.0, 0.0, 0.0, 0.0, true)

julia> log(q)
QuaternionF64(0.0, 0.0, 0.0, 0.0, false)

Related answer on math.stackexchange: https://math.stackexchange.com/a/3750503

Note in particular:

For negative reals, the imaginary component of the branches has the form (2n+1)πu^. Thus (log(k),0,0,0) is not a valid value of log(k,0,0,0).

I think a fix would be to choose the branch of the logarithm that corresponds to the logarithm on complex numbers, again quoting above math.stackexchange answer:

But, for example, if you choose u^=(1,0,0) then you get log(−2,0,0,0)=(log(|−2|),(2n+1)π,0,0). This is exactly the logarithm on C.

Algebraic quaternions vs geometric quaternions

Hi @SimonDanisch, we at Roames (myself, @awbsmith and @c42f) have been using and thinking about quaternions, and there seems to be a few conceptual blockers to e.g. using * as in you suggest in #3.

Primarily, a quaternion is a algebraic object, Quaternion{T} <: Number, much like Complex{T} <: Number. They form a ring, and algebraic operations + and * have a single unambiguous meaning. However, some quaternions, especially normalized quaternions, are useful for geometry, and it sure would be useful to implement rotations of 3-vectors.

I'm proposing that we break up the structure a little bit, so that Quaternion is a pure algebraic number and UnitQuaternion or RotationQuaternion or similar is an object that either inherits from AbstractQuaternion or else is a simple struct containing a Quaternion.

As an exaple, we would want

Quaternion(1.0,2.0,3.0,4.0) * Vec(1.0,2.0,3.0) <: Vec{3,Quaternion{Float64}} # elementwise multiplication with standard multiplication rules

(you could replace Vec with Vector, Point, any FixedVector, etc).

and

RotationQuaternion(cos(t), sin(t), 0.0, 0.0) * Vec(1.0,0.0,0.0) == Vec(cos(t), sin(t), 0.0) # or whatever it is for this rotation...

For a simple gist, see here.

This would help us in Rotations.jl, plus clean up the whole interface for people who want to do either just algebra or just geometry. In fact, there is some small argument to say the algebraic part belongs outside of JuliaGeometry, but the simplest thing is to make a separation.

in exp() function, eps yields an error when used on an integer

Using the quaternion package and instantiating quaternions of the shape
a = Quaternion(1,1,1,1)
the command
exp(a)
returned an error of the function eps : "no method matching eps(::Int64)". Maybe an automatic convert of the coordinates of the quaternion to Float could solve the problem?
In the meantime, I just changed my initialization for:
a = Quaternion(1.0,1.0,1.0,1.0) which lifted the error.

sqrt( DualQuaterion ) error

v0.5-dev

julia> adualquat = DualQuaternion( Quaternion(1,3,5,7), Quaternion(8,6,4,2) )
1 + 3im + 5jm + 7km + ( 8 + 6im + 4jm + 2km )du

julia> sqrt(adualquat)
ERROR: type Dual has no field re
in /(::DualQuaternion{Int64}, ::Dual{Float64}) at ./none:1
in normalizea(::DualQuaternion{Int64}) at ./none:7
in log(::DualQuaternion{Int64}) at ./none:2
in sqrt(::DualQuaternion{Int64}) at ./none:2
in eval(::Module, ::Any) at ./boot.jl:243

New docs pages

I propose in addition to the dual quaternions docs page introduced in #92 we add 2 more docs pages:

  • Representing rotations with unit quaternions
  • Extending complex analytic functions to the quaternions

The first would introduce how to use Quaternions directly to transform a point represented as a pure quaternion and also showcase slerp.

The second would give an informal derivation of the method we use for extending complex analytic functions to the quaternions.

Release v0.7?

Please, please consider releasing v0.7 very soon. The currently released state of the package can be viewed as nearly-broken. This is due to the many deprecation warnings. For instance, see JuliaLinearAlgebra/GenericLinearAlgebra.jl#91, whose CI was failing, because it couldn't finish its test suite within 5 hours. Usually, it takes just a handful of minutes. Essentially all of the 5 hours were spent on computing the QR decomposition of a 50x50 quaternion matrix. There, dot products of quaternion vectors are computed, and in every product of quaternions the package itself uses a quaternion constructor that is deprecated, so it's not even the "fault" of the downstream package. Releasing a new minor version should do no harm, because all downstream packages are stuck at v0.6.x by their Project.toml. Anyone who wants to upgrade will run CI and fix all issues that appear, so there shouldn't be any breakage in the ecosystem.

implement `rotate(q::Quaternion, v::Vector)`?

When using a quaternion q as rotations in 3D it's common to want to rotate a Vector v. As far as I can tell the way to do that right now would be something like imag(q*Quaternion(v)*conj(q)). Would you be interested in a rotate(q::Quaternion, v::Vector) method or something that applies the quaternion rotation operator and returns the rotated vector?

Grassmann.jl vs Quaternions.jl

Grassmann.jl has its own dedicated Quaternion type for a while now, I've been focused on smoothing out various general things related to this package. Now things have stabilized and smoothed out again, I can look into further standardizing the quaternion interface. Grassmann.jl supports quaternions in a much more general way, but it the definitions are such that k = -v13

julia> using Grassmann; basis"3"
(⟨×××⟩, v, v₁, v₂, v₃, v₁₂, v₁₃, v₂₃, v₁₂₃)

julia> s,i,j,k = v,v12,-v13,v23
(v, v₁₂, -1v₁₃, v₂₃)

julia> i*j == k
true

julia> quatvalues(s+2i+3j+4k)
4-element StaticVectors.Values{4, Int64} with indices SOneTo(4):
 1
 2
 3
 4

julia> v1  exp*i/2)
-1.0v₁ + 1.2246467991473532e-16v₂ + 0.0v₃

julia> v1  exp*i/4)
2.220446049250313e-16v₁ + 1.0v₂ + 0.0v₃

julia> v1  exp*i/8)
0.7071067811865475v₁ + 0.7071067811865476v₂ + 0.0v₃

julia> (v1+v2+v3)  exp*i/8+j*π/3)
-0.1577202379738252v₁ + 1.6085211528719414v₂ - 0.6227230743251773v₃

If anybody has anything to say about this, let me know.

Adding logo to the document

I'm planning to add a logo based on Cayley Q8 quaternion multiplication graph.

image

  • The left one ignores the -1 vertex. (We can also think of the vertex of -1 as the infinity point)
  • The middle one draws -1 vertex as a circle
  • The right one is a "complete" graph

I prefer the first one because simplicity is important in logo design.

@sethaxen Do you have any thoughts on this?

`quat([1,2,3,4])` should return `Vector{Quaternion{Int}}`

The function quat should be similar to complex, but the behavior is different.

julia> complex(1)
1 + 0im

julia> quat(1)
Quaternion{Int64}(1, 0, 0, 0, true)

julia> complex([1,2,3,4])
4-element Vector{Complex{Int64}}:
 1 + 0im
 2 + 0im
 3 + 0im
 4 + 0im

julia> quat([1,2,3,4])
Quaternion{Int64}(0, 1, 2, 3, false)

I think quat([1,2,3,4]) should be [Quaternion{Int64}(1,0,0,0), Quaternion{Int64}(2,0,0,0), Quaternion{Int64}(3,0,0,0), Quaternion{Int64}(4,0,0,0)].

Reexport Base.imag?

Currently, Base.imag is not imported and re-exported, which causes shadowing errors when intermixing Complex and Quaternion numbers in arithmetic computations. Since im^2 == -1 in both algebras, the algebras themselves are not in conflict, and the analogous mixed-algebra computations should be similar to Real * Complex in Base. At least on paper, I don't foresee any problems in allowing Quaternions.imag to extend Base.imag.

Use `𝒊, 𝒋, 𝒌` as symbols of the basis of `Quaternion`?

We can define symbols for basis of Quaternion like im.

const 𝒊 = Quaternion(false,true,false,false,true)  # type \bii in REPL
const 𝒋 = Quaternion(false,false,true,false,true)  # type \bij in REPL
const 𝒌 = Quaternion(false,false,false,true,true)  # type \bik in REPL

Additionally, we can add Base.show(io::IO, q::Quaternion) method to show the quaternion in REPL.

Is this sounds great? If so, I'll make a PR.

Rename `imag_part` to `imag_quat`?

Since Octionion has been split into Octonions.jl, both Quaternions.jl and Octonions.jl exports imag_part. This causes the following problem:

julia> using Quaternions, Octonions

julia> imag_part
WARNING: both Octonions and Quaternions export "imag_part"; uses of it in module Main must be qualified
ERROR: UndefVarError: imag_part not defined

Octonions.jl has a dependency on Quaternions.jl, so we can avoid this issue by adding the method Quaternions.imag_part(::Octonion). However, this approach is not good because the dependency on Quaternions.jl should be removed just like #62.

Therefore, I think the best approach here is renaming imag_part to imag_quat (and imag_octo).

Implementing matrix functions with quaternion eltypes

Quaternions can be represented as $2 \times 2$ complex matrices whose matrix product preserves the Hamilton product. This means that one can map an $n \times n$ quaternion matrix to a $2n \times 2n$ complex matrix with $2 \times 2$ blocks from quaternions to perform any complicated operations that only consist of matrix addition and multiplication.

Matrix functions such as the matrix exponential satisfy this property. We could generically support these matrix functions for quaternion eltypes by explicitly generating these complex matrices, dispatching to the complex matrix functions, and then mapping back to a matrix of quaternions.

This would generalize #46 and extend #56 to matrices.

Add constructor like `Complex{Float64}`

The following MethodError should be fixed.

julia> using Quaternions

julia> Quaternion{Float64}(1)
QuaternionF64(1.0, 0.0, 0.0, 0.0, true)

julia> Quaternion{Float64}(1,2,3,4)
ERROR: MethodError: no method matching QuaternionF64(::Int64, ::Int64, ::Int64, ::Int64)
Closest candidates are:
  Quaternion{T}(::Any, ::Any, ::Any, ::Any, ::Any) where T<:Real at ~/.julia/dev/Quaternions/src/Quaternion.jl:2
  Quaternion{T}(::Real) where T<:Real at ~/.julia/dev/Quaternions/src/Quaternion.jl:13
  (::Type{T})(::T) where T<:Number at ~/julia/julia-1.7.1/share/julia/base/boot.jl:770
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

julia> Complex{Float64}(1)
1.0 + 0.0im

julia> Complex{Float64}(1,2)
1.0 + 2.0im

`hash(::Quaternion, ::UInt)` should be defined

The following behavior is not consistent, and can be fixed by adding hash(::Quaternion, ::UInt) method.

julia> using Quaternions

julia> isequal(1, complex(1))
true

julia> 1 |> hash
0x5bca7c69b794f8ce

julia> complex(1) |> hash
0x5bca7c69b794f8ce

julia> isequal(1, quat(1))  # isequal(a,b) == true should imply `hash(a) == hash(b)`
true

julia> quat(1) |> hash
0x37cb992a8d339608

julia> unique(Complex[complex(2), complex(big"2")])
1-element Vector{Complex}:
 2 + 0im

julia> unique(Quaternion[quat(2), quat(big"2")])
2-element Vector{Quaternion}:
  Quaternion{Int64}(2, 0, 0, 0)
 Quaternion{BigInt}(2, 0, 0, 0)

x-ref: #121 (comment)

Lipschitz and Hurwitz quaternions

My understanding is that the current implementation of Quaternions with Int inputs is aligned with the idea of a Lipschitz quaternion.

julia> Quaternion(1,2,3,4)
Quaternion{Int64}(1, 2, 3, 4, false)

On the other hand, in many cases one wants a Hurwitz quaternion. Even though I don't have a clear idea how a Hurwitz quaternion would be integrated with this package, I thought perhaps an issue about it would at least provide a place to discuss the topic.

Docstring pollution

It appears that some Base functions extended to ::Quaternion arguments contribute their own docstrings that are near-verbatim repeats of the existing versions from Base. They don't provide any new information but they are printed via >help? anyway, cluttering the output. It doesn't seem sustainable for every package that adds a new type to document semantically-unchanged functions just for the sake of saying that they do exactly what the docstrings already said they do. If anything, this makes them more confusing (in addition to the clutter) because the user may try to spot the difference.

Other packages don't seem to take this approach. Base doesn't feel the need to document that sign works on Rational specifically, nor does DualNumbers feel the need to do the same for Dual. For that matter, Quaternions doesn't (yet) feel the need to do this for the majority of its extended functions (arithmetic, ==, exp, sin, etc.).

Offending functions at the present time appear to be:

  • real -- only adds a "See also: imag_part, quat" to the real(::Quaternion) method
  • conj
  • sign -- we don't even provide a custom implementation
  • inv -- proposed by #133

At least the real docstring adds literally something, if only references. I can't see why quat is relevant there, and it's already linked from Quaternion which seems a more reasonable place. Discoverability of imag_part is important (arguably, field access is fine) but I would argue imag is a much better place to find it than real. That said, I wouldn't be excited to clutter that docstring either. I despise "not implemented" errors, but this is more of a "the operation is ill-defined for this type" error so I could be convinced to do something like

Base.imag(::Quaternion) = throw(ArgumentError("`Base.imag(::Quaternion)` is not defined. See Quaternions.imag_part`."))

to help discoverability. I vaguely recall there were "error hints" being considered for Base at some point (which seems like an even better solution), but I don't know if those ever became a thing.

It's unfortunate that round does appear to need ::Quaternion-specific documentation for the imaginary-part rounding modes, but I don't see a way around that. The only thing I would have considered (breaking change now, technically, although perhaps not in any actual use) would be that the imaginary parts all share a rounding mode (in which case we would have followed the round(::Complex,...) semantics). It's not easy to imagine that many people have needs to round imaginary parts differently from each other, so I wouldn't have hated making users roll-their-own for that level of control. It doesn't benefit from being part of Base.round anyway, because no other version accepts 4 RoundingModes. But this is a matter for a separate issue and not one that I feel strongly about since it'd be breaking to remove that functionality.

type of element unparametrized and one() not implemented

I was just stumbling across something interesting. In our code we have a function that determines the element type of a vector like

number_eltype(::AbstractArray{T}) where {T<:Number} = T

and we use this to allocate new variables like

one(number_eltype(pi))

Now if I take a vector of Quaternions (and floats internally)

p = Quaternion[1.0 + 0im, 0.0, 0.0]

now

julia> number_eltype(p)
Quaternion

just returns Quaternion not Quaternion{Float64}

so we get

one(number_eltype(p))
Quaternion{Int64}(1, 0, 0, 0, true)

Is there a possibility to get the T in the first function to be parametrised?
The main problem seems to be that p is a Vector{Quaternion} and not a `Vector{Quaternion{Float64}}' ?

I also noticed that maybe one(p) would be something nice to have for vector os Quaternions.

Here's the MWE to test

using Quaternions
number_eltype(::AbstractArray{T}) where {T<:Number} = T
p = Quaternion[1.0 + 0im, 0.0, 0.0]
number_eltype(p)
one(number_eltype(p))
typeof(p)

New Minor Version

Thank you for fixing the type piracy issue in PR #53.
However, I was relying on that bug somehow in my package because I was using Rotations.jl, and the normalize function.
This was very confusing for me to figure out because I don't have a direct dependency on Quaternions.jl, just Rotations.jl, and the function that broke wasn't using any rotations or quaternions.

I think it could be helpful to update the minor version for such a big bug fix, so Rotations.jl will also hopefully release a new version with the fix.

Not full support Quaternion{BigFloat}

There is omitted comparison operations

julia> q=Quaternion(BigFloat(0),0,0,0);
julia> zero(q)==zero(q)
false

Could you to add explicit specialized functions for BigFloat?

DualQuaternion construction from Duals

It looks like the type DualQuaternion may be constructed from four DualNumbers.Dual values. However, when I try this, I get an error.

It appears that (in my version of DualNumbers, anyway) the field names are value and epsilon. DualQuaternion is trying to access a field called re.

New :patch release?

Would you be willing to tag a new patch version to bring in #13? I'd like to be able to point to a tagged version rather than check out the master branch of Quaternions every time.

Do we really need normalized-flag in `Quaternion`?

The problems

The following are what I am concerned about.

  • Someone just needs quaternion operations, without the flag.
  • Performance disadvantage
  • The flag is not well-documented, and some operations don’t work properly with the flag.

Someone just needs quaternion operations, without the flag.

See comments in Makie.jl for example.

Performance disadvantage

The following MyQuaternion is a quaternion without the flag. The benchmark shows -5.49% => improvement (5.00% tolerance).

julia> using Quaternions, BenchmarkTools

julia> struct MyQuaternion{T<:Real} <: Number
           s::T
           v1::T
           v2::T
           v3::T
       end

julia> Base.:*(q::MyQuaternion, w::MyQuaternion) = MyQuaternion(q.s * w.s - q.v1 * w.v1 - q.v2 * w.v2 - q.v3 * w.v3,
                                                      q.s * w.v1 + q.v1 * w.s + q.v2 * w.v3 - q.v3 * w.v2,
                                                      q.s * w.v2 - q.v1 * w.v3 + q.v2 * w.s + q.v3 * w.v1,
                                                      q.s * w.v3 + q.v1 * w.v2 - q.v2 * w.v1 + q.v3 * w.s)

julia> mq = MyQuaternion(1.0, 2.0, 3.0, 4.0)
MyQuaternion{Float64}(1.0, 2.0, 3.0, 4.0)

julia> q = Quaternion(1.0, 2.0, 3.0, 4.0)
Quaternion{Float64}(1.0, 2.0, 3.0, 4.0, false)

julia> a = @benchmark $mq*$mq
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min  max):  2.793 ns  31.009 ns  ┊ GC (min  max): 0.00%  0.00%
 Time  (median):     4.260 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   4.229 ns ±  0.486 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

                  ▁           ▃▃ ▆ ▄       ▆ █ █ ▅       ▂ ▁ ▂
  ▅▁▇▁▆▁▁▁▁▁▁▁▅▁█▁█▁▇▁▁▁▁▁▃▁▇▁██▁█▁█▁▁▁▁▁█▁█▁█▁█▁█▁▁▁▁▁█▁█▁█ █
  2.79 ns      Histogram: log(frequency) by time     4.75 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> b = @benchmark $q*$q
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min  max):  3.213 ns  18.089 ns  ┊ GC (min  max): 0.00%  0.00%
 Time  (median):     4.330 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   4.474 ns ±  0.401 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

                                 ▅ █           ▂              
  ▂▁▂▁▂▁▂▁▁▁▁▁▁▁▂▁▂▁▂▁▂▁▁▁▁▁▂▁▄▄▁█▁█▁▅▁▁▁▁▁▃▁▇▁█▁▇▁▃▁▁▁▁▁▂▁▂ ▃
  3.21 ns        Histogram: frequency by time        5.17 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> judge(mean(a),mean(b))
BenchmarkTools.TrialJudgement: 
  time:   -5.49% => improvement (5.00% tolerance)
  memory: +0.00% => invariant (1.00% tolerance)

The flag is not well-documented, and some operations don’t work properly with the flag.

See #36, #39, #51 and #59.

How to fix these problems

  • If you need unit quaternions for rotations, I think Rotations.QuatRotation would be helpful.
    • Rotations.jl package have a dependency on Quaternions.jl, and conversion Quaterion <-> QuatRotation is supported.
  • If you do not prefer the Rotations package, adding a new type Quaternions.UnitQuaternon seems better, I guess.
    • QuatRotaions is a rotation parametrization with Quaternion, but it is regarded as AbstractMatrix. i.e. the antipodal quaternions are equal. (JuliaGeometry/Rotations.jl#171)

Documentation?

Some documentation comments would be a really nice addition to this package.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.