Skip to content

Commit 3b3bbe6

Browse files
sethaxenhyrodium
andauthored
Extend all analytic functions in base for quaternions (#56)
* Fix and test broken conversion * Add tests for all complex analytic functions * Add extensions of complex analytic functions * Remove slower methods * Avoid invalidations * Increment version number * Add test for resolved issue * Add inverse-specific overloads to avoid computing inverse * Remove ambiguous case * Don't overload sincospi if unavailable * Simplify log implementation * Test cis and cispi * Add sinpi and cospi tests * Add cis and cispi docstrings * Actually overload method * Use testset * Remove real exponent overloads It takes too many subsequent overloads to resolve ambiguities. Can be handled in a future PR * Fix and test zero branch of sincos and sincospi * Add docstring for extend_analytic * Move randn call into testset * Move identity tests into a testset * Run tests many times * Move exp test with others * Move exp tests near other funs * Add additional comment * Separate exponentiation tests * Make log more efficient and accurate * Move exp tests with others * Apply suggestions from code review Co-authored-by: Yuto Horikawa <hyrodium@gmail.com> * Add function overloads to ReadMe * Remove useless spaces * Increment version number Co-authored-by: Yuto Horikawa <hyrodium@gmail.com>
1 parent 4c86b46 commit 3b3bbe6

File tree

4 files changed

+265
-73
lines changed

4 files changed

+265
-73
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "Quaternions"
22
uuid = "94ee1d12-ae83-5a48-8b1c-48b8ff168ae0"
3-
version = "0.4.9"
3+
version = "0.5.0"
44

55
[deps]
66
DualNumbers = "fa6b7ba4-c1ee-5f82-b5fc-ecf0adba8f74"

README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,49 @@ Implemented functions are:
1717
conj
1818
abs
1919
abs2
20-
exp
21-
log
2220
normalize
2321
normalizea (return normalized quaternion and absolute value as a pair)
2422
angleaxis (taken as an orientation, return the angle and axis (3 vector) as a tuple)
2523
angle
2624
axis
25+
sqrt
2726
exp
28-
log
27+
exp2
28+
exp10
29+
expm1
30+
log2
31+
log10
32+
log1p
33+
cis
34+
cispi
2935
sin
3036
cos
31-
sqrt
37+
tan
38+
asin
39+
acos
40+
atan
41+
sinh
42+
cosh
43+
tanh
44+
asinh
45+
acosh
46+
atanh
47+
csc
48+
sec
49+
cot
50+
acsc
51+
asec
52+
acot
53+
csch
54+
sech
55+
coth
56+
acsch
57+
asech
58+
acoth
59+
sinpi
60+
cospi
61+
sincos
62+
sincospi
3263
linpol (interpolate between 2 normalized quaternions)
3364
rand
3465
randn

src/Quaternion.jl

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -115,43 +115,118 @@ end
115115

116116
argq(q::Quaternion) = normalizeq(Quaternion(0, q.v1, q.v2, q.v3))
117117

118-
function exp(q::Quaternion)
119-
s = q.s
120-
se = exp(s)
121-
scale = se
122-
th = abs_imag(q)
123-
if th > 0
124-
scale *= sin(th) / th
125-
end
126-
Quaternion(se * cos(th), scale * q.v1, scale * q.v2, scale * q.v3, iszero(s))
127-
end
118+
"""
119+
extend_analytic(f, q::Quaternion)
128120
129-
function log(q::Quaternion)
130-
q, a = normalizea(q)
121+
Evaluate the extension of the complex analytic function `f` to the quaternions at `q`.
122+
123+
Given ``q = s + a u``, where ``s`` is the real part, ``u`` is a pure unit quaternion,
124+
and ``a \\ge 0`` is the magnitude of the imaginary part of ``q``,
125+
126+
```math
127+
f(q) = \\Re(f(z)) + \\Im(f(z)) u,
128+
```
129+
is the extension of `f` to the quaternions, where ``z = a + s i`` is a complex analog to
130+
``q``.
131+
132+
See Theorem 5 of [^Sudbery1970] for details.
133+
134+
[^Sudbery1970]
135+
Sudbery (1979). Quaternionic analysis. Mathematical Proceedings of the Cambridge
136+
Philosophical Society,85, pp 199­225
137+
doi:[10.1017/S030500410005563](https://doi.org/10.1017/S0305004100055638)
138+
"""
139+
function extend_analytic(f, q::Quaternion)
140+
a = abs_imag(q)
131141
s = q.s
132-
M = abs_imag(q)
133-
th = atan(M, s)
134-
if M > 0
135-
M = th / M
136-
return Quaternion(log(a), q.v1 * M, q.v2 * M, q.v3 * M)
142+
z = complex(s, a)
143+
w = f(z)
144+
wr, wi = reim(w)
145+
scale = wi / a
146+
norm = _isexpfun(f) && iszero(s)
147+
if a > 0
148+
return Quaternion(wr, scale * q.v1, scale * q.v2, scale * q.v3, norm)
137149
else
138-
return Quaternion(log(a), th, 0.0, 0.0)
150+
# q == real(q), so f(real(q)) may be real or complex, i.e. wi may be nonzero.
151+
# we choose to embed complex numbers in the quaternions by identifying the first
152+
# imaginary quaternion basis with the complex imaginary basis.
153+
return Quaternion(wr, oftype(scale, wi), zero(scale), zero(scale), norm)
139154
end
140155
end
141156

142-
function sin(q::Quaternion)
143-
L = argq(q)
144-
return (exp(L * q) - exp(-L * q)) / (2 * L)
157+
_isexpfun(::Union{typeof(exp),typeof(exp2),typeof(exp10)}) = true
158+
_isexpfun(::Any) = false
159+
160+
"""
161+
cis(q::Quaternion)
162+
163+
Return ``\\exp(u * q)``, where ``u`` is the normalized imaginary part of `q`.
164+
165+
Let ``q = s + a u``, where ``s`` is the real part, ``u`` is a pure unit quaternion,
166+
and ``a \\ge 0`` is the magnitude of the imaginary part of ``q``.
167+
168+
!!! Note
169+
This is the extension of `cis(z)` for complex `z` to the quaternions and is not
170+
equivalent to `exp(im * q)`. As a result, `cis(Quaternion(z)) ≠ cis(z)` when
171+
`imag(z) < 0`.
172+
"""
173+
cis(q::Quaternion)
174+
175+
if VERSION v"1.6"
176+
"""
177+
cispi(q::Quaternion)
178+
179+
Compute `cis(π * q)` more accurately.
180+
181+
!!! Note
182+
This is not equivalent to `exp(π*im*q)`. See [cis(::Quaternion)](@ref) for details.
183+
"""
184+
Base.cispi(q::Quaternion) = extend_analytic(cispi, q)
145185
end
146186

147-
function cos(q::Quaternion)
148-
L = argq(q)
149-
return (exp(L * q) + exp(-L * q)) / 2
187+
for f in (
188+
:sqrt, :exp, :exp2, :exp10, :expm1, :log2, :log10, :log1p, :cis,
189+
:sin, :cos, :tan, :asin, :acos, :atan, :sinh, :cosh, :tanh, :asinh, :acosh, :atanh,
190+
:csc, :sec, :cot, :acsc, :asec, :acot, :csch, :sech, :coth, :acsch, :asech, :acoth,
191+
:sinpi, :cospi,
192+
)
193+
@eval Base.$f(q::Quaternion) = extend_analytic($f, q)
150194
end
151195

152-
(^)(q::Quaternion, w::Quaternion) = exp(w * log(q))
196+
for f in (@static(VERSION v"1.6" ? (:sincos, :sincospi) : (:sincos,)))
197+
@eval begin
198+
function Base.$f(q::Quaternion)
199+
a = abs_imag(q)
200+
z = complex(q.s, a)
201+
s, c = $f(z)
202+
sr, si = reim(s)
203+
cr, ci = reim(c)
204+
sscale = si / a
205+
cscale = ci / a
206+
if a > 0
207+
return (
208+
Quaternion(sr, sscale * q.v1, sscale * q.v2, sscale * q.v3),
209+
Quaternion(cr, cscale * q.v1, cscale * q.v2, cscale * q.v3),
210+
)
211+
else
212+
return (
213+
Quaternion(sr, oftype(sscale, si), zero(sscale), zero(sscale)),
214+
Quaternion(cr, oftype(cscale, ci), zero(cscale), zero(cscale)),
215+
)
216+
end
217+
end
218+
end
219+
end
153220

154-
sqrt(q::Quaternion) = exp(0.5 * log(q))
221+
function log(q::Quaternion)
222+
a = abs(q)
223+
M = abs_imag(q)
224+
theta = atan(M, q.s)
225+
scale = theta / ifelse(iszero(M), oneunit(M), M)
226+
return Quaternion(log(a), q.v1 * scale, q.v2 * scale, q.v3 * scale)
227+
end
228+
229+
(^)(q::Quaternion, w::Quaternion) = exp(w * log(q))
155230

156231
function linpol(p::Quaternion, q::Quaternion, t::Real)
157232
p = normalize(p)

test/test_Quaternion.jl

Lines changed: 128 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,139 @@ Base.:(/)(a::MyReal, b::Real) = a.val / b
156156
# this used to throw an error
157157
@test qrotation([1, 0, 0], MyReal(1.5)) == qrotation([1, 0, 0], 1.5)
158158

159-
for _ in 1:100
160-
let # test specialfunctions
161-
c = Complex(randn(2)...)
162-
q, q2 = sample(Quaternion{Float64}, 4)
163-
unary_funs = [exp, log, sin, cos, sqrt, inv, conj, abs2, norm]
164-
# since every quaternion is conjugate to a complex number,
165-
# one can establish correctness as follows:
166-
for fun in unary_funs
167-
@test fun(Quaternion(c)) Quaternion(fun(c))
159+
@testset "non-analytic functions" begin
160+
q, q2 = randn(Quaternion{Float64}, 2)
161+
unary_funs = [conj, abs, abs2, norm, sign]
162+
# since every quaternion is conjugate to a complex number,
163+
# one can establish correctness as follows:
164+
@testset for fun in unary_funs
165+
for _ in 1:100
166+
c = randn(ComplexF64)
167+
@test fun(Quaternion(c)) fun(c)
168168
@test q2 * fun(q) * inv(q2) fun(q2 * q * inv(q2))
169169
end
170+
end
171+
end
172+
173+
@testset "extended complex analytic functions" begin
174+
# all complex analytic functions can be extended to the quaternions
175+
unary_funs = [
176+
sqrt, inv, exp, exp2, exp10, expm1, log, log2, log10, log1p, cis,
177+
sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh,
178+
csc, sec, cot, acsc, asec, acot, csch, sech, coth, acsch, asech, acoth,
179+
sinpi, cospi,
180+
]
181+
# since every quaternion is conjugate to a complex number,
182+
# one can establish correctness as follows:
183+
@testset for fun in unary_funs
184+
q, q2 = randn(QuaternionF64, 2)
185+
for _ in 1:100
186+
c = randn(ComplexF64)
187+
fun !== cis && @test fun(Quaternion(c)) fun(c)
188+
@test q2 * fun(q) * inv(q2) fun(q2 * q * inv(q2))
189+
end
190+
end
170191

171-
@test exp(log(q)) q
172-
@test exp(zero(q)) one(q)
192+
@testset "identities" begin
193+
for _ in 1:100
194+
q = randn(QuaternionF64)
195+
@test inv(q) * q q * inv(q) one(q)
196+
@test sqrt(q) * sqrt(q) q
197+
@test exp(log(q)) q
198+
@test exp(zero(q)) one(q)
199+
@test log(one(q)) zero(q)
200+
@test exp2(log2(q)) q
201+
@test exp10(log10(q)) q
202+
@test expm1(log1p(q)) q
203+
@test sinpi(q) sin* q)
204+
@test cospi(q) cos* q)
205+
@test all(sincos(q) .≈ (sin(q), cos(q)))
206+
@test all(sincos(zero(q)) .≈ (sin(zero(q)), cos(zero(q))))
207+
if VERSION v"1.6"
208+
@test all(sincospi(q) .≈ (sinpi(q), cospi(q)))
209+
@test all(sincospi(zero(q)) .≈ (sinpi(zero(q)), cospi(zero(q))))
210+
end
211+
@test tan(q) cos(q) \ sin(q) sin(q) / cos(q)
212+
@test tanh(q) cosh(q) \ sinh(q) sinh(q) / cosh(q)
213+
@testset for (f, finv) in [(sin, csc), (cos, sec), (tan, cot), (sinh, csch), (cosh, sech), (tanh, coth)]
214+
@test f(q) inv(finv(q))
215+
end
216+
@testset for (f, finv) in [(asin, acsc), (acos, asec), (atan, acot), (asinh, acsch), (acosh, asech), (atanh, acoth)]
217+
@test f(q) finv(inv(q))
218+
end
219+
@test cis(q) exp(normalize(q - real(q)) * q)
220+
VERSION v"1.6" && @test cispi(q) cis* q)
221+
end
173222
end
174223

