Skip to content

Commit

Permalink
Remove norm field and update normalization functions (#108)
Browse files Browse the repository at this point in the history
* Remove norm field

* Update tests

* Replace normalize with sign

* Rename normalizea to sign_abs

* Make sign_abs type-stable

* Document sign function

* Remove normalizeq

* Add sign and sign_abs to docs

* Increase version number

* Remove sign_abs

* Mark test non-broken

* Remove sign_abs actually

* Add missing period

* Remove argq

* Apply suggestions from code review

Co-authored-by: Yuto Horikawa <hyrodium@gmail.com>

* Remove depwarn

* Remove old method

* Fix docstring

* Make doctest less like complex

Co-authored-by: Yuto Horikawa <hyrodium@gmail.com>
  • Loading branch information
sethaxen and hyrodium authored Dec 1, 2022
1 parent b04badb commit 9c62d64
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 178 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Quaternions"
uuid = "94ee1d12-ae83-5a48-8b1c-48b8ff168ae0"
version = "0.6.1"
version = "0.7.0-DEV"

[deps]
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand Down
4 changes: 4 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ imag_part
conj
```

```@docs
sign
```

```@docs
slerp
```
Expand Down
126 changes: 44 additions & 82 deletions src/Quaternion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ struct Quaternion{T<:Real} <: Number
v1::T
v2::T
v3::T
norm::Bool
end

const QuaternionF16 = Quaternion{Float16}
Expand All @@ -25,15 +24,12 @@ function Quaternion{T}(x::Complex) where {T<:Real}
Base.depwarn("`Complex`-`Quaternion` compatibility is deprecated and will be removed in the next breaking release (v0.7.0).", :Quaternion)
Quaternion(convert(Complex{T}, x))
end
Quaternion{T}(q::Quaternion) where {T<:Real} = Quaternion{T}(q.s, q.v1, q.v2, q.v3, q.norm)
function Quaternion(s::Real, v1::Real, v2::Real, v3::Real, n::Bool = false)
Base.depwarn("The `norm` field is deprecated and will be removed in the next breaking release (v0.7.0).", :Quaternion)
Quaternion(promote(s, v1, v2, v3)..., n)
end
Quaternion(x::Real) = Quaternion(x, zero(x), zero(x), zero(x), abs(x) == one(x))
Quaternion{T}(q::Quaternion) where {T<:Real} = Quaternion{T}(q.s, q.v1, q.v2, q.v3)
Quaternion(s::Real, v1::Real, v2::Real, v3::Real) = Quaternion(promote(s, v1, v2, v3)...)
Quaternion(x::Real) = Quaternion(x, zero(x), zero(x), zero(x))
function Quaternion(z::Complex)
Base.depwarn("`Complex`-`Quaternion` compatibility is deprecated and will be removed in the next breaking release (v0.7.0).", :Quaternion)
Quaternion(z.re, z.im, zero(z.re), zero(z.re), abs(z) == one(z.re))
Quaternion(z.re, z.im, zero(z.re), zero(z.re))
end
function Quaternion(s::Real, a::AbstractVector)
Base.depwarn("`Quaternion(s::Real, a::AbstractVector)` is deprecated and will be removed in the next breaking release (v0.7.0). Please use `Quaternion(s, a[1], a[2], a[3])` instead.", :Quaternion)
Expand All @@ -51,15 +47,6 @@ function Base.promote_rule(::Type{Quaternion{T}}, ::Type{Complex{S}}) where {T <
end
Base.promote_rule(::Type{Quaternion{T}}, ::Type{Quaternion{S}}) where {T <: Real, S <: Real} = Quaternion{promote_type(T, S)}

function Base.getproperty(q::Quaternion, s::Symbol)
if s === :norm
Base.depwarn("The `norm` field is deprecated and will be removed in the next breaking release (v0.7.0).", :Quaternion)
getfield(q,:norm)
else
getfield(q,s)
end
end

"""
quat(r, [i, j, k])
Expand All @@ -68,19 +55,18 @@ Convert real numbers or arrays to quaternion. `i, j, k` defaults to zero.
# Examples
```jldoctest
julia> quat(7)
Quaternion{Int64}(7, 0, 0, 0, false)
Quaternion{Int64}(7, 0, 0, 0)
julia> quat(1.0, 2, 3, 4)
QuaternionF64(1.0, 2.0, 3.0, 4.0, false)
QuaternionF64(1.0, 2.0, 3.0, 4.0)
julia> quat([1, 2, 3]) # This output will be changed in the next breaking release for consistency. (#94)
Quaternion{Int64}(0, 1, 2, 3, false)
Quaternion{Int64}(0, 1, 2, 3)
```
"""
quat

quat(p, v1, v2, v3) = Quaternion(p, v1, v2, v3)
quat(p, v1, v2, v3, n) = Quaternion(p, v1, v2, v3, n)
quat(x) = Quaternion(x)
quat(s, a) = Quaternion(s, a)

Expand Down Expand Up @@ -116,7 +102,7 @@ Base.real(q::Quaternion) = q.s

"""
real(A::AbstractArray{<:Quaternion})
Return an array containing the real part of each quaternion in `A`.
# Examples
Expand Down Expand Up @@ -158,53 +144,40 @@ Compute the quaternion conjugate of a quaternion `q`.
# Examples
```jldoctest
julia> conj(Quaternion(1,2,3,4))
Quaternion{Int64}(1, -2, -3, -4, false)
Quaternion{Int64}(1, -2, -3, -4)
```
"""
Base.conj(q::Quaternion) = Quaternion(q.s, -q.v1, -q.v2, -q.v3, q.norm)
Base.conj(q::Quaternion) = Quaternion(q.s, -q.v1, -q.v2, -q.v3)
Base.abs(q::Quaternion) = sqrt(abs2(q))
Base.float(q::Quaternion{T}) where T = convert(Quaternion{float(T)}, q)
abs_imag(q::Quaternion) = sqrt(q.v2 * q.v2 + (q.v1 * q.v1 + q.v3 * q.v3)) # ordered to match abs2
Base.abs2(q::Quaternion) = (q.s * q.s + q.v2 * q.v2) + (q.v1 * q.v1 + q.v3 * q.v3)
Base.inv(q::Quaternion) = q.norm ? conj(q) : conj(q) / abs2(q)
Base.inv(q::Quaternion) = conj(q) / abs2(q)

Base.isreal(q::Quaternion) = iszero(q.v1) & iszero(q.v2) & iszero(q.v3)
Base.isfinite(q::Quaternion) = q.norm | (isfinite(q.s) & isfinite(q.v1) & isfinite(q.v2) & isfinite(q.v3))
Base.iszero(q::Quaternion) = ~q.norm & iszero(real(q)) & iszero(q.v1) & iszero(q.v2) & iszero(q.v3)
Base.isfinite(q::Quaternion) = isfinite(q.s) & isfinite(q.v1) & isfinite(q.v2) & isfinite(q.v3)
Base.iszero(q::Quaternion) = iszero(real(q)) & iszero(q.v1) & iszero(q.v2) & iszero(q.v3)
Base.isnan(q::Quaternion) = isnan(real(q)) | isnan(q.v1) | isnan(q.v2) | isnan(q.v3)
Base.isinf(q::Quaternion) = ~q.norm & (isinf(q.s) | isinf(q.v1) | isinf(q.v2) | isinf(q.v3))
Base.isinf(q::Quaternion) = isinf(q.s) | isinf(q.v1) | isinf(q.v2) | isinf(q.v3)

function LinearAlgebra.normalize(q::Quaternion)
Base.depwarn("`LinearAlgebra.normalize(q::Quaternion)` is deprecated. Please use `sign(q)` instead.", :normalize)
if (q.norm)
return q
end
q = q / abs(q)
Quaternion(q.s, q.v1, q.v2, q.v3, true)
end
# included strictly for documentation; the base implementation is sufficient
"""
sign(q::Quaternion) -> Quaternion
function normalizea(q::Quaternion)
Base.depwarn("`normalizea(q::Quaternion)` is deprecated. Please use `sign(q), abs(q)` instead.", :normalizea)
if (q.norm)
return (q, one(q.s))
end
a = abs(q)
q = q / a
(Quaternion(q.s, q.v1, q.v2, q.v3, true), a)
end
Return zero if `q==0` and ``q/|q|`` otherwise.
function normalizeq(q::Quaternion)
Base.depwarn("`normalizeq(q::Quaternion)` is deprecated. Please use `sign(q)` instead.", :normalizea)
a = abs(q)
if a > 0
q = q / a
Quaternion(q.s, q.v1, q.v2, q.v3, true)
else
Quaternion(0.0, 1.0, 0.0, 0.0, true)
end
end
# Examples
```jldoctest
julia> sign(Quaternion(4, 0, 0, 0))
QuaternionF64(1.0, 0.0, 0.0, 0.0)
Base.:-(q::Quaternion) = Quaternion(-q.s, -q.v1, -q.v2, -q.v3, q.norm)
julia> sign(Quaternion(1, 0, 1, 0))
QuaternionF64(0.7071067811865475, 0.0, 0.7071067811865475, 0.0)
```
"""
sign(::Quaternion)

Base.:-(q::Quaternion) = Quaternion(-q.s, -q.v1, -q.v2, -q.v3)

Base.:+(q::Quaternion, w::Quaternion) =
Quaternion(q.s + w.s, q.v1 + w.v1, q.v2 + w.v2, q.v3 + w.v3)
Expand All @@ -217,12 +190,12 @@ function Base.:*(q::Quaternion, w::Quaternion)
v1 = (q.s * w.v1 + q.v1 * w.s) + (q.v2 * w.v3 - q.v3 * w.v2)
v2 = (q.s * w.v2 + q.v2 * w.s) + (q.v3 * w.v1 - q.v1 * w.v3)
v3 = (q.s * w.v3 + q.v3 * w.s) + (q.v1 * w.v2 - q.v2 * w.v1)
return Quaternion(s, v1, v2, v3, q.norm & w.norm)
return Quaternion(s, v1, v2, v3)
end

Base.:/(q::Quaternion, w::Quaternion) = q * inv(w)

Base.:(==)(q::Quaternion, w::Quaternion) = (q.s == w.s) & (q.v1 == w.v1) & (q.v2 == w.v2) & (q.v3 == w.v3) # ignore .norm field
Base.:(==)(q::Quaternion, w::Quaternion) = (q.s == w.s) & (q.v1 == w.v1) & (q.v2 == w.v2) & (q.v3 == w.v3)

angleaxis(q::Quaternion) = angle(q), axis(q)

Expand All @@ -233,18 +206,13 @@ end

function axis(q::Quaternion)
Base.depwarn("`axis(::Quaternion)` is deprecated. Please consider using Rotations package instead.", :axis)
q = normalize(q)
q = sign(q)
s = sin(angle(q) / 2)
abs(s) > 0 ?
[q.v1, q.v2, q.v3] / s :
[1.0, 0.0, 0.0]
end

function argq(q::Quaternion)
Base.depwarn("`argq(q::Quaternion)` is deprecated. Use `quat(0, imag_part(q)...)` instead.", :argq)
normalizeq(Quaternion(0, q.v1, q.v2, q.v3))
end

"""
extend_analytic(f, q::Quaternion)
Expand Down Expand Up @@ -273,20 +241,16 @@ function extend_analytic(f, q::Quaternion)
w = f(z)
wr, wi = reim(w)
scale = wi / a
norm = _isexpfun(f) && iszero(s)
if a > 0
return Quaternion(wr, scale * q.v1, scale * q.v2, scale * q.v3, norm)
return Quaternion(wr, scale * q.v1, scale * q.v2, scale * q.v3)
else
# q == real(q), so f(real(q)) may be real or complex, i.e. wi may be nonzero.
# we choose to embed complex numbers in the quaternions by identifying the first
# imaginary quaternion basis with the complex imaginary basis.
return Quaternion(wr, oftype(scale, wi), zero(scale), zero(scale), norm)
return Quaternion(wr, oftype(scale, wi), zero(scale), zero(scale))
end
end

_isexpfun(::Union{typeof(exp),typeof(exp2),typeof(exp10)}) = true
_isexpfun(::Any) = false

for f in (
:sqrt, :exp, :exp2, :exp10, :expm1, :log2, :log10, :log1p,
:sin, :cos, :tan, :asin, :acos, :atan, :sinh, :cosh, :tanh, :asinh, :acosh, :atanh,
Expand Down Expand Up @@ -332,10 +296,10 @@ end
Base.:^(q::Quaternion, w::Quaternion) = exp(w * log(q))

quatrand(rng = Random.GLOBAL_RNG) = quat(randn(rng), randn(rng), randn(rng), randn(rng))
nquatrand(rng = Random.GLOBAL_RNG) = normalize(quatrand(rng))
nquatrand(rng = Random.GLOBAL_RNG) = sign(quatrand(rng))

function Base.rand(rng::AbstractRNG, ::Random.SamplerType{Quaternion{T}}) where {T<:Real}
Quaternion{T}(rand(rng, T), rand(rng, T), rand(rng, T), rand(rng, T), false)
Quaternion{T}(rand(rng, T), rand(rng, T), rand(rng, T), rand(rng, T))
end

function Base.randn(rng::AbstractRNG, ::Type{Quaternion{T}}) where {T<:AbstractFloat}
Expand All @@ -344,7 +308,6 @@ function Base.randn(rng::AbstractRNG, ::Type{Quaternion{T}}) where {T<:AbstractF
randn(rng, T) * 1//2,
randn(rng, T) * 1//2,
randn(rng, T) * 1//2,
false,
)
end

Expand All @@ -362,7 +325,7 @@ function qrotation(axis::AbstractVector{T}, theta) where {T <: Real}
end
s,c = sincos(theta / 2)
scaleby = s / normaxis
Quaternion(c, scaleby * axis[1], scaleby * axis[2], scaleby * axis[3], true)
Quaternion(c, scaleby * axis[1], scaleby * axis[2], scaleby * axis[3])
end

# Variant of the above where norm(rotvec) encodes theta
Expand All @@ -374,7 +337,7 @@ function qrotation(rotvec::AbstractVector{T}) where {T <: Real}
theta = norm(rotvec)
s,c = sincos(theta / 2)
scaleby = s / (iszero(theta) ? one(theta) : theta)
Quaternion(c, scaleby * rotvec[1], scaleby * rotvec[2], scaleby * rotvec[3], true)
Quaternion(c, scaleby * rotvec[1], scaleby * rotvec[2], scaleby * rotvec[3])
end

function qrotation(dcm::AbstractMatrix{T}) where {T<:Real}
Expand All @@ -399,9 +362,9 @@ function qrotation(dcm::AbstractMatrix{T}) where {T<:Real}
a,b,c = (dcm[2,1]-dcm[1,2])/4d, (dcm[1,3]+dcm[3,1])/4d, (dcm[3,2]+dcm[2,3])/4d
end
if a > 0
return Quaternion(a,b,c,d,true)
return Quaternion(a,b,c,d)
else
return Quaternion(-a,-b,-c,-d,true)
return Quaternion(-a,-b,-c,-d)
end
end

Expand All @@ -411,7 +374,7 @@ function qrotation(dcm::AbstractMatrix{T}, qa::Quaternion) where {T<:Real}
abs(q-qa) < abs(q+qa) ? q : -q
end

rotationmatrix(q::Quaternion) = rotationmatrix_normalized(normalize(q))
rotationmatrix(q::Quaternion) = rotationmatrix_normalized(sign(q))

function rotationmatrix_normalized(q::Quaternion)
Base.depwarn("`rotationmatrix_normalized(::Quaternion)` is deprecated. Please consider using Rotations package instead.", :rotationmatrix_normalized)
Expand All @@ -434,13 +397,13 @@ Since the input is normalized inside the function, the absolute value of the ret
julia> using Quaternions
julia> qa = Quaternion(1,0,0,0)
Quaternion{Int64}(1, 0, 0, 0, false)
Quaternion{Int64}(1, 0, 0, 0)
julia> qb = Quaternion(0,1,0,0)
Quaternion{Int64}(0, 1, 0, 0, false)
Quaternion{Int64}(0, 1, 0, 0)
julia> slerp(qa, qb, 0.6)
QuaternionF64(0.5877852522924731, 0.8090169943749475, 0.0, 0.0, true)
QuaternionF64(0.5877852522924731, 0.8090169943749475, 0.0, 0.0)
julia> ans ≈ Quaternion(cospi(0.3), sinpi(0.3), 0, 0)
true
Expand Down Expand Up @@ -475,7 +438,6 @@ true
qa.v1 * ratio_a + qb.v1 * ratio_b,
qa.v2 * ratio_a + qb.v2 * ratio_b,
qa.v3 * ratio_a + qb.v3 * ratio_b,
true
)
end

Expand Down
2 changes: 0 additions & 2 deletions src/Quaternions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ module Quaternions
export angleaxis
export angle
export axis
export normalize
export normalizea
export quatrand
export nquatrand
export qrotation
Expand Down
Loading

0 comments on commit 9c62d64

Please sign in to comment.