Skip to content

Commit bce46bd

Browse files
sethaxenhyrodium
andauthored
Implement sylvester and lyap for quaternions (#65)
* Implement iszero for Quaternion * Implement sylvester and lyap * Simplify implementation * Hit other branch * Test for more random numbers * Define isnan * Match real/complex behavior for returning NaNs * Apply suggestions from code review Co-authored-by: Yuto Horikawa <hyrodium@gmail.com> * Add promotion methods * Add methods for combinations of real arguments * Implement isreal * Simplify when one is real * Use bitwise operations * Add isinf * Test isfinite/isinf * Test sylvester/lyap more thoroughly * Avoid huge fractions * Skip test when lyap did not promote * Call sylvester * Move sylvester/lyap tests to new test file * Increment version number Co-authored-by: Yuto Horikawa <hyrodium@gmail.com>
1 parent 0b5b431 commit bce46bd

File tree

4 files changed

+107
-4
lines changed

4 files changed

+107
-4
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.5.0"
3+
version = "0.5.1"
44

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

src/Quaternion.jl

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ abs_imag(q::Quaternion) = sqrt(q.v1 * q.v1 + q.v2 * q.v2 + q.v3 * q.v3)
5555
abs2(q::Quaternion) = q.s * q.s + q.v1 * q.v1 + q.v2 * q.v2 + q.v3 * q.v3
5656
inv(q::Quaternion) = q.norm ? conj(q) : conj(q) / abs2(q)
5757

58-
isfinite(q::Quaternion) = q.norm ? true : (isfinite(q.s) && isfinite(q.v1) && isfinite(q.v2) && isfinite(q.v3))
58+
isreal(q::Quaternion) = iszero(q.v1) & iszero(q.v2) & iszero(q.v3)
59+
isfinite(q::Quaternion) = q.norm | (isfinite(q.s) & isfinite(q.v1) & isfinite(q.v2) & isfinite(q.v3))
60+
iszero(q::Quaternion) = ~q.norm & iszero(real(q)) & iszero(q.v1) & iszero(q.v2) & iszero(q.v3)
61+
isnan(q::Quaternion) = isnan(real(q)) | isnan(q.v1) | isnan(q.v2) | isnan(q.v3)
62+
isinf(q::Quaternion) = ~q.norm & (isinf(q.s) | isinf(q.v1) | isinf(q.v2) | isinf(q.v3))
5963

6064
function normalize(q::Quaternion)
6165
if (q.norm)
@@ -381,3 +385,38 @@ function slerp(qa::Quaternion{T}, qb::Quaternion{T}, t::T) where {T}
381385
qa.v3 * ratio_a + qm.v3 * ratio_b,
382386
)
383387
end
388+
389+
function sylvester(a::Quaternion{T}, b::Quaternion{T}, c::Quaternion{T}) where {T<:Real}
390+
isreal(a) && return sylvester(real(a), b, c)
391+
isreal(b) && return sylvester(a, real(b), c)
392+
abs2a = abs2(a)
393+
abs2b = abs2(b)
394+
if abs2a > abs2b
395+
inva = conj(a) / abs2a
396+
d1 = -2real(b) - a - inva * abs2b
397+
x = d1 \ (c + inva * c * conj(b))
398+
else
399+
invb = conj(b) / abs2b
400+
d2 = -2real(a) - b - invb * abs2a
401+
x = (c + conj(a) * c * invb) / d2
402+
end
403+
return x
404+
end
405+
sylvester(a::Quaternion, b::Quaternion, c::Quaternion) = sylvester(promote(a, b, c)...)
406+
sylvester(a::Quaternion, b::Quaternion, c::Real) = sylvester(promote(a, b, c)...)
407+
# if either a or b commute with x, use a simpler expression
408+
sylvester(a::Real, b::Real, c::Quaternion) = c / -(a + b)
409+
sylvester(a::Real, b::Quaternion, c::Quaternion) = c / -(a + b)
410+
sylvester(a::Quaternion, b::Real, c::Quaternion) = -(a + b) \ c
411+
sylvester(a::Real, b::Quaternion, c::Real) = -c / (a + b)
412+
sylvester(a::Quaternion, b::Real, c::Real) = (a + b) \ -c
413+
414+
function lyap(a::Quaternion{T}, c::Quaternion{T}) where {T<:Real}
415+
# if a commutes with c, use a simpler expression
416+
(isreal(a) || isreal(c)) && return c / -2real(a)
417+
return (c + a \ c * a) / -4real(a)
418+
end
419+
lyap(a::Quaternion, c::Quaternion) = lyap(promote(a, c)...)
420+
# if a commutes with c, use a simpler expression
421+
lyap(a::Real, c::Quaternion) = c / -2a
422+
lyap(a::Quaternion, c::Real) = c / -2real(a)