224+
@testset "additional properties" begin
225+
@testset "log" begin
226+
@test log(zero(QuaternionF64)) === Quaternion(-Inf, 0, 0, 0)
227+
end
228+
229+
@testset "exp" begin
230+
@test exp(Quaternion(0, 0, 0, 0)) === Quaternion(1., 0., 0., 0., true)
231+
@test exp(Quaternion(2, 0, 0, 0)) === Quaternion(exp(2), 0, 0, 0, false)
232+
@test exp(Quaternion(0, 2, 0, 0)) === Quaternion(cos(2), sin(2), 0, 0, true)
233+
@test exp(Quaternion(0, 0, 2, 0)) === Quaternion(cos(2), 0, sin(2), 0, true)
234+
@test exp(Quaternion(0, 0, 0, 2)) === Quaternion(cos(2), 0, 0, sin(2), true)
235+
236+
@test norm(exp(Quaternion(0, 0, 0, 0))) 1
237+
@test norm(exp(Quaternion(2, 0, 0, 0))) 1
238+
@test norm(exp(Quaternion(0, 2, 0, 0))) 1
239+
@test norm(exp(Quaternion(0, 0, 2, 0))) 1
240+
@test norm(exp(Quaternion(0, 0, 0, 2))) 1
241+
242+
@test exp(Quaternion(0., 0., 0., 0.)) === Quaternion(1., 0., 0., 0., true)
243+
@test exp(Quaternion(2., 0., 0., 0.)) === Quaternion(exp(2), 0, 0, 0, false)
244+
@test exp(Quaternion(0., 2., 0., 0.)) === Quaternion(cos(2), sin(2), 0, 0, true)
245+
@test exp(Quaternion(0., 0., 2., 0.)) === Quaternion(cos(2), 0, sin(2), 0, true)
246+
@test exp(Quaternion(0., 0., 0., 2.)) === Quaternion(cos(2), 0, 0, sin(2), true)
247+
248+
@test norm(exp(Quaternion(0., 0., 0., 0.))) 1
249+
@test norm(exp(Quaternion(2., 0., 0., 0.))) 1
250+
@test norm(exp(Quaternion(0., 2., 0., 0.))) 1
251+
@test norm(exp(Quaternion(0., 0., 2., 0.))) 1
252+
@test norm(exp(Quaternion(0., 0., 0., 2.))) 1
253+
254+
@test exp(Quaternion(0,0,0,0)) isa Quaternion{Float64}
255+
@test exp(Quaternion(0.,0,0,0)) isa Quaternion{Float64}
256+
@test exp(Quaternion(0//1,0,0,0)) isa Quaternion{Float64}
257+
@test exp(Quaternion(BigFloat(0),0,0,0)) isa Quaternion{BigFloat}
258+
259+
# https://github.com/JuliaGeometry/Quaternions.jl/issues/39
260+
@testset "exp(::Quaternion{Int})" begin
261+
@test exp(Quaternion(1,1,1,1)) exp(Quaternion(1.0,1.0,1.0,1.0))
262+
end
263+
end
264+
end
265+
end
266+
267+
@testset "^" begin
268+
@testset "^(::Quaternion, ::Real)" begin
269+
for _ in 1:100
270+
q = randn(QuaternionF64)
271+
@test q^2.0 q * q
272+
@test q^1.0 q
273+
@test q^-1.0 inv(q)
274+
@test q^1.3 exp(1.3 * log(q))
275+
@test q^7.8 exp(7.8 * log(q))
276+
@test q^1.3f0 exp(1.3f0 * log(q))
277+
@test q^7.8f0 exp(7.8f0 * log(q))
278+
end
279+
end
280+
@testset "^(::Quaternion, ::Quaternion)" begin
281+
@test Quaternion(ℯ,0,0,0)^Quaternion(0,0/2,0) Quaternion(0,0,1,0)
282+
@test Quaternion(3.5,0,0,2.3)^Quaternion(0.2,0,0,1.7)
283+
Quaternion(real((3.5+2.3im)^(0.2+1.7im)),0,0,imag((3.5+2.3im)^(0.2+1.7im)))
284+
for _ in 1:100
285+
q, p = randn(QuaternionF64, 2)
286+
@test q^p exp(p * log(q))
287+
end
288+
end
289+
end
290+
291+
for _ in 1:100
175292
let # test qrotation and angleaxis inverse
176293
ax = randn(3); ax = ax / norm(ax)
177294
Θ = π * rand()
@@ -230,37 +347,6 @@ for _ in 1:100
230347
end
231348
end
232349

233-
@testset "exp" begin
234-
@test exp(Quaternion(0, 0, 0, 0)) === Quaternion(1., 0., 0., 0., true)
235-
@test exp(Quaternion(2, 0, 0, 0)) === Quaternion(exp(2), 0, 0, 0, false)
236-
@test exp(Quaternion(0, 2, 0, 0)) === Quaternion(cos(2), sin(2), 0, 0, true)
237-
@test exp(Quaternion(0, 0, 2, 0)) === Quaternion(cos(2), 0, sin(2), 0, true)
238-
@test exp(Quaternion(0, 0, 0, 2)) === Quaternion(cos(2), 0, 0, sin(2), true)
239-
240-
@test norm(exp(Quaternion(0, 0, 0, 0))) 1
241-
@test norm(exp(Quaternion(2, 0, 0, 0))) 1
242-
@test norm(exp(Quaternion(0, 2, 0, 0))) 1
243-
@test norm(exp(Quaternion(0, 0, 2, 0))) 1
244-
@test norm(exp(Quaternion(0, 0, 0, 2))) 1
245-
246-
@test exp(Quaternion(0., 0., 0., 0.)) === Quaternion(1., 0., 0., 0., true)
247-
@test exp(Quaternion(2., 0., 0., 0.)) === Quaternion(exp(2), 0, 0, 0, false)
248-
@test exp(Quaternion(0., 2., 0., 0.)) === Quaternion(cos(2), sin(2), 0, 0, true)
249-
@test exp(Quaternion(0., 0., 2., 0.)) === Quaternion(cos(2), 0, sin(2), 0, true)
250-
@test exp(Quaternion(0., 0., 0., 2.)) === Quaternion(cos(2), 0, 0, sin(2), true)
251-
252-
@test norm(exp(Quaternion(0., 0., 0., 0.))) 1
253-
@test norm(exp(Quaternion(2., 0., 0., 0.))) 1
254-
@test norm(exp(Quaternion(0., 2., 0., 0.))) 1
255-
@test norm(exp(Quaternion(0., 0., 2., 0.))) 1
256-
@test norm(exp(Quaternion(0., 0., 0., 2.))) 1
257-
258-
@test exp(Quaternion(0,0,0,0)) isa Quaternion{Float64}
259-
@test exp(Quaternion(0.,0,0,0)) isa Quaternion{Float64}
260-
@test exp(Quaternion(0//1,0,0,0)) isa Quaternion{Float64}
261-
@test exp(Quaternion(BigFloat(0),0,0,0)) isa Quaternion{BigFloat}
262-
end
263-
264350
@testset "random quaternions" begin
265351
@testset "quatrand" begin
266352
rng = Random.MersenneTwister(42)

0 commit comments

Comments
 (0)