Skip to content

Commit cbb1a07

Browse files
mikmooredkarraschsethaxen
authored
improve promotion and equality checking (#52)
* improve promotion and equality checking * Apply suggestions from code review better styling and formatting Co-authored-by: Daniel Karrasch <[email protected]> * change && to & in == * extend promotion and equality improvements to Octonion, DualQuaternion * Update src/Octonion.jl Co-authored-by: Seth Axen <[email protected]> * add promotion and equality tests for DualQuaternion and Octonion * Update test/test_Quaternion.jl Co-authored-by: Seth Axen <[email protected]> * reorganize testing and add convert() tests * fix troublesome shorthand constructors that gave potentially-wrong .norm field. but they should probably be removed altogether. * fix broken DualQuaternion interactions with DualNumbers.Dual * Update src/DualQuaternion.jl Co-authored-by: Seth Axen <[email protected]> * revert mistake to norm field * Revert "fix troublesome shorthand constructors that gave potentially-wrong .norm field. but they should probably be removed altogether." This reverts commit 3809886. * quat(), dualquat(), and octo() don't set their own defaults for norm * make Quaternion(1) === Quaternion{T}(1) * fix norm fields of DualQuaternion(::Dual) * fix test that was broken because it was misplaced * Update test/test_Quaternion.jl Co-authored-by: Seth Axen <[email protected]> * increase test coverage * a few more tests * Update test/test_Quaternion.jl Co-authored-by: Seth Axen <[email protected]> Co-authored-by: Daniel Karrasch <[email protected]> Co-authored-by: Seth Axen <[email protected]> Co-authored-by: mikmoore <[email protected]>
1 parent be0fc14 commit cbb1a07

File tree

5 files changed

+106
-36
lines changed

5 files changed

+106
-36
lines changed

src/DualQuaternion.jl

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ end
99
DualQuaternion(q0::Quaternion, qe::Quaternion, n::Bool = false) =
1010
DualQuaternion(promote(q0, qe)..., n)
1111

12-
DualQuaternion(d1::Dual, d2::Dual, d3::Dual, d4::Dual, n::Bool = false) =
13-
DualQuaternion(Quaternion(d1.re, d2.re, d3.re, d4.re, n),
14-
Quaternion(d1.du, d2.du, d3.du, d4.du), n)
12+
DualQuaternion(d1::Dual, d2::Dual, d3::Dual, d4::Dual) =
13+
DualQuaternion(Quaternion(DualNumbers.value(d1), DualNumbers.value(d2), DualNumbers.value(d3), DualNumbers.value(d4)),
14+
Quaternion(DualNumbers.epsilon(d1), DualNumbers.epsilon(d2), DualNumbers.epsilon(d3), DualNumbers.epsilon(d4)))
15+
DualQuaternion(d1::Dual, d2::Dual, d3::Dual, d4::Dual, n::Bool) =
16+
DualQuaternion(Quaternion(DualNumbers.value(d1), DualNumbers.value(d2), DualNumbers.value(d3), DualNumbers.value(d4), n),
17+
Quaternion(DualNumbers.epsilon(d1), DualNumbers.epsilon(d2), DualNumbers.epsilon(d3), DualNumbers.epsilon(d4)), n)
1518

1619
DualQuaternion(x::Real) = DualQuaternion(Quaternion(x), Quaternion(zero(x)), abs(x) == one(x))
1720

18-
DualQuaternion(d::Dual) = DualQuaternion(d, zero(d), zero(d), zero(d), abs(d) == one(d.re))
21+
DualQuaternion(d::Dual) = DualQuaternion(Quaternion(DualNumbers.value(d)), Quaternion(DualNumbers.epsilon(d)), (DualNumbers.value(d)==one(DualNumbers.value(d))) & iszero(DualNumbers.epsilon(d)))
1922

2023
DualQuaternion(q::Quaternion) = DualQuaternion(q, zero(q), q.norm)
2124

@@ -25,14 +28,11 @@ const DualQuaternionF16 = DualQuaternion{Float16}
2528
const DualQuaternionF32 = DualQuaternion{Float32}
2629
const DualQuaternionF64 = DualQuaternion{Float64}
2730

28-
convert(::Type{DualQuaternion{T}}, x::Real) where {T} =
29-
DualQuaternion(convert(Quaternion{T}, x), convert(Quaternion{T}, 0))
31+
convert(::Type{DualQuaternion{T}}, x::Real) where {T} = DualQuaternion(convert(T, x))
3032

31-
convert(::Type{DualQuaternion{T}}, d::Dual) where {T} =
32-
DualQuaternion(convert(Dual{T}, d), convert(Dual{T}, 0), convert(Dual{T}, 0), convert(Dual{T}, 0))
33+
convert(::Type{DualQuaternion{T}}, d::Dual) where {T} = DualQuaternion(convert(Dual{T}, d))
3334

34-
convert(::Type{DualQuaternion{T}}, q::Quaternion) where {T} =
35-
DualQuaternion(convert(Quaternion{T}, q), convert(Quaternion{T}, 0), q.norm)
35+
convert(::Type{DualQuaternion{T}}, q::Quaternion) where {T} = DualQuaternion(convert(Quaternion{T}, q))
3636

3737
convert(::Type{DualQuaternion{T}}, q::DualQuaternion{T}) where {T <: Real} = q
3838

@@ -45,8 +45,10 @@ promote_rule(::Type{DualQuaternion{T}}, ::Type{S}) where {T <: Real, S <: Real}
4545
promote_rule(::Type{Quaternion{T}}, ::Type{DualQuaternion{S}}) where {T <: Real, S <: Real} = DualQuaternion{promote_type(T, S)}
4646
promote_rule(::Type{DualQuaternion{T}}, ::Type{DualQuaternion{S}}) where {T <: Real, S <: Real} = DualQuaternion{promote_type(T, S)}
4747

48-
dualquat(q1, q2, n=false) = DualQuaternion(q1, q2, n)
49-
dualquat(d1, d2, d3, d4, n=false) = DualQuaternion(d1, d2, d3, d4, n)
48+
dualquat(q1, q2) = DualQuaternion(q1, q2)
49+
dualquat(q1, q2, n) = DualQuaternion(q1, q2, n)
50+
dualquat(d1, d2, d3, d4) = DualQuaternion(d1, d2, d3, d4)
51+
dualquat(d1, d2, d3, d4, n) = DualQuaternion(d1, d2, d3, d4, n)
5052
dualquat(x) = DualQuaternion(x)
5153

5254
function show(io::IO, dq::DualQuaternion)
@@ -116,6 +118,7 @@ end
116118
dq.q0 * dw.qe + dq.qe * dw.q0,
117119
dq.norm && dw.norm)
118120
(/)(dq::DualQuaternion, dw::DualQuaternion) = dq * inv(dw)
121+
(==)(q::DualQuaternion, w::DualQuaternion) = (q.q0 == w.q0) & (q.qe == w.qe) # ignore .norm field
119122