src/Quaternions.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ __precompile__()
33
module Quaternions
44

55
import Base: +, -, *, /, ^, ==
6-
import Base: abs, abs2, angle, conj, cos, exp, inv, isfinite, log, real, sin, sqrt
6+
import Base: abs, abs2, angle, conj, cos, exp, inv, isreal, isfinite, isinf, iszero, isnan, log, real, sin, sqrt
77
import Base: convert, promote_rule, float
88
import Base: rand, randn
9-
import LinearAlgebra: norm, normalize
9+
import LinearAlgebra: lyap, norm, normalize, sylvester
1010
using Random
1111

1212
Base.@irrational INV_SQRT_EIGHT 0.3535533905932737622004 sqrt(big(0.125))

test/Quaternion.jl

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,68 @@ Base.:(/)(a::MyReal, b::Real) = a.val / b
439439
end
440440
end
441441
end
442+
443+
@testset "sylvester/lyap" begin
444+
Ts = (Float64, QuaternionF64)
445+
Ttrips = [(Ta, Tb, Tc) for Ta in Ts for Tb in Ts for Tc in Ts]
446+
Ttrips = filter(x -> any(y -> y <: Quaternion, x), Ttrips)
447+
@testset "($Ta, $Tb, $Tc)" for (Ta, Tb, Tc) in Ttrips
448+
for _ in 1:100
449+
a = randn(Ta)
450+
b = randn(Tb)
451+
c = randn(Tc)
452+
x = @inferred sylvester(a, b, c)
453+
@test a * x + x * b -c
454+
x = @inferred sylvester(b, a, c)
455+
@test b * x + x * a -c
456+
@test iszero(sylvester(a, b, zero(c)))
457+
@test sylvester(a, zero(b), c) a \ -c
458+
@test sylvester(zero(a), b, c) -c / b
459+
@test iszero(sylvester(zero(a), b, zero(c)))
460+
@test iszero(sylvester(a, zero(b), zero(c)))
461+
@test iszero(sylvester(a, b, zero(c)))
462+
# @test isnan(sylvester(zero(a), zero(b), c))
463+
464+
@test @inferred(lyap(a, c)) sylvester(a, a', c)
465+
@test @inferred(lyap(b, c)) sylvester(b, b', c)
466+
@test iszero(lyap(a, zero(c)))
467+
end
468+
@testset "nan/inf return same as for complex" begin
469+
Tza, Tzb, Tzc = map(T -> T <: Quaternion ? complex(real(T)) : T, (Ta, Tb, Tc))
470+
a, b = zero(Ta), zero(Tb)
471+
za, zb = zero(Tza), zero(Tzb)
472+
@testset for f in (one, zero, randn)
473+
x = sylvester(a, b, f(Tc))
474+
zx = sylvester(za, zb, f(Tzc))
475+
if isinf(zx)
476+
@test isinf(x)
477+
elseif isnan(zx)
478+
@test isnan(x)
479+
end
480+
if VERSION v"1.7"
481+
x = lyap(a, f(Tc))
482+
zx = lyap(za, f(Tzc))
483+
if isinf(zx)
484+
@test isinf(x)
485+
elseif isnan(zx)
486+
@test isnan(x)
487+
end
488+
end
489+
end
490+
end
491+
end
492+
@testset "rational" begin
493+
a = Quaternion(1, 2, 3, 4)
494+
b = Quaternion(1//2, 2//2, 3//2, 4//2)
495+
c = Quaternion(-1//2, 2//2, -4//2, -3//2)
496+
@test @inferred(sylvester(a, b, c)) isa Quaternion{Rational{Int}}
497+
@test @inferred(lyap(a, c)) isa Quaternion{Rational{Int}}
498+
a = randn(QuaternionF32)
499+
@test @inferred(sylvester(a, b, c)) isa QuaternionF32
500+
@test @inferred(lyap(a, c)) isa QuaternionF32
501+
null = zero(Quaternion{Rational{Int}})
502+
@test_throws DivideError sylvester(null, null, null)
503+
@test_throws DivideError lyap(null, null)
504+
end
505+
end
442506
end

0 commit comments

Comments
 (0)