diff --git a/docs/src/spacetime_algebra.md b/docs/src/spacetime_algebra.md index 0572725..5fd99ed 100644 --- a/docs/src/spacetime_algebra.md +++ b/docs/src/spacetime_algebra.md @@ -27,13 +27,13 @@ this more general algebra. One crucial new feature is the *spinor norm*: for complex quaternions, the GA reverse gives ``𝐐𝐐̃ = wΒ² + xΒ² + yΒ² + zΒ²``, which is a -*"complex"* scalar, not the Euclidean norm ``|w|Β² + |x|Β² + |y|Β² + -|z|Β²`` which will always be real. The surprising point is that the -normalization condition ``𝐐𝐐̃ = 1`` is now *two real* conditions, -because this says that the "imaginary" part is zero. This reduces the -eight real degrees of freedom in a complex quaternion down to six, -which is the correct number for a Lorentz transformation in four -dimensions. +*complex* scalar, distinct from the Euclidean norm ``|w|Β² + |x|Β² + +|y|Β² + |z|Β²`` which will always be real. The surprising point is that +the normalization condition ``𝐐𝐐̃ = 1`` is now *two real* +conditions, because this says that the "imaginary" part is zero. This +reduces the eight real degrees of freedom in a complex quaternion down +to six, which is the correct number for a Lorentz transformation in +four dimensions. ## The spacetime algebra @@ -197,8 +197,6 @@ which is not equal to ``1`` in general. Phase factors therefore do *not* belong to the Lorentz rotor group, even though they are unit elements under the Euclidean norm (``|\exp(i\varphi)| = 1``). - - !!! note "Real vs. complex quaternions" For real quaternions, ``𝐐\,\widetilde{𝐐} = \sum r_i^2 = \sum |r_i|^2``, @@ -247,11 +245,177 @@ Lorentz transformations. and confirming that a positive rapidity boosts ``t \to \cosh\varphi\,t + \sinh\varphi\,x`` and ``x \to \sinh\varphi\,t + \cosh\varphi\,x``. + +## [Iwasawa's ``KA\,N`` decomposition](@id iwasawa-kan) + +The [Iwasawa +decomposition](https://en.wikipedia.org/wiki/Iwasawa_decomposition) is +a factorization of the Lorentz group or its double cover +``\mathrm{Spin}^+(3,1)`` (or any semisimple Lie group) into three +subgroups: + +```math +G = {KA\,N}. +``` + +This decomposition is most useful when we have a preferred time axis +and preferred spatial direction. We use those to pick out boosts +along that spatial direction, and null rotations that fix the +corresponding null vector. Specifically, we have the following +subgroups: + +- **``K``** is the maximal compact subgroup, which in the case of the + Lorentz group is isomorphic to the rotation group + ``\mathrm{SO}(3)``, and in the case of ``\mathrm{Spin}(3,1)`` is + isomorphic to ``\mathrm{Spin}(3)``. +- **``A``** is the abelian subgroup of boosts in a fixed direction. + We take this direction to be the z-axis, so that we have ``A = + \left\{ \exp\left[\tfrac{φₐ}{2} \, 𝐭𝐳 \right] \mid φₐ ∈ ℝ + \right\}``. +- **``N``** is the nilpotent subgroup of null rotations, which + consists of all Lorentz transformations that can be represented as + null rotations about a fixed null vector. We take this vector to be + the null vector ``\boldsymbol{β„“}``, so that we have ``N = \left\{ + \exp\left[\tfrac{1}{2} \boldsymbol{β„“ ΞΎ}\right] \mid \boldsymbol{ΞΎ} = + ξˣ𝐱 + ξʸ𝐲 \right\}``, where ``\boldsymbol{β„“} = (𝐭+𝐳)/√2``. + +Note that ``φₐ`` here is *not* the rapidity of the overall boost if we +factor a transformation just as a boost and a rotation. Rather, it is +the rapidity of the particular boost picked out as ``A`` by this +construction. ``N`` also contributes to the overall boost, so the +overall rapidity is not just ``φₐ``. + +One nice feature of this decomposition is that we can compute the +factorization of a given Lorentz transformation ``Ξ›`` fairly simply. +We first introduce the idempotent + +```math +uβ‚Š = \frac{1}{2} (1 + 𝐭𝐳). +``` + +It is easy to verify ``uβ‚Šπ­π³ = 𝐭𝐳uβ‚Š = uβ‚Š`` and hence the idempotent +identity: ``uβ‚ŠΒ² = uβ‚Š``. Next, we write + +```math +Ξ› = Rβ‚– Rₐ Rβ‚™, +``` + +where ``Rβ‚– ∈ K``, ``Rₐ ∈ A``, and ``Rβ‚™ ∈ N``. Starting with the +rightmost factor, we have + +```math +Rβ‚™ = \exp\left[\tfrac{1}{2} \boldsymbol{β„“ ΞΎ}\right] = 1 + \frac{1}{2} \boldsymbol{β„“ ΞΎ}, +``` + +because ``\boldsymbol{β„“}`` and ``\boldsymbol{ΞΎ}`` anticommute, and +because ``\boldsymbol{β„“}Β² = 0``, their product ``\boldsymbol{β„“ ΞΎ}`` +itself is also nilpotent. (Remember, that's what the "N" stands for.) +We can simply compute the product ``Rβ‚™ uβ‚Š`` by using the fact that +``\boldsymbol{ΞΎ} uβ‚Š = uβ‚Š \boldsymbol{ΞΎ}`` and expanding terms in our +basis, then find + +```math +Rβ‚™ uβ‚Š %&= \left(1 + \frac{1}{2} \boldsymbol{β„“ ΞΎ}\right) uβ‚Š, \\ +% &= uβ‚Š + \frac{1}{2} \boldsymbol{β„“ ΞΎ} uβ‚Š, \\ +% &= uβ‚Š + \frac{1}{2} \boldsymbol{β„“} uβ‚Š \boldsymbol{ΞΎ}, \\ +% &= uβ‚Š + \frac{1}{4\sqrt{2}} (𝐭+𝐳) (1+𝐭𝐳) \boldsymbol{ΞΎ}, \\ +% &= uβ‚Š + \frac{1}{4\sqrt{2}} (𝐭+𝐳+𝐭𝐭𝐳+𝐳𝐭𝐳) \boldsymbol{ΞΎ}, \\ +% &= uβ‚Š + \frac{1}{4\sqrt{2}} (𝐭+𝐳-𝐳-𝐭) \boldsymbol{ΞΎ}, \\ += uβ‚Š. +``` + +Now, with + +```math +Rₐ = \exp\left[\tfrac{φₐ}{2} \, 𝐭𝐳 \right] += \cosh\left(\frac{φₐ}{2}\right) + \sinh\left(\frac{φₐ}{2}\right) 𝐭𝐳, +``` + +and the fact that ``𝐭𝐳uβ‚Š = uβ‚Š``, we have + +```math +\begin{aligned} +Rₐ uβ‚Š +&= \left[\cosh\left(\frac{φₐ}{2}\right) + \sinh\left(\frac{φₐ}{2}\right)\right] uβ‚Š +&= e^{φₐ/2} uβ‚Š. +\end{aligned} +``` + +Putting these together, we have + +```math +Ξ› uβ‚Š = e^{φₐ/2} Rβ‚– uβ‚Š. +``` + +Recall that ``e^{φₐ/2}`` is a strictly positive real number, and +``Rβ‚–`` is a pure rotation so it is "β„‚-real" β€” meaning that it is a +linear combination of only basis elements that do not have a factor of +``𝐭`` β€” whereas ``uβ‚Š`` is just a combination of 1 and a "β„‚-imaginary" +part. Therefore, we can take the real part of this expression to find + +```math +β„‚\Re\{Ξ› uβ‚Š\} = \frac{e^{φₐ/2}}{2} Rβ‚–. +``` + +This is just a strictly positive real number times ``Rβ‚–``, which we +know must have unit magnitude, so we can find ``Rβ‚–`` by normalizing +this real part: + +```math +Rβ‚– = \mathrm{normalize}\left(β„‚\Re\{Ξ› uβ‚Š\}\right). +``` + +We can continue solving for the remaining factors ``Rₐ`` and ``Rβ‚™``. +One option would be to take the norm of the expression above and solve +for ``φₐ``, but it requires no transcendental functions and is more +numerically stable to extract the components directly from the +remaining product. We can evaluate ``RₐRβ‚™ = Rβ‚–^{-1} Ξ›``. But now, +let us examine the left-hand side analytically: + +```math +Rₐ Rβ‚™ = \left[\cosh\left(\frac{φₐ}{2}\right) + \sinh\left(\frac{φₐ}{2}\right) 𝐭𝐳\right] + \left[1 + \frac{1}{2} \boldsymbol{β„“ ΞΎ}\right] +``` + +The ``\boldsymbol{β„“ ΞΎ}`` term is entirely within the span of ``\{𝐭𝐱, +𝐭𝐲, 𝐳𝐱, 𝐳𝐲\}``; therefore, it is not hard to show that the +scalar and ``𝐭𝐳`` terms in this product equal ``Rₐ``. As usual, we +write ``βŸ¨Β·βŸ©β‚€`` to extract the scalar (grade‑0) part, and we have + +```math +Rₐ = \left⟨ Rβ‚–^{-1} Ξ› \rightβŸ©β‚€ + \left⟨ Rβ‚–^{-1} Ξ›\, 𝐭𝐳 \rightβŸ©β‚€\, 𝐭𝐳. +``` + +Finally, we can find the remaining factor by rearranging the original +factorization: + +```math +Rβ‚™ = Rₐ^{-1} Rβ‚–^{-1} Ξ›. +``` + +This decomposition is implemented in the [`Quaternionic.KAN`](@ref) +function. + ## API reference -```@autodocs -Modules = [Quaternionic] -Pages = ["Lorentz.jl"] +```@meta +CurrentModule = Quaternionic +``` + +```@docs +Lorentz +Lorentz(::AbstractVector) +Boost +ga_components +RB +BR +Rv +vR +KAN +β„‚real +β„‚imag +β„‚reim +β„‚conj ``` ## Further reading diff --git a/src/Lorentz.jl b/src/Lorentz.jl index bc968e9..c20c360 100644 --- a/src/Lorentz.jl +++ b/src/Lorentz.jl @@ -268,7 +268,7 @@ This is distinct from the GA "real" part, given by `real(Ξ›)`, which is construc reverse, rather than the complex-conjugate. That will just be the scalar part of `Ξ›`, but may be complex; this function returns a full quaternion, with all components real. """ -β„‚real(Ξ›::Lorentz{T}) where {T<:Real} = Quaternion{T}( +β„‚real(Ξ›::AbstractQuaternion{Complex{T}}) where {T<:Real} = Quaternion{T}( real(Ξ›[1]), real(Ξ›[2]), real(Ξ›[3]), real(Ξ›[4]) ) @@ -285,7 +285,7 @@ the reverse, rather than the complex-conjugate. That will just be the quaternio part of `Ξ›`, but may be complex; this function returns a full quaternion, with all components real. """ -β„‚imag(Ξ›::Lorentz{T}) where {T<:Real} = Quaternion{T}( +β„‚imag(Ξ›::AbstractQuaternion{Complex{T}}) where {T<:Real} = Quaternion{T}( imag(Ξ›[1]), imag(Ξ›[2]), imag(Ξ›[3]), imag(Ξ›[4]) ) @@ -323,7 +323,8 @@ find `cosh(Ξ·/2)` as the magnitude of the complex-real part of `Ξ›`. Then, we f `sinh(Ξ·/2)*nΜ‚` simply by multiplying the complex-imaginary part of `Ξ›` by `inv(R)`, and reconstruct `B` simply by adding the result to `cosh(Ξ·/2)`. -See also [`BR`](@ref). +See also [`BR`](@ref), [`vR`](@ref), and [`Rv`](@ref) for similar forms, and [`KAN`](@ref) +for the Iwasawa decomposition. """ function RB(Ξ›::Lorentz{T}) where {T<:Real} β„œΞ›, β„‘Ξ› = β„‚reim(Ξ›) @@ -339,6 +340,9 @@ Polar decomposition `Ξ› = B * R`: pure boost `B` followed by pure rotation `R`. The algorithm here is the same as for [`RB`](@ref), but with the order of multiplication by `inv(R)` reversed. + +See also [`vR`](@ref) and [`Rv`](@ref) for similar forms, and [`KAN`](@ref) for the Iwasawa +decomposition. """ function BR(Ξ›::Lorentz{T}) where {T<:Real} β„œΞ›, β„‘Ξ› = β„‚reim(Ξ›) @@ -370,7 +374,8 @@ where the last equality uses the double-angle identity ``\cosh(Ξ·) = 2\cosh^2(Ξ· The last form is made up of entirely of the scalar component of `B`, does not involve any cancellation or division by small numbers, and avoids computing the norm of the vector part. -See also [`vR`](@ref), [`RB`](@ref), and [`BR`](@ref). +See also [`vR`](@ref), [`RB`](@ref), and [`BR`](@ref) for similar forms, and [`KAN`](@ref) +for the Iwasawa decomposition. """ function Rv(Ξ›::Lorentz{T}) where {T<:Real} R, B = RB(Ξ›) @@ -386,7 +391,8 @@ end Return the boost velocity `vβƒ—` and pure rotation `R` such that `Ξ› = Boost(Ξ·, vΜ‚) * R`. See [`Rv`](@ref) for more details. -See also [`BR`](@ref) and [`RB`](@ref). +See also [`BR`](@ref) and [`RB`](@ref) for similar forms, and [`KAN`](@ref) for the Iwasawa +decomposition. """ function vR(Ξ›::Lorentz{T}) where {T<:Real} B, R = BR(Ξ›) @@ -395,3 +401,45 @@ function vR(Ξ›::Lorentz{T}) where {T<:Real} vβƒ— = vΜ‚sinhΞ·β•±2 * (2coshΞ·β•±2 / (2coshΞ·β•±2^2 - 1)) return vβƒ—, R end + +# --------------------------------------------------------------------------- +# Iwasawa's KAN decomposition +# --------------------------------------------------------------------------- + +@doc raw""" + KAN(Ξ›::Lorentz{T}) β†’ (Rβ‚–, Rₐ, Rβ‚™) + +Compute the Iwasawa ``KAN`` decomposition of the Lorentz transformation `Ξ›`, returning the +three factors such that `Ξ› = Rβ‚– * Rₐ * Rβ‚™`, where + +- `Rβ‚– ∈ K` is a pure rotation (the maximal compact factor), +- `Rₐ ∈ A` is a boost along the ``𝐳`` axis, `Rₐ = cosh(φₐ/2) + sinh(φₐ/2) 𝐭𝐳`, and +- `Rβ‚™ ∈ N` is a null rotation fixing the null vector ``β„“ = (𝐭+𝐳)/√2``. + +See the [`KAN` decomposition section](@ref iwasawa-kan) of the spacetime-algebra +documentation for a full discussion and derivation of the method used in this function. + +See also [`BR`](@ref), [`RB`](@ref), [`vR`](@ref), and [`Rv`](@ref) for polar +decompositions. +""" +function KAN(Ξ›::Lorentz{T}) where {T<:Real} + 𝐭𝐳 = im * 𝐀 + uβ‚Š = (1 + 𝐭𝐳) / T(2) + # We readily find the rotation factor by projecting with the idempotent, taking the pure + # spatial part, and normalizing. + β„‚β„œΞ›uβ‚Š = β„‚real(Ξ› * uβ‚Š) + Rβ‚– = rotor(β„‚β„œΞ›uβ‚Š) + # We avoid transcendental functions `ln`, `sinh`, and `cosh` by using the structure of + # the following product to read off just the relevant components. + RₐRβ‚™ = conj(Rβ‚–) * Ξ› + coshΟ†β•±2, sinhΟ†β•±2 = real(RₐRβ‚™.w), imag(RₐRβ‚™.z) + Rₐ = coshΟ†β•±2 + sinhΟ†β•±2 * 𝐭𝐳 + # Rather than returning the raw `Rₐ⁻¹ * RₐRβ‚™`, which may have accumulated roundoff + # errors, we project that result onto the null-rotation sector by averaging 𝐒 and 𝐣 + # components, and setting 𝟏 and 𝐀 components to their exact values. + Rβ‚™raw = conj(Rₐ) * RₐRβ‚™ + ΞΎΛ£β•±2sqrt2 = (imag(Rβ‚™raw.x) - real(Rβ‚™raw.y)) / 2 + ΞΎΚΈβ•±2sqrt2 = (imag(Rβ‚™raw.y) + real(Rβ‚™raw.x)) / 2 + Rβ‚™ = Lorentz{T}(1, ΞΎΚΈβ•±2sqrt2 + im * ΞΎΛ£β•±2sqrt2, -ΞΎΛ£β•±2sqrt2 + im * ΞΎΚΈβ•±2sqrt2, 0) + return Rβ‚–, Rₐ, Rβ‚™ +end diff --git a/src/Quaternionic.jl b/src/Quaternionic.jl index 677c80b..06bae6e 100644 --- a/src/Quaternionic.jl +++ b/src/Quaternionic.jl @@ -15,7 +15,7 @@ export QuatVec, quatvec, QuatVecF64, QuatVecF32, QuatVecF16 export components, basetype public value, iszerovalue public β„‚conj, β„‚real, β„‚imag, β„‚reim -public RB, BR, Rv, vR +public RB, BR, Rv, vR, KAN export (β‹…), (Γ—), (Γ—Μ‚), normalize, norm export abs2vec, absvec export from_float_array, to_float_array, diff --git a/test/lorentz_decomposition.jl b/test/lorentz_decomposition.jl index 874b837..bb15b88 100644 --- a/test/lorentz_decomposition.jl +++ b/test/lorentz_decomposition.jl @@ -10,7 +10,7 @@ using Random: Xoshiro using Quaternionic using DoubleFloats: Double64 - import Quaternionic: RB, BR, β„‚conj, β„‚real, β„‚imag, β„‚reim + import Quaternionic: RB, BR, KAN, β„‚conj, β„‚real, β„‚imag, β„‚reim const n = 20 @@ -57,6 +57,35 @@ abs(imag(cs[1])) < atol && all(c -> abs(real(c)) < atol, cs[2:4]) end + + # ── KAN-specific helpers ─────────────────────────────────────────────────── + + # A null rotation in N (fixing β„“ = (𝐭+𝐳)/√2), built from screw parameters ΞΎΛ£, ΞΎΚΈ + # using the same encoding as `KAN` returns for its Rβ‚™ factor. + function null_rotation(::Type{T}, ΞΎΛ£, ΞΎΚΈ) where {T} + a, b = T(ΞΎΛ£)/sqrt(T(2)), T(ΞΎΚΈ)/sqrt(T(2)) + Lorentz{T}(1, (im*a + b)/2, (im*b - a)/2, 0) + end + + # A boost along the z-axis with rapidity Ο† β€” an element of A. + z_boost(::Type{T}, Ο†) where {T} = Boost(T(Ο†), QuatVec{T}(0, 0, 1)) + + # The null vector β„“ = (𝐭+𝐳)/√2 fixed by N, as a Minkowski 4-vector. + β„“_vec(::Type{T}) where {T} = T[1, 0, 0, 1] / sqrt(T(2)) + + # Structural predicate for A: pure z-boost (w real, x=y=0, z pure imaginary). + # Accepts any AbstractQuaternion (KAN returns Rₐ as a bare `Quaternion`). + function is_z_boost(R; atol) + w, x, y, z = components(R) + abs(imag(w)) < atol && abs(x) < atol && abs(y) < atol && abs(real(z)) < atol + end + + # Structural predicate for N: null rotation (w=1, z=0, Re(x)=Im(y), Im(x)=βˆ’Re(y)). + function is_null_rotation(R; atol) + w, x, y, z = components(R) + abs(w - 1) < atol && abs(z) < atol && + abs(real(x) - imag(y)) < atol && abs(imag(x) + real(y)) < atol + end end # ── Special cases ───────────────────────────────────────────────────────────── @@ -381,3 +410,185 @@ end @test vr_error(Double64) ≀ 10eps(Double64) @test vr_error(BigFloat) ≀ 10eps(BigFloat) end + +# ── KAN (Iwasawa) decomposition ──────────────────────────────────────────────── +# +# KAN(Ξ›) β†’ (Rβ‚–, Rₐ, Rβ‚™) with Ξ› = Rβ‚–Β·Rₐ·Rβ‚™, where Rβ‚– ∈ K is a rotation, Rₐ ∈ A is a +# boost along 𝐳, and Rβ‚™ ∈ N is a null rotation fixing β„“ = (𝐭+𝐳)/√2. Errors scale +# with the overall transformation magnitude, captured by `maximum(abs, components(Ξ›))`. + +@testitem "KAN: reconstruction round-trip" tags=[:validation, :fast] setup=[LorentzDecompData] begin + import Quaternionic: KAN, components + using .LorentzDecompData: mixed, rand_rotations, rand_boosts, same_Ξ› + using DoubleFloats: Double64 + for T ∈ (Float32, Float64, Double64) + for Ξ› ∈ [mixed(T); rand_rotations(T); rand_boosts(T)] + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + Ξ΅ = 1024eps(T) * maximum(abs, components(Ξ›)) + @test same_Ξ›(Lorentz(Rβ‚–) * Lorentz(Rₐ) * Lorentz(Rβ‚™), Ξ›; atol=Ξ΅) + end + end +end + +@testitem "KAN: structural membership (K, A, N)" tags=[:validation, :fast] setup=[LorentzDecompData] begin + import Quaternionic: KAN, components + using .LorentzDecompData: mixed, rand_rotations, rand_boosts, is_real_rotor, is_z_boost, is_null_rotation + using DoubleFloats: Double64 + for T ∈ (Float32, Float64, Double64) + for Ξ› ∈ [mixed(T); rand_rotations(T); rand_boosts(T)] + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + Ξ΅ = 1024eps(T) * maximum(abs, components(Ξ›)) + @test is_real_rotor(Rβ‚–; atol=Ξ΅) # Rβ‚– ∈ K: pure rotation (no boost part) + @test is_z_boost(Rₐ; atol=Ξ΅) # Rₐ ∈ A: boost along 𝐳 only + @test is_null_rotation(Rβ‚™; atol=Ξ΅) # Rβ‚™ ∈ N: null rotation, w=1, z=0 + end + end +end + +@testitem "KAN: fixed-point oracles" tags=[:validation, :fast] setup=[LorentzDecompData] begin + import Quaternionic: KAN, components + using .LorentzDecompData: mixed, rand_rotations, rand_boosts, β„“_vec + using DoubleFloats: Double64 + # K fixes the time axis 𝐭; A (boost along 𝐳) fixes the transverse axes 𝐱, 𝐲; + # N (null rotation) fixes the null vector β„“ = (𝐭+𝐳)/√2. + for T ∈ (Float32, Float64, Double64) + 𝐭, 𝐱, 𝐲 = T[1,0,0,0], T[0,1,0,0], T[0,0,1,0] + β„“ = β„“_vec(T) + for Ξ› ∈ [mixed(T); rand_rotations(T); rand_boosts(T)] + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + Ξ΅ = 1024eps(T) * maximum(abs, components(Ξ›)) + @test isapprox(Lorentz(Rβ‚–)(𝐭), 𝐭; atol=Ξ΅) + @test isapprox(Lorentz(Rₐ)(𝐱), 𝐱; atol=Ξ΅) + @test isapprox(Lorentz(Rₐ)(𝐲), 𝐲; atol=Ξ΅) + @test isapprox(Lorentz(Rβ‚™)(β„“), β„“; atol=Ξ΅) + end + end +end + +@testitem "KAN: special cases" tags=[:unit, :fast, :validation] setup=[LorentzDecompData] begin + import Quaternionic: KAN + using .LorentzDecompData: rand_rotations, null_rotation, z_boost, same_Ξ› + using DoubleFloats: Double64 + for T ∈ (Float32, Float64, Double64) + Ξ΅ = 64eps(T) + πŸ™ = one(Lorentz{T}) + + # Identity and minus-identity β†’ all three factors trivial + for Ξ› ∈ (πŸ™, -πŸ™) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + @test same_Ξ›(Lorentz(Rβ‚–), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rₐ), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚™), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚–) * Lorentz(Rₐ) * Lorentz(Rβ‚™), Ξ›; atol=Ξ΅) + end + + # Pure rotation β†’ Rβ‚– β‰ˆ Ξ›, Rₐ β‰ˆ Rβ‚™ β‰ˆ πŸ™ + for Ξ› ∈ rand_rotations(T) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + @test same_Ξ›(Lorentz(Rβ‚–), Ξ›; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rₐ), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚™), πŸ™; atol=Ξ΅) + end + + # Pure z-boost β†’ Rₐ β‰ˆ Ξ›, Rβ‚– β‰ˆ Rβ‚™ β‰ˆ πŸ™ + for Ο† ∈ T[0.3, 1.0, 2.5] + Ξ› = z_boost(T, Ο†) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + @test same_Ξ›(Lorentz(Rβ‚–), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rₐ), Ξ›; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚™), πŸ™; atol=Ξ΅) + end + + # Pure null rotation β†’ Rβ‚™ β‰ˆ Ξ›, Rβ‚– β‰ˆ Rₐ β‰ˆ πŸ™ + for (ΞΎΛ£, ΞΎΚΈ) ∈ [(T(0.4), T(-0.7)), (T(1.5), T(0.0)), (T(0.0), T(2.0))] + Ξ› = null_rotation(T, ΞΎΛ£, ΞΎΚΈ) + Ξ΅β‚™ = 64eps(T) * maximum(abs, Quaternionic.components(Ξ›)) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + @test same_Ξ›(Lorentz(Rβ‚–), πŸ™; atol=Ξ΅β‚™) + @test same_Ξ›(Lorentz(Rₐ), πŸ™; atol=Ξ΅β‚™) + @test same_Ξ›(Lorentz(Rβ‚™), Ξ›; atol=Ξ΅β‚™) + end + end +end + +@testitem "KAN: factor recovery (uniqueness)" tags=[:validation, :fast] setup=[LorentzDecompData] begin + import Quaternionic: KAN, components + using .LorentzDecompData: rand_rotations, null_rotation, z_boost, same_Ξ› + using DoubleFloats: Double64 + # Build Ξ› from known canonical factors Kβ‚€Β·Aβ‚€Β·Nβ‚€ and verify KAN recovers each. + # The Iwasawa decomposition is unique, so recovery is an oracle test. + for T ∈ (Float32, Float64, Double64) + for Kβ‚€ ∈ rand_rotations(T) + for Ο† ∈ T[0.5, 2.0] + Aβ‚€ = z_boost(T, Ο†) + for (ΞΎΛ£, ΞΎΚΈ) ∈ [(T(0.4), T(-0.7)), (T(1.3), T(0.9))] + Nβ‚€ = null_rotation(T, ΞΎΛ£, ΞΎΚΈ) + Ξ› = Kβ‚€ * Aβ‚€ * Nβ‚€ + Ξ΅ = 1024eps(T) * maximum(abs, components(Ξ›)) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + @test same_Ξ›(Lorentz(Rβ‚–), Kβ‚€; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rₐ), Aβ‚€; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚™), Nβ‚€; atol=Ξ΅) + end + end + end + end +end + +@testitem "KAN: idempotency (decomposing a factor)" tags=[:validation, :fast] setup=[LorentzDecompData] begin + import Quaternionic: KAN, components + using .LorentzDecompData: rand_rotations, null_rotation, z_boost, same_Ξ› + using DoubleFloats: Double64 + # Decomposing an element that already lives in a single subgroup returns it in + # the matching slot and the identity in the other two. + for T ∈ (Float32, Float64, Double64) + πŸ™ = one(Lorentz{T}) + Ξ΅ = 64eps(T) + + for Kβ‚€ ∈ rand_rotations(T) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Kβ‚€) + @test same_Ξ›(Lorentz(Rβ‚–), Kβ‚€; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rₐ), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚™), πŸ™; atol=Ξ΅) + end + + for Ο† ∈ T[0.5, 2.0] + Aβ‚€ = z_boost(T, Ο†) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Aβ‚€) + @test same_Ξ›(Lorentz(Rβ‚–), πŸ™; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rₐ), Aβ‚€; atol=Ξ΅) + @test same_Ξ›(Lorentz(Rβ‚™), πŸ™; atol=Ξ΅) + end + + for (ΞΎΛ£, ΞΎΚΈ) ∈ [(T(0.4), T(-0.7)), (T(1.3), T(0.9))] + Nβ‚€ = null_rotation(T, ΞΎΛ£, ΞΎΚΈ) + Ξ΅β‚™ = 64eps(T) * maximum(abs, components(Nβ‚€)) + Rβ‚–, Rₐ, Rβ‚™ = KAN(Nβ‚€) + @test same_Ξ›(Lorentz(Rβ‚–), πŸ™; atol=Ξ΅β‚™) + @test same_Ξ›(Lorentz(Rₐ), πŸ™; atol=Ξ΅β‚™) + @test same_Ξ›(Lorentz(Rβ‚™), Nβ‚€; atol=Ξ΅β‚™) + end + end +end + +@testitem "KAN: multi-precision convergence" tags=[:validation, :slow] begin + import Quaternionic: KAN, components + using DoubleFloats: Double64 + # Algebraically direct algorithm: reconstruction error stays at the machine-epsilon + # floor across precisions (no Float64-specific constants or thresholds). + function recon_error(T) + c = sin(T(13)/20) / √T(3) + Kβ‚€ = Lorentz(rotor(cos(T(13)/20), c, c, c)) + Aβ‚€ = Boost(T(8)/10, QuatVec{T}(0, 0, 1)) + a, b = T(4)/10/√T(2), T(-7)/10/√T(2) + Nβ‚€ = Lorentz{T}(1, (im*a + b)/2, (im*b - a)/2, 0) + Ξ› = Kβ‚€ * Aβ‚€ * Nβ‚€ + Rβ‚–, Rₐ, Rβ‚™ = KAN(Ξ›) + recon = Lorentz(Rβ‚–) * Lorentz(Rₐ) * Lorentz(Rβ‚™) + maximum(abs, components(recon) - components(Ξ›)) + end + @test recon_error(Float32) ≀ 10eps(Float32) + @test recon_error(Float64) ≀ 10eps(Float64) + @test recon_error(Double64) ≀ 10eps(Double64) + @test recon_error(BigFloat) ≀ 10eps(BigFloat) +end