120123
function angleaxis(dq::DualQuaternion)
121124
tq = dq.qe * conj(dq.q0)

src/Octonion.jl

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@ const OctonionF16 = Octonion{Float16}
2222
const OctonionF32 = Octonion{Float32}
2323
const OctonionF64 = Octonion{Float64}
2424

25-
convert(::Type{Octonion{T}}, x::Real) where {T} =
26-
Octonion(convert(T, x), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0))
27-
convert(::Type{Octonion{T}}, z::Complex) where {T} =
28-
Octonion(convert(T, real(z)), convert(T, imag(z)), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0))
29-
convert(::Type{Octonion{T}}, q::Quaternion) where {T} =
30-
Octonion(convert(T, real(q)), convert(T, q.v1), convert(T, q.v2), convert(T, q.v3), convert(T, 0), convert(T, 0), convert(T, 0), convert(T, 0))
25+
convert(::Type{Octonion{T}}, x::Real) where {T} = Octonion(convert(T, x))
26+
convert(::Type{Octonion{T}}, z::Complex) where {T} = Octonion(convert(Complex{T}, z))
27+
convert(::Type{Octonion{T}}, q::Quaternion) where {T} = Octonion(convert(Quaternion{T}, q))
3128
convert(::Type{Octonion{T}}, o::Octonion{T}) where {T <: Real} = o
3229
convert(::Type{Octonion{T}}, o::Octonion) where {T} =
3330
Octonion(convert(T, o.s), convert(T, o.v1), convert(T, o.v2), convert(T, o.v3), convert(T, o.v4), convert(T, o.v5), convert(T, o.v6), convert(T, o.v7), o.norm)
@@ -39,7 +36,8 @@ promote_rule(::Type{Complex{T}}, ::Type{Octonion{S}}) where {T <: Real, S <: Rea
3936
promote_rule(::Type{Quaternion{T}}, ::Type{Octonion{S}}) where {T <: Real, S <: Real} = Octonion{promote_type(T, S)}
4037
promote_rule(::Type{Octonion{T}}, ::Type{Octonion{S}}) where {T <: Real, S <: Real} = Octonion{promote_type(T, S)}
4138

42-
octo(p, v1, v2, v3, v4, v5, v6, v7, n = false) = Octonion(p, v1, v2, v3, v4, v5, v6, v7, n)
39+
octo(p, v1, v2, v3, v4, v5, v6, v7) = Octonion(p, v1, v2, v3, v4, v5, v6, v7)
40+
octo(p, v1, v2, v3, v4, v5, v6, v7, n) = Octonion(p, v1, v2, v3, v4, v5, v6, v7, n)
4341
octo(x) = Octonion(x)
4442
octo(s, a) = Octonion(s, a)
4543

@@ -117,6 +115,9 @@ end
117115

118116
(/)(o::Octonion, w::Octonion) = o * inv(w)
119117

118+
(==)(q::Octonion, w::Octonion) = (q.s == w.s) & (q.v1 == w.v1) & (q.v2 == w.v2) & (q.v3 == w.v3) &
119+
(q.v4 == w.v4) & (q.v5 == w.v5) & (q.v6 == w.v6) & (q.v7 == w.v7) # ignore .norm field
120+
120121
function exp(o::Octonion)
121122
s = o.s
122123
se = exp(s)

src/Quaternion.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const QuaternionF16 = Quaternion{Float16}
1010
const QuaternionF32 = Quaternion{Float32}
1111
const QuaternionF64 = Quaternion{Float64}
1212

13-
(::Type{Quaternion{T}})(x::Real) where {T} = Quaternion{T}(x, 0, 0, 0, false)
13+
(::Type{Quaternion{T}})(x::Real) where {T<:Real} = Quaternion(convert(T, x))
1414
(::Type{Quaternion{T}})(q::Quaternion{T}) where {T<:Real} = q
1515
(::Type{Quaternion{T}})(q::Quaternion) where {T<:Real} = Quaternion{T}(q.s, q.v1, q.v2, q.v3, q.norm)
1616
Quaternion(s::Real, v1::Real, v2::Real, v3::Real, n::Bool = false) =
@@ -20,10 +20,8 @@ Quaternion(z::Complex) = Quaternion(z.re, z.im, zero(z.re), zero(z.re), abs(z) =
2020
Quaternion(s::Real, a::Vector) = Quaternion(s, a[1], a[2], a[3])
2121
Quaternion(a::Vector) = Quaternion(0, a[1], a[2], a[3])
2222

23-
convert(::Type{Quaternion{T}}, x::Real) where {T} =
24-
Quaternion(convert(T, x), convert(T, 0), convert(T, 0), convert(T, 0))
25-
convert(::Type{Quaternion{T}}, z::Complex) where {T} =
26-
Quaternion(convert(T, real(z)), convert(T, imag(z)), convert(T, 0), convert(T, 0))
23+
convert(::Type{Quaternion{T}}, x::Real) where {T} = Quaternion(convert(T, x))
24+
convert(::Type{Quaternion{T}}, z::Complex) where {T} = Quaternion(convert(Complex{T}, z))
2725
convert(::Type{Quaternion{T}}, q::Quaternion{T}) where {T <: Real} = q
2826
convert(::Type{Quaternion{T}}, q::Quaternion) where {T} =
2927
Quaternion(convert(T, q.s), convert(T, q.v1), convert(T, q.v2), convert(T, q.v3), q.norm)
@@ -34,7 +32,8 @@ promote_rule(::Type{Quaternion{T}}, ::Type{S}) where {T <: Real, S <: Real} = Qu
3432
promote_rule(::Type{Complex{T}}, ::Type{Quaternion{S}}) where {T <: Real, S <: Real} = Quaternion{promote_type(T, S)}
3533
promote_rule(::Type{Quaternion{T}}, ::Type{Quaternion{S}}) where {T <: Real, S <: Real} = Quaternion{promote_type(T, S)}
3634

37-
quat(p, v1, v2, v3, n = false) = Quaternion(p, v1, v2, v3, n)
35+
quat(p, v1, v2, v3) = Quaternion(p, v1, v2, v3)
36+
quat(p, v1, v2, v3, n) = Quaternion(p, v1, v2, v3, n)
3837
quat(x) = Quaternion(x)
3938
quat(s, a) = Quaternion(s, a)
4039

@@ -100,6 +99,8 @@ end
10099
q.norm && w.norm)
101100
(/)(q::Quaternion, w::Quaternion) = q * inv(w)
102101

102+
(==)(q::Quaternion, w::Quaternion) = (q.s == w.s) & (q.v1 == w.v1) & (q.v2 == w.v2) & (q.v3 == w.v3) # ignore .norm field
103+
103104
angleaxis(q::Quaternion) = angle(q), axis(q)
104105

105106
angle(q::Quaternion) = 2 * atan((q.v1^2 + q.v2^2 + q.v3^2), q.s)

src/Quaternions.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ __precompile__()
22

33
module Quaternions
44

5-
import Base: +, -, *, /, ^
5+
import Base: +, -, *, /, ^, ==
66
import Base: abs, abs2, angle, conj, cos, exp, inv, isfinite, log, real, sin, sqrt
77
import Base: convert, promote_rule, float
88
import Base: rand, randn

test/test_Quaternion.jl

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
using Quaternions: argq
3+
using DualNumbers
34
using LinearAlgebra
45
using Random
56

@@ -37,7 +38,71 @@ for _ in 1:10, T in (Float32, Float64, Int32, Int64)
3738
test_multiplicative(c1, c2, +, Quaternion)
3839
end
3940

40-
let # test rotations
41+
@testset "promotions and equalities" begin
42+
@test Quaternion(1,0,0,0,false) == Quaternion(1,0,0,0,true) # test that .norm field does not affect equality
43+
@test Quaternion(1) == 1.0 # test promotion
44+
@test Quaternion(1,2,0,0) == Complex(1.0,2.0) # test promotion
45+
@test Quaternion{Float64}(1) === Quaternion(1.0) # explicit type construction
46+
@test quat(1) === Quaternion(1) # checking the .norm field in particular
47+
@test quat(1,0,0,0) === Quaternion(1,0,0,0) # checking the .norm field in particular
48+
@test quat(1,2,3,4) === Quaternion(1,2,3,4)
49+
@test quat(Quaternion(1,0,0,0)) === Quaternion(1,0,0,0) # checking the .norm field in particular
50+
@test quat(Quaternion(1,2,3,4)) === Quaternion(1,2,3,4)
51+
@test quat(1,0,0,0,false).norm == false # respect the .norm input (even if wrong)
52+
@test quat(1,2,3,4,true).norm == true # respect the .norm input (even if wrong)
53+
54+
@test Quaternion(1,2,3,4) == DualQuaternion(Quaternion(1,2,3,4))
55+
@test Quaternion(1,2,3,4) != DualQuaternion(Quaternion(1,2,3,4),Quaternion(5,6,7,8))
56+
@test DualQuaternion(1) == 1.0 # test promotion
57+
@test DualQuaternion(Quaternion(1,2,3,4),Quaternion(5,6,7,8)) == DualQuaternion(Quaternion(1.0,2,3,4),Quaternion(5,6,7,8))
58+
@test DualQuaternion(Quaternion(1,2,3,4),Quaternion(5,6,7,8)) != DualQuaternion(Quaternion(1.0,2,3,4),Quaternion(1,2,3,4))
59+
@test dualquat(Quaternion(1,0,0,0)) == Quaternion(1,0,0,0)
60+
@test dualquat(Quaternion(1,2,3,4)) == Quaternion(1,2,3,4)
61+
@test dualquat(Quaternion(1,0,0,0)) === DualQuaternion(Quaternion(1,0,0,0)) # checking the .norm field in particular
62+
@test dualquat(Quaternion(1,2,3,4)) === DualQuaternion(Quaternion(1,2,3,4))
63+
@test dualquat(1) === DualQuaternion(1)
64+
@test dualquat(Dual(1,2)) === DualQuaternion(Dual(1,2))
65+
@test dualquat(Dual(1,2),Dual(0),Dual(0),Dual(0)) === DualQuaternion(Dual(1,2),Dual(0),Dual(0),Dual(0))
66+
@test dualquat(Quaternion(1,2,3,4),Quaternion(5,6,7,8)) == DualQuaternion(Quaternion(1,2,3,4),Quaternion(5,6,7,8))
67+
@test dualquat(Quaternion(1,0,0,0),Quaternion(0)).norm == false
68+
@test dualquat(Quaternion(1,0,0,0),Quaternion(0),false).norm == false # respect the .norm input (even if wrong)
69+
@test dualquat(Quaternion(1,2,3,4),Quaternion(0),true).norm == true # respect the .norm input (even if wrong)
70+
@test dualquat(Dual(2,0),Dual(0),Dual(0),Dual(0),true).norm == true # respect the .norm input (even if wrong)
71+
@test dualquat(Dual(1,0),Dual(0),Dual(0),Dual(0),false).norm == false # respect the .norm input (even if wrong)
72+
73+
@test Quaternion(1,2,3,4) == Octonion(1,2,3,4,0,0,0,0)
74+
@test Quaternion(1,2,3,4) != Octonion(1,2,3,4,5,6,7,8)
75+
@test Octonion(1,0,0,0,0,0,0,0,false) == Octonion(1,0,0,0,0,0,0,0,true) # test that .norm field does not affect equality
76+
@test Octonion(1) == 1.0 # test promotion
77+
@test Octonion(Complex(1,2)) == Complex(1,2)
78+
@test Octonion(1.0,2,3,4,5,6,7,8) == Octonion(1,2,3,4,5,6,7,8)
79+
@test Octonion(1.0,2,3,4,5,6,7,8) != Octonion(1,2,3,4,1,2,3,4)
80+
@test octo(1) === Octonion(1) # checking the .norm field in particular
81+
@test octo(1,0,0,0,0,0,0,0) === Octonion(1,0,0,0,0,0,0,0) # checking the .norm field in particular
82+
@test octo(1,2,3,4,5,6,7,8) === Octonion(1,2,3,4,5,6,7,8)
83+
@test octo(Octonion(1,0,0,0,0,0,0,0)) === Octonion(1,0,0,0,0,0,0,0) # checking the .norm field in particular
84+
@test octo(Octonion(1,2,3,4,5,6,7,8)) === Octonion(1,2,3,4,5,6,7,8)
85+
@test octo(1,0,0,0,0,0,0,0,false).norm == false # respect the .norm input (even if wrong)
86+
@test octo(1,2,3,4,5,6,7,8,true).norm == true # respect the .norm input (even if wrong)
87+
end
88+
89+
@testset "conversions" begin
90+
@test convert(Quaternion{Float64},1) === Quaternion(1.0)
91+
@test convert(Quaternion{Float64},Complex(1,2)) === Quaternion(1.0,2.0,0.0,0.0)
92+
@test convert(Quaternion{Float64},Quaternion(1,2,3,4)) === Quaternion(1.0,2.0,3.0,4.0)
93+
94+
@test convert(DualQuaternion{Float64},1) === DualQuaternion(1.0)
95+
@test convert(DualQuaternion{Float64},DualNumbers.Dual(1,2)) === DualQuaternion(Quaternion(1.0),Quaternion(2.0))
96+
@test convert(DualQuaternion{Float64},Quaternion(1,2,3,4)) === DualQuaternion(Quaternion(1.0,2.0,3.0,4.0))
97+
@test convert(DualQuaternion{Float64},DualQuaternion(Quaternion(1,2,3,4),Quaternion(5,6,7,8))) === DualQuaternion(Quaternion(1.0,2.0,3.0,4.0),Quaternion(5.0,6.0,7.0,8.0))
98+
99+
@test convert(Octonion{Float64},1) === Octonion(1.0)
100+
@test convert(Octonion{Float64},Complex(1,2)) === Octonion(1.0,2.0,0.0,0.0,0.0,0.0,0.0,0.0)
101+
@test convert(Octonion{Float64},Quaternion(1,2,3,4)) === Octonion(1.0,2.0,3.0,4.0,0.0,0.0,0.0,0.0)
102+
@test convert(Octonion{Float64},Octonion(1,2,3,4,5,6,7,8)) === Octonion(1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0)
103+
end
104+
105+
@testset "rotations" begin # test rotations
41106
@test qrotation([0, 0, 0], 1.0) == Quaternion(1.0) # a zero axis should act like zero rotation
42107
@test qrotation([1, 0, 0], 0.0) == Quaternion(1.0)
43108
@test qrotation([0, 0, 0]) == Quaternion(1.0)
@@ -73,15 +138,6 @@ let # test rotations
73138
@test angle(qrotation([0, 1, 0], pi / 4)) pi / 4
74139
@test angle(qrotation([0, 0, 1], pi / 2)) pi / 2
75140

76-
# Regression test for
77-
# https://github.com/JuliaGeometry/Quaternions.jl/issues/8#issuecomment-610640094
78-
struct MyReal <: Real
79-
val::Real
80-
end
81-
Base.:(/)(a::MyReal, b::Real) = a.val / b
82-
# this used to throw an error
83-
qrotation([1, 0, 0], MyReal(1.5))
84-
85141
let # test numerical stability of angle
86142
ax = randn(3)
87143
for θ in [1e-9, π - 1e-9]
@@ -91,6 +147,15 @@ let # test rotations
91147
end
92148
end
93149

150+
# Regression test for
151+
# https://github.com/JuliaGeometry/Quaternions.jl/issues/8#issuecomment-610640094
152+
struct MyReal <: Real
153+
val::Real
154+
end
155+
Base.:(/)(a::MyReal, b::Real) = a.val / b
156+
# this used to throw an error
157+
@test qrotation([1, 0, 0], MyReal(1.5)) == qrotation([1, 0, 0], 1.5)
158+
94159
for _ in 1:100
95160
let # test specialfunctions
96161
c = Complex(randn(2)...)

0 commit comments

Comments
 (0)