From 93368775ff49e4c6df7bacec6f7a4e3feb2170dd Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 4 Dec 2024 23:40:10 -0500 Subject: [PATCH 001/329] Add explicit Condon-Shortley computation --- Project.toml | 4 +- docs/src/references.bib | 13 +++ test/conventions/condon_shortley.jl | 133 ++++++++++++++++++++++++++++ test/utilities/nanchecker.jl | 6 ++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 test/conventions/condon_shortley.jl diff --git a/Project.toml b/Project.toml index e6d4df97..d01630b9 100644 --- a/Project.toml +++ b/Project.toml @@ -26,6 +26,7 @@ Coverage = "1.6" DoubleFloats = "1" ForwardDiff = "0.10, 1" FFTW = "1" +FastDifferentiation = "0.3.17" FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17" Hwloc = "2, 3" LinearAlgebra = "1" @@ -47,6 +48,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FastDifferentiation = "eb9bf01b-bf85-4b60-bf87-ee5de06c00be" FastTransforms = "057dd010-8810-581a-b7be-e3fc3b93f78c" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Hwloc = "0e44f5e4-bd66-52a0-8798-143a42290a1d" @@ -61,4 +63,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastTransforms", "ForwardDiff", "Hwloc", "LinearAlgebra", "Logging", "OffsetArrays", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] +test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "Hwloc", "LinearAlgebra", "Logging", "OffsetArrays", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] diff --git a/docs/src/references.bib b/docs/src/references.bib index 7fde3911..22bd3b1b 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -221,6 +221,19 @@ @book{TorresDelCastillo_2003 year = 2003, } +@article{UffordShortley_1932, + title = {Atomic Eigenfunctions and Energies}, + volume = 42, + url = {https://link.aps.org/doi/10.1103/PhysRev.42.167}, + doi = {10.1103/PhysRev.42.167}, + number = 2, + journal = {Physical Review}, + author = {Ufford, C. W. and Shortley, G. H.}, + month = oct, + year = 1932, + pages = {167--175} +} + @article{Waldvogel_2006, doi = {10.1007/s10543-006-0045-4}, url = {https://doi.org/10.1007/s10543-006-0045-4}, diff --git a/test/conventions/condon_shortley.jl b/test/conventions/condon_shortley.jl new file mode 100644 index 00000000..277d6b61 --- /dev/null +++ b/test/conventions/condon_shortley.jl @@ -0,0 +1,133 @@ +raw""" +Formulas and conventions from [Condon and Shortley's "The Theory Of Atomic Spectra"](@cite +CondonShortley_1935). + +The method we use here is as direct and explicit as possible. In particular, Condon and +Shortley provide a formula for the φ=0 part in terms of iterated derivatives of a power of +sin(θ). Rather than expressing these derivatives in terms of the Legendre polynomials — +which would subject us to another round of ambiguity — the functions in this module use +automatic differentiation to compute the derivatives explicitly. + +The result is that the original Condon-Shortley spherical harmonics agree perfectly with the +ones computed by this package. + +Note that Condony and Shortley do not give an explicit formula for what are now called the +Wigner D-matrices. + +""" +@testmodule CondonShortley begin + +import FastDifferentiation + +const 𝒾 = im + +include("../utilities/naive_factorial.jl") +import .NaiveFactorials: ❗ + + +""" + Θ(ℓ, m, θ) + +Equation (15) of section 4³ (page 52) of [Condon-Shortley](@cite CondonShortley_1935), +implementing +```math + Θ(ℓ, m), +``` +which is implicitly a function of the spherical coordinate ``θ``. +""" +function Θ(ℓ, m, θ::T) where {T} + (-1)^ℓ * T(√(((2ℓ+1) * (ℓ+m)❗) / (2 * (ℓ - m)❗)) * (1 / (2^ℓ * (ℓ)❗))) * + (1 / sin(θ)^T(m)) * dʲsin²ᵏθdcosθʲ(ℓ-m, ℓ, θ) +end + + +@doc raw""" + dʲsin²ᵏθdcosθʲ(j, k, θ) + +Compute the ``j``th derivative of the function ``\sin^{2k}(θ)`` with respect to ``\cos(θ)``. +Note that ``\sin^{2k}(θ) = (1 - \cos^2(θ))^k``, so this is equivalent to evaluating the +``j``th derivative of ``(1-x^2)^k`` with respect to ``x``, evaluated at ``x = \cos(θ)``. +""" +function dʲsin²ᵏθdcosθʲ(j, k, θ) + if j < 0 + throw(ArgumentError("j=$j must be non-negative")) + end + if j == 0 + return sin(θ)^(2k) + end + x = FastDifferentiation.make_variables(:x)[1] + ∂ₓʲfᵏ = FastDifferentiation.derivative((1 - x^2)^k, (x for _ ∈ 1:j)...) + return FastDifferentiation.make_function([∂ₓʲfᵏ,], [x,])(cos(θ))[1] +end + + +""" + Φ(mₗ, φ) + +Equation (5) of section 4³ (page 50) of [Condon-Shortley](@cite CondonShortley_1935), +implementing +```math + Φ(mₗ), +``` +which is implicitly a function of the spherical coordinate ``φ``. +""" +function Φ(mₗ, φ::T) where {T} + 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) +end + + +""" + ϕ(ℓ, m, θ, φ) + +Spherical harmonics. This is defined as such below Eq. (5) of section 5⁵ (page 127) of +[Condon-Shortley](@cite CondonShortley_1935), implementing +```math + ϕ(ℓ, mₗ), +``` +which is implicitly a function of the spherical coordinates ``θ`` and ``φ``. +""" +function ϕ(ℓ, mₗ, θ, φ) + Θ(ℓ, mₗ, θ) * Φ(mₗ, φ) +end + + +end # @testmodule CondonShortley + + +@testitem "Condon-Shortley conventions" setup=[Utilities, NaNChecker, CondonShortley] begin + using Random + using Quaternionic: from_spherical_coordinates + const check = NaNChecker.NaNCheck + + Random.seed!(1234) + const T = Float64 + const ℓₘₐₓ = 4 + ϵₐ = 4eps(T) + ϵᵣ = 1000eps(T) + + # Tests for Y(ℓ, m, θ, ϕ) + let Y=CondonShortley.ϕ, ϕ=zero(T) + for θ ∈ βrange(T) + if abs(sin(θ)) < ϵₐ + continue + end + + # Test Eq. (2.6) of [Goldberg et al.](@cite GoldbergEtAl_1967) + for ℓ ∈ 0:ℓₘₐₓ + for m ∈ -ℓ:ℓ + # Y(ℓ, m, check(θ), check(ϕ)) + # Y(ℓ, -m, check(θ), check(ϕ)) + @test conj(Y(ℓ, m, θ, ϕ)) ≈ (-1)^(m) * Y(ℓ, -m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end + end + + # Compare to SphericalHarmonics Y + let s = 0 + Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + Y₂ = [Y(ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] + @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ + end + end + end + +end diff --git a/test/utilities/nanchecker.jl b/test/utilities/nanchecker.jl index ec5b858f..69b65462 100644 --- a/test/utilities/nanchecker.jl +++ b/test/utilities/nanchecker.jl @@ -29,6 +29,9 @@ struct NaNCheck{T<:Real} <: Real end end export NaNCheck +function NaNCheck(a::T) where {T<:Real} + NaNCheck{T}(a) +end Base.isnan(a::NaNCheck{T}) where{T} = isnan(a.val) Base.isinf(a::NaNCheck{T}) where{T} = isinf(a.val) Base.typemin(::Type{NaNCheck{T}}) where{T} = NaNCheck{T}(typemin(T)) @@ -48,6 +51,9 @@ Base.promote_rule(::Type{S}, ::Type{NaNCheck{T}}) where {T<:Number, S<:Number} = Base.promote_rule(::Type{NaNCheck{T}}, ::Type{S}) where {T<:Number, S<:Number} = NaNCheck{promote_type(T,S)} Base.promote_rule(::Type{NaNCheck{S}}, ::Type{NaNCheck{T}}) where {T<:Number, S<:Number} = NaNCheck{promote_type(T,S)} +# This needs to be here to avoid an ambiguity +Base.promote_rule(::Type{BigFloat}, ::Type{NaNCheck{T}}) where T<:Number = NaNCheck{promote_type(T,BigFloat)} + for op = (:sin, :cos, :tan, :log, :exp, :sqrt, :abs, :-, :atan, :acos, :asin, :log1p, :floor, :ceil, :float) eval(quote function Base.$op(a::NaNCheck{T}) where{T} From 935ee050462990a1716dc684d8210c9e23970b1d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 5 Dec 2024 09:44:42 -0500 Subject: [PATCH 002/329] Test Condon-Shortley's explicit formulas --- test/conventions/condon_shortley.jl | 54 +++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/test/conventions/condon_shortley.jl b/test/conventions/condon_shortley.jl index 277d6b61..3fb92eb1 100644 --- a/test/conventions/condon_shortley.jl +++ b/test/conventions/condon_shortley.jl @@ -90,14 +90,40 @@ function ϕ(ℓ, mₗ, θ, φ) Θ(ℓ, mₗ, θ) * Φ(mₗ, φ) end +@doc raw""" + ϴ(ℓ, m, θ) + +Explicit formulas for the first few spherical harmonics as given by Condon-Shortley in the +footnote to Eq. (15) of Sec. 4³ (page 52). + +Note that the name of this function is `\varTheta`, as opposed to the `\Theta` function +that implements Condon-Shortley's general form. +""" +ϴ(ℓ, m, θ) = ϴ(Val(ℓ), Val(m), θ) / √(2π) +ϴ(::Val{0}, ::Val{0}, θ) = √(1/2) +ϴ(::Val{1}, ::Val{0}, θ) = √(3/2) * cos(θ) +ϴ(::Val{2}, ::Val{0}, θ) = √(5/8) * (2cos(θ)^2 - sin(θ)^2) +ϴ(::Val{3}, ::Val{0}, θ) = √(7/8) * (2cos(θ)^3 - 3cos(θ)sin(θ)^2) +ϴ(::Val{1}, ::Val{+1}, θ) = -√(3/4) * sin(θ) +ϴ(::Val{1}, ::Val{-1}, θ) = +√(3/4) * sin(θ) +ϴ(::Val{2}, ::Val{+1}, θ) = -√(15/4) * cos(θ) * sin(θ) +ϴ(::Val{2}, ::Val{-1}, θ) = +√(15/4) * cos(θ) * sin(θ) +ϴ(::Val{3}, ::Val{+1}, θ) = -√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) +ϴ(::Val{3}, ::Val{-1}, θ) = +√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) +ϴ(::Val{2}, ::Val{+2}, θ) = √(15/16) * sin(θ)^2 +ϴ(::Val{2}, ::Val{-2}, θ) = √(15/16) * sin(θ)^2 +ϴ(::Val{3}, ::Val{+2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 +ϴ(::Val{3}, ::Val{-2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 +ϴ(::Val{3}, ::Val{+3}, θ) = -√(35/32) * sin(θ)^3 +ϴ(::Val{3}, ::Val{-3}, θ) = +√(35/32) * sin(θ)^3 end # @testmodule CondonShortley -@testitem "Condon-Shortley conventions" setup=[Utilities, NaNChecker, CondonShortley] begin +@testitem "Condon-Shortley conventions" setup=[Utilities, CondonShortley] begin using Random using Quaternionic: from_spherical_coordinates - const check = NaNChecker.NaNCheck + #const check = NaNChecker.NaNCheck Random.seed!(1234) const T = Float64 @@ -106,18 +132,32 @@ end # @testmodule CondonShortley ϵᵣ = 1000eps(T) # Tests for Y(ℓ, m, θ, ϕ) - let Y=CondonShortley.ϕ, ϕ=zero(T) + let Y=CondonShortley.ϕ, Θ=CondonShortley.Θ, ϴ=CondonShortley.ϴ, ϕ=zero(T) for θ ∈ βrange(T) if abs(sin(θ)) < ϵₐ continue end - # Test Eq. (2.6) of [Goldberg et al.](@cite GoldbergEtAl_1967) + # # Find where NaNs are coming from + # for ℓ ∈ 0:ℓₘₐₓ + # for m ∈ -ℓ:ℓ + # Θ(ℓ, m, check(θ)) + # end + # end + + # Test footnote to Eq. (15) of Sec. 4³ of Condon-Shortley + let Y = ₛ𝐘(0, 3, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + for ℓ ∈ 0:3 + for m ∈ -ℓ:ℓ + @test ϴ(ℓ, m, θ) ≈ Y[Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ + end + end + end + + # Test Eq. (18) of Sec. 4³ of Condon-Shortley for ℓ ∈ 0:ℓₘₐₓ for m ∈ -ℓ:ℓ - # Y(ℓ, m, check(θ), check(ϕ)) - # Y(ℓ, -m, check(θ), check(ϕ)) - @test conj(Y(ℓ, m, θ, ϕ)) ≈ (-1)^(m) * Y(ℓ, -m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test Θ(ℓ, m, θ) ≈ (-1)^(m) * Θ(ℓ, -m, θ) atol=ϵₐ rtol=ϵᵣ end end From a544097feb946cac8626e84abc6ef9fe745fcdf8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 13:35:40 -0500 Subject: [PATCH 003/329] Use more consistent notation --- test/conventions/condon_shortley.jl | 7 +++---- test/conventions/sakurai.jl | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test/conventions/condon_shortley.jl b/test/conventions/condon_shortley.jl index 3fb92eb1..1c36151d 100644 --- a/test/conventions/condon_shortley.jl +++ b/test/conventions/condon_shortley.jl @@ -11,8 +11,7 @@ automatic differentiation to compute the derivatives explicitly. The result is that the original Condon-Shortley spherical harmonics agree perfectly with the ones computed by this package. -Note that Condony and Shortley do not give an explicit formula for what are now called the -Wigner D-matrices. +(Condon and Shortley do not give an expression for the Wigner D-matrices.) """ @testmodule CondonShortley begin @@ -99,7 +98,7 @@ footnote to Eq. (15) of Sec. 4³ (page 52). Note that the name of this function is `\varTheta`, as opposed to the `\Theta` function that implements Condon-Shortley's general form. """ -ϴ(ℓ, m, θ) = ϴ(Val(ℓ), Val(m), θ) / √(2π) +ϴ(ℓ, m, θ) = ϴ(Val(ℓ), Val(m), θ) ϴ(::Val{0}, ::Val{0}, θ) = √(1/2) ϴ(::Val{1}, ::Val{0}, θ) = √(3/2) * cos(θ) ϴ(::Val{2}, ::Val{0}, θ) = √(5/8) * (2cos(θ)^2 - sin(θ)^2) @@ -149,7 +148,7 @@ end # @testmodule CondonShortley let Y = ₛ𝐘(0, 3, T, [from_spherical_coordinates(θ, ϕ)])[1,:] for ℓ ∈ 0:3 for m ∈ -ℓ:ℓ - @test ϴ(ℓ, m, θ) ≈ Y[Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ + @test ϴ(ℓ, m, θ) / √(2π) ≈ Y[Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ end end end diff --git a/test/conventions/sakurai.jl b/test/conventions/sakurai.jl index d767c7ea..187d8a92 100644 --- a/test/conventions/sakurai.jl +++ b/test/conventions/sakurai.jl @@ -44,8 +44,8 @@ end @doc raw""" d(j, m′, m, β) -Eqs. (3.5.50)-(3.5.51) of [Sakurai](@cite Sakurai_1994), p. 194, -implementing +Eqs. (3.5.50)-(3.5.51) of [Sakurai](@cite Sakurai_1994), p. 194 +(or Eq. (3.8.33), p. 223), implementing ```math d^{(j)}_{m',m}(\beta). ``` From 583a82f30a29330dc4f62d3203e65179157e1e9a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 13:35:54 -0500 Subject: [PATCH 004/329] Add broken Edmonds tests --- docs/src/references.bib | 11 +++ test/conventions/edmonds.jl | 170 ++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 test/conventions/edmonds.jl diff --git a/docs/src/references.bib b/docs/src/references.bib index 22bd3b1b..6b270fcc 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -50,6 +50,17 @@ @book{CondonShortley_1935 url = {https://archive.org/details/in.ernet.dli.2015.212979} } +@book{Edmonds_2016, + title = {Angular Momentum in Quantum Mechanics}, + isbn = {978-1-4008-8418-6}, + url = {https://www.degruyter.com/document/doi/10.1515/9781400884186/html}, + doi = {10.1515/9781400884186}, + publisher = {Princeton University Press}, + author = {Edmonds, A. R.}, + month = aug, + year = 2016 +} + @article{Elahi_2018, doi = {10.1109/lsp.2018.2865676}, url = {https://doi.org/10.1109/lsp.2018.2865676}, diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl new file mode 100644 index 00000000..f68562fb --- /dev/null +++ b/test/conventions/edmonds.jl @@ -0,0 +1,170 @@ +raw""" +Formulas and conventions from [Edmonds' "Angular Momentum in Quantum Mechanics"](@cite +Edmonds_2016). + +Note that Edmonds explains on page 8 that his Euler angles agree with ours. + +""" +@testmodule Edmonds begin + +import FastDifferentiation + +const 𝒾 = im + +include("../utilities/naive_factorial.jl") +import .NaiveFactorials: ❗ + + +@doc raw""" + Y(ℓ, m, θ, φ) + +Eq. (2.5.5) of [Edmonds](@cite Edmonds_2016), implementing +```math + Yₗₘ(θ, φ). +``` +""" +function Y(ℓ, m, θ::T, φ::T)::Complex{T} where {T} + (-1)^(ℓ+m) / (2^ℓ * (ℓ)❗) * √((2ℓ+1)*(ℓ-m)❗/(4big(π) * (ℓ+m)❗)) * + (sin(θ)^T(m)) * dʲsin²ᵏθdcosθʲ(ℓ+m, ℓ, θ) * exp(𝒾*m*φ) +end + + +@doc raw""" + 𝒟(j, m′, m, α, β, γ) + +Eqs. (4.1.12) of [Edmonds](@cite Edmonds_2016), implementing +```math + \mathcal{D}^{(j)}_{m',m}(\alpha, \beta, \gamma). +``` + +See also [`d`](@ref) for Edmonds' version the Wigner d-function. +""" +function 𝒟(j, m′, m, α, β, γ) + exp(𝒾*m′*γ) * d(j, m′, m, β) * exp(𝒾*m*α) +end + + +@doc raw""" + d(j, m′, m, β) + +Eqs. (4.1.15) of [Edmonds](@cite Edmonds_2016), implementing +```math + d^{(j)}_{m',m}(\beta). +``` + +See also [`𝒟`](@ref) for Edmonds' version the Wigner D-function. +""" +function d(j, m′, m, β) + if j < 0 + throw(DomainError("j=$j must be non-negative")) + end + if abs(m′) > j || abs(m) > j + throw(DomainError("abs(m′=$m′) and abs(m=$m) must be ≤ j=$j")) + end + if j ≥ 8 + throw(DomainError("j=$j≥8 will lead to overflow errors")) + end + + # The summation index `k` ranges over all values for which the factorials are + # non-negative. + σₘᵢₙ = 0 + σₘₐₓ = j - m′ + + T = typeof(β) + + # Note that Edmonds' actual formula is reproduced here, even though it leads to overflow + # errors for `j ≥ 8`, which could be eliminated by other means. + return √T((j+m′)❗ * (j-m′)❗ / ((j+m)❗ * (j-m)❗)) * + sum( + σ -> ( + binomial(j+m, j-m′-σ) * binomial(j-m, σ) * + (-1)^(j-m′-σ) * cos(β/2)^(2σ+m′+m) * sin(β/2)^(2j-2σ-m′-m) + ), + σₘᵢₙ:σₘₐₓ, + init=zero(T) + ) +end + + +@doc raw""" + dʲsin²ᵏθdcosθʲ(j, k, θ) + +Compute the ``j``th derivative of the function ``\sin^{2k}(θ)`` with respect to ``\cos(θ)``. +Note that ``\sin^{2k}(θ) = (1 - \cos^2(θ))^k``, so this is equivalent to evaluating the +``j``th derivative of ``(1-x^2)^k`` with respect to ``x``, evaluated at ``x = \cos(θ)``. +""" +function dʲsin²ᵏθdcosθʲ(j, k, θ) + if j < 0 + throw(ArgumentError("j=$j must be non-negative")) + end + if j == 0 + return sin(θ)^(2k) + end + x = FastDifferentiation.make_variables(:x)[1] + ∂ₓʲfᵏ = FastDifferentiation.derivative((1 - x^2)^k, (x for _ ∈ 1:j)...) + return FastDifferentiation.make_function([∂ₓʲfᵏ,], [x,])(cos(θ))[1] +end + + +end # @testmodule Edmonds + + +@testitem "Edmonds conventions" setup=[Utilities, Edmonds] begin + using Random + using Quaternionic: from_spherical_coordinates + + Random.seed!(1234) + const T = Float64 + const ℓₘₐₓ = 4 + ϵₐ = nextfloat(T(0), 4) + ϵᵣ = 20eps(T) + + # Tests for Y(ℓ, m, θ, ϕ) + for θ ∈ βrange(T) + if abs(sin(θ)) < ϵₐ + continue + end + + for ϕ ∈ αrange(T) + # Test Edmonds' Eq. (2.5.5) + for ℓ in 0:ℓₘₐₓ + for m in -ℓ:-1 + @test Edmonds.Y(ℓ, -m, θ, ϕ) ≈ (-1)^-m * conj(Edmonds.Y(ℓ, m, θ, ϕ)) + end + end + + # # Compare to SphericalFunctions + # let s=0 + # Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + # i = 1 + # for ℓ in 0:ℓₘₐₓ + # for m in -ℓ:ℓ + # @test Edmonds.Y(ℓ, m, θ, ϕ) ≈ Y[i] + # i += 1 + # end + # end + # end + end + end + + # # Tests for 𝒟(j, m′, m, α, β, γ) + # let ϵₐ=√ϵᵣ, ϵᵣ=√ϵᵣ, 𝒟=Edmonds.𝒟 + # for α ∈ αrange(T) + # for β ∈ βrange(T) + # for γ ∈ γrange(T) + # D = D_matrices(α, β, γ, ℓₘₐₓ) + # i = 1 + # for j in 0:ℓₘₐₓ + # for m′ in -j:j + # for m in -j:j + # @test 𝒟(j, m′, m, α, β, γ) ≈ conj(D[i]) atol=ϵₐ rtol=ϵᵣ + # i += 1 + # end + # end + # end + # end + # end + # end + # end + +end From bfb0258969f595dcf82bc7042ca929bf0b580575 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 13:36:07 -0500 Subject: [PATCH 005/329] Add unfinished notes about conventions --- docs/src/conventions.md | 299 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 docs/src/conventions.md diff --git a/docs/src/conventions.md b/docs/src/conventions.md new file mode 100644 index 00000000..a3eb179c --- /dev/null +++ b/docs/src/conventions.md @@ -0,0 +1,299 @@ +We first define the rotor that takes ``(\hat{x}, \hat{y}, \hat{z})`` +onto ``(\hat{\theta}, \hat{\phi}, \hat{r})``. Then, we can invert +that, so that given a rotor that specifies such a rotation exactly, we +can get the spherical coordinates — or specifically ``\sin\theta``, +``\cos\theta``, and ``\exp(i\phi)``. + +Then, with the universally agreed-upon ``Y`` as given in terms of +spherical coordinates, we can rewrite it directly to work with +quaternion components, and then it immediately applies to general +rotations, which allows us to figure out where the ``s`` should go. +That is, we can essentially derive ``{}_sY`` from the universal +formula for ``Y``. + +Then, we can simply follow Wigner around Eq. (15.21) to derived a +transformation law in the form +```math +{}_sY_{\ell,m'}(R_{\theta', \phi'}) = \sum_m M_{m',m}(R) +{}_sY_{\ell,m}(R_{\theta, \phi}), +``` +for some matrix ``M``. Note that I have written this as if the +``{}_sY`` functions are column vectors. The reason this happens is +because I want to write ``R_{\theta', \phi'} = R\, R_{\theta, \phi}``, +rather than swapping the order of the rotations on the right-hand +side. + +The big problem here is that Wigner, in his Eq. (15.21) defines the +transformation matrix as if the eigenfunctions formed a row vector +instead of a column vector, which means that his matrix is transposed +compared to what I want to write. I suppose maybe other authors then +just consider the inverse rotation, so that they can work with the +conjugate transpose, which is why we see the relative conjugate. + +* Since ``Y`` is universal, let's start with that as non-negotiable, + and see if we can derive the relationship to ``\mathfrak{D}``. +* ``R_{\theta, \phi}`` is a unit quaternion that rotates the point + described by Cartesian coordinates (0,0,1) onto the point described + by spherical coordinates ``(\theta, \phi)``. +* Just textually, it makes the most sense to write + ```math + R_{\theta', \phi'} = R\, R_{\theta, \phi} + ``` + for some rotation ``R``. Now, we just need to interpret ``R``. +* Again, just textually, it makes the most sense to write + ```math + Y_{\ell,m'}(\theta', \phi') = \sum_m \mathfrak{D}^{(\ell)}_{m',m}(R) + Y_{\ell,m}(\theta, \phi), + ``` + or, generalizing to spin-weighted spherical harmonics + ```math + {}_{s}Y_{\ell,m'}(R_{\theta', \phi'}) = \sum_m \mathfrak{D}^{(\ell)}_{m',m}(R) + {}_{s}Y_{\ell,m}(R_{\theta, \phi}). + ``` +* We also have that ``\mathfrak{D}`` obeys the representation + property, so + ```math + \mathfrak{D}^{(\ell)}_{m',m''}(R_{\theta', \phi'}) + = \sum_{m} \mathfrak{D}^{(\ell)}_{m',m}(R) + \mathfrak{D}^{(\ell)}_{m,m''}(R_{\theta, \phi}). + ``` + - There is no reason that I can see to introduce a conjugation + - The fact that ``m''`` appears on both sides of the equation means + that it must correspond to ``s`` — though we have to check the + behavior under final rotation to determine the sign. + +```math +{}_{s}Y_{\ell,m}(R_{\theta, \phi}) +\propto +\mathfrak{D}^{(\ell)}_{m,\propto s}(R_{\theta, \phi}) +``` + +# Conventions + +## Quaternions + + +## Rotations + + +## Euler angles and spherical coordinates + +We start with a standard Cartesian coordinate system ``(x, y, z)``. +The spherical coordinates ``(r, \theta, \phi)`` are defined by +```math +\begin{aligned} +x &= r \sin\theta \cos\phi, \\ +y &= r \sin\theta \sin\phi, \\ +z &= r \cos\theta. +\end{aligned} +``` +The inverse transformation is given by +```math +\begin{aligned} +r &= \sqrt{x^2 + y^2 + z^2}, \\ +\theta &= \arccos\left(\frac{z}{r}\right), \\ +\phi &= \arctan\left(\frac{y}{x}\right). +\end{aligned} +``` + + + + +## Spherical harmonics + +Fortunately, there does not seem to be any disagreement in the physics +literature about the definition of the spherical harmonics; everyone +uses the Condon-Shortley convention. Or at least, they say they do. +The problem arises when people define the spherical harmonics in terms +of the Legendre polynomials, for which there is a sign ambiguity. +Therefore, to ensure that we are using the same conventions, we need +to go back to the original definition of the spherical harmonics by +Condon and Shortley. + +### Condon-Shortley phase + +The [Condon-Shortley](@cite CondonShortley_1935) phase convention is a +choice of phase factors in the definition of the spherical harmonics +that requires the coefficients in +```math +L_{\pm} |\ell,m\rangle = \alpha^{\pm}_{\ell,m} |\ell, m \pm 1\rangle +``` +to be real and positive. The reasoning behind this choice is +explained more clearly in Section 2 of [Ufford and Shortley +(1932)](@cite UffordShortley_1932). As a more practical matter, the +Condon-Shortley phase describes signs chosen in the expression for +spherical harmonics. The key expression is Eq. (15) of section 4³ +(page 52) of [Condon-Shortley](@cite CondonShortley_1935): +```math +\Theta(\ell, m) = (-1)^\ell \sqrt{\frac{2\ell+1}{2} \frac{(\ell+m)!}{(\ell-m)!}} +\frac{1}{2^\ell \ell!} \frac{1}{\sin^m\theta} +\frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. +``` +When multiplied by Eq. (5) ``\Phi(m) = e^{im\phi} / \sqrt{2\pi}``, +this gives the spherical harmonic function. The right-hand side of +the expression above is usually immediately replaced by a simpler +expression using Legendre polynomials, but this just shifts sign +ambiguity into the definition of the Legendre polynomials. Instead, +we can expand the above expression directly for the first few ``\ell`` +values and/or use automatic differentiation to actually test their +original expression as such against the function implemented in this +package. The first few values are given in a footnote to Condon and +Shortley's Eq. (15) (and have been verified separately by hand and by +computation with SymPy): +```math +\begin{aligned} +\Theta(0,0) &= \sqrt{\frac{1}{2}} \\ +\Theta(1,0) &= \sqrt{\frac{3}{2}} \cos\theta & +\Theta(1,\pm1) &= \mp \sqrt{\frac{3}{4}} \sin\theta \\ +\Theta(2,0) &= \sqrt{\frac{5}{8}} (2\cos^2\theta - \sin^2\theta) & +\Theta(2,\pm1) &= \mp \sqrt{\frac{15}{4}} \cos\theta \sin\theta & +\Theta(2,\pm2) &= \sqrt{\frac{15}{16}} \sin^2\theta \\ +\Theta(3,0) &= \sqrt{\frac{7}{8}} (2\cos^3\theta - 3\cos\theta\sin^2\theta) & +\Theta(3,\pm1) &= \mp \sqrt{\frac{21}{32}} (4\cos^2\theta\sin\theta - \sin^3\theta) & +\Theta(3,\pm2) &= \sqrt{\frac{105}{16}} \cos\theta \sin^2\theta & +\Theta(3,\pm3) &= \mp \sqrt{\frac{35}{32}} \sin^3\theta +\end{aligned} +``` +These are tested, along with the results from automatic +differentiation, every time this package is updated. The result is +perfect agreement, so that we can definitively say that ***the +spherical-harmonic functions provided by this package obey the +Condon-Shortley phase convention.*** + +## Angular-momentum operators + +Wigner's $𝔇$ matrices are defined as matrix elements of a rotation in +the basis of spherical harmonics. That rotation is defined in terms +of the generators of rotation, which are expressed in terms of the +angular-momentum operators. Therefore, to really understand +conventions for the $𝔇$ matrices, we need to understand conventions +for the angular-momentum operators. + +There is universal agreement that the angular momentum is defined as +``\mathbf{L} = \mathbf{x} \times \mathbf{p}``, where ``\mathbf{x}`` is +the position vector and ``\mathbf{p}`` is the momentum vector. In +quantum mechanics, there is further agreement that the momentum +operator becomes ``-i\hbar\nabla``. Thus, in operator form, the +angular momentum can be decomposed as +```math +\begin{aligned} +L_x &= -i\hbar \left( y \frac{\partial}{\partial z} - z \frac{\partial}{\partial y} \right), \\ +L_y &= -i\hbar \left( z \frac{\partial}{\partial x} - x \frac{\partial}{\partial z} \right), \\ +L_z &= -i\hbar \left( x \frac{\partial}{\partial y} - y \frac{\partial}{\partial x} \right). +\end{aligned} +``` +We can transform these to use spherical coordinates and obtain +```math +\begin{aligned} +L_x &= -i\hbar \left( \sin\phi \frac{\partial}{\partial\theta} + \cot\theta \cos\phi \frac{\partial}{\partial\phi} \right), \\ +L_y &= -i\hbar \left( \cos\phi \frac{\partial}{\partial\theta} - \cot\theta \sin\phi \frac{\partial}{\partial\phi} \right), \\ +L_z &= -i\hbar \frac{\partial}{\partial\phi}. +\end{aligned} +``` +The conventions we choose *must* be chosen to agree with these — +modulo factors of ``\hbar``, which are nonstandard in mathematics. We +will have to check this, and the Condon-Shortley requirement that when +applied to spherical harmonics they produce real and positive +coefficients. + +I defined these in Eqs. (42) and (43) of [Boyle (2016)](@cite Boyle_2016) as +```math +\begin{aligned} +L_{j} f(\mathbf{R}) &\colonequals -z \left. \frac{\partial}{\partial \theta} +f\left(e^{\theta \mathbf{e}_j / 2} \mathbf{R} \right) \right|_{\theta=0}, \\ +K_{j} f(\mathbf{R}) &\colonequals -z \left. \frac{\partial}{\partial \theta} +f\left(\mathbf{R} e^{\theta \mathbf{e}_j / 2}\right) \right|_{\theta=0}, +\end{aligned} +``` +where ``\mathbf{e}_j`` is the unit vector in the ``j`` direction. +Surprisingly, I found that [Edmonds](@cite Edmonds_2016) expresses +essentially the same thing in the equations following his Eq. (4.1.5). + +Condon and Shortley's Eq. (1) of section 4³ (page 50) defines +```math +L_z = -i \hbar \frac{\partial}{\partial \phi}, +``` +while Eq. (8) on the following page defines +```math +\begin{aligned} +L_x + i L_y &= \hbar e^{i\phi} \left( \frac{\partial}{\partial \theta} + i \cot\theta \frac{\partial}{\partial \phi} \right), \\ +L_x - i L_y &= \hbar e^{-i\phi} \left(-\frac{\partial}{\partial \theta} + i \cot\theta \frac{\partial}{\partial \phi} \right). +\end{aligned} +``` +Note that one is not the conjugate of the other! This is because of +the factors of ``-i`` in the definitions of ``L_x`` and ``L_y``. + +[Edmonds](@cite Edmonds_2016) gives the *total* angular-momentum +operator for a rigid body in Eq. (2.2.2) as +```math +\begin{aligned} +L_x &= -i\hbar \left(-\cos \alpha \cot\beta \frac{\partial}{\partial\alpha} - \sin\alpha \frac{\partial}{\partial\beta} + \frac{\cos\alpha}{\sin\beta} \frac{\partial}{\partial\gamma} \right), \\ +L_y &= -i\hbar \left(-\sin\alpha \cot\beta \frac{\partial}{\partial \alpha} + \cos\alpha \frac{\partial}{\partial\beta} + \frac{\sin\alpha}{\sin\beta} \frac{\partial}{\partial\gamma} \right), \\ +L_z &= -i\hbar \frac{\partial}{\partial\alpha}. +\end{aligned} +``` + + +## Wigner $𝔇$ and $d$ matrices + +Wigner's Eqs. (11.18) and (11.19) define the real orthogonal +transformation ``\mathbf{R}`` by +```math +x'_i = R_{ij} x_j +``` +and the operator ``\mathbf{P}_{\mathbf{R}}`` to act on a function +``f`` such that +```math +\mathbf{P}_{\mathbf{R}} f(x'_1, \ldots) = f(x_1, \ldots). +``` +Then, his Eq. (15.5) presumably implies +```math +Y_{\ell,m}(\vartheta', \varphi') += \mathbf{P}_{\{\alpha, \beta, \gamma\}} Y_{\ell,m}(\vartheta, \varphi) += \sum_{m'} \mathfrak{D}^{(\ell)}(\{\alpha, \beta, \gamma\})_{m',m} + Y_{\ell,m'}(\vartheta, \varphi), +``` +where ``\{\alpha, \beta, \gamma\}`` takes ``(\vartheta, \varphi)`` to +``(\vartheta', \varphi')``. In any case, we can now leave behind this +``\mathbf{P}`` notation and just look at the beginning and end of the +equation above as the critical relationship in Wigner's notation. + + +Eq. (44b) of [Boyle (2016)](@cite Boyle_2016) says +```math +L_{\pm} \mathfrak{D}^{(\ell)}_{m',m}(\mathbf{R}) += \sqrt{(\ell \mp m')(\ell \pm m' + 1)} \mathfrak{D}^{(\ell)}_{m' \pm 1, m}(\mathbf{R}). +``` +while Eq. (21) relates the Wigner D-matrix to the spin-weighted spherical harmonics as +```math +{}_{s}Y_{\ell,m}(\mathbf{R}) += (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \mathfrak{D}^{(\ell)}_{m,-s}(\mathbf{R}). +``` +Plugging the latter into the former, we get +```math +L_{\pm} {}_{s}Y_{\ell,m}(\mathbf{R}) += \sqrt{(\ell \mp m)(\ell \pm m + 1)} {}_{s}Y_{\ell,m \pm 1}(\mathbf{R}). +``` +That is, in our conventions we have +```math +\alpha^{\pm}_{\ell,m} = \sqrt{(\ell \mp m)(\ell \pm m + 1)}, +``` +which is always real and positive, and thus consistent with the Condon-Shortley phase +convention. + + +### Properties + +* $D^j_{m'm}(\alpha,\beta,\gamma) = (-1)^{m'-m} D^j_{-m',-m}(\alpha,\beta,\gamma)^*$ +* $(-1)^{m'-m}D^{j}_{mm'}(\alpha,\beta,\gamma)=D^{j}_{m'm}(\gamma,\beta,\alpha)$ +* $d_{m',m}^{j}=(-1)^{m-m'}d_{m,m'}^{j}=d_{-m,-m'}^{j}$ + +$ +\begin{aligned} +d_{m',m}^{j}(\pi) &= (-1)^{j-m} \delta_{m',-m} \\[6pt] +d_{m',m}^{j}(\pi-\beta) &= (-1)^{j+m'} d_{m',-m}^{j}(\beta)\\[6pt] +d_{m',m}^{j}(\pi+\beta) &= (-1)^{j-m} d_{m',-m}^{j}(\beta)\\[6pt] +d_{m',m}^{j}(2\pi+\beta) &= (-1)^{2j} d_{m',m}^{j}(\beta)\\[6pt] +d_{m',m}^{j}(-\beta) &= d_{m,m'}^{j}(\beta) = (-1)^{m'-m} d_{m',m}^{j}(\beta) +\end{aligned} +$ From ccb8f3ba0fc0ce5acf6d93230cd67ffa309affb5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 22:58:21 -0500 Subject: [PATCH 006/329] Get Edmonds tests working --- docs/src/conventions.md | 7 +++ test/conventions/edmonds.jl | 89 +++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/docs/src/conventions.md b/docs/src/conventions.md index a3eb179c..92840850 100644 --- a/docs/src/conventions.md +++ b/docs/src/conventions.md @@ -1,3 +1,10 @@ +Saul felt that following Wigner was a mistake, and to just bite the +bullet and use the conjugate. That's reasonable; I just have to +conjugate the entire representation equation to turn it into an +equation for rotating spherical harmonics. + +--- + We first define the rotor that takes ``(\hat{x}, \hat{y}, \hat{z})`` onto ``(\hat{\theta}, \hat{\phi}, \hat{r})``. Then, we can invert that, so that given a rotor that specifies such a rotation exactly, we diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl index f68562fb..e8e3d129 100644 --- a/test/conventions/edmonds.jl +++ b/test/conventions/edmonds.jl @@ -2,7 +2,12 @@ raw""" Formulas and conventions from [Edmonds' "Angular Momentum in Quantum Mechanics"](@cite Edmonds_2016). -Note that Edmonds explains on page 8 that his Euler angles agree with ours. +Note that Edmonds explains on page 8 that his Euler angles agree with ours. His spherical +harmonics agree also, but his ``𝔇`` is transposed. Alternatively, we could think of his +``𝔇`` being conjugated — just like other modern conventions — but taking the inverse +rotation as argument. + +TODO: Figure out the meaning of those rotations. """ @testmodule Edmonds begin @@ -115,56 +120,62 @@ end # @testmodule Edmonds Random.seed!(1234) const T = Float64 - const ℓₘₐₓ = 4 - ϵₐ = nextfloat(T(0), 4) + const ℓₘₐₓ = 3 + ϵₐ = 8eps(T) ϵᵣ = 20eps(T) # Tests for Y(ℓ, m, θ, ϕ) - for θ ∈ βrange(T) - if abs(sin(θ)) < ϵₐ + for θ ∈ βrange(T, 3) + if abs(sin(θ)) ≤ eps(T) continue end - for ϕ ∈ αrange(T) + for ϕ ∈ αrange(T, 3) # Test Edmonds' Eq. (2.5.5) + let Y = Edmonds.Y for ℓ in 0:ℓₘₐₓ - for m in -ℓ:-1 - @test Edmonds.Y(ℓ, -m, θ, ϕ) ≈ (-1)^-m * conj(Edmonds.Y(ℓ, m, θ, ϕ)) + for m in -ℓ:0 + @test Y(ℓ, -m, θ, ϕ) ≈ (-1)^-m * conj(Y(ℓ, m, θ, ϕ)) atol=ϵₐ rtol=ϵᵣ end end - # # Compare to SphericalFunctions - # let s=0 - # Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) - # i = 1 - # for ℓ in 0:ℓₘₐₓ - # for m in -ℓ:ℓ - # @test Edmonds.Y(ℓ, m, θ, ϕ) ≈ Y[i] - # i += 1 - # end - # end - # end + # Compare to SphericalFunctions + let s=0 + Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + i = 1 + for ℓ in 0:ℓₘₐₓ + for m in -ℓ:ℓ + @test Edmonds.Y(ℓ, m, θ, ϕ) ≈ Y[i] atol=ϵₐ rtol=ϵᵣ + i += 1 + end + end + end end - end + end + + # Tests for 𝒟(j, m′, m, α, β, γ) + let ϵₐ=√ϵᵣ, ϵᵣ=√ϵᵣ, 𝒟=Edmonds.𝒟 + for α ∈ αrange(T) + for β ∈ βrange(T) + if abs(sin(β)) ≤ eps(T) + continue + end - # # Tests for 𝒟(j, m′, m, α, β, γ) - # let ϵₐ=√ϵᵣ, ϵᵣ=√ϵᵣ, 𝒟=Edmonds.𝒟 - # for α ∈ αrange(T) - # for β ∈ βrange(T) - # for γ ∈ γrange(T) - # D = D_matrices(α, β, γ, ℓₘₐₓ) - # i = 1 - # for j in 0:ℓₘₐₓ - # for m′ in -j:j - # for m in -j:j - # @test 𝒟(j, m′, m, α, β, γ) ≈ conj(D[i]) atol=ϵₐ rtol=ϵᵣ - # i += 1 - # end - # end - # end - # end - # end - # end - # end + for γ ∈ γrange(T) + D = D_matrices(α, β, γ, ℓₘₐₓ) + i = 1 + for j in 0:ℓₘₐₓ + for m′ in -j:j + for m in -j:j + #@test 𝒟(j, m, m′, α, β, γ) ≈ D[i] atol=ϵₐ rtol=ϵᵣ + @test 𝒟(j, m′, m, -γ, -β, -α) ≈ conj(D[i]) atol=ϵₐ rtol=ϵᵣ + i += 1 + end + end + end + end + end + end + end end From acabe21ccb6c6b1ce9302d5f2481566c7e2e04bc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 10 Dec 2024 13:30:05 -0500 Subject: [PATCH 007/329] Add some references about functional / harmonic analysis --- docs/src/references.bib | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/src/references.bib b/docs/src/references.bib index 6b270fcc..5b4f3e4b 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -78,6 +78,18 @@ @article{Elahi_2018 primaryClass = "astro-ph.IM", } +@book{Folland_2016, + address = {New York}, + edition = 2, + title = {A Course in Abstract Harmonic Analysis}, + isbn = {978-0-429-15469-0}, + publisher = {Chapman and {Hall/CRC}}, + author = {Folland, Gerald B.}, + month = feb, + year = 2016, + doi = {10.1201/b19172} +} + @article{Fukushima_2011, doi = {10.1007/s00190-011-0519-2}, url = {https://doi.org/10.1007/s00190-011-0519-2}, @@ -93,6 +105,19 @@ @article{Fukushima_2011 journal = {Journal of Geodesy} } +@book{Fulton_2004, + address = {New York, {NY}}, + series = {Graduate Texts in Mathematics}, + title = {Representation Theory}, + volume = 129, + isbn = {978-3-540-00539-1 978-1-4612-0979-9}, + url = {http://link.springer.com/10.1007/978-1-4612-0979-9}, + publisher = {Springer}, + author = {Fulton, William and Harris, Joe}, + year = 2004, + doi = {10.1007/978-1-4612-0979-9} +} + @article{GoldbergEtAl_1967, author = {Goldberg, J. N. and Macfarlane, A. J. and Newman, E. T. and Rohrlich, F. and Sudarshan, E. C. G.}, @@ -122,6 +147,18 @@ @incollection{Gumerov_2015 primaryClass = "math.NA", } +@book{HansonYakovlev_2002, + address = {New York, {NY}}, + title = {Operator Theory for Electromagnetics}, + isbn = {978-1-4419-2934-1 978-1-4757-3679-3}, + url = {http://link.springer.com/10.1007/978-1-4757-3679-3}, + publisher = {Springer}, + author = {Hanson, George W. and Yakovlev, Alexander B.}, + year = 2002, + doi = {10.1007/978-1-4757-3679-3} +} + + @article{Holmes_2002, doi = {10.1007/s00190-002-0216-2}, url = {https://doi.org/10.1007/s00190-002-0216-2}, @@ -245,6 +282,19 @@ @article{UffordShortley_1932 pages = {167--175} } +@book{vanNeerven_2022, + address = {Cambridge}, + series = {Cambridge Studies in Advanced Mathematics}, + title = {Functional Analysis}, + isbn = {978-1-00-923247-0}, + url = + {https://www.cambridge.org/core/books/functional-analysis/62B852DFB4D6F11D21C04309DCF7584F}, + publisher = {Cambridge University Press}, + author = {van Neerven, Jan}, + year = 2022, + doi = {10.1017/9781009232487} +} + @article{Waldvogel_2006, doi = {10.1007/s10543-006-0045-4}, url = {https://doi.org/10.1007/s10543-006-0045-4}, From 3dcfda1332b1dffc7946eac05aa9a3dee41e917b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 10 Dec 2024 13:30:35 -0500 Subject: [PATCH 008/329] Sketch an outline --- docs/src/conventions.md | 116 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/docs/src/conventions.md b/docs/src/conventions.md index 92840850..22d9aaf8 100644 --- a/docs/src/conventions.md +++ b/docs/src/conventions.md @@ -5,6 +5,104 @@ equation for rotating spherical harmonics. --- +# Outline + +* Three-dimensional Euclidean space + - Cartesian coordinates ``(x, y, z)`` => ℝ³ + - Cartesian basis vectors ``(𝐱, 𝐲, 𝐳,)`` + - Euclidean norm => Euclidean metric + - Spherical coordinates + - Specifically give transformation to/from ``(x, y, z)`` + - Derive metric in these coordinates from transformation + - Integration / measure on two-sphere + - Derive as restriction of full metric, in both coordinate systems +* Four-dimensional Euclidean space + - Eight-dimensional Clifford algebra over the tangent *vector space* ``Tℝ³`` + - Four-dimensional even sub-algebra => ℝ⁴ + - Coordinates ``(W, X, Y, Z)`` + - Basis vectors ``(𝟏, 𝐢, 𝐣, 𝐤)``, but we usually just omit ``𝟏`` + - Show a few essential formulas establishing the product and its conventions + - Unit quaternions are isomorphic to ``\mathbf{Spin}(3) = + \mathbf{SU}(2)``; double covers ``\mathbf{SO}(3)`` + - Be explicit about the mapping between vector in ℝ³ and quaternions + - Show how a unit quaternion can be used to rotate a vector + - Spherical coordinates (hyperspherical / Euler) + - Specifically give transformation to/from ``(W, X, Y, Z)`` + - Derive metric in these coordinates from transformation + - Express unit quaternion in Euler angles + - Integration / measure / Haar measure on three-sphere + - Derive as restriction of full metric, in both coordinate systems +* Angular momentum operators / functional analysis + - Express angular momentum operators in terms of quaternion components + - Express angular momentum operators in terms of Euler angles + - Show for both the three- and two-spheres + - Show how they act on functions on the three-sphere +* Representation theory / harmonic analysis + - Representations show up in Fourier analysis on groups + - Peter-Weyl theorem + - Generalizes Fourier analysis to compact groups + - A basis of functions on the group is given by matrix elements of + group representations + - Representation theory of ``\mathbf{Spin}(3)`` + - Show how the Lie algebra is represented by the angular-momentum operators + - Show how the Lie group is represented by the Wigner D-matrices + - Demonstrate that ``\mathfrak{D}`` is a representation + - Demonstrate its behavior under left and right rotation + - Demonstrate orthonormality + - Representation theory of ``\mathbf{SO}(3)`` + - There are several places in [Folland](@cite Folland_2016) (e.g., + above corollary 5.48) where he mentions that representations of + a quotient group are just representations that are trivial + (evidently meaning mapping everything to the identity matrix) on + the factor. I can't find anywhere that he explains this + explicitly, but it seems easy enough to show. He might do it + using characters. + - For ``\mathbf{Spin}(3)`` and ``\mathbf{SO}(3)``, the factor + group is just ``\{1, -1\}``. Presumably, every representation + acting on ``1`` will give the identity matrix, so that's + trivial. So we just need a criterion for when a representation + is trivial on ``-1``. Noting that ``\exp(\pi \vec{v}) = -1`` + for any ``\vec{v}``, I think we can show that this requires + ``m \in \mathbb{Z}``. + - Basically, the point is that the representations of + ``\mathbf{SO}(3)`` are just the integer representations of + ``\mathbf{Spin}(3)``. + - Restrict to homogeneous space (S³ -> S²) + - The circle group is a closed (normal?) subgroup of + ``\mathbf{Spin}(3)``, which we might implement as initial + multiplication about a particular axis. + - In Eq. (2.47) [Folland (2016)](@cite Folland_2016) defines a + functional taking a function on the group to a function on the + homogeneous space by integrating over the factor (the circle + group). This gives you the spherical harmonics, but *not* the + spin-weighted spherical harmonics — because the spin-weighted + spherical harmonics cannot be defined on the 2-sphere. + - Spin weight comes from Fourier analysis on the subgroup. + - Representation matrices transfer to the homogeneous space, with + sparsity patterns + + + +--- + +Spherical harmonics as functions on homogeneous space. +https://www.youtube.com/watch?v=TnFvOa9v7do gives some nice +discussion; maybe the paper has better references. + +Theorem 2.16 of [Hanson-Yakovlev](@cite HansonYakovlev_2002) says that +an orthonormal basis of a product of ``L^2`` spaces is given by the +product of the orthonormal bases of the individual spaces. +Furthermore, on page 354, they point out that ``\{(1/\sqrt{2\pi}) +e^{im\phi}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the +set ``\{1/c_{n,m} P_n^m(\cos\theta)`` is an orthonormal basis of +``L^2(0, \pi)`` in the ``\theta`` coordinate. Therefore, the product +of these two sets is an orthonormal basis of the product space +``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate +space for ``S^2``. I would probably modify this to point out that +``(0,2\pi)`` is really ``S^1``, and then we could extend it to point +out that you can throw on another factor of ``S^1`` to cover ``S^3``, +which happens to give us the Wigner D-matrices. + We first define the rotor that takes ``(\hat{x}, \hat{y}, \hat{z})`` onto ``(\hat{\theta}, \hat{\phi}, \hat{r})``. Then, we can invert that, so that given a rotor that specifies such a rotation exactly, we @@ -169,6 +267,24 @@ Condon-Shortley phase convention.*** ## Angular-momentum operators +* First, a couple points about ``-i\hbar``: + - The finite transformations look like ``\exp[-i \theta L_j]``, but + the factor of ``i`` introduced here just cancels the one in the + ``L_j``, and the sign is just chosen to make the result consistent + with our notion of active or passive transformations. + - Any factors of ``\hbar`` are included *purely* for the sake of + convenience. + - The factor ``i`` comes from plain functional analysis: We need a + self-adjoint operator, and ``\partial_x`` by itself is + anti-self-adjoint (as can be verified by evaluating on ``\langle + x' | x \rangle = \delta(x-x')``, which switches sign based on + which is being differentiated). We want self-adjoint operators so + that we get purely real eigenvalues. [Van Neerven](@cite + vanNeerven_2022) cites this in a more rigorous context in his + Example (10.40) (page 331), with more explanation around Eq. + (15.17) (page 592). The "self-adjoint ``\iff`` real eigenvalues" + condition is item (1) in his Corollary 9.18. + Wigner's $𝔇$ matrices are defined as matrix elements of a rotation in the basis of spherical harmonics. That rotation is defined in terms of the generators of rotation, which are expressed in terms of the From 8e96c6203c3fc6978a0994d021716b524dce7220 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 11 Dec 2024 09:52:00 -0500 Subject: [PATCH 009/329] Rearrange conventions docs --- docs/make.jl | 4 + docs/src/conventions/conventions.md | 108 ++++++++++++++++++ .../outline.md} | 41 ++----- 3 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 docs/src/conventions/conventions.md rename docs/src/{conventions.md => conventions/outline.md} (95%) diff --git a/docs/make.jl b/docs/make.jl index b6eec523..e51ceea4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -34,6 +34,10 @@ makedocs( "internal.md", "functions.md", ], + "Conventions" => [ + "conventions/conventions.md", + "conventions/comparisons.md", + ], "Notes" => map( s -> "notes/$(s)", sort(readdir(joinpath(@__DIR__, "src/notes"))) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md new file mode 100644 index 00000000..6e687e18 --- /dev/null +++ b/docs/src/conventions/conventions.md @@ -0,0 +1,108 @@ +# Conventions + +Here, we work through all the conventions used in this package, +starting from first principles to motivate the choices and ensure that +each step is on firm footing. + +## Three-dimensional space + +The space we are working in is naturally three-dimensional Euclidean +space, so we start with Cartesian coordinates ``(x, y, z)``. These +also give us the unit basis vectors ``(𝐱, 𝐲, 𝐳)``. Note that these +basis vectors are assumed to have unit norm, but we omit the hats just +to keep the notation simple. Any vector in this space can be written +as +```math +\mathbf{v} = v_x \mathbf{𝐱} + v_y \mathbf{𝐲} + v_z \mathbf{𝐳}, +``` +in which case the Euclidean norm is given by +```math +\| \mathbf{v} \| = \sqrt{v_x^2 + v_y^2 + v_z^2}. +``` +Equivalently, we can write the components of the Euclidean metric as +```math +g_{ij} = \left( \begin{array}{ccc} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 +\end{array} \right)_{ij}. +``` +Note that, because the points of the space are in one-to-one +correspondence with the vectors, we will frequently use a vector to +label a point in space. + +We will be working on the sphere, so it will be very convenient to use +spherical coordinates ``(r, \theta, \phi)``. We choose the standard +"physics" conventions for these, in which we relate to the Cartesian +coordinates by +```math +\begin{aligned} +r &= \sqrt{x^2 + y^2 + z^2} &&\in [0, \infty), \\ +\theta &= \arccos\left(\frac{z}{r}\right) &&\in [0, \pi], \\ +\phi &= \arctan\left(\frac{y}{x}\right) &&\in [0, 2\pi), +\end{aligned} +``` +where we assume the ``\arctan`` in the expression for ``\phi`` is +really the two-argument form that gives the correct quadrant. The +inverse transformation is given by +```math +\begin{aligned} +x &= r \sin\theta \cos\phi, \\ +y &= r \sin\theta \sin\phi, \\ +z &= r \cos\theta. +\end{aligned} +``` +We can use this to find the components of the metric in spherical +coordinates: +```math +g_{i'j'} += \sum_{i,j} \frac{\partial x^i}{\partial x^{i'}} \frac{\partial x^j}{\partial x^{j'}} g_{ij} += \left( \begin{array}{ccc} + 1 & 0 & 0 \\ + 0 & r^2 & 0 \\ + 0 & 0 & r^2 \sin^2\theta +\end{array} \right)_{i'j'}. +``` +The unit coordinate vectors in spherical coordinates are then +```math +\begin{aligned} +\mathbf{𝐫} &= \sin\theta \cos\phi \mathbf{𝐱} + \sin\theta \sin\phi \mathbf{𝐲} + \cos\theta \mathbf{𝐳}, \\ +\boldsymbol{\theta} &= \cos\theta \cos\phi \mathbf{𝐱} + \cos\theta \sin\phi \mathbf{𝐲} - \sin\theta \mathbf{𝐳}, \\ +\boldsymbol{\phi} &= -\sin\phi \mathbf{𝐱} + \cos\phi \mathbf{𝐲}, +\end{aligned} +``` +where, again, we omit the hats on the unit vectors to keep the +notation simple. + +One seemingly obvious — but extremely important — fact is that the +unit basis frame ``(𝐱, 𝐲, 𝐳)`` can be rotated onto +``(\boldsymbol{\theta}, \boldsymbol{\phi}, \mathbf{r})`` by first +rotating through the "polar" angle ``\theta`` about the ``\mathbf{y}`` +axis, and then through the "azimuthal" angle ``\phi`` about the +``\mathbf{z}`` axis. This becomes important when we consider +spin-weighted functions. + +Integration in Cartesian coordinates is, of course, trivial as +```math +\int f\, d^3\mathbf{r} = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f\, dx\, dy\, dz. +``` +In spherical coordinates, the integrand involves the square-root of +the determinant of the metric, so we have +```math +\int f\, d^3\mathbf{r} = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. +``` +If we restrict to just the unit sphere, we can simplify this to +```math +\int f\, d^2\Omega = \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. +``` + + +## Four-dimensional space: Quaternions and rotations + + +## Rotations + + +## Euler angles and spherical coordinates + + diff --git a/docs/src/conventions.md b/docs/src/conventions/outline.md similarity index 95% rename from docs/src/conventions.md rename to docs/src/conventions/outline.md index 22d9aaf8..ccdbcb55 100644 --- a/docs/src/conventions.md +++ b/docs/src/conventions/outline.md @@ -1,10 +1,3 @@ -Saul felt that following Wigner was a mistake, and to just bite the -bullet and use the conjugate. That's reasonable; I just have to -conjugate the entire representation equation to turn it into an -equation for rotating spherical harmonics. - ---- - # Outline * Three-dimensional Euclidean space @@ -83,7 +76,7 @@ equation for rotating spherical harmonics. ---- +# Notes Spherical harmonics as functions on homogeneous space. https://www.youtube.com/watch?v=TnFvOa9v7do gives some nice @@ -173,35 +166,23 @@ conjugate transpose, which is why we see the relative conjugate. \mathfrak{D}^{(\ell)}_{m,\propto s}(R_{\theta, \phi}) ``` -# Conventions - -## Quaternions +## collapsible markdown? -## Rotations - - -## Euler angles and spherical coordinates +```@raw html +
CLICK ME +``` +#### yes, even hidden code blocks! -We start with a standard Cartesian coordinate system ``(x, y, z)``. -The spherical coordinates ``(r, \theta, \phi)`` are defined by -```math -\begin{aligned} -x &= r \sin\theta \cos\phi, \\ -y &= r \sin\theta \sin\phi, \\ -z &= r \cos\theta. -\end{aligned} +```julia +println("hello world!") ``` -The inverse transformation is given by -```math -\begin{aligned} -r &= \sqrt{x^2 + y^2 + z^2}, \\ -\theta &= \arccos\left(\frac{z}{r}\right), \\ -\phi &= \arctan\left(\frac{y}{x}\right). -\end{aligned} +```@raw html +
``` +# More notes ## Spherical harmonics From 77251953ea60fadcfb2895a12d24cdba886a9fe5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 11 Dec 2024 11:18:23 -0500 Subject: [PATCH 010/329] Make Documenter happy --- docs/Project.toml | 1 + docs/src/conventions/outline.md | 16 +-- docs/src/notes/sampling_theorems.md | 152 ++++++++++++++++------------ docs/src/operators.md | 2 +- 4 files changed, 98 insertions(+), 73 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 528a1a65..34a8c65f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -8,3 +8,4 @@ Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SphericalFunctions = "af6d55de-b1f7-4743-b797-0829a72cf84e" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index ccdbcb55..ff3a178a 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -266,11 +266,11 @@ Condon-Shortley phase convention.*** (15.17) (page 592). The "self-adjoint ``\iff`` real eigenvalues" condition is item (1) in his Corollary 9.18. -Wigner's $𝔇$ matrices are defined as matrix elements of a rotation in +Wigner's ``𝔇`` matrices are defined as matrix elements of a rotation in the basis of spherical harmonics. That rotation is defined in terms of the generators of rotation, which are expressed in terms of the angular-momentum operators. Therefore, to really understand -conventions for the $𝔇$ matrices, we need to understand conventions +conventions for the ``𝔇`` matrices, we need to understand conventions for the angular-momentum operators. There is universal agreement that the angular momentum is defined as @@ -338,7 +338,7 @@ L_z &= -i\hbar \frac{\partial}{\partial\alpha}. ``` -## Wigner $𝔇$ and $d$ matrices +## Wigner ``𝔇`` and ``d`` matrices Wigner's Eqs. (11.18) and (11.19) define the real orthogonal transformation ``\mathbf{R}`` by @@ -388,11 +388,11 @@ convention. ### Properties -* $D^j_{m'm}(\alpha,\beta,\gamma) = (-1)^{m'-m} D^j_{-m',-m}(\alpha,\beta,\gamma)^*$ -* $(-1)^{m'-m}D^{j}_{mm'}(\alpha,\beta,\gamma)=D^{j}_{m'm}(\gamma,\beta,\alpha)$ -* $d_{m',m}^{j}=(-1)^{m-m'}d_{m,m'}^{j}=d_{-m,-m'}^{j}$ +* ``D^j_{m'm}(\alpha,\beta,\gamma) = (-1)^{m'-m} D^j_{-m',-m}(\alpha,\beta,\gamma)^*`` +* ``(-1)^{m'-m}D^{j}_{mm'}(\alpha,\beta,\gamma)=D^{j}_{m'm}(\gamma,\beta,\alpha)`` +* ``d_{m',m}^{j}=(-1)^{m-m'}d_{m,m'}^{j}=d_{-m,-m'}^{j}`` -$ +```math \begin{aligned} d_{m',m}^{j}(\pi) &= (-1)^{j-m} \delta_{m',-m} \\[6pt] d_{m',m}^{j}(\pi-\beta) &= (-1)^{j+m'} d_{m',-m}^{j}(\beta)\\[6pt] @@ -400,4 +400,4 @@ d_{m',m}^{j}(\pi+\beta) &= (-1)^{j-m} d_{m',-m}^{j}(\beta)\\[6pt] d_{m',m}^{j}(2\pi+\beta) &= (-1)^{2j} d_{m',m}^{j}(\beta)\\[6pt] d_{m',m}^{j}(-\beta) &= d_{m,m'}^{j}(\beta) = (-1)^{m'-m} d_{m',m}^{j}(\beta) \end{aligned} -$ +``` diff --git a/docs/src/notes/sampling_theorems.md b/docs/src/notes/sampling_theorems.md index e4df7995..ecf6ec2b 100644 --- a/docs/src/notes/sampling_theorems.md +++ b/docs/src/notes/sampling_theorems.md @@ -1,55 +1,69 @@ # Sampling theorems and transformations of spin-weighted spherical harmonics -[McEwen_2011](@citet) (MW) provide a very thorough review of the literature on sampling theorems -related to spin-weighted spherical harmonics up to 2011. [Reinecke_2013](@citet) (RS) outlined one -of the more efficient and accurate implementations of spin-weighted spherical harmonic transforms -(``s``SHT) currently available as `libsharp`, but their algorithm is ``∼4L²``, whereas McEwen and -Wiaux's is``∼2L²``, while [Elahi_2018](@citet) (EKKM) have obtained the optimal result that scales -as ``∼L²``. - -The downside of the EKKM algorithm is that the ``θ`` values at which to sample have to be obtained -by iteratively minimizing the condition numbers of various matrices (which are involved in the -computation itself). This expensive step only has to be performed once per choice of spin ``s`` and -maximum ``ℓ`` value ``L``. Otherwise, the results of this algorithm seem to be relatively good — at -least for ``L`` up to 64. This does not compare favorably with the MW algorithm, which has slowly -growing errors through ``L = 4096``. +[McEwen_2011](@citet) (MW) provide a very thorough review of the +literature on sampling theorems related to spin-weighted spherical +harmonics up to 2011. [Reinecke_2013](@citet) (RS) outlined one of +the more efficient and accurate implementations of spin-weighted +spherical harmonic transforms (``s``SHT) currently available as +`libsharp`, but their algorithm is ``∼4L²``, whereas McEwen and +Wiaux's is``∼2L²``, while [Elahi_2018](@citet) (EKKM) have obtained +the optimal result that scales as ``∼L²``. + +The downside of the EKKM algorithm is that the ``θ`` values at which +to sample have to be obtained by iteratively minimizing the condition +numbers of various matrices (which are involved in the computation +itself). This expensive step only has to be performed once per choice +of spin ``s`` and maximum ``ℓ`` value ``L``. Otherwise, the results +of this algorithm seem to be relatively good — at least for ``L`` up +to 64. This does not compare favorably with the MW algorithm, which +has slowly growing errors through ``L = 4096``. ## EKKM analysis -The EKKM analysis looks like the following (with some notational changes). We begin by defining +The EKKM analysis looks like the following (with some notational +changes). We begin by defining ```math {}_{s}\tilde{f}_{\theta}(m) := \int_0^{2\pi} {}_sf(\theta, \phi)\, e^{-im\phi}\, d\phi. ``` -We will denote the vector of these quantities for all values of $\theta$ as -${}_{s}\tilde{\mathbf{f}}_m$. Inserting the ${}_sY_{\ell,m}$ expansion for ${}_sf(\theta, \phi)$, -and performing the integration using orthogonality of complex exponentials, we can find that +We will denote the vector of these quantities for all values of +``\theta`` as ``{}_{s}\tilde{\mathbf{f}}_m``. Inserting the +``{}_sY_{\ell,m}`` expansion for ``{}_sf(\theta, \phi)``, and +performing the integration using orthogonality of complex +exponentials, we can find that ```math {}_{s}\tilde{f}_{\theta}(m) = (-1)^s\, 2\pi \sum_{\ell=\Delta}^L \sqrt{\frac{2\ell+1}{4\pi}}\, d_{m,-s}^{\ell}(\theta)\, {}_sf_{\ell,m}. ``` -Now, denoting the vector of ${}_sf_{\ell,m}$ for all values of $\ell$ as ${}_s\mathbf{f}_m$, we can -write this as a matrix-vector equation: +Now, denoting the vector of ``{}_sf_{\ell,m}`` for all values of +``\ell`` as ``{}_s\mathbf{f}_m``, we can write this as a matrix-vector +equation: ```math {}_{s}\tilde{\mathbf{f}}_m = (-1)^s\, 2\pi\, {}_s\mathbf{d}_{m}\, {}_s\mathbf{f}_m. ``` -We are effectively measuring the ${}_{s}\tilde{\mathbf{f}}_m$ values, we can easily construct the -${}_s\mathbf{d}_{m}$ matrix, and we are seeking the ${}_s\mathbf{f}_m$ values, so we can just invert +We are effectively measuring the ``{}_{s}\tilde{\mathbf{f}}_m`` +values, we can easily construct the ``{}_s\mathbf{d}_{m}`` matrix, and +we are seeking the ``{}_s\mathbf{f}_m`` values, so we can just invert this equation to solve for the latter. ## Discretizing the Fourier transform -Now, the only flaw in this analysis is that we have undersampled everywhere except $\ell = L$, which -means that the second equation (re-expressing the Fourier transforms as a sum using orthogonality of -complex exponentials) isn't quite right; in general there is some folding due to aliasing of -higher-frequency modes, so we need an additional sum over $|m'|>|m|$. Or perhaps more precisely, -the first equation isn't actually what we implement. It should look more like this: +Now, the only flaw in this analysis is that we have undersampled +everywhere except ``\ell = L``, which means that the second equation +(re-expressing the Fourier transforms as a sum using orthogonality of +complex exponentials) isn't quite right; in general there is some +folding due to aliasing of higher-frequency modes, so we need an +additional sum over ``|m'|>|m|``. Or perhaps more precisely, the +first equation isn't actually what we implement. It should look more +like this: ```math {}_{s}\tilde{f}_{j}(m) := \sum_{k=0}^{2j} {}_sf(\theta_j, \phi_k)\, e^{-im\phi_k}\, \Delta \phi, ``` -where $\phi_k = \frac{2\pi k}{2j+1}$, and $\Delta \phi = \frac{2\pi}{2j+1}$. (Recall the subtle -notational distinction common in time-frequency analysis that $\tilde{s}(t_j) = \Delta t -\tilde{s}_j$, which would suggest we use ${}_{s}\tilde{f}_{j}(m) = \Delta \phi\, -{}_{s}\tilde{f}_{j,m}$.) Next, we can insert the expansion for ${}_sf(\theta, \phi)$: +where ``\phi_k = \frac{2\pi k}{2j+1}``, and ``\Delta \phi = +\frac{2\pi}{2j+1}``. (Recall the subtle notational distinction common +in time-frequency analysis that ``\tilde{s}(t_j) = \Delta t +\tilde{s}_j``, which would suggest we use ``{}_{s}\tilde{f}_{j}(m) = +\Delta \phi\, {}_{s}\tilde{f}_{j,m}``.) Next, we can insert the +expansion for ``{}_sf(\theta, \phi)``: ```math \begin{aligned} @@ -73,8 +87,8 @@ This allows us to simplify as {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{\ell,m'} {}_sf_{\ell,m'}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j), \end{aligned} ``` -where $m'$ ranges over $m + n(2j+1)$ for all $n\in \mathbb{Z}$ such that $|m + n(2j+1)| \leq \ell$ -— that is, all $n\in \mathbb{Z}$ such that +where ``m'`` ranges over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq \ell`` +— that is, all ``n\in \mathbb{Z}`` such that ```math \left \lceil \frac{-\ell-m}{2j+1} \right \rceil \leq n \leq \left \lfloor \frac{\ell-m}{2j+1} \right \rfloor. ``` @@ -82,42 +96,53 @@ where $m'$ ranges over $m + n(2j+1)$ for all $n\in \mathbb{Z}$ such that $|m + n ## Matrix representation -Usually, we would take the sum over $\ell$ ranging from $\mathrm{max}(|m|,|s|)$ to $L$, and the sum -over $m'$ ranging over $m + n(2j+1)$ for all $n\in \mathbb{Z}$ such that $|m + n(2j+1)| \leq \ell$. -However, we can also consider these sums to range over all possible values of $\ell, m'$, and just -set the coefficient to zero whenever these conditions are not satisfied. In that case, we can again -think of this as a (much larger) vector-matrix equation reading +Usually, we would take the sum over ``\ell`` ranging from ``\mathrm{max}(|m|,|s|)`` to ``L``, and the sum +over ``m'`` ranging over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq \ell``. +However, we can also consider these sums to range over all possible +values of ``\ell, m'``, and just set the coefficient to zero whenever +these conditions are not satisfied. In that case, we can again think +of this as a (much larger) vector-matrix equation reading ```math {}_s\tilde{\mathbf{f}} = (-1)^s\, 2\pi\, {}_s\mathbf{d}\, {}_s\mathbf{f}, ``` -where the index on ${}_s\tilde{\mathbf{f}}$ loops over $j$ and $m$, the index on ${}_s\mathbf{f}$ -loops over $\ell$ and $m'$, and the indices on ${}_s\mathbf{d}$ loop over each of those pairs. +where the index on ``{}_s\tilde{\mathbf{f}}`` loops over ``j`` and +``m``, the index on ``{}_s\mathbf{f}`` loops over ``\ell`` and ``m'``, +and the indices on ``{}_s\mathbf{d}`` loop over each of those pairs. ## De-aliasing -While it is *far* simpler to simply invert the full ${}_s\mathbf{d}$ matrix, its size scales as -$L^4$, which means that it very quickly becomes impractical to store and manipulate the full matrix. -In CMB astronomy, for example, it is not uncommon to use $L$ into the tens of thousands, which would -make the full matrix utterly impractical to use. - -However, the matrix has a fairly sparse structure, with the number of *nonzero* elements scaling as -$L^3$. More particularly, the sparsity has a fairly special structure, where the full matrix is -mostly block diagonal, along with some sparse upper triangular elements. Of course, the goal is to -solve the linear equation. For that, the first obvious choice is an LU decomposition. -Unfortunately, the L and U components are *not* sparse. A second obvious choice is the QR -decomposition, which is more tailored to the structure of this matrix — the Q factor being -essentially just the block diagonal, and the R factor being a somewhat less sparse upper triangle. - -In principle, this alone could delay the impracticality threshold — though still not enough for CMB -astronomy. We can use the unusual structure to solve the linear equation in a more piecewise -fashion, with fairly low memory overhead. Essentially, we start with the highest-$|k|$ values, and -solve for the corresponding highest-$|m|$ values. Those harmonics will alias to other frequencies -in $\theta_j$ rings with $j < |k|$. But crucially, we know *how* they alias, and can simply remove -them from the Fourier transforms of those rings. We then repeat, solving for the next-highest $|k|$ -values, and so on. - -The following pseudo-code summarizes the analysis algorithm, modifying the input in place: +While it is *far* simpler to simply invert the full ``{}_s\mathbf{d}`` +matrix, its size scales as ``L^4``, which means that it very quickly +becomes impractical to store and manipulate the full matrix. In CMB +astronomy, for example, it is not uncommon to use ``L`` into the tens +of thousands, which would make the full matrix utterly impractical to +use. + +However, the matrix has a fairly sparse structure, with the number of +*nonzero* elements scaling as ``L^3``. More particularly, the +sparsity has a fairly special structure, where the full matrix is +mostly block diagonal, along with some sparse upper triangular +elements. Of course, the goal is to solve the linear equation. For +that, the first obvious choice is an LU decomposition. Unfortunately, +the L and U components are *not* sparse. A second obvious choice is +the QR decomposition, which is more tailored to the structure of this +matrix — the Q factor being essentially just the block diagonal, and +the R factor being a somewhat less sparse upper triangle. + +In principle, this alone could delay the impracticality threshold — +though still not enough for CMB astronomy. We can use the unusual +structure to solve the linear equation in a more piecewise fashion, +with fairly low memory overhead. Essentially, we start with the +highest-``|k|`` values, and solve for the corresponding +highest-``|m|`` values. Those harmonics will alias to other +frequencies in ``\theta_j`` rings with ``j < |k|``. But crucially, we +know *how* they alias, and can simply remove them from the Fourier +transforms of those rings. We then repeat, solving for the +next-highest ``|k|`` values, and so on. + +The following pseudo-code summarizes the analysis algorithm, modifying +the input in place: ```julia # Iterate over rings, doing Fourier decompositions on each for j ∈ abs(s):ℓₘₐₓ @@ -155,9 +180,8 @@ for m ∈ AlternatingCountdown(ℓₘₐₓ) # Iterate over +m, then -m, down t end ``` - - -The following pseudo-code summarizes the synthesis algorithm, modifying the input in place: +The following pseudo-code summarizes the synthesis algorithm, +modifying the input in place: ```julia for m ∈ AlternatingCountup(ℓₘₐₓ) # Iterate over +m, then -m, up from m=0 Δ = max(abs(s), abs(m)) diff --git a/docs/src/operators.md b/docs/src/operators.md index 55920a0f..7bae6783 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -1,6 +1,6 @@ # Differential operators -Spin-weighted spherical functions *cannot* be defined on the sphere $S^2$, but +Spin-weighted spherical functions *cannot* be defined on the sphere ``S^2``, but are well defined on the group ``\mathrm{Spin}(3) \cong \mathrm{SU}(2)`` or the rotation group ``\mathrm{SO}(3)``. (See [Boyle_2016](@citet) for the explanation.) However, this also allows us to define a variety of differential From 2325f893150c35bb361a2a89be0a034fc15d2cd9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 11 Dec 2024 11:19:56 -0500 Subject: [PATCH 011/329] Compare to Kip's review --- docs/src/references.bib | 13 ++++++ test/conventions/thorne.jl | 90 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 test/conventions/thorne.jl diff --git a/docs/src/references.bib b/docs/src/references.bib index 5b4f3e4b..7f452656 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -259,6 +259,19 @@ @book{Sakurai_1994 year = 1994 } +@article{Thorne_1980, + title = {Multipole expansions of gravitational radiation}, + volume = 52, + url = {http://link.aps.org/abstract/RMP/v52/p299}, + doi = {10.1103/RevModPhys.52.299}, + number = 2, + journal = {Reviews of Modern Physics}, + author = {Thorne, Kip S.}, + month = apr, + year = 1980, + pages = 299 +} + @book{TorresDelCastillo_2003, address = {Boston, {MA}}, title = {{3-D} Spinors, Spin-Weighted Functions and their Applications}, diff --git a/test/conventions/thorne.jl b/test/conventions/thorne.jl new file mode 100644 index 00000000..7a7d3678 --- /dev/null +++ b/test/conventions/thorne.jl @@ -0,0 +1,90 @@ +raw""" + +Formulas and conventions from [Thorne (1980)](@cite Thorne_1980). + +""" +@testmodule Thorne begin + +const 𝒾 = im + +include("../utilities/naive_factorial.jl") +import .NaiveFactorials: ❗ + + +# Eq. (2.8) upper +function C(ℓ, m, T) + let π=convert(T, π), √=sqrt∘T + (-1)^m * √T( + ((2ℓ+1) * (ℓ-m)❗) + / (4π * (ℓ+m)❗) + ) + end +end + + +# Eq. (2.8) lower +function a(ℓ, m, j, T) + T(((-1)^j / (2^ℓ * (j)❗ * (ℓ-j)❗)) * ((2ℓ-2j)❗ / (ℓ-m-2j)❗)) +end + + +@doc raw""" + Y(ℓ, m, θ, ϕ) + +Eqs. (2.7) of [Thorne](@cite Thorne_1980), implementing +```math + Y^{\ell,m}(\theta, \phi). +``` +""" +function Y(ℓ, m, θ, ϕ) + if m < 0 + return (-1)^m * conj(Y(ℓ, abs(m), θ, ϕ)) + end + θ, ϕ = promote(θ, ϕ) + sinθ, cosθ = sincos(θ) + T = typeof(sinθ) + C(ℓ, m, T) * (exp(𝒾*ϕ) * sinθ)^m * sum( + j -> a(ℓ, m, j, T) * (cosθ)^(ℓ-m-2j), + 0:floor((ℓ-m)÷2), + init=zero(T) + ) +end + +end # @testmodule Thorne + + +@testitem "Thorne conventions" setup=[Utilities, Thorne] begin + using Random + using Quaternionic: from_spherical_coordinates + + Random.seed!(1234) + const T = Float64 + const ℓₘₐₓ = 5 + ϵₐ = nextfloat(T(0), 4) + ϵᵣ = 20eps(T) + + # Tests for Y(ℓ, m, θ, ϕ) + for θ ∈ βrange(T) + for ϕ ∈ αrange(T) + + # Test Thorne's Eq. (2.9b) + for ℓ in 0:ℓₘₐₓ + for m in -ℓ:-1 + @test conj(Thorne.Y(ℓ, m, θ, ϕ)) ≈ (-1)^-m * Thorne.Y(ℓ, -m, θ, ϕ) + end + end + + # Compare to SphericalFunctions + let s=0 + Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + i = 1 + for ℓ in 0:ℓₘₐₓ + for m in -ℓ:ℓ + @test Thorne.Y(ℓ, m, θ, ϕ) ≈ Y[i] + i += 1 + end + end + end + end + end +end From ea116a838356def7039e6fa0772ca89e85428f9d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:20:50 -0500 Subject: [PATCH 012/329] List comparisons I intend to make --- docs/src/conventions/comparisons.md | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/src/conventions/comparisons.md diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md new file mode 100644 index 00000000..52511f50 --- /dev/null +++ b/docs/src/conventions/comparisons.md @@ -0,0 +1,53 @@ +# Comparisons + +Here, we compare our conventions to other sources, including +references in the literature as well as other software that implements +some of these. Each of these comparisons is also performed explicitly +in [this package's test +suite](https://github.com/moble/SphericalFunctions.jl/tree/main/test/conventions). + +Among the items that would be good to compare are the following, when +actually used by any of these sources: +* Quaternions + - Order of components + - Basis + - Operation as rotations +* Euler angles +* Spherical coordinates +* Spherical harmonics + - Condon-Shortley phase + - Formula +* Spin-weighted spherical harmonics + - Behavior under rotation +* Wigner D-matrices + - Order of indices + - Conjugation + - Function of rotation or inverse rotation + - Formula + +One major result of this is that almost everyone since 1935 has used +the same exact expression for the (scalar) spherical harmonics. + +## Condon-Shortley + +## Wigner + +## Newman-Penrose + +## Goldberg + +## Wikipedia + +## Mathematica + +## SymPy + +## Sakurai + +## Thorne + +## Torres del Castillo + +## NINJA + +## LALSuite From 9508b8bc1ec6d95d455c7ff644bf2e01f7051948 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:21:27 -0500 Subject: [PATCH 013/329] Introduce quaternions --- docs/src/conventions/conventions.md | 120 +++++++++++++++++++++++++++- docs/src/references.bib | 12 +++ 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 6e687e18..22c15c85 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -91,14 +91,130 @@ the determinant of the metric, so we have ```math \int f\, d^3\mathbf{r} = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. ``` -If we restrict to just the unit sphere, we can simplify this to +Restricting to the unit sphere, and normalizing so that the integral +of 1 over the sphere is 1, we can simplify this to ```math -\int f\, d^2\Omega = \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. +\int f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. ``` ## Four-dimensional space: Quaternions and rotations +Given the basis vectors ``(𝐱, 𝐲, 𝐳)`` and the Euclidean norm, we +can define the [geometric +algebra](https://en.wikipedia.org/wiki/Geometric_algebra). The key +feature is the geometric product, which is defined for any pair of +vectors as ``𝐯`` and ``𝐰`` as +```math +𝐯 𝐰 = 𝐯 ⋅ 𝐰 + 𝐯 ∧ 𝐰, +``` +where the dot product is the usual scalar product and the wedge +product is the antisymmetric part of the tensor product — acting just +like the standard [exterior +product](https://en.wikipedia.org/wiki/Exterior_algebra) from the +algebra of [differential +forms](https://en.wikipedia.org/wiki/Differential_form). The +geometric product is associative, distributive, and has the property +that +```math +𝐯𝐯 = \| 𝐯 \|^2. +``` +The basis for this entire space is then the set +```math +\begin{gather} +𝟏, \\ +𝐱, 𝐲, 𝐳,\\ +𝐱𝐲, 𝐱𝐳, 𝐲𝐳, \\ +𝐱𝐲𝐳. +\end{gather} +``` +It's useful to note that the first four of these square to 1, while +the last four square to -1 — meaning that they could serve as a unit +imaginary to generate the complex numbers. The more standard symbols +— and the ones we will use — are +```math +\begin{gather} +𝟏, \\ +𝐱, 𝐲, 𝐳,\\ +𝐢, 𝐣, 𝐤, \\ +𝐈. +\end{gather} +``` +The interpretation of these is that ``𝟏`` represents the scalars; +``𝐱, 𝐲, 𝐳`` span the vectors; ``𝐢, 𝐣, 𝐤`` are the standard +quaternion components; and ``𝐈`` is the pseudoscalar, which can also +serve as the [Hodge +dual](https://en.wikipedia.org/wiki/Hodge_star_operator). (Note that +quaternions will only be spanned by elements made from an even number +of the basis vectors. It turns out that those with an odd number will +inherently produce reflections, rather than rotations. For details +see any geometric algebra text, like [Doran and Lasenby](@cite +DoranLasenby_2010).) + +We use coordinates ``(W, X, Y, Z)`` on the space of quaternions, so +that such a quaternion would be written as +```math +W𝟏 + X𝐢 + Y𝐣 + Z𝐤, +``` +though we usually omit the ``𝟏``. As with standard three-dimensional +space, we could introduce spherical coordinates, though we use a +slight variant: extended Euler coordinates. In our conventions, we +have +```math +\begin{aligned} +R &= \sqrt{W^2 + X^2 + Y^2 + Z^2} &&\in [0, \infty), \\ +\alpha &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi], \\ +\beta &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi), \\ +\gamma &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi], +\end{aligned} +``` +where we again assume the ``\arctan`` in the expressions for +``\alpha`` and ``\gamma`` is really the two-argument form that gives +the correct quadrant. Note that here, ``\beta`` ranges up to ``2\pi`` +rather than just ``\pi``, as in the standard Euler angles. This is +because we are describing the space of quaternions, rather than just +the space of rotations. If we restrict to ``R=1``, we have exactly +the group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, +which is a double cover of the rotation group ``\mathrm{SO}(3)``. +This extended range for ``\beta`` is necessary to cover the entire +space of quaternions; if we further restrict to ``[0, \pi)``, we would +cover the space of rotations. This and the inclusion of ``R`` +identify precisely how this coordinate system extends the standard +Euler angles. + +The inverse transformation is given by +```math +\begin{aligned} + W &= R\, \cos\frac{β}{2} \cos\frac{α+γ}{2}, \\ + X &= -R\, \sin\frac{β}{2} \sin\frac{α-γ}{2}, \\ + Y &= R\, \sin\frac{β}{2} \cos\frac{α-γ}{2}, \\ + Z &= R\, \cos\frac{β}{2} \sin\frac{α+γ}{2}. +\end{aligned} +``` +As with the spherical coordinates, we can use this to find the +components of the metric in our extended Euler coordinates: +```math +g_{i'j'} += \sum_{i,j} \frac{\partial X^i}{\partial X^{i'}} \frac{\partial X^j}{\partial X^{j'}} g_{ij} += \left( \begin{array}{cccc} + 1 & 0 & 0 & 0 \\ + 0 & \frac{R^2}{4} & 0 & \frac{R^2 \cos\beta}{4} \\ + 0 & 0 & \frac{R^2}{4} & 0 \\ + 0 & \frac{R^2 \cos\beta}{4} & 0 & \frac{R^2}{4} +\end{array} \right)_{i'j'}. +``` +Again, integration involves a square-root of the determinant of the +metric, which reduces to ``R^3 |\sin\beta| / 8``. +```math +\int f\, d^4𝐑 += \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty f\, dW\, dX\, dY\, dZ += \int_0^\infty \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, \frac{R^3}{8} |\sin β|\, dR\, dα\, dβ\, dγ. +``` +Restricting to the unit sphere, and normalizing so that the integral +of 1 over the sphere is 1, we can simplify this to +```math +\int f\, d^3\Omega = \frac{1}{16\pi^2} \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, |\sin β|\, dα\, dβ\, dγ. +``` ## Rotations diff --git a/docs/src/references.bib b/docs/src/references.bib index 7f452656..7f0eb8e1 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -50,6 +50,18 @@ @book{CondonShortley_1935 url = {https://archive.org/details/in.ernet.dli.2015.212979} } +@book{DoranLasenby_2010, + address = {Cambridge}, + title = {Geometric Algebra for Physicists}, + isbn = {978-0-521-71595-9}, + url = + {https://www.cambridge.org/core/books/geometric-algebra-for-physicists/FB8D3ACB76AB3AB10BA7F27505925091}, + publisher = {Cambridge University Press}, + author = {Doran, Chris and Lasenby, Anthony}, + year = 2003, + doi = {10.1017/CBO9780511807497} +} + @book{Edmonds_2016, title = {Angular Momentum in Quantum Mechanics}, isbn = {978-1-4008-8418-6}, From 1bb730e5e29b37b6cb4a42cb3e9e1accc96c16c6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:21:56 -0500 Subject: [PATCH 014/329] Fix missing `end` --- test/conventions/edmonds.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl index e8e3d129..928a018e 100644 --- a/test/conventions/edmonds.jl +++ b/test/conventions/edmonds.jl @@ -133,9 +133,10 @@ end # @testmodule Edmonds for ϕ ∈ αrange(T, 3) # Test Edmonds' Eq. (2.5.5) let Y = Edmonds.Y - for ℓ in 0:ℓₘₐₓ - for m in -ℓ:0 - @test Y(ℓ, -m, θ, ϕ) ≈ (-1)^-m * conj(Y(ℓ, m, θ, ϕ)) atol=ϵₐ rtol=ϵᵣ + for ℓ in 0:ℓₘₐₓ + for m in -ℓ:0 + @test Y(ℓ, -m, θ, ϕ) ≈ (-1)^-m * conj(Y(ℓ, m, θ, ϕ)) atol=ϵₐ rtol=ϵᵣ + end end end @@ -151,7 +152,7 @@ end # @testmodule Edmonds end end end - end + end # Tests for 𝒟(j, m′, m, α, β, γ) let ϵₐ=√ϵᵣ, ϵᵣ=√ϵᵣ, 𝒟=Edmonds.𝒟 From a2c6dc31686a8cfbdef03895d1f7b906568eb015 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:22:10 -0500 Subject: [PATCH 015/329] Start on Mathematica conventions --- test/conventions/mathematica.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/conventions/mathematica.jl diff --git a/test/conventions/mathematica.jl b/test/conventions/mathematica.jl new file mode 100644 index 00000000..57f97479 --- /dev/null +++ b/test/conventions/mathematica.jl @@ -0,0 +1,17 @@ +""" + +We can find conventions at [this +page](https://reference.wolfram.com/language/ref/WignerD.html). + +> The Wolfram Language uses phase conventions where ``D^j_{m_1, m_2}(\psi, \theta, \phi) = + \exp(i m_1 \psi + i m_2 \phi) D^j_{m_1, m_2}(0, \theta, 0)``. + +> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` +> ``-\sqrt{2} e^{i \phi} \cos\frac{\theta}{2} \sin\frac{\theta}{2}`` + +> `WignerD[{𝓁, 0, m}, θ, ϕ] == Sqrt[(4 π)/(2 𝓁 + 1)] SphericalHarmonicY[𝓁, m, θ, ϕ]` + +> `WignerD[{j, m1, m2},ψ, θ, ϕ]] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, ϕ]]` + +""" +𝐑 From 16aa2dd43e08322240f693bbed3813af68bed043 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:58:01 -0500 Subject: [PATCH 016/329] Remove errant character and make string valid --- test/conventions/mathematica.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/conventions/mathematica.jl b/test/conventions/mathematica.jl index 57f97479..9a936e2f 100644 --- a/test/conventions/mathematica.jl +++ b/test/conventions/mathematica.jl @@ -1,4 +1,4 @@ -""" +raw""" We can find conventions at [this page](https://reference.wolfram.com/language/ref/WignerD.html). @@ -14,4 +14,3 @@ page](https://reference.wolfram.com/language/ref/WignerD.html). > `WignerD[{j, m1, m2},ψ, θ, ϕ]] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, ϕ]]` """ -𝐑 From 0e092d60bf57eb5ae99064d3fcebb8fc745415fe Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:58:25 -0500 Subject: [PATCH 017/329] Mention quaternion multiplication convention --- docs/src/conventions/conventions.md | 33 +++++++++++++++++++++++------ docs/src/references.bib | 13 ++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 22c15c85..0f0beed1 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -7,11 +7,12 @@ each step is on firm footing. ## Three-dimensional space The space we are working in is naturally three-dimensional Euclidean -space, so we start with Cartesian coordinates ``(x, y, z)``. These -also give us the unit basis vectors ``(𝐱, 𝐲, 𝐳)``. Note that these -basis vectors are assumed to have unit norm, but we omit the hats just -to keep the notation simple. Any vector in this space can be written -as +space, so we start with a +[right-handed](https://en.wikipedia.org/wiki/Right-hand_rule) +Cartesian coordinate system ``(x, y, z)``. These also give us the +unit basis vectors ``(𝐱, 𝐲, 𝐳)``. Note that these basis vectors +are assumed to have unit norm, but we omit the hats just to keep the +notation simple. Any vector in this space can be written as ```math \mathbf{v} = v_x \mathbf{𝐱} + v_y \mathbf{𝐲} + v_z \mathbf{𝐳}, ``` @@ -147,10 +148,28 @@ serve as the [Hodge dual](https://en.wikipedia.org/wiki/Hodge_star_operator). (Note that quaternions will only be spanned by elements made from an even number of the basis vectors. It turns out that those with an odd number will -inherently produce reflections, rather than rotations. For details -see any geometric algebra text, like [Doran and Lasenby](@cite +produce reflections, rather than rotations, when acting on a vector — +as discussed below. This explains why quaternions are restricted to +just those elements with an even number to represent rotations. For +details see any geometric algebra text, like [Doran and Lasenby](@cite DoranLasenby_2010).) +The key expressions that help to determine the arbitrary choices we +have made thus far are the multiplications +```math +\begin{aligned} +𝐢 𝐣 &= 𝐤, \\ +𝐣 𝐤 &= 𝐢, \\ +𝐤 𝐢 &= 𝐣. +\end{aligned} +``` +Everyone agrees that ``𝐢² = 𝐣² = 𝐤² = -1``, so we can also use the +rules above to determine ``𝐢𝐣𝐤 = -𝟏``. Different conventions are +sometimes used (almost exclusively in aerospace) so that this last +equation and the three displayed above have a flipped sign. See +[Sommer et al.](@cite SommerEtAl_2018) for a discussion of the +different conventions. + We use coordinates ``(W, X, Y, Z)`` on the space of quaternions, so that such a quaternion would be written as ```math diff --git a/docs/src/references.bib b/docs/src/references.bib index 7f0eb8e1..b66dee8a 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -271,6 +271,19 @@ @book{Sakurai_1994 year = 1994 } +@article{SommerEtAl_2018, + title = {Why and How to Avoid the Flipped Quaternion Multiplication}, + url = {http://arxiv.org/abs/1801.07478}, + journal = {{arXiv:1801.07478} [cs]}, + author = {Sommer, Hannes and Gilitschenski, Igor and Bloesch, Michael and Weiss, Stephan and + Siegwart, Roland and Nieto, Juan}, + month = jan, + year = 2018, + eprint = {1801.07478}, + archivePrefix ={arXiv}, + primaryClass = {cs.RO}, +} + @article{Thorne_1980, title = {Multipole expansions of gravitational radiation}, volume = 52, From d72243d042009cbea05d99c805e1a2617dff7ca0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 20 Dec 2024 02:10:52 -0500 Subject: [PATCH 018/329] Add some more notes on conventions --- docs/src/conventions/conventions.md | 314 ++++++++++++++++--- docs/src/conventions/outline.md | 42 +++ docs/src/notes/condon_shortley_expression.py | 108 +++++++ docs/src/notes/conventions.py | 261 +++++++++++++++ 4 files changed, 676 insertions(+), 49 deletions(-) create mode 100644 docs/src/notes/condon_shortley_expression.py create mode 100644 docs/src/notes/conventions.py diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 0f0beed1..2dee40f1 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -1,8 +1,77 @@ # Conventions -Here, we work through all the conventions used in this package, -starting from first principles to motivate the choices and ensure that -each step is on firm footing. +In the following subsections, we work through all the conventions used +in this package, starting from first principles to motivate the +choices and ensure that each step is on firm footing. First, we can +just list the most important conventions. Note that we will use Euler +angles and spherical coordinates here. It is almost always a bad idea +to use Euler angles in *computing*; quaternions are clearly the +preferred representation for numerous reasons. However, Euler angles +are important for (a) comparing to other sources, and (b) performing +*analytic* integrations. These are the only two uses we will make of +Euler angles. + +1. Right-handed Cartesian coordinates ``(x, y, z)`` and unit basis + vectors ``(𝐱, 𝐲, 𝐳)``. +2. Spherical coordinates ``(r, \theta, \phi)`` and unit basis vectors + ``(𝐫, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar + angle" ``\theta \in [0, \pi]`` measures the angle between the + specified direction and the positive ``𝐳`` axis. The "azimuthal + angle" ``\phi \in [0, 2\pi)`` measures the angle between the + projection of the specified direction onto the ``𝐱``-``𝐲`` plane + and the positive ``𝐱`` axis, with the positive ``𝐲`` axis + corresponding to the positive angle ``\phi = \pi/2``. +3. Quaternions ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢𝐣𝐤 = -1``. + In software, this quaternion is represented by ``(W, X, Y, Z)``. + We will depict a three-dimensional vector ``𝐯 = v_x 𝐱 + v_y 𝐲 + + v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + v_z + 𝐤``. +4. A rotation represented by the unit quaternion ``𝐑`` acts on a + vector ``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. +5. Where relevant, rotations will be assumed to be right-handed, so + that a quaternion characterizing the rotation through an angle + ``\vartheta`` about a unit vector ``𝐮`` can be expressed as ``𝐑 = + \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` would deliver the same + rotation, which is why the group of unit quaternions + ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* of the + group of rotations ``\mathrm{SO}(3)``. +6. Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha + 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles + ``\alpha`` and ``\beta`` take values in ``[0, 2\pi)``. The angle + ``\beta`` takes values in ``[0, 2\pi]`` to parametrize the group of + unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, + \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. +7. A point on the unit sphere with spherical coordinates ``(\theta, + \phi)`` can be represented by Euler angles ``(\alpha, \beta, + \gamma) = (\phi, \theta, 0)``. The rotation with these Euler + angles takes the positive ``𝐳`` axis to the specified direction. + In particular, any function of spherical coordinates can be + promoted to a function on Euler angles using this identification. +8. For a complex-valued function ``f(𝐑)``, we define two operators, + the left and right Lie derivatives: + ```math + L_𝐮 f(𝐑) = \left.-i \frac{d}{d\epsilon}\right|_{\epsilon=0} + f\left(e^{\epsilon 𝐮/2}\, 𝐑\right) + \qquad \text{and} \qquad + R_𝐮 f(𝐑) = \left.-i \frac{d}{d\epsilon}\right|_{\epsilon=0} + f\left(𝐑\, e^{\epsilon 𝐮/2}\right), + ``` + where ``𝐮`` can be any pure-vector quaternion. In particular, + ``L`` represents the standard angular-momentum operators, and we + can compute the expressions in Euler angles for the basis vectors: + ```math + \begin{aligned} + L_x = L_𝐢 &= -i \left( \sin\alpha \frac{\partial}{\partial\beta} + \cos\alpha \cot\beta \frac{\partial}{\partial\alpha} \right), \\ + L_y = L_𝐣 &= -i \left( \cos\alpha \frac{\partial}{\partial\beta} - \sin\alpha \cot\beta \frac{\partial}{\partial\alpha} \right), \\ + L_z = L_𝐤 &= -i \frac{\partial}{\partial\alpha}. + \end{aligned} + ``` + Angular-momentum operators defined in Lie terms, translated to + Euler angles and spherical coordinates. +9. Spherical harmonics +10. Wigner D-matrices +11. Spin-weighted spherical harmonics + ## Three-dimensional space @@ -85,22 +154,24 @@ spin-weighted functions. Integration in Cartesian coordinates is, of course, trivial as ```math -\int f\, d^3\mathbf{r} = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f\, dx\, dy\, dz. +\int_{\mathbb{R}^3} f\, d^3\mathbf{r} = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f\, dx\, dy\, dz. ``` In spherical coordinates, the integrand involves the square-root of the determinant of the metric, so we have ```math -\int f\, d^3\mathbf{r} = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. +\int_{\mathbb{R}^3} f\, d^3\mathbf{r} = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. ``` Restricting to the unit sphere, and normalizing so that the integral of 1 over the sphere is 1, we can simplify this to ```math -\int f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. +\int_{S^2} f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. ``` ## Four-dimensional space: Quaternions and rotations +### Geometric algebra + Given the basis vectors ``(𝐱, 𝐲, 𝐳)`` and the Euclidean norm, we can define the [geometric algebra](https://en.wikipedia.org/wiki/Geometric_algebra). The key @@ -115,11 +186,16 @@ like the standard [exterior product](https://en.wikipedia.org/wiki/Exterior_algebra) from the algebra of [differential forms](https://en.wikipedia.org/wiki/Differential_form). The -geometric product is associative, distributive, and has the property -that +geometric product is linear, associative, distributive, and has the +property that ```math 𝐯𝐯 = \| 𝐯 \|^2. ``` +The most useful properties of the geometric product are that parallel +vectors commute with each other, while orthogonal vectors anticommute. +Since the geometric product is linear, the product of any two vectors +can be decomposed into parallel and orthogonal parts. + The basis for this entire space is then the set ```math \begin{gather} @@ -129,33 +205,70 @@ The basis for this entire space is then the set 𝐱𝐲𝐳. \end{gather} ``` -It's useful to note that the first four of these square to 1, while -the last four square to -1 — meaning that they could serve as a unit -imaginary to generate the complex numbers. The more standard symbols -— and the ones we will use — are +The standard presentation of quaternions (including the confused +historical development) uses different symbols for these last four +basis elements: ```math \begin{gather} -𝟏, \\ -𝐱, 𝐲, 𝐳,\\ -𝐢, 𝐣, 𝐤, \\ -𝐈. +𝐢 = 𝐳𝐲 = -𝐲𝐳, \\ +𝐣 = 𝐱𝐳 = -𝐳𝐱, \\ +𝐤 = 𝐲𝐱 = -𝐱𝐲, \\ +𝐈 = 𝐱𝐲𝐳. \end{gather} ``` -The interpretation of these is that ``𝟏`` represents the scalars; -``𝐱, 𝐲, 𝐳`` span the vectors; ``𝐢, 𝐣, 𝐤`` are the standard -quaternion components; and ``𝐈`` is the pseudoscalar, which can also -serve as the [Hodge -dual](https://en.wikipedia.org/wiki/Hodge_star_operator). (Note that -quaternions will only be spanned by elements made from an even number -of the basis vectors. It turns out that those with an odd number will -produce reflections, rather than rotations, when acting on a vector — -as discussed below. This explains why quaternions are restricted to -just those elements with an even number to represent rotations. For -details see any geometric algebra text, like [Doran and Lasenby](@cite -DoranLasenby_2010).) - -The key expressions that help to determine the arbitrary choices we -have made thus far are the multiplications +Note that each of these squares to -1. For example, recalling that +orthogonal vectors anticommute, the product is associative, and the +product of a vector with itself is just its squared norm, we have +```math +𝐱𝐲𝐱𝐲 = -𝐱𝐲𝐲𝐱 = -𝐱(𝐲𝐲)𝐱 = -𝐱𝐱 = -1. +``` +Any of these could act like the unit imaginary; ``𝐱𝐲`` is probably +the canonical choice. + +``𝐈`` is sometimes called the pseudoscalar. Its inverse is ``𝐈^{-1} += 𝐳𝐲𝐱 = -𝐱𝐲𝐳``, which can also serve as something very much like +the [Hodge star +operator](https://en.wikipedia.org/wiki/Hodge_star_operator),[^1] +mapping elements to their "dual" elements. In particular, we have +```math +\begin{aligned} +𝐢 &= 𝐈^{-1}𝐱, \\ +𝐣 &= 𝐈^{-1}𝐲, \\ +𝐤 &= 𝐈^{-1}𝐳. +\end{aligned} +``` +We will see that ``𝐢`` generates right-handed rotations in the +positive sense about ``𝐱``, ``𝐣`` about ``𝐲``, and ``𝐤`` about +``𝐳``. Moreover, this mapping between ``(𝐱, 𝐲, 𝐳)`` and ``(𝐢, +𝐣, 𝐤)`` is a vector-space isomorphism. In fact, the reader who is +not familiar with geometric algebra but is familiar with quaternions +may be able to read an expression like ``𝐣 𝐱 𝐣⁻¹`` as if it is just +an abuse of notation, and mentally replace ``𝐱`` with ``𝐢`` to read +those symbols as a valid quaternion expression; both viewpoints are +equally correct by the isomorphism. + +[^1]: Note that quaternions will only be spanned by elements made from + an even number of the basis vectors. It turns out that those + with an odd number will produce reflections, rather than + rotations, when acting on a vector — as discussed below. This + explains why quaternions are restricted to just those elements + with an even number to represent rotations. For details see any + geometric algebra text, like [Doran and Lasenby](@cite + DoranLasenby_2010). + +### Quaternions and Euler angles + +Note that there are different conventions for the signs of the ``(𝐢, +𝐣, 𝐤)`` basis. Everyone agrees that ``𝐢² = 𝐣² = 𝐤² = -1``, but +we could easily flip the sign of any basis element, and these would +still be satisfied. The identifications we chose above are made to +ensure that ``𝐢`` generates rotations about ``𝐱``, and so on, but +even that depends on how we define quaternions as acting on vectors +(to be discussed below). A different choice of the latter would +result in all flipping the sign of all three basis elements, which is +a convention that is commonly used — though almost exclusively in +aerospace. The key expressions that eliminate ambiguity are the +multiplications ```math \begin{aligned} 𝐢 𝐣 &= 𝐤, \\ @@ -163,28 +276,59 @@ have made thus far are the multiplications 𝐤 𝐢 &= 𝐣. \end{aligned} ``` -Everyone agrees that ``𝐢² = 𝐣² = 𝐤² = -1``, so we can also use the -rules above to determine ``𝐢𝐣𝐤 = -𝟏``. Different conventions are -sometimes used (almost exclusively in aerospace) so that this last -equation and the three displayed above have a flipped sign. See +We can also use these rules above to determine ``𝐢𝐣𝐤 = -𝟏``. All +four of these equations have flipped signs in other conventions. See [Sommer et al.](@cite SommerEtAl_2018) for a discussion of the different conventions. We use coordinates ``(W, X, Y, Z)`` on the space of quaternions, so -that such a quaternion would be written as +that a quaternion would be written as ```math -W𝟏 + X𝐢 + Y𝐣 + Z𝐤, +𝐐 = W𝟏 + X𝐢 + Y𝐣 + Z𝐤, ``` -though we usually omit the ``𝟏``. As with standard three-dimensional -space, we could introduce spherical coordinates, though we use a -slight variant: extended Euler coordinates. In our conventions, we -have +though we usually omit the ``𝟏``. The space of all quaternions is +thus four dimensional. The norm is just the standard Euclidean norm, +so that the norm of a quaternion is +```math +\| 𝐐 \| = \sqrt{W^2 + X^2 + Y^2 + Z^2}. +``` +An important operation is the conjugate, which is defined as +```math +\overline{𝐐} = W - X𝐢 - Y𝐣 - Z𝐤. +``` +Note that the squared norm can be written as the quaternion times its +conjugate. The inverse of a quaternion is thus just the conjugate +divided by the squared norm: +```math +𝐐^{-1} = \frac{\overline{𝐐}}{𝐐\overline{𝐐}} = \frac{\overline{𝐐}}{\| 𝐐 \|^2}. +``` +The other important operation is exponentiation. Since a scalar +commutes with any quaternion, including a nonzero scalar component in +the quaternion will simply multiply the result by the exponential of +that scalar component. Moreover, we will not have any use for such an +exponential, so we assume that the argument to the exponential +function is a "pure" quaternion — that is, one with zero scalar +component. Moreover, we write it as a unit quaternion ``𝐮`` times +some real number ``\sigma``. In particular, note that ``𝐮^2 = -1``, +so that it acts like the imaginary unit, which means we already know +how to exponentiate it: +```math +\exp(𝐮\, \sigma) = \cos\sigma + 𝐮\, \sin\sigma. +``` +Note that the inverse of the result can be obtained simply by negating +the argument, as usual. + +Much as with standard three-dimensional space, we could introduce a +generalization of spherical coordinates, though we use a slight +variant: extended Euler coordinates. We will see below how to +interpret these as a series of rotations. For now, we simply state +the relation: ```math \begin{aligned} R &= \sqrt{W^2 + X^2 + Y^2 + Z^2} &&\in [0, \infty), \\ -\alpha &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi], \\ -\beta &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi), \\ -\gamma &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi], +\alpha &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ +\beta &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ +\gamma &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), \end{aligned} ``` where we again assume the ``\arctan`` in the expressions for @@ -197,7 +341,7 @@ the group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, which is a double cover of the rotation group ``\mathrm{SO}(3)``. This extended range for ``\beta`` is necessary to cover the entire space of quaternions; if we further restrict to ``[0, \pi)``, we would -cover the space of rotations. This and the inclusion of ``R`` +only cover the space of rotations. This and the inclusion of ``R`` identify precisely how this coordinate system extends the standard Euler angles. @@ -223,21 +367,93 @@ g_{i'j'} \end{array} \right)_{i'j'}. ``` Again, integration involves a square-root of the determinant of the -metric, which reduces to ``R^3 |\sin\beta| / 8``. +metric, which reduces to ``R^3 |\sin\beta| / 8``. Note that — unlike +with standard spherical coordinates — the absolute value is necessary +because ``\beta`` ranges over the entire interval ``[0, 2\pi]``. The +integral over the entire space of quaternions is then ```math -\int f\, d^4𝐑 +\int_{\mathbb{R}^4} f\, d^4𝐐 = \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty f\, dW\, dX\, dY\, dZ = \int_0^\infty \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, \frac{R^3}{8} |\sin β|\, dR\, dα\, dβ\, dγ. ``` Restricting to the unit sphere, and normalizing so that the integral of 1 over the sphere is 1, we can simplify this to ```math -\int f\, d^3\Omega = \frac{1}{16\pi^2} \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, |\sin β|\, dα\, dβ\, dγ. +\int_{\mathrm{Spin}(3)} f\, d^3\Omega += \frac{1}{16\pi^2} \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, |\sin β|\, dα\, dβ\, dγ. +``` +Finally, restricting to the space of rotations, we can further +simplify this to +```math +\int_{\mathrm{SO}(3)} f\, d^3\Omega += \frac{1}{8\pi^2} \int_0^{2\pi} \int_0^{\pi} \int_0^{2\pi} f\, \sin β\, dα\, dβ\, dγ. ``` ## Rotations +We restrict to a unit quaternion ``𝐑``, for which ``W^2 + X^2 + Y^2 + +Z^2 = 1``. Given this constraint we can, without loss of generality, +write the quaternion as +```math +𝐑 += \exp\left(\frac{\rho}{2} \hat{\mathfrak{r}}\right) += \cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}, +``` +where ``\rho`` is an angle of rotation and ``\hat{\mathfrak{r}}`` is a +unit "pure-vector" quaternion. We can multiply a vector ``𝐯`` as +```math +𝐑\, 𝐯\, 𝐑^{-1}. +``` +Splitting ``𝐯 = 𝐯_⟂ + 𝐯_∥`` into components perpendicular and +parallel to ``\hat{\mathfrak{r}}``, we see that ``𝐯_∥`` commutes with +``𝐑`` and ``𝐑^{-1}``, while ``𝐯_⟂`` anticommutes with +``\hat{\mathfrak{r}}``. To find the full rotation, we expand the +product: +```math +\begin{aligned} +𝐑\, 𝐯\, 𝐑^{-1} +&= 𝐯_∥ + + \left(\cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\right) + 𝐯_⟂ + \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\right) \\ +&= 𝐯_∥ + + \left(\cos\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\, 𝐯_⟂\right) + \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\right) \\ +&= 𝐯_∥ + + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, \hat{\mathfrak{r}}\, 𝐯_⟂ + - \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, 𝐯_⟂ \, \hat{\mathfrak{r}} - \sin^2\frac{\rho}{2}\, \hat{\mathfrak{r}}\, 𝐯_⟂\, \hat{\mathfrak{r}} \\ +&= 𝐯_∥ + + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, [\hat{\mathfrak{r}}, 𝐯_⟂] - \sin^2\frac{\rho}{2}\, 𝐯_⟂ \\ +&= 𝐯_∥ + + \cos\rho\, 𝐯_⟂ + \sin\rho\, \hat{\mathfrak{r}}\times 𝐯_⟂ +\end{aligned} +``` +The final expression shows that this is precisely what we expect when +rotating ``𝐯`` through an angle ``\rho`` (in a positive, right-handed +sense) about the axis ``\hat{\mathfrak{r}}``. + +Note that the presence of two factors of ``𝐑`` in the expression for +rotating a vector explains two things. First, it explains why the +angle of rotation is twice the angle of the quaternion: one factor of +``𝐑`` either commutes and cancels or anti-commutes and combines with +the the other factor. Second, it explains why the quaternion group is +a double cover of the rotation group: negating ``𝐑`` results in the +same rotation. Thus, for any rotation, there are two (precisely +opposite) quaternions that represent it. -## Euler angles and spherical coordinates +### Euler angles and spherical coordinates +Now that we understand how rotations work, we can provide geometric +intuition for the expressions given above for Euler angles. The Euler +angles *in our convention* represent an initial rotation through +``\gamma`` about the ``𝐳`` axis, followed by a rotation through +``\beta`` about the ``𝐲`` axis, and finally a rotation through +``\alpha`` about the ``𝐳`` axis. Note that the axes are fixed, and +not subject to any preceding rotations. More precisely, we can write +the unit quaternion as +```math +𝐑 = \exp\left(\frac{\alpha}{2} 𝐤\right) + \exp\left(\frac{\beta}{2} 𝐣\right) + \exp\left(\frac{\gamma}{2} 𝐤\right). +``` diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index ff3a178a..70c1f6f8 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -401,3 +401,45 @@ d_{m',m}^{j}(2\pi+\beta) &= (-1)^{2j} d_{m',m}^{j}(\beta)\\[6pt] d_{m',m}^{j}(-\beta) &= d_{m,m'}^{j}(\beta) = (-1)^{m'-m} d_{m',m}^{j}(\beta) \end{aligned} ``` + + + + + + + +```math +\begin{gather} +R = \cos\epsilon + \sin\epsilon\, \hat{\mathfrak{r}} \\ +R𝐯 = \cos\epsilon 𝐯 + \sin\epsilon\, \hat{\mathfrak{r}}𝐯 \\ +R𝐯R^{-1} = (𝐯\cos\epsilon + \sin\epsilon\, \hat{\mathfrak{r}}𝐯)(\cos\epsilon - \sin\epsilon\, \hat{\mathfrak{r}}) \\ +R𝐯R^{-1} = 𝐯\cos^2\epsilon + \sin^2\epsilon\, \hat{\mathfrak{r}}𝐯\hat{\mathfrak{r}}^{-1} + \sin\epsilon \cos\epsilon\, (\hat{\mathfrak{r}}𝐯 - 𝐯\hat{\mathfrak{r}}) \\ +R𝐯R^{-1} = \begin{cases} +𝐯 & 𝐯 \hat{\mathfrak{r}} = \hat{\mathfrak{r}}𝐯 \\ +𝐯(\cos^2\epsilon - \sin^2\epsilon) + 2 \sin\epsilon \cos\epsilon\, \frac{[\hat{\mathfrak{r}}, 𝐯]}{2} & 𝐯 \hat{\mathfrak{r}} = -\hat{\mathfrak{r}}𝐯 \\ +\end{cases} \\ +R𝐯R^{-1} = \begin{cases} +𝐯 & 𝐯 \hat{\mathfrak{r}} = \hat{\mathfrak{r}}𝐯 \\ +\cos2\epsilon 𝐯 + \sin2\epsilon \frac{[\hat{\mathfrak{r}}, 𝐯]}{2} & 𝐯 \hat{\mathfrak{r}} = -\hat{\mathfrak{r}}𝐯 \\ +\end{cases} \\ +\end{gather} +``` + + + + +Using techniques from geometric algebra, we can easily prove that the +result is another vector, so we can measure its (squared) norm just by +multiplying it by itself: +```math +\begin{aligned} +\| 𝐑\, 𝐯\, 𝐑^{-1} \|^2 +&= 𝐑\, 𝐯\, 𝐑^{-1}\, 𝐑\, 𝐯\, 𝐑^{-1} \\ +&= 𝐑\, 𝐯\, 𝐯\, 𝐑^{-1} \\ +&= \|𝐯\|^2\, 𝐑\, 𝐑^{-1} \\ +&= \|𝐯\|^2 +\end{aligned} +``` +That is, ``𝐯' = 𝐑\, 𝐯\, 𝐑^{-1}`` has the same norm as ``𝐯``, +which means that ``𝐯'`` is a rotation of ``𝐯``. Given the constraint +on the norm of ``𝐑``, we can rewrite it as diff --git a/docs/src/notes/condon_shortley_expression.py b/docs/src/notes/condon_shortley_expression.py new file mode 100644 index 00000000..fe7d67c7 --- /dev/null +++ b/docs/src/notes/condon_shortley_expression.py @@ -0,0 +1,108 @@ +import marimo + +__generated_with = "0.9.20" +app = marimo.App(width="medium") + + +@app.cell(hide_code=True) +def __(mo): + mo.md( + r""" + Eq. (15) of Sec. 4³ (page 52) of Condon and Shortley (1935) defines the polar portion of the spherical harmonic function as + + \begin{equation} + \Theta(\ell, m) = (-1)^\ell \sqrt{\frac{2\ell+1}{2} \frac{(\ell+m)!}{(\ell-m)!}} + \frac{1}{2^\ell \ell!} \frac{1}{\sin^m\theta} + \frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. + \end{equation} + + A footnote gives the first few values through $\ell=3$. I explicitly test these explicit forms in [`SphericalFunctions.jl`](https://github.com/moble/SphericalFunctions.jl)`/test/conventions/condon_shortley.jl`. Here, I want to verify that they are correct. + + Visually comparing, and accounting for some minor differences in simplification, I find that the expressions in the book are correct. I also use the explicit expressions — as implemented in the test code and translated by AI — to check that sympy can simplify the difference to 0. + """ + ) + return + + +@app.cell +def __(): + from IPython.display import display + import marimo as mo + import sympy + from sympy import S + + θ = sympy.symbols("θ", real=True) + + def ϴ(ℓ, m): + cosθ = sympy.symbols("cosθ", real=True) + return ( + (-1)**ℓ + * sympy.sqrt( + ((2*ℓ+1) / 2) + * (sympy.factorial(ℓ+m) / sympy.factorial(ℓ-m)) + ) + * (1 / (2**ℓ * sympy.factorial(ℓ))) + * (1 / sympy.sin(θ)**m) + #* sympy.diff(sympy.sin(θ)**(2*ℓ), sympy.cos(θ), ℓ-m) # Can't differentiate wrt cos(θ), so we use a dummy and substitute + * sympy.diff((1 - cosθ**2)**ℓ, cosθ, ℓ-m).subs(cosθ, sympy.cos(θ)) + ).simplify() + return S, display, mo, sympy, Θ, θ + + +@app.cell +def __(S, display, Θ): + for ℓ in range(4): + for m in range(-ℓ, ℓ+1): + display(ℓ, m, ϴ(S(ℓ), S(m))) + return l, m + + +@app.cell +def __(S, sympy, Θ, θ): + def compare_explicit_expression(ℓ, m): + if (ℓ, m) == (0, 0): + expression = sympy.sqrt(1/S(2)) + elif (ℓ, m) == (1, 0): + expression = sympy.sqrt(3/S(2)) * sympy.cos(θ) + elif (ℓ, m) == (2, 0): + expression = sympy.sqrt(5/S(8)) * (2*sympy.cos(θ)**2 - sympy.sin(θ)**2) + elif (ℓ, m) == (3, 0): + expression = sympy.sqrt(7/S(8)) * (2*sympy.cos(θ)**3 - 3*sympy.cos(θ)*sympy.sin(θ)**2) + elif (ℓ, m) == (1, 1): + expression = -sympy.sqrt(3/S(4)) * sympy.sin(θ) + elif (ℓ, m) == (1, -1): + expression = sympy.sqrt(3/S(4)) * sympy.sin(θ) + elif (ℓ, m) == (2, 1): + expression = -sympy.sqrt(15/S(4)) * sympy.cos(θ) * sympy.sin(θ) + elif (ℓ, m) == (2, -1): + expression = sympy.sqrt(15/S(4)) * sympy.cos(θ) * sympy.sin(θ) + elif (ℓ, m) == (3, 1): + expression = -sympy.sqrt(21/S(32)) * (4*sympy.cos(θ)**2*sympy.sin(θ) - sympy.sin(θ)**3) + elif (ℓ, m) == (3, -1): + expression = sympy.sqrt(21/S(32)) * (4*sympy.cos(θ)**2*sympy.sin(θ) - sympy.sin(θ)**3) + elif (ℓ, m) == (2, 2): + expression = sympy.sqrt(15/S(16)) * sympy.sin(θ)**2 + elif (ℓ, m) == (2, -2): + expression = sympy.sqrt(15/S(16)) * sympy.sin(θ)**2 + elif (ℓ, m) == (3, 2): + expression = sympy.sqrt(105/S(16)) * sympy.cos(θ) * sympy.sin(θ)**2 + elif (ℓ, m) == (3, -2): + expression = sympy.sqrt(105/S(16)) * sympy.cos(θ) * sympy.sin(θ)**2 + elif (ℓ, m) == (3, 3): + expression = -sympy.sqrt(35/S(32)) * sympy.sin(θ)**3 + elif (ℓ, m) == (3, -3): + expression = sympy.sqrt(35/S(32)) * sympy.sin(θ)**3 + else: + raise ValueError(f"Unknown {ℓ=}, {m=}") + return sympy.simplify(ϴ(S(ℓ), S(m)) - expression) == 0 + + + for _ℓ in range(4): + for _m in range(-_ℓ, _ℓ+1): + print((_ℓ, _m), " \t", compare_explicit_expression(_ℓ, _m)) + + return (compare_explicit_expression,) + + +if __name__ == "__main__": + app.run() diff --git a/docs/src/notes/conventions.py b/docs/src/notes/conventions.py new file mode 100644 index 00000000..70895493 --- /dev/null +++ b/docs/src/notes/conventions.py @@ -0,0 +1,261 @@ +import marimo + +__generated_with = "0.10.6" +app = marimo.App(width="medium") + + +@app.cell +def _(): + import marimo as mo + + import numpy as np + import quaternion + import sympy + return mo, np, quaternion, sympy + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r"""## Three-dimensional space""") + return + + +@app.cell +def _(): + def three_dimensional_coordinates(): + import sympy + # Make symbols representing spherical coordinates + r, θ, ϕ = sympy.symbols("r θ ϕ", real=True, positive=True, zero=False) + # Define Cartesian coordinates in terms of spherical + x = r * sympy.sin(θ) * sympy.cos(ϕ) + y = r * sympy.sin(θ) * sympy.sin(ϕ) + z = r * sympy.cos(θ) + return (x, y, z), (r, θ, ϕ) + return (three_dimensional_coordinates,) + + +@app.cell +def _(sympy, three_dimensional_coordinates): + def three_dimensional_metric(): + (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() + # The Cartesian metric is just the 3x3 identity matrix + metric_cartesian = sympy.eye(3) + # Compute the coordinate transformation to obtain the spherical metric + jacobian = sympy.Matrix([x, y, z]).jacobian([r, θ, ϕ]) + metric_spherical = sympy.simplify((jacobian.T * metric_cartesian * jacobian)) + + return metric_spherical + + three_dimensional_metric() + return (three_dimensional_metric,) + + +@app.cell +def _(sympy, three_dimensional_coordinates): + def three_dimensional_coordinate_basis(): + (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() + basis = [ + [sympy.diff(xi, coord) for xi in (x, y, z)] + for coord in (r, θ, ϕ) + ] + # Normalize each basis vector + basis = [ + [sympy.simplify(comp / sympy.sqrt(sum(b**2 for b in vec))).subs(abs(sympy.sin(θ)), sympy.sin(θ)) + for comp in vec] + for vec in basis + ] + + return basis + + three_dimensional_coordinate_basis() + return (three_dimensional_coordinate_basis,) + + +@app.cell +def _(sympy, three_dimensional_coordinates, three_dimensional_metric): + def three_dimensional_volume_element(): + (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() + metric_spherical = three_dimensional_metric() + volume_element = sympy.simplify(sympy.sqrt(metric_spherical.det())).subs(abs(sympy.sin(θ)), sympy.sin(θ)) + + return volume_element + + three_dimensional_volume_element() + return (three_dimensional_volume_element,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r"""## Four-dimensional space""") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r"""### Geometric algebra""") + return + + +@app.cell +def _(): + import galgebra + + from galgebra.ga import Ga + return Ga, galgebra + + +@app.cell +def _(): + def three_dimensional_geometric_algebra(): + from galgebra.ga import Ga + o3d, x, y, z = Ga.build("x y z", g=[1, 1, 1]) + + i = z * y + j = x * z + k = y * x + I = x * y * z + + return x, y, z, i, j, k, I + return (three_dimensional_geometric_algebra,) + + +@app.cell +def _(three_dimensional_geometric_algebra): + def check_basis_definitions(): + x, y, z, i, j, k, I = three_dimensional_geometric_algebra() + return [ + i == z*y == -y*z, + j == x*z == -z*x, + k == y*x == -x*y, + I == x*y*z == y*z*x == z*x*y == -x*z*y == -y*x*z == -z*y*x, + ] + + check_basis_definitions() + return (check_basis_definitions,) + + +@app.cell +def _(three_dimensional_geometric_algebra): + def check_duals(): + x, y, z, i, j, k, I = three_dimensional_geometric_algebra() + return [ + i == I.inv() * x, + j == I.inv() * y, + k == I.inv() * z, + ] + + check_duals() + return (check_duals,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md("""### Quaternions and Euler angles""") + return + + +@app.cell +def _(three_dimensional_geometric_algebra): + def check_basis_multiplication(): + x, y, z, i, j, k, I = three_dimensional_geometric_algebra() + return [ + i*j == k, + j*k == i, + k*i == j, + i*j*k == -1, + ] + + check_basis_multiplication() + return (check_basis_multiplication,) + + +@app.cell +def _(): + def four_dimensional_coordinates(): + import sympy + # Make symbols representing spherical coordinates + R, α, β, γ = sympy.symbols("R α β γ", real=True, positive=True) + # Define Cartesian coordinates in terms of spherical + W = R * sympy.cos(β/2) * sympy.cos((α + γ)/2) + X = -R * sympy.sin(β/2) * sympy.sin((α - γ)/2) + Y = R * sympy.sin(β/2) * sympy.cos((α - γ)/2) + Z = R * sympy.cos(β/2) * sympy.sin((α + γ)/2) + + return (W, X, Y, Z), (R, α, β, γ) + return (four_dimensional_coordinates,) + + +@app.cell +def _(four_dimensional_coordinates): + def four_dimensional_metric(): + import sympy + (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() + # The Cartesian metric is just the 4x4 identity matrix + metric_cartesian = sympy.eye(4) + # Compute the coordinate transformation to obtain the spherical metric + jacobian = sympy.Matrix([W, X, Y, Z]).jacobian([R, α, β, γ]) + metric_spherical = sympy.simplify((jacobian.T * metric_cartesian * jacobian)) + + return metric_spherical + + four_dimensional_metric() + return (four_dimensional_metric,) + + +@app.cell +def _(four_dimensional_metric): + def four_dimensional_volume_element(): + import sympy + metric_spherical = four_dimensional_metric() + # Compute the volume element + volume_element = sympy.sqrt(metric_spherical.det()).simplify() + + return volume_element + + four_dimensional_volume_element() + return (four_dimensional_volume_element,) + + +@app.cell +def _(four_dimensional_coordinates, four_dimensional_volume_element): + def Spin3_integral(): + import sympy + (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() + volume_element = four_dimensional_volume_element() + # Integrate over the unit sphere + integral = (1/(16*sympy.pi**2)) * sympy.integrate(abs(sympy.sin(β)), (α, 0, 2*sympy.pi), (β, 0, 2*sympy.pi), (γ, 0, 2*sympy.pi)) + + return integral + + Spin3_integral() + return (Spin3_integral,) + + +@app.cell +def _(four_dimensional_coordinates, four_dimensional_volume_element): + def SO3_integral(): + import sympy + (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() + volume_element = four_dimensional_volume_element() + # Integrate over the unit sphere + integral = (1/(8*sympy.pi**2)) * sympy.integrate(sympy.sin(β), (α, 0, 2*sympy.pi), (β, 0, sympy.pi), (γ, 0, 2*sympy.pi)) + + return integral + + SO3_integral() + return (SO3_integral,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r"""## Rotations""") + return + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run() From 68af64aca69cbacc3a5733bc3b3363ba74a7fa97 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 23 Dec 2024 23:42:13 -0600 Subject: [PATCH 019/329] Rearrange the notebooks --- docs/make.jl | 4 +- .../condon_shortley_expression.py | 0 docs/{src/notes => notebooks}/conventions.py | 143 +++++++++++++----- 3 files changed, 106 insertions(+), 41 deletions(-) rename docs/{src/notes => notebooks}/condon_shortley_expression.py (100%) rename docs/{src/notes => notebooks}/conventions.py (59%) diff --git a/docs/make.jl b/docs/make.jl index e51ceea4..47e2cd14 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,6 @@ # Run with -# time julia --project=. make.jl && julia --project=. -e 'using LiveServer; serve(dir="build")' -# assuming you are in this `docs` directory (otherwise point the project argument here) +# julia -t 4 --project=. scripts/docs.jl +# assuming you are in this top-level directory using SphericalFunctions using Documenter diff --git a/docs/src/notes/condon_shortley_expression.py b/docs/notebooks/condon_shortley_expression.py similarity index 100% rename from docs/src/notes/condon_shortley_expression.py rename to docs/notebooks/condon_shortley_expression.py diff --git a/docs/src/notes/conventions.py b/docs/notebooks/conventions.py similarity index 59% rename from docs/src/notes/conventions.py rename to docs/notebooks/conventions.py index 70895493..7565e522 100644 --- a/docs/src/notes/conventions.py +++ b/docs/notebooks/conventions.py @@ -4,19 +4,19 @@ app = marimo.App(width="medium") -@app.cell +@app.cell(hide_code=True) def _(): import marimo as mo - import numpy as np - import quaternion import sympy - return mo, np, quaternion, sympy + # import numpy as np + # import quaternion + return mo, sympy @app.cell(hide_code=True) def _(mo): - mo.md(r"""## Three-dimensional space""") + mo.md("""## Three-dimensional space""") return @@ -35,8 +35,25 @@ def three_dimensional_coordinates(): @app.cell -def _(sympy, three_dimensional_coordinates): +def _(mo, sympy, three_dimensional_coordinates): + mo.output.append(mo.md("We define the spherical coordinates in terms of the Cartesian coordinates such that")) + mo.output.append( + [ + sympy.Eq( + sympy.Symbol(f"{cartesian}"), + spherical, + evaluate=False + ) + for cartesian, spherical in zip(("x", "y", "z"), three_dimensional_coordinates()[0]) + ] + ) + return + + +@app.cell +def _(mo, three_dimensional_coordinates): def three_dimensional_metric(): + import sympy (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() # The Cartesian metric is just the 3x3 identity matrix metric_cartesian = sympy.eye(3) @@ -46,13 +63,15 @@ def three_dimensional_metric(): return metric_spherical - three_dimensional_metric() + mo.output.append(mo.md("Using the Euclidean metric as the 3x3 identity in Cartesian coordinates, we can transform to spherical to obtain")) + mo.output.append(three_dimensional_metric()) return (three_dimensional_metric,) @app.cell -def _(sympy, three_dimensional_coordinates): +def _(mo, three_dimensional_coordinates): def three_dimensional_coordinate_basis(): + import sympy (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() basis = [ [sympy.diff(xi, coord) for xi in (x, y, z)] @@ -60,50 +79,79 @@ def three_dimensional_coordinate_basis(): ] # Normalize each basis vector basis = [ - [sympy.simplify(comp / sympy.sqrt(sum(b**2 for b in vec))).subs(abs(sympy.sin(θ)), sympy.sin(θ)) - for comp in vec] - for vec in basis + sympy.Eq( + sympy.Symbol(rf"\hat{{{symbol}}}"), + sympy.Matrix([ + sympy.simplify(comp / sympy.sqrt(sum(b**2 for b in vector))).subs(abs(sympy.sin(θ)), sympy.sin(θ)) + for comp in vector + ]), + evaluate=False + ) + for symbol, vector in zip((r, θ, ϕ), basis) ] return basis - three_dimensional_coordinate_basis() + mo.output.append(mo.md( + """ + Differentiating the $(x,y,z)$ coordinates with respect to $(r, θ, ϕ)$, we + find the unit spherical coordinate basis vectors to have Cartesian components + """ + )) + mo.output.append(three_dimensional_coordinate_basis()) return (three_dimensional_coordinate_basis,) @app.cell -def _(sympy, three_dimensional_coordinates, three_dimensional_metric): +def _(mo, three_dimensional_coordinates, three_dimensional_metric): def three_dimensional_volume_element(): + import sympy (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() metric_spherical = three_dimensional_metric() volume_element = sympy.simplify(sympy.sqrt(metric_spherical.det())).subs(abs(sympy.sin(θ)), sympy.sin(θ)) return volume_element - three_dimensional_volume_element() + + mo.output.append(mo.md("The volume form in spherical coordinates is")) + mo.output.append(three_dimensional_volume_element()) return (three_dimensional_volume_element,) +@app.cell +def _(mo, three_dimensional_coordinates, three_dimensional_volume_element): + def S2_normalized_volume_element(): + import sympy + (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() + volume_element = three_dimensional_volume_element().subs(r, 1) + # Integrate over the unit sphere + integral = sympy.integrate(sympy.sin(θ), (ϕ, 0, 2*sympy.pi), (θ, 0, sympy.pi)) + + return volume_element / integral + + + mo.output.append(mo.md( + """ + Restricting to the unit sphere and normalizing so that the integral + over the sphere is 1, we have the normalized volume element: + """ + )) + mo.output.append(S2_normalized_volume_element()) + return (S2_normalized_volume_element,) + + @app.cell(hide_code=True) def _(mo): - mo.md(r"""## Four-dimensional space""") + mo.md("""## Four-dimensional space""") return @app.cell(hide_code=True) def _(mo): - mo.md(r"""### Geometric algebra""") + mo.md("""### Geometric algebra""") return -@app.cell -def _(): - import galgebra - - from galgebra.ga import Ga - return Ga, galgebra - - @app.cell def _(): def three_dimensional_geometric_algebra(): @@ -217,43 +265,60 @@ def four_dimensional_volume_element(): @app.cell -def _(four_dimensional_coordinates, four_dimensional_volume_element): - def Spin3_integral(): +def _(four_dimensional_coordinates, four_dimensional_volume_element, mo): + def Spin3_normalized_volume_element(): import sympy (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() - volume_element = four_dimensional_volume_element() + volume_element = four_dimensional_volume_element().subs(R, 1) # Integrate over the unit sphere - integral = (1/(16*sympy.pi**2)) * sympy.integrate(abs(sympy.sin(β)), (α, 0, 2*sympy.pi), (β, 0, 2*sympy.pi), (γ, 0, 2*sympy.pi)) + integral = sympy.integrate(volume_element, (α, 0, 2*sympy.pi), (β, 0, 2*sympy.pi), (γ, 0, 2*sympy.pi)) + + return volume_element / integral - return integral - Spin3_integral() - return (Spin3_integral,) + mo.output.append(mo.md( + r""" + Restricting to the unit three-sphere, $\mathrm{Spin}(3)$, and normalizing so that the integral + over the sphere is 1, we have the normalized volume element: + """ + )) + mo.output.append(Spin3_normalized_volume_element()) + return (Spin3_normalized_volume_element,) @app.cell -def _(four_dimensional_coordinates, four_dimensional_volume_element): - def SO3_integral(): +def _(four_dimensional_coordinates, four_dimensional_volume_element, mo): + def SO3_normalized_volume_element(): import sympy (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() - volume_element = four_dimensional_volume_element() + volume_element = four_dimensional_volume_element().subs(R, 1) # Integrate over the unit sphere - integral = (1/(8*sympy.pi**2)) * sympy.integrate(sympy.sin(β), (α, 0, 2*sympy.pi), (β, 0, sympy.pi), (γ, 0, 2*sympy.pi)) + integral = sympy.integrate(volume_element, (α, 0, 2*sympy.pi), (β, 0, sympy.pi), (γ, 0, 2*sympy.pi)) - return integral + return volume_element / integral - SO3_integral() - return (SO3_integral,) + + mo.output.append(mo.md( + r""" + Restricting to $\mathrm{SO}(3)$ and normalizing so that the integral + over the sphere is 1, we have the normalized volume element: + """ + )) + mo.output.append(SO3_normalized_volume_element()) + return (SO3_normalized_volume_element,) @app.cell(hide_code=True) def _(mo): - mo.md(r"""## Rotations""") + mo.md("""## Rotations""") return @app.cell def _(): + # Check that Euler angles (α, β, γ) rotate the basis vectors onto (r, \theta, \phi) + + return From e06fccc0152e379ad1e952f1fd02be4b953385a4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 23 Dec 2024 23:43:07 -0600 Subject: [PATCH 020/329] Figure out the rotation structure --- docs/src/conventions/conventions.md | 122 ++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 2dee40f1..2db9cae8 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -61,13 +61,52 @@ Euler angles. can compute the expressions in Euler angles for the basis vectors: ```math \begin{aligned} - L_x = L_𝐢 &= -i \left( \sin\alpha \frac{\partial}{\partial\beta} + \cos\alpha \cot\beta \frac{\partial}{\partial\alpha} \right), \\ - L_y = L_𝐣 &= -i \left( \cos\alpha \frac{\partial}{\partial\beta} - \sin\alpha \cot\beta \frac{\partial}{\partial\alpha} \right), \\ - L_z = L_𝐤 &= -i \frac{\partial}{\partial\alpha}. + L_x = L_𝐢 &= -i \left\{ + -\frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + - \sin\alpha \frac{\partial} {\partial \beta} + +\frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \right\} \\ + L_y = L_𝐣 &= -i \left\{ + -\frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + + \cos\alpha \frac{\partial} {\partial \beta} + +\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \right\} \\ + L_z = L_𝐤 &= -i \frac{\partial} {\partial \alpha} \\ + K_x = K_𝐢 &= -i \left\{ + -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} + +\sin\gamma \frac{\partial} {\partial \beta} + +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} + \right\} \\ + K_y = K_𝐣 &= -i \left\{ + \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} + +\cos\gamma \frac{\partial} {\partial \beta} + -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} + \right\} \\ + K_z = K_𝐤 &= -i \frac{\partial} {\partial \gamma} \end{aligned} ``` - Angular-momentum operators defined in Lie terms, translated to - Euler angles and spherical coordinates. + We can lift any function on ``S^2`` to a function on ``S^3`` — or + more precisely any function on spherical coordinates to a function + on the space of Euler angles — by the correspondence ``(\theta, + \phi) \mapsto (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We + can then express the angular-momentum operators in their more + common form, in terms of spherical coordinates: + + ```math + \begin{aligned} + L_x &= -i \left\{ + -\frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} + - \sin\phi \frac{\partial} {\partial \theta} + \right\} \\ + L_y &= -i \left\{ + -\frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} + + \cos\phi \frac{\partial} {\partial \theta} + \right\} \\ + L_z &= -i \frac{\partial} {\partial \phi} + \end{aligned} + ``` + (The ``R`` operators make less sense for a function of spherical + coordinates.) 9. Spherical harmonics 10. Wigner D-matrices 11. Spin-weighted spherical harmonics @@ -456,4 +495,77 @@ the unit quaternion as \exp\left(\frac{\beta}{2} 𝐣\right) \exp\left(\frac{\gamma}{2} 𝐤\right). ``` +One of the more important interpretations of a rotor is considering +what it does to the basis triad ``(𝐱, 𝐲, 𝐳)``. In particular, the +vector ``𝐳`` is rotated onto the point given by spherical coordinates +``(\theta, \phi) = (\beta, \alpha)``, while ``𝐱`` and ``𝐲`` are +rotated into the plane spanned by the unit basis vectors +``\boldsymbol{\theta}`` and ``\boldsymbol{\phi}`` corresponding to +that point. If ``\gamma = 0`` the rotation is precise, meaning that +``𝐱`` is rotated onto ``\boldsymbol{\theta}`` and ``𝐲`` onto +``\boldsymbol{\phi}``; if ``\gamma ≠ 0`` then they are rotated within +that plane by the angle ``\gamma`` about the ``\mathbf{r}`` axis. +Thus, we identify the spherical coordinates ``(\theta, \phi)`` with +the Euler angles ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. + + +## Rotation and angular-momentum operators + +We have defined the spaces ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` +(topologically ``S^3``), ``\mathrm{SO}(3)`` (topologically +``\mathbb{RP}^3``), and ``S^2``. Specifically, we have *constructed* +each of those spaces starting with Cartesian coordinates and the +Euclidean norm on ``\mathbb{R}^3``, which naturally supplies +coordinates on each of those spaces. We will define functions from +these spaces (and their corresponding coordinates) to the complex +numbers. However, to construct and classify those functions, we will +need to define operators on them. We will start with operators +transforming them by finite rotations, then differentiate those +operators to get the angular-momentum operators. + + + + - Start with finite rotations — both left and right translations + - note the signs; these give us the signs + - Then, we differentiate those finite rotations, generating rotation + of a function by exponentiating a generator giving finite + rotation; this lets us set some signs + - Express angular momentum operators in terms of quaternion components + - Basic Lie definition + - Properties: form a Lie algebra with the commutator as the Lie bracket + - + - Express angular momentum operators in terms of Euler angles + - We just rewrite the ``R`` in the Lie definitions in terms of + Euler angles, multiply by ``\exp(\theta/2)``, rederive the new + Euler angles from that result, and use the chain rule + - Show for both the three- and two-spheres + - Show how they act on functions on the three-sphere + + +### Angular-momentum operators in Euler angles + +The idea here is to express, e.g., $e^{\theta \mathbf{e}_i / +2}\mathbf{R}_{\alpha, \beta, \gamma}$ in quaternion components, then +solve for the new Euler angles $\mathbf{R}_{\alpha', \beta', \gamma'}$ +in terms of the quaternion components, where these new angles all +depend on $\theta$. We then use the chain rule to express +$\partial_\theta$ in terms of $\partial_{\alpha'}$, etc., which become +$\partial_\alpha$, etc., when $\theta=0$. + +```math +\begin{align} + L_i f(\mathbf{R}) + &= + \left. -\mathbf{z} \frac{\partial} {\partial \theta} f \left( e^{\theta \mathbf{e}_i / 2} \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} \\ + &= + \left. -\mathbf{z} \frac{\partial} {\partial \theta} f \left( \mathbf{R}_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ + &= + \left. -\mathbf{z} \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha'} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta'} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma'} \right] f \left( \mathbf{R}_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ + &= + -\mathbf{z} \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right) \\ + K_i f(\mathbf{R}) + &= + -\mathbf{z} \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right), +\end{align} +``` \ No newline at end of file From 56c4224f0bbf76eb9ea7463dbe4423165be19a39 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 23 Dec 2024 23:44:57 -0600 Subject: [PATCH 021/329] Ignore marimo stuff --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 710fb36f..486dff8b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ benchmark/results.md # Ignore my notes and settings /notes .vscode - +conventions.slides.json rotate.jl From 27de3f6fc615530c5cabdc237aff267518e3ad80 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 27 Dec 2024 17:03:01 -0600 Subject: [PATCH 022/329] Add lifting diagram --- docs/make.jl | 2 +- docs/src/assets/Lifting_diagram.svg | 240 ++++++++++++++++++++++ docs/src/assets/Lifting_diagram_dark.svg | 242 +++++++++++++++++++++++ docs/src/assets/extras.css | 19 ++ 4 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 docs/src/assets/Lifting_diagram.svg create mode 100644 docs/src/assets/Lifting_diagram_dark.svg create mode 100644 docs/src/assets/extras.css diff --git a/docs/make.jl b/docs/make.jl index 47e2cd14..53a46cd6 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -21,7 +21,7 @@ makedocs( prettyurls = !("local" in ARGS), # Use clean URLs, unless built as a "local" build edit_link = "main", # Link out to "main" branch on github canonical = "https://moble.github.io/SphericalFunctions.jl/stable/", - assets = String["assets/citations.css"], + assets = String["assets/citations.css", "assets/extras.css"], ), pages = [ "index.md", diff --git a/docs/src/assets/Lifting_diagram.svg b/docs/src/assets/Lifting_diagram.svg new file mode 100644 index 00000000..d409a276 --- /dev/null +++ b/docs/src/assets/Lifting_diagram.svg @@ -0,0 +1,240 @@ + + + + diff --git a/docs/src/assets/Lifting_diagram_dark.svg b/docs/src/assets/Lifting_diagram_dark.svg new file mode 100644 index 00000000..1501484b --- /dev/null +++ b/docs/src/assets/Lifting_diagram_dark.svg @@ -0,0 +1,242 @@ + + + + diff --git a/docs/src/assets/extras.css b/docs/src/assets/extras.css new file mode 100644 index 00000000..0d05f2ea --- /dev/null +++ b/docs/src/assets/extras.css @@ -0,0 +1,19 @@ +.display-light-only { + display: block; + text-align: center; + margin: 0 auto; +} + +.display-dark-only { + display: none; +} + +.theme--documenter-dark .display-light-only { + display: none; +} + +.theme--documenter-dark .display-dark-only { + display: block; + text-align: center; + margin: 0 auto; +} From 96ec5b3eff8d2a252e9de4689fa57a0b39c2c536 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 28 Dec 2024 10:07:30 -0600 Subject: [PATCH 023/329] Add composition diagram --- docs/src/assets/Makefile | 41 ++++++++++++++++ docs/src/assets/composition_diagram.tex | 26 ++++++++++ docs/src/assets/composition_diagram_dark.svg | 49 +++++++++++++++++++ docs/src/assets/composition_diagram_light.svg | 49 +++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 docs/src/assets/Makefile create mode 100644 docs/src/assets/composition_diagram.tex create mode 100644 docs/src/assets/composition_diagram_dark.svg create mode 100644 docs/src/assets/composition_diagram_light.svg diff --git a/docs/src/assets/Makefile b/docs/src/assets/Makefile new file mode 100644 index 00000000..97414b7f --- /dev/null +++ b/docs/src/assets/Makefile @@ -0,0 +1,41 @@ +# Makefile to generate standard and inverted color diagrams + +# Define variables +TEX=composition_diagram.tex +STANDARD_PDF=composition_diagram_light.pdf +INVERTED_PDF=composition_diagram_dark.pdf +STANDARD_SVG=composition_diagram_light.svg +INVERTED_SVG=composition_diagram_dark.svg + +.PHONY: all clean allclean + +all: $(STANDARD_SVG) $(INVERTED_SVG) clean + +# Compile standard colors +$(STANDARD_PDF): $(TEX) + @echo "Compiling standard color diagram..." + pdflatex -interaction=nonstopmode -jobname=composition_diagram_light $(TEX) + +# Compile inverted colors +$(INVERTED_PDF): $(TEX) + @echo "Compiling inverted color diagram..." + pdflatex -interaction=nonstopmode -jobname=composition_diagram_dark "\def\invertcolors{} \input{$(TEX)}" + +# Convert PDFs to SVGs using pdf2svg +$(STANDARD_SVG): $(STANDARD_PDF) + @echo "Converting standard PDF to SVG..." + pdf2svg $(STANDARD_PDF) $(STANDARD_SVG) + +$(INVERTED_SVG): $(INVERTED_PDF) + @echo "Converting inverted PDF to SVG..." + pdf2svg $(INVERTED_PDF) $(INVERTED_SVG) + +clean: + @echo "Cleaning up extra generated files..." + rm -f composition_diagram*.aux composition_diagram*.log composition_diagram*.out + rm -f composition_diagram*.pdf composition_diagram*.fdb_latexmk composition_diagram*.fls + rm -f composition_diagram*.synctex.gz + +allclean: clean + @echo "Cleaning up generated SVGs..." + rm -f composition_diagram*.svg diff --git a/docs/src/assets/composition_diagram.tex b/docs/src/assets/composition_diagram.tex new file mode 100644 index 00000000..fe779cd7 --- /dev/null +++ b/docs/src/assets/composition_diagram.tex @@ -0,0 +1,26 @@ +\documentclass[tikz]{standalone} +\usepackage{tikz-cd} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{amsfonts} +\usepackage{xcolor} + +% Run with `pdflatex "\def\invertcolors{} \input{$(TEX)}"` to invert colors +\ifdefined\invertcolors + \color{white} +\else + \color{black} +\fi + +\begin{document} +% https://q.uiver.app/#q=WzAsMyxbMCwwLCJBIl0sWzIsMywiXFxtYXRoYmJ7Q30iXSxbNCwwLCJCIl0sWzAsMSwiZiIsMix7InN0eWxlIjp7ImJvZHkiOnsibmFtZSI6ImRhc2hlZCJ9fX1dLFswLDIsIm0iXSxbMiwxLCJGIl1d +\begin{tikzcd} + A &&&& B \\ + \\ + \\ + && {\mathbb{C}} + \arrow["m", from=1-1, to=1-5] + \arrow["f"', dashed, from=1-1, to=4-3] + \arrow["F", from=1-5, to=4-3] +\end{tikzcd} +\end{document} \ No newline at end of file diff --git a/docs/src/assets/composition_diagram_dark.svg b/docs/src/assets/composition_diagram_dark.svg new file mode 100644 index 00000000..e323f2f2 --- /dev/null +++ b/docs/src/assets/composition_diagram_dark.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/assets/composition_diagram_light.svg b/docs/src/assets/composition_diagram_light.svg new file mode 100644 index 00000000..fafabb59 --- /dev/null +++ b/docs/src/assets/composition_diagram_light.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c76cadd4799f2a469323b4c74263b8a7fce75e77 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 28 Dec 2024 10:08:04 -0600 Subject: [PATCH 024/329] Remove lifting diagrams --- docs/src/assets/Lifting_diagram.svg | 240 ---------------------- docs/src/assets/Lifting_diagram_dark.svg | 242 ----------------------- 2 files changed, 482 deletions(-) delete mode 100644 docs/src/assets/Lifting_diagram.svg delete mode 100644 docs/src/assets/Lifting_diagram_dark.svg diff --git a/docs/src/assets/Lifting_diagram.svg b/docs/src/assets/Lifting_diagram.svg deleted file mode 100644 index d409a276..00000000 --- a/docs/src/assets/Lifting_diagram.svg +++ /dev/null @@ -1,240 +0,0 @@ - - - - diff --git a/docs/src/assets/Lifting_diagram_dark.svg b/docs/src/assets/Lifting_diagram_dark.svg deleted file mode 100644 index 1501484b..00000000 --- a/docs/src/assets/Lifting_diagram_dark.svg +++ /dev/null @@ -1,242 +0,0 @@ - - - - From 8b1b33154003444a4d0fb08dd6d429a4de53077c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 28 Dec 2024 10:18:10 -0600 Subject: [PATCH 025/329] Start the section on functions --- docs/src/conventions/conventions.md | 67 +++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 2db9cae8..c8774450 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -336,8 +336,8 @@ An important operation is the conjugate, which is defined as \overline{𝐐} = W - X𝐢 - Y𝐣 - Z𝐤. ``` Note that the squared norm can be written as the quaternion times its -conjugate. The inverse of a quaternion is thus just the conjugate -divided by the squared norm: +conjugate. Any nonzero quaternion has an inverse, which is just the +conjugate divided by the squared norm: ```math 𝐐^{-1} = \frac{\overline{𝐐}}{𝐐\overline{𝐐}} = \frac{\overline{𝐐}}{\| 𝐐 \|^2}. ``` @@ -511,18 +511,57 @@ the Euler angles ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. ## Rotation and angular-momentum operators -We have defined the spaces ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` -(topologically ``S^3``), ``\mathrm{SO}(3)`` (topologically -``\mathbb{RP}^3``), and ``S^2``. Specifically, we have *constructed* -each of those spaces starting with Cartesian coordinates and the -Euclidean norm on ``\mathbb{R}^3``, which naturally supplies -coordinates on each of those spaces. We will define functions from -these spaces (and their corresponding coordinates) to the complex -numbers. However, to construct and classify those functions, we will -need to define operators on them. We will start with operators -transforming them by finite rotations, then differentiate those -operators to get the angular-momentum operators. - +## Complex-valued functions + +Starting with Cartesian coordinates and the Euclidean norm on +``\mathbb{R}^3``, we have *constructed* the geometric algebra over +that space, as well as the spaces ``\mathrm{Spin}(3) = +\mathrm{SU}(2)`` (topologically ``S^3``), ``\mathrm{SO}(3)`` +(topologically ``\mathbb{RP}^3``), and ``S^2``. We will be defining +complex-valued functions on these spaces, and defining operators to +construct and classify them. In particular, because we have +constructed the spaces, they are naturally supplied with coordinates +that are effectively inherited from the original Cartesian system. We +will be using these coordinate systems to construct both the operators +and functions. However, it is important to note that the coordinate +systems may have singularities, which means that the spaces of +coordinates may have different topologies than the spaces they +represent. For example, Euler angles have topology ``S^1 \times I +\times S^1`` instead of the ``S^3`` and ``\mathbb{RP}^3`` topologies +of the spaces they represent; spherical coordinates have topology +``S^1 \times I`` instead of ``S^2``. + +Defining functions on the coordinate system of a space is subtly +different from defining functions on the space itself. For example, +spin-weighted functions are generally written as functions of +(``S^2``) spherical coordinates. However, they *cannot* be defined as +functions on ``S^2`` itself; some notion of a reference tangent +direction is needed at each point. The difference is that spherical +*coordinates* supply a natural choice for the reference tangent +direction: the unit vector in the ``\boldsymbol{\theta}`` direction. +This supplies just enough information to define the spin-weighted +functions — though this ends up not being a useful form when more +general transformations or deeper understanding are needed. + +An important concept is that of a +["lift"](https://en.wikipedia.org/wiki/Lift_(mathematics)). Given +``f`` and ``g`` in the diagram below, a lift of ``f`` is a function +``h`` such that ``f = g \circ h``. +```@raw html + Lifting diagram showing relationships between spaces + Lifting diagram showing relationships between spaces +``` +Here, there are several relevant cases. Functions on ``S^2`` can be +lifted to ``S^3``; functions on either of those spaces can be lifted +to their coordinate spaces; etc. + + + +!!! note "Lifts a a a a a a a a a a a a a a a a a a a a a a a a a a a" + Because of lifts or pushbacks, we have some freedom to define + functions on the "largest" space available. - Start with finite rotations — both left and right translations From 65b3dc5c30a91d01d7c4762aa4e69eb659f14dfa Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 29 Dec 2024 00:53:42 -0600 Subject: [PATCH 026/329] Move diagram into markdown --- docs/src/assets/Makefile | 52 ++++++++---- docs/src/assets/composition_diagram.svg | 37 +++++++++ docs/src/assets/composition_diagram.tex | 3 +- docs/src/assets/composition_diagram_dark.svg | 82 ++++++++----------- docs/src/assets/composition_diagram_light.svg | 72 ++++++---------- docs/src/assets/extras.css | 27 ++++++ docs/src/conventions/conventions.md | 35 ++++++-- 7 files changed, 186 insertions(+), 122 deletions(-) create mode 100644 docs/src/assets/composition_diagram.svg diff --git a/docs/src/assets/Makefile b/docs/src/assets/Makefile index 97414b7f..c3210834 100644 --- a/docs/src/assets/Makefile +++ b/docs/src/assets/Makefile @@ -2,6 +2,8 @@ # Define variables TEX=composition_diagram.tex +STANDARD_DVI=composition_diagram_light.dvi +INVERTED_DVI=composition_diagram_dark.dvi STANDARD_PDF=composition_diagram_light.pdf INVERTED_PDF=composition_diagram_dark.pdf STANDARD_SVG=composition_diagram_light.svg @@ -11,30 +13,48 @@ INVERTED_SVG=composition_diagram_dark.svg all: $(STANDARD_SVG) $(INVERTED_SVG) clean -# Compile standard colors -$(STANDARD_PDF): $(TEX) - @echo "Compiling standard color diagram..." - pdflatex -interaction=nonstopmode -jobname=composition_diagram_light $(TEX) +$(STANDARD_DVI): $(TEX) + @echo "Compiling standard-color diagram to DVI..." + lualatex -output-format=dvi -jobname=composition_diagram_light $(TEX) -# Compile inverted colors -$(INVERTED_PDF): $(TEX) - @echo "Compiling inverted color diagram..." - pdflatex -interaction=nonstopmode -jobname=composition_diagram_dark "\def\invertcolors{} \input{$(TEX)}" +$(INVERTED_DVI): $(TEX) + @echo "Compiling inverted-color diagram to DVI..." + lualatex -output-format=dvi -jobname=composition_diagram_dark "\def\invertcolors{} \input{$(TEX)}" -# Convert PDFs to SVGs using pdf2svg -$(STANDARD_SVG): $(STANDARD_PDF) - @echo "Converting standard PDF to SVG..." - pdf2svg $(STANDARD_PDF) $(STANDARD_SVG) +$(STANDARD_SVG): $(STANDARD_DVI) + @echo "Converting standard DVI to SVG..." + dvisvgm --libgs=/opt/homebrew/lib/libgs.dylib -o $(STANDARD_SVG) $(STANDARD_DVI) -$(INVERTED_SVG): $(INVERTED_PDF) - @echo "Converting inverted PDF to SVG..." - pdf2svg $(INVERTED_PDF) $(INVERTED_SVG) +$(INVERTED_SVG): $(INVERTED_DVI) + @echo "Converting inverted DVI to SVG..." + dvisvgm --libgs=/opt/homebrew/lib/libgs.dylib -o $(INVERTED_SVG) $(INVERTED_DVI) + +# # Compile standard colors +# $(STANDARD_PDF): $(TEX) +# @echo "Compiling standard color diagram..." +# xelatex -interaction=nonstopmode -jobname=composition_diagram_light $(TEX) + +# # Compile inverted colors +# $(INVERTED_PDF): $(TEX) +# @echo "Compiling inverted color diagram..." +# xelatex -interaction=nonstopmode -jobname=composition_diagram_dark "\def\invertcolors{} \input{$(TEX)}" + +# # Convert PDFs to SVGs using pdf2svg +# $(STANDARD_SVG): $(STANDARD_PDF) +# @echo "Converting standard PDF to SVG..." +# pdf2svg $(STANDARD_PDF) $(STANDARD_SVG) + +# $(INVERTED_SVG): $(INVERTED_PDF) +# @echo "Converting inverted PDF to SVG..." +# pdf2svg $(INVERTED_PDF) $(INVERTED_SVG) clean: @echo "Cleaning up extra generated files..." rm -f composition_diagram*.aux composition_diagram*.log composition_diagram*.out - rm -f composition_diagram*.pdf composition_diagram*.fdb_latexmk composition_diagram*.fls + rm -f composition_diagram*.fdb_latexmk composition_diagram*.fls rm -f composition_diagram*.synctex.gz + rm -f composition_diagram*.dvi + rm -f composition_diagram*.pdf allclean: clean @echo "Cleaning up generated SVGs..." diff --git a/docs/src/assets/composition_diagram.svg b/docs/src/assets/composition_diagram.svg new file mode 100644 index 00000000..880f3add --- /dev/null +++ b/docs/src/assets/composition_diagram.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + +A +B +C + + +m + + +f + + +F + + \ No newline at end of file diff --git a/docs/src/assets/composition_diagram.tex b/docs/src/assets/composition_diagram.tex index fe779cd7..4c03a39a 100644 --- a/docs/src/assets/composition_diagram.tex +++ b/docs/src/assets/composition_diagram.tex @@ -4,6 +4,7 @@ \usepackage{amssymb} \usepackage{amsfonts} \usepackage{xcolor} +%\usepackage{fontspec} % For custom fonts % Run with `pdflatex "\def\invertcolors{} \input{$(TEX)}"` to invert colors \ifdefined\invertcolors @@ -14,7 +15,7 @@ \begin{document} % https://q.uiver.app/#q=WzAsMyxbMCwwLCJBIl0sWzIsMywiXFxtYXRoYmJ7Q30iXSxbNCwwLCJCIl0sWzAsMSwiZiIsMix7InN0eWxlIjp7ImJvZHkiOnsibmFtZSI6ImRhc2hlZCJ9fX1dLFswLDIsIm0iXSxbMiwxLCJGIl1d -\begin{tikzcd} +\begin{tikzcd}[every label/.append style = {font = \normalsize}] A &&&& B \\ \\ \\ diff --git a/docs/src/assets/composition_diagram_dark.svg b/docs/src/assets/composition_diagram_dark.svg index e323f2f2..359f2aee 100644 --- a/docs/src/assets/composition_diagram_dark.svg +++ b/docs/src/assets/composition_diagram_dark.svg @@ -1,49 +1,37 @@ - - + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + +A +B +C + + +m + + +f + + +F + + \ No newline at end of file diff --git a/docs/src/assets/composition_diagram_light.svg b/docs/src/assets/composition_diagram_light.svg index fafabb59..7e2e593c 100644 --- a/docs/src/assets/composition_diagram_light.svg +++ b/docs/src/assets/composition_diagram_light.svg @@ -1,49 +1,23 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +A +B +C + + +m + + +f + + +F + + \ No newline at end of file diff --git a/docs/src/assets/extras.css b/docs/src/assets/extras.css index 0d05f2ea..757819da 100644 --- a/docs/src/assets/extras.css +++ b/docs/src/assets/extras.css @@ -17,3 +17,30 @@ text-align: center; margin: 0 auto; } + +div .composition-diagram { + display: block; + text-align: center; + margin-top: 60px; + height: 130px; + transform: scale(1.94326); + transform-origin: center; +} + +svg text.f0 { + fill: currentColor; + font-family: 'KaTeX_AMS'; + font-size: 9.96264px; /* Adjust as needed */ +} + +svg text.f1 { + fill: currentColor; + font-family: 'KaTeX_Math'; + font-style: italic; + font-size: 9.96264px; /* Adjust as needed */ +} + +svg path { + stroke: currentColor; + fill: none; +} \ No newline at end of file diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index c8774450..cbd36bd5 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -545,18 +545,35 @@ general transformations or deeper understanding are needed. An important concept is that of a ["lift"](https://en.wikipedia.org/wiki/Lift_(mathematics)). Given -``f`` and ``g`` in the diagram below, a lift of ``f`` is a function -``h`` such that ``f = g \circ h``. -```@raw html - Lifting diagram showing relationships between spaces - Lifting diagram showing relationships between spaces -``` +``F`` and ``m`` in the diagram below, a lift of ``f`` is a function +``F`` such that ``f = F \circ m``. + Here, there are several relevant cases. Functions on ``S^2`` can be lifted to ``S^3``; functions on either of those spaces can be lifted -to their coordinate spaces; etc. +to their coordinate spaces; etc. +```@raw html +
+ + + + +A +B +C + + +m + + +f + + +F + + +
+``` !!! note "Lifts a a a a a a a a a a a a a a a a a a a a a a a a a a a" From f50082a0f46c81b31ff95c36eb3f32173e8bacee Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 29 Dec 2024 00:56:20 -0600 Subject: [PATCH 027/329] Simplify, having moved two-color versions into inline markdown --- docs/src/assets/Makefile | 45 +++---------------- docs/src/assets/composition_diagram.svg | 37 --------------- docs/src/assets/composition_diagram_dark.svg | 37 --------------- docs/src/assets/composition_diagram_light.svg | 23 ---------- 4 files changed, 6 insertions(+), 136 deletions(-) delete mode 100644 docs/src/assets/composition_diagram.svg delete mode 100644 docs/src/assets/composition_diagram_dark.svg delete mode 100644 docs/src/assets/composition_diagram_light.svg diff --git a/docs/src/assets/Makefile b/docs/src/assets/Makefile index c3210834..58f52cb0 100644 --- a/docs/src/assets/Makefile +++ b/docs/src/assets/Makefile @@ -1,53 +1,20 @@ -# Makefile to generate standard and inverted color diagrams - # Define variables TEX=composition_diagram.tex -STANDARD_DVI=composition_diagram_light.dvi -INVERTED_DVI=composition_diagram_dark.dvi -STANDARD_PDF=composition_diagram_light.pdf -INVERTED_PDF=composition_diagram_dark.pdf -STANDARD_SVG=composition_diagram_light.svg -INVERTED_SVG=composition_diagram_dark.svg +STANDARD_DVI=composition_diagram.dvi +STANDARD_SVG=composition_diagram.svg .PHONY: all clean allclean -all: $(STANDARD_SVG) $(INVERTED_SVG) clean +all: $(STANDARD_SVG) clean $(STANDARD_DVI): $(TEX) - @echo "Compiling standard-color diagram to DVI..." - lualatex -output-format=dvi -jobname=composition_diagram_light $(TEX) - -$(INVERTED_DVI): $(TEX) - @echo "Compiling inverted-color diagram to DVI..." - lualatex -output-format=dvi -jobname=composition_diagram_dark "\def\invertcolors{} \input{$(TEX)}" + @echo "Compiling diagram to DVI..." + lualatex -output-format=dvi $(TEX) $(STANDARD_SVG): $(STANDARD_DVI) - @echo "Converting standard DVI to SVG..." + @echo "Converting DVI to SVG..." dvisvgm --libgs=/opt/homebrew/lib/libgs.dylib -o $(STANDARD_SVG) $(STANDARD_DVI) -$(INVERTED_SVG): $(INVERTED_DVI) - @echo "Converting inverted DVI to SVG..." - dvisvgm --libgs=/opt/homebrew/lib/libgs.dylib -o $(INVERTED_SVG) $(INVERTED_DVI) - -# # Compile standard colors -# $(STANDARD_PDF): $(TEX) -# @echo "Compiling standard color diagram..." -# xelatex -interaction=nonstopmode -jobname=composition_diagram_light $(TEX) - -# # Compile inverted colors -# $(INVERTED_PDF): $(TEX) -# @echo "Compiling inverted color diagram..." -# xelatex -interaction=nonstopmode -jobname=composition_diagram_dark "\def\invertcolors{} \input{$(TEX)}" - -# # Convert PDFs to SVGs using pdf2svg -# $(STANDARD_SVG): $(STANDARD_PDF) -# @echo "Converting standard PDF to SVG..." -# pdf2svg $(STANDARD_PDF) $(STANDARD_SVG) - -# $(INVERTED_SVG): $(INVERTED_PDF) -# @echo "Converting inverted PDF to SVG..." -# pdf2svg $(INVERTED_PDF) $(INVERTED_SVG) - clean: @echo "Cleaning up extra generated files..." rm -f composition_diagram*.aux composition_diagram*.log composition_diagram*.out diff --git a/docs/src/assets/composition_diagram.svg b/docs/src/assets/composition_diagram.svg deleted file mode 100644 index 880f3add..00000000 --- a/docs/src/assets/composition_diagram.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - -A -B -C - - -m - - -f - - -F - - \ No newline at end of file diff --git a/docs/src/assets/composition_diagram_dark.svg b/docs/src/assets/composition_diagram_dark.svg deleted file mode 100644 index 359f2aee..00000000 --- a/docs/src/assets/composition_diagram_dark.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - -A -B -C - - -m - - -f - - -F - - \ No newline at end of file diff --git a/docs/src/assets/composition_diagram_light.svg b/docs/src/assets/composition_diagram_light.svg deleted file mode 100644 index 7e2e593c..00000000 --- a/docs/src/assets/composition_diagram_light.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - -A -B -C - - -m - - -f - - -F - - \ No newline at end of file From 3c71f9516c5b5876e757c06086ecd305e7893885 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 31 Dec 2024 16:37:48 -0500 Subject: [PATCH 028/329] A little more about finite rotations --- docs/src/conventions/conventions.md | 139 ++++++++++++++++++++++++---- 1 file changed, 123 insertions(+), 16 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index cbd36bd5..59d6a252 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -511,7 +511,7 @@ the Euler angles ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. ## Rotation and angular-momentum operators -## Complex-valued functions +### Complex-valued functions Starting with Cartesian coordinates and the Euclidean norm on ``\mathbb{R}^3``, we have *constructed* the geometric algebra over @@ -543,15 +543,11 @@ This supplies just enough information to define the spin-weighted functions — though this ends up not being a useful form when more general transformations or deeper understanding are needed. -An important concept is that of a -["lift"](https://en.wikipedia.org/wiki/Lift_(mathematics)). Given -``F`` and ``m`` in the diagram below, a lift of ``f`` is a function -``F`` such that ``f = F \circ m``. - -Here, there are several relevant cases. Functions on ``S^2`` can be -lifted to ``S^3``; functions on either of those spaces can be lifted -to their coordinate spaces; etc. - +Because of this variety of spaces, we will need to use function +composition in several ways; functions defined on one space can be +"lifted" or "lowered" to another via maps between the spaces. In the +diagram below, the function ``F`` can be used to define the function +``f`` via the mapping ``m`` as ``f = m \circ F``. ```@raw html
@@ -574,15 +570,126 @@ to their coordinate spaces; etc.
``` +For example, ``A`` could be the space of spherical coordinates, ``B`` +could be ``\mathrm{Spin}(3)``, and ``F`` could be a spin-weighted +function. There are many maps from spherical coordinates into +``\mathrm{Spin}(3)``; we expect that all such maps will be related by +rotations from ``\mathrm{SO}(3)``, and in some sense equivalent via +some universality relation. However, for singular maps — such as +coordinate singularities where multiple coordinate values correspond +to a single "physical" point — we find exceptions to the universality. +These compositions will be useful, in that we can define functions on +the "largest" available space, and extend them to any space that maps +into the first. + +In principle, our functions should be defined on ``\mathrm{Spin}(3)`` +or even the quaternions in general, though in practice we will define +them on the space of coordinates on those spaces. In any case, we +will classify the functions by their behavior with respect to actions +of ``\mathrm{Spin}(3)`` on the argument to the function. Therefore, +we need to consider the general behavior of functions under such +actions. + +### Finite rotations + +We work with functions ``f: A \to \mathbb{C}``, where ``A`` is either +the group of unit quaternions, or the full algebra of quaternions. +Any non-zero quaternion can be expressed as ``e^q`` for some finite +quaternion ``q``, which is referred to as the "generator" of the +action of ``e^q``. This can act on a function ``f`` by multiplying +the argument by ``e^q``. However, there is an ambiguity: we could +multiply either on the left or the right:[^2] +```math +f\left(\mathbf{Q}\right) \mapsto f\left(e^q \mathbf{Q}\right) +\qquad \text{or} \qquad +f\left(\mathbf{Q}\right) \mapsto f\left(\mathbf{Q} e^q\right). +``` +There is an additional ambiguity, in that this action rotates the +*argument* of the function, whereas we will often prefer to think in +terms of rotating the *function* itself. For example, our function +may describe the measurement of some field in a particular coordinate +system. Here, the argument ``\mathbf{Q}`` describes a particular +value of the coordinates, and ``e^q`` changes the point under +consideration. If, on the other hand ``e^q`` describes how the field +itself is rotated, then we can write the rotated field as a function +``f'`` which is related to the original function ``f`` by +```math +f'\left(\mathbf{Q}\right) = f\left(e^{-q} \mathbf{Q}\right) +\qquad \text{or} \qquad +f'\left(\mathbf{Q}\right) = f\left(\mathbf{Q} e^{-q}\right). +``` +Note that the exponent is negated, because the action of ``e^q`` on +the argument is the inverse of the action of ``e^{-q}`` on the +function. This is a general property of the action of a group on a +space, and is a consequence of the group action being a homomorphism. + +[^2]: In group theory, this type of transformation is often referred + to as a "translation", even when — as in this case — we would + usually describe these as rotations. + +To validate the signs here, it may be helpful to work through a simple +example involving the sphere ``S^2``. We define a function on +spherical coordinates as +```math +f(\theta, \phi) = \sin\theta \sin\phi. +``` +Recall that we can map the spherical coordinates into the Euler +angles, and the Euler angles into the quaternion +```math +(\theta, \phi) \mapsto (\phi, \theta, 0) \mapsto \mathbf{Q} += +\exp\left(\frac{\phi}{2} \mathbf{k}\right) +\exp\left(\frac{\theta}{2} \mathbf{j}\right). +``` +It is straightforward to see that we can write ``f`` as a function of +``\mathbf{Q}`` as +```math +f(\mathbf{Q}) = \left\langle \mathbf{Q}\, \mathbf{k}\, \mathbf{Q}^{-1} \right\rangle_{\mathbf{j}}, +``` +where the angle brackets and subscript indicate that we are taking the +``\mathbf{j}`` component. That is, ``f`` is the ``y`` component of +the vector ``\mathbf{z}`` rotated by ``\mathbf{Q}``. + +Now, we imagine rotating the field by an angle ``\alpha`` in the +positive sense about the ``z`` axis. Visualizing the situation, we +can see that the rotated field should be represented by +```math +f'(\theta, \phi) = \sin\theta \sin(\phi - \alpha). +``` +For example, the rotated field evaluated at the point ``(\theta, \phi) += (\pi/2, 0)`` along the positive ``x`` axis should correspond to the +original field evaluated at the point ``(\theta, \phi) = (\pi/2, +-\alpha)``. This rotation is generated by ``q = \alpha \mathbf{k} / +2``, which allows us to immediately calculate +```math +\begin{aligned} +f(e^q \mathbf{Q}) &= \sin\theta \sin(\phi + \alpha) & +f(\mathbf{Q} e^q) &= \sin\theta \sin\phi \\ +f(e^{-q} \mathbf{Q}) &= \sin\theta \sin(\phi - \alpha) & +f(\mathbf{Q} e^{-q}) &= \sin\theta \sin\phi. +\end{aligned} +``` +Thus, we see that left-multiplication by ``e^{-q}`` corresponds to +rotation of the field while leaving the coordinates fixed; +left-multiplication by ``e^q`` corresponds to rotation of the +coordinates while leaving the field fixed; and right-multiplication by +either doesn't affect this function at all. (Of course, +right-multiplication using other choices for ``q`` could certainly +have some effect on this function, and this choice of ``q`` could have +an effect on other functions.) +### Differential rotations -!!! note "Lifts a a a a a a a a a a a a a a a a a a a a a a a a a a a" - Because of lifts or pushbacks, we have some freedom to define - functions on the "largest" space available. - +We now define a pair of operators the differentiate the value of a +function with respect to infinitesimal rotations we apply to the +functions themselves: +```math +\begin{aligned} +L_{\mathrm{g}} f(\mathbf{Q}) &= \lambda \left. \frac{\partial} {\partial \theta} f \left( e^{-\theta \mathrm{g} / 2} \mathbf{Q} \right) \right|_{\theta=0}, \\ +R_{\mathrm{g}} f(\mathbf{Q}) &= \rho \left. \frac{\partial} {\partial \theta} f \left( \mathbf{Q} e^{\theta \mathrm{g} / 2} \right) \right|_{\theta=0}. +\end{aligned} +``` - - Start with finite rotations — both left and right translations - - note the signs; these give us the signs - Then, we differentiate those finite rotations, generating rotation of a function by exponentiating a generator giving finite rotation; this lets us set some signs From 9e65143704feec5cc0e92d969e23da878b419079 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:56:09 -0500 Subject: [PATCH 029/329] Accept Rational with denominator 1 --- test/utilities/naive_factorial.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/utilities/naive_factorial.jl b/test/utilities/naive_factorial.jl index 1fec1c27..ec12fee0 100644 --- a/test/utilities/naive_factorial.jl +++ b/test/utilities/naive_factorial.jl @@ -14,5 +14,12 @@ Use this snippet by including the following in your test file: module NaiveFactorials struct Factorial end Base.:*(n::Integer, ::Factorial) = factorial(big(n)) + function Base.:*(n::Rational, ::Factorial) where {Rational} + if denominator(n) == 1 + return factorial(big(numerator(n))) + else + throw(ArgumentError("Cannot compute factorial of a non-integer rational")) + end + end const ❗ = Factorial() end From 423f2b73b7a0bc6ee7affa18eb00c713e5958b11 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:57:30 -0500 Subject: [PATCH 030/329] Test composition formula and basis commutators --- test/operators.jl | 214 +++++++++++++++++++++++++++++++++------------- 1 file changed, 154 insertions(+), 60 deletions(-) diff --git a/test/operators.jl b/test/operators.jl index 5b04d61b..a57d4eef 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -13,10 +13,12 @@ end @testitem "Explicit definition" setup=[ExplicitOperators] begin using Quaternionic using DoubleFloats + using Random + Random.seed!(123) const L = ExplicitOperators.L const R = ExplicitOperators.R for T ∈ [Float32, Float64, Double64, BigFloat] - # Test the `L` and `R` operators as defined above + # Test the `L` and `R` operators as defined above compared to eigenvalues on 𝔇 ϵ = 100 * eps(T) for Q ∈ randn(Rotor{T}, 10) for ℓ ∈ 0:4 @@ -57,6 +59,61 @@ end end end +@testitem "Composition" setup=[ExplicitOperators] begin + # Test the order of operations: + # LₘLₙf(Q) = λ²∂ᵧ∂ᵨf(exp(ρn) exp(γm) Q) + # RₘRₙf(Q) = λ²∂ᵧ∂ᵨf(Q exp(γm) exp(ρn)) + using Quaternionic + using DoubleFloats + import ForwardDiff + using Random + Random.seed!(123) + + const L = ExplicitOperators.L + const R = ExplicitOperators.R + + for T ∈ [Float32, Float64, Double64] + z = zero(T) + function LL(m, n, f, Q) + - ForwardDiff.derivative( + γ -> ForwardDiff.derivative( + ρ -> f((cos(ρ) + sin(ρ)*n) * (cos(γ) + sin(γ)*m) * Q), + z + ), + z + ) / 4 + end + function RR(m, n, f, Q) + - ForwardDiff.derivative( + γ -> ForwardDiff.derivative( + ρ -> f(Q * (cos(γ) + sin(γ)*m) * (cos(ρ) + sin(ρ)*n)), + z + ), + z + ) / 4 + end + + ϵ = 100 * eps(T) + M = randn(QuatVec{T}, 5) + N = randn(QuatVec{T}, 5) + for Q ∈ randn(Rotor{T}, 10) + for ℓ ∈ 0:4 + for m ∈ -ℓ:ℓ + for m′ ∈ -ℓ:ℓ + f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + for n ∈ N + for m ∈ M + @test L(m, L(n, f))(Q) ≈ LL(m, n, f, Q) atol=ϵ rtol=ϵ + @test R(m, R(n, f))(Q) ≈ RR(m, n, f, Q) atol=ϵ rtol=ϵ + end + end + end + end + end + end + end +end + @testitem "Scalar multiplication" setup=[ExplicitOperators] begin using Quaternionic using DoubleFloats @@ -112,67 +169,34 @@ end end end -@testitem "Casimir" begin +@testitem "Basis commutators" setup=[ExplicitOperators] begin + # [Lⱼ, Lₖ] = im L_{[eⱼ,eₖ]/2} = im ∑ₗ ε(j,k,l) Lₗ + # [Rⱼ, Rₖ] = -im R_{[eⱼ,eₖ]/2} = -im ∑ₗ ε(j,k,l) Rₗ + # [Lⱼ, Rₖ] = 0 + using Quaternionic using DoubleFloats - for T ∈ [Float32, Float64, Double64, BigFloat] - # Test that L² = (L₊L₋ + L₋L₊ + 2Lz²)/2 = R² = (R₊R₋ + R₋R₊ + 2Rz²)/2 - ϵ = 100 * eps(T) - for s ∈ -3:3 - for ℓₘₐₓ ∈ 4:7 - for ℓₘᵢₙ ∈ 0:min(abs(s)+1, ℓₘₐₓ) - let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), - Lz=Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T), - L₊=L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T), - L₋=L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - L1 = L² - L2 = (L₊*L₋ .+ L₋*L₊ .+ 2Lz*Lz)/2 - @test L1 ≈ L2 atol=ϵ rtol=ϵ - end - let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), - R²=R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) - @test L² ≈ R² atol=ϵ rtol=ϵ - end - let - # R² = (2Rz² + R₊R₋ + R₋R₊)/2 - R1 = R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) - R2 = T.(Array( - R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - .+ R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - .+ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ) / 2) - @test R1 ≈ R2 atol=ϵ rtol=ϵ - end - end - end - end - end -end + import ForwardDiff + using Random + Random.seed!(1234) -@testitem "Applied to ₛYₗₘ" begin - using DoubleFloats - for T ∈ [Float32, Float64, Double64, BigFloat] - # Evaluate (on points) ðY = √((ℓ-s)(ℓ+s+1)) Y, and similarly for ð̄Y - ϵ = 100 * eps(T) - @testset "$ℓₘₐₓ" for ℓₘₐₓ ∈ 4:7 - for s in -3:3 - let ℓₘᵢₙ = 0 - 𝒯₊ = SSHT(s+1, ℓₘₐₓ; T=T, method="Direct", inplace=false) - 𝒯₋ = SSHT(s-1, ℓₘₐₓ; T=T, method="Direct", inplace=false) - i₊ = Yindex(abs(s+1), -abs(s+1), ℓₘᵢₙ) - i₋ = Yindex(abs(s-1), -abs(s-1), ℓₘᵢₙ) - Y = zeros(Complex{T}, Ysize(ℓₘᵢₙ, ℓₘₐₓ)) - for ℓ in abs(s):ℓₘₐₓ - for m in -ℓ:ℓ - Y[:] .= zero(T) - Y[Yindex(ℓ, m, ℓₘᵢₙ)] = one(T) - ðY = 𝒯₊ * (ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₊:end] - Y₊ = 𝒯₊ * Y[i₊:end] - c₊ = ℓ < abs(s+1) ? zero(T) : √T((ℓ-s)*(ℓ+s+1)) - @test ðY ≈ c₊ * Y₊ atol=ϵ rtol=ϵ - ð̄Y = 𝒯₋ * (ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₋:end] - Y₋ = 𝒯₋ * Y[i₋:end] - c₋ = ℓ < abs(s-1) ? zero(T) : -√T((ℓ+s)*(ℓ-s+1)) - @test ð̄Y ≈ c₋ * Y₋ atol=ϵ rtol=ϵ + const L = ExplicitOperators.L + const R = ExplicitOperators.R + + for T ∈ [Float32, Float64, Double64] + ϵ = 400 * eps(T) + E = QuatVec{T}[imx, imy, imz] + for Q ∈ randn(Rotor{T}, 10) + for ℓ ∈ 0:4 + for m ∈ -ℓ:ℓ + for m′ ∈ -ℓ:ℓ + f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + for eⱼ ∈ E + for eₖ ∈ E + eⱼeₖ = QuatVec{T}(eⱼ * eₖ - eₖ * eⱼ) / 2 + @test L(eⱼ, L(eₖ, f))(Q) - L(eₖ, L(eⱼ, f))(Q) ≈ im * L(eⱼeₖ, f)(Q) atol=ϵ rtol=ϵ + @test R(eⱼ, R(eₖ, f))(Q) - R(eₖ, R(eⱼ, f))(Q) ≈ -im * R(eⱼeₖ, f)(Q) atol=ϵ rtol=ϵ + @test L(eⱼ, R(eₖ, f))(Q) - R(eₖ, L(eⱼ, f))(Q) ≈ zero(T) atol=4ϵ + end end end end @@ -257,8 +281,78 @@ end end end +@testitem "Casimir" begin + using DoubleFloats + for T ∈ [Float32, Float64, Double64, BigFloat] + # Test that L² = (L₊L₋ + L₋L₊ + 2Lz²)/2 = R² = (R₊R₋ + R₋R₊ + 2Rz²)/2 + ϵ = 100 * eps(T) + for s ∈ -3:3 + for ℓₘₐₓ ∈ 4:7 + for ℓₘᵢₙ ∈ 0:min(abs(s)+1, ℓₘₐₓ) + let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), + Lz=Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T), + L₊=L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T), + L₋=L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + L1 = L² + L2 = (L₊*L₋ .+ L₋*L₊ .+ 2Lz*Lz)/2 + @test L1 ≈ L2 atol=ϵ rtol=ϵ + end + let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), + R²=R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) + @test L² ≈ R² atol=ϵ rtol=ϵ + end + let + # R² = (2Rz² + R₊R₋ + R₋R₊)/2 + R1 = R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) + R2 = T.(Array( + R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + .+ R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + .+ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ) / 2) + @test R1 ≈ R2 atol=ϵ rtol=ϵ + end + end + end + end + end +end + +@testitem "Applied to ₛYₗₘ" begin + using DoubleFloats + for T ∈ [Float32, Float64, Double64, BigFloat] + # Evaluate (on points) ðY = √((ℓ-s)(ℓ+s+1)) Y, and similarly for ð̄Y + ϵ = 100 * eps(T) + @testset "$ℓₘₐₓ" for ℓₘₐₓ ∈ 4:7 + for s in -3:3 + let ℓₘᵢₙ = 0 + 𝒯₊ = SSHT(s+1, ℓₘₐₓ; T=T, method="Direct", inplace=false) + 𝒯₋ = SSHT(s-1, ℓₘₐₓ; T=T, method="Direct", inplace=false) + i₊ = Yindex(abs(s+1), -abs(s+1), ℓₘᵢₙ) + i₋ = Yindex(abs(s-1), -abs(s-1), ℓₘᵢₙ) + Y = zeros(Complex{T}, Ysize(ℓₘᵢₙ, ℓₘₐₓ)) + for ℓ in abs(s):ℓₘₐₓ + for m in -ℓ:ℓ + Y[:] .= zero(T) + Y[Yindex(ℓ, m, ℓₘᵢₙ)] = one(T) + ðY = 𝒯₊ * (ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₊:end] + Y₊ = 𝒯₊ * Y[i₊:end] + c₊ = ℓ < abs(s+1) ? zero(T) : √T((ℓ-s)*(ℓ+s+1)) + @test ðY ≈ c₊ * Y₊ atol=ϵ rtol=ϵ + ð̄Y = 𝒯₋ * (ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₋:end] + Y₋ = 𝒯₋ * Y[i₋:end] + c₋ = ℓ < abs(s-1) ? zero(T) : -√T((ℓ+s)*(ℓ-s+1)) + @test ð̄Y ≈ c₋ * Y₋ atol=ϵ rtol=ϵ + end + end + end + end + end + end +end + ## TODO: Add L_x, L_y, R_x, and R_y, then test these commutators. ## Note that R is harder because the basis in which all the matrices are returned ## assumes that you are dealing with a particular `s` eigenvalue. # [Lⱼ, Lₖ] = im L_{[eⱼ,eₖ]/2} = im ∑ₗ ε(j,k,l) Lₗ # [Rⱼ, Rₖ] = -im R_{[eⱼ,eₖ]/2} = -im ∑ₗ ε(j,k,l) Rₗ +# [Lⱼ, Rₖ] = 0 From e7aef51fc17fb95c8f2f81e805fc7d4570221f14 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:59:10 -0500 Subject: [PATCH 031/329] Expand on differential rotations --- docs/src/conventions/conventions.md | 207 +++++++++++++++++++++------- 1 file changed, 160 insertions(+), 47 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 59d6a252..36a83bcc 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -594,34 +594,37 @@ actions. We work with functions ``f: A \to \mathbb{C}``, where ``A`` is either the group of unit quaternions, or the full algebra of quaternions. -Any non-zero quaternion can be expressed as ``e^q`` for some finite -quaternion ``q``, which is referred to as the "generator" of the -action of ``e^q``. This can act on a function ``f`` by multiplying -the argument by ``e^q``. However, there is an ambiguity: we could -multiply either on the left or the right:[^2] -```math -f\left(\mathbf{Q}\right) \mapsto f\left(e^q \mathbf{Q}\right) +Any non-zero quaternion can be expressed as ``e^\mathfrak{g}`` for +some finite quaternion ``\mathfrak{g}``, which is referred to as the +"generator" of the action of ``e^\mathfrak{g}``. This can act on a +function ``f`` by multiplying the argument by ``e^\mathfrak{g}``. +However, there is an ambiguity: we could multiply either on the left +or the right:[^2] +```math +f\left(\mathbf{Q}\right) \mapsto f\left(e^\mathfrak{g} \mathbf{Q}\right) \qquad \text{or} \qquad -f\left(\mathbf{Q}\right) \mapsto f\left(\mathbf{Q} e^q\right). +f\left(\mathbf{Q}\right) \mapsto f\left(\mathbf{Q} e^\mathfrak{g}\right). ``` There is an additional ambiguity, in that this action rotates the *argument* of the function, whereas we will often prefer to think in terms of rotating the *function* itself. For example, our function may describe the measurement of some field in a particular coordinate system. Here, the argument ``\mathbf{Q}`` describes a particular -value of the coordinates, and ``e^q`` changes the point under -consideration. If, on the other hand ``e^q`` describes how the field -itself is rotated, then we can write the rotated field as a function -``f'`` which is related to the original function ``f`` by +value of the coordinates, and ``e^\mathfrak{g}`` changes the point +under consideration. If, on the other hand ``e^\mathfrak{g}`` +describes how the field itself is rotated, then we can write the +rotated field as a function ``f'`` which is related to the original +function ``f`` by ```math -f'\left(\mathbf{Q}\right) = f\left(e^{-q} \mathbf{Q}\right) +f'\left(\mathbf{Q}\right) = f\left(e^{-\mathfrak{g}} \mathbf{Q}\right) \qquad \text{or} \qquad -f'\left(\mathbf{Q}\right) = f\left(\mathbf{Q} e^{-q}\right). +f'\left(\mathbf{Q}\right) = f\left(\mathbf{Q} e^{-\mathfrak{g}}\right). ``` -Note that the exponent is negated, because the action of ``e^q`` on -the argument is the inverse of the action of ``e^{-q}`` on the -function. This is a general property of the action of a group on a -space, and is a consequence of the group action being a homomorphism. +Note that the exponent is negated, because the action of +``e^\mathfrak{g}`` on the argument is the inverse of the action of +``e^{-\mathfrak{g}}`` on the function. This is a general property of +the action of a group on a space, and is a consequence of the group +action being a homomorphism. [^2]: In group theory, this type of transformation is often referred to as a "translation", even when — as in this case — we would @@ -659,44 +662,158 @@ f'(\theta, \phi) = \sin\theta \sin(\phi - \alpha). For example, the rotated field evaluated at the point ``(\theta, \phi) = (\pi/2, 0)`` along the positive ``x`` axis should correspond to the original field evaluated at the point ``(\theta, \phi) = (\pi/2, --\alpha)``. This rotation is generated by ``q = \alpha \mathbf{k} / -2``, which allows us to immediately calculate +-\alpha)``. This rotation is generated by ``\mathfrak{g} = \alpha +\mathbf{k} / 2``, which allows us to immediately calculate ```math \begin{aligned} -f(e^q \mathbf{Q}) &= \sin\theta \sin(\phi + \alpha) & -f(\mathbf{Q} e^q) &= \sin\theta \sin\phi \\ -f(e^{-q} \mathbf{Q}) &= \sin\theta \sin(\phi - \alpha) & -f(\mathbf{Q} e^{-q}) &= \sin\theta \sin\phi. +f(e^\mathfrak{g} \mathbf{Q}) &= \sin\theta \sin(\phi + \alpha) &&& +f(\mathbf{Q} e^\mathfrak{g}) &= \sin\theta \sin\phi \\ +f(e^{-\mathfrak{g}} \mathbf{Q}) &= \sin\theta \sin(\phi - \alpha) &&& +f(\mathbf{Q} e^{-\mathfrak{g}}) &= \sin\theta \sin\phi. \end{aligned} ``` -Thus, we see that left-multiplication by ``e^{-q}`` corresponds to -rotation of the field while leaving the coordinates fixed; -left-multiplication by ``e^q`` corresponds to rotation of the -coordinates while leaving the field fixed; and right-multiplication by -either doesn't affect this function at all. (Of course, -right-multiplication using other choices for ``q`` could certainly -have some effect on this function, and this choice of ``q`` could have -an effect on other functions.) - -### Differential rotations +Thus, we see that left-multiplication by ``e^{-\mathfrak{g}}`` +corresponds to rotation of the field while leaving the coordinates +fixed; left-multiplication by ``e^\mathfrak{g}`` corresponds to +rotation of the coordinates while leaving the field fixed; and +right-multiplication by either doesn't affect this function at all. -We now define a pair of operators the differentiate the value of a -function with respect to infinitesimal rotations we apply to the -functions themselves: +Of course, right-multiplication using other choices for +``\mathfrak{g}`` could certainly have some effect on this function, +and this choice of ``\mathfrak{g}`` could have an effect on other +functions. Note that right-multiplication can also be interpreted as +left-multiplication, where the generator itself is rotated by the +argument to the function. That is, ```math \begin{aligned} -L_{\mathrm{g}} f(\mathbf{Q}) &= \lambda \left. \frac{\partial} {\partial \theta} f \left( e^{-\theta \mathrm{g} / 2} \mathbf{Q} \right) \right|_{\theta=0}, \\ -R_{\mathrm{g}} f(\mathbf{Q}) &= \rho \left. \frac{\partial} {\partial \theta} f \left( \mathbf{Q} e^{\theta \mathrm{g} / 2} \right) \right|_{\theta=0}. +f(\mathbf{Q} e^\mathfrak{g}) + &= f(\mathbf{Q} e^{\mathfrak{g}} \mathbf{Q}^{-1} \mathbf{Q}) + = f(e^{\mathfrak{g}'} \mathbf{Q}) \\ +f(\mathbf{Q} e^{-\mathfrak{g}}) + &= f(\mathbf{Q} e^{-\mathfrak{g}} \mathbf{Q}^{-1} \mathbf{Q}) + = f(e^{-\mathfrak{g}'} \mathbf{Q}), \end{aligned} ``` +where ``\mathfrak{g}' = \mathbf{Q} \mathfrak{g} \mathbf{Q}^{-1}``. In +this example, ``\mathfrak{g}'`` generates a rotation by an angle +``\alpha`` about the point in question, which leaves that point fixed, +and since this is a scalar function it has no effect on the value. Of +course, we will see below that changing by a phase proportional to +``\alpha`` is the defining feature of a *spin-weighted* function. + +### Differential rotations - - Then, we differentiate those finite rotations, generating rotation - of a function by exponentiating a generator giving finite - rotation; this lets us set some signs - Express angular momentum operators in terms of quaternion components - Basic Lie definition - Properties: form a Lie algebra with the commutator as the Lie bracket - - + + +We now define a pair of operators that differentiate a function with +respect to infinitesimal rotations we apply to the functions +themselves: +```math +\begin{aligned} +L_{\mathfrak{g}} f(\mathbf{Q}) &= \lambda \left. \frac{\partial} {\partial \theta} f \left( e^{-\theta \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\theta=0}, \\ +R_{\mathfrak{g}} f(\mathbf{Q}) &= \rho \left. \frac{\partial} {\partial \theta} f \left( \mathbf{Q} e^{-\theta \mathfrak{g} / 2} \right) \right|_{\theta=0}. +\end{aligned} +``` +Here, we have introduced the constants ``\lambda`` and ``\rho`` +because we will actually be able to derive their — up to signs — based +on the requirement that raising and lowering operators exist for each. +Finally, we will choose the signs based on demands that these +operators correspond as naturally as possible to the standard +canonical angular-momentum operators. + +Note that when composing operators, it is critical to keep track of +the order of operations, which may look slightly unnatural: +```math +\begin{align} + L_\mathfrak{g} L_\mathfrak{h} f(\mathbf{Q}) + % &= \left. \lambda \frac{\partial} {\partial \gamma} f'\left(e^{-\gamma \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\gamma=0}, \\ + &= \left. \lambda^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left(e^{-\eta \mathfrak{h} / 2} e^{-\gamma \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\gamma=\eta=0}, \\ + R_\mathfrak{g} R_\mathfrak{h} f(\mathbf{Q}) + % &= \rho \left. \frac{\partial} {\partial \gamma} f' \left( \mathbf{Q} e^{-\gamma \mathfrak{g} / 2} \right) \right|_{\gamma=0} \\ + &= \left. \rho^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left( \mathbf{Q} e^{-\gamma \mathfrak{g} / 2} e^{-\eta \mathfrak{h} / 2} \right) \right|_{\gamma=\eta=0}. +\end{align} +``` +We can prove the first of these, for example, by defining +``f'(\mathbf{Q}) = L_\mathfrak{h} f(\mathbf{Q})``, then applying the +definition of ``L_\mathfrak{g}`` to ``f'(\mathbf{Q})``, and finally +substituting the definition of ``f'`` back in. If we failed to use +the correct order of operations, we would get sign errors when trying +to evaluate the commutators. + +These operators have some nice properties. For any scalar ``s``, we have +```math +\begin{aligned} +L_{s \mathfrak{g}} &= s L_{\mathfrak{g}}, \\ +R_{s \mathfrak{g}} &= s R_{\mathfrak{g}}. +\end{aligned} +``` +Given any basis ``\mathbf{e}_n`` for the quaternions, we can use +the multivariable chain rule to expand the operators in terms of +components: +```math +\begin{aligned} +L_{\mathfrak{g}} &= \sum_n g_n\, L_{\mathbf{e}_n}, \\ +R_{\mathfrak{g}} &= \sum_n g_n\, R_{\mathbf{e}_n}. +\end{aligned} +``` +This implies that vector addition holds more generally: +```math +\begin{aligned} +L_{\mathfrak{g} + \mathfrak{h}} &= L_{\mathfrak{g}} + L_{\mathfrak{h}} \\ +R_{\mathfrak{g} + \mathfrak{h}} &= R_{\mathfrak{g}} + R_{\mathfrak{h}}. +\end{aligned} +``` +Moreover, we can show that these operators form a Lie algebra with the +commutator as the Lie bracket. That is, we have +```math +\begin{aligned} +[L_{\mathfrak{g}}, L_{\mathfrak{h}}] &= -\lambda L_{[\mathfrak{g}, \mathfrak{h}]}, \\ +[R_{\mathfrak{g}}, R_{\mathfrak{h}}] &= \rho R_{[\mathfrak{g}, \mathfrak{h}]}, \\ +[L_{\mathfrak{g}}, R_{\mathfrak{h}}] &= 0. +\end{aligned} +``` + +Conventionally, we single out the ``\mathbf{z}`` axis — or +equivalently the generator ``\mathbf{k} = \mathbf{y}\mathbf{x}`` — as +a sort of fiducial axis, and ``L_z = L_\mathbf{k}`` and ``R_z = +R_\mathbf{k}`` as the fiducial operators. Then, *by definition*, +their raising operators ``L_+`` and ``R_+`` and lowering operators +``L_-`` and ``R_-`` satisfy +```math +\begin{aligned} +[L_z, L_\pm] &= \pm L_\pm, \\ +[R_z, R_\pm] &= \pm R_\pm. +\end{aligned} +``` +Assuming that the raising and lowering operators can be written as +linear combinations of the basis operators, these equations imply that +they have no component proportional ``L_\mathbf{z}``, and that both of +the remaining components must be nonzero. This actually allows us to +deduce that ``\lambda^2 = \rho^2 = -1``. This, in turn, allows us to +deduce the values of the raising and lowering operators up to an +overall factor. Conventionally the factor is chosen so that +```math +\begin{aligned} +L_\pm &= L_\mathbf{x} \pm i L_\mathbf{y}, \\ +R_\pm &= R_\mathbf{x} \pm i R_\mathbf{y}. +\end{aligned} +``` + +Using these relations, we can actually solve for the constants +``\lambda`` and ``\rho`` up to a sign. We find that +```math +\begin{aligned} +\lambda &= -i, \\ +\rho &= i. +\end{aligned} +``` + + +### Angular-momentum operators in Euler angles + - Express angular momentum operators in terms of Euler angles - We just rewrite the ``R`` in the Lie definitions in terms of Euler angles, multiply by ``\exp(\theta/2)``, rederive the new @@ -704,9 +821,6 @@ R_{\mathrm{g}} f(\mathbf{Q}) &= \rho \left. \frac{\partial} {\partial \theta} f - Show for both the three- and two-spheres - Show how they act on functions on the three-sphere - -### Angular-momentum operators in Euler angles - The idea here is to express, e.g., $e^{\theta \mathbf{e}_i / 2}\mathbf{R}_{\alpha, \beta, \gamma}$ in quaternion components, then solve for the new Euler angles $\mathbf{R}_{\alpha', \beta', \gamma'}$ @@ -716,7 +830,6 @@ $\partial_\theta$ in terms of $\partial_{\alpha'}$, etc., which become $\partial_\alpha$, etc., when $\theta=0$. ```math - \begin{align} L_i f(\mathbf{R}) &= From a013a71bcbc83b0a228670c1fc2a6e2f8eb3cf43 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:59:36 -0500 Subject: [PATCH 032/329] Find and cite Strakhov --- docs/src/notes/H_recursions.md | 28 +++++++++++--------- docs/src/references.bib | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/docs/src/notes/H_recursions.md b/docs/src/notes/H_recursions.md index 7e8c80f2..70c4398e 100644 --- a/docs/src/notes/H_recursions.md +++ b/docs/src/notes/H_recursions.md @@ -79,18 +79,22 @@ Here, ``k_0=1`` and ``k_m=2`` for ``m>0``, and ``P̄`` is defined as ```math P̄_{n,|m|} = \sqrt{\frac{k_m(2n+1)(n-m)!}{(n+m)!}} P_{n,|m|}. ``` -Note that the factor of ``(-1)^m`` in the first equation above is different from -the convention used here, and is related to the -[Condon-Shortley phase](https://en.wikipedia.org/wiki/Spherical_harmonics#Condon%E2%80%93Shortley_phase). -Note that Gumerov and Duraiswami use the notation ``P^{|m|}_{n}``, whereas we are -using the notation ``P_{n,|m|}`` — which usually differ by a factor of ``(-1)^m``. - -We use the "fully normalized" associated Legendre functions (fnALF) ``P̄`` because, as explained by -[Xing_2019](@citet), it is possible to compute these values very efficiently and accurately, while -also delaying the onset of overflow and underflow. - -The algorithm Xing et al. describe as the best for computing ``P̄`` is due to -Belikov (1991), and is given by them as +Note that the factor of ``(-1)^m`` in the first equation above is +different from the convention used here, and is related to the +[Condon-Shortley +phase](https://en.wikipedia.org/wiki/Spherical_harmonics#Condon%E2%80%93Shortley_phase). +Note that Gumerov and Duraiswami use the notation ``P^{|m|}_{n}``, +whereas we are using the notation ``P_{n,|m|}`` — which usually differ +by a factor of ``(-1)^m``. + +We use the "fully normalized" associated Legendre functions (fnALF) +``P̄`` because, as explained by [Xing_2019](@citet), it is possible to +compute these values very efficiently and accurately, while also +delaying the onset of overflow and underflow. + +The algorithm Xing et al. describe as the best for computing ``P̄`` is +due to [Strakhov_1980](@citet) via [Belikov_1991](@citet), and is +given by them as ```math \begin{aligned} P̄_{0,0} &= 1 \\ diff --git a/docs/src/references.bib b/docs/src/references.bib index b66dee8a..66c1be9f 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -12,6 +12,21 @@ @misc{Ajith_2007 primaryClass = "gr-qc", } +@article{Belikov_1991, + title = {Spherical harmonic analysis and synthesis with the use of column-wise recurrence + relations}, + volume = 16, + issn = {0340-8825}, + url = {https://doi.org/10.1007/BF03655428}, + doi = {10.1007/BF03655428}, + number = 6, + journal = {manuscripta geodaetica}, + author = {Belikov, M. V.}, + month = dec, + year = 1991, + pages = {384--410} +} + @article{Boyle_2016, doi = {10.1063/1.4962723}, url = {https://doi.org/10.1063/1.4962723}, @@ -231,6 +246,15 @@ @article{Newman_1966 journal = {Journal of Mathematical Physics} } +@misc{NIST_DLMF, + title = "{NIST Digital Library of Mathematical Functions}", + howpublished = "\url{https://dlmf.nist.gov/}, Release 1.2.3 of 2024-12-15", + url = "https://dlmf.nist.gov/", + note = "F.~W.~J. Olver, A.~B. {Olde Daalhuis}, D.~W. Lozier, B.~I. Schneider, + R.~F. Boisvert, C.~W. Clark, B.~R. Miller, B.~V. Saunders, H.~S. Cohl, and + M.~A. McClain, eds." +} + @article{Reinecke_2013, doi = {10.1051/0004-6361/201321494}, url = {https://doi.org/10.1051/0004-6361/201321494}, @@ -284,6 +308,17 @@ @article{SommerEtAl_2018 primaryClass = {cs.RO}, } +@article{Strakhov_1980, + title = {On synthesis of the outer gravitational potential in spherical harmonic series}, + volume = 254, + issn = {0002-3264}, + number = 4, + journal = {Doklady Akademii nauk {SSSR}}, + author = {{VN} Strakhov}, + pages = {839---841}, + year = 1980 +} + @article{Thorne_1980, title = {Multipole expansions of gravitational radiation}, volume = 52, @@ -333,6 +368,19 @@ @book{vanNeerven_2022 doi = {10.1017/9781009232487} } +@book{Varshalovich_1988, + address = {Singapore ; Teaneck, {NJ}, {USA}}, + title = {Quantum theory of angular momentum: {I}rreducible tensors, spherical harmonics, + vector coupling coefficients, 3nj symbols}, + isbn = {9971-5-0107-4}, + lccn = {{QC793.3.A5} V3713 1988}, + shorttitle = {Quantum theory of angular momentum}, + url = {https://doi.org/10.1142/0270}, + publisher = {World Scientific Pub}, + author = {Varshalovich, D. A. and Moskalev, A. N. and Khersonski\u{i}, V. K.}, + year = 1988, +} + @article{Waldvogel_2006, doi = {10.1007/s10543-006-0045-4}, url = {https://doi.org/10.1007/s10543-006-0045-4}, From b9884f6ac37b60eeefffadf957799f5d4a371696 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 15:15:56 -0500 Subject: [PATCH 033/329] Include Varshalovich conventions --- test/conventions/varshalovich.jl | 161 +++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 test/conventions/varshalovich.jl diff --git a/test/conventions/varshalovich.jl b/test/conventions/varshalovich.jl new file mode 100644 index 00000000..d1c39630 --- /dev/null +++ b/test/conventions/varshalovich.jl @@ -0,0 +1,161 @@ +raw""" +Formulas and conventions from [Varshalovich's "Quantum Theory of Angular Momentum"](@cite +Varshalovich_1988). + +Note that Varshalovich labels his indices with `M` and `M′`, respectively, but if we just +plug in `m′` and `m` (note the order), we get the expected result — his formulas are the +same as this package's, except with a conjugate. + +Varshalovich defines his Euler angles (scheme B, page 22) in the same way we do, except that +he specifies that this describes the rotation *of the coordinate system*. + +Sec. 4.8.2 (page 92) relates the integer-index elements to the following half-integer-index +elements. Specifically, Eqs. (14) and (15) derive the relationships from the Clebsch-Gordan +coefficients. That is, the product of two Wigner matrices can be given as a sum over a +Wigner matrices times a pair of Clebsch-Gordan coefficients. If one of the matrices has +spin 1/2, this gives us a series of relationships between the integer-index elements and the +half-integer-index elements, which can be combined to give the desired relationship. Then, +given knowledge of the 1/2-spin representation (which is essentially the standard +$\mathrm{SU}(2)$ representation), we can then get any half-integer spin result from the +preceding whole-integer spin results. + +Specifically, we have (from Table 4.3, page 119): +```julia +D(1//2, 1//2, 1//2, α, β, γ) = exp(-𝒾*α/2) * cos(β/2) * exp(-𝒾*γ/2) +D(1//2, 1//2, -1//2, α, β, γ) = -exp(-𝒾*α/2) * sin(β/2) * exp( 𝒾*γ/2) +D(1//2, -1//2, 1//2, α, β, γ) = exp( 𝒾*α/2) * sin(β/2) * exp(-𝒾*γ/2) +D(1//2, -1//2, -1//2, α, β, γ) = exp( 𝒾*α/2) * cos(β/2) * exp( 𝒾*γ/2) +``` +""" +@testmodule Varshalovich begin + +const 𝒾 = im + +include("../utilities/naive_factorial.jl") +import .NaiveFactorials: ❗ + + +@doc raw""" + D(J, M, M′, α, β, γ) + +Eq. 4.3(1) of [Varshalovich](@cite Varshalovich_1988), implementing +```math + D^{J}_{M,M'}(\alpha, \beta, \gamma). +``` + +See also [`d`](@ref) for Varshalovich's version the Wigner d-function. +""" +function D(J, M, M′, α, β, γ) + exp(-𝒾*M*α) * d(J, M, M′, β) * exp(-𝒾*M′*γ) +end + + +@doc raw""" + d(J, M, M′, β) + +Eqs. 4.3.1(2) of [Varshalovich](@cite Varshalovich_1988), implementing +```math + d^{J}_{M,M'}(\beta). +``` + +See also [`D`](@ref) for Varshalovich's version the Wigner D-function. +""" +function d(J::I, M::I, M′::I, β::T) where {I, T} + if J < 0 + throw(DomainError("J=$J must be non-negative")) + end + if abs(M) > J || abs(M′) > J + if I <: Rational && abs(M) ≤ J+2 && abs(M′) ≤ J+2 + return zero(β) # Simplify half-integer formulas by accepting this + end + #throw(DomainError("abs(M=$M) and abs(M=$M′) must be ≤ J=$J")) + end + if J ≥ 8 + throw(DomainError("J=$J≥8 will lead to overflow errors")) + end + + # The summation index `k` ranges over all values for which the factorials are + # non-negative. + kₘᵢₙ = max(0, -(M+M′)) + kₘₐₓ = min(J-M, J-M′) + + # Note that Varshalovich's actual formula is reproduced here, even though it leads to + # overflow errors for `J ≥ 8`, which could be eliminated by other means. + return (-1)^(J-M′) * √T((J+M)❗ * (J-M)❗ * (J+M′)❗ * (J-M′)❗) * + sum( + k -> ( + (-1)^(k) * cos(β/2)^(M+M′+2k) * sin(β/2)^(2J-M-M′-2k) + / T((k)❗ * (J-M-k)❗ * (J-M′-k)❗ * (M+M′+k)❗) + ), + kₘᵢₙ:kₘₐₓ, + init=zero(T) + ) +end + +end # @testmodule Varshalovich + + +@testitem "Varshalovich conventions" setup=[Utilities, Varshalovich] begin + using Random + using Quaternionic: from_spherical_coordinates + + Random.seed!(1234) + const 𝒾 = im + const T = Float64 + const ℓₘₐₓ = 7 + ϵₐ = 8eps(T) + ϵᵣ = 20eps(T) + + # Tests for 𝒟(j, m′, m, α, β, γ) + let ϵₐ=√ϵᵣ, ϵᵣ=√ϵᵣ, 𝒟=Varshalovich.D + n = 4 + for α ∈ αrange(T, n) + for β ∈ βrange(T, n) + if abs(sin(β)) ≤ eps(T) + continue + end + + for γ ∈ γrange(T, n) + D = D_matrices(α, β, γ, ℓₘₐₓ) + i = 1 + for j in 0:ℓₘₐₓ + for m′ in -j:j + for m in -j:j + @test 𝒟(j, m′, m, α, β, γ) ≈ conj(D[i]) atol=ϵₐ rtol=ϵᵣ + i += 1 + end + end + end + + # Test half-integer formula + for j in 1//2:ℓₘₐₓ + for m′ in -j:j + for m in -j:j + D1 = 𝒟(j, m, m′, α, β, γ) + D2 = if m′ ≠ j # use Eq. 4.8.2(14) + ( + √((j-m)/(j-m′)) * cos(β/2) * exp(𝒾*(α+γ)/2) * + 𝒟(j-1//2, m+1//2, m′+1//2, α, β, γ) + - + √((j+m)/(j-m′)) * sin(β/2) * exp(-𝒾*(α-γ)/2) * + 𝒟(j-1//2, m-1//2, m′+1//2, α, β, γ) + ) + else # use Eq. 4.8.2(15) + ( + √((j-m)/(j+m′)) * sin(β/2) * exp(𝒾*(α-γ)/2) * + 𝒟(j-1//2, m+1//2, m′-1//2, α, β, γ) + + + √((j+m)/(j+m′)) * cos(β/2) * exp(-𝒾*(α+γ)/2) * + 𝒟(j-1//2, m-1//2, m′-1//2, α, β, γ) + ) + end + @test D1 ≈ D2 atol=ϵₐ rtol=ϵᵣ + end + end + end + end + end + end + end + +end From 4dd1b4f5b2e61e4388f882ab1f6426ec794d8316 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 15:16:59 -0500 Subject: [PATCH 034/329] Translate formulas from my 2016 paper (via python spherical_functions pkg) --- test/conventions/boyle2016.jl | 182 ++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 test/conventions/boyle2016.jl diff --git a/test/conventions/boyle2016.jl b/test/conventions/boyle2016.jl new file mode 100644 index 00000000..9b74e131 --- /dev/null +++ b/test/conventions/boyle2016.jl @@ -0,0 +1,182 @@ +@testmodule Boyle2016 begin + + using Quaternionic + + """ + WignerDElement(R, ℓ, m′, m) + + Compute a single Wigner-D matrix element for half-integer or integer (ℓ, m′, m). + + `R` is a `Rotor`, and `ℓ`, `m′`, and `m` are the indices of the Wigner-D matrix element. + The indices must all be integers or all be `Rational` with denominators of 2. + + """ + function WignerDElement(R::Rotor{T}, ℓ::I, m′::I, m::I) where {T, I} + # If `I` is Rational, check that the denominators are 2 + if I <: Rational + if (denominator(ℓ) != 2) || (denominator(m′) != 2) || (denominator(m) != 2) + error("The indices ℓ, m′, and m must all be integers or all be half-integers") + end + end + + # Convert to twice the input values for half-integer support + L = Int(2ℓ) + M′ = Int(2m′) + M = Int(2m) + + if L > 16 + error( + "The maximum supported ℓ for this function is 8; " * + "larger numbers become numerically unstable.\n" * + "Consider using the `WignerD` function instead." + ) + end + + # Simple helper for 0+0im + zeroCT = zero(Complex{T}) + + if abs(M′) > L || abs(M) > L + return zeroCT + end + + let π = T(π) + # Split input `R` into its two complex components and extract magnitude and phase + Rₛ = Complex(R[1], R[4]) + Rₐ = Complex(R[3], R[2]) + rₛ = abs(Rₛ) + rₐ = abs(Rₐ) + ϕₛ = angle(Rₛ) + ϕₐ = angle(Rₐ) + + # Check simple limiting cases + if rₐ ≤ 4eps(rₛ) + if M′ != M + return zeroCT + else + return cis(M * ϕₛ) + end + + elseif rₛ ≤ 4eps(rₐ) + if -M′ != M + return zeroCT + else + return cis(M * ϕₐ) * (((L - M) % 4 == 0) ? 1 : -1) + end + + elseif rₐ ≤ rₛ + λ = -(rₐ/rₛ)^2 + ρₘᵢₙ = max(0, (M′ - M)÷2) + κ = √T( + (factorial((L + M)÷2) * factorial((L - M)÷2)) + / (factorial((L + M′)÷2) * factorial((L - M′)÷2)) + ) * + binomial((L + M′)÷2, ρₘᵢₙ) * binomial((L - M′)÷2, (L - M)÷2 - ρₘᵢₙ) + if (ρₘᵢₙ % 2) != 0 + κ = -κ + end + ρₘₐₓ = min((L + M′)÷2, (L - M)÷2) + N₁ = L + M′ + 2 + N₂ = L - M + 2 + N₃ = M - M′ + + total = one(T) + for P in reverse(2ρₘᵢₙ+2:2:2ρₘₐₓ) + total *= λ * ((N₁ - P)*(N₂ - P)) / (P*(N₃ + P)) + total += one(T) + end + return κ * + (rₛ ^ (L - (M - M′)÷2 - 2ρₘᵢₙ)) * + (rₐ ^ ((M - M′)÷2 + 2ρₘᵢₙ)) * + cis((M + M′)÷2 * ϕₛ + (M - M′)÷2 * ϕₐ) * + total + + # return κ * + # (rₛ ^ (L - (M - M′)÷2 - 2ρₘᵢₙ)) * (rₐ ^ ((M - M′)÷2 + 2ρₘᵢₙ)) * + # cis((M + M′)÷2 * ϕₛ + (M - M′)÷2 * ϕₐ) * + # foldl( + # (acc, P) -> acc * λ * ((N₁ - P)*(N₂ - P)) / (P*(N₃ + P)) + one(T), + # reverse(2ρₘᵢₙ+2:2:2ρₘₐₓ), + # init=one(T) + # ) + + else # rₛ < rₐ + λ = -(rₛ/rₐ)^2 + ρₘᵢₙ = max(0, -(M′ + M)÷2) + κ = √T( + (factorial((L + M)÷2) * factorial((L - M)÷2)) + / (factorial((L + M′)÷2) * factorial((L - M′)÷2)) + ) * + binomial((L + M′)÷2, (L - M)÷2 - ρₘᵢₙ) * binomial((L - M′)÷2, ρₘᵢₙ) + if (((L - M)÷2 - ρₘᵢₙ) % 2) != 0 + κ = -κ + end + ρₘₐₓ = min((L - M′)÷2, (L - M)÷2) + N₁ = L - M′ + 2 + N₂ = L - M + 2 + N₃ = M + M′ + + total = one(T) + for P in reverse(2ρₘᵢₙ+2:2:2ρₘₐₓ) + total *= λ * ((N₁ - P)*(N₂ - P)) / (P*(N₃ + P)) + total += one(T) + end + return κ * + (rₐ ^ (L - (M + M′)÷2 - 2ρₘᵢₙ)) * + (rₛ ^ ((M + M′)÷2 + 2ρₘᵢₙ)) * + cis((M - M′)÷2 * ϕₐ + (M + M′)÷2 * ϕₛ) * + total + + # return κ * + # (rₐ ^ (L - (M + M′)÷2 - 2ρₘᵢₙ)) * + # (rₛ ^ ((M + M′)÷2 + 2ρₘᵢₙ)) * + # cis((M - M′)÷2 * ϕₐ + (M + M′)÷2 * ϕₛ) * + # foldl( + # (acc, P) -> acc * λ * ((N₁ - P) * (N₂ - P)) / (P * (N₃ + P)) + one(T), + # reverse(2ρₘᵢₙ+2:2:2ρₘₐₓ), + # init=one(T) + # ) + + end + end + end + +end # @testmodule Boyle2016 + + +@testitem "WignerDElement" setup=[Boyle2016] begin + using Quaternionic + using Random + Random.seed!(1234) + const T = Float64 + const ℓₘₐₓ = 8 + ϵₐ = 8eps(T) + ϵᵣ = 20eps(T) + + Rs = [ + exp(3eps(T)*imx); + exp(3eps(T)*imy); + exp(3eps(T)*imz); + Rotor{T}(imx)*exp(3eps(T)*imx); + Rotor{T}(imx)*exp(3eps(T)*imy); + Rotor{T}(imx)*exp(3eps(T)*imz); + Rotor{T}(imy)*exp(3eps(T)*imx); + Rotor{T}(imy)*exp(3eps(T)*imy); + Rotor{T}(imy)*exp(3eps(T)*imz); + Rotor{T}(imz)*exp(3eps(T)*imx); + Rotor{T}(imz)*exp(3eps(T)*imy); + Rotor{T}(imz)*exp(3eps(T)*imz); + randn(Rotor{T}, 20) + ] + + for R′ ∈ Rs + for R ∈ (R′, conj(R′)) + 𝔇1 = [ + Boyle2016.WignerDElement(R, ℓ, m′, m) + for (ℓ, m′, m) ∈ eachrow(SphericalFunctions.WignerDrange(ℓₘₐₓ)) + ] + 𝔇2 = D_matrices(R, ℓₘₐₓ) + @test 𝔇1 ≈ 𝔇2 + end + end + +end # @testitem "WignerDElement" From 76e5fff8d5c8ff2c0960c1d25168b531f5613c75 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Jan 2025 14:49:40 -0500 Subject: [PATCH 035/329] Add and test explicit 1/2-integer functions --- test/conventions/varshalovich.jl | 193 ++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) diff --git a/test/conventions/varshalovich.jl b/test/conventions/varshalovich.jl index d1c39630..c8431b1f 100644 --- a/test/conventions/varshalovich.jl +++ b/test/conventions/varshalovich.jl @@ -3,8 +3,8 @@ Formulas and conventions from [Varshalovich's "Quantum Theory of Angular Momentu Varshalovich_1988). Note that Varshalovich labels his indices with `M` and `M′`, respectively, but if we just -plug in `m′` and `m` (note the order), we get the expected result — his formulas are the -same as this package's, except with a conjugate. +plug in `m′` and `m` (note the order), we get the expected result; his formulas are the same +as this package's, except with a conjugate. Varshalovich defines his Euler angles (scheme B, page 22) in the same way we do, except that he specifies that this describes the rotation *of the coordinate system*. @@ -92,6 +92,180 @@ function d(J::I, M::I, M′::I, β::T) where {I, T} ) end +""" + d_½_explicit(J, M, M′, β) + +Explicit values for the half-integer Wigner d-function, as given in Tables 4.3—4.12 of +[Varshalovich](@cite Varshalovich_1988). Only values with J ∈ [1/2, 9/2] are supported. +""" +function d_½_explicit(J::Rational{Int}, M::Rational{Int}, M′::Rational{Int}, β::T) where T + if denominator(J) != 2 || denominator(M) != 2 || denominator(M′) != 2 + error("Only half-integer J, M, M′ are supported") + end + if J < 1//2 || J > 9//2 + error("Only J = 1/2, 3/2, 5/2, 7/2, 9/2 are supported") + end + if abs(M) > J || abs(M′) > J + error("abs(M) and abs(M′) must be ≤ J") + end + if M < 0 + (-1)^(M-M′) * d_½_explicit(J, -M, -M′, β) + else + let √ = (x -> √T(x)) + if (J, M, M′) == (1//2, 1//2,-1//2) + -sin(β/2) + elseif (J, M, M′) == (1//2, 1//2, 1//2) + cos(β/2) + + elseif (J, M, M′) == (3//2, 1//2,-3//2) + √3 * sin(β/2)^2 * cos(β/2) + elseif (J, M, M′) == (3//2, 1//2,-1//2) + sin(β/2) * (3 * sin(β/2)^2 - 2) + elseif (J, M, M′) == (3//2, 1//2, 1//2) + cos(β/2) * (3 * cos(β/2)^2 - 2) + elseif (J, M, M′) == (3//2, 1//2, 3//2) + √3 * sin(β/2) * cos(β/2)^2 + elseif (J, M, M′) == (3//2, 3//2,-3//2) + -sin(β/2)^3 + elseif (J, M, M′) == (3//2, 3//2,-1//2) + √3 * sin(β/2)^2 * cos(β/2) + elseif (J, M, M′) == (3//2, 3//2, 1//2) + -√3 * sin(β/2) * cos(β/2)^2 + elseif (J, M, M′) == (3//2, 3//2, 3//2) + cos(β/2)^3 + + elseif (J, M, M′) == (5//2, 5//2, 5//2) + cos(β/2)^5 + elseif (J, M, M′) == (5//2, 5//2, 3//2) + -√5 * sin(β/2) * cos(β/2)^4 + elseif (J, M, M′) == (5//2, 5//2, 1//2) + √10 * sin(β/2)^2 * cos(β/2)^3 + elseif (J, M, M′) == (5//2, 5//2,-1//2) + -√10 * sin(β/2)^3 * cos(β/2)^2 + elseif (J, M, M′) == (5//2, 5//2,-3//2) + √5 * sin(β/2)^4 * cos(β/2) + elseif (J, M, M′) == (5//2, 5//2,-5//2) + -sin(β/2)^5 + elseif (J, M, M′) == (5//2, 3//2, 3//2) + cos(β/2)^3 * (1 - 5 * sin(β/2)^2) + elseif (J, M, M′) == (5//2, 3//2, 1//2) + -√2 * sin(β/2) * cos(β/2)^2 * (2 - 5 * sin(β/2)^2) + elseif (J, M, M′) == (5//2, 3//2,-1//2) + -√2 * sin(β/2)^2 * cos(β/2) * (2 - 5 * cos(β/2)^2) + elseif (J, M, M′) == (5//2, 3//2,-3//2) + sin(β/2)^3 * (1 - 5 * cos(β/2)^2) + elseif (J, M, M′) == (5//2, 1//2, 1//2) + cos(β/2) * (3 - 12 * cos(β/2)^2 + 10 * cos(β/2)^4) + elseif (J, M, M′) == (5//2, 1//2,-1//2) + -sin(β/2) * (3 - 12 * sin(β/2)^2 + 10 * sin(β/2)^4) + + elseif (J, M, M′) == (7//2, 7//2, 7//2) + cos(β/2)^7 + elseif (J, M, M′) == (7//2, 7//2, 5//2) + -√7 * cos(β/2)^6 * sin(β/2) + elseif (J, M, M′) == (7//2, 7//2, 3//2) + √21 * cos(β/2)^5 * sin(β/2)^2 + elseif (J, M, M′) == (7//2, 7//2, 1//2) + -√35 * cos(β/2)^4 * sin(β/2)^3 + elseif (J, M, M′) == (7//2, 7//2,-1//2) + √35 * cos(β/2)^3 * sin(β/2)^4 + elseif (J, M, M′) == (7//2, 7//2,-3//2) + -√21 * cos(β/2)^2 * sin(β/2)^5 + elseif (J, M, M′) == (7//2, 7//2,-5//2) + √7 * cos(β/2) * sin(β/2)^6 + elseif (J, M, M′) == (7//2, 7//2,-7//2) + -sin(β/2)^7 + elseif (J, M, M′) == (7//2, 5//2, 5//2) + cos(β/2)^5 * (1 - 7 * sin(β/2)^2) + elseif (J, M, M′) == (7//2, 5//2, 3//2) + -√3 * cos(β/2)^4 * sin(β/2) * (2 - 7 * sin(β/2)^2) + elseif (J, M, M′) == (7//2, 5//2, 1//2) + √5 * cos(β/2)^3 * sin(β/2)^2 * (3 - 7 * sin(β/2)^2) + elseif (J, M, M′) == (7//2, 5//2,-1//2) + √5 * cos(β/2)^2 * sin(β/2)^3 * (3 - 7 * cos(β/2)^2) + elseif (J, M, M′) == (7//2, 5//2,-3//2) + -√3 * cos(β/2) * sin(β/2)^4 * (2 - 7 * cos(β/2)^2) + elseif (J, M, M′) == (7//2, 5//2,-5//2) + sin(β/2)^5 * (1 - 7 * cos(β/2)^2) + elseif (J, M, M′) == (7//2, 3//2, 3//2) + cos(β/2)^3 * (10 - 30 * cos(β/2)^2 + 21 * cos(β/2)^4) + elseif (J, M, M′) == (7//2, 3//2, 1//2) + -√15 * cos(β/2)^2 * sin(β/2) * (2 - 8 * cos(β/2)^2 + 7 * cos(β/2)^4) + elseif (J, M, M′) == (7//2, 3//2,-1//2) + √15 * cos(β/2) * sin(β/2)^2 * (2 - 8 * sin(β/2)^2 + 7 * sin(β/2)^4) + elseif (J, M, M′) == (7//2, 3//2,-3//2) + -sin(β/2)^3 * (10 - 30 * sin(β/2)^2 + 21 * sin(β/2)^4) + elseif (J, M, M′) == (7//2, 1//2, 1//2) + -cos(β/2) * (4 - 30 * cos(β/2)^2 + 60 * cos(β/2)^4 - 35 * cos(β/2)^6) + elseif (J, M, M′) == (7//2, 1//2,-1//2) + -sin(β/2) * (4 - 30 * sin(β/2)^2 + 60 * sin(β/2)^4 - 35 * sin(β/2)^6) + + elseif (J, M, M′) == (9//2, 9//2, 9//2) + cos(β/2)^9 + elseif (J, M, M′) == (9//2, 9//2, 7//2) + -3 * cos(β/2)^8 * sin(β/2) + elseif (J, M, M′) == (9//2, 9//2, 5//2) + 6 * cos(β/2)^7 * sin(β/2)^2 + elseif (J, M, M′) == (9//2, 9//2, 3//2) + -2 * √21 * cos(β/2)^6 * sin(β/2)^3 + elseif (J, M, M′) == (9//2, 9//2, 1//2) + 3 * √14 * cos(β/2)^5 * sin(β/2)^4 + elseif (J, M, M′) == (9//2, 9//2,-1//2) + -3 * √14 * cos(β/2)^4 * sin(β/2)^5 + elseif (J, M, M′) == (9//2, 9//2,-3//2) + 2 * √21 * cos(β/2)^3 * sin(β/2)^6 + elseif (J, M, M′) == (9//2, 9//2,-5//2) + -6 * cos(β/2)^2 * sin(β/2)^7 + elseif (J, M, M′) == (9//2, 9//2,-7//2) + 3 * cos(β/2) * sin(β/2)^8 + elseif (J, M, M′) == (9//2, 9//2,-9//2) + -sin(β/2)^9 + elseif (J, M, M′) == (9//2, 7//2, 7//2) + cos(β/2)^7 * (1 - 9 * sin(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2, 5//2) + -2 * cos(β/2)^6 * sin(β/2) * (2 - 9 * sin(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2, 3//2) + 2 * √21 * cos(β/2)^5 * sin(β/2)^2 * (1 - 3 * sin(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2, 1//2) + -√14 * cos(β/2)^4 * sin(β/2)^3 * (4 - 9 * sin(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2,-1//2) + -√14 * cos(β/2)^3 * sin(β/2)^4 * (4 - 9 * cos(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2,-3//2) + 2 * √21 * cos(β/2)^2 * sin(β/2)^5 * (1 - 3 * cos(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2,-5//2) + -2 * cos(β/2) * sin(β/2)^6 * (2 - 9 * cos(β/2)^2) + elseif (J, M, M′) == (9//2, 7//2,-7//2) + sin(β/2)^7 * (1 - 9 * cos(β/2)^2) + elseif (J, M, M′) == (9//2, 5//2, 5//2) + cos(β/2)^5 * (21 - 56 * cos(β/2)^2 + 36 * cos(β/2)^4) + elseif (J, M, M′) == (9//2, 5//2, 3//2) + -√21 * cos(β/2)^4 * sin(β/2) * (5 - 16 * cos(β/2)^2 + 12 * cos(β/2)^4) + elseif (J, M, M′) == (9//2, 5//2, 1//2) + √14 * cos(β/2)^3 * sin(β/2)^2 * (5 - 20 * cos(β/2)^2 + 18 * cos(β/2)^4) + elseif (J, M, M′) == (9//2, 5//2,-1//2) + -√14 * cos(β/2)^2 * sin(β/2)^3 * (5 - 20 * sin(β/2)^2 + 18 * sin(β/2)^4) + elseif (J, M, M′) == (9//2, 5//2,-3//2) + √21 * cos(β/2) * sin(β/2)^4 * (5 - 16 * sin(β/2)^2 + 12 * sin(β/2)^4) + elseif (J, M, M′) == (9//2, 5//2,-5//2) + -sin(β/2)^5 * (21 - 56 * sin(β/2)^2 + 36 * sin(β/2)^4) + elseif (J, M, M′) == (9//2, 3//2, 3//2) + -cos(β/2)^3 * (20 - 105 * cos(β/2)^2 + 168 * cos(β/2)^4 - 84 * cos(β/2)^6) + elseif (J, M, M′) == (9//2, 3//2, 1//2) + √6 * cos(β/2)^2 * sin(β/2) * (5 - 35 * cos(β/2)^2 + 70 * cos(β/2)^4 - 42 * cos(β/2)^6) + elseif (J, M, M′) == (9//2, 3//2,-1//2) + √6 * cos(β/2) * sin(β/2)^2 * (5 - 35 * sin(β/2)^2 + 70 * sin(β/2)^4 - 42 * sin(β/2)^6) + elseif (J, M, M′) == (9//2, 3//2,-3//2) + -sin(β/2)^3 * (20 - 105 * sin(β/2)^2 + 168 * sin(β/2)^4 - 84 * sin(β/2)^6) + elseif (J, M, M′) == (9//2, 1//2, 1//2) + cos(β/2) * (5 - 60 * cos(β/2)^2 + 210 * cos(β/2)^4 - 280 * cos(β/2)^6 + 126 * cos(β/2)^8) + elseif (J, M, M′) == (9//2, 1//2,-1//2) + -sin(β/2) * (5 - 60 * sin(β/2)^2 + 210 * sin(β/2)^4 - 280 * sin(β/2)^6 + 126 * sin(β/2)^8) + end + end + end +end + + end # @testmodule Varshalovich @@ -153,9 +327,24 @@ end # @testmodule Varshalovich end end end + + end + end + end + + for β ∈ βrange(T, n) + # Test the explicit half-integer d-functions + for J ∈ 1//2:3//2 + for M ∈ -J:J + for M′ ∈ -J:J + d1 = Varshalovich.d(J, M, M′, β) + d2 = Varshalovich.d_½_explicit(J, M, M′, β) + @test d1 ≈ d2 atol=ϵₐ rtol=ϵᵣ + end end end end + end end From 7e300335791ad283f6059390722eceaf4610cea8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Jan 2025 14:50:05 -0500 Subject: [PATCH 036/329] Add discussion and a few functions for NIST (no tests yet) --- test/conventions/NIST_DLMF.jl | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/conventions/NIST_DLMF.jl diff --git a/test/conventions/NIST_DLMF.jl b/test/conventions/NIST_DLMF.jl new file mode 100644 index 00000000..3d43cf3c --- /dev/null +++ b/test/conventions/NIST_DLMF.jl @@ -0,0 +1,92 @@ +raw""" +Formulas and conventions from [NIST's Digital Library of Mathematical Functions](@cite +NIST_DLMF). + +The DLMF distinguishes between Ferrer's function (of the first kind) ``\mathup{P}_\nu^\mu`` +and the associated Legendre function (of the first kind) ``P_\nu^\mu``. Here, ``\nu`` is +called the "degree" and ``\mu`` is called the "order". We can see from their definitions in +Eqs. [14.3.1](http://dlmf.nist.gov/14.3#E1) and [14.3.6](http://dlmf.nist.gov/14.3#E6), +respectively, that they differ only by a factor of ``(-1)^{\mu/2}``. + +For integer degree and order, we have [Eq. 14.7.10](http://dlmf.nist.gov/14.7#E10) +```math + \mathsf{P}^{m}_{n}\left(x\right) + = + (-1)^{m+n} + \frac{\left(1-x^{2}\right)^{m/2}}{2^{n}n!} + \frac{{\mathrm{d}}^{m+n}}{{\mathrm{d}x}^{m+n}} + \left(1-x^{2}\right)^{n} +``` +or [Eq. 14.7.14](http://dlmf.nist.gov/14.7#E14) +```math + P^{m}_{n}\left(x\right) + = + \frac{\left(x^{2}-1\right)^{m/2}}{2^{n}n!} + \frac{{\mathrm{d}}^{m+n}}{{\mathrm{d}x}^{m+n}} + \left(x^{2}-1\right)^{n}. +``` +And for the spherical harmonics, [Eq. 14.30.1](http://dlmf.nist.gov/14.30#E1) gives +```math + Y_{\ell, m}\left(\theta,\phi\right) + = + \left(\frac{(\ell-m)!(2\ell+1)}{4\pi(\ell+m)!}\right)^{1/2} + \mathsf{e}^{im\phi} + \mathsf{P}_{\ell}^{m}\left(\cos\theta\right). +``` + +""" + +@testmodule NIST_DLMF begin + +import FastDifferentiation + +const 𝒾 = im + +include("../utilities/naive_factorial.jl") +import .NaiveFactorials: ❗ + +function P(x::T, n, m) where {T} + if m > n + zero(x) + else + (-1)^(m+n) * (1-x^2)^(m/2) / T(2^n * (n)❗) * + FastDifferentiation.derivative(x -> (1-x^2)^n, m+n) + end +end + +function 𝘗(x::T, n, m) where {T} + if m > n + zero(x) + else + (x^2-1)^(m/2) / T(2^n * (n)❗) * + FastDifferentiation.derivative(x -> (x^2-1)^n, m+n) + end +end + +function Y(ℓ, m, θ::T, φ::T) where {T} + if m > ℓ + zero(Complex{T}) + else + let π = T(π) + √T((ℓ-m)❗ * (2ℓ+1) / (4π * (ℓ+m)❗)) * + exp(𝒾*m*φ) * P(cos(θ), ℓ, m) + end + end +end + +end # @testmodule NIST_DLMF + + +@testitem "NIST_DLMF conventions" setup=[Utilities, NIST_DLMF] begin + using Random + using Quaternionic: from_spherical_coordinates + + Random.seed!(1234) + const T = Float64 + const ℓₘₐₓ = 3 + ϵₐ = 8eps(T) + ϵᵣ = 20eps(T) + + # TODO: Add tests + +end # @testitem NIST_DLMF From e679e67ddb64291adbc2331bfba60efc8b1c2656 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 14 Jan 2025 23:03:27 -0500 Subject: [PATCH 037/329] Add original Gumerov-Duraiswami references --- docs/src/references.bib | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/src/references.bib b/docs/src/references.bib index 66c1be9f..7ee2146e 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -160,6 +160,32 @@ @article{GoldbergEtAl_1967 url = {https://doi.org/10.1063/1.1705135}, } +@techreport{Gumerov_2001, + address = {College Park, {MD}}, + title = {Fast, Exact, and Stable Computation of Multipole Translation and Rotation + Coefficients for the {3-D} Helmholtz Equation}, + url = {https://users.umiacs.umd.edu/~ramani/pubs/multipole.pdf}, + number = {UMIACS {TR} 2001-44}, + institution = {University of Maryland}, + author = {Gumerov, Nail A and Duraiswami, Ramani}, + year = 2001 +} + +@article{Gumerov_2004, + title = {Recursions for the Computation of Multipole Translation and Rotation Coefficients + for the {3-D} Helmholtz Equation}, + volume = 25, + issn = {1064-8275}, + url = {https://epubs.siam.org/doi/10.1137/S1064827501399705}, + doi = {10.1137/S1064827501399705}, + number = 4, + journal = {{SIAM} Journal on Scientific Computing}, + author = {Gumerov, Nail A. and Duraiswami, Ramani}, + month = jan, + year = 2004, + pages = {1344--1381} +} + @incollection{Gumerov_2015, doi = {10.1007/978-3-319-13230-3_5}, url = {https://doi.org/10.1007/978-3-319-13230-3_5}, From 19cf41cfd9f8ad936e0ab5b40cab591751864b3f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Jan 2025 15:43:14 -0500 Subject: [PATCH 038/329] Include Bander and Lee --- docs/src/conventions/conventions.md | 193 +++++++++++++++++++++++++++- docs/src/references.bib | 26 ++++ 2 files changed, 217 insertions(+), 2 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 36a83bcc..8b48ab3c 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -181,7 +181,17 @@ The unit coordinate vectors in spherical coordinates are then \end{aligned} ``` where, again, we omit the hats on the unit vectors to keep the -notation simple. +notation simple. Conversely, we can express the Cartesian basis +vectors in terms of the spherical basis vectors as +```math +\begin{aligned} +\mathbf{𝐱} &= \sin\theta \cos\phi \mathbf{𝐫} + \cos\theta \cos\phi \boldsymbol{\theta} - \sin\phi \boldsymbol{\phi}, +\\ +\mathbf{𝐲} &= \sin\theta \sin\phi \mathbf{𝐫} + \cos\theta \sin\phi \boldsymbol{\theta} + \cos\phi \boldsymbol{\phi}, +\\ +\mathbf{𝐳} &= \cos\theta \mathbf{𝐫} - \sin\theta \boldsymbol{\theta}. +\end{aligned} +``` One seemingly obvious — but extremely important — fact is that the unit basis frame ``(𝐱, 𝐲, 𝐳)`` can be rotated onto @@ -405,6 +415,37 @@ g_{i'j'} 0 & \frac{R^2 \cos\beta}{4} & 0 & \frac{R^2}{4} \end{array} \right)_{i'j'}. ``` +The unit basis vectors in extended Euler coordinates in terms of the +unit basis vectors in quaternion coordinates are +```math +\begin{aligned} +\mathbf{𝐑} &= \frac{1}{R} \left( + \cos \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝟏 + - \sin \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐢 + + \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 + + \cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝐤 +\right), \\ +\boldsymbol{\alpha} &= \frac{R}{2} \left( + -\cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝟏 + - \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐢 + - \sin \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐣 + + \cos \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝐤 +\right), \\ +\boldsymbol{\beta} &= \frac{R}{2} \left( + -\sin \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝟏 + - \cos \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐢 + + \cos \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 + - \sin \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝐤 +\right), \\ +\boldsymbol{\gamma} &= \frac{R}{2} \left( + -\cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝟏 + + \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐢 + - \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 + - \cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝐤 +\right). +\end{aligned} +``` + Again, integration involves a square-root of the determinant of the metric, which reduces to ``R^3 |\sin\beta| / 8``. Note that — unlike with standard spherical coordinates — the absolute value is necessary @@ -802,6 +843,8 @@ R_\pm &= R_\mathbf{x} \pm i R_\mathbf{y}. \end{aligned} ``` +[Show how this happens:] + Using these relations, we can actually solve for the constants ``\lambda`` and ``\rho`` up to a sign. We find that ```math @@ -844,4 +887,150 @@ $\partial_\alpha$, etc., when $\theta=0$. &= -\mathbf{z} \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right), \end{align} -``` \ No newline at end of file +``` + +### Laplacians + +[Bander_1966](@citet) show that Wigner's D matrices (extended to the +full space of quaternions with arbitrary norm) are harmonic with +respect to the Laplacian of the full 4-D space. We also know that +```math +\Delta_{S^{n-1}} f(x) = \Delta_{\mathbb{R}^n} f(x/|x|), +``` +and +```math +\Delta_{\mathbb{R}^n} f(x) += +\frac{1}{r^{n-1}} \frac{\partial}{\partial r} \left( r^{n-1} \frac{\partial f}{\partial r} \right) ++ +\frac{1}{r^2} \Delta_{S^{n-1}} f. +``` +These imply that the restriction to the space of unit quaternions is +not harmonic with respect to the Laplacian on the 3-sphere, but is an +eigenfunction with eigenvalue ``-\ell(\ell+2)``. + +```math +\frac{1}{r^{n-1}} \frac{\partial}{\partial r} \left( r^{n-1} \frac{\partial f}{\partial r} \right) += +\frac{1}{r^{n-1}} \left( r^{n-1} \frac{\partial}{\partial r} \frac{\partial f}{\partial r} \right) ++ +\frac{1}{r^{n-1}} \frac{\partial}{\partial r} \left( r^{n-1} \right) \frac{\partial f}{\partial r} += +\frac{\partial^2 f}{\partial r^2} ++ +\frac{n-1}{r^{n-1}} r^{n-2} \frac{\partial f}{\partial r} += +\frac{\partial^2 f}{\partial r^2} ++ +\frac{n-1}{r} \frac{\partial f}{\partial r} +``` + +```math +\frac{\partial^2 f}{\partial r^2} ++ +\frac{n-1}{r} \frac{\partial f}{\partial r} += +\frac{f}{r^\ell} \frac{\partial^2 r^\ell}{\partial r^2} ++ +\frac{f}{r^\ell} \frac{n-1}{r} \frac{\partial r^\ell}{\partial r} += +\ell(\ell-1) \frac{f}{r^\ell} r^{\ell-2} ++ +\ell \frac{f}{r^\ell} \frac{n-1}{r} r^{\ell-1} += +\ell(\ell-1) \frac{f}{r^2} ++ +\ell (n-1) \frac{f}{r^2} +\to +\ell(\ell+2) \frac{f}{r^2} +``` + +Note that [Lee_2012](@citet) points out that there is a sign ambiguity +in the Laplacian. As I see it, the geometry community skews toward +including a negative sign (which means that all eigenvalues are +non-negative), while the physics community skews toward excluding it +(which means that all eigenvalues are non-positive). It's also easy +to prove that on a closed and connected manifold, eigenfunctions with +distinct eigenvalues are orthogonal, since +```math +(\lambda_u - \lambda_v) \int f_u f_v += \int (\lambda_u f_u) f_v - \int f_u (\lambda_v f_v) += \int (\Delta f_u) f_v - \int f_u (\Delta f_v) = 0 +``` +(the last equality by Green's theorem). Since the eigenvalues are +distinct, this can only be true if ``\int f_u f_v=0``. + + +## Representation theory / harmonic analysis + - Representations show up in Fourier analysis on groups + - Peter-Weyl theorem + - Generalizes Fourier analysis to compact groups + - Has three parts, [as given by Wikipedia](https://en.wikipedia.org/wiki/Peter%E2%80%93Weyl_theorem): + 1. "The matrix coefficients of irreducible representations of + ``G`` are dense in the space ``C(G)`` of continuous + complex-valued functions on ``G``, and thus also in the space + ``L^2(G)`` of square-integrable functions." + 2. Unitary representations of ``G`` are completely reducible. + 3. "The regular representation of ``G`` on ``L^2(G)`` decomposes + as the direct sum of all irreducible unitary representations. + Moreover, the matrix coefficients of the irreducible unitary + representations form an orthonormal basis of ``L^2(G)``." + - Representation theory of ``\mathbf{Spin}(3)`` + - Show how the Lie algebra is represented by the angular-momentum operators + - Show how the Lie group is represented by the Wigner D-matrices + - Demonstrate that ``\mathfrak{D}`` is a representation + - Demonstrate its behavior under left and right rotation + - Demonstrate orthonormality + - Representation theory of ``\mathbf{SO}(3)`` + - There are several places in [Folland](@cite Folland_2016) (e.g., + above corollary 5.48) where he mentions that representations of + a quotient group are just representations that are trivial + (evidently meaning mapping everything to the identity matrix) on + the factor. I can't find anywhere that he explains this + explicitly, but it seems easy enough to show. He might do it + using characters. + - For ``\mathbf{Spin}(3)`` and ``\mathbf{SO}(3)``, the factor + group is just ``\{1, -1\}``. Presumably, every representation + acting on ``1`` will give the identity matrix, so that's + trivial. So we just need a criterion for when a representation + is trivial on ``-1``. Noting that ``\exp(\pi \vec{v}) = -1`` + for any ``\vec{v}``, I think we can show that this requires + ``m \in \mathbb{Z}``. + - Basically, the point is that the representations of + ``\mathbf{SO}(3)`` are just the integer representations of + ``\mathbf{Spin}(3)``. + - Restrict to homogeneous space (S³ -> S²) + - The circle group is a closed (normal?) subgroup of + ``\mathbf{Spin}(3)``, which we might implement as initial + multiplication about a particular axis. + - In Eq. (2.47) [Folland (2016)](@cite Folland_2016) defines a + functional taking a function on the group to a function on the + homogeneous space by integrating over the factor (the circle + group). This gives you the spherical harmonics, but *not* the + spin-weighted spherical harmonics — because the spin-weighted + spherical harmonics cannot be defined on the 2-sphere. + - Spin weight comes from Fourier analysis on the subgroup. + - Representation matrices transfer to the homogeneous space, with + sparsity patterns + +## Recursion relations + +[Gumerov and Duraiswami (2001)](@cite Gumerov_2001) derive their +recursion relations by differentiating solutions of the Helmholtz +equation ``\nabla^2 \psi + k^2 \psi = 0`` as ``\tfrac{1}{k} \nabla +\psi``. More precisely, they differentiate both sides of the equation +relating one solution to its rotated form — which naturally involves +Wigner's ``\mathfrak{D}`` matrix. Using orthogonal basis functions +for the solution, this allows them to equate terms on the two sides +proportional to a given basis function, which leaves them with +expressions involving sums of only the ``\mathfrak{D}`` matrices and +some coefficients depending on the indices of the basis functions (and +hence of ``\mathfrak{D}``) on both sides of the equation. Since +``\nabla`` is a 3-vector operator, this gives them three relations. + +This, of course, is happening in 3-D space, since ``\psi`` is a +function of location in the Helmholtz equation. It seems likely to +me, however, that we could use the 4-D (quaternionic) version of the +functions + +The SWSHs/``\mathfrak{D}`` functions can be promoted to diff --git a/docs/src/references.bib b/docs/src/references.bib index 7ee2146e..8d02bc1b 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -12,6 +12,19 @@ @misc{Ajith_2007 primaryClass = "gr-qc", } +@article{Bander_1966, + title = {Group Theory and the Hydrogen Atom {(I)}}, + volume = 38, + url = {https://link.aps.org/doi/10.1103/RevModPhys.38.330}, + doi = {10.1103/RevModPhys.38.330}, + number = 2, + journal = {Reviews of Modern Physics}, + author = {Bander, M. and Itzykson, C.}, + month = apr, + year = 1966, + pages = {330--345} +} + @article{Belikov_1991, title = {Spherical harmonic analysis and synthesis with the use of column-wise recurrence relations}, @@ -241,6 +254,19 @@ @article{Kostelec_2008 journal = {Journal of Fourier Analysis and Applications} } +@book{Lee_2012, + address = {New York, {NY}}, + series = {Graduate Texts in Mathematics}, + title = {Introduction to Smooth Manifolds}, + volume = 218, + isbn = {978-1-4419-9981-8 978-1-4419-9982-5}, + url = {https://link.springer.com/10.1007/978-1-4419-9982-5}, + publisher = {Springer}, + author = {Lee, John M.}, + year = 2012, + doi = {10.1007/978-1-4419-9982-5}, +} + @article{McEwen_2011, doi = {10.1109/tsp.2011.2166394}, url = {https://doi.org/10.1109/tsp.2011.2166394}, From 393c5e45b7c662f342729973693a1bcdf067621b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 00:29:32 -0500 Subject: [PATCH 039/329] Show how to use Euler angles to derive ang.-mom. ops --- docs/src/conventions/conventions.md | 64 ++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 8b48ab3c..404e82cf 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -744,11 +744,6 @@ course, we will see below that changing by a phase proportional to ### Differential rotations - - Express angular momentum operators in terms of quaternion components - - Basic Lie definition - - Properties: form a Lie algebra with the commutator as the Lie bracket - - We now define a pair of operators that differentiate a function with respect to infinitesimal rotations we apply to the functions themselves: @@ -872,9 +867,10 @@ depend on $\theta$. We then use the chain rule to express $\partial_\theta$ in terms of $\partial_{\alpha'}$, etc., which become $\partial_\alpha$, etc., when $\theta=0$. + ```math \begin{align} - L_i f(\mathbf{R}) + L_i f(\mathbf{R}_{\alpha, \beta, \gamma}) &= \left. -\mathbf{z} \frac{\partial} {\partial \theta} f \left( e^{\theta \mathbf{e}_i / 2} \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} \\ &= @@ -883,12 +879,47 @@ $\partial_\alpha$, etc., when $\theta=0$. \left. -\mathbf{z} \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha'} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta'} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma'} \right] f \left( \mathbf{R}_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ &= -\mathbf{z} \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right) \\ - K_i f(\mathbf{R}) + K_i f(\mathbf{R}_{\alpha, \beta, \gamma}) &= -\mathbf{z} \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right), \end{align} ``` +```math +\begin{aligned} +\mathbf{R}_{\alpha, \beta, \gamma} +&= + R\, \cos\frac{β}{2} \cos\frac{α+γ}{2} + -R\, \sin\frac{β}{2} \sin\frac{α-γ}{2} \mathbf{i} + + R\, \sin\frac{β}{2} \cos\frac{α-γ}{2} \mathbf{j} + + R\, \cos\frac{β}{2} \sin\frac{α+γ}{2} \mathbf{k}. +\\ +e^{\theta \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} +&= \left(\cos\frac{\theta}{2} + \mathbf{u} \sin\frac{\theta}{2}\right) \mathbf{R}_{\alpha, \beta, \gamma} +\\ +&= + R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \cos\frac{α+γ}{2} + -R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \sin\frac{α-γ}{2} \mathbf{i} + + R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \cos\frac{α-γ}{2} \mathbf{j} + + R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \sin\frac{α+γ}{2} \mathbf{k} +\\ +&\quad + + R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \cos\frac{α+γ}{2} \mathbf{u} + -R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \sin\frac{α-γ}{2} \mathbf{u}\mathbf{i} + + R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \cos\frac{α-γ}{2} \mathbf{u}\mathbf{j} + + R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \sin\frac{α+γ}{2} \mathbf{u}\mathbf{k} +\end{aligned} +``` + +```math +\begin{aligned} +\alpha &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ +\beta &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ +\gamma &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), +\end{aligned} +``` + + ### Laplacians [Bander_1966](@citet) show that Wigner's D matrices (extended to the @@ -941,6 +972,8 @@ eigenfunction with eigenvalue ``-\ell(\ell+2)``. \ell(\ell-1) \frac{f}{r^2} + \ell (n-1) \frac{f}{r^2} += +\ell(\ell+n-2) \frac{f}{r^2} \to \ell(\ell+2) \frac{f}{r^2} ``` @@ -960,6 +993,8 @@ distinct eigenvalues are orthogonal, since (the last equality by Green's theorem). Since the eigenvalues are distinct, this can only be true if ``\int f_u f_v=0``. +[Show the relationship between the spherical Laplacian and the angular momentum operator.] + ## Representation theory / harmonic analysis - Representations show up in Fourier analysis on groups @@ -1031,6 +1066,15 @@ hence of ``\mathfrak{D}``) on both sides of the equation. Since This, of course, is happening in 3-D space, since ``\psi`` is a function of location in the Helmholtz equation. It seems likely to me, however, that we could use the 4-D (quaternionic) version of the -functions - -The SWSHs/``\mathfrak{D}`` functions can be promoted to +functions. Note that G&D use ``\partial_z`` and ``\partial_x \pm i +\partial_y`` as their operators to differentiate the functions — that +is, the derivatives are with respect to Cartesian coordinates, which +may be more similar to the right-derivative defined above. However, I +don't know that we'll necessarily be able to achieve the same results +with just angular-momentum operators, since their operators do involve +moving off of the sphere. Maybe we'd need to move off of the sphere +in 4-D space to get comparable results. + +The SWSHs/``\mathfrak{D}`` functions can be naturally promoted to +functions not just on the 3-sphere, but also in 4-D space just by +allowing the quaternions to be non-unit quaternions. From 41c981fdf493a8118ded34d6a515bdc02fde2f1d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 09:40:34 -0500 Subject: [PATCH 040/329] Rename "notebooks" to "literate_input" --- .../condon_shortley_expression.py | 0 .../conventions.py | 0 docs/literate_input/euler_angular_momentum.jl | 83 +++++++++++++++++++ 3 files changed, 83 insertions(+) rename docs/{notebooks => literate_input}/condon_shortley_expression.py (100%) rename docs/{notebooks => literate_input}/conventions.py (100%) create mode 100644 docs/literate_input/euler_angular_momentum.jl diff --git a/docs/notebooks/condon_shortley_expression.py b/docs/literate_input/condon_shortley_expression.py similarity index 100% rename from docs/notebooks/condon_shortley_expression.py rename to docs/literate_input/condon_shortley_expression.py diff --git a/docs/notebooks/conventions.py b/docs/literate_input/conventions.py similarity index 100% rename from docs/notebooks/conventions.py rename to docs/literate_input/conventions.py diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl new file mode 100644 index 00000000..0d5d9154 --- /dev/null +++ b/docs/literate_input/euler_angular_momentum.jl @@ -0,0 +1,83 @@ +# # Expressing angular-momentum operators in Euler angles +# Here, we will use SymPy to just grind through the algebra of expressing the +# angular-momentum operators in terms of Euler angles. + + +# Essential imports +import SymPyPythonCall +import SymPyPythonCall: sympy +import SymPyPythonCall: symbols, sqrt, exp, sin, cos, acos, atan, Matrix, simplify +const Derivative = sympy.Derivative +const Quaternion = sympy.Quaternion +const π = sympy.pi + +# Define the spherical coordinates +α, β, γ, θ = symbols("α β γ θ", real=true, positive=true) +uw, ux, uy, uz = symbols("u_w u_x u_y u_z", real=true) + +# Define our basis quaternions +i = Quaternion(0, 1, 0, 0) +j = Quaternion(0, 0, 1, 0) +k = Quaternion(0, 0, 0, 1) + +# And an arbitrary vector quaternion +u = Quaternion(0, ux, uy, uz) + +# Check that multiplication agrees with our conventions +@assert i*j == k +@assert j*k == i +@assert k*i == j +@assert i*j*k == Quaternion(-1, 0, 0, 0) + + +# +function L(u) + e = cos(θ/2) + u * sin(θ/2) + R = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() + eR = (e * R).expand().simplify() + w, x, y, z = eR.to_Matrix().transpose().tolist()[1] + αp = (atan(z/w) + atan(-x/y)).expand().simplify() + βp = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() + γp = (atan(z/w) - atan(-x/y)).expand().simplify() + return ( + Derivative(αp, θ).doit().subs(θ, 0).expand().simplify(), + Derivative(βp, θ).doit().subs(θ, 0).expand().simplify(), + Derivative(γp, θ).doit().subs(θ, 0).expand().simplify() + ) +end + +function R(u) + e = cos(θ/2) + u * sin(θ/2) + R1 = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() + Re = (R1 * e).expand().simplify() + w, x, y, z = Re.to_Matrix().transpose().tolist()[1] + αp = (atan(z/w) + atan(-x/y)).expand().simplify() + βp = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() + γp = (atan(z/w) - atan(-x/y)).expand().simplify() + return ( + Derivative(αp, θ).doit().subs(θ, 0).expand().simplify(), + Derivative(βp, θ).doit().subs(θ, 0).expand().simplify(), + Derivative(γp, θ).doit().subs(θ, 0).expand().simplify() + ) +end + + +# +L(i) + +# +L(j) + +# +L(k) + +# +R(i) + +# +R(j) + +# +R(k) + +# From 7c9b6979fd84d89283a524b939fd9f018c320970 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 09:40:48 -0500 Subject: [PATCH 041/329] Remove Hwloc --- Project.toml | 7 ++++--- src/SphericalFunctions.jl | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index d01630b9..fa4743f4 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,6 @@ AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FastTransforms = "057dd010-8810-581a-b7be-e3fc3b93f78c" -Hwloc = "0e44f5e4-bd66-52a0-8798-143a42290a1d" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" @@ -24,12 +23,13 @@ AbstractFFTs = "1" Aqua = "0.8" Coverage = "1.6" DoubleFloats = "1" -ForwardDiff = "0.10, 1" FFTW = "1" FastDifferentiation = "0.3.17" FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17" +ForwardDiff = "0.10" Hwloc = "2, 3" LinearAlgebra = "1" +Literate = "2.20" Logging = "1.11" LoopVectorization = "0.12" OffsetArrays = "1.10" @@ -53,6 +53,7 @@ FastTransforms = "057dd010-8810-581a-b7be-e3fc3b93f78c" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Hwloc = "0e44f5e4-bd66-52a0-8798-143a42290a1d" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" @@ -63,4 +64,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "Hwloc", "LinearAlgebra", "Logging", "OffsetArrays", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] +test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 5f0a2bde..3351884c 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -10,7 +10,6 @@ using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, using StaticArrays: @SVector using SpecialFunctions, DoubleFloats using LoopVectorization: @turbo -using Hwloc: num_physical_cores using Base.Threads: @threads, nthreads using TestItems: @testitem From d81a56153f30c3c6b6a517912b23b40c5289e7d8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 09:57:54 -0500 Subject: [PATCH 042/329] Incorporate Literate.jl --- .gitignore | 2 ++ docs/Project.toml | 2 ++ docs/make.jl | 26 +++++++++++++++++++++++--- scripts/docs.jl | 9 ++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 486dff8b..dfc0bf1d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ conventions.slides.json rotate.jl +docs/.CondaPkg +docs/src/literate_output diff --git a/docs/Project.toml b/docs/Project.toml index 34a8c65f..17afff78 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,9 +3,11 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SphericalFunctions = "af6d55de-b1f7-4743-b797-0829a72cf84e" +SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" diff --git a/docs/make.jl b/docs/make.jl index 53a46cd6..0b0e18fa 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,15 +2,34 @@ # julia -t 4 --project=. scripts/docs.jl # assuming you are in this top-level directory -using SphericalFunctions using Documenter +using Literate + +docs_src_dir = joinpath(@__DIR__, "src") + +# See LiveServer.jl docs for this: https://juliadocs.org/LiveServer.jl/dev/man/ls+lit/ +literate_input = joinpath(@__DIR__, "literate_input") +literate_output = joinpath(docs_src_dir, "literate_output") +for (root, _, files) ∈ walkdir(literate_input), file ∈ files + # ignore non julia files + splitext(file)[2] == ".jl" || continue + # full path to a literate script + input_path = joinpath(root, file) + # generated output path + output_path = splitdir(replace(input_path, literate_input=>literate_output))[1] + # generate the markdown file calling Literate + Literate.markdown(input_path, output_path) +end +relative_literate_output = relpath(literate_output, docs_src_dir) + using DocumenterCitations bib = CitationBibliography( - joinpath(@__DIR__, "src", "references.bib"); + joinpath(docs_src_dir, "references.bib"); #style=:authoryear, ) +using SphericalFunctions DocMeta.setdocmeta!(SphericalFunctions, :DocTestSetup, :(using SphericalFunctions); recursive=true) makedocs( @@ -37,10 +56,11 @@ makedocs( "Conventions" => [ "conventions/conventions.md", "conventions/comparisons.md", + joinpath(relative_literate_output, "euler_angular_momentum.md"), ], "Notes" => map( s -> "notes/$(s)", - sort(readdir(joinpath(@__DIR__, "src/notes"))) + sort(readdir(joinpath(docs_src_dir, "notes"))) ), "References" => "references.md", ], diff --git a/scripts/docs.jl b/scripts/docs.jl index f0b47455..f619717a 100644 --- a/scripts/docs.jl +++ b/scripts/docs.jl @@ -16,4 +16,11 @@ cd((@__DIR__) * "/..") Pkg.activate("docs") using LiveServer -servedocs(launch_browser=true) +literate_input = joinpath(pwd(), "docs", "literate_input") +literate_output = joinpath(pwd(), "docs", "src", "literate_output") +@info "Using input for Literate.jl from $literate_input" +servedocs( + literate_dir = literate_input, + skip_dir = literate_output, + launch_browser=true +) From eefdba5b16d5c5eace8aa9ed379bd32e2177c2e3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:49:00 -0500 Subject: [PATCH 043/329] A few minor consistency changes --- docs/src/conventions/conventions.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 404e82cf..e6621bde 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -50,11 +50,11 @@ Euler angles. 8. For a complex-valued function ``f(𝐑)``, we define two operators, the left and right Lie derivatives: ```math - L_𝐮 f(𝐑) = \left.-i \frac{d}{d\epsilon}\right|_{\epsilon=0} - f\left(e^{\epsilon 𝐮/2}\, 𝐑\right) + L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} + f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) \qquad \text{and} \qquad - R_𝐮 f(𝐑) = \left.-i \frac{d}{d\epsilon}\right|_{\epsilon=0} - f\left(𝐑\, e^{\epsilon 𝐮/2}\right), + R_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} + f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), ``` where ``𝐮`` can be any pure-vector quaternion. In particular, ``L`` represents the standard angular-momentum operators, and we @@ -72,17 +72,17 @@ Euler angles. +\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} \right\} \\ L_z = L_𝐤 &= -i \frac{\partial} {\partial \alpha} \\ - K_x = K_𝐢 &= -i \left\{ + R_x = R_𝐢 &= -i \left\{ -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} +\sin\gamma \frac{\partial} {\partial \beta} +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} \right\} \\ - K_y = K_𝐣 &= -i \left\{ + R_y = R_𝐣 &= -i \left\{ \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} +\cos\gamma \frac{\partial} {\partial \beta} -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} \right\} \\ - K_z = K_𝐤 &= -i \frac{\partial} {\partial \gamma} + R_z = R_𝐤 &= -i \frac{\partial} {\partial \gamma} \end{aligned} ``` We can lift any function on ``S^2`` to a function on ``S^3`` — or @@ -105,8 +105,8 @@ Euler angles. L_z &= -i \frac{\partial} {\partial \phi} \end{aligned} ``` - (The ``R`` operators make less sense for a function of spherical - coordinates.) + The ``R`` operators make less sense for a function of spherical + coordinates. 9. Spherical harmonics 10. Wigner D-matrices 11. Spin-weighted spherical harmonics From 3faca256343c3bcadc2edcfffb6430b55da89124 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:49:28 -0500 Subject: [PATCH 044/329] Show build start and elapsed times --- docs/make.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 0b0e18fa..c27cc19d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,6 +2,12 @@ # julia -t 4 --project=. scripts/docs.jl # assuming you are in this top-level directory +# Pretty-print the current time +using Dates +@info """Building docs starting at $(Dates.format(Dates.now(), "HH:MM:SS")).""" + +start = time() # We'll display the total after everything has finished + using Documenter using Literate @@ -18,7 +24,7 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files # generated output path output_path = splitdir(replace(input_path, literate_input=>literate_output))[1] # generate the markdown file calling Literate - Literate.markdown(input_path, output_path) + Literate.markdown(input_path, output_path, documenter=true, mdstrings=true) end relative_literate_output = relpath(literate_output, docs_src_dir) @@ -73,3 +79,5 @@ deploydocs( devbranch="main", push_preview=true ) + +println("Docs built in ", time() - start, " seconds.") From 02ecec67d899e807cf1a668a0854b28340dc9376 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:51:17 -0500 Subject: [PATCH 045/329] Add explanation, streamline operator definitions, and make pretty output --- docs/literate_input/euler_angular_momentum.jl | 231 ++++++++++++++---- 1 file changed, 178 insertions(+), 53 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 0d5d9154..778a5b8f 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -1,83 +1,208 @@ -# # Expressing angular-momentum operators in Euler angles -# Here, we will use SymPy to just grind through the algebra of expressing the -# angular-momentum operators in terms of Euler angles. - - -# Essential imports +md""" +# ``L_j`` in Euler angles +Here, we will use SymPy to just grind through the algebra of expressing the +angular-momentum operators in terms of Euler angles. + +The plan starts by defining a new set of Euler angles according to +```math +\mathbf{R}_{\alpha', \beta', \gamma'} += e^{-\theta \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} +\qquad \text{or} \qquad +\mathbf{R}_{\alpha', \beta', \gamma'} += \mathbf{R}_{\alpha, \beta, \gamma} e^{-\theta \mathbf{u} / 2} +``` +where ``\mathbf{u}`` will be each of the basis quaternions, and each of ``\alpha'``, +``\beta'``, and ``\gamma'`` is a function of ``\alpha``, ``\beta``, ``\gamma``, and +``\theta``. Then, we note that the chain rule tells us that +```math +\frac{\partial}{\partial \theta} += +\frac{\partial \alpha'}{\partial \theta} \frac{\partial}{\partial \alpha'} ++ \frac{\partial \beta'}{\partial \theta} \frac{\partial}{\partial \beta'} ++ \frac{\partial \gamma'}{\partial \theta} \frac{\partial}{\partial \gamma'}, +``` +which we will use to convert the general expression for the angular-momentum operators in +terms of ``\partial_\theta`` into an expression in terms of derivatives with respect to +these new Euler angles. + +```math +\begin{align} + L_j f(\mathbf{R}_{\alpha, \beta, \gamma}) + &= + \left. i \frac{\partial} {\partial \theta} f \left( e^{-\theta \mathbf{e}_j / 2} + \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} + \\ + &= + i \left[ \left( + \frac{\partial \alpha'}{\partial \theta} \frac{\partial}{\partial \alpha'} + + \frac{\partial \beta'}{\partial \theta} \frac{\partial}{\partial \beta'} + + \frac{\partial \gamma'}{\partial \theta} \frac{\partial}{\partial \gamma'} + \right) f \left(\alpha', \beta', \gamma'\right) \right]_{\theta=0} + \\ + &= + i \left[ \left( + \frac{\partial \alpha'}{\partial \theta} \frac{\partial}{\partial \alpha} + + \frac{\partial \beta'}{\partial \theta} \frac{\partial}{\partial \beta} + + \frac{\partial \gamma'}{\partial \theta} \frac{\partial}{\partial \gamma} + \right) f \left(\alpha, \beta, \gamma\right) \right]_{\theta=0}, +\end{align} +``` +and similarly for ``R_j``. + +So the objective is to find the new Euler angles, differentiate with respect to +``\theta``, and then evaluate at ``\theta = 0``. We do this by first expressing +``\mathbf{R}_{\alpha, \beta, \gamma}`` in terms of its quaternion components, then +multiplying by ``e^{-\theta \mathbf{u} / 2}`` and expanding. We then find the new Euler +angles in terms of the components of the resulting quaternion according to the usual +expression. + +""" + +#src # Do this first just to hide stdout of the conda installation step +# ````@setup euler_angular_momentum +# import SymPyPythonCall +# ```` + + +# We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. +using LaTeXStrings import SymPyPythonCall -import SymPyPythonCall: sympy -import SymPyPythonCall: symbols, sqrt, exp, sin, cos, acos, atan, Matrix, simplify +import SymPyPythonCall: sympy, symbols, sqrt, sin, cos, tan, acos, atan, latex +const expand_trig = sympy.expand_trig const Derivative = sympy.Derivative -const Quaternion = sympy.Quaternion +const Quaternion = sympy.Quaternion const π = sympy.pi +nothing #hide -# Define the spherical coordinates +# Define coordinates we will use α, β, γ, θ = symbols("α β γ θ", real=true, positive=true) uw, ux, uy, uz = symbols("u_w u_x u_y u_z", real=true) +nothing #hide -# Define our basis quaternions +# Define the basis quaternions i = Quaternion(0, 1, 0, 0) j = Quaternion(0, 0, 1, 0) k = Quaternion(0, 0, 0, 1) - -# And an arbitrary vector quaternion -u = Quaternion(0, ux, uy, uz) +nothing #hide # Check that multiplication agrees with our conventions @assert i*j == k @assert j*k == i @assert k*i == j @assert i*j*k == Quaternion(-1, 0, 0, 0) +nothing #hide - -# -function L(u) - e = cos(θ/2) + u * sin(θ/2) - R = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() - eR = (e * R).expand().simplify() - w, x, y, z = eR.to_Matrix().transpose().tolist()[1] - αp = (atan(z/w) + atan(-x/y)).expand().simplify() - βp = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() - γp = (atan(z/w) - atan(-x/y)).expand().simplify() - return ( - Derivative(αp, θ).doit().subs(θ, 0).expand().simplify(), - Derivative(βp, θ).doit().subs(θ, 0).expand().simplify(), - Derivative(γp, θ).doit().subs(θ, 0).expand().simplify() - ) -end - -function R(u) - e = cos(θ/2) + u * sin(θ/2) - R1 = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() - Re = (R1 * e).expand().simplify() - w, x, y, z = Re.to_Matrix().transpose().tolist()[1] - αp = (atan(z/w) + atan(-x/y)).expand().simplify() - βp = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() - γp = (atan(z/w) - atan(-x/y)).expand().simplify() - return ( - Derivative(αp, θ).doit().subs(θ, 0).expand().simplify(), - Derivative(βp, θ).doit().subs(θ, 0).expand().simplify(), - Derivative(γp, θ).doit().subs(θ, 0).expand().simplify() +# Next, we define functions to compute the Euler components of the left and right operators +function 𝒪(u, side) + subs = Dict( # Substitutions that sympy doesn't make but we want + cos(β)/sin(β) => 1/tan(β), + sqrt(1 - cos(β))*sqrt(cos(β) + 1) => sin(β) ) + e = cos(θ/2) + u * sin(-θ/2) + R₀ = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() + w, x, y, z = ( + side == :left ? e * R₀ : R₀ * e + ).expand().simplify().to_Matrix().transpose().tolist()[1] + α′ = (atan(z/w) + atan(-x/y)).expand().simplify() + β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() + γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() + ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) + ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) + ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) + return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ end +L(u) = 𝒪(u, :left) +R(u) = 𝒪(u, :right) + +# function L(u) +# e = cos(θ/2) + u * sin(-θ/2) +# R₀ = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() +# eR = (e * R₀).expand().simplify() +# w, x, y, z = eR.to_Matrix().transpose().tolist()[1] +# α′ = (atan(z/w) + atan(-x/y)).expand().simplify() +# β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() +# γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() +# ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) +# ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) +# ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) +# return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ +# end + +# function R(u) +# e = cos(θ/2) + u * sin(-θ/2) +# R1 = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() +# Re = (R1 * e).expand().simplify() +# w, x, y, z = Re.to_Matrix().transpose().tolist()[1] +# α′ = (atan(z/w) + atan(-x/y)).expand().simplify() +# β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() +# γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() +# ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) +# ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) +# ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) +# return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ +# end + +nothing #hide + + +# Now we can compute the Euler components of the angular momentum operators for the three +# generators and both choices of left and right operators. +L_x = L(i) +L""" +L_x = i\left[ + %$(latex(L_x[1])) \frac{\partial}{\partial \alpha} + + %$(latex(L_x[2])) \frac{\partial}{\partial \beta} + + %$(latex(L_x[3])) \frac{\partial}{\partial \gamma} +\right] +""" # -L(i) - -# -L(j) - -# -L(k) +L_y = L(j) +L""" +L_y = i\left[ + %$(latex(L_y[1])) \frac{\partial}{\partial \alpha} + + %$(latex(L_y[2])) \frac{\partial}{\partial \beta} + + %$(latex(L_y[3])) \frac{\partial}{\partial \gamma} +\right] +""" # -R(i) +L_z = L(k) +L""" +L_z = i\left[ + %$(latex(L_z[1])) \frac{\partial}{\partial \alpha} + + %$(latex(L_z[2])) \frac{\partial}{\partial \beta} + + %$(latex(L_z[3])) \frac{\partial}{\partial \gamma} +\right] +""" # -R(j) +R_x = R(i) +L""" +R_x = i\left[ + %$(latex(R_x[1])) \frac{\partial}{\partial \alpha} + + %$(latex(R_x[2])) \frac{\partial}{\partial \beta} + + %$(latex(R_x[3])) \frac{\partial}{\partial \gamma} +\right] +""" # -R(k) +R_y = R(j) +L""" +R_y = i\left[ + %$(latex(R_y[1])) \frac{\partial}{\partial \alpha} + + %$(latex(R_y[2])) \frac{\partial}{\partial \beta} + + %$(latex(R_y[3])) \frac{\partial}{\partial \gamma} +\right] +""" # +R_z = R(k) +L""" +R_z = i\left[ + %$(latex(R_z[1])) \frac{\partial}{\partial \alpha} + + %$(latex(R_z[2])) \frac{\partial}{\partial \beta} + + %$(latex(R_z[3])) \frac{\partial}{\partial \gamma} +\right] +""" From eddcb8d98946b09ce3a0d8a61f5f460e3afd59f9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:52:19 -0500 Subject: [PATCH 046/329] Clear out older code --- docs/literate_input/euler_angular_momentum.jl | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 778a5b8f..de64b788 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -115,34 +115,6 @@ end L(u) = 𝒪(u, :left) R(u) = 𝒪(u, :right) -# function L(u) -# e = cos(θ/2) + u * sin(-θ/2) -# R₀ = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() -# eR = (e * R₀).expand().simplify() -# w, x, y, z = eR.to_Matrix().transpose().tolist()[1] -# α′ = (atan(z/w) + atan(-x/y)).expand().simplify() -# β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() -# γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() -# ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) -# ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) -# ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) -# return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ -# end - -# function R(u) -# e = cos(θ/2) + u * sin(-θ/2) -# R1 = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() -# Re = (R1 * e).expand().simplify() -# w, x, y, z = Re.to_Matrix().transpose().tolist()[1] -# α′ = (atan(z/w) + atan(-x/y)).expand().simplify() -# β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() -# γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() -# ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) -# ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) -# ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) -# return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ -# end - nothing #hide From f4eadccd41f10b7667c912ab3fb12cf5a7e40069 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 14:25:25 -0500 Subject: [PATCH 047/329] Make Conventions/Calculations subgroup --- docs/make.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index c27cc19d..dddc5ebd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -62,7 +62,9 @@ makedocs( "Conventions" => [ "conventions/conventions.md", "conventions/comparisons.md", - joinpath(relative_literate_output, "euler_angular_momentum.md"), + "Calculations" => [ + joinpath(relative_literate_output, "euler_angular_momentum.md"), + ], ], "Notes" => map( s -> "notes/$(s)", From 9570fe26da370dd8379e9e4d11c3206c9750a226 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 21:43:26 -0500 Subject: [PATCH 048/329] Use Quaternionic; streamline display of results; explain R_j on 2-sphere --- docs/literate_input/euler_angular_momentum.jl | 221 +++++++++++------- 1 file changed, 135 insertions(+), 86 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index de64b788..8a2b70dc 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -1,7 +1,8 @@ md""" -# ``L_j`` in Euler angles -Here, we will use SymPy to just grind through the algebra of expressing the -angular-momentum operators in terms of Euler angles. +# ``L_j`` and ``R_j`` in Euler angles +## Analytical groundwork +Here, we will use SymPy to just grind through the algebra of expressing the angular-momentum +operators in terms of Euler angles. The plan starts by defining a new set of Euler angles according to ```math @@ -49,12 +50,11 @@ these new Euler angles. ``` and similarly for ``R_j``. -So the objective is to find the new Euler angles, differentiate with respect to -``\theta``, and then evaluate at ``\theta = 0``. We do this by first expressing -``\mathbf{R}_{\alpha, \beta, \gamma}`` in terms of its quaternion components, then -multiplying by ``e^{-\theta \mathbf{u} / 2}`` and expanding. We then find the new Euler -angles in terms of the components of the resulting quaternion according to the usual -expression. +So the objective is to find the new Euler angles, differentiate with respect to ``\theta``, +and then evaluate at ``\theta = 0``. We do this by first multiplying ``\mathbf{R}_{\alpha, +\beta, \gamma}`` and ``e^{-\theta \mathbf{u} / 2}`` in the desired order, then expanding the +results in terms of its quaternion components, and then computing the new Euler angles in +terms of those components according to the usual expression. """ @@ -63,118 +63,167 @@ expression. # import SymPyPythonCall # ```` - +# ## Computing infrastructure # We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. -using LaTeXStrings +import LaTeXStrings: @L_str, LaTeXString +import Quaternionic: Quaternionic, Quaternion, components import SymPyPythonCall import SymPyPythonCall: sympy, symbols, sqrt, sin, cos, tan, acos, atan, latex const expand_trig = sympy.expand_trig const Derivative = sympy.Derivative -const Quaternion = sympy.Quaternion const π = sympy.pi nothing #hide # Define coordinates we will use -α, β, γ, θ = symbols("α β γ θ", real=true, positive=true) -uw, ux, uy, uz = symbols("u_w u_x u_y u_z", real=true) -nothing #hide - -# Define the basis quaternions -i = Quaternion(0, 1, 0, 0) -j = Quaternion(0, 0, 1, 0) -k = Quaternion(0, 0, 0, 1) +α, β, γ, θ, ϕ = symbols("α β γ θ ϕ", real=true, positive=true) nothing #hide -# Check that multiplication agrees with our conventions -@assert i*j == k -@assert j*k == i -@assert k*i == j -@assert i*j*k == Quaternion(-1, 0, 0, 0) +# Reinterpret the quaternion basis elements for compatibility with SymPy. (`Quaternionic` +# defines the basis with `Bool` components, but SymPy can't handle that.) +const 𝐢 = Quaternion{Int}(Quaternionic.𝐢) +const 𝐣 = Quaternion{Int}(Quaternionic.𝐣) +const 𝐤 = Quaternion{Int}(Quaternionic.𝐤) nothing #hide # Next, we define functions to compute the Euler components of the left and right operators function 𝒪(u, side) - subs = Dict( # Substitutions that sympy doesn't make but we want + ## Substitutions that sympy doesn't make but we want + subs = Dict( cos(β)/sin(β) => 1/tan(β), sqrt(1 - cos(β))*sqrt(cos(β) + 1) => sin(β) ) + + ## Define the essential quaternions e = cos(θ/2) + u * sin(-θ/2) - R₀ = ((cos(α/2) + k * sin(α/2)) * (cos(β/2) + j * sin(β/2)) * (cos(γ/2) + k * sin(γ/2))).expand().simplify() - w, x, y, z = ( + R₀ = Quaternion(sympy.simplify.(sympy.expand.(components( + (cos(α/2) + 𝐤 * sin(α/2)) * (cos(β/2) + 𝐣 * sin(β/2)) * (cos(γ/2) + 𝐤 * sin(γ/2)) + )))) + + ## Extract the (simplified) components of the product + w, x, y, z = sympy.simplify.(sympy.expand.(components( side == :left ? e * R₀ : R₀ * e - ).expand().simplify().to_Matrix().transpose().tolist()[1] + ))) + + ## Convert back to Euler angles α′ = (atan(z/w) + atan(-x/y)).expand().simplify() β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() + + ## Differentiate with respect to θ, set θ to 0, and simplify ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) + return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ end L(u) = 𝒪(u, :left) R(u) = 𝒪(u, :right) - nothing #hide -# Now we can compute the Euler components of the angular momentum operators for the three -# generators and both choices of left and right operators. -L_x = L(i) -L""" -L_x = i\left[ - %$(latex(L_x[1])) \frac{\partial}{\partial \alpha} - + %$(latex(L_x[2])) \frac{\partial}{\partial \beta} - + %$(latex(L_x[3])) \frac{\partial}{\partial \gamma} -\right] -""" - -# -L_y = L(j) -L""" -L_y = i\left[ - %$(latex(L_y[1])) \frac{\partial}{\partial \alpha} - + %$(latex(L_y[2])) \frac{\partial}{\partial \beta} - + %$(latex(L_y[3])) \frac{\partial}{\partial \gamma} -\right] -""" - -# -L_z = L(k) -L""" -L_z = i\left[ - %$(latex(L_z[1])) \frac{\partial}{\partial \alpha} - + %$(latex(L_z[2])) \frac{\partial}{\partial \beta} - + %$(latex(L_z[3])) \frac{\partial}{\partial \gamma} -\right] -""" +# We need a quick helper macro to format the results. +macro display(expr) + op = string(expr.args[1]) + arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z")[expr.args[2]] + quote + ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ = latex.($expr) # Call expr; format results as LaTeX + expr = $op * "_" * $arg # Standard form of the operator + L"""%$expr = i\left[ + %$(∂α′∂θ) \frac{\partial}{\partial \alpha} + + %$(∂β′∂θ) \frac{\partial}{\partial \beta} + + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + \right]""" # Display the result in LaTeX form + end +end +nothing #hide -# -R_x = R(i) -L""" -R_x = i\left[ - %$(latex(R_x[1])) \frac{\partial}{\partial \alpha} - + %$(latex(R_x[2])) \frac{\partial}{\partial \beta} - + %$(latex(R_x[3])) \frac{\partial}{\partial \gamma} -\right] -""" +# And we'll need another for the angular-momentum operators in standard ``S^2`` form. +conversion(∂) = latex(∂.subs(Dict(α => ϕ, β => θ, γ => 0)).simplify()) +macro display2(expr) + op = string(expr.args[1]) + arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z")[expr.args[2]] + if op == "L" + quote + ∂φ′∂θ, ∂ϑ′∂θ, ∂γ′∂θ = $conversion.($expr) # Call expr; format results as LaTeX + expr = $op * "_" * $arg # Standard form of the operator + L"""%$expr = i\left[ + %$(∂ϑ′∂θ) \frac{\partial}{\partial \theta} + + %$(∂φ′∂θ) \frac{\partial}{\partial \phi} + \right]""" # Display the result in LaTeX form + end + else + quote + ∂φ′∂θ, ∂ϑ′∂θ, ∂γ′∂θ = $conversion.($expr) # Call expr; format results as LaTeX + expr = $op * "_" * $arg # Standard form of the operator + L"""%$expr = i\left[ + %$(∂ϑ′∂θ) \frac{\partial}{\partial \theta} + + %$(∂φ′∂θ) \frac{\partial}{\partial \phi} + + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + \right]""" # Display the result in LaTeX form + end + end +end +nothing #hide -# -R_y = R(j) -L""" -R_y = i\left[ - %$(latex(R_y[1])) \frac{\partial}{\partial \alpha} - + %$(latex(R_y[2])) \frac{\partial}{\partial \beta} - + %$(latex(R_y[3])) \frac{\partial}{\partial \gamma} -\right] -""" +# ## Full expressions on ``S^3`` +# Finally, we can actually compute the Euler components of the angular momentum operators. +@display L(𝐢) +#- +@display L(𝐣) +#- +@display L(𝐤) +#- +@display R(𝐢) +#- +@display R(𝐣) +#- +@display R(𝐤) + +# In their description of the Wigner 𝔇 functions as wave functions of a rigid symmetric +# top, [Varshalovich_1988](@citet) provide equivalent expressions in Eqs. (6) and (7) of +# their Sec. 4.2. + + +# ## Standard expressions on ``S^2`` +# We can substitute ``(α, β, γ) \to (φ, θ, 0)`` to get the standard expressions for the +# angular momentum operators on the 2-sphere. +@display2 L(𝐢) +#- +@display2 L(𝐣) +#- +@display2 L(𝐤) + +# Those are indeed the standard expressions for the angular-momentum operators on the +# 2-sphere, so we can declare success! # -R_z = R(k) -L""" -R_z = i\left[ - %$(latex(R_z[1])) \frac{\partial}{\partial \alpha} - + %$(latex(R_z[2])) \frac{\partial}{\partial \beta} - + %$(latex(R_z[3])) \frac{\partial}{\partial \gamma} -\right] -""" +# Now, note that including ``\partial_\gamma`` for an expression on the 2-sphere doesn't +# actually make any sense. However, for historical reasons, we include it here when showing +# the results of the ``R`` operator in Euler angles. These operators are really only +# relevant for spin-weighted spherical harmonics. This nonsensicality signals the fact that +# it doesn't actually make sense to define spin-weighted spherical functions on the +# 2-sphere; they really only make sense on the 3-sphere. Nonetheless, if we stipulate that +# the function in question has a specific spin weight, that means that it is an +# eigenfunction of ``-i\partial_\gamma`` on the 3-sphere, so we could just substitute the +# eigenvalue ``s`` for that derivative in the expression below, and recover the standard +# spin-weight operators. + +@display2 R(𝐢) +#- +@display2 R(𝐣) +#- +@display2 R(𝐤) + +# This last operator shows us just how little sense it makes to try to define spin-weighted +# spherical functions on the 2-sphere. The spin eigenvalue ``s`` has to come out of +# nowhere, like some sort of deus ex machina. Nonetheless, we can see that if we substitute +# the eigenvalue, we get +# ```math +# R_x \eta +# = i\left[ \frac{\partial}{\partial \phi} - \frac{s}{\tan \theta} \right] \eta +# = -(\sin \theta)^s \left\{\frac{\partial}{\partial \theta} \right\} +# \left\{ (\sin \theta)^{-s} \eta \right\}. +# ``` +# And in the latter form, we can see that ``R_x - i R_y`` is exactly the spin-raising +# operator ``\eth`` as originally defined by [Newman_1966](@citet) in their Eq. (3.8). From 459939452eea2e1d4c14d9f9f4e0b9106501ef62 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 20 Jan 2025 13:33:13 -0500 Subject: [PATCH 049/329] Write up comparison of J/L/R with Varshalovich --- docs/src/conventions/comparisons.md | 242 ++++++++++++++++++++++++++++ docs/src/conventions/conventions.md | 2 +- 2 files changed, 243 insertions(+), 1 deletion(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 52511f50..3a1b4684 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -51,3 +51,245 @@ the same exact expression for the (scalar) spherical harmonics. ## NINJA ## LALSuite + +## Varshalovich et al. + +Page 155 has a table of values for ``\ell \leq 5`` + +[Varshalovich_1988](@citet) distinguish in Sec. 1.1.3 between +*covariant* and *contravariant* spherical coordinates and the +corresponding basis vectors, which they define as +```math +\begin{align} + \mathbf{e}_{+1} &= - \frac{1}{\sqrt{2}} \left( \mathbf{e}_x + i \mathbf{e}_y\right) + &&& + \mathbf{e}^{+1} &= - \frac{1}{\sqrt{2}} \left( \mathbf{e}_x - i \mathbf{e}_y\right) \\ + \mathbf{e}_{0} &= \mathbf{e}_z &&& \mathbf{e}^{0} &= \mathbf{e}_z \\ + \mathbf{e}_{-1} &= \frac{1}{\sqrt{2}} \left( \mathbf{e}_x - i \mathbf{e}_y\right) + &&& + \mathbf{e}^{-1} &= \frac{1}{\sqrt{2}} \left( \mathbf{e}_x + i \mathbf{e}_y\right). +\end{align} +``` +Then, in Sec. 4.2 they define ``\hat{\mathbf{J}}`` as the operator of +angular momentum of the rigid symmetric top. They then give in Eq. +(6) the "covariant spherical coordinates of ``\hat{\mathbf{J}}`` in the +non-rotating (lab-fixed) system" as +```math +\begin{gather} + \hat{J}_{\pm 1} = \frac{i}{\sqrt{2}} e^{\pm i \alpha} \left[ + \mp \cot\beta \frac{\partial}{\partial \alpha} + + i \frac{\partial}{\partial \beta} + \pm \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + \right] \\ + \hat{J}_0 = - i \frac{\partial}{\partial \alpha}, +\end{gather} +``` +and "contravariant components of ``\hat{\mathbf{J}}`` in the rotating +(body-fixed) system" as +```math +\begin{gather} + \hat{J}'^{\pm 1} = \frac{i}{\sqrt{2}} e^{\mp i \gamma} \left[ + \pm \cot\beta \frac{\partial}{\partial \gamma} + + i \frac{\partial}{\partial \beta} + \mp \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + \right] \\ + \hat{J}'^0 = - i \frac{\partial}{\partial \gamma}. +\end{gather} +``` +(Note the prime in the last two equations.) We can expand these in +Cartesian components to compare to our expressions. First the +covariant components: +```math +\begin{align} + \hat{J}_{x} + &= -\frac{1}{\sqrt{2}} \left( \hat{J}_{+1} - \hat{J}_{-1} \right) \\ + % &= -\frac{1}{\sqrt{2}} \left( + % \frac{i}{\sqrt{2}} e^{i \alpha} \left[ + % - \cot\beta \frac{\partial}{\partial \alpha} + % + i \frac{\partial}{\partial \beta} + % + \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \right] + % - + % \frac{i}{\sqrt{2}} e^{-i \alpha} \left[ + % + \cot\beta \frac{\partial}{\partial \alpha} + % + i \frac{\partial}{\partial \beta} + % - \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \right] + % \right) \\ + &= i\left[ + \frac{\cos\alpha}{\tan\beta} \frac{\partial}{\partial \alpha} + + \sin\alpha \frac{\partial}{\partial \beta} + - \frac{\cos\alpha}{\sin\beta} \frac{\partial}{\partial \gamma} + \right] \\ + \hat{J}_{y} + &= -\frac{1}{i\sqrt{2}} \left( \hat{J}_{+1} + \hat{J}_{-1} \right) \\ + % &= -\frac{1}{i\sqrt{2}} \left( + % \frac{i}{\sqrt{2}} e^{i \alpha} \left[ + % - \cot\beta \frac{\partial}{\partial \alpha} + % + i \frac{\partial}{\partial \beta} + % + \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \right] + % + + % \frac{i}{\sqrt{2}} e^{-i \alpha} \left[ + % + \cot\beta \frac{\partial}{\partial \alpha} + % + i \frac{\partial}{\partial \beta} + % - \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \right] + % \right) \\ + &= i \left[ + \frac{\sin\alpha}{\tan\beta} \frac{\partial}{\partial \alpha} + - \cos\alpha \frac{\partial}{\partial \beta} + - \frac{\sin\alpha}{\sin\beta} \frac{\partial}{\partial \gamma} + \right] \\ + \hat{J}_{z} + &= \hat{J}_{0} \\ + &= -i \frac{\partial}{\partial \alpha} +\end{align} +``` +We can compare these to the [Full expressions on ``S^3``](@ref), and find +that they are precisely equivalent to expressions for ``L_j`` computed in +this package's conventions. + +Next, the contravariant components: +```math +\begin{align} + \hat{J}'_{x} + &= -\frac{1}{\sqrt{2}} \left( \hat{J}'^{+1} - \hat{J}'^{-1} \right) \\ + % &= -\frac{1}{\sqrt{2}} \left( + % \frac{i}{\sqrt{2}} e^{- i \gamma} \left[ + % + \cot\beta \frac{\partial}{\partial \gamma} + % + i \frac{\partial}{\partial \beta} + % - \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \right] + % - + % \frac{i}{\sqrt{2}} e^{+ i \gamma} \left[ + % - \cot\beta \frac{\partial}{\partial \gamma} + % + i \frac{\partial}{\partial \beta} + % + \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \right] + % \right) \\ + &= -i \left( + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) \\ + \hat{J}'_{y} + &= \frac{1}{i\sqrt{2}} \left( \hat{J}'^{+1} + \hat{J}'^{-1} \right) \\ + % &= \frac{1}{i\sqrt{2}} \left( + % \frac{i}{\sqrt{2}} e^{-i \gamma} \left[ + % + \cot\beta \frac{\partial}{\partial \gamma} + % + i \frac{\partial}{\partial \beta} + % - \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \right] + % + + % \frac{i}{\sqrt{2}} e^{+ i \gamma} \left[ + % - \cot\beta \frac{\partial}{\partial \gamma} + % + i \frac{\partial}{\partial \beta} + % + \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \right] + % \right) \\ + &= -i \left( + \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \cos\gamma \frac{\partial}{\partial \beta} + - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) \\ + \hat{J}'_{z} + &= \hat{J}'^{0} \\ + &= -i \frac{\partial}{\partial \gamma} +\end{align} +``` +Unfortunately, while ``\hat{J}'^{x} = R_x`` and ``\hat{J}'^{z} = +R_z``, we have ``\hat{J}'^{y} = -R_y`` with an unexplained relative +minus sign. + +Just to check that we have the right expression, let's check +``[\hat{J}'_{x}, \hat{J}'_{y}] = i \hat{J}'_{z}`` (Eq. 12): +```math +\begin{align} + [\hat{J}'_{x}, \hat{J}'_{y}] + &= \left[ + -i \left( + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right), + -i \left( + \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \cos\gamma \frac{\partial}{\partial \beta} + - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + \right] \\ + &= -\left[ + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + \left( + \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \cos\gamma \frac{\partial}{\partial \beta} + - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + + \sin\gamma \frac{\partial}{\partial \beta} \left( + \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \cos\gamma \frac{\partial}{\partial \beta} + - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + - + \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} \left( + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + + \cos\gamma \frac{\partial}{\partial \beta} \left( + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + \right] \\ + &= -\left[ + \frac{\cos\gamma}{\tan\beta} + \left( + \frac{\partial}{\partial \gamma}\frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \frac{\partial}{\partial \gamma}\cos\gamma \frac{\partial}{\partial \beta} + - \frac{\partial}{\partial \gamma}\frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + + \sin\gamma \left( + \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \cos\gamma \frac{\partial}{\partial \beta} + - \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + - + \frac{\sin\gamma}{\tan\beta} \left( + \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \frac{\partial}{\partial \gamma} \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + + \cos\gamma \left( + \frac{\partial}{\partial \beta}\frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\partial}{\partial \beta} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + \right] \\ + &= -\left[ + \frac{\cos\gamma}{\tan\beta} + \left( + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + + \sin\gamma \left( + \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + - + \frac{\sin\gamma}{\tan\beta} \left( + \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \frac{\partial}{\partial \gamma} \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + + \cos\gamma \left( + \frac{\partial}{\partial \beta}\frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\partial}{\partial \beta} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) + \right] \\ +\end{align} +``` + diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index e6621bde..0b6e28d3 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -1,4 +1,4 @@ -# Conventions +# Development In the following subsections, we work through all the conventions used in this package, starting from first principles to motivate the From b63cda2341ecdd1ed5d2a560f7c8590927f49a39 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 20 Jan 2025 13:33:23 -0500 Subject: [PATCH 050/329] Wording tweaks --- docs/literate_input/euler_angular_momentum.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 8a2b70dc..0d97c583 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -1,10 +1,10 @@ md""" -# ``L_j`` and ``R_j`` in Euler angles +# ``L_j`` and ``R_j`` with Euler angles ## Analytical groundwork Here, we will use SymPy to just grind through the algebra of expressing the angular-momentum operators in terms of Euler angles. -The plan starts by defining a new set of Euler angles according to +We start by defining a new set of Euler angles according to ```math \mathbf{R}_{\alpha', \beta', \gamma'} = e^{-\theta \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} @@ -24,8 +24,7 @@ where ``\mathbf{u}`` will be each of the basis quaternions, and each of ``\alpha ``` which we will use to convert the general expression for the angular-momentum operators in terms of ``\partial_\theta`` into an expression in terms of derivatives with respect to -these new Euler angles. - +these new Euler angles: ```math \begin{align} L_j f(\mathbf{R}_{\alpha, \beta, \gamma}) @@ -63,7 +62,7 @@ terms of those components according to the usual expression. # import SymPyPythonCall # ```` -# ## Computing infrastructure +# ## Computational infrastructure # We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. import LaTeXStrings: @L_str, LaTeXString import Quaternionic: Quaternionic, Quaternion, components From edc710194aefa10da48b919fb3b7d98f872b7c11 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Feb 2025 21:12:00 -0500 Subject: [PATCH 051/329] Calculate commutators --- docs/literate_input/euler_angular_momentum.jl | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 0d97c583..8cdda82c 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -71,6 +71,7 @@ import SymPyPythonCall: sympy, symbols, sqrt, sin, cos, tan, acos, atan, latex const expand_trig = sympy.expand_trig const Derivative = sympy.Derivative const π = sympy.pi +const I = sympy.I nothing #hide # Define coordinates we will use @@ -184,6 +185,57 @@ nothing #hide # top, [Varshalovich_1988](@citet) provide equivalent expressions in Eqs. (6) and (7) of # their Sec. 4.2. +# ### Commutators + +# We can also compute the commutators of the angular momentum operators, as derived above. + +f = symbols("f", cls=SymPyPythonCall.sympy.o.Function) +function Lx(f, α, β, γ) + let L = L(𝐢) + I * ( + L[1] * f(α, β, γ).diff(α) + + L[2] * f(α, β, γ).diff(β) + + L[3] * f(α, β, γ).diff(γ) + ) + end +end +function Ly(f, α, β, γ) + let L = L(𝐣) + I * ( + L[1] * f(α, β, γ).diff(α) + + L[2] * f(α, β, γ).diff(β) + + L[3] * f(α, β, γ).diff(γ) + ) + end +end +( + Lx((α, β, γ)->Ly(f, α, β, γ), α, β, γ) + - Ly((α, β, γ)->Lx(f, α, β, γ), α, β, γ) +).expand().simplify() +#- +function Rx(f, α, β, γ) + let L = R(𝐢) + I * ( + L[1] * f(α, β, γ).diff(α) + + L[2] * f(α, β, γ).diff(β) + + L[3] * f(α, β, γ).diff(γ) + ) + end +end +function Ry(f, α, β, γ) + let L = R(𝐣) + I * ( + L[1] * f(α, β, γ).diff(α) + + L[2] * f(α, β, γ).diff(β) + + L[3] * f(α, β, γ).diff(γ) + ) + end +end +commutator = ( + Rx((α, β, γ)->Ry(f, α, β, γ), α, β, γ) + - Ry((α, β, γ)->Rx(f, α, β, γ), α, β, γ) +).expand().simplify() + # ## Standard expressions on ``S^2`` # We can substitute ``(α, β, γ) \to (φ, θ, 0)`` to get the standard expressions for the From 7c52c53fe74d55655835e10aa243ddada39737e9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Feb 2025 21:12:20 -0500 Subject: [PATCH 052/329] Minor touch-ups --- docs/src/conventions/conventions.md | 17 +++++++++++------ docs/src/conventions/outline.md | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 0b6e28d3..eb4d7b3a 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -754,10 +754,10 @@ R_{\mathfrak{g}} f(\mathbf{Q}) &= \rho \left. \frac{\partial} {\partial \theta} \end{aligned} ``` Here, we have introduced the constants ``\lambda`` and ``\rho`` -because we will actually be able to derive their — up to signs — based -on the requirement that raising and lowering operators exist for each. -Finally, we will choose the signs based on demands that these -operators correspond as naturally as possible to the standard +because we will actually be able to derive their values — up to signs +— based on the requirement that raising and lowering operators exist +for each. Finally, we will choose the signs based on demands that +these operators correspond as naturally as possible to the standard canonical angular-momentum operators. Note that when composing operators, it is critical to keep track of @@ -838,7 +838,9 @@ R_\pm &= R_\mathbf{x} \pm i R_\mathbf{y}. \end{aligned} ``` -[Show how this happens:] +* TODO: Impose ``R_z = s`` +* TODO: Impose Condon-Shortley condition (positive, real eigenvalues of ``R_\pm``) +* TODO: Show how the following happens Using these relations, we can actually solve for the constants ``\lambda`` and ``\rho`` up to a sign. We find that @@ -993,7 +995,10 @@ distinct eigenvalues are orthogonal, since (the last equality by Green's theorem). Since the eigenvalues are distinct, this can only be true if ``\int f_u f_v=0``. -[Show the relationship between the spherical Laplacian and the angular momentum operator.] +* TODO: Show the relationship between the spherical Laplacian and the + angular momentum operator. +* TODO: Show how ``D`` matrices are harmonic with respect to the + Laplacian on the 3-sphere. ## Representation theory / harmonic analysis diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index 70c1f6f8..66df9ae2 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -205,7 +205,7 @@ that requires the coefficients in L_{\pm} |\ell,m\rangle = \alpha^{\pm}_{\ell,m} |\ell, m \pm 1\rangle ``` to be real and positive. The reasoning behind this choice is -explained more clearly in Section 2 of [Ufford and Shortley +explained more fully in Section 2 of [Ufford and Shortley (1932)](@cite UffordShortley_1932). As a more practical matter, the Condon-Shortley phase describes signs chosen in the expression for spherical harmonics. The key expression is Eq. (15) of section 4³ From 27e994733547dfedd7feecc6a1a6b9ecdb4606a8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Feb 2025 21:14:22 -0500 Subject: [PATCH 053/329] Check Varshalovich conventions --- docs/src/conventions/comparisons.md | 72 +++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 3a1b4684..a29bb808 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -202,8 +202,52 @@ Unfortunately, while ``\hat{J}'^{x} = R_x`` and ``\hat{J}'^{z} = R_z``, we have ``\hat{J}'^{y} = -R_y`` with an unexplained relative minus sign. +```math +\begin{align} + \hat{J}'_{x} + &= -i \left( + \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \sin\gamma \frac{\partial}{\partial \beta} + - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) \\ + \hat{J}'_{y} + &= -i \left( + \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + - \cos\gamma \frac{\partial}{\partial \beta} + - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \right) +\end{align} +``` +We can write out these two operators acting on a function `f` in SymPy: +```python +from sympy import symbols, sin, cos, tan, diff, I +α, β, γ = symbols("α, β, γ", real=True) +f = symbols("f", cls=Function) + +def Jx(f, α, β, γ): + return -I * ( + cos(γ) / tan(β) * diff(f(α, β, γ), γ) + + sin(γ) * diff(f(α, β, γ), β) + - cos(γ) / sin(β) * diff(f(α, β, γ), α) + ) +def Jy(f, α, β, γ): + return -I * ( + sin(γ) / tan(β) * diff(f(α, β, γ), γ) + - cos(γ) * diff(f(α, β, γ), β) + - sin(γ) / sin(β) * diff(f(α, β, γ), α) + ) +( + Jx(lambda α, β, γ: Jy(f, α, β, γ), α, β, γ) + - Jy(lambda α, β, γ: Jx(f, α, β, γ), α, β, γ) +).expand().simplify() +``` + Just to check that we have the right expression, let's check -``[\hat{J}'_{x}, \hat{J}'_{y}] = i \hat{J}'_{z}`` (Eq. 12): +``[\hat{J}'_{x}, \hat{J}'_{y}] = i \hat{J}'_{z}`` (Eq. 12). We can +skip any contributions where a derivative on the left will act on a +derivative on the right, and ``\partial_\alpha`` on the left will have +no other effect, so we just have six terms subtracted from six terms. + ```math \begin{align} [\hat{J}'_{x}, \hat{J}'_{y}] @@ -275,21 +319,33 @@ Just to check that we have the right expression, let's check - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} \right) + \sin\gamma \left( - \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \frac{\sin\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} + - \frac{\sin\gamma\cos\beta}{-\sin^2\beta} \frac{\partial}{\partial \alpha} \right) - \frac{\sin\gamma}{\tan\beta} \left( - \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \frac{\partial}{\partial \gamma} \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \frac{-\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + + \cos\gamma \frac{\partial}{\partial \beta} + - \frac{-\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} \right) + \cos\gamma \left( - \frac{\partial}{\partial \beta}\frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} + \frac{\cos\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\partial}{\partial \beta} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + - \frac{\cos\gamma\cos\beta}{-\sin^2\beta} \frac{\partial}{\partial \alpha} \right) \right] \\ + &= -\left[ + \left( + + \sin\gamma\cos\gamma \frac{\partial}{\partial \beta} + - \frac{\cos^2\gamma\cos\beta}{\sin^2\beta} \frac{\partial}{\partial \alpha} + + \frac{-\sin^2\gamma\cos\beta}{\sin^2\beta} \frac{\partial}{\partial \alpha} + + \frac{\cos\beta}{\sin^2\beta} \frac{\partial}{\partial \alpha} + + \frac{\cos^2\gamma}{\tan^2\beta} \frac{\partial}{\partial \gamma} + + \frac{\sin^2\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} + - \frac{-\sin^2\gamma}{\tan^2\beta} \frac{\partial}{\partial \gamma} + + \frac{\cos^2\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} + \right) + \right] \end{align} ``` From 42e9dfea5894cda67184fc2ba64284608759f40e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Feb 2025 23:33:46 -0500 Subject: [PATCH 054/329] Add the basics about Newman-Penrose and NINJA --- docs/src/conventions/comparisons.md | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index a29bb808..ee3723b3 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -34,6 +34,34 @@ the same exact expression for the (scalar) spherical harmonics. ## Newman-Penrose +In their 1966 paper, [Newman_1966](@citet), Newman and Penrose first +introduced the spin-weighted spherical harmonics, ``{}_sY_{\ell m}``. +They use the standard (physicists') convention for spherical +coordinates and introduce the stereographic coordinate ``\zeta = +e^{i\phi} \cot\frac{\theta}{2}``. They define the spin-raising +operator ``\eth`` acting on a function of spin weight ``s`` as +```math +\eth \eta += +-\left(\sin\theta\right)^s +\left\{ + \frac{\partial}{\partial\theta} + + \frac{i}{\sin\theta} \frac{\partial}{\partial\phi} +\right\} \left\{\left(\sin\theta\right)^{-s} \eta\right\}, +``` +They then compute +```math +{}_sY_{\ell, m} +\propto +\frac{1}{\left[(\ell-s)! (\ell+s)!\right]^{1/2}} +\left(1 + \zeta \bar{\zeta}\right)^{-\ell} +\sum_p \zeta^p (-\bar{\zeta})^{p+s-m} +\binom{\ell-s}{p} \binom{\ell+s}{p+s-m}, +``` +where the sum is over all integers ``p`` such that the factorials are +nonzero. + + ## Goldberg ## Wikipedia @@ -50,6 +78,43 @@ the same exact expression for the (scalar) spherical harmonics. ## NINJA +Combining Eqs. (II.7) and (II.8) of [Ajith_2007](@citet), we have +```math +\begin{align} + {}_{-s}Y_{lm} + &= + (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + \sum_{k = k_1}^{k_2} + \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell+s)!(\ell-s)!]^{1/2}} + {(\ell+m-k)!(\ell-s-k)!k!(k+s-m)!} + \\ &\qquad \times + \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m-s-2k} + \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k+s-m} +\end{align} +``` +with ``k_1 = \textrm{max}(0, m-s)`` and ``k_2=\textrm{min}(\ell+m, +\ell-s)``. Note that most of the above was copied directly from the +TeX source of the paper, but the two equations were trivially combined +into one. Also note the annoying negative sign on the left-hand side. +That's so annoying that I'm going to duplicate the expression just to +get rid of it: +```math +\begin{align} + {}_{s}Y_{lm} + &= + (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + \sum_{k = k_1}^{k_2} + \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell-s)!(\ell+s)!]^{1/2}} + {(\ell+m-k)!(\ell+s-k)!k!(k-s-m)!} + \\ &\qquad \times + \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m+s-2k} + \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-s-m} +\end{align} +``` +where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, +\ell+s)``. + + ## LALSuite ## Varshalovich et al. From 9c6da7b38ad447c97e0fe50e9fb313245e1a9bbb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 16:49:33 -0500 Subject: [PATCH 055/329] Add a crucial bit on Goldberg et al.; tidy up some Varshalovich et al. stuff --- docs/src/conventions/comparisons.md | 178 +++++----------------------- 1 file changed, 31 insertions(+), 147 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index ee3723b3..56a7e4d2 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -64,6 +64,27 @@ nonzero. ## Goldberg +Eq. (3.11) of [GoldbergEtAl_1967](@citet) naturally extends to +```math + {}_sY_{\ell, m}(\theta, \phi, \gamma) + = + \left[ \left(2\ell+1\right) / 4\pi \right]^{1/2} + D^{\ell}_{-s,m}(\phi, \theta, \gamma), +``` +where Eq. (3.4) also shows that ``D^{\ell}_{m', m}(\alpha, \beta, +\gamma) = D^{\ell}_{m', m}(\alpha, \beta, 0) e^{i m' \gamma}``, +so we have +```math + {}_sY_{\ell, m}(\theta, \phi, \gamma) + = + {}_sY_{\ell, m}(\theta, \phi)\, e^{-i s \gamma}. +``` +This is the most natural extension of the standard spin-weighted +spherical harmonics to ``\mathrm{Spin}(3)``. In particular, the +spin-weight operator is ``i \partial_\gamma``, which suggests that it +will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i +\partial_\gamma``. + ## Wikipedia ## Mathematica @@ -267,150 +288,13 @@ Unfortunately, while ``\hat{J}'^{x} = R_x`` and ``\hat{J}'^{z} = R_z``, we have ``\hat{J}'^{y} = -R_y`` with an unexplained relative minus sign. -```math -\begin{align} - \hat{J}'_{x} - &= -i \left( - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) \\ - \hat{J}'_{y} - &= -i \left( - \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \cos\gamma \frac{\partial}{\partial \beta} - - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) -\end{align} -``` -We can write out these two operators acting on a function `f` in SymPy: -```python -from sympy import symbols, sin, cos, tan, diff, I -α, β, γ = symbols("α, β, γ", real=True) -f = symbols("f", cls=Function) - -def Jx(f, α, β, γ): - return -I * ( - cos(γ) / tan(β) * diff(f(α, β, γ), γ) - + sin(γ) * diff(f(α, β, γ), β) - - cos(γ) / sin(β) * diff(f(α, β, γ), α) - ) -def Jy(f, α, β, γ): - return -I * ( - sin(γ) / tan(β) * diff(f(α, β, γ), γ) - - cos(γ) * diff(f(α, β, γ), β) - - sin(γ) / sin(β) * diff(f(α, β, γ), α) - ) -( - Jx(lambda α, β, γ: Jy(f, α, β, γ), α, β, γ) - - Jy(lambda α, β, γ: Jx(f, α, β, γ), α, β, γ) -).expand().simplify() -``` - -Just to check that we have the right expression, let's check -``[\hat{J}'_{x}, \hat{J}'_{y}] = i \hat{J}'_{z}`` (Eq. 12). We can -skip any contributions where a derivative on the left will act on a -derivative on the right, and ``\partial_\alpha`` on the left will have -no other effect, so we just have six terms subtracted from six terms. - -```math -\begin{align} - [\hat{J}'_{x}, \hat{J}'_{y}] - &= \left[ - -i \left( - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right), - -i \left( - \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \cos\gamma \frac{\partial}{\partial \beta} - - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - \right] \\ - &= -\left[ - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - \left( - \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \cos\gamma \frac{\partial}{\partial \beta} - - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - + \sin\gamma \frac{\partial}{\partial \beta} \left( - \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \cos\gamma \frac{\partial}{\partial \beta} - - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - - - \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} \left( - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - + \cos\gamma \frac{\partial}{\partial \beta} \left( - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - \right] \\ - &= -\left[ - \frac{\cos\gamma}{\tan\beta} - \left( - \frac{\partial}{\partial \gamma}\frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \frac{\partial}{\partial \gamma}\cos\gamma \frac{\partial}{\partial \beta} - - \frac{\partial}{\partial \gamma}\frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - + \sin\gamma \left( - \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \cos\gamma \frac{\partial}{\partial \beta} - - \frac{\partial}{\partial \beta} \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - - - \frac{\sin\gamma}{\tan\beta} \left( - \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \frac{\partial}{\partial \gamma} \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\partial}{\partial \gamma} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - + \cos\gamma \left( - \frac{\partial}{\partial \beta}\frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\partial}{\partial \beta} \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - \right] \\ - &= -\left[ - \frac{\cos\gamma}{\tan\beta} - \left( - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - + \sin\gamma \left( - \frac{\sin\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} - - \frac{\sin\gamma\cos\beta}{-\sin^2\beta} \frac{\partial}{\partial \alpha} - \right) - - - \frac{\sin\gamma}{\tan\beta} \left( - \frac{-\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \cos\gamma \frac{\partial}{\partial \beta} - - \frac{-\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} - \right) - + \cos\gamma \left( - \frac{\cos\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma\cos\beta}{-\sin^2\beta} \frac{\partial}{\partial \alpha} - \right) - \right] \\ - &= -\left[ - \left( - + \sin\gamma\cos\gamma \frac{\partial}{\partial \beta} - - \frac{\cos^2\gamma\cos\beta}{\sin^2\beta} \frac{\partial}{\partial \alpha} - + \frac{-\sin^2\gamma\cos\beta}{\sin^2\beta} \frac{\partial}{\partial \alpha} - + \frac{\cos\beta}{\sin^2\beta} \frac{\partial}{\partial \alpha} - + \frac{\cos^2\gamma}{\tan^2\beta} \frac{\partial}{\partial \gamma} - + \frac{\sin^2\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} - - \frac{-\sin^2\gamma}{\tan^2\beta} \frac{\partial}{\partial \gamma} - + \frac{\cos^2\gamma}{-\sin^2\beta} \frac{\partial}{\partial \gamma} - \right) - \right] -\end{align} -``` - +It's very easy to check, for example, that ``[\hat{J}'^{z}, +\hat{J}'^{x}] = i \hat{J}'^{y}``, as expected from the general +expression in their Eq. (12). So these expressions are — at least — +consistent with the claims of Varshalovich et al. But my conclusion +based on defining the operators generally and respecting the order of +operations is that the expressions for the commutators of ``R`` and +``L`` really must have opposite signs, regardless of any overall +constants chosen in the definitions of the operators. So I conclude +that there must be some more fundamental difference between what I +have done and what Varshalovich et al. have done. From 8c110a02ab3345f309587d284ebee920dcc4580c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 16:50:21 -0500 Subject: [PATCH 056/329] Sort out the conventions for L and R, including spin weight and raising/lowering operators --- docs/literate_input/euler_angular_momentum.jl | 232 ++++++++++++------ 1 file changed, 161 insertions(+), 71 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 8cdda82c..4720fa1c 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -1,8 +1,40 @@ md""" # ``L_j`` and ``R_j`` with Euler angles -## Analytical groundwork + +This package defines the angular-momentum operators ``L_j`` and ``R_j`` in terms of elements +of the Lie group and algebra: +```math +L_𝐮 f(𝐑) = \left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} +f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) +\qquad \text{and} \qquad +R_𝐮 f(𝐑) = -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} +f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), +``` +This is certainly the natural realm for these operators, but it is not the common one. In +particular, virtually all textbooks and papers on the subject define these operators in +terms of the standard spherical coordinates on the 2-sphere, rather than quaternions or even +Euler angles. In particular, the standard forms are essentially always given in terms of +the Cartesian basis, as in ``L_x``, ``L_y``, and ``L_z`` — though some times the first two +are expressed as ``L_{\pm} = L_x \pm i L_y``. + Here, we will use SymPy to just grind through the algebra of expressing the angular-momentum -operators in terms of Euler angles. +operators in terms of Euler angles, including evaluating the commutators in that form, and +further reduce them to operators in terms of spherical coordinates. We will find a couple +important results that help make contact with more standard expressions: + + 1. Our results for ``L_x``, ``L_x``, and ``L_z`` in spherical coordinates agree with + standard expressions. + 2. The commutators obey ``[L_x, L_y] = i L_z`` and cyclic permutations, in agreement with + the standard expressions. + 3. We also find ``[R_x, R_y] = i R_z`` and cyclic permutations. + 4. We can explicitly compute ``[L_i, R_j] = 0``, as expected. + 5. Using the natural extension of Goldberg et al.'s SWSHs to include ``\gamma``, we can + see that the natural spin-weight operator is ``R_z = i \partial_\gamma``. Thus, we + define ``R_z = s`` for a function with spin weight ``s``. + 6. The spin-raising operator for ``R_z`` is ``\eth = R_x + i R_y``; the spin-lowering + operator is ``\bar{\eth} = R_x - i R_y``. + +## Analytical groundwork We start by defining a new set of Euler angles according to ```math @@ -117,23 +149,45 @@ function 𝒪(u, side) return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ end +## Note that we are not including the factor of ``i`` here; for simplicity, we will insert +## it manually when displaying the results below, and when applying these operators to +## functions (`Lx` and related definitions below). L(u) = 𝒪(u, :left) R(u) = 𝒪(u, :right) nothing #hide -# We need a quick helper macro to format the results. +# We need a couple quick helper macros to display the results. +#md # The details are boring, but you can expand the source code to see them. +#md # ```@raw html +#md #
+#md # +#md # Click here to expand the source code for display macros +#md # +#md # ``` macro display(expr) op = string(expr.args[1]) arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z")[expr.args[2]] - quote - ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ = latex.($expr) # Call expr; format results as LaTeX - expr = $op * "_" * $arg # Standard form of the operator - L"""%$expr = i\left[ - %$(∂α′∂θ) \frac{\partial}{\partial \alpha} - + %$(∂β′∂θ) \frac{\partial}{\partial \beta} - + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} - \right]""" # Display the result in LaTeX form + if op == "L" + quote + ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ = latex.($expr) # Call expr; format results as LaTeX + expr = $op * "_" * $arg # Standard form of the operator + L"""%$expr = i\left[ + %$(∂α′∂θ) \frac{\partial}{\partial \alpha} + + %$(∂β′∂θ) \frac{\partial}{\partial \beta} + + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + \right]""" # Display the result in LaTeX form + end + else + quote + ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ = latex.($expr) # Call expr; format results as LaTeX + expr = $op * "_" * $arg # Standard form of the operator + L"""%$expr = -i\left[ + %$(∂α′∂θ) \frac{\partial}{\partial \alpha} + + %$(∂β′∂θ) \frac{\partial}{\partial \beta} + + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + \right]""" # Display the result in LaTeX form + end end end nothing #hide @@ -156,7 +210,7 @@ macro display2(expr) quote ∂φ′∂θ, ∂ϑ′∂θ, ∂γ′∂θ = $conversion.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator - L"""%$expr = i\left[ + L"""%$expr = -i\left[ %$(∂ϑ′∂θ) \frac{\partial}{\partial \theta} + %$(∂φ′∂θ) \frac{\partial}{\partial \phi} + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} @@ -166,6 +220,9 @@ macro display2(expr) end nothing #hide +#md # ```@raw html +#md #
+#md # ``` # ## Full expressions on ``S^3`` # Finally, we can actually compute the Euler components of the angular momentum operators. @@ -188,53 +245,71 @@ nothing #hide # ### Commutators # We can also compute the commutators of the angular momentum operators, as derived above. - +# First, we define the operators acting on functions of the Euler angles. f = symbols("f", cls=SymPyPythonCall.sympy.o.Function) -function Lx(f, α, β, γ) - let L = L(𝐢) - I * ( - L[1] * f(α, β, γ).diff(α) - + L[2] * f(α, β, γ).diff(β) - + L[3] * f(α, β, γ).diff(γ) +function 𝒪(u, side, f, α, β, γ) + let O = 𝒪(u, side) + (side==:left ? I : -I) * ( + O[1] * f(α, β, γ).diff(α) + + O[2] * f(α, β, γ).diff(β) + + O[3] * f(α, β, γ).diff(γ) ) end end -function Ly(f, α, β, γ) - let L = L(𝐣) - I * ( - L[1] * f(α, β, γ).diff(α) - + L[2] * f(α, β, γ).diff(β) - + L[3] * f(α, β, γ).diff(γ) - ) - end + +Lx(f, α, β, γ) = 𝒪(𝐢, :left, f, α, β, γ) +Ly(f, α, β, γ) = 𝒪(𝐣, :left, f, α, β, γ) +Lz(f, α, β, γ) = 𝒪(𝐤, :left, f, α, β, γ) + +Rx(f, α, β, γ) = 𝒪(𝐢, :right, f, α, β, γ) +Ry(f, α, β, γ) = 𝒪(𝐣, :right, f, α, β, γ) +Rz(f, α, β, γ) = 𝒪(𝐤, :right, f, α, β, γ) + +nothing #hide + +# Now we define their commutator ``[O₁, O₂] = O₁O₂ - O₂O₁``: +function commutator(O₁, O₂) + ( + O₁((α, β, γ)->O₂(f, α, β, γ), α, β, γ) + - O₂((α, β, γ)->O₁(f, α, β, γ), α, β, γ) + ).expand().simplify() end -( - Lx((α, β, γ)->Ly(f, α, β, γ), α, β, γ) - - Ly((α, β, γ)->Lx(f, α, β, γ), α, β, γ) -).expand().simplify() + +nothing #hide + +# And finally, evaluate each in turn. We expect ``[L_x, L_y] = i L_z`` and cyclic +# permutations: +commutator(Lx, Ly) #- -function Rx(f, α, β, γ) - let L = R(𝐢) - I * ( - L[1] * f(α, β, γ).diff(α) - + L[2] * f(α, β, γ).diff(β) - + L[3] * f(α, β, γ).diff(γ) - ) - end -end -function Ry(f, α, β, γ) - let L = R(𝐣) - I * ( - L[1] * f(α, β, γ).diff(α) - + L[2] * f(α, β, γ).diff(β) - + L[3] * f(α, β, γ).diff(γ) - ) - end -end -commutator = ( - Rx((α, β, γ)->Ry(f, α, β, γ), α, β, γ) - - Ry((α, β, γ)->Rx(f, α, β, γ), α, β, γ) -).expand().simplify() +commutator(Ly, Lz) +#- +commutator(Lz, Lx) +# Similarly, we expect ``[R_x, R_y] = i R_z`` and cyclic permutations: +commutator(Rx, Ry) +#- +commutator(Ry, Rz) +#- +commutator(Rz, Rx) + +# Just for completeness, let's evaluate the commutators of the left and right operators, +# which should all be zero. +commutator(Lx, Rx) +#- +commutator(Lx, Ry) +#- +commutator(Lx, Rz) +#- +commutator(Ly, Rx) +#- +commutator(Ly, Ry) +#- +commutator(Ly, Rz) +#- +commutator(Lz, Rx) +#- +commutator(Lz, Ry) +#- +commutator(Lz, Rz) # ## Standard expressions on ``S^2`` @@ -250,15 +325,9 @@ commutator = ( # 2-sphere, so we can declare success! # # Now, note that including ``\partial_\gamma`` for an expression on the 2-sphere doesn't -# actually make any sense. However, for historical reasons, we include it here when showing -# the results of the ``R`` operator in Euler angles. These operators are really only -# relevant for spin-weighted spherical harmonics. This nonsensicality signals the fact that -# it doesn't actually make sense to define spin-weighted spherical functions on the -# 2-sphere; they really only make sense on the 3-sphere. Nonetheless, if we stipulate that -# the function in question has a specific spin weight, that means that it is an -# eigenfunction of ``-i\partial_\gamma`` on the 3-sphere, so we could just substitute the -# eigenvalue ``s`` for that derivative in the expression below, and recover the standard -# spin-weight operators. +# actually make any sense: ``\gamma`` isn't even a coordinate for the 2-sphere! However, +# for historical reasons, we include it here when showing the results of the ``R`` operator +# in Euler angles. @display2 R(𝐢) #- @@ -266,15 +335,36 @@ commutator = ( #- @display2 R(𝐤) -# This last operator shows us just how little sense it makes to try to define spin-weighted -# spherical functions on the 2-sphere. The spin eigenvalue ``s`` has to come out of -# nowhere, like some sort of deus ex machina. Nonetheless, we can see that if we substitute -# the eigenvalue, we get +# We get nonzero components of ``\partial_\gamma``, showing that these operators really *do +# not* make sense for the 2-sphere, and therefore that it doesn't actually make sense to +# define spin-weighted spherical functions on the 2-sphere; they really only make sense on +# the 3-sphere. Nonetheless, if we stipulate that the function ``\eta`` has a specific spin +# weight, that means that *on the 3-sphere* it is an eigenfunction with ``R_z\eta = +# i\partial_\gamma \eta = s\eta``. So we could just substitute ``-i s`` for +# ``\partial_\gamma`` in the expressions above, and recover the standard spin-weight +# operators. We get # ```math -# R_x \eta -# = i\left[ \frac{\partial}{\partial \phi} - \frac{s}{\tan \theta} \right] \eta -# = -(\sin \theta)^s \left\{\frac{\partial}{\partial \theta} \right\} +# \left[R_x + i R_y\right] \eta +# = \left[ +# -i \frac{1}{\sin\theta} \frac{\partial}{\partial \phi} +# + \frac{s}{\tan \theta} +# - \frac{\partial}{\partial \theta} +# \right] \eta +# = -(\sin \theta)^s \left\{ +# \frac{\partial}{\partial \theta} +# +i \frac{1}{\sin\theta} \frac{\partial}{\partial \phi} +# \right\} # \left\{ (\sin \theta)^{-s} \eta \right\}. # ``` -# And in the latter form, we can see that ``R_x - i R_y`` is exactly the spin-raising -# operator ``\eth`` as originally defined by [Newman_1966](@citet) in their Eq. (3.8). +# And in the latter form, we can see that ``R_x + i R_y`` is exactly the spin-raising +# operator ``\eth`` as originally defined by [Newman_1966](@citet) in their Eq. (3.8). The +# complex-conjugate of this operator is the spin-lowering operator ``\bar{\eth}`` for +# ``R_z``. *By definition* of raising and lowering operators, this means that +# ``[R_z, \eth] = \eth`` and ``[R_z, \bar{\eth}] = -\bar{\eth}``. We can verify these +# results by computing the commutators directly from the expressions above: +# ```math +# \begin{aligned} +# [R_z, \eth] &= [R_z, R_x] + i [R_z, R_y] = i R_y - i i R_x = R_x + i R_y = \eth, \\ +# [R_z, \bar{\eth}] &= [R_z, R_x] - i [R_z, R_y] = i R_y + i i R_x = -R_x + i R_y = -\bar{\eth}. +# \end{aligned} +# ``` From 807ec2061b4ca5d95a91d3a0491dc3466dbb81f0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 17:17:44 -0500 Subject: [PATCH 057/329] Update Varshalovich disagreement --- docs/src/conventions/comparisons.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 56a7e4d2..5cf529b0 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -284,17 +284,16 @@ Next, the contravariant components: &= -i \frac{\partial}{\partial \gamma} \end{align} ``` -Unfortunately, while ``\hat{J}'^{x} = R_x`` and ``\hat{J}'^{z} = -R_z``, we have ``\hat{J}'^{y} = -R_y`` with an unexplained relative -minus sign. +Unfortunately, while we have agreement on ``\hat{J}'^{y} = R_y``, we +also have disagreement on ``\hat{J}'^{x} = -R_x`` and ``\hat{J}'^{z} = +-R_z``, as they have relative minus signs. It's very easy to check, for example, that ``[\hat{J}'^{z}, \hat{J}'^{x}] = i \hat{J}'^{y}``, as expected from the general expression in their Eq. (12). So these expressions are — at least — -consistent with the claims of Varshalovich et al. But my conclusion -based on defining the operators generally and respecting the order of -operations is that the expressions for the commutators of ``R`` and -``L`` really must have opposite signs, regardless of any overall -constants chosen in the definitions of the operators. So I conclude -that there must be some more fundamental difference between what I -have done and what Varshalovich et al. have done. +consistent with the claims of Varshalovich et al. I wonder if there +is some subtlety involving the order of operations and passing to the +"body-fixed" frame. I'm confident that my definitions are internally +consistent, and fit in nicely with the spin-weighted function +literature; maybe Varshalovich et al. are just doing something +different. From ae960fb18413caa6dcd4f5a9848284e5da5e0883 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 22:26:32 -0500 Subject: [PATCH 058/329] Summarize new interpretation of spin-weight operators --- docs/literate_input/euler_angular_momentum.jl | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 4720fa1c..3c463676 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -359,12 +359,27 @@ commutator(Lz, Rz) # And in the latter form, we can see that ``R_x + i R_y`` is exactly the spin-raising # operator ``\eth`` as originally defined by [Newman_1966](@citet) in their Eq. (3.8). The # complex-conjugate of this operator is the spin-lowering operator ``\bar{\eth}`` for -# ``R_z``. *By definition* of raising and lowering operators, this means that -# ``[R_z, \eth] = \eth`` and ``[R_z, \bar{\eth}] = -\bar{\eth}``. We can verify these -# results by computing the commutators directly from the expressions above: +# ``R_z``. *By definition* of raising and lowering operators, this means that ``[R_z, \eth] +# = \eth`` and ``[R_z, \bar{\eth}] = -\bar{\eth}``. We can verify these results by +# computing the commutators directly from the expressions above: # ```math # \begin{aligned} -# [R_z, \eth] &= [R_z, R_x] + i [R_z, R_y] = i R_y - i i R_x = R_x + i R_y = \eth, \\ -# [R_z, \bar{\eth}] &= [R_z, R_x] - i [R_z, R_y] = i R_y + i i R_x = -R_x + i R_y = -\bar{\eth}. +# [R_z, \eth] +# &= [R_z, R_x] + i [R_z, R_y] = i R_y - i i R_x = R_x + i R_y = \eth, +# \\ +# [R_z, \bar{\eth}] +# &= [R_z, R_x] - i [R_z, R_y] = i R_y + i i R_x = -R_x + i R_y = -\bar{\eth}. # \end{aligned} # ``` +# +# So we see that we've reproduced precisely the standard expressions for the spin-weighted +# spherical functions (depicted as functions on the 2-sphere) from the expressions for the +# angular-momentum operators acting on general functions on the 3-sphere. The standard +# expressions appear arbitrarily, and are not even well defined as functions on the 2-sphere +# because they also need input from tangent space of the sphere — which is not part of the +# 2-sphere proper. On the other hand, the expressions from the 3-sphere are mathematically +# and physically well defined and intuitive. Note that the latter is complete in itself; it +# can stand alone without reference to the 2-sphere. Rather, what we have done here is just +# shown the connection to the inadequate standard presentation. But it is important to +# recognize that our complete treatment on ``\mathrm{Spin}(3)`` is the more fundamental one, +# and can be used without reference to the older treatment. From bdced963172cf46f5198db530e1381b9fa72ac28 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 07:37:57 -0500 Subject: [PATCH 059/329] Carefully examine the definition of spin in Newman-Penrose --- docs/src/conventions/comparisons.md | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 5cf529b0..e9d320e3 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -61,6 +61,57 @@ They then compute where the sum is over all integers ``p`` such that the factorials are nonzero. +They are a little ambiguous about the relationship of the complex +basis vector ``m^\mu`` to the coordinates. + +> The vectors ``\Re(m^\mu)`` and ``\Im(m^\mu)`` may be regarded as +> orthogonal tangent vectors (of length ``2^{-1/2}``) at each point of +> the surface. [...] If spherical polar coordinates are used, a +> natural choice for ``m^\mu`` is to make ``\Re(m^\mu)`` and +> ``\Im(m^\mu)`` tangential, respectively, to the curves ``\phi = +> \mathrm{const}`` and ``\theta = \mathrm{const}.`` + +The ambiguity is in the sign implied by "tangential", but the natural +choice is to assume they mean that the components are *positive* +multiples of ``\partial_\theta`` and ``\partial_\phi`` respectively, +in which case we have +```math +m^\mu = \frac{1}{\sqrt{2}} +\left[ \partial_\theta + i \csc\theta \partial_\phi \right]. +``` +They define the spin weight in terms of behavior of a quantity under +rotation of ``m^\mu`` in its own plane as +```math +(m^\mu)' += +e^{i\psi} m^\mu += +\frac{1}{\sqrt{2}} +\left[ + \left(\cos\psi\partial_\theta - \sin\psi\csc\theta \partial_\phi\right) + + i \left(\cos\psi\csc\theta \partial_\phi + \sin\psi\partial_\theta\right) +\right]. +``` +Raising the spherical coordinates ``(\theta, \phi)`` to Euler angles +``(\phi, \theta, -\psi)``, we see that the rotor ``R_{\phi, \theta, +-\psi}`` rotates the ``𝐳`` basis vector to the point ``(\theta, +\phi)``, and it rotates ``(𝐱 + i 𝐲) / \sqrt{2}`` onto ``(m^\mu)'``. +Under this rotation, a quantity ``\eta`` has spin weight ``s`` if it +transforms as +```math +\eta' = e^{i s \psi} \eta. +``` +Now, supposing that these quantities are functions of Euler angles, we +can write +```math +\eta(\phi, \theta, -\psi) = e^{i s \psi} \eta(\phi, \theta, 0), +``` +or +```math +\eta(\phi, \theta, \gamma) = e^{-i s \gamma} \eta(\phi, \theta, 0). +``` +Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. + ## Goldberg From 3ec83c77f8724a8c034073d50b6d0e15b3ee9026 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 07:39:45 -0500 Subject: [PATCH 060/329] Finalize angular-momentum conventions --- docs/src/conventions/conventions.md | 159 ++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 44 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index eb4d7b3a..6728ece5 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -53,61 +53,113 @@ Euler angles. L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) \qquad \text{and} \qquad - R_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} + R_𝐮 f(𝐑) = -\left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), ``` - where ``𝐮`` can be any pure-vector quaternion. In particular, - ``L`` represents the standard angular-momentum operators, and we - can compute the expressions in Euler angles for the basis vectors: + where ``𝐮`` can be any quaternion, though unit pure-vector + quaternions are the most common. In particular, ``L`` represents + the standard angular-momentum operators, and we can compute the + expressions in Euler angles for the basis vectors: ```math \begin{aligned} - L_x = L_𝐢 &= -i \left\{ - -\frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - - \sin\alpha \frac{\partial} {\partial \beta} - +\frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} - \right\} \\ - L_y = L_𝐣 &= -i \left\{ - -\frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - + \cos\alpha \frac{\partial} {\partial \beta} - +\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} - \right\} \\ - L_z = L_𝐤 &= -i \frac{\partial} {\partial \alpha} \\ - R_x = R_𝐢 &= -i \left\{ + L_𝐢 &= i \left\{ + \frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + + \sin\alpha \frac{\partial} {\partial \beta} + - \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \right\}, + & + R_𝐢 &= i \left\{ -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} +\sin\gamma \frac{\partial} {\partial \beta} +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} - \right\} \\ - R_y = R_𝐣 &= -i \left\{ + \right\}, + \\ + L_𝐣 &= i \left\{ + \frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + - \cos\alpha \frac{\partial} {\partial \beta} + -\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \right\}, + & + R_𝐣 &= i \left\{ \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} +\cos\gamma \frac{\partial} {\partial \beta} -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} - \right\} \\ - R_z = R_𝐤 &= -i \frac{\partial} {\partial \gamma} + \right\}, + \\ + L_𝐤 &= -i \frac{\partial} {\partial \alpha}, + & + R_𝐤 &= i \frac{\partial} {\partial \gamma}. \end{aligned} ``` - We can lift any function on ``S^2`` to a function on ``S^3`` — or - more precisely any function on spherical coordinates to a function - on the space of Euler angles — by the correspondence ``(\theta, - \phi) \mapsto (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We - can then express the angular-momentum operators in their more - common form, in terms of spherical coordinates: - + These correspond precisely to the standard expressions for the + angular-momentum operators, with ``𝐢 \leftrightarrow 𝐱``, etc. + We also obtain a generalization of the usual commutator relations + and find that + ```math + [L_𝐮, L_𝐯] = \frac{i}{2} L_{[𝐮,𝐯]} + \qquad + [R_𝐮, R_𝐯] = \frac{i}{2} R_{[𝐮,𝐯]} + \qquad + [L_𝐮, R_𝐯] = 0. + ``` + Restricting to just the basis vectors, indexed as ``a,b,c``, the + first of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, + which is precisely the standard result. We can also lift any + function on ``S^2`` to a function on ``S^3`` — or more precisely + any function on spherical coordinates to a function on the space of + Euler angles — by the correspondence ``(\theta, \phi) \mapsto + (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We can then express + the angular-momentum operators in their more common form, in terms + of spherical coordinates: + ```math + L_x = i \left\{ + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} + + \sin\phi \frac{\partial} {\partial \theta} + \right\} + \qquad + L_y = i \left\{ + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} + - \cos\phi \frac{\partial} {\partial \theta} + \right\} + \qquad + L_z = -i \frac{\partial} {\partial \phi} + ``` + The ``R`` operators make less sense for a function of spherical + coordinates, because of their inherent dependence on ``\gamma``. + Nonetheless, we can relate them to the standard spin-raising and + -lowering operators for a function of spin weight ``s``: ```math \begin{aligned} - L_x &= -i \left\{ - -\frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} - - \sin\phi \frac{\partial} {\partial \theta} - \right\} \\ - L_y &= -i \left\{ - -\frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} - + \cos\phi \frac{\partial} {\partial \theta} - \right\} \\ - L_z &= -i \frac{\partial} {\partial \phi} + R_z &= s, \\ + R_x + i R_y &= \eth, \\ + R_x - i R_y &= \bar{\eth}, \end{aligned} ``` - The ``R`` operators make less sense for a function of spherical - coordinates. -9. Spherical harmonics + where ``\eth \eta = -\sin^s \theta (\partial_\theta + i + \csc\theta\, \partial_\phi) (\eta \sin^{-s} \theta)`` is + the spin-raising operator introduced by [Newman_1966](@citet). In + the expressions for ``R``, any derivative with respect to + ``\gamma`` is simply replaced by a factor of ``-i s``, allowing us + to interpret them as operators on the 2-sphere, even though this is + mathematically ill-defined and spin-weighted functions really + should be defined on the 3-sphere. +9. There is essentially no disagreement in the literature about the + definitions of the spherical harmonics, so we adopt the standard + expressions. Explicitly, in terms of spherical coordinates, + ```math + Y_{\ell, m}(\theta, \phi) + = + \sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-m)!}{(\ell+m)!}} + e^{im\phi} + (-1)^{\ell+m} \frac{(1-\cos^2\theta)^{m/2}} {2^\ell \ell!} + \frac{d^{\ell+m}}{d\cos\theta^{\ell+m}} (1-\cos^2\theta)^\ell. + ``` + This package does not actually use this form; we generalize it to + spin-weighted spherical harmonics, and express those as functions + of a quaternion. Nonetheless, we choose our conventions to ensure + that the generalized definition reduces to this expression for spin + weight ``s=0``, and transforming the spherical coordinates as + ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` 10. Wigner D-matrices 11. Spin-weighted spherical harmonics @@ -806,8 +858,12 @@ Moreover, we can show that these operators form a Lie algebra with the commutator as the Lie bracket. That is, we have ```math \begin{aligned} -[L_{\mathfrak{g}}, L_{\mathfrak{h}}] &= -\lambda L_{[\mathfrak{g}, \mathfrak{h}]}, \\ -[R_{\mathfrak{g}}, R_{\mathfrak{h}}] &= \rho R_{[\mathfrak{g}, \mathfrak{h}]}, \\ +[L_{\mathfrak{g}}, L_{\mathfrak{h}}] + &= \frac{\lambda}{2} L_{[\mathfrak{g}, \mathfrak{h}]}, +\\ +[R_{\mathfrak{g}}, R_{\mathfrak{h}}] + &= -\frac{\rho}{2} R_{[\mathfrak{g}, \mathfrak{h}]}, +\\ [L_{\mathfrak{g}}, R_{\mathfrak{h}}] &= 0. \end{aligned} ``` @@ -846,8 +902,8 @@ Using these relations, we can actually solve for the constants ``\lambda`` and ``\rho`` up to a sign. We find that ```math \begin{aligned} -\lambda &= -i, \\ -\rho &= i. +\lambda &= i, \\ +\rho &= -i. \end{aligned} ``` @@ -1053,6 +1109,20 @@ distinct, this can only be true if ``\int f_u f_v=0``. - Representation matrices transfer to the homogeneous space, with sparsity patterns +Theorem 2.16 of [Hanson-Yakovlev](@cite HansonYakovlev_2002) says that +an orthonormal basis of a product of ``L^2`` spaces is given by the +product of the orthonormal bases of the individual spaces. +Furthermore, on page 354, they point out that ``\{(1/\sqrt{2\pi}) +e^{im\phi}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the +set ``\{1/c_{n,m} P_n^m(\cos\theta)`` is an orthonormal basis of +``L^2(0, \pi)`` in the ``\theta`` coordinate. Therefore, the product +of these two sets is an orthonormal basis of the product space +``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate +space for ``S^2``. I would probably modify this to point out that +``(0,2\pi)`` is really ``S^1``, and then we could extend it to point +out that you can throw on another factor of ``S^1`` to cover ``S^3``, +which happens to give us the Wigner D-matrices. + ## Recursion relations [Gumerov and Duraiswami (2001)](@cite Gumerov_2001) derive their @@ -1078,7 +1148,8 @@ may be more similar to the right-derivative defined above. However, I don't know that we'll necessarily be able to achieve the same results with just angular-momentum operators, since their operators do involve moving off of the sphere. Maybe we'd need to move off of the sphere -in 4-D space to get comparable results. +in 4-D space to get comparable results. Or maybe just use something +like ``𝐫 ∧ L``, which should also have 3 degrees of freedom. The SWSHs/``\mathfrak{D}`` functions can be naturally promoted to functions not just on the 3-sphere, but also in 4-D space just by From c73247b7e58e523a4ded0b76628a5f7bbe2cbb46 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 08:36:03 -0500 Subject: [PATCH 061/329] Define spin weight and explain relationship with R_u operators --- docs/src/conventions/conventions.md | 82 ++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/conventions.md index 6728ece5..d33ea48c 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/conventions.md @@ -126,23 +126,10 @@ Euler angles. ``` The ``R`` operators make less sense for a function of spherical coordinates, because of their inherent dependence on ``\gamma``. - Nonetheless, we can relate them to the standard spin-raising and - -lowering operators for a function of spin weight ``s``: - ```math - \begin{aligned} - R_z &= s, \\ - R_x + i R_y &= \eth, \\ - R_x - i R_y &= \bar{\eth}, - \end{aligned} - ``` - where ``\eth \eta = -\sin^s \theta (\partial_\theta + i - \csc\theta\, \partial_\phi) (\eta \sin^{-s} \theta)`` is - the spin-raising operator introduced by [Newman_1966](@citet). In - the expressions for ``R``, any derivative with respect to - ``\gamma`` is simply replaced by a factor of ``-i s``, allowing us - to interpret them as operators on the 2-sphere, even though this is - mathematically ill-defined and spin-weighted functions really - should be defined on the 3-sphere. + We will come back to them, however, when we consider spin-weighted + functions — which are inherently ill-defined on the 2-sphere, but + can be interpreted as restrictions of functions on the 3-sphere + with this special "weight" property. 9. There is essentially no disagreement in the literature about the definitions of the spherical harmonics, so we adopt the standard expressions. Explicitly, in terms of spherical coordinates, @@ -160,8 +147,65 @@ Euler angles. that the generalized definition reduces to this expression for spin weight ``s=0``, and transforming the spherical coordinates as ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` -10. Wigner D-matrices -11. Spin-weighted spherical harmonics +10. Following [Newman_1966](@citet), we find that they define the + spherical tangent basis vectors as + ```math + m^\mu = \frac{1}{\sqrt{2}} \left( + \boldsymbol{\theta} + i \boldsymbol{\phi} + \right) + ``` + and discuss spin weight in terms of the rotation + ```math + (m^\mu)' = e^{i\psi} m^\nu, + ``` + where the tangent basis rotates but we are "keeping the + coordinates fixed". We find that we can emulate this using Euler + angles ``(\phi, \theta, -\psi)``. Note the negative sign in the + last angle. As usual, this rotates the positive ``𝐳`` axis to + the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / + \sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have + spin weight ``s`` if it transforms as + ```math + \eta' = e^{is\psi} \eta. + ``` + In our notation, we can realize this function as a function of + Euler angles, and that equation becomes + ```math + \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), + ``` + or + ```math + \eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). + ``` + This is the crucial definition giving us the behavior of + spin-weighted functions: they are eigenfunctions of the operator + ``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also + immediately find the spin-raising and -lowering operators — + canonically denoted ``\eth`` and ``\bar{\eth}`` — from the + commutator relations for ``R``: + ```math + \begin{aligned} + \eth \eta &= \left(R_x + i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right), \\ + \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right). \\ + \end{aligned} + ``` + Here, we have used the full expressions for ``R_x`` and ``R_y`` + given above in terms of Euler angles, replacing the derivatives + with respect to ``\gamma`` by a factor of ``-i s``, and converting + the remaining Euler angles to spherical coordinates. This allows + us to write them as if they were operators on the 2-sphere, even + though this is mathematically ill-defined and spin-weighted + functions really must be defined on the 3-sphere. +11. Wigner D-matrices +12. Spin-weighted spherical harmonics ## Three-dimensional space From 8118d7c517c1a663e05cb8a9dd2af283b983b69b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 08:43:37 -0500 Subject: [PATCH 062/329] Split conventions out into summary and details --- docs/make.jl | 3 +- .../{conventions.md => details.md} | 219 ++---------------- docs/src/conventions/summary.md | 211 +++++++++++++++++ 3 files changed, 226 insertions(+), 207 deletions(-) rename docs/src/conventions/{conventions.md => details.md} (82%) create mode 100644 docs/src/conventions/summary.md diff --git a/docs/make.jl b/docs/make.jl index dddc5ebd..5fd424e2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -60,7 +60,8 @@ makedocs( "functions.md", ], "Conventions" => [ - "conventions/conventions.md", + "conventions/summary.md", + "conventions/details.md", "conventions/comparisons.md", "Calculations" => [ joinpath(relative_literate_output, "euler_angular_momentum.md"), diff --git a/docs/src/conventions/conventions.md b/docs/src/conventions/details.md similarity index 82% rename from docs/src/conventions/conventions.md rename to docs/src/conventions/details.md index d33ea48c..aace35c0 100644 --- a/docs/src/conventions/conventions.md +++ b/docs/src/conventions/details.md @@ -1,212 +1,19 @@ -# Development - -In the following subsections, we work through all the conventions used -in this package, starting from first principles to motivate the -choices and ensure that each step is on firm footing. First, we can -just list the most important conventions. Note that we will use Euler -angles and spherical coordinates here. It is almost always a bad idea -to use Euler angles in *computing*; quaternions are clearly the -preferred representation for numerous reasons. However, Euler angles -are important for (a) comparing to other sources, and (b) performing +# Details of conventions + +This page carefully works through all the conventions used in this +package, starting from first principles to motivate the choices and +ensure that each step is on firm footing. The [previous page](@ref +"Summary of conventions") collects the results in a more concise form. + +Note that we will use Euler angles and spherical coordinates here, but +*they are not used internally in this package* — though conversion +functions are available. It is almost always a bad idea to use Euler +angles in *computing*; quaternions are clearly the preferred +representation for numerous reasons. However, Euler angles are +important for (a) comparing to other sources, and (b) performing *analytic* integrations. These are the only two uses we will make of Euler angles. -1. Right-handed Cartesian coordinates ``(x, y, z)`` and unit basis - vectors ``(𝐱, 𝐲, 𝐳)``. -2. Spherical coordinates ``(r, \theta, \phi)`` and unit basis vectors - ``(𝐫, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar - angle" ``\theta \in [0, \pi]`` measures the angle between the - specified direction and the positive ``𝐳`` axis. The "azimuthal - angle" ``\phi \in [0, 2\pi)`` measures the angle between the - projection of the specified direction onto the ``𝐱``-``𝐲`` plane - and the positive ``𝐱`` axis, with the positive ``𝐲`` axis - corresponding to the positive angle ``\phi = \pi/2``. -3. Quaternions ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢𝐣𝐤 = -1``. - In software, this quaternion is represented by ``(W, X, Y, Z)``. - We will depict a three-dimensional vector ``𝐯 = v_x 𝐱 + v_y 𝐲 + - v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + v_z - 𝐤``. -4. A rotation represented by the unit quaternion ``𝐑`` acts on a - vector ``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. -5. Where relevant, rotations will be assumed to be right-handed, so - that a quaternion characterizing the rotation through an angle - ``\vartheta`` about a unit vector ``𝐮`` can be expressed as ``𝐑 = - \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` would deliver the same - rotation, which is why the group of unit quaternions - ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* of the - group of rotations ``\mathrm{SO}(3)``. -6. Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha - 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles - ``\alpha`` and ``\beta`` take values in ``[0, 2\pi)``. The angle - ``\beta`` takes values in ``[0, 2\pi]`` to parametrize the group of - unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, - \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. -7. A point on the unit sphere with spherical coordinates ``(\theta, - \phi)`` can be represented by Euler angles ``(\alpha, \beta, - \gamma) = (\phi, \theta, 0)``. The rotation with these Euler - angles takes the positive ``𝐳`` axis to the specified direction. - In particular, any function of spherical coordinates can be - promoted to a function on Euler angles using this identification. -8. For a complex-valued function ``f(𝐑)``, we define two operators, - the left and right Lie derivatives: - ```math - L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} - f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) - \qquad \text{and} \qquad - R_𝐮 f(𝐑) = -\left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} - f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), - ``` - where ``𝐮`` can be any quaternion, though unit pure-vector - quaternions are the most common. In particular, ``L`` represents - the standard angular-momentum operators, and we can compute the - expressions in Euler angles for the basis vectors: - ```math - \begin{aligned} - L_𝐢 &= i \left\{ - \frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - + \sin\alpha \frac{\partial} {\partial \beta} - - \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} - \right\}, - & - R_𝐢 &= i \left\{ - -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} - +\sin\gamma \frac{\partial} {\partial \beta} - +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} - \right\}, - \\ - L_𝐣 &= i \left\{ - \frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - - \cos\alpha \frac{\partial} {\partial \beta} - -\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} - \right\}, - & - R_𝐣 &= i \left\{ - \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} - +\cos\gamma \frac{\partial} {\partial \beta} - -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} - \right\}, - \\ - L_𝐤 &= -i \frac{\partial} {\partial \alpha}, - & - R_𝐤 &= i \frac{\partial} {\partial \gamma}. - \end{aligned} - ``` - These correspond precisely to the standard expressions for the - angular-momentum operators, with ``𝐢 \leftrightarrow 𝐱``, etc. - We also obtain a generalization of the usual commutator relations - and find that - ```math - [L_𝐮, L_𝐯] = \frac{i}{2} L_{[𝐮,𝐯]} - \qquad - [R_𝐮, R_𝐯] = \frac{i}{2} R_{[𝐮,𝐯]} - \qquad - [L_𝐮, R_𝐯] = 0. - ``` - Restricting to just the basis vectors, indexed as ``a,b,c``, the - first of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, - which is precisely the standard result. We can also lift any - function on ``S^2`` to a function on ``S^3`` — or more precisely - any function on spherical coordinates to a function on the space of - Euler angles — by the correspondence ``(\theta, \phi) \mapsto - (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We can then express - the angular-momentum operators in their more common form, in terms - of spherical coordinates: - ```math - L_x = i \left\{ - \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} - + \sin\phi \frac{\partial} {\partial \theta} - \right\} - \qquad - L_y = i \left\{ - \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} - - \cos\phi \frac{\partial} {\partial \theta} - \right\} - \qquad - L_z = -i \frac{\partial} {\partial \phi} - ``` - The ``R`` operators make less sense for a function of spherical - coordinates, because of their inherent dependence on ``\gamma``. - We will come back to them, however, when we consider spin-weighted - functions — which are inherently ill-defined on the 2-sphere, but - can be interpreted as restrictions of functions on the 3-sphere - with this special "weight" property. -9. There is essentially no disagreement in the literature about the - definitions of the spherical harmonics, so we adopt the standard - expressions. Explicitly, in terms of spherical coordinates, - ```math - Y_{\ell, m}(\theta, \phi) - = - \sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-m)!}{(\ell+m)!}} - e^{im\phi} - (-1)^{\ell+m} \frac{(1-\cos^2\theta)^{m/2}} {2^\ell \ell!} - \frac{d^{\ell+m}}{d\cos\theta^{\ell+m}} (1-\cos^2\theta)^\ell. - ``` - This package does not actually use this form; we generalize it to - spin-weighted spherical harmonics, and express those as functions - of a quaternion. Nonetheless, we choose our conventions to ensure - that the generalized definition reduces to this expression for spin - weight ``s=0``, and transforming the spherical coordinates as - ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` -10. Following [Newman_1966](@citet), we find that they define the - spherical tangent basis vectors as - ```math - m^\mu = \frac{1}{\sqrt{2}} \left( - \boldsymbol{\theta} + i \boldsymbol{\phi} - \right) - ``` - and discuss spin weight in terms of the rotation - ```math - (m^\mu)' = e^{i\psi} m^\nu, - ``` - where the tangent basis rotates but we are "keeping the - coordinates fixed". We find that we can emulate this using Euler - angles ``(\phi, \theta, -\psi)``. Note the negative sign in the - last angle. As usual, this rotates the positive ``𝐳`` axis to - the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / - \sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have - spin weight ``s`` if it transforms as - ```math - \eta' = e^{is\psi} \eta. - ``` - In our notation, we can realize this function as a function of - Euler angles, and that equation becomes - ```math - \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), - ``` - or - ```math - \eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). - ``` - This is the crucial definition giving us the behavior of - spin-weighted functions: they are eigenfunctions of the operator - ``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also - immediately find the spin-raising and -lowering operators — - canonically denoted ``\eth`` and ``\bar{\eth}`` — from the - commutator relations for ``R``: - ```math - \begin{aligned} - \eth \eta &= \left(R_x + i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right), \\ - \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right). \\ - \end{aligned} - ``` - Here, we have used the full expressions for ``R_x`` and ``R_y`` - given above in terms of Euler angles, replacing the derivatives - with respect to ``\gamma`` by a factor of ``-i s``, and converting - the remaining Euler angles to spherical coordinates. This allows - us to write them as if they were operators on the 2-sphere, even - though this is mathematically ill-defined and spin-weighted - functions really must be defined on the 3-sphere. -11. Wigner D-matrices -12. Spin-weighted spherical harmonics - ## Three-dimensional space diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md new file mode 100644 index 00000000..69cae36c --- /dev/null +++ b/docs/src/conventions/summary.md @@ -0,0 +1,211 @@ +# Summary of conventions + +This page lists the most important conventions used in this package. +The [following page](@ref "Details of conventions") derives all of +these conventions from the very basics (i.e., starting from Cartesian +coordinates of 3-dimensional space). + +Note that we will use Euler angles and spherical coordinates here, but +*they are not used internally in this package* — though conversion +functions are available. It is almost always a bad idea to use Euler +angles in *computing*; quaternions are clearly the preferred +representation for numerous reasons. However, Euler angles are +important for (a) comparing to other sources, and (b) performing +*analytic* integrations. These are the only two uses we will make of +Euler angles. + +1. Right-handed Cartesian coordinates ``(x, y, z)`` and unit basis + vectors ``(𝐱, 𝐲, 𝐳)``. +2. Spherical coordinates ``(r, \theta, \phi)`` and unit basis vectors + ``(𝐫, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar + angle" ``\theta \in [0, \pi]`` measures the angle between the + specified direction and the positive ``𝐳`` axis. The "azimuthal + angle" ``\phi \in [0, 2\pi)`` measures the angle between the + projection of the specified direction onto the ``𝐱``-``𝐲`` plane + and the positive ``𝐱`` axis, with the positive ``𝐲`` axis + corresponding to the positive angle ``\phi = \pi/2``. +3. Quaternions ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢𝐣𝐤 = -1``. + In software, this quaternion is represented by ``(W, X, Y, Z)``. + We will depict a three-dimensional vector ``𝐯 = v_x 𝐱 + v_y 𝐲 + + v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + v_z + 𝐤``. +4. A rotation represented by the unit quaternion ``𝐑`` acts on a + vector ``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. +5. Where relevant, rotations will be assumed to be right-handed, so + that a quaternion characterizing the rotation through an angle + ``\vartheta`` about a unit vector ``𝐮`` can be expressed as ``𝐑 = + \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` would deliver the same + rotation, which is why the group of unit quaternions + ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* of the + group of rotations ``\mathrm{SO}(3)``. +6. Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha + 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles + ``\alpha`` and ``\beta`` take values in ``[0, 2\pi)``. The angle + ``\beta`` takes values in ``[0, 2\pi]`` to parametrize the group of + unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, + \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. +7. A point on the unit sphere with spherical coordinates ``(\theta, + \phi)`` can be represented by Euler angles ``(\alpha, \beta, + \gamma) = (\phi, \theta, 0)``. The rotation with these Euler + angles takes the positive ``𝐳`` axis to the specified direction. + In particular, any function of spherical coordinates can be + promoted to a function on Euler angles using this identification. +8. For a complex-valued function ``f(𝐑)``, we define two operators, + the left and right Lie derivatives: + ```math + L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} + f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) + \qquad \text{and} \qquad + R_𝐮 f(𝐑) = -\left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} + f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), + ``` + where ``𝐮`` can be any quaternion, though unit pure-vector + quaternions are the most common. In particular, ``L`` represents + the standard angular-momentum operators, and we can compute the + expressions in Euler angles for the basis vectors: + ```math + \begin{aligned} + L_𝐢 &= i \left\{ + \frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + + \sin\alpha \frac{\partial} {\partial \beta} + - \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \right\}, + & + R_𝐢 &= i \left\{ + -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} + +\sin\gamma \frac{\partial} {\partial \beta} + +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} + \right\}, + \\ + L_𝐣 &= i \left\{ + \frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + - \cos\alpha \frac{\partial} {\partial \beta} + -\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \right\}, + & + R_𝐣 &= i \left\{ + \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} + +\cos\gamma \frac{\partial} {\partial \beta} + -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} + \right\}, + \\ + L_𝐤 &= -i \frac{\partial} {\partial \alpha}, + & + R_𝐤 &= i \frac{\partial} {\partial \gamma}. + \end{aligned} + ``` + These correspond precisely to the standard expressions for the + angular-momentum operators, with ``𝐢 \leftrightarrow 𝐱``, etc. + We also obtain a generalization of the usual commutator relations + and find that + ```math + [L_𝐮, L_𝐯] = \frac{i}{2} L_{[𝐮,𝐯]} + \qquad + [R_𝐮, R_𝐯] = \frac{i}{2} R_{[𝐮,𝐯]} + \qquad + [L_𝐮, R_𝐯] = 0. + ``` + Restricting to just the basis vectors, indexed as ``a,b,c``, the + first of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, + which is precisely the standard result. We can also lift any + function on ``S^2`` to a function on ``S^3`` — or more precisely + any function on spherical coordinates to a function on the space of + Euler angles — by the correspondence ``(\theta, \phi) \mapsto + (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We can then express + the angular-momentum operators in their more common form, in terms + of spherical coordinates: + ```math + L_x = i \left\{ + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} + + \sin\phi \frac{\partial} {\partial \theta} + \right\} + \qquad + L_y = i \left\{ + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} + - \cos\phi \frac{\partial} {\partial \theta} + \right\} + \qquad + L_z = -i \frac{\partial} {\partial \phi} + ``` + The ``R`` operators make less sense for a function of spherical + coordinates, because of their inherent dependence on ``\gamma``. + We will come back to them, however, when we consider spin-weighted + functions — which are inherently ill-defined on the 2-sphere, but + can be interpreted as restrictions of functions on the 3-sphere + with this special "weight" property. +9. There is essentially no disagreement in the literature about the + definitions of the spherical harmonics, so we adopt the standard + expressions. Explicitly, in terms of spherical coordinates, + ```math + Y_{\ell, m}(\theta, \phi) + = + \sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-m)!}{(\ell+m)!}} + e^{im\phi} + (-1)^{\ell+m} \frac{(1-\cos^2\theta)^{m/2}} {2^\ell \ell!} + \frac{d^{\ell+m}}{d\cos\theta^{\ell+m}} (1-\cos^2\theta)^\ell. + ``` + This package does not actually use this form; we generalize it to + spin-weighted spherical harmonics, and express those as functions + of a quaternion. Nonetheless, we choose our conventions to ensure + that the generalized definition reduces to this expression for spin + weight ``s=0``, and transforming the spherical coordinates as + ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` +10. Following [Newman_1966](@citet), we find that they define the + spherical tangent basis vectors as + ```math + m^\mu = \frac{1}{\sqrt{2}} \left( + \boldsymbol{\theta} + i \boldsymbol{\phi} + \right) + ``` + and discuss spin weight in terms of the rotation + ```math + (m^\mu)' = e^{i\psi} m^\nu, + ``` + where the tangent basis rotates but we are "keeping the + coordinates fixed". We find that we can emulate this using Euler + angles ``(\phi, \theta, -\psi)``. Note the negative sign in the + last angle. As usual, this rotates the positive ``𝐳`` axis to + the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / + \sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have + spin weight ``s`` if it transforms as + ```math + \eta' = e^{is\psi} \eta. + ``` + In our notation, we can realize this function as a function of + Euler angles, and that equation becomes + ```math + \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), + ``` + or + ```math + \eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). + ``` + This is the crucial definition giving us the behavior of + spin-weighted functions: they are eigenfunctions of the operator + ``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also + immediately find the spin-raising and -lowering operators — + canonically denoted ``\eth`` and ``\bar{\eth}`` — from the + commutator relations for ``R``: + ```math + \begin{aligned} + \eth \eta &= \left(R_x + i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right), \\ + \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right). \\ + \end{aligned} + ``` + Here, we have used the full expressions for ``R_x`` and ``R_y`` + given above in terms of Euler angles, replacing the derivatives + with respect to ``\gamma`` by a factor of ``-i s``, and converting + the remaining Euler angles to spherical coordinates. This allows + us to write them as if they were operators on the 2-sphere, even + though this is mathematically ill-defined and spin-weighted + functions really must be defined on the 3-sphere. +11. Wigner D-matrices +12. Spin-weighted spherical harmonics From 3f23b952c018c7282a35be381aa8fcc97924b0e0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:30:45 -0500 Subject: [PATCH 063/329] Fix problem due to excessive indentation --- docs/src/conventions/summary.md | 112 ++++++++++++++++---------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index 69cae36c..fefa3bbb 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -151,61 +151,61 @@ Euler angles. weight ``s=0``, and transforming the spherical coordinates as ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` 10. Following [Newman_1966](@citet), we find that they define the - spherical tangent basis vectors as - ```math - m^\mu = \frac{1}{\sqrt{2}} \left( - \boldsymbol{\theta} + i \boldsymbol{\phi} - \right) - ``` - and discuss spin weight in terms of the rotation - ```math - (m^\mu)' = e^{i\psi} m^\nu, - ``` - where the tangent basis rotates but we are "keeping the - coordinates fixed". We find that we can emulate this using Euler - angles ``(\phi, \theta, -\psi)``. Note the negative sign in the - last angle. As usual, this rotates the positive ``𝐳`` axis to - the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / - \sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have - spin weight ``s`` if it transforms as - ```math - \eta' = e^{is\psi} \eta. - ``` - In our notation, we can realize this function as a function of - Euler angles, and that equation becomes - ```math - \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), - ``` - or - ```math - \eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). - ``` - This is the crucial definition giving us the behavior of - spin-weighted functions: they are eigenfunctions of the operator - ``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also - immediately find the spin-raising and -lowering operators — - canonically denoted ``\eth`` and ``\bar{\eth}`` — from the - commutator relations for ``R``: - ```math - \begin{aligned} - \eth \eta &= \left(R_x + i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right), \\ - \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right). \\ - \end{aligned} - ``` - Here, we have used the full expressions for ``R_x`` and ``R_y`` - given above in terms of Euler angles, replacing the derivatives - with respect to ``\gamma`` by a factor of ``-i s``, and converting - the remaining Euler angles to spherical coordinates. This allows - us to write them as if they were operators on the 2-sphere, even - though this is mathematically ill-defined and spin-weighted - functions really must be defined on the 3-sphere. + spherical tangent basis vectors as + ```math + m^\mu = \frac{1}{\sqrt{2}} \left( + \boldsymbol{\theta} + i \boldsymbol{\phi} + \right) + ``` + and discuss spin weight in terms of the rotation + ```math + (m^\mu)' = e^{i\psi} m^\nu, + ``` + where the tangent basis rotates but we are "keeping the + coordinates fixed". We find that we can emulate this using Euler + angles ``(\phi, \theta, -\psi)``. Note the negative sign in the + last angle. As usual, this rotates the positive ``𝐳`` axis to + the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / + \sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have + spin weight ``s`` if it transforms as + ```math + \eta' = e^{is\psi} \eta. + ``` + In our notation, we can realize this function as a function of + Euler angles, and that equation becomes + ```math + \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), + ``` + or + ```math + \eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). + ``` + This is the crucial definition giving us the behavior of + spin-weighted functions: they are eigenfunctions of the operator + ``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also + immediately find the spin-raising and -lowering operators — + canonically denoted ``\eth`` and ``\bar{\eth}`` — from the + commutator relations for ``R``: + ```math + \begin{aligned} + \eth \eta &= \left(R_x + i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right), \\ + \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right). \\ + \end{aligned} + ``` + Here, we have used the full expressions for ``R_x`` and ``R_y`` + given above in terms of Euler angles, replacing the derivatives + with respect to ``\gamma`` by a factor of ``-i s``, and converting + the remaining Euler angles to spherical coordinates. This allows + us to write them as if they were operators on the 2-sphere, even + though this is mathematically ill-defined and spin-weighted + functions really must be defined on the 3-sphere. 11. Wigner D-matrices 12. Spin-weighted spherical harmonics From c501f8d5035ade2634f6b7f1d138ff28b7b4fd38 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:38:09 -0500 Subject: [PATCH 064/329] Tiny tweaks --- docs/src/conventions/summary.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index fefa3bbb..6e4161d1 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -150,8 +150,8 @@ Euler angles. that the generalized definition reduces to this expression for spin weight ``s=0``, and transforming the spherical coordinates as ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` -10. Following [Newman_1966](@citet), we find that they define the - spherical tangent basis vectors as +10. [Newman_1966](@citet) define the spherical tangent basis vectors + as ```math m^\mu = \frac{1}{\sqrt{2}} \left( \boldsymbol{\theta} + i \boldsymbol{\phi} @@ -197,7 +197,7 @@ Euler angles. = -\sin^s \theta \left\{ \frac{\partial}{\partial \theta} - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right). \\ + \right\} \left(\eta \sin^{-s} \theta\right). \end{aligned} ``` Here, we have used the full expressions for ``R_x`` and ``R_y`` From ab94e7606d47f5625afda79e66cc557bf4f955ee Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:52:47 -0500 Subject: [PATCH 065/329] Change from enumerated list to subsections --- docs/src/conventions/summary.md | 411 +++++++++++++++++--------------- 1 file changed, 216 insertions(+), 195 deletions(-) diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index 6e4161d1..bb4d47b8 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -14,198 +14,219 @@ important for (a) comparing to other sources, and (b) performing *analytic* integrations. These are the only two uses we will make of Euler angles. -1. Right-handed Cartesian coordinates ``(x, y, z)`` and unit basis - vectors ``(𝐱, 𝐲, 𝐳)``. -2. Spherical coordinates ``(r, \theta, \phi)`` and unit basis vectors - ``(𝐫, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar - angle" ``\theta \in [0, \pi]`` measures the angle between the - specified direction and the positive ``𝐳`` axis. The "azimuthal - angle" ``\phi \in [0, 2\pi)`` measures the angle between the - projection of the specified direction onto the ``𝐱``-``𝐲`` plane - and the positive ``𝐱`` axis, with the positive ``𝐲`` axis - corresponding to the positive angle ``\phi = \pi/2``. -3. Quaternions ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢𝐣𝐤 = -1``. - In software, this quaternion is represented by ``(W, X, Y, Z)``. - We will depict a three-dimensional vector ``𝐯 = v_x 𝐱 + v_y 𝐲 + - v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + v_z - 𝐤``. -4. A rotation represented by the unit quaternion ``𝐑`` acts on a - vector ``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. -5. Where relevant, rotations will be assumed to be right-handed, so - that a quaternion characterizing the rotation through an angle - ``\vartheta`` about a unit vector ``𝐮`` can be expressed as ``𝐑 = - \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` would deliver the same - rotation, which is why the group of unit quaternions - ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* of the - group of rotations ``\mathrm{SO}(3)``. -6. Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha - 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles - ``\alpha`` and ``\beta`` take values in ``[0, 2\pi)``. The angle - ``\beta`` takes values in ``[0, 2\pi]`` to parametrize the group of - unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, - \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. -7. A point on the unit sphere with spherical coordinates ``(\theta, - \phi)`` can be represented by Euler angles ``(\alpha, \beta, - \gamma) = (\phi, \theta, 0)``. The rotation with these Euler - angles takes the positive ``𝐳`` axis to the specified direction. - In particular, any function of spherical coordinates can be - promoted to a function on Euler angles using this identification. -8. For a complex-valued function ``f(𝐑)``, we define two operators, - the left and right Lie derivatives: - ```math - L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} - f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) - \qquad \text{and} \qquad - R_𝐮 f(𝐑) = -\left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} - f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), - ``` - where ``𝐮`` can be any quaternion, though unit pure-vector - quaternions are the most common. In particular, ``L`` represents - the standard angular-momentum operators, and we can compute the - expressions in Euler angles for the basis vectors: - ```math - \begin{aligned} - L_𝐢 &= i \left\{ - \frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - + \sin\alpha \frac{\partial} {\partial \beta} - - \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} - \right\}, - & - R_𝐢 &= i \left\{ - -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} - +\sin\gamma \frac{\partial} {\partial \beta} - +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} - \right\}, - \\ - L_𝐣 &= i \left\{ - \frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - - \cos\alpha \frac{\partial} {\partial \beta} - -\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} - \right\}, - & - R_𝐣 &= i \left\{ - \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} - +\cos\gamma \frac{\partial} {\partial \beta} - -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} - \right\}, - \\ - L_𝐤 &= -i \frac{\partial} {\partial \alpha}, - & - R_𝐤 &= i \frac{\partial} {\partial \gamma}. - \end{aligned} - ``` - These correspond precisely to the standard expressions for the - angular-momentum operators, with ``𝐢 \leftrightarrow 𝐱``, etc. - We also obtain a generalization of the usual commutator relations - and find that - ```math - [L_𝐮, L_𝐯] = \frac{i}{2} L_{[𝐮,𝐯]} - \qquad - [R_𝐮, R_𝐯] = \frac{i}{2} R_{[𝐮,𝐯]} - \qquad - [L_𝐮, R_𝐯] = 0. - ``` - Restricting to just the basis vectors, indexed as ``a,b,c``, the - first of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, - which is precisely the standard result. We can also lift any - function on ``S^2`` to a function on ``S^3`` — or more precisely - any function on spherical coordinates to a function on the space of - Euler angles — by the correspondence ``(\theta, \phi) \mapsto - (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We can then express - the angular-momentum operators in their more common form, in terms - of spherical coordinates: - ```math - L_x = i \left\{ - \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} - + \sin\phi \frac{\partial} {\partial \theta} - \right\} - \qquad - L_y = i \left\{ - \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} - - \cos\phi \frac{\partial} {\partial \theta} - \right\} - \qquad - L_z = -i \frac{\partial} {\partial \phi} - ``` - The ``R`` operators make less sense for a function of spherical - coordinates, because of their inherent dependence on ``\gamma``. - We will come back to them, however, when we consider spin-weighted - functions — which are inherently ill-defined on the 2-sphere, but - can be interpreted as restrictions of functions on the 3-sphere - with this special "weight" property. -9. There is essentially no disagreement in the literature about the - definitions of the spherical harmonics, so we adopt the standard - expressions. Explicitly, in terms of spherical coordinates, - ```math - Y_{\ell, m}(\theta, \phi) - = - \sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-m)!}{(\ell+m)!}} - e^{im\phi} - (-1)^{\ell+m} \frac{(1-\cos^2\theta)^{m/2}} {2^\ell \ell!} - \frac{d^{\ell+m}}{d\cos\theta^{\ell+m}} (1-\cos^2\theta)^\ell. - ``` - This package does not actually use this form; we generalize it to - spin-weighted spherical harmonics, and express those as functions - of a quaternion. Nonetheless, we choose our conventions to ensure - that the generalized definition reduces to this expression for spin - weight ``s=0``, and transforming the spherical coordinates as - ``(\theta, \phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` -10. [Newman_1966](@citet) define the spherical tangent basis vectors - as - ```math - m^\mu = \frac{1}{\sqrt{2}} \left( - \boldsymbol{\theta} + i \boldsymbol{\phi} - \right) - ``` - and discuss spin weight in terms of the rotation - ```math - (m^\mu)' = e^{i\psi} m^\nu, - ``` - where the tangent basis rotates but we are "keeping the - coordinates fixed". We find that we can emulate this using Euler - angles ``(\phi, \theta, -\psi)``. Note the negative sign in the - last angle. As usual, this rotates the positive ``𝐳`` axis to - the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / - \sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have - spin weight ``s`` if it transforms as - ```math - \eta' = e^{is\psi} \eta. - ``` - In our notation, we can realize this function as a function of - Euler angles, and that equation becomes - ```math - \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), - ``` - or - ```math - \eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). - ``` - This is the crucial definition giving us the behavior of - spin-weighted functions: they are eigenfunctions of the operator - ``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also - immediately find the spin-raising and -lowering operators — - canonically denoted ``\eth`` and ``\bar{\eth}`` — from the - commutator relations for ``R``: - ```math - \begin{aligned} - \eth \eta &= \left(R_x + i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right), \\ - \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right). - \end{aligned} - ``` - Here, we have used the full expressions for ``R_x`` and ``R_y`` - given above in terms of Euler angles, replacing the derivatives - with respect to ``\gamma`` by a factor of ``-i s``, and converting - the remaining Euler angles to spherical coordinates. This allows - us to write them as if they were operators on the 2-sphere, even - though this is mathematically ill-defined and spin-weighted - functions really must be defined on the 3-sphere. -11. Wigner D-matrices -12. Spin-weighted spherical harmonics +## Fundamental coordinates +We use standard right-handed Cartesian coordinates ``(x, y, z)`` and +unit basis vectors ``(𝐱, 𝐲, 𝐳)``. + +## Spherical coordinates +We define spherical coordinates ``(r, \theta, \phi)`` and unit basis +vectors ``(𝐫, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar +angle" ``\theta \in [0, \pi]`` measures the angle between the +specified direction and the positive ``𝐳`` axis. The "azimuthal +angle" ``\phi \in [0, 2\pi)`` measures the angle between the +projection of the specified direction onto the ``𝐱``-``𝐲`` plane and +the positive ``𝐱`` axis, with the positive ``𝐲`` axis corresponding +to the positive angle ``\phi = \pi/2``. + +## Quaternions +A quaternion is written ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢𝐣𝐤 = +-1``. In software, this quaternion is represented by ``(W, X, Y, +Z)``. We will depict a three-dimensional vector ``𝐯 = v_x 𝐱 + v_y +𝐲 + v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + v_z +𝐤``. + +## Quaternion rotations +A rotation represented by the unit quaternion ``𝐑`` acts on a vector +``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. Where relevant, rotations will be +assumed to be right-handed, so that a quaternion characterizing the +rotation through an angle ``\vartheta`` about a unit vector ``𝐮`` can +be expressed as ``𝐑 = \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` +would deliver the same rotation, which is why the group of unit +quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* +of the group of rotations ``\mathrm{SO}(3)``. + +## Euler angles +Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha +𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles ``\alpha`` +and ``\beta`` take values in ``[0, 2\pi)``. The angle ``\beta`` takes +values in ``[0, 2\pi]`` to parametrize the group of unit quaternions +``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, \pi]`` to +parametrize the group of rotations ``\mathrm{SO}(3)``. + +## Spherical coordinates as Euler angles +A point on the unit sphere with spherical coordinates ``(\theta, +\phi)`` can be represented by Euler angles ``(\alpha, \beta, \gamma) = +(\phi, \theta, 0)``. The rotation with these Euler angles takes the +positive ``𝐳`` axis to the specified direction. In particular, any +function of spherical coordinates can be promoted to a function on +Euler angles using this identification. + +## Left and right angular-momentum operators +For a complex-valued function ``f(𝐑)``, we define two operators, the +left and right angular-momentum operators: +```math +L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} +f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) +\qquad \text{and} \qquad +R_𝐮 f(𝐑) = -\left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} +f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), +``` +where ``𝐮`` can be any quaternion, though unit pure-vector +quaternions are the most common. In particular, ``L`` represents the +standard angular-momentum operators, and we can compute the +expressions in Euler angles for the basis vectors: +```math +\begin{aligned} +L_𝐢 &= i \left\{ + \frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + + \sin\alpha \frac{\partial} {\partial \beta} + - \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} +\right\}, +& +R_𝐢 &= i \left\{ + -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} + +\sin\gamma \frac{\partial} {\partial \beta} + +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} +\right\}, +\\ +L_𝐣 &= i \left\{ + \frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + - \cos\alpha \frac{\partial} {\partial \beta} + -\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} +\right\}, +& +R_𝐣 &= i \left\{ + \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} + +\cos\gamma \frac{\partial} {\partial \beta} + -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} +\right\}, +\\ +L_𝐤 &= -i \frac{\partial} {\partial \alpha}, +& +R_𝐤 &= i \frac{\partial} {\partial \gamma}. +\end{aligned} +``` +These correspond precisely to the standard expressions for the +angular-momentum operators, with ``𝐢 \leftrightarrow 𝐱``, etc. We +also obtain a generalization of the usual commutator relations and +find that +```math +[L_𝐮, L_𝐯] = \frac{i}{2} L_{[𝐮,𝐯]} +\qquad +[R_𝐮, R_𝐯] = \frac{i}{2} R_{[𝐮,𝐯]} +\qquad +[L_𝐮, R_𝐯] = 0. +``` +Restricting to just the basis vectors, indexed as ``a,b,c``, the first +of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, which is +precisely the standard result. We can also lift any function on +``S^2`` to a function on ``S^3`` — or more precisely any function on +spherical coordinates to a function on the space of Euler angles — by +the correspondence ``(\theta, \phi) \mapsto (\alpha, \beta, \gamma) = +(\phi, \theta, 0)``. We can then express the angular-momentum +operators in their more common form, in terms of spherical +coordinates: +```math +L_x = i \left\{ + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} + + \sin\phi \frac{\partial} {\partial \theta} +\right\} +\qquad +L_y = i \left\{ + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} + - \cos\phi \frac{\partial} {\partial \theta} +\right\} +\qquad +L_z = -i \frac{\partial} {\partial \phi} +``` +The ``R`` operators make less sense for a function of spherical +coordinates, because of their inherent dependence on ``\gamma``. We +will come back to them, however, when we consider spin-weighted +functions — which are inherently ill-defined on the 2-sphere, but can +be interpreted as restrictions of functions on the 3-sphere with this +special "weight" property. + +## Spherical harmonics +There is essentially no disagreement in the literature about the +definitions of the spherical harmonics, so we adopt the standard +expressions. Explicitly, in terms of spherical coordinates, +```math +Y_{\ell, m}(\theta, \phi) += +\sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-m)!}{(\ell+m)!}} +e^{im\phi} +(-1)^{\ell+m} \frac{(1-\cos^2\theta)^{m/2}} {2^\ell \ell!} +\frac{d^{\ell+m}}{d\cos\theta^{\ell+m}} (1-\cos^2\theta)^\ell. +``` +This package does not actually use this form; we generalize it to +spin-weighted spherical harmonics, and express those as functions of a +quaternion. Nonetheless, we choose our conventions to ensure that the +generalized definition reduces to this expression for spin weight +``s=0``, and transforming the spherical coordinates as ``(\theta, +\phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` + +## Spin-weighted functions +[Newman_1966](@citet) define the spherical tangent basis vectors +as +```math +m^\mu = \frac{1}{\sqrt{2}} \left( + \boldsymbol{\theta} + i \boldsymbol{\phi} +\right) +``` +and discuss spin weight in terms of the rotation +```math +(m^\mu)' = e^{i\psi} m^\mu, +``` +where the tangent basis rotates but we are "keeping the +coordinates fixed". We find that we can emulate this using Euler +angles ``(\phi, \theta, -\psi)``. Note the negative sign in the +last angle. As usual, this rotates the positive ``𝐳`` axis to +the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / +\sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have +spin weight ``s`` if it transforms as +```math +\eta' = e^{is\psi} \eta. +``` +In our notation, we can realize this function as a function of +Euler angles, and that equation becomes +```math +\eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), +``` +or +```math +\eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). +``` +This is the crucial definition giving us the behavior of +spin-weighted functions: they are eigenfunctions of the operator +``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also +immediately find the spin-raising and -lowering operators — +canonically denoted ``\eth`` and ``\bar{\eth}`` — from the +commutator relations for ``R``: +```math +\begin{aligned} +\eth \eta &= \left(R_x + i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right), \\ +\bar{\eth} \eta &= \left(R_x - i R_y\right)\eta + = -\sin^s \theta \left\{ + \frac{\partial}{\partial \theta} + - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} + \right\} \left(\eta \sin^{-s} \theta\right). +\end{aligned} +``` +Here, we have used the full expressions for ``R_x`` and ``R_y`` +given above in terms of Euler angles, replacing the derivatives +with respect to ``\gamma`` by a factor of ``-i s``, and converting +the remaining Euler angles to spherical coordinates. This allows +us to write them as if they were operators on the 2-sphere, even +though this is mathematically ill-defined and spin-weighted +functions really must be defined on the 3-sphere. + +## Wigner D-matrices + + +## Spin-weighted spherical harmonics + + From 6037817b6b90c323e0b89c9c95c4dc80ca6d998a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 11:26:48 -0500 Subject: [PATCH 066/329] Shorten page titles --- docs/src/conventions/details.md | 4 ++-- docs/src/conventions/summary.md | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index aace35c0..a8503273 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -1,9 +1,9 @@ -# Details of conventions +# Details This page carefully works through all the conventions used in this package, starting from first principles to motivate the choices and ensure that each step is on firm footing. The [previous page](@ref -"Summary of conventions") collects the results in a more concise form. +"Summary") collects the results in a more concise form. Note that we will use Euler angles and spherical coordinates here, but *they are not used internally in this package* — though conversion diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index bb4d47b8..bf6325e2 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -1,9 +1,9 @@ -# Summary of conventions +# Summary This page lists the most important conventions used in this package. -The [following page](@ref "Details of conventions") derives all of -these conventions from the very basics (i.e., starting from Cartesian -coordinates of 3-dimensional space). +The [following page](@ref "Details") derives all of these conventions +from the very basics (i.e., starting from Cartesian coordinates of +3-dimensional space). Note that we will use Euler angles and spherical coordinates here, but *they are not used internally in this package* — though conversion @@ -41,17 +41,18 @@ A rotation represented by the unit quaternion ``𝐑`` acts on a vector assumed to be right-handed, so that a quaternion characterizing the rotation through an angle ``\vartheta`` about a unit vector ``𝐮`` can be expressed as ``𝐑 = \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` -would deliver the same rotation, which is why the group of unit +would deliver the same *rotation*, which makes the group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* -of the group of rotations ``\mathrm{SO}(3)``. +of the group of rotations ``\mathrm{SO}(3)``. Nonetheless, ``𝐑`` and +``-𝐑`` are distinct quaternions, and represent distinct "spinors". ## Euler angles Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles ``\alpha`` -and ``\beta`` take values in ``[0, 2\pi)``. The angle ``\beta`` takes -values in ``[0, 2\pi]`` to parametrize the group of unit quaternions -``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, \pi]`` to -parametrize the group of rotations ``\mathrm{SO}(3)``. +and ``\gamma`` take values in ``[0, 2\pi)``. The angle ``\beta`` +takes values in ``[0, 2\pi]`` to parametrize the group of unit +quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, \pi]`` +to parametrize the group of rotations ``\mathrm{SO}(3)``. ## Spherical coordinates as Euler angles A point on the unit sphere with spherical coordinates ``(\theta, From 46dd07619f7a845378dca5073a66fbbbd7f319d8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 14:30:05 -0500 Subject: [PATCH 067/329] Sort out Mathematica's Euler angles --- test/conventions/mathematica.jl | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/test/conventions/mathematica.jl b/test/conventions/mathematica.jl index 9a936e2f..9d0b1b05 100644 --- a/test/conventions/mathematica.jl +++ b/test/conventions/mathematica.jl @@ -6,11 +6,36 @@ page](https://reference.wolfram.com/language/ref/WignerD.html). > The Wolfram Language uses phase conventions where ``D^j_{m_1, m_2}(\psi, \theta, \phi) = \exp(i m_1 \psi + i m_2 \phi) D^j_{m_1, m_2}(0, \theta, 0)``. -> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` -> ``-\sqrt{2} e^{i \phi} \cos\frac{\theta}{2} \sin\frac{\theta}{2}`` +> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` ``-\sqrt{2} e^{i \phi} \cos\frac{\theta}{2} +> \sin\frac{\theta}{2}`` > `WignerD[{𝓁, 0, m}, θ, ϕ] == Sqrt[(4 π)/(2 𝓁 + 1)] SphericalHarmonicY[𝓁, m, θ, ϕ]` -> `WignerD[{j, m1, m2},ψ, θ, ϕ]] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, ϕ]]` +> `WignerD[{j, m1, m2},ψ, θ, ϕ]] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, +> ϕ]]` + +The Euler angles are defined generally such that + +> `EulerMatrix[{α,β,γ},{a,b,c}]` is equivalent to ``R_{α,a} R_{β,b} R_{γ,c}``, where +> ``R_{α,a}``=`RotationMatrix[α,UnitVector[3,a]]`, etc. + +and + +> `EulerMatrix[{α,β,γ}]` is equivalent to `EulerMatrix[{α,β,γ},{3,2,3}]` + +(representing the ``z-y-z`` convention). + +Finally, we find that they say that `EulerMatrix`` corresponds to three rotations: + +```mathematica +rα = RotationMatrix[α, {0, 0, 1}]; +rβ = RotationMatrix[β, {0, 1, 0}]; +rγ = RotationMatrix[γ, {0, 0, 1}]; + +Simplify[rα . rβ . rγ == EulerMatrix[{α, β, γ}]] +``` + +This agrees with the conventions used in this package, so we can directly compare +expressions in terms of Euler angles. """ From ae1fa5f814923b1f122145c151965036e49bf734 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 14:30:42 -0500 Subject: [PATCH 068/329] Compare to Wikipedia's rigid rotor angular-momentum operators --- docs/literate_input/euler_angular_momentum.jl | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 3c463676..9348e4ca 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -240,7 +240,33 @@ nothing #hide # In their description of the Wigner 𝔇 functions as wave functions of a rigid symmetric # top, [Varshalovich_1988](@citet) provide equivalent expressions in Eqs. (6) and (7) of -# their Sec. 4.2. +# their Sec. 4.2 — except that ``R_x`` and ``R_z`` have the wrong signs. +# [Wikipedia](https://en.wikipedia.org/wiki/Wigner_D-matrix#Properties_of_the_Wigner_D-matrix), +# meanwhile, provides equivalent expressions, except that their ``\hat{\mathcal{P}}`` has +# (consistently) the opposite sign to ``R`` defined here. + +# Note that the Wikipedia convention is actually entirely sensible — maybe more sensible +# than the one we use. In that convention ``\hat{J}`` is in the inertial frame, whereas +# ``\hat{P}`` is exactly that operator in the body-fixed frame. In our notation, we have +# ```math +# \begin{align} +# R_𝐮 f(𝐑) +# &= +# -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} f\left(𝐑\, e^{-\epsilon 𝐮/2}\right) \\ +# &= +# -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} +# f\left(𝐑\, e^{-\epsilon 𝐮/2}\, 𝐑^{-1}\, 𝐑\right) \\ +# &= +# -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} +# f\left(e^{-\epsilon 𝐑\, 𝐮\, 𝐑^{-1}/2}\, 𝐑\right) \\ +# &= +# -L_{𝐑\, 𝐮\, 𝐑^{-1}} f(𝐑), +# \end{align} +# ``` +# which says that ``R`` is just the *negative of* the ``L`` operator transformed to the +# body-fixed frame. That negative sign is slightly unnatural, but the reason we choose to +# define ``R`` in this way is for its more natural connection to the literature on +# spin-weighted spherical functions. # ### Commutators From 7a6301d882f5188384817c5b5be282138aebb5d9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 8 Feb 2025 22:48:59 -0500 Subject: [PATCH 069/329] More serious work on D conventions --- docs/src/conventions/details.md | 41 ++++++++++++++++ docs/src/conventions/summary.md | 84 +++++++++++++++++++++++++-------- test/conventions/sakurai.jl | 34 +++++++++++++ 3 files changed, 139 insertions(+), 20 deletions(-) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index a8503273..ef4b83d2 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -908,6 +908,47 @@ distinct, this can only be true if ``\int f_u f_v=0``. Laplacian on the 3-sphere. +## Wigner's 𝔇 matrices + +[Sakurai_1994](@citet) says that + +> [...] rotations affect physical systems, the state ket corresponding +> to a rotated system is expected to look different from the state ket +> corresponding to the original unrotated system. Given a rotation +> operation ``R``, characterized by a ``3\times 3`` orthogonal matrix +> ``R``, we associate an operator ``\mathscr{D}(R)`` in the +> appropriate ket space such that +> ```math +> |\alpha\rangle_R = \mathscr{D}(R) |\alpha\rangle, +> ``` +> ``|\alpha\rangle_R`` and ``|\alpha\rangle`` stand for the kets of +> the rotated and original system, respectively. + +If the field is represented as a function ``f(𝐑)``, then rotating the +field by ``e^{\epsilon 𝐮/2}`` is equivalent to rotating the argument +of the function by ``e^{-\epsilon 𝐮/2}``: +```math +\begin{aligned} +f\left(𝐑\right) +&\to +f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +&\approx +f\left(𝐑\right) + \epsilon \left. \frac{d}{d\epsilon} \right|_{\epsilon=0} +f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +&= +f\left(𝐑\right) - i \epsilon L_𝐮 f\left(𝐑\right). +\end{aligned} +``` +This final expression is precisely equivalent to Sakurai's Eq. (3.1.15): +```math +\mathscr{D}\left(\hat{\mathbf{n}}, d\phi \right) += +1 - i \left( \mathbf{J} \cdot \hat{\mathbf{n}} \right) d\phi. +``` + + + + ## Representation theory / harmonic analysis - Representations show up in Fourier analysis on groups - Peter-Weyl theorem diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index bf6325e2..ec235393 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -141,7 +141,7 @@ L_y = i \left\{ L_z = -i \frac{\partial} {\partial \phi} ``` The ``R`` operators make less sense for a function of spherical -coordinates, because of their inherent dependence on ``\gamma``. We +coordinates, because of their inherent dependence on ``\gamma.`` We will come back to them, however, when we consider spin-weighted functions — which are inherently ill-defined on the 2-sphere, but can be interpreted as restrictions of functions on the 3-sphere with this @@ -149,22 +149,34 @@ special "weight" property. ## Spherical harmonics There is essentially no disagreement in the literature about the -definitions of the spherical harmonics, so we adopt the standard -expressions. Explicitly, in terms of spherical coordinates, +definitions of the spherical harmonics, so we adopt a function that is +consistent with the standard expressions. More specifically, this +package defines the spherical harmonics in terms of Wigner's 𝔇 +matrices, by way of the spin-weighted spherical harmonics, as a +function of a quaternion. + +For concreteness, however, we can write the standard expression in +terms of spherical coordinates. This is what our definition will +reduce to for spin weight ``s=0``, and transforming the spherical +coordinates into a quaternion in the way given above. Explicitly, in +terms of spherical coordinates, that expression is ```math -Y_{\ell, m}(\theta, \phi) -= -\sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-m)!}{(\ell+m)!}} -e^{im\phi} -(-1)^{\ell+m} \frac{(1-\cos^2\theta)^{m/2}} {2^\ell \ell!} -\frac{d^{\ell+m}}{d\cos\theta^{\ell+m}} (1-\cos^2\theta)^\ell. +\begin{align} + Y_{l,m} + &= + \sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + \sum_{k = k_1}^{k_2} + \frac{(-1)^k \ell! [(\ell+m)!(\ell-m)!]^{1/2}} + {(\ell+m-k)!(\ell-k)!k!(k-m)!} + \\ &\qquad \times + \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m-2k} + \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-m} +\end{align} ``` -This package does not actually use this form; we generalize it to -spin-weighted spherical harmonics, and express those as functions of a -quaternion. Nonetheless, we choose our conventions to ensure that the -generalized definition reduces to this expression for spin weight -``s=0``, and transforming the spherical coordinates as ``(\theta, -\phi) \mapsto \exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2).`` +where ``k_1 = \textrm{max}(0, m)`` and ``k_2=\textrm{min}(\ell+m, +\ell)``. Again, we must emphasize that this package does not actually +use this form; it is just shown here to make it easier to compare to +other sources. ## Spin-weighted functions [Newman_1966](@citet) define the spherical tangent basis vectors @@ -199,9 +211,19 @@ or ``` This is the crucial definition giving us the behavior of spin-weighted functions: they are eigenfunctions of the operator -``R_z = i \partial_\gamma`` with eigenvalue ``s``. We can also -immediately find the spin-raising and -lowering operators — -canonically denoted ``\eth`` and ``\bar{\eth}`` — from the +``R_z = i \partial_\gamma`` with eigenvalue ``s``. + +We can make this a little less dependent on the choice of Euler +angles by writing ``\eta`` not as a function of Euler angles, but as +a function of a quaternion. We then have +```math +\eta(\mathbf{Q}\, e^{\gamma 𝐤/2}) = e^{-is\gamma} \eta(\mathbf{Q}), +``` +which means that spin-weighted functions are eigenfunctions of the +operator ``R_𝐤`` with eigenvalue ``s``. + +We can also immediately find the spin-raising and -lowering operators +— canonically denoted ``\eth`` and ``\bar{\eth}`` — from the commutator relations for ``R``: ```math \begin{aligned} @@ -225,9 +247,31 @@ us to write them as if they were operators on the 2-sphere, even though this is mathematically ill-defined and spin-weighted functions really must be defined on the 3-sphere. -## Wigner D-matrices +## Spin-weighted spherical harmonics +Given the (scalar) spherical harmonics, and the spin-raising and +-lowering operators, we can now define the spin-weighted spherical +harmonics. These are obtained by applying the relevant operator to +the scalar spherical harmonics the specified number of times, and +normalizing. Again, this results in a function of a quaternion, but +we can write it in terms of spherical coordinates purely for the sake +of comparison with other sources. The expression is +```math +\begin{align} + {}_{s}Y_{l,m} + &= + (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + \sum_{k = k_1}^{k_2} + \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell-s)!(\ell+s)!]^{1/2}} + {(\ell+m-k)!(\ell+s-k)!k!(k-s-m)!} + \\ &\qquad \times + \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m+s-2k} + \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-s-m} +\end{align} +``` +where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, +\ell+s)``. -## Spin-weighted spherical harmonics +## Wigner D-matrices diff --git a/test/conventions/sakurai.jl b/test/conventions/sakurai.jl index 187d8a92..8be8bdea 100644 --- a/test/conventions/sakurai.jl +++ b/test/conventions/sakurai.jl @@ -17,6 +17,40 @@ The conclusion here is that Sakurai's Yₗᵐ(θ, ϕ) is the same as ours, but h - On p. 194 he gives the expression in terms of Euler angles. - On p. 223 he gives an explicit formula for ``d``. - On p. 203 he relates ``\mathcal{D} to Y_{\ell}^m$ (note the upper index of ``m``). + + +Below (1.6.14), we find the translation operator acts as +``\mathscr{T}_{dx'} \alpha(x') = \alpha(x' - dx')``. Then Eq. +(1.6.32) +```math +\mathscr{T}_{dx'} = 1 - i p\, dx', +``` +for infinitesimal ``dx'``. Eq. (1.7.17) gives the momentum operator +as ``p \alpha(x') = -i \partial_{x'} \alpha(x')``. Combining these, +we can verify consistency: +```math +\mathscr{T}_{dx'} \alpha(x') += +\alpha(x' - dx') += +\alpha(x') - \partial_{x'}\, \alpha(x')\, dx', +``` +which is exactly what we expect from Taylor expanding ``\alpha(x' - +dx')``. + + +```math +\begin{aligned} +f\left(𝐑\right) +&\to +f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +&\approx +f\left(𝐑\right) + \epsilon \left. \frac{d}{d\epsilon} \right|_{\epsilon=0} +f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +&= +f\left(𝐑\right) - i \epsilon L_𝐮 f\left(𝐑\right) +``` + """ @testmodule Sakurai begin From 4d614a54c5b2c48b27c6ecbac8c7be3853b4154d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 9 Feb 2025 11:14:07 -0500 Subject: [PATCH 070/329] Find non-d behavior of D --- docs/src/conventions/details.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index ef4b83d2..68a16ed2 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -946,7 +946,34 @@ This final expression is precisely equivalent to Sakurai's Eq. (3.1.15): 1 - i \left( \mathbf{J} \cdot \hat{\mathbf{n}} \right) d\phi. ``` - +Now, we can write the eigenkets of ``L^2`` and ``L_z`` as ``|\ell, +m\rangle``, where the eigenvalues are ``\ell(\ell+1)`` and ``m``, +respectively. Finally, define the 𝔇 matrix as (Eq. 3.5.42) +```math +𝔇^{(\ell)}_{m',m}(R) += +\langle \ell, m' | 𝔇(R) | \ell, m \rangle. +``` +Sakurai notes the important result that (Eq. 3.5.46) +```math +𝔇^{(\ell)}_{m'',m}(R_1\, R_2) += +\sum_{m'} 𝔇^{(\ell)}_{m'',m'}(R_1) 𝔇^{(\ell)}_{m',m}(R_2), +``` +and we can readily find the essential behavior with respect to the +first and last Euler angles (Eq. 3.5.50): +```math +\begin{aligned} +𝔇^{(\ell)}_{m',m}(\alpha, \beta, \gamma) +&= +\langle \ell, m' | + \exp[-iL_z \alpha]\exp[-iL_y \beta]\exp[-iL_z \gamma] +| \ell, m \rangle \\ +&= +\exp[-i(m' \alpha+m\gamma)] +\langle \ell, m' | \exp[-iL_y \beta] | \ell, m \rangle. +\end{aligned} +``` ## Representation theory / harmonic analysis From e1ef1dd3492aacafb4cbe75be68c6b2cd3282057 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 10 Feb 2025 13:49:03 -0500 Subject: [PATCH 071/329] Specify the edition of Edmonds --- docs/src/references.bib | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/references.bib b/docs/src/references.bib index 8d02bc1b..550ab263 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -98,7 +98,8 @@ @book{Edmonds_2016 publisher = {Princeton University Press}, author = {Edmonds, A. R.}, month = aug, - year = 2016 + year = 1960, + edition = {second}, } @article{Elahi_2018, From a34d300dba2840f89e3644de771ddd1eaa1dc2f8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 10 Feb 2025 13:49:59 -0500 Subject: [PATCH 072/329] Figure out some Edmonds conventions --- docs/src/conventions/comparisons.md | 162 ++++++++++++++++++++++------ 1 file changed, 128 insertions(+), 34 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index e9d320e3..829a2d7a 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -30,7 +30,112 @@ the same exact expression for the (scalar) spherical harmonics. ## Condon-Shortley -## Wigner +## Edmonds + +[Edmonds_2016](@citet) is a standard reference for the theory of +angular momentum. + +In Sec. 1.3 he actually does a fair job of defining the Euler angles. +The upshot is that his definition agrees with ours, though he uses the +"active" definition style. That is, the rotations are to be performed +successively in order: + +> 1. A rotation ``\alpha(0 \leq \alpha < 2\pi)`` about the ``z``-axis, +> bringing the frame of axes from the initial position ``S`` into +> the position ``S'``. The axis of this rotation is commonly +> called the *vertical*. +> +> 2. A rotation ``\beta(0 \leq \beta < \pi)`` about the ``y``-axis of +> the frame ``S'``, called the *line of nodes*. Note that its +> position is in general different from the initial position of the +> ``y``-axis of the frame ``S``. The resulting position of the +> frame of axes is symbolized by ``S''``. +> +> 3. A rotation ``\gamma(0 \leq \gamma < 2\pi)`` about the ``z``-axis +> of the frame of axes ``S''``, called the *figure axis*; the +> position of this axis depends on the previous rotations +> ``\alpha`` and ``\beta``. The final position of the frame is +> symbolized by ``S'''``. + +I would simply write the "``y``-axis of the frame ``S'``" as ``y'``, +and so on. In quaternionic language, I would write these rotations as +``\exp[\gamma 𝐤''/2]\, \exp[\beta 𝐣'/2]\, \exp[\alpha 𝐤/2]``. But +we also have +```math +\exp[\beta 𝐣'/2] = \exp[\alpha 𝐤/2]\, \exp[\beta 𝐣'/2]\, \exp[-\alpha 𝐤/2] +``` +and so on for the third rotation, so any easy calculation shows that +```math +\exp[\gamma 𝐤''/2]\, \exp[\beta 𝐣'/2]\, \exp[\alpha 𝐤/2] += +\exp[\alpha 𝐤/2]\, \exp[\beta 𝐣/2]\, \exp[\gamma 𝐤/2], +``` +which is precisely our definition. + +The spherical coordinates are implicitly defined by + +> It should be noted that the polar coordinates ``\varphi, \theta`` +> with respect to the original frame ``S`` of the ``z``-axis in its +> final position are identical with the Euler angles ``\alpha, \beta`` +> respectively. + +Again, this agrees with our definition. + +His expression for the angular-momentum operator in Euler angles — Eq. +(2.2.2) — agrees with ours: +```math +\begin{aligned} +L_x &= -i \hbar \left\{ + -\frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + - \sin\alpha \frac{\partial} {\partial \beta} + + \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} +\right\}, +\\ +L_y &= -i \hbar \left\{ + -\frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} + + \cos\alpha \frac{\partial} {\partial \beta} + +\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} +\right\}, +\\ +L_z &= -i \hbar \frac{\partial} {\partial \alpha}. +\end{aligned} +``` +(The corresponding restriction to spherical coordinates also precisely +agrees with our results, with the extra factor of ``\hbar``.) + +Unfortunately, there is disagreement over the definition of the +Wigner D-matrices. In Eq. (4.1.12) he defines +```math +\mathcal{D}_{\alpha \beta \gamma} = +\exp\big( \frac{i\alpha}{\hbar} J_z\big) +\exp\big( \frac{i\beta}{\hbar} J_y\big) +\exp\big( \frac{i\gamma}{\hbar} J_z\big), +``` +which is the *conjugate* of most other definitions. + + +## Goldberg + +Eq. (3.11) of [GoldbergEtAl_1967](@citet) naturally extends to +```math + {}_sY_{\ell, m}(\theta, \phi, \gamma) + = + \left[ \left(2\ell+1\right) / 4\pi \right]^{1/2} + D^{\ell}_{-s,m}(\phi, \theta, \gamma), +``` +where Eq. (3.4) also shows that ``D^{\ell}_{m', m}(\alpha, \beta, +\gamma) = D^{\ell}_{m', m}(\alpha, \beta, 0) e^{i m' \gamma}``, +so we have +```math + {}_sY_{\ell, m}(\theta, \phi, \gamma) + = + {}_sY_{\ell, m}(\theta, \phi)\, e^{-i s \gamma}. +``` +This is the most natural extension of the standard spin-weighted +spherical harmonics to ``\mathrm{Spin}(3)``. In particular, the +spin-weight operator is ``i \partial_\gamma``, which suggests that it +will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i +\partial_\gamma``. ## Newman-Penrose @@ -113,41 +218,10 @@ or Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. -## Goldberg - -Eq. (3.11) of [GoldbergEtAl_1967](@citet) naturally extends to -```math - {}_sY_{\ell, m}(\theta, \phi, \gamma) - = - \left[ \left(2\ell+1\right) / 4\pi \right]^{1/2} - D^{\ell}_{-s,m}(\phi, \theta, \gamma), -``` -where Eq. (3.4) also shows that ``D^{\ell}_{m', m}(\alpha, \beta, -\gamma) = D^{\ell}_{m', m}(\alpha, \beta, 0) e^{i m' \gamma}``, -so we have -```math - {}_sY_{\ell, m}(\theta, \phi, \gamma) - = - {}_sY_{\ell, m}(\theta, \phi)\, e^{-i s \gamma}. -``` -This is the most natural extension of the standard spin-weighted -spherical harmonics to ``\mathrm{Spin}(3)``. In particular, the -spin-weight operator is ``i \partial_\gamma``, which suggests that it -will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i -\partial_\gamma``. - -## Wikipedia +## LALSuite ## Mathematica -## SymPy - -## Sakurai - -## Thorne - -## Torres del Castillo - ## NINJA Combining Eqs. (II.7) and (II.8) of [Ajith_2007](@citet), we have @@ -186,8 +260,24 @@ get rid of it: where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, \ell+s)``. +## SymPy -## LALSuite +SymPy gives what I would consider to be the *conjugate* D matrix of +the *inverse* rotation. Specifically, the +[source](https://github.com/sympy/sympy/blob/b4ce69ad5d40e4e545614b6c76ca9b0be0b98f0b/sympy/physics/wigner.py#L1136-L1191) +cites [Edmonds_2016](@citet) (4.1.12) when defining +```math +\mathcal{D}_{\alpha \beta \gamma} = +\exp\big( \frac{i\alpha}{\hbar} J_z\big) +\exp\big( \frac{i\beta}{\hbar} J_y\big) +\exp\big( \frac{i\gamma}{\hbar} J_z\big). +``` + +## Sakurai + +## Thorne + +## Torres del Castillo ## Varshalovich et al. @@ -348,3 +438,7 @@ is some subtlety involving the order of operations and passing to the consistent, and fit in nicely with the spin-weighted function literature; maybe Varshalovich et al. are just doing something different. + +## Wikipedia + +## Wigner From a4a2761abfd3376950b45cce579f5f68a402beae Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 10 Feb 2025 13:50:13 -0500 Subject: [PATCH 073/329] Throw together some junk on computing d --- docs/src/conventions/details.md | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 68a16ed2..74f316de 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -976,6 +976,51 @@ first and last Euler angles (Eq. 3.5.50): ``` +Using +```math +L_y = (L₊ − L₋) / (2i) +``` +we can expand +```math +exp[-iL_y β] += +Σ_k (-iL_y β)^k / k! += +Σ_k (L₋ - L₊)^k (β/2)^k / k! +``` + +Now, writing ``d_+(X) = [L_+, X]``, Eq. (9) of https://arxiv.org/pdf/1707.03861 says +```math +(L₋ - L₊)^k = \sum_{j=0}^k \binom{k, j} ((L₋ - d_+)^j 1) (-L₊)^{k-j} +``` +The sum will automatically be zero unless ``m+k-j ≤ ℓ`` — which means ``j ≥ m+k-ℓ`` +```math +(-L₊)^{k-j}|ℓ,m\rangle = (-1)^{k-j} \sqrt{\frac{(\ell+m+k-j)!}{(\ell+m)!},\frac{(\ell-m)!}{(\ell-m-k+j)!}} |ℓ,m+k-j\rangle +``` + +``[L₊, L₋] = 2 L_z`` + +``[L_z, L_\pm] = \pm L_\pm`` + +I wonder if there's a nicer approach using the symmetry transformation +Edmonds notes in Sec. 4.5 (and credits to Wigner) — or the presumably +equivalent one McEwan and Wieux use (and credit to Risbo): +```math +\exp\left[ \beta 𝐣 / 2 \right] += +\exp\left[ \pi 𝐤 / 4 \right] +\exp\left[ \pi 𝐣 / 4 \right] +\exp\left[ \beta 𝐤 / 2 \right] +\exp\left[ -\pi 𝐣 / 4 \right] +\exp\left[ -\pi 𝐤 / 4 \right] +``` +The 𝔇 matrices corresponding to the ``𝐤`` rotations are simple +phases, which converts the problem into one of finding the 𝔇 matrices +for the ``𝐣`` rotations through angles of ``\pm\pi/2`` — which are +presumably simpler to compute. See, e.g., Varshalovich's Eq. +4.16.(5), where they are given by purely combinatorial terms. + + ## Representation theory / harmonic analysis - Representations show up in Fourier analysis on groups - Peter-Weyl theorem From 98bc4539aef3093a018575c176ec4199b85d3f02 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 12 Feb 2025 09:39:16 -0500 Subject: [PATCH 074/329] Clarify second-hand citation --- docs/src/conventions/details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 74f316de..21fd44be 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -1004,7 +1004,7 @@ The sum will automatically be zero unless ``m+k-j ≤ ℓ`` — which means ``j I wonder if there's a nicer approach using the symmetry transformation Edmonds notes in Sec. 4.5 (and credits to Wigner) — or the presumably -equivalent one McEwan and Wieux use (and credit to Risbo): +equivalent one McEwan and Wieux use (and credit Risbo): ```math \exp\left[ \beta 𝐣 / 2 \right] = From 6294c420dc49a57072372399671ab730cae1ed92 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 12 Feb 2025 09:39:43 -0500 Subject: [PATCH 075/329] Figure out some SymPy details, and more info for Varshalovich --- docs/src/conventions/comparisons.md | 69 +++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 829a2d7a..a7f4291d 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -262,16 +262,37 @@ where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, ## SymPy -SymPy gives what I would consider to be the *conjugate* D matrix of -the *inverse* rotation. Specifically, the +There is no specific Euler angle convention in SymPy, however it is +informative to see what the `sympy.algebras.Quaternion.from_euler` +class method does. You can specify + +SymPy uses what I would consider just a wrong expression for ``D``. +Specifically, the [source](https://github.com/sympy/sympy/blob/b4ce69ad5d40e4e545614b6c76ca9b0be0b98f0b/sympy/physics/wigner.py#L1136-L1191) -cites [Edmonds_2016](@citet) (4.1.12) when defining +cites [Edmonds_2016](@citet) when defining ```math \mathcal{D}_{\alpha \beta \gamma} = \exp\big( \frac{i\alpha}{\hbar} J_z\big) \exp\big( \frac{i\beta}{\hbar} J_y\big) \exp\big( \frac{i\gamma}{\hbar} J_z\big). ``` +But that is an incorrect copy of Edmonds' Eq. (4.1.9), in which the +``\alpha`` and ``\gamma`` on the right-hand side are swapped. The +code also implements D in the `wigner_d` function as (essentially) +```python +exp(I*mprime*alpha)*d[i, j]*exp(I*m*gamma) +``` +even though the actual equation Eq. (4.1.12) says +```math +\mathscr{D}^{(j)}_{m' m}(\alpha \beta \gamma) = +\exp i m' \gamma d^{(j)}_{m' m}(\alpha, \beta) \exp(i m \alpha). +``` +The ``d`` matrix appears to be implemented consistently with Edmonds, +and thus not affected. + +Basically, it appears that SymPy just swapped the order of the Euler +angles relative to Edmonds, who already introduced a conjugate to the +definition of the D matrix. ## Sakurai @@ -281,6 +302,48 @@ cites [Edmonds_2016](@citet) (4.1.12) when defining ## Varshalovich et al. +[Varshalovich_1988](@citet) has a fairly decent comparison of +definitions related to the rotation matrix by previous authors. + +Eq. 1.4.(31) defines the operator +```math +\hat{D}(\alpha, \beta, \gamma) += +e^{-i\alpha \hat{J}_z} +e^{-i\beta \hat{J}_y} +e^{-i\gamma \hat{J}_z}, +``` +where the ``\hat{J}`` operators are defined in + +> In quantum mechanics the total angular momentum operator ``\hat{J}`` +> is defined as an operator which generates transformations of wave +> functions (state vectors) and quantum operators under infinitesimal +> rotations of the coordinate system (see Eqs. 2.1.(1) and 2.1.(2)). +> +> A transformation of an arbitrary wave function ``\Psi`` under +> rotation of the coordinate system through an infinitesimal angle +> ``\delta \omega`` about an axis ``\mathbf{n}`` may be written as +> ```math +> \Psi \to \Psi' = \left(1 - i \delta \omega \mathbf{n} \cdot \hat{J} \right)\Psi, +> ``` +> where ``\hat{J}`` is the total angular momentum operator. + +Eq. 4.1.(1) defines the Wigner D-functions according to +```math +\langle J M | \hat{D}(\alpha, \beta, \gamma) | J' M' \rangle += +\delta_{J J'} D^J_{M M'}(\alpha, \beta, \gamma). +``` +Eq. 4.3.(1) states +```math +D^J_{M M'}(\alpha, \beta, \gamma) += +e^{-i M \alpha} +d^J_{M M'}(\beta) +e^{-i M' \gamma} +``` + + Page 155 has a table of values for ``\ell \leq 5`` [Varshalovich_1988](@citet) distinguish in Sec. 1.1.3 between From 4c216d99fe9d60b4fe8e69332fe4ec1d0c3ff415 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:04:55 -0500 Subject: [PATCH 076/329] Add some explicit formulas for Y --- test/conventions/shankar.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test/conventions/shankar.jl diff --git a/test/conventions/shankar.jl b/test/conventions/shankar.jl new file mode 100644 index 00000000..fa5befce --- /dev/null +++ b/test/conventions/shankar.jl @@ -0,0 +1,20 @@ +@testmodule Shankar begin + +const 𝒾 = im + +include("../utilities/naive_factorial.jl") +import .NaiveFactorials: ❗ + + +# Shankar's explicit formulas from Eq. (12.5.39) +Y₀⁰(θ, ϕ) = 1 / √(4π) +Y₁⁻¹(θ, ϕ) = +√(3/(8π)) * sin(θ) * exp(-𝒾*ϕ) +Y₁⁰(θ, ϕ) = √(3/(4π)) * cos(θ) +Y₁⁺¹(θ, ϕ) = -√(3/(8π)) * sin(θ) * exp(+𝒾*ϕ) +Y₂⁻²(θ, ϕ) = √(15/(32π)) * sin(θ)^2 * exp(-2𝒾*ϕ) +Y₂⁻¹(θ, ϕ) = +√(15/(8π)) * sin(θ) * cos(θ) * exp(-𝒾*ϕ) +Y₂⁰(θ, ϕ) = √(5/(16π)) * (3cos(θ)^2 - 1) +Y₂⁺¹(θ, ϕ) = -√(15/(8π)) * sin(θ) * cos(θ) * exp(+𝒾*ϕ) +Y₂⁺²(θ, ϕ) = √(15/(32π)) * sin(θ)^2 * exp(+2𝒾*ϕ) + +end # @testmodule Shankar From 1501af0bb4b8145fcc9899d7c7caf2a5c60550ad Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:05:15 -0500 Subject: [PATCH 077/329] Remove silly sign change --- test/conventions/wikipedia.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/conventions/wikipedia.jl b/test/conventions/wikipedia.jl index bd8fb392..aaf9183e 100644 --- a/test/conventions/wikipedia.jl +++ b/test/conventions/wikipedia.jl @@ -66,9 +66,7 @@ function D_formula(n, m′, m, expiα::Complex{T}, expiβ::Complex{T}, expiγ::Complex{T}) where T # https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix - # Note that the convention in this package is conjugated relative to the convention - # used by Wikipedia, so we include that conjugation here. - return expiα^(m′) * d_formula(n, m′, m, expiβ) * expiγ^(m) + return expiα^(-m′) * d_formula(n, m′, m, expiβ) * expiγ^(-m) end end # module ExplicitWignerMatrices From be6d671d5277c4a19f0a4e90378a36e9d9fd2485 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:06:35 -0500 Subject: [PATCH 078/329] Add some details from Shankar --- docs/src/conventions/comparisons.md | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index a7f4291d..be2b3086 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -260,6 +260,69 @@ get rid of it: where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, \ell+s)``. + +## Sakurai + + +## Scipy + + +[`scipy.special.sph_harm_y`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.sph_harm_y.html) + + +## Shankar + + +Eq. (12.5.35) writes the spherical harmonics as +```math +Y_{\ell}^{m}(\theta, \phi) += +(-1)^\ell +\left[ \frac{(2\ell+1)!}{4\pi} \right]^{1/2} +\frac{1}{2^\ell \ell!} +\left[ \frac{(\ell+m)!}{(2\ell)!(\ell-m)!} \right]^{1/2} +e^{i m \phi} +(\sin \theta)^{-m} +\frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} +(\sin\theta)^{2\ell} +``` +for ``m \geq 0``, with (12.5.40) giving the expression +```math +Y_{\ell}^{-m}(\theta, \phi) += +(-1)^m \left( Y_{\ell}^{m}(\theta, \phi) \right)^\ast. +``` +The angular-momentum operators are given below (12.5.27) as +```math +\begin{aligned} +L_x &= i \hbar \left( + \sin\phi \frac{\partial} {\partial \theta} + + \cos\phi \cot\theta \frac{\partial} {\partial \phi} +\right), +\\ +L_y &= i \hbar \left( + -\cos\phi \frac{\partial} {\partial \theta} + + \sin\phi \cot\theta \frac{\partial} {\partial \phi} +\right), +\\ +L_z &= -i \hbar \frac{\partial} {\partial \phi}. +\end{aligned} +``` +In Exercise 12.5.7, the rotation operator is defined by +```math +U\left[ R(\alpha, \beta, \gamma) \right] += +e^{-i \alpha J_z/\hbar} +e^{-i \beta J_y/\hbar} +e^{-i \gamma J_z/\hbar}, +``` +That ``U`` becomes a ``D^{(j)}`` when the operator is acting on the +states ``|j, m\rangle`` for a given ``j``. Thus, while Shankar never +actually uses notation like ``D^{(j)}_{m', m}``, he does talk about +``\langle j, m' | D^{(j)}\left[ R(\alpha, \beta, \gamma) \right] | j, +m \rangle``. + + ## SymPy There is no specific Euler angle convention in SymPy, however it is From b5aea6910bafbf4ab9521f69e34a96b287b21d0c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:06:50 -0500 Subject: [PATCH 079/329] Add details from Mathematica --- docs/src/conventions/comparisons.md | 80 ++++++++++++++++++++++++++--- test/conventions/mathematica.jl | 37 ------------- 2 files changed, 73 insertions(+), 44 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index be2b3086..6f8d78d5 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -10,16 +10,23 @@ Among the items that would be good to compare are the following, when actually used by any of these sources: * Quaternions - Order of components - - Basis + - Basis and multiplication table - Operation as rotations * Euler angles * Spherical coordinates +* Angular momentum operators + - Fundamental definitions + - Expression in terms of spherical coordinates + - Expression in terms of Euler angles + - Right-derivative form * Spherical harmonics - Condon-Shortley phase - Formula * Spin-weighted spherical harmonics - Behavior under rotation * Wigner D-matrices + - Representation à la $\langle \ell, m' | e^{-i \alpha J_z} e^{-i \beta J_y} e^{-i \gamma J_z} | \ell, m \rangle$ + - Rotation of spherical harmonics - Order of indices - Conjugation - Function of rotation or inverse rotation @@ -137,6 +144,61 @@ spin-weight operator is ``i \partial_\gamma``, which suggests that it will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i \partial_\gamma``. + +## LALSuite + + +## Mathematica + +The Euler angles are defined generally such that + +> `EulerMatrix[{α,β,γ},{a,b,c}]` is equivalent to ``R_{α,a} R_{β,b} R_{γ,c}``, where +> ``R_{α,a}``=`RotationMatrix[α,UnitVector[3,a]]`, etc. + +and + +> `EulerMatrix[{α,β,γ}]` is equivalent to `EulerMatrix[{α,β,γ},{3,2,3}]` + +(representing the ``z-y-z`` convention). + +Finally, we find that they say that `EulerMatrix`` corresponds to three rotations: + +```mathematica +rα = RotationMatrix[α, {0, 0, 1}]; +rβ = RotationMatrix[β, {0, 1, 0}]; +rγ = RotationMatrix[γ, {0, 0, 1}]; + +Simplify[rα . rβ . rγ == EulerMatrix[{α, β, γ}]] +``` + +This agrees with the conventions used in this package, so we can directly compare +expressions in terms of Euler angles. + + +We can find conventions at [this +page](https://reference.wolfram.com/language/ref/WignerD.html). + +> The Wolfram Language uses phase conventions where ``D^j_{m_1, m_2}(\psi, \theta, \phi) = \exp(i m_1 \psi + i m_2 \phi) D^j_{m_1, m_2}(0, \theta, 0)``. + +> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` = ``-\sqrt{2} e^{i \phi} \cos\frac{\theta}{2} +> \sin\frac{\theta}{2}`` + +> `WignerD[{𝓁, 0, m}, θ, ϕ] == Sqrt[(4 π)/(2 𝓁 + 1)] SphericalHarmonicY[𝓁, m, θ, ϕ]` + +> `WignerD[{j, m1, m2},ψ, θ, ϕ] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, +> ϕ]]` + + +> For ``\ell \geq 0``, ``Y_\ell^m = \sqrt{(2\ell+1)/(4\pi)} \sqrt{(\ell-m)! / (\ell+m)!} P_\ell^m(\cos \theta) e^{im\phi}`` where ``P_\ell^m`` is the associated Legendre function. + +> The associated Legendre polynomials are defined by ``P_n^m(x) = (-1)^m (1-x^2)^{m/2}(d^m/dx^m)P_n(x)`` where ``P_n(x)`` is the Legendre polynomial. + +[NIST (14.7.13)](https://dlmf.nist.gov/14.7#E13) gives the Legendre polynomial for nonnegative integer ``n`` as +```math +P_n(x) = \frac{1}{2^n n!} \frac{d^n}{dx^n} (x^2 - 1)^n. +``` + + ## Newman-Penrose In their 1966 paper, [Newman_1966](@citet), Newman and Penrose first @@ -218,10 +280,6 @@ or Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. -## LALSuite - -## Mathematica - ## NINJA Combining Eqs. (II.7) and (II.8) of [Ajith_2007](@citet), we have @@ -357,8 +415,6 @@ Basically, it appears that SymPy just swapped the order of the Euler angles relative to Edmonds, who already introduced a conjugate to the definition of the D matrix. -## Sakurai - ## Thorne ## Torres del Castillo @@ -567,4 +623,14 @@ different. ## Wikipedia +Defining the operator +```math +\mathcal{R}(\alpha,\beta,\gamma) = e^{-i\alpha J_z}e^{-i\beta J_y}e^{-i\gamma J_z}, +``` +[Wikipedia defines the Wigner D-matrix](https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix) as +```math +D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | \mathcal{R}(\alpha,\beta,\gamma)| jm \rangle =e^{-im'\alpha } d^j_{m'm}(\beta)e^{-i m\gamma}. +``` + + ## Wigner diff --git a/test/conventions/mathematica.jl b/test/conventions/mathematica.jl index 9d0b1b05..fe665efc 100644 --- a/test/conventions/mathematica.jl +++ b/test/conventions/mathematica.jl @@ -1,41 +1,4 @@ raw""" -We can find conventions at [this -page](https://reference.wolfram.com/language/ref/WignerD.html). - -> The Wolfram Language uses phase conventions where ``D^j_{m_1, m_2}(\psi, \theta, \phi) = - \exp(i m_1 \psi + i m_2 \phi) D^j_{m_1, m_2}(0, \theta, 0)``. - -> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` ``-\sqrt{2} e^{i \phi} \cos\frac{\theta}{2} -> \sin\frac{\theta}{2}`` - -> `WignerD[{𝓁, 0, m}, θ, ϕ] == Sqrt[(4 π)/(2 𝓁 + 1)] SphericalHarmonicY[𝓁, m, θ, ϕ]` - -> `WignerD[{j, m1, m2},ψ, θ, ϕ]] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, -> ϕ]]` - -The Euler angles are defined generally such that - -> `EulerMatrix[{α,β,γ},{a,b,c}]` is equivalent to ``R_{α,a} R_{β,b} R_{γ,c}``, where -> ``R_{α,a}``=`RotationMatrix[α,UnitVector[3,a]]`, etc. - -and - -> `EulerMatrix[{α,β,γ}]` is equivalent to `EulerMatrix[{α,β,γ},{3,2,3}]` - -(representing the ``z-y-z`` convention). - -Finally, we find that they say that `EulerMatrix`` corresponds to three rotations: - -```mathematica -rα = RotationMatrix[α, {0, 0, 1}]; -rβ = RotationMatrix[β, {0, 1, 0}]; -rγ = RotationMatrix[γ, {0, 0, 1}]; - -Simplify[rα . rβ . rγ == EulerMatrix[{α, β, γ}]] -``` - -This agrees with the conventions used in this package, so we can directly compare -expressions in terms of Euler angles. """ From a4a0f9b5c857932322ebea2af45a5ae39066b933 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:11:12 -0500 Subject: [PATCH 080/329] Tweak some wording --- docs/src/conventions/comparisons.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 6f8d78d5..f2f3be66 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -69,9 +69,13 @@ and so on. In quaternionic language, I would write these rotations as ``\exp[\gamma 𝐤''/2]\, \exp[\beta 𝐣'/2]\, \exp[\alpha 𝐤/2]``. But we also have ```math -\exp[\beta 𝐣'/2] = \exp[\alpha 𝐤/2]\, \exp[\beta 𝐣'/2]\, \exp[-\alpha 𝐤/2] +\exp[\beta 𝐣'/2] = \exp[\alpha 𝐤/2]\, \exp[\beta 𝐣/2]\, \exp[-\alpha 𝐤/2] ``` -and so on for the third rotation, so any easy calculation shows that +so we can just swap the ``\alpha`` rotation with the ``\beta`` +rotation while dropping the prime from ``𝐣'``. We can do a similar +trick swapping the ``\alpha`` and ``\beta`` rotations with the +``\gamma`` rotation while dropping the double prime from ``𝐤''``. +That is, an easy calculation shows that ```math \exp[\gamma 𝐤''/2]\, \exp[\beta 𝐣'/2]\, \exp[\alpha 𝐤/2] = From 1c21711b0d9d895bc461604dfdd1406e502ce00d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 12:34:15 -0500 Subject: [PATCH 081/329] Add Zettili and auto-sort --- docs/src/references.bib | 56 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/src/references.bib b/docs/src/references.bib index 550ab263..0f537d8f 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -225,7 +225,6 @@ @book{HansonYakovlev_2002 doi = {10.1007/978-1-4757-3679-3} } - @article{Holmes_2002, doi = {10.1007/s00190-002-0216-2}, url = {https://doi.org/10.1007/s00190-002-0216-2}, @@ -285,6 +284,15 @@ @article{McEwen_2011 primaryClass = "cs.IT", } +@misc{NIST_DLMF, + title = "{NIST Digital Library of Mathematical Functions}", + howpublished = "\url{https://dlmf.nist.gov/}, Release 1.2.3 of 2024-12-15", + url = "https://dlmf.nist.gov/", + note = "F.~W.~J. Olver, A.~B. {Olde Daalhuis}, D.~W. Lozier, B.~I. Schneider, + R.~F. Boisvert, C.~W. Clark, B.~R. Miller, B.~V. Saunders, H.~S. Cohl, and + M.~A. McClain, eds." +} + @article{Newman_1966, doi = {10.1063/1.1931221}, url = {https://doi.org/10.1063/1.1931221}, @@ -299,15 +307,6 @@ @article{Newman_1966 journal = {Journal of Mathematical Physics} } -@misc{NIST_DLMF, - title = "{NIST Digital Library of Mathematical Functions}", - howpublished = "\url{https://dlmf.nist.gov/}, Release 1.2.3 of 2024-12-15", - url = "https://dlmf.nist.gov/", - note = "F.~W.~J. Olver, A.~B. {Olde Daalhuis}, D.~W. Lozier, B.~I. Schneider, - R.~F. Boisvert, C.~W. Clark, B.~R. Miller, B.~V. Saunders, H.~S. Cohl, and - M.~A. McClain, eds." -} - @article{Reinecke_2013, doi = {10.1051/0004-6361/201321494}, url = {https://doi.org/10.1051/0004-6361/201321494}, @@ -408,19 +407,6 @@ @article{UffordShortley_1932 pages = {167--175} } -@book{vanNeerven_2022, - address = {Cambridge}, - series = {Cambridge Studies in Advanced Mathematics}, - title = {Functional Analysis}, - isbn = {978-1-00-923247-0}, - url = - {https://www.cambridge.org/core/books/functional-analysis/62B852DFB4D6F11D21C04309DCF7584F}, - publisher = {Cambridge University Press}, - author = {van Neerven, Jan}, - year = 2022, - doi = {10.1017/9781009232487} -} - @book{Varshalovich_1988, address = {Singapore ; Teaneck, {NJ}, {USA}}, title = {Quantum theory of angular momentum: {I}rreducible tensors, spherical harmonics, @@ -472,3 +458,27 @@ @article{Xing_2019 normalized associated Legendre functions of ultra-high degree and order}, journal = {Journal of Geodesy} } + +@book{Zettili_2009, + address = {New York, {NY}}, + title = {Quantum Mechanics: {C}oncepts and Applications}, + isbn = {978-0-470-74656-1}, + shorttitle = {Quantum Mechanics}, + url = {http://ebookcentral.proquest.com/lib/cornell/detail.action?docID=416494}, + publisher = {John Wiley \& Sons, Incorporated}, + author = {Zettili, Nouredine}, + year = 2009 +} + +@book{vanNeerven_2022, + address = {Cambridge}, + series = {Cambridge Studies in Advanced Mathematics}, + title = {Functional Analysis}, + isbn = {978-1-00-923247-0}, + url = + {https://www.cambridge.org/core/books/functional-analysis/62B852DFB4D6F11D21C04309DCF7584F}, + publisher = {Cambridge University Press}, + author = {van Neerven, Jan}, + year = 2022, + doi = {10.1017/9781009232487} +} From 8b499443dd66b73a7509efedfa9e2ad5a65a008b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 13:06:45 -0500 Subject: [PATCH 082/329] Include Zettili's conventions --- docs/src/conventions/comparisons.md | 109 ++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index f2f3be66..7a328667 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -35,6 +35,25 @@ actually used by any of these sources: One major result of this is that almost everyone since 1935 has used the same exact expression for the (scalar) spherical harmonics. +When choosing my conventions, I intend to prioritize consistency (to +the extent that any of these references actually have anything to say +about the above items) with the following sources, in order: + +1. LALSuite +2. NINJA +3. Thorne / MTW +4. Goldberg +5. Newman-Penrose +6. Wikipedia +7. Sakurai +8. Shankar +9. Zettili + +I think that should be sufficient to find a consensus on conventions +for each of the above — with the possible exception of quaternions, +for which I have my own strong opinions. + + ## Condon-Shortley ## Edmonds @@ -638,3 +657,93 @@ D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | \mathcal{R}(\alpha,\beta,\ga ## Wigner + + + + +## Zettili + +[Zettili_2009](@citet) denotes by ``\hat{R}_z(\delta \phi)`` the + +> rotation of the coordinates of a *spinless* particle over an +> *infinitesimal* angle ``\delta \phi`` about the ``z``-axis + +and shows its action [Eq. (7.16)] +```math +\hat{R}_z (\delta \phi) \psi(r, \theta, \phi) += +\psi(r, \theta, \phi - \delta \phi). +``` + +> We may generalize this relation to a rotation of angle ``\delta +> \phi`` about an arbitrary axis whose direction is given by the unit +> vector ``\vec{n}``: + +```math +\hat{R}(\delta \phi) += +1 - \frac{i}{\hbar} \delta \phi \vec{n} \cdot \hat{\vec{L}}. +``` +This extends to finite rotation by defining the operator [Eq. (7.48)] +```math +\hat{R}(\alpha, \beta, \gamma) += +e^{-i\alpha J_z / \hbar} e^{-i\beta J_y / \hbar} e^{-i\gamma J_z / \hbar}. +``` +Equation (7.52) then defines +```math +D^{(j)}_{m', m}(\alpha, \beta, \gamma) += +\langle j, m' | \hat{R}(\alpha, \beta, \gamma) | j, m \rangle, +``` +So that [Eq. (7.54)] +```math +D^{(j)}_{m', m}(\alpha, \beta, \gamma) += +e^{-i (m' \alpha + m \gamma)} d^{(j)}_{m', m}(\beta), +``` +where [Eq. (7.55)] +```math +d^{(j)}_{m', m}(\beta) += +\langle j, m' | e^{-i\beta J_y / \hbar} | j, m \rangle. +``` +The explicit expression for ``d`` is [Eq. (7.56)] +```math +d^{(j)}_{m', m}(\beta) += +\sum_k (-1)^{k+m'-m} +\frac{\sqrt{(j+m)!(j-m)!(j+m')!(j-m')!}} +{(j-m'-k)!(j+m-k)!(k+m'-m)!k!} +\left(\cos\frac{\beta}{2}\right)^{2j+m-m'-2k} +\left(\sin\frac{\beta}{2}\right)^{m'-m+2k}. +``` +In Sec. 7.2.6, we find that if the operator ``\hat{R}(\alpha, \beta, +\gamma)`` rotates a vector pointing in the ``(\theta, \phi)`` to a +vector pointing in the ``(\theta', \phi')`` direction, then the +spherical harmonics transform as [Eq. (7.70)] +```math +Y_{\ell, m}^\ast (\theta', \phi') += +\sum_{m'} D^{(\ell)}_{m, m'}(\alpha, \beta, \gamma) Y_{\ell, m'}^\ast (\theta, \phi). +``` + +In Appendix B.1, we find that the spherical coordinates are related to +Cartesian coordinates in the usual (physicist's) way, and Eqs. +(B.25)—(B.27) give the components of the angular-momentum operator in +spherical coordinates as +```math +\begin{aligned} +L_x &= i \hbar \left( + \sin\phi \frac{\partial}{\partial \theta} + + \cot\theta \cos\phi \frac{\partial}{\partial \phi} +\right), +\\ +L_y &= i \hbar \left( + -\cos\phi \frac{\partial}{\partial \theta} + + \cot\theta \sin\phi \frac{\partial}{\partial \phi} +\right), +\\ +L_z &= -i \hbar \frac{\partial}{\partial \phi}. +\end{aligned} +``` \ No newline at end of file From 6c622fc671dfa128526cfe63b18350ffe867b77a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 14:56:49 -0500 Subject: [PATCH 083/329] Add Le Bellac and Cohen-Tannoudji --- docs/src/conventions/comparisons.md | 65 +++++++++++++++++++++++++++++ docs/src/references.bib | 25 +++++++++++ 2 files changed, 90 insertions(+) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 7a328667..df92fb0d 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -53,6 +53,71 @@ I think that should be sufficient to find a consensus on conventions for each of the above — with the possible exception of quaternions, for which I have my own strong opinions. +## Le Bellac + +[LeBellac_2006](@citet) (with Foreword by Cohen-Tannoudji) takes an +odd approach, defining [Eq. (10.32)] +```math +D^{(j)}_{m', m} \left[ \right] += +\langle j, m' | e^{-i\phi J_z} e^{-i\theta J_y} | j, m \rangle, +``` +but later allowing that ``e^{-i \psi J_z}`` usually goes on the +right-hand side of the others, in which case ``D^{(j)}(\theta, \phi) +\to D^{(j)}(\phi, \theta, \psi)``. Figure 10.1 shows that the +spherical coordinates are standard (physicist's) coordinates. + +Equation (10.65) shows the rotation law: +```math +Y_{\ell}^{m}\left( \mathcal{R}^{-1} \hat{r} \right) += +\sum_{m'} D^{(\ell)}_{m', m}(\mathcal{R}) Y_{\ell}^{m'}(\hat{r}), +``` +and Eq. (10.66) relates the spherical harmonics to the Wigner +D-matrices: +```math +D^{(\ell)}_{m, 0}(\theta, \phi) += +\sqrt{\frac{4\pi}{2\ell+1}} \left[Y_{\ell}^{m}(\theta, \phi)\right]^\ast. +``` + + +## Cohen-Tannoudji + +[CohenTannoudji_1991](@citet) derives the spherical harmonics in two +ways and gets two different, but equivalent, expressions in Complement +``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) +```math +Y_{l}^{m}(\theta, \phi) += +\frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l+m)!}{(l-m)!}} +e^{i m \phi} (\sin \theta)^m +\frac{d^{l-m}}{d(\cos \theta)^{l-m}} (\sin \theta)^{2l}, +``` +while the second is Eq. (30) +```math +Y_{l}^{m}(\theta, \phi) += +\frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l-m)!}{(l+m)!}} +e^{i m \phi} (\sin \theta)^m +\frac{d^{l+m}}{d(\cos \theta)^{l+m}} (\sin \theta)^{2l}. +``` + +In Complement ``\mathrm{B}_{\mathrm{VI}}`` he defines a rotation +operator ``R`` as acting on a state such that [Eq. (21)] +```math +\langle \mathbf{r} | R | \psi \rangle += +\langle \mathscr{R}^{-1} \mathbf{r} | \psi \rangle. +``` +For an infinitesimal rotation through angle ``d\alpha`` about the axis +``\mathbf{u}``, he shows [Eq. (49)] +```math +R_{\mathbf{u}}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha \mathbf{L}.\mathbf{u}. +``` + +Cohen-Tannoudji does not appear to define the Wigner D-matrices. + ## Condon-Shortley diff --git a/docs/src/references.bib b/docs/src/references.bib index 0f537d8f..0506e8f4 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -69,6 +69,18 @@ @article{BrauchartGrabner_2015 author = {Johann S. Brauchart and Peter J. Grabner} } +@book{CohenTannoudji_1991, + address = {New York}, + edition = {1st edition}, + title = {Quantum Mechanics}, + isbn = {978-0-471-16433-3}, + publisher = {Wiley}, + author = {{Cohen-Tannoudji}, Claude and Diu, Bernard and Laloe, Frank}, + month = jan, + year = 1991, + note = {bibtex: {Cohen-Tannoudji1991}} +} + @book{CondonShortley_1935, address = {London}, title = {The Theory Of Atomic Spectra}, @@ -254,6 +266,19 @@ @article{Kostelec_2008 journal = {Journal of Fourier Analysis and Applications} } +@book{LeBellac_2006, + address = {Cambridge}, + title = {Quantum Physics}, + isbn = {978-1-107-60276-2}, + url = + {https://www.cambridge.org/core/books/quantum-physics/9A3A0754B265D451C931E0C98E6C1ED9}, + publisher = {Cambridge University Press}, + author = {Le Bellac, Michel}, + translator = {{Forcrand-Millard}, Patricia de}, + year = 2006, + doi = {10.1017/CBO9780511616471}, +} + @book{Lee_2012, address = {New York, {NY}}, series = {Graduate Texts in Mathematics}, From 0812e304ec95dedf04eb0794f40023c61f619721 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 15:06:05 -0500 Subject: [PATCH 084/329] Account for change in ExplicitWignerMatrices.D_formula --- test/wigner_matrices/big_D.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/wigner_matrices/big_D.jl b/test/wigner_matrices/big_D.jl index 860d4747..4e5d4713 100644 --- a/test/wigner_matrices/big_D.jl +++ b/test/wigner_matrices/big_D.jl @@ -49,7 +49,7 @@ end n, m′, m, expiα, expiβ, expiγ ) 𝔇_recurrence = 𝔇[WignerDindex(n, m′, m)] - @test 𝔇_formula ≈ 𝔇_recurrence atol=200eps(T) rtol=200eps(T) + @test conj(𝔇_formula) ≈ 𝔇_recurrence atol=200eps(T) rtol=200eps(T) end end end @@ -81,7 +81,7 @@ end n, m′, m, expiα, expiβ, expiγ ) 𝔇_recurrence = 𝔇[WignerDindex(n, m′, m)] - @test 𝔇_formula ≈ 𝔇_recurrence atol=400eps(T) rtol=400eps(T) + @test conj(𝔇_formula) ≈ 𝔇_recurrence atol=400eps(T) rtol=400eps(T) end end end From dadce7355496e9a86f5bd29ffc2c8d4da633be4b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 21:06:20 -0500 Subject: [PATCH 085/329] Replace missing operator --- docs/src/conventions/comparisons.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index df92fb0d..fca92de2 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -58,7 +58,7 @@ for which I have my own strong opinions. [LeBellac_2006](@citet) (with Foreword by Cohen-Tannoudji) takes an odd approach, defining [Eq. (10.32)] ```math -D^{(j)}_{m', m} \left[ \right] +D^{(j)}_{m', m} \left[ \mathcal{R}(\theta, \phi) \right] = \langle j, m' | e^{-i\phi J_z} e^{-i\theta J_y} | j, m \rangle, ``` From 4fc566a56d4f5b0a8bb0d4fefff1865cde9e2c6c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:13:32 -0500 Subject: [PATCH 086/329] Add Shankar --- docs/src/references.bib | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/references.bib b/docs/src/references.bib index 0506e8f4..fc6b0a54 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -372,6 +372,15 @@ @book{Sakurai_1994 year = 1994 } +@book{Shankar_1994, + address = {New York}, + edition = {second}, + title = {Principles of Quantum Mechanics}, + publisher = {Plenum Press}, + author = {Shankar, Ramamurti}, + year = 1994 +} + @article{SommerEtAl_2018, title = {Why and How to Avoid the Flipped Quaternion Multiplication}, url = {http://arxiv.org/abs/1801.07478}, From b1d808702bd66ca5c4aab6b93f1eed8a51e54ec9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:13:51 -0500 Subject: [PATCH 087/329] Add LALSuite --- docs/src/references.bib | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/src/references.bib b/docs/src/references.bib index fc6b0a54..61f21936 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -1,6 +1,6 @@ @misc{Ajith_2007, doi = {10.48550/arxiv.0709.0093}, - url = {https://arxiv.org/abs/0709.0093}, + url = {https://arxiv.org/abs/0709.0093v3}, author = {Ajith, P. and Boyle, M. and Brown, D. A. and Fairhurst, S. and Hannam, M. and Hinder, I. and Husa, S. and Krishnan, B. and Mercer, R. A. and Ohme, F. and Ott, C. D. and Read, J. S. and Santamaria, L. and Whelan, J. T.}, @@ -10,6 +10,7 @@ @misc{Ajith_2007 archivePrefix ="arXiv", eprint = "0709.0093", primaryClass = "gr-qc", + note = {"There was a serious error in the original version of this paper. The error was corrected in version 2."} } @article{Bander_1966, @@ -77,8 +78,7 @@ @book{CohenTannoudji_1991 publisher = {Wiley}, author = {{Cohen-Tannoudji}, Claude and Diu, Bernard and Laloe, Frank}, month = jan, - year = 1991, - note = {bibtex: {Cohen-Tannoudji1991}} + year = 1991 } @book{CondonShortley_1935, @@ -266,6 +266,15 @@ @article{Kostelec_2008 journal = {Journal of Fourier Analysis and Applications} } +@misc{LALSuite_2018, + author = "{LIGO Scientific Collaboration} and {Virgo Collaboration} and {KAGRA + Collaboration}", + title = "{LVK} {A}lgorithm {L}ibrary - {LALS}uite", + howpublished = "Free software (GPL)", + doi = "10.7935/GT1W-FZ16", + year = 2018 +} + @book{LeBellac_2006, address = {Cambridge}, title = {Quantum Physics}, From b7d99850f1c36ad4819b207d70b857b35ffeb72e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:14:50 -0500 Subject: [PATCH 088/329] Review LAL, NINJA, and Torres del Castillo --- docs/src/conventions/comparisons.md | 191 ++++++++++++++++++++-------- test/conventions/lal.jl | 76 ++++++++++- 2 files changed, 212 insertions(+), 55 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index fca92de2..7fbb1e5b 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -53,36 +53,8 @@ I think that should be sufficient to find a consensus on conventions for each of the above — with the possible exception of quaternions, for which I have my own strong opinions. -## Le Bellac - -[LeBellac_2006](@citet) (with Foreword by Cohen-Tannoudji) takes an -odd approach, defining [Eq. (10.32)] -```math -D^{(j)}_{m', m} \left[ \mathcal{R}(\theta, \phi) \right] -= -\langle j, m' | e^{-i\phi J_z} e^{-i\theta J_y} | j, m \rangle, -``` -but later allowing that ``e^{-i \psi J_z}`` usually goes on the -right-hand side of the others, in which case ``D^{(j)}(\theta, \phi) -\to D^{(j)}(\phi, \theta, \psi)``. Figure 10.1 shows that the -spherical coordinates are standard (physicist's) coordinates. - -Equation (10.65) shows the rotation law: -```math -Y_{\ell}^{m}\left( \mathcal{R}^{-1} \hat{r} \right) -= -\sum_{m'} D^{(\ell)}_{m', m}(\mathcal{R}) Y_{\ell}^{m'}(\hat{r}), -``` -and Eq. (10.66) relates the spherical harmonics to the Wigner -D-matrices: -```math -D^{(\ell)}_{m, 0}(\theta, \phi) -= -\sqrt{\frac{4\pi}{2\ell+1}} \left[Y_{\ell}^{m}(\theta, \phi)\right]^\ast. -``` - -## Cohen-Tannoudji +## Cohen-Tannoudji (1991) [CohenTannoudji_1991](@citet) derives the spherical harmonics in two ways and gets two different, but equivalent, expressions in Complement @@ -121,7 +93,7 @@ Cohen-Tannoudji does not appear to define the Wigner D-matrices. ## Condon-Shortley -## Edmonds +## Edmonds (1960) [Edmonds_2016](@citet) is a standard reference for the theory of angular momentum. @@ -209,7 +181,7 @@ Wigner D-matrices. In Eq. (4.1.12) he defines which is the *conjugate* of most other definitions. -## Goldberg +## Goldberg et al. (1967) Eq. (3.11) of [GoldbergEtAl_1967](@citet) naturally extends to ```math @@ -235,6 +207,50 @@ will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i ## LALSuite +[LALSuite (LSC Algorithm Library Suite)](@citet LALSuite_2018) is a +collection of software routines, comprising the primary official +software used by the LIGO-Virgo-KAGRA Collaboration to detect and +characterize gravitational waves. As far as I can tell, the ultimate +source for all spin-weighted spherical harmonic values used in +LALSuite is the function +[`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), +which cites the NINJA paper [Ajith_2007](@citet) as its source. +Unfortunately, it cites version *1*, which contained a serious error, +using ``\tfrac{\cos\iota}{2}`` instead of ``\cos \tfrac{\iota}{2}`` +and similarly for ``\sin``. This error was corrected in version 2, +but the citation was not updated. I will test to + +TODO: Check the actual values of the spin-weighted spherical harmonics + + +## Le Bellac (2006) + +[LeBellac_2006](@citet) (with Foreword by Cohen-Tannoudji) takes an +odd approach, defining [Eq. (10.32)] +```math +D^{(j)}_{m', m} \left[ \mathcal{R}(\theta, \phi) \right] += +\langle j, m' | e^{-i\phi J_z} e^{-i\theta J_y} | j, m \rangle, +``` +but later allowing that ``e^{-i \psi J_z}`` usually goes on the +right-hand side of the others, in which case ``D^{(j)}(\theta, \phi) +\to D^{(j)}(\phi, \theta, \psi)``. Figure 10.1 shows that the +spherical coordinates are standard (physicist's) coordinates. + +Equation (10.65) shows the rotation law: +```math +Y_{\ell}^{m}\left( \mathcal{R}^{-1} \hat{r} \right) += +\sum_{m'} D^{(\ell)}_{m', m}(\mathcal{R}) Y_{\ell}^{m'}(\hat{r}), +``` +and Eq. (10.66) relates the spherical harmonics to the Wigner +D-matrices: +```math +D^{(\ell)}_{m, 0}(\theta, \phi) += +\sqrt{\frac{4\pi}{2\ell+1}} \left[Y_{\ell}^{m}(\theta, \phi)\right]^\ast. +``` + ## Mathematica @@ -370,26 +386,44 @@ Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. ## NINJA -Combining Eqs. (II.7) and (II.8) of [Ajith_2007](@citet), we have +[Ajith_2007](@citet) was prepared by a broad cross-section of +researchers (including the author of this package) involved in +modeling gravitational waves with the intent of providing a shared set +of conventions. The spherical coordinates are standard physicist's +coordinates, except that the polar angle is denoted ``\iota``. +Equation (II.7) is ```math -\begin{align} - {}_{-s}Y_{lm} - &= - (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + {}^{-s}Y_{l,m} = (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} + d^{\ell}_{m,s}(\iota)e^{im\phi}, +``` +where +```math + d^{\ell}_{m,s}(\iota) + = \sum_{k = k_1}^{k_2} \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell+s)!(\ell-s)!]^{1/2}} {(\ell+m-k)!(\ell-s-k)!k!(k+s-m)!} - \\ &\qquad \times \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m-s-2k} \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k+s-m} -\end{align} ``` with ``k_1 = \textrm{max}(0, m-s)`` and ``k_2=\textrm{min}(\ell+m, -\ell-s)``. Note that most of the above was copied directly from the -TeX source of the paper, but the two equations were trivially combined -into one. Also note the annoying negative sign on the left-hand side. -That's so annoying that I'm going to duplicate the expression just to -get rid of it: +\ell-s)``. For reference, they provide several values [Eqs. +(II.9)--(II.13)]: +```math +\begin{align} + {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}}(1+\cos\iota)^2e^{2i\phi},\\ + {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 + \cos\iota )e^{i\phi},\\ + {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ + {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 - \cos\iota + )e^{-i\phi},\\ + {}^{-2}Y_{2,-2} &=& \sqrt{\frac{5}{64\pi}}(1-\cos\iota)^2e^{-2i\phi}. +\end{align} +``` +Note that most of the above was copied directly from the TeX source of +the paper. Also note the annoying negative sign on the left-hand side +of the first expression. Getting rid of it and combining the first +two expressions, we have the full formula for the spin-weighted +spherical harmonics in this convention: ```math \begin{align} {}_{s}Y_{lm} @@ -407,7 +441,9 @@ where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, \ell+s)``. -## Sakurai +## Sakurai (1994) + + ## Scipy @@ -416,10 +452,10 @@ where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, [`scipy.special.sph_harm_y`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.sph_harm_y.html) -## Shankar - +## Shankar (1994) -Eq. (12.5.35) writes the spherical harmonics as +[Shankar_1994](@citet) writes in Eq. (12.5.35) the spherical harmonics +as ```math Y_{\ell}^{m}(\theta, \phi) = @@ -505,9 +541,64 @@ definition of the D matrix. ## Thorne -## Torres del Castillo +## Torres del Castillo (2003) + +[TorresDelCastillo_2003](@citet) starts by defining a rotation +``\mathcal{R}`` as transforming a point ``x_i`` into another point +with coordinates ``x_i' = a_{ij}x_j``. Under that rotation, any +scalar function ``f`` transforms into another function ``f' = +\mathcal{R} f`` defined by [Eq. (2.43)] +```math +f'\big(x_i\big) = f\big( a^{-1}_{ij} x_j \big). +``` +In particular, ``f'(x'_i) = f(x_i)``. He then defines Wigner's +D-matrix to satisfy [Eq. (2.45)] +```math +\mathcal{R} Y_{l,m} = \sum_{m} D^l_{m',m}(\mathcal{R}) Y_{l,m'}. +``` +Including the arguments to the spherical harmonics, this becomes +```math +Y_{l,m}\big(\mathcal{R}^{-1} R_{\theta, \phi}\big) += +\sum_{m} D^l_{m',m}(\mathcal{R}) Y_{l,m'}\big(R_{\theta, \phi}\big). +``` +In this form, we have [Eq. (2.46)] +```math +D^l_{m'',m}(\mathcal{R}_1 \mathcal{R}_2) += +\sum_{m'} D^l_{m'',m'}(\mathcal{R}_1) D^l_{m',m}(\mathcal{R}_2). +``` +He computes [Eq. (2.53)] +```math +D^l_{m',m}(\phi, \theta, \chi) += +e^{-i m' \phi} d^l_{m',m}(\theta) e^{-i m \chi}, +``` +where the ``d`` matrix is given by +```math +d^l_{m',m}(\theta) += +\sqrt{(l+m)!(l-m)!(l+m')!(l-m')!} +\sum_{k} \frac{ + (-1)^k + (\sin \tfrac{1}{2} \theta)^{m-m'+2k} + (\cos \tfrac{1}{2} \theta)^{2l-m+m'-2k} +} { + k!(l+m'-k)!(l-m-k)!(m-m'+k)! +}, +``` +and the spin-weighted spherical harmonic is related to ``D`` by +```math +{}_{s}Y_{j,m}(\theta, \phi) += +(-1)^m +\sqrt{\frac{2j+1}{4\pi}} +d^j_{-m,s}(\theta) +e^{i m \phi}. +``` + -## Varshalovich et al. +## Varshalovich et al. (1988) [Varshalovich_1988](@citet) has a fairly decent comparison of definitions related to the rotation matrix by previous authors. @@ -726,7 +817,7 @@ D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | \mathcal{R}(\alpha,\beta,\ga -## Zettili +## Zettili (2009) [Zettili_2009](@citet) denotes by ``\hat{R}_z(\delta \phi)`` the diff --git a/test/conventions/lal.jl b/test/conventions/lal.jl index 106fae00..f81af58b 100644 --- a/test/conventions/lal.jl +++ b/test/conventions/lal.jl @@ -1,10 +1,11 @@ @testmodule LAL begin + # The code in this section is translated from C code in LALSuite: + # + # https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c + # + # That code is licensed under GPLv2. See the link for details. - """ - Reproduces the XLALSpinWeightedSphericalHarmonic function from the LALSuite C library: - https://lscsoft.docs.ligo.org/lalsuite/lal/_spherical_harmonics_8c_source.html#l00042 - """ - function LALSpinWeightedSphericalHarmonic( + function XLALSpinWeightedSphericalHarmonic( theta::Float64, # polar angle (rad) phi::Float64, # azimuthal angle (rad) s::Int, # spin weight @@ -202,4 +203,69 @@ end end + function XLALJacobiPolynomial( + n::Int, # degree + alpha::Int, # alpha parameter + beta::Int, # beta parameter + x::Float64 # argument + ) + f1 = (x-1.0)/2.0 + f2 = (x+1.0)/2.0 + sum = 0.0 + if n == 0 + return 1.0 + end + for s = 0:n + val = 1.0 + val *= binomial(n+alpha, s) + val *= binomial(n+beta, n-s) + if n-s != 0 + val *= f1^(n-s) + end + if s != 0 + val *= f2^s + end + sum += val + end + return sum + end + + function XLALWignerdMatrix( + l::Int, # mode number l + mp::Int, # mode number m' + m::Int, # mode number m + beta::Float64 # euler angle (rad) + ) + k = min(l+m, min(l-m, min(l+mp, l-mp))) + a = 0 + lam = 0 + if k == l+m + a = mp-m + lam = mp-m + elseif k == l-m + a = m-mp + lam = 0 + elseif k == l+mp + a = m-mp + lam = 0 + elseif k == l-mp + a = mp-m + lam = mp-m + end + b = 2*l-2*k-a + pref = (-1)^lam * sqrt(binomial(2*l-k, k+a)) / sqrt(binomial(k+b, b)) + return pref * sin(beta/2.0)^a * cos(beta/2.0)^b * XLALJacobiPolynomial(k, a, b, cos(beta)) + end + + function XLALWignerDMatrix( + l::Int, # mode number l + mp::Int, # mode number m' + m::Int, # mode number m + alpha::Float64, # euler angle (rad) + beta::Float64, # euler angle (rad) + gam::Float64 # euler angle (rad) + ) + return cis(-1im*mp*alpha) * XLALWignerdMatrix(l, mp, m, beta) * cis(-1im*m*gam) + end + end # module LAL From 4ba11d71184b2c582853ad5c3e2a2ee3d9a38e67 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:51:59 -0500 Subject: [PATCH 089/329] Update changed name of LAL function --- test/wigner_matrices/sYlm.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/wigner_matrices/sYlm.jl b/test/wigner_matrices/sYlm.jl index d4203661..8881ef18 100644 --- a/test/wigner_matrices/sYlm.jl +++ b/test/wigner_matrices/sYlm.jl @@ -55,7 +55,7 @@ end sYlm2 = NINJA.sYlm(spin, ℓ, m, ι, ϕ) @test sYlm1 ≈ sYlm2 atol=tol rtol=tol if spin==-2 && T===Float64 - sYlm3 = LAL.LALSpinWeightedSphericalHarmonic(ι, ϕ, spin, ℓ, m) + sYlm3 = LAL.XLALSpinWeightedSphericalHarmonic(ι, ϕ, spin, ℓ, m) @test sYlm1 ≈ sYlm3 atol=tol rtol=tol end i += 1 From 64d78c89c572c644b94a29504f06acd5277ff13e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:52:23 -0500 Subject: [PATCH 090/329] Emphasize that we're using v3 of the NINJA paper --- test/conventions/ninja.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/conventions/ninja.jl b/test/conventions/ninja.jl index 937a9a14..2a44b6be 100644 --- a/test/conventions/ninja.jl +++ b/test/conventions/ninja.jl @@ -4,7 +4,7 @@ import .NaiveFactorials: ❗ function Wigner_d(ι::T, ℓ, m, s) where {T<:Real} - # Eq. II.8 of Ajith et al. (2007) 'Data formats...' + # Eq. II.8 of v3 of Ajith et al. (2007) 'Data formats...' k_min = max(0, m - s) k_max = min(ℓ + m, ℓ - s) sum( @@ -21,7 +21,7 @@ raw""" - Eq. II.7 of Ajith et al. (2007) 'Data formats...' says + Eq. II.7 of v3 of Ajith et al. (2007) 'Data formats...' says ```math {}_sY_{\ell,m} = (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} d^\ell_{m,-s}(\iota) e^{im\phi} ``` @@ -44,11 +44,10 @@ The last line assumes that `j`, `m`, and `s` are integers. But in that case, the NINJA expression agrees with the Torres del Castillo expression. - """ function sYlm(s, ell, m, ι::T, ϕ::T) where {T<:Real} - # Eq. II.7 of Ajith et al. (2007) 'Data formats...' + # Eq. II.7 of v3 of Ajith et al. (2007) 'Data formats...' # Note the weird definition w.r.t. `-s` if abs(s) > ell || abs(m) > ell return zero(complex(T)) From 9d510cc0643d8431d45ace0050656586fb5c11ff Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:55:56 -0500 Subject: [PATCH 091/329] Note that LALSuite appears to correctly use the newer (corrected) versions of the NINJA paper --- docs/src/conventions/comparisons.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 7fbb1e5b..c0865615 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -218,10 +218,11 @@ which cites the NINJA paper [Ajith_2007](@citet) as its source. Unfortunately, it cites version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` instead of ``\cos \tfrac{\iota}{2}`` and similarly for ``\sin``. This error was corrected in version 2, -but the citation was not updated. I will test to - -TODO: Check the actual values of the spin-weighted spherical harmonics - +but the citation was not updated. Nonetheless, it appears that the +actual code is consistent with the corrected versions of the NINJA +paper; the equivalence is +[tested](https://github.com/moble/SphericalFunctions.jl/blob/0f57c77e65da85e4996f0969fe0a931b460135ac/test/wigner_matrices/sYlm.jl#L59) +in this package's test suite. ## Le Bellac (2006) From a3e3364391fbbb3faf47397eb292d6a320c0a0d8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 13:02:09 -0500 Subject: [PATCH 092/329] Correct citation syntax --- docs/src/conventions/comparisons.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index c0865615..cf9364a7 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -207,7 +207,7 @@ will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i ## LALSuite -[LALSuite (LSC Algorithm Library Suite)](@citet LALSuite_2018) is a +[LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software routines, comprising the primary official software used by the LIGO-Virgo-KAGRA Collaboration to detect and characterize gravitational waves. As far as I can tell, the ultimate From 14a464f7d8647b18fe74e79416702c98a6ef4356 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 14:52:34 -0500 Subject: [PATCH 093/329] Include Griffiths; more on Goldberg and on Cohen-Tannoudji --- docs/src/conventions/comparisons.md | 138 +++++++++++++++++++++++++--- docs/src/conventions/details.md | 8 +- docs/src/conventions/summary.md | 1 + docs/src/references.bib | 9 ++ 4 files changed, 139 insertions(+), 17 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index cf9364a7..e4f246a6 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -56,14 +56,34 @@ for which I have my own strong opinions. ## Cohen-Tannoudji (1991) -[CohenTannoudji_1991](@citet) derives the spherical harmonics in two +[CohenTannoudji_1991](@citet) defines spherical coordinates in the +usual (physicist's) way in Chapter VI. He then computes the +angular-momentum operators as [Eqs. (D-5)] +```math +\begin{aligned} +L_x &= i \hbar \left( + \sin\phi \frac{\partial} {\partial \theta} + + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} +\right), +\\ +L_y &= i \hbar \left( + -\cos\phi \frac{\partial} {\partial \theta} + + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} +\right), +\\ +L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. +\end{aligned} +``` + + +He derives the spherical harmonics in two ways and gets two different, but equivalent, expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) ```math Y_{l}^{m}(\theta, \phi) = \frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l+m)!}{(l-m)!}} -e^{i m \phi} (\sin \theta)^m +e^{i m \phi} (\sin \theta)^{-m} \frac{d^{l-m}}{d(\cos \theta)^{l-m}} (\sin \theta)^{2l}, ``` while the second is Eq. (30) @@ -183,7 +203,39 @@ which is the *conjugate* of most other definitions. ## Goldberg et al. (1967) -Eq. (3.11) of [GoldbergEtAl_1967](@citet) naturally extends to +[GoldbergEtAl_1967](@citet) presented the first paper specifically +about spin-weighted spherical harmonics (after [Newman_1966](@citet) +introduced them), and the first to relate them to the Wigner +D-matrices. + +If we relate two vectors by a rotation matrix as ``x'^k = R^{kl} +x^l``, then Goldberg et al. define ``D`` by its action on spherical +harmonics [Eq. (3.3)]: +```math +Y_{ell,m}(x') = \sum_{m'} Y_{ell,m'}(x) D^{ell}_{m',m}\left( R^{-1} \right). +``` +They then define the Euler angles as we do, and write [Eq. (3.4)] +```math +D^{\ell}_{m', m}(\alpha, \beta, \gamma) +\equiv +D^{\ell}_{m', m}\left( R(\alpha \beta \gamma)^{-1} \right) += +e^{i m' \gamma} d^{\ell}_{m', m}(\beta) e^{i m \alpha}. +``` +Finally, they derive [Eq. (3.9)] +```math +D^{j}_{m', m}(\alpha, \beta, \gamma) += +\left[\frac{(j+m)!(j-m)!}{(j+m')!(j-m')!}\right]^{1/2} +(\sin \frac{1}{2}\beta)^{2j} +\sum_r \binom{j+m'}{r} \binom{j-m'}{r-m-m'} +(-1)^{j+m'-r} +e^{im\alpha} +(\cot \tfrac{1}{2}\beta)^{2r-m-m'} +e^{im'\gamma}. +``` + +Equation (3.11) naturally extends to ```math {}_sY_{\ell, m}(\theta, \phi, \gamma) = @@ -205,6 +257,66 @@ will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i \partial_\gamma``. +## Griffiths (1995) + +Griffiths' ["Introduction to Quantum Mechanics"](@cite Griffiths_1995) +is probably the most common introductory text used in undergraduate +physics programs, so it would be useful to compare. + +Equation (4.27) gives the associated Legendre function as +```math +P_{\ell}^{m}(x) += +(1-x^2)^{|m|/2} \left(\frac{d}{dx}\right)^{|m|} P_{\ell}(x), +``` +and (4.28) gives the Legendre polynomial as +```math +P_{\ell}(x) += +\frac{1}{2^\ell \ell!} \left(\frac{d}{dx}\right)^\ell (x^2-1)^\ell. +``` +Then, (4.32) gives the spherical harmonics as +```math +Y_{\ell}^{m}(\theta, \phi) += +\epsilon +\sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-|m|)!}{(\ell+|m|)!}} +e^{im\phi} P_{\ell}^{m}(\cos\theta), +``` +where ``\epsilon = (-1)^m`` for ``m\geq 0`` and ``\epsilon = 1`` for +``m\leq 0``. In Table 4.2, he explicitly lists the first few +spherical harmonics: +```math +\begin{aligned} + Y_{0}^{0} &= \left(\frac{1}{4\pi}\right)^{1/2},\\ + Y_{1}^{0} &= \left(\frac{3}{4\pi}\right)^{1/2} \cos\theta,\\ + Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8\pi}\right)^{1/2} \sin\theta e^{\pm i\phi},\\ + Y_{2}^{0} &= \left(\frac{5}{16\pi}\right)^{1/2} \left(3\cos^2\theta - 1\right),\\ + Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8\pi}\right)^{1/2} \sin\theta \cos\theta e^{\pm i\phi},\\ + Y_{2}^{\pm 2} &= \left(\frac{15}{32\pi}\right)^{1/2} \sin^2\theta e^{\pm 2i\phi}. + Y_{3}^{0} &= \left(\frac{7}{16\pi}\right)^{1/2} \left(5\cos^3\theta - 3\cos\theta\right),\\ + Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64\pi}\right)^{1/2} \sin\theta \left(5\cos^2\theta - 1\right) e^{\pm i\phi},\\ + Y_{3}^{\pm 2} &= \left(\frac{105}{32\pi}\right)^{1/2} \sin^2\theta \cos\theta e^{\pm 2i\phi},\\ + Y_{3}^{\pm 3} &= \mp \left(\frac{35}{64\pi}\right)^{1/2} \sin^3\theta e^{\pm 3i\phi}. +\end{aligned} +``` +In Eqs. (4.127)—(4.129), he gives the angular-momentum operators in +terms of spherical coordinates: +```math +\begin{aligned} +L_x &= \frac{\hbar}{i} \left( + -\sin\phi \frac{\partial} {\partial \theta} + - \cos\phi \cot\theta \frac{\partial} {\partial \phi} +\right), \\ +L_y &= \frac{\hbar}{i} \left( + \cos\phi \frac{\partial} {\partial \theta} + - \sin\phi \cot\theta \frac{\partial} {\partial \phi} +\right), \\ +L_z &= -i \hbar \frac{\partial} {\partial \phi}. +\end{aligned} +``` + + ## LALSuite [LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a @@ -411,14 +523,14 @@ with ``k_1 = \textrm{max}(0, m-s)`` and ``k_2=\textrm{min}(\ell+m, \ell-s)``. For reference, they provide several values [Eqs. (II.9)--(II.13)]: ```math -\begin{align} +\begin{aligned} {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}}(1+\cos\iota)^2e^{2i\phi},\\ {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 + \cos\iota )e^{i\phi},\\ {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 - \cos\iota )e^{-i\phi},\\ {}^{-2}Y_{2,-2} &=& \sqrt{\frac{5}{64\pi}}(1-\cos\iota)^2e^{-2i\phi}. -\end{align} +\end{aligned} ``` Note that most of the above was copied directly from the TeX source of the paper. Also note the annoying negative sign on the left-hand side @@ -426,7 +538,7 @@ of the first expression. Getting rid of it and combining the first two expressions, we have the full formula for the spin-weighted spherical harmonics in this convention: ```math -\begin{align} +\begin{aligned} {}_{s}Y_{lm} &= (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} @@ -436,7 +548,7 @@ spherical harmonics in this convention: \\ &\qquad \times \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m+s-2k} \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-s-m} -\end{align} +\end{aligned} ``` where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, \ell+s)``. @@ -649,7 +761,7 @@ Page 155 has a table of values for ``\ell \leq 5`` *covariant* and *contravariant* spherical coordinates and the corresponding basis vectors, which they define as ```math -\begin{align} +\begin{aligned} \mathbf{e}_{+1} &= - \frac{1}{\sqrt{2}} \left( \mathbf{e}_x + i \mathbf{e}_y\right) &&& \mathbf{e}^{+1} &= - \frac{1}{\sqrt{2}} \left( \mathbf{e}_x - i \mathbf{e}_y\right) \\ @@ -657,7 +769,7 @@ corresponding basis vectors, which they define as \mathbf{e}_{-1} &= \frac{1}{\sqrt{2}} \left( \mathbf{e}_x - i \mathbf{e}_y\right) &&& \mathbf{e}^{-1} &= \frac{1}{\sqrt{2}} \left( \mathbf{e}_x + i \mathbf{e}_y\right). -\end{align} +\end{aligned} ``` Then, in Sec. 4.2 they define ``\hat{\mathbf{J}}`` as the operator of angular momentum of the rigid symmetric top. They then give in Eq. @@ -689,7 +801,7 @@ and "contravariant components of ``\hat{\mathbf{J}}`` in the rotating Cartesian components to compare to our expressions. First the covariant components: ```math -\begin{align} +\begin{aligned} \hat{J}_{x} &= -\frac{1}{\sqrt{2}} \left( \hat{J}_{+1} - \hat{J}_{-1} \right) \\ % &= -\frac{1}{\sqrt{2}} \left( @@ -733,7 +845,7 @@ covariant components: \hat{J}_{z} &= \hat{J}_{0} \\ &= -i \frac{\partial}{\partial \alpha} -\end{align} +\end{aligned} ``` We can compare these to the [Full expressions on ``S^3``](@ref), and find that they are precisely equivalent to expressions for ``L_j`` computed in @@ -741,7 +853,7 @@ this package's conventions. Next, the contravariant components: ```math -\begin{align} +\begin{aligned} \hat{J}'_{x} &= -\frac{1}{\sqrt{2}} \left( \hat{J}'^{+1} - \hat{J}'^{-1} \right) \\ % &= -\frac{1}{\sqrt{2}} \left( @@ -785,7 +897,7 @@ Next, the contravariant components: \hat{J}'_{z} &= \hat{J}'^{0} \\ &= -i \frac{\partial}{\partial \gamma} -\end{align} +\end{aligned} ``` Unfortunately, while we have agreement on ``\hat{J}'^{y} = R_y``, we also have disagreement on ``\hat{J}'^{x} = -R_x`` and ``\hat{J}'^{z} = diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 21fd44be..52e16f22 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -666,14 +666,14 @@ canonical angular-momentum operators. Note that when composing operators, it is critical to keep track of the order of operations, which may look slightly unnatural: ```math -\begin{align} +\begin{aligned} L_\mathfrak{g} L_\mathfrak{h} f(\mathbf{Q}) % &= \left. \lambda \frac{\partial} {\partial \gamma} f'\left(e^{-\gamma \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\gamma=0}, \\ &= \left. \lambda^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left(e^{-\eta \mathfrak{h} / 2} e^{-\gamma \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\gamma=\eta=0}, \\ R_\mathfrak{g} R_\mathfrak{h} f(\mathbf{Q}) % &= \rho \left. \frac{\partial} {\partial \gamma} f' \left( \mathbf{Q} e^{-\gamma \mathfrak{g} / 2} \right) \right|_{\gamma=0} \\ &= \left. \rho^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left( \mathbf{Q} e^{-\gamma \mathfrak{g} / 2} e^{-\eta \mathfrak{h} / 2} \right) \right|_{\gamma=\eta=0}. -\end{align} +\end{aligned} ``` We can prove the first of these, for example, by defining ``f'(\mathbf{Q}) = L_\mathfrak{h} f(\mathbf{Q})``, then applying the @@ -778,7 +778,7 @@ $\partial_\alpha$, etc., when $\theta=0$. ```math -\begin{align} +\begin{aligned} L_i f(\mathbf{R}_{\alpha, \beta, \gamma}) &= \left. -\mathbf{z} \frac{\partial} {\partial \theta} f \left( e^{\theta \mathbf{e}_i / 2} \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} \\ @@ -791,7 +791,7 @@ $\partial_\alpha$, etc., when $\theta=0$. K_i f(\mathbf{R}_{\alpha, \beta, \gamma}) &= -\mathbf{z} \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right), -\end{align} +\end{aligned} ``` ```math diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index ec235393..e7ac49bd 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -275,3 +275,4 @@ where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, ## Wigner D-matrices + diff --git a/docs/src/references.bib b/docs/src/references.bib index 61f21936..0c9528bf 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -186,6 +186,15 @@ @article{GoldbergEtAl_1967 url = {https://doi.org/10.1063/1.1705135}, } +@book{Griffiths_1995, + address = {Upper Saddle River, {NJ}}, + edition = {first}, + title = {Introduction to quantum mechanics}, + publisher = {Prentice Hall}, + author = {Griffiths, David J.}, + year = 1995 +} + @techreport{Gumerov_2001, address = {College Park, {MD}}, title = {Fast, Exact, and Stable Computation of Multipole Translation and Rotation From cc17a756700d21e7a9d53df420542b73c29b5370 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 24 Feb 2025 21:37:57 -0500 Subject: [PATCH 094/329] Fill in Condon-Shortley --- docs/src/conventions/comparisons.md | 71 +++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index e4f246a6..4d97236a 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -75,9 +75,8 @@ L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. \end{aligned} ``` - -He derives the spherical harmonics in two -ways and gets two different, but equivalent, expressions in Complement +He derives the spherical harmonics in two ways and gets two different, +but equivalent, expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) ```math Y_{l}^{m}(\theta, \phi) @@ -111,7 +110,71 @@ R_{\mathbf{u}}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha \mathbf{L}.\mathbf{u}. Cohen-Tannoudji does not appear to define the Wigner D-matrices. -## Condon-Shortley +## Condon-Shortley (1935) + +[Condon and Shortley's "The Theory Of Atomic Spectra"](@cite +CondonShortley_1935) is the standard reference for the +"Condon-Shortley phase convention" — though no one is ever too clear +about precisely what that means. To avoid ambiguity, we can just look +at the actual spherical harmonics they define. + +They are not very explicit about the meaning of the spherical +coordinates, but they do describe them as "spherical polar coordinates +``r, \theta, \varphi``" immediately before equation (1) of section 4³ +(page 50), +```math +L_z = -i \hbar \frac{\partial}{\partial \varphi}, +``` +followed by equation (8): +```math +\begin{aligned} +L_x + i L_y &= \hbar e^{i\varphi} \left( + \frac{\partial}{\partial \theta} + + i \cot\theta \frac{\partial}{\partial \varphi} +\right) \\ +L_x - i L_y &= \hbar e^{-i\varphi} \left( + -\frac{\partial}{\partial \theta} + + i \cot\theta \frac{\partial}{\partial \varphi} +\right). +\end{aligned} +``` + +Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence +of the spherical harmonic as +```math +\Theta(\ell, m) += +(-1)^\ell +\sqrt{\frac{(2\ell+1)}{2} \frac{(\ell+m)!}{(\ell-m)!}} +\frac{1}{2^\ell \ell!} +\frac{1}{\sin^m \theta} +\frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. +``` +The ``\varphi`` part is given by equation (5) of section 4³ (page 50): +```julia +1 / √(2T(π)) * exp(𝒾 * mₗ * φ) +``` +```math +\Phi(m_\ell) += +\frac{1}{\sqrt{2\pi}} e^{i m_\ell \varphi}. +``` +Equation (12) of section 4³ (page 51) writes the solution to the +three-dimensional Laplace equation in spherical coordinates as +```math +\psi(\gamma, \ell, m_\ell) += +B(\gamma, \ell) \Theta(\ell, m_\ell) \Phi(m_\ell), +``` +where ``B`` is independent of ``\theta`` and ``\varphi``, and ``\gamma`` +represents any number of eigenvalues required to specify the state. +Thus, we take the angular factors, normalized, to define the spherical +harmonics. The result is that the original Condon-Shortley spherical +harmonics agree perfectly with the ones computed by this package. + +Condon and Shortley do not give an expression for the Wigner +D-matrices. + ## Edmonds (1960) From 0805a0267cb2ad7d0705a5c579a2a2fe918931af Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 12:17:54 -0500 Subject: [PATCH 095/329] Use epsilon consistently as differentiation variable --- docs/literate_input/euler_angular_momentum.jl | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 9348e4ca..5007e1bd 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -39,53 +39,53 @@ important results that help make contact with more standard expressions: We start by defining a new set of Euler angles according to ```math \mathbf{R}_{\alpha', \beta', \gamma'} -= e^{-\theta \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} += e^{-\epsilon \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} \qquad \text{or} \qquad \mathbf{R}_{\alpha', \beta', \gamma'} -= \mathbf{R}_{\alpha, \beta, \gamma} e^{-\theta \mathbf{u} / 2} += \mathbf{R}_{\alpha, \beta, \gamma} e^{-\epsilon \mathbf{u} / 2} ``` where ``\mathbf{u}`` will be each of the basis quaternions, and each of ``\alpha'``, ``\beta'``, and ``\gamma'`` is a function of ``\alpha``, ``\beta``, ``\gamma``, and -``\theta``. Then, we note that the chain rule tells us that +``\epsilon``. Then, we note that the chain rule tells us that ```math -\frac{\partial}{\partial \theta} +\frac{\partial}{\partial \epsilon} = -\frac{\partial \alpha'}{\partial \theta} \frac{\partial}{\partial \alpha'} -+ \frac{\partial \beta'}{\partial \theta} \frac{\partial}{\partial \beta'} -+ \frac{\partial \gamma'}{\partial \theta} \frac{\partial}{\partial \gamma'}, +\frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha'} ++ \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta'} ++ \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma'}, ``` which we will use to convert the general expression for the angular-momentum operators in -terms of ``\partial_\theta`` into an expression in terms of derivatives with respect to +terms of ``\partial_\epsilon`` into an expression in terms of derivatives with respect to these new Euler angles: ```math \begin{align} L_j f(\mathbf{R}_{\alpha, \beta, \gamma}) &= - \left. i \frac{\partial} {\partial \theta} f \left( e^{-\theta \mathbf{e}_j / 2} - \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} + \left. i \frac{\partial} {\partial \epsilon} f \left( e^{-\epsilon \mathbf{e}_j / 2} + \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\epsilon=0} \\ &= i \left[ \left( - \frac{\partial \alpha'}{\partial \theta} \frac{\partial}{\partial \alpha'} - + \frac{\partial \beta'}{\partial \theta} \frac{\partial}{\partial \beta'} - + \frac{\partial \gamma'}{\partial \theta} \frac{\partial}{\partial \gamma'} - \right) f \left(\alpha', \beta', \gamma'\right) \right]_{\theta=0} + \frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha'} + + \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta'} + + \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma'} + \right) f \left(\alpha', \beta', \gamma'\right) \right]_{\epsilon=0} \\ &= i \left[ \left( - \frac{\partial \alpha'}{\partial \theta} \frac{\partial}{\partial \alpha} - + \frac{\partial \beta'}{\partial \theta} \frac{\partial}{\partial \beta} - + \frac{\partial \gamma'}{\partial \theta} \frac{\partial}{\partial \gamma} - \right) f \left(\alpha, \beta, \gamma\right) \right]_{\theta=0}, + \frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha} + + \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta} + + \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma} + \right) f \left(\alpha, \beta, \gamma\right) \right]_{\epsilon=0}, \end{align} ``` and similarly for ``R_j``. -So the objective is to find the new Euler angles, differentiate with respect to ``\theta``, -and then evaluate at ``\theta = 0``. We do this by first multiplying ``\mathbf{R}_{\alpha, -\beta, \gamma}`` and ``e^{-\theta \mathbf{u} / 2}`` in the desired order, then expanding the -results in terms of its quaternion components, and then computing the new Euler angles in -terms of those components according to the usual expression. +So the objective is to find the new Euler angles, differentiate with respect to +``\epsilon``, and then evaluate at ``\epsilon = 0``. We do this by first multiplying +``\mathbf{R}_{\alpha, \beta, \gamma}`` and ``e^{-\epsilon \mathbf{u} / 2}`` in the desired +order, then expanding the results in terms of its quaternion components, and then computing +the new Euler angles in terms of those components according to the usual expression. """ @@ -107,7 +107,7 @@ const I = sympy.I nothing #hide # Define coordinates we will use -α, β, γ, θ, ϕ = symbols("α β γ θ ϕ", real=true, positive=true) +α, β, γ, θ, ϕ, ϵ = symbols("α β γ θ ϕ ϵ", real=true, positive=true) nothing #hide # Reinterpret the quaternion basis elements for compatibility with SymPy. (`Quaternionic` @@ -126,7 +126,7 @@ function 𝒪(u, side) ) ## Define the essential quaternions - e = cos(θ/2) + u * sin(-θ/2) + e = cos(ϵ/2) + u * sin(-ϵ/2) R₀ = Quaternion(sympy.simplify.(sympy.expand.(components( (cos(α/2) + 𝐤 * sin(α/2)) * (cos(β/2) + 𝐣 * sin(β/2)) * (cos(γ/2) + 𝐤 * sin(γ/2)) )))) @@ -141,12 +141,12 @@ function 𝒪(u, side) β′ = (2*acos(sqrt(w^2 + z^2) / sqrt(w^2 + x^2 + y^2 + z^2))).expand().simplify() γ′ = (atan(z/w) - atan(-x/y)).expand().simplify() - ## Differentiate with respect to θ, set θ to 0, and simplify - ∂α′∂θ = expand_trig(Derivative(α′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) - ∂β′∂θ = expand_trig(Derivative(β′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) - ∂γ′∂θ = expand_trig(Derivative(γ′, θ).doit().subs(θ, 0).expand().simplify().subs(subs)) + ## Differentiate with respect to ϵ, set ϵ to 0, and simplify + ∂α′∂ϵ = expand_trig(Derivative(α′, ϵ).doit().subs(ϵ, 0).expand().simplify().subs(subs)) + ∂β′∂ϵ = expand_trig(Derivative(β′, ϵ).doit().subs(ϵ, 0).expand().simplify().subs(subs)) + ∂γ′∂ϵ = expand_trig(Derivative(γ′, ϵ).doit().subs(ϵ, 0).expand().simplify().subs(subs)) - return ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ + return ∂α′∂ϵ, ∂β′∂ϵ, ∂γ′∂ϵ end ## Note that we are not including the factor of ``i`` here; for simplicity, we will insert @@ -170,22 +170,22 @@ macro display(expr) arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z")[expr.args[2]] if op == "L" quote - ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ = latex.($expr) # Call expr; format results as LaTeX + ∂α′∂ϵ, ∂β′∂ϵ, ∂γ′∂ϵ = latex.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = i\left[ - %$(∂α′∂θ) \frac{\partial}{\partial \alpha} - + %$(∂β′∂θ) \frac{\partial}{\partial \beta} - + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + %$(∂α′∂ϵ) \frac{\partial}{\partial \alpha} + + %$(∂β′∂ϵ) \frac{\partial}{\partial \beta} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial \gamma} \right]""" # Display the result in LaTeX form end else quote - ∂α′∂θ, ∂β′∂θ, ∂γ′∂θ = latex.($expr) # Call expr; format results as LaTeX + ∂α′∂ϵ, ∂β′∂ϵ, ∂γ′∂ϵ = latex.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = -i\left[ - %$(∂α′∂θ) \frac{\partial}{\partial \alpha} - + %$(∂β′∂θ) \frac{\partial}{\partial \beta} - + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + %$(∂α′∂ϵ) \frac{\partial}{\partial \alpha} + + %$(∂β′∂ϵ) \frac{\partial}{\partial \beta} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial \gamma} \right]""" # Display the result in LaTeX form end end @@ -199,21 +199,21 @@ macro display2(expr) arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z")[expr.args[2]] if op == "L" quote - ∂φ′∂θ, ∂ϑ′∂θ, ∂γ′∂θ = $conversion.($expr) # Call expr; format results as LaTeX + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = $conversion.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = i\left[ - %$(∂ϑ′∂θ) \frac{\partial}{\partial \theta} - + %$(∂φ′∂θ) \frac{\partial}{\partial \phi} + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial \phi} \right]""" # Display the result in LaTeX form end else quote - ∂φ′∂θ, ∂ϑ′∂θ, ∂γ′∂θ = $conversion.($expr) # Call expr; format results as LaTeX + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = $conversion.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = -i\left[ - %$(∂ϑ′∂θ) \frac{\partial}{\partial \theta} - + %$(∂φ′∂θ) \frac{\partial}{\partial \phi} - + %$(∂γ′∂θ) \frac{\partial}{\partial \gamma} + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial \phi} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial \gamma} \right]""" # Display the result in LaTeX form end end From cf2dbeece6b4d58ce2c74a5937a25578bc382670 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 12:41:07 -0500 Subject: [PATCH 096/329] Explicitly lay the groundwork for R --- docs/literate_input/euler_angular_momentum.jl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 5007e1bd..452fc8e2 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -79,7 +79,22 @@ these new Euler angles: \right) f \left(\alpha, \beta, \gamma\right) \right]_{\epsilon=0}, \end{align} ``` -and similarly for ``R_j``. +or for ``R_j``: +```math +\begin{align} + R_j f(\mathbf{R}_{\alpha, \beta, \gamma}) + &= + -\left. i \frac{\partial} {\partial \epsilon} f \left( \mathbf{R}_{\alpha, \beta, \gamma} + e^{-\epsilon \mathbf{e}_j / 2} \right) \right|_{\epsilon=0} + \\ + &= + -i \left[ \left( + \frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha} + + \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta} + + \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma} + \right) f \left(\alpha, \beta, \gamma\right) \right]_{\epsilon=0}. +\end{align} +``` So the objective is to find the new Euler angles, differentiate with respect to ``\epsilon``, and then evaluate at ``\epsilon = 0``. We do this by first multiplying From 294046c38a276f20cbab68d3712054468daa5001 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 12:43:25 -0500 Subject: [PATCH 097/329] Try memoization to speed up computations --- docs/Project.toml | 1 + docs/literate_input/euler_angular_momentum.jl | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 17afff78..997086ca 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,6 +5,7 @@ LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +Memoization = "6fafb56a-5788-4b4e-91ca-c0cea6611c73" Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SphericalFunctions = "af6d55de-b1f7-4743-b797-0829a72cf84e" diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 452fc8e2..7e01b45e 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -111,6 +111,7 @@ the new Euler angles in terms of those components according to the usual express # ## Computational infrastructure # We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. +import Memoization: @memoize import LaTeXStrings: @L_str, LaTeXString import Quaternionic: Quaternionic, Quaternion, components import SymPyPythonCall @@ -133,7 +134,7 @@ const 𝐤 = Quaternion{Int}(Quaternionic.𝐤) nothing #hide # Next, we define functions to compute the Euler components of the left and right operators -function 𝒪(u, side) +@memoize function 𝒪(u, side) ## Substitutions that sympy doesn't make but we want subs = Dict( cos(β)/sin(β) => 1/tan(β), From 5b18bf267c7fe23b38288209e847b1552123d5df Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 13:05:03 -0500 Subject: [PATCH 098/329] Point out anomalous commutator relations --- docs/literate_input/euler_angular_momentum.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 7e01b45e..5a054481 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -282,7 +282,9 @@ nothing #hide # which says that ``R`` is just the *negative of* the ``L`` operator transformed to the # body-fixed frame. That negative sign is slightly unnatural, but the reason we choose to # define ``R`` in this way is for its more natural connection to the literature on -# spin-weighted spherical functions. +# spin-weighted spherical functions. Also, ``R`` defined here obeys the same commutation +# relations as the standard angular-momentum operators, whereas the Wikipedia convention +# leads to "anomalous" commutation relations with an extra minus sign. # ### Commutators From 237078afe3feeb6ddfbf8afde1224ea20a3d7711 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 13:05:29 -0500 Subject: [PATCH 099/329] See what happens if I just hide the first SymPyPythonCall import --- docs/literate_input/euler_angular_momentum.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 5a054481..af38c70d 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -105,9 +105,8 @@ the new Euler angles in terms of those components according to the usual express """ #src # Do this first just to hide stdout of the conda installation step -# ````@setup euler_angular_momentum -# import SymPyPythonCall -# ```` +import SymPyPythonCall #hide + # ## Computational infrastructure # We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. From 1298ab6348533938c6cce956a375d7c47b1530df Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:22:20 -0500 Subject: [PATCH 100/329] Bad things happened, that's what --- docs/literate_input/euler_angular_momentum.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index af38c70d..4e9a8347 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -104,9 +104,11 @@ the new Euler angles in terms of those components according to the usual express """ -#src # Do this first just to hide stdout of the conda installation step -import SymPyPythonCall #hide - +#src # Do this first just to hide stdout of the conda installation step. +#src # Note that we can't just use `#hide` because that still shows stdout. +# ````@setup euler_angular_momentum +# import SymPyPythonCall +# ```` # ## Computational infrastructure # We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. From 33248e8b622992b5bfa9d55ba98efe0992af45cb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:25:21 -0500 Subject: [PATCH 101/329] Include anchors for significant results --- docs/literate_input/euler_angular_momentum.jl | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 4e9a8347..335ccf3f 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -243,12 +243,19 @@ nothing #hide # ## Full expressions on ``S^3`` # Finally, we can actually compute the Euler components of the angular momentum operators. + +#md # ```@raw html +#md # +#md # ``` @display L(𝐢) #- @display L(𝐣) #- @display L(𝐤) #- +#md # ```@raw html +#md # +#md # ``` @display R(𝐢) #- @display R(𝐣) @@ -324,12 +331,19 @@ nothing #hide # And finally, evaluate each in turn. We expect ``[L_x, L_y] = i L_z`` and cyclic # permutations: + +#md # ```@raw html +#md # +#md # ``` commutator(Lx, Ly) #- commutator(Ly, Lz) #- commutator(Lz, Lx) -# Similarly, we expect ``[R_x, R_y] = i R_z`` and cyclic permutations: + +#md # ```@raw html +#md # +#md # ``` commutator(Rx, Ry) #- commutator(Ry, Rz) @@ -338,28 +352,19 @@ commutator(Rz, Rx) # Just for completeness, let's evaluate the commutators of the left and right operators, # which should all be zero. -commutator(Lx, Rx) -#- -commutator(Lx, Ry) -#- -commutator(Lx, Rz) -#- -commutator(Ly, Rx) -#- -commutator(Ly, Ry) -#- -commutator(Ly, Rz) -#- -commutator(Lz, Rx) -#- -commutator(Lz, Ry) -#- -commutator(Lz, Rz) + +#md # ```@raw html +#md # +#md # ``` # ## Standard expressions on ``S^2`` # We can substitute ``(α, β, γ) \to (φ, θ, 0)`` to get the standard expressions for the # angular momentum operators on the 2-sphere. + +#md # ```@raw html +#md # +#md # ``` @display2 L(𝐢) #- @display2 L(𝐣) @@ -374,6 +379,9 @@ commutator(Lz, Rz) # for historical reasons, we include it here when showing the results of the ``R`` operator # in Euler angles. +#md # ```@raw html +#md # +#md # ``` @display2 R(𝐢) #- @display2 R(𝐣) From 6eed970db9d24d742b3b90761234eca481abd55e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:26:03 -0500 Subject: [PATCH 102/329] Explicitly compute L_{\pm} --- docs/literate_input/euler_angular_momentum.jl | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 335ccf3f..ae61d871 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -210,13 +210,31 @@ end nothing #hide # And we'll need another for the angular-momentum operators in standard ``S^2`` form. -conversion(∂) = latex(∂.subs(Dict(α => ϕ, β => θ, γ => 0)).simplify()) +conversion(∂) = ∂.subs(Dict(α => ϕ, β => θ, γ => 0)).simplify() macro display2(expr) op = string(expr.args[1]) - arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z")[expr.args[2]] - if op == "L" + element = expr.args[2] + arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z", :+ => "+", :- => "-")[element] + @info element + if op == "L" && arg ∈ ("+", "-") quote - ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = $conversion.($expr) # Call expr; format results as LaTeX + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = ( + ( + ($element)(I * $conversion(i), -$conversion(j)).rewrite(exp) + / exp(($element)(I) * ϕ) + ).simplify() + for (i, j) ∈ zip(L(𝐢), L(𝐣)) + ) + expr = $op * "_" * $arg # Standard form of the operator + expsign = ($arg=="+" ? "" : "-") + L"""%$expr = e^{%$expsign i \phi} \left[ + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial \phi} + \right]""" # Display the result in LaTeX form + end + elseif op == "L" + quote + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = latex.($conversion.($expr)) # Call expr; format as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = i\left[ %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} @@ -225,7 +243,7 @@ macro display2(expr) end else quote - ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = $conversion.($expr) # Call expr; format results as LaTeX + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = latex.($conversion.($expr)) # Call expr; format as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = -i\left[ %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} @@ -371,8 +389,19 @@ commutator(Rz, Rx) #- @display2 L(𝐤) -# Those are indeed the standard expressions for the angular-momentum operators on the -# 2-sphere, so we can declare success! +# We can also provide the usual expressions for the raising and lowering operators in terms +# of spherical coordinates with ``L_{\pm} = L_x \pm i L_y``: + +#md # ```@raw html +#md # +#md # ``` +@display2 L(+) +#- +@display2 L(-) + +# These are all indeed the standard expressions for the angular-momentum operators on the +# 2-sphere, as seen in numerous references, so we can declare compatibility between our +# unusual definition of ``L`` and more standard definitions. # # Now, note that including ``\partial_\gamma`` for an expression on the 2-sphere doesn't # actually make any sense: ``\gamma`` isn't even a coordinate for the 2-sphere! However, From c92e204891e15069beb2b577930d69f26117b770 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:26:26 -0500 Subject: [PATCH 103/329] Simplify zero commutators --- docs/literate_input/euler_angular_momentum.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index ae61d871..78b0e906 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -374,6 +374,8 @@ commutator(Rz, Rx) #md # ```@raw html #md # #md # ``` +[commutator(L, R) for L ∈ (Lx, Ly, Lz), R ∈ (Rx, Ry, Rz)] +# This completes independent commutator results, which are all as we expect them to be. # ## Standard expressions on ``S^2`` From bd8f9c96f3d733a350f29df981dbb5d75dff3725 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:26:48 -0500 Subject: [PATCH 104/329] A little more explanation --- docs/literate_input/euler_angular_momentum.jl | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 78b0e906..32ede654 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -123,8 +123,9 @@ const π = sympy.pi const I = sympy.I nothing #hide -# Define coordinates we will use +# Define symbols we will use throughout α, β, γ, θ, ϕ, ϵ = symbols("α β γ θ ϕ ϵ", real=true, positive=true) +f = symbols("f", cls=SymPyPythonCall.sympy.o.Function) nothing #hide # Reinterpret the quaternion basis elements for compatibility with SymPy. (`Quaternionic` @@ -315,8 +316,9 @@ nothing #hide # ### Commutators # We can also compute the commutators of the angular momentum operators, as derived above. -# First, we define the operators acting on functions of the Euler angles. -f = symbols("f", cls=SymPyPythonCall.sympy.o.Function) +# First, we define the operators acting on functions of the Euler angles. Note that this +# function differs from the one above because it explicitly takes the function ``f`` and the +# Euler angles as arguments — which will be necessary to compute the commutators. function 𝒪(u, side, f, α, β, γ) let O = 𝒪(u, side) (side==:left ? I : -I) * ( @@ -354,19 +356,22 @@ nothing #hide #md # #md # ``` commutator(Lx, Ly) -#- +# which equals ``i L_z``, commutator(Ly, Lz) -#- +# which equals ``i L_x``, and commutator(Lz, Lx) +# which equals ``i L_y``. Similarly, we expect ``[R_x, R_y] = i R_z`` and cyclic +# permutations: #md # ```@raw html #md # #md # ``` commutator(Rx, Ry) -#- +# which equals ``i R_z``, commutator(Ry, Rz) -#- +# which equals ``i R_x``, and commutator(Rz, Rx) +# which equals ``i R_y`` — all as expected. # Just for completeness, let's evaluate the commutators of the left and right operators, # which should all be zero. From f7b77e05b7f923812758ba75bc01e86f0d57b2f9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:28:37 -0500 Subject: [PATCH 105/329] Remove literate_output between runs --- docs/make.jl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 5fd424e2..d01366bf 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,15 +10,20 @@ start = time() # We'll display the total after everything has finished using Documenter using Literate +using DocumenterCitations + docs_src_dir = joinpath(@__DIR__, "src") # See LiveServer.jl docs for this: https://juliadocs.org/LiveServer.jl/dev/man/ls+lit/ literate_input = joinpath(@__DIR__, "literate_input") literate_output = joinpath(docs_src_dir, "literate_output") +rm(literate_output; force=true, recursive=true) for (root, _, files) ∈ walkdir(literate_input), file ∈ files # ignore non julia files splitext(file)[2] == ".jl" || continue + # If the file is "euler_angular_momentum.jl", skip it + #file == "euler_angular_momentum.jl" && (@warn "Re-enable euler_angular_momentum.jl"; continue) # full path to a literate script input_path = joinpath(root, file) # generated output path @@ -27,8 +32,7 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files Literate.markdown(input_path, output_path, documenter=true, mdstrings=true) end relative_literate_output = relpath(literate_output, docs_src_dir) - -using DocumenterCitations +relative_convention_comparisons = joinpath(relative_literate_output, "conventions_comparisons") bib = CitationBibliography( joinpath(docs_src_dir, "references.bib"); @@ -38,6 +42,8 @@ bib = CitationBibliography( using SphericalFunctions DocMeta.setdocmeta!(SphericalFunctions, :DocTestSetup, :(using SphericalFunctions); recursive=true) +@warn """Re-enable "Calculations" below""" + makedocs( plugins=[bib], sitename="SphericalFunctions.jl", @@ -63,6 +69,9 @@ makedocs( "conventions/summary.md", "conventions/details.md", "conventions/comparisons.md", + "Comparisons" => [ + joinpath(relative_convention_comparisons, "condon_shortley_1935.md"), + ], "Calculations" => [ joinpath(relative_literate_output, "euler_angular_momentum.md"), ], @@ -83,4 +92,4 @@ deploydocs( push_preview=true ) -println("Docs built in ", time() - start, " seconds.") +println("Docs built in ", time() - start, " seconds.\n\n") From 24bc6ec1e12d5e80104e781baa5709362348793e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:29:10 -0500 Subject: [PATCH 106/329] Don't warn about something that's been removed --- docs/make.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index d01366bf..e2bfb06f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -42,8 +42,6 @@ bib = CitationBibliography( using SphericalFunctions DocMeta.setdocmeta!(SphericalFunctions, :DocTestSetup, :(using SphericalFunctions); recursive=true) -@warn """Re-enable "Calculations" below""" - makedocs( plugins=[bib], sitename="SphericalFunctions.jl", From 437966e532a2765bcd95a7db04a550298b5b1e58 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 17:04:55 -0500 Subject: [PATCH 107/329] Start moving comparisons to separate Literate files --- .../condon_shortley_1935.jl | 70 +++++++++++++++++++ docs/src/conventions/comparisons.md | 28 +------- 2 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 docs/literate_input/conventions_comparisons/condon_shortley_1935.jl diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl new file mode 100644 index 00000000..a2538a0d --- /dev/null +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -0,0 +1,70 @@ +md""" +# Condon-Shortley (1935) + +[Condon and Shortley's "The Theory Of Atomic Spectra"](@cite CondonShortley_1935) is the +standard reference for the "Condon-Shortley phase convention". Though some references are +not very clear about precisely what they mean by that phrase, it seems clear that the real +meaning includes the idea that the angular-momentum raising and lowering operators have +*real* eigenvalues when acting on the spherical harmonics. To avoid ambiguity, we can just +look at the actual spherical harmonics they define. + +The method we use here is as direct and explicit as possible. In particular, Condon and +Shortley provide a formula for the φ=0 part in terms of iterated derivatives of a power of +sin(θ). Rather than expressing these derivatives in terms of the Legendre polynomials — +which would subject us to another round of ambiguity — the functions in this module use +automatic differentiation to compute the derivatives explicitly. + +Condon and Shortley are not very explicit about the meaning of the spherical coordinates, +but they do describe them as "spherical polar coordinates ``r, \theta, \varphi``" +immediately before equation (1) of section 4³ (page 50), +```math +L_z = -i \hbar \frac{\partial}{\partial \varphi}, +``` +followed by equation (8): +```math +\begin{aligned} +L_x + i L_y &= \hbar e^{i\varphi} \left( + \frac{\partial}{\partial \theta} + + i \cot\theta \frac{\partial}{\partial \varphi} +\right) \\ +L_x - i L_y &= \hbar e^{-i\varphi} \left( + -\frac{\partial}{\partial \theta} + + i \cot\theta \frac{\partial}{\partial \varphi} +\right). +\end{aligned} +``` + +Because these expressions agree nicely with our results, + + +The result is that the original Condon-Shortley spherical harmonics agree perfectly with the +ones computed by this package. + +(Condon and Shortley do not give an expression for the Wigner D-matrices.) + +""" + +using TestItems: @testmodule, @testitem #hide + +# ## Function definitions +@testmodule CondonShortley1935 begin #hide + +#+ +# and so on +const 𝒾 = im + +end #hide + +# ## Tests + +@testitem "Condon-Shortley (1935)" setup=[CondonShortley1935] begin #hide + +#+ +# Here's a test +@test 2+2 == 4 + +#+ +# And another +@test CondonShortley1935.𝒾^2 == -1 + +end #hide diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 4d97236a..b65b4d18 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -112,32 +112,6 @@ Cohen-Tannoudji does not appear to define the Wigner D-matrices. ## Condon-Shortley (1935) -[Condon and Shortley's "The Theory Of Atomic Spectra"](@cite -CondonShortley_1935) is the standard reference for the -"Condon-Shortley phase convention" — though no one is ever too clear -about precisely what that means. To avoid ambiguity, we can just look -at the actual spherical harmonics they define. - -They are not very explicit about the meaning of the spherical -coordinates, but they do describe them as "spherical polar coordinates -``r, \theta, \varphi``" immediately before equation (1) of section 4³ -(page 50), -```math -L_z = -i \hbar \frac{\partial}{\partial \varphi}, -``` -followed by equation (8): -```math -\begin{aligned} -L_x + i L_y &= \hbar e^{i\varphi} \left( - \frac{\partial}{\partial \theta} - + i \cot\theta \frac{\partial}{\partial \varphi} -\right) \\ -L_x - i L_y &= \hbar e^{-i\varphi} \left( - -\frac{\partial}{\partial \theta} - + i \cot\theta \frac{\partial}{\partial \varphi} -\right). -\end{aligned} -``` Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence of the spherical harmonic as @@ -910,7 +884,7 @@ covariant components: &= -i \frac{\partial}{\partial \alpha} \end{aligned} ``` -We can compare these to the [Full expressions on ``S^3``](@ref), and find +We can compare these to the [Full expressions on ``S^3``]() `@ref`, and find that they are precisely equivalent to expressions for ``L_j`` computed in this package's conventions. From c2590b71c00c0eb1be8183c625327a2f32ab2c86 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 09:45:16 -0500 Subject: [PATCH 108/329] Just use section headers for anchors, as Documenter is not yet ready for arbitrary anchors --- docs/literate_input/euler_angular_momentum.jl | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/euler_angular_momentum.jl index 32ede654..7b81e970 100644 --- a/docs/literate_input/euler_angular_momentum.jl +++ b/docs/literate_input/euler_angular_momentum.jl @@ -263,18 +263,14 @@ nothing #hide # ## Full expressions on ``S^3`` # Finally, we can actually compute the Euler components of the angular momentum operators. -#md # ```@raw html -#md # -#md # ``` +#md # ### ``L`` operators in terms of Euler angles @display L(𝐢) #- @display L(𝐣) #- @display L(𝐤) #- -#md # ```@raw html -#md # -#md # ``` +#md # ### ``R`` operators in terms of Euler angles @display R(𝐢) #- @display R(𝐣) @@ -313,7 +309,7 @@ nothing #hide # relations as the standard angular-momentum operators, whereas the Wikipedia convention # leads to "anomalous" commutation relations with an extra minus sign. -# ### Commutators +# ## Commutators # We can also compute the commutators of the angular momentum operators, as derived above. # First, we define the operators acting on functions of the Euler angles. Note that this @@ -352,9 +348,7 @@ nothing #hide # And finally, evaluate each in turn. We expect ``[L_x, L_y] = i L_z`` and cyclic # permutations: -#md # ```@raw html -#md # -#md # ``` +#md # ### ``L`` commutators in Euler angles commutator(Lx, Ly) # which equals ``i L_z``, commutator(Ly, Lz) @@ -363,9 +357,7 @@ commutator(Lz, Lx) # which equals ``i L_y``. Similarly, we expect ``[R_x, R_y] = i R_z`` and cyclic # permutations: -#md # ```@raw html -#md # -#md # ``` +#md # ### ``R`` commutators in Euler angles commutator(Rx, Ry) # which equals ``i R_z``, commutator(Ry, Rz) @@ -376,9 +368,7 @@ commutator(Rz, Rx) # Just for completeness, let's evaluate the commutators of the left and right operators, # which should all be zero. -#md # ```@raw html -#md # -#md # ``` +#md # ### ``L,R`` commutators in Euler angles [commutator(L, R) for L ∈ (Lx, Ly, Lz), R ∈ (Rx, Ry, Rz)] # This completes independent commutator results, which are all as we expect them to be. @@ -387,9 +377,7 @@ commutator(Rz, Rx) # We can substitute ``(α, β, γ) \to (φ, θ, 0)`` to get the standard expressions for the # angular momentum operators on the 2-sphere. -#md # ```@raw html -#md # -#md # ``` +#md # ### ``L`` operators in spherical coordinates @display2 L(𝐢) #- @display2 L(𝐣) @@ -399,9 +387,7 @@ commutator(Rz, Rx) # We can also provide the usual expressions for the raising and lowering operators in terms # of spherical coordinates with ``L_{\pm} = L_x \pm i L_y``: -#md # ```@raw html -#md # -#md # ``` +#md # ### ``L_{\pm}`` operators in spherical coordinates @display2 L(+) #- @display2 L(-) @@ -415,9 +401,7 @@ commutator(Rz, Rx) # for historical reasons, we include it here when showing the results of the ``R`` operator # in Euler angles. -#md # ```@raw html -#md # -#md # ``` +#md # ### ``R`` operators in spherical coordinates @display2 R(𝐢) #- @display2 R(𝐣) From 4236a4673bbdc6be84f741dbd2c9115a40527d73 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 09:46:36 -0500 Subject: [PATCH 109/329] Move Condon-Shortley documentation to its new literate_input home --- .../condon_shortley_1935.jl | 181 ++++++++++++++++-- docs/src/conventions/comparisons.md | 36 ---- 2 files changed, 161 insertions(+), 56 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index a2538a0d..a2045406 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -3,10 +3,10 @@ md""" [Condon and Shortley's "The Theory Of Atomic Spectra"](@cite CondonShortley_1935) is the standard reference for the "Condon-Shortley phase convention". Though some references are -not very clear about precisely what they mean by that phrase, it seems clear that the real -meaning includes the idea that the angular-momentum raising and lowering operators have -*real* eigenvalues when acting on the spherical harmonics. To avoid ambiguity, we can just -look at the actual spherical harmonics they define. +not very clear about precisely what they mean by that phrase, it seems clear that the +original meaning included the idea that the angular-momentum raising and lowering operators +have eigenvalues that are *real and positive* when acting on the spherical harmonics. To +avoid ambiguity, we can just look at the actual spherical harmonics they define. The method we use here is as direct and explicit as possible. In particular, Condon and Shortley provide a formula for the φ=0 part in terms of iterated derivatives of a power of @@ -15,12 +15,14 @@ which would subject us to another round of ambiguity — the functions in this m automatic differentiation to compute the derivatives explicitly. Condon and Shortley are not very explicit about the meaning of the spherical coordinates, -but they do describe them as "spherical polar coordinates ``r, \theta, \varphi``" -immediately before equation (1) of section 4³ (page 50), +but they do describe them as "spherical polar coordinates ``r, \theta, \varphi``". +Immediately before equation (1) of section 4³ (page 50), they define the angular-momentum +operator ```math L_z = -i \hbar \frac{\partial}{\partial \varphi}, ``` -followed by equation (8): +which agrees with [our expression](@ref "``L`` operators in spherical coordinates"). This +is followed by equation (8): ```math \begin{aligned} L_x + i L_y &= \hbar e^{i\varphi} \left( @@ -30,12 +32,11 @@ L_x + i L_y &= \hbar e^{i\varphi} \left( L_x - i L_y &= \hbar e^{-i\varphi} \left( -\frac{\partial}{\partial \theta} + i \cot\theta \frac{\partial}{\partial \varphi} -\right). +\right), \end{aligned} ``` - -Because these expressions agree nicely with our results, - +which also agrees with [our results.](@ref "``L_{\pm}`` operators in spherical coordinates") +We can infer that the definitions of the spherical coordinates are consistent with ours. The result is that the original Condon-Shortley spherical harmonics agree perfectly with the ones computed by this package. @@ -47,24 +48,164 @@ ones computed by this package. using TestItems: @testmodule, @testitem #hide # ## Function definitions +# +# We begin with some basic code + @testmodule CondonShortley1935 begin #hide -#+ -# and so on +import FastDifferentiation const 𝒾 = im +struct Factorial end +Base.:*(n::Integer, ::Factorial) = factorial(big(n)) +const ❗ = Factorial() -end #hide +#+ +# Equation (12) of section 4³ (page 51) writes the solution to the three-dimensional Laplace +# equation in spherical coordinates as +# ```math +# \psi(\gamma, \ell, m_\ell) +# = +# B(\gamma, \ell) \Theta(\ell, m_\ell) \Phi(m_\ell), +# ``` +# where ``B`` is independent of ``\theta`` and ``\varphi``, and ``\gamma`` represents any +# number of eigenvalues required to specify the state. More explicitly, below Eq. (5) of +# section 5⁵ (page 127), they specifically define the spherical harmonics as +# ```math +# \phi(\ell, m_\ell) = \Theta(\ell, m_\ell) \Phi(m_\ell). +# ``` +# One quirk of their notation is that the dependence on ``\theta`` and ``\varphi`` is +# implicit in their functions; we make it explicit, as Julia requires: +function ϕ(ℓ, mₗ, θ, φ) + Θ(ℓ, mₗ, θ) * Φ(mₗ, φ) +end -# ## Tests +#+ +# The ``\varphi`` part is given by equation (5) of section 4³ (page 50): +# ```julia +# 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) +# ``` +# ```math +# \Phi(m_\ell) +# = +# \frac{1}{\sqrt{2\pi}} e^{i m_\ell \varphi}. +# ``` +# The dependence on ``\varphi`` is implicit, but we make it explicit here: +function Φ(mₗ, φ::T) where {T} + 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) +end + +#+ +# Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence as +# ```math +# \Theta(\ell, m) +# = +# (-1)^\ell +# \sqrt{\frac{(2\ell+1)}{2} \frac{(\ell+m)!}{(\ell-m)!}} +# \frac{1}{2^\ell \ell!} +# \frac{1}{\sin^m \theta} +# \frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. +# ``` +# Again, the dependence on ``\theta`` is implicit, but we make it explicit here: +function Θ(ℓ, m, θ::T) where {T} + (-1)^ℓ * T(√(((2ℓ+1) * (ℓ+m)❗) / (2 * (ℓ - m)❗)) * (1 / (2^ℓ * (ℓ)❗))) * + (1 / sin(θ)^T(m)) * dʲsin²ᵏθdcosθʲ(ℓ-m, ℓ, θ) +end -@testitem "Condon-Shortley (1935)" setup=[CondonShortley1935] begin #hide +#+ +# We can use `FastDifferentiation` to compute the derivative term: +function dʲsin²ᵏθdcosθʲ(j, k, θ) + if j < 0 + throw(ArgumentError("j=$j must be non-negative")) + end + if j == 0 + return sin(θ)^(2k) + end + x = FastDifferentiation.make_variables(:x)[1] + ∂ₓʲfᵏ = FastDifferentiation.derivative((1 - x^2)^k, (x for _ ∈ 1:j)...) + return FastDifferentiation.make_function([∂ₓʲfᵏ,], [x,])(cos(θ))[1] +end #+ -# Here's a test -@test 2+2 == 4 + +# It may be helpful to check some values against explicit formulas for the first few +# spherical harmonics as given by Condon-Shortley in the footnote to Eq. (15) of Sec. 4³ +# (page 52): +ϴ(ℓ, m, θ) = ϴ(Val(ℓ), Val(m), θ) +ϴ(::Val{0}, ::Val{0}, θ) = √(1/2) +ϴ(::Val{1}, ::Val{0}, θ) = √(3/2) * cos(θ) +ϴ(::Val{2}, ::Val{0}, θ) = √(5/8) * (2cos(θ)^2 - sin(θ)^2) +ϴ(::Val{3}, ::Val{0}, θ) = √(7/8) * (2cos(θ)^3 - 3cos(θ)sin(θ)^2) +ϴ(::Val{1}, ::Val{+1}, θ) = -√(3/4) * sin(θ) +ϴ(::Val{1}, ::Val{-1}, θ) = +√(3/4) * sin(θ) +ϴ(::Val{2}, ::Val{+1}, θ) = -√(15/4) * cos(θ) * sin(θ) +ϴ(::Val{2}, ::Val{-1}, θ) = +√(15/4) * cos(θ) * sin(θ) +ϴ(::Val{3}, ::Val{+1}, θ) = -√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) +ϴ(::Val{3}, ::Val{-1}, θ) = +√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) +ϴ(::Val{2}, ::Val{+2}, θ) = √(15/16) * sin(θ)^2 +ϴ(::Val{2}, ::Val{-2}, θ) = √(15/16) * sin(θ)^2 +ϴ(::Val{3}, ::Val{+2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 +ϴ(::Val{3}, ::Val{-2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 +ϴ(::Val{3}, ::Val{+3}, θ) = -√(35/32) * sin(θ)^3 +ϴ(::Val{3}, ::Val{-3}, θ) = +√(35/32) * sin(θ)^3 #+ -# And another -@test CondonShortley1935.𝒾^2 == -1 +# Condon and Shortley do not give an expression for the Wigner D-matrices, but the +# convention for spherical harmonics is what they are known for, so this will suffice. + +end #hide + + +# ## Tests + +@testitem "Condon-Shortley conventions" setup=[Utilities, CondonShortley] begin #hide + +using Random +using Quaternionic: from_spherical_coordinates +#const check = NaNChecker.NaNCheck + +Random.seed!(1234) +const T = Float64 +const ℓₘₐₓ = 4 +ϵₐ = 4eps(T) +ϵᵣ = 1000eps(T) + +## Tests for Y(ℓ, m, θ, ϕ) +let Y=CondonShortley.ϕ, Θ=CondonShortley.Θ, ϴ=CondonShortley.ϴ, ϕ=zero(T) + for θ ∈ βrange(T) + if abs(sin(θ)) < ϵₐ + continue + end + + ## # Find where NaNs are coming from + ## for ℓ ∈ 0:ℓₘₐₓ + ## for m ∈ -ℓ:ℓ + ## Θ(ℓ, m, check(θ)) + ## end + ## end + + ## Test footnote to Eq. (15) of Sec. 4³ of Condon-Shortley + let Y = ₛ𝐘(0, 3, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + for ℓ ∈ 0:3 + for m ∈ -ℓ:ℓ + @test ϴ(ℓ, m, θ) / √(2π) ≈ Y[Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ + end + end + end + + ## Test Eq. (18) of Sec. 4³ of Condon-Shortley + for ℓ ∈ 0:ℓₘₐₓ + for m ∈ -ℓ:ℓ + @test Θ(ℓ, m, θ) ≈ (-1)^(m) * Θ(ℓ, -m, θ) atol=ϵₐ rtol=ϵᵣ + end + end + + ## Compare to SphericalHarmonics Y + let s = 0 + Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + Y₂ = [Y(ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] + @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ + end + end +end end #hide diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index b65b4d18..03f573df 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -113,42 +113,6 @@ Cohen-Tannoudji does not appear to define the Wigner D-matrices. ## Condon-Shortley (1935) -Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence -of the spherical harmonic as -```math -\Theta(\ell, m) -= -(-1)^\ell -\sqrt{\frac{(2\ell+1)}{2} \frac{(\ell+m)!}{(\ell-m)!}} -\frac{1}{2^\ell \ell!} -\frac{1}{\sin^m \theta} -\frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. -``` -The ``\varphi`` part is given by equation (5) of section 4³ (page 50): -```julia -1 / √(2T(π)) * exp(𝒾 * mₗ * φ) -``` -```math -\Phi(m_\ell) -= -\frac{1}{\sqrt{2\pi}} e^{i m_\ell \varphi}. -``` -Equation (12) of section 4³ (page 51) writes the solution to the -three-dimensional Laplace equation in spherical coordinates as -```math -\psi(\gamma, \ell, m_\ell) -= -B(\gamma, \ell) \Theta(\ell, m_\ell) \Phi(m_\ell), -``` -where ``B`` is independent of ``\theta`` and ``\varphi``, and ``\gamma`` -represents any number of eigenvalues required to specify the state. -Thus, we take the angular factors, normalized, to define the spherical -harmonics. The result is that the original Condon-Shortley spherical -harmonics agree perfectly with the ones computed by this package. - -Condon and Shortley do not give an expression for the Wigner -D-matrices. - ## Edmonds (1960) From a846efbf0f9a6e2f2ddb96d7439f9448bfa112c2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:26:07 -0500 Subject: [PATCH 110/329] Add some things to make it convenient to run tests on conventions --- .../ConventionsSetup.jl | 6 +++ .../ConventionsUtilities.jl | 53 +++++++++++++++++++ docs/make.jl | 8 +-- src/evaluate.jl | 6 +++ test/utilities/utilities.jl | 16 +++++- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 docs/literate_input/conventions_comparisons/ConventionsSetup.jl create mode 100644 docs/literate_input/conventions_comparisons/ConventionsUtilities.jl diff --git a/docs/literate_input/conventions_comparisons/ConventionsSetup.jl b/docs/literate_input/conventions_comparisons/ConventionsSetup.jl new file mode 100644 index 00000000..d3e829dd --- /dev/null +++ b/docs/literate_input/conventions_comparisons/ConventionsSetup.jl @@ -0,0 +1,6 @@ +@testsnippet ConventionsSetup begin + +using Random +Random.seed!(1234) + +end diff --git a/docs/literate_input/conventions_comparisons/ConventionsUtilities.jl b/docs/literate_input/conventions_comparisons/ConventionsUtilities.jl new file mode 100644 index 00000000..1e062a6c --- /dev/null +++ b/docs/literate_input/conventions_comparisons/ConventionsUtilities.jl @@ -0,0 +1,53 @@ +@testmodule ConventionsUtilities begin + import FastDifferentiation + + const 𝒾 = im + + struct Factorial end + Base.:*(n::Integer, ::Factorial) = factorial(big(n)) + function Base.:*(n::Rational, ::Factorial) where {Rational} + if denominator(n) == 1 + return factorial(big(numerator(n))) + else + throw(ArgumentError("Cannot compute factorial of a non-integer rational")) + end + end + const ❗ = Factorial() + + function dʲsin²ᵏθdcosθʲ(;j, k, θ) + if j < 0 + throw(ArgumentError("j=$j must be non-negative")) + end + if j == 0 + return sin(θ)^(2k) + end + x = FastDifferentiation.make_variables(:x)[1] + ∂ₓʲfᵏ = FastDifferentiation.derivative((1 - x^2)^k, (x for _ ∈ 1:j)...) + return FastDifferentiation.make_function([∂ₓʲfᵏ,], [x,])(cos(θ))[1] + end + +end + + +@testitem "dʲsin²ᵏθdcosθʲ" setup=[ConventionsUtilities, Utilities] begin + # dʲsin²ᵏθdcosθʲ is intended to represent the jth derivative of sin(θ)^(2k) with respect + # to cos(θ). We can compare it to some actual derivatives of sin(θ)^(2k) to verify its + # correctness. + import .ConventionsUtilities: dʲsin²ᵏθdcosθʲ + for θ ∈ βrange(Float64, 15) + @test dʲsin²ᵏθdcosθʲ(j=0, k=0, θ=θ) ≈ 1 + @test dʲsin²ᵏθdcosθʲ(j=0, k=1, θ=θ) ≈ sin(θ)^2 + @test dʲsin²ᵏθdcosθʲ(j=1, k=1, θ=θ) ≈ -2cos(θ) + @test dʲsin²ᵏθdcosθʲ(j=2, k=1, θ=θ) ≈ -2 + @test dʲsin²ᵏθdcosθʲ(j=3, k=1, θ=θ) ≈ 0 + @test dʲsin²ᵏθdcosθʲ(j=0, k=2, θ=θ) ≈ sin(θ)^4 + @test dʲsin²ᵏθdcosθʲ(j=1, k=2, θ=θ) ≈ -4 * cos(θ) * sin(θ)^2 atol=4eps() + @test dʲsin²ᵏθdcosθʲ(j=2, k=2, θ=θ) ≈ -4 + 12cos(θ)^2 + @test dʲsin²ᵏθdcosθʲ(j=3, k=2, θ=θ) ≈ 24cos(θ) + @test dʲsin²ᵏθdcosθʲ(j=4, k=2, θ=θ) ≈ 24 + @test dʲsin²ᵏθdcosθʲ(j=5, k=2, θ=θ) ≈ 0 + @test dʲsin²ᵏθdcosθʲ(j=0, k=3, θ=θ) ≈ sin(θ)^6 + @test dʲsin²ᵏθdcosθʲ(j=1, k=3, θ=θ) ≈ -6 * cos(θ) * sin(θ)^4 atol=4eps() + @test dʲsin²ᵏθdcosθʲ(j=2, k=3, θ=θ) ≈ -6 * sin(θ)^4 + 24cos(θ)^2 * sin(θ)^2 atol=100eps() + end +end diff --git a/docs/make.jl b/docs/make.jl index e2bfb06f..808c2bfb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -4,6 +4,7 @@ # Pretty-print the current time using Dates +println("\n") @info """Building docs starting at $(Dates.format(Dates.now(), "HH:MM:SS")).""" start = time() # We'll display the total after everything has finished @@ -22,8 +23,9 @@ rm(literate_output; force=true, recursive=true) for (root, _, files) ∈ walkdir(literate_input), file ∈ files # ignore non julia files splitext(file)[2] == ".jl" || continue - # If the file is "euler_angular_momentum.jl", skip it - #file == "euler_angular_momentum.jl" && (@warn "Re-enable euler_angular_momentum.jl"; continue) + # If the file is "ConventionsUtilities.jl" or "ConventionsSetup.jl", skip it + file == "ConventionsUtilities.jl" && continue + file == "ConventionsSetup.jl" && continue # full path to a literate script input_path = joinpath(root, file) # generated output path @@ -90,4 +92,4 @@ deploydocs( push_preview=true ) -println("Docs built in ", time() - start, " seconds.\n\n") +println("Docs built in ", time() - start, " seconds.\n") diff --git a/src/evaluate.jl b/src/evaluate.jl index c9e3d234..65610c5b 100644 --- a/src/evaluate.jl +++ b/src/evaluate.jl @@ -608,3 +608,9 @@ function ₛ𝐘(s, ℓₘₐₓ, ::Type{T}=Float64, Rθϕ=golden_ratio_spiral_r end ₛ𝐘 end + +# PRIVATE FUNCTION: This function is not intended for use outside of `SphericalFunctions` +function Y(ℓ, m, θ, ϕ) + θ, ϕ = promote(θ, ϕ) + ₛ𝐘(0, ℓ, typeof(θ), [Quaternionic.from_spherical_coordinates(θ, ϕ)])[1, Yindex(ℓ, m)] +end diff --git a/test/utilities/utilities.jl b/test/utilities/utilities.jl index a4b3431c..96e06e30 100644 --- a/test/utilities/utilities.jl +++ b/test/utilities/utilities.jl @@ -1,13 +1,24 @@ @testsnippet Utilities begin +ℓmrange(ℓₘₐₓ) = eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) + αrange(::Type{T}, n=15) where T = T[ 0; nextfloat(T(0)); rand(T(0):eps(T(π)):T(π), n÷2); prevfloat(T(π)); T(π); nextfloat(T(π)); rand(T(π):eps(2T(π)):2T(π), n÷2); prevfloat(T(π)); 2T(π) ] -βrange(::Type{T}, n=15) where T = T[ - 0; nextfloat(T(0)); rand(T(0):eps(T(π)):T(π), n); prevfloat(T(π)); T(π) +βrange(::Type{T}=Float64, n=15; avoid_zeros=0) where T = T[ + avoid_zeros; nextfloat(T(avoid_zeros)); + rand(T(0):eps(T(π)):T(π), n); + prevfloat(T(π)-avoid_zeros); T(π)-avoid_zeros ] γrange(::Type{T}, n=15) where T = αrange(T, n) + +const θrange = βrange +const φrange = αrange +θϕrange(::Type{T}=Float64, n=15; avoid_zeros=0) where T = vec(collect( + Iterators.product(θrange(T, n; avoid_zeros), φrange(T, n)) +)) + v̂range(::Type{T}, n=15) where T = QuatVec{T}[ 𝐢; 𝐣; 𝐤; -𝐢; -𝐣; -𝐤; @@ -29,6 +40,7 @@ function Rrange(::Type{T}, n=15) where T randn(Rotor{T}, n) ] end + epsilon(k) = ifelse(k>0 && isodd(k), -1, 1) """ From 7a1dfe14de46c14baf028ceba864605def1f13b7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:27:56 -0500 Subject: [PATCH 111/329] Streamline tests --- .../condon_shortley_1935.jl | 175 +++++++----------- 1 file changed, 72 insertions(+), 103 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index a2045406..9cc70f4d 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -45,21 +45,20 @@ ones computed by this package. """ -using TestItems: @testmodule, @testitem #hide - -# ## Function definitions +# ## Implementing formulas # -# We begin with some basic code +# We begin by writing code that implements the formulas from Condon-Shortley. We +# encapsulate the formulas in a module so that we can test them against the +# SphericalHarmonics package. -@testmodule CondonShortley1935 begin #hide +using TestItems: @testitem #hide +@testitem "Condon-Shortley conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide -import FastDifferentiation -const 𝒾 = im -struct Factorial end -Base.:*(n::Integer, ::Factorial) = factorial(big(n)) -const ❗ = Factorial() +module CondonShortley +import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ #+ + # Equation (12) of section 4³ (page 51) writes the solution to the three-dimensional Laplace # equation in spherical coordinates as # ```math @@ -75,26 +74,24 @@ const ❗ = Factorial() # ``` # One quirk of their notation is that the dependence on ``\theta`` and ``\varphi`` is # implicit in their functions; we make it explicit, as Julia requires: -function ϕ(ℓ, mₗ, θ, φ) - Θ(ℓ, mₗ, θ) * Φ(mₗ, φ) +function 𝜙(ℓ, mₗ, 𝜃, φ) + Θ(ℓ, mₗ, 𝜃) * Φ(mₗ, φ) end - #+ + # The ``\varphi`` part is given by equation (5) of section 4³ (page 50): -# ```julia -# 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) -# ``` # ```math # \Phi(m_\ell) # = # \frac{1}{\sqrt{2\pi}} e^{i m_\ell \varphi}. # ``` -# The dependence on ``\varphi`` is implicit, but we make it explicit here: +# Again, we make the dependence on ``\varphi`` explicit, and we capture its type to ensure +# that we don't lose precision when converting π to a floating-point number. function Φ(mₗ, φ::T) where {T} 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) end - #+ + # Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence as # ```math # \Theta(\ell, m) @@ -105,106 +102,78 @@ end # \frac{1}{\sin^m \theta} # \frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. # ``` -# Again, the dependence on ``\theta`` is implicit, but we make it explicit here: -function Θ(ℓ, m, θ::T) where {T} +# Again, we make the dependence on ``\theta`` explicit, and we capture its type to ensure +# that we don't lose precision when converting the factorials to a floating-point number. +function Θ(ℓ, m, 𝜃::T) where {T} (-1)^ℓ * T(√(((2ℓ+1) * (ℓ+m)❗) / (2 * (ℓ - m)❗)) * (1 / (2^ℓ * (ℓ)❗))) * - (1 / sin(θ)^T(m)) * dʲsin²ᵏθdcosθʲ(ℓ-m, ℓ, θ) + (1 / sin(𝜃)^T(m)) * dʲsin²ᵏθdcosθʲ(j=ℓ-m, k=ℓ, θ=𝜃) end - -#+ -# We can use `FastDifferentiation` to compute the derivative term: -function dʲsin²ᵏθdcosθʲ(j, k, θ) - if j < 0 - throw(ArgumentError("j=$j must be non-negative")) - end - if j == 0 - return sin(θ)^(2k) - end - x = FastDifferentiation.make_variables(:x)[1] - ∂ₓʲfᵏ = FastDifferentiation.derivative((1 - x^2)^k, (x for _ ∈ 1:j)...) - return FastDifferentiation.make_function([∂ₓʲfᵏ,], [x,])(cos(θ))[1] -end - #+ # It may be helpful to check some values against explicit formulas for the first few # spherical harmonics as given by Condon-Shortley in the footnote to Eq. (15) of Sec. 4³ -# (page 52): -ϴ(ℓ, m, θ) = ϴ(Val(ℓ), Val(m), θ) -ϴ(::Val{0}, ::Val{0}, θ) = √(1/2) -ϴ(::Val{1}, ::Val{0}, θ) = √(3/2) * cos(θ) -ϴ(::Val{2}, ::Val{0}, θ) = √(5/8) * (2cos(θ)^2 - sin(θ)^2) -ϴ(::Val{3}, ::Val{0}, θ) = √(7/8) * (2cos(θ)^3 - 3cos(θ)sin(θ)^2) -ϴ(::Val{1}, ::Val{+1}, θ) = -√(3/4) * sin(θ) -ϴ(::Val{1}, ::Val{-1}, θ) = +√(3/4) * sin(θ) -ϴ(::Val{2}, ::Val{+1}, θ) = -√(15/4) * cos(θ) * sin(θ) -ϴ(::Val{2}, ::Val{-1}, θ) = +√(15/4) * cos(θ) * sin(θ) -ϴ(::Val{3}, ::Val{+1}, θ) = -√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) -ϴ(::Val{3}, ::Val{-1}, θ) = +√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) -ϴ(::Val{2}, ::Val{+2}, θ) = √(15/16) * sin(θ)^2 -ϴ(::Val{2}, ::Val{-2}, θ) = √(15/16) * sin(θ)^2 -ϴ(::Val{3}, ::Val{+2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 -ϴ(::Val{3}, ::Val{-2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 -ϴ(::Val{3}, ::Val{+3}, θ) = -√(35/32) * sin(θ)^3 -ϴ(::Val{3}, ::Val{-3}, θ) = +√(35/32) * sin(θ)^3 - +# (page 52). Note the subtle difference between the character `Θ` defining the function above +# and the character `ϴ` defining the function below. +ϴ(ℓ, m, 𝜃) = ϴ(Val(ℓ), Val(m), 𝜃) +ϴ(::Val{0}, ::Val{0}, 𝜃) = √(1/2) +ϴ(::Val{1}, ::Val{0}, 𝜃) = √(3/2) * cos(𝜃) +ϴ(::Val{2}, ::Val{0}, 𝜃) = √(5/8) * (2cos(𝜃)^2 - sin(𝜃)^2) +ϴ(::Val{3}, ::Val{0}, 𝜃) = √(7/8) * (2cos(𝜃)^3 - 3cos(𝜃)sin(𝜃)^2) +ϴ(::Val{1}, ::Val{+1}, 𝜃) = -√(3/4) * sin(𝜃) +ϴ(::Val{1}, ::Val{-1}, 𝜃) = +√(3/4) * sin(𝜃) +ϴ(::Val{2}, ::Val{+1}, 𝜃) = -√(15/4) * cos(𝜃) * sin(𝜃) +ϴ(::Val{2}, ::Val{-1}, 𝜃) = +√(15/4) * cos(𝜃) * sin(𝜃) +ϴ(::Val{3}, ::Val{+1}, 𝜃) = -√(21/32) * (4cos(𝜃)^2*sin(𝜃) - sin(𝜃)^3) +ϴ(::Val{3}, ::Val{-1}, 𝜃) = +√(21/32) * (4cos(𝜃)^2*sin(𝜃) - sin(𝜃)^3) +ϴ(::Val{2}, ::Val{+2}, 𝜃) = √(15/16) * sin(𝜃)^2 +ϴ(::Val{2}, ::Val{-2}, 𝜃) = √(15/16) * sin(𝜃)^2 +ϴ(::Val{3}, ::Val{+2}, 𝜃) = √(105/16) * cos(𝜃) * sin(𝜃)^2 +ϴ(::Val{3}, ::Val{-2}, 𝜃) = √(105/16) * cos(𝜃) * sin(𝜃)^2 +ϴ(::Val{3}, ::Val{+3}, 𝜃) = -√(35/32) * sin(𝜃)^3 +ϴ(::Val{3}, ::Val{-3}, 𝜃) = +√(35/32) * sin(𝜃)^3 #+ + # Condon and Shortley do not give an expression for the Wigner D-matrices, but the # convention for spherical harmonics is what they are known for, so this will suffice. -end #hide - +end # module CondonShortley +#+ # ## Tests +# +# We can now test the functions against the equivalent functions from the SphericalHarmonics +# package. We will need to test approximate floating-point equality, so we set absolute and +# relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 100eps() +ϵᵣ = 1000eps() +#+ -@testitem "Condon-Shortley conventions" setup=[Utilities, CondonShortley] begin #hide +# The explicit formulas will be a good preliminary test. In this case, the formulas are +# only given up to +ℓₘₐₓ = 3 +#+ +# so we test up to that point, and just compare the general form to the explicit formulas — +# again, noting the subtle difference between the characters `Θ` and `ϴ`. Note that the +# ``1/\sin\theta`` factor in the general form will cause problems at the poles, so we avoid +# the poles by using `βrange` with a small offset: +for θ ∈ θrange(; avoid_zeros=ϵₐ/10) + for (ℓ, m) ∈ eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) + @test CondonShortley.ϴ(ℓ, m, θ) ≈ CondonShortley.Θ(ℓ, m, θ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ -using Random +# Finally, we can test Condon-Shortley's full expressions for spherical harmonics against +# the SphericalHarmonics package. We will only test up to +ℓₘₐₓ = 4 +#+ +# because the formulas are very slow, and this will be sufficient to sort out any sign +# differences, which are the most likely source of error. using Quaternionic: from_spherical_coordinates -#const check = NaNChecker.NaNCheck - -Random.seed!(1234) -const T = Float64 -const ℓₘₐₓ = 4 -ϵₐ = 4eps(T) -ϵᵣ = 1000eps(T) - -## Tests for Y(ℓ, m, θ, ϕ) -let Y=CondonShortley.ϕ, Θ=CondonShortley.Θ, ϴ=CondonShortley.ϴ, ϕ=zero(T) - for θ ∈ βrange(T) - if abs(sin(θ)) < ϵₐ - continue - end - - ## # Find where NaNs are coming from - ## for ℓ ∈ 0:ℓₘₐₓ - ## for m ∈ -ℓ:ℓ - ## Θ(ℓ, m, check(θ)) - ## end - ## end - - ## Test footnote to Eq. (15) of Sec. 4³ of Condon-Shortley - let Y = ₛ𝐘(0, 3, T, [from_spherical_coordinates(θ, ϕ)])[1,:] - for ℓ ∈ 0:3 - for m ∈ -ℓ:ℓ - @test ϴ(ℓ, m, θ) / √(2π) ≈ Y[Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ - end - end - end - - ## Test Eq. (18) of Sec. 4³ of Condon-Shortley - for ℓ ∈ 0:ℓₘₐₓ - for m ∈ -ℓ:ℓ - @test Θ(ℓ, m, θ) ≈ (-1)^(m) * Θ(ℓ, -m, θ) atol=ϵₐ rtol=ϵᵣ - end - end - - ## Compare to SphericalHarmonics Y - let s = 0 - Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] - Y₂ = [Y(ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] - @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ - end +for (θ, ϕ) ∈ θϕrange(; avoid_zeros=ϵₐ/40) + Y = SphericalFunctions.ₛ𝐘(0, ℓₘₐₓ, typeof(θ), [from_spherical_coordinates(θ, ϕ)])[1,:] + for (ℓ, m) ∈ eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) + @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ Y[SphericalFunctions.Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ end end From 1e9adf77d09f175986751202b7593572878ceb67 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:31:19 -0500 Subject: [PATCH 112/329] Further streamline tests --- .../conventions_comparisons/condon_shortley_1935.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index 9cc70f4d..f9ba5095 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -167,13 +167,11 @@ end # the SphericalHarmonics package. We will only test up to ℓₘₐₓ = 4 #+ -# because the formulas are very slow, and this will be sufficient to sort out any sign -# differences, which are the most likely source of error. -using Quaternionic: from_spherical_coordinates +# because the formulas are very slow, and this will be sufficient to sort out any sign or +# normalization differences, which are the most likely source of error. for (θ, ϕ) ∈ θϕrange(; avoid_zeros=ϵₐ/40) - Y = SphericalFunctions.ₛ𝐘(0, ℓₘₐₓ, typeof(θ), [from_spherical_coordinates(θ, ϕ)])[1,:] - for (ℓ, m) ∈ eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) - @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ Y[SphericalFunctions.Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ + for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) + @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end From ba15b64084f3bd8224ae7a3a9374e624f445fd3a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:32:49 -0500 Subject: [PATCH 113/329] Rename argument for clarity --- .../conventions_comparisons/condon_shortley_1935.jl | 4 ++-- test/utilities/utilities.jl | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index f9ba5095..bbe1c212 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -156,7 +156,7 @@ end # module CondonShortley # again, noting the subtle difference between the characters `Θ` and `ϴ`. Note that the # ``1/\sin\theta`` factor in the general form will cause problems at the poles, so we avoid # the poles by using `βrange` with a small offset: -for θ ∈ θrange(; avoid_zeros=ϵₐ/10) +for θ ∈ θrange(; avoid_poles=ϵₐ/10) for (ℓ, m) ∈ eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) @test CondonShortley.ϴ(ℓ, m, θ) ≈ CondonShortley.Θ(ℓ, m, θ) atol=ϵₐ rtol=ϵᵣ end @@ -169,7 +169,7 @@ end #+ # because the formulas are very slow, and this will be sufficient to sort out any sign or # normalization differences, which are the most likely source of error. -for (θ, ϕ) ∈ θϕrange(; avoid_zeros=ϵₐ/40) +for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end diff --git a/test/utilities/utilities.jl b/test/utilities/utilities.jl index 96e06e30..26a9f7be 100644 --- a/test/utilities/utilities.jl +++ b/test/utilities/utilities.jl @@ -6,17 +6,17 @@ 0; nextfloat(T(0)); rand(T(0):eps(T(π)):T(π), n÷2); prevfloat(T(π)); T(π); nextfloat(T(π)); rand(T(π):eps(2T(π)):2T(π), n÷2); prevfloat(T(π)); 2T(π) ] -βrange(::Type{T}=Float64, n=15; avoid_zeros=0) where T = T[ - avoid_zeros; nextfloat(T(avoid_zeros)); +βrange(::Type{T}=Float64, n=15; avoid_poles=0) where T = T[ + avoid_poles; nextfloat(T(avoid_poles)); rand(T(0):eps(T(π)):T(π), n); - prevfloat(T(π)-avoid_zeros); T(π)-avoid_zeros + prevfloat(T(π)-avoid_poles); T(π)-avoid_poles ] γrange(::Type{T}, n=15) where T = αrange(T, n) const θrange = βrange const φrange = αrange -θϕrange(::Type{T}=Float64, n=15; avoid_zeros=0) where T = vec(collect( - Iterators.product(θrange(T, n; avoid_zeros), φrange(T, n)) +θϕrange(::Type{T}=Float64, n=15; avoid_poles=0) where T = vec(collect( + Iterators.product(θrange(T, n; avoid_poles), φrange(T, n)) )) v̂range(::Type{T}, n=15) where T = QuatVec{T}[ From a10af141560659d1e9bc4a422a70fe1525eaea65 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:25:24 -0500 Subject: [PATCH 114/329] Correct the name of THIS package! --- .../conventions_comparisons/condon_shortley_1935.jl | 6 +++--- test/conventions/condon_shortley.jl | 2 +- test/conventions/goldbergetal.jl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index bbe1c212..0945a320 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -49,7 +49,7 @@ ones computed by this package. # # We begin by writing code that implements the formulas from Condon-Shortley. We # encapsulate the formulas in a module so that we can test them against the -# SphericalHarmonics package. +# SphericalFunctions package. using TestItems: @testitem #hide @testitem "Condon-Shortley conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide @@ -141,7 +141,7 @@ end # module CondonShortley # ## Tests # -# We can now test the functions against the equivalent functions from the SphericalHarmonics +# We can now test the functions against the equivalent functions from the SphericalFunctions # package. We will need to test approximate floating-point equality, so we set absolute and # relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 100eps() @@ -164,7 +164,7 @@ end #+ # Finally, we can test Condon-Shortley's full expressions for spherical harmonics against -# the SphericalHarmonics package. We will only test up to +# the SphericalFunctions package. We will only test up to ℓₘₐₓ = 4 #+ # because the formulas are very slow, and this will be sufficient to sort out any sign or diff --git a/test/conventions/condon_shortley.jl b/test/conventions/condon_shortley.jl index 1c36151d..fd4a5bfb 100644 --- a/test/conventions/condon_shortley.jl +++ b/test/conventions/condon_shortley.jl @@ -160,7 +160,7 @@ end # @testmodule CondonShortley end end - # Compare to SphericalHarmonics Y + # Compare to SphericalFunctions Y let s = 0 Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] Y₂ = [Y(ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] diff --git a/test/conventions/goldbergetal.jl b/test/conventions/goldbergetal.jl index 2714a6f9..a62ae208 100644 --- a/test/conventions/goldbergetal.jl +++ b/test/conventions/goldbergetal.jl @@ -133,7 +133,7 @@ end # @testmodule GoldbergEtAl end end - # Compare to SphericalHarmonics Y + # Compare to SphericalFunctions Y for s ∈ -ℓₘₐₓ:ℓₘₐₓ Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] Y₂ = [(-1)^m * Y(s, ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] From 663783acdbe3694bae96b3f6aa206024034f96df Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:25:46 -0500 Subject: [PATCH 115/329] Document Y function --- docs/src/internal.md | 1 + src/evaluate.jl | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/src/internal.md b/docs/src/internal.md index c6d88cd2..81036060 100644 --- a/docs/src/internal.md +++ b/docs/src/internal.md @@ -43,6 +43,7 @@ interacting with [`SSHT`](@ref). ```@docs ₛ𝐘 +SphericalFunctions.Y ``` diff --git a/src/evaluate.jl b/src/evaluate.jl index 65610c5b..96cecc42 100644 --- a/src/evaluate.jl +++ b/src/evaluate.jl @@ -609,8 +609,21 @@ function ₛ𝐘(s, ℓₘₐₓ, ::Type{T}=Float64, Rθϕ=golden_ratio_spiral_r ₛ𝐘 end -# PRIVATE FUNCTION: This function is not intended for use outside of `SphericalFunctions` -function Y(ℓ, m, θ, ϕ) + +@doc raw""" + Y(ℓ, m, θ, ϕ) + Y(s, ℓ, m, θ, ϕ) + +NOTE: This function is primarily a test function just to make comparisons between this +package's spherical harmonics and other references' more clear. It is inefficient, both in +terms of memory and computation time, and should generally not be used in production code. + +Computes a single (complex) value of the spherical harmonic ``(\ell, m)`` at the given +spherical coordinate ``(\theta, \phi)``. +""" +function Y(s, ℓ, m, θ, ϕ) θ, ϕ = promote(θ, ϕ) - ₛ𝐘(0, ℓ, typeof(θ), [Quaternionic.from_spherical_coordinates(θ, ϕ)])[1, Yindex(ℓ, m)] + Rθϕ = Quaternionic.from_spherical_coordinates(θ, ϕ) + ₛ𝐘(s, ℓ, typeof(θ), [Rθϕ])[1, Yindex(ℓ, m, abs(s))] end +Y(ℓ, m, θ, ϕ) = Y(0, ℓ, m, θ, ϕ) From 7cb08174aecfc13e7c67ffab1b08ec433aba091e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:30:48 -0500 Subject: [PATCH 116/329] Summarize the results --- .../conventions_comparisons/condon_shortley_1935.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index 0945a320..6556044c 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -1,6 +1,10 @@ md""" # Condon-Shortley (1935) +!!! info "Summary" + Condon and Shortley's definition of the spherical harmonics agrees with the definition + used in the `SphericalFunctions` package. + [Condon and Shortley's "The Theory Of Atomic Spectra"](@cite CondonShortley_1935) is the standard reference for the "Condon-Shortley phase convention". Though some references are not very clear about precisely what they mean by that phrase, it seems clear that the @@ -174,5 +178,9 @@ for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end +#+ + +# This successful test shows that the function ``\phi`` defined by Condon and Shortley +# agrees with the spherical harmonics defined by the SphericalFunctions package. end #hide From a034635612393a80abd439cd2733674dbb29d84e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:31:43 -0500 Subject: [PATCH 117/329] Delete raw tests for Condon-Shortley, as they have been moved to the docs --- test/conventions/condon_shortley.jl | 172 ---------------------------- 1 file changed, 172 deletions(-) delete mode 100644 test/conventions/condon_shortley.jl diff --git a/test/conventions/condon_shortley.jl b/test/conventions/condon_shortley.jl deleted file mode 100644 index fd4a5bfb..00000000 --- a/test/conventions/condon_shortley.jl +++ /dev/null @@ -1,172 +0,0 @@ -raw""" -Formulas and conventions from [Condon and Shortley's "The Theory Of Atomic Spectra"](@cite -CondonShortley_1935). - -The method we use here is as direct and explicit as possible. In particular, Condon and -Shortley provide a formula for the φ=0 part in terms of iterated derivatives of a power of -sin(θ). Rather than expressing these derivatives in terms of the Legendre polynomials — -which would subject us to another round of ambiguity — the functions in this module use -automatic differentiation to compute the derivatives explicitly. - -The result is that the original Condon-Shortley spherical harmonics agree perfectly with the -ones computed by this package. - -(Condon and Shortley do not give an expression for the Wigner D-matrices.) - -""" -@testmodule CondonShortley begin - -import FastDifferentiation - -const 𝒾 = im - -include("../utilities/naive_factorial.jl") -import .NaiveFactorials: ❗ - - -""" - Θ(ℓ, m, θ) - -Equation (15) of section 4³ (page 52) of [Condon-Shortley](@cite CondonShortley_1935), -implementing -```math - Θ(ℓ, m), -``` -which is implicitly a function of the spherical coordinate ``θ``. -""" -function Θ(ℓ, m, θ::T) where {T} - (-1)^ℓ * T(√(((2ℓ+1) * (ℓ+m)❗) / (2 * (ℓ - m)❗)) * (1 / (2^ℓ * (ℓ)❗))) * - (1 / sin(θ)^T(m)) * dʲsin²ᵏθdcosθʲ(ℓ-m, ℓ, θ) -end - - -@doc raw""" - dʲsin²ᵏθdcosθʲ(j, k, θ) - -Compute the ``j``th derivative of the function ``\sin^{2k}(θ)`` with respect to ``\cos(θ)``. -Note that ``\sin^{2k}(θ) = (1 - \cos^2(θ))^k``, so this is equivalent to evaluating the -``j``th derivative of ``(1-x^2)^k`` with respect to ``x``, evaluated at ``x = \cos(θ)``. -""" -function dʲsin²ᵏθdcosθʲ(j, k, θ) - if j < 0 - throw(ArgumentError("j=$j must be non-negative")) - end - if j == 0 - return sin(θ)^(2k) - end - x = FastDifferentiation.make_variables(:x)[1] - ∂ₓʲfᵏ = FastDifferentiation.derivative((1 - x^2)^k, (x for _ ∈ 1:j)...) - return FastDifferentiation.make_function([∂ₓʲfᵏ,], [x,])(cos(θ))[1] -end - - -""" - Φ(mₗ, φ) - -Equation (5) of section 4³ (page 50) of [Condon-Shortley](@cite CondonShortley_1935), -implementing -```math - Φ(mₗ), -``` -which is implicitly a function of the spherical coordinate ``φ``. -""" -function Φ(mₗ, φ::T) where {T} - 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) -end - - -""" - ϕ(ℓ, m, θ, φ) - -Spherical harmonics. This is defined as such below Eq. (5) of section 5⁵ (page 127) of -[Condon-Shortley](@cite CondonShortley_1935), implementing -```math - ϕ(ℓ, mₗ), -``` -which is implicitly a function of the spherical coordinates ``θ`` and ``φ``. -""" -function ϕ(ℓ, mₗ, θ, φ) - Θ(ℓ, mₗ, θ) * Φ(mₗ, φ) -end - -@doc raw""" - ϴ(ℓ, m, θ) - -Explicit formulas for the first few spherical harmonics as given by Condon-Shortley in the -footnote to Eq. (15) of Sec. 4³ (page 52). - -Note that the name of this function is `\varTheta`, as opposed to the `\Theta` function -that implements Condon-Shortley's general form. -""" -ϴ(ℓ, m, θ) = ϴ(Val(ℓ), Val(m), θ) -ϴ(::Val{0}, ::Val{0}, θ) = √(1/2) -ϴ(::Val{1}, ::Val{0}, θ) = √(3/2) * cos(θ) -ϴ(::Val{2}, ::Val{0}, θ) = √(5/8) * (2cos(θ)^2 - sin(θ)^2) -ϴ(::Val{3}, ::Val{0}, θ) = √(7/8) * (2cos(θ)^3 - 3cos(θ)sin(θ)^2) -ϴ(::Val{1}, ::Val{+1}, θ) = -√(3/4) * sin(θ) -ϴ(::Val{1}, ::Val{-1}, θ) = +√(3/4) * sin(θ) -ϴ(::Val{2}, ::Val{+1}, θ) = -√(15/4) * cos(θ) * sin(θ) -ϴ(::Val{2}, ::Val{-1}, θ) = +√(15/4) * cos(θ) * sin(θ) -ϴ(::Val{3}, ::Val{+1}, θ) = -√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) -ϴ(::Val{3}, ::Val{-1}, θ) = +√(21/32) * (4cos(θ)^2*sin(θ) - sin(θ)^3) -ϴ(::Val{2}, ::Val{+2}, θ) = √(15/16) * sin(θ)^2 -ϴ(::Val{2}, ::Val{-2}, θ) = √(15/16) * sin(θ)^2 -ϴ(::Val{3}, ::Val{+2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 -ϴ(::Val{3}, ::Val{-2}, θ) = √(105/16) * cos(θ) * sin(θ)^2 -ϴ(::Val{3}, ::Val{+3}, θ) = -√(35/32) * sin(θ)^3 -ϴ(::Val{3}, ::Val{-3}, θ) = +√(35/32) * sin(θ)^3 - -end # @testmodule CondonShortley - - -@testitem "Condon-Shortley conventions" setup=[Utilities, CondonShortley] begin - using Random - using Quaternionic: from_spherical_coordinates - #const check = NaNChecker.NaNCheck - - Random.seed!(1234) - const T = Float64 - const ℓₘₐₓ = 4 - ϵₐ = 4eps(T) - ϵᵣ = 1000eps(T) - - # Tests for Y(ℓ, m, θ, ϕ) - let Y=CondonShortley.ϕ, Θ=CondonShortley.Θ, ϴ=CondonShortley.ϴ, ϕ=zero(T) - for θ ∈ βrange(T) - if abs(sin(θ)) < ϵₐ - continue - end - - # # Find where NaNs are coming from - # for ℓ ∈ 0:ℓₘₐₓ - # for m ∈ -ℓ:ℓ - # Θ(ℓ, m, check(θ)) - # end - # end - - # Test footnote to Eq. (15) of Sec. 4³ of Condon-Shortley - let Y = ₛ𝐘(0, 3, T, [from_spherical_coordinates(θ, ϕ)])[1,:] - for ℓ ∈ 0:3 - for m ∈ -ℓ:ℓ - @test ϴ(ℓ, m, θ) / √(2π) ≈ Y[Yindex(ℓ, m)] atol=ϵₐ rtol=ϵᵣ - end - end - end - - # Test Eq. (18) of Sec. 4³ of Condon-Shortley - for ℓ ∈ 0:ℓₘₐₓ - for m ∈ -ℓ:ℓ - @test Θ(ℓ, m, θ) ≈ (-1)^(m) * Θ(ℓ, -m, θ) atol=ϵₐ rtol=ϵᵣ - end - end - - # Compare to SphericalFunctions Y - let s = 0 - Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] - Y₂ = [Y(ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] - @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ - end - end - end - -end From f6fedaee560ad719c0a8eedce2f0daab1306c079 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:45:23 -0500 Subject: [PATCH 118/329] Remove TestItems from main package; src contains no testing code --- Project.toml | 2 -- src/SphericalFunctions.jl | 1 - 2 files changed, 3 deletions(-) diff --git a/Project.toml b/Project.toml index fa4743f4..6166a788 100644 --- a/Project.toml +++ b/Project.toml @@ -16,7 +16,6 @@ Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] AbstractFFTs = "1" @@ -40,7 +39,6 @@ SpecialFunctions = "2" StaticArrays = "1" Test = "1.11" TestItemRunner = "1" -TestItems = "1" julia = "1.6" [extras] diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 3351884c..d058a710 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -11,7 +11,6 @@ using StaticArrays: @SVector using SpecialFunctions, DoubleFloats using LoopVectorization: @turbo using Base.Threads: @threads, nthreads -using TestItems: @testitem const MachineFloat = Union{Float16, Float32, Float64} From d73c6f8cf7fa908bb6f5e63260fde052e2a26b6e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 14:44:34 -0500 Subject: [PATCH 119/329] Add a couple details about Zettili --- docs/src/conventions/comparisons.md | 64 ++++++++++++++++++----------- docs/src/references.bib | 9 ++-- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 03f573df..ea6cbd68 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -213,7 +213,7 @@ If we relate two vectors by a rotation matrix as ``x'^k = R^{kl} x^l``, then Goldberg et al. define ``D`` by its action on spherical harmonics [Eq. (3.3)]: ```math -Y_{ell,m}(x') = \sum_{m'} Y_{ell,m'}(x) D^{ell}_{m',m}\left( R^{-1} \right). +Y_{\ell,m}(x') = \sum_{m'} Y_{\ell,m'}(x) D^{\ell}_{m',m}\left( R^{-1} \right). ``` They then define the Euler angles as we do, and write [Eq. (3.4)] ```math @@ -294,7 +294,7 @@ spherical harmonics: Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8\pi}\right)^{1/2} \sin\theta e^{\pm i\phi},\\ Y_{2}^{0} &= \left(\frac{5}{16\pi}\right)^{1/2} \left(3\cos^2\theta - 1\right),\\ Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8\pi}\right)^{1/2} \sin\theta \cos\theta e^{\pm i\phi},\\ - Y_{2}^{\pm 2} &= \left(\frac{15}{32\pi}\right)^{1/2} \sin^2\theta e^{\pm 2i\phi}. + Y_{2}^{\pm 2} &= \left(\frac{15}{32\pi}\right)^{1/2} \sin^2\theta e^{\pm 2i\phi},\\ Y_{3}^{0} &= \left(\frac{7}{16\pi}\right)^{1/2} \left(5\cos^3\theta - 3\cos\theta\right),\\ Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64\pi}\right)^{1/2} \sin\theta \left(5\cos^2\theta - 1\right) e^{\pm i\phi},\\ Y_{3}^{\pm 2} &= \left(\frac{105}{32\pi}\right)^{1/2} \sin^2\theta \cos\theta e^{\pm 2i\phi},\\ @@ -933,7 +933,45 @@ D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | \mathcal{R}(\alpha,\beta,\ga ## Zettili (2009) -[Zettili_2009](@citet) denotes by ``\hat{R}_z(\delta \phi)`` the +[Zettili_2009](@citet) is a relatively recent textbook that seems to +be gaining popularity. (Note that there is a 3rd edition from 2022, +but I do not have access to it; all the references here are to the 2nd +edition from 2009.) + +In Appendix B.1, we find that the spherical coordinates are related to +Cartesian coordinates in the usual (physicist's) way. Equation +(5.132) gives the angular-momentum operator +```math +\hat{L}_z = -i \hbar \frac{\partial}{\partial \varphi}, +``` +which agrees with [our expression](@ref "``L`` operators in spherical +coordinates"). This is followed by equation (5.134): +```math +\hat{L}_{\pm} += +\hat{L}_x \pm i \hat{L}_y += +\pm \hbar e^{\pm i\varphi} \left( + \frac{\partial}{\partial \theta} + \pm i \cot\theta \frac{\partial}{\partial \varphi} +\right), +``` +which also agrees with [our results.](@ref "``L_{\pm}`` operators in +spherical coordinates") + +Equation (5.180) gives the spherical harmonics as +```math +Y_{l, m}(\theta, \varphi) += +\frac{(-1)^l}{2^l l!} +\sqrt{\frac{2l+1}{4\pi} \frac{(l+m)!}{(l-m)!}} +e^{im\varphi} +\frac{1}{\sin^m \theta} +\frac{d^{l-m}}{d(\cos\theta)^{l-m}} +(\sin \theta)^{2l}. +``` + +Section 7.2.1 denotes by ``\hat{R}_z(\delta \phi)`` the > rotation of the coordinates of a *spinless* particle over an > *infinitesimal* angle ``\delta \phi`` about the ``z``-axis @@ -997,23 +1035,3 @@ Y_{\ell, m}^\ast (\theta', \phi') = \sum_{m'} D^{(\ell)}_{m, m'}(\alpha, \beta, \gamma) Y_{\ell, m'}^\ast (\theta, \phi). ``` - -In Appendix B.1, we find that the spherical coordinates are related to -Cartesian coordinates in the usual (physicist's) way, and Eqs. -(B.25)—(B.27) give the components of the angular-momentum operator in -spherical coordinates as -```math -\begin{aligned} -L_x &= i \hbar \left( - \sin\phi \frac{\partial}{\partial \theta} - + \cot\theta \cos\phi \frac{\partial}{\partial \phi} -\right), -\\ -L_y &= i \hbar \left( - -\cos\phi \frac{\partial}{\partial \theta} - + \cot\theta \sin\phi \frac{\partial}{\partial \phi} -\right), -\\ -L_z &= -i \hbar \frac{\partial}{\partial \phi}. -\end{aligned} -``` \ No newline at end of file diff --git a/docs/src/references.bib b/docs/src/references.bib index 0c9528bf..d4e2073e 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -514,12 +514,13 @@ @article{Xing_2019 @book{Zettili_2009, address = {New York, {NY}}, title = {Quantum Mechanics: {C}oncepts and Applications}, - isbn = {978-0-470-74656-1}, + isbn = {978-0-470-02678-6}, shorttitle = {Quantum Mechanics}, - url = {http://ebookcentral.proquest.com/lib/cornell/detail.action?docID=416494}, - publisher = {John Wiley \& Sons, Incorporated}, + url = {https://www.wiley.com/en-us/Quantum+Mechanics%3A+Concepts+and+Applications%2C+2nd+Edition-p-9780470746561}, + publisher = {John Wiley \& Sons}, author = {Zettili, Nouredine}, - year = 2009 + year = 2009, + edition = {2nd}, } @book{vanNeerven_2022, From ea91c8dfb1f2ed7595e45aabb9b13b1c18a62553 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 14:50:28 -0500 Subject: [PATCH 120/329] Denote edition uniformly --- docs/src/references.bib | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/references.bib b/docs/src/references.bib index d4e2073e..07377c1d 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -72,7 +72,7 @@ @article{BrauchartGrabner_2015 @book{CohenTannoudji_1991, address = {New York}, - edition = {1st edition}, + edition = {1st}, title = {Quantum Mechanics}, isbn = {978-0-471-16433-3}, publisher = {Wiley}, @@ -111,7 +111,7 @@ @book{Edmonds_2016 author = {Edmonds, A. R.}, month = aug, year = 1960, - edition = {second}, + edition = {2nd}, } @article{Elahi_2018, @@ -133,7 +133,7 @@ @article{Elahi_2018 @book{Folland_2016, address = {New York}, - edition = 2, + edition = {2nd}, title = {A Course in Abstract Harmonic Analysis}, isbn = {978-0-429-15469-0}, publisher = {Chapman and {Hall/CRC}}, @@ -188,7 +188,7 @@ @article{GoldbergEtAl_1967 @book{Griffiths_1995, address = {Upper Saddle River, {NJ}}, - edition = {first}, + edition = {1st}, title = {Introduction to quantum mechanics}, publisher = {Prentice Hall}, author = {Griffiths, David J.}, @@ -382,7 +382,7 @@ @article{SaffKuijlaars_1997 @book{Sakurai_1994, address = {New York}, - edition = {revised}, + edition = {Revised}, title = {Modern Quantum Mechanics}, isbn = {0-201-53929-2}, publisher = {Addison Wesley}, @@ -392,7 +392,7 @@ @book{Sakurai_1994 @book{Shankar_1994, address = {New York}, - edition = {second}, + edition = {2nd}, title = {Principles of Quantum Mechanics}, publisher = {Plenum Press}, author = {Shankar, Ramamurti}, From 3bba9cb42932536a295b62a6ccac7ba8d09646d6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 14:54:45 -0500 Subject: [PATCH 121/329] Minor corrections --- docs/src/conventions/comparisons.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index ea6cbd68..70f2978a 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -41,9 +41,9 @@ about the above items) with the following sources, in order: 1. LALSuite 2. NINJA -3. Thorne / MTW +3. Newman-Penrose 4. Goldberg -5. Newman-Penrose +5. Thorne / MTW 6. Wikipedia 7. Sakurai 8. Shankar @@ -56,8 +56,8 @@ for which I have my own strong opinions. ## Cohen-Tannoudji (1991) -[CohenTannoudji_1991](@citet) defines spherical coordinates in the -usual (physicist's) way in Chapter VI. He then computes the +[CohenTannoudji_1991](@citet) define spherical coordinates in the +usual (physicist's) way in Chapter VI. They then compute the angular-momentum operators as [Eqs. (D-5)] ```math \begin{aligned} @@ -75,7 +75,7 @@ L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. \end{aligned} ``` -He derives the spherical harmonics in two ways and gets two different, +They derives the spherical harmonics in two ways and gets two different, but equivalent, expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) ```math @@ -94,7 +94,7 @@ e^{i m \phi} (\sin \theta)^m \frac{d^{l+m}}{d(\cos \theta)^{l+m}} (\sin \theta)^{2l}. ``` -In Complement ``\mathrm{B}_{\mathrm{VI}}`` he defines a rotation +In Complement ``\mathrm{B}_{\mathrm{VI}}`` they define a rotation operator ``R`` as acting on a state such that [Eq. (21)] ```math \langle \mathbf{r} | R | \psi \rangle @@ -107,7 +107,7 @@ For an infinitesimal rotation through angle ``d\alpha`` about the axis R_{\mathbf{u}}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha \mathbf{L}.\mathbf{u}. ``` -Cohen-Tannoudji does not appear to define the Wigner D-matrices. +They do not appear to define the Wigner D-matrices. ## Condon-Shortley (1935) @@ -379,7 +379,7 @@ and (representing the ``z-y-z`` convention). -Finally, we find that they say that `EulerMatrix`` corresponds to three rotations: +Finally, we find that they say that `EulerMatrix` corresponds to three rotations: ```mathematica rα = RotationMatrix[α, {0, 0, 1}]; @@ -530,7 +530,7 @@ with ``k_1 = \textrm{max}(0, m-s)`` and ``k_2=\textrm{min}(\ell+m, {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 - \cos\iota )e^{-i\phi},\\ - {}^{-2}Y_{2,-2} &=& \sqrt{\frac{5}{64\pi}}(1-\cos\iota)^2e^{-2i\phi}. + {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64\pi}}(1-\cos\iota)^2e^{-2i\phi}. \end{aligned} ``` Note that most of the above was copied directly from the TeX source of From e1dcbb73f46d42ff6d91f228ddfd734ce56044f1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 15:38:13 -0500 Subject: [PATCH 122/329] Clarify a minor point for SymPy --- docs/src/conventions/comparisons.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 70f2978a..234cd041 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -641,7 +641,7 @@ code also implements D in the `wigner_d` function as (essentially) ```python exp(I*mprime*alpha)*d[i, j]*exp(I*m*gamma) ``` -even though the actual equation Eq. (4.1.12) says +even though the actual equation Eq. (4.1.12) of Edmonds says ```math \mathscr{D}^{(j)}_{m' m}(\alpha \beta \gamma) = \exp i m' \gamma d^{(j)}_{m' m}(\alpha, \beta) \exp(i m \alpha). From 169de91e0fb7cd2f8ee80788fb755b815f5571aa Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 28 Feb 2025 20:57:01 -0500 Subject: [PATCH 123/329] Improve formatting --- docs/src/references.bib | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/references.bib b/docs/src/references.bib index 07377c1d..36ec38ad 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -10,7 +10,7 @@ @misc{Ajith_2007 archivePrefix ="arXiv", eprint = "0709.0093", primaryClass = "gr-qc", - note = {"There was a serious error in the original version of this paper. The error was corrected in version 2."} + note = {There was a serious error in the original version of this paper. The error was corrected in version 2.} } @article{Bander_1966, @@ -399,10 +399,9 @@ @book{Shankar_1994 year = 1994 } -@article{SommerEtAl_2018, +@misc{SommerEtAl_2018, title = {Why and How to Avoid the Flipped Quaternion Multiplication}, url = {http://arxiv.org/abs/1801.07478}, - journal = {{arXiv:1801.07478} [cs]}, author = {Sommer, Hannes and Gilitschenski, Igor and Bloesch, Michael and Weiss, Stephan and Siegwart, Roland and Nieto, Juan}, month = jan, From d73c717a86c089e450164675617655b30b4b1531 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Mar 2025 13:12:59 -0500 Subject: [PATCH 124/329] Move NINJA tests to Literate docs --- .../conventions_comparisons/ninja_2011.jl | 152 ++++++++++++++++++ docs/make.jl | 1 + docs/src/conventions/comparisons.md | 58 +------ docs/src/references.bib | 2 +- src/evaluate.jl | 23 ++- test/conventions/ninja.jl | 82 ---------- test/map2salm.jl | 18 --- test/ssht.jl | 18 +-- test/utilities/utilities.jl | 22 ++- test/wigner_matrices/sYlm.jl | 34 +--- 10 files changed, 212 insertions(+), 198 deletions(-) create mode 100644 docs/literate_input/conventions_comparisons/ninja_2011.jl delete mode 100644 test/conventions/ninja.jl diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl new file mode 100644 index 00000000..2f60e635 --- /dev/null +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -0,0 +1,152 @@ +md""" +# NINJA (2011) + +!!! info "Summary" + The NINJA collaboration's definitions of the spherical harmonics and Wigner's ``d`` + functions agrees with the definitions used in the `SphericalFunctions` package. + +Motivated by the need for a shared set of conventions in the NINJA project, a broad +cross-section of researchers involved in modeling gravitational waves (including the author +of this package) prepared Ref. [AjithEtAl_2011](@cite). It is worth noting that the first +version posted to the arXiv included an unfortunate typo in the definition of the +spin-weighted spherical harmonics. This was corrected in the second version, and remains +correct in the final — third — version. + +The spherical coordinates are standard physicists' coordinates, except that the polar angle +is denoted ``\iota``: + +> we define standard spherical coordinates ``(r, ι, φ)`` where ``ι`` is the inclination +> angle from the z-axis and ``φ`` is the phase angle. + + +## Implementing formulas + +We begin by writing code that implements the formulas from Ref. [AjithEtAl_2011](@cite). We +encapsulate the formulas in a module so that we can test them against the SphericalFunctions +package. + +""" +using TestItems: @testitem #hide +@testitem "NINJA conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide + +module NINJA + +import ..ConventionsUtilities: 𝒾, ❗ +#+ + +# The spin-weighted spherical harmonics are defined in Eq. (II.7) as +# ```math +# {}^{-s}Y_{l,m} = (-1)^s \sqrt{\frac{2l+1}{4\pi}} +# d^{l}_{m,s}(\iota) e^{im\phi}. +# ``` +# Just for convenience, we eliminate the negative sign on the left-hand side: +# ```math +# {}^{s}Y_{l,m} = (-1)^{-s} \sqrt{\frac{2l+1}{4\pi}} +# d^{l}_{m,-s}(\iota) e^{im\phi}. +# ``` +function ₛYₗₘ(s, l, m, ι::T, ϕ::T) where {T<:Real} + (-1)^(-s) * √((2l + 1) / (4T(π))) * d(l, m, -s, ι) * exp(𝒾 * m * ϕ) +end +#+ + +# Immediately following that, in Eq. (II.8), we find the definition of Wigner's ``d`` +# function (again, noting that this expression was incorrect in version 1 of the paper, but +# correct in versions 2 and 3): +# ```math +# d^{l}_{m,s}(\iota) +# = +# \sum_{k = k_1}^{k_2} +# \frac{(-1)^k [(l+m)!(l-m)!(l+s)!(l-s)!]^{1/2}} +# {(l+m-k)!(l-s-k)!k!(k+s-m)!} +# \left(\cos\left(\frac{\iota}{2}\right)\right)^{2l+m-s-2k} +# \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k+s-m}, +# ``` +# with ``k_1 = \textrm{max}(0, m-s)`` and ``k_2=\textrm{min}(l+m, l-s)``. +function d(l, m, s, ι::T) where {T<:Real} + k₁ = max(0, m - s) + k₂ = min(l + m, l - s) + sum( + (-1)^k + * T( + ((l + m)❗ * (l - m)❗ * (l + s)❗ * (l - s)❗)^(1//2) + / ((l + m - k)❗ * (l - s - k)❗ * (k)❗ * (k + s - m)❗) + ) + * cos(ι / 2) ^ (2l + m - s - 2k) + * sin(ι / 2) ^ (2k + s - m) + for k in k₁:k₂; + init=zero(T) + ) +end +#+ + +# For reference, several explicit formulas are also provided in Eqs. (II.9)--(II.13): +# ```math +# \begin{aligned} +# {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}} (1+\cos\iota)^2 e^{2i\phi},\\ +# {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 + \cos\iota) e^{i\phi},\\ +# {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ +# {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 - \cos\iota) e^{-i\phi},\\ +# {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64\pi}} (1-\cos\iota)^2 e^{-2i\phi}. +# \end{aligned} +# ``` +₋₂Y₂₂(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 + cos(ι))^2 * exp(2𝒾*ϕ) +₋₂Y₂₁(ι::T, ϕ::T) where {T<:Real} = √(5 / (16T(π))) * sin(ι) * (1 + cos(ι)) * exp(𝒾*ϕ) +₋₂Y₂₀(ι::T, ϕ::T) where {T<:Real} = √(15 / (32T(π))) * sin(ι)^2 +₋₂Y₂₋₁(ι::T, ϕ::T) where {T<:Real} = √(5 / (16T(π))) * sin(ι) * (1 - cos(ι)) * exp(-𝒾*ϕ) +₋₂Y₂₋₂(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 - cos(ι))^2 * exp(-2𝒾*ϕ) +#+ + +# The paper did not give an expression for the Wigner D-matrices, but the definition of the +# spin-weighted spherical harmonics is probably most relevant, so this will suffice. + +end # module NINJA +#+ + +# ## Tests +# +# We can now test the functions against the equivalent functions from the SphericalFunctions +# package. We will need to test approximate floating-point equality, so we set absolute and +# relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 10eps() +ϵᵣ = 10eps() +#+ + +# First, we compare the explicit formulas to the general formulas. +for (ι, ϕ) ∈ θϕrange(Float64, 1) + @test NINJA.ₛYₗₘ(-2, 2, 2, ι, ϕ) ≈ NINJA.₋₂Y₂₂(ι, ϕ) atol=ϵₐ rtol=ϵᵣ + @test NINJA.ₛYₗₘ(-2, 2, 1, ι, ϕ) ≈ NINJA.₋₂Y₂₁(ι, ϕ) atol=ϵₐ rtol=ϵᵣ + @test NINJA.ₛYₗₘ(-2, 2, 0, ι, ϕ) ≈ NINJA.₋₂Y₂₀(ι, ϕ) atol=ϵₐ rtol=ϵᵣ + @test NINJA.ₛYₗₘ(-2, 2, -1, ι, ϕ) ≈ NINJA.₋₂Y₂₋₁(ι, ϕ) atol=ϵₐ rtol=ϵᵣ + @test NINJA.ₛYₗₘ(-2, 2, -2, ι, ϕ) ≈ NINJA.₋₂Y₂₋₂(ι, ϕ) atol=ϵₐ rtol=ϵᵣ +end +#+ + +# Next, we compare the general formulas to the SphericalFunctions package. +# We will only test up to +ℓₘₐₓ = 4 +#+ +# and +sₘₐₓ = 2 +#+ +# because the formulas are very slow, and this will be sufficient to sort out any sign or +# normalization differences, which are the most likely source of error. +for (θ, ϕ) ∈ θϕrange() + for (s, ℓ, m) ∈ sℓmrange(ℓₘₐₓ, sₘₐₓ) + @test NINJA.ₛYₗₘ(s, ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# Finally, we compare the Wigner ``d`` matrix to the SphericalFunctions package. +for ι ∈ θrange() + for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) + @test NINJA.d(ℓ, m′, m, ι) ≈ SphericalFunctions.d(ℓ, m′, m, ι) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# These successful tests show that both the spin-weighted spherical harmonics and the Wigner +# ``d`` matrix defined by the NINJA collaboration agree with the corresponding functions +# defined by the SphericalFunctions package. + +end #hide diff --git a/docs/make.jl b/docs/make.jl index 808c2bfb..20223e59 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -71,6 +71,7 @@ makedocs( "conventions/comparisons.md", "Comparisons" => [ joinpath(relative_convention_comparisons, "condon_shortley_1935.md"), + joinpath(relative_convention_comparisons, "ninja_2011.md"), ], "Calculations" => [ joinpath(relative_literate_output, "euler_angular_momentum.md"), diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 234cd041..71b678d3 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -112,7 +112,7 @@ They do not appear to define the Wigner D-matrices. ## Condon-Shortley (1935) - +(moved) ## Edmonds (1960) @@ -327,7 +327,7 @@ characterize gravitational waves. As far as I can tell, the ultimate source for all spin-weighted spherical harmonic values used in LALSuite is the function [`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), -which cites the NINJA paper [Ajith_2007](@citet) as its source. +which cites the NINJA paper [AjithEtAl_2011](@cite) as its source. Unfortunately, it cites version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` instead of ``\cos \tfrac{\iota}{2}`` and similarly for ``\sin``. This error was corrected in version 2, @@ -500,59 +500,7 @@ Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. ## NINJA -[Ajith_2007](@citet) was prepared by a broad cross-section of -researchers (including the author of this package) involved in -modeling gravitational waves with the intent of providing a shared set -of conventions. The spherical coordinates are standard physicist's -coordinates, except that the polar angle is denoted ``\iota``. -Equation (II.7) is -```math - {}^{-s}Y_{l,m} = (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} - d^{\ell}_{m,s}(\iota)e^{im\phi}, -``` -where -```math - d^{\ell}_{m,s}(\iota) - = - \sum_{k = k_1}^{k_2} - \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell+s)!(\ell-s)!]^{1/2}} - {(\ell+m-k)!(\ell-s-k)!k!(k+s-m)!} - \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m-s-2k} - \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k+s-m} -``` -with ``k_1 = \textrm{max}(0, m-s)`` and ``k_2=\textrm{min}(\ell+m, -\ell-s)``. For reference, they provide several values [Eqs. -(II.9)--(II.13)]: -```math -\begin{aligned} - {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}}(1+\cos\iota)^2e^{2i\phi},\\ - {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 + \cos\iota )e^{i\phi},\\ - {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ - {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota( 1 - \cos\iota - )e^{-i\phi},\\ - {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64\pi}}(1-\cos\iota)^2e^{-2i\phi}. -\end{aligned} -``` -Note that most of the above was copied directly from the TeX source of -the paper. Also note the annoying negative sign on the left-hand side -of the first expression. Getting rid of it and combining the first -two expressions, we have the full formula for the spin-weighted -spherical harmonics in this convention: -```math -\begin{aligned} - {}_{s}Y_{lm} - &= - (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} - \sum_{k = k_1}^{k_2} - \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell-s)!(\ell+s)!]^{1/2}} - {(\ell+m-k)!(\ell+s-k)!k!(k-s-m)!} - \\ &\qquad \times - \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m+s-2k} - \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-s-m} -\end{aligned} -``` -where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, -\ell+s)``. +(moved) ## Sakurai (1994) diff --git a/docs/src/references.bib b/docs/src/references.bib index 36ec38ad..27c4fdf4 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -1,4 +1,4 @@ -@misc{Ajith_2007, +@misc{AjithEtAl_2011, doi = {10.48550/arxiv.0709.0093}, url = {https://arxiv.org/abs/0709.0093v3}, author = {Ajith, P. and Boyle, M. and Brown, D. A. and Fairhurst, S. and Hannam, M. and diff --git a/src/evaluate.jl b/src/evaluate.jl index 96cecc42..39ea5c8e 100644 --- a/src/evaluate.jl +++ b/src/evaluate.jl @@ -29,7 +29,6 @@ with the result instead. """ d_matrices(β::Real, ℓₘₐₓ) = d_matrices(cis(β), ℓₘₐₓ) - @doc raw""" d_matrices!(d_storage, β) d_matrices!(d_storage, expiβ) @@ -180,6 +179,22 @@ function dprep(ℓₘₐₓ, ::Type{T}) where {T<:Real} d, H_rec_coeffs end +@doc raw""" + d(ℓ, m′, m, β) + d(ℓ, m′, m, expiβ) + +NOTE: This function is primarily a test function just to make comparisons between this +package's Wigner ``d`` function and other references' more clear. It is inefficient, both +in terms of memory and computation time, and should generally not be used in production +code. + +Computes a single (complex) value of the ``d`` matrix ``(\ell, m', m)`` at the given +angle ``(\iota)``. +""" +function d(ℓ, m′, m, β) + d(β, ℓ)[WignerDindex(ℓ, m′, m)] +end + @doc raw""" D_matrices(R, ℓₘₐₓ) @@ -621,9 +636,11 @@ terms of memory and computation time, and should generally not be used in produc Computes a single (complex) value of the spherical harmonic ``(\ell, m)`` at the given spherical coordinate ``(\theta, \phi)``. """ -function Y(s, ℓ, m, θ, ϕ) +function Y(s::Int, ℓ::Int, m::Int, θ, ϕ) θ, ϕ = promote(θ, ϕ) Rθϕ = Quaternionic.from_spherical_coordinates(θ, ϕ) ₛ𝐘(s, ℓ, typeof(θ), [Rθϕ])[1, Yindex(ℓ, m, abs(s))] end -Y(ℓ, m, θ, ϕ) = Y(0, ℓ, m, θ, ϕ) +Y(ℓ::Int, m::Int, θ, ϕ) = Y(0, ℓ, m, θ, ϕ) +Y(s::Int, ℓ::Int, m::Int, θϕ) = Y(s, ℓ, m, θϕ[1], θϕ[2]) +Y(ℓ::Int, m::Int, θϕ) = Y(0, ℓ, m, θϕ[1], θϕ[2]) diff --git a/test/conventions/ninja.jl b/test/conventions/ninja.jl deleted file mode 100644 index 2a44b6be..00000000 --- a/test/conventions/ninja.jl +++ /dev/null @@ -1,82 +0,0 @@ -@testmodule NINJA begin - - include("../utilities/naive_factorial.jl") - import .NaiveFactorials: ❗ - - function Wigner_d(ι::T, ℓ, m, s) where {T<:Real} - # Eq. II.8 of v3 of Ajith et al. (2007) 'Data formats...' - k_min = max(0, m - s) - k_max = min(ℓ + m, ℓ - s) - sum( - (-1)^k - * cos(ι / 2) ^ (2 * ℓ + m - s - 2 * k) - * sin(ι / 2) ^ (2 * k + s - m) - * T( - √((ℓ + m)❗ * (ℓ - m)❗ * (ℓ + s)❗ * (ℓ - s)❗) - / ((ℓ + m - k)❗ * (ℓ - s - k)❗ * (k)❗ * (k + s - m)❗) - ) - for k in k_min:k_max - ) - end - - raw""" - - Eq. II.7 of v3 of Ajith et al. (2007) 'Data formats...' says - ```math - {}_sY_{\ell,m} = (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} d^\ell_{m,-s}(\iota) e^{im\phi} - ``` - - Below Eq. (2.53) of [Torres del Castillo](@cite TorresDelCastillo_2003), we see - ```math - {}_sY_{j,m} = (-1)^m \sqrt{\frac{2j+1}{4\pi}} d^j_{-m,s}(\iota) e^{im\phi} - ``` - We can use identities to modify the latter as follows: - ```math - \begin{aligned} - {}_sY_{j,m} &= (-1)^m \sqrt{\frac{2j+1}{4\pi}} d^j_{-m,s}(\iota) e^{im\phi} \\ - &= (-1)^m \sqrt{\frac{2j+1}{4\pi}} d^j_{-s,m}(\iota) e^{im\phi} \\ - &= (-1)^{j-s+m} \sqrt{\frac{2j+1}{4\pi}} d^j_{-s,-m}(\pi-\iota) e^{im\phi} \\ - &= (-1)^{j-s+m} \sqrt{\frac{2j+1}{4\pi}} d^j_{m,s}(\pi-\iota) e^{im\phi} \\ - &= (-1)^{2j-s+2m} \sqrt{\frac{2j+1}{4\pi}} d^j_{m,-s}(\pi-(\pi-\iota)) e^{im\phi} \\ - &= (-1)^{s} \sqrt{\frac{2j+1}{4\pi}} d^j_{m,-s}(\iota) e^{im\phi} \\ - \end{aligned} - ``` - The last line assumes that `j`, `m`, and `s` are integers. But in that case, the NINJA - expression agrees with the Torres del Castillo expression. - - """ - - function sYlm(s, ell, m, ι::T, ϕ::T) where {T<:Real} - # Eq. II.7 of v3 of Ajith et al. (2007) 'Data formats...' - # Note the weird definition w.r.t. `-s` - if abs(s) > ell || abs(m) > ell - return zero(complex(T)) - end - ( - (-1)^s - * √((2ell + 1) / (4T(π))) - * Wigner_d(ι, ell, m, -s) - * cis(m * ϕ) - ) - end - - function sYlm(s, ℓ, m, ιϕ) - sYlm(s, ℓ, m, ιϕ[1], ιϕ[2]) - end - - # Eqs. (II.9) through (II.13) of https://arxiv.org/abs/0709.0093v3 [Ajith_2007](@cite) - m2Y22(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 + cos(ι))^2 * cis(2ϕ) - m2Y21(ι::T, ϕ::T) where {T<:Real} = √(5 / (16T(π))) * sin(ι) * (1 + cos(ι)) * cis(ϕ) - m2Y20(ι::T, ϕ::T) where {T<:Real} = √(15 / (32T(π))) * sin(ι)^2 - m2Y2m1(ι::T, ϕ::T) where {T<:Real} = √(5 / (16T(π))) * sin(ι) * (1 - cos(ι)) * cis(-1ϕ) - m2Y2m2(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 - cos(ι))^2 * cis(-2ϕ) - - m_m2Y2m = [ - (2, m2Y22), - (1, m2Y21), - (0, m2Y20), - (-1, m2Y2m1), - (-2, m2Y2m2) - ] - -end # module NINJA diff --git a/test/map2salm.jl b/test/map2salm.jl index 1045986a..4dc86128 100644 --- a/test/map2salm.jl +++ b/test/map2salm.jl @@ -1,24 +1,6 @@ # NOTE: Float16 irfft returns Float32, which leads to type conflicts, so we just don't # test map2salm on Float16 -@testitem "Input expressions" setup=[Utilities, NINJA] begin - for T in [BigFloat, Float64, Float32] - # These are just internal consistency tests of the sYlm function - # above, against the explicit expressions `mY2.` - s = -2 - ℓ = 2 - Nϑ = 17 - Nφ = 18 - for (m, m2Y2m) in NINJA.m_m2Y2m - f1 = mapslices(ϕθ -> sYlm(s, ℓ, m, ϕθ[2], ϕθ[1]), phi_theta(Nφ, Nϑ, T), dims=[3]) - f2 = mapslices(ϕθ -> NINJA.sYlm(s, ℓ, m, ϕθ[2], ϕθ[1]), phi_theta(Nφ, Nϑ, T), dims=[3]) - f3 = mapslices(ϕθ -> m2Y2m(ϕθ[2], ϕθ[1]), phi_theta(Nφ, Nϑ, T), dims=[3]) - @test f1 ≈ f2 atol=10eps(T) rtol=10eps(T) - @test f1 ≈ f3 atol=10eps(T) rtol=10eps(T) - end - end -end - @testitem "map2salm" setup=[Utilities] begin for T in [BigFloat, Float64, Float32] # These test the ability of map2salm to precisely decompose the results of `sYlm`. diff --git a/test/ssht.jl b/test/ssht.jl index 90ab6ed9..e3ac88c0 100644 --- a/test/ssht.jl +++ b/test/ssht.jl @@ -77,14 +77,13 @@ end # These test the ability of ssht to precisely reconstruct a pure `sYlm`. -@testitem "Synthesis" setup=[NINJA,SSHT] begin +@testitem "Synthesis" setup=[SSHT] begin for (method, T) in cases - # We can't go to very high ℓ, because NINJA.sYlm fails for low-precision numbers for ℓmax ∈ 3:7 - # We need ϵ to be huge, seemingly mostly due to the low-precision method - # used for NINJA.sYlm; it is used because it is a simple reference method. + # This was huge because we used to use NINJA expressions, which were + # low-accuracy; we can probably reduce this now. ϵ = 500ℓmax^3 * eps(T) for s in -2:2 @@ -97,7 +96,7 @@ end f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) computed = 𝒯 * f - expected = NINJA.sYlm.(s, ℓ, m, pixels(𝒯)) + expected = SphericalFunctions.Y.(s, ℓ, m, pixels(𝒯)) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @test computed ≈ expected atol=ϵ rtol=ϵ end @@ -110,14 +109,13 @@ end # These test the ability of ssht to precisely decompose the results of `sYlm`. -@testitem "Analysis" setup=[NINJA,SSHT] begin +@testitem "Analysis" setup=[SSHT] begin for (method, T) in cases - # We can't go to very high ℓ, because NINJA.sYlm fails for low-precision numbers for ℓmax ∈ 3:7 - # We need ϵ to be huge, seemingly mostly due to the low-precision method - # used for NINJA.sYlm; it is used because it is a simple reference method. + # This was huge because we used to use NINJA expressions, which were + # low-accuracy; we can probably reduce this now. ϵ = 500ℓmax^3 * eps(T) if method == "Minimal" ϵ *= 50 @@ -128,7 +126,7 @@ end let ℓmin = abs(s) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ - f = NINJA.sYlm.(s, ℓ, m, pixels(𝒯)) + f = SphericalFunctions.Y.(s, ℓ, m, pixels(𝒯)) computed = 𝒯 \ f expected = zeros(Complex{T}, size(computed)) expected[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) diff --git a/test/utilities/utilities.jl b/test/utilities/utilities.jl index 26a9f7be..e41f4d59 100644 --- a/test/utilities/utilities.jl +++ b/test/utilities/utilities.jl @@ -1,6 +1,23 @@ @testsnippet Utilities begin ℓmrange(ℓₘₐₓ) = eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) +function sℓmrange(ℓₘₐₓ, sₘₐₓ) + sₘₐₓ = min(abs(sₘₐₓ), ℓₘₐₓ) + [ + (s, ℓ, m) + for s in -sₘₐₓ:sₘₐₓ + for ℓ in abs(s):ℓₘₐₓ + for m in -ℓ:ℓ + ] +end +function ℓm′mrange(ℓₘₐₓ) + [ + (ℓ, m′, m) + for ℓ in 0:ℓₘₐₓ + for m′ in -ℓ:ℓ + for m in -ℓ:ℓ + ] +end αrange(::Type{T}, n=15) where T = T[ 0; nextfloat(T(0)); rand(T(0):eps(T(π)):T(π), n÷2); prevfloat(T(π)); T(π); @@ -12,6 +29,9 @@ prevfloat(T(π)-avoid_poles); T(π)-avoid_poles ] γrange(::Type{T}, n=15) where T = αrange(T, n) +αβγrange(::Type{T}=Float64, n=15; avoid_poles=0) where T = vec(collect( + Iterators.product(αrange(T, n), βrange(T, n; avoid_poles), γrange(T, n)) +)) const θrange = βrange const φrange = αrange @@ -62,7 +82,7 @@ function array_equal(a1::T1, a2::T2, equal_nan=false) where {T1, T2} end function sYlm(s::Int, ell::Int, m::Int, theta::T, phi::T) where {T<:Real} - # Eqs. (II.7) and (II.8) of https://arxiv.org/abs/0709.0093v3 [Ajith_2007](@cite) + # Eqs. (II.7) and (II.8) of https://arxiv.org/abs/0709.0093v3 [AjithEtAl_2011](@cite) # Note their weird definition w.r.t. `-s` k_min = max(0, m + s) k_max = min(ell + m, ell + s) diff --git a/test/wigner_matrices/sYlm.jl b/test/wigner_matrices/sYlm.jl index 8881ef18..c84ee493 100644 --- a/test/wigner_matrices/sYlm.jl +++ b/test/wigner_matrices/sYlm.jl @@ -2,32 +2,14 @@ @test maximum(abs, sYlm_values(0.0, 0.0, 3, -2)) > 0 end -@testitem "Test NINJA expressions" setup=[NINJA,Utilities] begin - using ProgressMeter - @testset "$T" for T in [Float64, Float32, BigFloat] - ## This is just to test my implementation of the equations give in the paper. - ## Note that this is a test of the testing code itself, not of the main code. - tol = 2eps(T) - @showprogress desc="Test NINJA expressions ($T)" for ι in βrange(T) - for ϕ in αrange(T) - @test NINJA.sYlm(-2, 2, 2, ι, ϕ) ≈ NINJA.m2Y22(ι, ϕ) atol=tol rtol=tol - @test NINJA.sYlm(-2, 2, 1, ι, ϕ) ≈ NINJA.m2Y21(ι, ϕ) atol=tol rtol=tol - @test NINJA.sYlm(-2, 2, 0, ι, ϕ) ≈ NINJA.m2Y20(ι, ϕ) atol=tol rtol=tol - @test NINJA.sYlm(-2, 2, -1, ι, ϕ) ≈ NINJA.m2Y2m1(ι, ϕ) atol=tol rtol=tol - @test NINJA.sYlm(-2, 2, -2, ι, ϕ) ≈ NINJA.m2Y2m2(ι, ϕ) atol=tol rtol=tol - end - end - end -end - -@testitem "Compare to NINJA expressions" setup=[NINJA,LAL,Utilities] begin +@testitem "Compare to LAL expressions" setup=[LAL,Utilities] begin using ProgressMeter using Quaternionic - @testset "$T" for T in [Float64, Float32, BigFloat] + @testset "$T" for T in [Float64] ℓₘₐₓ = 8 sₘₐₓ = 2 ℓₘᵢₙ = 0 - tol = ℓₘₐₓ^2 * 2eps(T) # Mostly because the NINJA.sYlm expressions are inaccurate + tol = ℓₘₐₓ^2 * 2eps(T) sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) let R = randn(Rotor{T}) @@ -35,7 +17,7 @@ end @test_throws ErrorException sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) end - @showprogress desc="Compare to NINJA expressions ($T)" for spin in -sₘₐₓ:sₘₐₓ + @showprogress desc="Compare to LAL expressions ($T)" for spin in [-2] for ι in βrange(T) for ϕ in αrange(T) R = from_spherical_coordinates(ι, ϕ) @@ -52,12 +34,8 @@ end for ℓ in abs(spin):ℓₘₐₓ for m in -ℓ:ℓ sYlm1 = Y[i] - sYlm2 = NINJA.sYlm(spin, ℓ, m, ι, ϕ) - @test sYlm1 ≈ sYlm2 atol=tol rtol=tol - if spin==-2 && T===Float64 - sYlm3 = LAL.XLALSpinWeightedSphericalHarmonic(ι, ϕ, spin, ℓ, m) - @test sYlm1 ≈ sYlm3 atol=tol rtol=tol - end + sYlm3 = LAL.XLALSpinWeightedSphericalHarmonic(ι, ϕ, spin, ℓ, m) + @test sYlm1 ≈ sYlm3 atol=tol rtol=tol i += 1 end end From c5a51340d5b35372d9386d66e1589df17696c0c6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Mar 2025 16:13:03 -0500 Subject: [PATCH 125/329] Include `d` in docs --- docs/src/internal.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/internal.md b/docs/src/internal.md index 81036060..428c4d80 100644 --- a/docs/src/internal.md +++ b/docs/src/internal.md @@ -32,7 +32,7 @@ SphericalFunctions.AlternatingCountdown -## ₛ𝐘 +## ``Y``, ``d``, and ``D`` Various `d`, `D`, and `sYlm` functions are important in the main API. Their names and signatures have been tweaked from older versions of this package. The @@ -44,6 +44,7 @@ interacting with [`SSHT`](@ref). ```@docs ₛ𝐘 SphericalFunctions.Y +SphericalFunctions.d ``` From 798a83d4cd1b8fd499ffadfabad1b1fd04fd83b7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 09:12:08 -0500 Subject: [PATCH 126/329] Point out some nice arguments by Boydand Petschek --- docs/src/conventions/details.md | 11 +++++++++++ docs/src/references.bib | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 52e16f22..459f0096 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -902,6 +902,17 @@ distinct eigenvalues are orthogonal, since (the last equality by Green's theorem). Since the eigenvalues are distinct, this can only be true if ``\int f_u f_v=0``. +[BoydPetschek_2014](@citet) produced an interesting discussion with +numerous little insights into the use of special functions on +different spaces. In particular, they show why associated Legendre +functions are preferred to Chebyshev polynomials for the spherical +harmonics. They also mention that since the Laplacian measures +curvature, and spherical harmonics of a given degree have the same +Laplacian eigenvalue, they all have the same measure of curvature. +So, for example, the ``\ell = m`` mode varies most rapidly with +longitude but not at all with latitude, while the ``\ell = 0`` mode +varies just as rapidly with latitude but not at all with longitude. + * TODO: Show the relationship between the spherical Laplacian and the angular momentum operator. * TODO: Show how ``D`` matrices are harmonic with respect to the diff --git a/docs/src/references.bib b/docs/src/references.bib index 27c4fdf4..34f23d8c 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -41,6 +41,22 @@ @article{Belikov_1991 pages = {384--410} } +@article{BoydPetschek_2014, + title = {The Relationships Between {C}hebyshev, {L}egendre and {J}acobi Polynomials: The + Generic Superiority of {C}hebyshev Polynomials and Three Important Exceptions}, + volume = 59, + issn = {1573-7691}, + shorttitle = {The Relationships Between Chebyshev, Legendre and Jacobi Polynomials}, + url = {https://link.springer.com/article/10.1007/s10915-013-9751-7}, + doi = {10.1007/s10915-013-9751-7}, + number = 1, + journal = {Journal of Scientific Computing}, + author = {Boyd, John P. and Petschek, Rolfe}, + month = apr, + year = 2014, + pages = {1--27} +} + @article{Boyle_2016, doi = {10.1063/1.4962723}, url = {https://doi.org/10.1063/1.4962723}, From 6fae3a110c8abc50d55020fddba06a893bacad31 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:46:37 -0500 Subject: [PATCH 127/329] Add test utility function to compute just one element of D --- docs/src/internal.md | 1 + src/evaluate.jl | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/docs/src/internal.md b/docs/src/internal.md index 428c4d80..7d79a173 100644 --- a/docs/src/internal.md +++ b/docs/src/internal.md @@ -45,6 +45,7 @@ interacting with [`SSHT`](@ref). ₛ𝐘 SphericalFunctions.Y SphericalFunctions.d +SphericalFunctions.D ``` diff --git a/src/evaluate.jl b/src/evaluate.jl index 39ea5c8e..5d767d68 100644 --- a/src/evaluate.jl +++ b/src/evaluate.jl @@ -386,6 +386,28 @@ function Dworkspace(ℓₘₐₓ, ::Type{T}) where {T<:Real} eⁱᵐᵞ = Vector{Complex{T}}(undef, ℓₘₐₓ+1) H_rec_coeffs, eⁱᵐᵅ, eⁱᵐᵞ end +@doc raw""" + D(ℓ, m′, m, β) + D(ℓ, m′, m, expiβ) + +NOTE: This function is primarily a test function just to make comparisons between this +package's Wigner ``D`` function and other references' more clear. It is inefficient, both +in terms of memory and computation time, and should generally not be used in production +code. + +Computes a single (complex) value of the ``D`` matrix ``(\ell, m', m)`` at the given +angle ``(\iota)``. +""" +function D(ℓ, m′, m, α, β, γ) + D(α, β, γ, ℓ)[WignerDindex(ℓ, m′, m)] +end +function D(α, β, γ, ℓₘₐₓ) + α, β, γ = promote(α, β, γ) + D_storage = D_prep(ℓₘₐₓ, typeof(β)) + D_matrices!(D_storage, α, β, γ) + D_storage[1] +end + @doc raw""" From 5853257b06b6e0507f739b39c9dc737f7273bcbf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:47:56 -0500 Subject: [PATCH 128/329] Minor wording tweaks --- .../conventions_comparisons/condon_shortley_1935.jl | 12 +++++------- .../conventions_comparisons/ninja_2011.jl | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index 6556044c..b614cfd1 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -47,13 +47,11 @@ ones computed by this package. (Condon and Shortley do not give an expression for the Wigner D-matrices.) -""" +## Implementing formulas -# ## Implementing formulas -# -# We begin by writing code that implements the formulas from Condon-Shortley. We -# encapsulate the formulas in a module so that we can test them against the -# SphericalFunctions package. +We begin by writing code that implements the formulas from Condon-Shortley. We encapsulate +the formulas in a module so that we can test them against the SphericalFunctions package. +""" using TestItems: @testitem #hide @testitem "Condon-Shortley conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide @@ -181,6 +179,6 @@ end #+ # This successful test shows that the function ``\phi`` defined by Condon and Shortley -# agrees with the spherical harmonics defined by the SphericalFunctions package. +# agrees with the spherical harmonics defined by the `SphericalFunctions` package. end #hide diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl index 2f60e635..42808f72 100644 --- a/docs/literate_input/conventions_comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -147,6 +147,6 @@ end # These successful tests show that both the spin-weighted spherical harmonics and the Wigner # ``d`` matrix defined by the NINJA collaboration agree with the corresponding functions -# defined by the SphericalFunctions package. +# defined by the `SphericalFunctions` package. end #hide From d38c0fd4ef7ceb7fda84b2d995d7ab6c1d785e59 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:56:00 -0500 Subject: [PATCH 129/329] =?UTF-8?q?Add=20option=20to=20=E2=84=93mrange=20f?= =?UTF-8?q?or=20ell=5Fmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/utilities/utilities.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/utilities/utilities.jl b/test/utilities/utilities.jl index e41f4d59..94b52c74 100644 --- a/test/utilities/utilities.jl +++ b/test/utilities/utilities.jl @@ -1,6 +1,7 @@ @testsnippet Utilities begin -ℓmrange(ℓₘₐₓ) = eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) +ℓmrange(ℓₘᵢₙ, ℓₘₐₓ) = eachrow(SphericalFunctions.Yrange(ℓₘᵢₙ, ℓₘₐₓ)) +ℓmrange(ℓₘₐₓ) = ℓmrange(0, ℓₘₐₓ) function sℓmrange(ℓₘₐₓ, sₘₐₓ) sₘₐₓ = min(abs(sₘₐₓ), ℓₘₐₓ) [ From 3f8fc6d6fc911f79fabcb4c2d53213c9026fb172 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:57:00 -0500 Subject: [PATCH 130/329] Move LALSuite docs to Literate TestItem --- Project.toml | 4 +- .../conventions_comparisons/lalsuite_2025.jl | 210 +++++++ .../lalsuite_SphericalHarmonics.c | 585 ++++++++++++++++++ .../conventions_comparisons/ninja_2011.jl | 2 +- docs/make.jl | 5 + docs/src/conventions/comparisons.md | 17 +- test/conventions/lal.jl | 271 -------- test/wigner_matrices/sYlm.jl | 12 +- 8 files changed, 807 insertions(+), 299 deletions(-) create mode 100644 docs/literate_input/conventions_comparisons/lalsuite_2025.jl create mode 100644 docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c delete mode 100644 test/conventions/lal.jl diff --git a/Project.toml b/Project.toml index 6166a788..75242b55 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,7 @@ Literate = "2.20" Logging = "1.11" LoopVectorization = "0.12" OffsetArrays = "1.10" +Printf = "1.11.0" ProgressMeter = "1" Quaternionic = "3" Random = "1" @@ -54,6 +55,7 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -62,4 +64,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] +test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "Printf", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl new file mode 100644 index 00000000..b7bac149 --- /dev/null +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -0,0 +1,210 @@ +md""" +# LALSuite (2025) + +!!! info "Summary" + The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` + functions agree with the definitions used in the `SphericalFunctions` package. + +[LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software +routines, comprising the primary official software used by the LIGO-Virgo-KAGRA +Collaboration to detect and characterize gravitational waves. As far as I can tell, the +ultimate source for all spin-weighted spherical harmonic values used in LALSuite is the +function +[`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), +which cites the NINJA paper [AjithEtAl_2011](@cite) as its source. Unfortunately, it cites +version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` instead of +``\cos \tfrac{\iota}{2}`` and similarly for ``\sin``. This error was corrected in version +2, but the citation was not updated. Nonetheless, it appears that the actual code is +consistent with the *corrected* versions of the NINJA paper. + + +## Implementing formulas + +We begin by directly translating the C code of LALSuite over to Julia code. There are three +functions that we will want to compare with the definitions in this package: + +```c +COMPLEX16 XLALSpinWeightedSphericalHarmonic( REAL8 theta, REAL8 phi, int s, int l, int m ); +double XLALWignerdMatrix( int l, int mp, int m, double beta ); +COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ); +``` + +The source code is stored alongside this file, so we will read it in to a `String` and then +apply a series of regular expressions to convert it to Julia code, parse it and evaluate it +to turn it into runnable Julia. We encapsulate the formulas in a module so that we can test +them against the `SphericalFunctions` package. + +We begin by setting up that module, and introducing a set of basic replacements that would +usually be defined in separate C headers. + +""" +using TestItems: @testitem #hide +@testitem "LALSuite conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide + +module LALSuite + + +using Printf: @sprintf + +const I = im +const LAL_PI = π +const XLAL_EINVAL = "XLAL Error: Invalid arguments" +MIN(a, b) = min(a, b) +gsl_sf_choose(a, b) = binomial(a, b) +pow(a, b) = a^b +cexp(a) = exp(a) +cpolar(a, b) = a * cis(b) +macro XLALPrError(msg, args...) + quote + @error @sprintf($msg, $(args...)) + end +end +#+ + +# Next, we simply read the source file into a string. +lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) +#+ + +# Now we define a series of replacements to apply to the C code to convert it to Julia code. +# Note that many of these will be quite specific to this particular file, and may not be +# generally applicable. +replacements = ( + ## Deal with newlines in the middle of an assignment + r"( = .*[^;]\s*)\n" => s"\1", + + ## Remove a couple old, unused functions + r"(?ms)XLALScalarSphericalHarmonic.*?\n}" => "# Removed", + r"(?ms)XLALSphHarm.*?\n}" => "# Removed", + + ## Remove type annotations + r"COMPLEX16 ?" => "", + r"REAL8 ?" => "", + r"INT4 ?" => "", + r"int ?" => "", + r"double ?" => "", + + ## Translate comments + "/*" => "#=", + "*/" => "=#", + + ## Brackets + r" ?{" => "", + r"}.*(\n *else)" => s"\1", + r"} *else" => "else", + r"^}" => "", + "}" => "end", + + ## Flow control + r"( *if.*);"=>s"\1 end\n", ## one-line `if` statements + "for( s=0; n-s >= 0; s++ )" => "for s=0:n", + "else if" => "elseif", + r"(?m) break;\n *\n *case(.*?):" => s"elseif m == \1", + r"(?m) break;\n\s*case(.*?):" => s"elseif m == \1", + r"(?m) break;\n *\n *default:" => "else", + r"(?m) break;\n *default:" => "else", + r"(?m)switch.*?\n *\n( *)case(.*?):" => s"\n\1if m == \2", + r"\n *break;" => "", + r"(?m)(else\n *ans = fac;)(\n *return ans;)" => s"\1\n end\2", + + ## Deal with ugly C declarations + "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", + "sum=0, val=0" => "sum=0; val=0", + "a=0, lam=0" => "a=0; lam=0", + r"\n *fac;" => "", + r"\n *ans;" => "", + r"\n *gslStatus;" => "", + r"\n *gsl_sf_result pLm;" => "", + r"\n ?XLAL" => "\nfunction XLAL", + + ## Differences in Julia syntax + "++" => "+=1", + ".*" => ". *", + "./" => ". /", + ".+" => ". +", + ".-" => ". -", + + ## Deal with random bad syntax + "if (m)" => "if m != 0", + "case 4:" => "elseif m == 4", + "XLALPrError" => "@XLALPrError", + "__func__" => "\"\"", +) +#+ + +# And we apply the replacements to the source code to convert it to Julia code. Note that +# we apply them successively, even though `replace` can handle multiple "simultaneous" +# replacements, because the order of replacements is important. +for (pattern, replacement) in replacements + global lalsource = replace(lalsource, pattern => replacement) +end +#+ + +# Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are +# done defining the module +eval(Meta.parseall(lalsource)) + +end # module LALSuite +#+ + +# ## Tests +# +# We can now test the functions against the equivalent functions from the SphericalFunctions +# package. We will need to test approximate floating-point equality, so we set absolute and +# relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 100eps() +ϵᵣ = 1000eps() +#+ + +# The spin-weighted spherical harmonics are defined explicitly, but only for +s = -2 +#+ +# and only up to +ℓₘₐₓ = 8 +#+ +# so we only test up to that point. +for (θ, ϕ) ∈ θϕrange() + for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) + @test LALSuite.XLALSpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ + SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# Now, the Wigner ``d`` matrices are defined generally, but we only need to test up to +ℓₘₐₓ = 4 +#+ +# because the formulas are fairly inefficient and inaccurate, and this will be sufficient to +# sort out any sign or normalization differences, which are the most likely sources of +# error. +for β ∈ βrange() + for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) + @test LALSuite.XLALWignerdMatrix(ℓ, m′, m, β) ≈ SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# We can see more-or-less by inspection that the code defines the ``D`` matrix in agreement +# with our convention, the key line being +# ```c +# cexp( -(1.0I)*mp*alpha ) * XLALWignerdMatrix( l, mp, m, beta ) * cexp( -(1.0I)*m*gam ); +# ``` +# And because of the higher dimensionality of the space in which to test, we want to +# restrict the range of the tests to avoid excessive computation. We will test up to +ℓₘₐₓ = 2 +#+ +# because the space of options for disagreement is smaller. +for (α,β,γ) ∈ αβγrange() + for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) + @test LALSuite.XLALWignerDMatrix(ℓ, m′, m, α, β, γ) ≈ + conj(SphericalFunctions.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ + end +end +@test_broken false # We haven't flipped the conjugation of D yet + +#+ + +# These successful tests show that the spin-weighted spherical harmonics and the Wigner +# ``d`` and ``D`` matrices defined in LALSuite agree with the corresponding functions +# defined by the `SphericalFunctions` package. + +end #hide diff --git a/docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c b/docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c new file mode 100644 index 00000000..3d4e56d4 --- /dev/null +++ b/docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2007 S.Fairhurst, B. Krishnan, L.Santamaria, C. Robinson, + * C. Pankow + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with with program; see the file COPYING. If not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include +#include +#include + +#include +#include + +/** + * Computes the (s)Y(l,m) spin-weighted spherical harmonic. + * + * From somewhere .... + * + * See also: + * Implements Equations (II.9)-(II.13) of + * D. A. Brown, S. Fairhurst, B. Krishnan, R. A. Mercer, R. K. Kopparapu, + * L. Santamaria, and J. T. Whelan, + * "Data formats for numerical relativity waves", + * arXiv:0709.0093v1 (2007). + * + * Currently only supports s=-2, l=2,3,4,5,6,7,8 modes. + */ +COMPLEX16 XLALSpinWeightedSphericalHarmonic( + REAL8 theta, /**< polar angle (rad) */ + REAL8 phi, /**< azimuthal angle (rad) */ + int s, /**< spin weight */ + int l, /**< mode number l */ + int m /**< mode number m */ + ) +{ + REAL8 fac; + COMPLEX16 ans; + + /* sanity checks ... */ + if ( l < abs(s) ) + { + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |s| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + if ( l < abs(m) ) + { + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + + if ( s == -2 ) + { + if ( l == 2 ) + { + switch ( m ) + { + case -2: + fac = sqrt( 5.0 / ( 64.0 * LAL_PI ) ) * ( 1.0 - cos( theta ))*( 1.0 - cos( theta )); + break; + case -1: + fac = sqrt( 5.0 / ( 16.0 * LAL_PI ) ) * sin( theta )*( 1.0 - cos( theta )); + break; + + case 0: + fac = sqrt( 15.0 / ( 32.0 * LAL_PI ) ) * sin( theta )*sin( theta ); + break; + + case 1: + fac = sqrt( 5.0 / ( 16.0 * LAL_PI ) ) * sin( theta )*( 1.0 + cos( theta )); + break; + + case 2: + fac = sqrt( 5.0 / ( 64.0 * LAL_PI ) ) * ( 1.0 + cos( theta ))*( 1.0 + cos( theta )); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } /* switch (m) */ + } /* l==2*/ + else if ( l == 3 ) + { + switch ( m ) + { + case -3: + fac = sqrt(21.0/(2.0*LAL_PI))*cos(theta/2.0)*pow(sin(theta/2.0),5.0); + break; + case -2: + fac = sqrt(7.0/(4.0*LAL_PI))*(2.0 + 3.0*cos(theta))*pow(sin(theta/2.0),4.0); + break; + case -1: + fac = sqrt(35.0/(2.0*LAL_PI))*(sin(theta) + 4.0*sin(2.0*theta) - 3.0*sin(3.0*theta))/32.0; + break; + case 0: + fac = (sqrt(105.0/(2.0*LAL_PI))*cos(theta)*pow(sin(theta),2.0))/4.0; + break; + case 1: + fac = -sqrt(35.0/(2.0*LAL_PI))*(sin(theta) - 4.0*sin(2.0*theta) - 3.0*sin(3.0*theta))/32.0; + break; + + case 2: + fac = sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),4.0)*(-2.0 + 3.0*cos(theta))/2.0; + break; + + case 3: + fac = -sqrt(21.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*sin(theta/2.0); + break; + + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==3 */ + else if ( l == 4 ) + { + switch ( m ) + { + case -4: + fac = 3.0*sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),2.0)*pow(sin(theta/2.0),6.0); + break; + case -3: + fac = 3.0*sqrt(7.0/(2.0*LAL_PI))*cos(theta/2.0)*(1.0 + 2.0*cos(theta))*pow(sin(theta/2.0),5.0); + break; + + case -2: + fac = (3.0*(9.0 + 14.0*cos(theta) + 7.0*cos(2.0*theta))*pow(sin(theta/2.0),4.0))/(4.0*sqrt(LAL_PI)); + break; + case -1: + fac = (3.0*(3.0*sin(theta) + 2.0*sin(2.0*theta) + 7.0*sin(3.0*theta) - 7.0*sin(4.0*theta)))/(32.0*sqrt(2.0*LAL_PI)); + break; + case 0: + fac = (3.0*sqrt(5.0/(2.0*LAL_PI))*(5.0 + 7.0*cos(2.0*theta))*pow(sin(theta),2.0))/16.0; + break; + case 1: + fac = (3.0*(3.0*sin(theta) - 2.0*sin(2.0*theta) + 7.0*sin(3.0*theta) + 7.0*sin(4.0*theta)))/(32.0*sqrt(2.0*LAL_PI)); + break; + case 2: + fac = (3.0*pow(cos(theta/2.0),4.0)*(9.0 - 14.0*cos(theta) + 7.0*cos(2.0*theta)))/(4.0*sqrt(LAL_PI)); + break; + case 3: + fac = -3.0*sqrt(7.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*(-1.0 + 2.0*cos(theta))*sin(theta/2.0); + break; + case 4: + fac = 3.0*sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),6.0)*pow(sin(theta/2.0),2.0); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==4 */ + else if ( l == 5 ) + { + switch ( m ) + { + case -5: + fac = sqrt(330.0/LAL_PI)*pow(cos(theta/2.0),3.0)*pow(sin(theta/2.0),7.0); + break; + case -4: + fac = sqrt(33.0/LAL_PI)*pow(cos(theta/2.0),2.0)*(2.0 + 5.0*cos(theta))*pow(sin(theta/2.0),6.0); + break; + case -3: + fac = (sqrt(33.0/(2.0*LAL_PI))*cos(theta/2.0)*(17.0 + 24.0*cos(theta) + 15.0*cos(2.0*theta))*pow(sin(theta/2.0),5.0))/4.0; + break; + case -2: + fac = (sqrt(11.0/LAL_PI)*(32.0 + 57.0*cos(theta) + 36.0*cos(2.0*theta) + 15.0*cos(3.0*theta))*pow(sin(theta/2.0),4.0))/8.0; + break; + case -1: + fac = (sqrt(77.0/LAL_PI)*(2.0*sin(theta) + 8.0*sin(2.0*theta) + 3.0*sin(3.0*theta) + 12.0*sin(4.0*theta) - 15.0*sin(5.0*theta)))/256.0; + break; + case 0: + fac = (sqrt(1155.0/(2.0*LAL_PI))*(5.0*cos(theta) + 3.0*cos(3.0*theta))*pow(sin(theta),2.0))/32.0; + break; + case 1: + fac = sqrt(77.0/LAL_PI)*(-2.0*sin(theta) + 8.0*sin(2.0*theta) - 3.0*sin(3.0*theta) + 12.0*sin(4.0*theta) + 15.0*sin(5.0*theta))/256.0; + break; + case 2: + fac = sqrt(11.0/LAL_PI)*pow(cos(theta/2.0),4.0)*(-32.0 + 57.0*cos(theta) - 36.0*cos(2.0*theta) + 15.0*cos(3.0*theta))/8.0; + break; + case 3: + fac = -sqrt(33.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*(17.0 - 24.0*cos(theta) + 15.0*cos(2.0*theta))*sin(theta/2.0)/4.0; + break; + case 4: + fac = sqrt(33.0/LAL_PI)*pow(cos(theta/2.0),6.0)*(-2.0 + 5.0*cos(theta))*pow(sin(theta/2.0),2.0); + break; + case 5: + fac = -sqrt(330.0/LAL_PI)*pow(cos(theta/2.0),7.0)*pow(sin(theta/2.0),3.0); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==5 */ + else if ( l == 6 ) + { + switch ( m ) + { + case -6: + fac = (3.*sqrt(715./LAL_PI)*pow(cos(theta/2.0),4)*pow(sin(theta/2.0),8))/2.0; + break; + case -5: + fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),3)*(1. + 3.*cos(theta))*pow(sin(theta/2.0),7))/2.0; + break; + case -4: + fac = (sqrt(195./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(35. + 44.*cos(theta) + + 33.*cos(2.*theta))*pow(sin(theta/2.0),6))/8.0; + break; + case -3: + fac = (3.*sqrt(13./LAL_PI)*cos(theta/2.0)*(98. + 185.*cos(theta) + 110.*cos(2*theta) + + 55.*cos(3.*theta))*pow(sin(theta/2.0),5))/32.0; + break; + case -2: + fac = (sqrt(13./LAL_PI)*(1709. + 3096.*cos(theta) + 2340.*cos(2.*theta) + 1320.*cos(3.*theta) + + 495.*cos(4.*theta))*pow(sin(theta/2.0),4))/256.0; + break; + case -1: + fac = (sqrt(65./(2.0*LAL_PI))*cos(theta/2.0)*(161. + 252.*cos(theta) + 252.*cos(2.*theta) + + 132.*cos(3.*theta) + 99.*cos(4.*theta))*pow(sin(theta/2.0),3))/64.0; + break; + case 0: + fac = (sqrt(1365./LAL_PI)*(35. + 60.*cos(2.*theta) + 33.*cos(4.*theta))*pow(sin(theta),2))/512.0; + break; + case 1: + fac = (sqrt(65./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(161. - 252.*cos(theta) + 252.*cos(2.*theta) + - 132.*cos(3.*theta) + 99.*cos(4.*theta))*sin(theta/2.0))/64.0; + break; + case 2: + fac = (sqrt(13./LAL_PI)*pow(cos(theta/2.0),4)*(1709. - 3096.*cos(theta) + 2340.*cos(2.*theta) + - 1320*cos(3*theta) + 495*cos(4*theta)))/256.0; + break; + case 3: + fac = (-3.*sqrt(13./LAL_PI)*pow(cos(theta/2.0),5)*(-98. + 185.*cos(theta) - 110.*cos(2*theta) + + 55.*cos(3.*theta))*sin(theta/2.0))/32.0; + break; + case 4: + fac = (sqrt(195./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(35. - 44.*cos(theta) + + 33.*cos(2*theta))*pow(sin(theta/2.0),2))/8.0; + break; + case 5: + fac = -(sqrt(2145./LAL_PI)*pow(cos(theta/2.0),7)*(-1. + 3.*cos(theta))*pow(sin(theta/2.0),3))/2.0; + break; + case 6: + fac = (3.*sqrt(715./LAL_PI)*pow(cos(theta/2.0),8)*pow(sin(theta/2.0),4))/2.0; + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==6 */ + else if ( l == 7 ) + { + switch ( m ) + { + case -7: + fac = sqrt(15015./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*pow(sin(theta/2.0),9); + break; + case -6: + fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),4)*(2. + 7.*cos(theta))*pow(sin(theta/2.0),8))/2.0; + break; + case -5: + fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(93. + 104.*cos(theta) + + 91.*cos(2.*theta))*pow(sin(theta/2.0),7))/8.0; + break; + case -4: + fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(140. + 285.*cos(theta) + + 156.*cos(2.*theta) + 91.*cos(3.*theta))*pow(sin(theta/2.0),6))/16.0; + break; + case -3: + fac = (sqrt(15./(2.0*LAL_PI))*cos(theta/2.0)*(3115. + 5456.*cos(theta) + 4268.*cos(2.*theta) + + 2288.*cos(3.*theta) + 1001.*cos(4.*theta))*pow(sin(theta/2.0),5))/128.0; + break; + case -2: + fac = (sqrt(15./LAL_PI)*(5220. + 9810.*cos(theta) + 7920.*cos(2.*theta) + 5445.*cos(3.*theta) + + 2860.*cos(4.*theta) + 1001.*cos(5.*theta))*pow(sin(theta/2.0),4))/512.0; + break; + case -1: + fac = (3.*sqrt(5./(2.0*LAL_PI))*cos(theta/2.0)*(1890. + 4130.*cos(theta) + 3080.*cos(2.*theta) + + 2805.*cos(3.*theta) + 1430.*cos(4.*theta) + 1001.*cos(5*theta))*pow(sin(theta/2.0),3))/512.0; + break; + case 0: + fac = (3.*sqrt(35./LAL_PI)*cos(theta)*(109. + 132.*cos(2.*theta) + + 143.*cos(4.*theta))*pow(sin(theta),2))/512.0; + break; + case 1: + fac = (3.*sqrt(5./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(-1890. + 4130.*cos(theta) - 3080.*cos(2.*theta) + + 2805.*cos(3.*theta) - 1430.*cos(4.*theta) + 1001.*cos(5.*theta))*sin(theta/2.0))/512.0; + break; + case 2: + fac = (sqrt(15./LAL_PI)*pow(cos(theta/2.0),4)*(-5220. + 9810.*cos(theta) - 7920.*cos(2.*theta) + + 5445.*cos(3.*theta) - 2860.*cos(4.*theta) + 1001.*cos(5.*theta)))/512.0; + break; + case 3: + fac = -(sqrt(15./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(3115. - 5456.*cos(theta) + 4268.*cos(2.*theta) + - 2288.*cos(3.*theta) + 1001.*cos(4.*theta))*sin(theta/2.0))/128.0; + break; + case 4: + fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(-140. + 285.*cos(theta) - 156.*cos(2*theta) + + 91.*cos(3.*theta))*pow(sin(theta/2.0),2))/16.0; + break; + case 5: + fac = -(sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),7)*(93. - 104.*cos(theta) + + 91.*cos(2.*theta))*pow(sin(theta/2.0),3))/8.0; + break; + case 6: + fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),8)*(-2. + 7.*cos(theta))*pow(sin(theta/2.0),4))/2.0; + break; + case 7: + fac = -(sqrt(15015./(2.0*LAL_PI))*pow(cos(theta/2.0),9)*pow(sin(theta/2.0),5)); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==7 */ + else if ( l == 8 ) + { + switch ( m ) + { + case -8: + fac = sqrt(34034./LAL_PI)*pow(cos(theta/2.0),6)*pow(sin(theta/2.0),10); + break; + case -7: + fac = sqrt(17017./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(1. + 4.*cos(theta))*pow(sin(theta/2.0),9); + break; + case -6: + fac = sqrt(255255./LAL_PI)*pow(cos(theta/2.0),4)*(1. + 2.*cos(theta)) + *sin(LAL_PI/4.0 - theta/2.0)*sin(LAL_PI/4.0 + theta/2.0)*pow(sin(theta/2.0),8); + break; + case -5: + fac = (sqrt(12155./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(19. + 42.*cos(theta) + + 21.*cos(2.*theta) + 14.*cos(3.*theta))*pow(sin(theta/2.0),7))/8.0; + break; + case -4: + fac = (sqrt(935./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(265. + 442.*cos(theta) + 364.*cos(2.*theta) + + 182.*cos(3.*theta) + 91.*cos(4.*theta))*pow(sin(theta/2.0),6))/32.0; + break; + case -3: + fac = (sqrt(561./(2.0*LAL_PI))*cos(theta/2.0)*(869. + 1660.*cos(theta) + 1300.*cos(2.*theta) + + 910.*cos(3.*theta) + 455.*cos(4.*theta) + 182.*cos(5.*theta))*pow(sin(theta/2.0),5))/128.0; + break; + case -2: + fac = (sqrt(17./LAL_PI)*(7626. + 14454.*cos(theta) + 12375.*cos(2.*theta) + 9295.*cos(3.*theta) + + 6006.*cos(4.*theta) + 3003.*cos(5.*theta) + 1001.*cos(6.*theta))*pow(sin(theta/2.0),4))/512.0; + break; + case -1: + fac = (sqrt(595./(2.0*LAL_PI))*cos(theta/2.0)*(798. + 1386.*cos(theta) + 1386.*cos(2.*theta) + + 1001.*cos(3.*theta) + 858.*cos(4.*theta) + 429.*cos(5.*theta) + 286.*cos(6.*theta))*pow(sin(theta/2.0),3))/512.0; + break; + case 0: + fac = (3.*sqrt(595./LAL_PI)*(210. + 385.*cos(2.*theta) + 286.*cos(4.*theta) + + 143.*cos(6.*theta))*pow(sin(theta),2))/4096.0; + break; + case 1: + fac = (sqrt(595./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(798. - 1386.*cos(theta) + 1386.*cos(2.*theta) + - 1001.*cos(3.*theta) + 858.*cos(4.*theta) - 429.*cos(5.*theta) + 286.*cos(6.*theta))*sin(theta/2.0))/512.0; + break; + case 2: + fac = (sqrt(17./LAL_PI)*pow(cos(theta/2.0),4)*(7626. - 14454.*cos(theta) + 12375.*cos(2.*theta) + - 9295.*cos(3.*theta) + 6006.*cos(4.*theta) - 3003.*cos(5.*theta) + 1001.*cos(6.*theta)))/512.0; + break; + case 3: + fac = -(sqrt(561./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(-869. + 1660.*cos(theta) - 1300.*cos(2.*theta) + + 910.*cos(3.*theta) - 455.*cos(4.*theta) + 182.*cos(5.*theta))*sin(theta/2.0))/128.0; + break; + case 4: + fac = (sqrt(935./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(265. - 442.*cos(theta) + 364.*cos(2.*theta) + - 182.*cos(3.*theta) + 91.*cos(4.*theta))*pow(sin(theta/2.0),2))/32.0; + break; + case 5: + fac = -(sqrt(12155./(2.0*LAL_PI))*pow(cos(theta/2.0),7)*(-19. + 42.*cos(theta) - 21.*cos(2.*theta) + + 14.*cos(3.*theta))*pow(sin(theta/2.0),3))/8.0; + break; + case 6: + fac = sqrt(255255./LAL_PI)*pow(cos(theta/2.0),8)*(-1. + 2.*cos(theta))*sin(LAL_PI/4.0 - theta/2.0) + *sin(LAL_PI/4.0 + theta/2.0)*pow(sin(theta/2.0),4); + break; + case 7: + fac = -(sqrt(17017./(2.0*LAL_PI))*pow(cos(theta/2.0),9)*(-1. + 4.*cos(theta))*pow(sin(theta/2.0),5)); + break; + case 8: + fac = sqrt(34034./LAL_PI)*pow(cos(theta/2.0),10)*pow(sin(theta/2.0),6); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==8 */ + else + { + XLALPrintError("XLAL Error - %s: Unsupported mode l=%d (only l in [2,8] implemented)\n", __func__, l); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + } + else + { + XLALPrintError("XLAL Error - %s: Unsupported mode s=%d (only s=-2 implemented)\n", __func__, s); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + if (m) + ans = cpolar(1.0, m*phi) * fac; + else + ans = fac; + return ans; +} + + +/** + * Computes the scalar spherical harmonic \f$ Y_{lm}(\theta, \phi) \f$. + */ +int +XLALScalarSphericalHarmonic( + COMPLEX16 *y, /**< output */ + UINT4 l, /**< value of l */ + INT4 m, /**< value of m */ + REAL8 theta, /**< angle theta */ + REAL8 phi /**< angle phi */ + ) +{ + + int gslStatus; + gsl_sf_result pLm; + + INT4 absM = abs( m ); + + if ( absM > (INT4) l ) + { + XLAL_ERROR( XLAL_EINVAL ); + } + + /* For some reason GSL will not take negative m */ + /* We will have to use the relation between sph harmonics of +ve and -ve m */ + XLAL_CALLGSL( gslStatus = gsl_sf_legendre_sphPlm_e((INT4)l, absM, cos(theta), &pLm ) ); + if (gslStatus != GSL_SUCCESS) + { + XLALPrintError("Error in GSL function\n" ); + XLAL_ERROR( XLAL_EFUNC ); + } + + /* Compute the values for the spherical harmonic */ + *y = cpolar(pLm.val, m * phi); + + /* If m is negative, perform some jiggery-pokery */ + if ( m < 0 && absM % 2 == 1 ) + { + *y = - *y; + } + + return XLAL_SUCCESS; +} + +/** + * Computes the spin 2 weighted spherical harmonic. This function is now + * deprecated and will be removed soon. All calls should be replaced with + * calls to XLALSpinWeightedSphericalHarmonic(). + */ +INT4 XLALSphHarm ( COMPLEX16 *out, /**< output */ + UINT4 L, /**< value of L */ + INT4 M, /**< value of M */ + REAL4 theta, /**< angle with respect to the z axis */ + REAL4 phi /**< angle with respect to the x axis */ + ) +{ + + XLAL_PRINT_DEPRECATION_WARNING("XLALSpinWeightedSphericalHarmonic"); + + *out = XLALSpinWeightedSphericalHarmonic( theta, phi, -2, L, M ); + if ( xlalErrno ) + { + XLAL_ERROR( XLAL_EFUNC ); + } + + return XLAL_SUCCESS; +} + +/** + * Computes the n-th Jacobi polynomial for polynomial weights alpha and beta. + * The implementation here is only valid for real x -- enforced by the argument + * type. An extension to complex values would require evaluation of several + * gamma functions. + * + * See http://en.wikipedia.org/wiki/Jacobi_polynomials + */ +double XLALJacobiPolynomial(int n, int alpha, int beta, double x){ + double f1 = (x-1)/2.0, f2 = (x+1)/2.0; + int s=0; + double sum=0, val=0; + if( n == 0 ) return 1.0; + for( s=0; n-s >= 0; s++ ){ + val=1.0; + val *= gsl_sf_choose( n+alpha, s ); + val *= gsl_sf_choose( n+beta, n-s ); + if( n-s != 0 ) val *= pow( f1, n-s ); + if( s != 0 ) val*= pow( f2, s ); + + sum += val; + } + return sum; +} + +/** + * Computes the 'little' d Wigner matrix for the Euler angle beta. Single angle + * small d transform with major index 'l' and minor index transition from m to + * mp. + * + * Uses a slightly unconventional method since the intuitive version by Wigner + * is less suitable to algorthmic development. + * + * See http://en.wikipedia.org/wiki/Wigner_D-matrix#Wigner_.28small.29_d-matrix + */ +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +double XLALWignerdMatrix( + int l, /**< mode number l */ + int mp, /**< mode number m' */ + int m, /**< mode number m */ + double beta /**< euler angle (rad) */ + ) +{ + + int k = MIN( l+m, MIN( l-m, MIN( l+mp, l-mp ))); + double a=0, lam=0; + if(k == l+m){ + a = mp-m; + lam = mp-m; + } else if(k == l-m) { + a = m-mp; + lam = 0; + } else if(k == l+mp) { + a = m-mp; + lam = 0; + } else if(k == l-mp) { + a = mp-m; + lam = mp-m; + } + + int b = 2*l-2*k-a; + double pref = pow(-1, lam) * sqrt(gsl_sf_choose( 2*l-k, k+a )) / sqrt(gsl_sf_choose( k+b, b )); + + return pref * pow(sin(beta/2.0), a) * pow( cos(beta/2.0), b) * XLALJacobiPolynomial(k, a, b, cos(beta)); + +} + +/** + * Computes the full Wigner D matrix for the Euler angle alpha, beta, and gamma + * with major index 'l' and minor index transition from m to mp. + * + * Uses a slightly unconventional method since the intuitive version by Wigner + * is less suitable to algorthmic development. + * + * See http://en.wikipedia.org/wiki/Wigner_D-matrix + * + * Currently only supports the modes which are implemented for the spin + * weighted spherical harmonics. + */ +COMPLEX16 XLALWignerDMatrix( + int l, /**< mode number l */ + int mp, /**< mode number m' */ + int m, /**< mode number m */ + double alpha, /**< euler angle (rad) */ + double beta, /**< euler angle (rad) */ + double gam /**< euler angle (rad) */ + ) +{ + return cexp( -(1.0I)*mp*alpha ) * + XLALWignerdMatrix( l, mp, m, beta ) * + cexp( -(1.0I)*m*gam ); +} diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl index 42808f72..dd807c55 100644 --- a/docs/literate_input/conventions_comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -3,7 +3,7 @@ md""" !!! info "Summary" The NINJA collaboration's definitions of the spherical harmonics and Wigner's ``d`` - functions agrees with the definitions used in the `SphericalFunctions` package. + functions agree with the definitions used in the `SphericalFunctions` package. Motivated by the need for a shared set of conventions in the NINJA project, a broad cross-section of researchers involved in modeling gravitational waves (including the author diff --git a/docs/make.jl b/docs/make.jl index 20223e59..a4c93c25 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -33,6 +33,10 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files # generate the markdown file calling Literate Literate.markdown(input_path, output_path, documenter=true, mdstrings=true) end +cp( + joinpath(literate_input, "conventions_comparisons", "lalsuite_SphericalHarmonics.c"), + joinpath(literate_output, "conventions_comparisons", "lalsuite_SphericalHarmonics.c") +) relative_literate_output = relpath(literate_output, docs_src_dir) relative_convention_comparisons = joinpath(relative_literate_output, "conventions_comparisons") @@ -71,6 +75,7 @@ makedocs( "conventions/comparisons.md", "Comparisons" => [ joinpath(relative_convention_comparisons, "condon_shortley_1935.md"), + joinpath(relative_convention_comparisons, "lalsuite_2025.md"), joinpath(relative_convention_comparisons, "ninja_2011.md"), ], "Calculations" => [ diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 71b678d3..72bad5ea 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -320,22 +320,7 @@ L_z &= -i \hbar \frac{\partial} {\partial \phi}. ## LALSuite -[LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a -collection of software routines, comprising the primary official -software used by the LIGO-Virgo-KAGRA Collaboration to detect and -characterize gravitational waves. As far as I can tell, the ultimate -source for all spin-weighted spherical harmonic values used in -LALSuite is the function -[`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), -which cites the NINJA paper [AjithEtAl_2011](@cite) as its source. -Unfortunately, it cites version *1*, which contained a serious error, -using ``\tfrac{\cos\iota}{2}`` instead of ``\cos \tfrac{\iota}{2}`` -and similarly for ``\sin``. This error was corrected in version 2, -but the citation was not updated. Nonetheless, it appears that the -actual code is consistent with the corrected versions of the NINJA -paper; the equivalence is -[tested](https://github.com/moble/SphericalFunctions.jl/blob/0f57c77e65da85e4996f0969fe0a931b460135ac/test/wigner_matrices/sYlm.jl#L59) -in this package's test suite. +(moved) ## Le Bellac (2006) diff --git a/test/conventions/lal.jl b/test/conventions/lal.jl deleted file mode 100644 index f81af58b..00000000 --- a/test/conventions/lal.jl +++ /dev/null @@ -1,271 +0,0 @@ -@testmodule LAL begin - # The code in this section is translated from C code in LALSuite: - # - # https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c - # - # That code is licensed under GPLv2. See the link for details. - - function XLALSpinWeightedSphericalHarmonic( - theta::Float64, # polar angle (rad) - phi::Float64, # azimuthal angle (rad) - s::Int, # spin weight - l::Int, # mode number l - m::Int # mode number m - ) - # Sanity checks - if l < abs(s) - error("Invalid mode s=$s, l=$l, m=$m - require |s| <= l") - end - if l < abs(m) - error("Invalid mode s=$s, l=$l, m=$m - require |m| <= l") - end - if s != -2 - error("Unsupported mode s=$s (only s=-2 implemented)") - end - if l < 2 || l > 8 - error("Unsupported mode l=$l (only l in [2,8] implemented)") - end - # Compute real factor - fac = if l == 2 - if m == -2 - sqrt(5.0 / (64.0 * π)) * (1.0 - cos(theta)) * (1.0 - cos(theta)) - elseif m == -1 - sqrt(5.0 / (16.0 * π)) * sin(theta) * (1.0 - cos(theta)) - elseif m == 0 - sqrt(15.0 / (32.0 * π)) * sin(theta) * sin(theta) - elseif m == 1 - sqrt(5.0 / (16.0 * π)) * sin(theta) * (1.0 + cos(theta)) - elseif m == 2 - sqrt(5.0 / (64.0 * π)) * (1.0 + cos(theta)) * (1.0 + cos(theta)) - end - elseif l == 3 - if m == -3 - sqrt(21.0 / (2.0 * π)) * cos(theta/2.0) * sin(theta/2.0)^5 - elseif m == -2 - sqrt(7.0 / (4.0 * π)) * (2.0 + 3.0 * cos(theta)) * sin(theta/2.0)^4 - elseif m == -1 - sqrt(35.0 / (2.0 * π)) * (sin(theta) + 4.0 * sin(2.0 * theta) - 3.0 * sin(3.0 * theta)) / 32.0 - elseif m == 0 - sqrt(105.0 / (2.0 * π)) * cos(theta) * sin(theta)^2 / 4.0 - elseif m == 1 - -sqrt(35.0 / (2.0 * π)) * (sin(theta) - 4.0 * sin(2.0 * theta) - 3.0 * sin(3.0 * theta)) / 32.0 - elseif m == 2 - sqrt(7.0 / π) * cos(theta/2.0)^4 * (-2.0 + 3.0 * cos(theta)) / 2.0 - elseif m == 3 - -sqrt(21.0 / (2.0 * π)) * cos(theta/2.0)^5 * sin(theta/2.0) - end - elseif l == 4 - if m == -4 - 3.0 * sqrt(7.0 / π) * cos(theta/2.0)^2 * sin(theta/2.0)^6 - elseif m == -3 - 3.0 * sqrt(7.0 / (2.0 * π)) * cos(theta/2.0) * (1.0 + 2.0 * cos(theta)) * sin(theta/2.0)^5 - elseif m == -2 - 3.0 * (9.0 + 14.0 * cos(theta) + 7.0 * cos(2.0 * theta)) * sin(theta/2.0)^4 / (4.0 * sqrt(π)) - elseif m == -1 - 3.0 * (3.0 * sin(theta) + 2.0 * sin(2.0 * theta) + 7.0 * sin(3.0 * theta) - 7.0 * sin(4.0 * theta)) / (32.0 * sqrt(2.0 * π)) - elseif m == 0 - 3.0 * sqrt(5.0 / (2.0 * π)) * (5.0 + 7.0 * cos(2.0 * theta)) * sin(theta)^2 / 16.0 - elseif m == 1 - 3.0 * (3.0 * sin(theta) - 2.0 * sin(2.0 * theta) + 7.0 * sin(3.0 * theta) + 7.0 * sin(4.0 * theta)) / (32.0 * sqrt(2.0 * π)) - elseif m == 2 - 3.0 * cos(theta/2.0)^4 * (9.0 - 14.0 * cos(theta) + 7.0 * cos(2.0 * theta)) / (4.0 * sqrt(π)) - elseif m == 3 - -3.0 * sqrt(7.0 / (2.0 * π)) * cos(theta/2.0)^5 * (-1.0 + 2.0 * cos(theta)) * sin(theta/2.0) - elseif m == 4 - 3.0 * sqrt(7.0 / π) * cos(theta/2.0)^6 * sin(theta/2.0)^2 - end - elseif l == 5 - if m == -5 - sqrt(330.0 / π) * cos(theta/2.0)^3 * sin(theta/2.0)^7 - elseif m == -4 - sqrt(33.0 / π) * cos(theta/2.0)^2 * (2.0 + 5.0 * cos(theta)) * sin(theta/2.0)^6 - elseif m == -3 - sqrt(33.0 / (2.0 * π)) * cos(theta/2.0) * (17.0 + 24.0 * cos(theta) + 15.0 * cos(2.0 * theta)) * sin(theta/2.0)^5 / 4.0 - elseif m == -2 - sqrt(11.0 / π) * (32.0 + 57.0 * cos(theta) + 36.0 * cos(2.0 * theta) + 15.0 * cos(3.0 * theta)) * sin(theta/2.0)^4 / 8.0 - elseif m == -1 - sqrt(77.0 / π) * (2.0 * sin(theta) + 8.0 * sin(2.0 * theta) + 3.0 * sin(3.0 * theta) + 12.0 * sin(4.0 * theta) - 15.0 * sin(5.0 * theta)) / 256.0 - elseif m == 0 - sqrt(1155.0 / (2.0 * π)) * (5.0 * cos(theta) + 3.0 * cos(3.0 * theta)) * sin(theta)^2 / 32.0 - elseif m == 1 - sqrt(77.0 / π) * (-2.0 * sin(theta) + 8.0 * sin(2.0 * theta) - 3.0 * sin(3.0 * theta) + 12.0 * sin(4.0 * theta) + 15.0 * sin(5.0 * theta)) / 256.0 - elseif m == 2 - sqrt(11.0 / π) * cos(theta/2.0)^4 * (-32.0 + 57.0 * cos(theta) - 36.0 * cos(2.0 * theta) + 15.0 * cos(3.0 * theta)) / 8.0 - elseif m == 3 - -sqrt(33.0 / (2.0 * π)) * cos(theta/2.0)^5 * (17.0 - 24.0 * cos(theta) + 15.0 * cos(2.0 * theta)) * sin(theta/2.0) / 4.0 - elseif m == 4 - sqrt(33.0 / π) * cos(theta/2.0)^6 * (-2.0 + 5.0 * cos(theta)) * sin(theta/2.0)^2 - elseif m == 5 - -sqrt(330.0 / π) * cos(theta/2.0)^7 * sin(theta/2.0)^3 - end - elseif l == 6 - if m == -6 - (3.0 * sqrt(715.0 / π) * cos(theta/2.0)^4 * sin(theta/2.0)^8) / 2.0 - elseif m == -5 - (sqrt(2145.0 / π) * cos(theta/2.0)^3 * (1.0 + 3.0 * cos(theta)) * sin(theta/2.0)^7) / 2.0 - elseif m == -4 - (sqrt(195.0 / (2.0 * π)) * cos(theta/2.0)^2 * (35.0 + 44.0 * cos(theta) + 33.0 * cos(2.0 * theta)) * sin(theta/2.0)^6) / 8.0 - elseif m == -3 - (3.0 * sqrt(13.0 / π) * cos(theta/2.0) * (98.0 + 185.0 * cos(theta) + 110.0 * cos(2.0 * theta) + 55.0 * cos(3.0 * theta)) * sin(theta/2.0)^5) / 32.0 - elseif m == -2 - (sqrt(13.0 / π) * (1709.0 + 3096.0 * cos(theta) + 2340.0 * cos(2.0 * theta) + 1320.0 * cos(3.0 * theta) + 495.0 * cos(4.0 * theta)) * sin(theta/2.0)^4) / 256.0 - elseif m == -1 - (sqrt(65.0 / (2.0 * π)) * cos(theta/2.0) * (161.0 + 252.0 * cos(theta) + 252.0 * cos(2.0 * theta) + 132.0 * cos(3.0 * theta) + 99.0 * cos(4.0 * theta)) * sin(theta/2.0)^3) / 64.0 - elseif m == 0 - (sqrt(1365.0 / π) * (35.0 + 60.0 * cos(2.0 * theta) + 33.0 * cos(4.0 * theta)) * sin(theta)^2) / 512.0 - elseif m == 1 - (sqrt(65.0 / (2.0 * π)) * cos(theta/2.0)^3 * (161.0 - 252.0 * cos(theta) + 252.0 * cos(2.0 * theta) - 132.0 * cos(3.0 * theta) + 99.0 * cos(4.0 * theta)) * sin(theta/2.0)) / 64.0 - elseif m == 2 - (sqrt(13.0 / π) * cos(theta/2.0)^4 * (1709.0 - 3096.0 * cos(theta) + 2340.0 * cos(2.0 * theta) - 1320.0 * cos(3.0 * theta) + 495.0 * cos(4.0 * theta))) / 256.0 - elseif m == 3 - (-3.0 * sqrt(13.0 / π) * cos(theta/2.0)^5 * (-98.0 + 185.0 * cos(theta) - 110.0 * cos(2.0 * theta) + 55.0 * cos(3.0 * theta)) * sin(theta/2.0)) / 32.0 - elseif m == 4 - (sqrt(195.0 / (2.0 * π)) * cos(theta/2.0)^6 * (35.0 - 44.0 * cos(theta) + 33.0 * cos(2.0 * theta)) * sin(theta/2.0)^2) / 8.0 - elseif m == 5 - (-sqrt(2145.0 / π) * cos(theta/2.0)^7 * (-1.0 + 3.0 * cos(theta)) * sin(theta/2.0)^3) / 2.0 - elseif m == 6 - (3.0 * sqrt(715.0 / π) * cos(theta/2.0)^8 * sin(theta/2.0)^4) / 2.0 - end - elseif l == 7 - if m == -7 - sqrt(15015.0 / (2.0 * π)) * cos(theta/2.0)^5 * sin(theta/2.0)^9 - elseif m == -6 - (sqrt(2145.0 / π) * cos(theta/2.0)^4 * (2.0 + 7.0 * cos(theta)) * sin(theta/2.0)^8) / 2.0 - elseif m == -5 - (sqrt(165.0 / (2.0 * π)) * cos(theta/2.0)^3 * (93.0 + 104.0 * cos(theta) + 91.0 * cos(2.0 * theta)) * sin(theta/2.0)^7) / 8.0 - elseif m == -4 - (sqrt(165.0 / (2.0 * π)) * cos(theta/2.0)^2 * (140.0 + 285.0 * cos(theta) + 156.0 * cos(2.0 * theta) + 91.0 * cos(3.0 * theta)) * sin(theta/2.0)^6) / 16.0 - elseif m == -3 - (sqrt(15.0 / (2.0 * π)) * cos(theta/2.0) * (3115.0 + 5456.0 * cos(theta) + 4268.0 * cos(2.0 * theta) + 2288.0 * cos(3.0 * theta) + 1001.0 * cos(4.0 * theta)) * sin(theta/2.0)^5) / 128.0 - elseif m == -2 - (sqrt(15.0 / π) * (5220.0 + 9810.0 * cos(theta) + 7920.0 * cos(2.0 * theta) + 5445.0 * cos(3.0 * theta) + 2860.0 * cos(4.0 * theta) + 1001.0 * cos(5.0 * theta)) * sin(theta/2.0)^4) / 512.0 - elseif m == -1 - (3.0 * sqrt(5.0 / (2.0 * π)) * cos(theta/2.0) * (1890.0 + 4130.0 * cos(theta) + 3080.0 * cos(2.0 * theta) + 2805.0 * cos(3.0 * theta) + 1430.0 * cos(4.0 * theta) + 1001.0 * cos(5.0 * theta)) * sin(theta/2.0)^3) / 512.0 - elseif m == 0 - (3.0 * sqrt(35.0 / π) * cos(theta) * (109.0 + 132.0 * cos(2.0 * theta) + 143.0 * cos(4.0 * theta)) * sin(theta)^2) / 512.0 - elseif m == 1 - (3.0 * sqrt(5.0 / (2.0 * π)) * cos(theta/2.0)^3 * (-1890.0 + 4130.0 * cos(theta) - 3080.0 * cos(2.0 * theta) + 2805.0 * cos(3.0 * theta) - 1430.0 * cos(4.0 * theta) + 1001.0 * cos(5.0 * theta)) * sin(theta/2.0)) / 512.0 - elseif m == 2 - (sqrt(15.0 / π) * cos(theta/2.0)^4 * (-5220.0 + 9810.0 * cos(theta) - 7920.0 * cos(2.0 * theta) + 5445.0 * cos(3.0 * theta) - 2860.0 * cos(4.0 * theta) + 1001.0 * cos(5.0 * theta))) / 512.0 - elseif m == 3 - -(sqrt(15.0 / (2.0 * π)) * cos(theta/2.0)^5 * (3115.0 - 5456.0 * cos(theta) + 4268.0 * cos(2.0 * theta) - 2288.0 * cos(3.0 * theta) + 1001.0 * cos(4.0 * theta)) * sin(theta/2.0)) / 128.0 - elseif m == 4 - (sqrt(165.0 / (2.0 * π)) * cos(theta/2.0)^6 * (-140.0 + 285.0 * cos(theta) - 156.0 * cos(2.0 * theta) + 91.0 * cos(3.0 * theta)) * sin(theta/2.0)^2) / 16.0 - elseif m == 5 - -(sqrt(165.0 / (2.0 * π)) * cos(theta/2.0)^7 * (93.0 - 104.0 * cos(theta) + 91.0 * cos(2.0 * theta)) * sin(theta/2.0)^3) / 8.0 - elseif m == 6 - (sqrt(2145.0 / π) * cos(theta/2.0)^8 * (-2.0 + 7.0 * cos(theta)) * sin(theta/2.0)^4) / 2.0 - elseif m == 7 - -(sqrt(15015.0 / (2.0 * π)) * cos(theta/2.0)^9 * sin(theta/2.0)^5) - end - elseif l == 8 - if m == -8 - sqrt(34034.0 / π) * cos(theta/2.0)^6 * sin(theta/2.0)^10 - elseif m == -7 - sqrt(17017.0 / (2.0 * π)) * cos(theta/2.0)^5 * (1.0 + 4.0 * cos(theta)) * sin(theta/2.0)^9 - elseif m == -6 - sqrt(255255.0 / π) * cos(theta/2.0)^4 * (1.0 + 2.0 * cos(theta)) * sin(π/4.0 - theta/2.0) * sin(π/4.0 + theta/2.0) * sin(theta/2.0)^8 - elseif m == -5 - (sqrt(12155.0 / (2.0 * π)) * cos(theta/2.0)^3 * (19.0 + 42.0 * cos(theta) + 21.0 * cos(2.0 * theta) + 14.0 * cos(3.0 * theta)) * sin(theta/2.0)^7) / 8.0 - elseif m == -4 - (sqrt(935.0 / (2.0 * π)) * cos(theta/2.0)^2 * (265.0 + 442.0 * cos(theta) + 364.0 * cos(2.0 * theta) + 182.0 * cos(3.0 * theta) + 91.0 * cos(4.0 * theta)) * sin(theta/2.0)^6) / 32.0 - elseif m == -3 - (sqrt(561.0 / (2.0 * π)) * cos(theta/2.0) * (869.0 + 1660.0 * cos(theta) + 1300.0 * cos(2.0 * theta) + 910.0 * cos(3.0 * theta) + 455.0 * cos(4.0 * theta) + 182.0 * cos(5.0 * theta)) * sin(theta/2.0)^5) / 128.0 - elseif m == -2 - (sqrt(17.0 / π) * (7626.0 + 14454.0 * cos(theta) + 12375.0 * cos(2.0 * theta) + 9295.0 * cos(3.0 * theta) + 6006.0 * cos(4.0 * theta) + 3003.0 * cos(5.0 * theta) + 1001.0 * cos(6.0 * theta)) * sin(theta/2.0)^4) / 512.0 - elseif m == -1 - (sqrt(595.0 / (2.0 * π)) * cos(theta/2.0) * (798.0 + 1386.0 * cos(theta) + 1386.0 * cos(2.0 * theta) + 1001.0 * cos(3.0 * theta) + 858.0 * cos(4.0 * theta) + 429.0 * cos(5.0 * theta) + 286.0 * cos(6.0 * theta)) * sin(theta/2.0)^3) / 512.0 - elseif m == 0 - (3.0 * sqrt(595.0 / π) * (210.0 + 385.0 * cos(2.0 * theta) + 286.0 * cos(4.0 * theta) + 143.0 * cos(6.0 * theta)) * sin(theta)^2) / 4096.0 - elseif m == 1 - (sqrt(595.0 / (2.0 * π)) * cos(theta/2.0)^3 * (798.0 - 1386.0 * cos(theta) + 1386.0 * cos(2.0 * theta) - 1001.0 * cos(3.0 * theta) + 858.0 * cos(4.0 * theta) - 429.0 * cos(5.0 * theta) + 286.0 * cos(6.0 * theta)) * sin(theta/2.0)) / 512.0 - elseif m == 2 - (sqrt(17.0 / π) * cos(theta/2.0)^4 * (7626.0 - 14454.0 * cos(theta) + 12375.0 * cos(2.0 * theta) - 9295.0 * cos(3.0 * theta) + 6006.0 * cos(4.0 * theta) - 3003.0 * cos(5.0 * theta) + 1001.0 * cos(6.0 * theta))) / 512.0 - elseif m == 3 - -(sqrt(561.0 / (2.0 * π)) * cos(theta/2.0)^5 * (-869.0 + 1660.0 * cos(theta) - 1300.0 * cos(2.0 * theta) + 910.0 * cos(3.0 * theta) - 455.0 * cos(4.0 * theta) + 182.0 * cos(5.0 * theta)) * sin(theta/2.0)) / 128.0 - elseif m == 4 - (sqrt(935.0 / (2.0 * π)) * cos(theta/2.0)^6 * (265.0 - 442.0 * cos(theta) + 364.0 * cos(2.0 * theta) - 182.0 * cos(3.0 * theta) + 91.0 * cos(4.0 * theta)) * sin(theta/2.0)^2) / 32.0 - elseif m == 5 - -(sqrt(12155.0 / (2.0 * π)) * cos(theta/2.0)^7 * (-19.0 + 42.0 * cos(theta) - 21.0 * cos(2.0 * theta) + 14.0 * cos(3.0 * theta)) * sin(theta/2.0)^3) / 8.0 - elseif m == 6 - (sqrt(255255.0 / π) * cos(theta/2.0)^8 * (-1.0 + 2.0 * cos(theta)) * sin(theta/2.0)^4) * sin(π/4.0 - theta/2.0) * sin(π/4.0 + theta/2.0); - elseif m == 7 - -(sqrt(17017.0 / (2.0 * π)) * cos(theta/2.0)^9 * (-1.0 + 4.0 * cos(theta)) * sin(theta/2.0)^5) - elseif m == 8 - sqrt(34034.0 / π) * cos(theta/2.0)^10 * sin(theta/2.0)^6 - end - end - # Include complex phase factor - if m ≠ 0 - ans = cis(m*phi) * fac - else - ans = fac - end - end - - function XLALJacobiPolynomial( - n::Int, # degree - alpha::Int, # alpha parameter - beta::Int, # beta parameter - x::Float64 # argument - ) - f1 = (x-1.0)/2.0 - f2 = (x+1.0)/2.0 - sum = 0.0 - if n == 0 - return 1.0 - end - for s = 0:n - val = 1.0 - val *= binomial(n+alpha, s) - val *= binomial(n+beta, n-s) - if n-s != 0 - val *= f1^(n-s) - end - if s != 0 - val *= f2^s - end - sum += val - end - return sum - end - - function XLALWignerdMatrix( - l::Int, # mode number l - mp::Int, # mode number m' - m::Int, # mode number m - beta::Float64 # euler angle (rad) - ) - k = min(l+m, min(l-m, min(l+mp, l-mp))) - a = 0 - lam = 0 - if k == l+m - a = mp-m - lam = mp-m - elseif k == l-m - a = m-mp - lam = 0 - elseif k == l+mp - a = m-mp - lam = 0 - elseif k == l-mp - a = mp-m - lam = mp-m - end - b = 2*l-2*k-a - pref = (-1)^lam * sqrt(binomial(2*l-k, k+a)) / sqrt(binomial(k+b, b)) - return pref * sin(beta/2.0)^a * cos(beta/2.0)^b * XLALJacobiPolynomial(k, a, b, cos(beta)) - end - - function XLALWignerDMatrix( - l::Int, # mode number l - mp::Int, # mode number m' - m::Int, # mode number m - alpha::Float64, # euler angle (rad) - beta::Float64, # euler angle (rad) - gam::Float64 # euler angle (rad) - ) - return cis(-1im*mp*alpha) * XLALWignerdMatrix(l, mp, m, beta) * cis(-1im*m*gam) - end - -end # module LAL diff --git a/test/wigner_matrices/sYlm.jl b/test/wigner_matrices/sYlm.jl index c84ee493..5274fba4 100644 --- a/test/wigner_matrices/sYlm.jl +++ b/test/wigner_matrices/sYlm.jl @@ -2,7 +2,7 @@ @test maximum(abs, sYlm_values(0.0, 0.0, 3, -2)) > 0 end -@testitem "Compare to LAL expressions" setup=[LAL,Utilities] begin +@testitem "Internal consistency" setup=[Utilities] begin using ProgressMeter using Quaternionic @testset "$T" for T in [Float64] @@ -17,7 +17,7 @@ end @test_throws ErrorException sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) end - @showprogress desc="Compare to LAL expressions ($T)" for spin in [-2] + for spin in [0, -1, -2] for ι in βrange(T) for ϕ in αrange(T) R = from_spherical_coordinates(ι, ϕ) @@ -31,14 +31,6 @@ end i += 1 end end - for ℓ in abs(spin):ℓₘₐₓ - for m in -ℓ:ℓ - sYlm1 = Y[i] - sYlm3 = LAL.XLALSpinWeightedSphericalHarmonic(ι, ϕ, spin, ℓ, m) - @test sYlm1 ≈ sYlm3 atol=tol rtol=tol - i += 1 - end - end end end end From 0fef3dc350683b3fc29b32f9d130a5982f9061d4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 14:27:29 -0500 Subject: [PATCH 131/329] Debug windows garbage --- docs/literate_input/conventions_comparisons/lalsuite_2025.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index b7bac149..70d862ca 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -137,6 +137,7 @@ replacements = ( for (pattern, replacement) in replacements global lalsource = replace(lalsource, pattern => replacement) end +println.(lalsource); @debug "Remember to remove this line" #+ # Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are @@ -152,7 +153,7 @@ end # module LALSuite # package. We will need to test approximate floating-point equality, so we set absolute and # relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 100eps() -ϵᵣ = 1000eps() +ϵᵣ = 100eps() #+ # The spin-weighted spherical harmonics are defined explicitly, but only for From 1acc94ad4a670a2c4db015841775db462468bea3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 16:11:32 -0500 Subject: [PATCH 132/329] Deal with windows line endings --- .../conventions_comparisons/lalsuite_2025.jl | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index 70d862ca..ab8bf446 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -66,15 +66,15 @@ lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) #+ # Now we define a series of replacements to apply to the C code to convert it to Julia code. -# Note that many of these will be quite specific to this particular file, and may not be -# generally applicable. +# Note that some of these will be quite specific to this particular file, and may not be +# generally applicable. Also, to work on Windows, we need to use `\r?\n` to match newlines. replacements = ( ## Deal with newlines in the middle of an assignment - r"( = .*[^;]\s*)\n" => s"\1", + r"( = .*[^;]\s*)\r?\n" => s"\1", ## Remove a couple old, unused functions - r"(?ms)XLALScalarSphericalHarmonic.*?\n}" => "# Removed", - r"(?ms)XLALSphHarm.*?\n}" => "# Removed", + r"(?ms)XLALScalarSphericalHarmonic.*?\r?\n}" => "# Removed", + r"(?ms)XLALSphHarm.*?\r?\n}" => "# Removed", ## Remove type annotations r"COMPLEX16 ?" => "", @@ -89,7 +89,7 @@ replacements = ( ## Brackets r" ?{" => "", - r"}.*(\n *else)" => s"\1", + r"}.*(\r?\n *else)" => s"\1", r"} *else" => "else", r"^}" => "", "}" => "end", @@ -98,23 +98,23 @@ replacements = ( r"( *if.*);"=>s"\1 end\n", ## one-line `if` statements "for( s=0; n-s >= 0; s++ )" => "for s=0:n", "else if" => "elseif", - r"(?m) break;\n *\n *case(.*?):" => s"elseif m == \1", - r"(?m) break;\n\s*case(.*?):" => s"elseif m == \1", - r"(?m) break;\n *\n *default:" => "else", - r"(?m) break;\n *default:" => "else", - r"(?m)switch.*?\n *\n( *)case(.*?):" => s"\n\1if m == \2", - r"\n *break;" => "", - r"(?m)(else\n *ans = fac;)(\n *return ans;)" => s"\1\n end\2", + r"(?m) break;\r?\n *\r?\n *case(.*?):" => s"elseif m == \1", + r"(?m) break;\r?\n\s*case(.*?):" => s"elseif m == \1", + r"(?m) break;\r?\n *\r?\n *default:" => "else", + r"(?m) break;\r?\n *default:" => "else", + r"(?m)switch.*?\r?\n *\r?\n( *)case(.*?):" => s"\n\1if m == \2", + r"\r?\n *break;" => "", + r"(?m)(else\r?\n *ans = fac;)(\r?\n *return ans;)" => s"\1\n end\2", ## Deal with ugly C declarations "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", "sum=0, val=0" => "sum=0; val=0", "a=0, lam=0" => "a=0; lam=0", - r"\n *fac;" => "", - r"\n *ans;" => "", - r"\n *gslStatus;" => "", - r"\n *gsl_sf_result pLm;" => "", - r"\n ?XLAL" => "\nfunction XLAL", + r"\r?\n *fac;" => "", + r"\r?\n *ans;" => "", + r"\r?\n *gslStatus;" => "", + r"\r?\n *gsl_sf_result pLm;" => "", + r"\r?\n ?XLAL" => "\nfunction XLAL", ## Differences in Julia syntax "++" => "+=1", @@ -137,7 +137,6 @@ replacements = ( for (pattern, replacement) in replacements global lalsource = replace(lalsource, pattern => replacement) end -println.(lalsource); @debug "Remember to remove this line" #+ # Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are From c461aab96ee36931cf92177c2b7fee12c6302bd6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 17:04:54 -0500 Subject: [PATCH 133/329] Debug windows garbage --- docs/literate_input/conventions_comparisons/lalsuite_2025.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index ab8bf446..a145bc82 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -137,6 +137,7 @@ replacements = ( for (pattern, replacement) in replacements global lalsource = replace(lalsource, pattern => replacement) end +println.(lalsource); @debug "Remember to remove this line" #+ # Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are From 870d6cb505e7e27bf6d9948e74128b799d4b4de1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 17:37:23 -0500 Subject: [PATCH 134/329] Fix one-line `if` statements --- .../conventions_comparisons/lalsuite_2025.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index a145bc82..8e8a6c91 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -95,16 +95,16 @@ replacements = ( "}" => "end", ## Flow control - r"( *if.*);"=>s"\1 end\n", ## one-line `if` statements + r"( *if.*);(\r?\n)"=>s"\1 end\2", ## one-line `if` statements "for( s=0; n-s >= 0; s++ )" => "for s=0:n", "else if" => "elseif", r"(?m) break;\r?\n *\r?\n *case(.*?):" => s"elseif m == \1", r"(?m) break;\r?\n\s*case(.*?):" => s"elseif m == \1", r"(?m) break;\r?\n *\r?\n *default:" => "else", r"(?m) break;\r?\n *default:" => "else", - r"(?m)switch.*?\r?\n *\r?\n( *)case(.*?):" => s"\n\1if m == \2", + r"(?m)switch.*?\r?\n *(\r?\n)( *)case(.*?):" => s"\1\2if m == \3", r"\r?\n *break;" => "", - r"(?m)(else\r?\n *ans = fac;)(\r?\n *return ans;)" => s"\1\n end\2", + r"(?m)(else\r?\n *ans = fac;)(\r?\n)( *return ans;)" => s"\1\2 end\2\3", ## Deal with ugly C declarations "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", @@ -114,7 +114,7 @@ replacements = ( r"\r?\n *ans;" => "", r"\r?\n *gslStatus;" => "", r"\r?\n *gsl_sf_result pLm;" => "", - r"\r?\n ?XLAL" => "\nfunction XLAL", + r"(\r?\n) ?XLAL" => s"\1function XLAL", ## Differences in Julia syntax "++" => "+=1", From e258816f946038bf866493e40856daaaa78dd50c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 18:46:15 -0500 Subject: [PATCH 135/329] Deal with windows line endings --- docs/literate_input/conventions_comparisons/lalsuite_2025.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index 8e8a6c91..7dc1078d 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -104,7 +104,7 @@ replacements = ( r"(?m) break;\r?\n *default:" => "else", r"(?m)switch.*?\r?\n *(\r?\n)( *)case(.*?):" => s"\1\2if m == \3", r"\r?\n *break;" => "", - r"(?m)(else\r?\n *ans = fac;)(\r?\n)( *return ans;)" => s"\1\2 end\2\3", + r"(?m)( *ans = fac;)(\r?\n)" => s"\1\2 end\2", ## Deal with ugly C declarations "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", From 0053176f77660bbfce29c609efe803297d7f32c9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 19:12:47 -0500 Subject: [PATCH 136/329] Add gitattributes file to preserve line endings in the C code file --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..79ce8f35 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c text eol=lf \ No newline at end of file From 04ec7d141e4ec89cea9aa1d18e8959eedabca8fa Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 19:27:58 -0500 Subject: [PATCH 137/329] Revert all the windows nonsense now that git will be sensible --- .../conventions_comparisons/lalsuite_2025.jl | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index 7dc1078d..e7033ec4 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -67,14 +67,14 @@ lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) # Now we define a series of replacements to apply to the C code to convert it to Julia code. # Note that some of these will be quite specific to this particular file, and may not be -# generally applicable. Also, to work on Windows, we need to use `\r?\n` to match newlines. +# generally applicable. replacements = ( ## Deal with newlines in the middle of an assignment - r"( = .*[^;]\s*)\r?\n" => s"\1", + r"( = .*[^;]\s*)\n" => s"\1", ## Remove a couple old, unused functions - r"(?ms)XLALScalarSphericalHarmonic.*?\r?\n}" => "# Removed", - r"(?ms)XLALSphHarm.*?\r?\n}" => "# Removed", + r"(?ms)XLALScalarSphericalHarmonic.*?\n}" => "# Removed", + r"(?ms)XLALSphHarm.*?\n}" => "# Removed", ## Remove type annotations r"COMPLEX16 ?" => "", @@ -89,32 +89,32 @@ replacements = ( ## Brackets r" ?{" => "", - r"}.*(\r?\n *else)" => s"\1", + r"}.*(\n *else)" => s"\1", r"} *else" => "else", r"^}" => "", "}" => "end", ## Flow control - r"( *if.*);(\r?\n)"=>s"\1 end\2", ## one-line `if` statements + r"( *if.*);"=>s"\1 end", ## one-line `if` statements "for( s=0; n-s >= 0; s++ )" => "for s=0:n", "else if" => "elseif", - r"(?m) break;\r?\n *\r?\n *case(.*?):" => s"elseif m == \1", - r"(?m) break;\r?\n\s*case(.*?):" => s"elseif m == \1", - r"(?m) break;\r?\n *\r?\n *default:" => "else", - r"(?m) break;\r?\n *default:" => "else", - r"(?m)switch.*?\r?\n *(\r?\n)( *)case(.*?):" => s"\1\2if m == \3", - r"\r?\n *break;" => "", - r"(?m)( *ans = fac;)(\r?\n)" => s"\1\2 end\2", + r"(?m) break;\n *\n *case(.*?):" => s"elseif m == \1", + r"(?m) break;\n\s*case(.*?):" => s"elseif m == \1", + r"(?m) break;\n *\n *default:" => "else", + r"(?m) break;\n *default:" => "else", + r"(?m)switch.*?\n *\n( *)case(.*?):" => s"\n\1if m == \2", + r"\n *break;" => "", + r"(?m)( *ans = fac;)\n" => s"\1\n end\n", ## Deal with ugly C declarations "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", "sum=0, val=0" => "sum=0; val=0", "a=0, lam=0" => "a=0; lam=0", - r"\r?\n *fac;" => "", - r"\r?\n *ans;" => "", - r"\r?\n *gslStatus;" => "", - r"\r?\n *gsl_sf_result pLm;" => "", - r"(\r?\n) ?XLAL" => s"\1function XLAL", + r"\n *fac;" => "", + r"\n *ans;" => "", + r"\n *gslStatus;" => "", + r"\n *gsl_sf_result pLm;" => "", + r"\n ?XLAL" => "\nfunction XLAL", ## Differences in Julia syntax "++" => "+=1", @@ -137,7 +137,6 @@ replacements = ( for (pattern, replacement) in replacements global lalsource = replace(lalsource, pattern => replacement) end -println.(lalsource); @debug "Remember to remove this line" #+ # Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are From b1c078dd38bb26453322f1b262b77d9f73375f60 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 00:55:31 -0500 Subject: [PATCH 138/329] Note the source of the d/D matrices --- .../conventions_comparisons/lalsuite_2025.jl | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index e7033ec4..b0cb2523 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -1,9 +1,8 @@ md""" # LALSuite (2025) -!!! info "Summary" - The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` - functions agree with the definitions used in the `SphericalFunctions` package. +!!! info "Summary" The LALSuite definitions of the spherical harmonics and Wigner's ``d`` + and ``D`` functions agree with the definitions used in the `SphericalFunctions` package. [LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software routines, comprising the primary official software used by the LIGO-Virgo-KAGRA @@ -17,6 +16,13 @@ version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` ins 2, but the citation was not updated. Nonetheless, it appears that the actual code is consistent with the *corrected* versions of the NINJA paper. +They also (quite separately) define Wigner's ``D`` matrices in terms of the ``d`` matrices, +which are — in turn — defined in terms of Jacobi polynomials. For all of these, they cite +Wikipedia (despite the fact that the NINJA paper defined the spin-weighted spherical +harmonics in terms of the ``d`` matrices). Nonetheless, the definitions in the code are +consistent with the definitions in the NINJA paper, which are consistent with the +definitions in the `SphericalFunctions` package. + ## Implementing formulas @@ -29,10 +35,10 @@ double XLALWignerdMatrix( int l, int mp, int m, double beta ); COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ); ``` -The source code is stored alongside this file, so we will read it in to a `String` and then -apply a series of regular expressions to convert it to Julia code, parse it and evaluate it -to turn it into runnable Julia. We encapsulate the formulas in a module so that we can test -them against the `SphericalFunctions` package. +The original source code (as of early 2025) is stored alongside this file, so we will read +it in to a `String` and then apply a series of regular expressions to convert it to Julia +code, parse it and evaluate it to turn it into runnable Julia. We encapsulate the formulas +in a module so that we can test them against the `SphericalFunctions` package. We begin by setting up that module, and introducing a set of basic replacements that would usually be defined in separate C headers. @@ -148,9 +154,9 @@ end # module LALSuite # ## Tests # -# We can now test the functions against the equivalent functions from the SphericalFunctions -# package. We will need to test approximate floating-point equality, so we set absolute and -# relative tolerances (respectively) in terms of the machine epsilon: +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 100eps() ϵᵣ = 100eps() #+ From d98f107b0d65205be03b75c7d0d533d2baefa187 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 00:55:39 -0500 Subject: [PATCH 139/329] Formatting --- .../condon_shortley_1935.jl | 13 ++++++------ .../conventions_comparisons/ninja_2011.jl | 20 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index b614cfd1..cec5f50f 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -45,12 +45,13 @@ We can infer that the definitions of the spherical coordinates are consistent wi The result is that the original Condon-Shortley spherical harmonics agree perfectly with the ones computed by this package. -(Condon and Shortley do not give an expression for the Wigner D-matrices.) +Condon and Shortley do not give an expression for the Wigner D-matrices. + ## Implementing formulas We begin by writing code that implements the formulas from Condon-Shortley. We encapsulate -the formulas in a module so that we can test them against the SphericalFunctions package. +the formulas in a module so that we can test them against the `SphericalFunctions` package. """ using TestItems: @testitem #hide @@ -143,9 +144,9 @@ end # module CondonShortley # ## Tests # -# We can now test the functions against the equivalent functions from the SphericalFunctions -# package. We will need to test approximate floating-point equality, so we set absolute and -# relative tolerances (respectively) in terms of the machine epsilon: +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 100eps() ϵᵣ = 1000eps() #+ @@ -166,7 +167,7 @@ end #+ # Finally, we can test Condon-Shortley's full expressions for spherical harmonics against -# the SphericalFunctions package. We will only test up to +# the `SphericalFunctions` package. We will only test up to ℓₘₐₓ = 4 #+ # because the formulas are very slow, and this will be sufficient to sort out any sign or diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl index dd807c55..7c518cb8 100644 --- a/docs/literate_input/conventions_comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -1,9 +1,9 @@ md""" # NINJA (2011) -!!! info "Summary" - The NINJA collaboration's definitions of the spherical harmonics and Wigner's ``d`` - functions agree with the definitions used in the `SphericalFunctions` package. +!!! info "Summary" The NINJA collaboration's definitions of the spherical harmonics and + Wigner's ``d`` functions agree with the definitions used in the `SphericalFunctions` + package. Motivated by the need for a shared set of conventions in the NINJA project, a broad cross-section of researchers involved in modeling gravitational waves (including the author @@ -22,8 +22,8 @@ is denoted ``\iota``: ## Implementing formulas We begin by writing code that implements the formulas from Ref. [AjithEtAl_2011](@cite). We -encapsulate the formulas in a module so that we can test them against the SphericalFunctions -package. +encapsulate the formulas in a module so that we can test them against the +`SphericalFunctions` package. """ using TestItems: @testitem #hide @@ -104,9 +104,9 @@ end # module NINJA # ## Tests # -# We can now test the functions against the equivalent functions from the SphericalFunctions -# package. We will need to test approximate floating-point equality, so we set absolute and -# relative tolerances (respectively) in terms of the machine epsilon: +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 10eps() ϵᵣ = 10eps() #+ @@ -121,7 +121,7 @@ for (ι, ϕ) ∈ θϕrange(Float64, 1) end #+ -# Next, we compare the general formulas to the SphericalFunctions package. +# Next, we compare the general formulas to the `SphericalFunctions` package. # We will only test up to ℓₘₐₓ = 4 #+ @@ -137,7 +137,7 @@ for (θ, ϕ) ∈ θϕrange() end #+ -# Finally, we compare the Wigner ``d`` matrix to the SphericalFunctions package. +# Finally, we compare the Wigner ``d`` matrix to the `SphericalFunctions` package. for ι ∈ θrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) @test NINJA.d(ℓ, m′, m, ι) ≈ SphericalFunctions.d(ℓ, m′, m, ι) atol=ϵₐ rtol=ϵᵣ From 1ce686565beeeac4c372dfe0b364f76d94b06565 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 00:56:15 -0500 Subject: [PATCH 140/329] Move Cohen-Tannoudji to Literate TestItems --- .../cohen_tannoudji_1991.jl | 131 ++++++++++++++++++ docs/src/conventions/comparisons.md | 53 +------ 2 files changed, 132 insertions(+), 52 deletions(-) create mode 100644 docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl diff --git a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl new file mode 100644 index 00000000..0c398284 --- /dev/null +++ b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl @@ -0,0 +1,131 @@ +md""" +# Cohen-Tannoudji (1991) + +!!! info "Summary" + Cohen-Tannoudji's definition of the spherical harmonics agrees with the definition used + in the `SphericalFunctions` package. + + TODO: Compare angular-momentum operators and rotation operator. +""" + +md""" +[CohenTannoudji_1991](@citet), by a Nobel-prize winner and collaborators, is an extensive +two-volume set on quantum mechanics that is widely used in graduate courses. + +They define spherical coordinates in the usual (physicist's) way in Chapter VI. They then +compute the angular-momentum operators as [Eqs. (D-5)] +```math +\begin{aligned} +L_x &= i \hbar \left( + \sin\phi \frac{\partial} {\partial \theta} + + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} +\right), +\\ +L_y &= i \hbar \left( + -\cos\phi \frac{\partial} {\partial \theta} + + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} +\right), +\\ +L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. +\end{aligned} +``` +In Complement ``\mathrm{B}_{\mathrm{VI}}`` they define a rotation operator ``R`` as acting +on a state such that [Eq. (21)] +```math +\langle \mathbf{r} | R | \psi \rangle += +\langle \mathscr{R}^{-1} \mathbf{r} | \psi \rangle. +``` +For an infinitesimal rotation through angle ``d\alpha`` about the axis ``\mathbf{u}``, he +shows [Eq. (49)] +```math +R_{\mathbf{u}}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha \mathbf{L}.\mathbf{u}. +``` + + +## Implementing formulas + +We begin by writing code that implements the formulas from Cohen-Tannoudji. We encapsulate +the formulas in a module so that we can test them against the `SphericalFunctions` package. +""" + +using TestItems: @testitem #hide +@testitem "Cohen-Tannoudji conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide + +module CohenTannoudji + +import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ +#+ + +# They derive the spherical harmonics in two ways and gets two different, but equivalent, +# expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) +# ```math +# Y_{l}^{m}(\theta, \phi) +# = +# \frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l+m)!}{(l-m)!}} +# e^{i m \phi} (\sin \theta)^{-m} +# \frac{d^{l-m}}{d(\cos \theta)^{l-m}} (\sin \theta)^{2l}, +# ``` +function Y₁(l, m, θ::T, ϕ::T) where {T<:Real} + ( + (-1)^l / (2^l * (l)❗) + * √((2l + 1) / (4T(π)) * (l + m)❗ / (l - m)❗) + * exp(𝒾 * m * ϕ) * sin(θ)^(-m) + * dʲsin²ᵏθdcosθʲ(j=l-m, k=l, θ=θ) + ) +end +#+ + +# while the second is Eq. (30) +# ```math +# Y_{l}^{m}(\theta, \phi) +# = +# \frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l-m)!}{(l+m)!}} +# e^{i m \phi} (\sin \theta)^m +# \frac{d^{l+m}}{d(\cos \theta)^{l+m}} (\sin \theta)^{2l}. +# ``` +function Y₂(l, m, θ::T, ϕ::T) where {T<:Real} + ( + (-1)^(l+m) / (2^l * (l)❗) + * √((2l + 1) / (4T(π)) * (l - m)❗ / (l + m)❗) + * exp(𝒾 * m * ϕ) * sin(θ)^m + * dʲsin²ᵏθdcosθʲ(j=l+m, k=l, θ=θ) + ) +end + +# Cohen-Tannoudji do not give an expression for the Wigner D-matrices, but the comparisons +# of the definitions of the angular-momentum operators and the rotation operator are also +# useful for comparison, and comparing the spherical harmonics is also important. + +end # module CohenTannoudji +#+ + +# ## Tests +# +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 100eps() +ϵᵣ = 1000eps() +#+ + +# We will only test up to +ℓₘₐₓ = 2 +#+ +# +# because the formulas are very slow, and this will be sufficient to sort out any sign or +# normalization differences, which are the most likely source of error. Also, the formulas +# are singular at the poles, so we avoid evaluating there. +for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) + for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) + @test CohenTannoudji.Y₁(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test CohenTannoudji.Y₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# This successful test shows that both versions of the spherical harmonics given by +# Cohen-Tannoudji agree with the spherical harmonics defined by the `SphericalFunctions` +# package. + +end #hide diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 72bad5ea..1bd47180 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -56,58 +56,7 @@ for which I have my own strong opinions. ## Cohen-Tannoudji (1991) -[CohenTannoudji_1991](@citet) define spherical coordinates in the -usual (physicist's) way in Chapter VI. They then compute the -angular-momentum operators as [Eqs. (D-5)] -```math -\begin{aligned} -L_x &= i \hbar \left( - \sin\phi \frac{\partial} {\partial \theta} - + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} -\right), -\\ -L_y &= i \hbar \left( - -\cos\phi \frac{\partial} {\partial \theta} - + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} -\right), -\\ -L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. -\end{aligned} -``` - -They derives the spherical harmonics in two ways and gets two different, -but equivalent, expressions in Complement -``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) -```math -Y_{l}^{m}(\theta, \phi) -= -\frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l+m)!}{(l-m)!}} -e^{i m \phi} (\sin \theta)^{-m} -\frac{d^{l-m}}{d(\cos \theta)^{l-m}} (\sin \theta)^{2l}, -``` -while the second is Eq. (30) -```math -Y_{l}^{m}(\theta, \phi) -= -\frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l-m)!}{(l+m)!}} -e^{i m \phi} (\sin \theta)^m -\frac{d^{l+m}}{d(\cos \theta)^{l+m}} (\sin \theta)^{2l}. -``` - -In Complement ``\mathrm{B}_{\mathrm{VI}}`` they define a rotation -operator ``R`` as acting on a state such that [Eq. (21)] -```math -\langle \mathbf{r} | R | \psi \rangle -= -\langle \mathscr{R}^{-1} \mathbf{r} | \psi \rangle. -``` -For an infinitesimal rotation through angle ``d\alpha`` about the axis -``\mathbf{u}``, he shows [Eq. (49)] -```math -R_{\mathbf{u}}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha \mathbf{L}.\mathbf{u}. -``` - -They do not appear to define the Wigner D-matrices. +(moved) ## Condon-Shortley (1935) From 0f409f4d3eb89f004c9419a2fc7ba3f6ac69cdef Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 08:00:03 -0500 Subject: [PATCH 141/329] Fix missing continuation --- .../conventions_comparisons/cohen_tannoudji_1991.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl index 0c398284..1527a816 100644 --- a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl @@ -92,6 +92,7 @@ function Y₂(l, m, θ::T, ϕ::T) where {T<:Real} * dʲsin²ᵏθdcosθʲ(j=l+m, k=l, θ=θ) ) end +#+ # Cohen-Tannoudji do not give an expression for the Wigner D-matrices, but the comparisons # of the definitions of the angular-momentum operators and the rotation operator are also From e9c31acd134f21a78c91c99222b4d0b6c94b1a79 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 09:02:45 -0500 Subject: [PATCH 142/329] Include Cohen-Tannoudji in docs --- .../conventions_comparisons/cohen_tannoudji_1991.jl | 4 +++- docs/make.jl | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl index 1527a816..e48da659 100644 --- a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl @@ -53,11 +53,13 @@ using TestItems: @testitem #hide @testitem "Cohen-Tannoudji conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide module CohenTannoudji +#+ +# We'll also use some predefined utilities to make the code look more like the equations. import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ #+ -# They derive the spherical harmonics in two ways and gets two different, but equivalent, +# They derive the spherical harmonics in two ways and get two different, but equivalent, # expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) # ```math # Y_{l}^{m}(\theta, \phi) diff --git a/docs/make.jl b/docs/make.jl index a4c93c25..238652fe 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -74,6 +74,7 @@ makedocs( "conventions/details.md", "conventions/comparisons.md", "Comparisons" => [ + joinpath(relative_convention_comparisons, "cohen_tannoudji_1991.md"), joinpath(relative_convention_comparisons, "condon_shortley_1935.md"), joinpath(relative_convention_comparisons, "lalsuite_2025.md"), joinpath(relative_convention_comparisons, "ninja_2011.md"), From 817e978b0b9616b6f0ad41edbc5a89a9f1d72229 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 09:03:03 -0500 Subject: [PATCH 143/329] Tweak explanation of code --- .../conventions_comparisons/condon_shortley_1935.jl | 2 ++ docs/literate_input/conventions_comparisons/ninja_2011.jl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index cec5f50f..38f5b7af 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -58,7 +58,9 @@ using TestItems: @testitem #hide @testitem "Condon-Shortley conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide module CondonShortley +#+ +# We'll also use some predefined utilities to make the code look more like the equations. import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ #+ diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl index 7c518cb8..03af3c52 100644 --- a/docs/literate_input/conventions_comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -30,7 +30,9 @@ using TestItems: @testitem #hide @testitem "NINJA conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide module NINJA +#+ +# We'll also use some predefined utilities to make the code look more like the equations. import ..ConventionsUtilities: 𝒾, ❗ #+ From de50a2b09bbd06fb9fcabf0d6910e761a5e47bf1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 10:05:20 -0500 Subject: [PATCH 144/329] Explicitly state what the Condon-Shortley phase convention is --- .../condon_shortley_1935.jl | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index 38f5b7af..4e5a68a8 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -8,15 +8,27 @@ md""" [Condon and Shortley's "The Theory Of Atomic Spectra"](@cite CondonShortley_1935) is the standard reference for the "Condon-Shortley phase convention". Though some references are not very clear about precisely what they mean by that phrase, it seems clear that the -original meaning included the idea that the angular-momentum raising and lowering operators -have eigenvalues that are *real and positive* when acting on the spherical harmonics. To -avoid ambiguity, we can just look at the actual spherical harmonics they define. +original meaning revolved around the idea that the angular-momentum raising and lowering +operators have eigenvalues that are *real and positive* when acting on the spherical +harmonics. Specifically, they discuss the phase ambiguity of the eigenfunction ``\psi`` — +which includes spherical harmonics indexed by ``j`` and ``m`` for the angular part — in +section 3³ (page 48). This culminates in Eq. (3) of that section, which is as explicit as +they get: +```math +\left( J_x \pm i J_y \right) \psi(\gamma j m) += +\hbar \sqrt{(j \mp m)(j \pm m + 1)} \psi(\gamma j m \pm 1). +``` +This eliminates any *relative* phase ambiguity between modes with neighboring ``m`` values, +and specifically determines what factors of ``(-1)^m`` should be included in the definition +of the spherical harmonics. -The method we use here is as direct and explicit as possible. In particular, Condon and -Shortley provide a formula for the φ=0 part in terms of iterated derivatives of a power of -sin(θ). Rather than expressing these derivatives in terms of the Legendre polynomials — -which would subject us to another round of ambiguity — the functions in this module use -automatic differentiation to compute the derivatives explicitly. +To avoid re-introducing ambiguity, we can just look at the actual spherical harmonics they +define. The method we use here is as direct and explicit as possible. In particular, +Condon and Shortley provide a formula for the φ=0 part in terms of iterated derivatives of a +power of sin(θ). Rather than expressing these derivatives in terms of the Legendre +polynomials — which would subject us to another round of ambiguity — the functions in this +module use automatic differentiation to compute the derivatives explicitly. Condon and Shortley are not very explicit about the meaning of the spherical coordinates, but they do describe them as "spherical polar coordinates ``r, \theta, \varphi``". @@ -42,9 +54,6 @@ L_x - i L_y &= \hbar e^{-i\varphi} \left( which also agrees with [our results.](@ref "``L_{\pm}`` operators in spherical coordinates") We can infer that the definitions of the spherical coordinates are consistent with ours. -The result is that the original Condon-Shortley spherical harmonics agree perfectly with the -ones computed by this package. - Condon and Shortley do not give an expression for the Wigner D-matrices. @@ -162,7 +171,7 @@ end # module CondonShortley # ``1/\sin\theta`` factor in the general form will cause problems at the poles, so we avoid # the poles by using `βrange` with a small offset: for θ ∈ θrange(; avoid_poles=ϵₐ/10) - for (ℓ, m) ∈ eachrow(SphericalFunctions.Yrange(ℓₘₐₓ)) + for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) @test CondonShortley.ϴ(ℓ, m, θ) ≈ CondonShortley.Θ(ℓ, m, θ) atol=ϵₐ rtol=ϵᵣ end end From 5947472dfdce4434ca4da15c9a6f960b02fc28c1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 10:21:32 -0500 Subject: [PATCH 145/329] Remind myself to verify the ang.-mom. ops. --- .../conventions_comparisons/condon_shortley_1935.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl index 4e5a68a8..d978d520 100644 --- a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl @@ -5,6 +5,8 @@ md""" Condon and Shortley's definition of the spherical harmonics agrees with the definition used in the `SphericalFunctions` package. + TODO: Compare angular-momentum operators. + [Condon and Shortley's "The Theory Of Atomic Spectra"](@cite CondonShortley_1935) is the standard reference for the "Condon-Shortley phase convention". Though some references are not very clear about precisely what they mean by that phrase, it seems clear that the From c8b34e681cbacdee0c4e86d21a520ab3246925f1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 11:16:48 -0500 Subject: [PATCH 146/329] Include the LAL source code on a syntax-highlighted page of its own. --- .../conventions_comparisons/lalsuite_2025.jl | 9 +++-- docs/make.jl | 38 +++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index b0cb2523..cc673150 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -35,10 +35,11 @@ double XLALWignerdMatrix( int l, int mp, int m, double beta ); COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ); ``` -The original source code (as of early 2025) is stored alongside this file, so we will read -it in to a `String` and then apply a series of regular expressions to convert it to Julia -code, parse it and evaluate it to turn it into runnable Julia. We encapsulate the formulas -in a module so that we can test them against the `SphericalFunctions` package. +The [original source code](./lalsuite_SphericalHarmonics.md) (as of early 2025) is stored +alongside this file, so we will read it in to a `String` and then apply a series of regular +expressions to convert it to Julia code, parse it and evaluate it to turn it into runnable +Julia. We encapsulate the formulas in a module so that we can test them against the +`SphericalFunctions` package. We begin by setting up that module, and introducing a set of basic replacements that would usually be defined in separate C headers. diff --git a/docs/make.jl b/docs/make.jl index 238652fe..6faa4e4d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -19,13 +19,18 @@ docs_src_dir = joinpath(@__DIR__, "src") # See LiveServer.jl docs for this: https://juliadocs.org/LiveServer.jl/dev/man/ls+lit/ literate_input = joinpath(@__DIR__, "literate_input") literate_output = joinpath(docs_src_dir, "literate_output") +relative_literate_output = relpath(literate_output, docs_src_dir) +relative_convention_comparisons = joinpath(relative_literate_output, "conventions_comparisons") rm(literate_output; force=true, recursive=true) +skip_files = ( # Non-.jl files will be skipped anyway + "ConventionsUtilities.jl", + "ConventionsSetup.jl", +) for (root, _, files) ∈ walkdir(literate_input), file ∈ files - # ignore non julia files - splitext(file)[2] == ".jl" || continue - # If the file is "ConventionsUtilities.jl" or "ConventionsSetup.jl", skip it - file == "ConventionsUtilities.jl" && continue - file == "ConventionsSetup.jl" && continue + # Skip some files + if splitext(file)[2] != ".jl" || file ∈ skip_files + continue + end # full path to a literate script input_path = joinpath(root, file) # generated output path @@ -33,12 +38,23 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files # generate the markdown file calling Literate Literate.markdown(input_path, output_path, documenter=true, mdstrings=true) end -cp( - joinpath(literate_input, "conventions_comparisons", "lalsuite_SphericalHarmonics.c"), - joinpath(literate_output, "conventions_comparisons", "lalsuite_SphericalHarmonics.c") -) -relative_literate_output = relpath(literate_output, docs_src_dir) -relative_convention_comparisons = joinpath(relative_literate_output, "conventions_comparisons") + +# Make "lalsuite_SphericalHarmonics.c" available in the docs +let + lalsource = read( + joinpath(literate_input, "conventions_comparisons", "lalsuite_SphericalHarmonics.c"), + String + ) + write( + joinpath(literate_output, "conventions_comparisons", "lalsuite_SphericalHarmonics.md"), + "# LALSuite: Spherical Harmonics original source code\n" + * "The official repository is [here](" + * "https://git.ligo.org/lscsoft/lalsuite/-/blob/22e4cd8fff0487c7b42a2c26772ae9204c995637/lal/lib/utilities/SphericalHarmonics.c" + * ")\n" + * "```c\n$lalsource\n```\n" + ) +end + bib = CitationBibliography( joinpath(docs_src_dir, "references.bib"); From 4001734c030d07f799e67448f66ae13ce229cfde Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 11:34:59 -0500 Subject: [PATCH 147/329] Remind myself what to do for Wikipedia --- docs/src/conventions/comparisons.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 1bd47180..39835e8b 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -798,11 +798,19 @@ different. ## Wikipedia +Euler angles + +Angular-momentum operators + +Spherical harmonics + +Spin-weighted spherical harmonics + Defining the operator ```math \mathcal{R}(\alpha,\beta,\gamma) = e^{-i\alpha J_z}e^{-i\beta J_y}e^{-i\gamma J_z}, ``` -[Wikipedia defines the Wigner D-matrix](https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix) as +[Wikipedia expresses the Wigner D-matrix](https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix) as ```math D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | \mathcal{R}(\alpha,\beta,\gamma)| jm \rangle =e^{-im'\alpha } d^j_{m'm}(\beta)e^{-i m\gamma}. ``` From a780374894cada5a63f0d8b4a025919d78af7f40 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 11:54:40 -0500 Subject: [PATCH 148/329] Fix admonition formatting --- .../conventions_comparisons/cohen_tannoudji_1991.jl | 2 -- .../literate_input/conventions_comparisons/lalsuite_2025.jl | 5 +++-- docs/literate_input/conventions_comparisons/ninja_2011.jl | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl index e48da659..e928c42d 100644 --- a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl @@ -6,9 +6,7 @@ md""" in the `SphericalFunctions` package. TODO: Compare angular-momentum operators and rotation operator. -""" -md""" [CohenTannoudji_1991](@citet), by a Nobel-prize winner and collaborators, is an extensive two-volume set on quantum mechanics that is widely used in graduate courses. diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index cc673150..babc8616 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -1,8 +1,9 @@ md""" # LALSuite (2025) -!!! info "Summary" The LALSuite definitions of the spherical harmonics and Wigner's ``d`` - and ``D`` functions agree with the definitions used in the `SphericalFunctions` package. +!!! info "Summary" + The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` + functions agree with the definitions used in the `SphericalFunctions` package. [LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software routines, comprising the primary official software used by the LIGO-Virgo-KAGRA diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl index 03af3c52..001dfe3d 100644 --- a/docs/literate_input/conventions_comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -1,9 +1,9 @@ md""" # NINJA (2011) -!!! info "Summary" The NINJA collaboration's definitions of the spherical harmonics and - Wigner's ``d`` functions agree with the definitions used in the `SphericalFunctions` - package. +!!! info "Summary" + The NINJA collaboration's definitions of the spherical harmonics and Wigner's ``d`` + functions agree with the definitions used in the `SphericalFunctions` package. Motivated by the need for a shared set of conventions in the NINJA project, a broad cross-section of researchers involved in modeling gravitational waves (including the author From 6c65705a59d9d6f6a43e0e288def5d9fd28f917a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 5 Mar 2025 09:48:05 -0500 Subject: [PATCH 149/329] Ensure that my SVG modifications only apply to the composition diagram --- docs/src/assets/extras.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/assets/extras.css b/docs/src/assets/extras.css index 757819da..21d84c2c 100644 --- a/docs/src/assets/extras.css +++ b/docs/src/assets/extras.css @@ -27,20 +27,20 @@ div .composition-diagram { transform-origin: center; } -svg text.f0 { +.composition-diagram svg text.f0 { fill: currentColor; font-family: 'KaTeX_AMS'; font-size: 9.96264px; /* Adjust as needed */ } -svg text.f1 { +.composition-diagram svg text.f1 { fill: currentColor; font-family: 'KaTeX_Math'; font-style: italic; font-size: 9.96264px; /* Adjust as needed */ } -svg path { +.composition-diagram svg path { stroke: currentColor; fill: none; } \ No newline at end of file From b8f6125da739567921d31360adcdc02ec400fdb1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 5 Mar 2025 10:55:36 -0500 Subject: [PATCH 150/329] Remind myself of future work with TODOs --- docs/src/index.md | 90 +++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index c03f94eb..0ee49a5a 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,46 +1,68 @@ # Introduction -This is a Julia package for evaluating and transforming Wigner's 𝔇 matrices, -and spin-weighted spherical harmonics ``{}_{s}Y_{\ell,m}`` (which includes the -ordinary scalar spherical harmonics). Because [*both* 𝔇 *and* the harmonics -are most correctly considered](@cite Boyle_2016) functions on the rotation group -``𝐒𝐎(3)`` — or more generally, the spin group ``𝐒𝐩𝐢𝐧(3)`` that covers it — -these functions are evaluated directly in terms of quaternions. Concessions are -also made for more standard forms of spherical coordinates and Euler angles.[^1] -Among other applications, those functions permit "synthesis" (evaluation of the -spin-weighted spherical functions) of spin-weighted spherical harmonic -coefficients on regular or distorted grids. This package also includes -functions enabling efficient "analysis" (decomposition into mode coefficients) -of functions evaluated on regular grids to high order and accuracy. - -These quantities are computed using recursion relations, which makes it possible -to compute to very high ℓ values. Unlike direct evaluation of individual -elements, which would generally cause overflow or underflow beyond ℓ≈30 when -using double precision, these recursion relations should be valid for far higher -ℓ values. More precisely, when using *this* package, `Inf` values appear -starting at ℓ=128 for `Float16`, but I have not yet found any for values up to -at least ℓ=1024 with `Float32`, and presumably far higher for `Float64`. -`BigFloat` also works, and presumably will not overflow for any ℓ value that -could reasonably fit into computer memory — though it is far slower. Also note -that [`DoubleFloats`](https://github.com/JuliaMath/DoubleFloats.jl) will work, -and achieve significantly greater accuracy (but no greater ℓ range) than -`Float64`. In all cases, results are typically accurate to roughly ℓ times the -precision of the input quaternion. - -The conventions for this package are mostly inherited from — and are described -in detail by — its predecessors found +1. TODO: Figure out how to define conventions for the operators programmatically. + +2. TODO: Finalize my conventions + +3. TODO: Finish comparisons, using final conventions + +4. TODO: Review front matter; make consistent with new conventions + +8. TODO: Enable both `m′ₘₐₓ` and `mₘₐₓ` limits + +5. TODO: Try to create a simpler interface `D(ℓₘₐₓ, R)` and `D!` that can operate just on that return value (with optional `m′ₘₐₓ, mₘₐₓ`) + +6. TODO: Make return values a special object that can iterate and be indexed, returning `OffsetArray`s of views into the underlying `Vector`. + +7. TODO: Break iterations into more-reusable pieces + + +This is a Julia package for evaluating and transforming Wigner's 𝔇 +matrices, and spin-weighted spherical harmonics ``{}_{s}Y_{\ell,m}`` +(which includes the ordinary scalar spherical harmonics). Because +[*both* 𝔇 *and* the harmonics are most correctly considered](@cite +Boyle_2016) functions on the rotation group ``𝐒𝐎(3)`` — or more +generally, the spin group ``𝐒𝐩𝐢𝐧(3)`` that covers it — these +functions are evaluated directly in terms of quaternions. Concessions +are also made for more standard forms of spherical coordinates and +Euler angles.[^1] Among other applications, those functions permit +"synthesis" (evaluation of the spin-weighted spherical functions) of +spin-weighted spherical harmonic coefficients on regular or distorted +grids. This package also includes functions enabling efficient +"analysis" (decomposition into mode coefficients) of functions +evaluated on regular grids to high order and accuracy. + +These quantities are computed using recursion relations, which makes +it possible to compute to very high ℓ values. Unlike direct +evaluation of individual elements, which would generally cause +overflow or underflow beyond ℓ≈30 when using double precision +(`Float64`), these recursion relations should be valid for far higher +ℓ values. More precisely, when using *this* package, `Inf` values +appear starting at ℓ=128 for `Float16`, but I have not yet found any +for values up to at least ℓ=1024 with `Float32`, and presumably far +higher for `Float64`. `BigFloat` also works, and presumably will not +overflow for any ℓ value that could reasonably fit into computer +memory — though it is far slower. Also note that +[`DoubleFloats`](https://github.com/JuliaMath/DoubleFloats.jl) will +work, and achieve significantly greater accuracy (but no greater ℓ +range) than `Float64`. In all cases, results are typically accurate +to roughly ℓ times the precision of the input quaternion. + +The conventions for this package are mostly inherited from — and are +described in detail by — its predecessors found [here](https://moble.github.io/spherical_functions/) and [here](https://moble.github.io/spherical/). -Note that numerous other packages cover some of these use cases, including +Note that numerous other packages cover some of these use cases, +including [`FastTransforms.jl`](https://JuliaApproximation.github.io/FastTransforms.jl/), [`FastSphericalHarmonics.jl`](https://eschnett.github.io/FastSphericalHarmonics.jl/dev/), [`WignerSymbols.jl`](https://github.com/Jutho/WignerSymbols.jl), and -[`WignerFamilies.jl`](https://github.com/xzackli/WignerFamilies.jl). However, I -need support for quaternions (via +[`WignerFamilies.jl`](https://github.com/xzackli/WignerFamilies.jl). +However, I need support for quaternions (via [`Quaternionic.jl`](https://github.com/moble/Quaternionic.jl)) and for -higher-precision numbers — even at the cost of a very slight decrease in speed -in some cases — which are what this package provides. +higher-precision numbers — even at the cost of a very slight decrease +in speed in some cases — which are what this package provides. [^1]: From 0fadfc2c73a6ab9fd24518104cab49b9b91bfd74 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 5 Mar 2025 15:00:32 -0500 Subject: [PATCH 151/329] Note that we *could* just use lalsuite directly --- docs/literate_input/conventions_comparisons/lalsuite_2025.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl index babc8616..78a0c659 100644 --- a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions_comparisons/lalsuite_2025.jl @@ -1,3 +1,5 @@ +# TODO: `python -m pip install --no-deps lalsuite; python -m pip install numpy` to directly compare without translating source code... except that this doesn't work on Windows + md""" # LALSuite (2025) From 93ef7a64c3fbb4a24ad280c53169ec27939415e3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 14 Mar 2025 17:29:27 -0400 Subject: [PATCH 152/329] Test over full range --- docs/literate_input/conventions_comparisons/ninja_2011.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions_comparisons/ninja_2011.jl index 001dfe3d..6de73d26 100644 --- a/docs/literate_input/conventions_comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions_comparisons/ninja_2011.jl @@ -114,7 +114,7 @@ end # module NINJA #+ # First, we compare the explicit formulas to the general formulas. -for (ι, ϕ) ∈ θϕrange(Float64, 1) +for (ι, ϕ) ∈ θϕrange() @test NINJA.ₛYₗₘ(-2, 2, 2, ι, ϕ) ≈ NINJA.₋₂Y₂₂(ι, ϕ) atol=ϵₐ rtol=ϵᵣ @test NINJA.ₛYₗₘ(-2, 2, 1, ι, ϕ) ≈ NINJA.₋₂Y₂₁(ι, ϕ) atol=ϵₐ rtol=ϵᵣ @test NINJA.ₛYₗₘ(-2, 2, 0, ι, ϕ) ≈ NINJA.₋₂Y₂₀(ι, ϕ) atol=ϵₐ rtol=ϵᵣ From 868951845762bea0c26e03d97dca4392d06df739 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 14 Mar 2025 17:29:43 -0400 Subject: [PATCH 153/329] Include broken Blanchet conventions --- .../conventions_comparisons/blanchet_2024.jl | 117 ++++++++++++++++++ docs/src/references.bib | 13 ++ 2 files changed, 130 insertions(+) create mode 100644 docs/literate_input/conventions_comparisons/blanchet_2024.jl diff --git a/docs/literate_input/conventions_comparisons/blanchet_2024.jl b/docs/literate_input/conventions_comparisons/blanchet_2024.jl new file mode 100644 index 00000000..15b5f885 --- /dev/null +++ b/docs/literate_input/conventions_comparisons/blanchet_2024.jl @@ -0,0 +1,117 @@ +md""" +# Blanchet (2024) + +!!! info "Summary" + TODO + +Luc Blanchet is one of the pre-eminent researchers in post-Newtonian approximations, and has +written a "living" review article on the subject [Blanchet_2024](@cite), which he has kept +up-to-date with the latest developments. + +The spherical coordinates are standard physicists' coordinates, except that the polar angle +is denoted ``\iota``: + +> we define standard spherical coordinates ``(r, ι, φ)`` where ``ι`` is the inclination +> angle from the z-axis and ``φ`` is the phase angle. + + +## Implementing formulas + +We begin by writing code that implements the formulas from Ref. [AjithEtAl_2011](@cite). We +encapsulate the formulas in a module so that we can test them against the +`SphericalFunctions` package. + +""" +using TestItems: @testitem #hide +@testitem "Blanchet conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide + +module Blanchet +#+ + +# We'll also use some predefined utilities to make the code look more like the equations. +import ..ConventionsUtilities: 𝒾, ❗ +#+ + +# The ``s=-2`` spin-weighted spherical harmonics are defined in Eq. (184a) as +# ```math +# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4\pi}} d^{\ell m}(\theta) e^{im\phi}. +# ``` +function Yˡᵐ₋₂(l, m, θ::T, ϕ::T) where {T<:Real} + √((2l + 1) / (4T(π))) * d(l, m, θ) * exp(𝒾 * m * ϕ) +end +#+ + +# Immediately following that, in Eq. (184b), we find the definition of the ``d`` function: +# ```math +# d^{\ell m}(\theta) +# = +# \sum_{k = k_1}^{k_2} +# \frac{(-1)^k}{k!} +# e_k^{\ell m} +# \left(\cos\frac{\theta}{2}\right)^{2\ell+m-2k-2} +# \left(\sin\frac{\theta}{2}\right)^{2k-m+2}, +# ``` +# with ``k_1 = \textrm{max}(0, m-2)`` and ``k_2=\textrm{min}(l+m, l-2)``. +function d(l, m, θ::T) where {T<:Real} + k₁ = max(0, m - 2) + k₂ = min(l + m, l - 2) + sum( + T((-1)^k / (k)❗ * eₖˡᵐ(k, l, m)) + * cos(θ / 2) ^ (2l + m - 2k - 2) + * sin(θ / 2) ^ (2k - m + 2) + for k in k₁:k₂; + init=zero(T) + ) +end +#+ + +# Note that he seems to have flipped the sign of ``s=-2`` in that equation, so that he has +# evidently given formulas for the ``s=2`` harmonics instead. His notation seems consistent +# with the NINJA paper [AjithEtAl_2011](@cite), which unfortunately included a confusing +# negative sign on the left-hand side of the definition of the spin-weighted spherical +# harmonics. +# +# The ``e_k^{\ell m}`` symbol is defined in Eq. (184c) as +# ```math +# e_k^{\ell m} = \frac{ +# \sqrt{(\ell+m)!(\ell-m)!(\ell+2)!(\ell-2)!} +# }{ +# (k-m+2)!(\ell+m-k)!(\ell-k-2)! +# }. +# ``` +function eₖˡᵐ(k, l, m) + ( + √((l + m)❗ * (l - m)❗ * (l + 2)❗ * (l - 2)❗) + / ((k - m + 2)❗ * (l + m - k)❗ * (l - k - 2)❗) + ) +end + + +end # module Blanchet +#+ + +# ## Tests +# +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 10eps() +ϵᵣ = 10eps() +#+ + +# We will only test up to +ℓₘₐₓ = 2 +#+ +# because the formulas are very slow, and this will be sufficient to sort out any sign or +# normalization differences, which are the most likely source of error. +for (θ, ϕ) ∈ θϕrange(Float64, 1) + for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) + @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(2, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# These successful tests show that TODO: finish this sentence + + +end #hide diff --git a/docs/src/references.bib b/docs/src/references.bib index 34f23d8c..559f8d9a 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -41,6 +41,19 @@ @article{Belikov_1991 pages = {384--410} } +@article{Blanchet_2024, + author = {Blanchet, Luc}, + year = {2024}, + month = {07}, + day = {10}, + title = {Post-Newtonian theory for gravitational waves}, + journal = {Living Reviews in Relativity}, + volume = {27}, + number = {4}, + url = {https://doi.org/10.1007/s41114-024-00050-z}, + doi = {10.1007/s41114-024-00050-z}, +} + @article{BoydPetschek_2014, title = {The Relationships Between {C}hebyshev, {L}egendre and {J}acobi Polynomials: The Generic Superiority of {C}hebyshev Polynomials and Three Important Exceptions}, From 1f94ab1460fc7575c04bbe2d1c3b15c18eb20a89 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 4 Apr 2025 20:38:43 -0400 Subject: [PATCH 154/329] Mention Vasil et al. --- docs/src/conventions/details.md | 4 ++++ docs/src/references.bib | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 459f0096..ef7fd466 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -913,6 +913,10 @@ So, for example, the ``\ell = m`` mode varies most rapidly with longitude but not at all with latitude, while the ``\ell = 0`` mode varies just as rapidly with latitude but not at all with longitude. +[Vasil_2019](@citet) use spin-weighted spherical harmonics to do +tensor calculus in the 3-ball, and have a lot formulas for +derivatives, as a result. + * TODO: Show the relationship between the spherical Laplacian and the angular momentum operator. * TODO: Show how ``D`` matrices are harmonic with respect to the diff --git a/docs/src/references.bib b/docs/src/references.bib index 559f8d9a..c9dd858d 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -563,3 +563,17 @@ @book{vanNeerven_2022 year = 2022, doi = {10.1017/9781009232487} } + +@article{Vasil_2019, + title = {Tensor calculus in spherical coordinates using Jacobi polynomials. {Part-I:} Mathematical analysis and derivations}, + volume = {3}, + issn = {2590-0552}, + shorttitle = {Tensor calculus in spherical coordinates using Jacobi polynomials. {Part-I}}, + url = {https://www.sciencedirect.com/science/article/pii/S2590055219300290}, + doi = {10.1016/j.jcpx.2019.100013}, + journal = {Journal of Computational Physics: X}, + author = {Vasil, Geoffrey M. and Lecoanet, Daniel and Burns, Keaton J. and Oishi, Jeffrey S. and Brown, Benjamin P.}, + month = jun, + year = {2019}, + pages = {100013} +} From 9913d7394d9134d149874e6624245ece73388a3f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 09:54:47 -0400 Subject: [PATCH 155/329] Explain arguments to Literate.markdown --- docs/make.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 6faa4e4d..bd654753 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -22,13 +22,21 @@ literate_output = joinpath(docs_src_dir, "literate_output") relative_literate_output = relpath(literate_output, docs_src_dir) relative_convention_comparisons = joinpath(relative_literate_output, "conventions_comparisons") rm(literate_output; force=true, recursive=true) -skip_files = ( # Non-.jl files will be skipped anyway +skip_input_files = ( # Non-.jl files will be skipped anyway "ConventionsUtilities.jl", "ConventionsSetup.jl", ) +# I am writing these specifically to be consumed by Documenter; setting this option +# enables lots of nice conversions. +documenter=true +# To support markdown strings, as in md""" ... """, we need to set this option. +mdstrings=true +# We *don't* want to execute the code in the literate script, because they are meant to +# be used with TestItems.jl, and we don't want to run the tests here. +execute=false for (root, _, files) ∈ walkdir(literate_input), file ∈ files # Skip some files - if splitext(file)[2] != ".jl" || file ∈ skip_files + if splitext(file)[2] != ".jl" || file ∈ skip_input_files continue end # full path to a literate script @@ -36,7 +44,7 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files # generated output path output_path = splitdir(replace(input_path, literate_input=>literate_output))[1] # generate the markdown file calling Literate - Literate.markdown(input_path, output_path, documenter=true, mdstrings=true) + Literate.markdown(input_path, output_path; documenter, mdstrings, execute) end # Make "lalsuite_SphericalHarmonics.c" available in the docs From 834a514591aab00bb3f4dac2a81f7aaf12e2118f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 09:59:52 -0400 Subject: [PATCH 156/329] Include Blanchet comparison --- docs/make.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/make.jl b/docs/make.jl index bd654753..232321a7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -98,6 +98,7 @@ makedocs( "conventions/details.md", "conventions/comparisons.md", "Comparisons" => [ + joinpath(relative_convention_comparisons, "blanchet_2024.md"), joinpath(relative_convention_comparisons, "cohen_tannoudji_1991.md"), joinpath(relative_convention_comparisons, "condon_shortley_1935.md"), joinpath(relative_convention_comparisons, "lalsuite_2025.md"), From 9c8b61c927d3e987b590e3647ef0957afc4d7f57 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 10:00:41 -0400 Subject: [PATCH 157/329] Remember that we can run in draft mode --- docs/make.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 232321a7..1b99347d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -115,7 +115,8 @@ makedocs( "References" => "references.md", ], #warnonly=true, - #doctest = false + #doctest = false, + #draft=true, # Skips running code in the docs for speed ) deploydocs( From f2ee349c74aaa3be1b6a91a89c62b1c75a622e3c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 10:09:33 -0400 Subject: [PATCH 158/329] Finish up Blanchet comparison --- .../conventions_comparisons/blanchet_2024.jl | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/literate_input/conventions_comparisons/blanchet_2024.jl b/docs/literate_input/conventions_comparisons/blanchet_2024.jl index 15b5f885..a42b957d 100644 --- a/docs/literate_input/conventions_comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions_comparisons/blanchet_2024.jl @@ -2,7 +2,8 @@ md""" # Blanchet (2024) !!! info "Summary" - TODO + Blanchet's definition of the spherical harmonics agrees with the definition used in the + `SphericalFunctions` package. Luc Blanchet is one of the pre-eminent researchers in post-Newtonian approximations, and has written a "living" review article on the subject [Blanchet_2024](@cite), which he has kept @@ -17,7 +18,7 @@ is denoted ``\iota``: ## Implementing formulas -We begin by writing code that implements the formulas from Ref. [AjithEtAl_2011](@cite). We +We begin by writing code that implements the formulas from Ref. [Blanchet_2024](@cite). We encapsulate the formulas in a module so that we can test them against the `SphericalFunctions` package. @@ -43,10 +44,10 @@ end # Immediately following that, in Eq. (184b), we find the definition of the ``d`` function: # ```math -# d^{\ell m}(\theta) +# d^{\ell m} # = # \sum_{k = k_1}^{k_2} -# \frac{(-1)^k}{k!} +# \frac{(-)^k}{k!} # e_k^{\ell m} # \left(\cos\frac{\theta}{2}\right)^{2\ell+m-2k-2} # \left(\sin\frac{\theta}{2}\right)^{2k-m+2}, @@ -65,12 +66,6 @@ function d(l, m, θ::T) where {T<:Real} end #+ -# Note that he seems to have flipped the sign of ``s=-2`` in that equation, so that he has -# evidently given formulas for the ``s=2`` harmonics instead. His notation seems consistent -# with the NINJA paper [AjithEtAl_2011](@cite), which unfortunately included a confusing -# negative sign on the left-hand side of the definition of the spin-weighted spherical -# harmonics. -# # The ``e_k^{\ell m}`` symbol is defined in Eq. (184c) as # ```math # e_k^{\ell m} = \frac{ @@ -85,7 +80,10 @@ function eₖˡᵐ(k, l, m) / ((k - m + 2)❗ * (l + m - k)❗ * (l - k - 2)❗) ) end +#+ +# The paper did not give an expression for the Wigner D-matrices, but the definition of the +# spin-weighted spherical harmonics is probably most relevant, so this will suffice. end # module Blanchet #+ @@ -93,25 +91,32 @@ end # module Blanchet # ## Tests # # We can now test the functions against the equivalent functions from the -# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# `SphericalFunctions` package. We will test up to +ℓₘₐₓ = 8 +#+ + +# because that's the maximum ``\ell`` used for PN results — and that's roughly the limit to +# which I'd trust these expressions anyway. We will also only test the +s = -2 +#+ + +# case, which is the only one defined in the paper. +# We will need to test approximate floating-point equality, # so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 10eps() -ϵᵣ = 10eps() +ϵᵣ = 500eps() #+ -# We will only test up to -ℓₘₐₓ = 2 -#+ -# because the formulas are very slow, and this will be sufficient to sort out any sign or -# normalization differences, which are the most likely source of error. -for (θ, ϕ) ∈ θϕrange(Float64, 1) - for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) - @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(2, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ +# This loose relative tolerance is necessary because the numerical errors in Blanchet's +# explicit expressions grow rapidly with ``\ell``. +for (θ, ϕ) ∈ θϕrange() + for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) + @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end #+ -# These successful tests show that TODO: finish this sentence +# These successful tests show that Blanchet's expression agrees with ours. end #hide From c49dd67e4ed643905e8576f0eeb5565e35bc68ce Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 12:19:20 -0400 Subject: [PATCH 159/329] Rearrange Literate outputs --- .gitignore | 12 +- .../calculations}/euler_angular_momentum.jl | 0 .../comparisons}/ConventionsSetup.jl | 0 .../comparisons}/ConventionsUtilities.jl | 0 .../comparisons}/blanchet_2024.jl | 0 .../comparisons}/cohen_tannoudji_1991.jl | 0 .../comparisons}/condon_shortley_1935.jl | 0 .../comparisons}/lalsuite_2025.jl | 0 .../lalsuite_SphericalHarmonics.c | 0 .../comparisons}/ninja_2011.jl | 0 docs/make.jl | 74 +++-------- docs/make_literate.jl | 119 ++++++++++++++++++ scripts/docs.jl | 15 ++- 13 files changed, 156 insertions(+), 64 deletions(-) rename docs/literate_input/{ => conventions/calculations}/euler_angular_momentum.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/ConventionsSetup.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/ConventionsUtilities.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/blanchet_2024.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/cohen_tannoudji_1991.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/condon_shortley_1935.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/lalsuite_2025.jl (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/lalsuite_SphericalHarmonics.c (100%) rename docs/literate_input/{conventions_comparisons => conventions/comparisons}/ninja_2011.jl (100%) create mode 100644 docs/make_literate.jl diff --git a/.gitignore b/.gitignore index dfc0bf1d..7e39ad61 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,14 @@ conventions.slides.json rotate.jl docs/.CondaPkg -docs/src/literate_output + +## The following are generated during the documentation build process +## from files in docs/literate_input, and added to this file automatically +## by docs/make_literate.jl +docs/src/conventions/calculations/euler_angular_momentum.md +docs/src/conventions/comparisons/blanchet_2024.md +docs/src/conventions/comparisons/cohen_tannoudji_1991.md +docs/src/conventions/comparisons/condon_shortley_1935.md +docs/src/conventions/comparisons/lalsuite_2025.md +docs/src/conventions/comparisons/ninja_2011.md +docs/src/conventions/comparisons/lalsuite_SphericalHarmonics.md diff --git a/docs/literate_input/euler_angular_momentum.jl b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl similarity index 100% rename from docs/literate_input/euler_angular_momentum.jl rename to docs/literate_input/conventions/calculations/euler_angular_momentum.jl diff --git a/docs/literate_input/conventions_comparisons/ConventionsSetup.jl b/docs/literate_input/conventions/comparisons/ConventionsSetup.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/ConventionsSetup.jl rename to docs/literate_input/conventions/comparisons/ConventionsSetup.jl diff --git a/docs/literate_input/conventions_comparisons/ConventionsUtilities.jl b/docs/literate_input/conventions/comparisons/ConventionsUtilities.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/ConventionsUtilities.jl rename to docs/literate_input/conventions/comparisons/ConventionsUtilities.jl diff --git a/docs/literate_input/conventions_comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/blanchet_2024.jl rename to docs/literate_input/conventions/comparisons/blanchet_2024.jl diff --git a/docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/cohen_tannoudji_1991.jl rename to docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl diff --git a/docs/literate_input/conventions_comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/condon_shortley_1935.jl rename to docs/literate_input/conventions/comparisons/condon_shortley_1935.jl diff --git a/docs/literate_input/conventions_comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/lalsuite_2025.jl rename to docs/literate_input/conventions/comparisons/lalsuite_2025.jl diff --git a/docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c b/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c similarity index 100% rename from docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c rename to docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c diff --git a/docs/literate_input/conventions_comparisons/ninja_2011.jl b/docs/literate_input/conventions/comparisons/ninja_2011.jl similarity index 100% rename from docs/literate_input/conventions_comparisons/ninja_2011.jl rename to docs/literate_input/conventions/comparisons/ninja_2011.jl diff --git a/docs/make.jl b/docs/make.jl index 1b99347d..14521174 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,53 +15,10 @@ using DocumenterCitations docs_src_dir = joinpath(@__DIR__, "src") +package_root = dirname(@__DIR__) -# See LiveServer.jl docs for this: https://juliadocs.org/LiveServer.jl/dev/man/ls+lit/ -literate_input = joinpath(@__DIR__, "literate_input") -literate_output = joinpath(docs_src_dir, "literate_output") -relative_literate_output = relpath(literate_output, docs_src_dir) -relative_convention_comparisons = joinpath(relative_literate_output, "conventions_comparisons") -rm(literate_output; force=true, recursive=true) -skip_input_files = ( # Non-.jl files will be skipped anyway - "ConventionsUtilities.jl", - "ConventionsSetup.jl", -) -# I am writing these specifically to be consumed by Documenter; setting this option -# enables lots of nice conversions. -documenter=true -# To support markdown strings, as in md""" ... """, we need to set this option. -mdstrings=true -# We *don't* want to execute the code in the literate script, because they are meant to -# be used with TestItems.jl, and we don't want to run the tests here. -execute=false -for (root, _, files) ∈ walkdir(literate_input), file ∈ files - # Skip some files - if splitext(file)[2] != ".jl" || file ∈ skip_input_files - continue - end - # full path to a literate script - input_path = joinpath(root, file) - # generated output path - output_path = splitdir(replace(input_path, literate_input=>literate_output))[1] - # generate the markdown file calling Literate - Literate.markdown(input_path, output_path; documenter, mdstrings, execute) -end - -# Make "lalsuite_SphericalHarmonics.c" available in the docs -let - lalsource = read( - joinpath(literate_input, "conventions_comparisons", "lalsuite_SphericalHarmonics.c"), - String - ) - write( - joinpath(literate_output, "conventions_comparisons", "lalsuite_SphericalHarmonics.md"), - "# LALSuite: Spherical Harmonics original source code\n" - * "The official repository is [here](" - * "https://git.ligo.org/lscsoft/lalsuite/-/blob/22e4cd8fff0487c7b42a2c26772ae9204c995637/lal/lib/utilities/SphericalHarmonics.c" - * ")\n" - * "```c\n$lalsource\n```\n" - ) -end +# Run `make_literate.jl` to generate the literate files +include(joinpath(@__DIR__, "make_literate.jl")) bib = CitationBibliography( @@ -97,19 +54,22 @@ makedocs( "conventions/summary.md", "conventions/details.md", "conventions/comparisons.md", - "Comparisons" => [ - joinpath(relative_convention_comparisons, "blanchet_2024.md"), - joinpath(relative_convention_comparisons, "cohen_tannoudji_1991.md"), - joinpath(relative_convention_comparisons, "condon_shortley_1935.md"), - joinpath(relative_convention_comparisons, "lalsuite_2025.md"), - joinpath(relative_convention_comparisons, "ninja_2011.md"), - ], - "Calculations" => [ - joinpath(relative_literate_output, "euler_angular_momentum.md"), - ], + "Comparisons" => map( + s -> joinpath("conventions", "comparisons", s), + sort( + filter( + s -> s != "lalsuite_SphericalHarmonics.md", + readdir(joinpath(docs_src_dir, "conventions", "comparisons")) + ) + ) + ), + "Calculations" => map( + s -> joinpath("conventions", "calculations", s), + sort(readdir(joinpath(docs_src_dir, "conventions", "calculations"))) + ), ], "Notes" => map( - s -> "notes/$(s)", + s -> joinpath("notes", s), sort(readdir(joinpath(docs_src_dir, "notes"))) ), "References" => "references.md", diff --git a/docs/make_literate.jl b/docs/make_literate.jl new file mode 100644 index 00000000..0ed90261 --- /dev/null +++ b/docs/make_literate.jl @@ -0,0 +1,119 @@ +### Currently, all the generated output goes into the `docs/src/literate_output` directory. +### This is nice just because it lets me add just that directory to the `.gitignore` file; +### I can't add `docs/src` to the `.gitignore` file because it would ignore all the +### non-generated files in that directory. However, it would be nice to have the generated +### files in directories that are more consistent with the documentation structure. For +### example, the `docs/src/literate_output/conventions/comparisons` directory contains files +### that really should be in the `docs/src/conventions/comparisons` directory. I intend to +### reorganize the functionality in this file so that the outputs are in directories that +### are more consistent with the documentation structure. +### +### Instead of just plain for loops doing all the work, I will create functions that +### encapsulate things, and then call those functions in the for loops. This will make it +### easier to add new functionality in the future, and make it easier to read the code. +### +### To deal with the gitignore issue, I will add a step that ensures the output file is +### listed in the `.gitignore` file. +### +### The files in the `docs/src/literate_input` directory will be rearranged so that they +### are in directories that are more consistent with the documentation structure. For +### example, the `docs/literate_input/conventions/comparisons` directory will be +### moved to `docs/literate_input/conventions/comparisons`, and the output for every file in +### that directory will be sent to `docs/src/conventions/comparisons`. There will no longer +### be a `docs/src/literate_output` directory; all the output will be in the same +### directory as the non-generated files. + +literate_input = joinpath(@__DIR__, "literate_input") +skip_input_files = ( # Non-.jl files will be skipped anyway + "ConventionsUtilities.jl", # Used for TestItemRunners.jl + "ConventionsSetup.jl", # Used for TestItemRunners.jl +) + +# Ensure a file is listed in the .gitignore file +function ensure_in_gitignore(file_path) + gitignore_path = joinpath(package_root, ".gitignore") + if isfile(gitignore_path) + existing_entries = readlines(gitignore_path) + if file_path in existing_entries + return # File is already listed + end + end + open(gitignore_path, "a") do io + write(io, file_path * "\n") + end +end + +# Generate markdown file for Documenter.jl from a Literate script +function generate_markdown(inputfile) + # I've written the docs specifically to be consumed by Documenter; setting this option + # enables lots of nice conversions. + documenter=true + # To support markdown strings, as in md""" ... """, we need to set this option. + mdstrings=true + # We *don't* want to execute the code in the literate script, because they are meant to + # be used with TestItems.jl, and we don't want to run the tests here. + execute=false + # Output will be generated here: + outputfile = replace(inputfile, "literate_input"=>"src") + outputdir = dirname(outputfile) + # Ensure the output path is in .gitignore + ensure_in_gitignore(relpath(replace(outputfile, ".jl"=>".md"), package_root)) + # Generate the markdown file calling Literate + Literate.markdown(inputfile, outputdir; documenter, mdstrings, execute) +end + +for (root, _, files) ∈ walkdir(literate_input), file ∈ files + # Skip some files + if splitext(file)[2] != ".jl" || file ∈ skip_input_files + continue + end + # full path to a literate script + inputfile = joinpath(root, file) + generate_markdown(inputfile) +end + + + +# # See LiveServer.jl docs for this: https://juliadocs.org/LiveServer.jl/dev/man/ls+lit/ +# literate_input = joinpath(@__DIR__, "literate_input") +# literate_output = joinpath(docs_src_dir, "literate_output") +# relative_literate_output = relpath(literate_output, docs_src_dir) +# relative_convention_comparisons = joinpath(relative_literate_output, "conventions", "comparisons") +# rm(literate_output; force=true, recursive=true) +# skip_input_files = ( # Non-.jl files will be skipped anyway +# "ConventionsUtilities.jl", # Used for TestItemRunners.jl +# "ConventionsSetup.jl", # Used for TestItemRunners.jl +# ) +# for (root, _, files) ∈ walkdir(literate_input), file ∈ files +# # Skip some files +# if splitext(file)[2] != ".jl" || file ∈ skip_input_files +# continue +# end +# # full path to a literate script +# inputfile = joinpath(root, file) +# # generated output path +# output_path = splitdir(replace(inputfile, literate_input=>literate_output))[1] +# # Ensure the output path is in .gitignore +# ensure_in_gitignore(relpath(output_path, @__DIR__)) +# # generate the markdown file calling Literate +# Literate.markdown(inputfile, output_path; documenter, mdstrings, execute) +# end + +# Make "lalsuite_SphericalHarmonics.c" available in the docs +let + inputfile = joinpath(literate_input, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c") + outputfile = joinpath(docs_src_dir, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c") + ensure_in_gitignore(relpath(replace(outputfile, ".c"=>".md"), package_root)) + lalsource = read( + joinpath(literate_input, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c"), + String + ) + write( + joinpath(docs_src_dir, "conventions", "comparisons", "lalsuite_SphericalHarmonics.md"), + "# LALSuite: Spherical Harmonics original source code\n" + * "The official repository is [here](" + * "https://git.ligo.org/lscsoft/lalsuite/-/blob/22e4cd8fff0487c7b42a2c26772ae9204c995637/lal/lib/utilities/SphericalHarmonics.c" + * ")\n" + * "```c\n$lalsource\n```\n" + ) +end diff --git a/scripts/docs.jl b/scripts/docs.jl index f619717a..70de204a 100644 --- a/scripts/docs.jl +++ b/scripts/docs.jl @@ -6,21 +6,24 @@ # will monitor the docs for any changes, then rebuild them and refresh the browser # until this script is stopped. -using Revise +import Revise +Revise.revise() import Dates println("Building docs starting at ", Dates.format(Dates.now(), "HH:MM:SS"), ".") -using Pkg +import Pkg cd((@__DIR__) * "/..") Pkg.activate("docs") -using LiveServer +import LiveServer: servedocs literate_input = joinpath(pwd(), "docs", "literate_input") literate_output = joinpath(pwd(), "docs", "src", "literate_output") @info "Using input for Literate.jl from $literate_input" servedocs( - literate_dir = literate_input, - skip_dir = literate_output, - launch_browser=true + include_dirs=["src/"], # So that docstring changes are picked up + include_files=["docs/make_literate.jl"], + skip_files=["docs/src/conventions/comparisons/lalsuite_SphericalHarmonics.md"], + literate_dir=literate_input, + launch_browser=true, ) From 69957b7a8664c5fc6c5d93d7f747709eedc1384f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 13:52:50 -0400 Subject: [PATCH 160/329] Correct description of spherical coordinates --- .../conventions/comparisons/blanchet_2024.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl index a42b957d..575359a3 100644 --- a/docs/literate_input/conventions/comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -9,11 +9,11 @@ Luc Blanchet is one of the pre-eminent researchers in post-Newtonian approximati written a "living" review article on the subject [Blanchet_2024](@cite), which he has kept up-to-date with the latest developments. -The spherical coordinates are standard physicists' coordinates, except that the polar angle -is denoted ``\iota``: - -> we define standard spherical coordinates ``(r, ι, φ)`` where ``ι`` is the inclination -> angle from the z-axis and ``φ`` is the phase angle. +The spherical coordinates are standard physicists' coordinates, implicitly defined by the +direction vector below Eq. (188b): +```math + N_i = \left(\sin\theta\cos\phi, \sin\theta\sin\phi, \cos\theta\right). +``` ## Implementing formulas From 86c5e8e8bf91172d42625b05e258fc130002af68 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 14:53:03 -0400 Subject: [PATCH 161/329] Bump compat for Quaternionic and FastTransforms --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 75242b55..b56c4371 100644 --- a/Project.toml +++ b/Project.toml @@ -24,7 +24,7 @@ Coverage = "1.6" DoubleFloats = "1" FFTW = "1" FastDifferentiation = "0.3.17" -FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17" +FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.17" ForwardDiff = "0.10" Hwloc = "2, 3" LinearAlgebra = "1" From 0c8c090eeee17fb2f422cad4e1aed7a91c31627b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 17:04:09 -0400 Subject: [PATCH 162/329] Test lalsuite via python --- Project.toml | 4 +- .../conventions_install_lalsuite.jl | 29 ++++ .../conventions/comparisons/lalsuite_2025b.jl | 135 ++++++++++++++++++ docs/make_literate.jl | 1 + 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl create mode 100644 docs/literate_input/conventions/comparisons/lalsuite_2025b.jl diff --git a/Project.toml b/Project.toml index b56c4371..4a05908f 100644 --- a/Project.toml +++ b/Project.toml @@ -44,6 +44,7 @@ julia = "1.6" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" @@ -57,6 +58,7 @@ Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" @@ -64,4 +66,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "Printf", "ProgressMeter", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] +test = ["Aqua", "CondaPkg", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "Printf", "ProgressMeter", "PythonCall", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] diff --git a/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl b/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl new file mode 100644 index 00000000..c3c738dd --- /dev/null +++ b/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl @@ -0,0 +1,29 @@ +# Construct the CondaPkg.toml file to use to make sure we get the right Python version and +# we get pip installed. +conda_pkg_toml = """ +[deps] +python = "<3.13" +pip = "" +numpy = "==2.2.4" +""" +try + open(joinpath(LOAD_PATH[2], "CondaPkg.toml"), "w") do io + write(io, conda_pkg_toml) + end +catch e + println("Error copying CondaPkg.toml: $e") +end + +# Now we'll set up the CondaPkg environment +import CondaPkg +import PythonCall + +# This ugly hack is to ensure that lalsuite is installed without any dependencies; by +# default it comes with lots of things we don't need that break all the time, so I really +# don't want to bother fixing them. The `--no-deps` flag is not supported by CondaPkg, so +# we have to use PythonCall to install it. +PythonCall.@pyexec ` +from sys import executable as python; +import subprocess; +subprocess.call([python, "-m", "pip", "install", "-q", "--no-deps", "lalsuite==7.25.1"]); +` diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025b.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025b.jl new file mode 100644 index 00000000..85e60113 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025b.jl @@ -0,0 +1,135 @@ +md""" +# LALSuite (2025) + +!!! info "Summary" + The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` + functions agree with the definitions used in the `SphericalFunctions` package. + +""" +md""" +[LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software +routines, comprising the primary official software used by the LIGO-Virgo-KAGRA +Collaboration to detect and characterize gravitational waves. As far as I can tell, the +ultimate source for all spin-weighted spherical harmonic values used in LALSuite is the +function +[`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), +which cites the NINJA paper [AjithEtAl_2011](@cite) as its source. Unfortunately, it cites +version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` instead of +``\cos \tfrac{\iota}{2}`` and similarly for ``\sin``. This error was corrected in version +2, but the citation was not updated. Nonetheless, it appears that the actual code is +consistent with the *corrected* versions of the NINJA paper. + +They also (quite separately) define Wigner's ``D`` matrices in terms of the ``d`` matrices, +which are — in turn — defined in terms of Jacobi polynomials. For all of these, they cite +Wikipedia (despite the fact that the NINJA paper defined the spin-weighted spherical +harmonics in terms of the ``d`` matrices). Nonetheless, the definitions in the code are +consistent with the definitions in the NINJA paper, which are consistent with the +definitions in the `SphericalFunctions` package. + + +## Implementing formulas + +We will call the python module `lal` directly, but there are some minor inconveniences to +deal with first. We have to install the `lalsuite` package, but we don't want all its +dependencies, so we run `python -m pip install --no-deps lalsuite`. Then, we have to +translate to native Julia types, so we'll just write three quick and easy wrappers. We +encapsulate the formulas in a module so that we can test them against the +`SphericalFunctions` package. + +""" +using TestItems: @testitem #hide +@testitem "LALSuite conventions B" setup=[ConventionsSetup, Utilities] begin #hide + +module LALSuite + +include("conventions_install_lalsuite.jl") +import PythonCall + +const lal = PythonCall.pyimport("lal") + +function SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) + PythonCall.pyconvert( + ComplexF64, + lal.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m), + ) +end +function WignerdMatrix(ℓ, m′, m, β) + PythonCall.pyconvert( + Float64, + lal.WignerdMatrix(ℓ, m′, m, β), + ) +end +function WignerDMatrix(ℓ, m′, m, α, β, γ) + PythonCall.pyconvert( + ComplexF64, + lal.WignerDMatrix(ℓ, m′, m, α, β, γ), + ) +end + +end # module LALSuite +#+ + + +# ## Tests +# +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 100eps() +ϵᵣ = 100eps() +#+ + +# The spin-weighted spherical harmonics are defined explicitly, but only for +s = -2 +#+ +# and only up to +ℓₘₐₓ = 3 +#+ +# so we only test up to that point. +for (θ, ϕ) ∈ θϕrange() + for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) + @test LALSuite.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ + SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# Now, the Wigner ``d`` matrices are defined generally, but we only need to test up to +ℓₘₐₓ = 4 +#+ +# because the formulas are fairly inefficient and inaccurate, and this will be sufficient to +# sort out any sign or normalization differences, which are the most likely sources of +# error. +for β ∈ βrange() + for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) + @test LALSuite.WignerdMatrix(ℓ, m′, m, β) ≈ + SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# We can see more-or-less by inspection that the code defines the ``D`` matrix in agreement +# with our convention, the key line being +# ```c +# cexp( -(1.0I)*mp*alpha ) * XLALWignerdMatrix( l, mp, m, beta ) * cexp( -(1.0I)*m*gam ); +# ``` +# And because of the higher dimensionality of the space in which to test, we want to +# restrict the range of the tests to avoid excessive computation. We will test up to +ℓₘₐₓ = 2 +#+ +# because the space of options for disagreement is smaller. +for (α,β,γ) ∈ αβγrange() + for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) + @test LALSuite.WignerDMatrix(ℓ, m′, m, α, β, γ) ≈ + conj(SphericalFunctions.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ + end +end +@test_broken false # We haven't flipped the conjugation of D yet + +#+ + +# These successful tests show that the spin-weighted spherical harmonics and the Wigner +# ``d`` and ``D`` matrices defined in LALSuite agree with the corresponding functions +# defined by the `SphericalFunctions` package. + +end #hide diff --git a/docs/make_literate.jl b/docs/make_literate.jl index 0ed90261..74fd7c3f 100644 --- a/docs/make_literate.jl +++ b/docs/make_literate.jl @@ -27,6 +27,7 @@ literate_input = joinpath(@__DIR__, "literate_input") skip_input_files = ( # Non-.jl files will be skipped anyway "ConventionsUtilities.jl", # Used for TestItemRunners.jl "ConventionsSetup.jl", # Used for TestItemRunners.jl + "conventions_install_lalsuite.jl", # lalsuite_2025.jl ) # Ensure a file is listed in the .gitignore file From 75c30975f866ec4e4bcb18cbfc07262cdada7980 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 17:10:13 -0400 Subject: [PATCH 163/329] Remove old tests for LALSuite and raw code --- .../conventions/comparisons/lalsuite_2025.jl | 149 +---- .../conventions/comparisons/lalsuite_2025b.jl | 135 ---- .../comparisons/lalsuite_SphericalHarmonics.c | 585 ------------------ docs/make_literate.jl | 46 -- 4 files changed, 32 insertions(+), 883 deletions(-) delete mode 100644 docs/literate_input/conventions/comparisons/lalsuite_2025b.jl delete mode 100644 docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index 78a0c659..efa18161 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -1,5 +1,3 @@ -# TODO: `python -m pip install --no-deps lalsuite; python -m pip install numpy` to directly compare without translating source code... except that this doesn't work on Windows - md""" # LALSuite (2025) @@ -7,6 +5,8 @@ md""" The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` functions agree with the definitions used in the `SphericalFunctions` package. +""" +md""" [LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software routines, comprising the primary official software used by the LIGO-Virgo-KAGRA Collaboration to detect and characterize gravitational waves. As far as I can tell, the @@ -29,133 +29,47 @@ definitions in the `SphericalFunctions` package. ## Implementing formulas -We begin by directly translating the C code of LALSuite over to Julia code. There are three -functions that we will want to compare with the definitions in this package: - -```c -COMPLEX16 XLALSpinWeightedSphericalHarmonic( REAL8 theta, REAL8 phi, int s, int l, int m ); -double XLALWignerdMatrix( int l, int mp, int m, double beta ); -COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ); -``` - -The [original source code](./lalsuite_SphericalHarmonics.md) (as of early 2025) is stored -alongside this file, so we will read it in to a `String` and then apply a series of regular -expressions to convert it to Julia code, parse it and evaluate it to turn it into runnable -Julia. We encapsulate the formulas in a module so that we can test them against the +We will call the python module `lal` directly, but there are some minor inconveniences to +deal with first. We have to install the `lalsuite` package, but we don't want all its +dependencies, so we run `python -m pip install --no-deps lalsuite`. Then, we have to +translate to native Julia types, so we'll just write three quick and easy wrappers. We +encapsulate the formulas in a module so that we can test them against the `SphericalFunctions` package. -We begin by setting up that module, and introducing a set of basic replacements that would -usually be defined in separate C headers. - """ using TestItems: @testitem #hide -@testitem "LALSuite conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide +@testitem "LALSuite conventions B" setup=[ConventionsSetup, Utilities] begin #hide module LALSuite +include("conventions_install_lalsuite.jl") +import PythonCall -using Printf: @sprintf +const lal = PythonCall.pyimport("lal") -const I = im -const LAL_PI = π -const XLAL_EINVAL = "XLAL Error: Invalid arguments" -MIN(a, b) = min(a, b) -gsl_sf_choose(a, b) = binomial(a, b) -pow(a, b) = a^b -cexp(a) = exp(a) -cpolar(a, b) = a * cis(b) -macro XLALPrError(msg, args...) - quote - @error @sprintf($msg, $(args...)) - end +function SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) + PythonCall.pyconvert( + ComplexF64, + lal.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m), + ) end -#+ - -# Next, we simply read the source file into a string. -lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) -#+ - -# Now we define a series of replacements to apply to the C code to convert it to Julia code. -# Note that some of these will be quite specific to this particular file, and may not be -# generally applicable. -replacements = ( - ## Deal with newlines in the middle of an assignment - r"( = .*[^;]\s*)\n" => s"\1", - - ## Remove a couple old, unused functions - r"(?ms)XLALScalarSphericalHarmonic.*?\n}" => "# Removed", - r"(?ms)XLALSphHarm.*?\n}" => "# Removed", - - ## Remove type annotations - r"COMPLEX16 ?" => "", - r"REAL8 ?" => "", - r"INT4 ?" => "", - r"int ?" => "", - r"double ?" => "", - - ## Translate comments - "/*" => "#=", - "*/" => "=#", - - ## Brackets - r" ?{" => "", - r"}.*(\n *else)" => s"\1", - r"} *else" => "else", - r"^}" => "", - "}" => "end", - - ## Flow control - r"( *if.*);"=>s"\1 end", ## one-line `if` statements - "for( s=0; n-s >= 0; s++ )" => "for s=0:n", - "else if" => "elseif", - r"(?m) break;\n *\n *case(.*?):" => s"elseif m == \1", - r"(?m) break;\n\s*case(.*?):" => s"elseif m == \1", - r"(?m) break;\n *\n *default:" => "else", - r"(?m) break;\n *default:" => "else", - r"(?m)switch.*?\n *\n( *)case(.*?):" => s"\n\1if m == \2", - r"\n *break;" => "", - r"(?m)( *ans = fac;)\n" => s"\1\n end\n", - - ## Deal with ugly C declarations - "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", - "sum=0, val=0" => "sum=0; val=0", - "a=0, lam=0" => "a=0; lam=0", - r"\n *fac;" => "", - r"\n *ans;" => "", - r"\n *gslStatus;" => "", - r"\n *gsl_sf_result pLm;" => "", - r"\n ?XLAL" => "\nfunction XLAL", - - ## Differences in Julia syntax - "++" => "+=1", - ".*" => ". *", - "./" => ". /", - ".+" => ". +", - ".-" => ". -", - - ## Deal with random bad syntax - "if (m)" => "if m != 0", - "case 4:" => "elseif m == 4", - "XLALPrError" => "@XLALPrError", - "__func__" => "\"\"", -) -#+ - -# And we apply the replacements to the source code to convert it to Julia code. Note that -# we apply them successively, even though `replace` can handle multiple "simultaneous" -# replacements, because the order of replacements is important. -for (pattern, replacement) in replacements - global lalsource = replace(lalsource, pattern => replacement) +function WignerdMatrix(ℓ, m′, m, β) + PythonCall.pyconvert( + Float64, + lal.WignerdMatrix(ℓ, m′, m, β), + ) +end +function WignerDMatrix(ℓ, m′, m, α, β, γ) + PythonCall.pyconvert( + ComplexF64, + lal.WignerDMatrix(ℓ, m′, m, α, β, γ), + ) end -#+ - -# Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are -# done defining the module -eval(Meta.parseall(lalsource)) end # module LALSuite #+ + # ## Tests # # We can now test the functions against the equivalent functions from the @@ -174,7 +88,7 @@ s = -2 # so we only test up to that point. for (θ, ϕ) ∈ θϕrange() for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) - @test LALSuite.XLALSpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ + @test LALSuite.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end @@ -188,7 +102,8 @@ end # error. for β ∈ βrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test LALSuite.XLALWignerdMatrix(ℓ, m′, m, β) ≈ SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ + @test LALSuite.WignerdMatrix(ℓ, m′, m, β) ≈ + SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ end end #+ @@ -205,7 +120,7 @@ end # because the space of options for disagreement is smaller. for (α,β,γ) ∈ αβγrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test LALSuite.XLALWignerDMatrix(ℓ, m′, m, α, β, γ) ≈ + @test LALSuite.WignerDMatrix(ℓ, m′, m, α, β, γ) ≈ conj(SphericalFunctions.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ end end diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025b.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025b.jl deleted file mode 100644 index 85e60113..00000000 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025b.jl +++ /dev/null @@ -1,135 +0,0 @@ -md""" -# LALSuite (2025) - -!!! info "Summary" - The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` - functions agree with the definitions used in the `SphericalFunctions` package. - -""" -md""" -[LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software -routines, comprising the primary official software used by the LIGO-Virgo-KAGRA -Collaboration to detect and characterize gravitational waves. As far as I can tell, the -ultimate source for all spin-weighted spherical harmonic values used in LALSuite is the -function -[`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), -which cites the NINJA paper [AjithEtAl_2011](@cite) as its source. Unfortunately, it cites -version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` instead of -``\cos \tfrac{\iota}{2}`` and similarly for ``\sin``. This error was corrected in version -2, but the citation was not updated. Nonetheless, it appears that the actual code is -consistent with the *corrected* versions of the NINJA paper. - -They also (quite separately) define Wigner's ``D`` matrices in terms of the ``d`` matrices, -which are — in turn — defined in terms of Jacobi polynomials. For all of these, they cite -Wikipedia (despite the fact that the NINJA paper defined the spin-weighted spherical -harmonics in terms of the ``d`` matrices). Nonetheless, the definitions in the code are -consistent with the definitions in the NINJA paper, which are consistent with the -definitions in the `SphericalFunctions` package. - - -## Implementing formulas - -We will call the python module `lal` directly, but there are some minor inconveniences to -deal with first. We have to install the `lalsuite` package, but we don't want all its -dependencies, so we run `python -m pip install --no-deps lalsuite`. Then, we have to -translate to native Julia types, so we'll just write three quick and easy wrappers. We -encapsulate the formulas in a module so that we can test them against the -`SphericalFunctions` package. - -""" -using TestItems: @testitem #hide -@testitem "LALSuite conventions B" setup=[ConventionsSetup, Utilities] begin #hide - -module LALSuite - -include("conventions_install_lalsuite.jl") -import PythonCall - -const lal = PythonCall.pyimport("lal") - -function SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) - PythonCall.pyconvert( - ComplexF64, - lal.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m), - ) -end -function WignerdMatrix(ℓ, m′, m, β) - PythonCall.pyconvert( - Float64, - lal.WignerdMatrix(ℓ, m′, m, β), - ) -end -function WignerDMatrix(ℓ, m′, m, α, β, γ) - PythonCall.pyconvert( - ComplexF64, - lal.WignerDMatrix(ℓ, m′, m, α, β, γ), - ) -end - -end # module LALSuite -#+ - - -# ## Tests -# -# We can now test the functions against the equivalent functions from the -# `SphericalFunctions` package. We will need to test approximate floating-point equality, -# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: -ϵₐ = 100eps() -ϵᵣ = 100eps() -#+ - -# The spin-weighted spherical harmonics are defined explicitly, but only for -s = -2 -#+ -# and only up to -ℓₘₐₓ = 3 -#+ -# so we only test up to that point. -for (θ, ϕ) ∈ θϕrange() - for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) - @test LALSuite.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ - SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ - end -end -#+ - -# Now, the Wigner ``d`` matrices are defined generally, but we only need to test up to -ℓₘₐₓ = 4 -#+ -# because the formulas are fairly inefficient and inaccurate, and this will be sufficient to -# sort out any sign or normalization differences, which are the most likely sources of -# error. -for β ∈ βrange() - for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test LALSuite.WignerdMatrix(ℓ, m′, m, β) ≈ - SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ - end -end -#+ - -# We can see more-or-less by inspection that the code defines the ``D`` matrix in agreement -# with our convention, the key line being -# ```c -# cexp( -(1.0I)*mp*alpha ) * XLALWignerdMatrix( l, mp, m, beta ) * cexp( -(1.0I)*m*gam ); -# ``` -# And because of the higher dimensionality of the space in which to test, we want to -# restrict the range of the tests to avoid excessive computation. We will test up to -ℓₘₐₓ = 2 -#+ -# because the space of options for disagreement is smaller. -for (α,β,γ) ∈ αβγrange() - for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test LALSuite.WignerDMatrix(ℓ, m′, m, α, β, γ) ≈ - conj(SphericalFunctions.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ - end -end -@test_broken false # We haven't flipped the conjugation of D yet - -#+ - -# These successful tests show that the spin-weighted spherical harmonics and the Wigner -# ``d`` and ``D`` matrices defined in LALSuite agree with the corresponding functions -# defined by the `SphericalFunctions` package. - -end #hide diff --git a/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c b/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c deleted file mode 100644 index 3d4e56d4..00000000 --- a/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c +++ /dev/null @@ -1,585 +0,0 @@ -/* - * Copyright (C) 2007 S.Fairhurst, B. Krishnan, L.Santamaria, C. Robinson, - * C. Pankow - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with with program; see the file COPYING. If not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301 USA - */ - -#include -#include -#include - -#include -#include - -/** - * Computes the (s)Y(l,m) spin-weighted spherical harmonic. - * - * From somewhere .... - * - * See also: - * Implements Equations (II.9)-(II.13) of - * D. A. Brown, S. Fairhurst, B. Krishnan, R. A. Mercer, R. K. Kopparapu, - * L. Santamaria, and J. T. Whelan, - * "Data formats for numerical relativity waves", - * arXiv:0709.0093v1 (2007). - * - * Currently only supports s=-2, l=2,3,4,5,6,7,8 modes. - */ -COMPLEX16 XLALSpinWeightedSphericalHarmonic( - REAL8 theta, /**< polar angle (rad) */ - REAL8 phi, /**< azimuthal angle (rad) */ - int s, /**< spin weight */ - int l, /**< mode number l */ - int m /**< mode number m */ - ) -{ - REAL8 fac; - COMPLEX16 ans; - - /* sanity checks ... */ - if ( l < abs(s) ) - { - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |s| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - } - if ( l < abs(m) ) - { - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - } - - if ( s == -2 ) - { - if ( l == 2 ) - { - switch ( m ) - { - case -2: - fac = sqrt( 5.0 / ( 64.0 * LAL_PI ) ) * ( 1.0 - cos( theta ))*( 1.0 - cos( theta )); - break; - case -1: - fac = sqrt( 5.0 / ( 16.0 * LAL_PI ) ) * sin( theta )*( 1.0 - cos( theta )); - break; - - case 0: - fac = sqrt( 15.0 / ( 32.0 * LAL_PI ) ) * sin( theta )*sin( theta ); - break; - - case 1: - fac = sqrt( 5.0 / ( 16.0 * LAL_PI ) ) * sin( theta )*( 1.0 + cos( theta )); - break; - - case 2: - fac = sqrt( 5.0 / ( 64.0 * LAL_PI ) ) * ( 1.0 + cos( theta ))*( 1.0 + cos( theta )); - break; - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } /* switch (m) */ - } /* l==2*/ - else if ( l == 3 ) - { - switch ( m ) - { - case -3: - fac = sqrt(21.0/(2.0*LAL_PI))*cos(theta/2.0)*pow(sin(theta/2.0),5.0); - break; - case -2: - fac = sqrt(7.0/(4.0*LAL_PI))*(2.0 + 3.0*cos(theta))*pow(sin(theta/2.0),4.0); - break; - case -1: - fac = sqrt(35.0/(2.0*LAL_PI))*(sin(theta) + 4.0*sin(2.0*theta) - 3.0*sin(3.0*theta))/32.0; - break; - case 0: - fac = (sqrt(105.0/(2.0*LAL_PI))*cos(theta)*pow(sin(theta),2.0))/4.0; - break; - case 1: - fac = -sqrt(35.0/(2.0*LAL_PI))*(sin(theta) - 4.0*sin(2.0*theta) - 3.0*sin(3.0*theta))/32.0; - break; - - case 2: - fac = sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),4.0)*(-2.0 + 3.0*cos(theta))/2.0; - break; - - case 3: - fac = -sqrt(21.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*sin(theta/2.0); - break; - - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } - } /* l==3 */ - else if ( l == 4 ) - { - switch ( m ) - { - case -4: - fac = 3.0*sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),2.0)*pow(sin(theta/2.0),6.0); - break; - case -3: - fac = 3.0*sqrt(7.0/(2.0*LAL_PI))*cos(theta/2.0)*(1.0 + 2.0*cos(theta))*pow(sin(theta/2.0),5.0); - break; - - case -2: - fac = (3.0*(9.0 + 14.0*cos(theta) + 7.0*cos(2.0*theta))*pow(sin(theta/2.0),4.0))/(4.0*sqrt(LAL_PI)); - break; - case -1: - fac = (3.0*(3.0*sin(theta) + 2.0*sin(2.0*theta) + 7.0*sin(3.0*theta) - 7.0*sin(4.0*theta)))/(32.0*sqrt(2.0*LAL_PI)); - break; - case 0: - fac = (3.0*sqrt(5.0/(2.0*LAL_PI))*(5.0 + 7.0*cos(2.0*theta))*pow(sin(theta),2.0))/16.0; - break; - case 1: - fac = (3.0*(3.0*sin(theta) - 2.0*sin(2.0*theta) + 7.0*sin(3.0*theta) + 7.0*sin(4.0*theta)))/(32.0*sqrt(2.0*LAL_PI)); - break; - case 2: - fac = (3.0*pow(cos(theta/2.0),4.0)*(9.0 - 14.0*cos(theta) + 7.0*cos(2.0*theta)))/(4.0*sqrt(LAL_PI)); - break; - case 3: - fac = -3.0*sqrt(7.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*(-1.0 + 2.0*cos(theta))*sin(theta/2.0); - break; - case 4: - fac = 3.0*sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),6.0)*pow(sin(theta/2.0),2.0); - break; - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } - } /* l==4 */ - else if ( l == 5 ) - { - switch ( m ) - { - case -5: - fac = sqrt(330.0/LAL_PI)*pow(cos(theta/2.0),3.0)*pow(sin(theta/2.0),7.0); - break; - case -4: - fac = sqrt(33.0/LAL_PI)*pow(cos(theta/2.0),2.0)*(2.0 + 5.0*cos(theta))*pow(sin(theta/2.0),6.0); - break; - case -3: - fac = (sqrt(33.0/(2.0*LAL_PI))*cos(theta/2.0)*(17.0 + 24.0*cos(theta) + 15.0*cos(2.0*theta))*pow(sin(theta/2.0),5.0))/4.0; - break; - case -2: - fac = (sqrt(11.0/LAL_PI)*(32.0 + 57.0*cos(theta) + 36.0*cos(2.0*theta) + 15.0*cos(3.0*theta))*pow(sin(theta/2.0),4.0))/8.0; - break; - case -1: - fac = (sqrt(77.0/LAL_PI)*(2.0*sin(theta) + 8.0*sin(2.0*theta) + 3.0*sin(3.0*theta) + 12.0*sin(4.0*theta) - 15.0*sin(5.0*theta)))/256.0; - break; - case 0: - fac = (sqrt(1155.0/(2.0*LAL_PI))*(5.0*cos(theta) + 3.0*cos(3.0*theta))*pow(sin(theta),2.0))/32.0; - break; - case 1: - fac = sqrt(77.0/LAL_PI)*(-2.0*sin(theta) + 8.0*sin(2.0*theta) - 3.0*sin(3.0*theta) + 12.0*sin(4.0*theta) + 15.0*sin(5.0*theta))/256.0; - break; - case 2: - fac = sqrt(11.0/LAL_PI)*pow(cos(theta/2.0),4.0)*(-32.0 + 57.0*cos(theta) - 36.0*cos(2.0*theta) + 15.0*cos(3.0*theta))/8.0; - break; - case 3: - fac = -sqrt(33.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*(17.0 - 24.0*cos(theta) + 15.0*cos(2.0*theta))*sin(theta/2.0)/4.0; - break; - case 4: - fac = sqrt(33.0/LAL_PI)*pow(cos(theta/2.0),6.0)*(-2.0 + 5.0*cos(theta))*pow(sin(theta/2.0),2.0); - break; - case 5: - fac = -sqrt(330.0/LAL_PI)*pow(cos(theta/2.0),7.0)*pow(sin(theta/2.0),3.0); - break; - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } - } /* l==5 */ - else if ( l == 6 ) - { - switch ( m ) - { - case -6: - fac = (3.*sqrt(715./LAL_PI)*pow(cos(theta/2.0),4)*pow(sin(theta/2.0),8))/2.0; - break; - case -5: - fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),3)*(1. + 3.*cos(theta))*pow(sin(theta/2.0),7))/2.0; - break; - case -4: - fac = (sqrt(195./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(35. + 44.*cos(theta) - + 33.*cos(2.*theta))*pow(sin(theta/2.0),6))/8.0; - break; - case -3: - fac = (3.*sqrt(13./LAL_PI)*cos(theta/2.0)*(98. + 185.*cos(theta) + 110.*cos(2*theta) - + 55.*cos(3.*theta))*pow(sin(theta/2.0),5))/32.0; - break; - case -2: - fac = (sqrt(13./LAL_PI)*(1709. + 3096.*cos(theta) + 2340.*cos(2.*theta) + 1320.*cos(3.*theta) - + 495.*cos(4.*theta))*pow(sin(theta/2.0),4))/256.0; - break; - case -1: - fac = (sqrt(65./(2.0*LAL_PI))*cos(theta/2.0)*(161. + 252.*cos(theta) + 252.*cos(2.*theta) - + 132.*cos(3.*theta) + 99.*cos(4.*theta))*pow(sin(theta/2.0),3))/64.0; - break; - case 0: - fac = (sqrt(1365./LAL_PI)*(35. + 60.*cos(2.*theta) + 33.*cos(4.*theta))*pow(sin(theta),2))/512.0; - break; - case 1: - fac = (sqrt(65./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(161. - 252.*cos(theta) + 252.*cos(2.*theta) - - 132.*cos(3.*theta) + 99.*cos(4.*theta))*sin(theta/2.0))/64.0; - break; - case 2: - fac = (sqrt(13./LAL_PI)*pow(cos(theta/2.0),4)*(1709. - 3096.*cos(theta) + 2340.*cos(2.*theta) - - 1320*cos(3*theta) + 495*cos(4*theta)))/256.0; - break; - case 3: - fac = (-3.*sqrt(13./LAL_PI)*pow(cos(theta/2.0),5)*(-98. + 185.*cos(theta) - 110.*cos(2*theta) - + 55.*cos(3.*theta))*sin(theta/2.0))/32.0; - break; - case 4: - fac = (sqrt(195./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(35. - 44.*cos(theta) - + 33.*cos(2*theta))*pow(sin(theta/2.0),2))/8.0; - break; - case 5: - fac = -(sqrt(2145./LAL_PI)*pow(cos(theta/2.0),7)*(-1. + 3.*cos(theta))*pow(sin(theta/2.0),3))/2.0; - break; - case 6: - fac = (3.*sqrt(715./LAL_PI)*pow(cos(theta/2.0),8)*pow(sin(theta/2.0),4))/2.0; - break; - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } - } /* l==6 */ - else if ( l == 7 ) - { - switch ( m ) - { - case -7: - fac = sqrt(15015./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*pow(sin(theta/2.0),9); - break; - case -6: - fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),4)*(2. + 7.*cos(theta))*pow(sin(theta/2.0),8))/2.0; - break; - case -5: - fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(93. + 104.*cos(theta) - + 91.*cos(2.*theta))*pow(sin(theta/2.0),7))/8.0; - break; - case -4: - fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(140. + 285.*cos(theta) - + 156.*cos(2.*theta) + 91.*cos(3.*theta))*pow(sin(theta/2.0),6))/16.0; - break; - case -3: - fac = (sqrt(15./(2.0*LAL_PI))*cos(theta/2.0)*(3115. + 5456.*cos(theta) + 4268.*cos(2.*theta) - + 2288.*cos(3.*theta) + 1001.*cos(4.*theta))*pow(sin(theta/2.0),5))/128.0; - break; - case -2: - fac = (sqrt(15./LAL_PI)*(5220. + 9810.*cos(theta) + 7920.*cos(2.*theta) + 5445.*cos(3.*theta) - + 2860.*cos(4.*theta) + 1001.*cos(5.*theta))*pow(sin(theta/2.0),4))/512.0; - break; - case -1: - fac = (3.*sqrt(5./(2.0*LAL_PI))*cos(theta/2.0)*(1890. + 4130.*cos(theta) + 3080.*cos(2.*theta) - + 2805.*cos(3.*theta) + 1430.*cos(4.*theta) + 1001.*cos(5*theta))*pow(sin(theta/2.0),3))/512.0; - break; - case 0: - fac = (3.*sqrt(35./LAL_PI)*cos(theta)*(109. + 132.*cos(2.*theta) - + 143.*cos(4.*theta))*pow(sin(theta),2))/512.0; - break; - case 1: - fac = (3.*sqrt(5./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(-1890. + 4130.*cos(theta) - 3080.*cos(2.*theta) - + 2805.*cos(3.*theta) - 1430.*cos(4.*theta) + 1001.*cos(5.*theta))*sin(theta/2.0))/512.0; - break; - case 2: - fac = (sqrt(15./LAL_PI)*pow(cos(theta/2.0),4)*(-5220. + 9810.*cos(theta) - 7920.*cos(2.*theta) - + 5445.*cos(3.*theta) - 2860.*cos(4.*theta) + 1001.*cos(5.*theta)))/512.0; - break; - case 3: - fac = -(sqrt(15./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(3115. - 5456.*cos(theta) + 4268.*cos(2.*theta) - - 2288.*cos(3.*theta) + 1001.*cos(4.*theta))*sin(theta/2.0))/128.0; - break; - case 4: - fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(-140. + 285.*cos(theta) - 156.*cos(2*theta) - + 91.*cos(3.*theta))*pow(sin(theta/2.0),2))/16.0; - break; - case 5: - fac = -(sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),7)*(93. - 104.*cos(theta) - + 91.*cos(2.*theta))*pow(sin(theta/2.0),3))/8.0; - break; - case 6: - fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),8)*(-2. + 7.*cos(theta))*pow(sin(theta/2.0),4))/2.0; - break; - case 7: - fac = -(sqrt(15015./(2.0*LAL_PI))*pow(cos(theta/2.0),9)*pow(sin(theta/2.0),5)); - break; - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } - } /* l==7 */ - else if ( l == 8 ) - { - switch ( m ) - { - case -8: - fac = sqrt(34034./LAL_PI)*pow(cos(theta/2.0),6)*pow(sin(theta/2.0),10); - break; - case -7: - fac = sqrt(17017./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(1. + 4.*cos(theta))*pow(sin(theta/2.0),9); - break; - case -6: - fac = sqrt(255255./LAL_PI)*pow(cos(theta/2.0),4)*(1. + 2.*cos(theta)) - *sin(LAL_PI/4.0 - theta/2.0)*sin(LAL_PI/4.0 + theta/2.0)*pow(sin(theta/2.0),8); - break; - case -5: - fac = (sqrt(12155./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(19. + 42.*cos(theta) - + 21.*cos(2.*theta) + 14.*cos(3.*theta))*pow(sin(theta/2.0),7))/8.0; - break; - case -4: - fac = (sqrt(935./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(265. + 442.*cos(theta) + 364.*cos(2.*theta) - + 182.*cos(3.*theta) + 91.*cos(4.*theta))*pow(sin(theta/2.0),6))/32.0; - break; - case -3: - fac = (sqrt(561./(2.0*LAL_PI))*cos(theta/2.0)*(869. + 1660.*cos(theta) + 1300.*cos(2.*theta) - + 910.*cos(3.*theta) + 455.*cos(4.*theta) + 182.*cos(5.*theta))*pow(sin(theta/2.0),5))/128.0; - break; - case -2: - fac = (sqrt(17./LAL_PI)*(7626. + 14454.*cos(theta) + 12375.*cos(2.*theta) + 9295.*cos(3.*theta) - + 6006.*cos(4.*theta) + 3003.*cos(5.*theta) + 1001.*cos(6.*theta))*pow(sin(theta/2.0),4))/512.0; - break; - case -1: - fac = (sqrt(595./(2.0*LAL_PI))*cos(theta/2.0)*(798. + 1386.*cos(theta) + 1386.*cos(2.*theta) - + 1001.*cos(3.*theta) + 858.*cos(4.*theta) + 429.*cos(5.*theta) + 286.*cos(6.*theta))*pow(sin(theta/2.0),3))/512.0; - break; - case 0: - fac = (3.*sqrt(595./LAL_PI)*(210. + 385.*cos(2.*theta) + 286.*cos(4.*theta) - + 143.*cos(6.*theta))*pow(sin(theta),2))/4096.0; - break; - case 1: - fac = (sqrt(595./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(798. - 1386.*cos(theta) + 1386.*cos(2.*theta) - - 1001.*cos(3.*theta) + 858.*cos(4.*theta) - 429.*cos(5.*theta) + 286.*cos(6.*theta))*sin(theta/2.0))/512.0; - break; - case 2: - fac = (sqrt(17./LAL_PI)*pow(cos(theta/2.0),4)*(7626. - 14454.*cos(theta) + 12375.*cos(2.*theta) - - 9295.*cos(3.*theta) + 6006.*cos(4.*theta) - 3003.*cos(5.*theta) + 1001.*cos(6.*theta)))/512.0; - break; - case 3: - fac = -(sqrt(561./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(-869. + 1660.*cos(theta) - 1300.*cos(2.*theta) - + 910.*cos(3.*theta) - 455.*cos(4.*theta) + 182.*cos(5.*theta))*sin(theta/2.0))/128.0; - break; - case 4: - fac = (sqrt(935./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(265. - 442.*cos(theta) + 364.*cos(2.*theta) - - 182.*cos(3.*theta) + 91.*cos(4.*theta))*pow(sin(theta/2.0),2))/32.0; - break; - case 5: - fac = -(sqrt(12155./(2.0*LAL_PI))*pow(cos(theta/2.0),7)*(-19. + 42.*cos(theta) - 21.*cos(2.*theta) - + 14.*cos(3.*theta))*pow(sin(theta/2.0),3))/8.0; - break; - case 6: - fac = sqrt(255255./LAL_PI)*pow(cos(theta/2.0),8)*(-1. + 2.*cos(theta))*sin(LAL_PI/4.0 - theta/2.0) - *sin(LAL_PI/4.0 + theta/2.0)*pow(sin(theta/2.0),4); - break; - case 7: - fac = -(sqrt(17017./(2.0*LAL_PI))*pow(cos(theta/2.0),9)*(-1. + 4.*cos(theta))*pow(sin(theta/2.0),5)); - break; - case 8: - fac = sqrt(34034./LAL_PI)*pow(cos(theta/2.0),10)*pow(sin(theta/2.0),6); - break; - default: - XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - break; - } - } /* l==8 */ - else - { - XLALPrintError("XLAL Error - %s: Unsupported mode l=%d (only l in [2,8] implemented)\n", __func__, l); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - } - } - else - { - XLALPrintError("XLAL Error - %s: Unsupported mode s=%d (only s=-2 implemented)\n", __func__, s); - XLAL_ERROR_VAL(0, XLAL_EINVAL); - } - if (m) - ans = cpolar(1.0, m*phi) * fac; - else - ans = fac; - return ans; -} - - -/** - * Computes the scalar spherical harmonic \f$ Y_{lm}(\theta, \phi) \f$. - */ -int -XLALScalarSphericalHarmonic( - COMPLEX16 *y, /**< output */ - UINT4 l, /**< value of l */ - INT4 m, /**< value of m */ - REAL8 theta, /**< angle theta */ - REAL8 phi /**< angle phi */ - ) -{ - - int gslStatus; - gsl_sf_result pLm; - - INT4 absM = abs( m ); - - if ( absM > (INT4) l ) - { - XLAL_ERROR( XLAL_EINVAL ); - } - - /* For some reason GSL will not take negative m */ - /* We will have to use the relation between sph harmonics of +ve and -ve m */ - XLAL_CALLGSL( gslStatus = gsl_sf_legendre_sphPlm_e((INT4)l, absM, cos(theta), &pLm ) ); - if (gslStatus != GSL_SUCCESS) - { - XLALPrintError("Error in GSL function\n" ); - XLAL_ERROR( XLAL_EFUNC ); - } - - /* Compute the values for the spherical harmonic */ - *y = cpolar(pLm.val, m * phi); - - /* If m is negative, perform some jiggery-pokery */ - if ( m < 0 && absM % 2 == 1 ) - { - *y = - *y; - } - - return XLAL_SUCCESS; -} - -/** - * Computes the spin 2 weighted spherical harmonic. This function is now - * deprecated and will be removed soon. All calls should be replaced with - * calls to XLALSpinWeightedSphericalHarmonic(). - */ -INT4 XLALSphHarm ( COMPLEX16 *out, /**< output */ - UINT4 L, /**< value of L */ - INT4 M, /**< value of M */ - REAL4 theta, /**< angle with respect to the z axis */ - REAL4 phi /**< angle with respect to the x axis */ - ) -{ - - XLAL_PRINT_DEPRECATION_WARNING("XLALSpinWeightedSphericalHarmonic"); - - *out = XLALSpinWeightedSphericalHarmonic( theta, phi, -2, L, M ); - if ( xlalErrno ) - { - XLAL_ERROR( XLAL_EFUNC ); - } - - return XLAL_SUCCESS; -} - -/** - * Computes the n-th Jacobi polynomial for polynomial weights alpha and beta. - * The implementation here is only valid for real x -- enforced by the argument - * type. An extension to complex values would require evaluation of several - * gamma functions. - * - * See http://en.wikipedia.org/wiki/Jacobi_polynomials - */ -double XLALJacobiPolynomial(int n, int alpha, int beta, double x){ - double f1 = (x-1)/2.0, f2 = (x+1)/2.0; - int s=0; - double sum=0, val=0; - if( n == 0 ) return 1.0; - for( s=0; n-s >= 0; s++ ){ - val=1.0; - val *= gsl_sf_choose( n+alpha, s ); - val *= gsl_sf_choose( n+beta, n-s ); - if( n-s != 0 ) val *= pow( f1, n-s ); - if( s != 0 ) val*= pow( f2, s ); - - sum += val; - } - return sum; -} - -/** - * Computes the 'little' d Wigner matrix for the Euler angle beta. Single angle - * small d transform with major index 'l' and minor index transition from m to - * mp. - * - * Uses a slightly unconventional method since the intuitive version by Wigner - * is less suitable to algorthmic development. - * - * See http://en.wikipedia.org/wiki/Wigner_D-matrix#Wigner_.28small.29_d-matrix - */ -#define MIN(a,b) ((a) < (b) ? (a) : (b)) -double XLALWignerdMatrix( - int l, /**< mode number l */ - int mp, /**< mode number m' */ - int m, /**< mode number m */ - double beta /**< euler angle (rad) */ - ) -{ - - int k = MIN( l+m, MIN( l-m, MIN( l+mp, l-mp ))); - double a=0, lam=0; - if(k == l+m){ - a = mp-m; - lam = mp-m; - } else if(k == l-m) { - a = m-mp; - lam = 0; - } else if(k == l+mp) { - a = m-mp; - lam = 0; - } else if(k == l-mp) { - a = mp-m; - lam = mp-m; - } - - int b = 2*l-2*k-a; - double pref = pow(-1, lam) * sqrt(gsl_sf_choose( 2*l-k, k+a )) / sqrt(gsl_sf_choose( k+b, b )); - - return pref * pow(sin(beta/2.0), a) * pow( cos(beta/2.0), b) * XLALJacobiPolynomial(k, a, b, cos(beta)); - -} - -/** - * Computes the full Wigner D matrix for the Euler angle alpha, beta, and gamma - * with major index 'l' and minor index transition from m to mp. - * - * Uses a slightly unconventional method since the intuitive version by Wigner - * is less suitable to algorthmic development. - * - * See http://en.wikipedia.org/wiki/Wigner_D-matrix - * - * Currently only supports the modes which are implemented for the spin - * weighted spherical harmonics. - */ -COMPLEX16 XLALWignerDMatrix( - int l, /**< mode number l */ - int mp, /**< mode number m' */ - int m, /**< mode number m */ - double alpha, /**< euler angle (rad) */ - double beta, /**< euler angle (rad) */ - double gam /**< euler angle (rad) */ - ) -{ - return cexp( -(1.0I)*mp*alpha ) * - XLALWignerdMatrix( l, mp, m, beta ) * - cexp( -(1.0I)*m*gam ); -} diff --git a/docs/make_literate.jl b/docs/make_literate.jl index 74fd7c3f..12312d89 100644 --- a/docs/make_literate.jl +++ b/docs/make_literate.jl @@ -72,49 +72,3 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files inputfile = joinpath(root, file) generate_markdown(inputfile) end - - - -# # See LiveServer.jl docs for this: https://juliadocs.org/LiveServer.jl/dev/man/ls+lit/ -# literate_input = joinpath(@__DIR__, "literate_input") -# literate_output = joinpath(docs_src_dir, "literate_output") -# relative_literate_output = relpath(literate_output, docs_src_dir) -# relative_convention_comparisons = joinpath(relative_literate_output, "conventions", "comparisons") -# rm(literate_output; force=true, recursive=true) -# skip_input_files = ( # Non-.jl files will be skipped anyway -# "ConventionsUtilities.jl", # Used for TestItemRunners.jl -# "ConventionsSetup.jl", # Used for TestItemRunners.jl -# ) -# for (root, _, files) ∈ walkdir(literate_input), file ∈ files -# # Skip some files -# if splitext(file)[2] != ".jl" || file ∈ skip_input_files -# continue -# end -# # full path to a literate script -# inputfile = joinpath(root, file) -# # generated output path -# output_path = splitdir(replace(inputfile, literate_input=>literate_output))[1] -# # Ensure the output path is in .gitignore -# ensure_in_gitignore(relpath(output_path, @__DIR__)) -# # generate the markdown file calling Literate -# Literate.markdown(inputfile, output_path; documenter, mdstrings, execute) -# end - -# Make "lalsuite_SphericalHarmonics.c" available in the docs -let - inputfile = joinpath(literate_input, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c") - outputfile = joinpath(docs_src_dir, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c") - ensure_in_gitignore(relpath(replace(outputfile, ".c"=>".md"), package_root)) - lalsource = read( - joinpath(literate_input, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c"), - String - ) - write( - joinpath(docs_src_dir, "conventions", "comparisons", "lalsuite_SphericalHarmonics.md"), - "# LALSuite: Spherical Harmonics original source code\n" - * "The official repository is [here](" - * "https://git.ligo.org/lscsoft/lalsuite/-/blob/22e4cd8fff0487c7b42a2c26772ae9204c995637/lal/lib/utilities/SphericalHarmonics.c" - * ")\n" - * "```c\n$lalsource\n```\n" - ) -end From 2686fbc4f188cce5080ef75deea4cae302fc1503 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 20:44:30 -0400 Subject: [PATCH 164/329] Clean up old comments --- docs/make_literate.jl | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/docs/make_literate.jl b/docs/make_literate.jl index 12312d89..cf46a901 100644 --- a/docs/make_literate.jl +++ b/docs/make_literate.jl @@ -1,34 +1,14 @@ -### Currently, all the generated output goes into the `docs/src/literate_output` directory. -### This is nice just because it lets me add just that directory to the `.gitignore` file; -### I can't add `docs/src` to the `.gitignore` file because it would ignore all the -### non-generated files in that directory. However, it would be nice to have the generated -### files in directories that are more consistent with the documentation structure. For -### example, the `docs/src/literate_output/conventions/comparisons` directory contains files -### that really should be in the `docs/src/conventions/comparisons` directory. I intend to -### reorganize the functionality in this file so that the outputs are in directories that -### are more consistent with the documentation structure. -### -### Instead of just plain for loops doing all the work, I will create functions that -### encapsulate things, and then call those functions in the for loops. This will make it -### easier to add new functionality in the future, and make it easier to read the code. -### -### To deal with the gitignore issue, I will add a step that ensures the output file is -### listed in the `.gitignore` file. -### -### The files in the `docs/src/literate_input` directory will be rearranged so that they -### are in directories that are more consistent with the documentation structure. For -### example, the `docs/literate_input/conventions/comparisons` directory will be -### moved to `docs/literate_input/conventions/comparisons`, and the output for every file in -### that directory will be sent to `docs/src/conventions/comparisons`. There will no longer -### be a `docs/src/literate_output` directory; all the output will be in the same -### directory as the non-generated files. +# This file is intended to be included in the `docs/make.jl` file, and is responsible for +# converting the Literate-formatted scripts in `docs/literate_input` into +# Documenter-friendly markdown files in `docs/src`. -literate_input = joinpath(@__DIR__, "literate_input") +# Set up which files will be converted, and which will be skipped skip_input_files = ( # Non-.jl files will be skipped anyway "ConventionsUtilities.jl", # Used for TestItemRunners.jl "ConventionsSetup.jl", # Used for TestItemRunners.jl "conventions_install_lalsuite.jl", # lalsuite_2025.jl ) +literate_input = joinpath(@__DIR__, "literate_input") # Ensure a file is listed in the .gitignore file function ensure_in_gitignore(file_path) @@ -63,12 +43,15 @@ function generate_markdown(inputfile) Literate.markdown(inputfile, outputdir; documenter, mdstrings, execute) end +# Now, just walk through the literate_input directory and generate the markdown files for +# each literate script. for (root, _, files) ∈ walkdir(literate_input), file ∈ files # Skip some files if splitext(file)[2] != ".jl" || file ∈ skip_input_files continue end - # full path to a literate script + # Full path to the literate script inputfile = joinpath(root, file) + # Run the conversion generate_markdown(inputfile) end From 4378db3557c2e089f27afa5cd977101652dbb6bb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 01:50:11 -0400 Subject: [PATCH 165/329] Try (and fail) to avoid python segfaults with lalsuite --- .../conventions_install_lalsuite.jl | 4 ++ .../conventions/comparisons/lalsuite_2025.jl | 70 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl b/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl index c3c738dd..38ca5ef6 100644 --- a/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl +++ b/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl @@ -1,3 +1,7 @@ +# First, we set this, in hopes of avoiding "benign" segfaults associated with garbage +# collection. +ENV["PYTHON_JULIACALL_HANDLE_SIGNALS"] = "yes" + # Construct the CondaPkg.toml file to use to make sure we get the right Python version and # we get pip installed. conda_pkg_toml = """ diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index efa18161..0ebefdad 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -2,16 +2,13 @@ md""" # LALSuite (2025) !!! info "Summary" - The LALSuite definitions of the spherical harmonics and Wigner's ``d`` and ``D`` + The `LALSuite`` definitions of the spherical harmonics and Wigner's ``d`` and ``D`` functions agree with the definitions used in the `SphericalFunctions` package. -""" -md""" -[LALSuite (LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of software +[`LALSuite` (the LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of routines, comprising the primary official software used by the LIGO-Virgo-KAGRA Collaboration to detect and characterize gravitational waves. As far as I can tell, the -ultimate source for all spin-weighted spherical harmonic values used in LALSuite is the -function +ultimate source for all spin-weighted spherical harmonics used in `LALSuite` is the function [`XLALSpinWeightedSphericalHarmonic`](https://git.ligo.org/lscsoft/lalsuite/-/blob/6e653c91b6e8a6728c4475729c4f967c9e09f020/lal/lib/utilities/SphericalHarmonics.c), which cites the NINJA paper [AjithEtAl_2011](@cite) as its source. Unfortunately, it cites version *1*, which contained a serious error, using ``\tfrac{\cos\iota}{2}`` instead of @@ -27,14 +24,20 @@ consistent with the definitions in the NINJA paper, which are consistent with th definitions in the `SphericalFunctions` package. -## Implementing formulas +## Implementing code We will call the python module `lal` directly, but there are some minor inconveniences to -deal with first. We have to install the `lalsuite` package, but we don't want all its -dependencies, so we run `python -m pip install --no-deps lalsuite`. Then, we have to -translate to native Julia types, so we'll just write three quick and easy wrappers. We -encapsulate the formulas in a module so that we can test them against the -`SphericalFunctions` package. +deal with first. We have to install the `lalsuite` python package, but we don't want all +its dependencies; only `numpy` is required for what we want to do, so we run +```bash +python -m pip install -q numpy +python -m pip install -q --no-deps lalsuite +``` +The details are messy, so we hide them in a separate file, and just include it here. + +Then, we have to translate to native Julia types, so we'll just write three quick and easy +wrappers for the three functions we will actually test. We encapsulate the formulas in a +module so that we can test them against the `SphericalFunctions` package. """ using TestItems: @testitem #hide @@ -43,28 +46,27 @@ using TestItems: @testitem #hide module LALSuite include("conventions_install_lalsuite.jl") -import PythonCall +import PythonCall const lal = PythonCall.pyimport("lal") -function SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) - PythonCall.pyconvert( - ComplexF64, - lal.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m), - ) -end -function WignerdMatrix(ℓ, m′, m, β) - PythonCall.pyconvert( - Float64, - lal.WignerdMatrix(ℓ, m′, m, β), - ) -end -function WignerDMatrix(ℓ, m′, m, α, β, γ) - PythonCall.pyconvert( - ComplexF64, - lal.WignerDMatrix(ℓ, m′, m, α, β, γ), - ) -end +## COMPLEX16 XLALSpinWeightedSphericalHarmonic( REAL8 theta, REAL8 phi, int s, int l, int m ) +SpinWeightedSphericalHarmonic(theta, phi, s, l, m) = copy(PythonCall.pyconvert( + ComplexF64, + lal.SpinWeightedSphericalHarmonic(theta, phi, s, l, m) +)) + +## double XLALWignerdMatrix( int l, int mp, int m, double beta ) +WignerdMatrix(l, mp, m, beta) = PythonCall.pyconvert( + Float64, + lal.WignerdMatrix(l, mp, m, beta) +) + +## COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ) +WignerDMatrix(l, mp, m, alpha, beta, gam) = copy(PythonCall.pyconvert( + ComplexF64, + lal.WignerDMatrix(l, mp, m, alpha, beta, gam) +)) end # module LALSuite #+ @@ -72,7 +74,7 @@ end # module LALSuite # ## Tests # -# We can now test the functions against the equivalent functions from the +# We can now test the `LALSuite` functions against the equivalent functions from the # `SphericalFunctions` package. We will need to test approximate floating-point equality, # so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: ϵₐ = 100eps() @@ -117,6 +119,7 @@ end # restrict the range of the tests to avoid excessive computation. We will test up to ℓₘₐₓ = 2 #+ + # because the space of options for disagreement is smaller. for (α,β,γ) ∈ αβγrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) @@ -125,11 +128,10 @@ for (α,β,γ) ∈ αβγrange() end end @test_broken false # We haven't flipped the conjugation of D yet - #+ # These successful tests show that the spin-weighted spherical harmonics and the Wigner -# ``d`` and ``D`` matrices defined in LALSuite agree with the corresponding functions +# ``d`` and ``D`` matrices defined in `LALSuite` agree with the corresponding functions # defined by the `SphericalFunctions` package. end #hide From a8a9f615cbe6210b11eb6803eef76211d70bea5e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 02:20:10 -0400 Subject: [PATCH 166/329] Revert to old hacky way of comparing lalsuite, instead of fancy new segfaulty way via python --- .../conventions_install_lalsuite.jl | 33 - .../conventions/comparisons/lalsuite_2025.jl | 156 +++-- .../comparisons/lalsuite_SphericalHarmonics.c | 585 ++++++++++++++++++ docs/make_literate.jl | 20 +- 4 files changed, 720 insertions(+), 74 deletions(-) delete mode 100644 docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl create mode 100644 docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c diff --git a/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl b/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl deleted file mode 100644 index 38ca5ef6..00000000 --- a/docs/literate_input/conventions/comparisons/conventions_install_lalsuite.jl +++ /dev/null @@ -1,33 +0,0 @@ -# First, we set this, in hopes of avoiding "benign" segfaults associated with garbage -# collection. -ENV["PYTHON_JULIACALL_HANDLE_SIGNALS"] = "yes" - -# Construct the CondaPkg.toml file to use to make sure we get the right Python version and -# we get pip installed. -conda_pkg_toml = """ -[deps] -python = "<3.13" -pip = "" -numpy = "==2.2.4" -""" -try - open(joinpath(LOAD_PATH[2], "CondaPkg.toml"), "w") do io - write(io, conda_pkg_toml) - end -catch e - println("Error copying CondaPkg.toml: $e") -end - -# Now we'll set up the CondaPkg environment -import CondaPkg -import PythonCall - -# This ugly hack is to ensure that lalsuite is installed without any dependencies; by -# default it comes with lots of things we don't need that break all the time, so I really -# don't want to bother fixing them. The `--no-deps` flag is not supported by CondaPkg, so -# we have to use PythonCall to install it. -PythonCall.@pyexec ` -from sys import executable as python; -import subprocess; -subprocess.call([python, "-m", "pip", "install", "-q", "--no-deps", "lalsuite==7.25.1"]); -` diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index 0ebefdad..8fcebe22 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -2,7 +2,7 @@ md""" # LALSuite (2025) !!! info "Summary" - The `LALSuite`` definitions of the spherical harmonics and Wigner's ``d`` and ``D`` + The `LALSuite` definitions of the spherical harmonics and Wigner's ``d`` and ``D`` functions agree with the definitions used in the `SphericalFunctions` package. [`LALSuite` (the LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of @@ -24,49 +24,128 @@ consistent with the definitions in the NINJA paper, which are consistent with th definitions in the `SphericalFunctions` package. -## Implementing code +## Implementing formulas -We will call the python module `lal` directly, but there are some minor inconveniences to -deal with first. We have to install the `lalsuite` python package, but we don't want all -its dependencies; only `numpy` is required for what we want to do, so we run -```bash -python -m pip install -q numpy -python -m pip install -q --no-deps lalsuite +We begin by directly translating the C code of LALSuite over to Julia code. There are three +functions that we will want to compare with the definitions in this package: +```c +COMPLEX16 XLALSpinWeightedSphericalHarmonic( REAL8 theta, REAL8 phi, int s, int l, int m ); +double XLALWignerdMatrix( int l, int mp, int m, double beta ); +COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ); ``` -The details are messy, so we hide them in a separate file, and just include it here. +The [original source code](./lalsuite_SphericalHarmonics.md) (as of early 2025) is stored +alongside this file, so we will read it in to a `String` and then apply a series of regular +expressions to convert it to Julia code, parse it and evaluate it to turn it into runnable +Julia. We encapsulate the formulas in a module so that we can test them against the +`SphericalFunctions` package. -Then, we have to translate to native Julia types, so we'll just write three quick and easy -wrappers for the three functions we will actually test. We encapsulate the formulas in a -module so that we can test them against the `SphericalFunctions` package. +We begin by setting up that module, and introducing a set of basic replacements that would +usually be defined in separate C headers. """ using TestItems: @testitem #hide -@testitem "LALSuite conventions B" setup=[ConventionsSetup, Utilities] begin #hide +@testitem "LALSuite conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide module LALSuite -include("conventions_install_lalsuite.jl") - -import PythonCall -const lal = PythonCall.pyimport("lal") +using Printf: @sprintf + +const I = im +const LAL_PI = π +const XLAL_EINVAL = "XLAL Error: Invalid arguments" +MIN(a, b) = min(a, b) +gsl_sf_choose(a, b) = binomial(a, b) +pow(a, b) = a^b +cexp(a) = exp(a) +cpolar(a, b) = a * cis(b) +macro XLALPrError(msg, args...) + quote + @error @sprintf($msg, $(args...)) + end +end +#+ -## COMPLEX16 XLALSpinWeightedSphericalHarmonic( REAL8 theta, REAL8 phi, int s, int l, int m ) -SpinWeightedSphericalHarmonic(theta, phi, s, l, m) = copy(PythonCall.pyconvert( - ComplexF64, - lal.SpinWeightedSphericalHarmonic(theta, phi, s, l, m) -)) +# Next, we simply read the source file into a string. +lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) +#+ -## double XLALWignerdMatrix( int l, int mp, int m, double beta ) -WignerdMatrix(l, mp, m, beta) = PythonCall.pyconvert( - Float64, - lal.WignerdMatrix(l, mp, m, beta) +# Now we define a series of replacements to apply to the C code to convert it to Julia code. +# Note that some of these will be quite specific to this particular file, and may not be +# generally applicable. +replacements = ( + ## Deal with newlines in the middle of an assignment + r"( = .*[^;]\s*)\n" => s"\1", + + ## Remove a couple old, unused functions + r"(?ms)XLALScalarSphericalHarmonic.*?\n}" => "# Removed", + r"(?ms)XLALSphHarm.*?\n}" => "# Removed", + + ## Remove type annotations + r"COMPLEX16 ?" => "", + r"REAL8 ?" => "", + r"INT4 ?" => "", + r"int ?" => "", + r"double ?" => "", + + ## Translate comments + "/*" => "#=", + "*/" => "=#", + + ## Brackets + r" ?{" => "", + r"}.*(\n *else)" => s"\1", + r"} *else" => "else", + r"^}" => "", + "}" => "end", + + ## Flow control + r"( *if.*);"=>s"\1 end", ## one-line `if` statements + "for( s=0; n-s >= 0; s++ )" => "for s=0:n", + "else if" => "elseif", + r"(?m) break;\n *\n *case(.*?):" => s"elseif m == \1", + r"(?m) break;\n\s*case(.*?):" => s"elseif m == \1", + r"(?m) break;\n *\n *default:" => "else", + r"(?m) break;\n *default:" => "else", + r"(?m)switch.*?\n *\n( *)case(.*?):" => s"\n\1if m == \2", + r"\n *break;" => "", + r"(?m)( *ans = fac;)\n" => s"\1\n end\n", + + ## Deal with ugly C declarations + "f1 = (x-1)/2.0, f2 = (x+1)/2.0" => "f1 = (x-1)/2.0; f2 = (x+1)/2.0", + "sum=0, val=0" => "sum=0; val=0", + "a=0, lam=0" => "a=0; lam=0", + r"\n *fac;" => "", + r"\n *ans;" => "", + r"\n *gslStatus;" => "", + r"\n *gsl_sf_result pLm;" => "", + r"\n ?XLAL" => "\nfunction XLAL", + + ## Differences in Julia syntax + "++" => "+=1", + ".*" => ". *", + "./" => ". /", + ".+" => ". +", + ".-" => ". -", + + ## Deal with random bad syntax + "if (m)" => "if m != 0", + "case 4:" => "elseif m == 4", + "XLALPrError" => "@XLALPrError", + "__func__" => "\"\"", ) +#+ -## COMPLEX16 XLALWignerDMatrix( int l, int mp, int m, double alpha, double beta, double gam ) -WignerDMatrix(l, mp, m, alpha, beta, gam) = copy(PythonCall.pyconvert( - ComplexF64, - lal.WignerDMatrix(l, mp, m, alpha, beta, gam) -)) +# And we apply the replacements to the source code to convert it to Julia code. Note that +# we apply them successively, even though `replace` can handle multiple "simultaneous" +# replacements, because the order of replacements is important. +for (pattern, replacement) in replacements + global lalsource = replace(lalsource, pattern => replacement) +end +#+ + +# Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are +# done defining the module +eval(Meta.parseall(lalsource)) end # module LALSuite #+ @@ -90,7 +169,7 @@ s = -2 # so we only test up to that point. for (θ, ϕ) ∈ θϕrange() for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) - @test LALSuite.SpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ + @test LALSuite.XLALSpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end @@ -104,7 +183,7 @@ end # error. for β ∈ βrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test LALSuite.WignerdMatrix(ℓ, m′, m, β) ≈ + @test LALSuite.XLALWignerdMatrix(ℓ, m′, m, β) ≈ SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ end end @@ -115,18 +194,15 @@ end # ```c # cexp( -(1.0I)*mp*alpha ) * XLALWignerdMatrix( l, mp, m, beta ) * cexp( -(1.0I)*m*gam ); # ``` -# And because of the higher dimensionality of the space in which to test, we want to -# restrict the range of the tests to avoid excessive computation. We will test up to -ℓₘₐₓ = 2 -#+ - -# because the space of options for disagreement is smaller. for (α,β,γ) ∈ αβγrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test LALSuite.WignerDMatrix(ℓ, m′, m, α, β, γ) ≈ + @test LALSuite.XLALWignerDMatrix(ℓ, m′, m, α, β, γ) ≈ conj(SphericalFunctions.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ end end +#+ + +# Now, just to remind ourselves, we will be changing the convention for ``D`` soon @test_broken false # We haven't flipped the conjugation of D yet #+ diff --git a/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c b/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c new file mode 100644 index 00000000..3d4e56d4 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2007 S.Fairhurst, B. Krishnan, L.Santamaria, C. Robinson, + * C. Pankow + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with with program; see the file COPYING. If not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include +#include +#include + +#include +#include + +/** + * Computes the (s)Y(l,m) spin-weighted spherical harmonic. + * + * From somewhere .... + * + * See also: + * Implements Equations (II.9)-(II.13) of + * D. A. Brown, S. Fairhurst, B. Krishnan, R. A. Mercer, R. K. Kopparapu, + * L. Santamaria, and J. T. Whelan, + * "Data formats for numerical relativity waves", + * arXiv:0709.0093v1 (2007). + * + * Currently only supports s=-2, l=2,3,4,5,6,7,8 modes. + */ +COMPLEX16 XLALSpinWeightedSphericalHarmonic( + REAL8 theta, /**< polar angle (rad) */ + REAL8 phi, /**< azimuthal angle (rad) */ + int s, /**< spin weight */ + int l, /**< mode number l */ + int m /**< mode number m */ + ) +{ + REAL8 fac; + COMPLEX16 ans; + + /* sanity checks ... */ + if ( l < abs(s) ) + { + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |s| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + if ( l < abs(m) ) + { + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + + if ( s == -2 ) + { + if ( l == 2 ) + { + switch ( m ) + { + case -2: + fac = sqrt( 5.0 / ( 64.0 * LAL_PI ) ) * ( 1.0 - cos( theta ))*( 1.0 - cos( theta )); + break; + case -1: + fac = sqrt( 5.0 / ( 16.0 * LAL_PI ) ) * sin( theta )*( 1.0 - cos( theta )); + break; + + case 0: + fac = sqrt( 15.0 / ( 32.0 * LAL_PI ) ) * sin( theta )*sin( theta ); + break; + + case 1: + fac = sqrt( 5.0 / ( 16.0 * LAL_PI ) ) * sin( theta )*( 1.0 + cos( theta )); + break; + + case 2: + fac = sqrt( 5.0 / ( 64.0 * LAL_PI ) ) * ( 1.0 + cos( theta ))*( 1.0 + cos( theta )); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } /* switch (m) */ + } /* l==2*/ + else if ( l == 3 ) + { + switch ( m ) + { + case -3: + fac = sqrt(21.0/(2.0*LAL_PI))*cos(theta/2.0)*pow(sin(theta/2.0),5.0); + break; + case -2: + fac = sqrt(7.0/(4.0*LAL_PI))*(2.0 + 3.0*cos(theta))*pow(sin(theta/2.0),4.0); + break; + case -1: + fac = sqrt(35.0/(2.0*LAL_PI))*(sin(theta) + 4.0*sin(2.0*theta) - 3.0*sin(3.0*theta))/32.0; + break; + case 0: + fac = (sqrt(105.0/(2.0*LAL_PI))*cos(theta)*pow(sin(theta),2.0))/4.0; + break; + case 1: + fac = -sqrt(35.0/(2.0*LAL_PI))*(sin(theta) - 4.0*sin(2.0*theta) - 3.0*sin(3.0*theta))/32.0; + break; + + case 2: + fac = sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),4.0)*(-2.0 + 3.0*cos(theta))/2.0; + break; + + case 3: + fac = -sqrt(21.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*sin(theta/2.0); + break; + + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==3 */ + else if ( l == 4 ) + { + switch ( m ) + { + case -4: + fac = 3.0*sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),2.0)*pow(sin(theta/2.0),6.0); + break; + case -3: + fac = 3.0*sqrt(7.0/(2.0*LAL_PI))*cos(theta/2.0)*(1.0 + 2.0*cos(theta))*pow(sin(theta/2.0),5.0); + break; + + case -2: + fac = (3.0*(9.0 + 14.0*cos(theta) + 7.0*cos(2.0*theta))*pow(sin(theta/2.0),4.0))/(4.0*sqrt(LAL_PI)); + break; + case -1: + fac = (3.0*(3.0*sin(theta) + 2.0*sin(2.0*theta) + 7.0*sin(3.0*theta) - 7.0*sin(4.0*theta)))/(32.0*sqrt(2.0*LAL_PI)); + break; + case 0: + fac = (3.0*sqrt(5.0/(2.0*LAL_PI))*(5.0 + 7.0*cos(2.0*theta))*pow(sin(theta),2.0))/16.0; + break; + case 1: + fac = (3.0*(3.0*sin(theta) - 2.0*sin(2.0*theta) + 7.0*sin(3.0*theta) + 7.0*sin(4.0*theta)))/(32.0*sqrt(2.0*LAL_PI)); + break; + case 2: + fac = (3.0*pow(cos(theta/2.0),4.0)*(9.0 - 14.0*cos(theta) + 7.0*cos(2.0*theta)))/(4.0*sqrt(LAL_PI)); + break; + case 3: + fac = -3.0*sqrt(7.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*(-1.0 + 2.0*cos(theta))*sin(theta/2.0); + break; + case 4: + fac = 3.0*sqrt(7.0/LAL_PI)*pow(cos(theta/2.0),6.0)*pow(sin(theta/2.0),2.0); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==4 */ + else if ( l == 5 ) + { + switch ( m ) + { + case -5: + fac = sqrt(330.0/LAL_PI)*pow(cos(theta/2.0),3.0)*pow(sin(theta/2.0),7.0); + break; + case -4: + fac = sqrt(33.0/LAL_PI)*pow(cos(theta/2.0),2.0)*(2.0 + 5.0*cos(theta))*pow(sin(theta/2.0),6.0); + break; + case -3: + fac = (sqrt(33.0/(2.0*LAL_PI))*cos(theta/2.0)*(17.0 + 24.0*cos(theta) + 15.0*cos(2.0*theta))*pow(sin(theta/2.0),5.0))/4.0; + break; + case -2: + fac = (sqrt(11.0/LAL_PI)*(32.0 + 57.0*cos(theta) + 36.0*cos(2.0*theta) + 15.0*cos(3.0*theta))*pow(sin(theta/2.0),4.0))/8.0; + break; + case -1: + fac = (sqrt(77.0/LAL_PI)*(2.0*sin(theta) + 8.0*sin(2.0*theta) + 3.0*sin(3.0*theta) + 12.0*sin(4.0*theta) - 15.0*sin(5.0*theta)))/256.0; + break; + case 0: + fac = (sqrt(1155.0/(2.0*LAL_PI))*(5.0*cos(theta) + 3.0*cos(3.0*theta))*pow(sin(theta),2.0))/32.0; + break; + case 1: + fac = sqrt(77.0/LAL_PI)*(-2.0*sin(theta) + 8.0*sin(2.0*theta) - 3.0*sin(3.0*theta) + 12.0*sin(4.0*theta) + 15.0*sin(5.0*theta))/256.0; + break; + case 2: + fac = sqrt(11.0/LAL_PI)*pow(cos(theta/2.0),4.0)*(-32.0 + 57.0*cos(theta) - 36.0*cos(2.0*theta) + 15.0*cos(3.0*theta))/8.0; + break; + case 3: + fac = -sqrt(33.0/(2.0*LAL_PI))*pow(cos(theta/2.0),5.0)*(17.0 - 24.0*cos(theta) + 15.0*cos(2.0*theta))*sin(theta/2.0)/4.0; + break; + case 4: + fac = sqrt(33.0/LAL_PI)*pow(cos(theta/2.0),6.0)*(-2.0 + 5.0*cos(theta))*pow(sin(theta/2.0),2.0); + break; + case 5: + fac = -sqrt(330.0/LAL_PI)*pow(cos(theta/2.0),7.0)*pow(sin(theta/2.0),3.0); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==5 */ + else if ( l == 6 ) + { + switch ( m ) + { + case -6: + fac = (3.*sqrt(715./LAL_PI)*pow(cos(theta/2.0),4)*pow(sin(theta/2.0),8))/2.0; + break; + case -5: + fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),3)*(1. + 3.*cos(theta))*pow(sin(theta/2.0),7))/2.0; + break; + case -4: + fac = (sqrt(195./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(35. + 44.*cos(theta) + + 33.*cos(2.*theta))*pow(sin(theta/2.0),6))/8.0; + break; + case -3: + fac = (3.*sqrt(13./LAL_PI)*cos(theta/2.0)*(98. + 185.*cos(theta) + 110.*cos(2*theta) + + 55.*cos(3.*theta))*pow(sin(theta/2.0),5))/32.0; + break; + case -2: + fac = (sqrt(13./LAL_PI)*(1709. + 3096.*cos(theta) + 2340.*cos(2.*theta) + 1320.*cos(3.*theta) + + 495.*cos(4.*theta))*pow(sin(theta/2.0),4))/256.0; + break; + case -1: + fac = (sqrt(65./(2.0*LAL_PI))*cos(theta/2.0)*(161. + 252.*cos(theta) + 252.*cos(2.*theta) + + 132.*cos(3.*theta) + 99.*cos(4.*theta))*pow(sin(theta/2.0),3))/64.0; + break; + case 0: + fac = (sqrt(1365./LAL_PI)*(35. + 60.*cos(2.*theta) + 33.*cos(4.*theta))*pow(sin(theta),2))/512.0; + break; + case 1: + fac = (sqrt(65./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(161. - 252.*cos(theta) + 252.*cos(2.*theta) + - 132.*cos(3.*theta) + 99.*cos(4.*theta))*sin(theta/2.0))/64.0; + break; + case 2: + fac = (sqrt(13./LAL_PI)*pow(cos(theta/2.0),4)*(1709. - 3096.*cos(theta) + 2340.*cos(2.*theta) + - 1320*cos(3*theta) + 495*cos(4*theta)))/256.0; + break; + case 3: + fac = (-3.*sqrt(13./LAL_PI)*pow(cos(theta/2.0),5)*(-98. + 185.*cos(theta) - 110.*cos(2*theta) + + 55.*cos(3.*theta))*sin(theta/2.0))/32.0; + break; + case 4: + fac = (sqrt(195./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(35. - 44.*cos(theta) + + 33.*cos(2*theta))*pow(sin(theta/2.0),2))/8.0; + break; + case 5: + fac = -(sqrt(2145./LAL_PI)*pow(cos(theta/2.0),7)*(-1. + 3.*cos(theta))*pow(sin(theta/2.0),3))/2.0; + break; + case 6: + fac = (3.*sqrt(715./LAL_PI)*pow(cos(theta/2.0),8)*pow(sin(theta/2.0),4))/2.0; + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==6 */ + else if ( l == 7 ) + { + switch ( m ) + { + case -7: + fac = sqrt(15015./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*pow(sin(theta/2.0),9); + break; + case -6: + fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),4)*(2. + 7.*cos(theta))*pow(sin(theta/2.0),8))/2.0; + break; + case -5: + fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(93. + 104.*cos(theta) + + 91.*cos(2.*theta))*pow(sin(theta/2.0),7))/8.0; + break; + case -4: + fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(140. + 285.*cos(theta) + + 156.*cos(2.*theta) + 91.*cos(3.*theta))*pow(sin(theta/2.0),6))/16.0; + break; + case -3: + fac = (sqrt(15./(2.0*LAL_PI))*cos(theta/2.0)*(3115. + 5456.*cos(theta) + 4268.*cos(2.*theta) + + 2288.*cos(3.*theta) + 1001.*cos(4.*theta))*pow(sin(theta/2.0),5))/128.0; + break; + case -2: + fac = (sqrt(15./LAL_PI)*(5220. + 9810.*cos(theta) + 7920.*cos(2.*theta) + 5445.*cos(3.*theta) + + 2860.*cos(4.*theta) + 1001.*cos(5.*theta))*pow(sin(theta/2.0),4))/512.0; + break; + case -1: + fac = (3.*sqrt(5./(2.0*LAL_PI))*cos(theta/2.0)*(1890. + 4130.*cos(theta) + 3080.*cos(2.*theta) + + 2805.*cos(3.*theta) + 1430.*cos(4.*theta) + 1001.*cos(5*theta))*pow(sin(theta/2.0),3))/512.0; + break; + case 0: + fac = (3.*sqrt(35./LAL_PI)*cos(theta)*(109. + 132.*cos(2.*theta) + + 143.*cos(4.*theta))*pow(sin(theta),2))/512.0; + break; + case 1: + fac = (3.*sqrt(5./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(-1890. + 4130.*cos(theta) - 3080.*cos(2.*theta) + + 2805.*cos(3.*theta) - 1430.*cos(4.*theta) + 1001.*cos(5.*theta))*sin(theta/2.0))/512.0; + break; + case 2: + fac = (sqrt(15./LAL_PI)*pow(cos(theta/2.0),4)*(-5220. + 9810.*cos(theta) - 7920.*cos(2.*theta) + + 5445.*cos(3.*theta) - 2860.*cos(4.*theta) + 1001.*cos(5.*theta)))/512.0; + break; + case 3: + fac = -(sqrt(15./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(3115. - 5456.*cos(theta) + 4268.*cos(2.*theta) + - 2288.*cos(3.*theta) + 1001.*cos(4.*theta))*sin(theta/2.0))/128.0; + break; + case 4: + fac = (sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(-140. + 285.*cos(theta) - 156.*cos(2*theta) + + 91.*cos(3.*theta))*pow(sin(theta/2.0),2))/16.0; + break; + case 5: + fac = -(sqrt(165./(2.0*LAL_PI))*pow(cos(theta/2.0),7)*(93. - 104.*cos(theta) + + 91.*cos(2.*theta))*pow(sin(theta/2.0),3))/8.0; + break; + case 6: + fac = (sqrt(2145./LAL_PI)*pow(cos(theta/2.0),8)*(-2. + 7.*cos(theta))*pow(sin(theta/2.0),4))/2.0; + break; + case 7: + fac = -(sqrt(15015./(2.0*LAL_PI))*pow(cos(theta/2.0),9)*pow(sin(theta/2.0),5)); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==7 */ + else if ( l == 8 ) + { + switch ( m ) + { + case -8: + fac = sqrt(34034./LAL_PI)*pow(cos(theta/2.0),6)*pow(sin(theta/2.0),10); + break; + case -7: + fac = sqrt(17017./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(1. + 4.*cos(theta))*pow(sin(theta/2.0),9); + break; + case -6: + fac = sqrt(255255./LAL_PI)*pow(cos(theta/2.0),4)*(1. + 2.*cos(theta)) + *sin(LAL_PI/4.0 - theta/2.0)*sin(LAL_PI/4.0 + theta/2.0)*pow(sin(theta/2.0),8); + break; + case -5: + fac = (sqrt(12155./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(19. + 42.*cos(theta) + + 21.*cos(2.*theta) + 14.*cos(3.*theta))*pow(sin(theta/2.0),7))/8.0; + break; + case -4: + fac = (sqrt(935./(2.0*LAL_PI))*pow(cos(theta/2.0),2)*(265. + 442.*cos(theta) + 364.*cos(2.*theta) + + 182.*cos(3.*theta) + 91.*cos(4.*theta))*pow(sin(theta/2.0),6))/32.0; + break; + case -3: + fac = (sqrt(561./(2.0*LAL_PI))*cos(theta/2.0)*(869. + 1660.*cos(theta) + 1300.*cos(2.*theta) + + 910.*cos(3.*theta) + 455.*cos(4.*theta) + 182.*cos(5.*theta))*pow(sin(theta/2.0),5))/128.0; + break; + case -2: + fac = (sqrt(17./LAL_PI)*(7626. + 14454.*cos(theta) + 12375.*cos(2.*theta) + 9295.*cos(3.*theta) + + 6006.*cos(4.*theta) + 3003.*cos(5.*theta) + 1001.*cos(6.*theta))*pow(sin(theta/2.0),4))/512.0; + break; + case -1: + fac = (sqrt(595./(2.0*LAL_PI))*cos(theta/2.0)*(798. + 1386.*cos(theta) + 1386.*cos(2.*theta) + + 1001.*cos(3.*theta) + 858.*cos(4.*theta) + 429.*cos(5.*theta) + 286.*cos(6.*theta))*pow(sin(theta/2.0),3))/512.0; + break; + case 0: + fac = (3.*sqrt(595./LAL_PI)*(210. + 385.*cos(2.*theta) + 286.*cos(4.*theta) + + 143.*cos(6.*theta))*pow(sin(theta),2))/4096.0; + break; + case 1: + fac = (sqrt(595./(2.0*LAL_PI))*pow(cos(theta/2.0),3)*(798. - 1386.*cos(theta) + 1386.*cos(2.*theta) + - 1001.*cos(3.*theta) + 858.*cos(4.*theta) - 429.*cos(5.*theta) + 286.*cos(6.*theta))*sin(theta/2.0))/512.0; + break; + case 2: + fac = (sqrt(17./LAL_PI)*pow(cos(theta/2.0),4)*(7626. - 14454.*cos(theta) + 12375.*cos(2.*theta) + - 9295.*cos(3.*theta) + 6006.*cos(4.*theta) - 3003.*cos(5.*theta) + 1001.*cos(6.*theta)))/512.0; + break; + case 3: + fac = -(sqrt(561./(2.0*LAL_PI))*pow(cos(theta/2.0),5)*(-869. + 1660.*cos(theta) - 1300.*cos(2.*theta) + + 910.*cos(3.*theta) - 455.*cos(4.*theta) + 182.*cos(5.*theta))*sin(theta/2.0))/128.0; + break; + case 4: + fac = (sqrt(935./(2.0*LAL_PI))*pow(cos(theta/2.0),6)*(265. - 442.*cos(theta) + 364.*cos(2.*theta) + - 182.*cos(3.*theta) + 91.*cos(4.*theta))*pow(sin(theta/2.0),2))/32.0; + break; + case 5: + fac = -(sqrt(12155./(2.0*LAL_PI))*pow(cos(theta/2.0),7)*(-19. + 42.*cos(theta) - 21.*cos(2.*theta) + + 14.*cos(3.*theta))*pow(sin(theta/2.0),3))/8.0; + break; + case 6: + fac = sqrt(255255./LAL_PI)*pow(cos(theta/2.0),8)*(-1. + 2.*cos(theta))*sin(LAL_PI/4.0 - theta/2.0) + *sin(LAL_PI/4.0 + theta/2.0)*pow(sin(theta/2.0),4); + break; + case 7: + fac = -(sqrt(17017./(2.0*LAL_PI))*pow(cos(theta/2.0),9)*(-1. + 4.*cos(theta))*pow(sin(theta/2.0),5)); + break; + case 8: + fac = sqrt(34034./LAL_PI)*pow(cos(theta/2.0),10)*pow(sin(theta/2.0),6); + break; + default: + XLALPrintError("XLAL Error - %s: Invalid mode s=%d, l=%d, m=%d - require |m| <= l\n", __func__, s, l, m ); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + break; + } + } /* l==8 */ + else + { + XLALPrintError("XLAL Error - %s: Unsupported mode l=%d (only l in [2,8] implemented)\n", __func__, l); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + } + else + { + XLALPrintError("XLAL Error - %s: Unsupported mode s=%d (only s=-2 implemented)\n", __func__, s); + XLAL_ERROR_VAL(0, XLAL_EINVAL); + } + if (m) + ans = cpolar(1.0, m*phi) * fac; + else + ans = fac; + return ans; +} + + +/** + * Computes the scalar spherical harmonic \f$ Y_{lm}(\theta, \phi) \f$. + */ +int +XLALScalarSphericalHarmonic( + COMPLEX16 *y, /**< output */ + UINT4 l, /**< value of l */ + INT4 m, /**< value of m */ + REAL8 theta, /**< angle theta */ + REAL8 phi /**< angle phi */ + ) +{ + + int gslStatus; + gsl_sf_result pLm; + + INT4 absM = abs( m ); + + if ( absM > (INT4) l ) + { + XLAL_ERROR( XLAL_EINVAL ); + } + + /* For some reason GSL will not take negative m */ + /* We will have to use the relation between sph harmonics of +ve and -ve m */ + XLAL_CALLGSL( gslStatus = gsl_sf_legendre_sphPlm_e((INT4)l, absM, cos(theta), &pLm ) ); + if (gslStatus != GSL_SUCCESS) + { + XLALPrintError("Error in GSL function\n" ); + XLAL_ERROR( XLAL_EFUNC ); + } + + /* Compute the values for the spherical harmonic */ + *y = cpolar(pLm.val, m * phi); + + /* If m is negative, perform some jiggery-pokery */ + if ( m < 0 && absM % 2 == 1 ) + { + *y = - *y; + } + + return XLAL_SUCCESS; +} + +/** + * Computes the spin 2 weighted spherical harmonic. This function is now + * deprecated and will be removed soon. All calls should be replaced with + * calls to XLALSpinWeightedSphericalHarmonic(). + */ +INT4 XLALSphHarm ( COMPLEX16 *out, /**< output */ + UINT4 L, /**< value of L */ + INT4 M, /**< value of M */ + REAL4 theta, /**< angle with respect to the z axis */ + REAL4 phi /**< angle with respect to the x axis */ + ) +{ + + XLAL_PRINT_DEPRECATION_WARNING("XLALSpinWeightedSphericalHarmonic"); + + *out = XLALSpinWeightedSphericalHarmonic( theta, phi, -2, L, M ); + if ( xlalErrno ) + { + XLAL_ERROR( XLAL_EFUNC ); + } + + return XLAL_SUCCESS; +} + +/** + * Computes the n-th Jacobi polynomial for polynomial weights alpha and beta. + * The implementation here is only valid for real x -- enforced by the argument + * type. An extension to complex values would require evaluation of several + * gamma functions. + * + * See http://en.wikipedia.org/wiki/Jacobi_polynomials + */ +double XLALJacobiPolynomial(int n, int alpha, int beta, double x){ + double f1 = (x-1)/2.0, f2 = (x+1)/2.0; + int s=0; + double sum=0, val=0; + if( n == 0 ) return 1.0; + for( s=0; n-s >= 0; s++ ){ + val=1.0; + val *= gsl_sf_choose( n+alpha, s ); + val *= gsl_sf_choose( n+beta, n-s ); + if( n-s != 0 ) val *= pow( f1, n-s ); + if( s != 0 ) val*= pow( f2, s ); + + sum += val; + } + return sum; +} + +/** + * Computes the 'little' d Wigner matrix for the Euler angle beta. Single angle + * small d transform with major index 'l' and minor index transition from m to + * mp. + * + * Uses a slightly unconventional method since the intuitive version by Wigner + * is less suitable to algorthmic development. + * + * See http://en.wikipedia.org/wiki/Wigner_D-matrix#Wigner_.28small.29_d-matrix + */ +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +double XLALWignerdMatrix( + int l, /**< mode number l */ + int mp, /**< mode number m' */ + int m, /**< mode number m */ + double beta /**< euler angle (rad) */ + ) +{ + + int k = MIN( l+m, MIN( l-m, MIN( l+mp, l-mp ))); + double a=0, lam=0; + if(k == l+m){ + a = mp-m; + lam = mp-m; + } else if(k == l-m) { + a = m-mp; + lam = 0; + } else if(k == l+mp) { + a = m-mp; + lam = 0; + } else if(k == l-mp) { + a = mp-m; + lam = mp-m; + } + + int b = 2*l-2*k-a; + double pref = pow(-1, lam) * sqrt(gsl_sf_choose( 2*l-k, k+a )) / sqrt(gsl_sf_choose( k+b, b )); + + return pref * pow(sin(beta/2.0), a) * pow( cos(beta/2.0), b) * XLALJacobiPolynomial(k, a, b, cos(beta)); + +} + +/** + * Computes the full Wigner D matrix for the Euler angle alpha, beta, and gamma + * with major index 'l' and minor index transition from m to mp. + * + * Uses a slightly unconventional method since the intuitive version by Wigner + * is less suitable to algorthmic development. + * + * See http://en.wikipedia.org/wiki/Wigner_D-matrix + * + * Currently only supports the modes which are implemented for the spin + * weighted spherical harmonics. + */ +COMPLEX16 XLALWignerDMatrix( + int l, /**< mode number l */ + int mp, /**< mode number m' */ + int m, /**< mode number m */ + double alpha, /**< euler angle (rad) */ + double beta, /**< euler angle (rad) */ + double gam /**< euler angle (rad) */ + ) +{ + return cexp( -(1.0I)*mp*alpha ) * + XLALWignerdMatrix( l, mp, m, beta ) * + cexp( -(1.0I)*m*gam ); +} diff --git a/docs/make_literate.jl b/docs/make_literate.jl index cf46a901..ad26ac2d 100644 --- a/docs/make_literate.jl +++ b/docs/make_literate.jl @@ -6,7 +6,6 @@ skip_input_files = ( # Non-.jl files will be skipped anyway "ConventionsUtilities.jl", # Used for TestItemRunners.jl "ConventionsSetup.jl", # Used for TestItemRunners.jl - "conventions_install_lalsuite.jl", # lalsuite_2025.jl ) literate_input = joinpath(@__DIR__, "literate_input") @@ -55,3 +54,22 @@ for (root, _, files) ∈ walkdir(literate_input), file ∈ files # Run the conversion generate_markdown(inputfile) end + +# Make "lalsuite_SphericalHarmonics.c" available in the docs +let + inputfile = joinpath(literate_input, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c") + outputfile = joinpath(docs_src_dir, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c") + ensure_in_gitignore(relpath(replace(outputfile, ".c"=>".md"), package_root)) + lalsource = read( + joinpath(literate_input, "conventions", "comparisons", "lalsuite_SphericalHarmonics.c"), + String + ) + write( + joinpath(docs_src_dir, "conventions", "comparisons", "lalsuite_SphericalHarmonics.md"), + "# LALSuite: Spherical Harmonics original source code\n" + * "The official repository is [here](" + * "https://git.ligo.org/lscsoft/lalsuite/-/blob/22e4cd8fff0487c7b42a2c26772ae9204c995637/lal/lib/utilities/SphericalHarmonics.c" + * ")\n" + * "```c\n$lalsource\n```\n" + ) +end From ab691ef54bd01cb70f715c88bbc72ac4d0a45b76 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 02:45:41 -0400 Subject: [PATCH 167/329] Deal with annoying Windows line endings --- .../conventions/comparisons/lalsuite_2025.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index 8fcebe22..24beaad2 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -73,6 +73,9 @@ lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) # Note that some of these will be quite specific to this particular file, and may not be # generally applicable. replacements = ( + # Deal with annoying Windows line endings + "\r\n" => "\n", + ## Deal with newlines in the middle of an assignment r"( = .*[^;]\s*)\n" => s"\1", @@ -194,6 +197,7 @@ end # ```c # cexp( -(1.0I)*mp*alpha ) * XLALWignerdMatrix( l, mp, m, beta ) * cexp( -(1.0I)*m*gam ); # ``` +# Note that this package changed conventions in 2025 to use these signs. for (α,β,γ) ∈ αβγrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) @test LALSuite.XLALWignerDMatrix(ℓ, m′, m, α, β, γ) ≈ @@ -202,8 +206,10 @@ for (α,β,γ) ∈ αβγrange() end #+ -# Now, just to remind ourselves, we will be changing the convention for ``D`` soon +# Now, just to remind ourselves, we will be changing the convention for ``D`` soon, so the +# test above should have `conj` removed. @test_broken false # We haven't flipped the conjugation of D yet +## Remove `conj` from the test above when we do. #+ # These successful tests show that the spin-weighted spherical harmonics and the Wigner From 94ffbfae6017f37c496865e3934b07d5f62581e1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 02:59:14 -0400 Subject: [PATCH 168/329] Keep up with Quaternionic --- src/SphericalFunctions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index d058a710..8d76b706 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -6,7 +6,7 @@ using LinearAlgebra: LinearAlgebra, Bidiagonal, Diagonal, convert, ldiv!, mul! using OffsetArrays: OffsetArray, OffsetVector using ProgressMeter: Progress, next! using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, - to_euler_phases, to_spherical_coordinates + to_euler_phases, to_spherical_coordinates, basetype using StaticArrays: @SVector using SpecialFunctions, DoubleFloats using LoopVectorization: @turbo From 1fc620dd7052a181c88be2c08f46d48e51e87375 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 03:08:44 -0400 Subject: [PATCH 169/329] Include compat values for CondaPkg and PythonCall --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 4a05908f..301d3397 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] AbstractFFTs = "1" Aqua = "0.8" +CondaPkg = "0.2" Coverage = "1.6" DoubleFloats = "1" FFTW = "1" @@ -34,6 +35,7 @@ LoopVectorization = "0.12" OffsetArrays = "1.10" Printf = "1.11.0" ProgressMeter = "1" +PythonCall = "0.9" Quaternionic = "3" Random = "1" SpecialFunctions = "2" From 691306928a83049c593d4f3a0eda8242eba27506 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 11:13:42 -0400 Subject: [PATCH 170/329] Deal with annoying Windows line endings --- .gitattributes | 2 +- docs/literate_input/conventions/comparisons/lalsuite_2025.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 79ce8f35..3877f000 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -docs/literate_input/conventions_comparisons/lalsuite_SphericalHarmonics.c text eol=lf \ No newline at end of file +docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c text eol=lf \ No newline at end of file diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index 24beaad2..a27bbf9a 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -73,7 +73,7 @@ lalsource = read(joinpath(@__DIR__, "lalsuite_SphericalHarmonics.c"), String) # Note that some of these will be quite specific to this particular file, and may not be # generally applicable. replacements = ( - # Deal with annoying Windows line endings + ## Deal with annoying Windows line endings "\r\n" => "\n", ## Deal with newlines in the middle of an assignment From ceb93d1021a176dd235f51ea8bc04b913a028a36 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 12:46:53 -0400 Subject: [PATCH 171/329] Tweak wording --- .../literate_input/conventions/comparisons/lalsuite_2025.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index a27bbf9a..70d83467 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -26,8 +26,10 @@ definitions in the `SphericalFunctions` package. ## Implementing formulas -We begin by directly translating the C code of LALSuite over to Julia code. There are three -functions that we will want to compare with the definitions in this package: +Unfortunately, the `lalsuite` python package cannot be reliably tested on multiple +platforms, so we will take a much hackier, but ultimately more robust, approach. We will +simply read the C source code, and convert it to Julia code. There are three functions that +we will want to compare with the definitions in this package: ```c COMPLEX16 XLALSpinWeightedSphericalHarmonic( REAL8 theta, REAL8 phi, int s, int l, int m ); double XLALWignerdMatrix( int l, int mp, int m, double beta ); From e9fa9e2f5185df59712b0b180e58173b510ccb80 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 7 Apr 2025 11:48:52 -0400 Subject: [PATCH 172/329] A little more detail showing the D convention --- docs/src/conventions/details.md | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index ef7fd466..f2d5c740 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -989,15 +989,39 @@ first and last Euler angles (Eq. 3.5.50): \langle \ell, m' | \exp[-iL_y \beta] | \ell, m \rangle. \end{aligned} ``` +To belabor this point, recall that in general +```math +\left(\left\langle \psi | A\, B\, C | \chi \right\rangle\right)^\ast += +\left\langle \chi | C^\dag\, B^\dag\, A^\dag | \psi \right\rangle, +``` +and +```math +\left( e^{-i \epsilon L_u} \right)^\dag += +e^{i \epsilon L_u^\dag} += +e^{i \epsilon L_u}. +``` +Together with the eigenvalue property for the ``L_z`` operator acting +on a ket, this allows us to derive the above result by factoring out +the first and last operators. - +Now we are left with the middle operator, which we use to define +```math +\begin{aligned} +d^{(\ell)}_{m',m}(\beta) +&= +\langle \ell, m' | \exp[-iL_y \beta] | \ell, m \rangle. +\end{aligned} +``` Using ```math L_y = (L₊ − L₋) / (2i) ``` we can expand ```math -exp[-iL_y β] +\exp[-iL_y β] = Σ_k (-iL_y β)^k / k! = @@ -1006,11 +1030,11 @@ exp[-iL_y β] Now, writing ``d_+(X) = [L_+, X]``, Eq. (9) of https://arxiv.org/pdf/1707.03861 says ```math -(L₋ - L₊)^k = \sum_{j=0}^k \binom{k, j} ((L₋ - d_+)^j 1) (-L₊)^{k-j} +(L₋ - L₊)^k = \sum_{j=0}^k \binom{k}{j} \left((L₋ - d_+)^j 1\right) (-L₊)^{k-j} ``` The sum will automatically be zero unless ``m+k-j ≤ ℓ`` — which means ``j ≥ m+k-ℓ`` ```math -(-L₊)^{k-j}|ℓ,m\rangle = (-1)^{k-j} \sqrt{\frac{(\ell+m+k-j)!}{(\ell+m)!},\frac{(\ell-m)!}{(\ell-m-k+j)!}} |ℓ,m+k-j\rangle +(-L₊)^{k-j}|ℓ,m\rangle = (-1)^{k-j} \sqrt{\frac{(\ell+m+k-j)!}{(\ell+m)!}\,\frac{(\ell-m)!}{(\ell-m-k+j)!}} |ℓ,m+k-j\rangle ``` ``[L₊, L₋] = 2 L_z`` From 7f067b5a6fdb64f7ea2aa838d040eafd389f3b6a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 7 Apr 2025 11:49:46 -0400 Subject: [PATCH 173/329] More detail on sph.coord./Euler/quaternion equivalences --- docs/src/conventions/summary.md | 134 ++++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 32 deletions(-) diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index e7ac49bd..f00647f9 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -20,7 +20,7 @@ unit basis vectors ``(𝐱, 𝐲, 𝐳)``. ## Spherical coordinates We define spherical coordinates ``(r, \theta, \phi)`` and unit basis -vectors ``(𝐫, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar +vectors ``(𝐧, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar angle" ``\theta \in [0, \pi]`` measures the angle between the specified direction and the positive ``𝐳`` axis. The "azimuthal angle" ``\phi \in [0, 2\pi)`` measures the angle between the @@ -29,11 +29,17 @@ the positive ``𝐱`` axis, with the positive ``𝐲`` axis corresponding to the positive angle ``\phi = \pi/2``. ## Quaternions -A quaternion is written ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢𝐣𝐤 = --1``. In software, this quaternion is represented by ``(W, X, Y, -Z)``. We will depict a three-dimensional vector ``𝐯 = v_x 𝐱 + v_y -𝐲 + v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + v_z -𝐤``. +A quaternion is written ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢^2 = +𝐣^2 = 𝐤^2 = 𝐢𝐣𝐤 = -1``. In the code, this quaternion is +represented by ``(W, X, Y, Z)``. + +We will frequently depict a three-dimensional vector ``𝐯 = v_x 𝐱 + +v_y 𝐲 + v_z 𝐳`` interchangeably as a quaternion ``v_x 𝐢 + v_y 𝐣 + +v_z 𝐤``. Even though they really belong to different spaces, there +is a (vector-space) isomorphism between them, and the subspaces are +even isomorphic under the *algebra* isomorphism given by duality in +the geometric algebra. This translation allows us to operate on +vectors as if they were quaternions, and vice versa. ## Quaternion rotations A rotation represented by the unit quaternion ``𝐑`` acts on a vector @@ -41,26 +47,70 @@ A rotation represented by the unit quaternion ``𝐑`` acts on a vector assumed to be right-handed, so that a quaternion characterizing the rotation through an angle ``\vartheta`` about a unit vector ``𝐮`` can be expressed as ``𝐑 = \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` -would deliver the same *rotation*, which makes the group of unit +would deliver the same *rotation*, which means that the group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* of the group of rotations ``\mathrm{SO}(3)``. Nonetheless, ``𝐑`` and ``-𝐑`` are distinct quaternions, and represent distinct "spinors". -## Euler angles -Euler angles parametrize a unit quaternion as ``𝐑 = \exp(\alpha -𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2)``. The angles ``\alpha`` -and ``\gamma`` take values in ``[0, 2\pi)``. The angle ``\beta`` -takes values in ``[0, 2\pi]`` to parametrize the group of unit -quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, \pi]`` -to parametrize the group of rotations ``\mathrm{SO}(3)``. - -## Spherical coordinates as Euler angles +## Spherical coordinates as quaternions A point on the unit sphere with spherical coordinates ``(\theta, -\phi)`` can be represented by Euler angles ``(\alpha, \beta, \gamma) = -(\phi, \theta, 0)``. The rotation with these Euler angles takes the -positive ``𝐳`` axis to the specified direction. In particular, any -function of spherical coordinates can be promoted to a function on -Euler angles using this identification. +\phi)`` can be represented by the unit quaternion +```math +𝐑_{\theta, \phi} += +\exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2). +``` +This not only takes the positive ``𝐳`` axis to the specified +direction, but also takes the ``𝐱`` and ``𝐲`` axes onto the unit +basis vectors of the spherical coordinate system: +```math +\begin{aligned} +𝐧 &= 𝐑_{\theta, \phi}\, 𝐳\, 𝐑_{\theta, \phi}^{-1}, \\ +\boldsymbol{\theta} &= 𝐑_{\theta, \phi}\, 𝐱\, 𝐑_{\theta, \phi}^{-1}, \\ +\boldsymbol{\phi} &= 𝐑_{\theta, \phi}\, 𝐲\, 𝐑_{\theta, \phi}^{-1}. +\end{aligned} +``` + +## Euler angles (and spherical coordinates) +Euler angles parametrize a unit quaternion as +```math +𝐑_{\alpha, \beta, \gamma} += +\exp(\alpha 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2). +``` +The angles ``\alpha`` and ``\gamma`` take values in ``[0, 2\pi)``. +The angle ``\beta`` takes values in ``[0, 2\pi]`` to parametrize the +group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in +``[0, \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. + +By comparison, we can immediately see that spherical coordinates +``(\theta, \phi)`` can be represented as Euler angles with the +equivalence ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. In +particular, any function of spherical coordinates can be promoted to a +function on Euler angles using this identification. + +It's worth noting that the action of Euler angles on the Cartesian +basis is similar to the action of the spherical-coordinate quaternion, +but rotates the tangent basis ($\boldsymbol{\theta}, +\boldsymbol{\phi}$). That is, we still have +```math +𝐧 = 𝐑_{\phi, \theta, \gamma}\, 𝐳\, 𝐑_{\phi, \theta, \gamma}^{-1}, +``` +but the action on the ``𝐱`` and ``𝐲`` axes is a little more +complicated due to the initial rotation by ``\exp(\gamma 𝐤/2)``, +which is equivalent to a *final* rotation through ``\gamma`` about +``𝐧``. It's easier to write this down if we form the combination +```math +𝐦 = \frac{1}{\sqrt{2}} \left( + \boldsymbol{\theta} + i \boldsymbol{\phi} +\right), +``` +and find that +```math +𝐦 = e^{-i\gamma} 𝐑_{\phi, \theta, \gamma}\, \frac{1}{\sqrt{2}} \left( + 𝐱 + i 𝐲 +\right)\, 𝐑_{\phi, \theta, \gamma}^{-1}. +``` ## Left and right angular-momentum operators For a complex-valued function ``f(𝐑)``, we define two operators, the @@ -153,7 +203,12 @@ definitions of the spherical harmonics, so we adopt a function that is consistent with the standard expressions. More specifically, this package defines the spherical harmonics in terms of Wigner's 𝔇 matrices, by way of the spin-weighted spherical harmonics, as a -function of a quaternion. +function of a quaternion: +```math +Y_{l,m}(\mathbf{Q}) = \sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + D^{(l)}_{m,0}(\mathbf{Q}), +``` +where ``D^{(l)}_{m,0}`` is the Wigner 𝔇 matrix. This is a For concreteness, however, we can write the standard expression in terms of spherical coordinates. This is what our definition will @@ -169,8 +224,8 @@ terms of spherical coordinates, that expression is \frac{(-1)^k \ell! [(\ell+m)!(\ell-m)!]^{1/2}} {(\ell+m-k)!(\ell-k)!k!(k-m)!} \\ &\qquad \times - \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m-2k} - \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-m} + \left(\cos\left(\frac{\theta}{2}\right)\right)^{2\ell+m-2k} + \left(\sin\left(\frac{\theta}{2}\right)\right)^{2k-m} \end{align} ``` where ``k_1 = \textrm{max}(0, m)`` and ``k_2=\textrm{min}(\ell+m, @@ -184,24 +239,39 @@ as ```math m^\mu = \frac{1}{\sqrt{2}} \left( \boldsymbol{\theta} + i \boldsymbol{\phi} -\right) +\right)^\mu ``` and discuss spin weight in terms of the rotation ```math (m^\mu)' = e^{i\psi} m^\mu, ``` where the tangent basis rotates but we are "keeping the -coordinates fixed". We find that we can emulate this using Euler -angles ``(\phi, \theta, -\psi)``. Note the negative sign in the -last angle. As usual, this rotates the positive ``𝐳`` axis to -the point ``(\theta, \phi)``, and rotates ``(𝐱 + i 𝐲) / -\sqrt{2}`` onto ``(m^\mu)'``. They then define a function to have +coordinates fixed". They then define a function to have spin weight ``s`` if it transforms as ```math \eta' = e^{is\psi} \eta. ``` -In our notation, we can realize this function as a function of -Euler angles, and that equation becomes +Such functions are generally the result of contracting a tensor field +with some number of ``m^\mu`` and some number of ``\bar{m}^\mu`` +vectors (though spinor extensions resulting in half-integer spin +weights are also possible). + +Note that this definition shows that it is clearly *impossible* to +define spin-weighted functions on the 2-sphere; the 2-sphere alone +includes no information about the directions of basis vectors in its +tangent space. Instead, we *must* think of spin-weighted functions as +defined on the "unit tangent bundle over the 2-sphere" so that this +behavior with respect to rotation of tangent basis can possibly have +any effect. This unit tangent bundle happens to be homeomorphic to +the 3-sphere, which is also the space of unit quaternions. Thus, we +think of spin-weighted functions as defined on the group of unit +quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, and frequently +discuss them in terms of Euler angles. + +As we saw [above](@ref "Euler angles (and spherical coordinates)"), +``m^\mu`` corresponds to the Euler angles ``(\phi, \theta, 0)``, while +``(m^\mu)'`` corresponds to the Euler angles ``(\phi, \theta, +-\psi)``. The function, written in terms of Euler angles, becomes ```math \eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), ``` From 81824fd5653ff89b074755a37ddb12ccd7c8de91 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 7 Apr 2025 13:24:23 -0400 Subject: [PATCH 174/329] Spelling --- docs/src/conventions/details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index f2d5c740..16ba0d75 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -1043,7 +1043,7 @@ The sum will automatically be zero unless ``m+k-j ≤ ℓ`` — which means ``j I wonder if there's a nicer approach using the symmetry transformation Edmonds notes in Sec. 4.5 (and credits to Wigner) — or the presumably -equivalent one McEwan and Wieux use (and credit Risbo): +equivalent one McEwen and Wiaux use (and credit to Risbo): ```math \exp\left[ \beta 𝐣 / 2 \right] = From 3e213f720b7da122fede204b16facc3634b8eff8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 30 Apr 2025 22:44:39 -0400 Subject: [PATCH 175/329] Add redesigned Wigner matrices --- Project.toml | 2 + src/SphericalFunctions.jl | 2 + src/redesign/SphericalFunctions.jl | 26 +++ src/redesign/WignerDMatrices.jl | 248 +++++++++++++++++++++++++++++ src/redesign/WignerMatrix.jl | 128 +++++++++++++++ src/redesign/recurrence.jl | 11 ++ 6 files changed, 417 insertions(+) create mode 100644 src/redesign/SphericalFunctions.jl create mode 100644 src/redesign/WignerDMatrices.jl create mode 100644 src/redesign/WignerMatrix.jl create mode 100644 src/redesign/recurrence.jl diff --git a/Project.toml b/Project.toml index 301d3397..1f30fc11 100644 --- a/Project.toml +++ b/Project.toml @@ -16,6 +16,7 @@ Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] AbstractFFTs = "1" @@ -42,6 +43,7 @@ SpecialFunctions = "2" StaticArrays = "1" Test = "1.11" TestItemRunner = "1" +TestItems = "1.0.0" julia = "1.6" [extras] diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 8d76b706..7c7e9488 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -1,5 +1,6 @@ module SphericalFunctions +using TestItems: @testitem using FastTransforms: FFTW, fft, fftshift!, ifft, ifftshift!, irfft, plan_bfft!, plan_fft, plan_fft! using LinearAlgebra: LinearAlgebra, Bidiagonal, Diagonal, convert, ldiv!, mul! @@ -64,5 +65,6 @@ export L², Lz, L₊, L₋, R², Rz, R₊, R₋, ð, ð̄ #export rotate! +include("redesign/SphericalFunctions.jl") end # module diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl new file mode 100644 index 00000000..d92af12b --- /dev/null +++ b/src/redesign/SphericalFunctions.jl @@ -0,0 +1,26 @@ +module Redesign + +import Quaternionic +import TestItems: @testitem, @testsnippet +import OffsetArrays + + +include("WignerDMatrices.jl") +include("WignerMatrix.jl") + + +function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT; m′ₘₐₓ::IT=ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ) where {IT} + NT = complex(Quaternionic.basetype(R)) + D = WignerDMatrices(NT, ℓₘₐₓ; m′ₘₐₓ, mₘₐₓ) + WignerD!(D, R) +end + +function WignerD!(D::WignerDMatrices{Complex{FT1}}, R::Quaternionic.Rotor{FT2}) where {FT1, FT2} + throw(ErrorException("In `WignerD!`, float type of D=$FT1 and of R=$FT2 do not match")) +end + +function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} + throw(ErrorException("WignerD! is not yet implemented")) +end + +end # module Redesign diff --git a/src/redesign/WignerDMatrices.jl b/src/redesign/WignerDMatrices.jl new file mode 100644 index 00000000..e62c97f8 --- /dev/null +++ b/src/redesign/WignerDMatrices.jl @@ -0,0 +1,248 @@ +abstract type AbstractDMatrices end + + +""" + WignerDMatrices{NT, IT} + +A data structure to hold the Wigner D-matrices for a range values (stored in a `Vector{NT}`) +up to and including some `ℓₘₐₓ`, `m′ₘₐₓ`, and `mₘₐₓ` (which all have type `IT`). + +Indexing this object with an integer `ℓ` returns an `OffsetArray` of a view of the relevant +part of the data vector corresponding to the `ℓ` matrix. +""" +struct WignerDMatrices{NT, IT} <: AbstractDMatrices + data::Vector{NT} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + mₘₐₓ::IT +end + +data(D::WignerDMatrices) = D.data +ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = zero(IT) +ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = IT(1//2) +ℓₘₐₓ(D::WignerDMatrices) = D.ℓₘₐₓ +m′ₘₐₓ(D::WignerDMatrices) = D.m′ₘₐₓ +mₘₐₓ(D::WignerDMatrices) = D.mₘₐₓ +m′ₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(m′ₘₐₓ(D), ℓ) +mₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(mₘₐₓ(D), ℓ) + +Base.eltype(D::WignerDMatrices) = eltype(data(D)) + +isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = false +isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = true + + +""" + WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) + +Return the total size of the data stored in a `WignerDMatrices` object with the given sizes, +ranging over all matrices for all ℓ values. +""" +function WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)::Int + m₁, m₂ = m′ₘₐₓ, mₘₐₓ + if m₁ > m₂ + m₁, m₂ = m₂, m₁ + end + + if ℓₘₐₓ ≤ m₁ + (2ℓₘₐₓ + 1)*(2ℓₘₐₓ + 2)*(2ℓₘₐₓ + 3) ÷ 6 + elseif ℓₘₐₓ ≤ m₂ + ( + (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 + + (ℓₘₐₓ - m₁)*(2m₁ + 1)*(ℓₘₐₓ + m₁ + 2) + ) + else + ( + (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 + + (m₂ - m₁)*(2m₁ + 1)*(m₂ + m₁ + 2) + + (2m₁ + 1)*(2m₂ + 1)*(ℓₘₐₓ - m₂) + ) + end +end + + +@testsnippet WignerDUtilities begin + function indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) + data = Vector{Tuple{Int64, Int64, Int64}}(undef, sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ)) + i=1 + for ℓ ∈ 0:ℓₘₐₓ + for m ∈ -min(ℓ, mₘₐₓ):min(ℓ, mₘₐₓ) + for m′ ∈ -min(ℓ, m′ₘₐₓ):min(ℓ, m′ₘₐₓ) + data[i] = (ℓ, m′, m) + i += 1 + end + end + end + data + end +end + + +@testitem "Test WignerDsize" setup=[WignerDUtilities] begin + import SphericalFunctions.Redesign: WignerDsize + + for ℓₘₐₓ ∈ 0:8 + @test WignerDsize(ℓₘₐₓ, 0, 0) == ℓₘₐₓ + 1 + @test WignerDsize(ℓₘₐₓ, 1, 0) == 3ℓₘₐₓ + 1 + @test WignerDsize(ℓₘₐₓ, 0, 1) == 3ℓₘₐₓ + 1 + @test WignerDsize(ℓₘₐₓ, 1, 1) == (3^2)ℓₘₐₓ + 1 + @test WignerDsize(ℓₘₐₓ, 2, 0) == max(1, 5ℓₘₐₓ - 1) + @test WignerDsize(ℓₘₐₓ, 0, 2) == max(1, 5ℓₘₐₓ - 1) + @test WignerDsize(ℓₘₐₓ, 2, 1) == max(1, 15ℓₘₐₓ - 5) + @test WignerDsize(ℓₘₐₓ, 1, 2) == max(1, 15ℓₘₐₓ - 5) + @test WignerDsize(ℓₘₐₓ, 2, 2) == max(1, (5^2)ℓₘₐₓ - 15) + @test WignerDsize(ℓₘₐₓ, ℓₘₐₓ, ℓₘₐₓ) == sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ) + + for mₘₐₓ ∈ 0:ℓₘₐₓ + for m′ₘₐₓ ∈ 0:ℓₘₐₓ + @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == WignerDsize(ℓₘₐₓ, mₘₐₓ, m′ₘₐₓ) + + (m₁, m₂) = extrema((m′ₘₐₓ, mₘₐₓ)) + + @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == ( + sum(((2ℓ+1)^2 for ℓ ∈ 0:m₁), init=0) + + sum(((2m₁+1)*(2ℓ+1) for ℓ ∈ m₁+1:m₂); init=0) + + sum(((2m₁+1)*(2m₂+1) for ℓ ∈ m₂+1:ℓₘₐₓ); init=0) + ) + + data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) + for ℓ ∈ 0:ℓₘₐₓ-1 + @test data[WignerDsize(ℓ, m′ₘₐₓ, mₘₐₓ)] == (ℓ, min(m′ₘₐₓ, ℓ), min(mₘₐₓ, ℓ)) + end + + end + end + end +end + + +""" + WignerDMatrices(NT, ℓₘₐₓ; m′ₘₐₓ=ℓₘₐₓ, mₘₐₓ=ℓₘₐₓ) + +Create a `WignerDMatrices` object with the given parameters. The data is initialized to +zero. +""" +function WignerDMatrices(::Type{NT}, ℓₘₐₓ::IT; m′ₘₐₓ::IT=ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ) where {NT, IT} + # Massage the inputs + mₘₐₓ = abs(mₘₐₓ) + m′ₘₐₓ = abs(m′ₘₐₓ) + + # Check that the parameters are valid + if complex(NT) != NT + throw(ErrorException("NT=$NT must be a complex type")) + end + if ℓₘₐₓ < (limit = (IT<:Rational ? 1//2 : 0)) + throw(ErrorException("ℓₘₐₓ < $limit")) + end + if m′ₘₐₓ > ℓₘₐₓ + throw(ErrorException("m′ₘₐₓ > ℓₘₐₓ")) + end + if mₘₐₓ > ℓₘₐₓ + throw(ErrorException("mₘₐₓ > ℓₘₐₓ")) + end + + # Create the data array + data = zeros(NT, WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)) + + return WignerDMatrices{NT, IT}(data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) +end + + +""" + index(D, ℓ) + +Find the index in `data(D)` of the first element of the `WignerDMatrix` for the given ℓ +value. +""" +function index(D, ℓ) + if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) + throw(ErrorException("ℓ=$ℓ is out of range for D=$D")) + end + + if ℓ == ℓₘᵢₙ(D) + 1 + else + WignerDsize(ℓ-1, m′ₘₐₓ(D), mₘₐₓ(D)) + 1 + end +end + + +@testitem "Test WignerDMatrices index" setup=[WignerDUtilities] begin + import SphericalFunctions.Redesign: WignerDMatrices, index + + for ℓₘₐₓ ∈ 0:8 + for mₘₐₓ ∈ 0:ℓₘₐₓ + for m′ₘₐₓ ∈ 0:ℓₘₐₓ + data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) + D = WignerDMatrices(ComplexF64, ℓₘₐₓ; m′ₘₐₓ, mₘₐₓ) + for ℓ ∈ 0:ℓₘₐₓ + @test data[index(D, ℓ)] == (ℓ, -min(m′ₘₐₓ, ℓ), -min(mₘₐₓ, ℓ)) + end + + end + end + end +end + + +""" + size(D) + +Return the total size of the data stored in this WignerDMatrices object, ranging over all +matrices for all ℓ values. For the size of a particular matrix, use `size(D, ℓ)`. +""" +Base.size(D::WignerDMatrices) = WignerDsize(ℓₘₐₓ(D), m′ₘₐₓ(D), mₘₐₓ(D)) + + +""" + size(D, ℓ) + +Return the size of the data stored in this WignerDMatrices object for a particular ℓ value. +For the size of all matrices combined, use `size(D)`. +""" +function Base.size(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} + if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) + 0 + else + return (Int(2m′ₘₐₓ(D, ℓ)) + 1) * (Int(2mₘₐₓ(D, ℓ)) + 1) + end +end + +function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Rational} + throw(ErrorException("Don't yet know how to deal with Rational indices")) +end + +function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Integer} + i₁ = index(D, ℓ) + i₂ = i₁ + size(D, ℓ) - 1 + m′ = m′ₘₐₓ(D, ℓ) + m = mₘₐₓ(D, ℓ) + OffsetArrays.Origin(-m′, -m)(reshape((@view data(D)[i₁:i₂]), 2m′+1, 2m+1)) +end + + +@testitem "Test WignerDMatrices indices" setup=[WignerDUtilities] begin + import SphericalFunctions.Redesign: WignerDMatrices, index + + for ℓₘₐₓ ∈ 0:8 + for mₘₐₓ ∈ 0:ℓₘₐₓ + for m′ₘₐₓ ∈ 0:ℓₘₐₓ + data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) + D = WignerDMatrices{eltype(data), Int}( + data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ + ) + + for ℓ ∈ 0:ℓₘₐₓ + Dˡ = D[ℓ] + @test size(Dˡ) == (2min(m′ₘₐₓ, ℓ)+1, 2min(mₘₐₓ, ℓ)+1) + + for m ∈ -min(mₘₐₓ, ℓ):min(mₘₐₓ, ℓ) + for m′ ∈ -min(m′ₘₐₓ, ℓ):min(m′ₘₐₓ, ℓ) + @test Dˡ[m′, m] == (ℓ, m′, m) + end + end + end + end + end + end +end diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl new file mode 100644 index 00000000..2477649c --- /dev/null +++ b/src/redesign/WignerMatrix.jl @@ -0,0 +1,128 @@ +abstract type WignerMatrix{NT, IT} end + +### General methods for all WignerMatrix types + +data(w::WignerMatrix{NT, IT}) where {NT, IT} = w.data +ℓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.ℓ +m′ₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.m′ₘₐₓ +mₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.mₘₐₓ + +ℓₘᵢₙ(::WignerMatrix{NT, IT}) where {NT, IT<:Integer} = zero(IT) +ℓₘᵢₙ(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = IT(1//2) + +is_rational(::WignerMatrix{NT, IT}) where {NT, IT<:Integer} = false +is_rational(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = true + +Base.eltype(::WignerMatrix{NT, IT}) where {NT, IT} = NT +Base.size(w::WignerMatrix{NT, IT}) where {NT, IT} = size(data(w)) + +function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} + data(w)[i] +end +function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} + data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] +end + +function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, i::Int) where {NT, IT} + data(w)[i] = v +end +function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, m′::IT, m::IT) where {NT, IT} + data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v +end + +function Base.axes(w::WignerMatrix{NT, IT}) where {NT, IT} + (-m′ₘₐₓ(w):m′ₘₐₓ(w), -mₘₐₓ(w):mₘₐₓ(w)) +end + + +### Specialize to D and d matrices + +struct WignerDMatrix{NT, IT} <: WignerMatrix{NT, IT} + data::Matrix{NT} + ℓ::IT + m′ₘₐₓ::IT + mₘₐₓ::IT + function WignerDMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT, m′ₘₐₓ::IT=ℓ, mₘₐₓ::IT=ℓ) where {NT, IT} + if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT + throw(ErrorException( + "WignerDMatrix only supports complex types; the input type is $NT.\n" + * "Perhaps you meant to use WignerdMatrix?" + )) + end + if IT <: Rational + if denominator(ℓ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + throw(ErrorException( + "Index limits must be either integers or half-integer Rationals; " + * "the inputs are Rationals $ℓ, $m′ₘₐₓ, $mₘₐₓ." + )) + end + end + new(data, ℓ, abs(m′ₘₐₓ), abs(mₘₐₓ)) + end +end +function WignerDMatrix( + data::Matrix{NT}, ℓ::IT; + mpmax::IT=ℓ, mmax::IT=ℓ, + m′ₘₐₓ::IT=mpmax, mₘₐₓ::IT=mmax +) where {NT, IT} + WignerDMatrix{NT, IT}(data, ℓ, m′ₘₐₓ, mₘₐₓ) +end + + +struct WignerdMatrix{NT, IT} <: WignerMatrix{NT, IT} + data::Matrix{NT} + ℓ::IT + m′ₘₐₓ::IT + mₘₐₓ::IT + function WignerdMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT, m′ₘₐₓ::IT, mₘₐₓ::IT) where {NT, IT} + if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT + throw(ErrorException( + "WignerdMatrix only supports real types; the input type is $NT.\n" + * "Perhaps you meant to use WignerDMatrix?" + )) + end + if IT <: Rational + if denominator(ℓ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + throw(ErrorException( + "Index limits must be either integers or half-integer Rationals; " + * "the inputs are Rationals $ℓ, $m′ₘₐₓ, $mₘₐₓ." + )) + end + end + new(data, ℓ, abs(m′ₘₐₓ), abs(mₘₐₓ)) + end +end +function WignerdMatrix( + data::Matrix{NT}, ℓ::IT; + mpmax::IT=ℓ, mmax::IT=ℓ, + m′ₘₐₓ::IT=mpmax, mₘₐₓ::IT=mmax +) where {NT, IT} + WignerdMatrix{NT, IT}(data, ℓ, m′ₘₐₓ, mₘₐₓ) +end + + +@testitem "WignerMatrix" begin + import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix + + for ℓ ∈ 0:8 + for mₘₐₓ ∈ 0:ℓ + for m′ₘₐₓ ∈ 0:ℓ + data = [ + (ℓ, m′, m) + for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ, m ∈ -mₘₐₓ:mₘₐₓ + ] + for D ∈ (WignerDMatrix(data, ℓ; m′ₘₐₓ, mₘₐₓ), WignerdMatrix(data, ℓ; m′ₘₐₓ, mₘₐₓ)) + @test D.data == data + @test D.ℓ == ℓ + @test D.m′ₘₐₓ == m′ₘₐₓ + @test D.mₘₐₓ == mₘₐₓ + for m ∈ -mₘₐₓ:mₘₐₓ + for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ + @test D[m′, m] == (ℓ, m′, m) + end + end + end + end + end + end +end diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl new file mode 100644 index 00000000..9e197296 --- /dev/null +++ b/src/redesign/recurrence.jl @@ -0,0 +1,11 @@ +function initialize!(w::WignerMatrix{NT, IT}, sinβ, cosβ) where {NT, IT} + T = real(NT) + let √ = sqrt ∘ T + if ℓ(w) == 0 + w[0, 0] = 0 + elseif ℓ(w) == 1 + w[0, 0] = cosβ + w[0, 1] = sinβ / √2 + end + end +end From 256c5697fcd314e48d7828a29bc3b59c2a8c2411 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 30 Apr 2025 22:53:29 -0400 Subject: [PATCH 176/329] =?UTF-8?q?Infer=20m=E2=80=B2=20and=20m=20limits?= =?UTF-8?q?=20from=20data=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/redesign/WignerMatrix.jl | 38 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 2477649c..58330ae3 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -42,7 +42,12 @@ struct WignerDMatrix{NT, IT} <: WignerMatrix{NT, IT} ℓ::IT m′ₘₐₓ::IT mₘₐₓ::IT - function WignerDMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT, m′ₘₐₓ::IT=ℓ, mₘₐₓ::IT=ℓ) where {NT, IT} + function WignerDMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT) where {NT, IT} + m′ₘₐₓ = IT((size(data, 1) - 1) // 2) + mₘₐₓ = IT((size(data, 2) - 1) // 2) + if ℓ < 0 + throw(ErrorException("ℓ=$ℓ should be non-negative.")) + end if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT throw(ErrorException( "WignerDMatrix only supports complex types; the input type is $NT.\n" @@ -53,19 +58,15 @@ struct WignerDMatrix{NT, IT} <: WignerMatrix{NT, IT} if denominator(ℓ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 throw(ErrorException( "Index limits must be either integers or half-integer Rationals; " - * "the inputs are Rationals $ℓ, $m′ₘₐₓ, $mₘₐₓ." + * "the inputs are Rationals: $ℓ, $m′ₘₐₓ, $mₘₐₓ." )) end end new(data, ℓ, abs(m′ₘₐₓ), abs(mₘₐₓ)) end end -function WignerDMatrix( - data::Matrix{NT}, ℓ::IT; - mpmax::IT=ℓ, mmax::IT=ℓ, - m′ₘₐₓ::IT=mpmax, mₘₐₓ::IT=mmax -) where {NT, IT} - WignerDMatrix{NT, IT}(data, ℓ, m′ₘₐₓ, mₘₐₓ) +function WignerDMatrix(data::Matrix{NT}, ℓ::IT) where {NT, IT} + WignerDMatrix{NT, IT}(data, ℓ) end @@ -74,7 +75,12 @@ struct WignerdMatrix{NT, IT} <: WignerMatrix{NT, IT} ℓ::IT m′ₘₐₓ::IT mₘₐₓ::IT - function WignerdMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT, m′ₘₐₓ::IT, mₘₐₓ::IT) where {NT, IT} + function WignerdMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT) where {NT, IT} + m′ₘₐₓ = IT((size(data, 1) - 1) // 2) + mₘₐₓ = IT((size(data, 2) - 1) // 2) + if ℓ < 0 + throw(ErrorException("ℓ=$ℓ should be non-negative.")) + end if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT throw(ErrorException( "WignerdMatrix only supports real types; the input type is $NT.\n" @@ -85,19 +91,15 @@ struct WignerdMatrix{NT, IT} <: WignerMatrix{NT, IT} if denominator(ℓ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 throw(ErrorException( "Index limits must be either integers or half-integer Rationals; " - * "the inputs are Rationals $ℓ, $m′ₘₐₓ, $mₘₐₓ." + * "the inputs are Rationals: $ℓ, $m′ₘₐₓ, $mₘₐₓ." )) end end - new(data, ℓ, abs(m′ₘₐₓ), abs(mₘₐₓ)) + new(data, ℓ, m′ₘₐₓ, mₘₐₓ) end end -function WignerdMatrix( - data::Matrix{NT}, ℓ::IT; - mpmax::IT=ℓ, mmax::IT=ℓ, - m′ₘₐₓ::IT=mpmax, mₘₐₓ::IT=mmax -) where {NT, IT} - WignerdMatrix{NT, IT}(data, ℓ, m′ₘₐₓ, mₘₐₓ) +function WignerdMatrix(data::Matrix{NT}, ℓ::IT) where {NT, IT} + WignerdMatrix{NT, IT}(data, ℓ) end @@ -111,7 +113,7 @@ end (ℓ, m′, m) for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ, m ∈ -mₘₐₓ:mₘₐₓ ] - for D ∈ (WignerDMatrix(data, ℓ; m′ₘₐₓ, mₘₐₓ), WignerdMatrix(data, ℓ; m′ₘₐₓ, mₘₐₓ)) + for D ∈ (WignerDMatrix(data, ℓ), WignerdMatrix(data, ℓ)) @test D.data == data @test D.ℓ == ℓ @test D.m′ₘₐₓ == m′ₘₐₓ From 4189aadb2361e9c0cc7a99a2e1f6ad6db0e86c11 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 01:06:49 -0400 Subject: [PATCH 177/329] Add new iterator interface for complex powers --- docs/src/references.bib | 12 ++++++ src/complex_powers.jl | 96 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/docs/src/references.bib b/docs/src/references.bib index c9dd858d..2f4bd112 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -440,6 +440,18 @@ @misc{SommerEtAl_2018 primaryClass = {cs.RO}, } +@book{StoerBulirsch_2002, + series = {Texts in Applied Mathematics}, + title = {Introduction to Numerical Analysis}, + isbn = {978-1-4419-3006-4 978-0-387-21738-3}, + url = {https://link.springer.com/book/10.1007%2F978-0-387-21738-3}, + number = 12, + publisher = {Springer New York}, + author = {Stoer, J. and Bulirsch, R.}, + year = 2002, + doi = {10.1007/978-0-387-21738-3\_1}, +} + @article{Strakhov_1980, title = {On synthesis of the outer gravitational potential in spherical harmonic series}, volume = 254, diff --git a/src/complex_powers.jl b/src/complex_powers.jl index a5464c8c..ecea4af7 100644 --- a/src/complex_powers.jl +++ b/src/complex_powers.jl @@ -80,3 +80,99 @@ function complex_powers(z, m::Int) complex_powers!(zpowers, z) end + + + + +struct ComplexPowers{T} + z¹::T + ϕ::Complex{Int} + τ::T + function ComplexPowers{T}(z::T, factor_phase=true) where {T} + z¹ = z + ϕ = 1 + 0im + if factor_phase + for _ ∈ 1:3 # We need at most 3 iterations to get the phase right + if z¹.re < 0 || abs(z¹.im) > z¹.re + ϕ *= im + z¹ *= -im + end + end + end + τ = -4 * sqrt(z¹).im^2 + new(z¹, ϕ, τ) + end +end + +@doc raw""" + ComplexPowers(z) + +Construct an iterator to compute powers of the complex phase factor ``z`` (assumed to have +magnitude 1). The iterator will return the complex number ``zᵐ`` for each integer ``m = 0, +1, 2, \ldots``. + +# Example +```julia-repl +julia> cp = ComplexPowers(cis(0.1)); + +julia> first(cp, 5) # Get the first 5 values from the iterator +5-element Vector{ComplexF64}: + 1.0 + 0.0im + 0.9950041652780258 + 0.09983341664682815im + 0.9800665778412417 + 0.19866933079506122im + 0.9553364891256061 + 0.2955202066613396im + 0.9210609940028851 + 0.3894183423086505im + +julia> cis(0.1).^(0:4) +5-element Vector{ComplexF64}: + 1.0 + 0.0im + 0.9950041652780258 + 0.09983341664682815im + 0.9800665778412417 + 0.19866933079506124im + 0.9553364891256062 + 0.2955202066613396im + 0.9210609940028853 + 0.3894183423086506im +``` + +# Notes + +[StoerBulirsch_2002](@citet) described the basic algorithm on page 24 (Example 4), though +there is a dramatic improvement to be made. The basic idea is a recurrence relation, where +``zᵐ`` is computed from ``zᵐ⁻¹`` by adding a small increment ``δz``, which itself is updated +by adding a small increment given by ``zᵐ`` times a constant ``τ``. + +Although this algorithm is numerically stable, we can improve its accuracy by factoring out +``ϕ``, the smallest power of ``i`` that minimizes the phase of ``z``. This power of ``i`` +can be separately exponentiated exactly and efficiently because it is exactly representable +as a complex integer, while the error in the computation of ``zᵐ`` is reduced significantly +for certain values of ``z``. + +As implemented here, this algorithm achieves a worst-case accuracy of roughly ``m ϵ`` for +``zᵐ`` — where ``ϵ`` is the precision of the type of the input argument — across the range +of inputs ``z = \exp^{iθ}`` for ``θ ∈ [0, 2π]``. The original algorithm can be far worse +for values of ``θ`` close to ``π`` — often orders of magnitude worse. + +""" +ComplexPowers(cisθ::T, factor_phase=true) where {T} = ComplexPowers{T}(cisθ, factor_phase) + + +function Base.iterate(cp::ComplexPowers{T}) where {T} + z⁰ = one(T) + δc = cp.τ / 2 + δz = δc + im * √(-δc * (2 + δc)) * (cp.z¹.im ≥ 0 ? 1 : -1) + Φ = 1 + 0im + z⁰, (z⁰, δz, Φ) +end + +function Base.iterate(cp::ComplexPowers{T}, state) where {T} + (zᵐ⁻¹, δz, Φ) = state + zᵐ = zᵐ⁻¹ + δz + δz = δz + zᵐ * cp.τ + Φ = Φ * cp.ϕ + zᵐ*Φ, (zᵐ, δz, Φ) +end + +Base.IteratorSize(::Type{<:ComplexPowers}) = Base.IsInfinite() + +Base.eltype(::Type{ComplexPowers{T}}) where {T} = T + +Base.isdone(iterator::ComplexPowers{T}) where {T} = false +Base.isdone(iterator::ComplexPowers{T}, state) where {T} = false From 78b7ca87da55a7323e972f8fbb1db42380029783 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 01:07:25 -0400 Subject: [PATCH 178/329] Add more validation and tests to WignerMatrix --- src/redesign/WignerMatrix.jl | 96 +++++++++++++---- src/redesign/recurrence.jl | 200 +++++++++++++++++++++++++++++++++-- 2 files changed, 270 insertions(+), 26 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 58330ae3..c833e11d 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -16,17 +16,25 @@ is_rational(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = true Base.eltype(::WignerMatrix{NT, IT}) where {NT, IT} = NT Base.size(w::WignerMatrix{NT, IT}) where {NT, IT} = size(data(w)) -function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} +@propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} data(w)[i] end -function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} - data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] +@propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} + @boundscheck begin + if abs(m′) > m′ₘₐₓ(w) + throw(BoundsError("m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w)).")) + end + if abs(m) > mₘₐₓ(w) + throw(BoundsError("m=$m out of bounds for WignerMatrix with mₘₐₓ=$(mₘₐₓ(w)).")) + end + end + @inbounds data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] end -function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, i::Int) where {NT, IT} +@propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, i::Int) where {NT, IT} data(w)[i] = v end -function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, m′::IT, m::IT) where {NT, IT} +@propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, m′::IT, m::IT) where {NT, IT} data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v end @@ -43,11 +51,22 @@ struct WignerDMatrix{NT, IT} <: WignerMatrix{NT, IT} m′ₘₐₓ::IT mₘₐₓ::IT function WignerDMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT) where {NT, IT} - m′ₘₐₓ = IT((size(data, 1) - 1) // 2) - mₘₐₓ = IT((size(data, 2) - 1) // 2) if ℓ < 0 throw(ErrorException("ℓ=$ℓ should be non-negative.")) end + if size(data, 1) == 0 + throw(ErrorException("Input data has 0 extent along first dimension.")) + end + if size(data, 2) == 0 + throw(ErrorException("Input data has 0 extent along second dimension.")) + end + m′ₘₐₓ = IT((size(data, 1) - 1) // 2) + mₘₐₓ = IT((size(data, 2) - 1) // 2) + if ℓ < max(m′ₘₐₓ, mₘₐₓ) + throw(ErrorException( + "ℓ=$ℓ should be greater than or equal to both m′ₘₐₓ=$m′ₘₐₓ and mₘₐₓ=$mₘₐₓ." + )) + end if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT throw(ErrorException( "WignerDMatrix only supports complex types; the input type is $NT.\n" @@ -76,11 +95,22 @@ struct WignerdMatrix{NT, IT} <: WignerMatrix{NT, IT} m′ₘₐₓ::IT mₘₐₓ::IT function WignerdMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT) where {NT, IT} - m′ₘₐₓ = IT((size(data, 1) - 1) // 2) - mₘₐₓ = IT((size(data, 2) - 1) // 2) if ℓ < 0 throw(ErrorException("ℓ=$ℓ should be non-negative.")) end + if size(data, 1) == 0 + throw(ErrorException("Input data has 0 extent along first dimension.")) + end + if size(data, 2) == 0 + throw(ErrorException("Input data has 0 extent along second dimension.")) + end + m′ₘₐₓ = IT((size(data, 1) - 1) // 2) + mₘₐₓ = IT((size(data, 2) - 1) // 2) + if ℓ < max(m′ₘₐₓ, mₘₐₓ) + throw(ErrorException( + "ℓ=$ℓ should be greater than or equal to both m′ₘₐₓ=$m′ₘₐₓ and mₘₐₓ=$mₘₐₓ." + )) + end if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT throw(ErrorException( "WignerdMatrix only supports real types; the input type is $NT.\n" @@ -106,21 +136,51 @@ end @testitem "WignerMatrix" begin import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix - for ℓ ∈ 0:8 - for mₘₐₓ ∈ 0:ℓ - for m′ₘₐₓ ∈ 0:ℓ + # Check that a negative ℓ value throws an error + @test_throws "should be non-negative." WignerDMatrix(rand(ComplexF64, 3, 3), -1) + @test_throws "should be non-negative." WignerdMatrix(rand(Float64, 3, 3), -1) + @test_throws "should be non-negative." WignerDMatrix(rand(ComplexF64, 2, 2), -1//2) + @test_throws "should be non-negative." WignerdMatrix(rand(Float64, 2, 2), -1//2) + + for ℓ ∈ Any[collect(0:8); collect(1//2:15//2)] + # Check that ℓ < m′ₘₐₓ and ℓ < mₘₐₓ throw errors + @test_throws "both m′ₘₐₓ=" WignerDMatrix(Array{Float64}(undef, Int(2ℓ)+3, Int(2ℓ)+1), ℓ) + @test_throws "both m′ₘₐₓ=" WignerDMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+3), ℓ) + @test_throws "both m′ₘₐₓ=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+3, Int(2ℓ)+1), ℓ) + @test_throws "both m′ₘₐₓ=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+3), ℓ) + + # Check that a mismatch between integer/half-integer throws an error + if ℓ>0 && ℓ isa Int + @test_throws "InexactError: Int64(1//2)" WignerDMatrix(rand(ComplexF64, 2, 2), ℓ) + @test_throws "InexactError: Int64(1//2)" WignerdMatrix(rand(Float64, 2, 2), ℓ) + elseif ℓ isa Rational + @test_throws "either integers or half-integer" WignerDMatrix(rand(ComplexF64, 1, 1), ℓ) + @test_throws "either integers or half-integer" WignerdMatrix(rand(Float64, 1, 1), ℓ) + end + + for mₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) + # Check a data array with a dimension of 0 extent throws an error. + # (Note that we're pretending mₘₐₓ is m′ₘₐₓ for two cases, just for efficiency.) + @test_throws "along second dim" WignerDMatrix(Array{Float64}(undef, Int(2mₘₐₓ)+1, 0), ℓ) + @test_throws "along first dim" WignerDMatrix(Array{Float64}(undef, 0, Int(2mₘₐₓ)+1), ℓ) + @test_throws "along second dim" WignerdMatrix(Array{Float64}(undef, Int(2mₘₐₓ)+1, 0), ℓ) + @test_throws "along first dim" WignerdMatrix(Array{Float64}(undef, 0, Int(2mₘₐₓ)+1), ℓ) + + for m′ₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) + # Make a big, dumb array full of the explicit indices to check that indexing + # works as expected. data = [ (ℓ, m′, m) for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ, m ∈ -mₘₐₓ:mₘₐₓ ] - for D ∈ (WignerDMatrix(data, ℓ), WignerdMatrix(data, ℓ)) - @test D.data == data - @test D.ℓ == ℓ - @test D.m′ₘₐₓ == m′ₘₐₓ - @test D.mₘₐₓ == mₘₐₓ + for w ∈ (WignerDMatrix(data, ℓ), WignerdMatrix(data, ℓ)) + @test w.data == data + @test w.ℓ == ℓ + @test w.m′ₘₐₓ == m′ₘₐₓ + @test w.mₘₐₓ == mₘₐₓ for m ∈ -mₘₐₓ:mₘₐₓ for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ - @test D[m′, m] == (ℓ, m′, m) + @test w[m′, m] == (ℓ, m′, m) end end end diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 9e197296..5a629e05 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -1,11 +1,195 @@ -function initialize!(w::WignerMatrix{NT, IT}, sinβ, cosβ) where {NT, IT} - T = real(NT) - let √ = sqrt ∘ T - if ℓ(w) == 0 - w[0, 0] = 0 - elseif ℓ(w) == 1 - w[0, 0] = cosβ - w[0, 1] = sinβ / √2 +# Eq. (44) in Gumerov and Duraiswami (2015). Note that they define `sgn` as follows, which +# is different from the usual definition, including from Julia's `sign` function, at 0: +sgn(m) = m ≥ 0 ? 1 : -1 + +# Eq. (7) in Gumerov and Duraiswami (2015) +ϵ(m) = (m ≥ 0 ? (-1)^m : 1) + + +function initialize!(Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T) where {NT, IT<:Integer, T} + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) + if ℓ == 0 + Hˡ[0, 0] = 0 + elseif ℓ == 1 + Hˡ[0, 0] = cosβ + Hˡ[0, 1] = sinβ / √2 + end + end +end + +function recurrence_0_m!( + Hˡ::WignerMatrix{NT, IT}, Hˡ⁻¹::WignerMatrix{NT, IT}, sinβ::T, cosβ::T +) where {NT, IT<:Integer, T} + @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 + # Note that in this step only, we use notation derived from Xing et al., denoting the + # coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following steps, we will use notation + # from Gumerov and Duraiswami, who denote their different coefficients aₗᵐ, etc. + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) + if ℓ > 1 + b̄ₗ = √(T(ℓ-1)/ℓ) + Hˡ[0, 0] = cosβ * Hˡ⁻¹[0, 0] - b̄ₗ * sinβ * Hˡ⁻¹[0, 1] + for m ∈ 1:ℓ-2 + c̄ₙₘ = √((ℓ+m)*(ℓ-m)) / ℓ + d̄ₙₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ + ēₙₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + Hˡ[0, m] = ( + c̄ₗₘ * cosβ * Hˡ⁻¹[0, m] + - sinβ * (d̄ₗₘ * Hˡ⁻¹[0, m+1] - ēₗₘ * Hˡ⁻¹[0, m-1]) + ) + end + let m = ℓ-1 + c̄ₙₘ = √((ℓ+m)*(ℓ-m)) / ℓ + ēₙₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + Hˡ[0, m] = ( + c̄ₗₘ * cosβ * Hˡ⁻¹[0, m] + - sinβ * (- ēₗₘ * Hˡ⁻¹[0, m-1]) + ) + end + let m = ℓ + ēₙₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + Hˡ[0, m] = ( + - sinβ * (- ēₗₘ * Hˡ⁻¹[0, m-1]) + ) + end + end + end +end + +function recurrence_1_m!( + Hˡ::WignerMatrix{NT, IT}, Hˡ⁺¹::WignerMatrix{NT, IT}, sinβ::T, cosβ::T +) where {NT, IT<:Integer, T} + @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + if ℓ > 0 && m′ₘₐₓ ≥ 1 + c = 1 / √(ℓ*(ℓ+1)) + for m ∈ 0:ℓ + āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) + b̄ₗ₊₁ᵐ⁻¹ = √((ℓ-m+1)*(ℓ-m+2)) + b̄ₗ₊₁⁻ᵐ⁻¹ = √((ℓ+m+1)*(ℓ+m+2)) + Hˡ[1, m] = -c * ( + b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * Hˡ⁺¹[0, m+1] + + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * Hˡ⁺¹[0, m-1] + + āₗᵐ * sinβ * Hˡ⁺¹[0, m] + ) + end + end + end +end + +function recurrence_m′₊!( + Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T +) where {NT, IT<:Integer, T} + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 + # Note that the signs of m′ and m are always +1, so we leave them out of the + # calculations of d̄ in this function. + d̄ₗᵐ′ = √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = √((ℓ-m′+1)*(ℓ+m′)) + for m ∈ m′:ℓ-1 + d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) + d̄ₗᵐ = √((ℓ-m)*(ℓ+m+1)) + Hˡ[m′+1, m] = ( + d̄ₗᵐ′⁻¹ * Hˡ[m′-1, m] + - d̄ₗᵐ⁻¹ * Hˡ[m′, m-1] + + d̄ₗᵐ * Hˡ[m′, m+1] + ) / d̄ₗᵐ′ + end + let m = ℓ + d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) + Hˡ[m′+1, m] = ( + d̄ₗᵐ′⁻¹ * Hˡ[m′-1, m] + - d̄ₗᵐ⁻¹ * Hˡ[m′, m-1] + ) / d̄ₗᵐ′ + end + end + end +end + +function recurrence_m′₋!( + Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T +) where {NT, IT<:Integer, T} + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 + d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) + for m ∈ -m′:ℓ-1 + d̄ₗᵐ = sgn(m) * √((ℓ-m)*(ℓ+m+1)) + d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + Hˡ[m′-1, m] = ( + d̄ₗᵐ′ * Hˡ[m′+1, m] + + d̄ₗᵐ⁻¹ * Hˡ[m′, m-1] + - d̄ₗᵐ * Hˡ[m′, m+1] + ) / d̄ₗᵐ′⁻¹ + end + let m = ℓ + d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + Hˡ[m′-1, m] = ( + d̄ₗᵐ′ * Hˡ[m′+1, m] + + d̄ₗᵐ⁻¹ * Hˡ[m′, m-1] + ) / d̄ₗᵐ′⁻¹ + end + end + end +end + +@doc raw""" + impose_symmetries!(Hˡ) + +Assuming that `Hˡ` has already been computed as much as possible by the recurrence +relations, this function imposes the symmetries, rather than recalculating terms. +Specifically, the recurrence relations will calculate the terms for all `m`, and +`m′ ≥ abs(m)`, and this function will complete the calculations using the symmetries +```math +\begin{aligned} +H^ℓ_{m′, m} &= H^ℓ_{m, m′}, \\ +H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. +\end{aligned} +``` + +""" +function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}, cisα::NT, cisβ::NT) where {NT, IT<:Integer} + @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m ∈ -ℓ:ℓ + for m′ ∈ abs(m):m′ₘₐₓ + Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] + end + end + end +end + + +""" + convert_H_to_d!(Hˡ) + +Convert the Wigner matrix `Hˡ` to the d matrix `dˡ`, which just involves multiplying by +signs related to the `m′` and `m` indices. + +""" +function convert_H_to_d!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Integer} + @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m ∈ -ℓ:ℓ + for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ + Hˡ[m′, m] *= ϵ(m′) * ϵ(-m) + end + end + end +end + + +""" + convert_H_to_D!(Hˡ) + +Convert the Wigner matrix `Hˡ` to the D matrix `Dˡ`, which just involves multiplying by +complex phases related to the `m′` and `m` indices. + +""" +function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Integer} + @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + throw(ErrorException("convert_H_to_D! is not yet implemented")) + for m ∈ -ℓ:ℓ + for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ + Hˡ[m′, m] *= ϵ(m′) * ϵ(-m) + end end end end From cc7cbc894ce9451f368020b351c6d19edd26a931 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 09:45:26 -0400 Subject: [PATCH 179/329] Export and test ComplexPowers iterator interface --- src/SphericalFunctions.jl | 2 +- src/complex_powers.jl | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 7c7e9488..a420fc51 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -24,7 +24,7 @@ export sorted_rings, sorted_ring_pixels, sorted_ring_rotors export fejer1_rings, fejer2_rings, clenshaw_curtis_rings include("complex_powers.jl") -export complex_powers, complex_powers! +export complex_powers, complex_powers!, ComplexPowers include("indexing.jl") export Ysize, Yrange, Yindex, deduce_limits, theta_phi, phi_theta diff --git a/src/complex_powers.jl b/src/complex_powers.jl index ecea4af7..12cb5165 100644 --- a/src/complex_powers.jl +++ b/src/complex_powers.jl @@ -81,9 +81,6 @@ function complex_powers(z, m::Int) end - - - struct ComplexPowers{T} z¹::T ϕ::Complex{Int} @@ -176,3 +173,20 @@ Base.eltype(::Type{ComplexPowers{T}}) where {T} = T Base.isdone(iterator::ComplexPowers{T}) where {T} = false Base.isdone(iterator::ComplexPowers{T}, state) where {T} = false + + +@testitem "ComplexPowers" begin + mₘₐₓ = 10_000 + for θ ∈ BigFloat(0):big(1//10):2big(π) + z¹ = cis(θ) + p = ComplexPowers(ComplexF64(z¹)) + for (i, zᵐ) in enumerate(p) + m = i-1 + err = Float64(abs(zᵐ - z¹^m)) + @test err < mₘₐₓ * eps(Float64) + if m == mₘₐₓ + break + end + end + end +end From dc932879133c5bad9344197b829d0280c67c9f58 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 11:00:02 -0400 Subject: [PATCH 180/329] Include missing import --- src/redesign/WignerMatrix.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index c833e11d..d9974255 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -1,3 +1,5 @@ +import Base: @propagate_inbounds + abstract type WignerMatrix{NT, IT} end ### General methods for all WignerMatrix types From 74d69edc76a4938d30c9573c7ee493c853ddb249 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 22:29:51 -0400 Subject: [PATCH 181/329] "valculated" is not a word --- docs/src/notes/H_recursions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/notes/H_recursions.md b/docs/src/notes/H_recursions.md index 70c4398e..5f90b693 100644 --- a/docs/src/notes/H_recursions.md +++ b/docs/src/notes/H_recursions.md @@ -19,7 +19,7 @@ where ``` ``H`` has various advantages over ``d``, including the fact that it can be efficiently -and robustly valculated via recurrence relations, and the following symmetry +and robustly calculated via recurrence relations, and the following symmetry relations: ```math From 13a8c3a37344f9010d562f919c002b8974e4c2f7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 22:30:28 -0400 Subject: [PATCH 182/329] Remove unused arguments --- src/redesign/recurrence.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 5a629e05..f029fb76 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -147,7 +147,7 @@ H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. ``` """ -function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}, cisα::NT, cisβ::NT) where {NT, IT<:Integer} +function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Integer} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m ∈ -ℓ:ℓ for m′ ∈ abs(m):m′ₘₐₓ From e763f8aeaa75214f2a5bab3780e525f2ea2fdb6b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 22:33:20 -0400 Subject: [PATCH 183/329] Finish `convert_H_to_D!` --- src/redesign/recurrence.jl | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index f029fb76..928d989e 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -183,12 +183,24 @@ Convert the Wigner matrix `Hˡ` to the D matrix `Dˡ`, which just involves multi complex phases related to the `m′` and `m` indices. """ -function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Integer} - @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) - throw(ErrorException("convert_H_to_D! is not yet implemented")) - for m ∈ -ℓ:ℓ - for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ - Hˡ[m′, m] *= ϵ(m′) * ϵ(-m) +function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}, eⁱᵅ::NT, eⁱᵞ::NT) where {NT, IT<:Integer} + # NOTE: This function will have to be modified to work for Rational indices because the + # phases will not be integer powers; we'll have to incorporate √eⁱᵅ and √eⁱᵞ. + @inbounds let ℓ=ℓ(Hˡ), ℓₘᵢₙ=ℓₘᵢₙ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + ϕᵞ = ComplexPowers(eⁱᵞ) + ϕᵅ = ComplexPowers(eⁱᵅ) + for (m, eⁱᵐᵞ) ∈ zip(ℓₘᵢₙ:ℓ, ϕᵞ) + for (m′, eⁱᵐ′ᵅ) ∈ zip(ℓₘᵢₙ:m′ₘₐₓ, ϕᵅ) + Hˡ[m′, m] *= ϵ(m′) * ϵ(-m) * conj(eⁱᵐ′ᵅ) * conj(eⁱᵐᵞ) + if m′ ≠ 0 + Hˡ[-m′, m] *= ϵ(-m′) * ϵ(-m) * eⁱᵐ′ᵅ * conj(eⁱᵐᵞ) + if m ≠ 0 + Hˡ[-m′, -m] *= ϵ(-m′) * ϵ(m) * eⁱᵐ′ᵅ * eⁱᵐᵞ + end + end + if m ≠ 0 + Hˡ[m′, -m] *= ϵ(m′) * ϵ(m) * conj(eⁱᵐ′ᵅ) * eⁱᵐᵞ + end end end end From 403c2e9d72c1327224c8ce9613bca801fddc28c8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 4 May 2025 14:22:49 -0400 Subject: [PATCH 184/329] Add bounds checks --- src/redesign/WignerMatrix.jl | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index d9974255..2e234ee3 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -17,26 +17,57 @@ is_rational(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = true Base.eltype(::WignerMatrix{NT, IT}) where {NT, IT} = NT Base.size(w::WignerMatrix{NT, IT}) where {NT, IT} = size(data(w)) +Base.length(w::WignerMatrix{NT, IT}) where {NT, IT} = length(data(w)) @propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} + @boundscheck begin + if i<1 || i>length(w) + throw(BoundsError( + "i=$i out of bounds for WignerMatrix with length=$(length(w))." + )) + end + end data(w)[i] end @propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} @boundscheck begin if abs(m′) > m′ₘₐₓ(w) - throw(BoundsError("m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w)).")) + throw(BoundsError( + "m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w))." + )) end if abs(m) > mₘₐₓ(w) - throw(BoundsError("m=$m out of bounds for WignerMatrix with mₘₐₓ=$(mₘₐₓ(w)).")) + throw(BoundsError( + "m=$m out of bounds for WignerMatrix with mₘₐₓ=$(mₘₐₓ(w))." + )) end end @inbounds data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] end @propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, i::Int) where {NT, IT} + @boundscheck begin + if i<1 || i>length(w) + throw(BoundsError( + "i=$i out of bounds for WignerMatrix with length=$(length(w))." + )) + end + end data(w)[i] = v end @propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, m′::IT, m::IT) where {NT, IT} + @boundscheck begin + if abs(m′) > m′ₘₐₓ(w) + throw(BoundsError( + "m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w))." + )) + end + if abs(m) > mₘₐₓ(w) + throw(BoundsError( + "m=$m out of bounds for WignerMatrix with mₘₐₓ=$(mₘₐₓ(w))." + )) + end + end data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v end From 5a04d6f60e40df73547e1416caab81c9c3c1c870 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 4 May 2025 22:20:34 -0400 Subject: [PATCH 185/329] Add tests for basic behavior of floating-point types --- src/redesign/WignerMatrix.jl | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 2e234ee3..1454f7cc 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -200,12 +200,12 @@ end @test_throws "along first dim" WignerdMatrix(Array{Float64}(undef, 0, Int(2mₘₐₓ)+1), ℓ) for m′ₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) - # Make a big, dumb array full of the explicit indices to check that indexing - # works as expected. + # Make a big, dumb array full of the explicit indices. data = [ (ℓ, m′, m) for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ, m ∈ -mₘₐₓ:mₘₐₓ ] + # Check that indexing works as expected. for w ∈ (WignerDMatrix(data, ℓ), WignerdMatrix(data, ℓ)) @test w.data == data @test w.ℓ == ℓ @@ -218,6 +218,30 @@ end end end end + + for m′ₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) + for WignerMatrixType ∈ (WignerDMatrix, WignerdMatrix) + data = rand( + WignerMatrixType<:WignerDMatrix ? ComplexF64 : Float64, + Int(2mₘₐₓ)+1, Int(2m′ₘₐₓ)+1 + ) + w = WignerMatrixType(data, ℓ) + + # Check that the data array is stored correctly. + @test w.data == data + @test w.ℓ == ℓ + @test w.m′ₘₐₓ == m′ₘₐₓ + @test w.mₘₐₓ == mₘₐₓ + + # The Julia docs say that the `axes` function should + # > Return a tuple of `AbstractUnitRange{<:Integer}` of valid indices. + # > The axes should be their own axes, that is `axes.(axes(A),1) == + # > axes(A)` should be satisfied. + # https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array + @test typeof(axes(w)) <: AbstractUnitRange{<:Integer} + @test axes.(axes(w),1) == axes(w) + end + end end end end From 56f9906a8813279ec197062d3d3e748d46db250c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 2 Oct 2025 22:05:47 -0400 Subject: [PATCH 186/329] Simplify the structure --- src/redesign/SphericalFunctions.jl | 10 +- src/redesign/WignerDMatrices.jl | 248 ----------------- src/redesign/WignerMatrices.jl | 264 ++++++++++++++++++ src/redesign/WignerMatrix.jl | 429 ++++++++++++++++++++--------- 4 files changed, 567 insertions(+), 384 deletions(-) delete mode 100644 src/redesign/WignerDMatrices.jl create mode 100644 src/redesign/WignerMatrices.jl diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index d92af12b..a96fe0dc 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -2,21 +2,21 @@ module Redesign import Quaternionic import TestItems: @testitem, @testsnippet -import OffsetArrays -include("WignerDMatrices.jl") include("WignerMatrix.jl") +include("WignerMatrices.jl") -function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT; m′ₘₐₓ::IT=ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ) where {IT} +function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} NT = complex(Quaternionic.basetype(R)) - D = WignerDMatrices(NT, ℓₘₐₓ; m′ₘₐₓ, mₘₐₓ) + D = WignerDMatrices(NT, ℓₘₐₓ, m′ₘₐₓ) WignerD!(D, R) end function WignerD!(D::WignerDMatrices{Complex{FT1}}, R::Quaternionic.Rotor{FT2}) where {FT1, FT2} - throw(ErrorException("In `WignerD!`, float type of D=$FT1 and of R=$FT2 do not match")) + R1 = Quaternionic.Rotor{FT1}(R) + WignerD!(D, R1) end function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} diff --git a/src/redesign/WignerDMatrices.jl b/src/redesign/WignerDMatrices.jl deleted file mode 100644 index e62c97f8..00000000 --- a/src/redesign/WignerDMatrices.jl +++ /dev/null @@ -1,248 +0,0 @@ -abstract type AbstractDMatrices end - - -""" - WignerDMatrices{NT, IT} - -A data structure to hold the Wigner D-matrices for a range values (stored in a `Vector{NT}`) -up to and including some `ℓₘₐₓ`, `m′ₘₐₓ`, and `mₘₐₓ` (which all have type `IT`). - -Indexing this object with an integer `ℓ` returns an `OffsetArray` of a view of the relevant -part of the data vector corresponding to the `ℓ` matrix. -""" -struct WignerDMatrices{NT, IT} <: AbstractDMatrices - data::Vector{NT} - ℓₘₐₓ::IT - m′ₘₐₓ::IT - mₘₐₓ::IT -end - -data(D::WignerDMatrices) = D.data -ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = zero(IT) -ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = IT(1//2) -ℓₘₐₓ(D::WignerDMatrices) = D.ℓₘₐₓ -m′ₘₐₓ(D::WignerDMatrices) = D.m′ₘₐₓ -mₘₐₓ(D::WignerDMatrices) = D.mₘₐₓ -m′ₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(m′ₘₐₓ(D), ℓ) -mₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(mₘₐₓ(D), ℓ) - -Base.eltype(D::WignerDMatrices) = eltype(data(D)) - -isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = false -isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = true - - -""" - WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) - -Return the total size of the data stored in a `WignerDMatrices` object with the given sizes, -ranging over all matrices for all ℓ values. -""" -function WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)::Int - m₁, m₂ = m′ₘₐₓ, mₘₐₓ - if m₁ > m₂ - m₁, m₂ = m₂, m₁ - end - - if ℓₘₐₓ ≤ m₁ - (2ℓₘₐₓ + 1)*(2ℓₘₐₓ + 2)*(2ℓₘₐₓ + 3) ÷ 6 - elseif ℓₘₐₓ ≤ m₂ - ( - (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 - + (ℓₘₐₓ - m₁)*(2m₁ + 1)*(ℓₘₐₓ + m₁ + 2) - ) - else - ( - (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 - + (m₂ - m₁)*(2m₁ + 1)*(m₂ + m₁ + 2) - + (2m₁ + 1)*(2m₂ + 1)*(ℓₘₐₓ - m₂) - ) - end -end - - -@testsnippet WignerDUtilities begin - function indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) - data = Vector{Tuple{Int64, Int64, Int64}}(undef, sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ)) - i=1 - for ℓ ∈ 0:ℓₘₐₓ - for m ∈ -min(ℓ, mₘₐₓ):min(ℓ, mₘₐₓ) - for m′ ∈ -min(ℓ, m′ₘₐₓ):min(ℓ, m′ₘₐₓ) - data[i] = (ℓ, m′, m) - i += 1 - end - end - end - data - end -end - - -@testitem "Test WignerDsize" setup=[WignerDUtilities] begin - import SphericalFunctions.Redesign: WignerDsize - - for ℓₘₐₓ ∈ 0:8 - @test WignerDsize(ℓₘₐₓ, 0, 0) == ℓₘₐₓ + 1 - @test WignerDsize(ℓₘₐₓ, 1, 0) == 3ℓₘₐₓ + 1 - @test WignerDsize(ℓₘₐₓ, 0, 1) == 3ℓₘₐₓ + 1 - @test WignerDsize(ℓₘₐₓ, 1, 1) == (3^2)ℓₘₐₓ + 1 - @test WignerDsize(ℓₘₐₓ, 2, 0) == max(1, 5ℓₘₐₓ - 1) - @test WignerDsize(ℓₘₐₓ, 0, 2) == max(1, 5ℓₘₐₓ - 1) - @test WignerDsize(ℓₘₐₓ, 2, 1) == max(1, 15ℓₘₐₓ - 5) - @test WignerDsize(ℓₘₐₓ, 1, 2) == max(1, 15ℓₘₐₓ - 5) - @test WignerDsize(ℓₘₐₓ, 2, 2) == max(1, (5^2)ℓₘₐₓ - 15) - @test WignerDsize(ℓₘₐₓ, ℓₘₐₓ, ℓₘₐₓ) == sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ) - - for mₘₐₓ ∈ 0:ℓₘₐₓ - for m′ₘₐₓ ∈ 0:ℓₘₐₓ - @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == WignerDsize(ℓₘₐₓ, mₘₐₓ, m′ₘₐₓ) - - (m₁, m₂) = extrema((m′ₘₐₓ, mₘₐₓ)) - - @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == ( - sum(((2ℓ+1)^2 for ℓ ∈ 0:m₁), init=0) - + sum(((2m₁+1)*(2ℓ+1) for ℓ ∈ m₁+1:m₂); init=0) - + sum(((2m₁+1)*(2m₂+1) for ℓ ∈ m₂+1:ℓₘₐₓ); init=0) - ) - - data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) - for ℓ ∈ 0:ℓₘₐₓ-1 - @test data[WignerDsize(ℓ, m′ₘₐₓ, mₘₐₓ)] == (ℓ, min(m′ₘₐₓ, ℓ), min(mₘₐₓ, ℓ)) - end - - end - end - end -end - - -""" - WignerDMatrices(NT, ℓₘₐₓ; m′ₘₐₓ=ℓₘₐₓ, mₘₐₓ=ℓₘₐₓ) - -Create a `WignerDMatrices` object with the given parameters. The data is initialized to -zero. -""" -function WignerDMatrices(::Type{NT}, ℓₘₐₓ::IT; m′ₘₐₓ::IT=ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ) where {NT, IT} - # Massage the inputs - mₘₐₓ = abs(mₘₐₓ) - m′ₘₐₓ = abs(m′ₘₐₓ) - - # Check that the parameters are valid - if complex(NT) != NT - throw(ErrorException("NT=$NT must be a complex type")) - end - if ℓₘₐₓ < (limit = (IT<:Rational ? 1//2 : 0)) - throw(ErrorException("ℓₘₐₓ < $limit")) - end - if m′ₘₐₓ > ℓₘₐₓ - throw(ErrorException("m′ₘₐₓ > ℓₘₐₓ")) - end - if mₘₐₓ > ℓₘₐₓ - throw(ErrorException("mₘₐₓ > ℓₘₐₓ")) - end - - # Create the data array - data = zeros(NT, WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)) - - return WignerDMatrices{NT, IT}(data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) -end - - -""" - index(D, ℓ) - -Find the index in `data(D)` of the first element of the `WignerDMatrix` for the given ℓ -value. -""" -function index(D, ℓ) - if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) - throw(ErrorException("ℓ=$ℓ is out of range for D=$D")) - end - - if ℓ == ℓₘᵢₙ(D) - 1 - else - WignerDsize(ℓ-1, m′ₘₐₓ(D), mₘₐₓ(D)) + 1 - end -end - - -@testitem "Test WignerDMatrices index" setup=[WignerDUtilities] begin - import SphericalFunctions.Redesign: WignerDMatrices, index - - for ℓₘₐₓ ∈ 0:8 - for mₘₐₓ ∈ 0:ℓₘₐₓ - for m′ₘₐₓ ∈ 0:ℓₘₐₓ - data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) - D = WignerDMatrices(ComplexF64, ℓₘₐₓ; m′ₘₐₓ, mₘₐₓ) - for ℓ ∈ 0:ℓₘₐₓ - @test data[index(D, ℓ)] == (ℓ, -min(m′ₘₐₓ, ℓ), -min(mₘₐₓ, ℓ)) - end - - end - end - end -end - - -""" - size(D) - -Return the total size of the data stored in this WignerDMatrices object, ranging over all -matrices for all ℓ values. For the size of a particular matrix, use `size(D, ℓ)`. -""" -Base.size(D::WignerDMatrices) = WignerDsize(ℓₘₐₓ(D), m′ₘₐₓ(D), mₘₐₓ(D)) - - -""" - size(D, ℓ) - -Return the size of the data stored in this WignerDMatrices object for a particular ℓ value. -For the size of all matrices combined, use `size(D)`. -""" -function Base.size(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} - if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) - 0 - else - return (Int(2m′ₘₐₓ(D, ℓ)) + 1) * (Int(2mₘₐₓ(D, ℓ)) + 1) - end -end - -function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Rational} - throw(ErrorException("Don't yet know how to deal with Rational indices")) -end - -function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Integer} - i₁ = index(D, ℓ) - i₂ = i₁ + size(D, ℓ) - 1 - m′ = m′ₘₐₓ(D, ℓ) - m = mₘₐₓ(D, ℓ) - OffsetArrays.Origin(-m′, -m)(reshape((@view data(D)[i₁:i₂]), 2m′+1, 2m+1)) -end - - -@testitem "Test WignerDMatrices indices" setup=[WignerDUtilities] begin - import SphericalFunctions.Redesign: WignerDMatrices, index - - for ℓₘₐₓ ∈ 0:8 - for mₘₐₓ ∈ 0:ℓₘₐₓ - for m′ₘₐₓ ∈ 0:ℓₘₐₓ - data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) - D = WignerDMatrices{eltype(data), Int}( - data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ - ) - - for ℓ ∈ 0:ℓₘₐₓ - Dˡ = D[ℓ] - @test size(Dˡ) == (2min(m′ₘₐₓ, ℓ)+1, 2min(mₘₐₓ, ℓ)+1) - - for m ∈ -min(mₘₐₓ, ℓ):min(mₘₐₓ, ℓ) - for m′ ∈ -min(m′ₘₐₓ, ℓ):min(m′ₘₐₓ, ℓ) - @test Dˡ[m′, m] == (ℓ, m′, m) - end - end - end - end - end - end -end diff --git a/src/redesign/WignerMatrices.jl b/src/redesign/WignerMatrices.jl new file mode 100644 index 00000000..1b416970 --- /dev/null +++ b/src/redesign/WignerMatrices.jl @@ -0,0 +1,264 @@ +abstract type AbstractWignerMatrices{NT, IT, MT} <: AbstractVector{MT} end + + + +struct WignerDMatrices{NT, IT, MT} <: AbstractWignerMatrice{NT, IT, MT} + D::Vector{MT} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + function WignerDMatrices( + D::Vector{WignerDMatrix{NT, IT}} + ) where {NT, IT} + new{NT, IT, MT}(D, ℓₘₐₓ, m′ₘₐₓ) + end +end + + +# abstract type AbstractDMatrices end + + +# """ +# WignerDMatrices{NT, IT} + +# A data structure to hold the Wigner D-matrices for a range of `ℓ` values (stored in a +# `Vector{NT}`) up to and including some `ℓₘₐₓ`, `m′ₘₐₓ`, and `mₘₐₓ` (which all have type +# `IT`). + +# Indexing this object with an integer `ℓ` returns an `OffsetArray` of a view of the relevant +# part of the data vector corresponding to the `ℓ` matrix. +# """ +# struct WignerDMatrices{NT, IT} <: AbstractDMatrices +# data::Vector{NT} +# ℓₘₐₓ::IT +# m′ₘₐₓ::IT +# end + +# data(D::WignerDMatrices) = D.data +# ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = zero(IT) +# ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = IT(1//2) +# ℓₘₐₓ(D::WignerDMatrices) = D.ℓₘₐₓ +# m′ₘₐₓ(D::WignerDMatrices) = D.m′ₘₐₓ +# mₘₐₓ(D::WignerDMatrices) = D.mₘₐₓ +# m′ₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(m′ₘₐₓ(D), ℓ) +# mₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(mₘₐₓ(D), ℓ) + +# Base.eltype(D::WignerDMatrices) = eltype(data(D)) + +# isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = false +# isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = true + + +# """ +# WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) + +# Return the total size of the data stored in a `WignerDMatrices` object with the given sizes, +# ranging over all matrices for all ℓ values. +# """ +# function WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)::Int +# m₁, m₂ = m′ₘₐₓ, mₘₐₓ +# if m₁ > m₂ +# m₁, m₂ = m₂, m₁ +# end + +# if ℓₘₐₓ ≤ m₁ +# (2ℓₘₐₓ + 1)*(2ℓₘₐₓ + 2)*(2ℓₘₐₓ + 3) ÷ 6 +# elseif ℓₘₐₓ ≤ m₂ +# ( +# (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 +# + (ℓₘₐₓ - m₁)*(2m₁ + 1)*(ℓₘₐₓ + m₁ + 2) +# ) +# else +# ( +# (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 +# + (m₂ - m₁)*(2m₁ + 1)*(m₂ + m₁ + 2) +# + (2m₁ + 1)*(2m₂ + 1)*(ℓₘₐₓ - m₂) +# ) +# end +# end + + +# @testsnippet WignerDUtilities begin +# function indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) +# data = Vector{Tuple{Int64, Int64, Int64}}(undef, sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ)) +# i=1 +# for ℓ ∈ 0:ℓₘₐₓ +# for m ∈ -min(ℓ, mₘₐₓ):min(ℓ, mₘₐₓ) +# for m′ ∈ -min(ℓ, m′ₘₐₓ):min(ℓ, m′ₘₐₓ) +# data[i] = (ℓ, m′, m) +# i += 1 +# end +# end +# end +# data +# end +# end + + +# @testitem "Test WignerDsize" setup=[WignerDUtilities] begin +# import SphericalFunctions.Redesign: WignerDsize + +# for ℓₘₐₓ ∈ 0:8 +# @test WignerDsize(ℓₘₐₓ, 0, 0) == ℓₘₐₓ + 1 +# @test WignerDsize(ℓₘₐₓ, 1, 0) == 3ℓₘₐₓ + 1 +# @test WignerDsize(ℓₘₐₓ, 0, 1) == 3ℓₘₐₓ + 1 +# @test WignerDsize(ℓₘₐₓ, 1, 1) == (3^2)ℓₘₐₓ + 1 +# @test WignerDsize(ℓₘₐₓ, 2, 0) == max(1, 5ℓₘₐₓ - 1) +# @test WignerDsize(ℓₘₐₓ, 0, 2) == max(1, 5ℓₘₐₓ - 1) +# @test WignerDsize(ℓₘₐₓ, 2, 1) == max(1, 15ℓₘₐₓ - 5) +# @test WignerDsize(ℓₘₐₓ, 1, 2) == max(1, 15ℓₘₐₓ - 5) +# @test WignerDsize(ℓₘₐₓ, 2, 2) == max(1, (5^2)ℓₘₐₓ - 15) +# @test WignerDsize(ℓₘₐₓ, ℓₘₐₓ, ℓₘₐₓ) == sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ) + +# for mₘₐₓ ∈ 0:ℓₘₐₓ +# for m′ₘₐₓ ∈ 0:ℓₘₐₓ +# @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == WignerDsize(ℓₘₐₓ, mₘₐₓ, m′ₘₐₓ) + +# (m₁, m₂) = extrema((m′ₘₐₓ, mₘₐₓ)) + +# @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == ( +# sum(((2ℓ+1)^2 for ℓ ∈ 0:m₁), init=0) +# + sum(((2m₁+1)*(2ℓ+1) for ℓ ∈ m₁+1:m₂); init=0) +# + sum(((2m₁+1)*(2m₂+1) for ℓ ∈ m₂+1:ℓₘₐₓ); init=0) +# ) + +# data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) +# for ℓ ∈ 0:ℓₘₐₓ-1 +# @test data[WignerDsize(ℓ, m′ₘₐₓ, mₘₐₓ)] == (ℓ, min(m′ₘₐₓ, ℓ), min(mₘₐₓ, ℓ)) +# end + +# end +# end +# end +# end + + +# """ +# WignerDMatrices(NT, ℓₘₐₓ; m′ₘₐₓ=ℓₘₐₓ, mₘₐₓ=ℓₘₐₓ) + +# Create a `WignerDMatrices` object with the given parameters. The data is initialized to +# zero. +# """ +# function WignerDMatrices(::Type{NT}, ℓₘₐₓ::IT; m′ₘₐₓ::IT=ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ) where {NT, IT} +# # Massage the inputs +# mₘₐₓ = abs(mₘₐₓ) +# m′ₘₐₓ = abs(m′ₘₐₓ) + +# # Check that the parameters are valid +# if complex(NT) != NT +# throw(ErrorException("NT=$NT must be a complex type")) +# end +# if ℓₘₐₓ < (limit = (IT<:Rational ? 1//2 : 0)) +# throw(ErrorException("ℓₘₐₓ < $limit")) +# end +# if m′ₘₐₓ > ℓₘₐₓ +# throw(ErrorException("m′ₘₐₓ > ℓₘₐₓ")) +# end +# if mₘₐₓ > ℓₘₐₓ +# throw(ErrorException("mₘₐₓ > ℓₘₐₓ")) +# end + +# # Create the data array +# data = zeros(NT, WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)) + +# return WignerDMatrices{NT, IT}(data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) +# end + + +# """ +# index(D, ℓ) + +# Find the index in `data(D)` of the first element of the `WignerDMatrix` for the given ℓ +# value. +# """ +# function index(D, ℓ) +# if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) +# throw(ErrorException("ℓ=$ℓ is out of range for D=$D")) +# end + +# if ℓ == ℓₘᵢₙ(D) +# 1 +# else +# WignerDsize(ℓ-1, m′ₘₐₓ(D), mₘₐₓ(D)) + 1 +# end +# end + + +# @testitem "Test WignerDMatrices index" setup=[WignerDUtilities] begin +# import SphericalFunctions.Redesign: WignerDMatrices, index + +# for ℓₘₐₓ ∈ 0:8 +# for mₘₐₓ ∈ 0:ℓₘₐₓ +# for m′ₘₐₓ ∈ 0:ℓₘₐₓ +# data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) +# D = WignerDMatrices(ComplexF64, ℓₘₐₓ; m′ₘₐₓ, mₘₐₓ) +# for ℓ ∈ 0:ℓₘₐₓ +# @test data[index(D, ℓ)] == (ℓ, -min(m′ₘₐₓ, ℓ), -min(mₘₐₓ, ℓ)) +# end + +# end +# end +# end +# end + + +# """ +# size(D) + +# Return the total size of the data stored in this WignerDMatrices object, ranging over all +# matrices for all ℓ values. For the size of a particular matrix, use `size(D, ℓ)`. +# """ +# Base.size(D::WignerDMatrices) = WignerDsize(ℓₘₐₓ(D), m′ₘₐₓ(D), mₘₐₓ(D)) + + +# """ +# size(D, ℓ) + +# Return the size of the data stored in this WignerDMatrices object for a particular ℓ value. +# For the size of all matrices combined, use `size(D)`. +# """ +# function Base.size(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} +# if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) +# 0 +# else +# return (Int(2m′ₘₐₓ(D, ℓ)) + 1) * (Int(2mₘₐₓ(D, ℓ)) + 1) +# end +# end + +# function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Rational} +# throw(ErrorException("Don't yet know how to deal with Rational indices")) +# end + +# function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Integer} +# i₁ = index(D, ℓ) +# i₂ = i₁ + size(D, ℓ) - 1 +# m′ = m′ₘₐₓ(D, ℓ) +# m = mₘₐₓ(D, ℓ) +# OffsetArrays.Origin(-m′, -m)(reshape((@view data(D)[i₁:i₂]), 2m′+1, 2m+1)) +# end + + +# @testitem "Test WignerDMatrices indices" setup=[WignerDUtilities] begin +# import SphericalFunctions.Redesign: WignerDMatrices, index + +# for ℓₘₐₓ ∈ 0:8 +# for mₘₐₓ ∈ 0:ℓₘₐₓ +# for m′ₘₐₓ ∈ 0:ℓₘₐₓ +# data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) +# D = WignerDMatrices{eltype(data), Int}( +# data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ +# ) + +# for ℓ ∈ 0:ℓₘₐₓ +# Dˡ = D[ℓ] +# @test size(Dˡ) == (2min(m′ₘₐₓ, ℓ)+1, 2min(mₘₐₓ, ℓ)+1) + +# for m ∈ -min(mₘₐₓ, ℓ):min(mₘₐₓ, ℓ) +# for m′ ∈ -min(m′ₘₐₓ, ℓ):min(m′ₘₐₓ, ℓ) +# @test Dˡ[m′, m] == (ℓ, m′, m) +# end +# end +# end +# end +# end +# end +# end diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 1454f7cc..26aaa5ba 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -1,23 +1,99 @@ import Base: @propagate_inbounds -abstract type WignerMatrix{NT, IT} end +""" + WignerMatrix{NT, IT} + +Abstract base type for Wigner rotation‐matrix objects of a specific ``ℓ`` value. +- `NT` is the number type (e.g., `ComplexF64` for D-matrices or `Float64` for d-matrices). +- `IT` is the index type (an `Integer` or half‐integer `Rational`), governing the allowed + ranges of `m′` and `m`. + +The basic concrete subtypes (`WignerDMatrix`, `WignerdMatrix`) store their data in a +`Matrix{NT}` and implement the usual `size`, `getindex` and `setindex!` so that one can use +`w[m′,m]`. Specifically, these indices can be negative or positive, and must obey `abs(m′) +≤ m′ₘₐₓ` and `abs(m) ≤ ℓ`. + +# Methods + +Methods defined for `WignerMatrix` objects include: +- `parent(w)`: the underlying data array. +- `ℓ(w)` or `ell(w)`: the value of ``ℓ``. +- `m′ₘₐₓ(w)` or `mpmax(w)`: the maximum value of ``m′``. +- `mₘₐₓ(w)` or `mmax(w)`: the maximum value of ``m``. +- `ℓₘᵢₙ(w)` or `ellmin(w)`: the minimum value of ``ℓ``, which is either 0 or 1//2. +- `isrational(w)`: whether the indices are rational (i.e., half‐integer). +- `size(w)`: the size of the underlying data array. +- `length(w)`: the length of the underlying data array. +- `getindex(w, i)`: get the value at index `i` in the underlying data array. +- `getindex(w, m′, m)`: get the value at index `(m′, m)`. +- `setindex!(w, v, i)`: set the value at index `i` in the underlying data array to `v`. +- `setindex!(w, v, m′, m)`: set the value at index `(m′, m)`. +- `axes(w)`: the axes of the matrix, which are 2-tuples of ranges for the `m′` and `m` + indices. + +# Implementation + +Any new subtypes of `WignerMatrix` should inherit from this type and re-implement any of the +methods mentioned above that are not appropriate for the new type. Specifically, the +default implementations assume that subtypes store the fields +- `parent::Matrix{NT}`: the underlying data array. +- `ℓ::IT`: the value of ``ℓ``. +- `m′ₘₐₓ::IT`: the maximum value of ``m′``. + +For example, if the parent Matrix is not stored as the `parent` field, then the `parent(w)` +method should be re-implemented to return the correct parent object. The `getindex` and +`setindex!` +""" +abstract type WignerMatrix{NT, IT} <: AbstractMatrix{NT} end ### General methods for all WignerMatrix types -data(w::WignerMatrix{NT, IT}) where {NT, IT} = w.data +Base.parent(w::WignerMatrix{NT, IT}) where {NT, IT} = w.parent ℓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.ℓ m′ₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.m′ₘₐₓ -mₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.mₘₐₓ +mₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = ℓ(w) -ℓₘᵢₙ(::WignerMatrix{NT, IT}) where {NT, IT<:Integer} = zero(IT) -ℓₘᵢₙ(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = IT(1//2) +ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) +ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) +ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) +ℓₘᵢₙ(::WignerMatrix{NT, IT}) where {NT, IT} = ℓₘᵢₙ(IT) -is_rational(::WignerMatrix{NT, IT}) where {NT, IT<:Integer} = false -is_rational(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = true +const ell = ℓ +const mpmax = m′ₘₐₓ +const mmax = mₘₐₓ +const ellmin = ℓₘᵢₙ + +isrational(::WignerMatrix{NT, IT}) where {NT, IT<:Integer} = false +isrational(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = true Base.eltype(::WignerMatrix{NT, IT}) where {NT, IT} = NT -Base.size(w::WignerMatrix{NT, IT}) where {NT, IT} = size(data(w)) -Base.length(w::WignerMatrix{NT, IT}) where {NT, IT} = length(data(w)) +Base.size(w::WignerMatrix{NT, IT}) where {NT, IT} = size(parent(w)) +Base.length(w::WignerMatrix{NT, IT}) where {NT, IT} = length(parent(w)) + +struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} + start::T + stop::T + + WignerRange(r::UnitRange{T}) where {T} = new{T}(r.start, r.stop) +end +@inline Base.axes(r::WignerRange) = (axes1(r),) +@inline axes1(r::WignerRange) = WignerRange(r.start:r.stop) +if VERSION < v"1.8.2" + Base.axes1(r::WignerRange) = axes1(r) +end +Base.inds2string(inds::NTuple{2, WignerRange}) = + string(inds[1].start, ":", inds[1].stop, "×", inds[2].start, ":", inds[2].stop) + +function Base.axes(w::WignerMatrix{NT, IT}) where {NT, IT} + (WignerRange(-m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(-mₘₐₓ(w):mₘₐₓ(w))) +end + +# We don't have to override Base.show; most of its machinery works just fine, except that +# printing the data itself gets screwed up when the indices are Rational. So we override +# this core part of the printing machinery to just print the parent matrix as usual. The +# only other thing show really does is add a "summary" line, for which the only +Base.print_array(io::IO, w::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = + Base.print_array(io, parent(w)) @propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} @boundscheck begin @@ -27,7 +103,7 @@ Base.length(w::WignerMatrix{NT, IT}) where {NT, IT} = length(data(w)) )) end end - data(w)[i] + Base.parent(w)[i] end @propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} @boundscheck begin @@ -36,13 +112,13 @@ end "m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w))." )) end - if abs(m) > mₘₐₓ(w) + if abs(m) > ℓ(w) throw(BoundsError( - "m=$m out of bounds for WignerMatrix with mₘₐₓ=$(mₘₐₓ(w))." + "m=$m out of bounds for WignerMatrix with ℓ=$(ℓ(w))." )) end end - @inbounds data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] + @inbounds Base.parent(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] end @propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, i::Int) where {NT, IT} @@ -53,7 +129,7 @@ end )) end end - data(w)[i] = v + Base.parent(w)[i] = v end @propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, m′::IT, m::IT) where {NT, IT} @boundscheck begin @@ -62,185 +138,276 @@ end "m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w))." )) end - if abs(m) > mₘₐₓ(w) + if abs(m) > ℓ(w) throw(BoundsError( - "m=$m out of bounds for WignerMatrix with mₘₐₓ=$(mₘₐₓ(w))." + "m=$m out of bounds for WignerMatrix with ℓ=$(ℓ(w))." )) end end - data(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v -end - -function Base.axes(w::WignerMatrix{NT, IT}) where {NT, IT} - (-m′ₘₐₓ(w):m′ₘₐₓ(w), -mₘₐₓ(w):mₘₐₓ(w)) + Base.parent(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v end ### Specialize to D and d matrices +""" + WignerDMatrix{NT, IT} + +Specialized subtype of [`WignerMatrix`](@ref) for D-matrices, which are complex matrices. +""" struct WignerDMatrix{NT, IT} <: WignerMatrix{NT, IT} - data::Matrix{NT} + parent::Matrix{NT} ℓ::IT m′ₘₐₓ::IT - mₘₐₓ::IT - function WignerDMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT) where {NT, IT} - if ℓ < 0 - throw(ErrorException("ℓ=$ℓ should be non-negative.")) - end - if size(data, 1) == 0 - throw(ErrorException("Input data has 0 extent along first dimension.")) + function WignerDMatrix{NT, IT}(parent::Matrix{NT}, ℓ::IT) where {NT, IT<:Union{Integer, Rational}} + # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use + # a restriction on NT in the type declaration. + if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT + throw(ErrorException( + "WignerDMatrix only supports complex types; the input type is $NT.\n" + * "Perhaps you meant to use WignerdMatrix?" + )) end - if size(data, 2) == 0 - throw(ErrorException("Input data has 0 extent along second dimension.")) + if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) + throw(ErrorException( + "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" + * "if ℓ is an integer its type must be <:Integer, not <:Rational." + )) end - m′ₘₐₓ = IT((size(data, 1) - 1) // 2) - mₘₐₓ = IT((size(data, 2) - 1) // 2) - if ℓ < max(m′ₘₐₓ, mₘₐₓ) + s₁, s₂ = size(parent) + if s₂ ≠ Int(2ℓ + 1) throw(ErrorException( - "ℓ=$ℓ should be greater than or equal to both m′ₘₐₓ=$m′ₘₐₓ and mₘₐₓ=$mₘₐₓ." + "The extent of the second dimension in the input data must be " + * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." )) end - if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT + if s₁ == 0 || s₁ > s₂ throw(ErrorException( - "WignerDMatrix only supports complex types; the input type is $NT.\n" - * "Perhaps you meant to use WignerdMatrix?" + "The extent of the first dimension in the input data must be greater than 0" + * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." )) end if IT <: Rational - if denominator(ℓ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + if isodd(s₁) throw(ErrorException( - "Index limits must be either integers or half-integer Rationals; " - * "the inputs are Rationals: $ℓ, $m′ₘₐₓ, $mₘₐₓ." + "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " + * "input data ($s₁) corresponds to whole-integer values of m′." + )) + end + else + if iseven(s₁) + throw(ErrorException( + "ℓ=$ℓ is an integer, but the extent of the first dimension in the " + * "input data ($s₁) corresponds to half-integer values of m′." )) end end - new(data, ℓ, abs(m′ₘₐₓ), abs(mₘₐₓ)) + m′ₘₐₓ = IT((s₁ - 1) // 2) + new(parent, ℓ, m′ₘₐₓ) end end -function WignerDMatrix(data::Matrix{NT}, ℓ::IT) where {NT, IT} - WignerDMatrix{NT, IT}(data, ℓ) + +""" + WignerDMatrix(parent, ℓ) + +Construct a `WignerDMatrix` object from the given parent matrix and ``ℓ`` value. Note that +the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the +denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent +matrix must have the correct size: the first dimension must be greater than 0 and less than +or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. +""" +function WignerDMatrix(parent::Matrix{NT}, ℓ::IT) where {NT, IT} + WignerDMatrix{NT, IT}(parent, ℓ) +end +function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} + if complex(NT) ≢ NT + throw(ErrorException( + "WignerDMatrix only supports complex types; the input type is $NT.\n" + * "Perhaps you meant to use WignerdMatrix?" + )) + end + WignerDMatrix{NT, IT}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) end + +""" + WignerdMatrix{NT, IT} + +Specialized subtype of [`WignerMatrix`](@ref) for d-matrices, which are real matrices. +""" struct WignerdMatrix{NT, IT} <: WignerMatrix{NT, IT} - data::Matrix{NT} + parent::Matrix{NT} ℓ::IT m′ₘₐₓ::IT - mₘₐₓ::IT - function WignerdMatrix{NT, IT}(data::Matrix{NT}, ℓ::IT) where {NT, IT} - if ℓ < 0 - throw(ErrorException("ℓ=$ℓ should be non-negative.")) - end - if size(data, 1) == 0 - throw(ErrorException("Input data has 0 extent along first dimension.")) + function WignerdMatrix{NT, IT}(parent::Matrix{NT}, ℓ::IT) where {NT, IT<:Union{Integer, Rational}} + # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use + # a restriction on NT in the type declaration. + if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT + throw(ErrorException( + "WignerdMatrix only supports real types; the input type is $NT.\n" + * "Perhaps you meant to use WignerDMatrix?" + )) end - if size(data, 2) == 0 - throw(ErrorException("Input data has 0 extent along second dimension.")) + if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) + throw(ErrorException( + "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" + * "if ℓ is an integer its type must be <:Integer, not <:Rational." + )) end - m′ₘₐₓ = IT((size(data, 1) - 1) // 2) - mₘₐₓ = IT((size(data, 2) - 1) // 2) - if ℓ < max(m′ₘₐₓ, mₘₐₓ) + s₁, s₂ = size(parent) + if s₂ ≠ Int(2ℓ + 1) throw(ErrorException( - "ℓ=$ℓ should be greater than or equal to both m′ₘₐₓ=$m′ₘₐₓ and mₘₐₓ=$mₘₐₓ." + "The extent of the second dimension in the input data must be " + * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." )) end - if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT + if s₁ == 0 || s₁ > s₂ throw(ErrorException( - "WignerdMatrix only supports real types; the input type is $NT.\n" - * "Perhaps you meant to use WignerDMatrix?" + "The extent of the first dimension in the input data must be greater than 0" + * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." )) end if IT <: Rational - if denominator(ℓ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + if isodd(s₁) + throw(ErrorException( + "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " + * "input data ($s₁) corresponds to whole-integer values of m′." + )) + end + else + if iseven(s₁) throw(ErrorException( - "Index limits must be either integers or half-integer Rationals; " - * "the inputs are Rationals: $ℓ, $m′ₘₐₓ, $mₘₐₓ." + "ℓ=$ℓ is an integer, but the extent of the first dimension in the " + * "input data ($s₁) corresponds to half-integer values of m′." )) end end - new(data, ℓ, m′ₘₐₓ, mₘₐₓ) + m′ₘₐₓ = IT((s₁ - 1) // 2) + new(parent, ℓ, m′ₘₐₓ) end end -function WignerdMatrix(data::Matrix{NT}, ℓ::IT) where {NT, IT} - WignerdMatrix{NT, IT}(data, ℓ) + +""" + WignerdMatrix(parent, ℓ) + +Construct a `WignerdMatrix` object from the given parent matrix and ``ℓ`` value. Note that +the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the +denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent +matrix must have the correct size: the first dimension must be greater than 0 and less than +or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. +""" +function WignerdMatrix(parent::Matrix{NT}, ℓ::IT) where {NT, IT} + WignerdMatrix{NT, IT}(parent, ℓ) +end +function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} + if real(NT) ≢ NT + throw(ErrorException( + "WignerdMatrix only supports real types; the input type is $NT.\n" + * "Perhaps you meant to use WignerDMatrix?" + )) + end + WignerdMatrix{NT, IT}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) end @testitem "WignerMatrix" begin - import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix + import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, + parent, ell, mpmax, mmax, m′ₘₐₓ, mₘₐₓ + + # Check that mixed-up types throw an error + @test_throws "WignerDMatrix only supports complex types" WignerDMatrix(rand(Float64, 3, 3), 1) + @test_throws "WignerdMatrix only supports real types" WignerdMatrix(rand(ComplexF64, 3, 3), 1) + @test_throws "WignerDMatrix only supports complex types" WignerDMatrix(rand(Float64, 2, 2), 1//2) + @test_throws "WignerdMatrix only supports real types" WignerdMatrix(rand(ComplexF64, 2, 2), 1//2) # Check that a negative ℓ value throws an error - @test_throws "should be non-negative." WignerDMatrix(rand(ComplexF64, 3, 3), -1) - @test_throws "should be non-negative." WignerdMatrix(rand(Float64, 3, 3), -1) - @test_throws "should be non-negative." WignerDMatrix(rand(ComplexF64, 2, 2), -1//2) - @test_throws "should be non-negative." WignerdMatrix(rand(Float64, 2, 2), -1//2) - - for ℓ ∈ Any[collect(0:8); collect(1//2:15//2)] - # Check that ℓ < m′ₘₐₓ and ℓ < mₘₐₓ throw errors - @test_throws "both m′ₘₐₓ=" WignerDMatrix(Array{Float64}(undef, Int(2ℓ)+3, Int(2ℓ)+1), ℓ) - @test_throws "both m′ₘₐₓ=" WignerDMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+3), ℓ) - @test_throws "both m′ₘₐₓ=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+3, Int(2ℓ)+1), ℓ) - @test_throws "both m′ₘₐₓ=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+3), ℓ) + @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 3, 3), -1) + @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 3, 3), -1) + @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 2, 2), -1//2) + @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 2, 2), -1//2) + + # Check that a non-half-integer ℓ value throws an error + @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 3, 3), 1//3) + @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 3, 3), 1//3) + @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 2, 2), 1//3) + @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 2, 2), 1//3) + @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 3, 3), 2//2) + @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 3, 3), 2//2) + @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 2, 2), 2//2) + @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 2, 2), 2//2) + + #for ℓ ∈ Any[collect(0:8); collect(1//2:15//2)] + for ℓ ∈ Any[collect(0:2); collect(1//2:3//2)] + mₘ = ℓ + + # Check that ℓ < m′ₘₐₓ and ℓ ≠ mₘₐₓ throw errors + @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) + @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) + @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) + @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) + @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) # Check that a mismatch between integer/half-integer throws an error if ℓ>0 && ℓ isa Int - @test_throws "InexactError: Int64(1//2)" WignerDMatrix(rand(ComplexF64, 2, 2), ℓ) - @test_throws "InexactError: Int64(1//2)" WignerdMatrix(rand(Float64, 2, 2), ℓ) + @test_throws "is an integer, but the extent of the first dimension" WignerDMatrix(rand(ComplexF64, 2ℓ, 2ℓ+1), ℓ) + @test_throws "is an integer, but the extent of the first dimension" WignerdMatrix(rand(Float64, 2ℓ, 2ℓ+1), ℓ) elseif ℓ isa Rational - @test_throws "either integers or half-integer" WignerDMatrix(rand(ComplexF64, 1, 1), ℓ) - @test_throws "either integers or half-integer" WignerdMatrix(rand(Float64, 1, 1), ℓ) - end - - for mₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) - # Check a data array with a dimension of 0 extent throws an error. - # (Note that we're pretending mₘₐₓ is m′ₘₐₓ for two cases, just for efficiency.) - @test_throws "along second dim" WignerDMatrix(Array{Float64}(undef, Int(2mₘₐₓ)+1, 0), ℓ) - @test_throws "along first dim" WignerDMatrix(Array{Float64}(undef, 0, Int(2mₘₐₓ)+1), ℓ) - @test_throws "along second dim" WignerdMatrix(Array{Float64}(undef, Int(2mₘₐₓ)+1, 0), ℓ) - @test_throws "along first dim" WignerdMatrix(Array{Float64}(undef, 0, Int(2mₘₐₓ)+1), ℓ) - - for m′ₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) - # Make a big, dumb array full of the explicit indices. - data = [ - (ℓ, m′, m) - for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ, m ∈ -mₘₐₓ:mₘₐₓ - ] - # Check that indexing works as expected. - for w ∈ (WignerDMatrix(data, ℓ), WignerdMatrix(data, ℓ)) - @test w.data == data - @test w.ℓ == ℓ - @test w.m′ₘₐₓ == m′ₘₐₓ - @test w.mₘₐₓ == mₘₐₓ - for m ∈ -mₘₐₓ:mₘₐₓ - for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ - @test w[m′, m] == (ℓ, m′, m) - end + @test_throws "is a half-integer, but the extent of the first dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ), Int(2ℓ+1)), ℓ) + @test_throws "is a half-integer, but the extent of the first dimension" WignerdMatrix(rand(Float64, Int(2ℓ), Int(2ℓ+1)), ℓ) + end + @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(rand(ComplexF64, Int(2ℓ+1), Int(2ℓ)), ℓ) + @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(rand(Float64, Int(2ℓ+1), Int(2ℓ)), ℓ) + + # Check that a data array with a dimension of 0 extent throws an error. + @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, 0), ℓ) + @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, 0, Int(2ℓ)+1), ℓ) + @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, 0), ℓ) + @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, 0, Int(2ℓ)+1), ℓ) + + for m′ₘ ∈ ℓₘᵢₙ(ℓ):ℓ + # Make a big, dumb array full of the explicit indices. + data = [ + (ℓ, m′, m) + for m′ ∈ -m′ₘ:m′ₘ, m ∈ -mₘ:mₘ + ] + # Check that indexing works as expected. + for WignerMatrixType ∈ (WignerDMatrix, WignerdMatrix) + w = WignerMatrixType(data, ℓ) + @test Base.parent(w) == data + @test ell(w) == ℓ + @test mpmax(w) == m′ₘ + @test mmax(w) == ℓ + for m ∈ -mₘ:mₘ + for m′ ∈ -m′ₘ:m′ₘ + @test w[m′, m] == (ℓ, m′, m) end end end + end - for m′ₘₐₓ ∈ (ℓ isa Rational ? (1//2:ℓ) : (0:ℓ)) - for WignerMatrixType ∈ (WignerDMatrix, WignerdMatrix) - data = rand( - WignerMatrixType<:WignerDMatrix ? ComplexF64 : Float64, - Int(2mₘₐₓ)+1, Int(2m′ₘₐₓ)+1 - ) - w = WignerMatrixType(data, ℓ) - - # Check that the data array is stored correctly. - @test w.data == data - @test w.ℓ == ℓ - @test w.m′ₘₐₓ == m′ₘₐₓ - @test w.mₘₐₓ == mₘₐₓ - - # The Julia docs say that the `axes` function should - # > Return a tuple of `AbstractUnitRange{<:Integer}` of valid indices. - # > The axes should be their own axes, that is `axes.(axes(A),1) == - # > axes(A)` should be satisfied. - # https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array - @test typeof(axes(w)) <: AbstractUnitRange{<:Integer} - @test axes.(axes(w),1) == axes(w) - end + for m′ₘ ∈ ℓₘᵢₙ(ℓ):ℓ + for WignerMatrixType ∈ (WignerDMatrix, WignerdMatrix) + data = rand( + WignerMatrixType<:WignerDMatrix ? ComplexF64 : Float64, + Int(2m′ₘ)+1, Int(2mₘ)+1 + ) + w = WignerMatrixType(data, ℓ) + + # Check that the data array is stored correctly. + @test Base.parent(w) == data + @test ell(w) == ℓ + @test m′ₘₐₓ(w) == m′ₘ + @test mₘₐₓ(w) == ℓ + + # The Julia docs say that the `axes` function should + # > Return a tuple of `AbstractUnitRange{<:Integer}` of valid indices. + # > The axes should be their own axes, that is `axes.(axes(A),1) == + # > axes(A)` should be satisfied. + # https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array + @test typeof(axes(w)) <: NTuple{2, AbstractUnitRange} + @test axes.(axes(w),1) == axes(w) end end end From d92b28858d10e5d5759834f47321a157e5dd1b9c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 6 Oct 2025 10:04:18 -0400 Subject: [PATCH 187/329] Practical steps to compute Wigner recurrences --- src/redesign/SphericalFunctions.jl | 1 + src/redesign/WignerMatrices.jl | 12 ++++++- src/redesign/WignerMatrix.jl | 14 ++++++++ src/redesign/recurrence.jl | 54 +++++++++++++++++++++++++----- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index a96fe0dc..7b745d51 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -6,6 +6,7 @@ import TestItems: @testitem, @testsnippet include("WignerMatrix.jl") include("WignerMatrices.jl") +include("recurrence.jl") function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} diff --git a/src/redesign/WignerMatrices.jl b/src/redesign/WignerMatrices.jl index 1b416970..ae455c1a 100644 --- a/src/redesign/WignerMatrices.jl +++ b/src/redesign/WignerMatrices.jl @@ -1,8 +1,18 @@ +""" + AbstractWignerMatrices{NT, IT, MT} + +A container for a series of Wigner matrices ( +- `NT` is the number type (e.g., `ComplexF64` for D-matrices or `Float64` for d-matrices). +- `IT` is the index type (an `Integer` or half‐integer `Rational`), governing the allowed + ranges of `m′` and `m` in each matrix. +- `MT` is the type of the matrices. + +""" abstract type AbstractWignerMatrices{NT, IT, MT} <: AbstractVector{MT} end -struct WignerDMatrices{NT, IT, MT} <: AbstractWignerMatrice{NT, IT, MT} +struct WignerDMatrices{NT, IT, MT} <: AbstractWignerMatrices{NT, IT, MT} D::Vector{MT} ℓₘₐₓ::IT m′ₘₐₓ::IT diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 26aaa5ba..e2270051 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -311,6 +311,20 @@ function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} end +""" + Hˡrow{NT, IT} + +Specialized subtype of [`WignerMatrix`](@ref) intended to store one row of the ``H`` matrix +— usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the +recurrence relations. +""" +struct Hˡrow{NT, IT} <: WignerMatrix{NT, IT} + parent::Matrix{NT} + ℓ::IT + m′ₘₐₓ::IT +end + + @testitem "WignerMatrix" begin import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, parent, ell, mpmax, mmax, m′ₘₐₓ, mₘₐₓ diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 928d989e..45119933 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -6,7 +6,17 @@ sgn(m) = m ≥ 0 ? 1 : -1 ϵ(m) = (m ≥ 0 ? (-1)^m : 1) -function initialize!(Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T) where {NT, IT<:Integer, T} +@doc raw""" + initialize!(Hˡ, sinβ, cosβ) + +Step 1 of the computation of ``H``: Initialize the Wigner matrix `Hˡ` for the recurrence +relations. This only sets the values ``H^0_{0,0}=0``, ``H^1_{0,0}=cosβ``, and +``H^1_{0,1}=sinβ/√2``. + +Note that `Hˡ` can be any `WignerMatrix` with integer indices. In particular, it can be a +`D` matrix or a `d` matrix. +""" +function initialize!(Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T) where {NT, IT<:Signed, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) if ℓ == 0 Hˡ[0, 0] = 0 @@ -17,9 +27,16 @@ function initialize!(Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T) where {NT, I end end +@doc raw""" + recurrence_0_m!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + +Step 2 of the computation of ``H``: Given ``H^{\ell-1}`` with its ``(0, m)`` entries, +compute ``H^{\ell}_{0,m}`` for all ``m \geq 0``. + +""" function recurrence_0_m!( Hˡ::WignerMatrix{NT, IT}, Hˡ⁻¹::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Integer, T} +) where {NT, IT<:Signed, T} @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 # Note that in this step only, we use notation derived from Xing et al., denoting the # coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following steps, we will use notation @@ -55,9 +72,16 @@ function recurrence_0_m!( end end +@doc raw""" + recurrence_1_m!(Hˡ, Hˡ⁺¹, sinβ, cosβ) + +Step 3 of the computation of ``H``: Given ``H^{\ell+1}`` with all its ``(0, m)`` entries, +compute ``H^{\ell}_{1,m}`` for all ``m \geq 1``. + +""" function recurrence_1_m!( Hˡ::WignerMatrix{NT, IT}, Hˡ⁺¹::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Integer, T} +) where {NT, IT<:Signed, T} @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) if ℓ > 0 && m′ₘₐₓ ≥ 1 @@ -76,9 +100,16 @@ function recurrence_1_m!( end end +@doc raw""" + recurrence_m′₊!(Hˡ, sinβ, cosβ) + +Step 4 of the computation of ``H``: Given ``H^{\ell}`` with all its ``(0, m)`` and ``(1, +m)`` entries, compute ``H^{\ell}_{m′+1,m}`` for all ``m′ \geq 1`` and ``m \geq 1``. + +""" function recurrence_m′₊!( Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Integer, T} +) where {NT, IT<:Signed, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 # Note that the signs of m′ and m are always +1, so we leave them out of the @@ -105,9 +136,16 @@ function recurrence_m′₊!( end end +@doc raw""" + recurrence_m′₋!(Hˡ, sinβ, cosβ) + +Step 5 of the computation of ``H``: Given ``H^{\ell}`` with all its ``(m′, m)`` entries for +`m′ ≥ 0`, compute ``H^{\ell}_{m′-1,m}`` for all `m′ ≤ -1` and `m`. + +""" function recurrence_m′₋!( Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Integer, T} +) where {NT, IT<:Signed, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) @@ -147,7 +185,7 @@ H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. ``` """ -function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Integer} +function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Signed} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m ∈ -ℓ:ℓ for m′ ∈ abs(m):m′ₘₐₓ @@ -165,7 +203,7 @@ Convert the Wigner matrix `Hˡ` to the d matrix `dˡ`, which just involves multi signs related to the `m′` and `m` indices. """ -function convert_H_to_d!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Integer} +function convert_H_to_d!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Signed} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m ∈ -ℓ:ℓ for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ @@ -183,7 +221,7 @@ Convert the Wigner matrix `Hˡ` to the D matrix `Dˡ`, which just involves multi complex phases related to the `m′` and `m` indices. """ -function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}, eⁱᵅ::NT, eⁱᵞ::NT) where {NT, IT<:Integer} +function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}, eⁱᵅ::NT, eⁱᵞ::NT) where {NT, IT<:Signed} # NOTE: This function will have to be modified to work for Rational indices because the # phases will not be integer powers; we'll have to incorporate √eⁱᵅ and √eⁱᵞ. @inbounds let ℓ=ℓ(Hˡ), ℓₘᵢₙ=ℓₘᵢₙ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) From 90c7ade4131f61d2d817ee72f2989ecd6a3b1082 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 6 Oct 2025 10:04:41 -0400 Subject: [PATCH 188/329] Temporarily document redesign components --- docs/make.jl | 1 + docs/src/redesign.md | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 docs/src/redesign.md diff --git a/docs/make.jl b/docs/make.jl index 14521174..28b0ff3b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -73,6 +73,7 @@ makedocs( sort(readdir(joinpath(docs_src_dir, "notes"))) ), "References" => "references.md", + "Redesign" => "redesign.md", ], #warnonly=true, #doctest = false, diff --git a/docs/src/redesign.md b/docs/src/redesign.md new file mode 100644 index 00000000..19a6b004 --- /dev/null +++ b/docs/src/redesign.md @@ -0,0 +1,5 @@ + +```@autodocs +Modules = [SphericalFunctions.Redesign] +Pages = ["redesign/SphericalFunctions.jl", "redesign/recurrence.jl", "redesign/WignerMatrix.jl", "redesign/WignerMatrices.jl"] +``` From b45bd1d7f19beb5be2f18c4546203ae77f680bb9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 7 Oct 2025 00:47:06 -0400 Subject: [PATCH 189/329] Add Hrow struct --- src/redesign/SphericalFunctions.jl | 3 ++ src/redesign/WignerMatrix.jl | 66 +++++++++++++----------------- src/redesign/recurrence.jl | 50 ++++++++++++++-------- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index 7b745d51..bb32f373 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -3,6 +3,9 @@ module Redesign import Quaternionic import TestItems: @testitem, @testsnippet +# TEMPORARY!!!! +import SphericalFunctions: ComplexPowers + include("WignerMatrix.jl") include("WignerMatrices.jl") diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index e2270051..cb446551 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -82,7 +82,7 @@ if VERSION < v"1.8.2" Base.axes1(r::WignerRange) = axes1(r) end Base.inds2string(inds::NTuple{2, WignerRange}) = - string(inds[1].start, ":", inds[1].stop, "×", inds[2].start, ":", inds[2].stop) + string("(", inds[1].start, ":", inds[1].stop, ")×(", inds[2].start, ":", inds[2].stop, ")") function Base.axes(w::WignerMatrix{NT, IT}) where {NT, IT} (WignerRange(-m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(-mₘₐₓ(w):mₘₐₓ(w))) @@ -96,53 +96,27 @@ Base.print_array(io::IO, w::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = Base.print_array(io, parent(w)) @propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} - @boundscheck begin - if i<1 || i>length(w) - throw(BoundsError( - "i=$i out of bounds for WignerMatrix with length=$(length(w))." - )) - end + @boundscheck if i<1 || i>length(w) + throw(BoundsError(w, i)) end Base.parent(w)[i] end @propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} - @boundscheck begin - if abs(m′) > m′ₘₐₓ(w) - throw(BoundsError( - "m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w))." - )) - end - if abs(m) > ℓ(w) - throw(BoundsError( - "m=$m out of bounds for WignerMatrix with ℓ=$(ℓ(w))." - )) - end + @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) + throw(BoundsError(w, (m′, m))) end @inbounds Base.parent(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] end -@propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, i::Int) where {NT, IT} - @boundscheck begin - if i<1 || i>length(w) - throw(BoundsError( - "i=$i out of bounds for WignerMatrix with length=$(length(w))." - )) - end +@propagate_inbounds function Base.setindex!(w::WignerMatrix, v, i) + @boundscheck if i<1 || i>length(w) + throw(BoundsError(w, i)) end Base.parent(w)[i] = v end -@propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v::NT, m′::IT, m::IT) where {NT, IT} - @boundscheck begin - if abs(m′) > m′ₘₐₓ(w) - throw(BoundsError( - "m′=$m′ out of bounds for WignerMatrix with m′ₘₐₓ=$(m′ₘₐₓ(w))." - )) - end - if abs(m) > ℓ(w) - throw(BoundsError( - "m=$m out of bounds for WignerMatrix with ℓ=$(ℓ(w))." - )) - end +@propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v, m′::IT, m::IT) where {NT, IT} + @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) + throw(BoundsError(w, (m′, m))) end Base.parent(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v end @@ -324,6 +298,24 @@ struct Hˡrow{NT, IT} <: WignerMatrix{NT, IT} m′ₘₐₓ::IT end +function Base.axes(w::Hˡrow) + (WignerRange(m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(0:mₘₐₓ(w))) +end + +@propagate_inbounds function Base.getindex(w::Hˡrow{NT, IT}, m′::IT, m::IT) where {NT, IT} + @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) + throw(BoundsError(w, (m′, m))) + end + @inbounds Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] +end + +@propagate_inbounds function Base.setindex!(w::Hˡrow{NT, IT}, v, m′::IT, m::IT) where {NT, IT} + @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) + throw(BoundsError(w, (m′, m))) + end + Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] = v +end + @testitem "WignerMatrix" begin import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 45119933..0352dd05 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -25,6 +25,7 @@ function initialize!(Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T) where {NT, I Hˡ[0, 1] = sinβ / √2 end end + Hˡ end @doc raw""" @@ -35,41 +36,44 @@ compute ``H^{\ell}_{0,m}`` for all ``m \geq 0``. """ function recurrence_0_m!( - Hˡ::WignerMatrix{NT, IT}, Hˡ⁻¹::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Signed, T} + Hˡ::WignerMatrix{NT, IT}, Hˡ⁻¹::WignerMatrix{NT2, IT}, sinβ::T, cosβ::T +) where {NT, NT2, IT<:Signed, T} @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 # Note that in this step only, we use notation derived from Xing et al., denoting the # coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following steps, we will use notation # from Gumerov and Duraiswami, who denote their different coefficients aₗᵐ, etc. - @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) + #@inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) + @warn "Turned off inbounds for debugging" + let √=sqrt∘T, ℓ=ℓ(Hˡ) if ℓ > 1 b̄ₗ = √(T(ℓ-1)/ℓ) Hˡ[0, 0] = cosβ * Hˡ⁻¹[0, 0] - b̄ₗ * sinβ * Hˡ⁻¹[0, 1] for m ∈ 1:ℓ-2 - c̄ₙₘ = √((ℓ+m)*(ℓ-m)) / ℓ - d̄ₙₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ - ēₙₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ + d̄ₗₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ Hˡ[0, m] = ( c̄ₗₘ * cosβ * Hˡ⁻¹[0, m] - sinβ * (d̄ₗₘ * Hˡ⁻¹[0, m+1] - ēₗₘ * Hˡ⁻¹[0, m-1]) ) end let m = ℓ-1 - c̄ₙₘ = √((ℓ+m)*(ℓ-m)) / ℓ - ēₙₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ Hˡ[0, m] = ( c̄ₗₘ * cosβ * Hˡ⁻¹[0, m] - sinβ * (- ēₗₘ * Hˡ⁻¹[0, m-1]) ) end let m = ℓ - ēₙₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ Hˡ[0, m] = ( - sinβ * (- ēₗₘ * Hˡ⁻¹[0, m-1]) ) end end end + Hˡ end @doc raw""" @@ -80,13 +84,13 @@ compute ``H^{\ell}_{1,m}`` for all ``m \geq 1``. """ function recurrence_1_m!( - Hˡ::WignerMatrix{NT, IT}, Hˡ⁺¹::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Signed, T} + Hˡ::WignerMatrix{NT, IT}, Hˡ⁺¹::WignerMatrix{NT2, IT}, sinβ::T, cosβ::T +) where {NT, NT2, IT<:Signed, T} @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) if ℓ > 0 && m′ₘₐₓ ≥ 1 c = 1 / √(ℓ*(ℓ+1)) - for m ∈ 0:ℓ + for m ∈ 1:ℓ āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) b̄ₗ₊₁ᵐ⁻¹ = √((ℓ-m+1)*(ℓ-m+2)) b̄ₗ₊₁⁻ᵐ⁻¹ = √((ℓ+m+1)*(ℓ+m+2)) @@ -98,6 +102,7 @@ function recurrence_1_m!( end end end + Hˡ end @doc raw""" @@ -116,7 +121,7 @@ function recurrence_m′₊!( # calculations of d̄ in this function. d̄ₗᵐ′ = √((ℓ-m′)*(ℓ+m′+1)) d̄ₗᵐ′⁻¹ = √((ℓ-m′+1)*(ℓ+m′)) - for m ∈ m′:ℓ-1 + for m ∈ (m′+1):ℓ-1 d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) d̄ₗᵐ = √((ℓ-m)*(ℓ+m+1)) Hˡ[m′+1, m] = ( @@ -134,6 +139,7 @@ function recurrence_m′₊!( end end end + Hˡ end @doc raw""" @@ -150,7 +156,7 @@ function recurrence_m′₋!( for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) - for m ∈ -m′:ℓ-1 + for m ∈ -(m′-1):ℓ-1 d̄ₗᵐ = sgn(m) * √((ℓ-m)*(ℓ+m+1)) d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) Hˡ[m′-1, m] = ( @@ -168,6 +174,7 @@ function recurrence_m′₋!( end end end + Hˡ end @doc raw""" @@ -187,12 +194,19 @@ H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. """ function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Signed} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) - for m ∈ -ℓ:ℓ - for m′ ∈ abs(m):m′ₘₐₓ - Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] + # The idea here is to impose + # Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] + # without double-counting any entries, and accounting for m′ₘₐₓ. + for m ∈ 1:ℓ + for m′ ∈ -min(m′ₘₐₓ, m):min(m′ₘₐₓ, m) + Hˡ[-m′, -m] = Hˡ[m′, m] + end + for m′ ∈ -min(m′ₘₐₓ, m-1):min(m′ₘₐₓ, m-1) + Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[m′, m] end end end + Hˡ end @@ -211,6 +225,7 @@ function convert_H_to_d!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Signed} end end end + Hˡ end @@ -242,4 +257,5 @@ function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}, eⁱᵅ::NT, eⁱᵞ::NT) wh end end end + Hˡ end From 8919c576ec158218997121297cad275c3e5d38c9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 7 Oct 2025 12:43:56 -0400 Subject: [PATCH 190/329] Reorder and extend type parameters --- src/redesign/WignerMatrix.jl | 84 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index cb446551..f61b0edc 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -1,14 +1,16 @@ import Base: @propagate_inbounds """ - WignerMatrix{NT, IT} + WignerMatrix{IT, NT, ST} Abstract base type for Wigner rotation‐matrix objects of a specific ``ℓ`` value. -- `NT` is the number type (e.g., `ComplexF64` for D-matrices or `Float64` for d-matrices). -- `IT` is the index type (an `Integer` or half‐integer `Rational`), governing the allowed +- `IT` is the index type (an `Integer` or half-integer `Rational`), governing the allowed ranges of `m′` and `m`. +- `NT` is the number type (e.g., `ComplexF64` for D-matrices or `Float64` for d-matrices). +- `ST` is the storage type (typically `Matrix{NT}`, but other `AbstractMatrix{NT}` storage + can be used). -The basic concrete subtypes (`WignerDMatrix`, `WignerdMatrix`) store their data in a +The basic concrete subtypes (`WignerDMatrix`, `WignerdMatrix`) default to storing their data in a `Matrix{NT}` and implement the usual `size`, `getindex` and `setindex!` so that one can use `w[m′,m]`. Specifically, these indices can be negative or positive, and must obey `abs(m′) ≤ m′ₘₐₓ` and `abs(m) ≤ ℓ`. @@ -44,31 +46,31 @@ For example, if the parent Matrix is not stored as the `parent` field, then the method should be re-implemented to return the correct parent object. The `getindex` and `setindex!` """ -abstract type WignerMatrix{NT, IT} <: AbstractMatrix{NT} end +abstract type WignerMatrix{IT, NT, ST} <: AbstractMatrix{NT} end ### General methods for all WignerMatrix types -Base.parent(w::WignerMatrix{NT, IT}) where {NT, IT} = w.parent -ℓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.ℓ -m′ₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = w.m′ₘₐₓ -mₘₐₓ(w::WignerMatrix{NT, IT}) where {NT, IT} = ℓ(w) +Base.parent(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = w.parent +ℓ(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = w.ℓ +m′ₘₐₓ(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = w.m′ₘₐₓ +mₘₐₓ(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = ℓ(w) ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) -ℓₘᵢₙ(::WignerMatrix{NT, IT}) where {NT, IT} = ℓₘᵢₙ(IT) +ℓₘᵢₙ(::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = ℓₘᵢₙ(IT) const ell = ℓ const mpmax = m′ₘₐₓ const mmax = mₘₐₓ const ellmin = ℓₘᵢₙ -isrational(::WignerMatrix{NT, IT}) where {NT, IT<:Integer} = false -isrational(::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = true +isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Integer, NT, ST} = false +isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Rational, NT, ST} = true -Base.eltype(::WignerMatrix{NT, IT}) where {NT, IT} = NT -Base.size(w::WignerMatrix{NT, IT}) where {NT, IT} = size(parent(w)) -Base.length(w::WignerMatrix{NT, IT}) where {NT, IT} = length(parent(w)) +Base.eltype(::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT +Base.size(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) +Base.length(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} start::T @@ -84,7 +86,7 @@ end Base.inds2string(inds::NTuple{2, WignerRange}) = string("(", inds[1].start, ":", inds[1].stop, ")×(", inds[2].start, ":", inds[2].stop, ")") -function Base.axes(w::WignerMatrix{NT, IT}) where {NT, IT} +function Base.axes(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} (WignerRange(-m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(-mₘₐₓ(w):mₘₐₓ(w))) end @@ -92,16 +94,16 @@ end # printing the data itself gets screwed up when the indices are Rational. So we override # this core part of the printing machinery to just print the parent matrix as usual. The # only other thing show really does is add a "summary" line, for which the only -Base.print_array(io::IO, w::WignerMatrix{NT, IT}) where {NT, IT<:Rational} = +Base.print_array(io::IO, w::WignerMatrix{IT, NT, ST}) where {IT<:Rational, NT, ST} = Base.print_array(io, parent(w)) -@propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, i::Int) where {NT, IT} +@propagate_inbounds function Base.getindex(w::WignerMatrix{IT, NT, ST}, i::Int) where {IT, NT, ST} @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end Base.parent(w)[i] end -@propagate_inbounds function Base.getindex(w::WignerMatrix{NT, IT}, m′::IT, m::IT) where {NT, IT} +@propagate_inbounds function Base.getindex(w::WignerMatrix{IT, NT, ST}, m′::IT, m::IT) where {IT, NT, ST} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end @@ -114,7 +116,7 @@ end end Base.parent(w)[i] = v end -@propagate_inbounds function Base.setindex!(w::WignerMatrix{NT, IT}, v, m′::IT, m::IT) where {NT, IT} +@propagate_inbounds function Base.setindex!(w::WignerMatrix{IT, NT, ST}, v, m′::IT, m::IT) where {IT, NT, ST} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end @@ -125,15 +127,15 @@ end ### Specialize to D and d matrices """ - WignerDMatrix{NT, IT} + WignerDMatrix{IT, NT, ST} Specialized subtype of [`WignerMatrix`](@ref) for D-matrices, which are complex matrices. """ -struct WignerDMatrix{NT, IT} <: WignerMatrix{NT, IT} - parent::Matrix{NT} +struct WignerDMatrix{IT, NT, ST<:AbstractMatrix{NT}} <: WignerMatrix{IT, NT, ST} + parent::ST ℓ::IT m′ₘₐₓ::IT - function WignerDMatrix{NT, IT}(parent::Matrix{NT}, ℓ::IT) where {NT, IT<:Union{Integer, Rational}} + function WignerDMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use # a restriction on NT in the type declaration. if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT @@ -190,31 +192,31 @@ denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, matrix must have the correct size: the first dimension must be greater than 0 and less than or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. """ -function WignerDMatrix(parent::Matrix{NT}, ℓ::IT) where {NT, IT} - WignerDMatrix{NT, IT}(parent, ℓ) +function WignerDMatrix(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} + WignerDMatrix{IT, NT, ST}(parent, ℓ) end -function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} +function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT<:Union{Integer, Rational}} if complex(NT) ≢ NT throw(ErrorException( "WignerDMatrix only supports complex types; the input type is $NT.\n" * "Perhaps you meant to use WignerdMatrix?" )) end - WignerDMatrix{NT, IT}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) + WignerDMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) end """ - WignerdMatrix{NT, IT} + WignerdMatrix{IT, NT, ST} Specialized subtype of [`WignerMatrix`](@ref) for d-matrices, which are real matrices. """ -struct WignerdMatrix{NT, IT} <: WignerMatrix{NT, IT} - parent::Matrix{NT} +struct WignerdMatrix{IT, NT, ST<:AbstractMatrix{NT}} <: WignerMatrix{IT, NT, ST} + parent::ST ℓ::IT m′ₘₐₓ::IT - function WignerdMatrix{NT, IT}(parent::Matrix{NT}, ℓ::IT) where {NT, IT<:Union{Integer, Rational}} + function WignerdMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use # a restriction on NT in the type declaration. if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT @@ -271,29 +273,29 @@ denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, matrix must have the correct size: the first dimension must be greater than 0 and less than or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. """ -function WignerdMatrix(parent::Matrix{NT}, ℓ::IT) where {NT, IT} - WignerdMatrix{NT, IT}(parent, ℓ) +function WignerdMatrix(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} + WignerdMatrix{IT, NT, ST}(parent, ℓ) end -function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} +function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT<:Union{Integer, Rational}} if real(NT) ≢ NT throw(ErrorException( "WignerdMatrix only supports real types; the input type is $NT.\n" * "Perhaps you meant to use WignerDMatrix?" )) end - WignerdMatrix{NT, IT}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) + WignerdMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) end """ - Hˡrow{NT, IT} + Hˡrow{IT, NT, ST} Specialized subtype of [`WignerMatrix`](@ref) intended to store one row of the ``H`` matrix — usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the recurrence relations. """ -struct Hˡrow{NT, IT} <: WignerMatrix{NT, IT} - parent::Matrix{NT} +struct Hˡrow{IT, NT, ST<:AbstractMatrix{NT}} <: WignerMatrix{IT, NT, ST} + parent::ST ℓ::IT m′ₘₐₓ::IT end @@ -302,14 +304,14 @@ function Base.axes(w::Hˡrow) (WignerRange(m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(0:mₘₐₓ(w))) end -@propagate_inbounds function Base.getindex(w::Hˡrow{NT, IT}, m′::IT, m::IT) where {NT, IT} +@propagate_inbounds function Base.getindex(w::Hˡrow{IT, NT, ST}, m′::IT, m::IT) where {IT, NT, ST} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end @inbounds Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] end -@propagate_inbounds function Base.setindex!(w::Hˡrow{NT, IT}, v, m′::IT, m::IT) where {NT, IT} +@propagate_inbounds function Base.setindex!(w::Hˡrow{IT, NT, ST}, v, m′::IT, m::IT) where {IT, NT, ST} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end From 911b1b24959aab53f778607502009d135631dabf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 7 Oct 2025 13:20:58 -0400 Subject: [PATCH 191/329] Minimize type restrictions; simplify m_min != -m_max cases --- src/redesign/WignerMatrix.jl | 128 ++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 54 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index f61b0edc..c2f91c03 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -10,10 +10,10 @@ Abstract base type for Wigner rotation‐matrix objects of a specific ``ℓ`` va - `ST` is the storage type (typically `Matrix{NT}`, but other `AbstractMatrix{NT}` storage can be used). -The basic concrete subtypes (`WignerDMatrix`, `WignerdMatrix`) default to storing their data in a -`Matrix{NT}` and implement the usual `size`, `getindex` and `setindex!` so that one can use -`w[m′,m]`. Specifically, these indices can be negative or positive, and must obey `abs(m′) -≤ m′ₘₐₓ` and `abs(m) ≤ ℓ`. +The basic concrete subtypes (`WignerDMatrix`, `WignerdMatrix`) default to storing their data +in a `Matrix{NT}` and implement the usual `size`, `getindex` and `setindex!` so that one can +use `w[m′,m]`. Specifically, these indices can be negative or positive, and must obey +`abs(m′) ≤ m′ₘₐₓ` and `abs(m) ≤ ℓ`. # Methods @@ -46,23 +46,27 @@ For example, if the parent Matrix is not stored as the `parent` field, then the method should be re-implemented to return the correct parent object. The `getindex` and `setindex!` """ -abstract type WignerMatrix{IT, NT, ST} <: AbstractMatrix{NT} end +abstract type WignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractMatrix{NT}} <: AbstractMatrix{NT} end ### General methods for all WignerMatrix types -Base.parent(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = w.parent -ℓ(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = w.ℓ -m′ₘₐₓ(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = w.m′ₘₐₓ -mₘₐₓ(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = ℓ(w) +Base.parent(w::WignerMatrix) = w.parent +ℓ(w::WignerMatrix) = w.ℓ +m′ₘₐₓ(w::WignerMatrix) = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerMatrix) = -m′ₘₐₓ(w) +mₘₐₓ(w::WignerMatrix) = ℓ(w) +mₘᵢₙ(w::WignerMatrix) = -mₘₐₓ(w) ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) -ℓₘᵢₙ(::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = ℓₘᵢₙ(IT) +ℓₘᵢₙ(::WignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) const ell = ℓ const mpmax = m′ₘₐₓ +const mpmin = m′ₘᵢₙ const mmax = mₘₐₓ +const mmin = mₘᵢₙ const ellmin = ℓₘᵢₙ isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Integer, NT, ST} = false @@ -84,58 +88,70 @@ if VERSION < v"1.8.2" Base.axes1(r::WignerRange) = axes1(r) end Base.inds2string(inds::NTuple{2, WignerRange}) = - string("(", inds[1].start, ":", inds[1].stop, ")×(", inds[2].start, ":", inds[2].stop, ")") + string( + "(", inds[1].start, ":", inds[1].stop, ")", + "×", + "(", inds[2].start, ":", inds[2].stop, ")" + ) + +function Base.getindex(v::WignerRange, i::Bool) + throw(ArgumentError("invalid index: $i of type Bool")) +end +@propagate_inbounds function Base.getindex(v::WignerRange{T}, i::Integer) where {T} + val = convert(T, v.start + (i - oneunit(i))) + @boundscheck (i>0 && val <= v.stop && val >= v.start) || throw(BoundsError(v, i)) + val +end -function Base.axes(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} - (WignerRange(-m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(-mₘₐₓ(w):mₘₐₓ(w))) +function Base.axes(w::WignerMatrix{IT}) where {IT} + (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) end # We don't have to override Base.show; most of its machinery works just fine, except that # printing the data itself gets screwed up when the indices are Rational. So we override # this core part of the printing machinery to just print the parent matrix as usual. The # only other thing show really does is add a "summary" line, for which the only -Base.print_array(io::IO, w::WignerMatrix{IT, NT, ST}) where {IT<:Rational, NT, ST} = - Base.print_array(io, parent(w)) +Base.print_array(io::IO, w::WignerMatrix) = Base.print_array(io, parent(w)) -@propagate_inbounds function Base.getindex(w::WignerMatrix{IT, NT, ST}, i::Int) where {IT, NT, ST} +@propagate_inbounds function Base.getindex(w::WignerMatrix, i::Int) @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end Base.parent(w)[i] end -@propagate_inbounds function Base.getindex(w::WignerMatrix{IT, NT, ST}, m′::IT, m::IT) where {IT, NT, ST} +@propagate_inbounds function Base.getindex(w::WignerMatrix{IT}, m′::IT, m::IT) where {IT} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end - @inbounds Base.parent(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] + @inbounds Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] end -@propagate_inbounds function Base.setindex!(w::WignerMatrix, v, i) +@propagate_inbounds function Base.setindex!(w::WignerMatrix, v, i::Int) @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end Base.parent(w)[i] = v end -@propagate_inbounds function Base.setindex!(w::WignerMatrix{IT, NT, ST}, v, m′::IT, m::IT) where {IT, NT, ST} +@propagate_inbounds function Base.setindex!(w::WignerMatrix{IT}, v, m′::IT, m::IT) where {IT} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end - Base.parent(w)[Int(m′+m′ₘₐₓ(w))+1, Int(m+mₘₐₓ(w))+1] = v + Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] = v end ### Specialize to D and d matrices """ - WignerDMatrix{IT, NT, ST} + WignerDMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} Specialized subtype of [`WignerMatrix`](@ref) for D-matrices, which are complex matrices. """ -struct WignerDMatrix{IT, NT, ST<:AbstractMatrix{NT}} <: WignerMatrix{IT, NT, ST} +struct WignerDMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} parent::ST ℓ::IT m′ₘₐₓ::IT - function WignerDMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} + function WignerDMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use # a restriction on NT in the type declaration. if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT @@ -192,14 +208,14 @@ denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, matrix must have the correct size: the first dimension must be greater than 0 and less than or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. """ -function WignerDMatrix(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} - WignerDMatrix{IT, NT, ST}(parent, ℓ) +function WignerDMatrix(parent::ST, ℓ::IT) where {IT, ST} + WignerDMatrix{IT, eltype(ST), ST}(parent, ℓ) end -function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT<:Union{Integer, Rational}} +function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} if complex(NT) ≢ NT throw(ErrorException( - "WignerDMatrix only supports complex types; the input type is $NT.\n" - * "Perhaps you meant to use WignerdMatrix?" + "`WignerDMatrix` only supports complex types; the input type is $NT.\n" + * "Perhaps you meant to use `WignerdMatrix`?" )) end WignerDMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) @@ -208,15 +224,15 @@ end """ - WignerdMatrix{IT, NT, ST} + WignerdMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} Specialized subtype of [`WignerMatrix`](@ref) for d-matrices, which are real matrices. """ -struct WignerdMatrix{IT, NT, ST<:AbstractMatrix{NT}} <: WignerMatrix{IT, NT, ST} +struct WignerdMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} parent::ST ℓ::IT m′ₘₐₓ::IT - function WignerdMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} + function WignerdMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use # a restriction on NT in the type declaration. if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT @@ -273,14 +289,14 @@ denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, matrix must have the correct size: the first dimension must be greater than 0 and less than or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. """ -function WignerdMatrix(parent::ST, ℓ::IT) where {IT<:Union{Integer, Rational}, NT, ST<:AbstractMatrix{NT}} - WignerdMatrix{IT, NT, ST}(parent, ℓ) +function WignerdMatrix(parent::ST, ℓ::IT) where {IT, ST} + WignerdMatrix{IT, eltype(ST), ST}(parent, ℓ) end -function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT<:Union{Integer, Rational}} +function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} if real(NT) ≢ NT throw(ErrorException( - "WignerdMatrix only supports real types; the input type is $NT.\n" - * "Perhaps you meant to use WignerDMatrix?" + "`WignerdMatrix` only supports real types; the input type is $NT.\n" + * "Perhaps you meant to use `WignerDMatrix`?" )) end WignerdMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) @@ -294,34 +310,34 @@ Specialized subtype of [`WignerMatrix`](@ref) intended to store one row of the ` — usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the recurrence relations. """ -struct Hˡrow{IT, NT, ST<:AbstractMatrix{NT}} <: WignerMatrix{IT, NT, ST} +struct Hˡrow{IT, NT, ST} <: WignerMatrix{IT, NT, ST} parent::ST ℓ::IT m′ₘₐₓ::IT end -function Base.axes(w::Hˡrow) - (WignerRange(m′ₘₐₓ(w):m′ₘₐₓ(w)), WignerRange(0:mₘₐₓ(w))) -end +m′ₘᵢₙ(w::Hˡrow) = m′ₘₐₓ(w) +mₘₐₓ(w::Hˡrow) = ℓ(w) +mₘᵢₙ(w::Hˡrow) = ℓₘᵢₙ(w) -@propagate_inbounds function Base.getindex(w::Hˡrow{IT, NT, ST}, m′::IT, m::IT) where {IT, NT, ST} - @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) - throw(BoundsError(w, (m′, m))) - end - @inbounds Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] -end +# @propagate_inbounds function Base.getindex(w::Hˡrow{IT}, m′::IT, m::IT) where {IT} +# @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) +# throw(BoundsError(w, (m′, m))) +# end +# @inbounds Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] +# end -@propagate_inbounds function Base.setindex!(w::Hˡrow{IT, NT, ST}, v, m′::IT, m::IT) where {IT, NT, ST} - @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) - throw(BoundsError(w, (m′, m))) - end - Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] = v -end +# @propagate_inbounds function Base.setindex!(w::Hˡrow{IT, NT, ST}, v, m′::IT, m::IT) where {IT, NT, ST} +# @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) +# throw(BoundsError(w, (m′, m))) +# end +# Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] = v +# end @testitem "WignerMatrix" begin import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, - parent, ell, mpmax, mmax, m′ₘₐₓ, mₘₐₓ + parent, ell, mpmax, mpmin, mmax, mmin, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, ℓₘᵢₙ # Check that mixed-up types throw an error @test_throws "WignerDMatrix only supports complex types" WignerDMatrix(rand(Float64, 3, 3), 1) @@ -387,6 +403,8 @@ end @test ell(w) == ℓ @test mpmax(w) == m′ₘ @test mmax(w) == ℓ + @test mpmin(w) == -mpmax(w) + @test mmin(w) == -mmax(w) for m ∈ -mₘ:mₘ for m′ ∈ -m′ₘ:m′ₘ @test w[m′, m] == (ℓ, m′, m) @@ -408,6 +426,8 @@ end @test ell(w) == ℓ @test m′ₘₐₓ(w) == m′ₘ @test mₘₐₓ(w) == ℓ + @test m′ₘᵢₙ(w) == -m′ₘₐₓ(w) + @test mₘᵢₙ(w) == -mₘₐₓ(w) # The Julia docs say that the `axes` function should # > Return a tuple of `AbstractUnitRange{<:Integer}` of valid indices. From fc47f5f625ce6f4a360338a418b7b97f50078a1b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 08:20:10 -0400 Subject: [PATCH 192/329] Easier constructors for H^lrow --- src/redesign/WignerMatrix.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index c2f91c03..4a51cc70 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -315,6 +315,25 @@ struct Hˡrow{IT, NT, ST} <: WignerMatrix{IT, NT, ST} ℓ::IT m′ₘₐₓ::IT end +function Hˡrow(parent::ST, ℓ::IT, m′::IT) where {IT, ST} + length_m′ = 1 + length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 + if size(parent,1) < length_m′ || size(parent,2) < length_m + error( + "The input `parent` matrix for ℓ=$ℓ must have size at least " + * "($length_m′,$length_m); it has size $(size(parent))." + ) + end + Hˡrow{IT, eltype(ST), ST}(parent, ℓ, m′) +end +function Hˡrow(::Type{NT}, ℓ::IT, m′::IT) where {NT, IT} + if real(NT) ≢ NT + error("`Hˡrow` only supports real types; the input type is $NT.") + end + length_m′ = 1 + length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 + Hˡrow{IT, NT, Matrix{NT}}(Matrix{NT}(undef, length_m′, length_m), ℓ, m′) +end m′ₘᵢₙ(w::Hˡrow) = m′ₘₐₓ(w) mₘₐₓ(w::Hˡrow) = ℓ(w) From e4a38d6256e6c0727c72a04436e87c3f4c4dcceb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 08:21:15 -0400 Subject: [PATCH 193/329] Remove redundant methods --- src/redesign/WignerMatrix.jl | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 4a51cc70..675f95b5 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -339,20 +339,6 @@ m′ₘᵢₙ(w::Hˡrow) = m′ₘₐₓ(w) mₘₐₓ(w::Hˡrow) = ℓ(w) mₘᵢₙ(w::Hˡrow) = ℓₘᵢₙ(w) -# @propagate_inbounds function Base.getindex(w::Hˡrow{IT}, m′::IT, m::IT) where {IT} -# @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) -# throw(BoundsError(w, (m′, m))) -# end -# @inbounds Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] -# end - -# @propagate_inbounds function Base.setindex!(w::Hˡrow{IT, NT, ST}, v, m′::IT, m::IT) where {IT, NT, ST} -# @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) -# throw(BoundsError(w, (m′, m))) -# end -# Base.parent(w)[Int(m′-ℓₘᵢₙ(w))+1, Int(m-ℓₘᵢₙ(w))+1] = v -# end - @testitem "WignerMatrix" begin import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, From 8215b839c902de576bb4c9cc666e895f51c6fdfc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 08:21:39 -0400 Subject: [PATCH 194/329] Simplify error calls --- src/redesign/SphericalFunctions.jl | 2 +- src/redesign/WignerMatrix.jl | 57 +++++++++++++++--------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index bb32f373..c635e20d 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -24,7 +24,7 @@ function WignerD!(D::WignerDMatrices{Complex{FT1}}, R::Quaternionic.Rotor{FT2}) end function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} - throw(ErrorException("WignerD! is not yet implemented")) + error("WignerD! is not yet implemented") end end # module Redesign diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 675f95b5..a7437bd3 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -155,43 +155,43 @@ struct WignerDMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use # a restriction on NT in the type declaration. if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT - throw(ErrorException( + error( "WignerDMatrix only supports complex types; the input type is $NT.\n" * "Perhaps you meant to use WignerdMatrix?" - )) + ) end if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) - throw(ErrorException( + error( "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" * "if ℓ is an integer its type must be <:Integer, not <:Rational." - )) + ) end s₁, s₂ = size(parent) if s₂ ≠ Int(2ℓ + 1) - throw(ErrorException( + error( "The extent of the second dimension in the input data must be " * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." - )) + ) end if s₁ == 0 || s₁ > s₂ - throw(ErrorException( + error( "The extent of the first dimension in the input data must be greater than 0" * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." - )) + ) end if IT <: Rational if isodd(s₁) - throw(ErrorException( + error( "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " * "input data ($s₁) corresponds to whole-integer values of m′." - )) + ) end else if iseven(s₁) - throw(ErrorException( + error( "ℓ=$ℓ is an integer, but the extent of the first dimension in the " * "input data ($s₁) corresponds to half-integer values of m′." - )) + ) end end m′ₘₐₓ = IT((s₁ - 1) // 2) @@ -213,10 +213,10 @@ function WignerDMatrix(parent::ST, ℓ::IT) where {IT, ST} end function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} if complex(NT) ≢ NT - throw(ErrorException( + error( "`WignerDMatrix` only supports complex types; the input type is $NT.\n" * "Perhaps you meant to use `WignerdMatrix`?" - )) + ) end WignerDMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) end @@ -236,43 +236,43 @@ struct WignerdMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use # a restriction on NT in the type declaration. if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT - throw(ErrorException( + error( "WignerdMatrix only supports real types; the input type is $NT.\n" * "Perhaps you meant to use WignerDMatrix?" - )) + ) end if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) - throw(ErrorException( + error( "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" * "if ℓ is an integer its type must be <:Integer, not <:Rational." - )) + ) end s₁, s₂ = size(parent) if s₂ ≠ Int(2ℓ + 1) - throw(ErrorException( + error( "The extent of the second dimension in the input data must be " * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." - )) + ) end if s₁ == 0 || s₁ > s₂ - throw(ErrorException( + error( "The extent of the first dimension in the input data must be greater than 0" * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." - )) + ) end if IT <: Rational if isodd(s₁) - throw(ErrorException( + error( "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " * "input data ($s₁) corresponds to whole-integer values of m′." - )) + ) end else if iseven(s₁) - throw(ErrorException( + error( "ℓ=$ℓ is an integer, but the extent of the first dimension in the " * "input data ($s₁) corresponds to half-integer values of m′." - )) + ) end end m′ₘₐₓ = IT((s₁ - 1) // 2) @@ -294,15 +294,14 @@ function WignerdMatrix(parent::ST, ℓ::IT) where {IT, ST} end function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} if real(NT) ≢ NT - throw(ErrorException( + error( "`WignerdMatrix` only supports real types; the input type is $NT.\n" * "Perhaps you meant to use `WignerDMatrix`?" - )) + ) end WignerdMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) end - """ Hˡrow{IT, NT, ST} From 5d284a84c63eddf85ecf95bba1149f70ea44842b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 08:22:13 -0400 Subject: [PATCH 195/329] Remember to use inbounds when indexing --- src/redesign/WignerMatrix.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index a7437bd3..1ed2b846 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -117,7 +117,7 @@ Base.print_array(io::IO, w::WignerMatrix) = Base.print_array(io, parent(w)) @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end - Base.parent(w)[i] + @inbounds Base.parent(w)[i] end @propagate_inbounds function Base.getindex(w::WignerMatrix{IT}, m′::IT, m::IT) where {IT} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) @@ -130,13 +130,13 @@ end @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end - Base.parent(w)[i] = v + @inbounds Base.parent(w)[i] = v end @propagate_inbounds function Base.setindex!(w::WignerMatrix{IT}, v, m′::IT, m::IT) where {IT} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end - Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] = v + @inbounds Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] = v end From bbfd8e05743d576f5ac8c197fb4eec2f6e43b0b2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 08:22:41 -0400 Subject: [PATCH 196/329] WignerRange gets first/lastindex --- src/redesign/WignerMatrix.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 1ed2b846..4af32179 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -93,7 +93,8 @@ Base.inds2string(inds::NTuple{2, WignerRange}) = "×", "(", inds[2].start, ":", inds[2].stop, ")" ) - +Base.firstindex(r::WignerRange) = 1 +Base.lastindex(r::WignerRange) = length(r) function Base.getindex(v::WignerRange, i::Bool) throw(ArgumentError("invalid index: $i of type Bool")) end From e31ad701a9ead173986d561655a4dfe51d5c9346 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 08:23:21 -0400 Subject: [PATCH 197/329] Specialize index-range functions for type stability --- src/redesign/WignerMatrix.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 4af32179..610c53e3 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -38,7 +38,7 @@ Methods defined for `WignerMatrix` objects include: Any new subtypes of `WignerMatrix` should inherit from this type and re-implement any of the methods mentioned above that are not appropriate for the new type. Specifically, the default implementations assume that subtypes store the fields -- `parent::Matrix{NT}`: the underlying data array. +- `parent::ST`: the underlying storage type. - `ℓ::IT`: the value of ``ℓ``. - `m′ₘₐₓ::IT`: the maximum value of ``m′``. @@ -51,11 +51,11 @@ abstract type WignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractMatrix{N ### General methods for all WignerMatrix types Base.parent(w::WignerMatrix) = w.parent -ℓ(w::WignerMatrix) = w.ℓ -m′ₘₐₓ(w::WignerMatrix) = w.m′ₘₐₓ -m′ₘᵢₙ(w::WignerMatrix) = -m′ₘₐₓ(w) -mₘₐₓ(w::WignerMatrix) = ℓ(w) -mₘᵢₙ(w::WignerMatrix) = -mₘₐₓ(w) +ℓ(w::WignerMatrix{IT}) where {IT} = w.ℓ +m′ₘₐₓ(w::WignerMatrix{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -m′ₘₐₓ(w) +mₘₐₓ(w::WignerMatrix{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -mₘₐₓ(w) ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) @@ -111,8 +111,9 @@ end # We don't have to override Base.show; most of its machinery works just fine, except that # printing the data itself gets screwed up when the indices are Rational. So we override # this core part of the printing machinery to just print the parent matrix as usual. The -# only other thing show really does is add a "summary" line, for which the only -Base.print_array(io::IO, w::WignerMatrix) = Base.print_array(io, parent(w)) +# only other thing show really does is add a "summary" line, for which the `axes` and thence +# `inds2string` methods above are used. +Base.print_array(io::IO, w::WignerMatrix{<:Rational}) = Base.print_array(io, parent(w)) @propagate_inbounds function Base.getindex(w::WignerMatrix, i::Int) @boundscheck if i<1 || i>length(w) From 763027ce2164467d7366cea7c63f11fbf1a74e5c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 09:02:37 -0400 Subject: [PATCH 198/329] Fix some type-parameter discrepancies --- src/redesign/recurrence.jl | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 0352dd05..e98905b9 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -16,10 +16,10 @@ relations. This only sets the values ``H^0_{0,0}=0``, ``H^1_{0,0}=cosβ``, and Note that `Hˡ` can be any `WignerMatrix` with integer indices. In particular, it can be a `D` matrix or a `d` matrix. """ -function initialize!(Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T) where {NT, IT<:Signed, T} +function initialize!(Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) if ℓ == 0 - Hˡ[0, 0] = 0 + Hˡ[0, 0] = 1 elseif ℓ == 1 Hˡ[0, 0] = cosβ Hˡ[0, 1] = sinβ / √2 @@ -36,15 +36,13 @@ compute ``H^{\ell}_{0,m}`` for all ``m \geq 0``. """ function recurrence_0_m!( - Hˡ::WignerMatrix{NT, IT}, Hˡ⁻¹::WignerMatrix{NT2, IT}, sinβ::T, cosβ::T -) where {NT, NT2, IT<:Signed, T} + Hˡ::WignerMatrix{IT, NT}, Hˡ⁻¹::WignerMatrix{IT, NT2}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, NT2, T} @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 # Note that in this step only, we use notation derived from Xing et al., denoting the # coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following steps, we will use notation # from Gumerov and Duraiswami, who denote their different coefficients aₗᵐ, etc. - #@inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) - @warn "Turned off inbounds for debugging" - let √=sqrt∘T, ℓ=ℓ(Hˡ) + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) if ℓ > 1 b̄ₗ = √(T(ℓ-1)/ℓ) Hˡ[0, 0] = cosβ * Hˡ⁻¹[0, 0] - b̄ₗ * sinβ * Hˡ⁻¹[0, 1] @@ -84,8 +82,8 @@ compute ``H^{\ell}_{1,m}`` for all ``m \geq 1``. """ function recurrence_1_m!( - Hˡ::WignerMatrix{NT, IT}, Hˡ⁺¹::WignerMatrix{NT2, IT}, sinβ::T, cosβ::T -) where {NT, NT2, IT<:Signed, T} + Hˡ::WignerMatrix{IT, NT}, Hˡ⁺¹::WignerMatrix{IT, NT2}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, NT2, T} @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) if ℓ > 0 && m′ₘₐₓ ≥ 1 @@ -113,8 +111,8 @@ m)`` entries, compute ``H^{\ell}_{m′+1,m}`` for all ``m′ \geq 1`` and ``m \g """ function recurrence_m′₊!( - Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Signed, T} + Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 # Note that the signs of m′ and m are always +1, so we leave them out of the @@ -150,8 +148,8 @@ Step 5 of the computation of ``H``: Given ``H^{\ell}`` with all its ``(m′, m)` """ function recurrence_m′₋!( - Hˡ::WignerMatrix{NT, IT}, sinβ::T, cosβ::T -) where {NT, IT<:Signed, T} + Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) @@ -192,7 +190,7 @@ H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. ``` """ -function impose_symmetries!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Signed} +function impose_symmetries!(Hˡ::WignerMatrix{IT, NT}) where {IT<:Signed, NT} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) # The idea here is to impose # Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] @@ -217,7 +215,7 @@ Convert the Wigner matrix `Hˡ` to the d matrix `dˡ`, which just involves multi signs related to the `m′` and `m` indices. """ -function convert_H_to_d!(Hˡ::WignerMatrix{NT, IT}) where {NT, IT<:Signed} +function convert_H_to_d!(Hˡ::WignerMatrix{IT, NT}) where {IT<:Signed, NT} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m ∈ -ℓ:ℓ for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ @@ -236,7 +234,7 @@ Convert the Wigner matrix `Hˡ` to the D matrix `Dˡ`, which just involves multi complex phases related to the `m′` and `m` indices. """ -function convert_H_to_D!(Hˡ::WignerMatrix{NT, IT}, eⁱᵅ::NT, eⁱᵞ::NT) where {NT, IT<:Signed} +function convert_H_to_D!(Hˡ::WignerMatrix{IT, NT}, eⁱᵅ::NT, eⁱᵞ::NT) where {IT<:Signed, NT} # NOTE: This function will have to be modified to work for Rational indices because the # phases will not be integer powers; we'll have to incorporate √eⁱᵅ and √eⁱᵞ. @inbounds let ℓ=ℓ(Hˡ), ℓₘᵢₙ=ℓₘᵢₙ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) From 88cd3dc77a0939388a5d630130b31aa089989c1f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 9 Oct 2025 13:49:24 -0400 Subject: [PATCH 199/329] Start rewrite of recurrences for streamlining --- docs/src/notes/H_recurrence.md | 254 +++++++++++++++++++++++++++++++++ src/redesign/recurrence.jl | 11 +- 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 docs/src/notes/H_recurrence.md diff --git a/docs/src/notes/H_recurrence.md b/docs/src/notes/H_recurrence.md new file mode 100644 index 00000000..bb3cd208 --- /dev/null +++ b/docs/src/notes/H_recurrence.md @@ -0,0 +1,254 @@ +# Algorithm for computing ``H`` (redesigned) + +The ``H`` array, as given by [Gumerov_2015](@citet), is related to Wigner's (small) ``d`` matrices — +which is itself related to the (big) ``\mathfrak{D}`` matrices and the various spin-weighted +spherical harmonics ``{}_{s}Y_{\ell,m}`` — via + +```math +d_{\ell}^{m',m} = \epsilon_{m'} \epsilon_{-m} H^{\ell}_{m',m}, +``` + +where + +```math +\epsilon_k = + \begin{cases} + 1 & k\leq 0, \\ + (-1)^k & k > 0. + \end{cases} +``` + +(Note that I have swapped superscripts and subscripts on ``H`` +compared to Gumerov and Duraiswami's paper, to be consistent with the +rest of this documentation.) + +``H`` has various advantages over ``d`` and ``\mathfrak{D}``, +including the fact that it can be efficiently and robustly calculated +via recurrence relations, and the following symmetry relations: + +```math +\begin{aligned} + H_{m', m}^\ell(β) &= H_{m, m'}^\ell(β) \\ + H_{m', m}^\ell(β) &= H_{-m', -m}^\ell(β) \\ + H_{m', m}^\ell(β) &= (-1)^{\ell+m+m'} H_{-m', m}^\ell(π - β) \\ + H_{m', m}^\ell(β) &= (-1)^{m+m'} H_{m', m}^\ell(-β) +\end{aligned} +``` + +Because of these symmetries — specifically the first two — we only +need to evaluate about 1/4 of all the elements for a given value of +``β``. + + +## Steps to compute ``H`` (redesigned) + +The following describes various details that are not spelled out +correctly by [Gumerov_2015](@citet). All equation numbers refer to +that paper unless otherwise noted. + +Because of the symmetries noted above, we only compute ``H_{m', +m}^\ell`` with ``m ≥ |m'|`` — roughly one quarter of all possible +values. Furthermore, for computations of spin-weighted spherical +harmonics of weight ``s``, we only need to compute values with ``|m'| +≤ |s|``, which constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. +The data are stored in the array `Hwedge`. + + +### Step 1: Initialize ``H^{0}_{0,0}`` + +Set ``H^{0}_{0,0}=1``. + + +### Step 2: ``H_{0,m}^{\ell} \to H_{0,m}^{\ell+1}`` for ``m \geq 0`` + +### Step 3: ``H_{0,m}^{\ell+1} \to H_{1,m}^{\ell}`` for ``m \geq 0`` + +### Step 4: ``H_{m',m-1}^{\ell}, H_{m'-1,m}^{\ell}, H_{m',m+1}^{\ell} \to H_{m'+1,m}^{\ell}`` for ``m' > 1`` and ``m > m'`` + +### Step 5: ``H_{m',m-1}^{\ell}, H_{m'+1,m}^{\ell}, H_{m',m+1}^{\ell} \to H_{m'-1,m}^{\ell}`` for ``m' \leq 0`` and ``m > -m'`` + +### Step 6: Use symmetries to fill in the rest of ``H`` + +### Step 7: Include phases to obtain ``d`` or ``\mathfrak{D}`` + + +Compute values ``H^{0,m}_{n}(β)`` for ``m=0,\ldots,n`` and +``H^{0,m}_{n+1}(β)`` for ``m=0,\ldots,n+1``. Using Eq. (32), we see +that within Gumerov and Duraiswami's conventions +```math +\begin{aligned} + H^{0,m}_{n}(β) &= (-1)^m \sqrt{\frac{(n-|m|)!}{(n+|m|)!}} P^{|m|}_{n}(\cos β) \\ + &= \frac{1}{\sqrt{k_m (2n+1)}} P̄_{n,|m|}(\cos β). +\end{aligned} +``` +Here, ``k_0=1`` and ``k_m=2`` for ``m>0``, and ``P̄`` is defined as +```math + P̄_{n,|m|} = \sqrt{\frac{k_m(2n+1)(n-m)!}{(n+m)!}} P_{n,|m|}. +``` +Note that the factor of ``(-1)^m`` in the first equation above is +different from the convention used here, and is related to the +[Condon-Shortley +phase](https://en.wikipedia.org/wiki/Spherical_harmonics#Condon%E2%80%93Shortley_phase). +Note that Gumerov and Duraiswami use the notation ``P^{|m|}_{n}``, +whereas we are using the notation ``P_{n,|m|}`` — which usually differ +by a factor of ``(-1)^m``. + +We use the "fully normalized" associated Legendre functions (fnALF) +``P̄`` because, as explained by [Xing_2019](@citet), it is possible to +compute these values very efficiently and accurately, while also +delaying the onset of overflow and underflow. + +The algorithm Xing et al. describe as the best for computing ``P̄`` is +due to [Strakhov_1980](@citet) via [Belikov_1991](@citet), and is +given by them as +```math +\begin{aligned} + P̄_{0,0} &= 1 \\ + P̄_{1,0} &= \sqrt{3} \cos β \\ + P̄_{1,1} &= \sqrt{3} \sin β \\ + P̄_{n,0} &= a_n \cos β P̄_{n-1,0} - b_n \frac{\sin β}{2} P̄_{n-1,1} \\ + P̄_{n,m} &= + c_{n,m} \cos β P̄_{n-1,m} + - \sin β \left[ d_{n,m} P̄_{n-1,m+1} - e_{n,m} P̄_{n-1,m-1} \right], +\end{aligned} +``` +where the coefficients are given by +```math +\begin{aligned} + a_n &= \sqrt{\frac{2n+1}{2n-1}} \\ + b_n &= \sqrt{\frac{2(n-1)(2n+1)}{n(2n-1)}} \\ + c_{n,m} &= \frac{1}{n} \sqrt{\frac{(n+m)(n-m)(2n+1)}{2n-1}} \\ + d_{n,m} &= \frac{1}{2n} \sqrt{\frac{(n-m)(n-m-1)(2n+1)}{2n-1}} \\ + e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-\delta_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. +\end{aligned} +``` + +Now, we can directly obtain a recurrence relation for +``H^{0,m}_{n} = P̄_{n,|m|} / \sqrt{k_m (2n+1)} `` from those expressions: +```math +\begin{aligned} + H^{0,0}_{0} &= 1 \\ + H^{0,0}_{1} &= \cos β \\ + H^{0,1}_{1} &= \sqrt{1/2} \sin β \\ + H^{0,0}_{n} &= \cos β H^{0,0}_{n-1} - b̄_n \sin β H^{0,1}_{n-1} \\ + H^{0,m}_{n} &= + c̄_{n,m} \cos β H^{0,m}_{n-1} + - \sin β \left[ d̄_{n,m} H^{0,m+1}_{n-1} - ē_{n,m} H^{0,m-1}_{n-1} \right], +\end{aligned} +``` +where the coefficients are given by +```math +\begin{aligned} + b̄_n &= \sqrt{\frac{n-1}{n}} \\ + c̄_{n,m} &= \frac{1}{n} \sqrt{(n+m)(n-m)} \\ + d̄_{n,m} &= \frac{1}{2n} \sqrt{(n-m)(n-m-1)} \\ + ē_{n,m} &= \frac{1}{2n} \sqrt{(n+m)(n+m-1)}. +\end{aligned} +``` +Note that the coefficients all simplified (in fact, ``a_n`` disappeared), without any +increase in the complexity of the recurrence relations themselves. Rewriting Belikov's +algorithm explicitly in terms of the ``H^{0,m}_{n}`` also allows us to avoid an extra +normalization step. + +### Step 3 +Compute ``H^{1,m}_{n}(β)`` for ``m=1,\ldots,n`` using relation (41). Symmetry +and shift of the indices allow this relation to be written as +```math +b^{0}_{n+1} H^{1, m}_{n} + = \frac{b^{−m−1}_{n+1} (1−\cos β)}{2} H^{0, m+1}_{n+1} + − \frac{b^{ m−1}_{n+1} (1+\cos β)}{2} H^{0, m−1}_{n+1} + − a^{m}_{n} \sin β H^{0, m}_{n+1}. +``` +Here the constants are defined by +```math +a^{m}_{n} = \sqrt{\frac{(n+m+1)(n-m+1)} {(2n+1)(2n+3)}}, +``` +```math +b^{m}_{n} = \mathrm{sgn}(m) \sqrt{\frac{(n-m-1)(n-m)} {(2n-1)(2n+1)}}. +``` +Note that all values are assumed to be zero whenever ``|m| > n``, we use +``\mathrm{sgn}(0)=1`` (unlike the common convention that +``\mathrm{sgn}(0)=0``), and we have ``a^{m}_{n} = a^{-m}_{n}``. Also note +that these coefficients *only* appear in this step, and because of how they +appear (specifically, because ``b`` always appears with argument ``n+1``), we +can factor out the denominators in the definitions of the constants. We +obtain this simplified formula +```math +H^{1, m}_{n} + = -\frac{1}{\sqrt{n(n+1)}} \left[ + \frac{\bar{b}^{−m−1}_{n+1} (1−\cos β)}{2} H^{0, m+1}_{n+1} + + \frac{\bar{b}^{ m−1}_{n+1} (1+\cos β)}{2} H^{0, m−1}_{n+1} + + \bar{a}^{m}_{n} \sin β H^{0, m}_{n+1} + \right], +``` +with +```math +\bar{a}^{m}_{n} = \sqrt{(n+m+1)(n-m+1)}, +``` +```math +\bar{b}^{m}_{n+1} = \sqrt{(n-m)(n-m+1)}. +``` + + +### Step 4 +Recursively compute ``H^{m'+1, m}_{n}(β)`` for ``m'=1,\ldots,n−1``, +``m=m',...,n`` using relation (50) resolved with respect to ``H^{m'+1, +m}_{n}``: +```math +d^{m'}_{n} H^{m'+1, m}_{n} + = d^{m'−1}_{n} H^{m'−1, m}_{n} + − d^{m−1}_{n} H^{m', m−1}_{n} + + d^{m}_{n} H^{m', m+1}_{n} +``` +(where the last term drops out for ``m=n``). The constants are defined by +```math +d^{m}_{n} = \frac{\mathrm{sgn}(m)}{2} \sqrt{(n-m)(n+m+1)}. +``` +Note that we can drop the factor of ``1/2``, and *for this case only* the sign +is always +1. + + +### Step 5 +Recursively compute ``H^{m'−1, m}_{n}(β)`` for ``m'=0,\ldots,−n+1``, +``m=−m',\ldots,n`` using relation (50) resolved with respect to ``H^{m'−1, +m}_{n}``: +```math +d^{m'−1}_{n} H^{m'−1, m}_{n} + = d^{m'}_{n} H^{m'+1, m}_{n} + + d^{m−1}_{n} H^{m', m−1}_{n} + − d^{m}_{n} H^{m', m+1}_{n} +``` +(where the last term drops out for ``m=n``). + +NOTE: Although Gumerov and Duraiswami specify the loop over ``m'`` to start at +-1, I find it necessary to start at 0, or there will be missing information. +This also requires setting the ``H^{0, -1}_{n}`` components (for all ``n``) +before beginning this loop. + + +## Pre-computing constants versus computing on the fly + +Each of the constants ``a^{m}_{n}``, ``b^{m}_{n}``, and ``c^{m}_{n}`` involves +divisions and square-roots, which can be very costly to compute. It can be +advantageous to pre-compute the constants, and simply index the pre-computed +arrays rather than re-computing them on each recursion. + +*If* we include the cost of computing all these constants in a single call to +the ``H`` recurrence, it can be much cheaper to compute each constant as needed +within the algorithm, rather than computing them all at once at the beginning +of the algorithm — but only for very small computations, such as those +involving ``n_{\mathrm{max}} ≈ 10``. Beyond this, despite the storage +penalties for all those constants, it turns out to be better to pre-compute +them. However, it should be noted that the fractional cost of storing the +constants is ``\sim 3/n_{\mathrm{max}}`` compared to just storing ``H`` itself, +so this will never be a very significant amount of space. + +On the other hand, if we can pre-compute the constants just once, and store +them between multiple calls to the ``H`` recurrence, then it is always +advantageous to do so — typically by factors of 2 or 3 in speed. The only +difficulty here is ensuring that each call to the recurrence has access to the +constants, which can be a little awkward when using multiple processes and/or +threads. However, it should be thread safe, since we only need to read those +constants within the ``H`` recurrence. All in all, I conclude that it is +probably not worth the effort to maintain separate versions of the recurrence +for pre-computed and on-the-fly constants. diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index e98905b9..dc827d45 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -43,7 +43,14 @@ function recurrence_0_m!( # coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following steps, we will use notation # from Gumerov and Duraiswami, who denote their different coefficients aₗᵐ, etc. @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) - if ℓ > 1 + if ℓ == 1 + # The ℓ>1 branch would try to access invalid indices of H⁰; if we treat those + # elements as zero, we can simplify that branch to just the following much + # simpler code anyway. So fundamentally, this branch is the same as the other + # branch. + Hˡ[0, 0] = cosβ + Hˡ[0, 1] = sinβ / √2 + elseif ℓ > 1 b̄ₗ = √(T(ℓ-1)/ℓ) Hˡ[0, 0] = cosβ * Hˡ⁻¹[0, 0] - b̄ₗ * sinβ * Hˡ⁻¹[0, 1] for m ∈ 1:ℓ-2 @@ -69,6 +76,8 @@ function recurrence_0_m!( - sinβ * (- ēₗₘ * Hˡ⁻¹[0, m-1]) ) end + else + error("Tried to recurse with ℓ=$ℓ; only ℓ ≥ 1 is supported.") end end Hˡ From c3861780ef8e7b00c369e626547b750221d2acde Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 10 Oct 2025 15:48:17 -0400 Subject: [PATCH 200/329] Rename recurrence steps for clarity --- src/redesign/recurrence.jl | 67 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index dc827d45..50d49b0c 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -7,35 +7,33 @@ sgn(m) = m ≥ 0 ? 1 : -1 @doc raw""" - initialize!(Hˡ, sinβ, cosβ) + recurrence_step1!(H⁰) -Step 1 of the computation of ``H``: Initialize the Wigner matrix `Hˡ` for the recurrence -relations. This only sets the values ``H^0_{0,0}=0``, ``H^1_{0,0}=cosβ``, and -``H^1_{0,1}=sinβ/√2``. +Initialize the Wigner matrix `H⁰` for the recurrence relations. This only sets the values +`H⁰[0,0]=1`. -Note that `Hˡ` can be any `WignerMatrix` with integer indices. In particular, it can be a +Note that `H⁰` can be any `WignerMatrix` with integer indices. In particular, it can be a `D` matrix or a `d` matrix. """ -function initialize!(Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T) where {IT<:Signed, NT, T} - @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ) +function initialize!(H⁰::WignerMatrix{IT, NT}) where {IT<:Signed, NT} + @inbounds let √=sqrt∘T, ℓ=ℓ(H⁰) if ℓ == 0 - Hˡ[0, 0] = 1 - elseif ℓ == 1 - Hˡ[0, 0] = cosβ - Hˡ[0, 1] = sinβ / √2 + H⁰[0, 0] = 1 + else + error("Trying to initialize ℓ=$ℓ; only ℓ=0 is supported.") end end - Hˡ + H⁰ end @doc raw""" - recurrence_0_m!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) -Step 2 of the computation of ``H``: Given ``H^{\ell-1}`` with its ``(0, m)`` entries, -compute ``H^{\ell}_{0,m}`` for all ``m \geq 0``. +Compute the values of ``H^{\ell}_{0,m}``, from the values of ``H^{\ell-1}_{0,m}`` for all +``m \geq 0``. """ -function recurrence_0_m!( +function recurrence_step2!( Hˡ::WignerMatrix{IT, NT}, Hˡ⁻¹::WignerMatrix{IT, NT2}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, NT2, T} @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 @@ -77,20 +75,20 @@ function recurrence_0_m!( ) end else - error("Tried to recurse with ℓ=$ℓ; only ℓ ≥ 1 is supported.") + error("Tried to recurse with ℓ=$ℓ; only integer ℓ ≥ 1 is supported.") end end Hˡ end @doc raw""" - recurrence_1_m!(Hˡ, Hˡ⁺¹, sinβ, cosβ) + recurrence_step3!(Hˡ, Hˡ⁺¹, sinβ, cosβ) -Step 3 of the computation of ``H``: Given ``H^{\ell+1}`` with all its ``(0, m)`` entries, -compute ``H^{\ell}_{1,m}`` for all ``m \geq 1``. +Compute the values of ``H^{\ell}_{1,m}``, from the values of ``H^{\ell+1}_{0,m}`` for all +``m \geq 0``. """ -function recurrence_1_m!( +function recurrence_step3!( Hˡ::WignerMatrix{IT, NT}, Hˡ⁺¹::WignerMatrix{IT, NT2}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, NT2, T} @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 @@ -113,13 +111,13 @@ function recurrence_1_m!( end @doc raw""" - recurrence_m′₊!(Hˡ, sinβ, cosβ) + recurrence_step4!(Hˡ, sinβ, cosβ) -Step 4 of the computation of ``H``: Given ``H^{\ell}`` with all its ``(0, m)`` and ``(1, -m)`` entries, compute ``H^{\ell}_{m′+1,m}`` for all ``m′ \geq 1`` and ``m \geq 1``. +Compute the values of ``H^{\ell}_{m'+1,m}``, from the values of ``H^{\ell}_{m',m-1}``, +``H^{\ell}_{m'-1,m}``, and ``H^{\ell}_{m',m+1}``, for all ``m' > 1`` and ``m \geq m'``. """ -function recurrence_m′₊!( +function recurrence_step4!( Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) @@ -150,13 +148,13 @@ function recurrence_m′₊!( end @doc raw""" - recurrence_m′₋!(Hˡ, sinβ, cosβ) + recurrence_step5!(Hˡ, sinβ, cosβ) -Step 5 of the computation of ``H``: Given ``H^{\ell}`` with all its ``(m′, m)`` entries for -`m′ ≥ 0`, compute ``H^{\ell}_{m′-1,m}`` for all `m′ ≤ -1` and `m`. +Compute the values of ``H^{\ell}_{m'-1,m}``, from the values of ``H^{\ell}_{m',m-1}``, +``H^{\ell}_{m'+1,m}``, and ``H^{\ell}_{m',m+1}``, for all ``m' \leq 0`` and ``m > -m'``. """ -function recurrence_m′₋!( +function recurrence_step5!( Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) @@ -185,12 +183,15 @@ function recurrence_m′₋!( end @doc raw""" - impose_symmetries!(Hˡ) + recurrence_step6!(Hˡ) + +Impose the symmetries of the Wigner matrix `Hˡ` to fill in all the values that have not yet +been computed. Assuming that `Hˡ` has already been computed as much as possible by the recurrence relations, this function imposes the symmetries, rather than recalculating terms. -Specifically, the recurrence relations will calculate the terms for all `m`, and -`m′ ≥ abs(m)`, and this function will complete the calculations using the symmetries +Specifically, the recurrence relations will calculate the terms for all `m`, and `m′ ≥ +abs(m)`, and this function will complete the calculations using the symmetries ```math \begin{aligned} H^ℓ_{m′, m} &= H^ℓ_{m, m′}, \\ @@ -199,7 +200,7 @@ H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. ``` """ -function impose_symmetries!(Hˡ::WignerMatrix{IT, NT}) where {IT<:Signed, NT} +function recurrence_step6!(Hˡ::WignerMatrix{IT, NT}) where {IT<:Signed, NT} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) # The idea here is to impose # Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] From 724741abb558991f775aa27a0addee28a9c8b7cc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 10 Oct 2025 15:48:29 -0400 Subject: [PATCH 201/329] Start WignerCalculator --- src/redesign/WignerCalculator.jl | 83 ++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/redesign/WignerCalculator.jl diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl new file mode 100644 index 00000000..2e2ea2d8 --- /dev/null +++ b/src/redesign/WignerCalculator.jl @@ -0,0 +1,83 @@ + + +struct WignerComputer{IT, RT, NT} + ℓₘₐₓ::IT + H⁻::Matrix{RT} + H⁺::Matrix{RT} + Wˡ::Matrix{NT} + function WignerComputer(ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt) where {IT, RT, NT} + if real(NT) ≠ RT + error("RT=$RT is supposed to be the real type of NT=$NT.") + end + if IT <: Rational + if denominator(ℓₘₐₓ) ≠ 2 + error("For IT=$IT <: Rational, ℓₘₐₓ must have denominator 2.") + end + end + if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") + end + # `H⁻p` may (eventually) be required to store all the coefficients for Hˡ₀ₘ with + # non-negative `m`; even though that won't strictly be necessary for `ℓₘₐₓ`, it is + # just one extra `RT`, and may simplify the coding significantly. `H⁺p` will + # (eventually) be required to store all the coefficients for Hˡ⁺¹₀ₘ with + # non-negative `m` (and that will be strictly necessary), so we give it one extra + # column. + H⁻p = Matrix{RT}(undef, 1, Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) + H⁺p = Matrix{RT}(undef, 1, Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + Wˡp = Matrix{NT}(undef, Int(2ℓₘₐₓ)+1, Int(2ℓₘₐₓ)+1) + new{IT, RT, NT}(ℓₘₐₓ, H⁻p, H⁺p, Wˡp) + end +end +function (wc::WignerComputer{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} + if ℓ < ℓₘᵢₙ(wc) || ℓ > ℓₘₐₓ(wc) + error( + "ℓ=$ℓ is out of range for this WignerComputer " + * "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc))).") + end + Wˡ = Redesign.WignerMatrix{IT, NT}(wc.Wˡparent, ℓ, ℓ) + H⁻ = Redesign.Hˡrow{IT, RT}(wc.H⁻parent, ℓ, 0) + H⁺ = Redesign.Hˡrow{IT, RT}(wc.H⁺parent, ℓ+1, 0) + return Wˡ, H⁻, H⁺ +end + +function initialize!(computer::WignerComputer{IT, RT}, sinβ::RT, cosβ::RT) where {IT, RT} + H⁰ = Redesign.Hˡrow(computer.H⁻parent, 0, 0) + H¹ = Redesign.Hˡrow(computer.H⁺parent, 1, 0) + Redesign.initialize!(H⁰, sinβ, cosβ) + Redesign.initialize!(H¹, sinβ, cosβ) + Redesign.recurrence_0_m!(H¹, H⁰, sinβ, cosβ) + computer +end + +""" + incrementH!(computer, ℓ, sinβ, cosβ) + + +""" +function incrementH!(computer::DˡComputer{IT, NT}, ℓ::IT, sinβ::NT, cosβ::NT) where {IT, NT} + Hˡ = Redesign.Hˡrow(computer.H⁻parent, ℓ, 0) + Hˡ⁺¹ = Redesign.Hˡrow(computer.H⁺parent, ℓ+1, 0) + Hˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] + Redesign.recurrence_0_m!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + computer +end + +function recurrence!(computer::DˡComputer{IT, NT}, α::NT, β::NT, γ::NT) where {IT, NT} + H⁺, H⁻, Dˡ = computer.H⁺, computer.H⁻, computer.Dˡ + sinβ, cosβ = sincos(β) + eⁱᵅ, eⁱᵞ = cis(α), cis(γ) + parent(Dˡ) .= 0 + Redesign.initialize!(H⁻, sinβ, cosβ) + Redesign.initialize!(H⁺, sinβ, cosβ) + Redesign.recurrence_0_m!(H⁻, H⁺, sinβ, cosβ) + + + Redesign.recurrence_0_m!(Hˡ⁺¹, Dˡ, sinβ, cosβ) + Redesign.recurrence_1_m!(Dˡ, Hˡ⁺¹, sinβ, cosβ) + Redesign.recurrence_m′₊!(Dˡ, sinβ, cosβ) + Redesign.recurrence_m′₋!(Dˡ, sinβ, cosβ) + Redesign.impose_symmetries!(Dˡ) + Redesign.convert_H_to_D!(Dˡ, eⁱᵅ, eⁱᵞ) + Dˡ +end From 27d0f4deb81a0e0105d2cd568f9ae57f68ece01d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Oct 2025 23:26:46 -0400 Subject: [PATCH 202/329] Finesse some wording --- docs/src/notes/H_recurrence.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/src/notes/H_recurrence.md b/docs/src/notes/H_recurrence.md index bb3cd208..286bf15f 100644 --- a/docs/src/notes/H_recurrence.md +++ b/docs/src/notes/H_recurrence.md @@ -48,10 +48,9 @@ that paper unless otherwise noted. Because of the symmetries noted above, we only compute ``H_{m', m}^\ell`` with ``m ≥ |m'|`` — roughly one quarter of all possible -values. Furthermore, for computations of spin-weighted spherical -harmonics of weight ``s``, we only need to compute values with ``|m'| -≤ |s|``, which constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. -The data are stored in the array `Hwedge`. +values. Furthermore, for spin-weighted spherical harmonics of weight +``s``, we only need to compute values with ``|m'| ≤ |s|``, which +constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. ### Step 1: Initialize ``H^{0}_{0,0}`` From d71a90947466873a486b2b44a7c92a51e21b59f0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Oct 2025 23:28:38 -0400 Subject: [PATCH 203/329] Rename first recurrence step, too --- src/redesign/recurrence.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 50d49b0c..739938ca 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -15,7 +15,7 @@ Initialize the Wigner matrix `H⁰` for the recurrence relations. This only set Note that `H⁰` can be any `WignerMatrix` with integer indices. In particular, it can be a `D` matrix or a `d` matrix. """ -function initialize!(H⁰::WignerMatrix{IT, NT}) where {IT<:Signed, NT} +function recurrence_step1!(H⁰::WignerMatrix{IT, NT}) where {IT<:Signed, NT} @inbounds let √=sqrt∘T, ℓ=ℓ(H⁰) if ℓ == 0 H⁰[0, 0] = 1 From f932a3b485407e066604ffa87c9f2da6aba7bd48 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Oct 2025 23:29:13 -0400 Subject: [PATCH 204/329] Apply recurrence to WignerCalculator --- src/redesign/WignerCalculator.jl | 175 +++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 45 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 2e2ea2d8..30b22422 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -1,22 +1,69 @@ +function validate_index_ranges(ℓₘₐₓ::IT, m′ₘᵢₙ::IT, m′ₘₐₓ::IT, mₘᵢₙ::IT, mₘₐₓ::IT) where + {IT<:Union{Signed, Rational}} + if IT <: Rational + if ( + denominator(ℓₘₐₓ) ≠ 2 || + denominator(m′ₘᵢₙ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || + denominator(mₘᵢₙ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + ) + error( + "For IT=$IT <: Rational, indices must have denominator 2:\n" + * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ, mₘᵢₙ=$mₘᵢₙ, mₘₐₓ=$mₘₐₓ." + ) + end + end + + if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") + end + # The m′ and m values must bracket ℓₘᵢₙ + if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type.") + end + if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type.") + end + if mₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("mₘᵢₙ=$mₘᵢₙ is too large for this index type.") + end + if mₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("mₘₐₓ=$mₘₐₓ is too small for this index type.") + end + + # The m′ and m values must be in range for ℓₘₐₓ + if abs(m′ₘᵢₙ) > ℓₘₐₓ + error("|m′ₘᵢₙ|=|$m′ₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(m′ₘₐₓ) > ℓₘₐₓ + error("|m′ₘₐₓ|=|$m′ₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(mₘᵢₙ) > ℓₘₐₓ + error("|mₘᵢₙ|=|$mₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(mₘₐₓ) > ℓₘₐₓ + error("|mₘₐₓ|=|$mₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + +end -struct WignerComputer{IT, RT, NT} +struct WignerComputer{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} ℓₘₐₓ::IT + m′ₘᵢₙ::IT + m′ₘₐₓ::IT + mₘᵢₙ::IT + mₘₐₓ::IT H⁻::Matrix{RT} H⁺::Matrix{RT} Wˡ::Matrix{NT} - function WignerComputer(ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt) where {IT, RT, NT} + function WignerComputer( + ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt; + m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ + ) where {IT, RT, NT} if real(NT) ≠ RT error("RT=$RT is supposed to be the real type of NT=$NT.") end - if IT <: Rational - if denominator(ℓₘₐₓ) ≠ 2 - error("For IT=$IT <: Rational, ℓₘₐₓ must have denominator 2.") - end - end - if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) - error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") - end + validate_index_ranges(ℓₘₐₓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) # `H⁻p` may (eventually) be required to store all the coefficients for Hˡ₀ₘ with # non-negative `m`; even though that won't strictly be necessary for `ℓₘₐₓ`, it is # just one extra `RT`, and may simplify the coding significantly. `H⁺p` will @@ -26,58 +73,96 @@ struct WignerComputer{IT, RT, NT} H⁻p = Matrix{RT}(undef, 1, Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) H⁺p = Matrix{RT}(undef, 1, Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) Wˡp = Matrix{NT}(undef, Int(2ℓₘₐₓ)+1, Int(2ℓₘₐₓ)+1) - new{IT, RT, NT}(ℓₘₐₓ, H⁻p, H⁺p, Wˡp) + new{IT, RT, NT}(ℓₘₐₓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ, H⁻p, H⁺p, Wˡp) end end + function (wc::WignerComputer{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} + if IT <: Rational + if denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end + end if ℓ < ℓₘᵢₙ(wc) || ℓ > ℓₘₐₓ(wc) error( "ℓ=$ℓ is out of range for this WignerComputer " * "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc))).") end - Wˡ = Redesign.WignerMatrix{IT, NT}(wc.Wˡparent, ℓ, ℓ) - H⁻ = Redesign.Hˡrow{IT, RT}(wc.H⁻parent, ℓ, 0) - H⁺ = Redesign.Hˡrow{IT, RT}(wc.H⁺parent, ℓ+1, 0) + Wˡ = WignerMatrix{IT, NT}(wc.Wˡ, ℓ, ℓ) + H⁻ = Hˡrow{IT, RT}(wc.H⁻, ℓ, 0) + H⁺ = Hˡrow{IT, RT}(wc.H⁺, ℓ+1, 0) return Wˡ, H⁻, H⁺ end -function initialize!(computer::WignerComputer{IT, RT}, sinβ::RT, cosβ::RT) where {IT, RT} - H⁰ = Redesign.Hˡrow(computer.H⁻parent, 0, 0) - H¹ = Redesign.Hˡrow(computer.H⁺parent, 1, 0) - Redesign.initialize!(H⁰, sinβ, cosβ) - Redesign.initialize!(H¹, sinβ, cosβ) - Redesign.recurrence_0_m!(H¹, H⁰, sinβ, cosβ) - computer +function WignerDComputer( + ℓₘₐₓ::IT, ::Type{RT}; + m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ +) where {IT, RT<:Real} + NT = complex(RT) + WignerComputer(ℓₘₐₓ, RT, NT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) +end + +function WignerdComputer( + ℓₘₐₓ::IT, ::Type{RT}; + m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ +) where {IT, RT<:Real} + WignerComputer(ℓₘₐₓ, RT, RT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) end -""" - incrementH!(computer, ℓ, sinβ, cosβ) +function recurrence_step1!(w::WignerComputer{IT}) where {IT<:Signed} + W⁰, H⁰, H¹ = w(0) + initialize!(H⁰) + w +end +function recurrence_step2!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) + sinβ, cosβ = reim(eⁱᵝ) + recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] + recurrence_step2!(Hˡ⁺¹, Hˡ, sinβ, cosβ) + w +end -""" -function incrementH!(computer::DˡComputer{IT, NT}, ℓ::IT, sinβ::NT, cosβ::NT) where {IT, NT} - Hˡ = Redesign.Hˡrow(computer.H⁻parent, ℓ, 0) - Hˡ⁺¹ = Redesign.Hˡrow(computer.H⁺parent, ℓ+1, 0) - Hˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] - Redesign.recurrence_0_m!(Hˡ, Hˡ⁻¹, sinβ, cosβ) - computer +function recurrence_step3!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + sinβ, cosβ = reim(eⁱᵝ) + recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) + w end -function recurrence!(computer::DˡComputer{IT, NT}, α::NT, β::NT, γ::NT) where {IT, NT} - H⁺, H⁻, Dˡ = computer.H⁺, computer.H⁻, computer.Dˡ - sinβ, cosβ = sincos(β) - eⁱᵅ, eⁱᵞ = cis(α), cis(γ) - parent(Dˡ) .= 0 - Redesign.initialize!(H⁻, sinβ, cosβ) - Redesign.initialize!(H⁺, sinβ, cosβ) - Redesign.recurrence_0_m!(H⁻, H⁺, sinβ, cosβ) +function recurrence_step4!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + sinβ, cosβ = reim(eⁱᵝ) + recurrence_step4!(Wˡ, sinβ, cosβ) + w +end +function recurrence_step5!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + sinβ, cosβ = reim(eⁱᵝ) + recurrence_step5!(Wˡ, sinβ, cosβ) + w +end - Redesign.recurrence_0_m!(Hˡ⁺¹, Dˡ, sinβ, cosβ) - Redesign.recurrence_1_m!(Dˡ, Hˡ⁺¹, sinβ, cosβ) - Redesign.recurrence_m′₊!(Dˡ, sinβ, cosβ) - Redesign.recurrence_m′₋!(Dˡ, sinβ, cosβ) - Redesign.impose_symmetries!(Dˡ) - Redesign.convert_H_to_D!(Dˡ, eⁱᵅ, eⁱᵞ) - Dˡ +function recurrence_step6!(w::WignerComputer{IT}, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + recurrence_step6!(Wˡ) + w +end + +function recurrence!(w::WignerComputer{IT, RT}, α::RT, β::RT, γ::RT, ℓ::IT) where {IT<:Signed, RT} + eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) + recurrence_step1!(w) + for ℓ′ in 1:ℓ + recurrence_step2!(w, eⁱᵝ, ℓ′) + recurrence_step3!(w, eⁱᵝ, ℓ′) + recurrence_step4!(w, eⁱᵝ, ℓ′) + recurrence_step5!(w, eⁱᵝ, ℓ′) + recurrence_step6!(w, ℓ′) + end + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) + Wˡ end From 12b9e086880eb0b48875153da610e4fece6d1b4e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 13:44:10 -0400 Subject: [PATCH 205/329] Just a little reordering --- src/redesign/WignerMatrix.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 610c53e3..b7806083 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -51,23 +51,24 @@ abstract type WignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractMatrix{N ### General methods for all WignerMatrix types Base.parent(w::WignerMatrix) = w.parent -ℓ(w::WignerMatrix{IT}) where {IT} = w.ℓ -m′ₘₐₓ(w::WignerMatrix{IT}) where {IT} = w.m′ₘₐₓ -m′ₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -m′ₘₐₓ(w) -mₘₐₓ(w::WignerMatrix{IT}) where {IT} = ℓ(w) -mₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -mₘₐₓ(w) +ℓ(w::WignerMatrix{IT}) where {IT} = w.ℓ ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) ℓₘᵢₙ(::WignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) +m′ₘₐₓ(w::WignerMatrix{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -m′ₘₐₓ(w) +mₘₐₓ(w::WignerMatrix{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -mₘₐₓ(w) + const ell = ℓ +const ellmin = ℓₘᵢₙ const mpmax = m′ₘₐₓ const mpmin = m′ₘᵢₙ const mmax = mₘₐₓ const mmin = mₘᵢₙ -const ellmin = ℓₘᵢₙ isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Integer, NT, ST} = false isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Rational, NT, ST} = true From 92def1c885d80024a4dc38eb96be3ab9f56d1054 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 14:41:23 -0400 Subject: [PATCH 206/329] Drop WignerMatrices for now --- src/redesign/SphericalFunctions.jl | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index c635e20d..26b04e9d 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -8,23 +8,24 @@ import SphericalFunctions: ComplexPowers include("WignerMatrix.jl") -include("WignerMatrices.jl") +#include("WignerMatrices.jl") include("recurrence.jl") +include("WignerCalculator.jl") -function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} - NT = complex(Quaternionic.basetype(R)) - D = WignerDMatrices(NT, ℓₘₐₓ, m′ₘₐₓ) - WignerD!(D, R) -end +# function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} +# NT = complex(Quaternionic.basetype(R)) +# D = WignerDMatrices(NT, ℓₘₐₓ, m′ₘₐₓ) +# WignerD!(D, R) +# end -function WignerD!(D::WignerDMatrices{Complex{FT1}}, R::Quaternionic.Rotor{FT2}) where {FT1, FT2} - R1 = Quaternionic.Rotor{FT1}(R) - WignerD!(D, R1) -end +# function WignerD!(D::WignerDMatrices{Complex{FT1}}, R::Quaternionic.Rotor{FT2}) where {FT1, FT2} +# R1 = Quaternionic.Rotor{FT1}(R) +# WignerD!(D, R1) +# end -function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} - error("WignerD! is not yet implemented") -end +# function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} +# error("WignerD! is not yet implemented") +# end end # module Redesign From 6e6f097dfb8bf44a2ab7926609593e596a1708d8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 14:41:53 -0400 Subject: [PATCH 207/329] Add generic case to avoid stack overflow --- src/redesign/WignerMatrix.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index b7806083..3cfcbacf 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -54,6 +54,7 @@ Base.parent(w::WignerMatrix) = w.parent ℓ(w::WignerMatrix{IT}) where {IT} = w.ℓ ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) +ℓₘᵢₙ(::Type{IT}) where {IT} = error("No method defined for `ℓₘᵢₙ(::Type{$IT})`.") ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) ℓₘᵢₙ(::WignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) From d338af16958665bb7365af4f21eec48a2f28e726 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 14:43:06 -0400 Subject: [PATCH 208/329] Change name to WignerCalculator --- src/redesign/WignerCalculator.jl | 35 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 30b22422..c362d491 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -47,7 +47,7 @@ function validate_index_ranges(ℓₘₐₓ::IT, m′ₘᵢₙ::IT, m′ₘₐ end -struct WignerComputer{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} +struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} ℓₘₐₓ::IT m′ₘᵢₙ::IT m′ₘₐₓ::IT @@ -56,7 +56,7 @@ struct WignerComputer{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} H⁻::Matrix{RT} H⁺::Matrix{RT} Wˡ::Matrix{NT} - function WignerComputer( + function WignerCalculator( ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt; m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ ) where {IT, RT, NT} @@ -77,7 +77,14 @@ struct WignerComputer{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} end end -function (wc::WignerComputer{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} +ℓₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = ℓₘᵢₙ(wc.ℓₘₐₓ) +ℓₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.ℓₘₐₓ +m′ₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘᵢₙ +m′ₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘₐₓ +mₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.mₘᵢₙ +mₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.mₘₐₓ + +function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} if IT <: Rational if denominator(ℓ) ≠ 2 error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") @@ -85,10 +92,10 @@ function (wc::WignerComputer{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} end if ℓ < ℓₘᵢₙ(wc) || ℓ > ℓₘₐₓ(wc) error( - "ℓ=$ℓ is out of range for this WignerComputer " + "ℓ=$ℓ is out of range for this WignerCalculator " * "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc))).") end - Wˡ = WignerMatrix{IT, NT}(wc.Wˡ, ℓ, ℓ) + Wˡ = WignerDMatrix(wc.Wˡ, ℓ) H⁻ = Hˡrow{IT, RT}(wc.H⁻, ℓ, 0) H⁺ = Hˡrow{IT, RT}(wc.H⁺, ℓ+1, 0) return Wˡ, H⁻, H⁺ @@ -99,23 +106,23 @@ function WignerDComputer( m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ ) where {IT, RT<:Real} NT = complex(RT) - WignerComputer(ℓₘₐₓ, RT, NT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) + WignerCalculator(ℓₘₐₓ, RT, NT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) end function WignerdComputer( ℓₘₐₓ::IT, ::Type{RT}; m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ ) where {IT, RT<:Real} - WignerComputer(ℓₘₐₓ, RT, RT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) + WignerCalculator(ℓₘₐₓ, RT, RT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) end -function recurrence_step1!(w::WignerComputer{IT}) where {IT<:Signed} +function recurrence_step1!(w::WignerCalculator{IT}) where {IT<:Signed} W⁰, H⁰, H¹ = w(0) initialize!(H⁰) w end -function recurrence_step2!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} +function recurrence_step2!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) sinβ, cosβ = reim(eⁱᵝ) recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) @@ -125,34 +132,34 @@ function recurrence_step2!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signe w end -function recurrence_step3!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} +function recurrence_step3!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) sinβ, cosβ = reim(eⁱᵝ) recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) w end -function recurrence_step4!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} +function recurrence_step4!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) sinβ, cosβ = reim(eⁱᵝ) recurrence_step4!(Wˡ, sinβ, cosβ) w end -function recurrence_step5!(w::WignerComputer{IT}, eⁱᵝ, ℓ) where {IT<:Signed} +function recurrence_step5!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) sinβ, cosβ = reim(eⁱᵝ) recurrence_step5!(Wˡ, sinβ, cosβ) w end -function recurrence_step6!(w::WignerComputer{IT}, ℓ) where {IT<:Signed} +function recurrence_step6!(w::WignerCalculator{IT}, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) recurrence_step6!(Wˡ) w end -function recurrence!(w::WignerComputer{IT, RT}, α::RT, β::RT, γ::RT, ℓ::IT) where {IT<:Signed, RT} +function recurrence!(w::WignerCalculator{IT, RT}, α::RT, β::RT, γ::RT, ℓ::IT) where {IT<:Signed, RT} eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) recurrence_step1!(w) for ℓ′ in 1:ℓ From 3087031a401690c34b40b2b3b53ebb4a49753b96 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 14:43:21 -0400 Subject: [PATCH 209/329] Remove unused type parameters --- src/redesign/WignerMatrix.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 3cfcbacf..80485234 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -71,8 +71,8 @@ const mpmin = m′ₘᵢₙ const mmax = mₘₐₓ const mmin = mₘᵢₙ -isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Integer, NT, ST} = false -isrational(::WignerMatrix{IT, NT, ST}) where {IT<:Rational, NT, ST} = true +isrational(::WignerMatrix{IT}) where {IT<:Integer} = false +isrational(::WignerMatrix{IT}) where {IT<:Rational} = true Base.eltype(::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT Base.size(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) From 292578e137d7f933c89170c721be44ea31f6caab Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 14:46:02 -0400 Subject: [PATCH 210/329] Specify Abstract nature of WignerMatrix type (to make room for concrete general WignerMatrix) --- src/redesign/WignerMatrix.jl | 62 ++++++++++++++++++------------------ src/redesign/recurrence.jl | 26 ++++++++------- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 80485234..6b2b9fd4 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -1,7 +1,7 @@ import Base: @propagate_inbounds """ - WignerMatrix{IT, NT, ST} + AbstractWignerMatrix{IT, NT, ST} Abstract base type for Wigner rotation‐matrix objects of a specific ``ℓ`` value. - `IT` is the index type (an `Integer` or half-integer `Rational`), governing the allowed @@ -17,7 +17,7 @@ use `w[m′,m]`. Specifically, these indices can be negative or positive, and m # Methods -Methods defined for `WignerMatrix` objects include: +Methods defined for `AbstractWignerMatrix` objects include: - `parent(w)`: the underlying data array. - `ℓ(w)` or `ell(w)`: the value of ``ℓ``. - `m′ₘₐₓ(w)` or `mpmax(w)`: the maximum value of ``m′``. @@ -35,7 +35,7 @@ Methods defined for `WignerMatrix` objects include: # Implementation -Any new subtypes of `WignerMatrix` should inherit from this type and re-implement any of the +Any new subtypes of `AbstractWignerMatrix` should inherit from this type and re-implement any of the methods mentioned above that are not appropriate for the new type. Specifically, the default implementations assume that subtypes store the fields - `parent::ST`: the underlying storage type. @@ -46,23 +46,23 @@ For example, if the parent Matrix is not stored as the `parent` field, then the method should be re-implemented to return the correct parent object. The `getindex` and `setindex!` """ -abstract type WignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractMatrix{NT}} <: AbstractMatrix{NT} end +abstract type AbstractWignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractMatrix{NT}} <: AbstractMatrix{NT} end -### General methods for all WignerMatrix types +### General methods for all AbstractWignerMatrix types -Base.parent(w::WignerMatrix) = w.parent +Base.parent(w::AbstractWignerMatrix) = w.parent -ℓ(w::WignerMatrix{IT}) where {IT} = w.ℓ +ℓ(w::AbstractWignerMatrix{IT}) where {IT} = w.ℓ ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) ℓₘᵢₙ(::Type{IT}) where {IT} = error("No method defined for `ℓₘᵢₙ(::Type{$IT})`.") ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) -ℓₘᵢₙ(::WignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) +ℓₘᵢₙ(::AbstractWignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) -m′ₘₐₓ(w::WignerMatrix{IT}) where {IT} = w.m′ₘₐₓ -m′ₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -m′ₘₐₓ(w) -mₘₐₓ(w::WignerMatrix{IT}) where {IT} = ℓ(w) -mₘᵢₙ(w::WignerMatrix{IT}) where {IT} = -mₘₐₓ(w) +m′ₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = -m′ₘₐₓ(w) +mₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = -mₘₐₓ(w) const ell = ℓ const ellmin = ℓₘᵢₙ @@ -71,12 +71,12 @@ const mpmin = m′ₘᵢₙ const mmax = mₘₐₓ const mmin = mₘᵢₙ -isrational(::WignerMatrix{IT}) where {IT<:Integer} = false -isrational(::WignerMatrix{IT}) where {IT<:Rational} = true +isrational(::AbstractWignerMatrix{IT}) where {IT<:Integer} = false +isrational(::AbstractWignerMatrix{IT}) where {IT<:Rational} = true -Base.eltype(::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT -Base.size(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) -Base.length(w::WignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) +Base.eltype(::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT +Base.size(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) +Base.length(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} start::T @@ -106,7 +106,7 @@ end val end -function Base.axes(w::WignerMatrix{IT}) where {IT} +function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) end @@ -115,28 +115,28 @@ end # this core part of the printing machinery to just print the parent matrix as usual. The # only other thing show really does is add a "summary" line, for which the `axes` and thence # `inds2string` methods above are used. -Base.print_array(io::IO, w::WignerMatrix{<:Rational}) = Base.print_array(io, parent(w)) +Base.print_array(io::IO, w::AbstractWignerMatrix{<:Rational}) = Base.print_array(io, parent(w)) -@propagate_inbounds function Base.getindex(w::WignerMatrix, i::Int) +@propagate_inbounds function Base.getindex(w::AbstractWignerMatrix, i::Int) @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end @inbounds Base.parent(w)[i] end -@propagate_inbounds function Base.getindex(w::WignerMatrix{IT}, m′::IT, m::IT) where {IT} +@propagate_inbounds function Base.getindex(w::AbstractWignerMatrix{IT}, m′::IT, m::IT) where {IT} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end @inbounds Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] end -@propagate_inbounds function Base.setindex!(w::WignerMatrix, v, i::Int) +@propagate_inbounds function Base.setindex!(w::AbstractWignerMatrix, v, i::Int) @boundscheck if i<1 || i>length(w) throw(BoundsError(w, i)) end @inbounds Base.parent(w)[i] = v end -@propagate_inbounds function Base.setindex!(w::WignerMatrix{IT}, v, m′::IT, m::IT) where {IT} +@propagate_inbounds function Base.setindex!(w::AbstractWignerMatrix{IT}, v, m′::IT, m::IT) where {IT} @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) throw(BoundsError(w, (m′, m))) end @@ -147,11 +147,11 @@ end ### Specialize to D and d matrices """ - WignerDMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} + WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} -Specialized subtype of [`WignerMatrix`](@ref) for D-matrices, which are complex matrices. +Specialized subtype of [`AbstractWignerMatrix`](@ref) for D-matrices, which are complex matrices. """ -struct WignerDMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} +struct WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} parent::ST ℓ::IT m′ₘₐₓ::IT @@ -228,11 +228,11 @@ end """ - WignerdMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} + WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} -Specialized subtype of [`WignerMatrix`](@ref) for d-matrices, which are real matrices. +Specialized subtype of [`AbstractWignerMatrix`](@ref) for d-matrices, which are real matrices. """ -struct WignerdMatrix{IT, NT, ST} <: WignerMatrix{IT, NT, ST} +struct WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} parent::ST ℓ::IT m′ₘₐₓ::IT @@ -309,11 +309,11 @@ end """ Hˡrow{IT, NT, ST} -Specialized subtype of [`WignerMatrix`](@ref) intended to store one row of the ``H`` matrix +Specialized subtype of [`AbstractWignerMatrix`](@ref) intended to store one row of the ``H`` matrix — usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the recurrence relations. """ -struct Hˡrow{IT, NT, ST} <: WignerMatrix{IT, NT, ST} +struct Hˡrow{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} parent::ST ℓ::IT m′ₘₐₓ::IT diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 739938ca..2ccca1ed 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -12,11 +12,11 @@ sgn(m) = m ≥ 0 ? 1 : -1 Initialize the Wigner matrix `H⁰` for the recurrence relations. This only sets the values `H⁰[0,0]=1`. -Note that `H⁰` can be any `WignerMatrix` with integer indices. In particular, it can be a -`D` matrix or a `d` matrix. +Note that `H⁰` can be any `AbstractWignerMatrix` with integer indices. In particular, it +can be a `D` matrix or a `d` matrix. """ -function recurrence_step1!(H⁰::WignerMatrix{IT, NT}) where {IT<:Signed, NT} - @inbounds let √=sqrt∘T, ℓ=ℓ(H⁰) +function recurrence_step1!(H⁰::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT} + @inbounds let ℓ=ℓ(H⁰) if ℓ == 0 H⁰[0, 0] = 1 else @@ -34,7 +34,7 @@ Compute the values of ``H^{\ell}_{0,m}``, from the values of ``H^{\ell-1}_{0,m}` """ function recurrence_step2!( - Hˡ::WignerMatrix{IT, NT}, Hˡ⁻¹::WignerMatrix{IT, NT2}, sinβ::T, cosβ::T + Hˡ::AbstractWignerMatrix{IT, NT}, Hˡ⁻¹::AbstractWignerMatrix{IT, NT2}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, NT2, T} @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 # Note that in this step only, we use notation derived from Xing et al., denoting the @@ -89,7 +89,7 @@ Compute the values of ``H^{\ell}_{1,m}``, from the values of ``H^{\ell+1}_{0,m}` """ function recurrence_step3!( - Hˡ::WignerMatrix{IT, NT}, Hˡ⁺¹::WignerMatrix{IT, NT2}, sinβ::T, cosβ::T + Hˡ::AbstractWignerMatrix{IT, NT}, Hˡ⁺¹::AbstractWignerMatrix{IT, NT2}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, NT2, T} @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) @@ -118,7 +118,7 @@ Compute the values of ``H^{\ell}_{m'+1,m}``, from the values of ``H^{\ell}_{m',m """ function recurrence_step4!( - Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T + Hˡ::AbstractWignerMatrix{IT, NT}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 @@ -155,7 +155,7 @@ Compute the values of ``H^{\ell}_{m'-1,m}``, from the values of ``H^{\ell}_{m',m """ function recurrence_step5!( - Hˡ::WignerMatrix{IT, NT}, sinβ::T, cosβ::T + Hˡ::AbstractWignerMatrix{IT, NT}, sinβ::T, cosβ::T ) where {IT<:Signed, NT, T} @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 @@ -200,16 +200,20 @@ H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. ``` """ -function recurrence_step6!(Hˡ::WignerMatrix{IT, NT}) where {IT<:Signed, NT} +function recurrence_step6!(Hˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) # The idea here is to impose # Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] # without double-counting any entries, and accounting for m′ₘₐₓ. for m ∈ 1:ℓ + @info m for m′ ∈ -min(m′ₘₐₓ, m):min(m′ₘₐₓ, m) + @show (-m′, -m) (m′, m) Hˡ[m′, m] Hˡ[-m′, -m] = Hˡ[m′, m] end for m′ ∈ -min(m′ₘₐₓ, m-1):min(m′ₘₐₓ, m-1) + @show (m, m′) (m′, m) Hˡ[m′, m] + @show (-m, -m′) (m′, m) Hˡ[m′, m] Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[m′, m] end end @@ -225,7 +229,7 @@ Convert the Wigner matrix `Hˡ` to the d matrix `dˡ`, which just involves multi signs related to the `m′` and `m` indices. """ -function convert_H_to_d!(Hˡ::WignerMatrix{IT, NT}) where {IT<:Signed, NT} +function convert_H_to_d!(Hˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m ∈ -ℓ:ℓ for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ @@ -244,7 +248,7 @@ Convert the Wigner matrix `Hˡ` to the D matrix `Dˡ`, which just involves multi complex phases related to the `m′` and `m` indices. """ -function convert_H_to_D!(Hˡ::WignerMatrix{IT, NT}, eⁱᵅ::NT, eⁱᵞ::NT) where {IT<:Signed, NT} +function convert_H_to_D!(Hˡ::AbstractWignerMatrix{IT, NT}, eⁱᵅ::NT, eⁱᵞ::NT) where {IT<:Signed, NT} # NOTE: This function will have to be modified to work for Rational indices because the # phases will not be integer powers; we'll have to incorporate √eⁱᵅ and √eⁱᵞ. @inbounds let ℓ=ℓ(Hˡ), ℓₘᵢₙ=ℓₘᵢₙ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) From deffa49bb14a60cc270399561aaf03954b35133b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 20:51:21 -0400 Subject: [PATCH 211/329] Add simple constructor for WignerMatrix, and default descriptor functions to this type --- src/redesign/WignerCalculator.jl | 48 -------------- src/redesign/WignerMatrix.jl | 108 ++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 51 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index c362d491..1ec2bed7 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -1,51 +1,3 @@ -function validate_index_ranges(ℓₘₐₓ::IT, m′ₘᵢₙ::IT, m′ₘₐₓ::IT, mₘᵢₙ::IT, mₘₐₓ::IT) where - {IT<:Union{Signed, Rational}} - if IT <: Rational - if ( - denominator(ℓₘₐₓ) ≠ 2 || - denominator(m′ₘᵢₙ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || - denominator(mₘᵢₙ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 - ) - error( - "For IT=$IT <: Rational, indices must have denominator 2:\n" - * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ, mₘᵢₙ=$mₘᵢₙ, mₘₐₓ=$mₘₐₓ." - ) - end - end - - if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) - error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") - end - - # The m′ and m values must bracket ℓₘᵢₙ - if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) - error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type.") - end - if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) - error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type.") - end - if mₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) - error("mₘᵢₙ=$mₘᵢₙ is too large for this index type.") - end - if mₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) - error("mₘₐₓ=$mₘₐₓ is too small for this index type.") - end - - # The m′ and m values must be in range for ℓₘₐₓ - if abs(m′ₘᵢₙ) > ℓₘₐₓ - error("|m′ₘᵢₙ|=|$m′ₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") - end - if abs(m′ₘₐₓ) > ℓₘₐₓ - error("|m′ₘₐₓ|=|$m′ₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") - end - if abs(mₘᵢₙ) > ℓₘₐₓ - error("|mₘᵢₙ|=|$mₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") - end - if abs(mₘₐₓ) > ℓₘₐₓ - error("|mₘₐₓ|=|$mₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") - end - -end struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} ℓₘₐₓ::IT diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 6b2b9fd4..8d792bd7 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -60,9 +60,9 @@ Base.parent(w::AbstractWignerMatrix) = w.parent ℓₘᵢₙ(::AbstractWignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) m′ₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = w.m′ₘₐₓ -m′ₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = -m′ₘₐₓ(w) -mₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = ℓ(w) -mₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = -mₘₐₓ(w) +m′ₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = w.m′ₘᵢₙ +mₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = w.mₘₐₓ +mₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = w.mₘᵢₙ const ell = ℓ const ellmin = ℓₘᵢₙ @@ -144,6 +144,108 @@ end end +function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT, mₘₐₓ::IT, mₘᵢₙ::IT) where + {IT<:Union{Signed, Rational}} + if IT <: Rational + if ( + denominator(ℓₘₐₓ) ≠ 2 || + denominator(m′ₘᵢₙ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || + denominator(mₘᵢₙ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + ) + error( + "For IT=$IT <: Rational, indices must have denominator 2:\n" + * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ, mₘᵢₙ=$mₘᵢₙ, mₘₐₓ=$mₘₐₓ." + ) + end + end + + if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") + end + + # The m′ and m values must bracket ℓₘᵢₙ + if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type.") + end + if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type.") + end + if mₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("mₘᵢₙ=$mₘᵢₙ is too large for this index type.") + end + if mₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("mₘₐₓ=$mₘₐₓ is too small for this index type.") + end + + # The m′ and m values must be in range for ℓₘₐₓ + if abs(m′ₘᵢₙ) > ℓₘₐₓ + error("|m′ₘᵢₙ|=|$m′ₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(m′ₘₐₓ) > ℓₘₐₓ + error("|m′ₘₐₓ|=|$m′ₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(mₘᵢₙ) > ℓₘₐₓ + error("|mₘᵢₙ|=|$mₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(mₘₐₓ) > ℓₘₐₓ + error("|mₘₐₓ|=|$mₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + +end + + +""" + WignerMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + +General concrete subtype of [`AbstractWignerMatrix`](@ref) for Wigner rotation matrices, +which can include D-matrices (when `NT` is complex) or d-matrices (when `NT` is real). + +In general, the storage type `ST` can be any `AbstractMatrix{NT}`, but should be 1-based. +That is, the storage should generally be either a `Matrix` or a view. That matrix will +represent a rectangular array of values representing some or all of the Wigner matrix for a +specific ``ℓ`` value. The first dimension corresponds to the `m′` index, and the second +dimension corresponds to the `m` index. The allowed ranges of `m′` and `m` are governed by +the fields `m′ₘₐₓ`, `m′ₘᵢₙ`, `mₘₐₓ`, and `mₘᵢₙ`, which must satisfy +```math +\begin{aligned} +-ℓₘₐₓ &≤ m′ₘᵢₙ ≤ ℓₘᵢₙ ≤ m′ₘₐₓ ≤ ℓₘₐₓ, \\ +-ℓₘₐₓ &≤ mₘᵢₙ ≤ ℓₘᵢₙ ≤ mₘₐₓ ≤ ℓₘₐₓ, +\end{aligned} +``` +where `ℓₘᵢₙ` is either 0 or 1//2 depending on whether `IT` is an integer or rational type. + +""" +struct WignerMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + parent::ST + ℓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + mₘₐₓ::IT + mₘᵢₙ::IT + function WignerMatrix( + parent::ST, ℓ::IT; + mp_max::IT=ℓ, mp_min::IT=-ℓ, m_max::IT=ℓ, m_min::IT=-ℓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min + ) where {IT, NT, ST<:AbstractMatrix{NT}} + validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + s₁, s₂ = size(parent) + if s₁ < Int(m′ₘₐₓ - m′ₘᵢₙ + 1) + error( + "The extent of the first dimension in the input data must be at least " + * "m′ₘₐₓ-m′ₘᵢₙ+1=$(Int(m′ₘₐₓ - m′ₘᵢₙ + 1)); it is $s₁." + ) + end + if s₂ < Int(mₘₐₓ - mₘᵢₙ + 1) + error( + "The extent of the second dimension in the input data must be at least " + * "mₘₐₓ-mₘᵢₙ+1=$(Int(mₘₐₓ - mₘᵢₙ + 1)); it is $s₂." + ) + end + new{IT, NT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + end +end + + ### Specialize to D and d matrices """ From 1c949867921ecf21c0bf5be171eadd4ff9cc7c39 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 20:52:38 -0400 Subject: [PATCH 212/329] Fix bugs through ell=1 recurrence --- src/redesign/WignerCalculator.jl | 85 ++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 1ec2bed7..52eef8a8 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -1,40 +1,40 @@ struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} ℓₘₐₓ::IT - m′ₘᵢₙ::IT m′ₘₐₓ::IT - mₘᵢₙ::IT + m′ₘᵢₙ::IT mₘₐₓ::IT + mₘᵢₙ::IT H⁻::Matrix{RT} H⁺::Matrix{RT} Wˡ::Matrix{NT} function WignerCalculator( ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt; - m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ + m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ ) where {IT, RT, NT} if real(NT) ≠ RT error("RT=$RT is supposed to be the real type of NT=$NT.") end - validate_index_ranges(ℓₘₐₓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) + validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) # `H⁻p` may (eventually) be required to store all the coefficients for Hˡ₀ₘ with # non-negative `m`; even though that won't strictly be necessary for `ℓₘₐₓ`, it is # just one extra `RT`, and may simplify the coding significantly. `H⁺p` will # (eventually) be required to store all the coefficients for Hˡ⁺¹₀ₘ with # non-negative `m` (and that will be strictly necessary), so we give it one extra # column. - H⁻p = Matrix{RT}(undef, 1, Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) - H⁺p = Matrix{RT}(undef, 1, Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) - Wˡp = Matrix{NT}(undef, Int(2ℓₘₐₓ)+1, Int(2ℓₘₐₓ)+1) - new{IT, RT, NT}(ℓₘₐₓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ, H⁻p, H⁺p, Wˡp) + H⁻p = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) + H⁺p = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + Wˡp = Matrix{NT}(undef, Int(m′ₘₐₓ-m′ₘᵢₙ)+1, Int(mₘₐₓ-mₘᵢₙ)+1) + new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, H⁻p, H⁺p, Wˡp) end end ℓₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = ℓₘᵢₙ(wc.ℓₘₐₓ) ℓₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.ℓₘₐₓ -m′ₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘᵢₙ m′ₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘₐₓ -mₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.mₘᵢₙ +m′ₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘᵢₙ mₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.mₘₐₓ +mₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.mₘᵢₙ function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} if IT <: Rational @@ -47,60 +47,72 @@ function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} "ℓ=$ℓ is out of range for this WignerCalculator " * "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc))).") end - Wˡ = WignerDMatrix(wc.Wˡ, ℓ) - H⁻ = Hˡrow{IT, RT}(wc.H⁻, ℓ, 0) - H⁺ = Hˡrow{IT, RT}(wc.H⁺, ℓ+1, 0) - return Wˡ, H⁻, H⁺ + let ℓₘᵢₙ=ℓₘᵢₙ(wc), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(wc)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(wc)), + mₘₐₓ⁺=min(ℓ+1, mₘₐₓ(wc)), mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)) + + #@info "Calling WignerCalculator" wc ℓ m′ₘₐₓ m′ₘᵢₙ mₘₐₓ⁺ mₘₐₓ mₘᵢₙ ℓₘᵢₙ + Wˡ = WignerMatrix(wc.Wˡ, ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + H⁻ = WignerMatrix(wc.H⁻, ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ) + H⁺ = WignerMatrix(wc.H⁺, ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ⁺, mₘᵢₙ=ℓₘᵢₙ) + # H⁻ = Hˡrow{IT, RT}(wc.H⁻, ℓ, 0) + # H⁺ = Hˡrow{IT, RT}(wc.H⁺, ℓ+1, 0) + Wˡ, H⁻, H⁺ + end end function WignerDComputer( ℓₘₐₓ::IT, ::Type{RT}; - m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ + mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min ) where {IT, RT<:Real} NT = complex(RT) - WignerCalculator(ℓₘₐₓ, RT, NT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) + WignerCalculator(ℓₘₐₓ, RT, NT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) end function WignerdComputer( ℓₘₐₓ::IT, ::Type{RT}; - m′ₘᵢₙ::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ + mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min ) where {IT, RT<:Real} - WignerCalculator(ℓₘₐₓ, RT, RT; m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ) + WignerCalculator(ℓₘₐₓ, RT, RT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) end function recurrence_step1!(w::WignerCalculator{IT}) where {IT<:Signed} W⁰, H⁰, H¹ = w(0) - initialize!(H⁰) + recurrence_step1!(H⁰) w end function recurrence_step2!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) - sinβ, cosβ = reim(eⁱᵝ) + cosβ, sinβ = reim(eⁱᵝ) recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] + Hˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] + Wˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] recurrence_step2!(Hˡ⁺¹, Hˡ, sinβ, cosβ) w end function recurrence_step3!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - sinβ, cosβ = reim(eⁱᵝ) + cosβ, sinβ = reim(eⁱᵝ) recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) + Wˡ⁺¹, Hˡ⁺¹, Hˡ⁺² = w(ℓ+1) + Hˡ⁺¹[0:0, 0:ℓ+1] .= Hˡ⁺²[0:0, 0:ℓ+1] w end function recurrence_step4!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - sinβ, cosβ = reim(eⁱᵝ) + cosβ, sinβ = reim(eⁱᵝ) recurrence_step4!(Wˡ, sinβ, cosβ) w end function recurrence_step5!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - sinβ, cosβ = reim(eⁱᵝ) + cosβ, sinβ = reim(eⁱᵝ) recurrence_step5!(Wˡ, sinβ, cosβ) w end @@ -110,18 +122,39 @@ function recurrence_step6!(w::WignerCalculator{IT}, ℓ) where {IT<:Signed} recurrence_step6!(Wˡ) w end - -function recurrence!(w::WignerCalculator{IT, RT}, α::RT, β::RT, γ::RT, ℓ::IT) where {IT<:Signed, RT} + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT +) where {IT<:Signed, RT, NT} eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) + # @info "Step 1" recurrence_step1!(w)(0) + # H⁰₀₀ = 1 recurrence_step1!(w) for ℓ′ in 1:ℓ + # @info "At ℓ′=$ℓ′" + # @info "Step 2" recurrence_step2!(w, eⁱᵝ, ℓ′)(ℓ′) + # @info "Step 3" recurrence_step3!(w, eⁱᵝ, ℓ′)(ℓ′) + # @info "Step 4" recurrence_step4!(w, eⁱᵝ, ℓ′)(ℓ′) + # @info "Step 5" recurrence_step5!(w, eⁱᵝ, ℓ′)(ℓ′) + # @info "Step 6" recurrence_step6!(w, ℓ′)(ℓ′) + + # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ recurrence_step2!(w, eⁱᵝ, ℓ′) + + # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ recurrence_step3!(w, eⁱᵝ, ℓ′) + + # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ recurrence_step4!(w, eⁱᵝ, ℓ′) + + # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ recurrence_step5!(w, eⁱᵝ, ℓ′) + + # Impose symmetries recurrence_step6!(w, ℓ′) end Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + #@info "Convert" convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) Wˡ end From 2648571b6672956ed57c56f0b08a840e1bf148d0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 22:54:09 -0400 Subject: [PATCH 213/329] Remove some old debug statements --- src/redesign/WignerCalculator.jl | 15 +++------------ src/redesign/recurrence.jl | 4 ---- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 52eef8a8..0f4806f2 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -50,12 +50,9 @@ function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} let ℓₘᵢₙ=ℓₘᵢₙ(wc), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(wc)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(wc)), mₘₐₓ⁺=min(ℓ+1, mₘₐₓ(wc)), mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)) - #@info "Calling WignerCalculator" wc ℓ m′ₘₐₓ m′ₘᵢₙ mₘₐₓ⁺ mₘₐₓ mₘᵢₙ ℓₘᵢₙ Wˡ = WignerMatrix(wc.Wˡ, ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) H⁻ = WignerMatrix(wc.H⁻, ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ) H⁺ = WignerMatrix(wc.H⁺, ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ⁺, mₘᵢₙ=ℓₘᵢₙ) - # H⁻ = Hˡrow{IT, RT}(wc.H⁻, ℓ, 0) - # H⁺ = Hˡrow{IT, RT}(wc.H⁺, ℓ+1, 0) Wˡ, H⁻, H⁺ end end @@ -127,17 +124,11 @@ function recurrence!( w::WignerCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT ) where {IT<:Signed, RT, NT} eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) - # @info "Step 1" recurrence_step1!(w)(0) + # H⁰₀₀ = 1 recurrence_step1!(w) - for ℓ′ in 1:ℓ - # @info "At ℓ′=$ℓ′" - # @info "Step 2" recurrence_step2!(w, eⁱᵝ, ℓ′)(ℓ′) - # @info "Step 3" recurrence_step3!(w, eⁱᵝ, ℓ′)(ℓ′) - # @info "Step 4" recurrence_step4!(w, eⁱᵝ, ℓ′)(ℓ′) - # @info "Step 5" recurrence_step5!(w, eⁱᵝ, ℓ′)(ℓ′) - # @info "Step 6" recurrence_step6!(w, ℓ′)(ℓ′) + for ℓ′ in 1:ℓ # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ recurrence_step2!(w, eⁱᵝ, ℓ′) @@ -153,8 +144,8 @@ function recurrence!( # Impose symmetries recurrence_step6!(w, ℓ′) end + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - #@info "Convert" convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) Wˡ end diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index 2ccca1ed..c59043d6 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -206,14 +206,10 @@ function recurrence_step6!(Hˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, # Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] # without double-counting any entries, and accounting for m′ₘₐₓ. for m ∈ 1:ℓ - @info m for m′ ∈ -min(m′ₘₐₓ, m):min(m′ₘₐₓ, m) - @show (-m′, -m) (m′, m) Hˡ[m′, m] Hˡ[-m′, -m] = Hˡ[m′, m] end for m′ ∈ -min(m′ₘₐₓ, m-1):min(m′ₘₐₓ, m-1) - @show (m, m′) (m′, m) Hˡ[m′, m] - @show (-m, -m′) (m′, m) Hˡ[m′, m] Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[m′, m] end end From 069c6bce9a08a1049249b23f3225ec8d9e0bd164 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 23:05:45 -0400 Subject: [PATCH 214/329] Correct range for H^+ --- src/redesign/WignerCalculator.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 0f4806f2..d613c77f 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -48,11 +48,11 @@ function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} * "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc))).") end let ℓₘᵢₙ=ℓₘᵢₙ(wc), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(wc)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(wc)), - mₘₐₓ⁺=min(ℓ+1, mₘₐₓ(wc)), mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)) + mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)) Wˡ = WignerMatrix(wc.Wˡ, ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) H⁻ = WignerMatrix(wc.H⁻, ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ) - H⁺ = WignerMatrix(wc.H⁺, ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ⁺, mₘᵢₙ=ℓₘᵢₙ) + H⁺ = WignerMatrix(wc.H⁺, ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) Wˡ, H⁻, H⁺ end end From 95087550d0e880a49e5b66606a17d620cb87212c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 23:07:07 -0400 Subject: [PATCH 215/329] Copy initialization to Wigner matrix --- src/redesign/WignerCalculator.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index d613c77f..87dcd6c2 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -77,6 +77,7 @@ end function recurrence_step1!(w::WignerCalculator{IT}) where {IT<:Signed} W⁰, H⁰, H¹ = w(0) recurrence_step1!(H⁰) + W⁰[0, 0] = H⁰[0, 0] w end From 30531f052fdeed53faffaa5874b022ccd396e0fb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 23:08:17 -0400 Subject: [PATCH 216/329] Use views when incrementing ell storage --- src/redesign/WignerCalculator.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 87dcd6c2..da9c4bbe 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -86,8 +86,10 @@ function recurrence_step2!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Sig cosβ, sinβ = reim(eⁱᵝ) recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - Hˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] - Wˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] + # Hˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] + # Wˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] + copyto!(view(Hˡ, 0:0, 0:ℓ), view(Hˡ⁺¹, 0:0, 0:ℓ)) + copyto!(view(Wˡ, 0:0, 0:ℓ), view(Hˡ⁺¹, 0:0, 0:ℓ)) recurrence_step2!(Hˡ⁺¹, Hˡ, sinβ, cosβ) w end @@ -97,7 +99,8 @@ function recurrence_step3!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Sig cosβ, sinβ = reim(eⁱᵝ) recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) Wˡ⁺¹, Hˡ⁺¹, Hˡ⁺² = w(ℓ+1) - Hˡ⁺¹[0:0, 0:ℓ+1] .= Hˡ⁺²[0:0, 0:ℓ+1] + # Hˡ⁺¹[0:0, 0:ℓ+1] .= Hˡ⁺²[0:0, 0:ℓ+1] + copyto!(view(Hˡ⁺¹, 0:0, 0:ℓ+1), view(Hˡ⁺², 0:0, 0:ℓ+1)) w end From 6af9214178585ce6d34676eed051776814afba49 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 15 Oct 2025 23:08:57 -0400 Subject: [PATCH 217/329] Remove old direct (view-less) access --- src/redesign/WignerCalculator.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index da9c4bbe..09cd8119 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -44,8 +44,9 @@ function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} end if ℓ < ℓₘᵢₙ(wc) || ℓ > ℓₘₐₓ(wc) error( - "ℓ=$ℓ is out of range for this WignerCalculator " - * "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc))).") + "ℓ=$ℓ is out of range for this WignerCalculator " * + "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc)))." + ) end let ℓₘᵢₙ=ℓₘᵢₙ(wc), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(wc)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(wc)), mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)) @@ -86,8 +87,6 @@ function recurrence_step2!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Sig cosβ, sinβ = reim(eⁱᵝ) recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - # Hˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] - # Wˡ[0:0, 0:ℓ] .= Hˡ⁺¹[0:0, 0:ℓ] copyto!(view(Hˡ, 0:0, 0:ℓ), view(Hˡ⁺¹, 0:0, 0:ℓ)) copyto!(view(Wˡ, 0:0, 0:ℓ), view(Hˡ⁺¹, 0:0, 0:ℓ)) recurrence_step2!(Hˡ⁺¹, Hˡ, sinβ, cosβ) @@ -99,7 +98,6 @@ function recurrence_step3!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Sig cosβ, sinβ = reim(eⁱᵝ) recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) Wˡ⁺¹, Hˡ⁺¹, Hˡ⁺² = w(ℓ+1) - # Hˡ⁺¹[0:0, 0:ℓ+1] .= Hˡ⁺²[0:0, 0:ℓ+1] copyto!(view(Hˡ⁺¹, 0:0, 0:ℓ+1), view(Hˡ⁺², 0:0, 0:ℓ+1)) w end From f7c05df3f2a8851970481661ec3eedb2032c5801 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 01:29:48 -0400 Subject: [PATCH 218/329] Swap H storage, rather than copying --- src/redesign/WignerCalculator.jl | 35 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 09cd8119..3c1fcb6c 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -5,9 +5,10 @@ struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} m′ₘᵢₙ::IT mₘₐₓ::IT mₘᵢₙ::IT - H⁻::Matrix{RT} - H⁺::Matrix{RT} + Hᵃ::Matrix{RT} + Hᵇ::Matrix{RT} Wˡ::Matrix{NT} + swapH::Base.RefValue{Bool} # wc(ℓ) returns (Wˡ, Hᵇ, Hᵃ) if `true`, otherwise (Wˡ, Hᵃ, Hᵇ) function WignerCalculator( ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt; m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ @@ -16,16 +17,14 @@ struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} error("RT=$RT is supposed to be the real type of NT=$NT.") end validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) - # `H⁻p` may (eventually) be required to store all the coefficients for Hˡ₀ₘ with - # non-negative `m`; even though that won't strictly be necessary for `ℓₘₐₓ`, it is - # just one extra `RT`, and may simplify the coding significantly. `H⁺p` will - # (eventually) be required to store all the coefficients for Hˡ⁺¹₀ₘ with - # non-negative `m` (and that will be strictly necessary), so we give it one extra - # column. - H⁻p = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) - H⁺p = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + # One of the H matrices will (eventually) be required to store all the coefficients + # for Hˡ⁺¹₀ₘ with non-negative `m` (and that will be strictly necessary), so we give + # it one extra column. Since we may not know which one that will be, we give both + # of them that extra column. + Hᵃp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + Hᵇp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) Wˡp = Matrix{NT}(undef, Int(m′ₘₐₓ-m′ₘᵢₙ)+1, Int(mₘₐₓ-mₘᵢₙ)+1) - new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, H⁻p, H⁺p, Wˡp) + new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, Hᵃp, Hᵇp, Wˡp, Ref(false)) end end @@ -36,6 +35,20 @@ m′ₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘᵢₙ mₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.mₘₐₓ mₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.mₘᵢₙ +Hᵃ(wc::WignerCalculator) = swapH(wc) ? wc.Hᵇ : wc.Hᵃ +Hᵇ(wc::WignerCalculator) = swapH(wc) ? wc.Hᵃ : wc.Hᵇ +Wˡ(wc::WignerCalculator) = wc.Wˡ + +function swapH(wc::WignerCalculator) + wc.swapH[] +end + +function swapH!(wc::WignerCalculator) + wc.swapH[] = !wc.swapH[] + wc +end + + function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} if IT <: Rational if denominator(ℓ) ≠ 2 From f67e1ec2a9188c61496d44affabddfbc2d2b2dca Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 01:37:20 -0400 Subject: [PATCH 219/329] Separate recurrence from application, and from memory bookkeeping --- src/redesign/WignerCalculator.jl | 58 +++++++++++++++++++------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 3c1fcb6c..6af0e717 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -48,6 +48,11 @@ function swapH!(wc::WignerCalculator) wc end +function fillW!(wc::WignerCalculator{IT}, ℓ::IT) where {IT} + Wˡ, Hˡ, Hˡ⁺¹ = wc(ℓ) + Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] + wc +end function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} if IT <: Rational @@ -62,16 +67,16 @@ function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} ) end let ℓₘᵢₙ=ℓₘᵢₙ(wc), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(wc)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(wc)), - mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)) + mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)), + Wˡ=WignerMatrix(Wˡ(wc), ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ), + Hˡ=WignerMatrix(Hᵃ(wc), ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ), + Hˡ⁺¹=WignerMatrix(Hᵇ(wc), ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) - Wˡ = WignerMatrix(wc.Wˡ, ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) - H⁻ = WignerMatrix(wc.H⁻, ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ) - H⁺ = WignerMatrix(wc.H⁺, ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) - Wˡ, H⁻, H⁺ + Wˡ, Hˡ, Hˡ⁺¹ end end -function WignerDComputer( +function WignerDCalculator( ℓₘₐₓ::IT, ::Type{RT}; mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min @@ -80,7 +85,7 @@ function WignerDComputer( WignerCalculator(ℓₘₐₓ, RT, NT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) end -function WignerdComputer( +function WignerdCalculator( ℓₘₐₓ::IT, ::Type{RT}; mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min @@ -99,10 +104,6 @@ function recurrence_step2!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Sig Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) cosβ, sinβ = reim(eⁱᵝ) recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - copyto!(view(Hˡ, 0:0, 0:ℓ), view(Hˡ⁺¹, 0:0, 0:ℓ)) - copyto!(view(Wˡ, 0:0, 0:ℓ), view(Hˡ⁺¹, 0:0, 0:ℓ)) - recurrence_step2!(Hˡ⁺¹, Hˡ, sinβ, cosβ) w end @@ -110,8 +111,6 @@ function recurrence_step3!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Sig Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) cosβ, sinβ = reim(eⁱᵝ) recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) - Wˡ⁺¹, Hˡ⁺¹, Hˡ⁺² = w(ℓ+1) - copyto!(view(Hˡ⁺¹, 0:0, 0:ℓ+1), view(Hˡ⁺², 0:0, 0:ℓ+1)) w end @@ -143,24 +142,37 @@ function recurrence!( # H⁰₀₀ = 1 recurrence_step1!(w) - for ℓ′ in 1:ℓ - # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ - recurrence_step2!(w, eⁱᵝ, ℓ′) + if ℓ == ℓₘᵢₙ(w) + fillW!(w, ℓₘᵢₙ(w)) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + Wˡ + else + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ+1 + # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ + recurrence_step2!(w, eⁱᵝ, ℓ′) + swapH!(w) + end + swapH!(w) + + # Copy Hˡ₀ₘ to Wˡ₀ₘ + fillW!(w, ℓ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ - recurrence_step3!(w, eⁱᵝ, ℓ′) + recurrence_step3!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ - recurrence_step4!(w, eⁱᵝ, ℓ′) + recurrence_step4!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ - recurrence_step5!(w, eⁱᵝ, ℓ′) + recurrence_step5!(w, eⁱᵝ, ℓ) # Impose symmetries - recurrence_step6!(w, ℓ′) + recurrence_step6!(w, ℓ) + + # Finish conversion to Dˡ + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) + Wˡ end - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) - Wˡ end From cfeaebb9d31aed68aedda2311941e8fe73c0b89f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 09:45:19 -0400 Subject: [PATCH 220/329] Change internal notation --- src/redesign/WignerCalculator.jl | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 6af0e717..aff54b71 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -8,7 +8,7 @@ struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} Hᵃ::Matrix{RT} Hᵇ::Matrix{RT} Wˡ::Matrix{NT} - swapH::Base.RefValue{Bool} # wc(ℓ) returns (Wˡ, Hᵇ, Hᵃ) if `true`, otherwise (Wˡ, Hᵃ, Hᵇ) + swapH::Base.RefValue{Bool} # w(ℓ) returns (Wˡ, Hᵇ, Hᵃ) if `true`, otherwise (Wˡ, Hᵃ, Hᵇ) function WignerCalculator( ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt; m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ @@ -28,49 +28,49 @@ struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} end end -ℓₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = ℓₘᵢₙ(wc.ℓₘₐₓ) -ℓₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.ℓₘₐₓ -m′ₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘₐₓ -m′ₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.m′ₘᵢₙ -mₘₐₓ(wc::WignerCalculator{IT}) where {IT} = wc.mₘₐₓ -mₘᵢₙ(wc::WignerCalculator{IT}) where {IT} = wc.mₘᵢₙ +ℓₘᵢₙ(w::WignerCalculator{IT}) where {IT} = ℓₘᵢₙ(w.ℓₘₐₓ) +ℓₘₐₓ(w::WignerCalculator{IT}) where {IT} = w.ℓₘₐₓ +m′ₘₐₓ(w::WignerCalculator{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerCalculator{IT}) where {IT} = w.m′ₘᵢₙ +mₘₐₓ(w::WignerCalculator{IT}) where {IT} = w.mₘₐₓ +mₘᵢₙ(w::WignerCalculator{IT}) where {IT} = w.mₘᵢₙ -Hᵃ(wc::WignerCalculator) = swapH(wc) ? wc.Hᵇ : wc.Hᵃ -Hᵇ(wc::WignerCalculator) = swapH(wc) ? wc.Hᵃ : wc.Hᵇ -Wˡ(wc::WignerCalculator) = wc.Wˡ +Hᵃ(w::WignerCalculator) = swapH(w) ? w.Hᵇ : w.Hᵃ +Hᵇ(w::WignerCalculator) = swapH(w) ? w.Hᵃ : w.Hᵇ +Wˡ(w::WignerCalculator) = w.Wˡ -function swapH(wc::WignerCalculator) - wc.swapH[] +function swapH(w::WignerCalculator) + w.swapH[] end -function swapH!(wc::WignerCalculator) - wc.swapH[] = !wc.swapH[] - wc +function swapH!(w::WignerCalculator) + w.swapH[] = !w.swapH[] + w end -function fillW!(wc::WignerCalculator{IT}, ℓ::IT) where {IT} - Wˡ, Hˡ, Hˡ⁺¹ = wc(ℓ) +function fillW!(w::WignerCalculator{IT}, ℓ::IT) where {IT} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] - wc + w end -function (wc::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} +function (w::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} if IT <: Rational if denominator(ℓ) ≠ 2 error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") end end - if ℓ < ℓₘᵢₙ(wc) || ℓ > ℓₘₐₓ(wc) + if ℓ < ℓₘᵢₙ(w) || ℓ > ℓₘₐₓ(w) error( "ℓ=$ℓ is out of range for this WignerCalculator " * - "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(wc)) and ℓₘₐₓ=$(ℓₘₐₓ(wc)))." + "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(w)) and ℓₘₐₓ=$(ℓₘₐₓ(w)))." ) end - let ℓₘᵢₙ=ℓₘᵢₙ(wc), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(wc)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(wc)), - mₘₐₓ=min(ℓ, mₘₐₓ(wc)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(wc)), - Wˡ=WignerMatrix(Wˡ(wc), ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ), - Hˡ=WignerMatrix(Hᵃ(wc), ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ), - Hˡ⁺¹=WignerMatrix(Hᵇ(wc), ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) + let ℓₘᵢₙ=ℓₘᵢₙ(w), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(w)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(w)), + mₘₐₓ=min(ℓ, mₘₐₓ(w)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(w)), + Wˡ=WignerMatrix(Wˡ(w), ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ), + Hˡ=WignerMatrix(Hᵃ(w), ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ), + Hˡ⁺¹=WignerMatrix(Hᵇ(w), ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) Wˡ, Hˡ, Hˡ⁺¹ end From 2cf3b9d386f59c647743d3c70e023ff84c9e52a4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 09:46:01 -0400 Subject: [PATCH 221/329] =?UTF-8?q?Add=20option=20to=20`skip=5F=E2=84=93?= =?UTF-8?q?=5Frecurrence`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/redesign/WignerCalculator.jl | 45 ++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index aff54b71..00069111 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -135,24 +135,49 @@ function recurrence_step6!(w::WignerCalculator{IT}, ℓ) where {IT<:Signed} end function recurrence!( - w::WignerCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT + w::WignerCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT, + skip_ℓ_recurrence::Bool=false ) where {IT<:Signed, RT, NT} eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) - # H⁰₀₀ = 1 - recurrence_step1!(w) + # NOTE: In the comments explaining the recurrence steps below, we use notation with + # ℓₘᵢₙ=0 for simplicity, but the code should work for ℓₘᵢₙ=1//2 as well. if ℓ == ℓₘᵢₙ(w) - fillW!(w, ℓₘᵢₙ(w)) + # H⁰₀₀ = 1 + recurrence_step1!(w) + + # Record the result in Wˡ + fillW!(w, ℓ) + + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ and swap + # H⁰₀ₘ -> H¹₀ₘ + recurrence_step2!(w, eⁱᵝ, ℓ+1) + swapH!(w) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) Wˡ else - for ℓ′ in ℓₘᵢₙ(w)+1:ℓ+1 - # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ + if !skip_ℓ_recurrence + # H⁰₀₀ = 1 + recurrence_step1!(w) + + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ + # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ + recurrence_step2!(w, eⁱᵝ, ℓ′) + swapH!(w) + end + end + + let ℓ′ = ℓ+1 + # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ recurrence_step2!(w, eⁱᵝ, ℓ′) - swapH!(w) end - swapH!(w) + + let + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + @info "recurrence! called with skip_ℓ_recurrence=$skip_ℓ_recurrence" Hˡ Hˡ⁺¹ + end # Copy Hˡ₀ₘ to Wˡ₀ₘ fillW!(w, ℓ) @@ -172,6 +197,10 @@ function recurrence!( # Finish conversion to Dˡ Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) + + # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ + swapH!(w) + Wˡ end From 8661fdf12bf3131be733e41565d0c45d6432840d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 09:46:27 -0400 Subject: [PATCH 222/329] Make simple functions more efficient --- src/redesign/recurrence.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index c59043d6..e5dd357c 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -1,9 +1,10 @@ # Eq. (44) in Gumerov and Duraiswami (2015). Note that they define `sgn` as follows, which # is different from the usual definition, including from Julia's `sign` function, at 0: -sgn(m) = m ≥ 0 ? 1 : -1 +sgn(m) = ifelse(m ≥ 0, 1, -1) # Eq. (7) in Gumerov and Duraiswami (2015) -ϵ(m) = (m ≥ 0 ? (-1)^m : 1) +# ϵ(m) = (m ≥ 0 ? (-1)^m : 1) +ϵ(m) = ifelse(m > 0 && isodd(m), -1, 1) @doc raw""" From 3b7096d1ef3e35893b80c198e95639ea1ac1901a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 09:46:55 -0400 Subject: [PATCH 223/329] Specify number types for H -> D/d conversions --- src/redesign/recurrence.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redesign/recurrence.jl b/src/redesign/recurrence.jl index e5dd357c..4a175aaa 100644 --- a/src/redesign/recurrence.jl +++ b/src/redesign/recurrence.jl @@ -226,7 +226,7 @@ Convert the Wigner matrix `Hˡ` to the d matrix `dˡ`, which just involves multi signs related to the `m′` and `m` indices. """ -function convert_H_to_d!(Hˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT} +function convert_H_to_d!(Hˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT<:Real} @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) for m ∈ -ℓ:ℓ for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ @@ -245,7 +245,7 @@ Convert the Wigner matrix `Hˡ` to the D matrix `Dˡ`, which just involves multi complex phases related to the `m′` and `m` indices. """ -function convert_H_to_D!(Hˡ::AbstractWignerMatrix{IT, NT}, eⁱᵅ::NT, eⁱᵞ::NT) where {IT<:Signed, NT} +function convert_H_to_D!(Hˡ::AbstractWignerMatrix{IT, NT}, eⁱᵅ::NT, eⁱᵞ::NT) where {IT<:Signed, NT<:Complex} # NOTE: This function will have to be modified to work for Rational indices because the # phases will not be integer powers; we'll have to incorporate √eⁱᵅ and √eⁱᵞ. @inbounds let ℓ=ℓ(Hˡ), ℓₘᵢₙ=ℓₘᵢₙ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) From 24f964cd85b292a04d17494ba6a9db2411b64427 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 10:43:06 -0400 Subject: [PATCH 224/329] Remove debugging message --- src/redesign/WignerCalculator.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 00069111..ac462b74 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -174,11 +174,6 @@ function recurrence!( recurrence_step2!(w, eⁱᵝ, ℓ′) end - let - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - @info "recurrence! called with skip_ℓ_recurrence=$skip_ℓ_recurrence" Hˡ Hˡ⁺¹ - end - # Copy Hˡ₀ₘ to Wˡ₀ₘ fillW!(w, ℓ) From 15101df2579c1816d06cd6fa07f9c3669707876d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 10:43:33 -0400 Subject: [PATCH 225/329] Add fill!(::WignerCalculator, x) --- src/redesign/WignerCalculator.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index ac462b74..eab04422 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -39,6 +39,15 @@ Hᵃ(w::WignerCalculator) = swapH(w) ? w.Hᵇ : w.Hᵃ Hᵇ(w::WignerCalculator) = swapH(w) ? w.Hᵃ : w.Hᵇ Wˡ(w::WignerCalculator) = w.Wˡ +function Base.fill!(w::WignerCalculator{IT, RT}, v::Real) where {IT, RT} + let Wˡ = Wˡ(w), Hᵃ = Hᵃ(w), Hᵇ = Hᵇ(w) + fill!(Wˡ, eltype(Wˡ)(v)) + fill!(Hᵃ, eltype(Hᵃ)(v)) + fill!(Hᵇ, eltype(Hᵇ)(v)) + end + w +end + function swapH(w::WignerCalculator) w.swapH[] end From c83085a62eccca8b3cbf288edc4ab20ba52bec30 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 10:43:54 -0400 Subject: [PATCH 226/329] Use views to avoid allocation --- src/redesign/WignerCalculator.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index eab04422..d66a50e1 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -59,7 +59,8 @@ end function fillW!(w::WignerCalculator{IT}, ℓ::IT) where {IT} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] + # Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] + @views copyto!(Wˡ[0:0, 0:ℓ], Hˡ[0:0, 0:ℓ]) w end From 1ea34bae137fe19556e71af424f47ac4d029b41e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Oct 2025 10:44:06 -0400 Subject: [PATCH 227/329] Remove older code --- src/redesign/WignerCalculator.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index d66a50e1..e7df4056 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -59,7 +59,6 @@ end function fillW!(w::WignerCalculator{IT}, ℓ::IT) where {IT} Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - # Wˡ[0:0, 0:ℓ] .= Hˡ[0:0, 0:ℓ] @views copyto!(Wˡ[0:0, 0:ℓ], Hˡ[0:0, 0:ℓ]) w end From 0f05fa8fd0a19f517f3390ab1396d8c3fecbbd88 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Oct 2025 11:19:29 -0400 Subject: [PATCH 228/329] Respect ell_min --- src/redesign/WignerCalculator.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index e7df4056..5a6b3f43 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -103,9 +103,10 @@ function WignerdCalculator( end function recurrence_step1!(w::WignerCalculator{IT}) where {IT<:Signed} - W⁰, H⁰, H¹ = w(0) + ℓ = ℓₘᵢₙ(w) + W⁰, H⁰, H¹ = w(ℓ) recurrence_step1!(H⁰) - W⁰[0, 0] = H⁰[0, 0] + fillW!(w, ℓ) w end From 3934024bfed0b69ea2509f46860282d3446abfdc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Oct 2025 11:20:22 -0400 Subject: [PATCH 229/329] Better dispatch for different argument and Wigner types --- src/redesign/WignerCalculator.jl | 91 +++++++++++++++++++------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index 5a6b3f43..a275ff33 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -147,66 +147,83 @@ end function recurrence!( w::WignerCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT, skip_ℓ_recurrence::Bool=false -) where {IT<:Signed, RT, NT} +) where {IT<:Signed, RT, NT<:Complex} eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) + recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ, skip_ℓ_recurrence) +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, β::RT, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Real} + eⁱᵝ = cis(β) + recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, + ℓ::IT, skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Complex} + _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) + Wˡ +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Real} + _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_d!(Wˡ) + Wˡ +end +function _recurrence!( + w::WignerCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT} # NOTE: In the comments explaining the recurrence steps below, we use notation with - # ℓₘᵢₙ=0 for simplicity, but the code should work for ℓₘᵢₙ=1//2 as well. + # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. if ℓ == ℓₘᵢₙ(w) - # H⁰₀₀ = 1 - recurrence_step1!(w) + recurrence_step1!(w) # H⁰₀₀ = 1 + fillW!(w, ℓ) # Record the result in Wˡ - # Record the result in Wˡ - fillW!(w, ℓ) - - # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ and swap - # H⁰₀ₘ -> H¹₀ₘ - recurrence_step2!(w, eⁱᵝ, ℓ+1) + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ and swap. + recurrence_step2!(w, eⁱᵝ, ℓ+1) # H⁰₀ₘ -> H¹₀ₘ swapH!(w) - - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - Wˡ else if !skip_ℓ_recurrence - # H⁰₀₀ = 1 - recurrence_step1!(w) + recurrence_step1!(w) # H⁰₀₀ = 1 for ℓ′ in ℓₘᵢₙ(w)+1:ℓ - # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ - recurrence_step2!(w, eⁱᵝ, ℓ′) - swapH!(w) + recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ + swapH!(w) # Prepare for the next iteration of ℓ′ end end + # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not we + # asked to skip the ℓ recurrence. If we did, the user is responsible for having + # already set Hˡ₀ₘ correctly; if we didn't, we just set it in the loop above. let ℓ′ = ℓ+1 - # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ - recurrence_step2!(w, eⁱᵝ, ℓ′) + recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ end - # Copy Hˡ₀ₘ to Wˡ₀ₘ - fillW!(w, ℓ) - - # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ - recurrence_step3!(w, eⁱᵝ, ℓ) - - # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ - recurrence_step4!(w, eⁱᵝ, ℓ) - - # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ - recurrence_step5!(w, eⁱᵝ, ℓ) + fillW!(w, ℓ) # Copy Hˡ₀ₘ to Wˡ₀ₘ + recurrence_step3!(w, eⁱᵝ, ℓ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ # Impose symmetries recurrence_step6!(w, ℓ) - # Finish conversion to Dˡ - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) - # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ swapH!(w) - - Wˡ end + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + Wˡ + end From 4fe8709f8629e16a9241e560c64b014cfcf7e412 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 12:56:23 -0400 Subject: [PATCH 230/329] Eliminate WignerRange as unnecessary --- src/redesign/WignerMatrix.jl | 60 +++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 8d792bd7..0b3f7397 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -78,36 +78,40 @@ Base.eltype(::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT Base.size(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) Base.length(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) -struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} - start::T - stop::T - - WignerRange(r::UnitRange{T}) where {T} = new{T}(r.start, r.stop) -end -@inline Base.axes(r::WignerRange) = (axes1(r),) -@inline axes1(r::WignerRange) = WignerRange(r.start:r.stop) -if VERSION < v"1.8.2" - Base.axes1(r::WignerRange) = axes1(r) -end -Base.inds2string(inds::NTuple{2, WignerRange}) = - string( - "(", inds[1].start, ":", inds[1].stop, ")", - "×", - "(", inds[2].start, ":", inds[2].stop, ")" - ) -Base.firstindex(r::WignerRange) = 1 -Base.lastindex(r::WignerRange) = length(r) -function Base.getindex(v::WignerRange, i::Bool) - throw(ArgumentError("invalid index: $i of type Bool")) -end -@propagate_inbounds function Base.getindex(v::WignerRange{T}, i::Integer) where {T} - val = convert(T, v.start + (i - oneunit(i))) - @boundscheck (i>0 && val <= v.stop && val >= v.start) || throw(BoundsError(v, i)) - val -end +# struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} +# start::T +# stop::T + +# WignerRange(r::UnitRange{T}) where {T} = new{T}(r.start, r.stop) +# end +# @inline Base.axes(r::WignerRange) = (axes1(r),) +# @inline axes1(r::WignerRange) = WignerRange(r.start:r.stop) +# if VERSION < v"1.8.2" +# Base.axes1(r::WignerRange) = axes1(r) +# end +# Base.inds2string(inds::NTuple{2, WignerRange}) = +# string( +# "(", inds[1].start, ":", inds[1].stop, ")", +# "×", +# "(", inds[2].start, ":", inds[2].stop, ")" +# ) +# Base.firstindex(r::WignerRange) = 1 +# Base.lastindex(r::WignerRange) = length(r) +# function Base.getindex(v::WignerRange, i::Bool) +# throw(ArgumentError("invalid index: $i of type Bool")) +# end +# @propagate_inbounds function Base.getindex(v::WignerRange{T}, i::Integer) where {T} +# val = convert(T, v.start + (i - oneunit(i))) +# @boundscheck (i>0 && val <= v.stop && val >= v.start) || throw(BoundsError(v, i)) +# val +# end + +# function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} +# (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) +# end function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} - (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) + ((m′ₘᵢₙ(w):m′ₘₐₓ(w)), (mₘᵢₙ(w):mₘₐₓ(w))) end # We don't have to override Base.show; most of its machinery works just fine, except that From 86fbf927a2b9a47c5487cd61ea6faad339130c60 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 12:57:41 -0400 Subject: [PATCH 231/329] Remove old WignerRange code --- src/redesign/WignerMatrix.jl | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 0b3f7397..10905576 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -77,39 +77,6 @@ isrational(::AbstractWignerMatrix{IT}) where {IT<:Rational} = true Base.eltype(::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT Base.size(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) Base.length(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) - -# struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} -# start::T -# stop::T - -# WignerRange(r::UnitRange{T}) where {T} = new{T}(r.start, r.stop) -# end -# @inline Base.axes(r::WignerRange) = (axes1(r),) -# @inline axes1(r::WignerRange) = WignerRange(r.start:r.stop) -# if VERSION < v"1.8.2" -# Base.axes1(r::WignerRange) = axes1(r) -# end -# Base.inds2string(inds::NTuple{2, WignerRange}) = -# string( -# "(", inds[1].start, ":", inds[1].stop, ")", -# "×", -# "(", inds[2].start, ":", inds[2].stop, ")" -# ) -# Base.firstindex(r::WignerRange) = 1 -# Base.lastindex(r::WignerRange) = length(r) -# function Base.getindex(v::WignerRange, i::Bool) -# throw(ArgumentError("invalid index: $i of type Bool")) -# end -# @propagate_inbounds function Base.getindex(v::WignerRange{T}, i::Integer) where {T} -# val = convert(T, v.start + (i - oneunit(i))) -# @boundscheck (i>0 && val <= v.stop && val >= v.start) || throw(BoundsError(v, i)) -# val -# end - -# function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} -# (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) -# end - function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} ((m′ₘᵢₙ(w):m′ₘₐₓ(w)), (mₘᵢₙ(w):mₘₐₓ(w))) end From 12dcb38e4aaa6a6082691c944c4c9cb3e9dec598 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 13:31:05 -0400 Subject: [PATCH 232/329] Accept any array for the backend storage --- src/redesign/WignerMatrix.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 10905576..0e981d1f 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -46,7 +46,7 @@ For example, if the parent Matrix is not stored as the `parent` field, then the method should be re-implemented to return the correct parent object. The `getindex` and `setindex!` """ -abstract type AbstractWignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractMatrix{NT}} <: AbstractMatrix{NT} end +abstract type AbstractWignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractArray{NT}} <: AbstractMatrix{NT} end ### General methods for all AbstractWignerMatrix types From d5877004e13bf4b0306eb9e14ba1dcce8e2c567a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 13:31:27 -0400 Subject: [PATCH 233/329] Allow access to the default WignerMatrix constructor --- src/redesign/WignerMatrix.jl | 108 ++++++----------------------------- 1 file changed, 19 insertions(+), 89 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 0e981d1f..3f20fa56 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -193,100 +193,30 @@ struct WignerMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} m′ₘᵢₙ::IT mₘₐₓ::IT mₘᵢₙ::IT - function WignerMatrix( - parent::ST, ℓ::IT; - mp_max::IT=ℓ, mp_min::IT=-ℓ, m_max::IT=ℓ, m_min::IT=-ℓ, - m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min - ) where {IT, NT, ST<:AbstractMatrix{NT}} - validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) - s₁, s₂ = size(parent) - if s₁ < Int(m′ₘₐₓ - m′ₘᵢₙ + 1) - error( - "The extent of the first dimension in the input data must be at least " - * "m′ₘₐₓ-m′ₘᵢₙ+1=$(Int(m′ₘₐₓ - m′ₘᵢₙ + 1)); it is $s₁." - ) - end - if s₂ < Int(mₘₐₓ - mₘᵢₙ + 1) - error( - "The extent of the second dimension in the input data must be at least " - * "mₘₐₓ-mₘᵢₙ+1=$(Int(mₘₐₓ - mₘᵢₙ + 1)); it is $s₂." - ) - end - new{IT, NT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) - end end - -### Specialize to D and d matrices - -""" - WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} - -Specialized subtype of [`AbstractWignerMatrix`](@ref) for D-matrices, which are complex matrices. -""" -struct WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} - parent::ST - ℓ::IT - m′ₘₐₓ::IT - function WignerDMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} - # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use - # a restriction on NT in the type declaration. - if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT - error( - "WignerDMatrix only supports complex types; the input type is $NT.\n" - * "Perhaps you meant to use WignerdMatrix?" - ) - end - if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) - error( - "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" - * "if ℓ is an integer its type must be <:Integer, not <:Rational." - ) - end - s₁, s₂ = size(parent) - if s₂ ≠ Int(2ℓ + 1) - error( - "The extent of the second dimension in the input data must be " - * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." - ) - end - if s₁ == 0 || s₁ > s₂ - error( - "The extent of the first dimension in the input data must be greater than 0" - * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." - ) - end - if IT <: Rational - if isodd(s₁) - error( - "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " - * "input data ($s₁) corresponds to whole-integer values of m′." - ) - end - else - if iseven(s₁) - error( - "ℓ=$ℓ is an integer, but the extent of the first dimension in the " - * "input data ($s₁) corresponds to half-integer values of m′." - ) - end - end - m′ₘₐₓ = IT((s₁ - 1) // 2) - new(parent, ℓ, m′ₘₐₓ) +function WignerMatrix( + parent::ST, ℓ::IT; + mp_max::IT=ℓ, mp_min::IT=-ℓ, m_max::IT=ℓ, m_min::IT=-ℓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min +) where {IT, NT, ST<:AbstractMatrix{NT}} + validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + s₁, s₂ = size(parent) + if s₁ < Int(m′ₘₐₓ - m′ₘᵢₙ + 1) + error( + "The extent of the first dimension in the input data must be at least " + * "m′ₘₐₓ-m′ₘᵢₙ+1=$(Int(m′ₘₐₓ - m′ₘᵢₙ + 1)); it is $s₁." + ) end + if s₂ < Int(mₘₐₓ - mₘᵢₙ + 1) + error( + "The extent of the second dimension in the input data must be at least " + * "mₘₐₓ-mₘᵢₙ+1=$(Int(mₘₐₓ - mₘᵢₙ + 1)); it is $s₂." + ) + end + WignerMatrix{IT, NT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) end -""" - WignerDMatrix(parent, ℓ) - -Construct a `WignerDMatrix` object from the given parent matrix and ``ℓ`` value. Note that -the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the -denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent -matrix must have the correct size: the first dimension must be greater than 0 and less than -or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. -""" -function WignerDMatrix(parent::ST, ℓ::IT) where {IT, ST} - WignerDMatrix{IT, eltype(ST), ST}(parent, ℓ) end function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} if complex(NT) ≢ NT From 9957010bcde57d539d630567b2261771f5403a39 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 13:36:03 -0400 Subject: [PATCH 234/329] Change from explicit subtypes of AbstractWignerMatrix to parameter-specialized aliases --- src/redesign/WignerMatrix.jl | 347 +++++++++++++++++++++++------------ 1 file changed, 233 insertions(+), 114 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 3f20fa56..5642b39d 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -217,133 +217,252 @@ function WignerMatrix( WignerMatrix{IT, NT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) end -end -function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} - if complex(NT) ≢ NT - error( - "`WignerDMatrix` only supports complex types; the input type is $NT.\n" - * "Perhaps you meant to use `WignerdMatrix`?" - ) - end - WignerDMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) -end - - """ - WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + WignerDMatrix{IT, RT, ST} -Specialized subtype of [`AbstractWignerMatrix`](@ref) for d-matrices, which are real matrices. -""" -struct WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} - parent::ST - ℓ::IT - m′ₘₐₓ::IT - function WignerdMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} - # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use - # a restriction on NT in the type declaration. - if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT - error( - "WignerdMatrix only supports real types; the input type is $NT.\n" - * "Perhaps you meant to use WignerDMatrix?" - ) - end - if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) - error( - "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" - * "if ℓ is an integer its type must be <:Integer, not <:Rational." - ) - end - s₁, s₂ = size(parent) - if s₂ ≠ Int(2ℓ + 1) - error( - "The extent of the second dimension in the input data must be " - * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." - ) - end - if s₁ == 0 || s₁ > s₂ - error( - "The extent of the first dimension in the input data must be greater than 0" - * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." - ) - end - if IT <: Rational - if isodd(s₁) - error( - "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " - * "input data ($s₁) corresponds to whole-integer values of m′." - ) - end - else - if iseven(s₁) - error( - "ℓ=$ℓ is an integer, but the extent of the first dimension in the " - * "input data ($s₁) corresponds to half-integer values of m′." - ) - end - end - m′ₘₐₓ = IT((s₁ - 1) // 2) - new(parent, ℓ, m′ₘₐₓ) - end -end +Type alias for [`WignerMatrix`](@ref) with complex number type `Complex{RT}`. +Represents Wigner D-matrices (complex rotation matrices). +# Example +```julia +D = WignerDMatrix(ComplexF64, 2) # Creates WignerMatrix with NT=ComplexF64 """ - WignerdMatrix(parent, ℓ) +const WignerDMatrix{IT, RT, ST} = WignerMatrix{IT, Complex{RT}, ST} where {IT, RT<:Real, ST<:AbstractMatrix{Complex{RT}}} -Construct a `WignerdMatrix` object from the given parent matrix and ``ℓ`` value. Note that -the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the -denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent -matrix must have the correct size: the first dimension must be greater than 0 and less than -or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. -""" -function WignerdMatrix(parent::ST, ℓ::IT) where {IT, ST} - WignerdMatrix{IT, eltype(ST), ST}(parent, ℓ) +const WignerdMatrix{IT, RT, ST} = WignerMatrix{IT, RT, ST} where {IT, RT<:Real, ST<:AbstractMatrix{RT}} + +# Constructors for WignerDMatrix (complex) +function WignerDMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{Complex{RT}}} + WignerMatrix(parent, ℓ; kwargs...) end -function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} - if real(NT) ≢ NT - error( - "`WignerdMatrix` only supports real types; the input type is $NT.\n" - * "Perhaps you meant to use `WignerDMatrix`?" - ) - end - WignerdMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) +function WignerDMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{RT}} + error( + "WignerDMatrix only supports complex types; the input type is $RT.\n" + * "Perhaps you meant to use WignerdMatrix?\n" + ) +end +function WignerDMatrix(::Type{Complex{RT}}, ℓ::IT, m′ₘₐₓ::IT=ℓ; kwargs...) where {RT<:Real, IT} + parent = Matrix{Complex{RT}}(undef, Int(m′ₘₐₓ - (-m′ₘₐₓ) + 1), Int(2ℓ + 1)) + WignerMatrix(parent, ℓ; m′ₘₐₓ=m′ₘₐₓ, m′ₘᵢₙ=-m′ₘₐₓ, kwargs...) end -""" - Hˡrow{IT, NT, ST} - -Specialized subtype of [`AbstractWignerMatrix`](@ref) intended to store one row of the ``H`` matrix -— usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the -recurrence relations. -""" -struct Hˡrow{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} - parent::ST - ℓ::IT - m′ₘₐₓ::IT +# Constructors for WignerdMatrix (real) +function WignerdMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{RT}} + WignerMatrix(parent, ℓ; kwargs...) end -function Hˡrow(parent::ST, ℓ::IT, m′::IT) where {IT, ST} - length_m′ = 1 - length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 - if size(parent,1) < length_m′ || size(parent,2) < length_m - error( - "The input `parent` matrix for ℓ=$ℓ must have size at least " - * "($length_m′,$length_m); it has size $(size(parent))." - ) - end - Hˡrow{IT, eltype(ST), ST}(parent, ℓ, m′) +function WignerdMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{Complex{RT}}} + error( + "WignerdMatrix only supports real types; the input type is Complex{$RT}.\n" + * "Perhaps you meant to use WignerDMatrix?" + ) end -function Hˡrow(::Type{NT}, ℓ::IT, m′::IT) where {NT, IT} - if real(NT) ≢ NT - error("`Hˡrow` only supports real types; the input type is $NT.") - end - length_m′ = 1 - length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 - Hˡrow{IT, NT, Matrix{NT}}(Matrix{NT}(undef, length_m′, length_m), ℓ, m′) +function WignerdMatrix(::Type{RT}, ℓ::IT, m′ₘₐₓ::IT=ℓ; kwargs...) where {RT<:Real, IT} + parent = Matrix{RT}(undef, Int(m′ₘₐₓ - (-m′ₘₐₓ) + 1), Int(2ℓ + 1)) + WignerMatrix(parent, ℓ; m′ₘₐₓ=m′ₘₐₓ, m′ₘᵢₙ=-m′ₘₐₓ, kwargs...) end -m′ₘᵢₙ(w::Hˡrow) = m′ₘₐₓ(w) -mₘₐₓ(w::Hˡrow) = ℓ(w) -mₘᵢₙ(w::Hˡrow) = ℓₘᵢₙ(w) + +### NOTE!!! The following is old, and should be subsumed into the WignerMatrix type + +### Specialize to D and d matrices + +# """ +# WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + +# Specialized subtype of [`AbstractWignerMatrix`](@ref) for D-matrices, which are complex matrices. +# """ +# struct WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} +# parent::ST +# ℓ::IT +# m′ₘₐₓ::IT +# function WignerDMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} +# # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use +# # a restriction on NT in the type declaration. +# if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT +# error( +# "WignerDMatrix only supports complex types; the input type is $NT.\n" +# * "Perhaps you meant to use WignerdMatrix?" +# ) +# end +# if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) +# error( +# "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" +# * "if ℓ is an integer its type must be <:Integer, not <:Rational." +# ) +# end +# s₁, s₂ = size(parent) +# if s₂ ≠ Int(2ℓ + 1) +# error( +# "The extent of the second dimension in the input data must be " +# * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." +# ) +# end +# if s₁ == 0 || s₁ > s₂ +# error( +# "The extent of the first dimension in the input data must be greater than 0" +# * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." +# ) +# end +# if IT <: Rational +# if isodd(s₁) +# error( +# "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " +# * "input data ($s₁) corresponds to whole-integer values of m′." +# ) +# end +# else +# if iseven(s₁) +# error( +# "ℓ=$ℓ is an integer, but the extent of the first dimension in the " +# * "input data ($s₁) corresponds to half-integer values of m′." +# ) +# end +# end +# m′ₘₐₓ = IT((s₁ - 1) // 2) +# new(parent, ℓ, m′ₘₐₓ) +# end +# end + +# """ +# WignerDMatrix(parent, ℓ) + +# Construct a `WignerDMatrix` object from the given parent matrix and ``ℓ`` value. Note that +# the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the +# denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent +# matrix must have the correct size: the first dimension must be greater than 0 and less than +# or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. +# """ +# function WignerDMatrix(parent::ST, ℓ::IT) where {IT, ST} +# WignerDMatrix{IT, eltype(ST), ST}(parent, ℓ) +# end +# function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} +# if complex(NT) ≢ NT +# error( +# "`WignerDMatrix` only supports complex types; the input type is $NT.\n" +# * "Perhaps you meant to use `WignerdMatrix`?" +# ) +# end +# WignerDMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) +# end + + + +# """ +# WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + +# Specialized subtype of [`AbstractWignerMatrix`](@ref) for d-matrices, which are real matrices. +# """ +# struct WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} +# parent::ST +# ℓ::IT +# m′ₘₐₓ::IT +# function WignerdMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} +# # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use +# # a restriction on NT in the type declaration. +# if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT +# error( +# "WignerdMatrix only supports real types; the input type is $NT.\n" +# * "Perhaps you meant to use WignerDMatrix?" +# ) +# end +# if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) +# error( +# "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" +# * "if ℓ is an integer its type must be <:Integer, not <:Rational." +# ) +# end +# s₁, s₂ = size(parent) +# if s₂ ≠ Int(2ℓ + 1) +# error( +# "The extent of the second dimension in the input data must be " +# * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." +# ) +# end +# if s₁ == 0 || s₁ > s₂ +# error( +# "The extent of the first dimension in the input data must be greater than 0" +# * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." +# ) +# end +# if IT <: Rational +# if isodd(s₁) +# error( +# "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " +# * "input data ($s₁) corresponds to whole-integer values of m′." +# ) +# end +# else +# if iseven(s₁) +# error( +# "ℓ=$ℓ is an integer, but the extent of the first dimension in the " +# * "input data ($s₁) corresponds to half-integer values of m′." +# ) +# end +# end +# m′ₘₐₓ = IT((s₁ - 1) // 2) +# new(parent, ℓ, m′ₘₐₓ) +# end +# end + +# """ +# WignerdMatrix(parent, ℓ) + +# Construct a `WignerdMatrix` object from the given parent matrix and ``ℓ`` value. Note that +# the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the +# denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent +# matrix must have the correct size: the first dimension must be greater than 0 and less than +# or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. +# """ +# function WignerdMatrix(parent::ST, ℓ::IT) where {IT, ST} +# WignerdMatrix{IT, eltype(ST), ST}(parent, ℓ) +# end +# function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} +# if real(NT) ≢ NT +# error( +# "`WignerdMatrix` only supports real types; the input type is $NT.\n" +# * "Perhaps you meant to use `WignerDMatrix`?" +# ) +# end +# WignerdMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) +# end + +# """ +# Hˡrow{IT, NT, ST} + +# Specialized subtype of [`AbstractWignerMatrix`](@ref) intended to store one row of the ``H`` matrix +# — usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the +# recurrence relations. +# """ +# struct Hˡrow{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} +# parent::ST +# ℓ::IT +# m′ₘₐₓ::IT +# end +# function Hˡrow(parent::ST, ℓ::IT, m′::IT) where {IT, ST} +# length_m′ = 1 +# length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 +# if size(parent,1) < length_m′ || size(parent,2) < length_m +# error( +# "The input `parent` matrix for ℓ=$ℓ must have size at least " +# * "($length_m′,$length_m); it has size $(size(parent))." +# ) +# end +# Hˡrow{IT, eltype(ST), ST}(parent, ℓ, m′) +# end +# function Hˡrow(::Type{NT}, ℓ::IT, m′::IT) where {NT, IT} +# if real(NT) ≢ NT +# error("`Hˡrow` only supports real types; the input type is $NT.") +# end +# length_m′ = 1 +# length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 +# Hˡrow{IT, NT, Matrix{NT}}(Matrix{NT}(undef, length_m′, length_m), ℓ, m′) +# end + +# m′ₘᵢₙ(w::Hˡrow) = m′ₘₐₓ(w) +# mₘₐₓ(w::Hˡrow) = ℓ(w) +# mₘᵢₙ(w::Hˡrow) = ℓₘᵢₙ(w) + @testitem "WignerMatrix" begin From d7311eea64a54bc252a231ecaeaf3738ec426bf0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 23:03:04 -0400 Subject: [PATCH 235/329] Better error messages --- src/redesign/WignerMatrix.jl | 78 ++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 5642b39d..4e4063d4 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -125,7 +125,8 @@ function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢ ) error( "For IT=$IT <: Rational, indices must have denominator 2:\n" - * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ, mₘᵢₙ=$mₘᵢₙ, mₘₐₓ=$mₘₐₓ." + * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ, mₘᵢₙ=$mₘᵢₙ, mₘₐₓ=$mₘₐₓ.\n" + * "If you want an integer index type, use IT=<:Integer instead." ) end end @@ -136,16 +137,16 @@ function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢ # The m′ and m values must bracket ℓₘᵢₙ if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) - error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type.") + error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type, $IT.") end if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) - error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type.") + error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type, $IT.") end if mₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) - error("mₘᵢₙ=$mₘᵢₙ is too large for this index type.") + error("mₘᵢₙ=$mₘᵢₙ is too large for this index type, $IT.") end if mₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) - error("mₘₐₓ=$mₘₐₓ is too small for this index type.") + error("mₘₐₓ=$mₘₐₓ is too small for this index type, $IT.") end # The m′ and m values must be in range for ℓₘₐₓ @@ -476,32 +477,39 @@ end @test_throws "WignerdMatrix only supports real types" WignerdMatrix(rand(ComplexF64, 2, 2), 1//2) # Check that a negative ℓ value throws an error - @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 3, 3), -1) - @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 3, 3), -1) - @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 2, 2), -1//2) - @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 2, 2), -1//2) + @test_throws "ℓₘₐₓ=-1 must be non-negative." WignerDMatrix(rand(ComplexF64, 3, 3), -1) + @test_throws "ℓₘₐₓ=-1 must be non-negative." WignerdMatrix(rand(Float64, 3, 3), -1) + @test_throws "ℓₘₐₓ=-1//2 must be non-negative." WignerDMatrix(rand(ComplexF64, 2, 2), -1//2) + @test_throws "ℓₘₐₓ=-1//2 must be non-negative." WignerdMatrix(rand(Float64, 2, 2), -1//2) # Check that a non-half-integer ℓ value throws an error - @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 3, 3), 1//3) - @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 3, 3), 1//3) - @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 2, 2), 1//3) - @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 2, 2), 1//3) - @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 3, 3), 2//2) - @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 3, 3), 2//2) - @test_throws "should be non-negative integer or half-integer." WignerDMatrix(rand(ComplexF64, 2, 2), 2//2) - @test_throws "should be non-negative integer or half-integer." WignerdMatrix(rand(Float64, 2, 2), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 3, 3), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 3, 3), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 2, 2), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 2, 2), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 3, 3), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 3, 3), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 2, 2), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 2, 2), 2//2) #for ℓ ∈ Any[collect(0:8); collect(1//2:15//2)] - for ℓ ∈ Any[collect(0:2); collect(1//2:3//2)] + ℓₘₐₓ = 2 + encode(ℓ, m′, m) = (ℓ+ℓₘₐₓ) + (m′+ℓₘₐₓ)*(4ℓₘₐₓ+1) + (m+ℓₘₐₓ)*(4ℓₘₐₓ+1)^2 + for ℓ ∈ Any[collect(0:ℓₘₐₓ); collect(1//2:(ℓₘₐₓ+1//2))] mₘ = ℓ - # Check that ℓ < m′ₘₐₓ and ℓ ≠ mₘₐₓ throw errors - @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) - @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) - @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) - @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) - @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) - @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + # These tests are old; the input array can be larger than necessary now. + # # Check that ℓ < m′ₘₐₓ and ℓ ≠ mₘₐₓ throw errors + # @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) + # @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) + # @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) + # @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) + + # # Check that the input is at least as big as needed for the given ℓ + @test_throws r"The extent of the first dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) + @test_throws r"The extent of the first dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) # Check that a mismatch between integer/half-integer throws an error if ℓ>0 && ℓ isa Int @@ -511,24 +519,24 @@ end @test_throws "is a half-integer, but the extent of the first dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ), Int(2ℓ+1)), ℓ) @test_throws "is a half-integer, but the extent of the first dimension" WignerdMatrix(rand(Float64, Int(2ℓ), Int(2ℓ+1)), ℓ) end - @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(rand(ComplexF64, Int(2ℓ+1), Int(2ℓ)), ℓ) - @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(rand(Float64, Int(2ℓ+1), Int(2ℓ)), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(rand(ComplexF64, Int(2ℓ+1), Int(2ℓ)), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerdMatrix(rand(Float64, Int(2ℓ+1), Int(2ℓ)), ℓ) # Check that a data array with a dimension of 0 extent throws an error. - @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, 0), ℓ) - @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, 0, Int(2ℓ)+1), ℓ) - @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, 0), ℓ) - @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, 0, Int(2ℓ)+1), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, 0), ℓ) + @test_throws r"The extent of the first dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, 0, Int(2ℓ)+1), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, 0), ℓ) + @test_throws r"The extent of the first dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, 0, Int(2ℓ)+1), ℓ) for m′ₘ ∈ ℓₘᵢₙ(ℓ):ℓ # Make a big, dumb array full of the explicit indices. data = [ - (ℓ, m′, m) + encode(ℓ, m′, m) for m′ ∈ -m′ₘ:m′ₘ, m ∈ -mₘ:mₘ ] # Check that indexing works as expected. - for WignerMatrixType ∈ (WignerDMatrix, WignerdMatrix) - w = WignerMatrixType(data, ℓ) + for (WignerMatrixType, NT) ∈ ((WignerDMatrix, ComplexF64), (WignerdMatrix, Float64)) + w = WignerMatrixType(NT.(data), ℓ) @test Base.parent(w) == data @test ell(w) == ℓ @test mpmax(w) == m′ₘ @@ -537,7 +545,7 @@ end @test mmin(w) == -mmax(w) for m ∈ -mₘ:mₘ for m′ ∈ -m′ₘ:m′ₘ - @test w[m′, m] == (ℓ, m′, m) + @test w[m′, m] == encode(ℓ, m′, m) end end end From 9de36c7133873bb1a23d46032d102af72b4f7b1a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 21 Oct 2025 23:03:50 -0400 Subject: [PATCH 236/329] Remove explicit WignerDMatrix, WignerdMatrix, and HRow types; all are superseded by WignerMatrix --- src/redesign/WignerMatrix.jl | 202 ----------------------------------- 1 file changed, 202 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 4e4063d4..fb902399 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -264,208 +264,6 @@ function WignerdMatrix(::Type{RT}, ℓ::IT, m′ₘₐₓ::IT=ℓ; kwargs...) wh end -### NOTE!!! The following is old, and should be subsumed into the WignerMatrix type - -### Specialize to D and d matrices - -# """ -# WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} - -# Specialized subtype of [`AbstractWignerMatrix`](@ref) for D-matrices, which are complex matrices. -# """ -# struct WignerDMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} -# parent::ST -# ℓ::IT -# m′ₘₐₓ::IT -# function WignerDMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} -# # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use -# # a restriction on NT in the type declaration. -# if !(NT <: NTuple{3, IT}) && complex(NT) ≢ NT -# error( -# "WignerDMatrix only supports complex types; the input type is $NT.\n" -# * "Perhaps you meant to use WignerdMatrix?" -# ) -# end -# if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) -# error( -# "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" -# * "if ℓ is an integer its type must be <:Integer, not <:Rational." -# ) -# end -# s₁, s₂ = size(parent) -# if s₂ ≠ Int(2ℓ + 1) -# error( -# "The extent of the second dimension in the input data must be " -# * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." -# ) -# end -# if s₁ == 0 || s₁ > s₂ -# error( -# "The extent of the first dimension in the input data must be greater than 0" -# * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." -# ) -# end -# if IT <: Rational -# if isodd(s₁) -# error( -# "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " -# * "input data ($s₁) corresponds to whole-integer values of m′." -# ) -# end -# else -# if iseven(s₁) -# error( -# "ℓ=$ℓ is an integer, but the extent of the first dimension in the " -# * "input data ($s₁) corresponds to half-integer values of m′." -# ) -# end -# end -# m′ₘₐₓ = IT((s₁ - 1) // 2) -# new(parent, ℓ, m′ₘₐₓ) -# end -# end - -# """ -# WignerDMatrix(parent, ℓ) - -# Construct a `WignerDMatrix` object from the given parent matrix and ``ℓ`` value. Note that -# the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the -# denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent -# matrix must have the correct size: the first dimension must be greater than 0 and less than -# or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. -# """ -# function WignerDMatrix(parent::ST, ℓ::IT) where {IT, ST} -# WignerDMatrix{IT, eltype(ST), ST}(parent, ℓ) -# end -# function WignerDMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} -# if complex(NT) ≢ NT -# error( -# "`WignerDMatrix` only supports complex types; the input type is $NT.\n" -# * "Perhaps you meant to use `WignerdMatrix`?" -# ) -# end -# WignerDMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) -# end - - - -# """ -# WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} - -# Specialized subtype of [`AbstractWignerMatrix`](@ref) for d-matrices, which are real matrices. -# """ -# struct WignerdMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} -# parent::ST -# ℓ::IT -# m′ₘₐₓ::IT -# function WignerdMatrix{IT, NT, ST}(parent::ST, ℓ::IT) where {IT, NT, ST} -# # We want to secretly allow NTuple{3, IT} for testing purposes, so we can't just use -# # a restriction on NT in the type declaration. -# if !(NT <: NTuple{3, IT}) && real(NT) ≢ NT -# error( -# "WignerdMatrix only supports real types; the input type is $NT.\n" -# * "Perhaps you meant to use WignerDMatrix?" -# ) -# end -# if ℓ < 0 || (IT <: Rational && denominator(ℓ) ≠ 2) -# error( -# "ℓ=$ℓ should be non-negative integer or half-integer. In particular,\n" -# * "if ℓ is an integer its type must be <:Integer, not <:Rational." -# ) -# end -# s₁, s₂ = size(parent) -# if s₂ ≠ Int(2ℓ + 1) -# error( -# "The extent of the second dimension in the input data must be " -# * "2ℓ+1=$(Int(2ℓ+1)); it is $s₂." -# ) -# end -# if s₁ == 0 || s₁ > s₂ -# error( -# "The extent of the first dimension in the input data must be greater than 0" -# * " and less than or equal to 2ℓ+1=$(Int(2ℓ+1)); it is $s₁." -# ) -# end -# if IT <: Rational -# if isodd(s₁) -# error( -# "ℓ=$ℓ is a half-integer, but the extent of the first dimension in the " -# * "input data ($s₁) corresponds to whole-integer values of m′." -# ) -# end -# else -# if iseven(s₁) -# error( -# "ℓ=$ℓ is an integer, but the extent of the first dimension in the " -# * "input data ($s₁) corresponds to half-integer values of m′." -# ) -# end -# end -# m′ₘₐₓ = IT((s₁ - 1) // 2) -# new(parent, ℓ, m′ₘₐₓ) -# end -# end - -# """ -# WignerdMatrix(parent, ℓ) - -# Construct a `WignerdMatrix` object from the given parent matrix and ``ℓ`` value. Note that -# the type of `ℓ` *must* be either `Integer` or `Rational`. If it is `Rational`, the -# denominator *must* be 2; if it is 1, you must convert to an `Int` first. Also, the parent -# matrix must have the correct size: the first dimension must be greater than 0 and less than -# or equal to `2ℓ+1`, and the second dimension must be equal to `2ℓ+1`. -# """ -# function WignerdMatrix(parent::ST, ℓ::IT) where {IT, ST} -# WignerdMatrix{IT, eltype(ST), ST}(parent, ℓ) -# end -# function WignerdMatrix(::Type{NT}, ℓ::IT, m′::IT=ℓ) where {NT, IT} -# if real(NT) ≢ NT -# error( -# "`WignerdMatrix` only supports real types; the input type is $NT.\n" -# * "Perhaps you meant to use `WignerDMatrix`?" -# ) -# end -# WignerdMatrix{IT, NT, Matrix{NT}}(Matrix{NT}(undef, Int(2m′)+1, Int(2ℓ)+1), ℓ) -# end - -# """ -# Hˡrow{IT, NT, ST} - -# Specialized subtype of [`AbstractWignerMatrix`](@ref) intended to store one row of the ``H`` matrix -# — usually the ``H^{\ell-1}_{0,m}`` or ``H^{\ell+1}_{0,m}`` components needed during the -# recurrence relations. -# """ -# struct Hˡrow{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} -# parent::ST -# ℓ::IT -# m′ₘₐₓ::IT -# end -# function Hˡrow(parent::ST, ℓ::IT, m′::IT) where {IT, ST} -# length_m′ = 1 -# length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 -# if size(parent,1) < length_m′ || size(parent,2) < length_m -# error( -# "The input `parent` matrix for ℓ=$ℓ must have size at least " -# * "($length_m′,$length_m); it has size $(size(parent))." -# ) -# end -# Hˡrow{IT, eltype(ST), ST}(parent, ℓ, m′) -# end -# function Hˡrow(::Type{NT}, ℓ::IT, m′::IT) where {NT, IT} -# if real(NT) ≢ NT -# error("`Hˡrow` only supports real types; the input type is $NT.") -# end -# length_m′ = 1 -# length_m = Int(ℓ - ℓₘᵢₙ(ℓ)) + 1 -# Hˡrow{IT, NT, Matrix{NT}}(Matrix{NT}(undef, length_m′, length_m), ℓ, m′) -# end - -# m′ₘᵢₙ(w::Hˡrow) = m′ₘₐₓ(w) -# mₘₐₓ(w::Hˡrow) = ℓ(w) -# mₘᵢₙ(w::Hˡrow) = ℓₘᵢₙ(w) - - - @testitem "WignerMatrix" begin import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, parent, ell, mpmax, mpmin, mmax, mmin, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, ℓₘᵢₙ From 301021bbb2c5c24d3a0bc5d83c3cfd6ac92f74b3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 22 Oct 2025 00:10:19 -0400 Subject: [PATCH 237/329] Bring back WignerRange --- src/redesign/WignerMatrix.jl | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index fb902399..e2a29474 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -77,10 +77,43 @@ isrational(::AbstractWignerMatrix{IT}) where {IT<:Rational} = true Base.eltype(::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT Base.size(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) Base.length(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) +# function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} +# ((m′ₘᵢₙ(w):m′ₘₐₓ(w)), (mₘᵢₙ(w):mₘₐₓ(w))) +# end + +struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} + start::T + stop::T + + WignerRange(r::UnitRange{T}) where {T} = new{T}(r.start, r.stop) +end +@inline Base.axes(r::WignerRange) = (axes1(r),) +@inline axes1(r::WignerRange) = WignerRange(r.start:r.stop) +if VERSION < v"1.8.2" + Base.axes1(r::WignerRange) = axes1(r) +end +Base.inds2string(inds::NTuple{2, WignerRange}) = + string( + "(", inds[1].start, ":", inds[1].stop, ")", + "×", + "(", inds[2].start, ":", inds[2].stop, ")" + ) +Base.firstindex(r::WignerRange) = 1 +Base.lastindex(r::WignerRange) = length(r) +function Base.getindex(v::WignerRange, i::Bool) + throw(ArgumentError("invalid index: $i of type Bool")) +end +@propagate_inbounds function Base.getindex(v::WignerRange{T}, i::Integer) where {T} + val = convert(T, v.start + (i - oneunit(i))) + @boundscheck (i>0 && val <= v.stop && val >= v.start) || throw(BoundsError(v, i)) + val +end + function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} - ((m′ₘᵢₙ(w):m′ₘₐₓ(w)), (mₘᵢₙ(w):mₘₐₓ(w))) + (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) end + # We don't have to override Base.show; most of its machinery works just fine, except that # printing the data itself gets screwed up when the indices are Rational. So we override # this core part of the printing machinery to just print the parent matrix as usual. The From b3c984db7701b0f74f84c0da5691b3f08a49aa78 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 22 Oct 2025 00:10:53 -0400 Subject: [PATCH 238/329] More explanation in error messages --- src/redesign/WignerMatrix.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index e2a29474..3a6f559f 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -239,13 +239,13 @@ function WignerMatrix( if s₁ < Int(m′ₘₐₓ - m′ₘᵢₙ + 1) error( "The extent of the first dimension in the input data must be at least " - * "m′ₘₐₓ-m′ₘᵢₙ+1=$(Int(m′ₘₐₓ - m′ₘᵢₙ + 1)); it is $s₁." + * "m′ₘₐₓ-m′ₘᵢₙ+1=$m′ₘₐₓ-$m′ₘᵢₙ+1=$(Int(m′ₘₐₓ - m′ₘᵢₙ + 1)); it is $s₁." ) end if s₂ < Int(mₘₐₓ - mₘᵢₙ + 1) error( "The extent of the second dimension in the input data must be at least " - * "mₘₐₓ-mₘᵢₙ+1=$(Int(mₘₐₓ - mₘᵢₙ + 1)); it is $s₂." + * "mₘₐₓ-mₘᵢₙ+1=$mₘₐₓ-$mₘᵢₙ+1=$(Int(mₘₐₓ - mₘᵢₙ + 1)); it is $s₂." ) end WignerMatrix{IT, NT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) From b23b137aa9c1a6309598d79f93515d7889c5bca8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 22 Oct 2025 00:11:19 -0400 Subject: [PATCH 239/329] Sync up error messages and tests --- src/redesign/WignerMatrix.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 3a6f559f..1b1e6e54 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -337,21 +337,21 @@ end # @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) # # Check that the input is at least as big as needed for the given ℓ - @test_throws r"The extent of the first dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) - @test_throws r"The extent of the first dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) - @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) - @test_throws r"The extent of the second dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + @test_throws "The extent of the first dimension" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) + @test_throws "The extent of the first dimension" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) + @test_throws "The extent of the second dimension" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + @test_throws "The extent of the second dimension" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) # Check that a mismatch between integer/half-integer throws an error if ℓ>0 && ℓ isa Int - @test_throws "is an integer, but the extent of the first dimension" WignerDMatrix(rand(ComplexF64, 2ℓ, 2ℓ+1), ℓ) - @test_throws "is an integer, but the extent of the first dimension" WignerdMatrix(rand(Float64, 2ℓ, 2ℓ+1), ℓ) + @test_throws "The extent of the first dimension" WignerDMatrix(rand(ComplexF64, 2ℓ, 2ℓ+1), ℓ) + @test_throws "The extent of the first dimension" WignerdMatrix(rand(Float64, 2ℓ, 2ℓ+1), ℓ) elseif ℓ isa Rational - @test_throws "is a half-integer, but the extent of the first dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ), Int(2ℓ+1)), ℓ) - @test_throws "is a half-integer, but the extent of the first dimension" WignerdMatrix(rand(Float64, Int(2ℓ), Int(2ℓ+1)), ℓ) + @test_throws "The extent of the first dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ), Int(2ℓ+1)), ℓ) + @test_throws "The extent of the first dimension" WignerdMatrix(rand(Float64, Int(2ℓ), Int(2ℓ+1)), ℓ) end - @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(rand(ComplexF64, Int(2ℓ+1), Int(2ℓ)), ℓ) - @test_throws r"The extent of the second dimension.*; it is 0." WignerdMatrix(rand(Float64, Int(2ℓ+1), Int(2ℓ)), ℓ) + @test_throws "The extent of the second dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ+1), Int(2ℓ)), ℓ) + @test_throws "The extent of the second dimension" WignerdMatrix(rand(Float64, Int(2ℓ+1), Int(2ℓ)), ℓ) # Check that a data array with a dimension of 0 extent throws an error. @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, 0), ℓ) From 59c07005657c4ce1baca81d486249ae9e769404a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 22 Oct 2025 00:12:01 -0400 Subject: [PATCH 240/329] Explicitly pass limits for incomplete Wigner matrices --- src/redesign/WignerMatrix.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 1b1e6e54..8df14cff 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -367,7 +367,7 @@ end ] # Check that indexing works as expected. for (WignerMatrixType, NT) ∈ ((WignerDMatrix, ComplexF64), (WignerdMatrix, Float64)) - w = WignerMatrixType(NT.(data), ℓ) + w = WignerMatrixType(NT.(data), ℓ; m′ₘₐₓ=m′ₘ, m′ₘᵢₙ=-m′ₘ, mₘₐₓ=mₘ, mₘᵢₙ=-mₘ) @test Base.parent(w) == data @test ell(w) == ℓ @test mpmax(w) == m′ₘ @@ -388,7 +388,7 @@ end WignerMatrixType<:WignerDMatrix ? ComplexF64 : Float64, Int(2m′ₘ)+1, Int(2mₘ)+1 ) - w = WignerMatrixType(data, ℓ) + w = WignerMatrixType(data, ℓ; m′ₘₐₓ=m′ₘ, m′ₘᵢₙ=-m′ₘ, mₘₐₓ=mₘ, mₘᵢₙ=-mₘ) # Check that the data array is stored correctly. @test Base.parent(w) == data From 5000fa0fe76ee328ebd02c9dc6253800f793aa5f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 22 Oct 2025 11:40:17 -0400 Subject: [PATCH 241/329] =?UTF-8?q?Validate=20order=20of=20m=E2=80=B2=20an?= =?UTF-8?q?d=20m=20ranges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/redesign/WignerMatrix.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 8df14cff..4c32ecd3 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -164,10 +164,19 @@ function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢ end end + # ℓₘₐₓ must be at least as big as ℓₘᵢₙ(ℓₘₐₓ) if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") end + # The m′ and m ranges must be ordered correctly + if m′ₘₐₓ < m′ₘᵢₙ + error("m′ₘₐₓ=$m′ₘₐₓ is less than m′ₘᵢₙ=$m′ₘᵢₙ.") + end + if mₘₐₓ < mₘᵢₙ + error("mₘₐₓ=$mₘₐₓ is less than mₘᵢₙ=$mₘᵢₙ.") + end + # The m′ and m values must bracket ℓₘᵢₙ if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type, $IT.") From f83468a5d142a5df501450ae1c07e1f1b2ea5b10 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 23 Oct 2025 12:06:32 -0400 Subject: [PATCH 242/329] Initial HWedge redesign --- Project.toml | 5 +- src/redesign/SphericalFunctions.jl | 1 + src/redesign/WignerMatrix.jl | 262 +++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1f30fc11..a0cbf188 100644 --- a/Project.toml +++ b/Project.toml @@ -1,5 +1,6 @@ name = "SphericalFunctions" uuid = "af6d55de-b1f7-4743-b797-0829a72cf84e" +version = "2.2.7" authors = ["Michael Boyle "] version = "2.2.8" @@ -8,6 +9,7 @@ AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FastTransforms = "057dd010-8810-581a-b7be-e3fc3b93f78c" +FixedSizeArrays = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" @@ -26,7 +28,8 @@ Coverage = "1.6" DoubleFloats = "1" FFTW = "1" FastDifferentiation = "0.3.17" -FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.17" +FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17" +FixedSizeArrays = "1.2.0" ForwardDiff = "0.10" Hwloc = "2, 3" LinearAlgebra = "1" diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index 26b04e9d..f12307aa 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -2,6 +2,7 @@ module Redesign import Quaternionic import TestItems: @testitem, @testsnippet +import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault # TEMPORARY!!!! import SphericalFunctions: ComplexPowers diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 4c32ecd3..cee36a82 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -207,6 +207,49 @@ function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢ end +function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where + {IT<:Union{Signed, Rational}} + if IT <: Rational + if ( + denominator(ℓₘₐₓ) ≠ 2 || + denominator(m′ₘᵢₙ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 + ) + error( + "For IT=$IT <: Rational, indices must have denominator 2:\n" + * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ.\n" + * "If you want an integer index type, use IT=<:Integer instead." + ) + end + end + + # ℓₘₐₓ must be at least as big as ℓₘᵢₙ(ℓₘₐₓ) + if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") + end + + # The m′ range must be ordered correctly + if m′ₘₐₓ < m′ₘᵢₙ + error("m′ₘₐₓ=$m′ₘₐₓ is less than m′ₘᵢₙ=$m′ₘᵢₙ.") + end + + # The m′ values must bracket ℓₘᵢₙ + if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type, $IT.") + end + if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type, $IT.") + end + + # The m′ values must be in range for ℓₘₐₓ + if abs(m′ₘᵢₙ) > ℓₘₐₓ + error("|m′ₘᵢₙ|=|$m′ₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(m′ₘₐₓ) > ℓₘₐₓ + error("|m′ₘₐₓ|=|$m′ₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + +end + """ WignerMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} @@ -418,3 +461,222 @@ end end end end + + +""" + HWedge{IT, RT, ST} <: AbstractWignerMatrix{IT, RT, ST} + +The ``Hˡ`` matrix is critical to efficient and stable computation of the Wigner ``D`` and +``d`` matrices — in fact, it essentially *is* the ``d`` matrix with signs adjusted to avoid +numerical problems with alternating signs. This gives it additional symmetries that reduce +the amount of data that needs to be stored to about 1/4 of the total ``d`` size. + +The purpose of an `HWedge` is to provide a workspace for the Wigner recurrences that is +efficient, both in terms of the size of memory used, and the implications for vectorization +and threading. Specifically, the data is stored as strictly `Real` values, in contiguous +storage. Indexing is performed efficiently via precomputed row offsets. Once the full +recurrence is done, the data can be used directly — computing phases and symmetry on the fly +— or copied into a full explicit matrix with the appropriate phases. + +The recurrences require ``m`` in the full range from 0 (or 1/2) to ``ℓ``, but ``m'`` only +needs to include the axis ``m'=0`` or 1/2. Thus, we store `m′ₘᵢₙ`and `m′ₘₐₓ` as fields, and +only require enough storage for those ranges. Specifically, an `HWedge` will store elements +in a vector as if they were components of the `Hˡ` matrix: + + [ + Hˡ[m′, m] + for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) + for m ∈ abs(m′):ℓ + ] + +However, for further efficiency when vectorizing and threading over multiple rotations, the +data is stored as a 2-dimensional array, with the first dimension indexing different +rotations, and the second dimension storing the vectorized `Hˡ` data as described above. + +""" +struct HWedge{IT, RT<:Real, ST<:DenseArray{RT}} <: AbstractWignerMatrix{IT, RT, ST} + parent::ST + row_index::FixedSizeVectorDefault{Int} + ℓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT +end + +function HWedge( + parent::ST, ℓ::IT; + mp_max::IT=ℓ, mp_min::IT=-ℓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min +) where {IT<:Union{Integer,Rational}, RT<:Real, ST<:DenseArray{RT}} + validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ) + expected_size = Hwedge_size(ℓ, m′ₘₐₓ, m′ₘᵢₙ) + if ndims(parent) ≠ 2 + error( + "Input must be a 2-dimensional array; it has $(ndims(parent)) dimensions.\n" + * "The first dimension is used to vectorize/thread over rotations, and can " + * "just have extent 1.\n" + * "The second must have length at least $(expected_size) for the input values " + * "ℓ=$ℓ, m′ₘₐₓ=$m′ₘₐₓ, and m′ₘᵢₙ=$m′ₘᵢₙ." + ) + end + s = size(parent, 2) + if s < expected_size + error( + "The length of the input data must be at least " + * "(m′ₘₐₓ-m′ₘᵢₙ+1)*(ℓₘₐₓ-ℓₘᵢₙ+1)=" + * "($m′ₘₐₓ-$m′ₘᵢₙ+1)*($ℓₘₐₓ-$(ℓₘᵢₙ(ℓₘₐₓ))+1)=" + * "$(expected_size); it is $s." + ) + end + row_index = HWedge_row_index_array(ℓ, m′ₘₐₓ, m′ₘᵢₙ) + HWedge{IT, RT, ST}(parent, row_index, ℓ, m′ₘₐₓ, m′ₘᵢₙ) +end + +function HWedge( + ::Type{RT}, Nᵣ::Int, ℓ::IT; + mp_max::IT=ℓ, mp_min::IT=-ℓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min +) where {IT<:Union{Integer,Rational}, RT<:Real} + validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ) + if Nᵣ < 1 + error("Number of rotors Nᵣ=$Nᵣ must be at least 1.") + end + parent = FixedSizeArrayDefault{RT}(undef, Nᵣ, HWedge_size(ℓ, m′ₘₐₓ, m′ₘᵢₙ)) + row_index = HWedge_row_index_array(ℓ, m′ₘₐₓ, m′ₘᵢₙ) + HWedge{IT, RT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, row_index) +end + +mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) + +function HWedge_row_index_array(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} + m′range = m′ₘᵢₙ:m′ₘₐₓ + row_index = FixedSizeVectorDefault{Int}(undef, length(m′range)) + index = 1 + for (i, m′) ∈ enumerate(m′range) + row_index[i] = index + index += Int(ℓ - abs(m′)) + 1 + end + row_index +end + +function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} + let ℓₘᵢₙ = ℓₘᵢₙ(IT) + Int( + (ℓₘᵢₙ - m′ₘᵢₙ) * (2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) + - (ℓₘᵢₙ - m′ₘₐₓ - 1) * (2ℓ - m′ₘₐₓ - ℓₘᵢₙ + 2) + ) ÷ 2 + end +end + +function HWedge_storage(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=ℓₘₐₓ) where {RT<:Real, IT} + FixedSizeArrayDefault{RT}( + undef, + Nᵣ, + HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + ) +end + +""" + +An Hˡ wedge will store elements in a vector as if it were the following matrix: + + [ + H[ℓ, m′, m] + for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) + for m ∈ abs(m′):ℓ + ] + +Here, m′ₘᵢₙ is a negative number and m′ₘₐₓ is a positive number. Note that for HWedge, we +currently impose mₘₐₓ = ℓ and mₘᵢₙ = ℓₘᵢₙ(IT), because these are all needed for the +recurrence relations. + +This function returns the linear index into that vector that belongs to the first element +with the given `m′` value (and therefore `m=abs(m′)`). The formula for that index involves +an `if` statement to account for the varying number of `m` values for each `m′` value. +Nonetheless, it can be computed in closed form (i.e., without an explicit sum or loop). + + +""" + + + +function row_index(w::HWedge{IT}, m′::IT) where {IT} + let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), ℓₘᵢₙ = ℓₘᵢₙ(IT) + ( + Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) + - + Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) + ) ÷ 2 + 1 + + # i = if m′<1 + # Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 # size of wedge to the left of m' + # else + # ( + # # size of entire left half of wedge + # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) + # + + # # size of right half of wedge to the left of m' + # Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) + # ) ÷ 2 + # end + # i + 1 + end +end + + +# function row_index(ℓ::IT, m′::IT) where {IT} +# let ℓₘᵢₙ = ℓₘᵢₙ(IT) +# i = if m′<ℓₘᵢₙ +# # size of wedge above m′ +# Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 +# else +# ( +# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ +# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) +# + +# # size of wedge at or below m′=ℓₘᵢₙ but above m′ +# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) +# ) ÷ 2 +# end +# i + 1 +# end +# end + +# function row_index(ℓ::IT, m′::IT, m′ₘᵢₙ::IT) where {IT} +# let ℓₘᵢₙ = ℓₘᵢₙ(IT) +# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ +# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) + +# i = if m′<ℓₘᵢₙ +# ( +# zero_index +# + +# # size of wedge at or below m′ but above m′=ℓₘᵢₙ +# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 +# else +# ( +# zero_index +# + +# # size of wedge at or below m′=ℓₘᵢₙ but above m′ +# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 +# end +# i + 1 +# end +# end + +# function row_index(ℓ::IT, m′::IT) where {IT} +# let ℓₘᵢₙ = ℓₘᵢₙ(IT)#, m′ₘᵢₙ = -ℓ +# # Size of upper half (m′ₘᵢₙ to ℓₘᵢₙ-1) +# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) + +# # Correction term (works for both m′ < ℓₘᵢₙ and m′ ≥ ℓₘᵢₙ) +# correction = Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) + +# # For m′ < ℓₘᵢₙ: correction is negative → subtract unwanted rows +# # For m′ ≥ ℓₘᵢₙ: correction is positive → add needed rows +# i = (zero_index + correction) ÷ 2 +# i + 1 +# end +# end From bc171fd9fd4cbbebb4710f3eb8c2123362b7774f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 23 Oct 2025 15:42:45 -0400 Subject: [PATCH 243/329] Change HWedge to explicitly include first dimension for rotors --- src/redesign/SphericalFunctions.jl | 2 +- src/redesign/WignerMatrix.jl | 209 ++++++++++++++++------------- test/hwedge.jl | 76 +++++++++++ 3 files changed, 191 insertions(+), 96 deletions(-) create mode 100644 test/hwedge.jl diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index f12307aa..61feb98b 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -2,7 +2,7 @@ module Redesign import Quaternionic import TestItems: @testitem, @testsnippet -import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault +import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, FixedSizeVector # TEMPORARY!!!! import SphericalFunctions: ComplexPowers diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index cee36a82..6a949a1b 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -490,71 +490,97 @@ in a vector as if they were components of the `Hˡ` matrix: ] However, for further efficiency when vectorizing and threading over multiple rotations, the -data is stored as a 2-dimensional array, with the first dimension indexing different -rotations, and the second dimension storing the vectorized `Hˡ` data as described above. +data is stored as a 1-dimensional vector, though it can be indexed as if it were a +three-dimensional array, with the first dimension indexing `Nᵣ` different rotations, and the +second dimension indexing `m′`, and the third dimension indexing `m`. Thus, this object can +be indexed as `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value for rotor index `iᵣ`, and matrix element +`(m′, m)`. + +Because of this complicated layout, the constructor is fairly restrictive, but will do all +the allocation needed. To avoid multiple allocations, it is advisable to first construct an +instance with the maximum `ℓ` value that will be needed, and then change the `ℓ` field as +needed to compute different orders. That is, if `H isa HWedge`, then `H.ℓ = new_ell` can be +used to change the current order being computed. The constructor starts out with the +smallest `ℓ` value possible (0 or 1/2), which is the natural choice for recurrence. + +!!! warning "Thread safety" + + The `HWedge` object is not thread safe. Its internal storage is intended to be changed + by different threads, but the code must be designed carefully to avoid accessing the + same memory locations from different threads. In particular, note that changing the `ℓ` + field changes internal storage, and is *not* thread-safe. It may be better to allocate + separate `HWedge` objects for each thread. """ -struct HWedge{IT, RT<:Real, ST<:DenseArray{RT}} <: AbstractWignerMatrix{IT, RT, ST} - parent::ST - row_index::FixedSizeVectorDefault{Int} +mutable struct HWedge{IT, RT<:Real, ST} <: AbstractWignerMatrix{IT, RT, ST} + const parent::ST + const row_index::FixedSizeVectorDefault{Int} + const Nᵣ::Int + const maxℓ::IT + const maxm′ₘₐₓ::IT + const minm′ₘᵢₙ::IT ℓ::IT m′ₘₐₓ::IT m′ₘᵢₙ::IT -end - -function HWedge( - parent::ST, ℓ::IT; - mp_max::IT=ℓ, mp_min::IT=-ℓ, - m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min -) where {IT<:Union{Integer,Rational}, RT<:Real, ST<:DenseArray{RT}} - validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ) - expected_size = Hwedge_size(ℓ, m′ₘₐₓ, m′ₘᵢₙ) - if ndims(parent) ≠ 2 - error( - "Input must be a 2-dimensional array; it has $(ndims(parent)) dimensions.\n" - * "The first dimension is used to vectorize/thread over rotations, and can " - * "just have extent 1.\n" - * "The second must have length at least $(expected_size) for the input values " - * "ℓ=$ℓ, m′ₘₐₓ=$m′ₘₐₓ, and m′ₘᵢₙ=$m′ₘᵢₙ." - ) + function HWedge(Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT} + HWedge(Float64, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) end - s = size(parent, 2) - if s < expected_size - error( - "The length of the input data must be at least " - * "(m′ₘₐₓ-m′ₘᵢₙ+1)*(ℓₘₐₓ-ℓₘᵢₙ+1)=" - * "($m′ₘₐₓ-$m′ₘᵢₙ+1)*($ℓₘₐₓ-$(ℓₘᵢₙ(ℓₘₐₓ))+1)=" - * "$(expected_size); it is $s." + function HWedge(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT, RT<:Real} + if Nᵣ < 1 + error("Number of rotors Nᵣ=$Nᵣ must be at least 1.") + end + validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + + # Set up storage for the biggest these values will ever be + parent = FixedSizeVector{RT}(undef, Nᵣ * HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ)) + row_index = FixedSizeVector{Int}(undef, Int(m′ₘₐₓ - m′ₘᵢₙ) + 1) + + # But start out assuming ℓ is the smallest it can be + maxℓ = ℓₘₐₓ + maxm′ₘₐₓ = m′ₘₐₓ + minm′ₘᵢₙ = m′ₘᵢₙ + ℓ = ℓₘᵢₙ(ℓₘₐₓ) + m′ₘₐₓ = min(ℓ, m′ₘₐₓ) + m′ₘᵢₙ = max(-ℓ, m′ₘᵢₙ) + HWedge_row_index!(row_index, Nᵣ, ℓ, m′ₘₐₓ, m′ₘᵢₙ) + + new{IT, RT, typeof(parent)}( + parent, row_index, Nᵣ, maxℓ, maxm′ₘₐₓ, minm′ₘᵢₙ, ℓ, m′ₘₐₓ, m′ₘᵢₙ ) end - row_index = HWedge_row_index_array(ℓ, m′ₘₐₓ, m′ₘᵢₙ) - HWedge{IT, RT, ST}(parent, row_index, ℓ, m′ₘₐₓ, m′ₘᵢₙ) -end - -function HWedge( - ::Type{RT}, Nᵣ::Int, ℓ::IT; - mp_max::IT=ℓ, mp_min::IT=-ℓ, - m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min -) where {IT<:Union{Integer,Rational}, RT<:Real} - validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ) - if Nᵣ < 1 - error("Number of rotors Nᵣ=$Nᵣ must be at least 1.") - end - parent = FixedSizeArrayDefault{RT}(undef, Nᵣ, HWedge_size(ℓ, m′ₘₐₓ, m′ₘᵢₙ)) - row_index = HWedge_row_index_array(ℓ, m′ₘₐₓ, m′ₘᵢₙ) - HWedge{IT, RT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, row_index) end mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) -function HWedge_row_index_array(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} - m′range = m′ₘᵢₙ:m′ₘₐₓ - row_index = FixedSizeVectorDefault{Int}(undef, length(m′range)) +function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + if IIT !== IT + error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") + end + if ℓ < ℓₘᵢₙ(IT) + error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") + end + if ℓ > H.maxℓ + error("Cannot set ℓ=$ℓ greater than maxℓ=$(H.maxℓ).") + end + m′ₘₐₓ = min(ℓ, H.maxm′ₘₐₓ) + m′ₘᵢₙ = max(-ℓ, H.minm′ₘᵢₙ) + HWedge_row_index!(H.row_index, H.Nᵣ, ℓ, m′ₘₐₓ, m′ₘᵢₙ) + Base.setfield!(H, :ℓ, ℓ) + Base.setfield!(H, :m′ₘₐₓ, m′ₘₐₓ) + Base.setfield!(H, :m′ₘᵢₙ, m′ₘᵢₙ) + ℓ + else + error("Cannot set property `$s` on HWedge; only `ℓ` is allowed to be changed.") + end +end + +function HWedge_row_index!(row_index, Nᵣ::Int, ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} index = 1 - for (i, m′) ∈ enumerate(m′range) - row_index[i] = index - index += Int(ℓ - abs(m′)) + 1 + for (i, m′) ∈ enumerate(m′ₘᵢₙ:m′ₘₐₓ) + @inbounds row_index[i] = index + index += Nᵣ * (Int(ℓ - abs(m′)) + 1) end row_index end @@ -568,60 +594,53 @@ function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} end end -function HWedge_storage(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=ℓₘₐₓ) where {RT<:Real, IT} - FixedSizeArrayDefault{RT}( - undef, - Nᵣ, - HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) - ) -end -""" +# """ -An Hˡ wedge will store elements in a vector as if it were the following matrix: +# An Hˡ wedge will store elements in a vector as if it were the following matrix: - [ - H[ℓ, m′, m] - for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) - for m ∈ abs(m′):ℓ - ] +# [ +# H[ℓ, m′, m] +# for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) +# for m ∈ abs(m′):ℓ +# ] -Here, m′ₘᵢₙ is a negative number and m′ₘₐₓ is a positive number. Note that for HWedge, we -currently impose mₘₐₓ = ℓ and mₘᵢₙ = ℓₘᵢₙ(IT), because these are all needed for the -recurrence relations. +# Here, m′ₘᵢₙ is a negative number and m′ₘₐₓ is a positive number. Note that for HWedge, we +# currently impose mₘₐₓ = ℓ and mₘᵢₙ = ℓₘᵢₙ(IT), because these are all needed for the +# recurrence relations. -This function returns the linear index into that vector that belongs to the first element -with the given `m′` value (and therefore `m=abs(m′)`). The formula for that index involves -an `if` statement to account for the varying number of `m` values for each `m′` value. -Nonetheless, it can be computed in closed form (i.e., without an explicit sum or loop). +# This function returns the linear index into that vector that belongs to the first element +# with the given `m′` value (and therefore `m=abs(m′)`). The formula for that index involves +# an `if` statement to account for the varying number of `m` values for each `m′` value. +# Nonetheless, it can be computed in closed form (i.e., without an explicit sum or loop). -""" +# """ -function row_index(w::HWedge{IT}, m′::IT) where {IT} - let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), ℓₘᵢₙ = ℓₘᵢₙ(IT) - ( - Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) - - - Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) - ) ÷ 2 + 1 - - # i = if m′<1 - # Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 # size of wedge to the left of m' - # else - # ( - # # size of entire left half of wedge - # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) - # + - # # size of right half of wedge to the left of m' - # Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) - # ) ÷ 2 - # end - # i + 1 - end -end +# function row_index(w::HWedge{IT}, m′::IT) where {IT} +# let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), ℓₘᵢₙ = ℓₘᵢₙ(IT) +# ( +# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) +# - +# Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 + 1 + +# # i = if m′<1 +# # Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 # size of wedge to the left of m' +# # else +# # ( +# # # size of entire left half of wedge +# # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) +# # + +# # # size of right half of wedge to the left of m' +# # Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) +# # ) ÷ 2 +# # end +# # i + 1 +# end +# end # function row_index(ℓ::IT, m′::IT) where {IT} diff --git a/test/hwedge.jl b/test/hwedge.jl new file mode 100644 index 00000000..8ef00f7d --- /dev/null +++ b/test/hwedge.jl @@ -0,0 +1,76 @@ +@testitem "HWedge" begin + using SphericalFunctions.Redesign: HWedge, HWedge_size, ℓₘᵢₙ + + ## First, I just need a way of storing the various index combinations + ## in a single integer — so that it can go into an HWedge, for example — for testing purposes. + encoder(i::Int) = i + encoder(i::Rational) = numerator(i) + function encoder(iᵣ, m′, m) + ( + encoder(abs(m)) + + (m < 0) * 10 + + encoder(abs(m′)) * 10^2 + + (m′ < 0) * 10^3 + + iᵣ * 10^4 + ) + end + function decoder(i) + d = digits(i) + m = d[1] * (d[2] == 1 ? -1 : 1) + m′ = d[3] * (d[4] == 1 ? -1 : 1) + iᵣ = d[5] + iᵣ, m′, m + end + # Test encoder/decoder + for ℓₘₐₓ ∈ (5, 7//2) + for iᵣ ∈ 1:7 + for m′ ∈ -ℓₘₐₓ:ℓₘₐₓ + for m ∈ -ℓₘₐₓ:ℓₘₐₓ + code = encoder(iᵣ, m′, m) + (iᵣ₂, m′₂, m₂) = decoder(code) + @test iᵣ == iᵣ₂ + if ℓₘₐₓ isa Int + @test m′ == m′₂ + @test m == m₂ + else + @test m′ == m′₂ // 2 + @test m == m₂ // 2 + end + end + end + end + end + + for ℓₘₐₓ ∈ (5, 9//2) + for Nᵣ ∈ (1, 2, 3, 7) + for m′ₘₐₓ ∈ ℓₘₐₓ:-1:ℓₘᵢₙ(ℓₘₐₓ) + for m′ₘᵢₙ in -ℓₘₐₓ:-ℓₘᵢₙ(ℓₘₐₓ) + RT = Float64 + H = HWedge(RT, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + + @test H.Nᵣ == Nᵣ + @test H.maxℓ == ℓₘₐₓ + @test H.maxm′ₘₐₓ == m′ₘₐₓ + @test H.minm′ₘᵢₙ == m′ₘᵢₙ + + # When first created, the indices should have their smallest values + @test H.ℓ == ℓₘᵢₙ(ℓₘₐₓ) + @test H.m′ₘₐₓ == H.ℓ + @test H.m′ₘᵢₙ == -H.ℓ + + # But the storage should be full size + expected_size = HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + @test size(H.parent) == (Nᵣ * expected_size, ) + + # Test changing ℓ + for new_ell in (ℓₘᵢₙ(ℓₘₐₓ):ℓₘₐₓ) + H.ℓ = new_ell + @test H.ℓ == new_ell + @test H.m′ₘₐₓ == min(new_ell, m′ₘₐₓ) + @test H.m′ₘᵢₙ == max(-new_ell, m′ₘᵢₙ) + end + end + end + end + end +end From 2578fbfba24feac8d1a0678af5a649488f843c65 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 12:18:36 -0400 Subject: [PATCH 244/329] Add utility for testing: encode set of integers as single integer --- test/hwedge.jl | 43 ++------------------------------- test/utilities/encoder.jl | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 test/utilities/encoder.jl diff --git a/test/hwedge.jl b/test/hwedge.jl index 8ef00f7d..74e54e3c 100644 --- a/test/hwedge.jl +++ b/test/hwedge.jl @@ -1,45 +1,6 @@ -@testitem "HWedge" begin +@testitem "HWedge" setup=[EncodeDecode] begin using SphericalFunctions.Redesign: HWedge, HWedge_size, ℓₘᵢₙ - - ## First, I just need a way of storing the various index combinations - ## in a single integer — so that it can go into an HWedge, for example — for testing purposes. - encoder(i::Int) = i - encoder(i::Rational) = numerator(i) - function encoder(iᵣ, m′, m) - ( - encoder(abs(m)) - + (m < 0) * 10 - + encoder(abs(m′)) * 10^2 - + (m′ < 0) * 10^3 - + iᵣ * 10^4 - ) - end - function decoder(i) - d = digits(i) - m = d[1] * (d[2] == 1 ? -1 : 1) - m′ = d[3] * (d[4] == 1 ? -1 : 1) - iᵣ = d[5] - iᵣ, m′, m - end - # Test encoder/decoder - for ℓₘₐₓ ∈ (5, 7//2) - for iᵣ ∈ 1:7 - for m′ ∈ -ℓₘₐₓ:ℓₘₐₓ - for m ∈ -ℓₘₐₓ:ℓₘₐₓ - code = encoder(iᵣ, m′, m) - (iᵣ₂, m′₂, m₂) = decoder(code) - @test iᵣ == iᵣ₂ - if ℓₘₐₓ isa Int - @test m′ == m′₂ - @test m == m₂ - else - @test m′ == m′₂ // 2 - @test m == m₂ // 2 - end - end - end - end - end + using .EncodeDecode: encode, decode for ℓₘₐₓ ∈ (5, 9//2) for Nᵣ ∈ (1, 2, 3, 7) diff --git a/test/utilities/encoder.jl b/test/utilities/encoder.jl new file mode 100644 index 00000000..e04d7e32 --- /dev/null +++ b/test/utilities/encoder.jl @@ -0,0 +1,51 @@ +""" +This module provides functions for encoding a (small) set of (small) integers into a single +integer, and decoding them back again. This is useful for storing those integers in data +structures that only accept single integers or floats, such as HWedge. +""" +@testmodule EncodeDecode begin + encode(i::Int) = i + encode(i::Rational) = numerator(i) + function encode(i...) + output = 0 + for (j, v) in enumerate(i) + if abs(v) >= 10 + error("Can only encode integers with absolute value less than 10") + end + output += (v < 0) * 10^(2j-2) + output += encode(abs(v)) * 10^(2j-1) + end + output + end + + function decode(i; pad=2) + d = digits(i; pad) + output = Int[] + for k in 1:(length(d) ÷ 2) + v = d[2k] * (d[2k-1] == 1 ? -1 : 1) + push!(output, v) + end + return Tuple(output) + end +end + +@testitem "EncodeDecode" setup=[EncodeDecode] begin + using .EncodeDecode: encode, decode + for ℓₘₐₓ ∈ (5, 7//2) + for iᵣ ∈ 1:7 + for m′ ∈ -ℓₘₐₓ:ℓₘₐₓ + for m ∈ -ℓₘₐₓ:ℓₘₐₓ + code = encode(iᵣ, m′, m) + (iᵣ₂, m′₂, m₂) = decode(code; pad=6) + @test iᵣ == iᵣ₂ + if ℓₘₐₓ isa Int + @test m′ == m′₂ + @test m == m₂ + else + @test m′ == m′₂ // 2 + @test m == m₂ // 2 + end + end + end + end +end From e080b605da68b16f22530654d40ab2da3c003485 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 14:55:37 -0400 Subject: [PATCH 245/329] Make EncodeDecode utility a little more robust --- test/utilities/encoder.jl | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/test/utilities/encoder.jl b/test/utilities/encoder.jl index e04d7e32..9e3e6bd4 100644 --- a/test/utilities/encoder.jl +++ b/test/utilities/encoder.jl @@ -4,16 +4,19 @@ integer, and decoding them back again. This is useful for storing those integer structures that only accept single integers or floats, such as HWedge. """ @testmodule EncodeDecode begin - encode(i::Int) = i - encode(i::Rational) = numerator(i) + encode(i::Int) = i + 50 + encode(i::Rational) = numerator(i) + 50 function encode(i...) output = 0 + max_integers = log10(typemax(output)) ÷ 2 + if length(i) > max_integers + error("Can only encode up to $max_integers integers into a single integer") + end for (j, v) in enumerate(i) - if abs(v) >= 10 - error("Can only encode integers with absolute value less than 10") + if !(-50 ≤ v ≤ 49) + error("Can only encode integers in -50:49; got $v") end - output += (v < 0) * 10^(2j-2) - output += encode(abs(v)) * 10^(2j-1) + output += encode(v) * 10^(2j-2) end output end @@ -21,8 +24,8 @@ structures that only accept single integers or floats, such as HWedge. function decode(i; pad=2) d = digits(i; pad) output = Int[] - for k in 1:(length(d) ÷ 2) - v = d[2k] * (d[2k-1] == 1 ? -1 : 1) + for k in 1:2:length(d) + v = (d[k] + 10d[k+1]) - 50 push!(output, v) end return Tuple(output) @@ -32,18 +35,19 @@ end @testitem "EncodeDecode" setup=[EncodeDecode] begin using .EncodeDecode: encode, decode for ℓₘₐₓ ∈ (5, 7//2) - for iᵣ ∈ 1:7 - for m′ ∈ -ℓₘₐₓ:ℓₘₐₓ - for m ∈ -ℓₘₐₓ:ℓₘₐₓ - code = encode(iᵣ, m′, m) - (iᵣ₂, m′₂, m₂) = decode(code; pad=6) - @test iᵣ == iᵣ₂ - if ℓₘₐₓ isa Int - @test m′ == m′₂ - @test m == m₂ - else - @test m′ == m′₂ // 2 - @test m == m₂ // 2 + for iᵣ ∈ 1:7 + for m′ ∈ -ℓₘₐₓ:ℓₘₐₓ + for m ∈ -ℓₘₐₓ:ℓₘₐₓ + code = encode(iᵣ, m′, m) + (iᵣ₂, m′₂, m₂) = decode(code; pad=6) + @test iᵣ == iᵣ₂ + if ℓₘₐₓ isa Int + @test m′ == m′₂ + @test m == m₂ + else + @test m′ == m′₂ // 2 + @test m == m₂ // 2 + end end end end From 74358ea5d32dd4f2af57156b4c14e6d69bf21cf1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 15:08:07 -0400 Subject: [PATCH 246/329] Convert to Int before decoding --- test/utilities/encoder.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/utilities/encoder.jl b/test/utilities/encoder.jl index 9e3e6bd4..e504940d 100644 --- a/test/utilities/encoder.jl +++ b/test/utilities/encoder.jl @@ -21,7 +21,8 @@ structures that only accept single integers or floats, such as HWedge. output end - function decode(i; pad=2) + decode(i; pad=2) = decode(Int(i); pad) + function decode(i::Int; pad=2) d = digits(i; pad) output = Int[] for k in 1:2:length(d) From 2c09bb4e79f6b1bff170b09de5256eac9975e381 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 15:09:44 -0400 Subject: [PATCH 247/329] Test indexing of HWedge --- src/redesign/WignerMatrix.jl | 52 +++++++++++++++++++++++++--- test/hwedge.jl | 67 ++++++++++++++++++++++++++++++++++-- 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 6a949a1b..87efd9e8 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -553,6 +553,12 @@ end mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) +row_index(w::HWedge{IT}) where {IT} = w.row_index +Nᵣ(w::HWedge{IT}) where {IT} = w.Nᵣ +maxℓ(w::HWedge{IT}) where {IT} = w.maxℓ +maxm′ₘₐₓ(w::HWedge{IT}) where {IT} = w.maxm′ₘₐₓ +minm′ₘᵢₙ(w::HWedge{IT}) where {IT} = w.minm′ₘᵢₙ + function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} if s === :ℓ if IIT !== IT @@ -561,12 +567,12 @@ function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} if ℓ < ℓₘᵢₙ(IT) error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") end - if ℓ > H.maxℓ - error("Cannot set ℓ=$ℓ greater than maxℓ=$(H.maxℓ).") + if ℓ > maxℓ(H) + error("Cannot set ℓ=$ℓ greater than maxℓ=$(maxℓ(H)).") end - m′ₘₐₓ = min(ℓ, H.maxm′ₘₐₓ) - m′ₘᵢₙ = max(-ℓ, H.minm′ₘᵢₙ) - HWedge_row_index!(H.row_index, H.Nᵣ, ℓ, m′ₘₐₓ, m′ₘᵢₙ) + m′ₘₐₓ = min(ℓ, maxm′ₘₐₓ(H)) + m′ₘᵢₙ = max(-ℓ, minm′ₘᵢₙ(H)) + HWedge_row_index!(row_index(H), Nᵣ(H), ℓ, m′ₘₐₓ, m′ₘᵢₙ) Base.setfield!(H, :ℓ, ℓ) Base.setfield!(H, :m′ₘₐₓ, m′ₘₐₓ) Base.setfield!(H, :m′ₘᵢₙ, m′ₘᵢₙ) @@ -594,6 +600,42 @@ function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} end end +function Base.checkbounds(::Type{Bool}, w::HWedge, i::Int) + i ≥ 1 && i ≤ length(w) +end +function Base.checkbounds(::Type{Bool}, w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + iᵣ > 0 && iᵣ ≤ Nᵣ(w) && m ≥ abs(m′) && m′ ≥ m′ₘᵢₙ(w) && m′ ≤ m′ₘₐₓ(w) +end + +@propagate_inbounds function Base.getindex(w::HWedge, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] + @inbounds Base.parent(w)[i] +end + +@propagate_inbounds function Base.setindex!(w::HWedge, v, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HWedge{IT}, v, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] + @inbounds Base.parent(w)[i] = v +end + + # """ diff --git a/test/hwedge.jl b/test/hwedge.jl index 74e54e3c..8d15acc3 100644 --- a/test/hwedge.jl +++ b/test/hwedge.jl @@ -1,10 +1,69 @@ @testitem "HWedge" setup=[EncodeDecode] begin - using SphericalFunctions.Redesign: HWedge, HWedge_size, ℓₘᵢₙ + using SphericalFunctions.Redesign: HWedge, HWedge_size, Nᵣ, ℓ, ℓₘᵢₙ, m′ₘᵢₙ, m′ₘₐₓ using .EncodeDecode: encode, decode + # We will fill the HWedge with integers that encode their indices. By iterating over + # valid indices in order, we can test that the storage layout is correct. Specifically, + # we want an inner loop over iᵣ, then m, then m′ because this is the order in which the + # recurrence relations will fill the data, vectorizing over iᵣ, then iterating most + # quickly over m. We will check that we can fill that data both using 1D indexing and + # 3D indexing, then verify that both methods give the same result. We will also test + # both methods for reading the data back out, which will verify that the indexing logic + # is correct in both setindex! and getindex. + function fill_1index!(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + i = 1 + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + w[i] = encode(iᵣ, m′, m) + i += 1 + end + end + end + end + return w + end + function fill_3index!(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + w[iᵣ, m′, m] = encode(iᵣ, m′, m) + end + end + end + end + return w + end + function test_1index(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + i = 1 + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(w[i]) == (iᵣ, numerator(m′), numerator(m)) + i += 1 + end + end + end + end + end + function test_3index(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(w[iᵣ, m′, m]) == (iᵣ, numerator(m′), numerator(m)) + end + end + end + end + end + for ℓₘₐₓ ∈ (5, 9//2) for Nᵣ ∈ (1, 2, 3, 7) - for m′ₘₐₓ ∈ ℓₘₐₓ:-1:ℓₘᵢₙ(ℓₘₐₓ) + for m′ₘₐₓ ∈ ℓₘᵢₙ(ℓₘₐₓ):ℓₘₐₓ for m′ₘᵢₙ in -ℓₘₐₓ:-ℓₘᵢₙ(ℓₘₐₓ) RT = Float64 H = HWedge(RT, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) @@ -29,6 +88,10 @@ @test H.ℓ == new_ell @test H.m′ₘₐₓ == min(new_ell, m′ₘₐₓ) @test H.m′ₘᵢₙ == max(-new_ell, m′ₘᵢₙ) + fill_1index!(H) + test_3index(H) + fill_3index!(H) + test_1index(H) end end end From 60e6455bc60af6f6cde0169cb492ff2e3fbfb263 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 18:19:11 -0400 Subject: [PATCH 248/329] Show HWedge nicely --- src/redesign/WignerMatrix.jl | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index 87efd9e8..ccf81c57 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -77,9 +77,6 @@ isrational(::AbstractWignerMatrix{IT}) where {IT<:Rational} = true Base.eltype(::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT Base.size(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) Base.length(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) -# function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} -# ((m′ₘᵢₙ(w):m′ₘₐₓ(w)), (mₘᵢₙ(w):mₘₐₓ(w))) -# end struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} start::T @@ -98,6 +95,14 @@ Base.inds2string(inds::NTuple{2, WignerRange}) = "×", "(", inds[2].start, ":", inds[2].stop, ")" ) +Base.inds2string(inds::Tuple{UnitRange, WignerRange, WignerRange}) = + string( + "(", inds[1].start, ":", inds[1].stop, ")", + "×", + "(", inds[2].start, ":", inds[2].stop, ")", + "×", + "(", inds[3].start, ":", inds[3].stop, ")" + ) Base.firstindex(r::WignerRange) = 1 Base.lastindex(r::WignerRange) = length(r) function Base.getindex(v::WignerRange, i::Bool) @@ -600,6 +605,10 @@ function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} end end +function Base.axes(w::HWedge{IT}) where {IT} + (1:Nᵣ(w), WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) +end + function Base.checkbounds(::Type{Bool}, w::HWedge, i::Int) i ≥ 1 && i ≤ length(w) end @@ -635,6 +644,17 @@ end @inbounds Base.parent(w)[i] = v end +function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, RT, ST} + let ℓ = ℓ(H), m′ₘᵢₙ = m′ₘᵢₙ(H), m′ₘₐₓ = m′ₘₐₓ(H), Nᵣ = Nᵣ(H) + print( + io, + "SphericalFunctions.HWedge{$IT, $RT} for ℓ=$(ℓ) with m′=$(m′ₘᵢₙ:m′ₘₐₓ), ", + "m=abs(m′):$(ℓ), and iᵣ=1:$(Nᵣ)\n", + "Stored in ", + ) + show(io, MIME("text/plain"), parent(H)) + end +end # """ From a966cbcadf04aebe2a7f21746faac9e6433166d7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 18:21:24 -0400 Subject: [PATCH 249/329] Move HWedge to separate file --- src/redesign/SphericalFunctions.jl | 1 + src/redesign/WignerH.jl | 295 +++++++++++++++++++++++++++++ src/redesign/WignerMatrix.jl | 295 ----------------------------- 3 files changed, 296 insertions(+), 295 deletions(-) create mode 100644 src/redesign/WignerH.jl diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index 61feb98b..8ceadbab 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -9,6 +9,7 @@ import SphericalFunctions: ComplexPowers include("WignerMatrix.jl") +include("WignerH.jl") #include("WignerMatrices.jl") include("recurrence.jl") include("WignerCalculator.jl") diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl new file mode 100644 index 00000000..9470d3cf --- /dev/null +++ b/src/redesign/WignerH.jl @@ -0,0 +1,295 @@ + + +""" + HWedge{IT, RT, ST} <: AbstractWignerMatrix{IT, RT, ST} + +The ``Hˡ`` matrix is critical to efficient and stable computation of the Wigner ``D`` and +``d`` matrices — in fact, it essentially *is* the ``d`` matrix with signs adjusted to avoid +numerical problems with alternating signs. This gives it additional symmetries that reduce +the amount of data that needs to be stored to about 1/4 of the total ``d`` size. + +The purpose of an `HWedge` is to provide a workspace for the Wigner recurrences that is +efficient, both in terms of the size of memory used, and the implications for vectorization +and threading. Specifically, the data is stored as strictly `Real` values, in contiguous +storage. Indexing is performed efficiently via precomputed row offsets. Once the full +recurrence is done, the data can be used directly — computing phases and symmetry on the fly +— or copied into a full explicit matrix with the appropriate phases. + +The recurrences require ``m`` in the full range from 0 (or 1/2) to ``ℓ``, but ``m'`` only +needs to include the axis ``m'=0`` or 1/2. Thus, we store `m′ₘᵢₙ`and `m′ₘₐₓ` as fields, and +only require enough storage for those ranges. Specifically, an `HWedge` will store elements +in a vector as if they were components of the `Hˡ` matrix: + + [ + Hˡ[m′, m] + for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) + for m ∈ abs(m′):ℓ + ] + +However, for further efficiency when vectorizing and threading over multiple rotations, the +data is stored as a 1-dimensional vector, though it can be indexed as if it were a +three-dimensional array, with the first dimension indexing `Nᵣ` different rotations, and the +second dimension indexing `m′`, and the third dimension indexing `m`. Thus, this object can +be indexed as `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value for rotor index `iᵣ`, and matrix element +`(m′, m)`. + +Because of this complicated layout, the constructor is fairly restrictive, but will do all +the allocation needed. To avoid multiple allocations, it is advisable to first construct an +instance with the maximum `ℓ` value that will be needed, and then change the `ℓ` field as +needed to compute different orders. That is, if `H isa HWedge`, then `H.ℓ = new_ell` can be +used to change the current order being computed. The constructor starts out with the +smallest `ℓ` value possible (0 or 1/2), which is the natural choice for recurrence. + +!!! warning "Thread safety" + + The `HWedge` object is not thread safe. Its internal storage is intended to be changed + by different threads, but the code must be designed carefully to avoid accessing the + same memory locations from different threads. In particular, note that changing the `ℓ` + field changes internal storage, and is *not* thread-safe. It may be better to allocate + separate `HWedge` objects for each thread. + +""" +mutable struct HWedge{IT, RT<:Real, ST} <: AbstractWignerMatrix{IT, RT, ST} + const parent::ST + const row_index::FixedSizeVectorDefault{Int} + const Nᵣ::Int + const maxℓ::IT + const maxm′ₘₐₓ::IT + const minm′ₘᵢₙ::IT + ℓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + function HWedge(Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT} + HWedge(Float64, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + end + function HWedge(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT, RT<:Real} + if Nᵣ < 1 + error("Number of rotors Nᵣ=$Nᵣ must be at least 1.") + end + validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + + # Set up storage for the biggest these values will ever be + parent = FixedSizeVector{RT}(undef, Nᵣ * HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ)) + row_index = FixedSizeVector{Int}(undef, Int(m′ₘₐₓ - m′ₘᵢₙ) + 1) + + # But start out assuming ℓ is the smallest it can be + maxℓ = ℓₘₐₓ + maxm′ₘₐₓ = m′ₘₐₓ + minm′ₘᵢₙ = m′ₘᵢₙ + ℓ = ℓₘᵢₙ(ℓₘₐₓ) + m′ₘₐₓ = min(ℓ, m′ₘₐₓ) + m′ₘᵢₙ = max(-ℓ, m′ₘᵢₙ) + HWedge_row_index!(row_index, Nᵣ, ℓ, m′ₘₐₓ, m′ₘᵢₙ) + + new{IT, RT, typeof(parent)}( + parent, row_index, Nᵣ, maxℓ, maxm′ₘₐₓ, minm′ₘᵢₙ, ℓ, m′ₘₐₓ, m′ₘᵢₙ + ) + end +end + +mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) + +row_index(w::HWedge{IT}) where {IT} = w.row_index +Nᵣ(w::HWedge{IT}) where {IT} = w.Nᵣ +maxℓ(w::HWedge{IT}) where {IT} = w.maxℓ +maxm′ₘₐₓ(w::HWedge{IT}) where {IT} = w.maxm′ₘₐₓ +minm′ₘᵢₙ(w::HWedge{IT}) where {IT} = w.minm′ₘᵢₙ + +function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + if IIT !== IT + error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") + end + if ℓ < ℓₘᵢₙ(IT) + error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") + end + if ℓ > maxℓ(H) + error("Cannot set ℓ=$ℓ greater than maxℓ=$(maxℓ(H)).") + end + m′ₘₐₓ = min(ℓ, maxm′ₘₐₓ(H)) + m′ₘᵢₙ = max(-ℓ, minm′ₘᵢₙ(H)) + HWedge_row_index!(row_index(H), Nᵣ(H), ℓ, m′ₘₐₓ, m′ₘᵢₙ) + Base.setfield!(H, :ℓ, ℓ) + Base.setfield!(H, :m′ₘₐₓ, m′ₘₐₓ) + Base.setfield!(H, :m′ₘᵢₙ, m′ₘᵢₙ) + ℓ + else + error("Cannot set property `$s` on HWedge; only `ℓ` is allowed to be changed.") + end +end + +function HWedge_row_index!(row_index, Nᵣ::Int, ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} + index = 1 + for (i, m′) ∈ enumerate(m′ₘᵢₙ:m′ₘₐₓ) + @inbounds row_index[i] = index + index += Nᵣ * (Int(ℓ - abs(m′)) + 1) + end + row_index +end + +function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} + let ℓₘᵢₙ = ℓₘᵢₙ(IT) + Int( + (ℓₘᵢₙ - m′ₘᵢₙ) * (2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) + - (ℓₘᵢₙ - m′ₘₐₓ - 1) * (2ℓ - m′ₘₐₓ - ℓₘᵢₙ + 2) + ) ÷ 2 + end +end + +function Base.axes(w::HWedge{IT}) where {IT} + (1:Nᵣ(w), WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) +end + +function Base.checkbounds(::Type{Bool}, w::HWedge, i::Int) + i ≥ 1 && i ≤ length(w) +end +function Base.checkbounds(::Type{Bool}, w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + iᵣ > 0 && iᵣ ≤ Nᵣ(w) && m ≥ abs(m′) && m′ ≥ m′ₘᵢₙ(w) && m′ ≤ m′ₘₐₓ(w) +end + +@propagate_inbounds function Base.getindex(w::HWedge, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] + @inbounds Base.parent(w)[i] +end + +@propagate_inbounds function Base.setindex!(w::HWedge, v, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HWedge{IT}, v, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] + @inbounds Base.parent(w)[i] = v +end + +function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, RT, ST} + let ℓ = ℓ(H), m′ₘᵢₙ = m′ₘᵢₙ(H), m′ₘₐₓ = m′ₘₐₓ(H), Nᵣ = Nᵣ(H) + print( + io, + "SphericalFunctions.HWedge{$IT, $RT} for ℓ=$(ℓ) with m′=$(m′ₘᵢₙ:m′ₘₐₓ), ", + "m=abs(m′):$(ℓ), and iᵣ=1:$(Nᵣ)\n", + "Stored in ", + ) + show(io, MIME("text/plain"), parent(H)) + end +end + + +# """ + +# An Hˡ wedge will store elements in a vector as if it were the following matrix: + +# [ +# H[ℓ, m′, m] +# for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) +# for m ∈ abs(m′):ℓ +# ] + +# Here, m′ₘᵢₙ is a negative number and m′ₘₐₓ is a positive number. Note that for HWedge, we +# currently impose mₘₐₓ = ℓ and mₘᵢₙ = ℓₘᵢₙ(IT), because these are all needed for the +# recurrence relations. + +# This function returns the linear index into that vector that belongs to the first element +# with the given `m′` value (and therefore `m=abs(m′)`). The formula for that index involves +# an `if` statement to account for the varying number of `m` values for each `m′` value. +# Nonetheless, it can be computed in closed form (i.e., without an explicit sum or loop). + + +# """ + + + +# function row_index(w::HWedge{IT}, m′::IT) where {IT} +# let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), ℓₘᵢₙ = ℓₘᵢₙ(IT) +# ( +# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) +# - +# Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 + 1 + +# # i = if m′<1 +# # Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 # size of wedge to the left of m' +# # else +# # ( +# # # size of entire left half of wedge +# # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) +# # + +# # # size of right half of wedge to the left of m' +# # Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) +# # ) ÷ 2 +# # end +# # i + 1 +# end +# end + + +# function row_index(ℓ::IT, m′::IT) where {IT} +# let ℓₘᵢₙ = ℓₘᵢₙ(IT) +# i = if m′<ℓₘᵢₙ +# # size of wedge above m′ +# Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 +# else +# ( +# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ +# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) +# + +# # size of wedge at or below m′=ℓₘᵢₙ but above m′ +# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) +# ) ÷ 2 +# end +# i + 1 +# end +# end + +# function row_index(ℓ::IT, m′::IT, m′ₘᵢₙ::IT) where {IT} +# let ℓₘᵢₙ = ℓₘᵢₙ(IT) +# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ +# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) + +# i = if m′<ℓₘᵢₙ +# ( +# zero_index +# + +# # size of wedge at or below m′ but above m′=ℓₘᵢₙ +# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 +# else +# ( +# zero_index +# + +# # size of wedge at or below m′=ℓₘᵢₙ but above m′ +# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 +# end +# i + 1 +# end +# end + +# function row_index(ℓ::IT, m′::IT) where {IT} +# let ℓₘᵢₙ = ℓₘᵢₙ(IT)#, m′ₘᵢₙ = -ℓ +# # Size of upper half (m′ₘᵢₙ to ℓₘᵢₙ-1) +# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) + +# # Correction term (works for both m′ < ℓₘᵢₙ and m′ ≥ ℓₘᵢₙ) +# correction = Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) + +# # For m′ < ℓₘᵢₙ: correction is negative → subtract unwanted rows +# # For m′ ≥ ℓₘᵢₙ: correction is positive → add needed rows +# i = (zero_index + correction) ÷ 2 +# i + 1 +# end +# end diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/WignerMatrix.jl index ccf81c57..df57f561 100644 --- a/src/redesign/WignerMatrix.jl +++ b/src/redesign/WignerMatrix.jl @@ -466,298 +466,3 @@ end end end end - - -""" - HWedge{IT, RT, ST} <: AbstractWignerMatrix{IT, RT, ST} - -The ``Hˡ`` matrix is critical to efficient and stable computation of the Wigner ``D`` and -``d`` matrices — in fact, it essentially *is* the ``d`` matrix with signs adjusted to avoid -numerical problems with alternating signs. This gives it additional symmetries that reduce -the amount of data that needs to be stored to about 1/4 of the total ``d`` size. - -The purpose of an `HWedge` is to provide a workspace for the Wigner recurrences that is -efficient, both in terms of the size of memory used, and the implications for vectorization -and threading. Specifically, the data is stored as strictly `Real` values, in contiguous -storage. Indexing is performed efficiently via precomputed row offsets. Once the full -recurrence is done, the data can be used directly — computing phases and symmetry on the fly -— or copied into a full explicit matrix with the appropriate phases. - -The recurrences require ``m`` in the full range from 0 (or 1/2) to ``ℓ``, but ``m'`` only -needs to include the axis ``m'=0`` or 1/2. Thus, we store `m′ₘᵢₙ`and `m′ₘₐₓ` as fields, and -only require enough storage for those ranges. Specifically, an `HWedge` will store elements -in a vector as if they were components of the `Hˡ` matrix: - - [ - Hˡ[m′, m] - for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) - for m ∈ abs(m′):ℓ - ] - -However, for further efficiency when vectorizing and threading over multiple rotations, the -data is stored as a 1-dimensional vector, though it can be indexed as if it were a -three-dimensional array, with the first dimension indexing `Nᵣ` different rotations, and the -second dimension indexing `m′`, and the third dimension indexing `m`. Thus, this object can -be indexed as `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value for rotor index `iᵣ`, and matrix element -`(m′, m)`. - -Because of this complicated layout, the constructor is fairly restrictive, but will do all -the allocation needed. To avoid multiple allocations, it is advisable to first construct an -instance with the maximum `ℓ` value that will be needed, and then change the `ℓ` field as -needed to compute different orders. That is, if `H isa HWedge`, then `H.ℓ = new_ell` can be -used to change the current order being computed. The constructor starts out with the -smallest `ℓ` value possible (0 or 1/2), which is the natural choice for recurrence. - -!!! warning "Thread safety" - - The `HWedge` object is not thread safe. Its internal storage is intended to be changed - by different threads, but the code must be designed carefully to avoid accessing the - same memory locations from different threads. In particular, note that changing the `ℓ` - field changes internal storage, and is *not* thread-safe. It may be better to allocate - separate `HWedge` objects for each thread. - -""" -mutable struct HWedge{IT, RT<:Real, ST} <: AbstractWignerMatrix{IT, RT, ST} - const parent::ST - const row_index::FixedSizeVectorDefault{Int} - const Nᵣ::Int - const maxℓ::IT - const maxm′ₘₐₓ::IT - const minm′ₘᵢₙ::IT - ℓ::IT - m′ₘₐₓ::IT - m′ₘᵢₙ::IT - function HWedge(Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT} - HWedge(Float64, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) - end - function HWedge(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT, RT<:Real} - if Nᵣ < 1 - error("Number of rotors Nᵣ=$Nᵣ must be at least 1.") - end - validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) - - # Set up storage for the biggest these values will ever be - parent = FixedSizeVector{RT}(undef, Nᵣ * HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ)) - row_index = FixedSizeVector{Int}(undef, Int(m′ₘₐₓ - m′ₘᵢₙ) + 1) - - # But start out assuming ℓ is the smallest it can be - maxℓ = ℓₘₐₓ - maxm′ₘₐₓ = m′ₘₐₓ - minm′ₘᵢₙ = m′ₘᵢₙ - ℓ = ℓₘᵢₙ(ℓₘₐₓ) - m′ₘₐₓ = min(ℓ, m′ₘₐₓ) - m′ₘᵢₙ = max(-ℓ, m′ₘᵢₙ) - HWedge_row_index!(row_index, Nᵣ, ℓ, m′ₘₐₓ, m′ₘᵢₙ) - - new{IT, RT, typeof(parent)}( - parent, row_index, Nᵣ, maxℓ, maxm′ₘₐₓ, minm′ₘᵢₙ, ℓ, m′ₘₐₓ, m′ₘᵢₙ - ) - end -end - -mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) -mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) - -row_index(w::HWedge{IT}) where {IT} = w.row_index -Nᵣ(w::HWedge{IT}) where {IT} = w.Nᵣ -maxℓ(w::HWedge{IT}) where {IT} = w.maxℓ -maxm′ₘₐₓ(w::HWedge{IT}) where {IT} = w.maxm′ₘₐₓ -minm′ₘᵢₙ(w::HWedge{IT}) where {IT} = w.minm′ₘᵢₙ - -function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} - if s === :ℓ - if IIT !== IT - error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") - end - if ℓ < ℓₘᵢₙ(IT) - error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") - end - if ℓ > maxℓ(H) - error("Cannot set ℓ=$ℓ greater than maxℓ=$(maxℓ(H)).") - end - m′ₘₐₓ = min(ℓ, maxm′ₘₐₓ(H)) - m′ₘᵢₙ = max(-ℓ, minm′ₘᵢₙ(H)) - HWedge_row_index!(row_index(H), Nᵣ(H), ℓ, m′ₘₐₓ, m′ₘᵢₙ) - Base.setfield!(H, :ℓ, ℓ) - Base.setfield!(H, :m′ₘₐₓ, m′ₘₐₓ) - Base.setfield!(H, :m′ₘᵢₙ, m′ₘᵢₙ) - ℓ - else - error("Cannot set property `$s` on HWedge; only `ℓ` is allowed to be changed.") - end -end - -function HWedge_row_index!(row_index, Nᵣ::Int, ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} - index = 1 - for (i, m′) ∈ enumerate(m′ₘᵢₙ:m′ₘₐₓ) - @inbounds row_index[i] = index - index += Nᵣ * (Int(ℓ - abs(m′)) + 1) - end - row_index -end - -function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} - let ℓₘᵢₙ = ℓₘᵢₙ(IT) - Int( - (ℓₘᵢₙ - m′ₘᵢₙ) * (2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) - - (ℓₘᵢₙ - m′ₘₐₓ - 1) * (2ℓ - m′ₘₐₓ - ℓₘᵢₙ + 2) - ) ÷ 2 - end -end - -function Base.axes(w::HWedge{IT}) where {IT} - (1:Nᵣ(w), WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) -end - -function Base.checkbounds(::Type{Bool}, w::HWedge, i::Int) - i ≥ 1 && i ≤ length(w) -end -function Base.checkbounds(::Type{Bool}, w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} - iᵣ > 0 && iᵣ ≤ Nᵣ(w) && m ≥ abs(m′) && m′ ≥ m′ₘᵢₙ(w) && m′ ≤ m′ₘₐₓ(w) -end - -@propagate_inbounds function Base.getindex(w::HWedge, i::Int) - @boundscheck if !checkbounds(Bool, w, i) - throw(BoundsError(w, i)) - end - @inbounds Base.parent(w)[i] -end -@propagate_inbounds function Base.getindex(w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} - @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) - throw(BoundsError(w, (iᵣ, m′, m))) - end - i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] - @inbounds Base.parent(w)[i] -end - -@propagate_inbounds function Base.setindex!(w::HWedge, v, i::Int) - @boundscheck if !checkbounds(Bool, w, i) - throw(BoundsError(w, i)) - end - @inbounds Base.parent(w)[i] = v -end -@propagate_inbounds function Base.setindex!(w::HWedge{IT}, v, iᵣ::Int, m′::IT, m::IT) where {IT} - @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) - throw(BoundsError(w, (iᵣ, m′, m))) - end - i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] - @inbounds Base.parent(w)[i] = v -end - -function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, RT, ST} - let ℓ = ℓ(H), m′ₘᵢₙ = m′ₘᵢₙ(H), m′ₘₐₓ = m′ₘₐₓ(H), Nᵣ = Nᵣ(H) - print( - io, - "SphericalFunctions.HWedge{$IT, $RT} for ℓ=$(ℓ) with m′=$(m′ₘᵢₙ:m′ₘₐₓ), ", - "m=abs(m′):$(ℓ), and iᵣ=1:$(Nᵣ)\n", - "Stored in ", - ) - show(io, MIME("text/plain"), parent(H)) - end -end - - -# """ - -# An Hˡ wedge will store elements in a vector as if it were the following matrix: - -# [ -# H[ℓ, m′, m] -# for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) -# for m ∈ abs(m′):ℓ -# ] - -# Here, m′ₘᵢₙ is a negative number and m′ₘₐₓ is a positive number. Note that for HWedge, we -# currently impose mₘₐₓ = ℓ and mₘᵢₙ = ℓₘᵢₙ(IT), because these are all needed for the -# recurrence relations. - -# This function returns the linear index into that vector that belongs to the first element -# with the given `m′` value (and therefore `m=abs(m′)`). The formula for that index involves -# an `if` statement to account for the varying number of `m` values for each `m′` value. -# Nonetheless, it can be computed in closed form (i.e., without an explicit sum or loop). - - -# """ - - - -# function row_index(w::HWedge{IT}, m′::IT) where {IT} -# let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), ℓₘᵢₙ = ℓₘᵢₙ(IT) -# ( -# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) -# - -# Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) -# ) ÷ 2 + 1 - -# # i = if m′<1 -# # Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 # size of wedge to the left of m' -# # else -# # ( -# # # size of entire left half of wedge -# # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) -# # + -# # # size of right half of wedge to the left of m' -# # Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) -# # ) ÷ 2 -# # end -# # i + 1 -# end -# end - - -# function row_index(ℓ::IT, m′::IT) where {IT} -# let ℓₘᵢₙ = ℓₘᵢₙ(IT) -# i = if m′<ℓₘᵢₙ -# # size of wedge above m′ -# Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 -# else -# ( -# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ -# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) -# + -# # size of wedge at or below m′=ℓₘᵢₙ but above m′ -# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) -# ) ÷ 2 -# end -# i + 1 -# end -# end - -# function row_index(ℓ::IT, m′::IT, m′ₘᵢₙ::IT) where {IT} -# let ℓₘᵢₙ = ℓₘᵢₙ(IT) -# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ -# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) - -# i = if m′<ℓₘᵢₙ -# ( -# zero_index -# + -# # size of wedge at or below m′ but above m′=ℓₘᵢₙ -# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) -# ) ÷ 2 -# else -# ( -# zero_index -# + -# # size of wedge at or below m′=ℓₘᵢₙ but above m′ -# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) -# ) ÷ 2 -# end -# i + 1 -# end -# end - -# function row_index(ℓ::IT, m′::IT) where {IT} -# let ℓₘᵢₙ = ℓₘᵢₙ(IT)#, m′ₘᵢₙ = -ℓ -# # Size of upper half (m′ₘᵢₙ to ℓₘᵢₙ-1) -# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) - -# # Correction term (works for both m′ < ℓₘᵢₙ and m′ ≥ ℓₘᵢₙ) -# correction = Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) - -# # For m′ < ℓₘᵢₙ: correction is negative → subtract unwanted rows -# # For m′ ≥ ℓₘᵢₙ: correction is positive → add needed rows -# i = (zero_index + correction) ÷ 2 -# i + 1 -# end -# end From 8b9855bb63d8c6eb296db073e7d73c9197853cfe Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 24 Oct 2025 18:24:51 -0400 Subject: [PATCH 250/329] Remove older explicit indexing stuff --- src/redesign/WignerH.jl | 113 +++------------------------------------- 1 file changed, 6 insertions(+), 107 deletions(-) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 9470d3cf..80399074 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -1,5 +1,3 @@ - - """ HWedge{IT, RT, ST} <: AbstractWignerMatrix{IT, RT, ST} @@ -188,108 +186,9 @@ function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, end end - -# """ - -# An Hˡ wedge will store elements in a vector as if it were the following matrix: - -# [ -# H[ℓ, m′, m] -# for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) -# for m ∈ abs(m′):ℓ -# ] - -# Here, m′ₘᵢₙ is a negative number and m′ₘₐₓ is a positive number. Note that for HWedge, we -# currently impose mₘₐₓ = ℓ and mₘᵢₙ = ℓₘᵢₙ(IT), because these are all needed for the -# recurrence relations. - -# This function returns the linear index into that vector that belongs to the first element -# with the given `m′` value (and therefore `m=abs(m′)`). The formula for that index involves -# an `if` statement to account for the varying number of `m` values for each `m′` value. -# Nonetheless, it can be computed in closed form (i.e., without an explicit sum or loop). - - -# """ - - - -# function row_index(w::HWedge{IT}, m′::IT) where {IT} -# let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), ℓₘᵢₙ = ℓₘᵢₙ(IT) -# ( -# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) -# - -# Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) -# ) ÷ 2 + 1 - -# # i = if m′<1 -# # Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 # size of wedge to the left of m' -# # else -# # ( -# # # size of entire left half of wedge -# # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) -# # + -# # # size of right half of wedge to the left of m' -# # Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) -# # ) ÷ 2 -# # end -# # i + 1 -# end -# end - - -# function row_index(ℓ::IT, m′::IT) where {IT} -# let ℓₘᵢₙ = ℓₘᵢₙ(IT) -# i = if m′<ℓₘᵢₙ -# # size of wedge above m′ -# Int(m′ - m′ₘᵢₙ) * Int(2ℓ + m′ + m′ₘᵢₙ + 1) ÷ 2 -# else -# ( -# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ -# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) -# + -# # size of wedge at or below m′=ℓₘᵢₙ but above m′ -# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) -# ) ÷ 2 -# end -# i + 1 -# end -# end - -# function row_index(ℓ::IT, m′::IT, m′ₘᵢₙ::IT) where {IT} -# let ℓₘᵢₙ = ℓₘᵢₙ(IT) -# # size of entire upper half of wedge excluding m′=ℓₘᵢₙ -# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) - -# i = if m′<ℓₘᵢₙ -# ( -# zero_index -# + -# # size of wedge at or below m′ but above m′=ℓₘᵢₙ -# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) -# ) ÷ 2 -# else -# ( -# zero_index -# + -# # size of wedge at or below m′=ℓₘᵢₙ but above m′ -# Int(m′ - ℓₘᵢₙ) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) -# ) ÷ 2 -# end -# i + 1 -# end -# end - -# function row_index(ℓ::IT, m′::IT) where {IT} -# let ℓₘᵢₙ = ℓₘᵢₙ(IT)#, m′ₘᵢₙ = -ℓ -# # Size of upper half (m′ₘᵢₙ to ℓₘᵢₙ-1) -# zero_index = Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + ℓₘᵢₙ + m′ₘᵢₙ + 1) - -# # Correction term (works for both m′ < ℓₘᵢₙ and m′ ≥ ℓₘᵢₙ) -# correction = Int(m′ - ℓₘᵢₙ) * Int(2ℓ - ℓₘᵢₙ - m′ + 3) - -# # For m′ < ℓₘᵢₙ: correction is negative → subtract unwanted rows -# # For m′ ≥ ℓₘᵢₙ: correction is positive → add needed rows -# i = (zero_index + correction) ÷ 2 -# i + 1 -# end -# end +# Explicit index formula, assuming no iᵣ: +# ( +# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) +# - +# Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 + 1 From 2017826320ce347dd18c5377014c1ae6b33f54b3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 25 Oct 2025 00:58:43 -0400 Subject: [PATCH 251/329] Add HAxis --- src/redesign/SphericalFunctions.jl | 1 + src/redesign/WignerCalculator.jl | 1 - src/redesign/WignerH.jl | 87 ++++++++++- src/redesign/WignerHCalculator.jl | 224 +++++++++++++++++++++++++++++ test/haxis.jl | 4 + 5 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/redesign/WignerHCalculator.jl create mode 100644 test/haxis.jl diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index 8ceadbab..fe7eaace 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -13,6 +13,7 @@ include("WignerH.jl") #include("WignerMatrices.jl") include("recurrence.jl") include("WignerCalculator.jl") +include("WignerHCalculator.jl") # function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/WignerCalculator.jl index a275ff33..2faeb309 100644 --- a/src/redesign/WignerCalculator.jl +++ b/src/redesign/WignerCalculator.jl @@ -1,4 +1,3 @@ - struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} ℓₘₐₓ::IT m′ₘₐₓ::IT diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 80399074..60b0f230 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -24,9 +24,9 @@ in a vector as if they were components of the `Hˡ` matrix: for m ∈ abs(m′):ℓ ] -However, for further efficiency when vectorizing and threading over multiple rotations, the +However, for further efficiency when vectorizing and threading over multiple rotors, the data is stored as a 1-dimensional vector, though it can be indexed as if it were a -three-dimensional array, with the first dimension indexing `Nᵣ` different rotations, and the +three-dimensional array, with the first dimension indexing `Nᵣ` different rotors, and the second dimension indexing `m′`, and the third dimension indexing `m`. Thus, this object can be indexed as `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value for rotor index `iᵣ`, and matrix element `(m′, m)`. @@ -192,3 +192,86 @@ end # - # Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) # ) ÷ 2 + 1 + + +""" + HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} + +The `HAxis` type represents the ``m'=0``, ``m≥0`` axis of the `Hˡ` matrix used in +calculation of the Wigner `D` and `d` matrices. + +As with [`HWedge`](@ref), the data is stored as a 1-dimensional vector, though it can be +indexed as if it were a two-dimensional array, with the first dimension indexing `Nᵣ` +different rotors, and the second dimension indexing `m` — or alternatively as if it were a +three-dimensional array with the second dimension indexing `m′` and the third indexing `m`. +Thus, this object can be indexed as `Hˡ₀[iᵣ, m]` or `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value +for rotor index `iᵣ`, and matrix element `(m′, m)`. + + +""" +struct HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} + parent::FixedSizeVectorDefault{RT} + Nᵣ::Int + ℓₘₐₓ::IT + function HAxis(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT) where {IT, RT<:Real} + H = FixedSizeVector{RT}(undef, Nᵣ * (Int(ℓₘₐₓ - ℓₘᵢₙ(IT)) + 1)) + new{IT, RT}(H, Nᵣ, ℓₘₐₓ) + end +end + +m′ₘₐₓ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) +m′ₘᵢₙ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) +mₘₐₓ(w::HAxis{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) + +function Base.checkbounds(::Type{Bool}, w::HAxis, i::Int) + i ≥ 1 && i ≤ length(w) +end +function Base.checkbounds(::Type{Bool}, w::HAxis{IT}, iᵣ::Int, m::IT) where {IT} + iᵣ ≤ Nᵣ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓₘₐₓ(w) +end +function Base.checkbounds(::Type{Bool}, w::HAxis{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + iᵣ ≤ Nᵣ(w) && m′ == ℓₘᵢₙ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓₘₐₓ(w) +end + +@propagate_inbounds function Base.getindex(w::HAxis, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HAxis{IT}, iᵣ::Int, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m) + throw(BoundsError(w, (iᵣ, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HAxis{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] +end + +@propagate_inbounds function Base.setindex!(w::HAxis, v, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HAxis{IT}, v, iᵣ::Int, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m) + throw(BoundsError(w, (iᵣ, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HAxis{IT}, v, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] = v +end diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl new file mode 100644 index 00000000..8e91156f --- /dev/null +++ b/src/redesign/WignerHCalculator.jl @@ -0,0 +1,224 @@ +struct WignerHCalculator{IT, RT<:Real, ST} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + Hᵃ::HAxis{IT, RT} + Hᵇ::HAxis{IT, RT} + Wˡ::HWedge{IT, RT, ST} + swapH::Base.RefValue{Bool} # w(ℓ) returns (Wˡ, Hᵇ, Hᵃ) if `true`, otherwise (Wˡ, Hᵃ, Hᵇ) + # function WignerHCalculator( + # ℓₘₐₓ::IT, rt::Type{RT}; + # m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ + # ) where {IT, RT} + # if real(NT) ≠ RT + # error("RT=$RT is supposed to be the real type of NT=$NT.") + # end + # validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + # # One of the H matrices will (eventually) be required to store all the coefficients + # # for Hˡ⁺¹₀ₘ with non-negative `m` (and that will be strictly necessary), so we give + # # it one extra column. Since we may not know which one that will be, we give both + # # of them that extra column. + # Hᵃp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + # Hᵇp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + # Wˡp = Matrix{NT}(undef, Int(m′ₘₐₓ-m′ₘᵢₙ)+1, Int(mₘₐₓ-mₘᵢₙ)+1) + # new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, Hᵃp, Hᵇp, Wˡp, Ref(false)) + # end +end + +ℓₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = ℓₘᵢₙ(w.ℓₘₐₓ) +ℓₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.ℓₘₐₓ +m′ₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘᵢₙ + +Hᵃ(w::WignerHCalculator) = swapH(w) ? w.Hᵇ : w.Hᵃ +Hᵇ(w::WignerHCalculator) = swapH(w) ? w.Hᵃ : w.Hᵇ +Wˡ(w::WignerHCalculator) = w.Wˡ + +function Base.fill!(w::WignerHCalculator{IT, RT}, v::Real) where {IT, RT} + let Wˡ = Wˡ(w), Hᵃ = Hᵃ(w), Hᵇ = Hᵇ(w) + fill!(Wˡ, eltype(Wˡ)(v)) + fill!(Hᵃ, eltype(Hᵃ)(v)) + fill!(Hᵇ, eltype(Hᵇ)(v)) + end + w +end + +function swapH(w::WignerHCalculator) + w.swapH[] +end + +function swapH!(w::WignerHCalculator) + w.swapH[] = !w.swapH[] + w +end + +function fillW!(w::WignerHCalculator{IT}, ℓ::IT) where {IT} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + @views copyto!(Wˡ[0:0, 0:ℓ], Hˡ[0:0, 0:ℓ]) + w +end + +function (w::WignerHCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} + if IT <: Rational + if denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end + end + if ℓ < ℓₘᵢₙ(w) || ℓ > ℓₘₐₓ(w) + error( + "ℓ=$ℓ is out of range for this WignerCalculator " * + "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(w)) and ℓₘₐₓ=$(ℓₘₐₓ(w)))." + ) + end + let ℓₘᵢₙ=ℓₘᵢₙ(w), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(w)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(w)), + mₘₐₓ=min(ℓ, mₘₐₓ(w)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(w)), + Wˡ=WignerMatrix(Wˡ(w), ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ), + Hˡ=WignerMatrix(Hᵃ(w), ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ), + Hˡ⁺¹=WignerMatrix(Hᵇ(w), ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) + + Wˡ, Hˡ, Hˡ⁺¹ + end +end + +# function WignerDCalculator( +# ℓₘₐₓ::IT, ::Type{RT}; +# mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, +# m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min +# ) where {IT, RT<:Real} +# NT = complex(RT) +# WignerHCalculator(ℓₘₐₓ, RT, NT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) +# end + +# function WignerdCalculator( +# ℓₘₐₓ::IT, ::Type{RT}; +# mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, +# m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min +# ) where {IT, RT<:Real} +# WignerHCalculator(ℓₘₐₓ, RT, RT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) +# end + +function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} + ℓ = ℓₘᵢₙ(w) + W⁰, H⁰, H¹ = w(ℓ) + recurrence_step1!(H⁰) + fillW!(w, ℓ) + w +end + +function recurrence_step2!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + w +end + +function recurrence_step3!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) + w +end + +function recurrence_step4!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step4!(Wˡ, sinβ, cosβ) + w +end + +function recurrence_step5!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step5!(Wˡ, sinβ, cosβ) + w +end + +function recurrence_step6!(w::WignerHCalculator{IT}, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + recurrence_step6!(Wˡ) + w +end + +function recurrence!( + w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Complex} + eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) + recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ, skip_ℓ_recurrence) +end + +function recurrence!( + w::WignerHCalculator{IT, RT, NT}, β::RT, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Real} + eⁱᵝ = cis(β) + recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) +end + +function recurrence!( + w::WignerHCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, + ℓ::IT, skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Complex} + _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) + Wˡ +end + +function recurrence!( + w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Real} + _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_d!(Wˡ) + Wˡ +end + +function _recurrence!( + w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT} + # NOTE: In the comments explaining the recurrence steps below, we use notation with + # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. + + if ℓ == ℓₘᵢₙ(w) + recurrence_step1!(w) # H⁰₀₀ = 1 + fillW!(w, ℓ) # Record the result in Wˡ + + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ and swap. + recurrence_step2!(w, eⁱᵝ, ℓ+1) # H⁰₀ₘ -> H¹₀ₘ + swapH!(w) + else + if !skip_ℓ_recurrence + recurrence_step1!(w) # H⁰₀₀ = 1 + + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ + recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ + swapH!(w) # Prepare for the next iteration of ℓ′ + end + end + + # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not we + # asked to skip the ℓ recurrence. If we did, the user is responsible for having + # already set Hˡ₀ₘ correctly; if we didn't, we just set it in the loop above. + let ℓ′ = ℓ+1 + recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + end + + fillW!(w, ℓ) # Copy Hˡ₀ₘ to Wˡ₀ₘ + recurrence_step3!(w, eⁱᵝ, ℓ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ + + # Impose symmetries + recurrence_step6!(w, ℓ) + + # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ + swapH!(w) + end + + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + Wˡ + +end diff --git a/test/haxis.jl b/test/haxis.jl new file mode 100644 index 00000000..256492d1 --- /dev/null +++ b/test/haxis.jl @@ -0,0 +1,4 @@ +@testitem "HAxis" setup=[EncodeDecode] begin + using SphericalFunctions.Redesign: HAxis, ℓ, ℓₘᵢₙ, m′ₘᵢₙ, m′ₘₐₓ + using .EncodeDecode: encode, decode +end From eb3b65d54c2f4ecd7e14ee46e550ecb3d3bfdd38 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 25 Oct 2025 10:53:42 -0400 Subject: [PATCH 252/329] Add adjustable ell value to HAxis --- src/redesign/WignerH.jl | 48 +++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 60b0f230..3ebcdf7b 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -209,13 +209,14 @@ for rotor index `iᵣ`, and matrix element `(m′, m)`. """ -struct HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} - parent::FixedSizeVectorDefault{RT} - Nᵣ::Int - ℓₘₐₓ::IT +mutable struct HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} + const parent::FixedSizeVectorDefault{RT} + const Nᵣ::Int + const maxℓ::IT + ℓ::IT function HAxis(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT) where {IT, RT<:Real} H = FixedSizeVector{RT}(undef, Nᵣ * (Int(ℓₘₐₓ - ℓₘᵢₙ(IT)) + 1)) - new{IT, RT}(H, Nᵣ, ℓₘₐₓ) + new{IT, RT}(H, Nᵣ, ℓₘₐₓ, ℓₘᵢₙ(IT)) end end @@ -224,14 +225,35 @@ m′ₘᵢₙ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) mₘₐₓ(w::HAxis{IT}) where {IT} = ℓ(w) mₘᵢₙ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) +Nᵣ(w::HAxis{IT}) where {IT} = w.Nᵣ +maxℓ(w::HAxis{IT}) where {IT} = w.maxℓ + +function Base.setproperty!(H::HAxis{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + if IIT !== IT + error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") + end + if ℓ < ℓₘᵢₙ(IT) + error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") + end + if ℓ > maxℓ(H) + error("Cannot set ℓ=$ℓ greater than maxℓ=$(maxℓ(H)).") + end + Base.setfield!(H, :ℓ, ℓ) + ℓ + else + error("Cannot set property `$s` on HAxis; only `ℓ` is allowed to be changed.") + end +end + function Base.checkbounds(::Type{Bool}, w::HAxis, i::Int) i ≥ 1 && i ≤ length(w) end function Base.checkbounds(::Type{Bool}, w::HAxis{IT}, iᵣ::Int, m::IT) where {IT} - iᵣ ≤ Nᵣ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓₘₐₓ(w) + iᵣ ≤ Nᵣ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓ(w) end function Base.checkbounds(::Type{Bool}, w::HAxis{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} - iᵣ ≤ Nᵣ(w) && m′ == ℓₘᵢₙ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓₘₐₓ(w) + iᵣ ≤ Nᵣ(w) && m′ == ℓₘᵢₙ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓ(w) end @propagate_inbounds function Base.getindex(w::HAxis, i::Int) @@ -275,3 +297,15 @@ end i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) @inbounds Base.parent(w)[i] = v end + +function Base.show(io::IO, ::MIME"text/plain", H::HAxis{IT, RT}) where {IT, RT} + let ℓ = ℓ(H), ℓₘᵢₙ = ℓₘᵢₙ(H), Nᵣ = Nᵣ(H) + print( + io, + "SphericalFunctions.HAxis{$IT, $RT} for ℓ=$(ℓ) with ", + "m=$(ℓₘᵢₙ):$(ℓ), and iᵣ=1:$(Nᵣ)\n", + "Stored in ", + ) + show(io, MIME("text/plain"), parent(H)) + end +end From e9a6d11cb2a55d94e615d460fe835e07d57975d7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 25 Oct 2025 10:54:01 -0400 Subject: [PATCH 253/329] Add tests for HAxis --- src/redesign/WignerH.jl | 2 +- test/haxis.jl | 144 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 3ebcdf7b..67b2faa1 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -186,7 +186,7 @@ function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, end end -# Explicit index formula, assuming no iᵣ: +# Explicit HWedge index formula, assuming no iᵣ: # ( # Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) # - diff --git a/test/haxis.jl b/test/haxis.jl index 256492d1..47e50d24 100644 --- a/test/haxis.jl +++ b/test/haxis.jl @@ -1,4 +1,146 @@ @testitem "HAxis" setup=[EncodeDecode] begin - using SphericalFunctions.Redesign: HAxis, ℓ, ℓₘᵢₙ, m′ₘᵢₙ, m′ₘₐₓ + using SphericalFunctions.Redesign: HAxis, Nᵣ, ℓ, ℓₘᵢₙ, maxℓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ using .EncodeDecode: encode, decode + + # HAxis stores only the m′=ℓₘᵢₙ axis (0 or 1/2), with m ranging from ℓₘᵢₙ to ℓₘₐₓ. + # The data layout is: [value for iᵣ ∈ 1:Nᵣ, m ∈ ℓₘᵢₙ:ℓₘₐₓ] + # We want inner loop over iᵣ, outer loop over m for vectorization. + + function fill_1index!(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + i = 1 + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + h[i] = encode(iᵣ, ℓₘᵢₙ, m) + i += 1 + end + end + end + return h + end + + function fill_2index!(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + h[iᵣ, m] = encode(iᵣ, ℓₘᵢₙ, m) + end + end + end + return h + end + + function fill_3index!(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + h[iᵣ, ℓₘᵢₙ, m] = encode(iᵣ, ℓₘᵢₙ, m) + end + end + end + return h + end + + function test_1index(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + i = 1 + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(h[i]) == (iᵣ, numerator(ℓₘᵢₙ), numerator(m)) + i += 1 + end + end + end + end + + function test_2index(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(h[iᵣ, m]) == (iᵣ, numerator(ℓₘᵢₙ), numerator(m)) + end + end + end + end + + function test_3index(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(h[iᵣ, ℓₘᵢₙ, m]) == (iᵣ, numerator(ℓₘᵢₙ), numerator(m)) + end + end + end + end + + # Test both integer and half-integer ℓ + for ℓₘₐₓ ∈ (5, 9//2) + for Nᵣ ∈ (1, 2, 3, 7) + IT = typeof(ℓₘₐₓ) + RT = Float64 + h = HAxis(RT, Nᵣ, ℓₘₐₓ) + + # Check fields + @test h.Nᵣ == Nᵣ + @test h.maxℓ == ℓₘₐₓ + + # When first created, ℓ should be at its minimum value + @test ℓ(h) == ℓₘᵢₙ(IT) + + # Check index ranges + @test m′ₘᵢₙ(h) == ℓₘᵢₙ(IT) + @test m′ₘₐₓ(h) == ℓₘᵢₙ(IT) + @test mₘᵢₙ(h) == ℓₘᵢₙ(IT) + @test mₘₐₓ(h) == ℓₘᵢₙ(IT) # mₘₐₓ should equal current ℓ, not ℓₘₐₓ + + # Check storage size (allocated for maximum ℓₘₐₓ) + expected_length = Nᵣ * (Int(ℓₘₐₓ - ℓₘᵢₙ(IT)) + 1) + @test length(h.parent) == expected_length + + # Test changing ℓ + for new_ell in (ℓₘᵢₙ(IT):ℓₘₐₓ) + h.ℓ = new_ell + @test h.ℓ == new_ell + @test ℓ(h) == new_ell + @test mₘₐₓ(h) == new_ell # mₘₐₓ should track current ℓ + + # Test all three indexing methods (1D, 2D, 3D) + fill_1index!(h) + test_2index(h) + test_3index(h) + + fill_2index!(h) + test_1index(h) + test_3index(h) + + fill_3index!(h) + test_1index(h) + test_2index(h) + + # Test bounds checking for current ℓ; check just for the string, because + # some tests will throw a `FixedSizeArrays.BoundsErrorLight` instead of a + # standard `BoundsError`. + @test_throws "BoundsError" h[0] + @test_throws "BoundsError" h[length(h.parent) + 1] + @test_throws "BoundsError" h[0, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[Nᵣ + 1, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT) - 1] + @test_throws "BoundsError" h[1, new_ell + 1] # Beyond current ℓ + + # 3D indexing: m′ must equal ℓₘᵢₙ + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT) - 1, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT) + 1, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT), ℓₘᵢₙ(IT) - 1] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT), new_ell + 1] # Beyond current ℓ + end + + # Test error conditions for changing ℓ + @test_throws "greater than maxℓ" h.ℓ = ℓₘₐₓ + 1 + @test_throws "less than ℓₘᵢₙ" h.ℓ = ℓₘᵢₙ(IT) - 1 + + # Test that we can't change other properties + @test_throws "only `ℓ` is allowed to be changed" h.Nᵣ = 10 + @test_throws "only `ℓ` is allowed to be changed" h.maxℓ = ℓₘₐₓ + 1 + end + end end From d30473439493ab764796b9ac84a8609267017575 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 25 Oct 2025 23:50:12 -0400 Subject: [PATCH 254/329] Tidy up --- src/redesign/WignerH.jl | 1 - src/redesign/WignerHCalculator.jl | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 67b2faa1..043f9dd0 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -207,7 +207,6 @@ three-dimensional array with the second dimension indexing `m′` and the third Thus, this object can be indexed as `Hˡ₀[iᵣ, m]` or `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value for rotor index `iᵣ`, and matrix element `(m′, m)`. - """ mutable struct HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} const parent::FixedSizeVectorDefault{RT} diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 8e91156f..29f5be12 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -80,23 +80,6 @@ function (w::WignerHCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} end end -# function WignerDCalculator( -# ℓₘₐₓ::IT, ::Type{RT}; -# mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, -# m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min -# ) where {IT, RT<:Real} -# NT = complex(RT) -# WignerHCalculator(ℓₘₐₓ, RT, NT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) -# end - -# function WignerdCalculator( -# ℓₘₐₓ::IT, ::Type{RT}; -# mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, -# m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min -# ) where {IT, RT<:Real} -# WignerHCalculator(ℓₘₐₓ, RT, RT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) -# end - function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} ℓ = ℓₘᵢₙ(w) W⁰, H⁰, H¹ = w(ℓ) From f4472e0c52d1d53bb20545292862d25206e2fd36 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 26 Oct 2025 00:09:19 -0400 Subject: [PATCH 255/329] Ensure that new ell is a half-integer in the Rational case --- src/redesign/WignerH.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 043f9dd0..73d160d8 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -99,6 +99,9 @@ function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} if IIT !== IT error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") end + if IT <: Rational && denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end if ℓ < ℓₘᵢₙ(IT) error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") end @@ -232,6 +235,9 @@ function Base.setproperty!(H::HAxis{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} if IIT !== IT error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") end + if IT <: Rational && denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end if ℓ < ℓₘᵢₙ(IT) error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") end From 28a2fbdcf73a60db0aca441c88bf5601e3c901b9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 26 Oct 2025 15:49:37 -0400 Subject: [PATCH 256/329] =?UTF-8?q?Rename=20fields;=20add=20e=E2=81=B1?= =?UTF-8?q?=E1=B5=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/redesign/WignerHCalculator.jl | 263 +++++++++++++++++------------- 1 file changed, 148 insertions(+), 115 deletions(-) diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 29f5be12..c8b27145 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -2,43 +2,53 @@ struct WignerHCalculator{IT, RT<:Real, ST} ℓₘₐₓ::IT m′ₘₐₓ::IT m′ₘᵢₙ::IT - Hᵃ::HAxis{IT, RT} - Hᵇ::HAxis{IT, RT} - Wˡ::HWedge{IT, RT, ST} - swapH::Base.RefValue{Bool} # w(ℓ) returns (Wˡ, Hᵇ, Hᵃ) if `true`, otherwise (Wˡ, Hᵃ, Hᵇ) - # function WignerHCalculator( - # ℓₘₐₓ::IT, rt::Type{RT}; - # m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ - # ) where {IT, RT} - # if real(NT) ≠ RT - # error("RT=$RT is supposed to be the real type of NT=$NT.") - # end - # validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) - # # One of the H matrices will (eventually) be required to store all the coefficients - # # for Hˡ⁺¹₀ₘ with non-negative `m` (and that will be strictly necessary), so we give - # # it one extra column. Since we may not know which one that will be, we give both - # # of them that extra column. - # Hᵃp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) - # Hᵇp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) - # Wˡp = Matrix{NT}(undef, Int(m′ₘₐₓ-m′ₘᵢₙ)+1, Int(mₘₐₓ-mₘᵢₙ)+1) - # new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, Hᵃp, Hᵇp, Wˡp, Ref(false)) - # end + h⃗ᵃ::HAxis{IT, RT} + h⃗ᵇ::HAxis{IT, RT} + Hˡ::HWedge{IT, RT, ST} + eⁱᵝ::FixedSizeVectorDefault{Complex{RT}} + swapH::Base.RefValue{Bool} # h⃗ˡ(w) returns h⃗ᵃ if `false`, otherwise h⃗ᵇ; and vice versa for h⃗ˡ⁺¹(w) + function WignerHCalculator( + eⁱᵝ::AbstractVector{Complex{RT}}, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ + ) where {IT, RT<:Real} + eⁱᵝ = FixedSizeVector(eⁱᵝ) + Nᵣ = length(eⁱᵝ) + # One of the H matrices will (eventually) be required to store all the coefficients + # for Hˡ⁺¹₀ₘ with non-negative `m` (and that will be strictly necessary), so we give + # it one extra column. Since we may not know which one that will be, we give both + # of them that extra column. + h⃗ᵃ = HAxis(RT, Nᵣ, ℓₘₐₓ+1) + h⃗ᵇ = HAxis(RT, Nᵣ, ℓₘₐₓ+1) + Hˡ = HWedge(RT, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, h⃗ᵃ, h⃗ᵇ, Hˡ, eⁱᵝ, Ref(false)) + end end +consistent_ℓ(w::WignerHCalculator{IT}) where {IT} = ℓ(h⃗ˡ(w)) == ℓ(Hˡ(w)) == ℓ(h⃗ˡ⁺¹(w)) - 1 + +function ℓ(w::WignerHCalculator{IT}) where {IT} + if !consistent_ℓ(w) + error( + "Inconsistent ℓ values in WignerHCalculator:\n" + * " ℓ(h⃗ˡ)=$(ℓ(h⃗ˡ(w))), ℓ(h⃗ˡ⁺¹)=$(ℓ(h⃗ˡ⁺¹(w))), ℓ(Hˡ)=$(ℓ(Hˡ(w)))." + ) + end + ℓ(Hˡ(w)) +end ℓₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = ℓₘᵢₙ(w.ℓₘₐₓ) ℓₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.ℓₘₐₓ m′ₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘₐₓ m′ₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘᵢₙ -Hᵃ(w::WignerHCalculator) = swapH(w) ? w.Hᵇ : w.Hᵃ -Hᵇ(w::WignerHCalculator) = swapH(w) ? w.Hᵃ : w.Hᵇ -Wˡ(w::WignerHCalculator) = w.Wˡ +h⃗ˡ(w::WignerHCalculator) = swapH(w) ? w.h⃗ᵇ : w.h⃗ᵃ +h⃗ˡ⁺¹(w::WignerHCalculator) = swapH(w) ? w.h⃗ᵃ : w.h⃗ᵇ +Hˡ(w::WignerHCalculator) = w.Hˡ +eⁱᵝ(w::WignerHCalculator) = w.eⁱᵝ function Base.fill!(w::WignerHCalculator{IT, RT}, v::Real) where {IT, RT} - let Wˡ = Wˡ(w), Hᵃ = Hᵃ(w), Hᵇ = Hᵇ(w) - fill!(Wˡ, eltype(Wˡ)(v)) - fill!(Hᵃ, eltype(Hᵃ)(v)) - fill!(Hᵇ, eltype(Hᵇ)(v)) + let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), Hˡ = Hˡ(w), v = convert(RT, v) + fill!(parent(h⃗ˡ), v) + fill!(parent(h⃗ˡ⁺¹), v) + fill!(parent(Hˡ), v) end w end @@ -47,161 +57,184 @@ function swapH(w::WignerHCalculator) w.swapH[] end -function swapH!(w::WignerHCalculator) +function increment_axes!(w::WignerHCalculator) + # The data that is now stored as h⃗ˡ(w) will get swapped below so that it will be + # returned by h⃗ˡ⁺¹(w), so we need to increment its ℓ value twice. + h⃗ˡ = h⃗ˡ(w) + h⃗ˡ.ℓ = ℓ(h⃗ˡ) + 2 + # The data that is now stored as h⃗ˡ⁺¹(w) will get swapped below so that it will be + # returned by h⃗ˡ(w), which will already be correct for the next ℓ value. w.swapH[] = !w.swapH[] w end -function fillW!(w::WignerHCalculator{IT}, ℓ::IT) where {IT} - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - @views copyto!(Wˡ[0:0, 0:ℓ], Hˡ[0:0, 0:ℓ]) +function increment_ℓ!(w::WignerHCalculator) + increment_axes!(w) + Hˡ = Hˡ(w) + Hˡ.ℓ = ℓ(Hˡ) + 1 w end -function (w::WignerHCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} - if IT <: Rational - if denominator(ℓ) ≠ 2 - error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") +function fillHˡ₀ₘ!(w::WignerHCalculator{IT}) where {IT} + let h⃗ˡ = h⃗ˡ(w), Hˡ = Hˡ(w) + if ℓ(h⃗ˡ) != ℓ(Hˡ) + error("Cannot fill Hˡ₀ₘ for ℓ=$(ℓ(Hˡ)) from h⃗ˡ for ℓ=$(ℓ(h⃗ˡ)).") end + # Get the index to the start of the central row, m′ = 0 or 1//2 + iˡ₀₀ = row_index(w)[Int(ℓₘᵢₙ(w) - m′ₘᵢₙ(w)) + 1] + # Figure out how many entries to copy + N = Nᵣ(Hˡ) * (Int(ℓ(w) - ℓₘᵢₙ(w)) + 1) + # Now just copy that many entries from h⃗ˡ₀ₘ into row Hˡ₀ₘ + copyto!(parent(Hˡ), iˡ₀₀, parent(h⃗ˡ), 1, N) end - if ℓ < ℓₘᵢₙ(w) || ℓ > ℓₘₐₓ(w) - error( - "ℓ=$ℓ is out of range for this WignerCalculator " * - "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(w)) and ℓₘₐₓ=$(ℓₘₐₓ(w)))." - ) - end - let ℓₘᵢₙ=ℓₘᵢₙ(w), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(w)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(w)), - mₘₐₓ=min(ℓ, mₘₐₓ(w)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(w)), - Wˡ=WignerMatrix(Wˡ(w), ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ), - Hˡ=WignerMatrix(Hᵃ(w), ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ), - Hˡ⁺¹=WignerMatrix(Hᵇ(w), ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) + w +end - Wˡ, Hˡ, Hˡ⁺¹ +function Base.setproperty!(w::WignerHCalculator{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + h⃗ˡ(w).ℓ = ℓ + h⃗ˡ⁺¹(w).ℓ = ℓ+1 + Hˡ(w).ℓ = ℓ + ℓ + else + error("Cannot set property `$s` on HWedge; only `ℓ` is allowed to be changed.") end end function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} - ℓ = ℓₘᵢₙ(w) - W⁰, H⁰, H¹ = w(ℓ) - recurrence_step1!(H⁰) - fillW!(w, ℓ) + let h⃗⁰ = h⃗ˡ(w), ℓ = ℓ(h⃗⁰) + if ℓ ≠ ℓₘᵢₙ(w) + error("recurrence_step1! can only be called for ℓ=$(ℓₘᵢₙ(w)); current ℓ=$ℓ.") + end + parent(h⃗⁰)[:, 1] .= 1 + end w end -function recurrence_step2!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} - Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) - cosβ, sinβ = reim(eⁱᵝ) - recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) +function recurrence_step2!(w::WignerHCalculator{IT}) where {IT<:Signed} + let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), ℓ = ℓ(h⃗ˡ), eⁱᵝ = eⁱᵝ(w) + if ℓ < ℓₘᵢₙ(IT) + error( + "recurrence_step2! can only be called for ℓ≥ℓₘᵢₙ=$(ℓₘᵢₙ(IT)); current ℓ=$ℓ." + ) + end + recurrence_step2!(h⃗ˡ⁺¹, h⃗ˡ, eⁱᵝ) + end w end -function recurrence_step3!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - cosβ, sinβ = reim(eⁱᵝ) - recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) +function recurrence_step3!(w::WignerHCalculator{IT}) where {IT<:Signed} + let Hˡ = Hˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), eⁱᵝ = eⁱᵝ(w) + recurrence_step3!(Hˡ, h⃗ˡ⁺¹, eⁱᵝ) + end w end -function recurrence_step4!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - cosβ, sinβ = reim(eⁱᵝ) - recurrence_step4!(Wˡ, sinβ, cosβ) +function recurrence_step4!(w::WignerHCalculator{IT}) where {IT<:Signed} + let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) + recurrence_step4!(Hˡ, eⁱᵝ) + end w end -function recurrence_step5!(w::WignerHCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - cosβ, sinβ = reim(eⁱᵝ) - recurrence_step5!(Wˡ, sinβ, cosβ) +function recurrence_step5!(w::WignerHCalculator{IT}, eⁱᵝ) where {IT<:Signed} + let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) + recurrence_step5!(Hˡ, eⁱᵝ) + end w end function recurrence_step6!(w::WignerHCalculator{IT}, ℓ) where {IT<:Signed} - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - recurrence_step6!(Wˡ) + let Hˡ = Hˡ(w) + recurrence_step6!(Hˡ) + end w end function recurrence!( - w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT, - skip_ℓ_recurrence::Bool=false + w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT ) where {IT<:Signed, RT, NT<:Complex} eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) - recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ, skip_ℓ_recurrence) + recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ) end function recurrence!( - w::WignerHCalculator{IT, RT, NT}, β::RT, ℓ::IT, - skip_ℓ_recurrence::Bool=false + w::WignerHCalculator{IT, RT, NT}, β::RT, ℓ::IT ) where {IT<:Signed, RT, NT<:Real} eⁱᵝ = cis(β) - recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + recurrence!(w, eⁱᵝ, ℓ) end function recurrence!( w::WignerHCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, - ℓ::IT, skip_ℓ_recurrence::Bool=false + ℓ::IT ) where {IT<:Signed, RT, NT<:Complex} - _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) - Wˡ + _recurrence!(w, eⁱᵝ, ℓ) + let Hˡ = Hˡ(w) + convert_H_to_D!(Hˡ, eⁱᵅ, eⁱᵞ) + Hˡ + end end function recurrence!( - w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, - skip_ℓ_recurrence::Bool=false + w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT ) where {IT<:Signed, RT, NT<:Real} - _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - convert_H_to_d!(Wˡ) - Wˡ + _recurrence!(w, eⁱᵝ, ℓ) + let Hˡ = Hˡ(w) + convert_H_to_d!(Hˡ) + Hˡ + end end function _recurrence!( - w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, - skip_ℓ_recurrence::Bool=false + w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT ) where {IT<:Signed, RT, NT} # NOTE: In the comments explaining the recurrence steps below, we use notation with # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. if ℓ == ℓₘᵢₙ(w) + w.ℓ = ℓ recurrence_step1!(w) # H⁰₀₀ = 1 - fillW!(w, ℓ) # Record the result in Wˡ + fillHˡ₀ₘ!(w) # Record the result in Hˡ (it was computed in h⃗ˡ) - # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ and swap. - recurrence_step2!(w, eⁱᵝ, ℓ+1) # H⁰₀ₘ -> H¹₀ₘ - swapH!(w) + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ. + recurrence_step2!(w, eⁱᵝ) # H⁰₀ₘ -> H¹₀ₘ else - if !skip_ℓ_recurrence + if !consistent_ℓ(w) || ℓ(w) ≠ ℓ-1 + # We need to start over, but will only be using the axes, so we only reset them. + h⃗ˡ(w).ℓ = ℓₘᵢₙ(w) + h⃗ˡ⁺¹(w).ℓ = ℓₘᵢₙ(w)+1 + recurrence_step1!(w) # H⁰₀₀ = 1 + recurrence_step2!(w, eⁱᵝ) # H⁰₀ₘ -> H¹₀ₘ - for ℓ′ in ℓₘᵢₙ(w)+1:ℓ - recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ - swapH!(w) # Prepare for the next iteration of ℓ′ + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ-1 + increment_axes!(w) + recurrence_step2!(w, eⁱᵝ) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ end end - # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not we - # asked to skip the ℓ recurrence. If we did, the user is responsible for having - # already set Hˡ₀ₘ correctly; if we didn't, we just set it in the loop above. - let ℓ′ = ℓ+1 - recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ - end - - fillW!(w, ℓ) # Copy Hˡ₀ₘ to Wˡ₀ₘ - recurrence_step3!(w, eⁱᵝ, ℓ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ - recurrence_step4!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ - recurrence_step5!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ + # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not the + # `if` block above was executed. If not, it was set in a previous call to this + # function, but is currently stored in h⃗ˡ⁺¹ so we have to swap and increment the + # axes first. If so, that block just got us to the same point. + increment_axes!(w) + recurrence_step2!(w, eⁱᵝ, ℓ) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ - # Impose symmetries - recurrence_step6!(w, ℓ) + # So far, we've only used h⃗ˡ and h⃗ˡ⁺¹. Now we're ready to start using Hˡ. + Hˡ(w).ℓ = ℓ - # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ - swapH!(w) - end + # The h⃗ˡ and h⃗ˡ⁺¹ are what we need to start the recurrence for Hˡ + fillHˡ₀ₘ!(w) # Copy h⃗ˡ₀ₘ to Hˡ₀ₘ + recurrence_step3!(w, eⁱᵝ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w, eⁱᵝ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w, eⁱᵝ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ - Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) - Wˡ + # # Impose symmetries + # recurrence_step6!(w, ℓ) + # # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ + # increment_axes!(w) + end + Hˡ(w) end From 7d4749641bcba8999af9178ae912fc51819f2253 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 26 Oct 2025 16:19:18 -0400 Subject: [PATCH 257/329] Specialize recurrences for WignerHCalculator, with @turbo --- src/redesign/WignerHCalculator.jl | 216 ++++++++++++++++++++++++------ 1 file changed, 173 insertions(+), 43 deletions(-) diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index c8b27145..2270303d 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -112,34 +112,164 @@ function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} end function recurrence_step2!(w::WignerHCalculator{IT}) where {IT<:Signed} - let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), ℓ = ℓ(h⃗ˡ), eⁱᵝ = eⁱᵝ(w) + let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w) if ℓ < ℓₘᵢₙ(IT) error( "recurrence_step2! can only be called for ℓ≥ℓₘᵢₙ=$(ℓₘᵢₙ(IT)); current ℓ=$ℓ." ) end - recurrence_step2!(h⃗ˡ⁺¹, h⃗ˡ, eⁱᵝ) + + # Note that in this step only, we use notation derived from (but not the same as) + # Xing et al., denoting the coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following + # steps, we will use notation from Gumerov and Duraiswami, who denote their + # different coefficients aₗᵐ, etc. + @inbounds let √=sqrt∘T, ℓ = ℓ(h⃗ˡ), eⁱᵝ = eⁱᵝ(w), Nᵣ = Nᵣ(h⃗ˡ) + if ℓ == 1 + # The ℓ>1 branch would try to access invalid indices of H⁰; if we treat those + # elements as zero, we can simplify that branch to just the following much + # simpler code anyway. So fundamentally, this branch is the same as the other + # branch. + @turbo for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + h⃗ˡ⁺¹[i, 0, 0] = cosβ + h⃗ˡ⁺¹[i, 0, 1] = sinβ / √2 + end + elseif ℓ > 1 + b̄ₗ = √(T(ℓ-1)/ℓ) + @turbo for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + h⃗ˡ⁺¹[i, 0, 0] = cosβ * h⃗ˡ[i, 0, 0] - b̄ₗ * sinβ * h⃗ˡ[i, 0, 1] + end + @turbo for m ∈ 1:ℓ-2 + c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ + d̄ₗₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + h⃗ˡ⁺¹[i, 0, m] = ( + c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] + - sinβ * (d̄ₗₘ * h⃗ˡ[i, 0, m+1] - ēₗₘ * h⃗ˡ[i, 0, m-1]) + ) + end + end + let m = ℓ-1 + c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + @turbo for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + h⃗ˡ⁺¹[i, 0, m] = ( + c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] + - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) + ) + end + end + let m = ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + @turbo for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + h⃗ˡ⁺¹[i, 0, m] = ( + - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) + ) + end + end + else + error("Tried to recurse with ℓ=$ℓ; only integer ℓ ≥ 1 is supported.") + end + end end w end function recurrence_step3!(w::WignerHCalculator{IT}) where {IT<:Signed} let Hˡ = Hˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), eⁱᵝ = eⁱᵝ(w) - recurrence_step3!(Hˡ, h⃗ˡ⁺¹, eⁱᵝ) + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), Nᵣ = Nᵣ(h⃗ˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + if ℓ > 0 && m′ₘₐₓ ≥ 1 + c = 1 / √(ℓ*(ℓ+1)) + @turbo for m ∈ 1:ℓ + āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) + b̄ₗ₊₁ᵐ⁻¹ = √((ℓ-m+1)*(ℓ-m+2)) + b̄ₗ₊₁⁻ᵐ⁻¹ = √((ℓ+m+1)*(ℓ+m+2)) + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + Hˡ[i, 1, m] = -c * ( + b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m+1] + + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m-1] + + āₗᵐ * sinβ * h⃗ˡ⁺¹[i, 0, m] + ) + end + end + end + end end w end function recurrence_step4!(w::WignerHCalculator{IT}) where {IT<:Signed} let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) - recurrence_step4!(Hˡ, eⁱᵝ) + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 + # Note that the signs of m′ and m are always +1, so we leave them out of the + # calculations of d̄ in this function. + d̄ₗᵐ′ = √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = √((ℓ-m′+1)*(ℓ+m′)) + @turbo for m ∈ (m′+1):ℓ-1 + d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) + d̄ₗᵐ = √((ℓ-m)*(ℓ+m+1)) + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + Hˡ[i, m′+1, m] = ( + d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] + - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + + d̄ₗᵐ * Hˡ[i, m′, m+1] + ) / d̄ₗᵐ′ + end + end + let m = ℓ + d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) + @turbo for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + Hˡ[i, m′+1, m] = ( + d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] + - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + ) / d̄ₗᵐ′ + end + end + end + end end w end -function recurrence_step5!(w::WignerHCalculator{IT}, eⁱᵝ) where {IT<:Signed} +function recurrence_step5!(w::WignerHCalculator{IT}) where {IT<:Signed} let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) - recurrence_step5!(Hˡ, eⁱᵝ) + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 + d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) + @turbo for m ∈ -(m′-1):ℓ-1 + d̄ₗᵐ = sgn(m) * √((ℓ-m)*(ℓ+m+1)) + d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + Hˡ[i, m′-1, m] = ( + d̄ₗᵐ′ * Hˡ[i, m′+1, m] + + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + - d̄ₗᵐ * Hˡ[i, m′, m+1] + ) / d̄ₗᵐ′⁻¹ + end + end + let m = ℓ + d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + @turbo for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + Hˡ[i, m′-1, m] = ( + d̄ₗᵐ′ * Hˡ[i, m′+1, m] + + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + ) / d̄ₗᵐ′⁻¹ + end + end + end + end end w end @@ -151,43 +281,43 @@ function recurrence_step6!(w::WignerHCalculator{IT}, ℓ) where {IT<:Signed} w end -function recurrence!( - w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT -) where {IT<:Signed, RT, NT<:Complex} - eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) - recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ) -end +# function recurrence!( +# w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT +# ) where {IT<:Signed, RT, NT<:Complex} +# eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) +# recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ) +# end -function recurrence!( - w::WignerHCalculator{IT, RT, NT}, β::RT, ℓ::IT -) where {IT<:Signed, RT, NT<:Real} - eⁱᵝ = cis(β) - recurrence!(w, eⁱᵝ, ℓ) -end +# function recurrence!( +# w::WignerHCalculator{IT, RT, NT}, β::RT, ℓ::IT +# ) where {IT<:Signed, RT, NT<:Real} +# eⁱᵝ = cis(β) +# recurrence!(w, eⁱᵝ, ℓ) +# end -function recurrence!( - w::WignerHCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, - ℓ::IT -) where {IT<:Signed, RT, NT<:Complex} - _recurrence!(w, eⁱᵝ, ℓ) - let Hˡ = Hˡ(w) - convert_H_to_D!(Hˡ, eⁱᵅ, eⁱᵞ) - Hˡ - end -end +# function recurrence!( +# w::WignerHCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, +# ℓ::IT +# ) where {IT<:Signed, RT, NT<:Complex} +# _recurrence!(w, eⁱᵝ, ℓ) +# let Hˡ = Hˡ(w) +# convert_H_to_D!(Hˡ, eⁱᵅ, eⁱᵞ) +# Hˡ +# end +# end function recurrence!( - w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT -) where {IT<:Signed, RT, NT<:Real} - _recurrence!(w, eⁱᵝ, ℓ) - let Hˡ = Hˡ(w) - convert_H_to_d!(Hˡ) - Hˡ - end + w::WignerHCalculator{IT, RT, ST}, ℓ::IT +) where {IT<:Signed, RT, ST} + _recurrence!(w, ℓ) + # let Hˡ = Hˡ(w) + # convert_H_to_d!(Hˡ) + # Hˡ + # end end function _recurrence!( - w::WignerHCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT + w::WignerHCalculator{IT, RT, NT}, ℓ::IT ) where {IT<:Signed, RT, NT} # NOTE: In the comments explaining the recurrence steps below, we use notation with # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. @@ -198,7 +328,7 @@ function _recurrence!( fillHˡ₀ₘ!(w) # Record the result in Hˡ (it was computed in h⃗ˡ) # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ. - recurrence_step2!(w, eⁱᵝ) # H⁰₀ₘ -> H¹₀ₘ + recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ else if !consistent_ℓ(w) || ℓ(w) ≠ ℓ-1 # We need to start over, but will only be using the axes, so we only reset them. @@ -206,11 +336,11 @@ function _recurrence!( h⃗ˡ⁺¹(w).ℓ = ℓₘᵢₙ(w)+1 recurrence_step1!(w) # H⁰₀₀ = 1 - recurrence_step2!(w, eⁱᵝ) # H⁰₀ₘ -> H¹₀ₘ + recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ for ℓ′ in ℓₘᵢₙ(w)+1:ℓ-1 increment_axes!(w) - recurrence_step2!(w, eⁱᵝ) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ end end @@ -219,16 +349,16 @@ function _recurrence!( # function, but is currently stored in h⃗ˡ⁺¹ so we have to swap and increment the # axes first. If so, that block just got us to the same point. increment_axes!(w) - recurrence_step2!(w, eⁱᵝ, ℓ) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ # So far, we've only used h⃗ˡ and h⃗ˡ⁺¹. Now we're ready to start using Hˡ. Hˡ(w).ℓ = ℓ # The h⃗ˡ and h⃗ˡ⁺¹ are what we need to start the recurrence for Hˡ fillHˡ₀ₘ!(w) # Copy h⃗ˡ₀ₘ to Hˡ₀ₘ - recurrence_step3!(w, eⁱᵝ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ - recurrence_step4!(w, eⁱᵝ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ - recurrence_step5!(w, eⁱᵝ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ + recurrence_step3!(w) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ # # Impose symmetries # recurrence_step6!(w, ℓ) From 0ea6fdc02fb1c882047b55538280de27c66d7d2e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 13:45:42 -0400 Subject: [PATCH 258/329] Make @turbo available --- src/redesign/SphericalFunctions.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index fe7eaace..c1f19b06 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -3,16 +3,17 @@ module Redesign import Quaternionic import TestItems: @testitem, @testsnippet import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, FixedSizeVector +import LoopVectorization: @turbo -# TEMPORARY!!!! +# TEMPORARY!!!! Should be able to remove once this moves to SphericalFunctions proper import SphericalFunctions: ComplexPowers include("WignerMatrix.jl") -include("WignerH.jl") -#include("WignerMatrices.jl") include("recurrence.jl") include("WignerCalculator.jl") + +include("WignerH.jl") include("WignerHCalculator.jl") From 88e810ee8bb7fd5cb3c502c1209de67a6c1c6290 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 13:45:57 -0400 Subject: [PATCH 259/329] Display a little more nicely --- src/redesign/WignerH.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 73d160d8..8b59772a 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -182,10 +182,11 @@ function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, print( io, "SphericalFunctions.HWedge{$IT, $RT} for ℓ=$(ℓ) with m′=$(m′ₘᵢₙ:m′ₘₐₓ), ", - "m=abs(m′):$(ℓ), and iᵣ=1:$(Nᵣ)\n", - "Stored in ", + "m=abs(m′):$(ℓ), and iᵣ=1:$(Nᵣ) stored in\n", + summary(parent(H)), ", currently using\n" ) - show(io, MIME("text/plain"), parent(H)) + i = row_index(H)[Int(m′ₘₐₓ - m′ₘᵢₙ) + 1] + Nᵣ * (Int(ℓ - abs(m′ₘₐₓ)) + 1) - 1 + show(io, MIME("text/plain"), parent(H)[begin:i]) end end From ed6284ffa555eef729881dc7ebb78c348039d954 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 13:47:13 -0400 Subject: [PATCH 260/329] Improve iterations --- src/redesign/WignerHCalculator.jl | 48 ++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 2270303d..085fc996 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -1,12 +1,12 @@ struct WignerHCalculator{IT, RT<:Real, ST} - ℓₘₐₓ::IT - m′ₘₐₓ::IT - m′ₘᵢₙ::IT - h⃗ᵃ::HAxis{IT, RT} - h⃗ᵇ::HAxis{IT, RT} - Hˡ::HWedge{IT, RT, ST} - eⁱᵝ::FixedSizeVectorDefault{Complex{RT}} - swapH::Base.RefValue{Bool} # h⃗ˡ(w) returns h⃗ᵃ if `false`, otherwise h⃗ᵇ; and vice versa for h⃗ˡ⁺¹(w) + h⃗ᵃ::HAxis{IT, RT} + h⃗ᵇ::HAxis{IT, RT} + Hˡ::HWedge{IT, RT, ST} + eⁱᵝ::FixedSizeVectorDefault{Complex{RT}} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + swapH::Base.RefValue{Bool} # h⃗ˡ(w) returns h⃗ᵃ if `false`, otherwise h⃗ᵇ; and vice versa for h⃗ˡ⁺¹(w) function WignerHCalculator( eⁱᵝ::AbstractVector{Complex{RT}}, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ ) where {IT, RT<:Real} @@ -19,7 +19,27 @@ struct WignerHCalculator{IT, RT<:Real, ST} h⃗ᵃ = HAxis(RT, Nᵣ, ℓₘₐₓ+1) h⃗ᵇ = HAxis(RT, Nᵣ, ℓₘₐₓ+1) Hˡ = HWedge(RT, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) - new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, h⃗ᵃ, h⃗ᵇ, Hˡ, eⁱᵝ, Ref(false)) + h⃗ᵇ.ℓ = ℓ(h⃗ᵇ) + 1 + WignerHCalculator(h⃗ᵃ, h⃗ᵇ, Hˡ, eⁱᵝ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, Ref(false)) + end + function WignerHCalculator( + h⃗ᵃ::HAxis{IT, RT}, + h⃗ᵇ::HAxis{IT, RT}, + Hˡ::HWedge{IT, RT, ST}, + eⁱᵝ::FixedSizeVectorDefault{Complex{RT}}, + ℓₘₐₓ::IT, + m′ₘₐₓ::IT, + m′ₘᵢₙ::IT, + swapH::Base.RefValue{Bool} + ) where {IT, RT<:Real, ST} + if !(Nᵣ(h⃗ᵃ) == Nᵣ(h⃗ᵇ) == Nᵣ(Hˡ) == length(eⁱᵝ)) + error( + "Inconsistent Nᵣ values in WignerHCalculator constructor:\n" + * " Nᵣ(h⃗ᵃ)=$(Nᵣ(h⃗ᵃ)), Nᵣ(h⃗ᵇ)=$(Nᵣ(h⃗ᵇ)), Nᵣ(Hˡ)=$(Nᵣ(Hˡ))," + * " length(eⁱᵝ)=$(length(eⁱᵝ))." + ) + end + new{IT, RT, ST}(h⃗ᵃ, h⃗ᵇ, Hˡ, eⁱᵝ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, swapH) end end @@ -38,6 +58,7 @@ end ℓₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.ℓₘₐₓ m′ₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘₐₓ m′ₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘᵢₙ +Nᵣ(w::WignerHCalculator{IT}) where {IT} = Nᵣ(Hˡ(w)) h⃗ˡ(w::WignerHCalculator) = swapH(w) ? w.h⃗ᵇ : w.h⃗ᵃ h⃗ˡ⁺¹(w::WignerHCalculator) = swapH(w) ? w.h⃗ᵃ : w.h⃗ᵇ @@ -60,8 +81,9 @@ end function increment_axes!(w::WignerHCalculator) # The data that is now stored as h⃗ˡ(w) will get swapped below so that it will be # returned by h⃗ˡ⁺¹(w), so we need to increment its ℓ value twice. - h⃗ˡ = h⃗ˡ(w) - h⃗ˡ.ℓ = ℓ(h⃗ˡ) + 2 + let h⃗ˡ = h⃗ˡ(w) + h⃗ˡ.ℓ = ℓ(h⃗ˡ) + 2 + end # The data that is now stored as h⃗ˡ⁺¹(w) will get swapped below so that it will be # returned by h⃗ˡ(w), which will already be correct for the next ℓ value. w.swapH[] = !w.swapH[] @@ -81,9 +103,9 @@ function fillHˡ₀ₘ!(w::WignerHCalculator{IT}) where {IT} error("Cannot fill Hˡ₀ₘ for ℓ=$(ℓ(Hˡ)) from h⃗ˡ for ℓ=$(ℓ(h⃗ˡ)).") end # Get the index to the start of the central row, m′ = 0 or 1//2 - iˡ₀₀ = row_index(w)[Int(ℓₘᵢₙ(w) - m′ₘᵢₙ(w)) + 1] + iˡ₀₀ = row_index(Hˡ)[Int(ℓₘᵢₙ(Hˡ) - m′ₘᵢₙ(Hˡ)) + 1] # Figure out how many entries to copy - N = Nᵣ(Hˡ) * (Int(ℓ(w) - ℓₘᵢₙ(w)) + 1) + N = Nᵣ(Hˡ) * (Int(ℓ(Hˡ) - ℓₘᵢₙ(Hˡ)) + 1) # Now just copy that many entries from h⃗ˡ₀ₘ into row Hˡ₀ₘ copyto!(parent(Hˡ), iˡ₀₀, parent(h⃗ˡ), 1, N) end From 5d4c13abc6420846384e8e339654f1078e170267 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 13:47:27 -0400 Subject: [PATCH 261/329] Improve display of WignerHCalculator --- src/redesign/WignerHCalculator.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 085fc996..33b0cde2 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -123,6 +123,18 @@ function Base.setproperty!(w::WignerHCalculator{IT}, s::Symbol, ℓ::IIT) where end end +function Base.show(io::IO, ::MIME"text/plain", w::WignerHCalculator{IT, RT, ST}) where {IT, RT, ST} + let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), m′ₘₐₓ = m′ₘₐₓ(w), Nᵣ = Nᵣ(w) + print( + io, + "SphericalFunctions.WignerHCalculator{$IT, $RT} for ℓ=$(ℓ) with ", + "m′=$(m′ₘᵢₙ:m′ₘₐₓ) and iᵣ=1:$(Nᵣ) up to ℓₘₐₓ=$(ℓₘₐₓ(w)), ", + "and swapH=$(swapH(w))."#\nStored in ", + ) + # show(io, MIME("text/plain"), Hˡ(w)) + end +end + function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} let h⃗⁰ = h⃗ˡ(w), ℓ = ℓ(h⃗⁰) if ℓ ≠ ℓₘᵢₙ(w) From c03bd6c42548c7726764ad61da1a8fcfb2e80638 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 13:48:02 -0400 Subject: [PATCH 262/329] Streamline recurrences --- src/redesign/WignerHCalculator.jl | 75 +++++++++++++++++-------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 33b0cde2..3ad2c57c 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -356,49 +356,58 @@ function _recurrence!( # NOTE: In the comments explaining the recurrence steps below, we use notation with # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. - if ℓ == ℓₘᵢₙ(w) - w.ℓ = ℓ - recurrence_step1!(w) # H⁰₀₀ = 1 - fillHˡ₀ₘ!(w) # Record the result in Hˡ (it was computed in h⃗ˡ) + if ℓ < ℓₘᵢₙ(w) || ℓ > ℓₘₐₓ(w) + error( + "Requested ℓ=$(ℓ) is out of bounds [$(ℓₘᵢₙ(w)), $(ℓₘₐₓ(w))] for WignerHCalculator." + ) + end - # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ. - recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ - else - if !consistent_ℓ(w) || ℓ(w) ≠ ℓ-1 - # We need to start over, but will only be using the axes, so we only reset them. - h⃗ˡ(w).ℓ = ℓₘᵢₙ(w) - h⃗ˡ⁺¹(w).ℓ = ℓₘᵢₙ(w)+1 + let h⃗ˡ=h⃗ˡ(w), h⃗ˡ⁺¹=h⃗ˡ⁺¹(w), Hˡ=Hˡ(w) + if ℓ == ℓₘᵢₙ(w) + w.ℓ = ℓ recurrence_step1!(w) # H⁰₀₀ = 1 + fillHˡ₀ₘ!(w) # Record the result in Hˡ (it was computed in h⃗ˡ) + + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ. recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ + else + if !consistent_ℓ(w) || h⃗ˡ.ℓ ≠ ℓ-1 + # We need to start over, but will only be using the axes, so we only reset them. + h⃗ˡ.ℓ = ℓₘᵢₙ(w) + h⃗ˡ⁺¹.ℓ = ℓₘᵢₙ(w)+1 - for ℓ′ in ℓₘᵢₙ(w)+1:ℓ-1 - increment_axes!(w) - recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + recurrence_step1!(w) # H⁰₀₀ = 1 + recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ + + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ-1 + increment_axes!(w) + recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + end end - end - # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not the - # `if` block above was executed. If not, it was set in a previous call to this - # function, but is currently stored in h⃗ˡ⁺¹ so we have to swap and increment the - # axes first. If so, that block just got us to the same point. - increment_axes!(w) - recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not the + # `if` block above was executed. If not, it was set in a previous call to this + # function, but is currently stored in h⃗ˡ⁺¹ so we have to swap and increment the + # axes first. If so, that block just got us to the same point. + increment_axes!(w) + recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ - # So far, we've only used h⃗ˡ and h⃗ˡ⁺¹. Now we're ready to start using Hˡ. - Hˡ(w).ℓ = ℓ + # So far, we've only used h⃗ˡ and h⃗ˡ⁺¹. Now we're ready to start using Hˡ. + Hˡ.ℓ = ℓ - # The h⃗ˡ and h⃗ˡ⁺¹ are what we need to start the recurrence for Hˡ - fillHˡ₀ₘ!(w) # Copy h⃗ˡ₀ₘ to Hˡ₀ₘ - recurrence_step3!(w) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ - recurrence_step4!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ - recurrence_step5!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ + # The h⃗ˡ and h⃗ˡ⁺¹ are what we need to start the recurrence for Hˡ + fillHˡ₀ₘ!(w) # Copy h⃗ˡ₀ₘ to Hˡ₀ₘ + recurrence_step3!(w) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ - # # Impose symmetries - # recurrence_step6!(w, ℓ) + # # Impose symmetries + # recurrence_step6!(w, ℓ) - # # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ - # increment_axes!(w) + # # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ + # increment_axes!(w) + end + Hˡ end - Hˡ(w) end From d8f252846caa1eb1b999b4b50928434e8ad8c677 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 13:48:26 -0400 Subject: [PATCH 263/329] Experiment with some choices for vectorization --- src/redesign/WignerHCalculator.jl | 80 ++++++++++++++----------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 3ad2c57c..1a7b6182 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -145,9 +145,9 @@ function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} w end -function recurrence_step2!(w::WignerHCalculator{IT}) where {IT<:Signed} - let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w) - if ℓ < ℓₘᵢₙ(IT) +function recurrence_step2!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} + let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), eⁱᵝ = eⁱᵝ(w) + if ℓ(h⃗ˡ) < ℓₘᵢₙ(IT) error( "recurrence_step2! can only be called for ℓ≥ℓₘᵢₙ=$(ℓₘᵢₙ(IT)); current ℓ=$ℓ." ) @@ -157,28 +157,28 @@ function recurrence_step2!(w::WignerHCalculator{IT}) where {IT<:Signed} # Xing et al., denoting the coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following # steps, we will use notation from Gumerov and Duraiswami, who denote their # different coefficients aₗᵐ, etc. - @inbounds let √=sqrt∘T, ℓ = ℓ(h⃗ˡ), eⁱᵝ = eⁱᵝ(w), Nᵣ = Nᵣ(h⃗ˡ) - if ℓ == 1 - # The ℓ>1 branch would try to access invalid indices of H⁰; if we treat those - # elements as zero, we can simplify that branch to just the following much - # simpler code anyway. So fundamentally, this branch is the same as the other - # branch. - @turbo for i ∈ 1:Nᵣ + @inbounds let √=sqrt∘RT, ℓ = ℓ(h⃗ˡ), Nᵣ = Nᵣ(h⃗ˡ) + if ℓ == 0 + # The ℓ>1 branch would try to access invalid indices of H⁰; if we treat + # those elements as zero, we can simplify that branch to just the following + # much simpler code anyway (because we know that h⃗ˡ₀₀=1). So + # fundamentally, this branch is the same as the other branch. + @turbo warn_check_args=false for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) h⃗ˡ⁺¹[i, 0, 0] = cosβ h⃗ˡ⁺¹[i, 0, 1] = sinβ / √2 end - elseif ℓ > 1 - b̄ₗ = √(T(ℓ-1)/ℓ) - @turbo for i ∈ 1:Nᵣ + else + b̄ₗ = √(RT(ℓ-1)/ℓ) + @turbo warn_check_args=false for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) h⃗ˡ⁺¹[i, 0, 0] = cosβ * h⃗ˡ[i, 0, 0] - b̄ₗ * sinβ * h⃗ˡ[i, 0, 1] end - @turbo for m ∈ 1:ℓ-2 + for m ∈ 1:ℓ-2 c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ d̄ₗₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ - for i ∈ 1:Nᵣ + @turbo warn_check_args=false for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) h⃗ˡ⁺¹[i, 0, m] = ( c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] @@ -189,7 +189,7 @@ function recurrence_step2!(w::WignerHCalculator{IT}) where {IT<:Signed} let m = ℓ-1 c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ - @turbo for i ∈ 1:Nᵣ + @turbo warn_check_args=false for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) h⃗ˡ⁺¹[i, 0, m] = ( c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] @@ -199,31 +199,29 @@ function recurrence_step2!(w::WignerHCalculator{IT}) where {IT<:Signed} end let m = ℓ ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ - @turbo for i ∈ 1:Nᵣ + @turbo warn_check_args=false for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) h⃗ˡ⁺¹[i, 0, m] = ( - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) ) end end - else - error("Tried to recurse with ℓ=$ℓ; only integer ℓ ≥ 1 is supported.") end end end w end -function recurrence_step3!(w::WignerHCalculator{IT}) where {IT<:Signed} +function recurrence_step3!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} let Hˡ = Hˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), eⁱᵝ = eⁱᵝ(w) - @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), Nᵣ = Nᵣ(h⃗ˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), Nᵣ = Nᵣ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) if ℓ > 0 && m′ₘₐₓ ≥ 1 c = 1 / √(ℓ*(ℓ+1)) - @turbo for m ∈ 1:ℓ + for m ∈ 1:ℓ āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) b̄ₗ₊₁ᵐ⁻¹ = √((ℓ-m+1)*(ℓ-m+2)) b̄ₗ₊₁⁻ᵐ⁻¹ = √((ℓ+m+1)*(ℓ+m+2)) - for i ∈ 1:Nᵣ + @turbo warn_check_args=false for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) Hˡ[i, 1, m] = -c * ( b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m+1] @@ -238,19 +236,18 @@ function recurrence_step3!(w::WignerHCalculator{IT}) where {IT<:Signed} w end -function recurrence_step4!(w::WignerHCalculator{IT}) where {IT<:Signed} +function recurrence_step4!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) - @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 # Note that the signs of m′ and m are always +1, so we leave them out of the # calculations of d̄ in this function. d̄ₗᵐ′ = √((ℓ-m′)*(ℓ+m′+1)) d̄ₗᵐ′⁻¹ = √((ℓ-m′+1)*(ℓ+m′)) - @turbo for m ∈ (m′+1):ℓ-1 + for m ∈ (m′+1):ℓ-1 d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) d̄ₗᵐ = √((ℓ-m)*(ℓ+m+1)) - for i ∈ 1:Nᵣ - cosβ, sinβ = reim(eⁱᵝ[i]) + @turbo warn_check_args=false for i ∈ 1:Nᵣ Hˡ[i, m′+1, m] = ( d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] @@ -260,8 +257,7 @@ function recurrence_step4!(w::WignerHCalculator{IT}) where {IT<:Signed} end let m = ℓ d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) - @turbo for i ∈ 1:Nᵣ - cosβ, sinβ = reim(eⁱᵝ[i]) + @turbo warn_check_args=false for i ∈ 1:Nᵣ Hˡ[i, m′+1, m] = ( d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] @@ -274,17 +270,16 @@ function recurrence_step4!(w::WignerHCalculator{IT}) where {IT<:Signed} w end -function recurrence_step5!(w::WignerHCalculator{IT}) where {IT<:Signed} +function recurrence_step5!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) - @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) - @turbo for m ∈ -(m′-1):ℓ-1 + @turbo warn_check_args=false for m ∈ -(m′-1):ℓ-1 d̄ₗᵐ = sgn(m) * √((ℓ-m)*(ℓ+m+1)) d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) for i ∈ 1:Nᵣ - cosβ, sinβ = reim(eⁱᵝ[i]) Hˡ[i, m′-1, m] = ( d̄ₗᵐ′ * Hˡ[i, m′+1, m] + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] @@ -294,8 +289,7 @@ function recurrence_step5!(w::WignerHCalculator{IT}) where {IT<:Signed} end let m = ℓ d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) - @turbo for i ∈ 1:Nᵣ - cosβ, sinβ = reim(eⁱᵝ[i]) + @turbo warn_check_args=false for i ∈ 1:Nᵣ Hˡ[i, m′-1, m] = ( d̄ₗᵐ′ * Hˡ[i, m′+1, m] + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] @@ -308,13 +302,13 @@ function recurrence_step5!(w::WignerHCalculator{IT}) where {IT<:Signed} w end -function recurrence_step6!(w::WignerHCalculator{IT}, ℓ) where {IT<:Signed} - let Hˡ = Hˡ(w) - recurrence_step6!(Hˡ) - end - w -end - +# function recurrence_step6!(w::WignerHCalculator{IT}, ℓ) where {IT<:Signed} +# let Hˡ = Hˡ(w) +# recurrence_step6!(Hˡ) +# end +# w +# end + # function recurrence!( # w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT # ) where {IT<:Signed, RT, NT<:Complex} From 9fc424cd079d785fee302fd61e9ba20b753a71ba Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 16:06:30 -0400 Subject: [PATCH 264/329] Use explicit indexing for easier SIMD --- src/redesign/WignerH.jl | 1 + src/redesign/WignerHCalculator.jl | 212 +++++++++++++++++++++++------- 2 files changed, 169 insertions(+), 44 deletions(-) diff --git a/src/redesign/WignerH.jl b/src/redesign/WignerH.jl index 8b59772a..06fb8444 100644 --- a/src/redesign/WignerH.jl +++ b/src/redesign/WignerH.jl @@ -89,6 +89,7 @@ mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) row_index(w::HWedge{IT}) where {IT} = w.row_index +row_index(w::HWedge{IT}, m′::IT) where {IT} = row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] Nᵣ(w::HWedge{IT}) where {IT} = w.Nᵣ maxℓ(w::HWedge{IT}) where {IT} = w.maxℓ maxm′ₘₐₓ(w::HWedge{IT}) where {IT} = w.maxm′ₘₐₓ diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/WignerHCalculator.jl index 1a7b6182..d6e07ce5 100644 --- a/src/redesign/WignerHCalculator.jl +++ b/src/redesign/WignerHCalculator.jl @@ -103,7 +103,7 @@ function fillHˡ₀ₘ!(w::WignerHCalculator{IT}) where {IT} error("Cannot fill Hˡ₀ₘ for ℓ=$(ℓ(Hˡ)) from h⃗ˡ for ℓ=$(ℓ(h⃗ˡ)).") end # Get the index to the start of the central row, m′ = 0 or 1//2 - iˡ₀₀ = row_index(Hˡ)[Int(ℓₘᵢₙ(Hˡ) - m′ₘᵢₙ(Hˡ)) + 1] + iˡ₀₀ = row_index(Hˡ, ℓₘᵢₙ(Hˡ)) # Figure out how many entries to copy N = Nᵣ(Hˡ) * (Int(ℓ(Hˡ) - ℓₘᵢₙ(Hˡ)) + 1) # Now just copy that many entries from h⃗ˡ₀ₘ into row Hˡ₀ₘ @@ -163,47 +163,74 @@ function recurrence_step2!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} # those elements as zero, we can simplify that branch to just the following # much simpler code anyway (because we know that h⃗ˡ₀₀=1). So # fundamentally, this branch is the same as the other branch. - @turbo warn_check_args=false for i ∈ 1:Nᵣ + i⁰⁰ = 0 + i⁰¹ = Nᵣ + for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) - h⃗ˡ⁺¹[i, 0, 0] = cosβ - h⃗ˡ⁺¹[i, 0, 1] = sinβ / √2 + # h⃗ˡ⁺¹[i, 0, 0] = cosβ + # h⃗ˡ⁺¹[i, 0, 1] = sinβ / √2 + h⃗ˡ⁺¹[i⁰⁰ + i] = cosβ + h⃗ˡ⁺¹[i⁰¹ + i] = sinβ / √2 end else b̄ₗ = √(RT(ℓ-1)/ℓ) - @turbo warn_check_args=false for i ∈ 1:Nᵣ + i⁰⁰ = 0 + i⁰¹ = Nᵣ + for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) - h⃗ˡ⁺¹[i, 0, 0] = cosβ * h⃗ˡ[i, 0, 0] - b̄ₗ * sinβ * h⃗ˡ[i, 0, 1] + # h⃗ˡ⁺¹[i, 0, 0] = cosβ * h⃗ˡ[i, 0, 0] - b̄ₗ * sinβ * h⃗ˡ[i, 0, 1] + h⃗ˡ⁺¹[i⁰⁰ + i] = cosβ * h⃗ˡ[i⁰⁰ + i] - b̄ₗ * sinβ * h⃗ˡ[i⁰¹ + i] end for m ∈ 1:ℓ-2 c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ d̄ₗₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ - @turbo warn_check_args=false for i ∈ 1:Nᵣ + + i⁰ᵐ = Nᵣ * m + i⁰ᵐ⁺¹ = Nᵣ * (m + 1) + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + + for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) - h⃗ˡ⁺¹[i, 0, m] = ( - c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] - - sinβ * (d̄ₗₘ * h⃗ˡ[i, 0, m+1] - ēₗₘ * h⃗ˡ[i, 0, m-1]) + # h⃗ˡ⁺¹[i, 0, m] = ( + # c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] + # - sinβ * (d̄ₗₘ * h⃗ˡ[i, 0, m+1] - ēₗₘ * h⃗ˡ[i, 0, m-1]) + # ) + h⃗ˡ⁺¹[i⁰ᵐ + i] = ( + c̄ₗₘ * cosβ * h⃗ˡ[i⁰ᵐ + i] + - sinβ * (d̄ₗₘ * h⃗ˡ[i⁰ᵐ⁺¹ + i] - ēₗₘ * h⃗ˡ[i⁰ᵐ⁻¹ + i]) ) end end let m = ℓ-1 c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ - @turbo warn_check_args=false for i ∈ 1:Nᵣ + + i⁰ᵐ = Nᵣ * m + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + + for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) - h⃗ˡ⁺¹[i, 0, m] = ( - c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] - - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) + # h⃗ˡ⁺¹[i, 0, m] = ( + # c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] + # - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) + # ) + h⃗ˡ⁺¹[i⁰ᵐ + i] = ( + c̄ₗₘ * cosβ * h⃗ˡ[i⁰ᵐ + i] + - sinβ * (- ēₗₘ * h⃗ˡ[i⁰ᵐ⁻¹ + i]) ) end end let m = ℓ ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ - @turbo warn_check_args=false for i ∈ 1:Nᵣ + + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + i⁰ᵐ = Nᵣ * m + + for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) - h⃗ˡ⁺¹[i, 0, m] = ( - - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) - ) + # h⃗ˡ⁺¹[i, 0, m] = (- sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1])) + h⃗ˡ⁺¹[i⁰ᵐ + i] = (- sinβ * (- ēₗₘ * h⃗ˡ[i⁰ᵐ⁻¹ + i])) end end end @@ -217,16 +244,33 @@ function recurrence_step3!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), Nᵣ = Nᵣ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) if ℓ > 0 && m′ₘₐₓ ≥ 1 c = 1 / √(ℓ*(ℓ+1)) + + # Precompute base offset for m′=1 row in Hˡ + r¹ = row_index(Hˡ, 1) - 1 + for m ∈ 1:ℓ - āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) + āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) b̄ₗ₊₁ᵐ⁻¹ = √((ℓ-m+1)*(ℓ-m+2)) b̄ₗ₊₁⁻ᵐ⁻¹ = √((ℓ+m+1)*(ℓ+m+2)) - @turbo warn_check_args=false for i ∈ 1:Nᵣ + + # Column offsets in Hˡ row 1 and h⃗ˡ⁺¹ row 0 + c¹ᵐ = Nᵣ * Int(m - 1) # Hˡ[i, 1, m] has m′=1, so column is m-abs(1)=m-1 + i¹ᵐ = r¹ + c¹ᵐ + i⁰ᵐ⁺¹ = Nᵣ * (m + 1) + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + i⁰ᵐ = Nᵣ * m + + for i ∈ 1:Nᵣ cosβ, sinβ = reim(eⁱᵝ[i]) - Hˡ[i, 1, m] = -c * ( - b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m+1] - + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m-1] - + āₗᵐ * sinβ * h⃗ˡ⁺¹[i, 0, m] + # Hˡ[i, 1, m] = -c * ( + # b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m+1] + # + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m-1] + # + āₗᵐ * sinβ * h⃗ˡ⁺¹[i, 0, m] + # ) + Hˡ[i¹ᵐ + i] = -c * ( + b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i⁰ᵐ⁺¹ + i] + + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * h⃗ˡ⁺¹[i⁰ᵐ⁻¹ + i] + + āₗᵐ * sinβ * h⃗ˡ⁺¹[i⁰ᵐ + i] ) end end @@ -238,30 +282,74 @@ end function recurrence_step4!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) - @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + @inbounds @fastmath let √=sqrt∘RT, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 # Note that the signs of m′ and m are always +1, so we leave them out of the # calculations of d̄ in this function. d̄ₗᵐ′ = √((ℓ-m′)*(ℓ+m′+1)) d̄ₗᵐ′⁻¹ = √((ℓ-m′+1)*(ℓ+m′)) + + # Precompute base offsets for m′-1, m′, m′+1 rows. Note that we subtract 1 + # here because row_index points to the beginning of the desired row, but we + # just want the offset. + rᵐ′⁻¹ = row_index(Hˡ, m′ - 1) - 1 + rᵐ′ = row_index(Hˡ, m′) - 1 + rᵐ′⁺¹ = row_index(Hˡ, m′ + 1) - 1 + for m ∈ (m′+1):ℓ-1 d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) d̄ₗᵐ = √((ℓ-m)*(ℓ+m+1)) - @turbo warn_check_args=false for i ∈ 1:Nᵣ - Hˡ[i, m′+1, m] = ( - d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] - - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] - + d̄ₗᵐ * Hˡ[i, m′, m+1] + + # Compute column offsets within each row. For row m′, column m has + # offset Nᵣ * (m - abs(m′)). + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′ᵐ⁺¹ = Nᵣ * Int(m + 1 - abs(m′)) + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + + # Final 1D index offsets (0-based for the loop) + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′ᵐ⁺¹ = rᵐ′ + cᵐ′ᵐ⁺¹ + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′+1, m] = ( + # d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] + # - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # + d̄ₗᵐ * Hˡ[i, m′, m+1] + # ) / d̄ₗᵐ′ + Hˡ[iᵐ′⁺¹ᵐ + i] = ( + d̄ₗᵐ′⁻¹ * Hˡ[iᵐ′⁻¹ᵐ + i] + - d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + + d̄ₗᵐ * Hˡ[iᵐ′ᵐ⁺¹ + i] ) / d̄ₗᵐ′ end end + + # Now, we do the m=ℓ case separately, since there is no m+1, so we would get + # out-of-bounds accesses; we just copy the body of the loop above, but + # remove anything that involves m+1. let m = ℓ d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) - @turbo warn_check_args=false for i ∈ 1:Nᵣ - Hˡ[i, m′+1, m] = ( - d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] - - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] - ) / d̄ₗᵐ′ + + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′+1, m] = ( + # d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] + # - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # ) / d̄ₗᵐ′ + Hˡ[iᵐ′⁺¹ᵐ + i] = ( + d̄ₗᵐ′⁻¹ * Hˡ[iᵐ′⁻¹ᵐ + i] + - d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + ) / d̄ₗᵐ′ end end end @@ -276,23 +364,59 @@ function recurrence_step5!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) - @turbo warn_check_args=false for m ∈ -(m′-1):ℓ-1 + + # Precompute base offsets for m′-1, m′, m′+1 rows + rᵐ′⁻¹ = row_index(Hˡ, m′ - 1) - 1 + rᵐ′ = row_index(Hˡ, m′) - 1 + rᵐ′⁺¹ = row_index(Hˡ, m′ + 1) - 1 + + for m ∈ -(m′-1):ℓ-1 d̄ₗᵐ = sgn(m) * √((ℓ-m)*(ℓ+m+1)) d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + + # Compute column offsets within each row + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′ᵐ⁺¹ = Nᵣ * Int(m + 1 - abs(m′)) + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′ᵐ⁺¹ = rᵐ′ + cᵐ′ᵐ⁺¹ + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + for i ∈ 1:Nᵣ - Hˡ[i, m′-1, m] = ( - d̄ₗᵐ′ * Hˡ[i, m′+1, m] - + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] - - d̄ₗᵐ * Hˡ[i, m′, m+1] + # Hˡ[i, m′-1, m] = ( + # d̄ₗᵐ′ * Hˡ[i, m′+1, m] + # + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # - d̄ₗᵐ * Hˡ[i, m′, m+1] + # ) / d̄ₗᵐ′⁻¹ + Hˡ[iᵐ′⁻¹ᵐ + i] = ( + d̄ₗᵐ′ * Hˡ[iᵐ′⁺¹ᵐ + i] + + d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + - d̄ₗᵐ * Hˡ[iᵐ′ᵐ⁺¹ + i] ) / d̄ₗᵐ′⁻¹ end end let m = ℓ d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) - @turbo warn_check_args=false for i ∈ 1:Nᵣ - Hˡ[i, m′-1, m] = ( - d̄ₗᵐ′ * Hˡ[i, m′+1, m] - + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′-1, m] = ( + # d̄ₗᵐ′ * Hˡ[i, m′+1, m] + # + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # ) / d̄ₗᵐ′⁻¹ + Hˡ[iᵐ′⁻¹ᵐ + i] = ( + d̄ₗᵐ′ * Hˡ[iᵐ′⁺¹ᵐ + i] + + d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] ) / d̄ₗᵐ′⁻¹ end end From 9dd814a61a9b63f9f7559c45f65bed83b7a1f5c1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 27 Oct 2025 20:51:25 -0400 Subject: [PATCH 265/329] Put all the Wigner D/d/H stuff in a "Wigner" directory --- src/redesign/SphericalFunctions.jl | 11 +++++------ src/redesign/{ => Wigner}/WignerCalculator.jl | 0 src/redesign/{ => Wigner}/WignerH.jl | 0 src/redesign/{ => Wigner}/WignerHCalculator.jl | 0 src/redesign/{ => Wigner}/WignerMatrices.jl | 0 src/redesign/{ => Wigner}/recurrence.jl | 0 6 files changed, 5 insertions(+), 6 deletions(-) rename src/redesign/{ => Wigner}/WignerCalculator.jl (100%) rename src/redesign/{ => Wigner}/WignerH.jl (100%) rename src/redesign/{ => Wigner}/WignerHCalculator.jl (100%) rename src/redesign/{ => Wigner}/WignerMatrices.jl (100%) rename src/redesign/{ => Wigner}/recurrence.jl (100%) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index c1f19b06..21a462bf 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -9,12 +9,11 @@ import LoopVectorization: @turbo import SphericalFunctions: ComplexPowers -include("WignerMatrix.jl") -include("recurrence.jl") -include("WignerCalculator.jl") - -include("WignerH.jl") -include("WignerHCalculator.jl") +include("Wigner/WignerMatrix.jl") +include("Wigner/WignerCalculator.jl") +include("Wigner/WignerH.jl") +include("Wigner/WignerHCalculator.jl") +include("Wigner/recurrence.jl") # function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} diff --git a/src/redesign/WignerCalculator.jl b/src/redesign/Wigner/WignerCalculator.jl similarity index 100% rename from src/redesign/WignerCalculator.jl rename to src/redesign/Wigner/WignerCalculator.jl diff --git a/src/redesign/WignerH.jl b/src/redesign/Wigner/WignerH.jl similarity index 100% rename from src/redesign/WignerH.jl rename to src/redesign/Wigner/WignerH.jl diff --git a/src/redesign/WignerHCalculator.jl b/src/redesign/Wigner/WignerHCalculator.jl similarity index 100% rename from src/redesign/WignerHCalculator.jl rename to src/redesign/Wigner/WignerHCalculator.jl diff --git a/src/redesign/WignerMatrices.jl b/src/redesign/Wigner/WignerMatrices.jl similarity index 100% rename from src/redesign/WignerMatrices.jl rename to src/redesign/Wigner/WignerMatrices.jl diff --git a/src/redesign/recurrence.jl b/src/redesign/Wigner/recurrence.jl similarity index 100% rename from src/redesign/recurrence.jl rename to src/redesign/Wigner/recurrence.jl From 3af07b04bbe7c9ddcd0fdfc883f72101873f6440 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 28 Oct 2025 13:21:58 -0400 Subject: [PATCH 266/329] Rename/reorganize Wigner files --- src/redesign/SphericalFunctions.jl | 10 +++------- src/redesign/Wigner/wigner.jl | 5 +++++ src/redesign/Wigner/{WignerH.jl => wigner_H.jl} | 0 .../{WignerHCalculator.jl => wigner_H_calculator.jl} | 0 .../{WignerCalculator.jl => wigner_calculator.jl} | 0 .../Wigner/{WignerMatrices.jl => wigner_matrices.jl} | 0 .../{WignerMatrix.jl => Wigner/wigner_matrix.jl} | 0 7 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 src/redesign/Wigner/wigner.jl rename src/redesign/Wigner/{WignerH.jl => wigner_H.jl} (100%) rename src/redesign/Wigner/{WignerHCalculator.jl => wigner_H_calculator.jl} (100%) rename src/redesign/Wigner/{WignerCalculator.jl => wigner_calculator.jl} (100%) rename src/redesign/Wigner/{WignerMatrices.jl => wigner_matrices.jl} (100%) rename src/redesign/{WignerMatrix.jl => Wigner/wigner_matrix.jl} (100%) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index 21a462bf..51d94649 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -2,18 +2,14 @@ module Redesign import Quaternionic import TestItems: @testitem, @testsnippet -import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, FixedSizeVector -import LoopVectorization: @turbo +import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, + FixedSizeVector # TEMPORARY!!!! Should be able to remove once this moves to SphericalFunctions proper import SphericalFunctions: ComplexPowers -include("Wigner/WignerMatrix.jl") -include("Wigner/WignerCalculator.jl") -include("Wigner/WignerH.jl") -include("Wigner/WignerHCalculator.jl") -include("Wigner/recurrence.jl") +include("wigner/wigner.jl") # function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} diff --git a/src/redesign/Wigner/wigner.jl b/src/redesign/Wigner/wigner.jl new file mode 100644 index 00000000..a67a14a2 --- /dev/null +++ b/src/redesign/Wigner/wigner.jl @@ -0,0 +1,5 @@ +include("wigner_matrix.jl") +include("wigner_calculator.jl") +include("wigner_H.jl") +include("wigner_H_calculator.jl") +include("recurrence.jl") diff --git a/src/redesign/Wigner/WignerH.jl b/src/redesign/Wigner/wigner_H.jl similarity index 100% rename from src/redesign/Wigner/WignerH.jl rename to src/redesign/Wigner/wigner_H.jl diff --git a/src/redesign/Wigner/WignerHCalculator.jl b/src/redesign/Wigner/wigner_H_calculator.jl similarity index 100% rename from src/redesign/Wigner/WignerHCalculator.jl rename to src/redesign/Wigner/wigner_H_calculator.jl diff --git a/src/redesign/Wigner/WignerCalculator.jl b/src/redesign/Wigner/wigner_calculator.jl similarity index 100% rename from src/redesign/Wigner/WignerCalculator.jl rename to src/redesign/Wigner/wigner_calculator.jl diff --git a/src/redesign/Wigner/WignerMatrices.jl b/src/redesign/Wigner/wigner_matrices.jl similarity index 100% rename from src/redesign/Wigner/WignerMatrices.jl rename to src/redesign/Wigner/wigner_matrices.jl diff --git a/src/redesign/WignerMatrix.jl b/src/redesign/Wigner/wigner_matrix.jl similarity index 100% rename from src/redesign/WignerMatrix.jl rename to src/redesign/Wigner/wigner_matrix.jl From 07b99808181994467eaa5cbfa336c139dcdec127 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 9 Dec 2025 12:04:53 -0500 Subject: [PATCH 267/329] Add some references --- docs/src/notes/sampling_theorems.md | 2 +- docs/src/references.bib | 65 ++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/src/notes/sampling_theorems.md b/docs/src/notes/sampling_theorems.md index ecf6ec2b..6495b635 100644 --- a/docs/src/notes/sampling_theorems.md +++ b/docs/src/notes/sampling_theorems.md @@ -1,6 +1,6 @@ # Sampling theorems and transformations of spin-weighted spherical harmonics -[McEwen_2011](@citet) (MW) provide a very thorough review of the +[McEwenWiaux_2011](@citet) (MW) provide a very thorough review of the literature on sampling theorems related to spin-weighted spherical harmonics up to 2011. [Reinecke_2013](@citet) (RS) outlined one of the more efficient and accurate implementations of spin-weighted diff --git a/docs/src/references.bib b/docs/src/references.bib index 2f4bd112..bcbe0009 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -41,6 +41,16 @@ @article{Belikov_1991 pages = {384--410} } +@misc{BelknerEtAl_2024, + title = {{$\texttt{cunuSHT}$:} {GPU} Accelerated Spherical Harmonic Transforms on Arbitrary Pixelizations}, + url = {http://arxiv.org/abs/2406.14542}, + doi = {10.48550/arXiv.2406.14542}, + publisher = {{arXiv}}, + author = {Belkner, Sebastian and Duivenvoorden, Adriaan J. and Carron, Julien and Schaeffer, Nathanael and Reinecke, Martin}, + month = jun, + year = {2024}, +} + @article{Blanchet_2024, author = {Blanchet, Luc}, year = {2024}, @@ -131,6 +141,20 @@ @book{DoranLasenby_2010 doi = {10.1017/CBO9780511807497} } +@article{DriscollHealy_1994, + title = {Computing Fourier Transforms and Convolutions on the {2-Sphere}}, + volume = {15}, + issn = {0196-8858}, + url = {http://www.sciencedirect.com/science/article/pii/S0196885884710086}, + doi = {10.1006/aama.1994.1008}, + number = {2}, + journal = {Advances in Applied Mathematics}, + author = {Driscoll, J. R. and Healy, D. M.}, + month = jun, + year = {1994}, + pages = {202--250} +} + @book{Edmonds_2016, title = {Angular Momentum in Quantum Mechanics}, isbn = {978-1-4008-8418-6}, @@ -290,6 +314,18 @@ @article{Holmes_2002 journal = {Journal of Geodesy} } +@article{Ishioka_2018, + title = {A New Recurrence Formula for Efficient Computation of Spherical Harmonic Transform}, + volume = {96}, + url = {https://www.jstage.jst.go.jp/article/jmsj/96/2/96_2018-019/_article}, + doi = {10.2151/jmsj.2018-019}, + number = {2}, + journal = {Journal of the Meteorological Society of Japan. Ser. {II}}, + author = {Ishioka, Keiichi}, + year = {2018}, + pages = {241--249} +} + @article{Kostelec_2008, doi = {10.1007/s00041-008-9013-5}, url = {https://doi.org/10.1007/s00041-008-9013-5}, @@ -339,7 +375,7 @@ @book{Lee_2012 doi = {10.1007/978-1-4419-9982-5}, } -@article{McEwen_2011, +@article{McEwenWiaux_2011, doi = {10.1109/tsp.2011.2166394}, url = {https://doi.org/10.1109/tsp.2011.2166394}, year = 2011, @@ -379,6 +415,19 @@ @article{Newman_1966 journal = {Journal of Mathematical Physics} } +@article{PriceMcEwen_2024, + title = {Differentiable and accelerated spherical harmonic and Wigner transforms}, + volume = {510}, + issn = {0021-9991}, + url = {https://www.sciencedirect.com/science/article/pii/S0021999124003589}, + doi = {10.1016/j.jcp.2024.113109}, + journal = {Journal of Computational Physics}, + author = {Price, Matthew A. and {McEwen}, Jason D.}, + month = aug, + year = {2024}, + pages = {113109} +} + @article{Reinecke_2013, doi = {10.1051/0004-6361/201321494}, url = {https://doi.org/10.1051/0004-6361/201321494}, @@ -428,6 +477,20 @@ @book{Shankar_1994 year = 1994 } +@article{Shukowsky_1986, + title = {A quadrature formula over the sphere with application to high resolution spherical harmonic analysis}, + volume = {60}, + issn = {1432-1394}, + url = {https://doi.org/10.1007/BF02519350}, + doi = {10.1007/BF02519350}, + number = {1}, + journal = {Bulletin g\'{e}od\'{e}sique}, + author = {Shukowsky, Wladimir}, + month = mar, + year = {1986}, + pages = {1--14} +} + @misc{SommerEtAl_2018, title = {Why and How to Avoid the Flipped Quaternion Multiplication}, url = {http://arxiv.org/abs/1801.07478}, From c9333740101565dc9a934333877b1b82482e21ee Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 9 Dec 2025 12:05:40 -0500 Subject: [PATCH 268/329] Add Driscoll-Healy and McEwen-Wiaux pixelizations --- src/pixelizations.jl | 194 +++++++++++++++++++++++++++++++------------ 1 file changed, 140 insertions(+), 54 deletions(-) diff --git a/src/pixelizations.jl b/src/pixelizations.jl index 920fe963..73e4f3da 100644 --- a/src/pixelizations.jl +++ b/src/pixelizations.jl @@ -1,20 +1,22 @@ @doc raw""" golden_ratio_spiral_pixels(s, ℓₘₐₓ, [T=Float64]) -Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. -Successive pixels are separated by the azimuthal angle ``Δϕ = -2π(2-φ)``, and are uniformly distributed in ``\cos θ``. - -This is also known as the "Fibonacci sphere" or "Fibonacci lattice". - -Visually, this is a very reasonable-looking pixelization, with fairly uniform -distance between neighbors, and approximate isotropy. No two pixels will share -the same values of either ``θ`` or ``ϕ``. Also note that no point is present -on either the North or South poles. - -The returned quantity is a vector of 2-SVectors providing the spherical -coordinates of each pixel. See also [`golden_ratio_spiral_rotors`](@ref) for -the corresponding `Rotor`s. +Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. Successive pixels +are separated by the azimuthal angle ``Δϕ = 2π(2-φ)``, and are uniformly distributed in +``\cos θ``. + +This is a common angular pattern of phyllotaxis in plants, found in sunflowers, pine cones, +and artichokes, for example. Wrapped around 𝕊², this is a very reasonable-looking +pixelization, with fairly uniform distance between neighbors, and approximate isotropy. No +two pixels will share the same values of either ``θ`` or ``ϕ``. Also note that no point is +present on either the North or South poles. + +This is also known as the "Fibonacci sphere" or "Fibonacci lattice" — though he had nothing +to do with it; later authors pointed out relationships between his sequence and the golden +ratio. + +The returned quantity is a vector of 2-SVectors providing the spherical coordinates of each +pixel. See also [`golden_ratio_spiral_rotors`](@ref) for the corresponding `Rotor`s. """ function golden_ratio_spiral_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T let π = T(π), φ = T(MathConstants.φ) @@ -23,7 +25,7 @@ function golden_ratio_spiral_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T # but just represents spiraling in the opposite direction. Δϕ = 2π * (2 - φ) ϕ = (0:N-1) * Δϕ - cosθ = range(1, -1, length=N+1)[begin:end-1] .- 1/T(N) + cosθ = LinRange{T}(1, -1, N+1)[begin:end-1] .- 1/T(N) [@SVector [acos(cosθ), ϕ] for (cosθ, ϕ) in zip(cosθ, ϕ)] end end @@ -33,8 +35,8 @@ end Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. -See [`golden_ratio_spiral_pixels`](@ref) for more detailed explanation. The -quantity returned by this function is a vector of `Rotor`s providing each pixel. +See [`golden_ratio_spiral_pixels`](@ref) for more detailed explanation. The quantity +returned by this function is a vector of `Rotor`s providing each pixel. """ function golden_ratio_spiral_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T from_spherical_coordinates.(golden_ratio_spiral_pixels(s, ℓₘₐₓ, T)) @@ -44,24 +46,23 @@ end sorted_rings(s, ℓₘₐₓ, [T=Float64]) Compute locations of a series of rings labelled by ``j ∈ |s|:ℓₘₐₓ`` (analogous -to ``ℓ``), where each ring will contain ``k = 2j+1`` (analogous to ``m``) -pixels distributed evenly around the ring. These rings are then sorted, so -that the ring with the most pixels (``j = ℓₘₐₓ``) is closest to the equator, -and the next-largest ring is placed just above or below the equator (depending -on the sign of ``s``), the next just below or above, and so on. This is -generally a fairly good first guess when minimizing the condition number of -matrices used to solve for mode weights from function values. In particular, I -use this to initialize the Minimal algorithm, which is then fed into an -optimizer to fine-tune the positions of the rings. - -This function does not provide the individual pixels; it just provides the -colatitude values of the rings on which the pixels will be placed. The pixels -themselves are provided by [`sorted_ring_pixels`](@ref). +to ``ℓ``), where each ring will contain ``k = 2j+1`` (analogous to ``m``) pixels distributed +evenly around the ring. These rings are then sorted, so that the ring with the most pixels +(``j = ℓₘₐₓ``) is closest to the equator, and the next-largest ring is placed just above or +below the equator (depending on the sign of ``s``), the next just below or above, and so on. +This is generally a fairly good first guess when minimizing the condition number of matrices +used to solve for mode weights from function values. In particular, I use this to +initialize the Minimal algorithm, which is then fed into an optimizer to fine-tune the +positions of the rings. + +This function does not provide the individual pixels; it just provides the colatitude values +of the rings on which the pixels will be placed. The pixels themselves are provided by +[`sorted_ring_pixels`](@ref). """ function sorted_rings(s, ℓₘₐₓ, ::Type{T}=Float64) where T let πo2 = prevfloat(T(π)/2, s) sort( - collect(LinRange(T(0), T(π), 2+ℓₘₐₓ-abs(s)+1))[begin+1:end-1], + collect(LinRange{T}(0, π, 2+ℓₘₐₓ-abs(s)+1))[begin+1:end-1], lt=(x,y)->(abs(x-πo2) Let ``f(\theta, \phi)`` be a band-limited function such that ``\hat{f}(l, m) = 0`` for ``l +> ≥ b``. We sample the function at the equiangular grid of points ``(\theta_i, \phi_j)``, +> ``i = 0, \ldots, 2b-1``, ``j = 0, \ldots, 2b-1``, where ``\theta_i = \pi i/2b`` and +> ``\phi_j = \pi j/b``. + +The returned quantity is a vector of 2-SVectors providing the spherical coordinates of each +pixel. See also [`driscoll_healy_rotors`](@ref) for the corresponding `Rotor`s. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function driscoll_healy_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T + let π = T(π) + b = ℓₘₐₓ + 1 + [ + @SVector [π*i/2b, π*j/b] + for i ∈ 0:(2b-1) + for j ∈ 0:(2b-1) + ] + end +end +driscoll_healy_pixels(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_pixels(0, ℓₘₐₓ, T) + +@doc raw""" + driscoll_healy_rotors(s, ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels given by the [DriscollHealy_1994](@citet) equiangular grid. + +See [`driscoll_healy_pixels`](@ref) for more detailed explanation. The quantity returned by +this function is a vector of `Rotor`s providing each pixel. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function driscoll_healy_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T + from_spherical_coordinates.(driscoll_healy_pixels(s, ℓₘₐₓ, T)) +end +driscoll_healy_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_rotors(0, ℓₘₐₓ, T) + + +@doc raw""" + mcewen_wiaux_pixels([s], ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid: + +> We adopt an equiangular sampling of the sphere with sample positions given by ``\theta_t = +> \frac{\pi(2t+1)}{2\ell_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, \ell_\mathrm{max}-1\}`` +> and ``\phi_p = \frac{2 \pi p}{2\ell_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, +> 2\ell_\mathrm{max}-2\}``. In order to extend the ``\theta`` domain to ``[0, 2\pi)`` we +> simply extend the domain of the ``\theta`` index to include ``\{\ell_\mathrm{max}, +> \ell_\mathrm{max}+1, \dotsc, 2\ell_\mathrm{max}-1\}``. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function mcewen_wiaux_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T + let π = T(π) + [ + @SVector [π*(2t+1) / (2ℓₘₐₓ - 1), 2π*p / (2ℓₘₐₓ - 1)] + for t ∈ 0:(2ℓₘₐₓ - 1) + for p ∈ 0:(2ℓₘₐₓ - 2) + ] + end +end +mcewen_wiaux_pixels(ℓₘₐₓ, ::Type{T}=Float64) where T = mcewen_wiaux_pixels(0, ℓₘₐₓ, T) + +@doc raw""" + mcewen_wiaux_rotors([s], ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid. + +See [`mcewen_wiaux_pixels`](@ref) for more detailed explanation. The quantity returned by +this function is a vector of `Rotor`s providing each pixel. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function mcewen_wiaux_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T + from_spherical_coordinates.(mcewen_wiaux_pixels(s, ℓₘₐₓ, T)) +end +mcewen_wiaux_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = mcewen_wiaux_rotors(0, ℓₘₐₓ, T) From f0cb6050080d1c6a2d657518003416b26beb61de Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 10 Dec 2025 22:50:31 -0500 Subject: [PATCH 269/329] Format documentation --- .../calculations/euler_angular_momentum.jl | 6 +- docs/make.jl | 5 +- docs/src/conventions/comparisons.md | 2 +- docs/src/conventions/details.md | 28 +-- docs/src/conventions/outline.md | 6 +- docs/src/conventions/summary.md | 2 +- docs/src/index.md | 20 +- docs/src/operators.md | 237 ++++++++++-------- docs/src/transformations.md | 159 ++++++++++-- 9 files changed, 294 insertions(+), 171 deletions(-) diff --git a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl index 7b81e970..9670689f 100644 --- a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl +++ b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl @@ -210,7 +210,7 @@ macro display(expr) end nothing #hide -# And we'll need another for the angular-momentum operators in standard ``S^2`` form. +# And we'll need another for the angular-momentum operators in standard ``𝕊²`` form. conversion(∂) = ∂.subs(Dict(α => ϕ, β => θ, γ => 0)).simplify() macro display2(expr) op = string(expr.args[1]) @@ -260,7 +260,7 @@ nothing #hide #md # #md # ``` -# ## Full expressions on ``S^3`` +# ## Full expressions on ``𝕊³`` # Finally, we can actually compute the Euler components of the angular momentum operators. #md # ### ``L`` operators in terms of Euler angles @@ -373,7 +373,7 @@ commutator(Rz, Rx) # This completes independent commutator results, which are all as we expect them to be. -# ## Standard expressions on ``S^2`` +# ## Standard expressions on ``𝕊²`` # We can substitute ``(α, β, γ) \to (φ, θ, 0)`` to get the standard expressions for the # angular momentum operators on the 2-sphere. diff --git a/docs/make.jl b/docs/make.jl index 28b0ff3b..7c509eb5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -27,6 +27,7 @@ bib = CitationBibliography( ) using SphericalFunctions +using SphericalFunctions: SSHTDirect, SSHTMinimal, SSHTRS DocMeta.setdocmeta!(SphericalFunctions, :DocTestSetup, :(using SphericalFunctions); recursive=true) makedocs( @@ -41,9 +42,9 @@ makedocs( ), pages = [ "index.md", - "transformations.md", "wigner_matrices.md", "sYlm.md", + "transformations.md", "operators.md", "utilities.md", "API" => [ @@ -75,7 +76,7 @@ makedocs( "References" => "references.md", "Redesign" => "redesign.md", ], - #warnonly=true, + warnonly=true, #doctest = false, #draft=true, # Skips running code in the docs for speed ) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 39835e8b..ce42dfb2 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -730,7 +730,7 @@ covariant components: &= -i \frac{\partial}{\partial \alpha} \end{aligned} ``` -We can compare these to the [Full expressions on ``S^3``]() `@ref`, and find +We can compare these to the [Full expressions on ``𝕊³``]() `@ref`, and find that they are precisely equivalent to expressions for ``L_j`` computed in this package's conventions. diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 16ba0d75..9896297e 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -116,7 +116,7 @@ the determinant of the metric, so we have Restricting to the unit sphere, and normalizing so that the integral of 1 over the sphere is 1, we can simplify this to ```math -\int_{S^2} f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. +\int_{𝕊²} f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. ``` @@ -460,8 +460,8 @@ the Euler angles ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. Starting with Cartesian coordinates and the Euclidean norm on ``\mathbb{R}^3``, we have *constructed* the geometric algebra over that space, as well as the spaces ``\mathrm{Spin}(3) = -\mathrm{SU}(2)`` (topologically ``S^3``), ``\mathrm{SO}(3)`` -(topologically ``\mathbb{RP}^3``), and ``S^2``. We will be defining +\mathrm{SU}(2)`` (topologically ``𝕊³``), ``\mathrm{SO}(3)`` +(topologically ``\mathbb{RP}^3``), and ``𝕊²``. We will be defining complex-valued functions on these spaces, and defining operators to construct and classify them. In particular, because we have constructed the spaces, they are naturally supplied with coordinates @@ -470,16 +470,16 @@ will be using these coordinate systems to construct both the operators and functions. However, it is important to note that the coordinate systems may have singularities, which means that the spaces of coordinates may have different topologies than the spaces they -represent. For example, Euler angles have topology ``S^1 \times I -\times S^1`` instead of the ``S^3`` and ``\mathbb{RP}^3`` topologies +represent. For example, Euler angles have topology ``𝕊¹ \times I +\times 𝕊¹`` instead of the ``𝕊³`` and ``\mathbb{RP}^3`` topologies of the spaces they represent; spherical coordinates have topology -``S^1 \times I`` instead of ``S^2``. +``𝕊¹ \times I`` instead of ``𝕊²``. Defining functions on the coordinate system of a space is subtly different from defining functions on the space itself. For example, spin-weighted functions are generally written as functions of -(``S^2``) spherical coordinates. However, they *cannot* be defined as -functions on ``S^2`` itself; some notion of a reference tangent +(``𝕊²``) spherical coordinates. However, they *cannot* be defined as +functions on ``𝕊²`` itself; some notion of a reference tangent direction is needed at each point. The difference is that spherical *coordinates* supply a natural choice for the reference tangent direction: the unit vector in the ``\boldsymbol{\theta}`` direction. @@ -575,7 +575,7 @@ action being a homomorphism. usually describe these as rotations. To validate the signs here, it may be helpful to work through a simple -example involving the sphere ``S^2``. We define a function on +example involving the sphere ``𝕊²``. We define a function on spherical coordinates as ```math f(\theta, \phi) = \sin\theta \sin\phi. @@ -835,7 +835,7 @@ e^{\theta \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} full space of quaternions with arbitrary norm) are harmonic with respect to the Laplacian of the full 4-D space. We also know that ```math -\Delta_{S^{n-1}} f(x) = \Delta_{\mathbb{R}^n} f(x/|x|), +\Delta_{𝕊^{n-1}} f(x) = \Delta_{\mathbb{R}^n} f(x/|x|), ``` and ```math @@ -843,7 +843,7 @@ and = \frac{1}{r^{n-1}} \frac{\partial}{\partial r} \left( r^{n-1} \frac{\partial f}{\partial r} \right) + -\frac{1}{r^2} \Delta_{S^{n-1}} f. +\frac{1}{r^2} \Delta_{𝕊^{n-1}} f. ``` These imply that the restriction to the space of unit quaternions is not harmonic with respect to the Laplacian on the 3-sphere, but is an @@ -1121,9 +1121,9 @@ set ``\{1/c_{n,m} P_n^m(\cos\theta)`` is an orthonormal basis of ``L^2(0, \pi)`` in the ``\theta`` coordinate. Therefore, the product of these two sets is an orthonormal basis of the product space ``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate -space for ``S^2``. I would probably modify this to point out that -``(0,2\pi)`` is really ``S^1``, and then we could extend it to point -out that you can throw on another factor of ``S^1`` to cover ``S^3``, +space for ``𝕊²``. I would probably modify this to point out that +``(0,2\pi)`` is really ``𝕊¹``, and then we could extend it to point +out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, which happens to give us the Wigner D-matrices. ## Recursion relations diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index 66df9ae2..44d9a064 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -91,9 +91,9 @@ set ``\{1/c_{n,m} P_n^m(\cos\theta)`` is an orthonormal basis of ``L^2(0, \pi)`` in the ``\theta`` coordinate. Therefore, the product of these two sets is an orthonormal basis of the product space ``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate -space for ``S^2``. I would probably modify this to point out that -``(0,2\pi)`` is really ``S^1``, and then we could extend it to point -out that you can throw on another factor of ``S^1`` to cover ``S^3``, +space for ``𝕊²``. I would probably modify this to point out that +``(0,2\pi)`` is really ``𝕊¹``, and then we could extend it to point +out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, which happens to give us the Wigner D-matrices. We first define the rotor that takes ``(\hat{x}, \hat{y}, \hat{z})`` diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index f00647f9..5849e79e 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -171,7 +171,7 @@ find that Restricting to just the basis vectors, indexed as ``a,b,c``, the first of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, which is precisely the standard result. We can also lift any function on -``S^2`` to a function on ``S^3`` — or more precisely any function on +``𝕊²`` to a function on ``𝕊³`` — or more precisely any function on spherical coordinates to a function on the space of Euler angles — by the correspondence ``(\theta, \phi) \mapsto (\alpha, \beta, \gamma) = (\phi, \theta, 0)``. We can then express the angular-momentum diff --git a/docs/src/index.md b/docs/src/index.md index 0ee49a5a..ad6a3ab1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -22,15 +22,15 @@ matrices, and spin-weighted spherical harmonics ``{}_{s}Y_{\ell,m}`` (which includes the ordinary scalar spherical harmonics). Because [*both* 𝔇 *and* the harmonics are most correctly considered](@cite Boyle_2016) functions on the rotation group ``𝐒𝐎(3)`` — or more -generally, the spin group ``𝐒𝐩𝐢𝐧(3)`` that covers it — these -functions are evaluated directly in terms of quaternions. Concessions -are also made for more standard forms of spherical coordinates and -Euler angles.[^1] Among other applications, those functions permit -"synthesis" (evaluation of the spin-weighted spherical functions) of -spin-weighted spherical harmonic coefficients on regular or distorted -grids. This package also includes functions enabling efficient -"analysis" (decomposition into mode coefficients) of functions -evaluated on regular grids to high order and accuracy. +generally, the spin group ``𝐒𝐩𝐢𝐧(3) \cong 𝐒𝐔(2)`` that covers it +— these functions are evaluated directly in terms of quaternions. +Concessions are also made for more standard forms of spherical +coordinates and Euler angles.[^1] Among other applications, those +functions permit "synthesis" (evaluation of the spin-weighted +spherical functions) of spin-weighted spherical harmonic coefficients +on regular or distorted grids. This package also includes functions +enabling efficient "analysis" (decomposition into mode coefficients) +of functions evaluated on regular grids to high order and accuracy. These quantities are computed using recursion relations, which makes it possible to compute to very high ℓ values. Unlike direct @@ -46,7 +46,7 @@ memory — though it is far slower. Also note that [`DoubleFloats`](https://github.com/JuliaMath/DoubleFloats.jl) will work, and achieve significantly greater accuracy (but no greater ℓ range) than `Float64`. In all cases, results are typically accurate -to roughly ℓ times the precision of the input quaternion. +to roughly ℓ times the precision of the underlying float type. The conventions for this package are mostly inherited from — and are described in detail by — its predecessors found diff --git a/docs/src/operators.md b/docs/src/operators.md index 7bae6783..715353c8 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -1,58 +1,67 @@ # Differential operators -Spin-weighted spherical functions *cannot* be defined on the sphere ``S^2``, but -are well defined on the group ``\mathrm{Spin}(3) \cong \mathrm{SU}(2)`` or the -rotation group ``\mathrm{SO}(3)``. (See [Boyle_2016](@citet) for the -explanation.) However, this also allows us to define a variety of differential -operators acting on these functions, relating to infinitesimal motions in these -groups, acting either from the left or the right on their arguments. Right or -left matters because the groups mentioned above are all non-commutative groups. - -In general, the *left* Lie derivative of a function ``f(Q)`` over the unit -quaternions with respect to a generator of rotation ``g`` is defined as +Spin-weighted spherical functions *cannot* be defined on the sphere +``𝕊²``, but are well defined on the sphere ``𝕊³`` or its projective +version ``ℝℙ³``. See [Boyle_2016](@citet) for the explanation. These +are the spaces underlying the Lie groups ``\mathrm{Spin}(3) \cong +\mathrm{SU}(2)`` or its projective version ``\mathrm{SO}(3)``. As a +result, we can define a variety of differential operators acting on +these functions, relating to infinitesimal motions in these groups, +acting either from the left or the right on their arguments. Right or +left matters because the groups mentioned above are all +non-commutative groups. + +In general, the *left* Lie derivative of a function ``f(Q)`` over the +unit quaternions with respect to a generator of rotation ``g`` is +defined as ```math L_g(f)\{Q\} := -\frac{i}{2} \left. \frac{df\left(\exp(t\,g)\, Q\right)}{dt} \right|_{t=0}. ``` -Note that the exponential multiplies ``Q`` *on the left* — hence the name. We -will see below that this agrees with the usual definition of the -angular-momentum from physics, except that in *quantum* physics a factor of -``\hbar`` is usually included. - -So, for example, a rotation about the ``z`` axis has the quaternion ``z`` as its -generator of rotation, and ``L_z`` defined in this way agrees with [the usual -angular-momentum -operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) ``L_z`` -familiar from spherical-harmonic theory, and reduces to it when the function has -spin weight 0, but also applies to functions of general spin weight. Similarly, -we can compute ``L_x`` and ``L_y``, and take appropriate combinations to find -[the usual raising and lowering (ladder) +Note that the exponential multiplies ``Q`` *on the left* — hence the +name. We will see below that this agrees with the usual definition of +the angular-momentum from physics, except that in *quantum* physics a +factor of ``\hbar`` is usually included. + +So, for example, a rotation about the ``z`` axis has the quaternion +``z`` as its generator of rotation, and ``L_z`` defined in this way +agrees with [the usual angular-momentum +operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) +``L_z`` familiar from spherical-harmonic theory, and reduces to it +when the function has spin weight 0, but also applies to functions of +general spin weight. Similarly, we can compute ``L_x`` and ``L_y``, +and take appropriate combinations to find [the usual raising and +lowering (ladder) operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) ``L_+`` and ``L_-``. -In just the same way, we can define the *right* Lie derivative of a function -``f(Q)`` over the unit quaternions with respect to a generator of rotation ``g`` -as +In just the same way, we can define the *right* Lie derivative of a +function ``f(Q)`` over the unit quaternions with respect to a +generator of rotation ``g`` as ```math R_g(f)\{Q\} := -\frac{i}{2} \left. \frac{df\left(Q\, \exp(t\,g)\right)}{dt} \right|_{t=0}. ``` -Note that the exponential multiplies ``Q`` *on the right* — hence the name. - -This operator is less common in physics, because it represents the dependence of -the function on the choice of frame (or coordinate system), which is not usually -of interest. Multiplication on the left represents a rotation of the physical -system, while rotation on the right represents a rotation of the coordinate -system. However, this dependence on coordinate system is precisely what defines -the *spin weight* of a function, so this class of operators is relevant in -discussions of spin-weighted spherical functions. In particular, the operators -``R_\pm`` correspond (up to a sign) to the spin-raising and -lowering operators -``\eth`` and ``\bar{\eth}`` originally introduced by [Newman_1966](@citet), as -explained in greater detail by [Boyle_2016](@citet). - -Note that these definitions are *extremely* general, in that they can be used -for *any* Lie group, and for any complex-valued function on that group. And in -full generality, we have the useful properties of linearity: +Note that the exponential multiplies ``Q`` *on the right* — hence the +name. + +This operator is less common in physics, because it represents the +dependence of the function on the choice of frame (or coordinate +system), which is not usually of interest. Multiplication on the left +represents a rotation of the physical system, while rotation on the +right represents a rotation of the coordinate system. However, this +dependence on coordinate system is precisely what defines the *spin +weight* of a function, so this class of operators is relevant in +discussions of spin-weighted spherical functions. In particular, the +operators ``R_\pm`` correspond (up to a sign) to the spin-raising and +-lowering operators ``\eth`` and ``\bar{\eth}`` originally introduced +by [Newman_1966](@citet), as explained in greater detail by +[Boyle_2016](@citet). + +Note that these definitions are *extremely* general, in that they can +be used for *any* Lie group, and for any complex-valued function on +that group. And in full generality, we have the useful properties of +linearity: ```math L_{s\mathbf{a}} = sL_{\mathbf{a}} \qquad \text{and} \qquad @@ -64,11 +73,12 @@ L_{\mathbf{a}+\mathbf{b}} = L_{\mathbf{a}} + L_{\mathbf{b}} \qquad \text{and} \qquad R_{\mathbf{a}+\mathbf{b}} = R_{\mathbf{a}} + R_{\mathbf{b}}, ``` -for any scalar ``s`` and any elements of the Lie algebra ``\mathbf{a}`` and -``\mathbf{b}``. In particular, if the Lie algebra has a basis -``\mathbf{e}_{(j)}``, we use the shorthand ``L_j`` and ``R_j`` for -``L_{\mathbf{e}_{(j)}}`` and ``R_{\mathbf{e}_{(j)}}``, respectively, and we can -expand any operator in terms of these basis operators: +for any scalar ``s`` and any elements of the Lie algebra +``\mathbf{a}`` and ``\mathbf{b}``. In particular, if the Lie algebra +has a basis ``\mathbf{e}_{(j)}``, we use the shorthand ``L_j`` and +``R_j`` for ``L_{\mathbf{e}_{(j)}}`` and ``R_{\mathbf{e}_{(j)}}``, +respectively, and we can expand any operator in terms of these basis +operators: ```math L_{\mathbf{a}} = \sum_{j} a_j L_j \qquad \text{and} \qquad @@ -78,98 +88,105 @@ R_{\mathbf{a}} = \sum_{j} a_j R_j. ## Commutators -In general, for generators ``a`` and ``b``, we have the commutator relations +In general, for generators ``a`` and ``b``, we have the commutator +relations ```math \left[ L_a, L_b \right] = \frac{i}{2} L_{[a,b]} \qquad \left[ R_a, R_b \right] = -\frac{i}{2} R_{[a,b]}, ``` -where ``[a,b]`` is the commutator of the two generators, which can be obtained -directly as the commutator of the corresponding quaternions. Note the sign -difference between these two equations. The factors of ``\pm i/2`` are inherited -directly from the definitions of ``L_g`` and ``R_g`` given above, but they -appear there with the *same* sign. The sign difference between these two -commutator equations results from the fact that the quaternions are multiplied -in opposite orders in the two cases. It *could* be absorbed by defining the -operators with opposite signs.[^1] The arbitrary sign choices used above are -purely for historical reasons. - -Again, these results are valid for general (finite-dimensional) Lie groups, but -a particularly interesting case is in application to the three-dimensional -rotation group. In the following, we will apply our results to this group. - -The commutator relations for ``L`` are consistent — except for -the differing use of ``\hbar`` — with the usual relations from quantum +where ``[a,b]`` is the commutator of the two generators, which can be +obtained directly as the commutator of the corresponding quaternions. +Note the sign difference between these two equations. The factors of +``\pm i/2`` are inherited directly from the definitions of ``L_g`` and +``R_g`` given above, but they appear there with the *same* sign. The +sign difference between these two commutator equations results from +the fact that the quaternions are multiplied in opposite orders in the +two cases. It *could* be absorbed by defining the operators with +opposite signs.[^1] The arbitrary sign choices used above are purely +for historical reasons. + +Again, these results are valid for general (finite-dimensional) Lie +groups, but a particularly interesting case is in application to the +three-dimensional rotation group. In the following, we will apply our +results to this group. + +The commutator relations for ``L`` are consistent — except for the +differing use of ``\hbar`` — with the usual relations from quantum mechanics: ```math \left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. ``` -Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and index the -set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we represent an -arbitrary basis vector as ``\hat{e}_j``, then the quaternion commutator -``[a,b]`` in the expression for ``[L_a, L_b]`` becomes +Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and +index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we +represent an arbitrary basis vector as ``\hat{e}_j``, then the +quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` +becomes ```math [\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. ``` Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} -L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version frequently -seen in quantum physics. +L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version +frequently seen in quantum physics. [^1]: - In fact, we can define the left and right Lie derivative operators quite - generally, for functions on *any* Lie group and for the corresponding Lie - algebra. And in all cases (at least for finite-dimensional Lie algebras) we - obtain the same commutator relations. The only potential difference is that - it may not make sense to use the coefficient ``i/2`` in general; it was - chosen here for consistency with the standard angular-momentum operators. - If that coefficient is changed in the definitions of the Lie derivatives, - the only change to the commutator relations would the substitution of that - coefficient. - -The raising and lowering operators relative to ``L_z`` and ``R_z`` satisfy — by -definition of raising and lowering operators — the relations + In fact, we can define the left and right Lie derivative operators + quite generally, for functions on *any* Lie group and for the + corresponding Lie algebra. And in all cases (at least for + finite-dimensional Lie algebras) we obtain the same commutator + relations. The only potential difference is that it may not make + sense to use the coefficient ``i/2`` in general; it was chosen + here for consistency with the standard angular-momentum operators. + If that coefficient is changed in the definitions of the Lie + derivatives, the only change to the commutator relations would the + substitution of that coefficient. + +The raising and lowering operators relative to ``L_z`` and ``R_z`` +satisfy — by definition of raising and lowering operators — the +relations ```math [L_z, L_\pm] = \pm L_\pm \qquad [R_z, R_\pm] = \pm R_\pm. ``` -These allow us to solve — up to an overall factor — for those operators in terms -of the basic generators (again, noting the sign difference): +These allow us to solve — up to an overall factor — for those +operators in terms of the basic generators (again, noting the sign +difference): ```math L_\pm = L_x \pm i L_y \qquad R_\pm = R_x \mp i R_y. ``` -(Interestingly, this procedure also shows that rasing and lowering operators can -only exist if the factor in front of the derivatives in the definitions of -``L_g`` and ``R_g`` are pure imaginary numbers.) In particular, this results in -the commutator relations +(Interestingly, this procedure also shows that rasing and lowering +operators can only exist if the factor in front of the derivatives in +the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In +particular, this results in the commutator relations ```math [L_+, L_-] = 2L_z \qquad [R_+, R_-] = 2R_z. ``` -Here, the signs are *similar* because the two sign differences noted above -essentially cancel each other out. +Here, the signs are *similar* because the two sign differences noted +above essentially cancel each other out. -In the functions [listed below](#Module-functions), these operators are returned -as matrices acting on vectors of mode weights. As such, we can actually -evaluate these commutators as given to cross-validate the expressions and those -functions. +In the functions [listed below](#Module-functions), these operators +are returned as matrices acting on vectors of mode weights. As such, +we can actually evaluate these commutators as given to cross-validate +the expressions and those functions. ## Transformations of functions vs. mode weights -One important point to note is that mode weights transform "contravariantly" -(very loosely speaking) relative to the spin-weighted spherical functions under -some operators. For example, take the action of the ``L_+`` operator, which -acts on a SWSH as +One important point to note is that mode weights transform +"contravariantly" (very loosely speaking) relative to the +spin-weighted spherical functions under some operators. For example, +take the action of the ``L_+`` operator, which acts on a SWSH as ```math L_+ \left\{{}_{s}Y_{\ell,m}\right\} (R) = \sqrt{(\ell-m)(\ell+m+1)} {}_{s}Y_{\ell,m+1}(R). ``` -We can use this to derive the mode weights of a general spin-weighted function -``f`` under the action of this operator:[^2] +We can use this to derive the mode weights of a general spin-weighted +function ``f`` under the action of this operator:[^2] ```math \begin{aligned} \left\{L_+ f\right\}_{\ell,m} @@ -191,10 +208,10 @@ We can use this to derive the mode weights of a general spin-weighted function f_{\ell,m-1}\, \sqrt{(\ell-m+1)(\ell+m)} \end{aligned} ``` -Note that this expression (and in particular its signs) more resembles the -expression for ``L_- \left\{{}_{s}Y_{\ell,m}\right\}`` than for ``L_+ -\left\{{}_{s}Y_{\ell,m}\right\}``. Similar relations hold for the action of -``L_-``. +Note that this expression (and in particular its signs) more resembles +the expression for ``L_- \left\{{}_{s}Y_{\ell,m}\right\}`` than for +``L_+ \left\{{}_{s}Y_{\ell,m}\right\}``. Similar relations hold for +the action of ``L_-``. [^2]: A technical note about the integrals above: the integrals should be taken @@ -202,12 +219,12 @@ expression for ``L_- \left\{{}_{s}Y_{\ell,m}\right\}`` than for ``L_+ SWSHs are orthonormal. In general, this integral should be over ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when - integrated over an ``S^2`` subspace (nor even is the integral invariant). + integrated over an ``𝕊²`` subspace (nor even is the integral invariant). However, if we know that the spins are the same in both cases, it *is* - possible to integrate over an ``S^2`` subspace. + possible to integrate over an ``𝕊²`` subspace. -However, it is important to note that the same "contravariance" is not present -for the spin-raising and -lowering operators: +However, it is important to note that the same "contravariance" is not +present for the spin-raising and -lowering operators: ```math \begin{aligned} \left\{\eth f\right\}_{\ell,m} @@ -225,8 +242,8 @@ for the spin-raising and -lowering operators: f_{\ell,m}\, \sqrt{(\ell-s)(\ell+s+1)} \end{aligned} ``` -Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more "covariant" -form of transformation. +Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more +"covariant" form of transformation. ## Docstrings diff --git a/docs/src/transformations.md b/docs/src/transformations.md index ac873328..b7aef4ed 100644 --- a/docs/src/transformations.md +++ b/docs/src/transformations.md @@ -1,35 +1,140 @@ # ``s``-SHT Transformations -One important capability of this package is the transformation between the two -representations of a spin-weighted spherical function: - - 1. Values `f` of the function evaluated on a set of points or "pixels" in the - domain of the function. - 2. Values `f̃` of the mode weights (coefficients) of an expansion in the - standard spin-weighted spherical-harmonic basis. - -In the literature, the transformation `f` ↦ `f̃` is usually called "analysis" or -`map2salm`, while the inverse transformation `f` ↦ `f̃` is called "synthesis" or -`salm2map`. These are both referred to as spin-spherical-harmonic transforms, -or ``s``-SHTs. - -To describe the values of a spin-``s`` function up to some maximum angular -resolution ``\ell_\mathrm{max}``, we need ``(\ell_\mathrm{max}+1)^2 - s^2`` mode -weights. We assume throughout that the values `f̃` are stored as +Any square-integrable function on the sphere 𝕊² or 𝕊³ can be +represented as an expansion in spherical harmonics or spin-weighted +spherical harmonics, respectively. For a particular spin-weight +``s``, we can restrict the spin-weighted spherical harmonics to 𝕊² +(because its behavior on the remaining [𝕊¹ factor of +𝕊³](https://en.wikipedia.org/wiki/Hopf_fibration) is determined by +the spin). So, for example, if ``f`` is a function with spin weight +``s``, we will frequently write +```math +\begin{gathered} +f(θ, ϕ) = \sum_{ℓ = |s|}^{ℓₘₐₓ} \sum_{m = -ℓ}^{ℓ} f̃_{ℓ, m} + \, {}_{s}Y_{ℓ, m}(θ, ϕ) \\ +f(𝐑) = \sum_{ℓ = |s|}^{ℓₘₐₓ} \sum_{m = -ℓ}^{ℓ} f̃_{ℓ, m} + \, {}_{s}Y_{ℓ, m}(𝐑), +\end{gathered} +``` +where ``(θ, ϕ)`` are spherical coordinates on 𝕊², and we use the +rotor ``𝐑`` to describe a point on 𝕊³. The upper limit ``ℓₘₐₓ`` is +— in principle — infinite, but because we are finite ``ℓₘₐₓ`` will +also be finite in all applications here. Similarly, the set of points +on which we evaluate the function will also be finite. A little +terminology will be helpful: + + 1. Values ``f`` of the function evaluated on a set of points or + "pixels" in the domain of the function — sometimes called "nodes" + or the "map" values. + 2. Values ``f̃`` of the mode weights (coefficients) of the expansion + — usually called "modes" or sometimes "salm" (for ``_sa_{ℓ,m}``). + +Here, we are concerned with transformations between these two +representations of the function. In the literature, the +transformation ``f ↦ f̃`` is usually called "analysis" or `map2salm`, +while the inverse transformation ``f̃ ↦ f`` is called "synthesis" or +`salm2map`. These are both referred to as spin-spherical-harmonic +transforms, or ``s``-SHTs. + +## Synthesis + +The expressions written above already show us one way to transform +*from* modes ``f̃`` *to* function values ``f``. If the pixels on +which we evaluate are indexed by ``k``, then we can write +```math +f(𝐑_k) = \sum_{ℓ,m} f̃_{ℓ, m}\, {}_{s}Y_{ℓ, m}(𝐑_k). +``` +Considering the ``(ℓ,m)`` pairs as a single index, this is just a +matrix-vector multiplication, where the matrix elements are given by +```math +𝒯_{k, (ℓ,m)} = {}_{s}Y_{ℓ, m}(𝐑_k), +``` +and we might more compactly write +```math +f = 𝒯 \, f̃. +``` +In this expression, ``f̃`` is being treated as a column vector of mode +weights, and ``f`` is being treated as a column vector of function +values at the sequence of pixels. For instance, we will frequently +store the values ``f̃`` as the (column) vector +```julia +f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] +``` +(Here, `mode_weight` is just for illustration.) If this +transformation will be performed repeatedly, it can be very efficient +to pre-compute the matrix ``𝒯``, and capitalize on the impressive +efficiency of linear-algebra libraries to perform the transformation. +And indeed, this is the approach taken by the [`SSHTDirect`](@ref) +method. + +However, we must consider the memory requirements of this approach. +The number of nonzero mode weights is ``M = (ℓₘₐₓ+1)^2 - s^2``. If +there are ``N`` pixels in the pixelization, then the matrix ``𝒯`` has +size ``N × M``. Typically, we will use roughly the same number of +pixels as there are mode weights to be able to express roughly the +same number of degrees of freedom, so that ``N ≈ M``. Therefore, the +size of this matrix will typically be ``N × M ∼ ℓₘₐₓ^4``. For +standard complex numbers stored in 128 bits, this will exceed 1 GiB of +memory for ``ℓₘₐₓ ≳ 90``, and grow rapidly. The computational cost of +the matrix-vector multiplication will scale just as poorly. This +full-storage-matrix approach is very attractive, but only for +relatively small ``ℓₘₐₓ``. + +Another approach would be to compute the matrix elements as needed. +Given that the SWSH values are best computed via recurrence relations, +it would make sense to compute them row-by-row. This would +essentially eliminate the memory requirements. Though the +computational cost would still scale poorly, it would parallelize very +well, since each row is independent until a final summation. This is +not currently implemented, but the pieces are all in place, and it +will be implemented in the future. + +However, we can achieve significantly better performance at high +``ℓₘₐₓ`` if we select the pixelization carefully. Recall that a +spherical harmonic of *any* spin weight still varies azimuthally as +``e^{i m ϕ}``. Therefore, if we select a pixelization made up of a +series of rings, each of which is at a constant ``θ`` and has pixels +equally spaced in ``ϕ``, then we can use Fast Fourier Transforms +(FFTs) to perform the azimuthal integration very quickly. + + + + +# Analysis + +Analytically, we use orthogonality of the spin-weighted spherical +harmonics to compute the mode weights from the function values as +```math +\begin{gathered} +f̃_{ℓ, m} = \int_{𝕊²} f(θ, ϕ)\, {}_{s}Ȳ_{ℓ, m}(θ, ϕ) \, d^2Ω, \\ +f̃_{ℓ, m} = \int_{𝕊³} f(𝐑)\, {}_{s}Ȳ_{ℓ, m}(𝐑) \, d^3Ω. +\end{gathered} +``` +But — again because we are finite — we will only be able to evaluate +the function at a finite number of points, and so we will need to use +discrete quadrature to evaluate the integrals. + + + +To describe the mode weights of a spin-``s`` function up to (and +including) some maximum angular resolution ``\ell_\mathrm{max}``, +there are ``(\ell_\mathrm{max}+1)^2 - s^2`` mode weights. We assume +throughout that the values `f̃` are stored as the (column) vector ```julia f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] ``` -(Here, `mode_weight` is a made-up function intended to provide a schematic.) In -particular, the ``m`` index varies most rapidly, and the ``\ell`` index varies -most slowly. Correspondingly, there must be *at least* -``(\ell_\mathrm{max}+1)^2 - s^2`` function values `f`. However, some ``s``-SHT -algorithms require more function values — usually by a factor of 2 or 4 — -trading off between speed and memory usage. - -The `SSHT` object implements these transformations, storing pre-computed -constants and pre-allocated workspace for the transformations. The interface is -designed to be similar to that of `FFTW.jl`, whereby an `SSHT` object `𝒯` can -be used to perform the transformation as either +(Here, `mode_weight` is a made-up function for schematic purposes.) In +particular, the ``m`` index varies most rapidly, and the ``\ell`` +index varies most slowly. Correspondingly, there must be *at least* +``(\ell_\mathrm{max}+1)^2 - s^2`` function values `f`. However, some +``s``-SHT algorithms require more function values — usually by a +factor of 2 or 4 — trading off between speed and memory usage. + +The `SSHT` object implements these transformations, storing +pre-computed constants and pre-allocated workspace for the +transformations. The interface is designed to be similar to that of +`AbstractFFTs.jl`, whereby an `SSHT` object `𝒯` can be used to +perform the transformation as either ```julia f = 𝒯 * f̃ ``` From 001086c47fd71a3ef1a99ac27cd96da08a65a2ef Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 10 Dec 2025 23:01:24 -0500 Subject: [PATCH 270/329] Point to conventions discussion --- docs/src/index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index ad6a3ab1..a4346ae1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -48,10 +48,12 @@ work, and achieve significantly greater accuracy (but no greater ℓ range) than `Float64`. In all cases, results are typically accurate to roughly ℓ times the precision of the underlying float type. -The conventions for this package are mostly inherited from — and are -described in detail by — its predecessors found +The conventions for this package diverge from its predecessors found [here](https://moble.github.io/spherical_functions/) and -[here](https://moble.github.io/spherical/). +[here](https://moble.github.io/spherical/), but are described in +detail on [this page](@ref Summary) and the following pages, including +detailed comparisons to other sources that are tested automatically +with each change to this code. Note that numerous other packages cover some of these use cases, including From 08a0ecb57926f245d873a505975873b005d15fd5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 10:35:29 -0500 Subject: [PATCH 271/329] Tweak some wording --- docs/src/conventions/comparisons.md | 9 +++++---- docs/src/conventions/details.md | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index ce42dfb2..847c6ff9 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -35,9 +35,10 @@ actually used by any of these sources: One major result of this is that almost everyone since 1935 has used the same exact expression for the (scalar) spherical harmonics. -When choosing my conventions, I intend to prioritize consistency (to -the extent that any of these references actually have anything to say -about the above items) with the following sources, in order: +When choosing conventions for this package, I intend to prioritize +consistency (to the extent that any of these references actually have +anything to say about the above items) with the following sources, in +order: 1. LALSuite 2. NINJA @@ -109,7 +110,7 @@ That is, an easy calculation shows that ``` which is precisely our definition. -The spherical coordinates are implicitly defined by +The spherical coordinates are implicitly defined by this statement: > It should be noted that the polar coordinates ``\varphi, \theta`` > with respect to the original frame ``S`` of the ``z``-axis in its diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 9896297e..0fc55d12 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -127,8 +127,8 @@ of 1 over the sphere is 1, we can simplify this to Given the basis vectors ``(𝐱, 𝐲, 𝐳)`` and the Euclidean norm, we can define the [geometric algebra](https://en.wikipedia.org/wiki/Geometric_algebra). The key -feature is the geometric product, which is defined for any pair of -vectors as ``𝐯`` and ``𝐰`` as +feature is the geometric product, which we could define for any pair +of vectors as ``𝐯`` and ``𝐰`` as ```math 𝐯 𝐰 = 𝐯 ⋅ 𝐰 + 𝐯 ∧ 𝐰, ``` From 4b49b1f8b8b8136517c4c20eae07bb5fa48b110b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 10:35:38 -0500 Subject: [PATCH 272/329] Make workspaces --- Project.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Project.toml b/Project.toml index a0cbf188..d256ff0c 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,9 @@ version = "2.2.7" authors = ["Michael Boyle "] version = "2.2.8" +[workspace] +projects = ["docs", "test"] + [deps] AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" From f68ac8ee6a1eb2fdd9cd810d09b9fdc769d48d9a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 10:53:47 -0500 Subject: [PATCH 273/329] Fix version specifier --- Project.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index d256ff0c..6183d3cc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,8 +1,7 @@ name = "SphericalFunctions" uuid = "af6d55de-b1f7-4743-b797-0829a72cf84e" -version = "2.2.7" +version = "3.0.0-dev" authors = ["Michael Boyle "] -version = "2.2.8" [workspace] projects = ["docs", "test"] From c0abfd307af36abe93ea53dad9e6f66f05181627 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 13:02:18 -0500 Subject: [PATCH 274/329] Update LALSuite name --- docs/literate_input/conventions/comparisons/lalsuite_2025.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index 70d83467..77224131 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -5,7 +5,7 @@ md""" The `LALSuite` definitions of the spherical harmonics and Wigner's ``d`` and ``D`` functions agree with the definitions used in the `SphericalFunctions` package. -[`LALSuite` (the LSC Algorithm Library Suite)](@cite LALSuite_2018) is a collection of +[`LALSuite` (the LVK Algorithm Library Suite)](@cite LALSuite_2018) is a collection of routines, comprising the primary official software used by the LIGO-Virgo-KAGRA Collaboration to detect and characterize gravitational waves. As far as I can tell, the ultimate source for all spin-weighted spherical harmonics used in `LALSuite` is the function From 8ce39ebdb845e040aecf0369cf9557504acd6c31 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 18:00:23 -0500 Subject: [PATCH 275/329] Remove old marimo calculation in favor of Literate version --- .../condon_shortley_expression.py | 108 ------------------ .../comparisons/condon_shortley_1935.jl | 4 +- 2 files changed, 2 insertions(+), 110 deletions(-) delete mode 100644 docs/literate_input/condon_shortley_expression.py diff --git a/docs/literate_input/condon_shortley_expression.py b/docs/literate_input/condon_shortley_expression.py deleted file mode 100644 index fe7d67c7..00000000 --- a/docs/literate_input/condon_shortley_expression.py +++ /dev/null @@ -1,108 +0,0 @@ -import marimo - -__generated_with = "0.9.20" -app = marimo.App(width="medium") - - -@app.cell(hide_code=True) -def __(mo): - mo.md( - r""" - Eq. (15) of Sec. 4³ (page 52) of Condon and Shortley (1935) defines the polar portion of the spherical harmonic function as - - \begin{equation} - \Theta(\ell, m) = (-1)^\ell \sqrt{\frac{2\ell+1}{2} \frac{(\ell+m)!}{(\ell-m)!}} - \frac{1}{2^\ell \ell!} \frac{1}{\sin^m\theta} - \frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. - \end{equation} - - A footnote gives the first few values through $\ell=3$. I explicitly test these explicit forms in [`SphericalFunctions.jl`](https://github.com/moble/SphericalFunctions.jl)`/test/conventions/condon_shortley.jl`. Here, I want to verify that they are correct. - - Visually comparing, and accounting for some minor differences in simplification, I find that the expressions in the book are correct. I also use the explicit expressions — as implemented in the test code and translated by AI — to check that sympy can simplify the difference to 0. - """ - ) - return - - -@app.cell -def __(): - from IPython.display import display - import marimo as mo - import sympy - from sympy import S - - θ = sympy.symbols("θ", real=True) - - def ϴ(ℓ, m): - cosθ = sympy.symbols("cosθ", real=True) - return ( - (-1)**ℓ - * sympy.sqrt( - ((2*ℓ+1) / 2) - * (sympy.factorial(ℓ+m) / sympy.factorial(ℓ-m)) - ) - * (1 / (2**ℓ * sympy.factorial(ℓ))) - * (1 / sympy.sin(θ)**m) - #* sympy.diff(sympy.sin(θ)**(2*ℓ), sympy.cos(θ), ℓ-m) # Can't differentiate wrt cos(θ), so we use a dummy and substitute - * sympy.diff((1 - cosθ**2)**ℓ, cosθ, ℓ-m).subs(cosθ, sympy.cos(θ)) - ).simplify() - return S, display, mo, sympy, Θ, θ - - -@app.cell -def __(S, display, Θ): - for ℓ in range(4): - for m in range(-ℓ, ℓ+1): - display(ℓ, m, ϴ(S(ℓ), S(m))) - return l, m - - -@app.cell -def __(S, sympy, Θ, θ): - def compare_explicit_expression(ℓ, m): - if (ℓ, m) == (0, 0): - expression = sympy.sqrt(1/S(2)) - elif (ℓ, m) == (1, 0): - expression = sympy.sqrt(3/S(2)) * sympy.cos(θ) - elif (ℓ, m) == (2, 0): - expression = sympy.sqrt(5/S(8)) * (2*sympy.cos(θ)**2 - sympy.sin(θ)**2) - elif (ℓ, m) == (3, 0): - expression = sympy.sqrt(7/S(8)) * (2*sympy.cos(θ)**3 - 3*sympy.cos(θ)*sympy.sin(θ)**2) - elif (ℓ, m) == (1, 1): - expression = -sympy.sqrt(3/S(4)) * sympy.sin(θ) - elif (ℓ, m) == (1, -1): - expression = sympy.sqrt(3/S(4)) * sympy.sin(θ) - elif (ℓ, m) == (2, 1): - expression = -sympy.sqrt(15/S(4)) * sympy.cos(θ) * sympy.sin(θ) - elif (ℓ, m) == (2, -1): - expression = sympy.sqrt(15/S(4)) * sympy.cos(θ) * sympy.sin(θ) - elif (ℓ, m) == (3, 1): - expression = -sympy.sqrt(21/S(32)) * (4*sympy.cos(θ)**2*sympy.sin(θ) - sympy.sin(θ)**3) - elif (ℓ, m) == (3, -1): - expression = sympy.sqrt(21/S(32)) * (4*sympy.cos(θ)**2*sympy.sin(θ) - sympy.sin(θ)**3) - elif (ℓ, m) == (2, 2): - expression = sympy.sqrt(15/S(16)) * sympy.sin(θ)**2 - elif (ℓ, m) == (2, -2): - expression = sympy.sqrt(15/S(16)) * sympy.sin(θ)**2 - elif (ℓ, m) == (3, 2): - expression = sympy.sqrt(105/S(16)) * sympy.cos(θ) * sympy.sin(θ)**2 - elif (ℓ, m) == (3, -2): - expression = sympy.sqrt(105/S(16)) * sympy.cos(θ) * sympy.sin(θ)**2 - elif (ℓ, m) == (3, 3): - expression = -sympy.sqrt(35/S(32)) * sympy.sin(θ)**3 - elif (ℓ, m) == (3, -3): - expression = sympy.sqrt(35/S(32)) * sympy.sin(θ)**3 - else: - raise ValueError(f"Unknown {ℓ=}, {m=}") - return sympy.simplify(ϴ(S(ℓ), S(m)) - expression) == 0 - - - for _ℓ in range(4): - for _m in range(-_ℓ, _ℓ+1): - print((_ℓ, _m), " \t", compare_explicit_expression(_ℓ, _m)) - - return (compare_explicit_expression,) - - -if __name__ == "__main__": - app.run() diff --git a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl index d978d520..0523bb63 100644 --- a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl @@ -128,8 +128,8 @@ end # It may be helpful to check some values against explicit formulas for the first few # spherical harmonics as given by Condon-Shortley in the footnote to Eq. (15) of Sec. 4³ -# (page 52). Note the subtle difference between the character `Θ` defining the function above -# and the character `ϴ` defining the function below. +# (page 52). Note the subtle difference between the character `Θ` defining the function +# above and its variant, the character `ϴ`, defining the function below. ϴ(ℓ, m, 𝜃) = ϴ(Val(ℓ), Val(m), 𝜃) ϴ(::Val{0}, ::Val{0}, 𝜃) = √(1/2) ϴ(::Val{1}, ::Val{0}, 𝜃) = √(3/2) * cos(𝜃) From e3ee5d6f296f3706b66c7d495275c6ade42c8bc5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 18:01:04 -0500 Subject: [PATCH 276/329] Remove old marimo calculation in favor of Literate version --- docs/literate_input/conventions.py | 326 ------------------ .../calculations/metrics_and_integration.jl | 217 ++++++++++++ 2 files changed, 217 insertions(+), 326 deletions(-) delete mode 100644 docs/literate_input/conventions.py create mode 100644 docs/literate_input/conventions/calculations/metrics_and_integration.jl diff --git a/docs/literate_input/conventions.py b/docs/literate_input/conventions.py deleted file mode 100644 index 7565e522..00000000 --- a/docs/literate_input/conventions.py +++ /dev/null @@ -1,326 +0,0 @@ -import marimo - -__generated_with = "0.10.6" -app = marimo.App(width="medium") - - -@app.cell(hide_code=True) -def _(): - import marimo as mo - - import sympy - # import numpy as np - # import quaternion - return mo, sympy - - -@app.cell(hide_code=True) -def _(mo): - mo.md("""## Three-dimensional space""") - return - - -@app.cell -def _(): - def three_dimensional_coordinates(): - import sympy - # Make symbols representing spherical coordinates - r, θ, ϕ = sympy.symbols("r θ ϕ", real=True, positive=True, zero=False) - # Define Cartesian coordinates in terms of spherical - x = r * sympy.sin(θ) * sympy.cos(ϕ) - y = r * sympy.sin(θ) * sympy.sin(ϕ) - z = r * sympy.cos(θ) - return (x, y, z), (r, θ, ϕ) - return (three_dimensional_coordinates,) - - -@app.cell -def _(mo, sympy, three_dimensional_coordinates): - mo.output.append(mo.md("We define the spherical coordinates in terms of the Cartesian coordinates such that")) - mo.output.append( - [ - sympy.Eq( - sympy.Symbol(f"{cartesian}"), - spherical, - evaluate=False - ) - for cartesian, spherical in zip(("x", "y", "z"), three_dimensional_coordinates()[0]) - ] - ) - return - - -@app.cell -def _(mo, three_dimensional_coordinates): - def three_dimensional_metric(): - import sympy - (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() - # The Cartesian metric is just the 3x3 identity matrix - metric_cartesian = sympy.eye(3) - # Compute the coordinate transformation to obtain the spherical metric - jacobian = sympy.Matrix([x, y, z]).jacobian([r, θ, ϕ]) - metric_spherical = sympy.simplify((jacobian.T * metric_cartesian * jacobian)) - - return metric_spherical - - mo.output.append(mo.md("Using the Euclidean metric as the 3x3 identity in Cartesian coordinates, we can transform to spherical to obtain")) - mo.output.append(three_dimensional_metric()) - return (three_dimensional_metric,) - - -@app.cell -def _(mo, three_dimensional_coordinates): - def three_dimensional_coordinate_basis(): - import sympy - (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() - basis = [ - [sympy.diff(xi, coord) for xi in (x, y, z)] - for coord in (r, θ, ϕ) - ] - # Normalize each basis vector - basis = [ - sympy.Eq( - sympy.Symbol(rf"\hat{{{symbol}}}"), - sympy.Matrix([ - sympy.simplify(comp / sympy.sqrt(sum(b**2 for b in vector))).subs(abs(sympy.sin(θ)), sympy.sin(θ)) - for comp in vector - ]), - evaluate=False - ) - for symbol, vector in zip((r, θ, ϕ), basis) - ] - - return basis - - mo.output.append(mo.md( - """ - Differentiating the $(x,y,z)$ coordinates with respect to $(r, θ, ϕ)$, we - find the unit spherical coordinate basis vectors to have Cartesian components - """ - )) - mo.output.append(three_dimensional_coordinate_basis()) - return (three_dimensional_coordinate_basis,) - - -@app.cell -def _(mo, three_dimensional_coordinates, three_dimensional_metric): - def three_dimensional_volume_element(): - import sympy - (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() - metric_spherical = three_dimensional_metric() - volume_element = sympy.simplify(sympy.sqrt(metric_spherical.det())).subs(abs(sympy.sin(θ)), sympy.sin(θ)) - - return volume_element - - - mo.output.append(mo.md("The volume form in spherical coordinates is")) - mo.output.append(three_dimensional_volume_element()) - return (three_dimensional_volume_element,) - - -@app.cell -def _(mo, three_dimensional_coordinates, three_dimensional_volume_element): - def S2_normalized_volume_element(): - import sympy - (x, y, z), (r, θ, ϕ) = three_dimensional_coordinates() - volume_element = three_dimensional_volume_element().subs(r, 1) - # Integrate over the unit sphere - integral = sympy.integrate(sympy.sin(θ), (ϕ, 0, 2*sympy.pi), (θ, 0, sympy.pi)) - - return volume_element / integral - - - mo.output.append(mo.md( - """ - Restricting to the unit sphere and normalizing so that the integral - over the sphere is 1, we have the normalized volume element: - """ - )) - mo.output.append(S2_normalized_volume_element()) - return (S2_normalized_volume_element,) - - -@app.cell(hide_code=True) -def _(mo): - mo.md("""## Four-dimensional space""") - return - - -@app.cell(hide_code=True) -def _(mo): - mo.md("""### Geometric algebra""") - return - - -@app.cell -def _(): - def three_dimensional_geometric_algebra(): - from galgebra.ga import Ga - o3d, x, y, z = Ga.build("x y z", g=[1, 1, 1]) - - i = z * y - j = x * z - k = y * x - I = x * y * z - - return x, y, z, i, j, k, I - return (three_dimensional_geometric_algebra,) - - -@app.cell -def _(three_dimensional_geometric_algebra): - def check_basis_definitions(): - x, y, z, i, j, k, I = three_dimensional_geometric_algebra() - return [ - i == z*y == -y*z, - j == x*z == -z*x, - k == y*x == -x*y, - I == x*y*z == y*z*x == z*x*y == -x*z*y == -y*x*z == -z*y*x, - ] - - check_basis_definitions() - return (check_basis_definitions,) - - -@app.cell -def _(three_dimensional_geometric_algebra): - def check_duals(): - x, y, z, i, j, k, I = three_dimensional_geometric_algebra() - return [ - i == I.inv() * x, - j == I.inv() * y, - k == I.inv() * z, - ] - - check_duals() - return (check_duals,) - - -@app.cell(hide_code=True) -def _(mo): - mo.md("""### Quaternions and Euler angles""") - return - - -@app.cell -def _(three_dimensional_geometric_algebra): - def check_basis_multiplication(): - x, y, z, i, j, k, I = three_dimensional_geometric_algebra() - return [ - i*j == k, - j*k == i, - k*i == j, - i*j*k == -1, - ] - - check_basis_multiplication() - return (check_basis_multiplication,) - - -@app.cell -def _(): - def four_dimensional_coordinates(): - import sympy - # Make symbols representing spherical coordinates - R, α, β, γ = sympy.symbols("R α β γ", real=True, positive=True) - # Define Cartesian coordinates in terms of spherical - W = R * sympy.cos(β/2) * sympy.cos((α + γ)/2) - X = -R * sympy.sin(β/2) * sympy.sin((α - γ)/2) - Y = R * sympy.sin(β/2) * sympy.cos((α - γ)/2) - Z = R * sympy.cos(β/2) * sympy.sin((α + γ)/2) - - return (W, X, Y, Z), (R, α, β, γ) - return (four_dimensional_coordinates,) - - -@app.cell -def _(four_dimensional_coordinates): - def four_dimensional_metric(): - import sympy - (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() - # The Cartesian metric is just the 4x4 identity matrix - metric_cartesian = sympy.eye(4) - # Compute the coordinate transformation to obtain the spherical metric - jacobian = sympy.Matrix([W, X, Y, Z]).jacobian([R, α, β, γ]) - metric_spherical = sympy.simplify((jacobian.T * metric_cartesian * jacobian)) - - return metric_spherical - - four_dimensional_metric() - return (four_dimensional_metric,) - - -@app.cell -def _(four_dimensional_metric): - def four_dimensional_volume_element(): - import sympy - metric_spherical = four_dimensional_metric() - # Compute the volume element - volume_element = sympy.sqrt(metric_spherical.det()).simplify() - - return volume_element - - four_dimensional_volume_element() - return (four_dimensional_volume_element,) - - -@app.cell -def _(four_dimensional_coordinates, four_dimensional_volume_element, mo): - def Spin3_normalized_volume_element(): - import sympy - (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() - volume_element = four_dimensional_volume_element().subs(R, 1) - # Integrate over the unit sphere - integral = sympy.integrate(volume_element, (α, 0, 2*sympy.pi), (β, 0, 2*sympy.pi), (γ, 0, 2*sympy.pi)) - - return volume_element / integral - - - mo.output.append(mo.md( - r""" - Restricting to the unit three-sphere, $\mathrm{Spin}(3)$, and normalizing so that the integral - over the sphere is 1, we have the normalized volume element: - """ - )) - mo.output.append(Spin3_normalized_volume_element()) - return (Spin3_normalized_volume_element,) - - -@app.cell -def _(four_dimensional_coordinates, four_dimensional_volume_element, mo): - def SO3_normalized_volume_element(): - import sympy - (W, X, Y, Z), (R, α, β, γ) = four_dimensional_coordinates() - volume_element = four_dimensional_volume_element().subs(R, 1) - # Integrate over the unit sphere - integral = sympy.integrate(volume_element, (α, 0, 2*sympy.pi), (β, 0, sympy.pi), (γ, 0, 2*sympy.pi)) - - return volume_element / integral - - - mo.output.append(mo.md( - r""" - Restricting to $\mathrm{SO}(3)$ and normalizing so that the integral - over the sphere is 1, we have the normalized volume element: - """ - )) - mo.output.append(SO3_normalized_volume_element()) - return (SO3_normalized_volume_element,) - - -@app.cell(hide_code=True) -def _(mo): - mo.md("""## Rotations""") - return - - -@app.cell -def _(): - # Check that Euler angles (α, β, γ) rotate the basis vectors onto (r, \theta, \phi) - - - return - - -if __name__ == "__main__": - app.run() diff --git a/docs/literate_input/conventions/calculations/metrics_and_integration.jl b/docs/literate_input/conventions/calculations/metrics_and_integration.jl new file mode 100644 index 00000000..af259183 --- /dev/null +++ b/docs/literate_input/conventions/calculations/metrics_and_integration.jl @@ -0,0 +1,217 @@ +md""" +# Metrics and integration + +As something of an exercise, we just work through the basic definitions of metrics and +volume forms in spherical coordinates using SymPy, to verify the volume-form factor that we +use when integrating on the sphere. We then extend this to three-dimensional spherical +coordinates. + + +""" + +#src # Do this first just to hide stdout of the conda installation step. +#src # Note that we can't just use `#hide` because that still shows stdout. +# ````@setup metrics_and_integration +# import PythonCall +# import SymPyPythonCall +# ```` + +# We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. +import PythonCall +import SymPyPythonCall: sympy + +const π = sympy.pi + +## And here are some handy definitions to make manipulations look more natural: +Base.exp(x::PythonCall.Py) = x.exp() +Base.:*(x::SymPyPythonCall.Sym{PythonCall.Py}, q::PythonCall.Py) = x.o * q +(⋅)(a::PythonCall.Py, b::Int) = sympy.S(a.scalar() * b).simplify() +(⋅)(a::PythonCall.Py, b::PythonCall.Py) = sympy.S((a | b.rev()).scalar()).simplify() +nothing #hide + +# ## Three-dimensional space +# Define symbols we will use throughout, including Cartesian coordinates in terms of +# spherical +r, θ, ϕ = sympy.symbols("r θ ϕ", real=true, positive=true, zero=false) +x = r * sympy.sin(θ) * sympy.cos(ϕ) +y = r * sympy.sin(θ) * sympy.sin(ϕ) +z = r * sympy.cos(θ) +nothing #hide + +# The Cartesian metric is just the 3x3 identity matrix +metric_cartesian_3 = sympy.eye(3) + +# so the Cartesian basis ``(𝐱, 𝐲, 𝐳)`` is also an *orthonormal* basis. A point with +# coordinates ``(r, θ, ϕ)`` is separated from the origin by the vector +# ```math +# 𝐩 = r \sin θ \cos ϕ \, 𝐱 + r \sin θ \sin ϕ \, 𝐲 + r \cos θ \, 𝐳, +# ``` +# If we treat a SymPy matrix as containing the components of a vector in the Cartesian +# basis, then we write this as +𝐩 = sympy.Matrix([x, y, z]) +# and the coordinate basis vectors for spherical coordinates are given by differentiating +# this with respect to each of the spherical coordinates: +𝐫 = 𝐩.diff(r) +𝛉 = 𝐩.diff(θ) +𝛟 = 𝐩.diff(ϕ) +nothing #hide + +# We can compute the Jacobian of the coordinate transformation from Cartesian to spherical +# coordinates, and then transform to obtain the spherical metric — the metric *with respect +# to the spherical coordinate basis*: +jacobian_3 = sympy.Matrix([x, y, z]).jacobian([r, θ, ϕ]) +metric_spherical = sympy.simplify((jacobian_3.T * metric_cartesian_3 * jacobian_3)) + +# Recall that there is also an associated *orthonormal* basis, which we can obtain by +# normalizing each of the coordinate basis vectors, which just involves dividing ``𝐫``, +# ``\mathbb{θ}``, and ``\mathbb{ϕ}`` by ``1``, ``r``, and ``r \sin θ`` respectively. But +# that is not the basis we are implicitly using when — say — integrating with spherical +# coordinates, so we must use the above metric. +# +# The volume-form factor is given by the square root of the determinant of the metric: +three_volume_form_factor = sympy.simplify(sympy.sqrt(metric_spherical.det())) + +# Note that SymPy correctly includes the absolute value of the ``\sin θ`` factor, but we +# can drop the absolute value since ``θ`` is in ``(0, π)``: +three_volume_form_factor = three_volume_form_factor.subs(abs(sympy.sin(θ)), sympy.sin(θ)) + +# Restricting to the unit sphere (``r=1``), we integrate naively to find the surface area: +S2_surface_area = sympy.integrate( + sympy.integrate( + three_volume_form_factor.subs(r, 1), + (ϕ, 0, 2π), + ), + (θ, 0, π), +) + +# Therefore, the normalized volume-form factor on the unit sphere 𝕊² is +S2_normalized_volume_form_factor = sympy.simplify( + three_volume_form_factor.subs(r, 1) / S2_surface_area +) + +# ## Four-dimensional space +# +# ### Geometric algebra +# +# Below, we will relate the extended Euler coordinates to Cartesian coordinates in four +# dimensions via the quaternions. First, we set up the geometric algebra of +# three-dimensional space to check our conventions for the quaternion basis elements against +# the `galgebra` Python package. We first import the package and set up a 3D Euclidean +# space with orthonormal basis vectors ``(𝐱, 𝐲, 𝐳)``: + +## This is equivalent to `from galgebra.ga import Ga` in Python: +const Ga = PythonCall.pyimport("galgebra.ga" => "Ga") + +o3d, 𝐱, 𝐲, 𝐳 = Ga.build("𝐱 𝐲 𝐳", g=[1, 1, 1]) +nothing #hide + +# Now we define the quaternion basis elements and the pseudoscalar: +𝐢 = 𝐳 * 𝐲 +𝐣 = 𝐱 * 𝐳 +𝐤 = 𝐲 * 𝐱 +𝐈 = 𝐱 * 𝐲 * 𝐳 +nothing #hide + +# Now we check some basic identities related to these definitions: +PythonCall.pyall([ + 𝐢 == 𝐳*𝐲, + 𝐣 == 𝐱*𝐳, + 𝐤 == 𝐲*𝐱, + 𝐳*𝐲 == -𝐲*𝐳, + 𝐱*𝐳 == -𝐳*𝐱, + 𝐲*𝐱 == -𝐱*𝐲, + 𝐈 == 𝐱*𝐲*𝐳, + 𝐈 == 𝐲*𝐳*𝐱, + 𝐈 == 𝐳*𝐱*𝐲, + 𝐈 == -𝐱*𝐳*𝐲, + 𝐈 == -𝐲*𝐱*𝐳, + 𝐈 == -𝐳*𝐲*𝐱, +]) + +# Next the basic quaternion relations: +PythonCall.pyall([ + 𝐢*𝐣 == 𝐤, + 𝐣*𝐤 == 𝐢, + 𝐤*𝐢 == 𝐣, + 𝐢*𝐣*𝐤 == -1, +]) + +# And the duality relations: +PythonCall.pyall([ + 𝐢 == 𝐈.inv() * 𝐱, + 𝐣 == 𝐈.inv() * 𝐲, + 𝐤 == 𝐈.inv() * 𝐳, +]) + + +# ### Extended-Euler coordinates +# We first define the four-dimensional Cartesian coordinates in terms of Euler angles and a +# radius ``R``: +R, α, β, γ = sympy.symbols("R α β γ", real=true, positive=true, zero=false) +nothing #hide + +# The corresponding quaternion can be written as +𝐐 = R * exp(α * 𝐤 / 2) * exp(β * 𝐣 / 2) * exp(γ * 𝐤 / 2) +nothing #hide + +# Now we define the Cartesian coordinates in four dimensions via the quaternion +# components: +W = 𝐐 ⋅ 1 +#- +X = 𝐐 ⋅ 𝐢 +#- +Y = 𝐐 ⋅ 𝐣 +#- +Z = 𝐐 ⋅ 𝐤 + +# The Cartesian metric in four dimensions is just the 4x4 identity matrix +metric_cartesian_4 = sympy.eye(4) + +# and again we can compute the Jacobian of the coordinate transformation from Cartesian to +# extended-Euler coordinates, and transform to obtain the metric with respect to the +# extended-Euler coordinate basis: +jacobian_4 = sympy.Matrix([W, X, Y, Z]).jacobian([R, α, β, γ]) +metric_extended_euler = sympy.simplify((jacobian_4.T * metric_cartesian_4 * jacobian_4)) + +# And again, the volume-form factor is given by the square root of the determinant of the +# metric: +four_volume_form_factor = sympy.simplify(sympy.sqrt(metric_extended_euler.det())) + +# Again, SymPy correctly includes the absolute value of the ``\sin β`` factor, but in this +# case, it can actually be negative, since ``β`` is in ``[0, 2π]``, so it is correct for +# integration over ``\mathrm{Spin}(3)`` to include the absolute value here. +# +# Restricting to the unit sphere (``R=1``), we integrate naively to find the surface area: +S3_surface_area = sympy.integrate( + sympy.integrate( + sympy.integrate( + four_volume_form_factor.subs(R, 1), + (γ, 0, 2π), + ), + (β, 0, 2π), + ), + (α, 0, 2π) +) + +# Therefore, the normalized volume-form factor on the unit sphere 𝕊³ is +S3_normalized_volume_form_factor = sympy.simplify( + four_volume_form_factor.subs(R, 1) / S3_surface_area +) + +# And finally, we can restrict back to ``\mathrm{SO}(3)`` by taking ``β ∈ [0, π]``, and +# integrate over that range: +SO3_volume = sympy.integrate( + sympy.integrate( + sympy.integrate( + four_volume_form_factor.subs(R, 1), + (γ, 0, 2π), + ), + (β, 0, π), + ), + (α, 0, 2π) +) + +# And the normalized volume-form factor on ``\mathrm{SO}(3)`` is +SO3_normalized_volume_form_factor = sympy.simplify( + four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1) / SO3_volume +) From d45ca9a7dbe3e2105ad0de6eb5cf03cc3167c0bc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 22:22:54 -0500 Subject: [PATCH 277/329] Add galgebra to available Python packages --- docs/CondaPkg.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/CondaPkg.toml diff --git a/docs/CondaPkg.toml b/docs/CondaPkg.toml new file mode 100644 index 00000000..41a8f2df --- /dev/null +++ b/docs/CondaPkg.toml @@ -0,0 +1,3 @@ + +[pip.deps] +galgebra = "" From d8bffa9bb6e09ca135bc5dcf8fe020c83495cced Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 22:23:22 -0500 Subject: [PATCH 278/329] Wording tweaks --- .../conventions/calculations/metrics_and_integration.jl | 2 +- docs/literate_input/conventions/comparisons/lalsuite_2025.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/literate_input/conventions/calculations/metrics_and_integration.jl b/docs/literate_input/conventions/calculations/metrics_and_integration.jl index af259183..013e96ab 100644 --- a/docs/literate_input/conventions/calculations/metrics_and_integration.jl +++ b/docs/literate_input/conventions/calculations/metrics_and_integration.jl @@ -211,7 +211,7 @@ SO3_volume = sympy.integrate( (α, 0, 2π) ) -# And the normalized volume-form factor on ``\mathrm{SO}(3)`` is +# So the normalized volume-form factor on ``\mathrm{SO}(3)`` is SO3_normalized_volume_form_factor = sympy.simplify( four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1) / SO3_volume ) diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index 77224131..e45d0580 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -148,8 +148,8 @@ for (pattern, replacement) in replacements end #+ -# Finally, we just parse and evaluate the code to turn it into a runnable Julia, and we are -# done defining the module +# Finally, we just parse the code to turn it into a runnable Julia expression, evaluate it +# inside this module, and we are done defining the module. eval(Meta.parseall(lalsource)) end # module LALSuite From 449b5fe0655ef886518be0f2b246898d862193ae Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 22:23:53 -0500 Subject: [PATCH 279/329] Add development notes --- docs/Project.toml | 3 + docs/make.jl | 4 ++ docs/src/development/index.md | 17 +++++ docs/src/development/literate_testitems.md | 73 ++++++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 docs/src/development/index.md create mode 100644 docs/src/development/literate_testitems.md diff --git a/docs/Project.toml b/docs/Project.toml index 997086ca..733d4f26 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -12,3 +12,6 @@ SphericalFunctions = "af6d55de-b1f7-4743-b797-0829a72cf84e" SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" + +[sources] +SphericalFunctions = {path = ".."} diff --git a/docs/make.jl b/docs/make.jl index 7c509eb5..010c558a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -73,6 +73,10 @@ makedocs( s -> joinpath("notes", s), sort(readdir(joinpath(docs_src_dir, "notes"))) ), + "Development" => [ + "development/index.md", + "development/literate_testitems.md", + ], "References" => "references.md", "Redesign" => "redesign.md", ], diff --git a/docs/src/development/index.md b/docs/src/development/index.md new file mode 100644 index 00000000..cd9d9870 --- /dev/null +++ b/docs/src/development/index.md @@ -0,0 +1,17 @@ +# Common development tasks + +## Running tests + + + +## Building the documentation + +To build the documentation locally, run the following command from the +package root: + + julia --project=. scripts/docs.jl + +By default, this will build the documentation, run the doctests, and +launch a local server to view the docs in your web browser. + + diff --git a/docs/src/development/literate_testitems.md b/docs/src/development/literate_testitems.md new file mode 100644 index 00000000..a54cb2c1 --- /dev/null +++ b/docs/src/development/literate_testitems.md @@ -0,0 +1,73 @@ +# TestItemRunner.jl + Literate.jl = 💪 + +(With honorable mention going to the excellent +[DocumenterCitations.jl](https://juliadocs.org/DocumenterCitations.jl/stable/) +and +[FastDifferentiation.jl](https://github.com/brianguenter/FastDifferentiation.jl) +packages.) + +I have a package called +[SphericalFunctions.jl](https://moble.github.io/SphericalFunctions.jl/dev/) +that computes things like Wigner D matrices and spherical harmonics. +If you've ever dealt with these things — or even just rotations +generally — you'll know that the literature is an absolute quagmire of +subtly differing conventions wrapped up in terminology and notation +from hundreds of years ago. In an effort to sort some of this out, I +decided to carefully compare the conventions of as many significant +sources as I could — everything from Wikipedia and Mathematica, to +current quantum-mechanics textbooks, all the way back to the original +books and papers that introduced some of these concepts. So I went +through each reference, and wrote a documentation page for each one +that carefully laid out the conventions used in that source. The +documentation for my package has a whole section collecting all of +these different pages. + +But I didn't just want to *document* these conventions; I wanted to +*test* how they compared to the implementations in my package. I +wanted to feed actual numbers into the actual expressions written down +by all these sources, and see if they agreed (or all too commonly, how +they disagreed) with my package's output. But I wanted thorough tests +— too much for simple doctests. + +All these expressions are, naturally, given very similar or identical +names in the literature, which could lead to collisions; we have to +define them in a separate module for each reference. Of course, I +couldn't have all these extra inefficient and inaccurate functions +cluttering up my actual package, so these modules would have to be +defined elsewhere. + +I wanted to be able to run the tests automatically, as part of the +documentation-build process and on their own. Of course, I also +wanted to be able to test what I was writing *as I was going through* +and writing the page for any given reference, because the results of +those tests would determine what I should write in the documentation. +This meant that I needed the test suite to be as modular and fast as +possible — preferably testing just the reference I was working on, +rather than my entire test suite, so that I could iterate quickly. + +* The critical feature of Literate.jl is that it takes actual runnable + Julia scripts as input — rather than markdown that has fenced Julia + code, for example — which allows TestItemRunner.jl to find the tests + and run them properly. This does not appear to be possible with + Quarto. It looks like it *would* work with Weave.jl; I just happen + to not use it in my workflow (because I'm not sure how easy it is to + integrate with Documenter.jl and DocumenterCitations.jl). +* TODO: Use TestItemRunner as part of doc building process, with + [filtering](https://www.julia-vscode.org/docs/stable/userguide/testitems/#Filtering-support-in-TestItemRunner.jl) + to select tests in the docs. +* TODO: I'm adding `using TestItems: @testmodule, @testitem #hide` at + the top of each page; could this be put somewhere else? +* `@testmodule` and `@testitem` and their corresponding `end` + statements go on their own lines, followed by `#hide` to make the + output look nicer, while still functioning properly. Note that + `#src` is *not* the right thing to do, as it will fail to provide + those lines to Literate's execution, which needs them in order to + skip them — which we presumably want to do so that the tests won't + run and possibly pollute the namespace. +* Inside those blocks, all the code output has to end up in the same + `@example` section, because they're all part of the same `@testitem` + or whatever, which means we have to put `#+` on its own line + immediately after any code and before any non-code (markdown). +* Mention how FastDifferentiation makes it easy to test expressions + that explicitly use derivatives. +* Mention DocumenterCitations.jl for managing citations in the docs. From 5665f26ca9a621d98f6cdcd03e4e07191551031a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 22:24:07 -0500 Subject: [PATCH 280/329] Remove unused output directory --- scripts/docs.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/docs.jl b/scripts/docs.jl index 70de204a..32d674c9 100644 --- a/scripts/docs.jl +++ b/scripts/docs.jl @@ -18,7 +18,6 @@ Pkg.activate("docs") import LiveServer: servedocs literate_input = joinpath(pwd(), "docs", "literate_input") -literate_output = joinpath(pwd(), "docs", "src", "literate_output") @info "Using input for Literate.jl from $literate_input" servedocs( include_dirs=["src/"], # So that docstring changes are picked up From 6e8018c07ff2ef733a46cb387b6212b8e798d7cb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 22:24:38 -0500 Subject: [PATCH 281/329] Ignore new conventions-calculations script --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7e39ad61..b99badf2 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ docs/src/conventions/comparisons/condon_shortley_1935.md docs/src/conventions/comparisons/lalsuite_2025.md docs/src/conventions/comparisons/ninja_2011.md docs/src/conventions/comparisons/lalsuite_SphericalHarmonics.md +docs/src/conventions/calculations/metrics_and_integration.md From 5d29114f008559dbdae3cfebacb59e67e86382eb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 11 Dec 2025 22:24:54 -0500 Subject: [PATCH 282/329] Add a little structure in the redesign --- src/redesign/README.md | 107 ++++++++ src/redesign/Wigner/calculators.jl | 33 +++ src/redesign/ssht/direct.jl | 5 + src/redesign/ssht/huffenberger_wandelt.jl | 288 ++++++++++++++++++++++ src/redesign/ssht/minimal.jl | 5 + src/redesign/ssht/reinecke_seljebotn.jl | 5 + src/redesign/ssht/ssht.jl | 66 +++++ 7 files changed, 509 insertions(+) create mode 100644 src/redesign/README.md create mode 100644 src/redesign/Wigner/calculators.jl create mode 100644 src/redesign/ssht/direct.jl create mode 100644 src/redesign/ssht/huffenberger_wandelt.jl create mode 100644 src/redesign/ssht/minimal.jl create mode 100644 src/redesign/ssht/reinecke_seljebotn.jl create mode 100644 src/redesign/ssht/ssht.jl diff --git a/src/redesign/README.md b/src/redesign/README.md new file mode 100644 index 00000000..a0a5da91 --- /dev/null +++ b/src/redesign/README.md @@ -0,0 +1,107 @@ +src/ +├── SphericalFunctions.jl +├── utilities +│ ├── utilities.jl +│ ├── quadrature_weights.jl +│ ├── pixelizations.jl +│ └── complex_powers.jl +├── ssht +│ ├── ssht.jl # pixels, rotors, mul!, ldiv!, synthesis, analysis, map2salm, salm2map +│ ├── direct.jl +│ ├── minimal.jl +│ └── reinecke_seljebotn.jl +├── wigner +│ ├── wigner.jl +│ ├── wigner_matrix.jl +│ ├── wigner_calculator.jl +│ ├── wigner_H.jl +│ ├── wigner_H_calculator.jl +│ └── recurrence.jl +└── mode_weights + ├── mode_weights.jl + └── operators.jl + + +# Single-Call API +```julia +D(R::AbstractQuaternion, ℓₘₐₓ, m′ₘₐₓ=ℓₘₐₓ, m′ₘᵢₙ=-m′ₘₐₓ)::Vector{Matrix{Complex{T}}} +D(R::Vector{AbstractQuaternion}, ℓₘₐₓ, m′ₘₐₓ=ℓₘₐₓ, m′ₘᵢₙ=-m′ₘₐₓ)::Matrix{Matrix{Complex{T}}} +``` + +# Incremental/Streaming API + +```julia +# For iterative computation (saves memory, enables streaming) +calc = DCalculator(rotors, ℓₘₐₓ) + +for ℓ in 0:ℓₘₐₓ + Dˡ = next!(calc) # Compute next ℓ, returns view + # ... use Dˡ ... +end + +# Or manual control +calc = DCalculator(rotors, ℓₘₐₓ) +D⁸ = compute!(calc, 8) # Jump to specific ℓ +D⁹ = compute!(calc, 9) # Incrementally compute next +``` + +# Reusable/Mutable API + +```julia +# Allocate once, recompute for different rotors +calc = DCalculator(ℓₘₐₓ, Nᵣ) # Pre-allocate storage + +# Compute for first batch +D_matrices = compute!(calc, rotors₁) + +# Recompute for different rotors (no allocation) +compute!(calc, rotors₂) # Updates in-place + +# Access results +Dˡ = calc[ℓ] # View of Dˡ for current rotors +``` + + +* SSHT + - SSHTDirect + - SSHTMinimal + - SSHTRS + * pixels + * rotors + * mul!(f, 𝒯, f̃) + * ldiv!(f̃, 𝒯, f) + * synthesis + * analysis + +* WignerRange +* AbstractWignerMatrix{IT, NT, ST} + - WignerMatrix{IT, NT, ST} (rectangular or square sub-array of Dˡ) + - WignerDMatrix{IT, RT<:Real, ST} = WignerMatrix{IT, Complex{RT}, ST} + - WignerMatrix{IT, RT<:Real, ST} = WignerMatrix{IT, RT, ST} + - HWedge{IT, RT<:Real, ST} + - HAxis{IT, RT<:Real} (ST is implicitly FixedSizeVectorDefault{RT}) + - ?WignerdWedge (stores an HWedge; symmetrizes and adjusts phase on the fly) + - ?WignerDWedge (" ; stores vectors of powers of eⁱᵅ, eⁱᵞ) + - ?ₛYₗₘWedge (" ; stores vectors of powers of eⁱᵅ, eⁱᵞ) + +* ?AbstractY: + - ?YVector +* AbstractModeWeights: + - ModeWeightsSymmetric: 0, 1, -1, 2, -2, 3, -3, ... + - ModeWeightsIncreasing: ..., -3, -2, -1, 0, 1, 2, 3, ... + - ModeWeightsDecreasing: ..., 3, 2, 1, 0, -1, -2, -3, ... +* ModeWeightOperators + - L² + - Lx + - Ly + - Lz + - L₊ + - L₋ + - R² + - Rx + - Ry + - Rz + - R₊ + - R₋ + - ð + - ð̄ diff --git a/src/redesign/Wigner/calculators.jl b/src/redesign/Wigner/calculators.jl new file mode 100644 index 00000000..48663e01 --- /dev/null +++ b/src/redesign/Wigner/calculators.jl @@ -0,0 +1,33 @@ +struct WignerCalculator{IT, RT<:Real, NT<:Union{RT,Complex{RT}}, ST} + H::WignerHCalculator{IT, RT, ST} + e⁻ⁱᵐ′ᵅ::FixedSizeArrayDefault{NT} + e⁻ⁱᵐᵞ::FixedSizeArrayDefault{NT} + + function WignerCalculator( + ::Type{NT}, + rotors::AbstractVector{AbstractQuaternion{RT}}, + ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ + ) where {IT, RT<:Real, NT<:Union{RT,Complex{RT}}} + eⁱᵝ = FixedSizeVectorDefault{NT}(undef, length(rotors)) + e⁻ⁱᵐ′ᵅ = FixedSizeArrayDefault{NT}(undef, length(rotors), Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) + e⁻ⁱᵐᵞ = FixedSizeArrayDefault{NT}(undef, length(rotors), Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) + @inbounds for (i, j) ∈ eachindex(rotors, eⁱᵝ) + eⁱᵅ⁰, eⁱᵝ⁰, eⁱᵞ⁰ = Quaternionic.to_euler_phases(rotors[i]) + eⁱᵝ[j] = eⁱᵝ⁰ + conjexpiℓₘᵢₙα╱2, conjexpiℓₘᵢₙγ╱2 = if IT <: Rational + conj(√eⁱᵅ⁰), conj(√eⁱᵞ⁰) + else + 1, 1 + end + α = ComplexPowers(eⁱᵅ⁰) + γ = ComplexPowers(eⁱᵞ⁰) + for (k, eⁱᵏᵅ, eⁱᵏᵞ) ∈ zip(0:Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ)), α, γ) + e⁻ⁱᵐ′ᵅ[j, k+1] = conj(eⁱᵏᵅ) * conjexpiℓₘᵢₙα╱2 + e⁻ⁱᵐᵞ[ j, k+1] = conj(eⁱᵏᵞ) * conjexpiℓₘᵢₙγ╱2 + end + end + H = WignerHCalculator(eⁱᵝ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + ST = typeof(parent(Hˡ(H))) + new{IT, RT, NT, ST}(H, e⁻ⁱᵐ′ᵅ, e⁻ⁱᵐᵞ) + end +end diff --git a/src/redesign/ssht/direct.jl b/src/redesign/ssht/direct.jl new file mode 100644 index 00000000..de9c2ec0 --- /dev/null +++ b/src/redesign/ssht/direct.jl @@ -0,0 +1,5 @@ +module Direct + + + +end # module Direct diff --git a/src/redesign/ssht/huffenberger_wandelt.jl b/src/redesign/ssht/huffenberger_wandelt.jl new file mode 100644 index 00000000..bf3c35e3 --- /dev/null +++ b/src/redesign/ssht/huffenberger_wandelt.jl @@ -0,0 +1,288 @@ +module HuffenbergerWandelt + +raw""" +The Wigner ``d`` matrix corresponds to a rotation about the ``y`` axis by an angle ``β``: +``\exp\left[ \beta 𝐣 / 2 \right]``. But computing the ``d`` matrix for a general angle is +a little awkward. However, computing the ``d`` matrix for a rotation by ``π/2`` is somewhat +simpler, and computing the full ``D`` matrix for a rotation about the ``z`` axis is trivial +— it's just a phase factor. Thus, we can re-express ``d`` for a general angle ``β`` in +terms of ``d`` for the angle ``π/2`` and a phase factor. + +We start with the fact that the ``y`` axis equals the ``z`` axis rotated by ``π/2`` about +the ``x`` axis: +```math +𝐣 = e^{\frac{\pi}{2} 𝐢/ 2}\, 𝐤\, e^{-\frac{\pi}{2} 𝐢/ 2}. +``` +So we have +```math +e^{\beta 𝐣 / 2} += +e^{\frac{\pi}{2} 𝐢/ 2}\, e^{\beta 𝐤 / 2}\, e^{-\frac{\pi}{2} 𝐢/ 2}. +``` +Unfortunately, this expression involves the ``x`` axis, which we don't want. But we can +similarly express the ``x`` axis in terms of the ``y`` axis rotated about the ``z`` axis: +```math +𝐢 = e^{-\frac{\pi}{2} 𝐤/ 2}\, 𝐣\, e^{\frac{\pi}{2} 𝐤/ 2}. +``` +And now we can use this in our first expression to find +```math +e^{\beta 𝐣 / 2} += +e^{-\frac{\pi}{2} 𝐤/ 2}\, e^{\frac{\pi}{2} 𝐣/ 2}\, e^{\frac{\pi}{2} 𝐤/ 2}\, +e^{\beta 𝐤 / 2}\, +e^{\frac{\pi}{2} 𝐤/ 2}\, e^{-\frac{\pi}{2} 𝐣/ 2}\, e^{-\frac{\pi}{2} 𝐤/ 2}. +``` +Now, we can use this expansion to find an expression for the ``d`` matrix value: +```math +\begin{align} +d^{\ell}_{m', m}(\beta) +&= +\mathfrak{D}^{\ell}_{m', m}\left(e^{\beta 𝐣 / 2}\right) \\ +&= +\mathfrak{D}^{\ell}_{m', m_1}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right)\, +\mathfrak{D}^{\ell}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +\mathfrak{D}^{\ell}_{m_2, m_3}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, +\mathfrak{D}^{\ell}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ +&\quad \times +\mathfrak{D}^{\ell}_{m_4, m_5}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, +\mathfrak{D}^{\ell}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, +\mathfrak{D}^{\ell}_{m_6, m}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right) \\ +&= +\delta_{m', m_1} e^{im'\frac{\pi}{2}}\, +\mathfrak{D}^{\ell}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +\delta_{m_2, m_3} e^{-im_2\frac{\pi}{2}}\, +\mathfrak{D}^{\ell}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ +&\quad \times +\delta_{m_4, m_5} e^{-im_4\frac{\pi}{2}}\, +\mathfrak{D}^{\ell}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, +\delta_{m_6, m} e^{im\frac{\pi}{2}} \\ +&= +e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, +e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, +&\quad \times +\mathfrak{D}^{\ell}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +\mathfrak{D}^{\ell}_{m'', m'''}\left(e^{\beta 𝐤 / 2}\right)\, \\ +\mathfrak{D}^{\ell}_{m''', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +&= +e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, +e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, +&\quad \times +\mathfrak{D}^{\ell}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +e^{-im''\beta}\, +\mathfrak{D}^{\ell}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +&= +i^{m'+m-2m''}\, +d^{\ell}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +e^{-im''\beta}\, +d^{\ell}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +&= +i^{m'+m}(-1)^{m''}\, +d^{\ell}_{m', m''}\left(\frac{\pi}{2}\right)\, +e^{-im''\beta}\, +d^{\ell}_{m'', m}\left(-\frac{\pi}{2}\right) \\ +&= +i^{m'+m}(-1)^{m}\, +d^{\ell}_{m', m''}\left(\frac{\pi}{2}\right)\, +e^{-im''\beta}\, +d^{\ell}_{m'', m}\left(\frac{\pi}{2}\right) \\ +&= +i^{m'-m}\, +d^{\ell}_{m', m''}\left(\frac{\pi}{2}\right)\, +e^{-im''\beta}\, +d^{\ell}_{m'', m}\left(\frac{\pi}{2}\right) +\end{align} +``` + +""" + + + +function deducelmax(salm) + N = length(salm) + lmax = floor(Int, sqrt(N) - 1) + if (lmax + 1)^2 != N + error("Cannot deduce lmax from salm length $N") + end + return lmax +end + + +""" + map2salm(𝒯, f) + +Map function values `f` to spin-weighted spherical-harmonic mode weights `f̃` using the +`SSHT` object `𝒯`. +""" +function map2salm end +function map2salm! end + +""" + salm2map(salm, s, [lmax=deducelmax(salm), Ntheta=2lmax+1, Nphi=2lmax+1]) + +Compatibility function for `spinsfast` package. Converts mode weights of spin-weighted +function, stored in the array `salm`, to values on an equiangular grid with `Ntheta` points +in θ and `Nphi` points in ϕ. The spin weight is `s` and the maximum ℓ value in the input +data is `lmax`. + +The optional arguments `lmax`, `Ntheta`, and `Nphi` are available for backwards +compatibility, but if they are given values that are inconsistent with the defaults, an +error will be thrown. + +See also [`map2salm`](@ref) for the (rough) inverse operation. + + +# Notes + +The input `salm` data should be given in increasing order of ℓ value, always starting with +`(ℓ, m) = (0, 0)` even if `s` is nonzero, proceeding to `(1, -1)`, `(1, 0)`, `(1, 1)`, etc. +Explicitly, the ordering should match this: + + [f̃[ℓ, m] for ℓ ∈ 0:lmax for m ∈ -ℓ:ℓ] + +The output data are presented on this grid of spherical coordinates: + + [f(θ, ϕ) for θ ∈ LinRange(0, π, Ntheta), ϕ ∈ LinRange(0, 2π, Nphi+1)[begin:end-1]] + +Note that `map2salm` and `salm2map` are not true inverses of each other for several reasons. +First, modes with `ell < |s|` should always be zero; they are simply assumed to be zero on +input to `salm2map`. It is also possible to define a `map` function that violates this +assumption -- for example, having a nonzero average value over the sphere; if the function +has nonzero spin `s`, this is impossible. Also, it is possible to define a map of a +function with so much angular dependence that it cannot be captured with the given `lmax` +value. For example, a discontinuous function will never be perfectly resolved. + +""" +function salm2map(salm, s, lmax=deducelmax(salm), Ntheta=2lmax+1, Nphi=2lmax+1) + # Not implemented + error("salm2map is not yet implemented") +end + +using FFTW +using LinearAlgebra + +""" + lm_index(ℓ, m, lmax) -> Int + +Map (ℓ, m) to the flattened index matching spinsfast ordering. +Inspired by Python wrapper ordering in `python/__init__.py` lines 31–49 and C helper `ind_lm` in `cextension.c` lines ~20–53. +""" +lm_index(ℓ, m, lmax) = ℓ^2 + (m + ℓ) + 1 # 1-based for Julia + +""" + ind_lm(idx, lmax) -> (ℓ, m) + +Inverse of `lm_index`. +""" +function ind_lm(idx, lmax) + i0 = idx - 1 + ℓ = floor(Int, sqrt(i0)) + while (ℓ + 1)^2 <= i0 + ℓ += 1 + end + m = i0 - ℓ^2 - ℓ + return ℓ, m +end + +""" + salm2map(salm, s, lmax; Nθ = 2lmax+1, Nφ = 2lmax+1) + +Spin-weighted (s) spherical-harmonic synthesis to grid samples. +Follows structure of `spinsfast_salm2map` in `code/spinsfast_backward_transform.c` lines 131–200 and wrapper in `python/cextension.c` lines 56–90. +""" +function salm2map(salm::AbstractVector{<:Complex}, s::Integer, lmax::Integer; + Nθ::Integer = 2lmax + 1, Nφ::Integer = 2lmax + 1) + + Nm = 2lmax + 1 + + # Placeholder Wigner d(π/2); replace with stable helper like `wdhp_TN_helper` (see `spinsfast_backward_transform.c` lines 151–160). + wig_d_halfpi(ℓ, m, mp) = wigner_d_halfpi(ℓ, m, mp) + + # G matrix (mode-coupled intermediate), cf. `Gmm` in `spinsfast_backward_transform.c` lines 151–186. + G = zeros(ComplexF64, Nm, Nm) + + # Build G via Δ products, cf. `spinsfast_backward_Gmm` call in `spinsfast_salm2map` lines 151–160. + for ℓ in 0:lmax + for m in -ℓ:ℓ + alm = salm[lm_index(ℓ, m, lmax)] + for mp in -ℓ:ℓ + Δ1 = wig_d_halfpi(ℓ, m, mp) + Δ2 = wig_d_halfpi(ℓ, mp, s) + G[m + lmax + 1, mp + lmax + 1] += alm * Δ1 * Δ2 + end + end + end + + # FFT work array F, matching the quadrant placement in `spinsfast_backward_transform.c` lines 151–186. + F = zeros(ComplexF64, Nθ, Nφ) + limit = lmax + for mp in 0:limit + for m in 0:limit + F[mp + 1, m + 1] = G[mp + 1, m + 1] # ++ + if m > 0 + F[mp + 1, Nφ - m + 1] = G[mp + 1, Nm - m + 1] # +- + end + if mp > 0 + F[Nθ - mp + 1, m + 1] = G[Nm - mp + 1, m + 1] # -+ + end + if mp > 0 && m > 0 + F[Nθ - mp + 1, Nφ - m + 1] = G[Nm - mp + 1, Nm - m + 1] # -- + end + end + end + + # Inverse FFT (2D), mirroring `fftw_execute` usage in `spinsfast_backward_transform.c` lines 188–198. + f = ifft(ifft(F, 1), 2) .* Nm # scale to align with C conventions + return f +end + +""" + map2salm(f, s, lmax) + +Spin-weighted analysis: grid samples -> spherical-harmonic coefficients. +Inspired by forward path (`spinsfast_map2salm`) inverse of the above; quadrant extraction mirrors `spinsfast_backward_transform.c` lines 151–186. +""" +function map2salm(f::AbstractMatrix{<:Complex}, s::Integer, lmax::Integer) + Nθ, Nφ = size(f) + Nm = 2lmax + 1 + + # Forward FFT (2D), inverse of salm2map’s iFFT; normalization mirrors scaling above. + F = fft(fft(f, 1), 2) ./ Nm + + # Reconstruct G from quadrants, inverse of the packing in `salm2map`. + G = zeros(ComplexF64, Nm, Nm) + for mp in 0:lmax + for m in 0:lmax + G[mp + 1, m + 1] = F[mp + 1, m + 1] # ++ + if m > 0 + G[mp + 1, Nm - m + 1] = F[mp + 1, Nφ - m + 1] # +- + end + if mp > 0 + G[Nm - mp + 1, m + 1] = F[Nθ - mp + 1, m + 1] # -+ + end + if mp > 0 && m > 0 + G[Nm - mp + 1, Nm - m + 1] = F[Nθ - mp + 1, Nφ - m + 1] # -- + end + end + end + + # Placeholder Wigner d(π/2); replace with stable helper (see `spinsfast_backward_transform.c` lines 151–160). + wig_d_halfpi(ℓ, m, mp) = wigner_d_halfpi(ℓ, m, mp) + + # Project G back to alm, paralleling the inverse of `spinsfast_backward_Gmm`. + salm = zeros(ComplexF64, (lmax + 1)^2) + for ℓ in 0:lmax + for m in -ℓ:ℓ + acc = 0.0 + 0im + for mp in -ℓ:ℓ + Δ1 = wig_d_halfpi(ℓ, m, mp) + Δ2 = wig_d_halfpi(ℓ, mp, s) + acc += conj(Δ1) * conj(Δ2) * G[m + lmax + 1, mp + lmax + 1] + end + salm[lm_index(ℓ, m, lmax)] = acc + end + end + return salm +end + + +end # module HuffenbergerWandelt diff --git a/src/redesign/ssht/minimal.jl b/src/redesign/ssht/minimal.jl new file mode 100644 index 00000000..5f1b6a74 --- /dev/null +++ b/src/redesign/ssht/minimal.jl @@ -0,0 +1,5 @@ +module Minimal + + + +end # module Minimal diff --git a/src/redesign/ssht/reinecke_seljebotn.jl b/src/redesign/ssht/reinecke_seljebotn.jl new file mode 100644 index 00000000..ab5da841 --- /dev/null +++ b/src/redesign/ssht/reinecke_seljebotn.jl @@ -0,0 +1,5 @@ +module ReineckeSeljebotn + + + +end # module ReineckeSeljebotn diff --git a/src/redesign/ssht/ssht.jl b/src/redesign/ssht/ssht.jl new file mode 100644 index 00000000..c553cc2c --- /dev/null +++ b/src/redesign/ssht/ssht.jl @@ -0,0 +1,66 @@ +"""Supertype of storage for spin-spherical-harmonic transforms""" +abstract type SSHT{T<:Real} end + +""" + pixels(𝒯) + +Return the spherical coordinates (θ, ϕ) on which the spin-weighted spherical harmonics are +evaluated. See also [`rotors`](@ref), which provides the actual `Rotor`s on which they are +evaluated. +""" +function pixels end + + +""" + rotors(𝒯) + +Return the `Rotor`s on which the spin-weighted spherical harmonics are evaluated. See also +[`pixels`](@ref), which provides the corresponding spherical coordinates. +""" +function rotors end + + +# mul!, ldiv!, synthesis, analysis, map2salm, salm2map + + +""" + synthesis(𝒯, f̃) + synthesis!(f, 𝒯, f̃) + synthesize(𝒯, f̃) + synthesize!(f, 𝒯, f̃) + +Synthesize function values `f` from spin-weighted spherical-harmonic mode weights `f̃` using +the `SSHT` object `𝒯`. +""" +function synthesis end +function synthesis! end +function synthesize end +function synthesize! end + +""" + analysis(𝒯, f) + analysis!(f̃, 𝒯, f) + analyze(𝒯, f) + analyze!(f̃, 𝒯, f) + +Analyze function values `f` to obtain spin-weighted spherical-harmonic mode weights `f̃` +using the `SSHT` object `𝒯`. +""" +function analysis end +function analysis! end +function analyze end +function analyze! end + + +function Base.show(io::IO, 𝒯::SSHT) + print(io, typeof(𝒯), "($(𝒯.s), $(𝒯.ℓₘₐₓ))") +end + + +inplaceable(s, ℓₘₐₓ, Rθϕ) = (ℓₘₐₓ + 1)^2 - s^2 == length(Rθϕ) + + +include("ssht/direct.jl") +include("ssht/minimal.jl") +include("ssht/reinecke_seljebotn.jl") +include("ssht/huffenberger_wandelt.jl") From 2b969b3b2e78280bf7c8ec7c26903ca671fb6ade Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 13 Dec 2025 15:58:00 -0500 Subject: [PATCH 283/329] Add ability to filter tests on command line --- Project.toml | 4 +- docs/src/development/index.md | 73 +++++++++++++++++- docs/src/development/literate_testitems.md | 4 +- scripts/test.jl | 4 +- test/Project.toml | 26 +++++++ test/runtests.jl | 90 +++++++++++++++++++++- 6 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 test/Project.toml diff --git a/Project.toml b/Project.toml index 6183d3cc..2d9c7edd 100644 --- a/Project.toml +++ b/Project.toml @@ -25,6 +25,7 @@ TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] AbstractFFTs = "1" Aqua = "0.8" +ArgParse = "1.2" CondaPkg = "0.2" Coverage = "1.6" DoubleFloats = "1" @@ -53,6 +54,7 @@ julia = "1.6" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" @@ -75,4 +77,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "CondaPkg", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "Printf", "ProgressMeter", "PythonCall", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] +test = ["Aqua", "ArgParse", "CondaPkg", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "Printf", "ProgressMeter", "PythonCall", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] diff --git a/docs/src/development/index.md b/docs/src/development/index.md index cd9d9870..3476f8b5 100644 --- a/docs/src/development/index.md +++ b/docs/src/development/index.md @@ -2,6 +2,73 @@ ## Running tests +You can run all tests with coverage and process that coverage *from +the package root* with: + +```bash +julia --project=. scripts/test.jl +``` + +Optionally, at the end of this command, specify names of individual +tests to run (in quotes if there are spaces), tags of tests to run +(which must start with a colon), or files to run all tests in (which +must end with `.jl`). If any are specified, only matching tests or +files will be run. By default, all tests in all files will be run. + +Optionally, either with or without any of the above specifications, +add `--skip` followed by one or more tests, tags, or files to skip. +These override any inclusion criteria specified earlier in the +command. + +The names of individual tests or files can be given as regex patterns +(probably in quotes), and all such matches will be via `occursin` +matching, so that partial matches will work. Tags must be given +exactly as they appear in the code (including the colon). + +Here are some example invocations: + +```bash +# Run all tests in all files +julia --project=. scripts/test.jl + +# Run everything in complex_powers.jl +julia --project=. scripts/test.jl complex_powers.jl + +# Run only the ComplexPowers test +julia --project=. scripts/test.jl ComplexPowers + +# Run everything in complex_powers.jl except ComplexPowers +julia --project=. scripts/test.jl complex_powers.jl --skip ComplexPowers + +# Run everything in every file except ComplexPowers +julia --project=. scripts/test.jl --skip ComplexPowers + +# Run only tests tagged :fast +julia --project=. scripts/test.jl :fast + +# Run everything except tests tagged :slow +julia --project=. scripts/test.jl --skip :slow +``` + + +## Writing tests and coverage + +Tags can be added to individual test items, which can then be used either in the VS Code interface or the command line to include or exclude certain tests. + +```julia +@testitem "My testitem" tags=[:skipci, :slow] begin + @test my_function() == expected_value +end +``` + +It's a well hidden fact that you can turn coverage on and off by +adding certain comments around the code you don't want to check: + +```julia +# COV_EXCL_START +untested_code_that_wont_show_up_in_coverage() +# COV_EXCL_STOP +``` ## Building the documentation @@ -9,9 +76,9 @@ To build the documentation locally, run the following command from the package root: - julia --project=. scripts/docs.jl +```bash +julia --project=. scripts/docs.jl +``` By default, this will build the documentation, run the doctests, and launch a local server to view the docs in your web browser. - - diff --git a/docs/src/development/literate_testitems.md b/docs/src/development/literate_testitems.md index a54cb2c1..cbf42f70 100644 --- a/docs/src/development/literate_testitems.md +++ b/docs/src/development/literate_testitems.md @@ -1,4 +1,6 @@ -# TestItemRunner.jl + Literate.jl = 💪 +# Literate TestItems + +TestItemRunner.jl + Literate.jl = 💪 (With honorable mention going to the excellent [DocumenterCitations.jl](https://juliadocs.org/DocumenterCitations.jl/stable/) diff --git a/scripts/test.jl b/scripts/test.jl index aecfa8c1..80451682 100644 --- a/scripts/test.jl +++ b/scripts/test.jl @@ -1,8 +1,6 @@ # Call this from the top-level directory as # julia -t auto scripts/test.jl -# Optionally, specify the name of a top-level test group — e.g., ssht — -# at the end of this command to only run the tests in that group. Or add -# --help at the end to see the possibilities. +# See docs/src/developments/index.md for more information. import Dates println("Running tests starting at ", Dates.format(Dates.now(), "HH:MM:SS"), ".") diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 00000000..c00d978f --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,26 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" +FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FastDifferentiation = "eb9bf01b-bf85-4b60-bf87-ee5de06c00be" +FastTransforms = "057dd010-8810-581a-b7be-e3fc3b93f78c" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SphericalFunctions = "af6d55de-b1f7-4743-b797-0829a72cf84e" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" + +[sources] +SphericalFunctions = {path = ".."} diff --git a/test/runtests.jl b/test/runtests.jl index e73befde..0f68898b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,91 @@ +# See docs/src/developments/index.md for details of how to run tests with this script. + using TestItemRunner +using ArgParse + +function parse_commandline() + settings = ArgParseSettings() + @add_arg_table! settings begin + # Collect everything before the optional `--skip` + "run" + nargs = '*' + # Collect everything after the optional `--skip` + "--skip" + nargs = '*' + default = String[] + end + parsed_args = parse_args(settings) + run_files = Tuple(Regex(s) for s ∈ parsed_args["run"] if endswith(s, ".jl")) + run_tags = Tuple(Symbol(s[2:end]) for s ∈ parsed_args["run"] if startswith(s, ":")) + run_tests = Tuple(Regex(s) for s ∈ parsed_args["run"] if !endswith(s, ".jl") && !startswith(s, ":")) + skip_files = Tuple(Regex(s) for s ∈ parsed_args["skip"] if endswith(s, ".jl")) + skip_tags = Tuple(Symbol(s[2:end]) for s ∈ parsed_args["skip"] if startswith(s, ":")) + skip_tests = Tuple(Regex(s) for s ∈ parsed_args["skip"] if !endswith(s, ".jl") && !startswith(s, ":")) + return run_files, run_tags, run_tests, skip_files, skip_tags, skip_tests +end + +const run_files, run_tags, run_tests, skip_files, skip_tags, skip_tests = parse_commandline() + +# Get the `CI` environment variable, defaulting to "false" if not set +const CI = get(ENV, "CI", "false") == "true" + +# Create the function that will filter which tests to run +function filter(testitem) + # Destructure the input NamedTuple. Note that `filename` is the full path to the file + # containing the test item, and `name` is the full string used to name the test item, + # while `tags` is a vector of `Symbol`s that can be used to tag test items. + (; filename, name, tags) = testitem + + if CI && :skipci ∈ tags + return false + end + + for skip ∈ skip_files + if occursin(skip, filename) + @info "Skipping test '$name' in file '$(relpath(filename))' " * + "due to skip filter '$skip'." + return false + end + end + + for skip ∈ skip_tags + if tag ∈ tags + @info "Skipping test '$name' tagged '$tag' due to skip filter." + return false + end + end + + for skip ∈ skip_tests + if occursin(skip, name) + @info "Skipping test '$name' due to skip filter '$skip'." + return false + end + end + + if !isempty(run_files) || !isempty(run_tags) || !isempty(run_tests) + for run ∈ run_files + if occursin(run, filename) + return true + end + end + for run ∈ run_tags + if run ∈ tags + return true + end + end + for run ∈ run_tests + if occursin(run, name) + return true + end + end + # @info "Excluding test '$name' in file '$(relpath(filename))' because " * + # "it does not match any requested tests." + return false + else + return true + end +end + +@info "Filtering tests with" run_files run_tags run_tests skip_files skip_tags skip_tests CI -@run_package_tests verbose = true +@run_package_tests verbose=true filter=filter From 0ae2a43c6784c8c141bc57c579ef60f4970ec124 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 13 Dec 2025 16:04:25 -0500 Subject: [PATCH 284/329] Clarify behavior with :skipci tags --- docs/src/development/index.md | 5 +++++ test/runtests.jl | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/src/development/index.md b/docs/src/development/index.md index 3476f8b5..4823b392 100644 --- a/docs/src/development/index.md +++ b/docs/src/development/index.md @@ -25,6 +25,11 @@ The names of individual tests or files can be given as regex patterns matching, so that partial matches will work. Tags must be given exactly as they appear in the code (including the colon). +Note that the tests with the `:skipci` tag will be skipped whenever +the environment variable `CI` is set to "true" (which is the case on +GitHub Actions), even if it is explicitly included in the command +line. + Here are some example invocations: ```bash diff --git a/test/runtests.jl b/test/runtests.jl index 0f68898b..c7e8a55f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -37,6 +37,7 @@ function filter(testitem) (; filename, name, tags) = testitem if CI && :skipci ∈ tags + @info "Skipping test '$name' tagged ':skipci' because `CI` is true." return false end From 41e93028f7bd3fff428217eda230dd10595702b1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 16 Dec 2025 22:55:29 -0500 Subject: [PATCH 285/329] Clarify :skipci behavior --- docs/src/development/index.md | 19 ++++++++++--------- test/runtests.jl | 21 ++++++++++++++------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/docs/src/development/index.md b/docs/src/development/index.md index 4823b392..90c273cd 100644 --- a/docs/src/development/index.md +++ b/docs/src/development/index.md @@ -18,22 +18,23 @@ files will be run. By default, all tests in all files will be run. Optionally, either with or without any of the above specifications, add `--skip` followed by one or more tests, tags, or files to skip. These override any inclusion criteria specified earlier in the -command. +command. Note that everything before `--skip` constitutes inclusion +criteria; everything after constitutes exclusion criteria. The names of individual tests or files can be given as regex patterns (probably in quotes), and all such matches will be via `occursin` matching, so that partial matches will work. Tags must be given exactly as they appear in the code (including the colon). -Note that the tests with the `:skipci` tag will be skipped whenever -the environment variable `CI` is set to "true" (which is the case on -GitHub Actions), even if it is explicitly included in the command -line. +Note that any test with the `:skipci` tag will be skipped whenever the +environment variable `CI` is set to "true" (which is the case on +GitHub Actions), unless it is explicitly included in the command line +as a test to run. Here are some example invocations: ```bash -# Run all tests in all files +# Run all tests in all files (except those tagged :skipci if CI=true) julia --project=. scripts/test.jl # Run everything in complex_powers.jl @@ -42,16 +43,16 @@ julia --project=. scripts/test.jl complex_powers.jl # Run only the ComplexPowers test julia --project=. scripts/test.jl ComplexPowers -# Run everything in complex_powers.jl except ComplexPowers +# Run everything in complex_powers.jl except ComplexPowers (or :skipci if CI=true) julia --project=. scripts/test.jl complex_powers.jl --skip ComplexPowers -# Run everything in every file except ComplexPowers +# Run everything in every file except ComplexPowers (or :skipci if CI=true) julia --project=. scripts/test.jl --skip ComplexPowers # Run only tests tagged :fast julia --project=. scripts/test.jl :fast -# Run everything except tests tagged :slow +# Run everything except tests tagged :slow (or :skipci if CI=true) julia --project=. scripts/test.jl --skip :slow ``` diff --git a/test/runtests.jl b/test/runtests.jl index c7e8a55f..171979f5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -36,11 +36,6 @@ function filter(testitem) # while `tags` is a vector of `Symbol`s that can be used to tag test items. (; filename, name, tags) = testitem - if CI && :skipci ∈ tags - @info "Skipping test '$name' tagged ':skipci' because `CI` is true." - return false - end - for skip ∈ skip_files if occursin(skip, filename) @info "Skipping test '$name' in file '$(relpath(filename))' " * @@ -66,25 +61,37 @@ function filter(testitem) if !isempty(run_files) || !isempty(run_tags) || !isempty(run_tests) for run ∈ run_files if occursin(run, filename) + # @warn "Dry run: including test '$name' in file '$(relpath(filename))' " * + # "due to run filter '$run'." + # return false return true end end for run ∈ run_tags if run ∈ tags + # @warn "Dry run: including test '$name' tagged '$run' due to run filter." + # return false return true end end for run ∈ run_tests if occursin(run, name) + # @warn "Dry run: including test '$name' due to run filter '$run'." + # return false return true end end # @info "Excluding test '$name' in file '$(relpath(filename))' because " * # "it does not match any requested tests." return false - else - return true end + + if CI && :skipci ∈ tags && :skipci ∉ run_tags + @info "Skipping test '$name' tagged ':skipci' because `CI` is true." + return false + end + + return true end @info "Filtering tests with" run_files run_tags run_tests skip_files skip_tags skip_tests CI From ddda1780c1b577eefd1e989943684b69b4a10dc4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 18 Dec 2025 15:58:27 -0500 Subject: [PATCH 286/329] Clarify some of the oldest references --- .gitignore | 4 + .../conventions/comparisons/clifford_1878.jl | 47 ++++++++ .../conventions/comparisons/euler_1776.jl | 34 ++++++ .../conventions/comparisons/hamilton_1844.jl | 58 ++++++++++ .../conventions/comparisons/tait_1868.jl | 68 ++++++++++++ docs/src/development/literate_testitems.md | 69 +++++++++--- docs/src/references.bib | 104 ++++++++++++++++++ 7 files changed, 370 insertions(+), 14 deletions(-) create mode 100644 docs/literate_input/conventions/comparisons/clifford_1878.jl create mode 100644 docs/literate_input/conventions/comparisons/euler_1776.jl create mode 100644 docs/literate_input/conventions/comparisons/hamilton_1844.jl create mode 100644 docs/literate_input/conventions/comparisons/tait_1868.jl diff --git a/.gitignore b/.gitignore index b99badf2..ea76e044 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ docs/src/conventions/comparisons/lalsuite_2025.md docs/src/conventions/comparisons/ninja_2011.md docs/src/conventions/comparisons/lalsuite_SphericalHarmonics.md docs/src/conventions/calculations/metrics_and_integration.md +docs/src/conventions/comparisons/euler_1776.md +docs/src/conventions/comparisons/clifford_1878.md +docs/src/conventions/comparisons/hamilton_1844.md +docs/src/conventions/comparisons/tait_1868.md diff --git a/docs/literate_input/conventions/comparisons/clifford_1878.jl b/docs/literate_input/conventions/comparisons/clifford_1878.jl new file mode 100644 index 00000000..548b79d7 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/clifford_1878.jl @@ -0,0 +1,47 @@ +md""" +# Clifford (1878) + +[Clifford_1878](@citet) introduced Clifford algebras in 1878. The paper starts off with an +introduction to quaternions — though interestingly from a "projective" perspective.[^1] This +first section is a bit muddled from a modern perspective, but Clifford is using it to +develop his ideas. Eventually, he gets to a definition that says + +> the symbols ``ι₁, ι₂, ι₃`` may be taken to mean unit vectors along the axes. + +[^1]: + + That is, in addition to what we would recognize as basis vectors ``ι₁, ι₂, ι₃``, he + includes an element ``ι₀`` that is "at an infinite distance" from the others. This + appears to be because he recognizes the importance of what we now call the even + subalgebra of the Clifford algebra, and wants to be able to include the pseudoscalar in + that subalgebra — which cannot happen with a three-dimensional vector space alone. + +But he actually settles on *negative* squares for these basis elements: + +> We are therefore obliged to write ``ι₂² = -1``, and in a similar way we may find ``ι₁² = +> ι₃² = -1``. + +This differs from our modern treatments that usually assume that the basis vectors square to +``+1``. To compensate for this, he goes on to identify Hamilton's quaternions with products +of pairs of ``ι₁, ι₂, ι₃`` that happen to be exactly the *negative* of modern +definitions — or at least the ones we use here: + +```math +i = ι₂ ι₃, +\quad +j = ι₃ ι₁, +\quad +k = ι₁ ι₂. +``` + +Again, only in later sections does the exposition become clearer and more modern. There, he +returns to quaternions briefly, and manages to describe them in exactly the way we would +today, in terms of the even subalgebra of the Clifford algebra of three-dimensional space. +(He even notes that they are isomorphic to the *full* Clifford algebra of two-dimensional +space.) + +Clifford — like Hamilton and Tait — failed to recognize the vital importance of conjugation +(or "sandwiching") when using quaternions to represent rotations; presumably by analogy with +complex numbers, he only considered left-multiplication by a quaternion as the operation + +""" diff --git a/docs/literate_input/conventions/comparisons/euler_1776.jl b/docs/literate_input/conventions/comparisons/euler_1776.jl new file mode 100644 index 00000000..75e742f7 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/euler_1776.jl @@ -0,0 +1,34 @@ +md""" +# Euler (1776) + +!!! info "Summary" + + Euler never actually used a succession of three simple rotations to represent a general + rotation, so the term "Euler angles" is a misnomer — besmirching Euler's good name. + Euler did, however, introduce the axis-angle representation and the direction-cosine + matrix, which are important concepts in analyzing rotations. + + +Euler wrote a lot about rigid-body dynamics, + +The poor man's name has been besmirched by association with "Euler angles", but his original +work never expresses rotations as a product of three rotations about fixed axes. Rather, he +came up with the axis-angle representation, and essentially devised the direction cosine +matrix as we know it today. His 1776 papers [Euler_1776a](@cite) and [Euler_1776b](@cite) +derived these ideas in service of understanding rigid-body dynamics — rather than any purely +mathematical investigations. + +Nonetheless, by 1868, Tait [Tait_1868](@cite) writes down a rotation in exactly what we +would now call Euler angles — which he calls "the usual angles ψ, θ, ϕ"! So somehow, this +had become normalized by that time. + +Whittaker has a section I.9 on "Euler's parametric specification of rotations round a point" +and cites sections 6 and following of [Euler_1776b](@cite). That section, I would say, +correctly cites Euler's work on direction cosines. However, Whittaker's section I.10 on +"The Eulerian angles" cites Euler's *preceding* work [Euler_1776a](@cite), which does not + +As far as I can find, Davenport is actually the first person to come along and try to +actually show when it is possible to decompose an arbitrary rotation into three rotations +about fixed axes [Davenport_1973](@cite). + +""" diff --git a/docs/literate_input/conventions/comparisons/hamilton_1844.jl b/docs/literate_input/conventions/comparisons/hamilton_1844.jl new file mode 100644 index 00000000..95d3b4bb --- /dev/null +++ b/docs/literate_input/conventions/comparisons/hamilton_1844.jl @@ -0,0 +1,58 @@ +md""" +# Hamilton (1844 and 1853) + +[Hamilton_1844](@cite) is the foundational work on quaternions, where William Rowan Hamilton +first introduced them in 1844. On page 492, he introduces the quaternion, denoted as ``a + +i b + j c + k d``, or ``(a, b, c, d)``. He quickly establishes the multiplication rules for +the basis elements ``(i, j, k)`` as +```math +\begin{gathered} +i^2 = j^2 = k^2 = -1; +\quad +ij = -ji = k; +\quad +jk = -kj = i; +\quad +ki = -ik = j. +\end{gathered} +``` +He doesn't include the common ``ijk = -1`` explicitly, but it follows from the above. + +He also introduces a parameterization: +```math +a = μ \cos ϱ, +b = μ \sin ϱ \cos ϕ, +c = μ \sin ϱ \sin ϕ \cos ψ, +d = μ \sin ϱ \sin ϕ \sin ψ. +``` +He then says (emphasis in original) + +> I would call ``ϱ`` the *amplitude* of the quaternion; ``ϕ`` its *colatitude*; and ``ψ`` +> its *longitude*. The *modulus* is ``μ``. + +(Note that Hamilton is using the archaic meaning of the word "amplitude", which was the +standard term from complex analysis in the early 1800s, but which we would now call the +"argument" or "angle"; he uses "modulus" where physicists and engineers would now frequently +use "amplitude" or "magnitude".) This is similar to the axis-angle representation of +rotations, where ``ϱ`` is the (whole, not half) angle of rotation, and ``(ϕ, ψ)`` are +spherical coordinates for the axis of rotation — except that this would imply that ``i`` +corresponds to the ``z`` axis if we take the name "colatitude" seriously. And the angle +``ψ`` is measured from the ``j`` axis, so we would probably identify ``j`` with the ``x`` +axis. So ``i, j, k`` correspond to the axes ``z, x, y``. + +There is no mention of "Euler angles" in Hamilton's introduction of quaternions. + + +[Hamilton_1853](@cite) is Hamilton's later monograph on quaternions, published in 1853. In +the table of contents — which is really a collection of summaries of each section — we find +in Lecture II §x this statement (emphasis in original): + +> the symbols ``i, j, k`` come to denote here *three rectangular vector-units* (supposed +> usually, in these Lectures, to be in the directions of *south*, *west*, and *up*) + +This is a *left*-handed system. One sometimes sees claims that the quaternions are somehow +"inherently" left-handed, but this is simply not true; Hamilton could have just as easily +chosen a right-handed system, and later authors generally did. + + +""" diff --git a/docs/literate_input/conventions/comparisons/tait_1868.jl b/docs/literate_input/conventions/comparisons/tait_1868.jl new file mode 100644 index 00000000..e5046389 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/tait_1868.jl @@ -0,0 +1,68 @@ +md""" +# Tait (1867 and 1868) + +[Tait_1867](@cite) provided an elementary introduction to quaternions, which was full of +examples and applications. He was quite disinterested in mathematics *per se*; his focus +was on applying mathematics in general — and quaternions specifically — to physics. Recall +that, at this time, vector algebra had not yet been developed, so quaternions were one of +the few available tools for dealing with three-dimensional quantities. (The tragic +quaternion-vector wars would play out over the next few decades, even though Clifford really +unified the two notions.) + +But, as such, this book was a broadly influential introduction to quaternions for many +years. + +In it, he defines the quaternion basis elements ``(i, j, k)`` with multiplication rules +```math +\begin{gathered} +i^2 = j^2 = k^2 = -1, +\qquad +ij = -ji = k, +\qquad +jk = -kj = i, +\qquad +ki = -ik = j, +\\ +ijk = -1. +\end{gathered} +``` +Note that Tait specifically writes expressions like ``xi + yj + zk`` and ``\xi i + \eta j + +\zeta k``; these suggest that Tait does think of ``i`` as corresponding to the ``x`` axis, +``j`` to the ``y`` axis, and ``k`` to the ``z`` axis — evidently unlike Hamilton himself. +Moreover, Tait even says + +> Suppose ``i`` to be drawn eastwards, ``j`` northwards, and ``k`` upwards. + +This is consistent with our standard right-handed system, with the ``k`` axis pointing out +of the page. + + + +[Tait_1868](@cite) discusses the decomposition of a general rotation into three rotations +about successive axes, which we now call "Euler angles". Tait uses the symbols ``(ψ, θ, +ϕ)`` for the three angles. He actually does so just to express that set of rotations in +terms of a single quaternion. + +> Here the vectors ``i``, ``j``, ``k`` in the original position of the body correspond to +``\overline{OA}``, ``\overline{OB}``, ``\overline{OC}`` respectively, at time ``t``. The +transposition is effected by — *first*, a rotation ``\psi`` about ``k``; *second*, a +rotation ``\theta`` about the new position of the line originally coinciding with ``j``; +*third*, a rotation ``\phi`` about the final position of the line at first coinciding with +``k'``. + +So this is what would probably now be called the ``z-y'-z''`` convention for Euler angles +``(\psi, \theta, \phi)``, which is equivalent to ``(\phi, \theta, \psi)`` in the ``z-y-z`` +convention used here. + +Indeed, Tait goes on to derive (somewhat laboriously) the expression for the quaternion: + +```math +q = \cos \frac{\phi + \psi}{2} \cos \frac{\theta}{2} + + i \sin \frac{\phi - \psi}{2} \sin \frac{\theta}{2} + + j \cos \frac{\phi - \psi}{2} \sin \frac{\theta}{2} + + k \sin \frac{\phi + \psi}{2} \cos \frac{\theta}{2}, +``` + +which is exactly the same as our expression from `from_euler_angles(ψ, θ, ϕ)`. + +""" diff --git a/docs/src/development/literate_testitems.md b/docs/src/development/literate_testitems.md index cbf42f70..bfca5221 100644 --- a/docs/src/development/literate_testitems.md +++ b/docs/src/development/literate_testitems.md @@ -1,29 +1,70 @@ # Literate TestItems -TestItemRunner.jl + Literate.jl = 💪 +* Use TestItems to be able to run tests via VS Code +* Use Documenter (and DocumenterCitations) to build docs +* Write pages that extensively document *and test* different + conventions from the literature against this package -(With honorable mention going to the excellent -[DocumenterCitations.jl](https://juliadocs.org/DocumenterCitations.jl/stable/) -and -[FastDifferentiation.jl](https://github.com/brianguenter/FastDifferentiation.jl) -packages.) +* Use Literate to write files that generate pages for Documenter + +Because the conventions pages require significant amounts of code, +including as their own modules, it is helpful to be able to run the +tests in those pages separately from the main test suite — i.e., with +more granularity. This cannot be done with simple doctests, but can +be done with TestItems, which necessitates using Literate. + +I want these pages to stand on their own as documentation of the +various conventions, while also actively being used to test the +package's conventions. + +So, basically, I couldn't use doctests for two reasons: the sheer +complexity of the code and tests, and the need for modularity and +granularity in running the tests. -I have a package called -[SphericalFunctions.jl](https://moble.github.io/SphericalFunctions.jl/dev/) -that computes things like Wigner D matrices and spherical harmonics. -If you've ever dealt with these things — or even just rotations -generally — you'll know that the literature is an absolute quagmire of -subtly differing conventions wrapped up in terminology and notation -from hundreds of years ago. In an effort to sort some of this out, I +Also note that these particular conventions often define things that +need to be differentiated, for which FastDifferentiation does a very +good job of converting those general formulas to useful +implementations. + + + + + +This package computes things like Wigner's 𝔇 matrices. If you've +ever dealt with these things — or even just rotations generally — +you'll know that the literature is an absolute quagmire of subtly +differing conventions wrapped up in terminology and notation from +hundreds of years ago. In an effort to sort some of this out, I decided to carefully compare the conventions of as many significant sources as I could — everything from Wikipedia and Mathematica, to current quantum-mechanics textbooks, all the way back to the original books and papers that introduced some of these concepts. So I went through each reference, and wrote a documentation page for each one -that carefully laid out the conventions used in that source. The +that carefully laid out the conventions used in that source. + + + + + + + + + + +The documentation for my package has a whole section collecting all of these different pages. + +TestItemRunner.jl + Literate.jl = 💪 + +(With honorable mention going to the excellent +[DocumenterCitations.jl](https://juliadocs.org/DocumenterCitations.jl/stable/) +and +[FastDifferentiation.jl](https://github.com/brianguenter/FastDifferentiation.jl) +packages.) + + But I didn't just want to *document* these conventions; I wanted to *test* how they compared to the implementations in my package. I wanted to feed actual numbers into the actual expressions written down diff --git a/docs/src/references.bib b/docs/src/references.bib index bcbe0009..0c97d17d 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -109,6 +109,19 @@ @article{BrauchartGrabner_2015 author = {Johann S. Brauchart and Peter J. Grabner} } +@article{Clifford_1878, + title = {Applications of Grassmann's Extensive Algebra}, + volume = {1}, + url = {http://www.jstor.org/stable/2369379}, + doi = {10.2307/2369379}, + number = {4}, + journal = {American Journal of Mathematics}, + author = {Clifford, William Kingdon}, + month = jan, + year = {1878}, + pages = {350--358} +} + @book{CohenTannoudji_1991, address = {New York}, edition = {1st}, @@ -129,6 +142,19 @@ @book{CondonShortley_1935 url = {https://archive.org/details/in.ernet.dli.2015.212979} } +@article{Davenport_1973, + title = {Rotations about nonorthogonal axes}, + volume = {11}, + issn = {0001-1452}, + url = {https://doi.org/10.2514/3.6842}, + doi = {10.2514/3.6842}, + number = {6}, + journal = {{AIAA} Journal}, + author = {Davenport, Paul B.}, + year = {1973}, + pages = {853--857} +} + @book{DoranLasenby_2010, address = {Cambridge}, title = {Geometric Algebra for Physicists}, @@ -184,6 +210,30 @@ @article{Elahi_2018 primaryClass = "astro-ph.IM", } +@article{Euler_1776a, + title = {Formulae generales pro translatione quacunque corporum rigidorum}, + volume = {20}, + location = {Euler Archive}, + url = {https://scholarlycommons.pacific.edu/euler-works/478}, + journal = {Novi Commentarii Academiae Scientiarum Imperialis Petropolitanae}, + author = {Euler, Leonhard}, + year = {1776}, + note = {Enestr\"{o}m Number: E478}, + pages = {189--207} +} + +@article{Euler_1776b, + title = {Nova methodus motum corporum rigidorum determinandi}, + volume = {20}, + location = {Euler Archive}, + url = {https://scholarlycommons.pacific.edu/euler-works/479}, + journal = {Novi Commentarii Academiae Scientiarum Imperialis Petropolitanae}, + author = {Euler, Leonhard}, + year = {1776}, + note = {Enestr\"{o}m Number: E479}, + pages = {208--238} +} + @book{Folland_2016, address = {New York}, edition = {2nd}, @@ -288,6 +338,32 @@ @incollection{Gumerov_2015 primaryClass = "math.NA", } +@article{Hamilton_1844, + title = {{LXXVIII.} On quaternions; or on a new system of imaginaries in Algebra: To the editors of the Philosophical Magazine and Journal}, + volume = {25}, + issn = {1941-5966}, + shorttitle = {{LXXVIII.} On quaternions; or on a new system of imaginaries in Algebra}, + url = {https://doi.org/10.1080/14786444408645047}, + doi = {10.1080/14786444408645047}, + number = {169}, + journal = {The London, Edinburgh, and Dublin Philosophical Magazine and Journal of Science}, + author = {Hamilton, William Rowan}, + month = jan, + year = {1844}, + pages = {489--495} +} + +@book{Hamilton_1853, + title = {Lectures on quaternions : containing a systematic statement of a new mathematical method, of which the principles were communicated in 1843 to the Royal Irish academy, and which has since formed the subject of successive courses of lectures, delivered in 1848 and subsequent years, in the halls of Trinity college, Dublin}, + lccn = {b17315189}, + shorttitle = {Lectures on quaternions}, + url = {http://archive.org/details/lecturesonquater00hami}, + publisher = {Hodges and Smith}, + address = {Dublin}, + author = {Hamilton, William Rowan}, + year = {1853} +} + @book{HansonYakovlev_2002, address = {New York, {NY}}, title = {Operator Theory for Electromagnetics}, @@ -526,6 +602,34 @@ @article{Strakhov_1980 year = 1980 } +@book{Tait_1867, + title = {An elementary treatise on quaternions}, + url = {http://archive.org/details/anelementarytre03taitgoog}, + publisher = {Oxford, Clarendon Press}, + author = {Tait, Peter Guthrie}, + year = {1867}, +} + +@article{Tait_1868, + title = {On the Rotation of a Rigid Body about a Fixed Point}, + volume = {25}, + url = {http://archive.org/details/scientificpapers029742mbp}, + journal = {Transactions of the Royal Society of Edinburgh}, + author = {Tait, Peter Guthrie}, + year = {1868}, + note = {Also found in "Scientific Papers by Peter Guthrie Tait", Vol. I.} +} + +@book{Tait_1898, + title = {Scientific Papers by Peter Guthrie Tait}, + volume = {1}, + lccn = {29742}, + url = {http://archive.org/details/scientificpapers029742mbp}, + publisher = {Cambridge University Press}, + author = {Tait, Peter Guthrie}, + year = {1898} +} + @article{Thorne_1980, title = {Multipole expansions of gravitational radiation}, volume = 52, From bd243acca7cb175d2f141749824ce32aea75c9db Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 22 Dec 2025 10:53:38 -0600 Subject: [PATCH 287/329] Fix typo --- .../conventions/calculations/metrics_and_integration.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate_input/conventions/calculations/metrics_and_integration.jl b/docs/literate_input/conventions/calculations/metrics_and_integration.jl index 013e96ab..8ba5170a 100644 --- a/docs/literate_input/conventions/calculations/metrics_and_integration.jl +++ b/docs/literate_input/conventions/calculations/metrics_and_integration.jl @@ -199,7 +199,7 @@ S3_normalized_volume_form_factor = sympy.simplify( ) # And finally, we can restrict back to ``\mathrm{SO}(3)`` by taking ``β ∈ [0, π]``, and -# integrate over that range: +# integrating over that range: SO3_volume = sympy.integrate( sympy.integrate( sympy.integrate( From cfbc51041447884bd37193cbfab1940621e6b0d7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 22 Dec 2025 11:02:42 -0600 Subject: [PATCH 288/329] Clarify the origins of Euler angles --- .gitignore | 3 + .../conventions/comparisons/euler_1776.jl | 55 ++-- .../conventions/comparisons/gibbs_1881.jl | 24 ++ .../conventions/comparisons/whittaker_1947.jl | 301 ++++++++++++++++++ .../conventions/comparisons/wilson_1921.jl | 61 ++++ docs/src/references.bib | 43 +++ 6 files changed, 466 insertions(+), 21 deletions(-) create mode 100644 docs/literate_input/conventions/comparisons/gibbs_1881.jl create mode 100644 docs/literate_input/conventions/comparisons/whittaker_1947.jl create mode 100644 docs/literate_input/conventions/comparisons/wilson_1921.jl diff --git a/.gitignore b/.gitignore index ea76e044..69cc9e37 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ docs/src/conventions/comparisons/euler_1776.md docs/src/conventions/comparisons/clifford_1878.md docs/src/conventions/comparisons/hamilton_1844.md docs/src/conventions/comparisons/tait_1868.md +docs/src/conventions/comparisons/gibbs_1881.md +docs/src/conventions/comparisons/wilson_1921.md +docs/src/conventions/comparisons/whittaker_1947.md diff --git a/docs/literate_input/conventions/comparisons/euler_1776.jl b/docs/literate_input/conventions/comparisons/euler_1776.jl index 75e742f7..edff856b 100644 --- a/docs/literate_input/conventions/comparisons/euler_1776.jl +++ b/docs/literate_input/conventions/comparisons/euler_1776.jl @@ -1,5 +1,12 @@ md""" -# Euler (1776) +# Euler (1767 and 1776) + +!!! warn "Change what's below" + + The following needs to talk mostly about [Euler_1767](@citet), which *did* actually use + Euler angles as we would recognize them now. It can talk about incorrect citations to + the other two, but that's secondary. + !!! info "Summary" @@ -8,27 +15,33 @@ md""" Euler did, however, introduce the axis-angle representation and the direction-cosine matrix, which are important concepts in analyzing rotations. - -Euler wrote a lot about rigid-body dynamics, - -The poor man's name has been besmirched by association with "Euler angles", but his original -work never expresses rotations as a product of three rotations about fixed axes. Rather, he -came up with the axis-angle representation, and essentially devised the direction cosine -matrix as we know it today. His 1776 papers [Euler_1776a](@cite) and [Euler_1776b](@cite) -derived these ideas in service of understanding rigid-body dynamics — rather than any purely -mathematical investigations. - -Nonetheless, by 1868, Tait [Tait_1868](@cite) writes down a rotation in exactly what we -would now call Euler angles — which he calls "the usual angles ψ, θ, ϕ"! So somehow, this -had become normalized by that time. - -Whittaker has a section I.9 on "Euler's parametric specification of rotations round a point" -and cites sections 6 and following of [Euler_1776b](@cite). That section, I would say, -correctly cites Euler's work on direction cosines. However, Whittaker's section I.10 on -"The Eulerian angles" cites Euler's *preceding* work [Euler_1776a](@cite), which does not +Among Leonhard Euler's vast body of contributions to math and physics, we find his research +on rigid-body dynamics. He was working at a time before vector (or quaternion) algebra had +been developed, and had to rely on awkward techniques reminiscent of Euclid's constructions. +Nonetheless, he found insights into the mathematics behind rotations that remain relevant +today. In particular, in his 1776 papers [Euler_1776a](@cite) and [Euler_1776b](@cite), he +developed the axis-angle representation of rotations and the direction-cosine matrix. But +nowhere in his writings — as far as I can find — did Euler ever use what we today call +"Euler angles". + +By 1868, Tait [Tait_1868](@cite) writes down a rotation in exactly what we would now call +ZYZ Euler angles — which he calls "the usual angles ψ, θ, ϕ". So somehow, this +representation had become normalized by that time. It's interesting to note that Tait was +actually using the "usual" angles to demonstrate how quaternions were superior. But at no +point does he refer to them as "Euler" angles. + +The very influential book by [Whittaker](@cite Whittaker_1947) contained section I.9 on +"Euler's parametric specification of rotations round a point", which cites Euler's sections +6 and following [Euler_1776b](@cite). That section, I would say, correctly cites Euler's +work on direction cosines, but doesn't yet get to "Euler angles". However, his very next +section I.10 on "The Eulerian angles" cites Euler's *preceding* work [Euler_1776a](@cite) +when constructing a sequence of three rotations that we would now recognize as the +``ZY'Z''`` convention. However, there is absolutely no support for this construction in +Euler's work. Whittaker has besmirched Euler's good name by associating him with this +construction. As far as I can find, Davenport is actually the first person to come along and try to -actually show when it is possible to decompose an arbitrary rotation into three rotations -about fixed axes [Davenport_1973](@cite). +actually show *when exactly* it is possible to decompose an arbitrary rotation into three +rotations about fixed axes [Davenport_1973](@cite). """ diff --git a/docs/literate_input/conventions/comparisons/gibbs_1881.jl b/docs/literate_input/conventions/comparisons/gibbs_1881.jl new file mode 100644 index 00000000..fde51c8b --- /dev/null +++ b/docs/literate_input/conventions/comparisons/gibbs_1881.jl @@ -0,0 +1,24 @@ +md""" + +# Gibbs (1881) + +Josiah Willard Gibbs was a pioneering American physicist and mathematician, and his work on +vector analysis was influential in the development of modern vector calculus. His 1881 +pamphlet on vector analysis [Gibbs_1881](@cite) was one of the earliest texts to present +vector analysis in a form similar to what we use today. + + +> The letters ``i``, ``j``, ``k`` are appropriated to the designation of a *normal system of +> unit vectors*, i. e., three unit vectors, each of which is at right angles to the other +> two and determined in direction by them in a perfectly definite manner. We shall always +> suppose that ``k`` is on the side of the ``i``-``j`` plane on which a rotation from ``i`` to +> ``j`` (through one right angle) appears counter-clock-wise. In other words, the directions +> of ``i``, ``j``, and ``k`` are to be so determined that if they be turned (remaining +> rigidly connected with each other) so that ``i`` points to the east, and ``j`` to the +> north, ``k`` will point upward. When rectangular axes of ``X``, ``Y``, and ``Z`` are +> employed, their directions will be conformed to a similar condition, and ``i``, ``j``, +> ``k`` (when the contrary is not stated) will be supposed parallel to these axes +> respectively. + + +""" diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl new file mode 100644 index 00000000..03bad3d3 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -0,0 +1,301 @@ +md""" +# Whittaker (1904) + +!!! info "Summary" + + Whittaker consolidated conventions that are still used by physicists today. That + includes a right-handed orthogonal coordinate system labeled ``(x, y, z)``; "Eulerian + angles" that he labels ``(θ, ϕ, ψ)``, but which we would label ``(ϕ, θ, ψ)``; the + resulting spherical coordinates as we now use them, with ``θ`` being colatitude, ``ϕ`` + being the azimuthal angle from the ``x``-axis, and ``ψ=0``; and the standard quaternion + basis elements ``(i, j, k)``, with rotation of a vector ``v`` by a quaternion ``q`` is + given by ``q v q⁻¹``. + +Whittaker's "Analytical Dynamics" [Whittaker_1947](@cite) was the most influential book on +classical mechanics and mathematical physics of the first half of the 20th century. In +particular, quantum physicists found its approach to be helpful when inventing their new +branch of physics. It was originally published in 1904, with the 4th edition coming out in +1947, which was reissued in 1988, with reprintings as late as 1993 — which perhaps says +something about its influence. Goldstein's "Classical Mechanics" came out in 1950, marking +the end of Whittaker's reign. + +For better or for worse, essentially all of the basic conventions used by physicists today +(including many used by this package) were consolidated here. Tragically, that includes a +dismissive snarkiness toward quaternions — presumably because Whittaker, like so many +physicists of his time, was poisoned by the harsh invective against quaternions coming +mostly from Gibbs and Heaviside, belying the impoverished understanding at the turn of the +20th century of the close *interdependence* between quaternions and vectors. Whittaker even +goes so far as to make the petty and false claim that quaternion multiplication had been +discovered independently by three other people in addition to Hamilton. His muddled and +tendentious treatment of quaternions surely sealed their fate until aerospace, computer +graphics, and robotics applications came to rescue them from obscurity. + + +## Implementing expressions + +We begin by writing code that implements the concepts described by Ref. +[Whittaker_1947](@cite). We encapsulate the formulas in a module so that we can test them +against the `Quaternionic` and `SphericalFunctions` package. + +""" + +using TestItems: @testitem #hide +@testitem "Whittaker conventions" setup=[ConventionsSetup, Utilities] begin #hide + +module Whittaker +#+ + +# ### Basis vectors and handedness +# +# We start with Sec. I.7, where Whittaker introduces the axis-angle representation: + +# > Let rectangular axes ``Oxyz`` be taken, fixed in space: these will be supposed to form a +# > right-handed system, i.e. if the axes are so placed that ``Oz`` is directed vertically +# > upwards and ``Oy`` is directed to the northern horizon, then ``Ox`` will be directed to +# > the east. + +# This is a right-handed orthogonal system, which we will represent by the corresponding +# unit vectors as `QuatVec`s: + +import Quaternionic: Quaternionic, 𝐢, 𝐣, 𝐤, ⋅, ×̂ + +const Ox = Quaternionic.𝐢 +const Oy = Quaternionic.𝐣 +const Oz = Quaternionic.𝐤 + +const east = Ox +const north = Oy +const up = Oz + +const south = -north +const west = -east +#+ + +# Whittaker continues: +# +# > Let the displacement considered be equivalent to a rotation through an angle ``ω`` about +# > a line whose direction-angles are ``(α, β, γ)``[...] +# +# The "direction-angles" are the angles that the line makes with the ``Ox``, ``Oy``, and +# ``Oz`` axes, respectively. In modern parlance, we would typically represent this "line" +# with a unit vector, which would have components ``(\cos α, \cos β, \cos γ)``. +line(α, β, γ) = cos(α) * Ox + cos(β) * Oy + cos(γ) * Oz +#+ + +# He then specifies the handedness of the rotation as follows: +# +# > The angle ``ω`` must be taken with its appropriate sign, the sign being positive when +# > the line ``(α, β, γ)`` being directed vertically upwards, the rotation from the southern +# > horizon to the northern is round by the east. +# +# This supposes that ``(α, β, γ) = (π/2, π/2, 0)``, so the "line" is ``𝐳``, and we rotate +# the vector ``-𝐣`` by *increasing* ``ω`` from ``ω=0`` until it reaches ``+𝐣`` at ``ω=π``, +# then we pass through ``+𝐢`` on the way. This is just a standard right-handed rotation, so +# we would write the rotation about this line as +axis_angle_rotation(ω, α, β, γ) = exp((ω / 2) * line(α, β, γ)) +#+ + +# We will test below that the angles the line makes with the axes are what Whittaker +# intended, and that the behavior in this scenario describing handedness is as expected. + + +# ### Quaternions +# +# Section I.9 introduces quaternions in a very awkward way, evidently hobbled by Whittaker's +# spite, driving what we might presume to be his intentional obfuscation of the elegance of +# quaternions. He starts out by arbitrarily introducing four combinations of the axis-angle +# parameters, which so happen to add in quadrature to 1. He then works through some very +# tedious math to eventually show that these four parameters happen to obey the laws of +# quaternion multiplication. He denotes the quaternion (after some evident indecision as to +# whether the scalar component should come first or last) as ``χ + ξi + ηj + ζk``, where +# ``i``, ``j``, ``k`` satisfy +# ```math +# i^2 = j^2 = k^2 = -1, +# \quad +# ij = -ji = k, +# \quad +# jk = -kj = i, +# \quad +# ki = -ik = j. +# ``` +# These are exactly our conventions. Whittaker goes on to say +# +# > The reader who is acquainted with quaternions will observe that the effect of the +# > rotation on any vector ``ρ`` is to convert it into the vector ``qρq⁻¹``, where ``q`` +# > denotes the quaternion ``χ + ξi + ηj + ζk``; the quaternion itself is *not* the +# > rotational operator. +# +# Note the petty little dig attempting to diminish quaternions in the last clause. This is +# exactly how we apply quaternions as well; certain other conventions use ``q⁻¹ρq``. + + +# ### "The Eulerian angles" +# +# Section I.10 is titled "The Eulerian angles". Whittaker is not just saying that the three +# angles he introduces in this section are *akin* to angles that Euler used; he is +# specifically crediting Euler with the particular construction he uses: +# +# > The most practically useful of the various methods of representing parametrically the +# > displacement of a rigid body due to a rotation round a fixed point is likewise due to +# > Euler†: it has the disadvantage of being unsymmetrical, but is otherwise very simple and +# > convenient. +# +# That dagger cites [Euler_1776a](@citet), which discusses the axis-angle representation, +# the direction-cosine matrix, and a reparameterization of the direction-cosine matrix with +# just three angles — which proves that the rotation group is three-dimensional, but *does +# not* provide a parameterization at all related to the one Whittaker now gives, nor can I +# find Euler ever using a similar one: +# +# > Let ``O`` be the fixed point round which the rotation takes place, and let ``OXYZ`` be a +# > right-handed system of rectangular axes fixed in space. Let ``Oxyz`` be rectangular axes +# > fixed relatively to the body and moving with it, and such that before the displacement +# > the two sets of axes ``OXYZ`` and ``Oxyz`` are coincident in position. Let ``OK`` be +# > perpendicular to the plane ``zOZ``, drawn so that if ``OZ`` is directed to the vertical +# > and the projection of ``Oz`` perpendicular to ``OZ`` is directed to the south, then +# > ``OK`` is directed to the east. Denote the angles ``z\hat{O}Z``, ``Y\hat{O}K``, +# > ``y\hat{O}K`` by ``\theta``, ``\phi``, ``\psi``, respectively: these are known as the +# > three *Eulerian angles* defining the position of the axes ``Oxyz`` with reference to the +# > axes ``OXYZ``. +# +# The line ``OK`` is often called the "line of nodes", and is not a very natural object to +# define, except in the case of successive rotations — which, again, is not something that +# Euler did. Nonetheless, Whittaker does not actually specify a sequence of rotations, so +# we have to devise one that will result in these angles between the various vectors (and we +# will test below that this works). +# +# The use of the angle between the initial and final positions of the ``OZ`` axis suggests +# that the direct rotation from one to the other should be one of our sequence, and the two +# in our sequence should be rotations about the inital ``OZ`` axis and the final ``Oz`` +# axis. Moreover, the use of the angles between ``OK`` and the initial and final positions +# of the ``OY`` axis suggest that ``OK`` is really the intermediate position of the ``OY`` +# axis. +# +# So essentially, this rotation is equivalent to an initial rotation about ``OZ`` by ``ϕ``, +# which takes ``OY`` to ``OY'=OK`` (hence ``YÔK=ϕ``); then a rotation about ``OY'=OK`` by +# ``θ`` to take ``OZ`` onto ``OZ'=OZ''=Oz`` (hence ``zÔZ=θ``); finally a rotation about +# ``OZ'=Oz`` by ``ψ``, which takes ``OY'=OK`` onto ``OY''=Oy`` (hence ``yÔK=ψ``). That is, +# in modern parlance this is a ``ZY'Z''`` rotation. We might write this in quaternion +# notation as +# ```math +# \begin{aligned} +# \exp\left[ \frac{ψ}{2} Oz \right]\, +# \exp\left[ \frac{θ}{2} OK \right]\, +# \exp\left[ \frac{ϕ}{2} OZ \right] +# &= +# \exp\left[ \frac{ϕ}{2} OZ \right]\, +# \exp\left[ \frac{θ}{2} OY \right]\, +# \exp\left[ \frac{ψ}{2} OZ \right] \\ +# &= +# \exp\left[ \frac{ϕ}{2} 𝐤 \right]\, +# \exp\left[ \frac{θ}{2} 𝐣 \right]\, +# \exp\left[ \frac{ψ}{2} 𝐤 \right]. +# \end{aligned} +# ``` +function eulerian_rotation(θ, ϕ, ψ) + OX, OY, OZ = 𝐢, 𝐣, 𝐤 + R₁ = exp((ϕ/2) * OZ) + OK = R₁(OY) + R₂ = exp((θ/2) * OK) + Oz = R₂(R₁(OZ)) + R₃ = exp((ψ/2) * Oz) + R₁ * R₂ * R₃ +end +#+ + +# We'll also need a function to find what Whittaker means by ``OK``, which is the normalized +# cross product ``OZ × Oz``: +function OK(θ, ϕ, ψ) + OZ = 𝐤 + Oz = eulerian_rotation(θ, ϕ, ψ)(OZ) + OZ ×̂ Oz ## Normalized cross product +end +#+ + +# Finally, for testing purposes, we implement functions to evaluate the three angles +# Whittaker refers to: +function YÔK(θ, ϕ, ψ) + OY = 𝐣 + let OK=OK(θ, ϕ, ψ) + acos(OY ⋅ OK) + end +end +function zÔZ(θ, ϕ, ψ) + OZ = 𝐤 + let OK=OK(θ, ϕ, ψ) + Oz = eulerian_rotation(θ, ϕ, ψ)(OZ) + acos(Oz ⋅ OZ) + end +end +function yÔK(θ, ϕ, ψ) + OY = 𝐣 + let OK=OK(θ, ϕ, ψ) + Oy = eulerian_rotation(θ, ϕ, ψ)(OY) + acos(Oy ⋅ OK) + end +end +#+ + +end #module Whittaker +#+ + +# ## Tests +# +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will need to test approximate floating-point equality, +# so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: +ϵₐ = 10eps() +ϵᵣ = 10eps() +#+ + +# Test that the angles the line makes with the axes are what Whittaker intended +# TODO: implement + + +# Now we'll test the behavior that +# > the line ``(α, β, γ)`` being directed vertically upwards, the rotation from the southern +# > horizon to the northern is round by the east. +let α=π/2, β=π/2, γ=0 + ω = range(0, π, length=21) + ## Check that the first element takes `south` to `south` + let R = Whittaker.axis_angle_rotation(ω[begin], α, β, γ) + @test R(Whittaker.south) ≈ Whittaker.south atol=ϵₐ rtol=ϵᵣ + end + ## Check that the final element takes `south` to `north` + let R = Whittaker.axis_angle_rotation(ω[end], α, β, γ) + @test R(Whittaker.south) ≈ Whittaker.north atol=ϵₐ rtol=ϵᵣ + end + ## Check that everything in between is "round by the east" by testing that every + ## intermediate point is closer to east than to west + for ωᵢ ∈ ω[begin+1:end-1] + let R = Whittaker.axis_angle_rotation(ωᵢ, α, β, γ) + p = R(Whittaker.south) + @test abs2(p - Whittaker.east) < abs2(p - Whittaker.west) + end + end +end +#+ + +# We can now test the "Eulerian" angles. The discussion above shows that the angles +# ``ϕ,θ,ψ`` in that order correspond to what we would denote as ``α,β,γ`` in that order. +# So we define our utility function for generating a variety of angles: +const ϕθψrange = αβγrange +#+ + +# First we test that the rotation as we've implemented it results in the angles between axes +# that Whittaker described: +for (ϕ,θ,ψ) ∈ ϕθψrange() + @test YÔK(θ, ϕ, ψ) ≈ ϕ + @test zÔZ(θ, ϕ, ψ) ≈ θ + @test yÔK(θ, ϕ, ψ) ≈ ψ +end +#+ + +# Next, we test that the rotation as we've implemented it does correspond to the Euler +# rotation implemented by `Quaternionic`: +for (ϕ,θ,ψ) ∈ ϕθψrange() + @test Whittaker.eulerian_rotation(θ, ϕ, ψ) ≈ Quaternionic.from_euler_angles(ϕ, θ, ψ) +end +#+ + + +end #@testitem #hide diff --git a/docs/literate_input/conventions/comparisons/wilson_1921.jl b/docs/literate_input/conventions/comparisons/wilson_1921.jl new file mode 100644 index 00000000..a5e61de5 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/wilson_1921.jl @@ -0,0 +1,61 @@ +md""" + +# Wilson (1929) + +Wilson's "Vector analysis" [Wilson_1929](@cite) was, historically, one of the most +influential textbooks on vector analysis in the early 20th century. It was based on the +lectures of [Gibbs](@ref Gibbs-(1881)), and it helped to popularize all aspects of what we +now understand as vector algebra — including the notion of a right-handed coordinate system. +He begins simply: + +> By the ``X``-, ``Y``-, or ``Z``-axis the *positive* half of that axis is meant. The ``X +> Y``-plane means the plane which contains the ``X``- and ``Y``-axis, i.e., the plane +> ``z=0``. + +Now, he gets to the heart of it: + +> In one case (Fig. 8, first part) the ``Z``-axis lies upon that side of the ``X Y``-plane +> on which rotation through a right angle from the ``Z``-axis to the ``Y``-axis appears +> *counterclockwise* or *positive* according to the convention adopted in Trigonometry. +> This relation may be stated in another form. If the ``X``-axis be directed to the right +> and the ``Y``-axis vertically, the ``Z``-axis will be directed toward the observer. Or if +> the ``X``-axis point toward the observer and the ``Y``-axis to the right, the ``Z``-axis +> will point upward. Still another method of statement is common in mathematical physics +> and engineering. If a right-handed screw be turned from the ``X``-axis to the ``Y``-axis +> it will advance along the (positive) ``Z``-axis. Such a system of axes is called +> right-handed, positive, or counterclockwise. + +This is followed by a footnote given another description that is more common now: + +> A convenient right-handed system and one which is always available consists of the thumb, +> first finger, and second finger of the right hand. If the thumb and first finger be +> stretched out from the palm perpendicular to each other, and if the second finger be bent +> over toward the palm at right angles to first finger, a right-handed system is formed by +> the fingers taken in the order thumb, first finger, second finger. + +I've left out some more discussion; in the above, he is just drawing the distinction between +left- and right-handed systems. But eventually, he gets to the final statement of his +choice: + +> In this book the right-handed or counterclockwise system will be invariably employed. + +Finally, he makes the important connection between these vectors and Cartesian coordinates: + +> *Definition:* The three letters ``𝐢``, ``𝐣``, ``𝐤`` will be reserved to denote three +> vectors of unit length drawn respectively in the directions of the ``X``-, ``Y``-, and +> ``Z``-axes of a right-handed rectangular system. +> +> In terms of these vectors, any vector may be expressed as +> ```math +> 𝐫 = x 𝐢 + y 𝐣 + z 𝐤. +> ``` +> The coefficients ``x``, ``y``, ``z`` are the ordinary Cartesian codrdinates of the +> terminus of ``𝐫`` if its origin be situated at the origin of coördinates. The components +> of ``𝐫`` parallel to the ``X``-, ``Y``-, and ``Z``-axes are respectively +> ```math +> x 𝐢, y 𝐣, z 𝐤. +> ``` +> The rotations about ``𝐢`` from ``𝐣`` to ``𝐤``, about ``𝐣`` from ``𝐤`` to ``𝐢``, and +> about ``𝐤`` from ``𝐢`` to ``𝐣`` are all positive. + +""" diff --git a/docs/src/references.bib b/docs/src/references.bib index 0c97d17d..896ea39f 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -210,6 +210,17 @@ @article{Elahi_2018 primaryClass = "astro-ph.IM", } +@article{Euler_1767, + title = {Du mouvement d'un corps solide quelconque lorsqu'il tourne autour d'un axe mobile}, + volume = {16}, + url = {https://bibliothek.bbaw.de/digitalisierte-sammlungen/akademieschriften/ansicht-akademieschriften?tx_bbaw_academicpublicationshow%5Baction%5D=show&tx_bbaw_academicpublicationshow%5Bcontroller%5D=AcademicPublication%5CVolume&tx_bbaw_academicpublicationshow%5Bpage%5D=186&tx_bbaw_academicpublicationshow%5Bvolume%5D=16&cHash=0d077822c83d8525b438a27723d92c19}, + journal = {M\'{e}moires de {l'Acad\'{e}mie} des Sciences de Berlin}, + author = {Euler, Leonhard}, + year = {1767}, + note = {Enestr\"{o}m Number: E336}, + pages = {176--227} +} + @article{Euler_1776a, title = {Formulae generales pro translatione quacunque corporum rigidorum}, volume = {20}, @@ -274,6 +285,16 @@ @book{Fulton_2004 doi = {10.1007/978-1-4612-0979-9} } +@book{Gibbs_1881, + title = {Elements of vector analysis : arranged for the use of students in physics}, + shorttitle = {Elements of vector analysis}, + url = {http://archive.org/details/elementsvectora00gibb}, + publisher = {Tuttle, Morehouse \& Taylor}, + address = {New Haven}, + author = {Gibbs, Josiah Willard}, + year = {1881} +} + @article{GoldbergEtAl_1967, author = {Goldberg, J. N. and Macfarlane, A. J. and Newman, E. T. and Rohrlich, F. and Sudarshan, E. C. G.}, @@ -756,3 +777,25 @@ @article{Vasil_2019 year = {2019}, pages = {100013} } + +@book{Whittaker_1947, + address = {Cambridge}, + series = {Cambridge Mathematical Library}, + title = {A Treatise on the Analytical Dynamics of Particles and Rigid Bodies}, + isbn = {978-0-521-35883-5}, + url = {https://www.cambridge.org/core/books/treatise-on-the-analytical-dynamics-of-particles-and-rigid-bodies/E4CEF3F091F516792B0D30FA0DE9C2BB}, + publisher = {Cambridge University Press}, + author = {Whittaker, E. T.}, + year = {1947}, + edition = {4th} +} + +@book{Wilson_1929, + title = {Vector Analysis : A {Text-Book} for the Use of Students of Mathematics \& Physics: Founded Upon the Lectures of J. W. Gibbs}, + shorttitle = {Vector Analysis}, + url = {http://archive.org/details/vectoranalysiste0000unse}, + publisher = {Yale University Press}, + address = {New Haven}, + author = {Wilson, Edwin Bidwell}, + year = {1929} +} \ No newline at end of file From c47af58e686377ed8c7f2efd3e03adc5a8e503c7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 24 Dec 2025 21:24:47 -0600 Subject: [PATCH 289/329] Update citation styling --- docs/src/assets/citations.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/assets/citations.css b/docs/src/assets/citations.css index 7bb7d01c..0bb3e83f 100644 --- a/docs/src/assets/citations.css +++ b/docs/src/assets/citations.css @@ -12,7 +12,7 @@ .citation ul { padding: 0 0 2.25em 0; margin: 0; - list-style: none; + list-style: none !important; } .citation ul li { text-indent: -2.25em; From 4d9cd209f6e076c686397dc4ff637e8665f861a9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 24 Dec 2025 21:25:02 -0600 Subject: [PATCH 290/329] Mention Sommerfeld --- .../conventions/comparisons/whittaker_1947.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index 03bad3d3..cbf7662c 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -16,8 +16,9 @@ classical mechanics and mathematical physics of the first half of the 20th centu particular, quantum physicists found its approach to be helpful when inventing their new branch of physics. It was originally published in 1904, with the 4th edition coming out in 1947, which was reissued in 1988, with reprintings as late as 1993 — which perhaps says -something about its influence. Goldstein's "Classical Mechanics" came out in 1950, marking -the end of Whittaker's reign. +something about its influence. Sommerfeld's "Lectures on Theoretical Physics" came out +starting in 1943, and Goldstein's "Classical Mechanics" in 1950, marking the end of +Whittaker's reign. For better or for worse, essentially all of the basic conventions used by physicists today (including many used by this package) were consolidated here. Tragically, that includes a @@ -174,8 +175,8 @@ axis_angle_rotation(ω, α, β, γ) = exp((ω / 2) * line(α, β, γ)) # which takes ``OY`` to ``OY'=OK`` (hence ``YÔK=ϕ``); then a rotation about ``OY'=OK`` by # ``θ`` to take ``OZ`` onto ``OZ'=OZ''=Oz`` (hence ``zÔZ=θ``); finally a rotation about # ``OZ'=Oz`` by ``ψ``, which takes ``OY'=OK`` onto ``OY''=Oy`` (hence ``yÔK=ψ``). That is, -# in modern parlance this is a ``ZY'Z''`` rotation. We might write this in quaternion -# notation as +# in modern parlance this is a ``z``-``y'``-``z''`` rotation. We might write this in +# quaternion notation as # ```math # \begin{aligned} # \exp\left[ \frac{ψ}{2} Oz \right]\, From 0cbcbdf488b1646ef022170dad434a103f58e4cf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 26 Dec 2025 20:13:17 -0600 Subject: [PATCH 291/329] Finish Euler conventions --- .../conventions/comparisons/euler_1776.jl | 191 ++++++++++++++---- docs/src/assets/extras.css | 53 ++++- docs/src/references.bib | 2 +- 3 files changed, 206 insertions(+), 40 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/euler_1776.jl b/docs/literate_input/conventions/comparisons/euler_1776.jl index edff856b..661a0eb7 100644 --- a/docs/literate_input/conventions/comparisons/euler_1776.jl +++ b/docs/literate_input/conventions/comparisons/euler_1776.jl @@ -1,47 +1,162 @@ md""" # Euler (1767 and 1776) -!!! warn "Change what's below" +!!! info "Summary" - The following needs to talk mostly about [Euler_1767](@citet), which *did* actually use - Euler angles as we would recognize them now. It can talk about incorrect citations to - the other two, but that's secondary. + Euler introduced his eponymous angles in a somewhat more generic form than we use them + today — without actually specifying reference axes or a rotated frame of axes. + Nevertheless, we can interpret his construction in modern terms, corresponding to the + conventions used in this package, though with a curious offset in the ``γ`` angle. + He also used spherical coordinates consistent with our conventions. +Euler angles are a sequence of three angles representing three *successive rotations* about +a set of orthogonal axes. Several more modern sources incorrectly attribute the +introduction of "Euler angles" to Euler's 1776 papers on rigid-body dynamics [Euler_1776a, +Euler_1776b](@cite). In fact, those papers never used Euler angles at all; they used the +axis-angle representation and the direction-cosine matrix. Euler actually introduced what +we would now call Euler angles in his 1767 paper "Du mouvement d'un corps solide quelconque +lorsqu'il tourne autour d'un axe mobile" ("On the motion of an arbitrary rigid body rotating +about a moving axis") [Euler_1767](@cite). -!!! info "Summary" +This paper was important because Euler fully distinguished between a space-fixed frame and a +body-fixed frame. The space-fixed frame was viewed in reference to the "celestial sphere", +with its origin ``O`` at the center of gravity of the body. Euler's description was +ambiguous, but he did include a figure, reproduced here: + +```@raw html +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + O + C + A + P + Q + L + D + R + B + M + N + Z + Y + X + v + + +
+``` + +```@raw html + +``` +*Euler's Figure 2.* This reproduces a figure from Euler's 1767 paper [Euler_1767](@cite), +showing his conventions for angles describing the orientation of a rigid body in space — +which we now know as Euler angles. He also used spherical coordinates for a point relative +to the body. + +From the figure, we can see that he introduced an orthogonal set of axes ``OA``, ``OD``, and +``OC`` corresponding to what we would now label as ``x``, ``y``, and ``z`` as a right-handed +coordinate system fixed in space. He also assigned the axis ``OM`` to be the body's +principal axis, which we would label as the body-fixed axis ``z''``. While he didn't +introduce a full body-fixed frame at this point,[^1] he did discuss a reference plane +spanned by ``MOL``. We can think of this as the rotated plane spanned by ``COB``, where +``L`` is simply the point at which this rotated plane intersects the "horizon" plane spanned +by ``AOD``. Thus, ``MOL`` tracks the rotation of the body about its axis. + +[^1]: + + Euler later defined an orthogonal set of body-fixed axes ``OS``, ``OT``, and ``OM``. He + never related the former two to the Euler angles, though; they are just used to define + moments of inertia. + +Finally, Euler defines a set of three angles, which we can interpret as corresponding to +three successive rotations. Here we translate from his original: +> - the angle ``ACM`` or the arc ``AP`` = ``p``, +> - the distance of the point ``M`` from the zenith ``C`` or the arc ``CM`` = ``q``, +> - and the angle ``CML`` = ``r``. + +We readily see that these correspond *roughly* to our angles ``α``, ``β``, ``γ``, +respectively — with the important exception that the origin of ``γ`` is different from the +origin of ``r``. Specifically, since ``r`` is measured by ``CML``, ``r=0`` when the +reference direction points back toward the zenith ``C`` from ``M``. This is opposite to +modern conventions, where the reference direction points toward the nadir when ``γ=0``. So, +putting it all together, we have: +```math +\begin{align*} + α &= p, \\ + β &= q, \\ + γ &= π - r. +\end{align*} +``` - Euler never actually used a succession of three simple rotations to represent a general - rotation, so the term "Euler angles" is a misnomer — besmirching Euler's good name. - Euler did, however, introduce the axis-angle representation and the direction-cosine - matrix, which are important concepts in analyzing rotations. - -Among Leonhard Euler's vast body of contributions to math and physics, we find his research -on rigid-body dynamics. He was working at a time before vector (or quaternion) algebra had -been developed, and had to rely on awkward techniques reminiscent of Euclid's constructions. -Nonetheless, he found insights into the mathematics behind rotations that remain relevant -today. In particular, in his 1776 papers [Euler_1776a](@cite) and [Euler_1776b](@cite), he -developed the axis-angle representation of rotations and the direction-cosine matrix. But -nowhere in his writings — as far as I can find — did Euler ever use what we today call -"Euler angles". - -By 1868, Tait [Tait_1868](@cite) writes down a rotation in exactly what we would now call -ZYZ Euler angles — which he calls "the usual angles ψ, θ, ϕ". So somehow, this -representation had become normalized by that time. It's interesting to note that Tait was -actually using the "usual" angles to demonstrate how quaternions were superior. But at no -point does he refer to them as "Euler" angles. - -The very influential book by [Whittaker](@cite Whittaker_1947) contained section I.9 on -"Euler's parametric specification of rotations round a point", which cites Euler's sections -6 and following [Euler_1776b](@cite). That section, I would say, correctly cites Euler's -work on direction cosines, but doesn't yet get to "Euler angles". However, his very next -section I.10 on "The Eulerian angles" cites Euler's *preceding* work [Euler_1776a](@cite) -when constructing a sequence of three rotations that we would now recognize as the -``ZY'Z''`` convention. However, there is absolutely no support for this construction in -Euler's work. Whittaker has besmirched Euler's good name by associating him with this -construction. - -As far as I can find, Davenport is actually the first person to come along and try to -actually show *when exactly* it is possible to decompose an arbitrary rotation into three -rotations about fixed axes [Davenport_1973](@cite). +Euler then goes on to define the position of an arbitrary point, labeled ``Z`` in the +figure, by what we would now call Cartesian coordinates relative to the fixed frame, and +also spherical coordinates *relative to the body frame*. First, he progresses from the +origin ``O`` along the ``OA`` axis to the point ``X``, then parallel to ``OD`` to the point +``Y``, and finally parallel to ``OC`` to the point ``Z``. He specifically lists the +distances ``OX=x``, ``XY=y``, and ``YZ=z``, which are precisely what we would label them as. +Then, he defines the distance ``OZ`` as ``s``, the arc ``MN`` as ``u``, and the angle +``LMN`` as ``v``. That is, we would denote these quantities as +```math +\begin{align*} + r &= s, \\ + θ &= u, \\ + ϕ &= v, +\end{align*} +``` +but *only in the body-fixed frame*. """ diff --git a/docs/src/assets/extras.css b/docs/src/assets/extras.css index 21d84c2c..ecbf9dc5 100644 --- a/docs/src/assets/extras.css +++ b/docs/src/assets/extras.css @@ -43,4 +43,55 @@ div .composition-diagram { .composition-diagram svg path { stroke: currentColor; fill: none; -} \ No newline at end of file +} + +.euler-figure2 { + display: block; + text-align: center; + margin: 0 auto; +} + +/* Make the inline SVG scale responsively */ +.euler-figure2 svg { + width: min(900px, 100%); + height: auto; +} + +/* Ensure all strokes/fills follow the page's current text color */ +.euler-figure2 svg path { + stroke: currentColor; +} + +.euler-figure2 svg text, +.euler-figure2 svg circle, +.euler-figure2 svg rect { + fill: currentColor; + stroke: currentColor; +} + +.euler-figure2 svg text.label { + font-family: 'KaTeX_Math', 'KaTeX_Main', 'KaTeX_AMS', serif !important; + font-style: italic !important; + font-weight: 400 !important; /* avoid bold */ + font-size: 14px !important; /* adjust to taste */ + fill: currentColor !important; + dominant-baseline: middle !important; +} + +.euler-figure2 svg text { + font-weight: 400 !important; +} + +.euler-figure2 + p { + text-align: center; + font-size: 0.95em; + margin-top: 0.4rem; + margin-bottom: 1.2rem; + margin-left: 5rem; + margin-right: 5rem; + max-width: 100%; +} + +.euler-figure2 + p em { + font-style: italic; +} diff --git a/docs/src/references.bib b/docs/src/references.bib index 896ea39f..f54dd0b2 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -217,7 +217,7 @@ @article{Euler_1767 journal = {M\'{e}moires de {l'Acad\'{e}mie} des Sciences de Berlin}, author = {Euler, Leonhard}, year = {1767}, - note = {Enestr\"{o}m Number: E336}, + note = {Enestr\"{o}m Number: E336. Figures are on a separate plate facing page 228.}, pages = {176--227} } From 7a9d58017adc9628746fc123427884e9ef547cf6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 30 Dec 2025 10:45:06 -0500 Subject: [PATCH 292/329] Note that Whittaker got the Euler angle reference wrong --- .../conventions/comparisons/whittaker_1947.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index cbf7662c..e42858b8 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -11,6 +11,12 @@ md""" basis elements ``(i, j, k)``, with rotation of a vector ``v`` by a quaternion ``q`` is given by ``q v q⁻¹``. +!!! warning + + Whittaker cited the wrong paper for the Euler angles, referring to Euler's 1776 paper on + rigid body dynamics [Euler_1776a](@cite), when in fact Euler introduced these angles + in his 1767 paper [Euler_1767](@cite). + Whittaker's "Analytical Dynamics" [Whittaker_1947](@cite) was the most influential book on classical mechanics and mathematical physics of the first half of the 20th century. In particular, quantum physicists found its approach to be helpful when inventing their new From 5fd67cea6cc48a71b74a15515787cb611ecc0578 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 30 Dec 2025 10:45:25 -0500 Subject: [PATCH 293/329] Correct entry for Euler_1767 --- docs/src/references.bib | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/references.bib b/docs/src/references.bib index f54dd0b2..17eb96e0 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -214,10 +214,10 @@ @article{Euler_1767 title = {Du mouvement d'un corps solide quelconque lorsqu'il tourne autour d'un axe mobile}, volume = {16}, url = {https://bibliothek.bbaw.de/digitalisierte-sammlungen/akademieschriften/ansicht-akademieschriften?tx_bbaw_academicpublicationshow%5Baction%5D=show&tx_bbaw_academicpublicationshow%5Bcontroller%5D=AcademicPublication%5CVolume&tx_bbaw_academicpublicationshow%5Bpage%5D=186&tx_bbaw_academicpublicationshow%5Bvolume%5D=16&cHash=0d077822c83d8525b438a27723d92c19}, - journal = {M\'{e}moires de {l'Acad\'{e}mie} des Sciences de Berlin}, + journal = {Histoire de {l'Acad\'{e}mie} Royale des Sciences et des Belles-Lettres de Berlin}, author = {Euler, Leonhard}, year = {1767}, - note = {Enestr\"{o}m Number: E336. Figures are on a separate plate facing page 228.}, + note = {Academy year 1760. Enestr\"{o}m Number: E336. Figures are on a separate plate facing page 228.}, pages = {176--227} } From b5f366b28402ba1c99167101c885bf7bdbd2545e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 30 Dec 2025 11:29:13 -0500 Subject: [PATCH 294/329] Rename Euler's conventions page --- .gitignore | 2 +- .../conventions/comparisons/{euler_1776.jl => euler_1767.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/literate_input/conventions/comparisons/{euler_1776.jl => euler_1767.jl} (100%) diff --git a/.gitignore b/.gitignore index 69cc9e37..c1b7c259 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,7 @@ docs/src/conventions/comparisons/lalsuite_2025.md docs/src/conventions/comparisons/ninja_2011.md docs/src/conventions/comparisons/lalsuite_SphericalHarmonics.md docs/src/conventions/calculations/metrics_and_integration.md -docs/src/conventions/comparisons/euler_1776.md +docs/src/conventions/comparisons/euler_1767.md docs/src/conventions/comparisons/clifford_1878.md docs/src/conventions/comparisons/hamilton_1844.md docs/src/conventions/comparisons/tait_1868.md diff --git a/docs/literate_input/conventions/comparisons/euler_1776.jl b/docs/literate_input/conventions/comparisons/euler_1767.jl similarity index 100% rename from docs/literate_input/conventions/comparisons/euler_1776.jl rename to docs/literate_input/conventions/comparisons/euler_1767.jl From bfe9dbca02eda6793f7cdfd123f15fec4de719b3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 30 Dec 2025 12:52:19 -0500 Subject: [PATCH 295/329] Finish off Whittaker conventions --- .../conventions/comparisons/whittaker_1947.jl | 135 ++++++++++++++---- 1 file changed, 107 insertions(+), 28 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index e42858b8..54bec1b6 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -14,28 +14,30 @@ md""" !!! warning Whittaker cited the wrong paper for the Euler angles, referring to Euler's 1776 paper on - rigid body dynamics [Euler_1776a](@cite), when in fact Euler introduced these angles - in his 1767 paper [Euler_1767](@cite). + rigid-body dynamics [Euler_1776a](@cite), when in fact Euler introduced these angles + in his 1767 paper on rigid-body dynamics [Euler_1767](@cite). Whittaker's "Analytical Dynamics" [Whittaker_1947](@cite) was the most influential book on classical mechanics and mathematical physics of the first half of the 20th century. In -particular, quantum physicists found its approach to be helpful when inventing their new -branch of physics. It was originally published in 1904, with the 4th edition coming out in -1947, which was reissued in 1988, with reprintings as late as 1993 — which perhaps says +particular, quantum physicists found Whittaker's approach to be helpful when inventing their +new branch of physics. It was originally published in 1904, with the 4th edition coming out +in 1947, which was reissued in 1988, with reprintings as late as 1993 — which perhaps says something about its influence. Sommerfeld's "Lectures on Theoretical Physics" came out starting in 1943, and Goldstein's "Classical Mechanics" in 1950, marking the end of -Whittaker's reign. +Whittaker's dominance. For better or for worse, essentially all of the basic conventions used by physicists today (including many used by this package) were consolidated here. Tragically, that includes a dismissive snarkiness toward quaternions — presumably because Whittaker, like so many physicists of his time, was poisoned by the harsh invective against quaternions coming mostly from Gibbs and Heaviside, belying the impoverished understanding at the turn of the -20th century of the close *interdependence* between quaternions and vectors. Whittaker even -goes so far as to make the petty and false claim that quaternion multiplication had been -discovered independently by three other people in addition to Hamilton. His muddled and -tendentious treatment of quaternions surely sealed their fate until aerospace, computer -graphics, and robotics applications came to rescue them from obscurity. +20th century of the close *interdependence* between quaternions and vectors. Whittaker +often declines to refer to them as "quaternions", referring to them simply as a set of +parameters. He even goes so far as to make the petty and false claim that quaternion +multiplication had been discovered independently by three other people in addition to +Hamilton. His muddled and tendentious treatment of quaternions surely sealed their fate +until computer graphics, aerospace, and robotics applications came to rescue them from +obscurity. ## Implementing expressions @@ -61,10 +63,12 @@ module Whittaker # > upwards and ``Oy`` is directed to the northern horizon, then ``Ox`` will be directed to # > the east. -# This is a right-handed orthogonal system, which we will represent by the corresponding -# unit vectors as `QuatVec`s: +# (Note that Whittaker will soon distinguish between the system ``OXYZ`` which will be fixed +# in space, and the system ``Oxyz`` which will be fixed in the rotating body.) This is a +# right-handed orthogonal system, which we will represent by the corresponding unit vectors +# as `QuatVec`s: -import Quaternionic: Quaternionic, 𝐢, 𝐣, 𝐤, ⋅, ×̂ +import Quaternionic: Quaternionic, Rotor, 𝐢, 𝐣, 𝐤, ⋅, ×̂ const Ox = Quaternionic.𝐢 const Oy = Quaternionic.𝐣 @@ -76,6 +80,7 @@ const up = Oz const south = -north const west = -east +const down = -up #+ # Whittaker continues: @@ -218,8 +223,8 @@ function OK(θ, ϕ, ψ) end #+ -# Finally, for testing purposes, we implement functions to evaluate the three angles -# Whittaker refers to: +# We also implement, for testing purposes, functions to evaluate the three angles Whittaker +# refers to: function YÔK(θ, ϕ, ψ) OY = 𝐣 let OK=OK(θ, ϕ, ψ) @@ -242,6 +247,33 @@ function yÔK(θ, ϕ, ψ) end #+ +# Finally, Whittaker provides a table of values for the "direction-cosines" — meaning the +# projections of the rotated basis vectors onto the fixed basis vectors: +function direction_cosine(θ, ϕ, ψ) + [ + ## X Y Z + #= x =# cos(ϕ)cos(θ)cos(ψ)-sin(ϕ)sin(ψ) sin(ϕ)cos(θ)cos(ψ)+cos(ϕ)sin(ψ) -sin(θ)cos(ψ); + #= y =# -cos(ϕ)cos(θ)sin(ψ)-sin(ϕ)cos(ψ) -sin(ϕ)cos(θ)sin(ψ)+cos(ϕ)cos(ψ) sin(θ)sin(ψ); + #= z =# cos(ϕ)sin(θ) sin(ϕ)sin(θ) cos(θ) + ] +end +#+ + +# ### Connecting Eulerian angles to quaternions +# +# In section I.11, Whittaker derives the relationship between the Eulerian angles and the +# quaternion components: +function quaternion_from_eulerian(θ, ϕ, ψ) + ξ = sin(θ/2) * sin((ψ - ϕ)/2) + η = sin(θ/2) * cos((ψ - ϕ)/2) + ζ = cos(θ/2) * sin((ψ + ϕ)/2) + χ = cos(θ/2) * cos((ψ + ϕ)/2) + χ + ξ*𝐢 + η*𝐣 + ζ*𝐤 +end +#+ + +# These are all the conventions we will need from Whittaker. + end #module Whittaker #+ @@ -254,9 +286,16 @@ end #module Whittaker ϵᵣ = 10eps() #+ -# Test that the angles the line makes with the axes are what Whittaker intended -# TODO: implement - +# ### Basis vectors and handedness +# +# Test that the angles the line makes with the axes are what Whittaker intended: +for (α,β,γ) ∈ αβγrange() + l = line(α, β, γ) + @test acos(l ⋅ Whittaker.Ox) ≈ α atol=ϵₐ rtol=ϵᵣ + @test acos(l ⋅ Whittaker.Oy) ≈ β atol=ϵₐ rtol=ϵᵣ + @test acos(l ⋅ Whittaker.Oz) ≈ γ atol=ϵₐ rtol=ϵᵣ +end +#+ # Now we'll test the behavior that # > the line ``(α, β, γ)`` being directed vertically upwards, the rotation from the southern @@ -282,14 +321,32 @@ let α=π/2, β=π/2, γ=0 end #+ -# We can now test the "Eulerian" angles. The discussion above shows that the angles -# ``ϕ,θ,ψ`` in that order correspond to what we would denote as ``α,β,γ`` in that order. -# So we define our utility function for generating a variety of angles: +# ### Quaternions +# +# We simply test the multiplication rules: +import Quaternionic: 𝐢, 𝐣, 𝐤 + +@test 𝐢^2 == 𝐣^2 == 𝐤^2 == -1 +@test 𝐢 * 𝐣 == -𝐣 * 𝐢 == 𝐤 +@test 𝐣 * 𝐤 == -𝐤 * 𝐣 == 𝐢 +@test 𝐤 * 𝐢 == -𝐢 * 𝐤 == -𝐣 +#+ + +# ### "The Eulerian angles" +# The discussion above shows that the angles ``ϕ,θ,ψ`` in that order correspond to what we +# would denote as ``α,β,γ`` in that order. So we rename our utility function for generating +# a variety of angles: const ϕθψrange = αβγrange #+ -# First we test that the rotation as we've implemented it results in the angles between axes -# that Whittaker described: +# First, we test that the rotation as we've implemented it does correspond to the Euler +# rotation implemented by `Quaternionic`: +for (ϕ,θ,ψ) ∈ ϕθψrange() + @test Whittaker.eulerian_rotation(θ, ϕ, ψ) ≈ Quaternionic.from_euler_angles(ϕ, θ, ψ) +end +#+ + +# Next, we test that it results in the angles between axes that Whittaker described: for (ϕ,θ,ψ) ∈ ϕθψrange() @test YÔK(θ, ϕ, ψ) ≈ ϕ @test zÔZ(θ, ϕ, ψ) ≈ θ @@ -297,12 +354,34 @@ for (ϕ,θ,ψ) ∈ ϕθψrange() end #+ -# Next, we test that the rotation as we've implemented it does correspond to the Euler -# rotation implemented by `Quaternionic`: +# Finally, we'll test that the rotated axes project onto the fixed axes according to +# Whittaker's table of direction-cosines: for (ϕ,θ,ψ) ∈ ϕθψrange() - @test Whittaker.eulerian_rotation(θ, ϕ, ψ) ≈ Quaternionic.from_euler_angles(ϕ, θ, ψ) + X = Whittaker.Ox + Y = Whittaker.Oy + Z = Whittaker.Oz + R = Whittaker.eulerian_rotation(θ, ϕ, ψ) + x = R(X) + y = R(Y) + z = R(Z) + projections = [ + ## X Y Z + #= x =# x ⋅ X x ⋅ Y x ⋅ Z ; + #= y =# y ⋅ X y ⋅ Y y ⋅ Z ; + #= z =# z ⋅ X z ⋅ Y z ⋅ Z + ] + dcm = Whittaker.direction_cosine(θ, ϕ, ψ) + @test projections ≈ dcm atol=ϵₐ rtol=ϵᵣ end -#+ +# ### Connecting Eulerian angles to quaternions +# +# Finally, we can just test that the components Whittaker derived are the components we've +# been using. +for (ϕ,θ,ψ) ∈ ϕθψrange() + R₁ = Whittaker.eulerian_rotation(θ, ϕ, ψ) + R₂ = quaternion_from_eulerian(θ, ϕ, ψ) + @test R₁ ≈ R₂ atol=ϵₐ rtol=ϵᵣ +end end #@testitem #hide From 4eef11a0ef714812755a1d3cfbe574bfb6af9ab8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 09:55:19 -0500 Subject: [PATCH 296/329] Document usage a little more --- scripts/test.jl | 2 +- test/runtests.jl | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/test.jl b/scripts/test.jl index 80451682..d0285a13 100644 --- a/scripts/test.jl +++ b/scripts/test.jl @@ -1,6 +1,6 @@ # Call this from the top-level directory as # julia -t auto scripts/test.jl -# See docs/src/developments/index.md for more information. +# See docs/src/development/index.md for more information. import Dates println("Running tests starting at ", Dates.format(Dates.now(), "HH:MM:SS"), ".") diff --git a/test/runtests.jl b/test/runtests.jl index 171979f5..b91c6dbd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,18 +1,37 @@ -# See docs/src/developments/index.md for details of how to run tests with this script. +# See docs/src/development/index.md for details of how to run tests with this script. using TestItemRunner using ArgParse function parse_commandline() - settings = ArgParseSettings() + settings = ArgParseSettings( + description = """Run selected tests from the SphericalFunctions.jl test suite. + + \ua0 + + See docs/src/development/index.md for details of how to run tests with this script. + + \ua0 + + The RUN and SKIP arguments may be names of individual tests (in quotes if there are + spaces), tags (which must be prefixed by `:`) that are given in the `@testitem`, or + files (which must end with `.jl`). Note that SKIP takes precedence over RUN if both + are specified; a test matching both a run and a skip filter will be skipped. Any + test with the `:skipci` tag will be skipped whenever the environment variable `CI` + is set to "true" (which is the case on, e.g., GitHub Actions). + """, + usage = "julia test/runtests.jl [-h] [RUN...] [--skip SKIP...]", + ) @add_arg_table! settings begin # Collect everything before the optional `--skip` "run" nargs = '*' + help = "names, tags, or files to run" # Collect everything after the optional `--skip` "--skip" nargs = '*' default = String[] + help = "names, tags, or files to skip" end parsed_args = parse_args(settings) run_files = Tuple(Regex(s) for s ∈ parsed_args["run"] if endswith(s, ".jl")) From 70668f64a45c3253a10616288c43b83d2b1eef1a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 09:56:36 -0500 Subject: [PATCH 297/329] Require `--coverage` flag to run coverage --- scripts/test.jl | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/test.jl b/scripts/test.jl index d0285a13..c14c7b66 100644 --- a/scripts/test.jl +++ b/scripts/test.jl @@ -1,5 +1,7 @@ # Call this from the top-level directory as # julia -t auto scripts/test.jl +# or to run with coverage as +# julia -t auto scripts/test.jl --coverage # See docs/src/development/index.md for more information. import Dates @@ -13,16 +15,25 @@ using LinearAlgebra using Base.Threads LinearAlgebra.BLAS.set_num_threads(nthreads()) +# Check for the `--coverage` flag +coverage = any(ARGS .== "--coverage") + try - Δt = @elapsed Pkg.test("SphericalFunctions"; coverage=true, test_args=ARGS) + Δt = @elapsed Pkg.test("SphericalFunctions"; coverage, test_args=ARGS) println("Running tests took $Δt seconds.") catch e - println("Tests failed; proceeding to coverage") + if coverage + println("Tests failed; proceeding to coverage") + else + println("Tests failed.") + end end -Pkg.activate() # Activate Julia's base (home) directory -using Coverage -cd((@__DIR__) * "/..") -coverage = Coverage.process_folder("src") -Coverage.writefile("lcov.info", coverage) -Coverage.clean_folder(".") +if coverage + Pkg.activate() # Activate Julia's base (home) directory + using Coverage + cd((@__DIR__) * "/..") + coverage = Coverage.process_folder("src") + Coverage.writefile("lcov.info", coverage) + Coverage.clean_folder(".") +end From c6cf964b15a0f222bb9828c9585b41f172ad766d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 10:42:15 -0500 Subject: [PATCH 298/329] Clarify Whittaker's spherical coordinates --- .../conventions/comparisons/whittaker_1947.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index 54bec1b6..33ca5c8a 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -5,11 +5,12 @@ md""" Whittaker consolidated conventions that are still used by physicists today. That includes a right-handed orthogonal coordinate system labeled ``(x, y, z)``; "Eulerian - angles" that he labels ``(θ, ϕ, ψ)``, but which we would label ``(ϕ, θ, ψ)``; the - resulting spherical coordinates as we now use them, with ``θ`` being colatitude, ``ϕ`` - being the azimuthal angle from the ``x``-axis, and ``ψ=0``; and the standard quaternion - basis elements ``(i, j, k)``, with rotation of a vector ``v`` by a quaternion ``q`` is - given by ``q v q⁻¹``. + angles" that he labels ``(θ, ϕ, ψ)`` but which we would label ``(ϕ, θ, ψ)``, meaning + that we use the same symbols for the same angles, but in different orders; the resulting + spherical coordinates as we now use them, with ``θ`` being colatitude, ``ϕ`` being the + azimuthal angle from the ``x``-axis, and ``ψ=0``; and the standard quaternion basis + elements ``(i, j, k)``, with rotation of a vector ``v`` by a quaternion ``q`` is given + by ``q v q⁻¹``. !!! warning @@ -380,7 +381,7 @@ end # been using. for (ϕ,θ,ψ) ∈ ϕθψrange() R₁ = Whittaker.eulerian_rotation(θ, ϕ, ψ) - R₂ = quaternion_from_eulerian(θ, ϕ, ψ) + R₂ = Whittaker.quaternion_from_eulerian(θ, ϕ, ψ) @test R₁ ≈ R₂ atol=ϵₐ rtol=ϵᵣ end From 1ed4c6105b99e2d4468deb31ae6bbeba82af50bc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 10:43:36 -0500 Subject: [PATCH 299/329] Upload coverage without nightly --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aa159a82..e44a3ecc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: - uses: julia-actions/julia-runtest@latest - uses: julia-actions/julia-processcoverage@latest - uses: codecov/codecov-action@v5 - if: "matrix.version == 'nightly' && matrix.os == 'ubuntu-latest'" + if: "matrix.version == '1' && matrix.os == 'ubuntu-latest'" with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} From bb36273345a67c6ea9e663eb28f3068211723782 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 14:08:17 -0500 Subject: [PATCH 300/329] Get map2salm! working with newer Julia threading approaches --- src/SphericalFunctions.jl | 2 +- src/map2salm.jl | 181 +++++++++++++++++++++++++------------- 2 files changed, 119 insertions(+), 64 deletions(-) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index a420fc51..d7b5346b 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -11,7 +11,7 @@ using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, using StaticArrays: @SVector using SpecialFunctions, DoubleFloats using LoopVectorization: @turbo -using Base.Threads: @threads, nthreads +using Base.Threads: @threads, threadpoolsize const MachineFloat = Union{Float16, Float32, Float64} diff --git a/src/map2salm.jl b/src/map2salm.jl index 50fc7cfc..b8bca32a 100644 --- a/src/map2salm.jl +++ b/src/map2salm.jl @@ -4,14 +4,14 @@ Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes. -The `map` array should have size Nφ along its first dimension and Nϑ along its -second; any number of dimensions may follow. The `spin` must be entered -explicitly, and `ℓmax` is the highest ℓ value you want in the output. +The `map` array should have size Nφ along its first dimension and Nϑ along its second; any +number of dimensions may follow. The `spin` must be entered explicitly, and `ℓmax` is the +highest ℓ value you want in the output. -For repeated applications of this function with different values of `map`, it -is more efficient to pre-compute `plan` using [`plan_map2salm`](@ref). These -functions will create a new `salm` array on each call. To operate in place on -a pre-allocated `salm` array, use [`map2salm!`](@ref). +For repeated applications of this function with different values of `map`, it is more +efficient to pre-compute `plan` using [`plan_map2salm`](@ref). These functions will create +a new `salm` array on each call. To operate in place on a pre-allocated `salm` array, use +[`map2salm!`](@ref). The core of this function follows the method described by [Reinecke and Seljebotn](@cite Reinecke_2013). @@ -28,8 +28,7 @@ end map2salm!(salm, map, spin, ℓmax) map2salm!(salm, map, plan) -Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes in -place. +Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes in place. For details, see [`map2salm`](@ref). @@ -43,35 +42,79 @@ function map2salm!( map2salm!(salm, map, plan, show_progress) end +# Per-worker workspace of preallocated memory to modify during map2salm computations. +struct Workspace_map2salm{T<:Real,P} + G::Vector{Complex{T}} + fftplan::P +end + +@inline function with_workspace(f, pool::Channel) + ws = take!(pool) + try + return f(ws) + finally + put!(pool, ws) + end +end + """ plan_map2salm(map, spin, ℓmax) -Precompute values to use in executing [`map2salm`](@ref) or -[`map2salm!`](@ref). +Precompute values to use in executing [`map2salm`](@ref) or [`map2salm!`](@ref). -The arguments to this function exactly mirror those of the first form of -[`map2salm`](@ref), and all but the first argument in the first form of -[`map2salm!`](@ref). The `plan` returned by this function can be passed to the -second forms of those functions to avoid some computation and allocation costs. +The arguments to this function exactly mirror those of the first form of [`map2salm`](@ref), +and all but the first argument in the first form of [`map2salm!`](@ref). The `plan` +returned by this function can be passed to the second forms of those functions to avoid some +computation and allocation costs. -Note that the `plan` object is not thread safe; a separate `plan` should be -created for each thread that will use one, or locks should be used to ensure -that a single `plan` is not used at the same time on different threads. +Note that the `plan` object is not thread safe; a separate `plan` should be created for each +thread that will use one, or locks/Channels should be used to ensure that a single `plan` is +not used at the same time on different threads. """ function plan_map2salm(map_data::AbstractArray{Complex{T}}, spin::Int, ℓmax::Int) where {T<:Real} Nφ, Nϑ, Nextra... = size(map_data) - Gs = [Array{complex(T)}(undef, (Nφ,)) for i = 1:nthreads()] m′max = abs(spin) Hwedge = Array{T}(undef, WignerHsize(ℓmax, m′max)) H_rec_coeffs = H_recursion_coefficients(ℓmax, T) weight = clenshaw_curtis(Nϑ, T) expiθ = complex_powers(cis(π / T(Nϑ-1)), Nϑ-1) ϵs = ϵ(-spin) - extra_dims = Base.Iterators.product((1:e for e in Nextra)...) - fftplan = T<:MachineFloat ? plan_fft(map_data[:, 1, first(extra_dims)...]) : nothing - return (spin, ℓmax, Nφ, Nϑ, Nextra, Gs, m′max, Hwedge, H_rec_coeffs, weight, expiθ, ϵs, extra_dims, fftplan) + # Indexable without allocation (unlike collect(product(...))). + extra_dims = CartesianIndices(Nextra) + + # Number of workers seen by `@thread` + Nworkers = threadpoolsize(:default) + + workspace_pool = if T <: MachineFloat + # Build FFT plan from a representative slice + proto_idx = extra_dims[1] + proto = @views map_data[:, 1, proto_idx.I...] + fftplan₁ = plan_fft(proto) + P = typeof(fftplan₁) + workspace_pool = Channel{Workspace_map2salm{T,P}}(Nworkers) + for i ∈ 1:Nworkers + Gᵢ = Vector{Complex{T}}(undef, Nφ) + planᵢ = plan_fft(proto) + put!(workspace_pool, Workspace_map2salm{T,P}(Gᵢ, planᵢ)) + end + workspace_pool + else + P = Nothing + workspace_pool = Channel{Workspace_map2salm{T,Nothing}}(Nworkers) + for i ∈ 1:Nworkers + Gᵢ = Vector{Complex{T}}(undef, Nφ) + planᵢ = nothing + put!(workspace_pool, Workspace_map2salm{T,P}(Gᵢ, planᵢ)) + end + workspace_pool + end + + return ( + spin, ℓmax, Nφ, Nϑ, Nextra, workspace_pool, m′max, + Hwedge, H_rec_coeffs, weight, expiθ, ϵs, extra_dims + ) end @@ -96,7 +139,10 @@ end function map2salm!( salm::AbstractArray{Complex{T}}, map::AbstractArray{Complex{T}}, - (spin, ℓmax, Nφ, Nϑ, Nextra, Gs, m′max, Hwedge, H_rec_coeffs, weight, expiθ, ϵs, extra_dims, fftplan), + ( + spin, ℓmax, Nφ, Nϑ, Nextra, pool, m′max, Hwedge, + H_rec_coeffs, weight, expiθ, ϵs, extra_dims + ), show_progress=false ) where {T<:Real} s1 = size(salm) @@ -105,57 +151,66 @@ function map2salm!( absspin = abs(spin) progress = Progress(Nϑ * prod(Nextra); showspeed=true, enabled=show_progress) + proglock = ReentrantLock() @inbounds for ϑ ∈ 1:Nϑ H!(Hwedge, expiθ[ϑ], ℓmax, m′max, H_rec_coeffs, WignerHindex) - @threads for extra ∈ collect(extra_dims) - # NOTE: We can't thread at a higher level because each thread could access the - # same element of `salm` simultaneously below; by threading at this level, we - # are assured that the `extra...` index used below is unique to each thread. - iₜ = Threads.threadid() - G = Gs[iₜ] - computeG!(G, map[:, ϑ, extra...], weight[ϑ], fftplan) - for ℓ ∈ absspin:ℓmax - λ_factor = ϵs * √((2ℓ+1)*T(π)) / Nφ - - i₀ = WignerHindex(ℓ, spin, 0, m′max) - - let m=0 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * λ_factor * Hwedge[i₀] - end - i₊ = i₀ - i₋ = i₀ - if !signbit(spin) - for m ∈ 1:min(ℓ, absspin) - i₊ -= ℓ-m+2 - i₋ += ℓ-m+1 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra...] += - G[Nφ-m+1] * λ_factor * Hwedge[i₋] + # NOTE: We can't thread at a higher level because each thread could access the + # same element of `salm` simultaneously below; by threading at this level, we + # are assured that the `extra.I...` index used below is unique to each thread. + @threads for extra ∈ extra_dims + # extra is a CartesianIndex; indices are in extra.I + with_workspace(pool) do ws + G = ws.G + @views computeG!(G, map[:, ϑ, extra.I...], weight[ϑ], ws.fftplan) + for ℓ ∈ absspin:ℓmax + λ_factor = ϵs * √((2ℓ+1)*T(π)) / Nφ + + i₀ = WignerHindex(ℓ, spin, 0, m′max) + + let m=0 + salm[Yindex(ℓ, m), extra.I...] += + G[m+1] * λ_factor * Hwedge[i₀] end - else - for m ∈ 1:min(ℓ, absspin) - i₊ += ℓ-m+1 - i₋ -= ℓ-m+2 - salm[Yindex(ℓ, m), extra...] += + + i₊ = i₀ + i₋ = i₀ + if !signbit(spin) + for m ∈ 1:min(ℓ, absspin) + i₊ -= ℓ-m+2 + i₋ += ℓ-m+1 + salm[Yindex(ℓ, m), extra.I...] += + G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] + salm[Yindex(ℓ, -m), extra.I...] += + G[Nφ-m+1] * λ_factor * Hwedge[i₋] + end + else + for m ∈ 1:min(ℓ, absspin) + i₊ += ℓ-m+1 + i₋ -= ℓ-m+2 + salm[Yindex(ℓ, m), extra.I...] += + G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] + salm[Yindex(ℓ, -m), extra.I...] += + G[Nφ-m+1] * λ_factor * Hwedge[i₋] + end + end + for m ∈ absspin+1:ℓ + i₊ += 1 + i₋ += 1 + salm[Yindex(ℓ, m), extra.I...] += G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra...] += + salm[Yindex(ℓ, -m), extra.I...] += G[Nφ-m+1] * λ_factor * Hwedge[i₋] end end - for m ∈ absspin+1:ℓ - i₊ += 1 - i₋ += 1 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra...] += - G[Nφ-m+1] * λ_factor * Hwedge[i₋] + end + + if show_progress + lock(proglock) do + next!(progress) end end - next!(progress) end end end From 9c360cbecc36cdfab7c98964aecb6a0419bb0c4d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 14:08:28 -0500 Subject: [PATCH 301/329] Fix syntax error causing docs to fail --- docs/literate_input/conventions/comparisons/whittaker_1947.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index 33ca5c8a..4f9d49ce 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -374,6 +374,7 @@ for (ϕ,θ,ψ) ∈ ϕθψrange() dcm = Whittaker.direction_cosine(θ, ϕ, ψ) @test projections ≈ dcm atol=ϵₐ rtol=ϵᵣ end +#+ # ### Connecting Eulerian angles to quaternions # From 05bb95778b850e36de50fc457b6a68baffbe222c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 15:21:38 -0500 Subject: [PATCH 302/329] Use the recommended iteration over CartesianIndices --- src/map2salm.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/map2salm.jl b/src/map2salm.jl index b8bca32a..a3a78148 100644 --- a/src/map2salm.jl +++ b/src/map2salm.jl @@ -158,19 +158,19 @@ function map2salm!( # NOTE: We can't thread at a higher level because each thread could access the # same element of `salm` simultaneously below; by threading at this level, we - # are assured that the `extra.I...` index used below is unique to each thread. + # are assured that the `Tuple(extra)...` index used below is unique to each thread. @threads for extra ∈ extra_dims - # extra is a CartesianIndex; indices are in extra.I + # extra is a CartesianIndex; indices are in Tuple(extra) with_workspace(pool) do ws G = ws.G - @views computeG!(G, map[:, ϑ, extra.I...], weight[ϑ], ws.fftplan) + @views computeG!(G, map[:, ϑ, Tuple(extra)...], weight[ϑ], ws.fftplan) for ℓ ∈ absspin:ℓmax λ_factor = ϵs * √((2ℓ+1)*T(π)) / Nφ i₀ = WignerHindex(ℓ, spin, 0, m′max) let m=0 - salm[Yindex(ℓ, m), extra.I...] += + salm[Yindex(ℓ, m), Tuple(extra)...] += G[m+1] * λ_factor * Hwedge[i₀] end @@ -180,27 +180,27 @@ function map2salm!( for m ∈ 1:min(ℓ, absspin) i₊ -= ℓ-m+2 i₋ += ℓ-m+1 - salm[Yindex(ℓ, m), extra.I...] += + salm[Yindex(ℓ, m), Tuple(extra)...] += G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra.I...] += + salm[Yindex(ℓ, -m), Tuple(extra)...] += G[Nφ-m+1] * λ_factor * Hwedge[i₋] end else for m ∈ 1:min(ℓ, absspin) i₊ += ℓ-m+1 i₋ -= ℓ-m+2 - salm[Yindex(ℓ, m), extra.I...] += + salm[Yindex(ℓ, m), Tuple(extra)...] += G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra.I...] += + salm[Yindex(ℓ, -m), Tuple(extra)...] += G[Nφ-m+1] * λ_factor * Hwedge[i₋] end end for m ∈ absspin+1:ℓ i₊ += 1 i₋ += 1 - salm[Yindex(ℓ, m), extra.I...] += + salm[Yindex(ℓ, m), Tuple(extra)...] += G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra.I...] += + salm[Yindex(ℓ, -m), Tuple(extra)...] += G[Nφ-m+1] * λ_factor * Hwedge[i₋] end end From a03cdcede79a6fe3bfb8e223786e5586c0f588e4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 15:21:58 -0500 Subject: [PATCH 303/329] Catch Ctrl-C when using newest TestItemRunners --- test/runtests.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index b91c6dbd..6fbd747b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,8 @@ # See docs/src/development/index.md for details of how to run tests with this script. +# This is to ensure that, even run as a script, Ctrl-C will actually interrupt the tests. +Base.exit_on_sigint(false) + using TestItemRunner using ArgParse From ef9f84b25d85a3be0cb9707705a56d1e021b042e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 15:22:05 -0500 Subject: [PATCH 304/329] Fix all the Whittaker tests --- .../conventions/comparisons/whittaker_1947.jl | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index 4f9d49ce..4f9d6f15 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -69,7 +69,7 @@ module Whittaker # right-handed orthogonal system, which we will represent by the corresponding unit vectors # as `QuatVec`s: -import Quaternionic: Quaternionic, Rotor, 𝐢, 𝐣, 𝐤, ⋅, ×̂ +import Quaternionic: Quaternionic, 𝐢, 𝐣, 𝐤, ⋅, ×̂ const Ox = Quaternionic.𝐢 const Oy = Quaternionic.𝐣 @@ -211,7 +211,7 @@ function eulerian_rotation(θ, ϕ, ψ) R₂ = exp((θ/2) * OK) Oz = R₂(R₁(OZ)) R₃ = exp((ψ/2) * Oz) - R₁ * R₂ * R₃ + R₃ * R₂ * R₁ end #+ @@ -229,21 +229,21 @@ end function YÔK(θ, ϕ, ψ) OY = 𝐣 let OK=OK(θ, ϕ, ψ) - acos(OY ⋅ OK) + acos(clamp(OY ⋅ OK, -1, 1)) end end function zÔZ(θ, ϕ, ψ) OZ = 𝐤 let OK=OK(θ, ϕ, ψ) Oz = eulerian_rotation(θ, ϕ, ψ)(OZ) - acos(Oz ⋅ OZ) + acos(clamp(Oz ⋅ OZ, -1, 1)) end end function yÔK(θ, ϕ, ψ) OY = 𝐣 let OK=OK(θ, ϕ, ψ) Oy = eulerian_rotation(θ, ϕ, ψ)(OY) - acos(Oy ⋅ OK) + acos(clamp(Oy ⋅ OK, -1, 1)) end end #+ @@ -289,9 +289,12 @@ end #module Whittaker # ### Basis vectors and handedness # -# Test that the angles the line makes with the axes are what Whittaker intended: -for (α,β,γ) ∈ αβγrange() - l = line(α, β, γ) +# We'll test that the angles the line makes with the axes are what Whittaker intended. +# First, we need a sampling of "direction angles", which should be in the range ``[0, π]``. +# This range is supplied by the `θrange` function: +import Quaternionic: ⋅ +for (α,β,γ) ∈ Iterators.product(θrange(), θrange(), θrange()) + l = Whittaker.line(α, β, γ) @test acos(l ⋅ Whittaker.Ox) ≈ α atol=ϵₐ rtol=ϵᵣ @test acos(l ⋅ Whittaker.Oy) ≈ β atol=ϵₐ rtol=ϵᵣ @test acos(l ⋅ Whittaker.Oz) ≈ γ atol=ϵₐ rtol=ϵᵣ @@ -325,12 +328,12 @@ end # ### Quaternions # # We simply test the multiplication rules: -import Quaternionic: 𝐢, 𝐣, 𝐤 +import Quaternionic: Quaternionic, 𝐢, 𝐣, 𝐤 @test 𝐢^2 == 𝐣^2 == 𝐤^2 == -1 @test 𝐢 * 𝐣 == -𝐣 * 𝐢 == 𝐤 @test 𝐣 * 𝐤 == -𝐤 * 𝐣 == 𝐢 -@test 𝐤 * 𝐢 == -𝐢 * 𝐤 == -𝐣 +@test 𝐤 * 𝐢 == -𝐢 * 𝐤 == 𝐣 #+ # ### "The Eulerian angles" @@ -347,16 +350,26 @@ for (ϕ,θ,ψ) ∈ ϕθψrange() end #+ -# Next, we test that it results in the angles between axes that Whittaker described: -for (ϕ,θ,ψ) ∈ ϕθψrange() - @test YÔK(θ, ϕ, ψ) ≈ ϕ - @test zÔZ(θ, ϕ, ψ) ≈ θ - @test yÔK(θ, ϕ, ψ) ≈ ψ +# Next, we test that it results in the angles between axes that Whittaker described. Note +# that this test is very susceptible to gimbal lock when ``θ`` is near ``0`` or ``π``, so we +# avoid the poles by moving ``θ`` slightly away from them: +avoid_poles = √(eps(Float64)) +#+ + +# Also, because we have to use "direction angles" again, we need to ensure that ϕ and ψ +# are in ``[-π, π]``, so we just subtract ``π`` from each to use the usual generator, and +# only test that the absolute values of those two angles are correct: +for (ϕ,θ,ψ) ∈ ϕθψrange(;avoid_poles) + ϕ = ϕ - π + ψ = ψ - π + @test Whittaker.YÔK(θ, ϕ, ψ) ≈ abs(ϕ) atol=√ϵₐ rtol=√ϵᵣ + @test Whittaker.zÔZ(θ, ϕ, ψ) ≈ θ atol=√ϵₐ rtol=√ϵᵣ + @test Whittaker.yÔK(θ, ϕ, ψ) ≈ abs(ψ) atol=√ϵₐ rtol=√ϵᵣ end #+ -# Finally, we'll test that the rotated axes project onto the fixed axes according to -# Whittaker's table of direction-cosines: +# Now, we'll test that the rotated axes project onto the fixed axes according to Whittaker's +# table of direction-cosines: for (ϕ,θ,ψ) ∈ ϕθψrange() X = Whittaker.Ox Y = Whittaker.Oy @@ -372,14 +385,14 @@ for (ϕ,θ,ψ) ∈ ϕθψrange() #= z =# z ⋅ X z ⋅ Y z ⋅ Z ] dcm = Whittaker.direction_cosine(θ, ϕ, ψ) - @test projections ≈ dcm atol=ϵₐ rtol=ϵᵣ + @test projections ≈ dcm atol=10ϵₐ rtol=10ϵᵣ end #+ # ### Connecting Eulerian angles to quaternions # -# Finally, we can just test that the components Whittaker derived are the components we've -# been using. +# Finally, we can just test that the quaternion components Whittaker derived are the +# components we've been using. for (ϕ,θ,ψ) ∈ ϕθψrange() R₁ = Whittaker.eulerian_rotation(θ, ϕ, ψ) R₂ = Whittaker.quaternion_from_eulerian(θ, ϕ, ψ) From c25f09c1c104b9e66afda38862b0a94bdf96967b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 21:34:40 -0500 Subject: [PATCH 305/329] Fix capitalization in include statement --- src/redesign/SphericalFunctions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index 51d94649..d655450e 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -5,11 +5,11 @@ import TestItems: @testitem, @testsnippet import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, FixedSizeVector -# TEMPORARY!!!! Should be able to remove once this moves to SphericalFunctions proper +# TODO: remove once this moves to SphericalFunctions proper import SphericalFunctions: ComplexPowers -include("wigner/wigner.jl") +include("Wigner/wigner.jl") # function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} From 1a3910067b7d44c764c2c73e50a2345edf9b5783 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 5 Jan 2026 22:08:09 -0500 Subject: [PATCH 306/329] Loosen test tolerances --- docs/literate_input/conventions/comparisons/blanchet_2024.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl index 575359a3..925f757d 100644 --- a/docs/literate_input/conventions/comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -103,8 +103,8 @@ s = -2 # case, which is the only one defined in the paper. # We will need to test approximate floating-point equality, # so we set absolute and relative tolerances (respectively) in terms of the machine epsilon: -ϵₐ = 10eps() -ϵᵣ = 500eps() +ϵₐ = 30eps() +ϵᵣ = 1500eps() #+ # This loose relative tolerance is necessary because the numerical errors in Blanchet's From a6d3656d9f28777b87d569b7ddf7aafd30dad6ce Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 09:03:42 -0500 Subject: [PATCH 307/329] Comment about why log* functions should stay --- src/utils.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 4bc6fae6..06e64c5f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -26,6 +26,8 @@ Base.parent(M::OffsetMat) = M.parent #@propagate_inbounds Base.getindex(M::OffsetMat, ::Colon, j::Int) = parent(M)[:, j-offset2(M)] +# Note that the loggamma and logbinomial functions below are not used directly in the +# package, but are used in sqrtbinomial, which *is* used. loggamma(a, ::Type{T}) where {T<:DoubleFloats.MultipartFloat} = DoubleFloats.loggamma(T(a)) loggamma(a, ::Type{T}) where T = SpecialFunctions.loggamma(T(a)) function logbinomial(n::T, k::T, S=float(T)) where {T<:Integer} @@ -38,7 +40,10 @@ function logbinomial(n::T, k::T, S=float(T)) where {T<:Integer} if k == 1 return log(S(n)) else - return -log1p(S(n)) - loggamma(n - k + one(T), S) - loggamma(k + one(T), S) + loggamma(n + 2one(T), S) + return ( + -log1p(S(n)) - loggamma(n - k + one(T), S) + - loggamma(k + one(T), S) + loggamma(n + 2one(T), S) + ) end end From 572c4c810ba84a403a2ef1780aa37c83be4334ee Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 13:23:35 -0500 Subject: [PATCH 308/329] Deprecate main interface; move redesign to front --- .../conventions/comparisons/blanchet_2024.jl | 2 +- .../comparisons/cohen_tannoudji_1991.jl | 4 +- .../comparisons/condon_shortley_1935.jl | 2 +- .../conventions/comparisons/lalsuite_2025.jl | 8 +- .../conventions/comparisons/ninja_2011.jl | 4 +- src/SphericalFunctions.jl | 66 +++-------- src/deprecated/Deprecated.jl | 67 +++++++++++ src/{ => deprecated}/Hrecursion.jl | 0 src/{ => deprecated}/associated_legendre.jl | 0 src/{ => deprecated}/evaluate.jl | 2 +- src/{ => deprecated}/indexing.jl | 0 src/{ => deprecated}/iterators.jl | 0 src/{ => deprecated}/map2salm.jl | 0 src/{ => deprecated}/operators.jl | 0 src/{ => deprecated}/ssht.jl | 0 src/{ => deprecated}/ssht/direct.jl | 0 src/{ => deprecated}/ssht/minimal.jl | 0 src/{ => deprecated}/ssht/rs.jl | 0 src/redesign/SphericalFunctions.jl | 12 +- src/redesign/Wigner/wigner_matrix.jl | 2 +- src/{ => utilities}/complex_powers.jl | 0 src/{ => utilities}/pixelizations.jl | 0 src/{ => utilities}/utils.jl | 0 src/{ => utilities}/weights.jl | 0 test/conventions/boyle2016.jl | 5 +- test/conventions/edmonds.jl | 5 +- test/conventions/goldbergetal.jl | 5 +- test/conventions/sakurai.jl | 5 +- test/conventions/thorne.jl | 3 +- test/conventions/torresdelcastillo.jl | 5 +- test/conventions/varshalovich.jl | 3 +- test/conventions/wigner.jl | 3 +- test/{ => deprecated}/associated_legendre.jl | 11 +- test/{ => deprecated}/indexing.jl | 102 ++++++++--------- test/{ => deprecated}/iterators.jl | 25 ++-- test/{ => deprecated}/map2salm.jl | 11 +- test/{ => deprecated}/operators.jl | 108 ++++++++++-------- test/{ => deprecated}/ssht.jl | 62 +++++----- test/{ => deprecated}/wigner_matrices.jl | 0 test/{ => deprecated}/wigner_matrices/H.jl | 28 +++-- .../{ => deprecated}/wigner_matrices/big_D.jl | 57 ++++----- test/{ => deprecated}/wigner_matrices/sYlm.jl | 51 +++++---- .../wigner_matrices/small_d.jl | 23 ++-- test/haxis.jl | 2 +- test/hwedge.jl | 2 +- test/runtests.jl | 4 +- test/utilities/utilities.jl | 2 +- 47 files changed, 381 insertions(+), 310 deletions(-) create mode 100644 src/deprecated/Deprecated.jl rename src/{ => deprecated}/Hrecursion.jl (100%) rename src/{ => deprecated}/associated_legendre.jl (100%) rename src/{ => deprecated}/evaluate.jl (99%) rename src/{ => deprecated}/indexing.jl (100%) rename src/{ => deprecated}/iterators.jl (100%) rename src/{ => deprecated}/map2salm.jl (100%) rename src/{ => deprecated}/operators.jl (100%) rename src/{ => deprecated}/ssht.jl (100%) rename src/{ => deprecated}/ssht/direct.jl (100%) rename src/{ => deprecated}/ssht/minimal.jl (100%) rename src/{ => deprecated}/ssht/rs.jl (100%) rename src/{ => utilities}/complex_powers.jl (100%) rename src/{ => utilities}/pixelizations.jl (100%) rename src/{ => utilities}/utils.jl (100%) rename src/{ => utilities}/weights.jl (100%) rename test/{ => deprecated}/associated_legendre.jl (73%) rename test/{ => deprecated}/indexing.jl (67%) rename test/{ => deprecated}/iterators.jl (88%) rename test/{ => deprecated}/map2salm.jl (81%) rename test/{ => deprecated}/operators.jl (67%) rename test/{ => deprecated}/ssht.jl (77%) rename test/{ => deprecated}/wigner_matrices.jl (100%) rename test/{ => deprecated}/wigner_matrices/H.jl (79%) rename test/{ => deprecated}/wigner_matrices/big_D.jl (72%) rename test/{ => deprecated}/wigner_matrices/sYlm.jl (66%) rename test/{ => deprecated}/wigner_matrices/small_d.jl (77%) diff --git a/docs/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl index 925f757d..cebbdcac 100644 --- a/docs/literate_input/conventions/comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -111,7 +111,7 @@ s = -2 # explicit expressions grow rapidly with ``\ell``. for (θ, ϕ) ∈ θϕrange() for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) - @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end #+ diff --git a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl index e928c42d..68972289 100644 --- a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl @@ -119,8 +119,8 @@ end # module CohenTannoudji # are singular at the poles, so we avoid evaluating there. for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) - @test CohenTannoudji.Y₁(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ - @test CohenTannoudji.Y₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test CohenTannoudji.Y₁(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test CohenTannoudji.Y₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end #+ diff --git a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl index 0523bb63..cd64c796 100644 --- a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl @@ -187,7 +187,7 @@ end # normalization differences, which are the most likely source of error. for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) - @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end #+ diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl index e45d0580..15ba491e 100644 --- a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -48,6 +48,8 @@ usually be defined in separate C headers. using TestItems: @testitem #hide @testitem "LALSuite conventions" setup=[ConventionsUtilities, ConventionsSetup, Utilities] begin #hide +import SphericalFunctions: Deprecated + module LALSuite using Printf: @sprintf @@ -175,7 +177,7 @@ s = -2 for (θ, ϕ) ∈ θϕrange() for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) @test LALSuite.XLALSpinWeightedSphericalHarmonic(θ, ϕ, s, ℓ, m) ≈ - SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + Deprecated.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end #+ @@ -189,7 +191,7 @@ end for β ∈ βrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) @test LALSuite.XLALWignerdMatrix(ℓ, m′, m, β) ≈ - SphericalFunctions.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ + Deprecated.d(ℓ, m′, m, β) atol=ϵₐ rtol=ϵᵣ end end #+ @@ -203,7 +205,7 @@ end for (α,β,γ) ∈ αβγrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) @test LALSuite.XLALWignerDMatrix(ℓ, m′, m, α, β, γ) ≈ - conj(SphericalFunctions.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ + conj(Deprecated.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ end end #+ diff --git a/docs/literate_input/conventions/comparisons/ninja_2011.jl b/docs/literate_input/conventions/comparisons/ninja_2011.jl index 6de73d26..e6f11d73 100644 --- a/docs/literate_input/conventions/comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions/comparisons/ninja_2011.jl @@ -134,7 +134,7 @@ sₘₐₓ = 2 # normalization differences, which are the most likely source of error. for (θ, ϕ) ∈ θϕrange() for (s, ℓ, m) ∈ sℓmrange(ℓₘₐₓ, sₘₐₓ) - @test NINJA.ₛYₗₘ(s, ℓ, m, θ, ϕ) ≈ SphericalFunctions.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test NINJA.ₛYₗₘ(s, ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ end end #+ @@ -142,7 +142,7 @@ end # Finally, we compare the Wigner ``d`` matrix to the `SphericalFunctions` package. for ι ∈ θrange() for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) - @test NINJA.d(ℓ, m′, m, ι) ≈ SphericalFunctions.d(ℓ, m′, m, ι) atol=ϵₐ rtol=ϵᵣ + @test NINJA.d(ℓ, m′, m, ι) ≈ SphericalFunctions.Deprecated.d(ℓ, m′, m, ι) atol=ϵₐ rtol=ϵᵣ end end #+ diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index d7b5346b..778a9e2a 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -1,70 +1,32 @@ module SphericalFunctions -using TestItems: @testitem -using FastTransforms: FFTW, fft, fftshift!, ifft, ifftshift!, irfft, - plan_bfft!, plan_fft, plan_fft! -using LinearAlgebra: LinearAlgebra, Bidiagonal, Diagonal, convert, ldiv!, mul! -using OffsetArrays: OffsetArray, OffsetVector -using ProgressMeter: Progress, next! -using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, - to_euler_phases, to_spherical_coordinates, basetype +using TestItems: @testitem, @testsnippet +using FastTransforms: ifft, irfft +using Quaternionic: from_spherical_coordinates using StaticArrays: @SVector using SpecialFunctions, DoubleFloats -using LoopVectorization: @turbo -using Base.Threads: @threads, threadpoolsize const MachineFloat = Union{Float16, Float32, Float64} +# Utilities (kept top-level; code lives in `src/utilities/`) +include("utilities/utils.jl") -include("utils.jl") - -include("pixelizations.jl") +include("utilities/pixelizations.jl") export golden_ratio_spiral_pixels, golden_ratio_spiral_rotors export sorted_rings, sorted_ring_pixels, sorted_ring_rotors export fejer1_rings, fejer2_rings, clenshaw_curtis_rings -include("complex_powers.jl") +include("utilities/complex_powers.jl") export complex_powers, complex_powers!, ComplexPowers -include("indexing.jl") -export Ysize, Yrange, Yindex, deduce_limits, theta_phi, phi_theta -export WignerHsize, WignerHindex, _WignerHindex, WignerHrange -export WignerDsize, WignerDindex, WignerDrange - -include("iterators.jl") -export D_iterator, d_iterator, sYlm_iterator, λ_iterator -# Legacy API: -export Diterator, diterator, Yiterator, λiterator - -include("associated_legendre.jl") -export ALFRecursionCoefficients, ALFrecurse!, ALFcompute!, ALFcompute - -include("Hrecursion.jl") -export H!, H_recursion_coefficients - -include("evaluate.jl") -export d_prep, d_matrices, d_matrices! -export D_prep, D_matrices, D_matrices! -export sYlm_prep, sYlm_values, sYlm_values! -# Legacy API: -export d!, d, D!, Y!, dprep, Dprep, Yprep, ₛ𝐘 - -include("weights.jl") +include("utilities/weights.jl") export fejer1, fejer2, clenshaw_curtis -include("ssht.jl") -export SSHT, pixels, rotors - -include("map2salm.jl") -export map2salm, map2salm!, plan_map2salm - -include("operators.jl") -export L², Lz, L₊, L₋, R², Rz, R₊, R₋, ð, ð̄ - -#include("rotate.jl") -#export rotate! - - +# New public API: promote redesign to the top-level interface include("redesign/SphericalFunctions.jl") -end # module +# Legacy API (no legacy names exported from the top-level module) +include("deprecated/Deprecated.jl") +export Deprecated + +end # module SphericalFunctions diff --git a/src/deprecated/Deprecated.jl b/src/deprecated/Deprecated.jl new file mode 100644 index 00000000..b5778714 --- /dev/null +++ b/src/deprecated/Deprecated.jl @@ -0,0 +1,67 @@ +module Deprecated + +using FastTransforms: FFTW, fft, fftshift!, ifft, ifftshift!, irfft, + plan_bfft!, plan_fft, plan_fft! +using LinearAlgebra: LinearAlgebra, Bidiagonal, Diagonal, convert, ldiv!, mul! +using OffsetArrays: OffsetArray, OffsetVector +using ProgressMeter: Progress, next! +using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, + to_euler_phases, to_spherical_coordinates, basetype +using StaticArrays: @SVector +using SpecialFunctions, DoubleFloats +using LoopVectorization: @turbo +using Base.Threads: @threads, threadpoolsize + +# Pull in shared utility functionality from the parent module (code lives in `src/utilities/`). +using ..SphericalFunctions: MachineFloat +using ..SphericalFunctions: OffsetVec, OffsetMat, offset +using ..SphericalFunctions: sqrtbinomial +using ..SphericalFunctions: golden_ratio_spiral_pixels, golden_ratio_spiral_rotors +using ..SphericalFunctions: sorted_rings, sorted_ring_pixels, sorted_ring_rotors +using ..SphericalFunctions: fejer1_rings, fejer2_rings, clenshaw_curtis_rings +using ..SphericalFunctions: complex_powers, complex_powers!, ComplexPowers +using ..SphericalFunctions: fejer1, fejer2, clenshaw_curtis + +include("indexing.jl") +export Ysize, Yrange, Yindex, deduce_limits, theta_phi, phi_theta +export WignerHsize, WignerHindex, _WignerHindex, WignerHrange +export WignerDsize, WignerDindex, WignerDrange + +include("iterators.jl") +export D_iterator, d_iterator, sYlm_iterator, λ_iterator +# Legacy API: +export Diterator, diterator, Yiterator, λiterator + +include("associated_legendre.jl") +export ALFRecursionCoefficients, ALFrecurse!, ALFcompute!, ALFcompute + +include("Hrecursion.jl") +export H!, H_recursion_coefficients + +include("evaluate.jl") +export d_prep, d_matrices, d_matrices! +export D_prep, D_matrices, D_matrices! +export sYlm_prep, sYlm_values, sYlm_values! +# Legacy API: +export d!, d, D!, D, Y!, Y, dprep, Dprep, Yprep, ₛ𝐘 + +# Legacy utilities / helpers that were historically exported from the top-level module. +export golden_ratio_spiral_pixels, golden_ratio_spiral_rotors +export sorted_rings, sorted_ring_pixels, sorted_ring_rotors +export fejer1_rings, fejer2_rings, clenshaw_curtis_rings +export complex_powers, complex_powers!, ComplexPowers +export fejer1, fejer2, clenshaw_curtis + +include("ssht.jl") +export SSHT, pixels, rotors + +include("map2salm.jl") +export map2salm, map2salm!, plan_map2salm + +include("operators.jl") +export L², Lz, L₊, L₋, R², Rz, R₊, R₋, ð, ð̄ + +# include("rotate.jl") +# export rotate! + +end # module Deprecated diff --git a/src/Hrecursion.jl b/src/deprecated/Hrecursion.jl similarity index 100% rename from src/Hrecursion.jl rename to src/deprecated/Hrecursion.jl diff --git a/src/associated_legendre.jl b/src/deprecated/associated_legendre.jl similarity index 100% rename from src/associated_legendre.jl rename to src/deprecated/associated_legendre.jl diff --git a/src/evaluate.jl b/src/deprecated/evaluate.jl similarity index 99% rename from src/evaluate.jl rename to src/deprecated/evaluate.jl index 5d767d68..df50c046 100644 --- a/src/evaluate.jl +++ b/src/deprecated/evaluate.jl @@ -4,7 +4,7 @@ ### as ℓ increases, reaching ~4% around ℓ=512. ### 2. This is probably a much more significant advantage for ALFs. -using .SphericalFunctions: complex_powers! +using ..SphericalFunctions: complex_powers! using Quaternionic: Quaternionic, AbstractQuaternion, to_euler_phases @inline ϵ(m) = ifelse(m > 0 && isodd(m), -1, 1) diff --git a/src/indexing.jl b/src/deprecated/indexing.jl similarity index 100% rename from src/indexing.jl rename to src/deprecated/indexing.jl diff --git a/src/iterators.jl b/src/deprecated/iterators.jl similarity index 100% rename from src/iterators.jl rename to src/deprecated/iterators.jl diff --git a/src/map2salm.jl b/src/deprecated/map2salm.jl similarity index 100% rename from src/map2salm.jl rename to src/deprecated/map2salm.jl diff --git a/src/operators.jl b/src/deprecated/operators.jl similarity index 100% rename from src/operators.jl rename to src/deprecated/operators.jl diff --git a/src/ssht.jl b/src/deprecated/ssht.jl similarity index 100% rename from src/ssht.jl rename to src/deprecated/ssht.jl diff --git a/src/ssht/direct.jl b/src/deprecated/ssht/direct.jl similarity index 100% rename from src/ssht/direct.jl rename to src/deprecated/ssht/direct.jl diff --git a/src/ssht/minimal.jl b/src/deprecated/ssht/minimal.jl similarity index 100% rename from src/ssht/minimal.jl rename to src/deprecated/ssht/minimal.jl diff --git a/src/ssht/rs.jl b/src/deprecated/ssht/rs.jl similarity index 100% rename from src/ssht/rs.jl rename to src/deprecated/ssht/rs.jl diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index d655450e..a543e23a 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -1,15 +1,9 @@ -module Redesign - import Quaternionic import TestItems: @testitem, @testsnippet -import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, +import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, FixedSizeVector -# TODO: remove once this moves to SphericalFunctions proper -import SphericalFunctions: ComplexPowers - - -include("Wigner/wigner.jl") +include("wigner/wigner.jl") # function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} @@ -26,5 +20,3 @@ include("Wigner/wigner.jl") # function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} # error("WignerD! is not yet implemented") # end - -end # module Redesign diff --git a/src/redesign/Wigner/wigner_matrix.jl b/src/redesign/Wigner/wigner_matrix.jl index df57f561..4e471f5b 100644 --- a/src/redesign/Wigner/wigner_matrix.jl +++ b/src/redesign/Wigner/wigner_matrix.jl @@ -355,7 +355,7 @@ end @testitem "WignerMatrix" begin - import SphericalFunctions.Redesign: WignerDMatrix, WignerdMatrix, + import SphericalFunctions: WignerDMatrix, WignerdMatrix, parent, ell, mpmax, mpmin, mmax, mmin, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, ℓₘᵢₙ # Check that mixed-up types throw an error diff --git a/src/complex_powers.jl b/src/utilities/complex_powers.jl similarity index 100% rename from src/complex_powers.jl rename to src/utilities/complex_powers.jl diff --git a/src/pixelizations.jl b/src/utilities/pixelizations.jl similarity index 100% rename from src/pixelizations.jl rename to src/utilities/pixelizations.jl diff --git a/src/utils.jl b/src/utilities/utils.jl similarity index 100% rename from src/utils.jl rename to src/utilities/utils.jl diff --git a/src/weights.jl b/src/utilities/weights.jl similarity index 100% rename from src/weights.jl rename to src/utilities/weights.jl diff --git a/test/conventions/boyle2016.jl b/test/conventions/boyle2016.jl index 9b74e131..8fd50735 100644 --- a/test/conventions/boyle2016.jl +++ b/test/conventions/boyle2016.jl @@ -144,6 +144,7 @@ end # @testmodule Boyle2016 @testitem "WignerDElement" setup=[Boyle2016] begin + import SphericalFunctions: Deprecated using Quaternionic using Random Random.seed!(1234) @@ -172,9 +173,9 @@ end # @testmodule Boyle2016 for R ∈ (R′, conj(R′)) 𝔇1 = [ Boyle2016.WignerDElement(R, ℓ, m′, m) - for (ℓ, m′, m) ∈ eachrow(SphericalFunctions.WignerDrange(ℓₘₐₓ)) + for (ℓ, m′, m) ∈ eachrow(Deprecated.WignerDrange(ℓₘₐₓ)) ] - 𝔇2 = D_matrices(R, ℓₘₐₓ) + 𝔇2 = Deprecated.D_matrices(R, ℓₘₐₓ) @test 𝔇1 ≈ 𝔇2 end end diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl index 928a018e..a4d4f8e6 100644 --- a/test/conventions/edmonds.jl +++ b/test/conventions/edmonds.jl @@ -117,6 +117,7 @@ end # @testmodule Edmonds @testitem "Edmonds conventions" setup=[Utilities, Edmonds] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -142,7 +143,7 @@ end # @testmodule Edmonds # Compare to SphericalFunctions let s=0 - Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + Y = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) i = 1 for ℓ in 0:ℓₘₐₓ for m in -ℓ:ℓ @@ -163,7 +164,7 @@ end # @testmodule Edmonds end for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/goldbergetal.jl b/test/conventions/goldbergetal.jl index a62ae208..2b89498c 100644 --- a/test/conventions/goldbergetal.jl +++ b/test/conventions/goldbergetal.jl @@ -112,6 +112,7 @@ end # @testmodule GoldbergEtAl @testitem "GoldbergEtAl conventions" setup=[Utilities, GoldbergEtAl] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -135,7 +136,7 @@ end # @testmodule GoldbergEtAl # Compare to SphericalFunctions Y for s ∈ -ℓₘₐₓ:ℓₘₐₓ - Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + Y₁ = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] Y₂ = [(-1)^m * Y(s, ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ end @@ -147,7 +148,7 @@ end # @testmodule GoldbergEtAl for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/sakurai.jl b/test/conventions/sakurai.jl index 8be8bdea..810f0a8a 100644 --- a/test/conventions/sakurai.jl +++ b/test/conventions/sakurai.jl @@ -149,6 +149,7 @@ end # @testmodule Sakurai @testitem "Sakurai conventions" setup=[Utilities, Sakurai] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -179,7 +180,7 @@ end # @testmodule Sakurai # Compare to SphericalFunctions let s=0 - Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + Y = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) i = 1 for ℓ in 0:ℓₘₐₓ for m in -ℓ:ℓ @@ -196,7 +197,7 @@ end # @testmodule Sakurai for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/thorne.jl b/test/conventions/thorne.jl index 7a7d3678..3f10e107 100644 --- a/test/conventions/thorne.jl +++ b/test/conventions/thorne.jl @@ -56,6 +56,7 @@ end # @testmodule Thorne @testitem "Thorne conventions" setup=[Utilities, Thorne] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -76,7 +77,7 @@ end # @testmodule Thorne # Compare to SphericalFunctions let s=0 - Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + Y = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) i = 1 for ℓ in 0:ℓₘₐₓ for m in -ℓ:ℓ diff --git a/test/conventions/torresdelcastillo.jl b/test/conventions/torresdelcastillo.jl index f9666c59..c2635614 100644 --- a/test/conventions/torresdelcastillo.jl +++ b/test/conventions/torresdelcastillo.jl @@ -115,6 +115,7 @@ end # @testmodule TorresDelCastillo @testitem "TorresDelCastillo conventions" setup=[Utilities, TorresDelCastillo] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -127,7 +128,7 @@ end # @testmodule TorresDelCastillo for θ ∈ βrange(T) for ϕ ∈ αrange(T) for s ∈ -ℓₘₐₓ:ℓₘₐₓ - Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + Y₁ = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] Y₂ = [Y(s, ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ end @@ -139,7 +140,7 @@ end # @testmodule TorresDelCastillo for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/varshalovich.jl b/test/conventions/varshalovich.jl index c8431b1f..fca806cc 100644 --- a/test/conventions/varshalovich.jl +++ b/test/conventions/varshalovich.jl @@ -272,6 +272,7 @@ end # @testmodule Varshalovich @testitem "Varshalovich conventions" setup=[Utilities, Varshalovich] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const 𝒾 = im @@ -290,7 +291,7 @@ end # @testmodule Varshalovich end for γ ∈ γrange(T, n) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/wigner.jl b/test/conventions/wigner.jl index 26aca927..2b3a5db7 100644 --- a/test/conventions/wigner.jl +++ b/test/conventions/wigner.jl @@ -104,6 +104,7 @@ end # @testmodule Wigner @testitem "Wigner conventions" setup=[Utilities, Wigner] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -116,7 +117,7 @@ end # @testmodule Wigner for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/associated_legendre.jl b/test/deprecated/associated_legendre.jl similarity index 73% rename from test/associated_legendre.jl rename to test/deprecated/associated_legendre.jl index 4a61e5c5..945c508f 100644 --- a/test/associated_legendre.jl +++ b/test/deprecated/associated_legendre.jl @@ -1,4 +1,5 @@ @testitem "Associated Legendre Functions" setup=[Utilities] begin + import SphericalFunctions: Deprecated using OffsetArrays nmax = 1_000 @@ -8,17 +9,17 @@ ϵ = eps(T) P̄ = fill(T(NaN), min_length) P̄′ = Vector{T}(undef, min_length) - recursion_coefficients = ALFRecursionCoefficients(nmax, T) + recursion_coefficients = Deprecated.ALFRecursionCoefficients(nmax, T) for β in βrange(T) expiβ = cis(β) # Make sure we're checking the size of P̄ against nmax - @test_throws ArgumentError ALFcompute!(P̄, expiβ, nmax+1, recursion_coefficients) + @test_throws ArgumentError Deprecated.ALFcompute!(P̄, expiβ, nmax+1, recursion_coefficients) - ALFcompute!(P̄, expiβ, nmax, recursion_coefficients) - ALFcompute!(P̄′, expiβ, nmax) - P̄″ = ALFcompute(expiβ, nmax) + Deprecated.ALFcompute!(P̄, expiβ, nmax, recursion_coefficients) + Deprecated.ALFcompute!(P̄′, expiβ, nmax) + P̄″ = Deprecated.ALFcompute(expiβ, nmax) @test eltype(P̄″) === eltype(P̄) offset = -1 diff --git a/test/indexing.jl b/test/deprecated/indexing.jl similarity index 67% rename from test/indexing.jl rename to test/deprecated/indexing.jl index 25588d29..c6ec75ae 100644 --- a/test/indexing.jl +++ b/test/deprecated/indexing.jl @@ -1,5 +1,5 @@ @testitem "WignerHrange" begin - import SphericalFunctions: WignerHrange + import SphericalFunctions: Deprecated ell_max = 16 r1(mp_max, ell_max) = hcat([ [ell, mp, m] for ell in 0:ell_max @@ -7,11 +7,11 @@ for m in abs(mp):ell ]...)' for ell_max in 0:ell_max - a = WignerHrange(ell_max) # Implicitly, mp_max=ell_max + a = Deprecated.WignerHrange(ell_max) # Implicitly, mp_max=ell_max b = r1(ell_max, ell_max) @test a == b for mp_max in 0:ell_max - a = WignerHrange(ell_max, mp_max) + a = Deprecated.WignerHrange(ell_max, mp_max) b = r1(mp_max, ell_max) @test a == b end @@ -19,24 +19,24 @@ end @testitem "WignerHsize" begin - import SphericalFunctions: WignerHsize, WignerHrange + import SphericalFunctions: Deprecated ell_max = 16 for i in -5:-1 - @test WignerHsize(i) == 0 + @test Deprecated.WignerHsize(i) == 0 for j in -5:5 - @test WignerHsize(i, j) == 0 + @test Deprecated.WignerHsize(i, j) == 0 end end for ell_max in 0:ell_max - @test WignerHsize(ell_max) == size(WignerHrange(ell_max, ell_max), 1) + @test Deprecated.WignerHsize(ell_max) == size(Deprecated.WignerHrange(ell_max, ell_max), 1) for mp_max in ell_max - @test WignerHsize(ell_max, mp_max) == size(WignerHrange(ell_max, mp_max), 1) + @test Deprecated.WignerHsize(ell_max, mp_max) == size(Deprecated.WignerHrange(ell_max, mp_max), 1) end end end @testitem "WignerHindex" begin - import SphericalFunctions: WignerHrange, WignerHindex + import SphericalFunctions: Deprecated ell_max = 16 ell_max_slow = ell_max ÷ 2 function fold_H_indices(ell, mp, m) @@ -56,24 +56,24 @@ end end for ell_max_i in 0:ell_max_slow - r = WignerHrange(ell_max_i) + r = Deprecated.WignerHrange(ell_max_i) for ell in 0:ell_max_i for mp in -ell:ell for m in -ell:ell - i = WignerHindex(ell, mp, m) + i = Deprecated.WignerHindex(ell, mp, m) @test r[i, :] == fold_H_indices(ell, mp, m) end end end for mp_max in 0:ell_max_i - r = WignerHrange(ell_max_i, mp_max) + r = Deprecated.WignerHrange(ell_max_i, mp_max) for ell in 0:ell_max_i for mp in -ell:ell for m in -ell:ell if abs(mp) > mp_max && abs(m) > mp_max continue end - i = WignerHindex(ell, mp, m, mp_max) + i = Deprecated.WignerHindex(ell, mp, m, mp_max) @test r[i, :] == fold_H_indices(ell, mp, m) end end @@ -83,7 +83,7 @@ end end @testitem "WignerDrange" begin - import SphericalFunctions: WignerDrange + import SphericalFunctions: Deprecated ell_max = 16 ell_max_slow = ell_max ÷ 2 function r2(ell_min, mp_max, ell_max) @@ -98,11 +98,11 @@ end for ell_max in 0:ell_max_slow÷2 let ell_min = 0 - a = WignerDrange(ell_max) # Implicitly, mp_max=ell_max + a = Deprecated.WignerDrange(ell_max) # Implicitly, mp_max=ell_max b = r2(ell_min, ell_max, ell_max) @test a == b for mp_max in 0:ell_max - a = WignerDrange(ell_max, mp_max) + a = Deprecated.WignerDrange(ell_max, mp_max) b = r2(ell_min, mp_max, ell_max) @test a == b end @@ -111,13 +111,13 @@ end end @testitem "WignerDsize" begin - import SphericalFunctions: WignerDsize, WignerDrange + import SphericalFunctions: Deprecated ell_max = 16 for ell_max in 0:ell_max let ell_min = 0 for mp_max in 0:ell_max - a = WignerDsize(ell_max, mp_max) - b = size(WignerDrange(ell_max, mp_max), 1) + a = Deprecated.WignerDsize(ell_max, mp_max) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -125,8 +125,8 @@ end for ell_max in 0:ell_max let ell_min = 0 for mp_max in 0:ell_max - a = WignerDsize(ell_max, mp_max) - b = size(WignerDrange(ell_max, mp_max), 1) + a = Deprecated.WignerDsize(ell_max, mp_max) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -134,9 +134,9 @@ end for ell_max in 0:ell_max let ell_min = 0 for mp_max in [ell_max] - a = WignerDsize(ell_max, mp_max) + a = Deprecated.WignerDsize(ell_max, mp_max) # a = WignerDsize_ellmin(ell_min, ell_max) - b = size(WignerDrange(ell_max, mp_max), 1) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -144,9 +144,9 @@ end for ell_max in 0:ell_max let ell_min = 0 for mp_max in [ell_max] - a = WignerDsize(ell_max, mp_max) + a = Deprecated.WignerDsize(ell_max, mp_max) # a = WignerDsize(ell_max) - b = size(WignerDrange(ell_max, mp_max), 1) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -154,35 +154,35 @@ end end @testitem "WignerDindex" begin - import SphericalFunctions: WignerDrange, WignerDindex + import SphericalFunctions: Deprecated ell_max = 16 ell_max_slow = ell_max ÷ 2 for ellmax in 0:ell_max_slow - r = WignerDrange(ellmax) + r = Deprecated.WignerDrange(ellmax) for ell in 0:ellmax for mp in -ell:ell for m in -ell:ell - i = WignerDindex(ell, mp, m) + i = Deprecated.WignerDindex(ell, mp, m) @test r[i, :] == [ell, mp, m] end end end let ell_min = 0 - r = WignerDrange(ellmax) + r = Deprecated.WignerDrange(ellmax) for ell in ell_min:ellmax for mp in -ell:ell for m in -ell:ell - i = WignerDindex(ell, mp, m) + i = Deprecated.WignerDindex(ell, mp, m) @test r[i, :] == [ell, mp, m] end end end for mp_max in 0:ellmax - r = WignerDrange(ellmax, mp_max) + r = Deprecated.WignerDrange(ellmax, mp_max) for ell in ell_min:ellmax for mp in -min(ell, mp_max):min(ell, mp_max) for m in -ell:ell - i = WignerDindex(ell, mp, m, mp_max) + i = Deprecated.WignerDindex(ell, mp, m, mp_max) @test r[i, :] == [ell, mp, m] end end @@ -193,7 +193,7 @@ end end @testitem "Yrange" begin - import SphericalFunctions: Yrange + import SphericalFunctions: Deprecated ell_max = 16 function r3(ell_min, ell_max) a = [ @@ -205,7 +205,7 @@ end end for ell_max in 0:ell_max for ell_min in 0:ell_max - a = Yrange(ell_min, ell_max) + a = Deprecated.Yrange(ell_min, ell_max) b = r3(ell_min, ell_max) @test a == b end @@ -213,68 +213,68 @@ end end @testitem "Ysize" begin - import SphericalFunctions: Ysize, Yrange + import SphericalFunctions: Deprecated ell_max = 16 for ell_max in 0:ell_max for ell_min in 0:ell_max - a = Ysize(ell_min, ell_max) - b = size(Yrange(ell_min, ell_max))[1] + a = Deprecated.Ysize(ell_min, ell_max) + b = size(Deprecated.Yrange(ell_min, ell_max))[1] @test a == b end end end @testitem "deduce_limits" begin - import SphericalFunctions: Ysize, deduce_limits + import SphericalFunctions: Deprecated ell_max = 16 for ℓmax in 0:4096 for ℓmin in 0:min(2, ℓmax) - deduced = deduce_limits(Ysize(ℓmin, ℓmax), nothing) + deduced = Deprecated.deduce_limits(Deprecated.Ysize(ℓmin, ℓmax), nothing) @test deduced == (ℓmin, ℓmax) end end for ℓmax in 0:ell_max for ℓmin in [0] - deduced = deduce_limits(Ysize(ℓmin, ℓmax)) + deduced = Deprecated.deduce_limits(Deprecated.Ysize(ℓmin, ℓmax)) @test deduced == (ℓmin, ℓmax) end end for ℓmax in 0:ell_max for ℓmin in 0:ℓmax - deduced = deduce_limits(Ysize(ℓmin, ℓmax), ℓmin) + deduced = Deprecated.deduce_limits(Deprecated.Ysize(ℓmin, ℓmax), ℓmin) @test deduced == (ℓmin, ℓmax) end end for ℓmax in 1:ell_max - prev_size = Ysize(ℓmax-1) - this_size = Ysize(ℓmax) + prev_size = Deprecated.Ysize(ℓmax-1) + this_size = Deprecated.Ysize(ℓmax) if abs(prev_size - this_size) > 1 mid_size = (prev_size + this_size) ÷ 2 - @test_throws ErrorException deduce_limits(mid_size) + @test_throws ErrorException Deprecated.deduce_limits(mid_size) end end end @testitem "Yindex" setup=[Utilities] begin - import SphericalFunctions: Yrange, Yindex + import SphericalFunctions: Deprecated ell_max = 16 for ell_max in 0:ell_max let ell_min = 0 - r = Yrange(ell_min, ell_max) + r = Deprecated.Yrange(ell_min, ell_max) for ell in ell_min:ell_max for m in -ell:ell - i = Yindex(ell, m) + i = Deprecated.Yindex(ell, m) @test r[i, :] == [ell, m] end end - s = Yrange(ell_max) + s = Deprecated.Yrange(ell_max) @test array_equal(r, s) end for ell_min in 0:ell_max - r = Yrange(ell_min, ell_max) + r = Deprecated.Yrange(ell_min, ell_max) for ell in ell_min:ell_max for m in -ell:ell - i = Yindex(ell, m, ell_min) + i = Deprecated.Yindex(ell, m, ell_min) @test r[i, :] == [ell, m] end end @@ -284,7 +284,7 @@ end @testitem "phi_theta <-> theta_phi" setup=[Utilities] begin for T in [Float64, Float32, BigFloat] - import SphericalFunctions: theta_phi + import SphericalFunctions.Deprecated: theta_phi, phi_theta ell_max = 16 for nθ in 2:(2ell_max+1) for nϕ in 2:(2ell_max+1) diff --git a/test/iterators.jl b/test/deprecated/iterators.jl similarity index 88% rename from test/iterators.jl rename to test/deprecated/iterators.jl index 38e03207..c2842e72 100644 --- a/test/iterators.jl +++ b/test/deprecated/iterators.jl @@ -1,15 +1,16 @@ @testitem "D iterators" begin + import SphericalFunctions: Deprecated import Logging: with_logger, NullLogger ℓₘₐₓ = 20 Drange = mapslices( ℓm′m -> tuple(ℓm′m...), - SphericalFunctions.WignerDrange(ℓₘₐₓ); + Deprecated.WignerDrange(ℓₘₐₓ); dims=[2] )[:, 1] let 𝔇 = with_logger(NullLogger()) do - D_iterator(Drange, ℓₘₐₓ) + Deprecated.D_iterator(Drange, ℓₘₐₓ) end for (ℓ, 𝔇ˡ) in enumerate(𝔇) ℓ -= 1 @@ -48,7 +49,7 @@ for ℓₘᵢₙ in 0:ℓₘₐₓ 𝔇 = with_logger(NullLogger()) do - D_iterator(Drange, ℓₘₐₓ, ℓₘᵢₙ) + Deprecated.D_iterator(Drange, ℓₘₐₓ, ℓₘᵢₙ) end for (ℓ, 𝔇ˡ) in enumerate(𝔇) ℓ += ℓₘᵢₙ - 1 @@ -79,17 +80,18 @@ end @testitem "d iterators" begin + import SphericalFunctions: Deprecated import Logging: with_logger, NullLogger ℓₘₐₓ = 20 drange = mapslices( ℓm′m -> tuple(ℓm′m...), - SphericalFunctions.WignerDrange(ℓₘₐₓ); + Deprecated.WignerDrange(ℓₘₐₓ); dims=[2] )[:, 1] let 𝔡 = with_logger(NullLogger()) do - d_iterator(drange, ℓₘₐₓ) + Deprecated.d_iterator(drange, ℓₘₐₓ) end for (ℓ, 𝔡ˡ) in enumerate(𝔡) ℓ -= 1 @@ -129,7 +131,7 @@ end for ℓₘᵢₙ in 0:ℓₘₐₓ 𝔡 = with_logger(NullLogger()) do - d_iterator(drange, ℓₘₐₓ, ℓₘᵢₙ) + Deprecated.d_iterator(drange, ℓₘₐₓ, ℓₘᵢₙ) end for (ℓ, 𝔡ˡ) in enumerate(𝔡) ℓ += ℓₘᵢₙ - 1 @@ -162,13 +164,14 @@ end end @testitem "Y iterators" begin + import SphericalFunctions: Deprecated ℓₘₐₓ = 20 Yrange = mapslices( ℓm -> tuple(ℓm...), - SphericalFunctions.Yrange(ℓₘₐₓ); + Deprecated.Yrange(ℓₘₐₓ); dims=[2] )[:, 1] - 𝔜 = sYlm_iterator(Yrange, ℓₘₐₓ) + 𝔜 = Deprecated.sYlm_iterator(Yrange, ℓₘₐₓ) for (ℓ, 𝔜ˡ) in enumerate(𝔜) ℓ -= 1 Yˡ = [ @@ -186,7 +189,7 @@ end @test eltype(collection) == eltype(𝔜) for ℓₘᵢₙ in 0:ℓₘₐₓ - local 𝔜 = sYlm_iterator(Yrange, ℓₘₐₓ, ℓₘᵢₙ) + local 𝔜 = Deprecated.sYlm_iterator(Yrange, ℓₘₐₓ, ℓₘᵢₙ) for (ℓ, 𝔜ˡ) in enumerate(𝔜) ℓ += ℓₘᵢₙ - 1 Yˡ = [ @@ -195,9 +198,9 @@ end ] @test 𝔜ˡ == Yˡ end - iₘᵢₙ = Ysize(ℓₘᵢₙ-1)+1 + iₘᵢₙ = Deprecated.Ysize(ℓₘᵢₙ-1)+1 @test Yrange[iₘᵢₙ] == (ℓₘᵢₙ, -ℓₘᵢₙ) - 𝔜 = sYlm_iterator(Yrange[iₘᵢₙ:end], ℓₘₐₓ, ℓₘᵢₙ, 1) + 𝔜 = Deprecated.sYlm_iterator(Yrange[iₘᵢₙ:end], ℓₘₐₓ, ℓₘᵢₙ, 1) for (ℓ, 𝔜ˡ) in enumerate(𝔜) ℓ += ℓₘᵢₙ - 1 Yˡ = [ diff --git a/test/map2salm.jl b/test/deprecated/map2salm.jl similarity index 81% rename from test/map2salm.jl rename to test/deprecated/map2salm.jl index 4dc86128..7e7a358d 100644 --- a/test/map2salm.jl +++ b/test/deprecated/map2salm.jl @@ -2,6 +2,7 @@ # test map2salm on Float16 @testitem "map2salm" setup=[Utilities] begin + import SphericalFunctions: Deprecated for T in [BigFloat, Float64, Float32] # These test the ability of map2salm to precisely decompose the results of `sYlm`. ℓmax = 7 @@ -14,12 +15,12 @@ for m in -ℓ:ℓ f = mapslices( ϕθ -> sYlm(s, ℓ, m, ϕθ[2], ϕθ[1]), - phi_theta(Nφ, Nϑ, T), + Deprecated.phi_theta(Nφ, Nϑ, T), dims=[3] ) - computed = map2salm(f, s, ℓmax) + computed = Deprecated.map2salm(f, s, ℓmax) expected = zeros(Complex{T}, size(computed)) - expected[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + expected[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) if ≉(computed, expected, atol=30eps(T), rtol=30eps(T)) @show T @show ℓmax @@ -36,8 +37,8 @@ end @test computed ≈ expected atol=30eps(T) rtol=30eps(T) - plan = plan_map2salm(f, s, ℓmax) - computed2 = map2salm(f, plan) + plan = Deprecated.plan_map2salm(f, s, ℓmax) + computed2 = Deprecated.map2salm(f, plan) @test array_equal(computed, computed2) end end diff --git a/test/operators.jl b/test/deprecated/operators.jl similarity index 67% rename from test/operators.jl rename to test/deprecated/operators.jl index a57d4eef..f92974eb 100644 --- a/test/operators.jl +++ b/test/deprecated/operators.jl @@ -11,6 +11,7 @@ end @testitem "Explicit definition" setup=[ExplicitOperators] begin + import SphericalFunctions: Deprecated using Quaternionic using DoubleFloats using Random @@ -24,32 +25,32 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] @test R(imz, f)(Q) ≈ m′ * f(Q) atol=ϵ rtol=ϵ @test L(imz, f)(Q) ≈ m * f(Q) atol=ϵ rtol=ϵ if ℓ ≥ abs(m+1) L₊1 = L(imx, f)(Q) + im * L(imy, f)(Q) - L₊2 = √T((ℓ-m)*(ℓ+m+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m+1, m′)] + L₊2 = √T((ℓ-m)*(ℓ+m+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m+1, m′)] @test L₊1 ≈ L₊2 atol=ϵ rtol=ϵ end if ℓ ≥ abs(m-1) L₋1 = L(imx, f)(Q) - im * L(imy, f)(Q) - L₋2 = √T((ℓ+m)*(ℓ-m+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m-1, m′)] + L₋2 = √T((ℓ+m)*(ℓ-m+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m-1, m′)] @test L₋1 ≈ L₋2 atol=ϵ rtol=ϵ end if ℓ ≥ abs(m′+1) K₊1 = R(imx, f)(Q) - im * R(imy, f)(Q) - K₊2 = √T((ℓ-m′)*(ℓ+m′+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′+1)] + K₊2 = √T((ℓ-m′)*(ℓ+m′+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′+1)] @test K₊1 ≈ K₊2 atol=ϵ rtol=ϵ end if ℓ ≥ abs(m′-1) K₋1 = R(imx, f)(Q) + im * R(imy, f)(Q) - K₋2 = √T((ℓ+m′)*(ℓ-m′+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′-1)] + K₋2 = √T((ℓ+m′)*(ℓ-m′+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′-1)] @test K₋1 ≈ K₋2 atol=ϵ rtol=ϵ end end @@ -68,6 +69,7 @@ end import ForwardDiff using Random Random.seed!(123) + import SphericalFunctions: Deprecated const L = ExplicitOperators.L const R = ExplicitOperators.R @@ -100,7 +102,7 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] for n ∈ N for m ∈ M @test L(m, L(n, f))(Q) ≈ LL(m, n, f, Q) atol=ϵ rtol=ϵ @@ -115,6 +117,7 @@ end end @testitem "Scalar multiplication" setup=[ExplicitOperators] begin + import SphericalFunctions: Deprecated using Quaternionic using DoubleFloats const L = ExplicitOperators.L @@ -128,7 +131,7 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] for s ∈ Ss for g ∈ Gs @test L(s*g, f)(Q) ≈ s*L(g, f)(Q) atol=ϵ rtol=ϵ @@ -143,6 +146,7 @@ end end @testitem "Additivity" setup=[ExplicitOperators] begin + import SphericalFunctions: Deprecated using Quaternionic using DoubleFloats const L = ExplicitOperators.L @@ -155,7 +159,7 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] for g₁ ∈ Gs for g₂ ∈ Gs @test L(g₁+g₂, f)(Q) ≈ L(g₁, f)(Q) + L(g₂, f)(Q) atol=ϵ rtol=ϵ @@ -178,6 +182,7 @@ end import ForwardDiff using Random Random.seed!(1234) + import SphericalFunctions: Deprecated const L = ExplicitOperators.L const R = ExplicitOperators.R @@ -189,7 +194,7 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] for eⱼ ∈ E for eₖ ∈ E eⱼeₖ = QuatVec{T}(eⱼ * eₖ - eₖ * eⱼ) / 2 @@ -206,6 +211,7 @@ end end @testitem "Commutators" begin + import SphericalFunctions: Deprecated using DoubleFloats for T ∈ [Float32, Float64, Double64, BigFloat] # Test the following relations: @@ -218,8 +224,8 @@ end @testset "$ℓₘₐₓ" for ℓₘₐₓ ∈ 4:7 for s in -3:3 let ℓₘᵢₙ = 0 - for Oᵢ ∈ [Lz, L₊, L₋, Rz, R₊, R₋] - for O² ∈ [L², R²] + for Oᵢ ∈ [Deprecated.Lz, Deprecated.L₊, Deprecated.L₋, Deprecated.Rz, Deprecated.R₊, Deprecated.R₋] + for O² ∈ [Deprecated.L², Deprecated.R²] let O²=O²(s, ℓₘᵢₙ, ℓₘₐₓ, T), Oᵢ=Oᵢ(s, ℓₘᵢₙ, ℓₘₐₓ, T) # [O², Oᵢ] = 0 @@ -227,9 +233,9 @@ end end end end - let Lz=Array(Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T)), - L₊=Array(L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)), - L₋=Array(L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)) + let Lz=Array(Deprecated.Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T)), + L₊=Array(Deprecated.L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)), + L₋=Array(Deprecated.L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)) # [Lz, L₊] = L₊ @test Lz*L₊ - L₊*Lz ≈ L₊ atol=ϵ rtol=ϵ # [Lz, L₋] = -L₋ @@ -240,39 +246,39 @@ end let # [Rz, R₊] = R₊ @test ( - Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [Rz, R₋] = -R₋ @test ( - Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ -R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ -Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [R₊, R₋] = 2Rz @test ( - R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - Deprecated.R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ 2Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [Rz, ð] = -ð @test ( - Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - ð(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ -ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ -Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [Rz, ð̄] = ð̄ @test ( - Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [ð, ð̄] = 2Rz @test ( - ð(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) - -ð̄(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.ð(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) + -Deprecated.ð̄(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ 2Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ end end @@ -282,6 +288,7 @@ end end @testitem "Casimir" begin + import SphericalFunctions: Deprecated using DoubleFloats for T ∈ [Float32, Float64, Double64, BigFloat] # Test that L² = (L₊L₋ + L₋L₊ + 2Lz²)/2 = R² = (R₊R₋ + R₋R₊ + 2Rz²)/2 @@ -289,25 +296,25 @@ end for s ∈ -3:3 for ℓₘₐₓ ∈ 4:7 for ℓₘᵢₙ ∈ 0:min(abs(s)+1, ℓₘₐₓ) - let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), - Lz=Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T), - L₊=L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T), - L₋=L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + let L²=Deprecated.L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), + Lz=Deprecated.Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T), + L₊=Deprecated.L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T), + L₋=Deprecated.L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) L1 = L² L2 = (L₊*L₋ .+ L₋*L₊ .+ 2Lz*Lz)/2 @test L1 ≈ L2 atol=ϵ rtol=ϵ end - let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), - R²=R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) + let L²=Deprecated.L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), + R²=Deprecated.R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) @test L² ≈ R² atol=ϵ rtol=ϵ end let # R² = (2Rz² + R₊R₋ + R₋R₊)/2 - R1 = R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) + R1 = Deprecated.R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) R2 = T.(Array( - R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - .+ R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - .+ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Deprecated.R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T) * Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + .+ Deprecated.R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T) * Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + .+ 2Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) / 2) @test R1 ≈ R2 atol=ϵ rtol=ϵ end @@ -318,6 +325,7 @@ end end @testitem "Applied to ₛYₗₘ" begin + import SphericalFunctions: Deprecated using DoubleFloats for T ∈ [Float32, Float64, Double64, BigFloat] # Evaluate (on points) ðY = √((ℓ-s)(ℓ+s+1)) Y, and similarly for ð̄Y @@ -325,20 +333,20 @@ end @testset "$ℓₘₐₓ" for ℓₘₐₓ ∈ 4:7 for s in -3:3 let ℓₘᵢₙ = 0 - 𝒯₊ = SSHT(s+1, ℓₘₐₓ; T=T, method="Direct", inplace=false) - 𝒯₋ = SSHT(s-1, ℓₘₐₓ; T=T, method="Direct", inplace=false) - i₊ = Yindex(abs(s+1), -abs(s+1), ℓₘᵢₙ) - i₋ = Yindex(abs(s-1), -abs(s-1), ℓₘᵢₙ) - Y = zeros(Complex{T}, Ysize(ℓₘᵢₙ, ℓₘₐₓ)) + 𝒯₊ = Deprecated.SSHT(s+1, ℓₘₐₓ; T=T, method="Direct", inplace=false) + 𝒯₋ = Deprecated.SSHT(s-1, ℓₘₐₓ; T=T, method="Direct", inplace=false) + i₊ = Deprecated.Yindex(abs(s+1), -abs(s+1), ℓₘᵢₙ) + i₋ = Deprecated.Yindex(abs(s-1), -abs(s-1), ℓₘᵢₙ) + Y = zeros(Complex{T}, Deprecated.Ysize(ℓₘᵢₙ, ℓₘₐₓ)) for ℓ in abs(s):ℓₘₐₓ for m in -ℓ:ℓ Y[:] .= zero(T) - Y[Yindex(ℓ, m, ℓₘᵢₙ)] = one(T) - ðY = 𝒯₊ * (ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₊:end] + Y[Deprecated.Yindex(ℓ, m, ℓₘᵢₙ)] = one(T) + ðY = 𝒯₊ * (Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₊:end] Y₊ = 𝒯₊ * Y[i₊:end] c₊ = ℓ < abs(s+1) ? zero(T) : √T((ℓ-s)*(ℓ+s+1)) @test ðY ≈ c₊ * Y₊ atol=ϵ rtol=ϵ - ð̄Y = 𝒯₋ * (ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₋:end] + ð̄Y = 𝒯₋ * (Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₋:end] Y₋ = 𝒯₋ * Y[i₋:end] c₋ = ℓ < abs(s-1) ? zero(T) : -√T((ℓ+s)*(ℓ-s+1)) @test ð̄Y ≈ c₋ * Y₋ atol=ϵ rtol=ϵ diff --git a/test/ssht.jl b/test/deprecated/ssht.jl similarity index 77% rename from test/ssht.jl rename to test/deprecated/ssht.jl index e3ac88c0..23b13be9 100644 --- a/test/ssht.jl +++ b/test/deprecated/ssht.jl @@ -21,13 +21,14 @@ end # Preliminary check that `sqrtbinomial` works as expected @testitem "Preliminaries: sqrtbinomial" begin + import SphericalFunctions: Deprecated using DoubleFloats for T ∈ [Float16, Float32, Float64, Double64, BigFloat] for ℓ ∈ [1, 2, 3, 4, 5, 13, 64, 1025] for s ∈ -2:2 # Note that `ℓ-abs(s)` is more relevant, but we test without `abs` here # to exercise more code paths - a = SphericalFunctions.sqrtbinomial(2ℓ, ℓ-s, T) + a = Deprecated.sqrtbinomial(2ℓ, ℓ-s, T) b = T(√binomial(big(2ℓ), big(ℓ-s))) @test a ≈ b end @@ -38,17 +39,19 @@ end # Check that an error results from a nonsense method request @testitem "Preliminaries: Nonsense method" begin let s=-2, ℓmax=8 - @test_throws ErrorException SSHT(s, ℓmax; method="NonsenseGarbage") + import SphericalFunctions: Deprecated + @test_throws ErrorException Deprecated.SSHT(s, ℓmax; method="NonsenseGarbage") end end # Check what `show` looks like @testitem "Preliminaries: SSHT show" begin let io=IOBuffer(), s=-2, ℓmax=8, T=Float64, method="Direct" + import SphericalFunctions: Deprecated TD = "LinearAlgebra.LU{ComplexF64, Matrix{ComplexF64}, Vector{Int64}}" for inplace ∈ [true, false] - expected = "SphericalFunctions.SSHT$method{$T, $inplace, $TD}($s, $ℓmax)" - 𝒯 = SSHT(s, ℓmax; T, method, inplace) + expected = "SphericalFunctions.Deprecated.SSHT$method{$T, $inplace, $TD}($s, $ℓmax)" + 𝒯 = Deprecated.SSHT(s, ℓmax; T, method, inplace) Base.show(io, MIME("text/plain"), 𝒯) @test String(take!(io)) == expected end @@ -58,18 +61,20 @@ end # Check that SSHTDirect warns if ℓₘₐₓ is too large @testitem "Preliminaries: Direct ℓₘₐₓ" begin let s=0, ℓₘₐₓ=65 - @test_warn """ "Direct" method for s-SHT is only """ SSHT(s, ℓₘₐₓ; method="Direct") + import SphericalFunctions: Deprecated + @test_warn """ "Direct" method for s-SHT is only """ Deprecated.SSHT(s, ℓₘₐₓ; method="Direct") end end # Check pixels and rotors of Minimal @testitem "Preliminaries: Minimal pixels" setup=[SSHT] begin + import SphericalFunctions: Deprecated for T ∈ FloatTypes for ℓmax ∈ [3, 4, 5, 13, 64] for s ∈ -min(2,abs(ℓmax)-1):min(2,abs(ℓmax)-1) - 𝒯 = SSHT(s, ℓmax; T=T, method="Minimal") - @test pixels(𝒯) ≈ sorted_ring_pixels(s, ℓmax, T) - @test rotors(𝒯) ≈ sorted_ring_rotors(s, ℓmax, T) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method="Minimal") + @test Deprecated.pixels(𝒯) ≈ sorted_ring_pixels(s, ℓmax, T) + @test Deprecated.rotors(𝒯) ≈ sorted_ring_rotors(s, ℓmax, T) end end end @@ -78,6 +83,7 @@ end # These test the ability of ssht to precisely reconstruct a pure `sYlm`. @testitem "Synthesis" setup=[SSHT] begin + import SphericalFunctions: Deprecated for (method, T) in cases for ℓmax ∈ 3:7 @@ -87,16 +93,16 @@ end ϵ = 500ℓmax^3 * eps(T) for s in -2:2 - 𝒯 = SSHT(s, ℓmax; T=T, method=method) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method) #for ℓmin in 0:abs(s) let ℓmin = abs(s) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) computed = 𝒯 * f - expected = SphericalFunctions.Y.(s, ℓ, m, pixels(𝒯)) + expected = Deprecated.Y.(s, ℓ, m, Deprecated.pixels(𝒯)) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @test computed ≈ expected atol=ϵ rtol=ϵ end @@ -110,6 +116,7 @@ end # These test the ability of ssht to precisely decompose the results of `sYlm`. @testitem "Analysis" setup=[SSHT] begin + import SphericalFunctions: Deprecated for (method, T) in cases for ℓmax ∈ 3:7 @@ -122,14 +129,14 @@ end end for s in -2:2 - 𝒯 = SSHT(s, ℓmax; T=T, method=method) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method) let ℓmin = abs(s) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ - f = SphericalFunctions.Y.(s, ℓ, m, pixels(𝒯)) + f = Deprecated.Y.(s, ℓ, m, Deprecated.pixels(𝒯)) computed = 𝒯 \ f expected = zeros(Complex{T}, size(computed)) - expected[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + expected[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @test computed ≈ expected atol=ϵ rtol=ϵ end @@ -143,6 +150,7 @@ end # These test the ability of ssht to precisely reconstruct a pure `sYlm`, # and then reverse that process to find the pure mode again. @testitem "A ∘ S" setup=[SSHT] begin + import SphericalFunctions: Deprecated for (method, T) in cases # Note that the number of tests here scales as ℓmax^2, and # the time needed for each scales as (ℓmax log(ℓmax))^2, @@ -154,13 +162,13 @@ end ϵ *= 50 end for s in -2:2 - 𝒯 = SSHT(s, ℓmax; T=T, method=method) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method) let ℓmin = abs(s) - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ f[:] .= false - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) expected = copy(f) computed = 𝒯 \ (𝒯 * f) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @@ -175,6 +183,7 @@ end # These test A ∘ S in the RS method when using different quadratures @testitem "RS quadratures" setup=[SSHT] begin + import SphericalFunctions: Deprecated using StaticArrays using Quaternionic for T in FloatTypes @@ -188,23 +197,23 @@ end (fejer2_rings(2ℓmax+1, T), fejer2(2ℓmax+1, T)), (clenshaw_curtis_rings(2ℓmax+1, T), clenshaw_curtis(2ℓmax+1, T)) ] - 𝒯 = SSHT(s, ℓmax; T=T, θ=θ, quadrature_weights=w, method="RS") + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, θ=θ, quadrature_weights=w, method="RS") p1 = [ @SVector [θi, ϕi] for θi ∈ θ for ϕi ∈ LinRange(T(0), 2T(π), 2ℓmax+2)[begin:end-1] ] - p2 = pixels(𝒯) + p2 = Deprecated.pixels(𝒯) @test p1 ≈ p2 r1 = [from_spherical_coordinates(θϕ...) for θϕ ∈ p1] - r2 = rotors(𝒯) + r2 = Deprecated.rotors(𝒯) @test r1 ≈ r2 let ℓmin = abs(s) - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ f[:] .= false - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) expected = copy(f) computed = 𝒯 \ (𝒯 * f) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @@ -221,6 +230,7 @@ end # These test that the non-inplace versions of transformers that *can* work in place # still work. @testitem "Non-inplace" setup=[SSHT] begin + import SphericalFunctions: Deprecated using LinearAlgebra @testset verbose=false "Non-inplace: $T $method" for (method, T) in inplacecases @testset "$ℓmax" for ℓmax ∈ [4,5] @@ -230,13 +240,13 @@ end ϵ *= 50 end for s in [-1, 1] - 𝒯 = SSHT(s, ℓmax; T=T, method=method, inplace=false) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method, inplace=false) let ℓmin = abs(s) - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ f[:] .= false - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) expected = f f′ = similar(f) LinearAlgebra.mul!(f′, 𝒯, f) diff --git a/test/wigner_matrices.jl b/test/deprecated/wigner_matrices.jl similarity index 100% rename from test/wigner_matrices.jl rename to test/deprecated/wigner_matrices.jl diff --git a/test/wigner_matrices/H.jl b/test/deprecated/wigner_matrices/H.jl similarity index 79% rename from test/wigner_matrices/H.jl rename to test/deprecated/wigner_matrices/H.jl index 46cfbe71..e0af8bf7 100644 --- a/test/wigner_matrices/H.jl +++ b/test/deprecated/wigner_matrices/H.jl @@ -1,16 +1,17 @@ @testitem "H(0)" begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # We have H_{n}^{m′,m}(0) = (-1)^m′ δ_{m′,m} let β = zero(T) expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in abs(m′):n - H_rec = Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + H_rec = Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] if m′ == m @test H_rec ≈ T(-1)^m′ atol=eps(T) else @@ -26,18 +27,19 @@ end @testitem "H(π)" begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # We have H_{n}^{m′,m}(0) = (-1)^m′ δ_{m′,m} let β = T(π) expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in abs(m′):n - H_rec = Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + H_rec = Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] if m′ == -m @test H_rec ≈ T(-1)^(n+m′) atol=eps(T(π)) else @@ -53,6 +55,7 @@ end end @testitem "Compare H to explicit d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the explicit Wigner d # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, @@ -60,13 +63,13 @@ end expiβ = cis(β) for ℓₘₐₓ in 0:2 # 2 is the max explicitly coded ℓ for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in -n:n d_expl = ExplicitWignerMatrices.d_explicit(n, m′, m, expiβ) - d_rec = epsilon(m′) * epsilon(-m) * Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + d_rec = epsilon(m′) * epsilon(-m) * Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] @test d_rec ≈ d_expl atol=30eps(T) rtol=30eps(T) end end @@ -105,6 +108,7 @@ end end @testitem "Compare H to formulaic d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the formulaic Wigner d # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, @@ -113,13 +117,13 @@ end expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in -n:n d_form = ExplicitWignerMatrices.d_formula(n, m′, m, expiβ) - d_rec = epsilon(m′) * epsilon(-m) * Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + d_rec = epsilon(m′) * epsilon(-m) * Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] @test d_rec ≈ d_form atol=tol rtol=tol end end diff --git a/test/wigner_matrices/big_D.jl b/test/deprecated/wigner_matrices/big_D.jl similarity index 72% rename from test/wigner_matrices/big_D.jl rename to test/deprecated/wigner_matrices/big_D.jl index 4e5d4713..ab5be769 100644 --- a/test/wigner_matrices/big_D.jl +++ b/test/deprecated/wigner_matrices/big_D.jl @@ -1,4 +1,5 @@ @testitem "Compare H/D indexing" setup=[NaNChecker] begin + import SphericalFunctions: Deprecated const NaNCheck = NaNChecker.NaNCheck @testset "$T" for T in [Float64, Float32] # Here, we check that we can pass in either an "H wedge" array to be used with @@ -9,18 +10,18 @@ expiβ = cis(rand(0:eps(T):π)) expiβNaNCheck = complex(NaNCheck{T}(expiβ.re), NaNCheck{T}(expiβ.im)) NCTN = NaNCheck{T}(NaN) - Hw = fill(NCTN, WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβNaNCheck, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) - 𝔇 = fill(NCTN, WignerDsize(ℓₘₐₓ, m′ₘₐₓ)) - H!( + Hw = fill(NCTN, Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβNaNCheck, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) + 𝔇 = fill(NCTN, Deprecated.WignerDsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!( 𝔇, expiβNaNCheck, ℓₘₐₓ, m′ₘₐₓ, - H_recursion_coefficients(ℓₘₐₓ, T), WignerDindex + Deprecated.H_recursion_coefficients(ℓₘₐₓ, T), Deprecated.WignerDindex ) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in abs(m′):n - Hnm′m = Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] - 𝔇nm′m = 𝔇[WignerDindex(n, m′, m, m′ₘₐₓ)] + Hnm′m = Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] + 𝔇nm′m = 𝔇[Deprecated.WignerDindex(n, m′, m, m′ₘₐₓ)] @test Hnm′m == 𝔇nm′m end end @@ -30,25 +31,26 @@ end @testitem "Compare 𝔇 to formulaic d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic @testset "$T" for T in [BigFloat, Float64, Float32] # Now, we're ready to check that d_{n}^{m′,m}(β) matches the expected values # for a range of β values for ℓₘₐₓ in 0:4 - D_storage = D_prep(ℓₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) expiα = complex(one(T)) expiγ = complex(one(T)) for β in βrange(T) expiβ = cis(β) R = from_euler_angles(zero(T), β, zero(T)) - 𝔇 = D_matrices!(D_storage, R) + 𝔇 = Deprecated.D_matrices!(D_storage, R) for n in 0:ℓₘₐₓ for m′ in -n:n for m in -n:n 𝔇_formula = ExplicitWignerMatrices.D_formula( n, m′, m, expiα, expiβ, expiγ ) - 𝔇_recurrence = 𝔇[WignerDindex(n, m′, m)] + 𝔇_recurrence = 𝔇[Deprecated.WignerDindex(n, m′, m)] @test conj(𝔇_formula) ≈ 𝔇_recurrence atol=200eps(T) rtol=200eps(T) end end @@ -59,6 +61,7 @@ end end @testitem "Compare 𝔇 to formulaic 𝔇" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic using ProgressMeter using Random @@ -67,20 +70,20 @@ end # for a range of α, β, γ values Random.seed!(123) ℓₘₐₓ = T===BigFloat ? 4 : 8 - D_storage = D_prep(ℓₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) @showprogress desc="Compare 𝔇 to formulaic 𝔇 ($T)" for α in αrange(T, 5) for β in βrange(T, 5) for γ in γrange(T, 5) R = from_euler_angles(α, β, γ) - expiα, expiβ, expiγ = to_euler_phases(R) - 𝔇 = D_matrices!(D_storage, R) + expiα, expiβ, expiγ = Deprecated.to_euler_phases(R) + 𝔇 = Deprecated.D_matrices!(D_storage, R) for n in 0:ℓₘₐₓ for m′ in -n:n for m in -n:n 𝔇_formula = ExplicitWignerMatrices.D_formula( n, m′, m, expiα, expiβ, expiγ ) - 𝔇_recurrence = 𝔇[WignerDindex(n, m′, m)] + 𝔇_recurrence = 𝔇[Deprecated.WignerDindex(n, m′, m)] @test conj(𝔇_formula) ≈ 𝔇_recurrence atol=400eps(T) rtol=400eps(T) end end @@ -92,6 +95,7 @@ end end @testitem "Group characters" setup=[Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic using ProgressMeter using Random @@ -101,11 +105,11 @@ end # conjugacy classes of SO(3) are rotations through the same angle about any axis. Random.seed!(123) ℓₘₐₓ = T===BigFloat ? 10 : 20 - D_storage = D_prep(ℓₘₐₓ, T) - d_storage = d_prep(ℓₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) + d_storage = Deprecated.d_prep(ℓₘₐₓ, T) @showprogress desc="Group characters $T" for β in βrange(T) expiβ = cis(β) - d = d_matrices!(d_storage, expiβ) + d = Deprecated.d_matrices!(d_storage, expiβ) for j in 0:ℓₘₐₓ sin_ratio = if abs(β) < 10eps(T) T(2j+1) @@ -114,12 +118,12 @@ end else sin((2j+1)*β/2) / sin(β/2) end - χʲ = sum(d[WignerDindex(j, m, m)] for m in -j:j) + χʲ = sum(d[Deprecated.WignerDindex(j, m, m)] for m in -j:j) @test χʲ ≈ sin_ratio atol=500eps(T) rtol=500eps(T) for v̂ in v̂range(T) R = exp(β/2 * v̂) - 𝔇 = D_matrices!(D_storage, R) - χʲ = sum(𝔇[WignerDindex(j, m, m)] for m in -j:j) + 𝔇 = Deprecated.D_matrices!(D_storage, R) + χʲ = sum(𝔇[Deprecated.WignerDindex(j, m, m)] for m in -j:j) @test χʲ ≈ sin_ratio atol=500eps(T) rtol=500eps(T) end end @@ -128,6 +132,7 @@ end end @testitem "Representation property" setup=[Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic using ProgressMeter using Random @@ -136,7 +141,7 @@ end Random.seed!(123) tol = 3eps(T) ℓₘₐₓ = 10 - D₁_storage = D_prep(ℓₘₐₓ, T) + D₁_storage = Deprecated.D_prep(ℓₘₐₓ, T) 𝔇₁ = D₁_storage[1] 𝔇₂ = similar(𝔇₁) D₂_storage = (𝔇₂, D₁_storage[2:end]...) @@ -144,12 +149,12 @@ end D₁₂_storage = (𝔇₁₂, D₁_storage[2:end]...) @showprogress desc="Representation property ($T)" for R₁ in Rrange(T) for R₂ in Rrange(T) - D_matrices!(D₁_storage, R₁) - D_matrices!(D₂_storage, R₂) - D_matrices!(D₁₂_storage, R₁*R₂) + Deprecated.D_matrices!(D₁_storage, R₁) + Deprecated.D_matrices!(D₂_storage, R₂) + Deprecated.D_matrices!(D₁₂_storage, R₁*R₂) for ℓ in 0:ℓₘₐₓ - i = WignerDindex(ℓ, -ℓ, -ℓ) - j = WignerDindex(ℓ, ℓ, ℓ) + i = Deprecated.WignerDindex(ℓ, -ℓ, -ℓ) + j = Deprecated.WignerDindex(ℓ, ℓ, ℓ) 𝔇₁ˡ = transpose(reshape(𝔇₁[i:j], 2ℓ+1, 2ℓ+1)) 𝔇₂ˡ = transpose(reshape(𝔇₂[i:j], 2ℓ+1, 2ℓ+1)) 𝔇₁₂ˡ = transpose(reshape(𝔇₁₂[i:j], 2ℓ+1, 2ℓ+1)) diff --git a/test/wigner_matrices/sYlm.jl b/test/deprecated/wigner_matrices/sYlm.jl similarity index 66% rename from test/wigner_matrices/sYlm.jl rename to test/deprecated/wigner_matrices/sYlm.jl index 5274fba4..aa4a434f 100644 --- a/test/wigner_matrices/sYlm.jl +++ b/test/deprecated/wigner_matrices/sYlm.jl @@ -1,8 +1,10 @@ @testitem "Issue #40" begin - @test maximum(abs, sYlm_values(0.0, 0.0, 3, -2)) > 0 + import SphericalFunctions: Deprecated + @test maximum(abs, Deprecated.sYlm_values(0.0, 0.0, 3, -2)) > 0 end @testitem "Internal consistency" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64] @@ -10,19 +12,19 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = ℓₘₐₓ^2 * 2eps(T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) let R = randn(Rotor{T}) - @test_throws ErrorException sYlm_values!(sYlm_storage, R, sₘₐₓ+1) - @test_throws ErrorException sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) + @test_throws ErrorException Deprecated.sYlm_values!(sYlm_storage, R, sₘₐₓ+1) + @test_throws ErrorException Deprecated.sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) end for spin in [0, -1, -2] for ι in βrange(T) for ϕ in αrange(T) - R = from_spherical_coordinates(ι, ϕ) - Y = sYlm_values!(sYlm_storage, R, spin) - Y′ = sYlm_values(ι, ϕ, ℓₘₐₓ, spin) # Also tests issue #40 + R = Deprecated.from_spherical_coordinates(ι, ϕ) + Y = Deprecated.sYlm_values!(sYlm_storage, R, spin) + Y′ = Deprecated.sYlm_values(ι, ϕ, ℓₘₐₓ, spin) # Also tests issue #40 @test Y[(spin^2+1):end] ≈ Y′ atol=tol rtol=tol i = 1 for ℓ in 0:abs(spin)-1 @@ -38,6 +40,7 @@ end end @testitem "Spin property" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64, Float32, BigFloat] @@ -48,14 +51,14 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) @showprogress desc="Spin property ($T)" for spin in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) for γ in γrange(T) - R = from_spherical_coordinates(ι, ϕ) - Y1 = copy(sYlm_values!(sYlm_storage, R * exp(γ*𝐤/2), spin)) - Y2 = sYlm_values!(sYlm_storage, R, spin) + R = Deprecated.from_spherical_coordinates(ι, ϕ) + Y1 = copy(Deprecated.sYlm_values!(sYlm_storage, R * exp(γ*𝐤/2), spin)) + Y2 = Deprecated.sYlm_values!(sYlm_storage, R, spin) @test Y1 ≈ Y2 * cis(-spin*γ) atol=tol rtol=tol end end @@ -65,6 +68,7 @@ end end @testitem "sYlm vs WignerD" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64, Float32, BigFloat] @@ -74,14 +78,14 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) - D_storage = D_prep(ℓₘₐₓ, T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) @showprogress desc="sYlm vs WignerD ($T)" for s in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) - R = from_spherical_coordinates(ι, ϕ) - 𝔇 = D_matrices!(D_storage, R) - Y = sYlm_values!(sYlm_storage, R, s) + R = Deprecated.from_spherical_coordinates(ι, ϕ) + 𝔇 = Deprecated.D_matrices!(D_storage, R) + Y = Deprecated.sYlm_values!(sYlm_storage, R, s) i = 1 for ℓ in 0:abs(s)-1 for m in -ℓ:ℓ @@ -92,7 +96,7 @@ end for ℓ in abs(s):ℓₘₐₓ for m in -ℓ:ℓ sYlm1 = Y[i] - sYlm2 = (-1)^s * √((2ℓ+1)/(4T(π))) * 𝔇[WignerDindex(ℓ, m, -s)] + sYlm2 = (-1)^s * √((2ℓ+1)/(4T(π))) * 𝔇[Deprecated.WignerDindex(ℓ, m, -s)] if ≉(sYlm1, sYlm2, atol=tol, rtol=tol) println("Unequal at i=$i (s,ℓ,m)=$((s,ℓ,m)): ") @show ι ϕ @@ -109,6 +113,7 @@ end end @testitem "sYlm conjugation" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64, Float32, BigFloat] @@ -117,18 +122,18 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) @showprogress desc="sYlm conjugation ($T)" for ι in βrange(T) for ϕ in αrange(T) for γ in γrange(T) for s in -sₘₐₓ:sₘₐₓ - R = from_spherical_coordinates(ι, ϕ) - Y1 = copy(sYlm_values!(sYlm_storage, R, s)) - Y2 = sYlm_values!(sYlm_storage, R, -s) + R = Deprecated.from_spherical_coordinates(ι, ϕ) + Y1 = copy(Deprecated.sYlm_values!(sYlm_storage, R, s)) + Y2 = Deprecated.sYlm_values!(sYlm_storage, R, -s) for ℓ in abs(s):ℓₘₐₓ for m in -ℓ:ℓ - sYlm1 = conj(Y1[Yindex(ℓ, m)]) - sYlm2 = (-1)^(s+m) * Y2[Yindex(ℓ, -m)] + sYlm1 = conj(Y1[Deprecated.Yindex(ℓ, m)]) + sYlm2 = (-1)^(s+m) * Y2[Deprecated.Yindex(ℓ, -m)] @test sYlm1 ≈ sYlm2 atol=tol rtol=tol end end diff --git a/test/wigner_matrices/small_d.jl b/test/deprecated/wigner_matrices/small_d.jl similarity index 77% rename from test/wigner_matrices/small_d.jl rename to test/deprecated/wigner_matrices/small_d.jl index eb844e1b..a76b1549 100644 --- a/test/wigner_matrices/small_d.jl +++ b/test/deprecated/wigner_matrices/small_d.jl @@ -1,4 +1,5 @@ @testitem "Compare d expressions" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This just compares the two versions of the `d` function from test_utilities # to ensure that later tests that use those functions are reliable @@ -23,6 +24,7 @@ end @testitem "Compare d to formulaic d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # Now, we're ready to check that d_{n}^{m′,m}(β) matches the expected values # for a range of β values @@ -30,13 +32,13 @@ end for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:4 - d_storage = d_prep(ℓₘₐₓ, T) - d = d_matrices!(d_storage, expiβ) + d_storage = Deprecated.d_prep(ℓₘₐₓ, T) + d = Deprecated.d_matrices!(d_storage, expiβ) for n in 0:ℓₘₐₓ for m′ in -n:n for m in -n:n d_formula = ExplicitWignerMatrices.d_formula(n, m′, m, expiβ) - d_recurrence = d[WignerDindex(n, m′, m)] + d_recurrence = d[Deprecated.WignerDindex(n, m′, m)] @test d_formula ≈ d_recurrence atol=tol rtol=tol end end @@ -47,6 +49,7 @@ end end @testitem "Test d signatures" setup=[Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # 1 d_matrices(β, ℓₘₐₓ) # 2 d_matrices(expiβ, ℓₘₐₓ) @@ -57,20 +60,20 @@ end ℓₘₐₓ = 8 for β in βrange(T) expiβ = cis(β) - dA = d_matrices(β, ℓₘₐₓ) # 1 - dB = d_matrices(expiβ, ℓₘₐₓ) # 2 + dA = Deprecated.d_matrices(β, ℓₘₐₓ) # 1 + dB = Deprecated.d_matrices(expiβ, ℓₘₐₓ) # 2 @test array_equal(dA, dB) dB .= 0 - d_matrices!(dB, β, ℓₘₐₓ) # 5 + Deprecated.d_matrices!(dB, β, ℓₘₐₓ) # 5 @test array_equal(dA, dB) dB .= 0 - d_matrices!(dB, expiβ, ℓₘₐₓ) # 6 + Deprecated.d_matrices!(dB, expiβ, ℓₘₐₓ) # 6 @test array_equal(dA, dB) - d_storage = d_prep(ℓₘₐₓ, T) - dB = d_matrices!(d_storage, β) # 3 + d_storage = Deprecated.d_prep(ℓₘₐₓ, T) + dB = Deprecated.d_matrices!(d_storage, β) # 3 @test array_equal(dA, dB) dB .= 0 - dB = d_matrices!(d_storage, expiβ) # 4 + dB = Deprecated.d_matrices!(d_storage, expiβ) # 4 @test array_equal(dA, dB) end end diff --git a/test/haxis.jl b/test/haxis.jl index 47e50d24..f9e52594 100644 --- a/test/haxis.jl +++ b/test/haxis.jl @@ -1,5 +1,5 @@ @testitem "HAxis" setup=[EncodeDecode] begin - using SphericalFunctions.Redesign: HAxis, Nᵣ, ℓ, ℓₘᵢₙ, maxℓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ + using SphericalFunctions: HAxis, Nᵣ, ℓ, ℓₘᵢₙ, maxℓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ using .EncodeDecode: encode, decode # HAxis stores only the m′=ℓₘᵢₙ axis (0 or 1/2), with m ranging from ℓₘᵢₙ to ℓₘₐₓ. diff --git a/test/hwedge.jl b/test/hwedge.jl index 8d15acc3..f862d501 100644 --- a/test/hwedge.jl +++ b/test/hwedge.jl @@ -1,5 +1,5 @@ @testitem "HWedge" setup=[EncodeDecode] begin - using SphericalFunctions.Redesign: HWedge, HWedge_size, Nᵣ, ℓ, ℓₘᵢₙ, m′ₘᵢₙ, m′ₘₐₓ + using SphericalFunctions: HWedge, HWedge_size, Nᵣ, ℓ, ℓₘᵢₙ, m′ₘᵢₙ, m′ₘₐₓ using .EncodeDecode: encode, decode # We will fill the HWedge with integers that encode their indices. By iterating over diff --git a/test/runtests.jl b/test/runtests.jl index 6fbd747b..cefef814 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -67,8 +67,8 @@ function filter(testitem) end for skip ∈ skip_tags - if tag ∈ tags - @info "Skipping test '$name' tagged '$tag' due to skip filter." + if skip ∈ tags + @info "Skipping test '$name' tagged '$skip' due to skip filter." return false end end diff --git a/test/utilities/utilities.jl b/test/utilities/utilities.jl index 94b52c74..54da4731 100644 --- a/test/utilities/utilities.jl +++ b/test/utilities/utilities.jl @@ -1,6 +1,6 @@ @testsnippet Utilities begin -ℓmrange(ℓₘᵢₙ, ℓₘₐₓ) = eachrow(SphericalFunctions.Yrange(ℓₘᵢₙ, ℓₘₐₓ)) +ℓmrange(ℓₘᵢₙ, ℓₘₐₓ) = eachrow(SphericalFunctions.Deprecated.Yrange(ℓₘᵢₙ, ℓₘₐₓ)) ℓmrange(ℓₘₐₓ) = ℓmrange(0, ℓₘₐₓ) function sℓmrange(ℓₘₐₓ, sₘₐₓ) sₘₐₓ = min(abs(sₘₐₓ), ℓₘₐₓ) From de4bd29d9b53b98bc2e337002503fa70c215edb5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 14:25:03 -0500 Subject: [PATCH 309/329] Broaden tolerances for deprecated H test with Float32 --- test/deprecated/wigner_matrices/H.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/deprecated/wigner_matrices/H.jl b/test/deprecated/wigner_matrices/H.jl index e0af8bf7..91aee9e4 100644 --- a/test/deprecated/wigner_matrices/H.jl +++ b/test/deprecated/wigner_matrices/H.jl @@ -112,7 +112,7 @@ end @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the formulaic Wigner d # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, - tol = ifelse(T === BigFloat, 100, 1) * 30eps(T) + tol = ifelse(T ∈ (BigFloat, Float32), 100, 1) * 30eps(T) for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 From 1722efa176876a6e89df8d13a9df10c0bf3f4c10 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 15:14:59 -0500 Subject: [PATCH 310/329] Move redesign subdirectory up to src level --- src/SphericalFunctions.jl | 16 ++++++++++++---- src/redesign/README.md | 2 ++ src/redesign/SphericalFunctions.jl | 7 ------- src/{redesign => }/ssht/direct.jl | 0 src/{redesign => }/ssht/huffenberger_wandelt.jl | 0 src/{redesign => }/ssht/minimal.jl | 0 src/{redesign => }/ssht/reinecke_seljebotn.jl | 0 src/{redesign => }/ssht/ssht.jl | 0 src/{redesign/Wigner => wigner}/calculators.jl | 0 src/{redesign/Wigner => wigner}/recurrence.jl | 0 src/{redesign/Wigner => wigner}/wigner.jl | 0 src/{redesign/Wigner => wigner}/wigner_H.jl | 0 .../Wigner => wigner}/wigner_H_calculator.jl | 0 .../Wigner => wigner}/wigner_calculator.jl | 0 .../Wigner => wigner}/wigner_matrices.jl | 0 src/{redesign/Wigner => wigner}/wigner_matrix.jl | 0 16 files changed, 14 insertions(+), 11 deletions(-) rename src/{redesign => }/ssht/direct.jl (100%) rename src/{redesign => }/ssht/huffenberger_wandelt.jl (100%) rename src/{redesign => }/ssht/minimal.jl (100%) rename src/{redesign => }/ssht/reinecke_seljebotn.jl (100%) rename src/{redesign => }/ssht/ssht.jl (100%) rename src/{redesign/Wigner => wigner}/calculators.jl (100%) rename src/{redesign/Wigner => wigner}/recurrence.jl (100%) rename src/{redesign/Wigner => wigner}/wigner.jl (100%) rename src/{redesign/Wigner => wigner}/wigner_H.jl (100%) rename src/{redesign/Wigner => wigner}/wigner_H_calculator.jl (100%) rename src/{redesign/Wigner => wigner}/wigner_calculator.jl (100%) rename src/{redesign/Wigner => wigner}/wigner_matrices.jl (100%) rename src/{redesign/Wigner => wigner}/wigner_matrix.jl (100%) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 778a9e2a..b5fd1394 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -2,9 +2,13 @@ module SphericalFunctions using TestItems: @testitem, @testsnippet using FastTransforms: ifft, irfft -using Quaternionic: from_spherical_coordinates +using Quaternionic: Quaternionic, from_spherical_coordinates using StaticArrays: @SVector -using SpecialFunctions, DoubleFloats +using SpecialFunctions +using DoubleFloats +using FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, + FixedSizeVector + const MachineFloat = Union{Float16, Float32, Float64} @@ -22,8 +26,12 @@ export complex_powers, complex_powers!, ComplexPowers include("utilities/weights.jl") export fejer1, fejer2, clenshaw_curtis -# New public API: promote redesign to the top-level interface -include("redesign/SphericalFunctions.jl") +include("wigner/wigner.jl") +export AbstractWignerMatrix, WignerMatrix, WignerDMatrix, WignerdMatrix +export WignerCalculator, WignerDCalculator, WignerdCalculator, WignerHCalculator +export recurrence! +public ℓ, ℓₘᵢₙ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ +public ell, ellmin, mpmax, mpmin, mmax, mmin # Legacy API (no legacy names exported from the top-level module) include("deprecated/Deprecated.jl") diff --git a/src/redesign/README.md b/src/redesign/README.md index a0a5da91..fe996920 100644 --- a/src/redesign/README.md +++ b/src/redesign/README.md @@ -1,3 +1,5 @@ +TODO: Redesign API + src/ ├── SphericalFunctions.jl ├── utilities diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl index a543e23a..c9b1b1b5 100644 --- a/src/redesign/SphericalFunctions.jl +++ b/src/redesign/SphericalFunctions.jl @@ -1,10 +1,3 @@ -import Quaternionic -import TestItems: @testitem, @testsnippet -import FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, - FixedSizeVector - -include("wigner/wigner.jl") - # function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} # NT = complex(Quaternionic.basetype(R)) diff --git a/src/redesign/ssht/direct.jl b/src/ssht/direct.jl similarity index 100% rename from src/redesign/ssht/direct.jl rename to src/ssht/direct.jl diff --git a/src/redesign/ssht/huffenberger_wandelt.jl b/src/ssht/huffenberger_wandelt.jl similarity index 100% rename from src/redesign/ssht/huffenberger_wandelt.jl rename to src/ssht/huffenberger_wandelt.jl diff --git a/src/redesign/ssht/minimal.jl b/src/ssht/minimal.jl similarity index 100% rename from src/redesign/ssht/minimal.jl rename to src/ssht/minimal.jl diff --git a/src/redesign/ssht/reinecke_seljebotn.jl b/src/ssht/reinecke_seljebotn.jl similarity index 100% rename from src/redesign/ssht/reinecke_seljebotn.jl rename to src/ssht/reinecke_seljebotn.jl diff --git a/src/redesign/ssht/ssht.jl b/src/ssht/ssht.jl similarity index 100% rename from src/redesign/ssht/ssht.jl rename to src/ssht/ssht.jl diff --git a/src/redesign/Wigner/calculators.jl b/src/wigner/calculators.jl similarity index 100% rename from src/redesign/Wigner/calculators.jl rename to src/wigner/calculators.jl diff --git a/src/redesign/Wigner/recurrence.jl b/src/wigner/recurrence.jl similarity index 100% rename from src/redesign/Wigner/recurrence.jl rename to src/wigner/recurrence.jl diff --git a/src/redesign/Wigner/wigner.jl b/src/wigner/wigner.jl similarity index 100% rename from src/redesign/Wigner/wigner.jl rename to src/wigner/wigner.jl diff --git a/src/redesign/Wigner/wigner_H.jl b/src/wigner/wigner_H.jl similarity index 100% rename from src/redesign/Wigner/wigner_H.jl rename to src/wigner/wigner_H.jl diff --git a/src/redesign/Wigner/wigner_H_calculator.jl b/src/wigner/wigner_H_calculator.jl similarity index 100% rename from src/redesign/Wigner/wigner_H_calculator.jl rename to src/wigner/wigner_H_calculator.jl diff --git a/src/redesign/Wigner/wigner_calculator.jl b/src/wigner/wigner_calculator.jl similarity index 100% rename from src/redesign/Wigner/wigner_calculator.jl rename to src/wigner/wigner_calculator.jl diff --git a/src/redesign/Wigner/wigner_matrices.jl b/src/wigner/wigner_matrices.jl similarity index 100% rename from src/redesign/Wigner/wigner_matrices.jl rename to src/wigner/wigner_matrices.jl diff --git a/src/redesign/Wigner/wigner_matrix.jl b/src/wigner/wigner_matrix.jl similarity index 100% rename from src/redesign/Wigner/wigner_matrix.jl rename to src/wigner/wigner_matrix.jl From 3db146621cbd2971bf60e94589f0ee1f63d74c19 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 15:52:21 -0500 Subject: [PATCH 311/329] Fix some imports --- docs/make.jl | 12 +++++++++--- docs/src/internal.md | 30 +++++++++++++++--------------- docs/src/operators.md | 4 ++-- docs/src/redesign.md | 10 ++++++++-- docs/src/sYlm.md | 5 +++++ docs/src/transformations.md | 10 +++++----- docs/src/utilities.md | 8 ++++---- docs/src/wigner_matrices.md | 6 ++++++ src/wigner/wigner_matrices.jl | 6 +++--- 9 files changed, 57 insertions(+), 34 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 010c558a..97391a48 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -27,13 +27,19 @@ bib = CitationBibliography( ) using SphericalFunctions -using SphericalFunctions: SSHTDirect, SSHTMinimal, SSHTRS -DocMeta.setdocmeta!(SphericalFunctions, :DocTestSetup, :(using SphericalFunctions); recursive=true) +using SphericalFunctions.Deprecated + +DocMeta.setdocmeta!( + SphericalFunctions, + :DocTestSetup, + :(using SphericalFunctions; using SphericalFunctions.Deprecated); + recursive=true, +) makedocs( plugins=[bib], sitename="SphericalFunctions.jl", - modules = [SphericalFunctions], + modules = [SphericalFunctions, SphericalFunctions.Deprecated], format = Documenter.HTML( prettyurls = !("local" in ARGS), # Use clean URLs, unless built as a "local" build edit_link = "main", # Link out to "main" branch on github diff --git a/docs/src/internal.md b/docs/src/internal.md index 7d79a173..5a33564f 100644 --- a/docs/src/internal.md +++ b/docs/src/internal.md @@ -11,7 +11,7 @@ harmonics ``{}_{s}Y_{\ell,m}``, as well as `map2salm` functions. ```@autodocs Modules = [SphericalFunctions] -Pages = ["Hrecursion.jl"] +Pages = ["deprecated/Hrecursion.jl"] ``` Internally, the ``H`` recursion relies on calculation of the Associated Legendre @@ -19,15 +19,15 @@ Functions (ALFs), which can also be called on their own: ```@autodocs Modules = [SphericalFunctions] -Pages = ["associated_legendre.jl"] +Pages = ["deprecated/associated_legendre.jl"] ``` The function ``{}_{s}\lambda_{\ell,m}`` is defined as essentially ``{}_{s}Y_{\ell,0}``, and is important internally for computing the ALFs. We have some important utilities for computing it: ```@docs -SphericalFunctions.λ_recursion_initialize -SphericalFunctions.λ_iterator -SphericalFunctions.AlternatingCountdown +SphericalFunctions.Deprecated.λ_recursion_initialize +SphericalFunctions.Deprecated.λ_iterator +SphericalFunctions.Deprecated.AlternatingCountdown ``` @@ -36,26 +36,26 @@ SphericalFunctions.AlternatingCountdown Various `d`, `D`, and `sYlm` functions are important in the main API. Their names and signatures have been tweaked from older versions of this package. The -only one with remaining documentation is [`ₛ𝐘`](@ref), which could probably be -replaced by [`sYlm_values`](@ref), except that the default pixelization is +only one with remaining documentation is [`ₛ𝐘`](@ref SphericalFunctions.Deprecated.ₛ𝐘), which could probably be +replaced by [`sYlm_values`](@ref SphericalFunctions.Deprecated.sYlm_values), except that the default pixelization is [`golden_ratio_spiral_rotors`](@ref), which makes it very convenient for -interacting with [`SSHT`](@ref). +interacting with [`SSHT`](@ref SphericalFunctions.Deprecated.SSHT). ```@docs -ₛ𝐘 -SphericalFunctions.Y -SphericalFunctions.d -SphericalFunctions.D +SphericalFunctions.Deprecated.ₛ𝐘 +SphericalFunctions.Deprecated.Y +SphericalFunctions.Deprecated.d +SphericalFunctions.Deprecated.D ``` # Transformation -The newer [`SSHT`](@ref) interface is more efficient for most purposes, but this +The newer [`SSHT`](@ref SphericalFunctions.Deprecated.SSHT) interface is more efficient for most purposes, but this package used to use functions named `map2salm`, which is still present, but may be deprecated. ```@autodocs -Modules = [SphericalFunctions] -Pages = ["map2salm.jl"] +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/map2salm.jl"] ``` diff --git a/docs/src/operators.md b/docs/src/operators.md index 715353c8..f3fa43b8 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -249,6 +249,6 @@ Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more ## Docstrings ```@autodocs -Modules = [SphericalFunctions] -Pages = ["operators.jl"] +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/operators.jl"] ``` diff --git a/docs/src/redesign.md b/docs/src/redesign.md index 19a6b004..378454cc 100644 --- a/docs/src/redesign.md +++ b/docs/src/redesign.md @@ -1,5 +1,11 @@ ```@autodocs -Modules = [SphericalFunctions.Redesign] -Pages = ["redesign/SphericalFunctions.jl", "redesign/recurrence.jl", "redesign/WignerMatrix.jl", "redesign/WignerMatrices.jl"] +Modules = [SphericalFunctions] +Pages = [ + "SphericalFunctions.jl", + "ssht/ssht.jl", + "wigner/recurrence.jl", + "wigner/wigner_matrix.jl", + "wigner/wigner_matrices.jl", +] ``` diff --git a/docs/src/sYlm.md b/docs/src/sYlm.md index f3695535..29aec86d 100644 --- a/docs/src/sYlm.md +++ b/docs/src/sYlm.md @@ -1,5 +1,9 @@ # ``{}_{s}Y_{\ell,m}`` functions +```@meta +CurrentModule = SphericalFunctions.Deprecated +``` + The spin-weighted spherical harmonics are an [important set of functions defined on](@cite Boyle_2016) the rotation group ``𝐒𝐎(3)``, or more generally, the spin group ``𝐒𝐩𝐢𝐧(3)`` that covers it. They are eigenfunctions of [the @@ -22,6 +26,7 @@ matrices](@ref): ```julia using Quaternionic using SphericalFunctions +using SphericalFunctions.Deprecated R = randn(RotorF64) ℓₘₐₓ = 8 diff --git a/docs/src/transformations.md b/docs/src/transformations.md index b7aef4ed..0f298da2 100644 --- a/docs/src/transformations.md +++ b/docs/src/transformations.md @@ -64,7 +64,7 @@ f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] transformation will be performed repeatedly, it can be very efficient to pre-compute the matrix ``𝒯``, and capitalize on the impressive efficiency of linear-algebra libraries to perform the transformation. -And indeed, this is the approach taken by the [`SSHTDirect`](@ref) +And indeed, this is the approach taken by the [`SSHTDirect`](@ref SphericalFunctions.Deprecated.SSHTDirect) method. However, we must consider the memory requirements of this approach. @@ -170,8 +170,8 @@ advantages and disadvantages: ## `SSHT` objects ```@autodocs -Modules = [SphericalFunctions] -Pages = ["ssht.jl", "ssht/direct.jl", "ssht/minimal.jl", "ssht/rs.jl"] +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/ssht.jl", "deprecated/ssht/direct.jl", "deprecated/ssht/minimal.jl", "deprecated/ssht/rs.jl"] ``` ## Pixelizations @@ -234,7 +234,7 @@ The various pixelizations may be computed as follows: ```@autodocs Modules = [SphericalFunctions] -Pages = ["pixelizations.jl"] +Pages = ["utilities/pixelizations.jl"] ``` @@ -247,6 +247,6 @@ corresponding pixelizations: ```@autodocs Modules = [SphericalFunctions] -Pages = ["weights.jl"] +Pages = ["utilities/weights.jl"] Order = [:module, :type, :constant, :function, :macro] ``` diff --git a/docs/src/utilities.md b/docs/src/utilities.md index 2d9b8500..c1e3e2b7 100644 --- a/docs/src/utilities.md +++ b/docs/src/utilities.md @@ -13,7 +13,7 @@ do a little better with a specialized routine. ```@autodocs Modules = [SphericalFunctions] -Pages = ["complex_powers.jl"] +Pages = ["utilities/complex_powers.jl"] Order = [:module, :type, :constant, :function, :macro] ``` @@ -25,8 +25,8 @@ data, we mean anything indexed like Wigner's ``\mathfrak{D}`` matrices, or special subsets of them, like the ``H`` matrices. ```@autodocs -Modules = [SphericalFunctions] -Pages = ["indexing.jl", "wigner_matrices/indexing.jl", "wigner_matrices/Hrecursions.jl"] +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/indexing.jl"] Order = [:module, :type, :constant, :function, :macro] ``` @@ -40,7 +40,7 @@ methods. Here, we collect any specialized methods that help us beat the limits. ```@autodocs Modules = [SphericalFunctions] -Pages = ["utils.jl"] +Pages = ["utilities/utils.jl"] ``` diff --git a/docs/src/wigner_matrices.md b/docs/src/wigner_matrices.md index 0f9c8b02..361fc375 100644 --- a/docs/src/wigner_matrices.md +++ b/docs/src/wigner_matrices.md @@ -1,5 +1,9 @@ # Wigner's ``𝔇`` and ``d`` matrices +```@meta +CurrentModule = SphericalFunctions.Deprecated +``` + Wigner's ``𝔇`` matrices — and to a lesser extent, the related ``d`` matrices — are extremely important in the theory of rotations. Each element is, itself, a special function of the rotation group: in particular, an eigenfunction of [the @@ -14,6 +18,7 @@ The actual computations can be done with the [`D_matrices`](@ref) function: ```julia using Quaternionic using SphericalFunctions +using SphericalFunctions.Deprecated R = randn(RotorF64) ℓₘₐₓ = 8 @@ -47,6 +52,7 @@ which can be computed directly in some cases), and the output is real-valued: ```julia using Quaternionic using SphericalFunctions +using SphericalFunctions.Deprecated β = π * rand(Float64) ℓₘₐₓ = 8 diff --git a/src/wigner/wigner_matrices.jl b/src/wigner/wigner_matrices.jl index ae455c1a..14680682 100644 --- a/src/wigner/wigner_matrices.jl +++ b/src/wigner/wigner_matrices.jl @@ -105,7 +105,7 @@ end # @testitem "Test WignerDsize" setup=[WignerDUtilities] begin -# import SphericalFunctions.Redesign: WignerDsize +# import SphericalFunctions: WignerDsize # for ℓₘₐₓ ∈ 0:8 # @test WignerDsize(ℓₘₐₓ, 0, 0) == ℓₘₐₓ + 1 @@ -194,7 +194,7 @@ end # @testitem "Test WignerDMatrices index" setup=[WignerDUtilities] begin -# import SphericalFunctions.Redesign: WignerDMatrices, index +# import SphericalFunctions: WignerDMatrices, index # for ℓₘₐₓ ∈ 0:8 # for mₘₐₓ ∈ 0:ℓₘₐₓ @@ -248,7 +248,7 @@ end # @testitem "Test WignerDMatrices indices" setup=[WignerDUtilities] begin -# import SphericalFunctions.Redesign: WignerDMatrices, index +# import SphericalFunctions: WignerDMatrices, index # for ℓₘₐₓ ∈ 0:8 # for mₘₐₓ ∈ 0:ℓₘₐₓ From 2ec502567763ff221e59fa7b89b54988a2c9b171 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 21:51:08 -0500 Subject: [PATCH 312/329] Deal with documenter warnings --- docs/make.jl | 1 + docs/src/internal.md | 4 ++-- docs/src/wigner_matrices.md | 12 ++++++++++++ src/redesign/SphericalFunctions.jl | 15 --------------- 4 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 src/redesign/SphericalFunctions.jl diff --git a/docs/make.jl b/docs/make.jl index 97391a48..57f51acc 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -34,6 +34,7 @@ DocMeta.setdocmeta!( :DocTestSetup, :(using SphericalFunctions; using SphericalFunctions.Deprecated); recursive=true, + warn=false, ) makedocs( diff --git a/docs/src/internal.md b/docs/src/internal.md index 5a33564f..dce3de84 100644 --- a/docs/src/internal.md +++ b/docs/src/internal.md @@ -10,7 +10,7 @@ needed for Wigner's ``d`` and ``𝔇`` matrices, and the spin-weighted spherical harmonics ``{}_{s}Y_{\ell,m}``, as well as `map2salm` functions. ```@autodocs -Modules = [SphericalFunctions] +Modules = [SphericalFunctions.Deprecated] Pages = ["deprecated/Hrecursion.jl"] ``` @@ -18,7 +18,7 @@ Internally, the ``H`` recursion relies on calculation of the Associated Legendre Functions (ALFs), which can also be called on their own: ```@autodocs -Modules = [SphericalFunctions] +Modules = [SphericalFunctions.Deprecated] Pages = ["deprecated/associated_legendre.jl"] ``` diff --git a/docs/src/wigner_matrices.md b/docs/src/wigner_matrices.md index 361fc375..50e31477 100644 --- a/docs/src/wigner_matrices.md +++ b/docs/src/wigner_matrices.md @@ -80,3 +80,15 @@ d_matrices! d_prep d_iterator ``` + + +## Workspaces + +```@meta +CurrentModule = SphericalFunctions +``` + +```@docs +HWedge +HAxis +``` diff --git a/src/redesign/SphericalFunctions.jl b/src/redesign/SphericalFunctions.jl deleted file mode 100644 index c9b1b1b5..00000000 --- a/src/redesign/SphericalFunctions.jl +++ /dev/null @@ -1,15 +0,0 @@ - -# function WignerD(R::Quaternionic.Rotor, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ) where {IT} -# NT = complex(Quaternionic.basetype(R)) -# D = WignerDMatrices(NT, ℓₘₐₓ, m′ₘₐₓ) -# WignerD!(D, R) -# end - -# function WignerD!(D::WignerDMatrices{Complex{FT1}}, R::Quaternionic.Rotor{FT2}) where {FT1, FT2} -# R1 = Quaternionic.Rotor{FT1}(R) -# WignerD!(D, R1) -# end - -# function WignerD!(D::WignerDMatrices{Complex{FT}}, R::Quaternionic.Rotor{FT}) where {FT} -# error("WignerD! is not yet implemented") -# end From 99a56e3fde7b298af377ec7b8f00c70d14859b58 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 22:01:30 -0500 Subject: [PATCH 313/329] Remove unused files --- src/wigner/calculators.jl | 33 ---- src/wigner/wigner_matrices.jl | 274 ---------------------------------- 2 files changed, 307 deletions(-) delete mode 100644 src/wigner/calculators.jl delete mode 100644 src/wigner/wigner_matrices.jl diff --git a/src/wigner/calculators.jl b/src/wigner/calculators.jl deleted file mode 100644 index 48663e01..00000000 --- a/src/wigner/calculators.jl +++ /dev/null @@ -1,33 +0,0 @@ -struct WignerCalculator{IT, RT<:Real, NT<:Union{RT,Complex{RT}}, ST} - H::WignerHCalculator{IT, RT, ST} - e⁻ⁱᵐ′ᵅ::FixedSizeArrayDefault{NT} - e⁻ⁱᵐᵞ::FixedSizeArrayDefault{NT} - - function WignerCalculator( - ::Type{NT}, - rotors::AbstractVector{AbstractQuaternion{RT}}, - ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ - ) where {IT, RT<:Real, NT<:Union{RT,Complex{RT}}} - eⁱᵝ = FixedSizeVectorDefault{NT}(undef, length(rotors)) - e⁻ⁱᵐ′ᵅ = FixedSizeArrayDefault{NT}(undef, length(rotors), Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) - e⁻ⁱᵐᵞ = FixedSizeArrayDefault{NT}(undef, length(rotors), Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+1) - @inbounds for (i, j) ∈ eachindex(rotors, eⁱᵝ) - eⁱᵅ⁰, eⁱᵝ⁰, eⁱᵞ⁰ = Quaternionic.to_euler_phases(rotors[i]) - eⁱᵝ[j] = eⁱᵝ⁰ - conjexpiℓₘᵢₙα╱2, conjexpiℓₘᵢₙγ╱2 = if IT <: Rational - conj(√eⁱᵅ⁰), conj(√eⁱᵞ⁰) - else - 1, 1 - end - α = ComplexPowers(eⁱᵅ⁰) - γ = ComplexPowers(eⁱᵞ⁰) - for (k, eⁱᵏᵅ, eⁱᵏᵞ) ∈ zip(0:Int(ℓₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ)), α, γ) - e⁻ⁱᵐ′ᵅ[j, k+1] = conj(eⁱᵏᵅ) * conjexpiℓₘᵢₙα╱2 - e⁻ⁱᵐᵞ[ j, k+1] = conj(eⁱᵏᵞ) * conjexpiℓₘᵢₙγ╱2 - end - end - H = WignerHCalculator(eⁱᵝ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) - ST = typeof(parent(Hˡ(H))) - new{IT, RT, NT, ST}(H, e⁻ⁱᵐ′ᵅ, e⁻ⁱᵐᵞ) - end -end diff --git a/src/wigner/wigner_matrices.jl b/src/wigner/wigner_matrices.jl deleted file mode 100644 index 14680682..00000000 --- a/src/wigner/wigner_matrices.jl +++ /dev/null @@ -1,274 +0,0 @@ -""" - AbstractWignerMatrices{NT, IT, MT} - -A container for a series of Wigner matrices ( -- `NT` is the number type (e.g., `ComplexF64` for D-matrices or `Float64` for d-matrices). -- `IT` is the index type (an `Integer` or half‐integer `Rational`), governing the allowed - ranges of `m′` and `m` in each matrix. -- `MT` is the type of the matrices. - -""" -abstract type AbstractWignerMatrices{NT, IT, MT} <: AbstractVector{MT} end - - - -struct WignerDMatrices{NT, IT, MT} <: AbstractWignerMatrices{NT, IT, MT} - D::Vector{MT} - ℓₘₐₓ::IT - m′ₘₐₓ::IT - function WignerDMatrices( - D::Vector{WignerDMatrix{NT, IT}} - ) where {NT, IT} - new{NT, IT, MT}(D, ℓₘₐₓ, m′ₘₐₓ) - end -end - - -# abstract type AbstractDMatrices end - - -# """ -# WignerDMatrices{NT, IT} - -# A data structure to hold the Wigner D-matrices for a range of `ℓ` values (stored in a -# `Vector{NT}`) up to and including some `ℓₘₐₓ`, `m′ₘₐₓ`, and `mₘₐₓ` (which all have type -# `IT`). - -# Indexing this object with an integer `ℓ` returns an `OffsetArray` of a view of the relevant -# part of the data vector corresponding to the `ℓ` matrix. -# """ -# struct WignerDMatrices{NT, IT} <: AbstractDMatrices -# data::Vector{NT} -# ℓₘₐₓ::IT -# m′ₘₐₓ::IT -# end - -# data(D::WignerDMatrices) = D.data -# ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = zero(IT) -# ℓₘᵢₙ(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = IT(1//2) -# ℓₘₐₓ(D::WignerDMatrices) = D.ℓₘₐₓ -# m′ₘₐₓ(D::WignerDMatrices) = D.m′ₘₐₓ -# mₘₐₓ(D::WignerDMatrices) = D.mₘₐₓ -# m′ₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(m′ₘₐₓ(D), ℓ) -# mₘₐₓ(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} = min(mₘₐₓ(D), ℓ) - -# Base.eltype(D::WignerDMatrices) = eltype(data(D)) - -# isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Integer} = false -# isrational(D::WignerDMatrices{NT, IT}) where {NT, IT<:Rational} = true - - -# """ -# WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) - -# Return the total size of the data stored in a `WignerDMatrices` object with the given sizes, -# ranging over all matrices for all ℓ values. -# """ -# function WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)::Int -# m₁, m₂ = m′ₘₐₓ, mₘₐₓ -# if m₁ > m₂ -# m₁, m₂ = m₂, m₁ -# end - -# if ℓₘₐₓ ≤ m₁ -# (2ℓₘₐₓ + 1)*(2ℓₘₐₓ + 2)*(2ℓₘₐₓ + 3) ÷ 6 -# elseif ℓₘₐₓ ≤ m₂ -# ( -# (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 -# + (ℓₘₐₓ - m₁)*(2m₁ + 1)*(ℓₘₐₓ + m₁ + 2) -# ) -# else -# ( -# (2m₁ + 1)*(2m₁ + 2)*(2m₁ + 3) ÷ 6 -# + (m₂ - m₁)*(2m₁ + 1)*(m₂ + m₁ + 2) -# + (2m₁ + 1)*(2m₂ + 1)*(ℓₘₐₓ - m₂) -# ) -# end -# end - - -# @testsnippet WignerDUtilities begin -# function indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) -# data = Vector{Tuple{Int64, Int64, Int64}}(undef, sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ)) -# i=1 -# for ℓ ∈ 0:ℓₘₐₓ -# for m ∈ -min(ℓ, mₘₐₓ):min(ℓ, mₘₐₓ) -# for m′ ∈ -min(ℓ, m′ₘₐₓ):min(ℓ, m′ₘₐₓ) -# data[i] = (ℓ, m′, m) -# i += 1 -# end -# end -# end -# data -# end -# end - - -# @testitem "Test WignerDsize" setup=[WignerDUtilities] begin -# import SphericalFunctions: WignerDsize - -# for ℓₘₐₓ ∈ 0:8 -# @test WignerDsize(ℓₘₐₓ, 0, 0) == ℓₘₐₓ + 1 -# @test WignerDsize(ℓₘₐₓ, 1, 0) == 3ℓₘₐₓ + 1 -# @test WignerDsize(ℓₘₐₓ, 0, 1) == 3ℓₘₐₓ + 1 -# @test WignerDsize(ℓₘₐₓ, 1, 1) == (3^2)ℓₘₐₓ + 1 -# @test WignerDsize(ℓₘₐₓ, 2, 0) == max(1, 5ℓₘₐₓ - 1) -# @test WignerDsize(ℓₘₐₓ, 0, 2) == max(1, 5ℓₘₐₓ - 1) -# @test WignerDsize(ℓₘₐₓ, 2, 1) == max(1, 15ℓₘₐₓ - 5) -# @test WignerDsize(ℓₘₐₓ, 1, 2) == max(1, 15ℓₘₐₓ - 5) -# @test WignerDsize(ℓₘₐₓ, 2, 2) == max(1, (5^2)ℓₘₐₓ - 15) -# @test WignerDsize(ℓₘₐₓ, ℓₘₐₓ, ℓₘₐₓ) == sum((2ℓ+1)^2 for ℓ ∈ 0:ℓₘₐₓ) - -# for mₘₐₓ ∈ 0:ℓₘₐₓ -# for m′ₘₐₓ ∈ 0:ℓₘₐₓ -# @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == WignerDsize(ℓₘₐₓ, mₘₐₓ, m′ₘₐₓ) - -# (m₁, m₂) = extrema((m′ₘₐₓ, mₘₐₓ)) - -# @test WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) == ( -# sum(((2ℓ+1)^2 for ℓ ∈ 0:m₁), init=0) -# + sum(((2m₁+1)*(2ℓ+1) for ℓ ∈ m₁+1:m₂); init=0) -# + sum(((2m₁+1)*(2m₂+1) for ℓ ∈ m₂+1:ℓₘₐₓ); init=0) -# ) - -# data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) -# for ℓ ∈ 0:ℓₘₐₓ-1 -# @test data[WignerDsize(ℓ, m′ₘₐₓ, mₘₐₓ)] == (ℓ, min(m′ₘₐₓ, ℓ), min(mₘₐₓ, ℓ)) -# end - -# end -# end -# end -# end - - -# """ -# WignerDMatrices(NT, ℓₘₐₓ; m′ₘₐₓ=ℓₘₐₓ, mₘₐₓ=ℓₘₐₓ) - -# Create a `WignerDMatrices` object with the given parameters. The data is initialized to -# zero. -# """ -# function WignerDMatrices(::Type{NT}, ℓₘₐₓ::IT; m′ₘₐₓ::IT=ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ) where {NT, IT} -# # Massage the inputs -# mₘₐₓ = abs(mₘₐₓ) -# m′ₘₐₓ = abs(m′ₘₐₓ) - -# # Check that the parameters are valid -# if complex(NT) != NT -# throw(ErrorException("NT=$NT must be a complex type")) -# end -# if ℓₘₐₓ < (limit = (IT<:Rational ? 1//2 : 0)) -# throw(ErrorException("ℓₘₐₓ < $limit")) -# end -# if m′ₘₐₓ > ℓₘₐₓ -# throw(ErrorException("m′ₘₐₓ > ℓₘₐₓ")) -# end -# if mₘₐₓ > ℓₘₐₓ -# throw(ErrorException("mₘₐₓ > ℓₘₐₓ")) -# end - -# # Create the data array -# data = zeros(NT, WignerDsize(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ)) - -# return WignerDMatrices{NT, IT}(data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) -# end - - -# """ -# index(D, ℓ) - -# Find the index in `data(D)` of the first element of the `WignerDMatrix` for the given ℓ -# value. -# """ -# function index(D, ℓ) -# if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) -# throw(ErrorException("ℓ=$ℓ is out of range for D=$D")) -# end - -# if ℓ == ℓₘᵢₙ(D) -# 1 -# else -# WignerDsize(ℓ-1, m′ₘₐₓ(D), mₘₐₓ(D)) + 1 -# end -# end - - -# @testitem "Test WignerDMatrices index" setup=[WignerDUtilities] begin -# import SphericalFunctions: WignerDMatrices, index - -# for ℓₘₐₓ ∈ 0:8 -# for mₘₐₓ ∈ 0:ℓₘₐₓ -# for m′ₘₐₓ ∈ 0:ℓₘₐₓ -# data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) -# D = WignerDMatrices(ComplexF64, ℓₘₐₓ; m′ₘₐₓ, mₘₐₓ) -# for ℓ ∈ 0:ℓₘₐₓ -# @test data[index(D, ℓ)] == (ℓ, -min(m′ₘₐₓ, ℓ), -min(mₘₐₓ, ℓ)) -# end - -# end -# end -# end -# end - - -# """ -# size(D) - -# Return the total size of the data stored in this WignerDMatrices object, ranging over all -# matrices for all ℓ values. For the size of a particular matrix, use `size(D, ℓ)`. -# """ -# Base.size(D::WignerDMatrices) = WignerDsize(ℓₘₐₓ(D), m′ₘₐₓ(D), mₘₐₓ(D)) - - -# """ -# size(D, ℓ) - -# Return the size of the data stored in this WignerDMatrices object for a particular ℓ value. -# For the size of all matrices combined, use `size(D)`. -# """ -# function Base.size(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT} -# if ℓ < ℓₘᵢₙ(D) || ℓ > ℓₘₐₓ(D) -# 0 -# else -# return (Int(2m′ₘₐₓ(D, ℓ)) + 1) * (Int(2mₘₐₓ(D, ℓ)) + 1) -# end -# end - -# function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Rational} -# throw(ErrorException("Don't yet know how to deal with Rational indices")) -# end - -# function Base.getindex(D::WignerDMatrices{NT, IT}, ℓ::IT) where {NT, IT<:Integer} -# i₁ = index(D, ℓ) -# i₂ = i₁ + size(D, ℓ) - 1 -# m′ = m′ₘₐₓ(D, ℓ) -# m = mₘₐₓ(D, ℓ) -# OffsetArrays.Origin(-m′, -m)(reshape((@view data(D)[i₁:i₂]), 2m′+1, 2m+1)) -# end - - -# @testitem "Test WignerDMatrices indices" setup=[WignerDUtilities] begin -# import SphericalFunctions: WignerDMatrices, index - -# for ℓₘₐₓ ∈ 0:8 -# for mₘₐₓ ∈ 0:ℓₘₐₓ -# for m′ₘₐₓ ∈ 0:ℓₘₐₓ -# data = indices_vector(ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ) -# D = WignerDMatrices{eltype(data), Int}( -# data, ℓₘₐₓ, m′ₘₐₓ, mₘₐₓ -# ) - -# for ℓ ∈ 0:ℓₘₐₓ -# Dˡ = D[ℓ] -# @test size(Dˡ) == (2min(m′ₘₐₓ, ℓ)+1, 2min(mₘₐₓ, ℓ)+1) - -# for m ∈ -min(mₘₐₓ, ℓ):min(mₘₐₓ, ℓ) -# for m′ ∈ -min(m′ₘₐₓ, ℓ):min(m′ₘₐₓ, ℓ) -# @test Dˡ[m′, m] == (ℓ, m′, m) -# end -# end -# end -# end -# end -# end -# end From a983ac155684c683afb4fb5cefe0eec2c01a6dc0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 22:05:38 -0500 Subject: [PATCH 314/329] Tweak formatting --- src/wigner/wigner_H_calculator.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/wigner/wigner_H_calculator.jl b/src/wigner/wigner_H_calculator.jl index d6e07ce5..7518445e 100644 --- a/src/wigner/wigner_H_calculator.jl +++ b/src/wigner/wigner_H_calculator.jl @@ -1,12 +1,13 @@ struct WignerHCalculator{IT, RT<:Real, ST} - h⃗ᵃ::HAxis{IT, RT} - h⃗ᵇ::HAxis{IT, RT} - Hˡ::HWedge{IT, RT, ST} - eⁱᵝ::FixedSizeVectorDefault{Complex{RT}} - ℓₘₐₓ::IT - m′ₘₐₓ::IT - m′ₘᵢₙ::IT - swapH::Base.RefValue{Bool} # h⃗ˡ(w) returns h⃗ᵃ if `false`, otherwise h⃗ᵇ; and vice versa for h⃗ˡ⁺¹(w) + h⃗ᵃ::HAxis{IT, RT} + h⃗ᵇ::HAxis{IT, RT} + Hˡ::HWedge{IT, RT, ST} + eⁱᵝ::FixedSizeVectorDefault{Complex{RT}} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + swapH::Base.RefValue{Bool} # h⃗ˡ(w) returns h⃗ᵃ if `false`, otherwise h⃗ᵇ; and vice versa for h⃗ˡ⁺¹(w) + function WignerHCalculator( eⁱᵝ::AbstractVector{Complex{RT}}, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ ) where {IT, RT<:Real} From 1da27dc52543a2f3b8e558649bd3a4880e121bb3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 22:37:14 -0500 Subject: [PATCH 315/329] Remove unused page --- docs/make.jl | 1 - docs/src/redesign.md | 11 ----------- 2 files changed, 12 deletions(-) delete mode 100644 docs/src/redesign.md diff --git a/docs/make.jl b/docs/make.jl index 57f51acc..b3233e5d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -85,7 +85,6 @@ makedocs( "development/literate_testitems.md", ], "References" => "references.md", - "Redesign" => "redesign.md", ], warnonly=true, #doctest = false, diff --git a/docs/src/redesign.md b/docs/src/redesign.md deleted file mode 100644 index 378454cc..00000000 --- a/docs/src/redesign.md +++ /dev/null @@ -1,11 +0,0 @@ - -```@autodocs -Modules = [SphericalFunctions] -Pages = [ - "SphericalFunctions.jl", - "ssht/ssht.jl", - "wigner/recurrence.jl", - "wigner/wigner_matrix.jl", - "wigner/wigner_matrices.jl", -] -``` From cd605a6154cb44681c89fd7479d980028e4984f4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 22:59:05 -0500 Subject: [PATCH 316/329] Move operators back from deprecation --- docs/src/operators.md | 4 +- src/SphericalFunctions.jl | 3 + src/deprecated/Deprecated.jl | 3 - src/{deprecated => utilities}/operators.jl | 0 test/deprecated/operators.jl | 70 +++++++++++----------- 5 files changed, 40 insertions(+), 40 deletions(-) rename src/{deprecated => utilities}/operators.jl (100%) diff --git a/docs/src/operators.md b/docs/src/operators.md index f3fa43b8..715353c8 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -249,6 +249,6 @@ Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more ## Docstrings ```@autodocs -Modules = [SphericalFunctions, SphericalFunctions.Deprecated] -Pages = ["deprecated/operators.jl"] +Modules = [SphericalFunctions] +Pages = ["operators.jl"] ``` diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index b5fd1394..085451c7 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -26,6 +26,9 @@ export complex_powers, complex_powers!, ComplexPowers include("utilities/weights.jl") export fejer1, fejer2, clenshaw_curtis +include("utilities/operators.jl") +export L², Lz, L₊, L₋, R², Rz, R₊, R₋, ð, ð̄ + include("wigner/wigner.jl") export AbstractWignerMatrix, WignerMatrix, WignerDMatrix, WignerdMatrix export WignerCalculator, WignerDCalculator, WignerdCalculator, WignerHCalculator diff --git a/src/deprecated/Deprecated.jl b/src/deprecated/Deprecated.jl index b5778714..041685ce 100644 --- a/src/deprecated/Deprecated.jl +++ b/src/deprecated/Deprecated.jl @@ -58,9 +58,6 @@ export SSHT, pixels, rotors include("map2salm.jl") export map2salm, map2salm!, plan_map2salm -include("operators.jl") -export L², Lz, L₊, L₋, R², Rz, R₊, R₋, ð, ð̄ - # include("rotate.jl") # export rotate! diff --git a/src/deprecated/operators.jl b/src/utilities/operators.jl similarity index 100% rename from src/deprecated/operators.jl rename to src/utilities/operators.jl diff --git a/test/deprecated/operators.jl b/test/deprecated/operators.jl index f92974eb..7375f794 100644 --- a/test/deprecated/operators.jl +++ b/test/deprecated/operators.jl @@ -224,8 +224,8 @@ end @testset "$ℓₘₐₓ" for ℓₘₐₓ ∈ 4:7 for s in -3:3 let ℓₘᵢₙ = 0 - for Oᵢ ∈ [Deprecated.Lz, Deprecated.L₊, Deprecated.L₋, Deprecated.Rz, Deprecated.R₊, Deprecated.R₋] - for O² ∈ [Deprecated.L², Deprecated.R²] + for Oᵢ ∈ [Lz, L₊, L₋, Rz, R₊, R₋] + for O² ∈ [L², R²] let O²=O²(s, ℓₘᵢₙ, ℓₘₐₓ, T), Oᵢ=Oᵢ(s, ℓₘᵢₙ, ℓₘₐₓ, T) # [O², Oᵢ] = 0 @@ -233,9 +233,9 @@ end end end end - let Lz=Array(Deprecated.Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T)), - L₊=Array(Deprecated.L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)), - L₋=Array(Deprecated.L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)) + let Lz=Array(Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T)), + L₊=Array(L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)), + L₋=Array(L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)) # [Lz, L₊] = L₊ @test Lz*L₊ - L₊*Lz ≈ L₊ atol=ϵ rtol=ϵ # [Lz, L₋] = -L₋ @@ -246,39 +246,39 @@ end let # [Rz, R₊] = R₊ @test ( - Deprecated.Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [Rz, R₋] = -R₋ @test ( - Deprecated.Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ -Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ -R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [R₊, R₋] = 2Rz @test ( - Deprecated.R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - Deprecated.R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ 2Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [Rz, ð] = -ð @test ( - Deprecated.Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ -Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Rz(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - ð(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ -ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [Rz, ð̄] = ð̄ @test ( - Deprecated.Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) - - Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) + Rz(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) + - ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T)*Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ # [ð, ð̄] = 2Rz @test ( - Deprecated.ð(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) - -Deprecated.ð̄(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) - ≈ 2Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ð(s-1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) + -ð̄(s+1, ℓₘᵢₙ, ℓₘₐₓ, T)*ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) + ≈ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) atol=ϵ rtol=ϵ end end @@ -296,25 +296,25 @@ end for s ∈ -3:3 for ℓₘₐₓ ∈ 4:7 for ℓₘᵢₙ ∈ 0:min(abs(s)+1, ℓₘₐₓ) - let L²=Deprecated.L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), - Lz=Deprecated.Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T), - L₊=Deprecated.L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T), - L₋=Deprecated.L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), + Lz=Lz(s, ℓₘᵢₙ, ℓₘₐₓ, T), + L₊=L₊(s, ℓₘᵢₙ, ℓₘₐₓ, T), + L₋=L₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) L1 = L² L2 = (L₊*L₋ .+ L₋*L₊ .+ 2Lz*Lz)/2 @test L1 ≈ L2 atol=ϵ rtol=ϵ end - let L²=Deprecated.L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), - R²=Deprecated.R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) + let L²=L²(s, ℓₘᵢₙ, ℓₘₐₓ, T), + R²=R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) @test L² ≈ R² atol=ϵ rtol=ϵ end let # R² = (2Rz² + R₊R₋ + R₋R₊)/2 - R1 = Deprecated.R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) + R1 = R²(s, ℓₘᵢₙ, ℓₘₐₓ, T) R2 = T.(Array( - Deprecated.R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T) * Deprecated.R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) - .+ Deprecated.R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T) * Deprecated.R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) - .+ 2Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Deprecated.Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) + R₊(s+1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₋(s, ℓₘᵢₙ, ℓₘₐₓ, T) + .+ R₋(s-1, ℓₘᵢₙ, ℓₘₐₓ, T) * R₊(s, ℓₘᵢₙ, ℓₘₐₓ, T) + .+ 2Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Rz(s, ℓₘᵢₙ, ℓₘₐₓ, T) ) / 2) @test R1 ≈ R2 atol=ϵ rtol=ϵ end @@ -342,11 +342,11 @@ end for m in -ℓ:ℓ Y[:] .= zero(T) Y[Deprecated.Yindex(ℓ, m, ℓₘᵢₙ)] = one(T) - ðY = 𝒯₊ * (Deprecated.ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₊:end] + ðY = 𝒯₊ * (ð(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₊:end] Y₊ = 𝒯₊ * Y[i₊:end] c₊ = ℓ < abs(s+1) ? zero(T) : √T((ℓ-s)*(ℓ+s+1)) @test ðY ≈ c₊ * Y₊ atol=ϵ rtol=ϵ - ð̄Y = 𝒯₋ * (Deprecated.ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₋:end] + ð̄Y = 𝒯₋ * (ð̄(s, ℓₘᵢₙ, ℓₘₐₓ, T) * Y)[i₋:end] Y₋ = 𝒯₋ * Y[i₋:end] c₋ = ℓ < abs(s-1) ? zero(T) : -√T((ℓ+s)*(ℓ-s+1)) @test ð̄Y ≈ c₋ * Y₋ atol=ϵ rtol=ϵ From 2ed0b22cff24a344e78cbcf63da6518c49da3b21 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 23:03:21 -0500 Subject: [PATCH 317/329] Rename MachineFloat to IEEEFloat, consistently with Base.Math --- src/SphericalFunctions.jl | 3 ++- src/deprecated/Deprecated.jl | 2 +- src/deprecated/map2salm.jl | 4 ++-- src/utilities/weights.jl | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 085451c7..65a1755f 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -10,7 +10,8 @@ using FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeA FixedSizeVector -const MachineFloat = Union{Float16, Float32, Float64} +# Base.Math.IEEEFloat is not public API, so we just define our own +const IEEEFloat = Union{Float16, Float32, Float64} # Utilities (kept top-level; code lives in `src/utilities/`) include("utilities/utils.jl") diff --git a/src/deprecated/Deprecated.jl b/src/deprecated/Deprecated.jl index 041685ce..6fd94ce1 100644 --- a/src/deprecated/Deprecated.jl +++ b/src/deprecated/Deprecated.jl @@ -13,7 +13,7 @@ using LoopVectorization: @turbo using Base.Threads: @threads, threadpoolsize # Pull in shared utility functionality from the parent module (code lives in `src/utilities/`). -using ..SphericalFunctions: MachineFloat +using ..SphericalFunctions: IEEEFloat using ..SphericalFunctions: OffsetVec, OffsetMat, offset using ..SphericalFunctions: sqrtbinomial using ..SphericalFunctions: golden_ratio_spiral_pixels, golden_ratio_spiral_rotors diff --git a/src/deprecated/map2salm.jl b/src/deprecated/map2salm.jl index a3a78148..5d032da2 100644 --- a/src/deprecated/map2salm.jl +++ b/src/deprecated/map2salm.jl @@ -87,7 +87,7 @@ function plan_map2salm(map_data::AbstractArray{Complex{T}}, spin::Int, ℓmax::I # Number of workers seen by `@thread` Nworkers = threadpoolsize(:default) - workspace_pool = if T <: MachineFloat + workspace_pool = if T <: IEEEFloat # Build FFT plan from a representative slice proto_idx = extra_dims[1] proto = @views map_data[:, 1, proto_idx.I...] @@ -131,7 +131,7 @@ function computeG!( G::AbstractArray{Complex{T}}, map::AbstractArray{Complex{T}}, weight::T, fftplan -) where {T<:MachineFloat} +) where {T<:IEEEFloat} @views mul!(G[:], fftplan, map) @views G[:] *= weight end diff --git a/src/utilities/weights.jl b/src/utilities/weights.jl index 4949b8c8..ff827528 100644 --- a/src/utilities/weights.jl +++ b/src/utilities/weights.jl @@ -68,7 +68,7 @@ function fejer2(n, ::Type{T}) where {T<:AbstractFloat} w[2:end] end -function fejer2(n, ::Type{T}=Float64) where {T<:MachineFloat} +function fejer2(n, ::Type{T}=Float64) where {T<:IEEEFloat} # Specialized to "machine" floats; significant reduction in memory and increase in speed v = Vector{T}(undef, (n+1)÷2 + 1) @inbounds begin @@ -122,7 +122,7 @@ function clenshaw_curtis(n, ::Type{T}) where {T<:AbstractFloat} [w; w[1]] end -function clenshaw_curtis(n, ::Type{T}=Float64) where {T<:MachineFloat} +function clenshaw_curtis(n, ::Type{T}=Float64) where {T<:IEEEFloat} # Specialized to "machine" floats; significant reduction in memory and increase in speed nmod2 = mod(n-1, 2) w₀ᶜᶜ = inv(T((n-1)^2 - 1 + nmod2)) From a40f3d2cdaa9bf52dd5bd9fad5b1277f74460fa6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 6 Jan 2026 23:32:07 -0500 Subject: [PATCH 318/329] Tidy up some imports --- src/SphericalFunctions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 65a1755f..66e4ca1b 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -6,8 +6,8 @@ using Quaternionic: Quaternionic, from_spherical_coordinates using StaticArrays: @SVector using SpecialFunctions using DoubleFloats -using FixedSizeArrays: FixedSizeArrayDefault, FixedSizeVectorDefault, FixedSizeArray, - FixedSizeVector +using LinearAlgebra: Diagonal, Bidiagonal +using FixedSizeArrays: FixedSizeVectorDefault, FixedSizeVector # Base.Math.IEEEFloat is not public API, so we just define our own From 08c7862eef6616083234c208617d16852e39667e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 7 Jan 2026 00:15:40 -0500 Subject: [PATCH 319/329] Be more precise about where IEEEFloat comes from --- src/SphericalFunctions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 66e4ca1b..3e79b01a 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -10,7 +10,7 @@ using LinearAlgebra: Diagonal, Bidiagonal using FixedSizeArrays: FixedSizeVectorDefault, FixedSizeVector -# Base.Math.IEEEFloat is not public API, so we just define our own +# Base.IEEEFloat is not public, so we just define our own const IEEEFloat = Union{Float16, Float32, Float64} # Utilities (kept top-level; code lives in `src/utilities/`) From c5239859afc04cb499146075d152c3e74024fd1a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 7 Jan 2026 08:32:06 -0500 Subject: [PATCH 320/329] Reorganize docs --- docs/make.jl | 23 ++++++++++++++------- docs/src/{ => api}/functions.md | 0 docs/src/{ => api}/internal.md | 0 docs/src/deprecated/index.md | 2 ++ docs/src/{ => interface}/operators.md | 0 docs/src/{ => interface}/sYlm.md | 0 docs/src/{ => interface}/transformations.md | 0 docs/src/{ => interface}/utilities.md | 0 docs/src/{ => interface}/wigner_matrices.md | 0 9 files changed, 17 insertions(+), 8 deletions(-) rename docs/src/{ => api}/functions.md (100%) rename docs/src/{ => api}/internal.md (100%) create mode 100644 docs/src/deprecated/index.md rename docs/src/{ => interface}/operators.md (100%) rename docs/src/{ => interface}/sYlm.md (100%) rename docs/src/{ => interface}/transformations.md (100%) rename docs/src/{ => interface}/utilities.md (100%) rename docs/src/{ => interface}/wigner_matrices.md (100%) diff --git a/docs/make.jl b/docs/make.jl index b3233e5d..ee5cc5f8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -49,14 +49,14 @@ makedocs( ), pages = [ "index.md", - "wigner_matrices.md", - "sYlm.md", - "transformations.md", - "operators.md", - "utilities.md", - "API" => [ - "internal.md", - "functions.md", + "Background" => [ + ], + "Interface" => [ + "interface/wigner_matrices.md", + "interface/sYlm.md", + "interface/transformations.md", + "interface/operators.md", + "interface/utilities.md", ], "Conventions" => [ "conventions/summary.md", @@ -76,6 +76,10 @@ makedocs( sort(readdir(joinpath(docs_src_dir, "conventions", "calculations"))) ), ], + "API" => [ + "api/internal.md", + "api/functions.md", + ], "Notes" => map( s -> joinpath("notes", s), sort(readdir(joinpath(docs_src_dir, "notes"))) @@ -84,6 +88,9 @@ makedocs( "development/index.md", "development/literate_testitems.md", ], + "Deprecated" => [ + "deprecated/index.md", + ], "References" => "references.md", ], warnonly=true, diff --git a/docs/src/functions.md b/docs/src/api/functions.md similarity index 100% rename from docs/src/functions.md rename to docs/src/api/functions.md diff --git a/docs/src/internal.md b/docs/src/api/internal.md similarity index 100% rename from docs/src/internal.md rename to docs/src/api/internal.md diff --git a/docs/src/deprecated/index.md b/docs/src/deprecated/index.md new file mode 100644 index 00000000..37c8e389 --- /dev/null +++ b/docs/src/deprecated/index.md @@ -0,0 +1,2 @@ +# Deprecations + diff --git a/docs/src/operators.md b/docs/src/interface/operators.md similarity index 100% rename from docs/src/operators.md rename to docs/src/interface/operators.md diff --git a/docs/src/sYlm.md b/docs/src/interface/sYlm.md similarity index 100% rename from docs/src/sYlm.md rename to docs/src/interface/sYlm.md diff --git a/docs/src/transformations.md b/docs/src/interface/transformations.md similarity index 100% rename from docs/src/transformations.md rename to docs/src/interface/transformations.md diff --git a/docs/src/utilities.md b/docs/src/interface/utilities.md similarity index 100% rename from docs/src/utilities.md rename to docs/src/interface/utilities.md diff --git a/docs/src/wigner_matrices.md b/docs/src/interface/wigner_matrices.md similarity index 100% rename from docs/src/wigner_matrices.md rename to docs/src/interface/wigner_matrices.md From 73f7d0066fcbfe50f3d362f87a01fe3e5667b6a4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 7 Jan 2026 15:36:13 -0500 Subject: [PATCH 321/329] Separate background from interface a little --- docs/src/background/domain.md | 133 +++++++++++++ docs/src/background/operators.md | 248 +++++++++++++++++++++++++ docs/src/background/sYlm.md | 9 + docs/src/background/transformations.md | 0 docs/src/background/wigner_matrices.md | 0 docs/src/interface/operators.md | 247 +----------------------- docs/src/interface/sYlm.md | 61 +++--- docs/src/interface/transformations.md | 20 +- docs/src/interface/utilities.md | 4 +- 9 files changed, 436 insertions(+), 286 deletions(-) create mode 100644 docs/src/background/domain.md create mode 100644 docs/src/background/operators.md create mode 100644 docs/src/background/sYlm.md create mode 100644 docs/src/background/transformations.md create mode 100644 docs/src/background/wigner_matrices.md diff --git a/docs/src/background/domain.md b/docs/src/background/domain.md new file mode 100644 index 00000000..0a4fbd41 --- /dev/null +++ b/docs/src/background/domain.md @@ -0,0 +1,133 @@ +# [Domain](@id background_domain) + +This package deals with standard spherical harmonics, spin-weighted +spherical harmonics, and Wigner's 𝔇 matrices. The key question is +what domains these functions are defined on — what their arguments +are. We will discover that it's best to use quaternions for all +three. + +Usually, these are written as functions of spherical coordinates for +both types of harmonics, and Euler angles for the 𝔇 matrices: +```math +\begin{gathered} +Y_{ℓ,m}(θ, ϕ), \\ +{}_sY_{ℓ,m}(θ, ϕ), \\ +𝔇^{(ℓ)}_{m', m}(α, β, γ). +\end{gathered} +``` +While that's good enough for many purposes, it obscures the true +nature of these functions, and makes certain manipulations more +difficult or even impossible. We can make a few observations: + + 1. Standard (scalar) spherical harmonics are just a special case of + spin-weighted spherical harmonics with spin weight ``s=0``. + 2. Spin-weighted spherical harmonics *cannot* be defined as + functions on the sphere ``𝕊²`` alone; they depend on an + additional choice of frame at each point on the sphere — a choice + of basis for the tangent space at that point. This extra + structure is necessary to define what "spin weight" means at all. + (We'll define it [soon](@ref background_differential_operators).) + 3. Euler angles are a bad way to deal with rotations. Besides the + coordinate singularities at the poles, they make composing + rotations far more difficult. More importantly, they make it + hard to deal with *generators* of rotations, which are crucial + for angular-momentum operators. + +The second point especially requires a little more explanation. In +fact, I wrote an entire paper that gets very deep into the details +about this point [Boyle_2016](@cite). It may seem odd that +spin-weighted spherical functions cannot be defined on the sphere +``𝕊²``, when they're written in terms of spherical coordinates. The +key subtlety here is that the spherical coordinates ``(θ, ϕ)`` +implicitly define a conventional choice of tangent basis. But spin +weight is *defined* in terms of what happens to a function when you +rotate that tangent basis. Thus, if we start from spherical +coordinates, we are naturally led to introduce a third angle ``ψ`` +describing rotation of the tangent basis about the radial direction. +And those three angles together are just Euler angles. In fact, ``(θ, +ϕ, ψ)`` is *precisely* the set of "Eulerian" angles defined by the +very influential [book by Whittaker](@ref Whittaker-(1904)) +[Whittaker_1947](@cite). It turns out [GoldbergEtAl_1967](@cite) that +with this extra angle included explicitly, the original spin-weighted +spherical harmonics [Newman_1966](@cite) are simply proportional to +Wigner's 𝔇 matrices, +```math +{}_{s}Y_{ℓ,m}(θ, ϕ, ψ) += (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} \, 𝔇^{(ℓ)}_{m, -s}(ϕ, θ, ψ), +``` +so we might as well think of them as being parameterized in the same +way. + +It might be helpful to pause for a moment and consider a more +intuitive physical picture. Consider a telescope. We can point it +any which way we want, which corresponds to choosing a point on the +sphere ``𝕊²``. Now, imagine an ideal pixel at the very center of the +image formed by that telescope. The entire sky can be described in +terms of that pixel as a function of where the telescope is pointed. +That is, it's a function of ``𝕊²``. But now, suppose we put a +polarizer on the telescope. The pixel's value will now depend not +only on where the telescope is pointed, but also on the orientation of +the polarizer about the optical axis. This extra degree of freedom is +exactly the extra angle ``ψ`` we mentioned above. But really, what +we're doing is rotating our telescope in three-dimensional space, so +the pixel's value is a function on the *rotation group* +``\mathrm{SO}(3)``, not just the sphere ``𝕊²``. Now, with light, we +know every possible polarized value once we measure the value with one +polarization and the value with the orthogonal polarization, which are +combined into a single complex number. (Compare [Jones +vectors](https://en.wikipedia.org/wiki/Jones_calculus).) As we rotate +our choice of the first polarization by an angle ``ψ``, the complex +combination varies as ``e^{iψ}``. This is exactly what it means to +have spin weight ``s=1`` — which is, not coincidentally, the spin of a +photon. Imagining that we had a polarizing telescope measuring some +other kind of field with spin ``s``, the complex combination would +vary as ``e^{isψ}``. And since we could have half-integer spins, we +actually need to consider not just the rotation group +``\mathrm{SO}(3)``, but its double cover ``\mathrm{Spin}(3) \cong +\mathrm{SU}(2)``, to fully capture the behavior of general fields. + +So, at this point, we see that all three of the functions we care +about — standard spherical harmonics, spin-weighted spherical +harmonics, and Wigner's 𝔇 matrices — are most naturally thought of as +functions on ``\mathrm{Spin}(3)``. The traditional way to +parameterize this would be with (extended) Euler angles. However, as +mentioned above, Euler angles are a poor choice for many purposes. +First, we have the problem of coordinate singularities at the "poles" +(aka ["gimbal lock"](https://en.wikipedia.org/wiki/Gimbal_lock)). +This is not a problem if we use quaternions instead. Second, we have +the problem of composing rotations; given rotations described by Euler +angles ``(α, β, γ)`` and ``(α', β', γ')``, finding another set of +angles ``(α'', β'', γ'')`` representing their composition is a nasty, +nonlinear problem. With quaternions, composition is just quaternion +multiplication: ``R'' = R'\, R``. Third, and most importantly, we have +the problem of generators of rotations. With Euler angles, +infinitesimal rotations are described by complicated derivatives with +bizarre trigonometric factors. With quaternions, infinitesimal +rotations are described by "pure-vector" quaternions that can be +exponentiated almost as simply as imaginary numbers. That is, if +``𝐮`` is a unit quaternion with 0 scalar part, and ``θ`` is a real +number, then +```math +\exp \left[ 𝐮 \frac{θ}{2} \right] += \cos \frac{θ}{2} + 𝐮 \sin \frac{θ}{2} +``` +is a unit quaternion representing a rotation by ``θ`` about ``𝐮``. +Note the similarity to Euler's formula for complex exponentials. +Alternatively, going from a unit quaternion to its generator just uses +the logarithm, which is only slightly more complicated than the +complex logarithm. + +So the conclusion is clear: we should represent the arguments of all +these functions as unit quaternions, which is what this package does: +```math +\begin{gathered} +Y_{ℓ,m}(Q), \\ +{}_sY_{ℓ,m}(Q), \\ +𝔇^{(ℓ)}_{m', m}(Q). +\end{gathered} +``` +We can still provide convenience methods that convert from spherical +coordinates or Euler angles to quaternions, but internally everything +is done with quaternions, and the documentation will generally be +written in terms of quaternions. See [Boyle_2016](@citet) for full +details. diff --git a/docs/src/background/operators.md b/docs/src/background/operators.md new file mode 100644 index 00000000..3f18592d --- /dev/null +++ b/docs/src/background/operators.md @@ -0,0 +1,248 @@ +# [Differential operators](@id background_differential_operators) + +On [the previous page](@ref background_domain), we saw that the domain +on which our most important functions — spherical harmonics and Wigner +𝔇 matrices — are defined is most naturally regarded as the group of +unit quaternions, ``\mathrm{Spin}(3) \cong \mathrm{SU}(2)``. As a +result, we can define a variety of differential operators acting on +these functions, relating to infinitesimal motions in this group, +acting either from the left or the right on their arguments. Right or +left matters because this group is non-commutative. + +In general, the *left* Lie derivative of a function ``f(Q)`` over the +unit quaternions with respect to a generator of rotation ``g`` is +defined as +```math +L_g(f)\{Q\} := -\frac{i}{2} + \left. \frac{df\left(\exp(t\,g)\, Q\right)}{dt} \right|_{t=0}. +``` +Note that the exponential multiplies ``Q`` *on the left* — hence the +name. We will see below that this agrees with the usual definition of +the angular-momentum from physics, except that in *quantum* physics a +factor of ``\hbar`` is usually included. + +So, for example, a rotation about the ``z`` axis has the quaternion +``z`` as its generator of rotation, and ``L_z`` defined in this way +agrees with [the usual angular-momentum +operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) +``L_z`` familiar from spherical-harmonic theory, and reduces to it +when the function has spin weight 0, but also applies to functions of +general spin weight. Similarly, we can compute ``L_x`` and ``L_y``, +and take appropriate combinations to find [the usual raising and +lowering (ladder) +operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) +``L_+`` and ``L_-``. + +In just the same way, we can define the *right* Lie derivative of a +function ``f(Q)`` over the unit quaternions with respect to a +generator of rotation ``g`` as +```math +R_g(f)\{Q\} := -\frac{i}{2} + \left. \frac{df\left(Q\, \exp(t\,g)\right)}{dt} \right|_{t=0}. +``` +Note that the exponential multiplies ``Q`` *on the right* — hence the +name. + +This operator is less common in physics, because it represents the +dependence of the function on the choice of frame (or coordinate +system), which is not usually interesting. Multiplication on the left +represents a rotation of the physical system, while rotation on the +right represents a rotation of the coordinate system. However, this +dependence on coordinate system is precisely what defines the *spin +weight* of a function, so this class of operators is relevant in +discussions of spin-weighted spherical functions. In particular, +``R_z`` is the spin-weight operator — meaning that when it acts on a +spin-weighted spherical harmonic of spin weight ``s``, it returns +``s`` times that same function. Moreover, the operators ``R_\pm`` +correspond (up to a sign) to the spin-raising and -lowering operators +``\eth`` and ``\bar{\eth}`` originally introduced by +[Newman_1966](@citet), as explained in greater detail by +[Boyle_2016](@citet). + +Note that these definitions are *extremely* general, in that they can +be used for *any* Lie group, and for any complex-valued function on +that group. And in full generality, we have the useful properties of +linearity: +```math +L_{s𝐚} = sL_{𝐚} +\qquad \text{and} \qquad +R_{s𝐚} = sR_{𝐚}, +``` +and +```math +L_{𝐚+𝐛} = L_{𝐚} + L_{𝐛} +\qquad \text{and} \qquad +R_{𝐚+𝐛} = R_{𝐚} + R_{𝐛}, +``` +for any scalar ``s`` and any elements of the Lie algebra +``𝐚`` and ``𝐛``. In particular, if the Lie algebra +has a basis ``𝐞_{(j)}``, we use the shorthand ``L_j`` and +``R_j`` for ``L_{𝐞_{(j)}}`` and ``R_{𝐞_{(j)}}``, +respectively, and we can expand any operator in terms of these basis +operators: +```math +L_{𝐚} = \sum_{j} a_j L_j +\qquad \text{and} \qquad +R_{𝐚} = \sum_{j} a_j R_j. +``` + + +## Commutators + +In general, for generators ``a`` and ``b``, we have the commutator +relations +```math +\left[ L_a, L_b \right] = \frac{i}{2} L_{[a,b]} +\qquad +\left[ R_a, R_b \right] = -\frac{i}{2} R_{[a,b]}, +``` +where ``[a,b]`` is the commutator of the two generators, which can be +obtained directly as the commutator of the corresponding quaternions. +Note the sign difference between these two equations. The factors of +``\pm i/2`` are inherited directly from the definitions of ``L_g`` and +``R_g`` given above, but they appear there with the *same* sign. The +sign difference between these two commutator equations results from +the fact that the quaternions are multiplied in opposite orders in the +two cases. It *could* be absorbed by defining the operators with +opposite signs.[^1] The arbitrary sign choices used above are purely +for historical reasons. + +Again, these results are valid for general (finite-dimensional) Lie +groups, but a particularly interesting case is in application to the +three-dimensional rotation group. In the following, we will apply our +results to this group. + +The commutator relations for ``L`` are consistent — except for the +differing use of ``\hbar`` — with the usual relations from quantum +mechanics: +```math +\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. +``` +Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and +index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we +represent an arbitrary basis vector as ``\hat{e}_j``, then the +quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` +becomes +```math +[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. +``` +Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} +L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version +frequently seen in quantum physics. + + +[^1]: + In fact, we can define the left and right Lie derivative operators + quite generally, for functions on *any* Lie group and for the + corresponding Lie algebra. And in all cases (at least for + finite-dimensional Lie algebras) we obtain the same commutator + relations. The only potential difference is that it may not make + sense to use the coefficient ``i/2`` in general; it was chosen + here for consistency with the standard angular-momentum operators. + If that coefficient is changed in the definitions of the Lie + derivatives, the only change to the commutator relations would the + substitution of that coefficient. + +The raising and lowering operators relative to ``L_z`` and ``R_z`` +satisfy — by definition of raising and lowering operators — the +relations +```math +[L_z, L_\pm] = \pm L_\pm +\qquad +[R_z, R_\pm] = \pm R_\pm. +``` +These allow us to solve — up to an overall factor — for those +operators in terms of the basic generators (again, noting the sign +difference): +```math +L_\pm = L_x \pm i L_y +\qquad +R_\pm = R_x \mp i R_y. +``` +(Interestingly, this procedure also shows that rasing and lowering +operators can only exist if the factor in front of the derivatives in +the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In +particular, this results in the commutator relations +```math +[L_+, L_-] = 2L_z +\qquad +[R_+, R_-] = 2R_z. +``` +Here, the signs are *similar* because the two sign differences noted +above essentially cancel each other out. + +In the functions [listed below](#Module-functions), these operators +are returned as matrices acting on vectors of mode weights. As such, +we can actually evaluate these commutators as given to cross-validate +the expressions and those functions. + + +## Transformations of functions vs. mode weights + +One important point to note is that mode weights transform +"contravariantly" (very loosely speaking) relative to the +spin-weighted spherical functions under some operators. For example, +take the action of the ``L_+`` operator, which acts on a SWSH as +```math +L_+ \left\{{}_{s}Y_{ℓ,m}\right\} (R) = \sqrt{(ℓ-m)(ℓ+m+1)} {}_{s}Y_{ℓ,m+1}(R). +``` +We can use this to derive the mode weights of a general spin-weighted +function ``f`` under the action of this operator:[^2] +```math +\begin{aligned} +\left\{L_+ f\right\}_{ℓ,m} +&= +\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{L_+ \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \sum_{ℓ',m'} f_{ℓ',m'}\, \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\sqrt{(ℓ'-m')(ℓ'+m'+1)} {}_{s}Y_{ℓ',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \delta_{ℓ,ℓ'} \delta_{m,m'+1} \\ +&= +f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} +\end{aligned} +``` +Note that this expression (and in particular its signs) more resembles +the expression for ``L_- \left\{{}_{s}Y_{ℓ,m}\right\}`` than for +``L_+ \left\{{}_{s}Y_{ℓ,m}\right\}``. Similar relations hold for +the action of ``L_-``. + +[^2]: + A technical note about the integrals above: the integrals should be taken + over the appropriate space and with the appropriate weight such that the + SWSHs are orthonormal. In general, this integral should be over + ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be + either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when + integrated over an ``𝕊²`` subspace (nor even is the integral invariant). + However, if we know that the spins are the same in both cases, it *is* + possible to integrate over an ``𝕊²`` subspace. + +However, it is important to note that the same "contravariance" is not +present for the spin-raising and -lowering operators: +```math +\begin{aligned} +\left\{\eth f\right\}_{ℓ,m} +&= +\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{\eth \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\eth {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \delta_{ℓ,ℓ'} \delta_{m,m'} \\ +&= +f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} +\end{aligned} +``` +Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more +"covariant" form of transformation. + diff --git a/docs/src/background/sYlm.md b/docs/src/background/sYlm.md new file mode 100644 index 00000000..8503face --- /dev/null +++ b/docs/src/background/sYlm.md @@ -0,0 +1,9 @@ +# [``{}_{s}Y_{ℓ,m}`` functions](@id background_sYlm) + +The [previous page](@ref background_differential_operators) introduced +the left and right Lie derivative operators acting on functions of a +quaternion argument. The familiar way of arriving at the standard +(scalar) spherical harmonics is to consider functions that are +eigenfunctions of the *left* Lie derivative operators. + + diff --git a/docs/src/background/transformations.md b/docs/src/background/transformations.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/background/wigner_matrices.md b/docs/src/background/wigner_matrices.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/interface/operators.md b/docs/src/interface/operators.md index 715353c8..c8e6f4e1 100644 --- a/docs/src/interface/operators.md +++ b/docs/src/interface/operators.md @@ -1,249 +1,4 @@ -# Differential operators - -Spin-weighted spherical functions *cannot* be defined on the sphere -``𝕊²``, but are well defined on the sphere ``𝕊³`` or its projective -version ``ℝℙ³``. See [Boyle_2016](@citet) for the explanation. These -are the spaces underlying the Lie groups ``\mathrm{Spin}(3) \cong -\mathrm{SU}(2)`` or its projective version ``\mathrm{SO}(3)``. As a -result, we can define a variety of differential operators acting on -these functions, relating to infinitesimal motions in these groups, -acting either from the left or the right on their arguments. Right or -left matters because the groups mentioned above are all -non-commutative groups. - -In general, the *left* Lie derivative of a function ``f(Q)`` over the -unit quaternions with respect to a generator of rotation ``g`` is -defined as -```math -L_g(f)\{Q\} := -\frac{i}{2} - \left. \frac{df\left(\exp(t\,g)\, Q\right)}{dt} \right|_{t=0}. -``` -Note that the exponential multiplies ``Q`` *on the left* — hence the -name. We will see below that this agrees with the usual definition of -the angular-momentum from physics, except that in *quantum* physics a -factor of ``\hbar`` is usually included. - -So, for example, a rotation about the ``z`` axis has the quaternion -``z`` as its generator of rotation, and ``L_z`` defined in this way -agrees with [the usual angular-momentum -operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) -``L_z`` familiar from spherical-harmonic theory, and reduces to it -when the function has spin weight 0, but also applies to functions of -general spin weight. Similarly, we can compute ``L_x`` and ``L_y``, -and take appropriate combinations to find [the usual raising and -lowering (ladder) -operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) -``L_+`` and ``L_-``. - -In just the same way, we can define the *right* Lie derivative of a -function ``f(Q)`` over the unit quaternions with respect to a -generator of rotation ``g`` as -```math -R_g(f)\{Q\} := -\frac{i}{2} - \left. \frac{df\left(Q\, \exp(t\,g)\right)}{dt} \right|_{t=0}. -``` -Note that the exponential multiplies ``Q`` *on the right* — hence the -name. - -This operator is less common in physics, because it represents the -dependence of the function on the choice of frame (or coordinate -system), which is not usually of interest. Multiplication on the left -represents a rotation of the physical system, while rotation on the -right represents a rotation of the coordinate system. However, this -dependence on coordinate system is precisely what defines the *spin -weight* of a function, so this class of operators is relevant in -discussions of spin-weighted spherical functions. In particular, the -operators ``R_\pm`` correspond (up to a sign) to the spin-raising and --lowering operators ``\eth`` and ``\bar{\eth}`` originally introduced -by [Newman_1966](@citet), as explained in greater detail by -[Boyle_2016](@citet). - -Note that these definitions are *extremely* general, in that they can -be used for *any* Lie group, and for any complex-valued function on -that group. And in full generality, we have the useful properties of -linearity: -```math -L_{s\mathbf{a}} = sL_{\mathbf{a}} -\qquad \text{and} \qquad -R_{s\mathbf{a}} = sR_{\mathbf{a}}, -``` -and -```math -L_{\mathbf{a}+\mathbf{b}} = L_{\mathbf{a}} + L_{\mathbf{b}} -\qquad \text{and} \qquad -R_{\mathbf{a}+\mathbf{b}} = R_{\mathbf{a}} + R_{\mathbf{b}}, -``` -for any scalar ``s`` and any elements of the Lie algebra -``\mathbf{a}`` and ``\mathbf{b}``. In particular, if the Lie algebra -has a basis ``\mathbf{e}_{(j)}``, we use the shorthand ``L_j`` and -``R_j`` for ``L_{\mathbf{e}_{(j)}}`` and ``R_{\mathbf{e}_{(j)}}``, -respectively, and we can expand any operator in terms of these basis -operators: -```math -L_{\mathbf{a}} = \sum_{j} a_j L_j -\qquad \text{and} \qquad -R_{\mathbf{a}} = \sum_{j} a_j R_j. -``` - - -## Commutators - -In general, for generators ``a`` and ``b``, we have the commutator -relations -```math -\left[ L_a, L_b \right] = \frac{i}{2} L_{[a,b]} -\qquad -\left[ R_a, R_b \right] = -\frac{i}{2} R_{[a,b]}, -``` -where ``[a,b]`` is the commutator of the two generators, which can be -obtained directly as the commutator of the corresponding quaternions. -Note the sign difference between these two equations. The factors of -``\pm i/2`` are inherited directly from the definitions of ``L_g`` and -``R_g`` given above, but they appear there with the *same* sign. The -sign difference between these two commutator equations results from -the fact that the quaternions are multiplied in opposite orders in the -two cases. It *could* be absorbed by defining the operators with -opposite signs.[^1] The arbitrary sign choices used above are purely -for historical reasons. - -Again, these results are valid for general (finite-dimensional) Lie -groups, but a particularly interesting case is in application to the -three-dimensional rotation group. In the following, we will apply our -results to this group. - -The commutator relations for ``L`` are consistent — except for the -differing use of ``\hbar`` — with the usual relations from quantum -mechanics: -```math -\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. -``` -Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and -index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we -represent an arbitrary basis vector as ``\hat{e}_j``, then the -quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` -becomes -```math -[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. -``` -Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} -L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version -frequently seen in quantum physics. - - -[^1]: - In fact, we can define the left and right Lie derivative operators - quite generally, for functions on *any* Lie group and for the - corresponding Lie algebra. And in all cases (at least for - finite-dimensional Lie algebras) we obtain the same commutator - relations. The only potential difference is that it may not make - sense to use the coefficient ``i/2`` in general; it was chosen - here for consistency with the standard angular-momentum operators. - If that coefficient is changed in the definitions of the Lie - derivatives, the only change to the commutator relations would the - substitution of that coefficient. - -The raising and lowering operators relative to ``L_z`` and ``R_z`` -satisfy — by definition of raising and lowering operators — the -relations -```math -[L_z, L_\pm] = \pm L_\pm -\qquad -[R_z, R_\pm] = \pm R_\pm. -``` -These allow us to solve — up to an overall factor — for those -operators in terms of the basic generators (again, noting the sign -difference): -```math -L_\pm = L_x \pm i L_y -\qquad -R_\pm = R_x \mp i R_y. -``` -(Interestingly, this procedure also shows that rasing and lowering -operators can only exist if the factor in front of the derivatives in -the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In -particular, this results in the commutator relations -```math -[L_+, L_-] = 2L_z -\qquad -[R_+, R_-] = 2R_z. -``` -Here, the signs are *similar* because the two sign differences noted -above essentially cancel each other out. - -In the functions [listed below](#Module-functions), these operators -are returned as matrices acting on vectors of mode weights. As such, -we can actually evaluate these commutators as given to cross-validate -the expressions and those functions. - - -## Transformations of functions vs. mode weights - -One important point to note is that mode weights transform -"contravariantly" (very loosely speaking) relative to the -spin-weighted spherical functions under some operators. For example, -take the action of the ``L_+`` operator, which acts on a SWSH as -```math -L_+ \left\{{}_{s}Y_{\ell,m}\right\} (R) = \sqrt{(\ell-m)(\ell+m+1)} {}_{s}Y_{\ell,m+1}(R). -``` -We can use this to derive the mode weights of a general spin-weighted -function ``f`` under the action of this operator:[^2] -```math -\begin{aligned} -\left\{L_+ f\right\}_{\ell,m} -&= -\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\int \left\{L_+ \sum_{\ell',m'}f_{\ell',m'}\, {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\int \sum_{\ell',m'} f_{\ell',m'}\, \left\{L_+ {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \int \left\{L_+ {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \int \left\{\sqrt{(\ell'-m')(\ell'+m'+1)} {}_{s}Y_{\ell',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-m')(\ell'+m'+1)} \int {}_{s}Y_{\ell',m'+1}(R)\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-m')(\ell'+m'+1)} \delta_{\ell,\ell'} \delta_{m,m'+1} \\ -&= -f_{\ell,m-1}\, \sqrt{(\ell-m+1)(\ell+m)} -\end{aligned} -``` -Note that this expression (and in particular its signs) more resembles -the expression for ``L_- \left\{{}_{s}Y_{\ell,m}\right\}`` than for -``L_+ \left\{{}_{s}Y_{\ell,m}\right\}``. Similar relations hold for -the action of ``L_-``. - -[^2]: - A technical note about the integrals above: the integrals should be taken - over the appropriate space and with the appropriate weight such that the - SWSHs are orthonormal. In general, this integral should be over - ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be - either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when - integrated over an ``𝕊²`` subspace (nor even is the integral invariant). - However, if we know that the spins are the same in both cases, it *is* - possible to integrate over an ``𝕊²`` subspace. - -However, it is important to note that the same "contravariance" is not -present for the spin-raising and -lowering operators: -```math -\begin{aligned} -\left\{\eth f\right\}_{\ell,m} -&= -\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\int \left\{\eth \sum_{\ell',m'}f_{\ell',m'}\, {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \int \left\{\eth {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-s)(\ell'+s+1)} \int {}_{s+1}Y_{\ell',m'}(R)\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ -&= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-s)(\ell'+s+1)} \delta_{\ell,\ell'} \delta_{m,m'} \\ -&= -f_{\ell,m}\, \sqrt{(\ell-s)(\ell+s+1)} -\end{aligned} -``` -Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more -"covariant" form of transformation. +# [Differential operators](@id interface_differential_operators) ## Docstrings diff --git a/docs/src/interface/sYlm.md b/docs/src/interface/sYlm.md index 29aec86d..c1b2a91e 100644 --- a/docs/src/interface/sYlm.md +++ b/docs/src/interface/sYlm.md @@ -1,28 +1,30 @@ -# ``{}_{s}Y_{\ell,m}`` functions +# [``{}_{s}Y_{ℓ,m}`` functions](@id interface_sYlm) ```@meta CurrentModule = SphericalFunctions.Deprecated ``` -The spin-weighted spherical harmonics are an [important set of functions defined -on](@cite Boyle_2016) the rotation group ``𝐒𝐎(3)``, or more generally, the -spin group ``𝐒𝐩𝐢𝐧(3)`` that covers it. They are eigenfunctions of [the -left- and right-Lie derivatives](@ref "Differential operators"), and are -particularly useful in describing the angular dependence of polarized fields, -like the electromagnetic field and gravitational-wave field. Originally -introduced by [Newman_1966](@citet), they are essentially components of Wigner's -``\frak{D}`` matrices: +The spin-weighted spherical harmonics are an [important set of +functions defined on](@cite Boyle_2016) the rotation group +``𝐒𝐎(3)``, or more generally, the spin group ``𝐒𝐩𝐢𝐧(3)`` that +covers it. They are eigenfunctions of [the left- and right-Lie +derivatives](@ref "Differential operators"), and are particularly +useful in describing the angular dependence of polarized fields, like +the electromagnetic field and gravitational-wave field. Originally +introduced by [Newman_1966](@citet), they are essentially components +of Wigner's ``𝔇`` matrices: ```math -{}_{s}Y_{\ell,m}(\mathbf{R}) - = (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \, \frak{D}^{(\ell)}_{m, -s}(\mathbf{R}). +{}_{s}Y_{ℓ,m}(𝐑) + = (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} \, 𝔇^{(ℓ)}_{m, -s}(𝐑). ``` -As such, they can be computed using the same [``H`` recursion](@ref "Algorithm -for computing ``H``") algorithm as the Wigner ``\frak{D}^{(\ell)}_{m, -s}`` -matrices. But because not all values of ``s \in -\ell:\ell`` are used, we can -be much more efficient in both storage and computation time. +As such, they can be computed using the same [``H`` recursion](@ref +"Algorithm for computing ``H``") algorithm as the Wigner +``𝔇^{(ℓ)}_{m, -s}`` matrices. But because not all values of ``s \in +-ℓ:ℓ`` are used, we can be much more efficient in both storage and +computation time. -The user interface is very similar to the one for [Wigner's ``𝔇`` and ``d`` -matrices](@ref): +The user interface is very similar to the one for [Wigner's ``𝔇`` and +``d`` matrices](@ref): ```julia using Quaternionic using SphericalFunctions @@ -33,22 +35,25 @@ R = randn(RotorF64) s = -2 Y = sYlm_values(R, ℓₘₐₓ, s) ``` -Again, the results can take up a lot of memory, so for maximum efficiency when -calling this function repeatedly with different `R` values, it is best to -pre-allocate the necessary memory with the [`sYlm_prep`](@ref) function, and the -pass that in as an argument to [`sYlm_values!`](@ref): +Again, the results can take up a lot of memory, so for maximum +efficiency when calling this function repeatedly with different `R` +values, it is best to pre-allocate the necessary memory with the +[`sYlm_prep`](@ref) function, and the pass that in as an argument to +[`sYlm_values!`](@ref): ```julia Y_storage = sYlm_prep(ℓₘₐₓ, s) Y = sYlm_values!(Y_storage, R, s) ``` -(Beware that, as noted in the documentation for [`sYlm_values!`](@ref), the -output `Y` is just a reference to part of the `Y_storage` object, so you should -not reuse `Y_storage` until you have copied or otherwise finished using `Y`.) +(Beware that, as noted in the documentation for +[`sYlm_values!`](@ref), the output `Y` is just a reference to part of +the `Y_storage` object, so you should not reuse `Y_storage` until you +have copied or otherwise finished using `Y`.) -The output `Y` is a single vector of `Complex` numbers with the same base type -as `R`. The ordering of the elements is described in the documentation for -[`sYlm_values!`](@ref). It is also possible to efficiently view slices of this -vector as a series of individual vectors using a [`sYlm_iterator`](@ref): +The output `Y` is a single vector of `Complex` numbers with the same +base type as `R`. The ordering of the elements is described in the +documentation for [`sYlm_values!`](@ref). It is also possible to +efficiently view slices of this vector as a series of individual +vectors using a [`sYlm_iterator`](@ref): ```julia for (ℓ, Yˡ) in zip(0:ℓₘₐₓ, sYlm_iterator(Y, ℓₘₐₓ)) # Do something with the matrix Yˡ[ℓ+m′+1, ℓ+m+1] diff --git a/docs/src/interface/transformations.md b/docs/src/interface/transformations.md index 0f298da2..8856a19b 100644 --- a/docs/src/interface/transformations.md +++ b/docs/src/interface/transformations.md @@ -117,16 +117,16 @@ discrete quadrature to evaluate the integrals. To describe the mode weights of a spin-``s`` function up to (and -including) some maximum angular resolution ``\ell_\mathrm{max}``, -there are ``(\ell_\mathrm{max}+1)^2 - s^2`` mode weights. We assume +including) some maximum angular resolution ``ℓ_\mathrm{max}``, +there are ``(ℓ_\mathrm{max}+1)^2 - s^2`` mode weights. We assume throughout that the values `f̃` are stored as the (column) vector ```julia f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] ``` (Here, `mode_weight` is a made-up function for schematic purposes.) In -particular, the ``m`` index varies most rapidly, and the ``\ell`` +particular, the ``m`` index varies most rapidly, and the ``ℓ`` index varies most slowly. Correspondingly, there must be *at least* -``(\ell_\mathrm{max}+1)^2 - s^2`` function values `f`. However, some +``(ℓ_\mathrm{max}+1)^2 - s^2`` function values `f`. However, some ``s``-SHT algorithms require more function values — usually by a factor of 2 or 4 — trading off between speed and memory usage. @@ -146,16 +146,16 @@ Currently, there are three algorithms implemented, each having different advantages and disadvantages: 1. The "Direct" algorithm (introduced here for the first time), which is the - default, but should only be used up to ``\ell_\mathrm{max} \lesssim 50`` + default, but should only be used up to ``ℓ_\mathrm{max} \lesssim 50`` because its intermediate storage requirements scale as - ``\ell_\mathrm{max}^4``. This algorithm is the fastest for small - ``\ell_\mathrm{max}``, it can be used with arbitrary (non-degenerate) + ``ℓ_\mathrm{max}^4``. This algorithm is the fastest for small + ``ℓ_\mathrm{max}``, it can be used with arbitrary (non-degenerate) pixelizations, and achieves optimal dimensionality. 2. The "Minimal" algorithm due to [Elahi_2018](@citet), with some minor improvements. This algorithm is fast and — as the name implies — also achieves optimal dimensionality, and its storage scales as - ``\ell_\mathrm{max}^3``. However, its pixelization is restricted, and its - accuracy at very high ``\ell_\mathrm{max}`` is not as good as the "RS" + ``ℓ_\mathrm{max}^3``. However, its pixelization is restricted, and its + accuracy at very high ``ℓ_\mathrm{max}`` is not as good as the "RS" algorithm. The algorithm itself is not actually fully specified by Elahi et al., and leaves out some relatively simple improvements, so I have had to take some liberties with my interpretation. @@ -164,7 +164,7 @@ advantages and disadvantages: [`ducc.sht`](https://gitlab.mpcdf.mpg.de/mtr/ducc#duccsht) packages. It requires pixelizations on "iso-latitude rings", and does not achieve optimal dimensionality. However, it is very fast, and its accuracy is - excellent at extremely high ``\ell_\mathrm{max}``. + excellent at extremely high ``ℓ_\mathrm{max}``. ## `SSHT` objects diff --git a/docs/src/interface/utilities.md b/docs/src/interface/utilities.md index c1e3e2b7..cab03f0f 100644 --- a/docs/src/interface/utilities.md +++ b/docs/src/interface/utilities.md @@ -20,8 +20,8 @@ Order = [:module, :type, :constant, :function, :macro] ## Sizes of and indexing into ``𝔇``, ``d``, and ``Y`` data -By ``Y`` data, we mean anything indexed like ``Y_{\ell, m}`` modes; by ``D`` -data, we mean anything indexed like Wigner's ``\mathfrak{D}`` matrices, or +By ``Y`` data, we mean anything indexed like ``Y_{ℓ, m}`` modes; by ``D`` +data, we mean anything indexed like Wigner's ``𝔇`` matrices, or special subsets of them, like the ``H`` matrices. ```@autodocs From 43628a512a284ef141f88ca4f1108d709e26f654 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 7 Jan 2026 15:36:26 -0500 Subject: [PATCH 322/329] Simplify some latex --- .../calculations/euler_angular_momentum.jl | 26 +- .../conventions/comparisons/blanchet_2024.jl | 20 +- .../comparisons/cohen_tannoudji_1991.jl | 8 +- .../comparisons/condon_shortley_1935.jl | 20 +- docs/make.jl | 6 + docs/src/api/internal.md | 4 +- docs/src/conventions/comparisons.md | 126 ++++---- docs/src/conventions/details.md | 300 +++++++++--------- docs/src/conventions/outline.md | 98 +++--- docs/src/conventions/summary.md | 30 +- docs/src/index.md | 2 +- docs/src/index_of_docstrings.md | 8 + docs/src/notes/H_recurrence.md | 28 +- docs/src/notes/H_recursions.md | 8 +- docs/src/notes/sampling_theorems.md | 48 +-- docs/src/operators.md | 254 +++++++++++++++ src/deprecated/evaluate.jl | 54 ++-- src/deprecated/iterators.jl | 10 +- src/deprecated/map2salm.jl | 4 +- src/ssht/huffenberger_wandelt.jl | 50 +-- src/utilities/operators.jl | 34 +- src/utilities/pixelizations.jl | 10 +- src/wigner/recurrence.jl | 12 +- test/conventions/NIST_DLMF.jl | 6 +- test/conventions/edmonds.jl | 2 +- test/conventions/goldbergetal.jl | 2 +- test/conventions/sakurai.jl | 16 +- test/conventions/thorne.jl | 2 +- test/conventions/wigner.jl | 4 +- test/deprecated/wigner_matrices/H.jl | 4 +- 30 files changed, 732 insertions(+), 464 deletions(-) create mode 100644 docs/src/index_of_docstrings.md create mode 100644 docs/src/operators.md diff --git a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl index 9670689f..9f394f68 100644 --- a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl +++ b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl @@ -38,13 +38,13 @@ important results that help make contact with more standard expressions: We start by defining a new set of Euler angles according to ```math -\mathbf{R}_{\alpha', \beta', \gamma'} -= e^{-\epsilon \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} +𝐑_{\alpha', \beta', \gamma'} += e^{-\epsilon 𝐮 / 2} 𝐑_{\alpha, \beta, \gamma} \qquad \text{or} \qquad -\mathbf{R}_{\alpha', \beta', \gamma'} -= \mathbf{R}_{\alpha, \beta, \gamma} e^{-\epsilon \mathbf{u} / 2} +𝐑_{\alpha', \beta', \gamma'} += 𝐑_{\alpha, \beta, \gamma} e^{-\epsilon 𝐮 / 2} ``` -where ``\mathbf{u}`` will be each of the basis quaternions, and each of ``\alpha'``, +where ``𝐮`` will be each of the basis quaternions, and each of ``\alpha'``, ``\beta'``, and ``\gamma'`` is a function of ``\alpha``, ``\beta``, ``\gamma``, and ``\epsilon``. Then, we note that the chain rule tells us that ```math @@ -59,10 +59,10 @@ terms of ``\partial_\epsilon`` into an expression in terms of derivatives with r these new Euler angles: ```math \begin{align} - L_j f(\mathbf{R}_{\alpha, \beta, \gamma}) + L_j f(𝐑_{\alpha, \beta, \gamma}) &= - \left. i \frac{\partial} {\partial \epsilon} f \left( e^{-\epsilon \mathbf{e}_j / 2} - \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\epsilon=0} + \left. i \frac{\partial} {\partial \epsilon} f \left( e^{-\epsilon 𝐞_j / 2} + 𝐑_{\alpha, \beta, \gamma} \right) \right|_{\epsilon=0} \\ &= i \left[ \left( @@ -82,10 +82,10 @@ these new Euler angles: or for ``R_j``: ```math \begin{align} - R_j f(\mathbf{R}_{\alpha, \beta, \gamma}) + R_j f(𝐑_{\alpha, \beta, \gamma}) &= - -\left. i \frac{\partial} {\partial \epsilon} f \left( \mathbf{R}_{\alpha, \beta, \gamma} - e^{-\epsilon \mathbf{e}_j / 2} \right) \right|_{\epsilon=0} + -\left. i \frac{\partial} {\partial \epsilon} f \left( 𝐑_{\alpha, \beta, \gamma} + e^{-\epsilon 𝐞_j / 2} \right) \right|_{\epsilon=0} \\ &= -i \left[ \left( @@ -98,7 +98,7 @@ or for ``R_j``: So the objective is to find the new Euler angles, differentiate with respect to ``\epsilon``, and then evaluate at ``\epsilon = 0``. We do this by first multiplying -``\mathbf{R}_{\alpha, \beta, \gamma}`` and ``e^{-\epsilon \mathbf{u} / 2}`` in the desired +``𝐑_{\alpha, \beta, \gamma}`` and ``e^{-\epsilon 𝐮 / 2}`` in the desired order, then expanding the results in terms of its quaternion components, and then computing the new Euler angles in terms of those components according to the usual expression. @@ -281,7 +281,7 @@ nothing #hide # top, [Varshalovich_1988](@citet) provide equivalent expressions in Eqs. (6) and (7) of # their Sec. 4.2 — except that ``R_x`` and ``R_z`` have the wrong signs. # [Wikipedia](https://en.wikipedia.org/wiki/Wigner_D-matrix#Properties_of_the_Wigner_D-matrix), -# meanwhile, provides equivalent expressions, except that their ``\hat{\mathcal{P}}`` has +# meanwhile, provides equivalent expressions, except that their ``\hat{𝒫}`` has # (consistently) the opposite sign to ``R`` defined here. # Note that the Wikipedia convention is actually entirely sensible — maybe more sensible diff --git a/docs/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl index cebbdcac..4b24f4e3 100644 --- a/docs/literate_input/conventions/comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -35,7 +35,7 @@ import ..ConventionsUtilities: 𝒾, ❗ # The ``s=-2`` spin-weighted spherical harmonics are defined in Eq. (184a) as # ```math -# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4\pi}} d^{\ell m}(\theta) e^{im\phi}. +# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4\pi}} d^{ℓ m}(\theta) e^{im\phi}. # ``` function Yˡᵐ₋₂(l, m, θ::T, ϕ::T) where {T<:Real} √((2l + 1) / (4T(π))) * d(l, m, θ) * exp(𝒾 * m * ϕ) @@ -44,12 +44,12 @@ end # Immediately following that, in Eq. (184b), we find the definition of the ``d`` function: # ```math -# d^{\ell m} +# d^{ℓ m} # = # \sum_{k = k_1}^{k_2} # \frac{(-)^k}{k!} -# e_k^{\ell m} -# \left(\cos\frac{\theta}{2}\right)^{2\ell+m-2k-2} +# e_k^{ℓ m} +# \left(\cos\frac{\theta}{2}\right)^{2ℓ+m-2k-2} # \left(\sin\frac{\theta}{2}\right)^{2k-m+2}, # ``` # with ``k_1 = \textrm{max}(0, m-2)`` and ``k_2=\textrm{min}(l+m, l-2)``. @@ -66,12 +66,12 @@ function d(l, m, θ::T) where {T<:Real} end #+ -# The ``e_k^{\ell m}`` symbol is defined in Eq. (184c) as +# The ``e_k^{ℓ m}`` symbol is defined in Eq. (184c) as # ```math -# e_k^{\ell m} = \frac{ -# \sqrt{(\ell+m)!(\ell-m)!(\ell+2)!(\ell-2)!} +# e_k^{ℓ m} = \frac{ +# \sqrt{(ℓ+m)!(ℓ-m)!(ℓ+2)!(ℓ-2)!} # }{ -# (k-m+2)!(\ell+m-k)!(\ell-k-2)! +# (k-m+2)!(ℓ+m-k)!(ℓ-k-2)! # }. # ``` function eₖˡᵐ(k, l, m) @@ -95,7 +95,7 @@ end # module Blanchet ℓₘₐₓ = 8 #+ -# because that's the maximum ``\ell`` used for PN results — and that's roughly the limit to +# because that's the maximum ``ℓ`` used for PN results — and that's roughly the limit to # which I'd trust these expressions anyway. We will also only test the s = -2 #+ @@ -108,7 +108,7 @@ s = -2 #+ # This loose relative tolerance is necessary because the numerical errors in Blanchet's -# explicit expressions grow rapidly with ``\ell``. +# explicit expressions grow rapidly with ``ℓ``. for (θ, ϕ) ∈ θϕrange() for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ diff --git a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl index 68972289..df639823 100644 --- a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl @@ -30,14 +30,14 @@ L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. In Complement ``\mathrm{B}_{\mathrm{VI}}`` they define a rotation operator ``R`` as acting on a state such that [Eq. (21)] ```math -\langle \mathbf{r} | R | \psi \rangle +\langle 𝐫 | R | \psi \rangle = -\langle \mathscr{R}^{-1} \mathbf{r} | \psi \rangle. +\langle \mathscr{R}^{-1} 𝐫 | \psi \rangle. ``` -For an infinitesimal rotation through angle ``d\alpha`` about the axis ``\mathbf{u}``, he +For an infinitesimal rotation through angle ``d\alpha`` about the axis ``𝐮``, he shows [Eq. (49)] ```math -R_{\mathbf{u}}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha \mathbf{L}.\mathbf{u}. +R_{𝐮}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha 𝐋.𝐮. ``` diff --git a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl index cd64c796..4e11bb3e 100644 --- a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl @@ -78,15 +78,15 @@ import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ # Equation (12) of section 4³ (page 51) writes the solution to the three-dimensional Laplace # equation in spherical coordinates as # ```math -# \psi(\gamma, \ell, m_\ell) +# \psi(\gamma, ℓ, m_ℓ) # = -# B(\gamma, \ell) \Theta(\ell, m_\ell) \Phi(m_\ell), +# B(\gamma, ℓ) \Theta(ℓ, m_ℓ) \Phi(m_ℓ), # ``` # where ``B`` is independent of ``\theta`` and ``\varphi``, and ``\gamma`` represents any # number of eigenvalues required to specify the state. More explicitly, below Eq. (5) of # section 5⁵ (page 127), they specifically define the spherical harmonics as # ```math -# \phi(\ell, m_\ell) = \Theta(\ell, m_\ell) \Phi(m_\ell). +# \phi(ℓ, m_ℓ) = \Theta(ℓ, m_ℓ) \Phi(m_ℓ). # ``` # One quirk of their notation is that the dependence on ``\theta`` and ``\varphi`` is # implicit in their functions; we make it explicit, as Julia requires: @@ -97,9 +97,9 @@ end # The ``\varphi`` part is given by equation (5) of section 4³ (page 50): # ```math -# \Phi(m_\ell) +# \Phi(m_ℓ) # = -# \frac{1}{\sqrt{2\pi}} e^{i m_\ell \varphi}. +# \frac{1}{\sqrt{2\pi}} e^{i m_ℓ \varphi}. # ``` # Again, we make the dependence on ``\varphi`` explicit, and we capture its type to ensure # that we don't lose precision when converting π to a floating-point number. @@ -110,13 +110,13 @@ end # Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence as # ```math -# \Theta(\ell, m) +# \Theta(ℓ, m) # = -# (-1)^\ell -# \sqrt{\frac{(2\ell+1)}{2} \frac{(\ell+m)!}{(\ell-m)!}} -# \frac{1}{2^\ell \ell!} +# (-1)^ℓ +# \sqrt{\frac{(2ℓ+1)}{2} \frac{(ℓ+m)!}{(ℓ-m)!}} +# \frac{1}{2^ℓ ℓ!} # \frac{1}{\sin^m \theta} -# \frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. +# \frac{d^{ℓ-m}}{d(\cos\theta)^{ℓ-m}} \sin^{2ℓ}\theta. # ``` # Again, we make the dependence on ``\theta`` explicit, and we capture its type to ensure # that we don't lose precision when converting the factorials to a floating-point number. diff --git a/docs/make.jl b/docs/make.jl index ee5cc5f8..718c627f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -50,6 +50,11 @@ makedocs( pages = [ "index.md", "Background" => [ + "background/domain.md", + "background/operators.md", + "background/wigner_matrices.md", + "background/sYlm.md", + "background/transformations.md", ], "Interface" => [ "interface/wigner_matrices.md", @@ -91,6 +96,7 @@ makedocs( "Deprecated" => [ "deprecated/index.md", ], + "index_of_docstrings.md", "References" => "references.md", ], warnonly=true, diff --git a/docs/src/api/internal.md b/docs/src/api/internal.md index dce3de84..24f02b6d 100644 --- a/docs/src/api/internal.md +++ b/docs/src/api/internal.md @@ -7,7 +7,7 @@ to be deprecated in the near future. These are documented here for completeness The fundamental algorithm is the ``H`` recursion, which is the core computation needed for Wigner's ``d`` and ``𝔇`` matrices, and the spin-weighted spherical -harmonics ``{}_{s}Y_{\ell,m}``, as well as `map2salm` functions. +harmonics ``{}_{s}Y_{ℓ,m}``, as well as `map2salm` functions. ```@autodocs Modules = [SphericalFunctions.Deprecated] @@ -22,7 +22,7 @@ Modules = [SphericalFunctions.Deprecated] Pages = ["deprecated/associated_legendre.jl"] ``` -The function ``{}_{s}\lambda_{\ell,m}`` is defined as essentially ``{}_{s}Y_{\ell,0}``, and is important internally for computing the ALFs. We have some important utilities for computing it: +The function ``{}_{s}\lambda_{ℓ,m}`` is defined as essentially ``{}_{s}Y_{ℓ,0}``, and is important internally for computing the ALFs. We have some important utilities for computing it: ```@docs SphericalFunctions.Deprecated.λ_recursion_initialize diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 847c6ff9..3d910395 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -25,7 +25,7 @@ actually used by any of these sources: * Spin-weighted spherical harmonics - Behavior under rotation * Wigner D-matrices - - Representation à la $\langle \ell, m' | e^{-i \alpha J_z} e^{-i \beta J_y} e^{-i \gamma J_z} | \ell, m \rangle$ + - Representation à la $\langle ℓ, m' | e^{-i \alpha J_z} e^{-i \beta J_y} e^{-i \gamma J_z} | ℓ, m \rangle$ - Rotation of spherical harmonics - Order of indices - Conjugation @@ -144,7 +144,7 @@ agrees with our results, with the extra factor of ``\hbar``.) Unfortunately, there is disagreement over the definition of the Wigner D-matrices. In Eq. (4.1.12) he defines ```math -\mathcal{D}_{\alpha \beta \gamma} = +𝒟_{\alpha \beta \gamma} = \exp\big( \frac{i\alpha}{\hbar} J_z\big) \exp\big( \frac{i\beta}{\hbar} J_y\big) \exp\big( \frac{i\gamma}{\hbar} J_z\big), @@ -163,15 +163,15 @@ If we relate two vectors by a rotation matrix as ``x'^k = R^{kl} x^l``, then Goldberg et al. define ``D`` by its action on spherical harmonics [Eq. (3.3)]: ```math -Y_{\ell,m}(x') = \sum_{m'} Y_{\ell,m'}(x) D^{\ell}_{m',m}\left( R^{-1} \right). +Y_{ℓ,m}(x') = \sum_{m'} Y_{ℓ,m'}(x) D^{ℓ}_{m',m}\left( R^{-1} \right). ``` They then define the Euler angles as we do, and write [Eq. (3.4)] ```math -D^{\ell}_{m', m}(\alpha, \beta, \gamma) +D^{ℓ}_{m', m}(\alpha, \beta, \gamma) \equiv -D^{\ell}_{m', m}\left( R(\alpha \beta \gamma)^{-1} \right) +D^{ℓ}_{m', m}\left( R(\alpha \beta \gamma)^{-1} \right) = -e^{i m' \gamma} d^{\ell}_{m', m}(\beta) e^{i m \alpha}. +e^{i m' \gamma} d^{ℓ}_{m', m}(\beta) e^{i m \alpha}. ``` Finally, they derive [Eq. (3.9)] ```math @@ -188,18 +188,18 @@ e^{im'\gamma}. Equation (3.11) naturally extends to ```math - {}_sY_{\ell, m}(\theta, \phi, \gamma) + {}_sY_{ℓ, m}(\theta, \phi, \gamma) = - \left[ \left(2\ell+1\right) / 4\pi \right]^{1/2} - D^{\ell}_{-s,m}(\phi, \theta, \gamma), + \left[ \left(2ℓ+1\right) / 4\pi \right]^{1/2} + D^{ℓ}_{-s,m}(\phi, \theta, \gamma), ``` -where Eq. (3.4) also shows that ``D^{\ell}_{m', m}(\alpha, \beta, -\gamma) = D^{\ell}_{m', m}(\alpha, \beta, 0) e^{i m' \gamma}``, +where Eq. (3.4) also shows that ``D^{ℓ}_{m', m}(\alpha, \beta, +\gamma) = D^{ℓ}_{m', m}(\alpha, \beta, 0) e^{i m' \gamma}``, so we have ```math - {}_sY_{\ell, m}(\theta, \phi, \gamma) + {}_sY_{ℓ, m}(\theta, \phi, \gamma) = - {}_sY_{\ell, m}(\theta, \phi)\, e^{-i s \gamma}. + {}_sY_{ℓ, m}(\theta, \phi)\, e^{-i s \gamma}. ``` This is the most natural extension of the standard spin-weighted spherical harmonics to ``\mathrm{Spin}(3)``. In particular, the @@ -216,23 +216,23 @@ physics programs, so it would be useful to compare. Equation (4.27) gives the associated Legendre function as ```math -P_{\ell}^{m}(x) +P_{ℓ}^{m}(x) = -(1-x^2)^{|m|/2} \left(\frac{d}{dx}\right)^{|m|} P_{\ell}(x), +(1-x^2)^{|m|/2} \left(\frac{d}{dx}\right)^{|m|} P_{ℓ}(x), ``` and (4.28) gives the Legendre polynomial as ```math -P_{\ell}(x) +P_{ℓ}(x) = -\frac{1}{2^\ell \ell!} \left(\frac{d}{dx}\right)^\ell (x^2-1)^\ell. +\frac{1}{2^ℓ ℓ!} \left(\frac{d}{dx}\right)^ℓ (x^2-1)^ℓ. ``` Then, (4.32) gives the spherical harmonics as ```math -Y_{\ell}^{m}(\theta, \phi) +Y_{ℓ}^{m}(\theta, \phi) = \epsilon -\sqrt{\frac{2\ell+1}{4\pi} \frac{(\ell-|m|)!}{(\ell+|m|)!}} -e^{im\phi} P_{\ell}^{m}(\cos\theta), +\sqrt{\frac{2ℓ+1}{4\pi} \frac{(ℓ-|m|)!}{(ℓ+|m|)!}} +e^{im\phi} P_{ℓ}^{m}(\cos\theta), ``` where ``\epsilon = (-1)^m`` for ``m\geq 0`` and ``\epsilon = 1`` for ``m\leq 0``. In Table 4.2, he explicitly lists the first few @@ -277,7 +277,7 @@ L_z &= -i \hbar \frac{\partial} {\partial \phi}. [LeBellac_2006](@citet) (with Foreword by Cohen-Tannoudji) takes an odd approach, defining [Eq. (10.32)] ```math -D^{(j)}_{m', m} \left[ \mathcal{R}(\theta, \phi) \right] +D^{(j)}_{m', m} \left[ ℛ(\theta, \phi) \right] = \langle j, m' | e^{-i\phi J_z} e^{-i\theta J_y} | j, m \rangle, ``` @@ -288,16 +288,16 @@ spherical coordinates are standard (physicist's) coordinates. Equation (10.65) shows the rotation law: ```math -Y_{\ell}^{m}\left( \mathcal{R}^{-1} \hat{r} \right) +Y_{ℓ}^{m}\left( ℛ^{-1} \hat{r} \right) = -\sum_{m'} D^{(\ell)}_{m', m}(\mathcal{R}) Y_{\ell}^{m'}(\hat{r}), +\sum_{m'} D^{(ℓ)}_{m', m}(ℛ) Y_{ℓ}^{m'}(\hat{r}), ``` and Eq. (10.66) relates the spherical harmonics to the Wigner D-matrices: ```math -D^{(\ell)}_{m, 0}(\theta, \phi) +D^{(ℓ)}_{m, 0}(\theta, \phi) = -\sqrt{\frac{4\pi}{2\ell+1}} \left[Y_{\ell}^{m}(\theta, \phi)\right]^\ast. +\sqrt{\frac{4\pi}{2ℓ+1}} \left[Y_{ℓ}^{m}(\theta, \phi)\right]^\ast. ``` @@ -342,7 +342,7 @@ page](https://reference.wolfram.com/language/ref/WignerD.html). > ϕ]]` -> For ``\ell \geq 0``, ``Y_\ell^m = \sqrt{(2\ell+1)/(4\pi)} \sqrt{(\ell-m)! / (\ell+m)!} P_\ell^m(\cos \theta) e^{im\phi}`` where ``P_\ell^m`` is the associated Legendre function. +> For ``ℓ \geq 0``, ``Y_ℓ^m = \sqrt{(2ℓ+1)/(4\pi)} \sqrt{(ℓ-m)! / (ℓ+m)!} P_ℓ^m(\cos \theta) e^{im\phi}`` where ``P_ℓ^m`` is the associated Legendre function. > The associated Legendre polynomials are defined by ``P_n^m(x) = (-1)^m (1-x^2)^{m/2}(d^m/dx^m)P_n(x)`` where ``P_n(x)`` is the Legendre polynomial. @@ -355,7 +355,7 @@ P_n(x) = \frac{1}{2^n n!} \frac{d^n}{dx^n} (x^2 - 1)^n. ## Newman-Penrose In their 1966 paper, [Newman_1966](@citet), Newman and Penrose first -introduced the spin-weighted spherical harmonics, ``{}_sY_{\ell m}``. +introduced the spin-weighted spherical harmonics, ``{}_sY_{ℓ m}``. They use the standard (physicists') convention for spherical coordinates and introduce the stereographic coordinate ``\zeta = e^{i\phi} \cot\frac{\theta}{2}``. They define the spin-raising @@ -371,12 +371,12 @@ operator ``\eth`` acting on a function of spin weight ``s`` as ``` They then compute ```math -{}_sY_{\ell, m} +{}_sY_{ℓ, m} \propto -\frac{1}{\left[(\ell-s)! (\ell+s)!\right]^{1/2}} -\left(1 + \zeta \bar{\zeta}\right)^{-\ell} +\frac{1}{\left[(ℓ-s)! (ℓ+s)!\right]^{1/2}} +\left(1 + \zeta \bar{\zeta}\right)^{-ℓ} \sum_p \zeta^p (-\bar{\zeta})^{p+s-m} -\binom{\ell-s}{p} \binom{\ell+s}{p+s-m}, +\binom{ℓ-s}{p} \binom{ℓ+s}{p+s-m}, ``` where the sum is over all integers ``p`` such that the factorials are nonzero. @@ -454,22 +454,22 @@ Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. [Shankar_1994](@citet) writes in Eq. (12.5.35) the spherical harmonics as ```math -Y_{\ell}^{m}(\theta, \phi) +Y_{ℓ}^{m}(\theta, \phi) = -(-1)^\ell -\left[ \frac{(2\ell+1)!}{4\pi} \right]^{1/2} -\frac{1}{2^\ell \ell!} -\left[ \frac{(\ell+m)!}{(2\ell)!(\ell-m)!} \right]^{1/2} +(-1)^ℓ +\left[ \frac{(2ℓ+1)!}{4\pi} \right]^{1/2} +\frac{1}{2^ℓ ℓ!} +\left[ \frac{(ℓ+m)!}{(2ℓ)!(ℓ-m)!} \right]^{1/2} e^{i m \phi} (\sin \theta)^{-m} -\frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} -(\sin\theta)^{2\ell} +\frac{d^{ℓ-m}}{d(\cos\theta)^{ℓ-m}} +(\sin\theta)^{2ℓ} ``` for ``m \geq 0``, with (12.5.40) giving the expression ```math -Y_{\ell}^{-m}(\theta, \phi) +Y_{ℓ}^{-m}(\theta, \phi) = -(-1)^m \left( Y_{\ell}^{m}(\theta, \phi) \right)^\ast. +(-1)^m \left( Y_{ℓ}^{m}(\theta, \phi) \right)^\ast. ``` The angular-momentum operators are given below (12.5.27) as ```math @@ -513,7 +513,7 @@ Specifically, the [source](https://github.com/sympy/sympy/blob/b4ce69ad5d40e4e545614b6c76ca9b0be0b98f0b/sympy/physics/wigner.py#L1136-L1191) cites [Edmonds_2016](@citet) when defining ```math -\mathcal{D}_{\alpha \beta \gamma} = +𝒟_{\alpha \beta \gamma} = \exp\big( \frac{i\alpha}{\hbar} J_z\big) \exp\big( \frac{i\beta}{\hbar} J_y\big) \exp\big( \frac{i\gamma}{\hbar} J_z\big). @@ -541,29 +541,29 @@ definition of the D matrix. ## Torres del Castillo (2003) [TorresDelCastillo_2003](@citet) starts by defining a rotation -``\mathcal{R}`` as transforming a point ``x_i`` into another point +``ℛ`` as transforming a point ``x_i`` into another point with coordinates ``x_i' = a_{ij}x_j``. Under that rotation, any scalar function ``f`` transforms into another function ``f' = -\mathcal{R} f`` defined by [Eq. (2.43)] +ℛ f`` defined by [Eq. (2.43)] ```math f'\big(x_i\big) = f\big( a^{-1}_{ij} x_j \big). ``` In particular, ``f'(x'_i) = f(x_i)``. He then defines Wigner's D-matrix to satisfy [Eq. (2.45)] ```math -\mathcal{R} Y_{l,m} = \sum_{m} D^l_{m',m}(\mathcal{R}) Y_{l,m'}. +ℛ Y_{l,m} = \sum_{m} D^l_{m',m}(ℛ) Y_{l,m'}. ``` Including the arguments to the spherical harmonics, this becomes ```math -Y_{l,m}\big(\mathcal{R}^{-1} R_{\theta, \phi}\big) +Y_{l,m}\big(ℛ^{-1} R_{\theta, \phi}\big) = -\sum_{m} D^l_{m',m}(\mathcal{R}) Y_{l,m'}\big(R_{\theta, \phi}\big). +\sum_{m} D^l_{m',m}(ℛ) Y_{l,m'}\big(R_{\theta, \phi}\big). ``` In this form, we have [Eq. (2.46)] ```math -D^l_{m'',m}(\mathcal{R}_1 \mathcal{R}_2) +D^l_{m'',m}(ℛ_1 ℛ_2) = -\sum_{m'} D^l_{m'',m'}(\mathcal{R}_1) D^l_{m',m}(\mathcal{R}_2). +\sum_{m'} D^l_{m'',m'}(ℛ_1) D^l_{m',m}(ℛ_2). ``` He computes [Eq. (2.53)] ```math @@ -617,9 +617,9 @@ where the ``\hat{J}`` operators are defined in > > A transformation of an arbitrary wave function ``\Psi`` under > rotation of the coordinate system through an infinitesimal angle -> ``\delta \omega`` about an axis ``\mathbf{n}`` may be written as +> ``\delta \omega`` about an axis ``𝐧`` may be written as > ```math -> \Psi \to \Psi' = \left(1 - i \delta \omega \mathbf{n} \cdot \hat{J} \right)\Psi, +> \Psi \to \Psi' = \left(1 - i \delta \omega 𝐧 \cdot \hat{J} \right)\Psi, > ``` > where ``\hat{J}`` is the total angular momentum operator. @@ -639,25 +639,25 @@ e^{-i M' \gamma} ``` -Page 155 has a table of values for ``\ell \leq 5`` +Page 155 has a table of values for ``ℓ \leq 5`` [Varshalovich_1988](@citet) distinguish in Sec. 1.1.3 between *covariant* and *contravariant* spherical coordinates and the corresponding basis vectors, which they define as ```math \begin{aligned} - \mathbf{e}_{+1} &= - \frac{1}{\sqrt{2}} \left( \mathbf{e}_x + i \mathbf{e}_y\right) + 𝐞_{+1} &= - \frac{1}{\sqrt{2}} \left( 𝐞_x + i 𝐞_y\right) &&& - \mathbf{e}^{+1} &= - \frac{1}{\sqrt{2}} \left( \mathbf{e}_x - i \mathbf{e}_y\right) \\ - \mathbf{e}_{0} &= \mathbf{e}_z &&& \mathbf{e}^{0} &= \mathbf{e}_z \\ - \mathbf{e}_{-1} &= \frac{1}{\sqrt{2}} \left( \mathbf{e}_x - i \mathbf{e}_y\right) + 𝐞^{+1} &= - \frac{1}{\sqrt{2}} \left( 𝐞_x - i 𝐞_y\right) \\ + 𝐞_{0} &= 𝐞_z &&& 𝐞^{0} &= 𝐞_z \\ + 𝐞_{-1} &= \frac{1}{\sqrt{2}} \left( 𝐞_x - i 𝐞_y\right) &&& - \mathbf{e}^{-1} &= \frac{1}{\sqrt{2}} \left( \mathbf{e}_x + i \mathbf{e}_y\right). + 𝐞^{-1} &= \frac{1}{\sqrt{2}} \left( 𝐞_x + i 𝐞_y\right). \end{aligned} ``` -Then, in Sec. 4.2 they define ``\hat{\mathbf{J}}`` as the operator of +Then, in Sec. 4.2 they define ``\hat{𝐉}`` as the operator of angular momentum of the rigid symmetric top. They then give in Eq. -(6) the "covariant spherical coordinates of ``\hat{\mathbf{J}}`` in the +(6) the "covariant spherical coordinates of ``\hat{𝐉}`` in the non-rotating (lab-fixed) system" as ```math \begin{gather} @@ -669,7 +669,7 @@ non-rotating (lab-fixed) system" as \hat{J}_0 = - i \frac{\partial}{\partial \alpha}, \end{gather} ``` -and "contravariant components of ``\hat{\mathbf{J}}`` in the rotating +and "contravariant components of ``\hat{𝐉}`` in the rotating (body-fixed) system" as ```math \begin{gather} @@ -809,11 +809,11 @@ Spin-weighted spherical harmonics Defining the operator ```math -\mathcal{R}(\alpha,\beta,\gamma) = e^{-i\alpha J_z}e^{-i\beta J_y}e^{-i\gamma J_z}, +ℛ(\alpha,\beta,\gamma) = e^{-i\alpha J_z}e^{-i\beta J_y}e^{-i\gamma J_z}, ``` [Wikipedia expresses the Wigner D-matrix](https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix) as ```math -D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | \mathcal{R}(\alpha,\beta,\gamma)| jm \rangle =e^{-im'\alpha } d^j_{m'm}(\beta)e^{-i m\gamma}. +D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | ℛ(\alpha,\beta,\gamma)| jm \rangle =e^{-im'\alpha } d^j_{m'm}(\beta)e^{-i m\gamma}. ``` @@ -922,7 +922,7 @@ In Sec. 7.2.6, we find that if the operator ``\hat{R}(\alpha, \beta, vector pointing in the ``(\theta', \phi')`` direction, then the spherical harmonics transform as [Eq. (7.70)] ```math -Y_{\ell, m}^\ast (\theta', \phi') +Y_{ℓ, m}^\ast (\theta', \phi') = -\sum_{m'} D^{(\ell)}_{m, m'}(\alpha, \beta, \gamma) Y_{\ell, m'}^\ast (\theta, \phi). +\sum_{m'} D^{(ℓ)}_{m, m'}(\alpha, \beta, \gamma) Y_{ℓ, m'}^\ast (\theta, \phi). ``` diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 0fc55d12..7511db06 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -25,11 +25,11 @@ unit basis vectors ``(𝐱, 𝐲, 𝐳)``. Note that these basis vectors are assumed to have unit norm, but we omit the hats just to keep the notation simple. Any vector in this space can be written as ```math -\mathbf{v} = v_x \mathbf{𝐱} + v_y \mathbf{𝐲} + v_z \mathbf{𝐳}, +𝐯 = v_x 𝐱 + v_y 𝐲 + v_z 𝐳, ``` in which case the Euclidean norm is given by ```math -\| \mathbf{v} \| = \sqrt{v_x^2 + v_y^2 + v_z^2}. +\| 𝐯 \| = \sqrt{v_x^2 + v_y^2 + v_z^2}. ``` Equivalently, we can write the components of the Euclidean metric as ```math @@ -78,9 +78,9 @@ g_{i'j'} The unit coordinate vectors in spherical coordinates are then ```math \begin{aligned} -\mathbf{𝐫} &= \sin\theta \cos\phi \mathbf{𝐱} + \sin\theta \sin\phi \mathbf{𝐲} + \cos\theta \mathbf{𝐳}, \\ -\boldsymbol{\theta} &= \cos\theta \cos\phi \mathbf{𝐱} + \cos\theta \sin\phi \mathbf{𝐲} - \sin\theta \mathbf{𝐳}, \\ -\boldsymbol{\phi} &= -\sin\phi \mathbf{𝐱} + \cos\phi \mathbf{𝐲}, +𝐫 &= \sin\theta \cos\phi 𝐱 + \sin\theta \sin\phi 𝐲 + \cos\theta 𝐳, \\ +\boldsymbol{\theta} &= \cos\theta \cos\phi 𝐱 + \cos\theta \sin\phi 𝐲 - \sin\theta 𝐳, \\ +\boldsymbol{\phi} &= -\sin\phi 𝐱 + \cos\phi 𝐲, \end{aligned} ``` where, again, we omit the hats on the unit vectors to keep the @@ -88,30 +88,30 @@ notation simple. Conversely, we can express the Cartesian basis vectors in terms of the spherical basis vectors as ```math \begin{aligned} -\mathbf{𝐱} &= \sin\theta \cos\phi \mathbf{𝐫} + \cos\theta \cos\phi \boldsymbol{\theta} - \sin\phi \boldsymbol{\phi}, +𝐱 &= \sin\theta \cos\phi 𝐫 + \cos\theta \cos\phi \boldsymbol{\theta} - \sin\phi \boldsymbol{\phi}, \\ -\mathbf{𝐲} &= \sin\theta \sin\phi \mathbf{𝐫} + \cos\theta \sin\phi \boldsymbol{\theta} + \cos\phi \boldsymbol{\phi}, +𝐲 &= \sin\theta \sin\phi 𝐫 + \cos\theta \sin\phi \boldsymbol{\theta} + \cos\phi \boldsymbol{\phi}, \\ -\mathbf{𝐳} &= \cos\theta \mathbf{𝐫} - \sin\theta \boldsymbol{\theta}. +𝐳 &= \cos\theta 𝐫 - \sin\theta \boldsymbol{\theta}. \end{aligned} ``` One seemingly obvious — but extremely important — fact is that the unit basis frame ``(𝐱, 𝐲, 𝐳)`` can be rotated onto -``(\boldsymbol{\theta}, \boldsymbol{\phi}, \mathbf{r})`` by first -rotating through the "polar" angle ``\theta`` about the ``\mathbf{y}`` +``(\boldsymbol{\theta}, \boldsymbol{\phi}, 𝐫)`` by first +rotating through the "polar" angle ``\theta`` about the ``𝐲`` axis, and then through the "azimuthal" angle ``\phi`` about the -``\mathbf{z}`` axis. This becomes important when we consider +``𝐳`` axis. This becomes important when we consider spin-weighted functions. Integration in Cartesian coordinates is, of course, trivial as ```math -\int_{\mathbb{R}^3} f\, d^3\mathbf{r} = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f\, dx\, dy\, dz. +\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f\, dx\, dy\, dz. ``` In spherical coordinates, the integrand involves the square-root of the determinant of the metric, so we have ```math -\int_{\mathbb{R}^3} f\, d^3\mathbf{r} = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. +\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. ``` Restricting to the unit sphere, and normalizing so that the integral of 1 over the sphere is 1, we can simplify this to @@ -322,7 +322,7 @@ The unit basis vectors in extended Euler coordinates in terms of the unit basis vectors in quaternion coordinates are ```math \begin{aligned} -\mathbf{𝐑} &= \frac{1}{R} \left( +𝐑 &= \frac{1}{R} \left( \cos \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝟏 - \sin \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐢 + \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 @@ -379,41 +379,41 @@ Z^2 = 1``. Given this constraint we can, without loss of generality, write the quaternion as ```math 𝐑 -= \exp\left(\frac{\rho}{2} \hat{\mathfrak{r}}\right) -= \cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}, += \exp\left(\frac{\rho}{2} \hat{𝔯}\right) += \cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{𝔯}, ``` -where ``\rho`` is an angle of rotation and ``\hat{\mathfrak{r}}`` is a +where ``\rho`` is an angle of rotation and ``\hat{𝔯}`` is a unit "pure-vector" quaternion. We can multiply a vector ``𝐯`` as ```math 𝐑\, 𝐯\, 𝐑^{-1}. ``` Splitting ``𝐯 = 𝐯_⟂ + 𝐯_∥`` into components perpendicular and -parallel to ``\hat{\mathfrak{r}}``, we see that ``𝐯_∥`` commutes with +parallel to ``\hat{𝔯}``, we see that ``𝐯_∥`` commutes with ``𝐑`` and ``𝐑^{-1}``, while ``𝐯_⟂`` anticommutes with -``\hat{\mathfrak{r}}``. To find the full rotation, we expand the +``\hat{𝔯}``. To find the full rotation, we expand the product: ```math \begin{aligned} 𝐑\, 𝐯\, 𝐑^{-1} &= 𝐯_∥ - + \left(\cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\right) + + \left(\cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{𝔯}\right) 𝐯_⟂ - \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\right) \\ + \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{𝔯}\right) \\ &= 𝐯_∥ - + \left(\cos\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\, 𝐯_⟂\right) - \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{\mathfrak{r}}\right) \\ + + \left(\cos\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \hat{𝔯}\, 𝐯_⟂\right) + \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{𝔯}\right) \\ &= 𝐯_∥ - + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, \hat{\mathfrak{r}}\, 𝐯_⟂ - - \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, 𝐯_⟂ \, \hat{\mathfrak{r}} - \sin^2\frac{\rho}{2}\, \hat{\mathfrak{r}}\, 𝐯_⟂\, \hat{\mathfrak{r}} \\ + + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, \hat{𝔯}\, 𝐯_⟂ + - \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, 𝐯_⟂ \, \hat{𝔯} - \sin^2\frac{\rho}{2}\, \hat{𝔯}\, 𝐯_⟂\, \hat{𝔯} \\ &= 𝐯_∥ - + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, [\hat{\mathfrak{r}}, 𝐯_⟂] - \sin^2\frac{\rho}{2}\, 𝐯_⟂ \\ + + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, [\hat{𝔯}, 𝐯_⟂] - \sin^2\frac{\rho}{2}\, 𝐯_⟂ \\ &= 𝐯_∥ - + \cos\rho\, 𝐯_⟂ + \sin\rho\, \hat{\mathfrak{r}}\times 𝐯_⟂ + + \cos\rho\, 𝐯_⟂ + \sin\rho\, \hat{𝔯}\times 𝐯_⟂ \end{aligned} ``` The final expression shows that this is precisely what we expect when rotating ``𝐯`` through an angle ``\rho`` (in a positive, right-handed -sense) about the axis ``\hat{\mathfrak{r}}``. +sense) about the axis ``\hat{𝔯}``. Note that the presence of two factors of ``𝐑`` in the expression for rotating a vector explains two things. First, it explains why the @@ -448,7 +448,7 @@ rotated into the plane spanned by the unit basis vectors that point. If ``\gamma = 0`` the rotation is precise, meaning that ``𝐱`` is rotated onto ``\boldsymbol{\theta}`` and ``𝐲`` onto ``\boldsymbol{\phi}``; if ``\gamma ≠ 0`` then they are rotated within -that plane by the angle ``\gamma`` about the ``\mathbf{r}`` axis. +that plane by the angle ``\gamma`` about the ``𝐫`` axis. Thus, we identify the spherical coordinates ``(\theta, \phi)`` with the Euler angles ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. @@ -538,35 +538,35 @@ actions. We work with functions ``f: A \to \mathbb{C}``, where ``A`` is either the group of unit quaternions, or the full algebra of quaternions. -Any non-zero quaternion can be expressed as ``e^\mathfrak{g}`` for -some finite quaternion ``\mathfrak{g}``, which is referred to as the -"generator" of the action of ``e^\mathfrak{g}``. This can act on a -function ``f`` by multiplying the argument by ``e^\mathfrak{g}``. +Any non-zero quaternion can be expressed as ``e^𝔤`` for +some finite quaternion ``𝔤``, which is referred to as the +"generator" of the action of ``e^𝔤``. This can act on a +function ``f`` by multiplying the argument by ``e^𝔤``. However, there is an ambiguity: we could multiply either on the left or the right:[^2] ```math -f\left(\mathbf{Q}\right) \mapsto f\left(e^\mathfrak{g} \mathbf{Q}\right) +f\left(𝐐\right) \mapsto f\left(e^𝔤 𝐐\right) \qquad \text{or} \qquad -f\left(\mathbf{Q}\right) \mapsto f\left(\mathbf{Q} e^\mathfrak{g}\right). +f\left(𝐐\right) \mapsto f\left(𝐐 e^𝔤\right). ``` There is an additional ambiguity, in that this action rotates the *argument* of the function, whereas we will often prefer to think in terms of rotating the *function* itself. For example, our function may describe the measurement of some field in a particular coordinate -system. Here, the argument ``\mathbf{Q}`` describes a particular -value of the coordinates, and ``e^\mathfrak{g}`` changes the point -under consideration. If, on the other hand ``e^\mathfrak{g}`` +system. Here, the argument ``𝐐`` describes a particular +value of the coordinates, and ``e^𝔤`` changes the point +under consideration. If, on the other hand ``e^𝔤`` describes how the field itself is rotated, then we can write the rotated field as a function ``f'`` which is related to the original function ``f`` by ```math -f'\left(\mathbf{Q}\right) = f\left(e^{-\mathfrak{g}} \mathbf{Q}\right) +f'\left(𝐐\right) = f\left(e^{-𝔤} 𝐐\right) \qquad \text{or} \qquad -f'\left(\mathbf{Q}\right) = f\left(\mathbf{Q} e^{-\mathfrak{g}}\right). +f'\left(𝐐\right) = f\left(𝐐 e^{-𝔤}\right). ``` Note that the exponent is negated, because the action of -``e^\mathfrak{g}`` on the argument is the inverse of the action of -``e^{-\mathfrak{g}}`` on the function. This is a general property of +``e^𝔤`` on the argument is the inverse of the action of +``e^{-𝔤}`` on the function. This is a general property of the action of a group on a space, and is a consequence of the group action being a homomorphism. @@ -583,19 +583,19 @@ f(\theta, \phi) = \sin\theta \sin\phi. Recall that we can map the spherical coordinates into the Euler angles, and the Euler angles into the quaternion ```math -(\theta, \phi) \mapsto (\phi, \theta, 0) \mapsto \mathbf{Q} +(\theta, \phi) \mapsto (\phi, \theta, 0) \mapsto 𝐐 = -\exp\left(\frac{\phi}{2} \mathbf{k}\right) -\exp\left(\frac{\theta}{2} \mathbf{j}\right). +\exp\left(\frac{\phi}{2} 𝐤\right) +\exp\left(\frac{\theta}{2} 𝐣\right). ``` It is straightforward to see that we can write ``f`` as a function of -``\mathbf{Q}`` as +``𝐐`` as ```math -f(\mathbf{Q}) = \left\langle \mathbf{Q}\, \mathbf{k}\, \mathbf{Q}^{-1} \right\rangle_{\mathbf{j}}, +f(𝐐) = \left\langle 𝐐\, 𝐤\, 𝐐^{-1} \right\rangle_{𝐣}, ``` where the angle brackets and subscript indicate that we are taking the -``\mathbf{j}`` component. That is, ``f`` is the ``y`` component of -the vector ``\mathbf{z}`` rotated by ``\mathbf{Q}``. +``𝐣`` component. That is, ``f`` is the ``y`` component of +the vector ``𝐳`` rotated by ``𝐐``. Now, we imagine rotating the field by an angle ``\alpha`` in the positive sense about the ``z`` axis. Visualizing the situation, we @@ -606,40 +606,40 @@ f'(\theta, \phi) = \sin\theta \sin(\phi - \alpha). For example, the rotated field evaluated at the point ``(\theta, \phi) = (\pi/2, 0)`` along the positive ``x`` axis should correspond to the original field evaluated at the point ``(\theta, \phi) = (\pi/2, --\alpha)``. This rotation is generated by ``\mathfrak{g} = \alpha -\mathbf{k} / 2``, which allows us to immediately calculate +-\alpha)``. This rotation is generated by ``𝔤 = \alpha +𝐤 / 2``, which allows us to immediately calculate ```math \begin{aligned} -f(e^\mathfrak{g} \mathbf{Q}) &= \sin\theta \sin(\phi + \alpha) &&& -f(\mathbf{Q} e^\mathfrak{g}) &= \sin\theta \sin\phi \\ -f(e^{-\mathfrak{g}} \mathbf{Q}) &= \sin\theta \sin(\phi - \alpha) &&& -f(\mathbf{Q} e^{-\mathfrak{g}}) &= \sin\theta \sin\phi. +f(e^𝔤 𝐐) &= \sin\theta \sin(\phi + \alpha) &&& +f(𝐐 e^𝔤) &= \sin\theta \sin\phi \\ +f(e^{-𝔤} 𝐐) &= \sin\theta \sin(\phi - \alpha) &&& +f(𝐐 e^{-𝔤}) &= \sin\theta \sin\phi. \end{aligned} ``` -Thus, we see that left-multiplication by ``e^{-\mathfrak{g}}`` +Thus, we see that left-multiplication by ``e^{-𝔤}`` corresponds to rotation of the field while leaving the coordinates -fixed; left-multiplication by ``e^\mathfrak{g}`` corresponds to +fixed; left-multiplication by ``e^𝔤`` corresponds to rotation of the coordinates while leaving the field fixed; and right-multiplication by either doesn't affect this function at all. Of course, right-multiplication using other choices for -``\mathfrak{g}`` could certainly have some effect on this function, -and this choice of ``\mathfrak{g}`` could have an effect on other +``𝔤`` could certainly have some effect on this function, +and this choice of ``𝔤`` could have an effect on other functions. Note that right-multiplication can also be interpreted as left-multiplication, where the generator itself is rotated by the argument to the function. That is, ```math \begin{aligned} -f(\mathbf{Q} e^\mathfrak{g}) - &= f(\mathbf{Q} e^{\mathfrak{g}} \mathbf{Q}^{-1} \mathbf{Q}) - = f(e^{\mathfrak{g}'} \mathbf{Q}) \\ -f(\mathbf{Q} e^{-\mathfrak{g}}) - &= f(\mathbf{Q} e^{-\mathfrak{g}} \mathbf{Q}^{-1} \mathbf{Q}) - = f(e^{-\mathfrak{g}'} \mathbf{Q}), +f(𝐐 e^𝔤) + &= f(𝐐 e^{𝔤} 𝐐^{-1} 𝐐) + = f(e^{𝔤'} 𝐐) \\ +f(𝐐 e^{-𝔤}) + &= f(𝐐 e^{-𝔤} 𝐐^{-1} 𝐐) + = f(e^{-𝔤'} 𝐐), \end{aligned} ``` -where ``\mathfrak{g}' = \mathbf{Q} \mathfrak{g} \mathbf{Q}^{-1}``. In -this example, ``\mathfrak{g}'`` generates a rotation by an angle +where ``𝔤' = 𝐐 𝔤 𝐐^{-1}``. In +this example, ``𝔤'`` generates a rotation by an angle ``\alpha`` about the point in question, which leaves that point fixed, and since this is a scalar function it has no effect on the value. Of course, we will see below that changing by a phase proportional to @@ -652,8 +652,8 @@ respect to infinitesimal rotations we apply to the functions themselves: ```math \begin{aligned} -L_{\mathfrak{g}} f(\mathbf{Q}) &= \lambda \left. \frac{\partial} {\partial \theta} f \left( e^{-\theta \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\theta=0}, \\ -R_{\mathfrak{g}} f(\mathbf{Q}) &= \rho \left. \frac{\partial} {\partial \theta} f \left( \mathbf{Q} e^{-\theta \mathfrak{g} / 2} \right) \right|_{\theta=0}. +L_{𝔤} f(𝐐) &= \lambda \left. \frac{\partial} {\partial \theta} f \left( e^{-\theta 𝔤 / 2} 𝐐 \right) \right|_{\theta=0}, \\ +R_{𝔤} f(𝐐) &= \rho \left. \frac{\partial} {\partial \theta} f \left( 𝐐 e^{-\theta 𝔤 / 2} \right) \right|_{\theta=0}. \end{aligned} ``` Here, we have introduced the constants ``\lambda`` and ``\rho`` @@ -667,17 +667,17 @@ Note that when composing operators, it is critical to keep track of the order of operations, which may look slightly unnatural: ```math \begin{aligned} - L_\mathfrak{g} L_\mathfrak{h} f(\mathbf{Q}) - % &= \left. \lambda \frac{\partial} {\partial \gamma} f'\left(e^{-\gamma \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\gamma=0}, \\ - &= \left. \lambda^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left(e^{-\eta \mathfrak{h} / 2} e^{-\gamma \mathfrak{g} / 2} \mathbf{Q} \right) \right|_{\gamma=\eta=0}, \\ - R_\mathfrak{g} R_\mathfrak{h} f(\mathbf{Q}) - % &= \rho \left. \frac{\partial} {\partial \gamma} f' \left( \mathbf{Q} e^{-\gamma \mathfrak{g} / 2} \right) \right|_{\gamma=0} \\ - &= \left. \rho^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left( \mathbf{Q} e^{-\gamma \mathfrak{g} / 2} e^{-\eta \mathfrak{h} / 2} \right) \right|_{\gamma=\eta=0}. + L_𝔤 L_𝔥 f(𝐐) + % &= \left. \lambda \frac{\partial} {\partial \gamma} f'\left(e^{-\gamma 𝔤 / 2} 𝐐 \right) \right|_{\gamma=0}, \\ + &= \left. \lambda^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left(e^{-\eta 𝔥 / 2} e^{-\gamma 𝔤 / 2} 𝐐 \right) \right|_{\gamma=\eta=0}, \\ + R_𝔤 R_𝔥 f(𝐐) + % &= \rho \left. \frac{\partial} {\partial \gamma} f' \left( 𝐐 e^{-\gamma 𝔤 / 2} \right) \right|_{\gamma=0} \\ + &= \left. \rho^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left( 𝐐 e^{-\gamma 𝔤 / 2} e^{-\eta 𝔥 / 2} \right) \right|_{\gamma=\eta=0}. \end{aligned} ``` We can prove the first of these, for example, by defining -``f'(\mathbf{Q}) = L_\mathfrak{h} f(\mathbf{Q})``, then applying the -definition of ``L_\mathfrak{g}`` to ``f'(\mathbf{Q})``, and finally +``f'(𝐐) = L_𝔥 f(𝐐)``, then applying the +definition of ``L_𝔤`` to ``f'(𝐐)``, and finally substituting the definition of ``f'`` back in. If we failed to use the correct order of operations, we would get sign errors when trying to evaluate the commutators. @@ -685,44 +685,44 @@ to evaluate the commutators. These operators have some nice properties. For any scalar ``s``, we have ```math \begin{aligned} -L_{s \mathfrak{g}} &= s L_{\mathfrak{g}}, \\ -R_{s \mathfrak{g}} &= s R_{\mathfrak{g}}. +L_{s 𝔤} &= s L_{𝔤}, \\ +R_{s 𝔤} &= s R_{𝔤}. \end{aligned} ``` -Given any basis ``\mathbf{e}_n`` for the quaternions, we can use +Given any basis ``𝐞_n`` for the quaternions, we can use the multivariable chain rule to expand the operators in terms of components: ```math \begin{aligned} -L_{\mathfrak{g}} &= \sum_n g_n\, L_{\mathbf{e}_n}, \\ -R_{\mathfrak{g}} &= \sum_n g_n\, R_{\mathbf{e}_n}. +L_{𝔤} &= \sum_n g_n\, L_{𝐞_n}, \\ +R_{𝔤} &= \sum_n g_n\, R_{𝐞_n}. \end{aligned} ``` This implies that vector addition holds more generally: ```math \begin{aligned} -L_{\mathfrak{g} + \mathfrak{h}} &= L_{\mathfrak{g}} + L_{\mathfrak{h}} \\ -R_{\mathfrak{g} + \mathfrak{h}} &= R_{\mathfrak{g}} + R_{\mathfrak{h}}. +L_{𝔤 + 𝔥} &= L_{𝔤} + L_{𝔥} \\ +R_{𝔤 + 𝔥} &= R_{𝔤} + R_{𝔥}. \end{aligned} ``` Moreover, we can show that these operators form a Lie algebra with the commutator as the Lie bracket. That is, we have ```math \begin{aligned} -[L_{\mathfrak{g}}, L_{\mathfrak{h}}] - &= \frac{\lambda}{2} L_{[\mathfrak{g}, \mathfrak{h}]}, +[L_{𝔤}, L_{𝔥}] + &= \frac{\lambda}{2} L_{[𝔤, 𝔥]}, \\ -[R_{\mathfrak{g}}, R_{\mathfrak{h}}] - &= -\frac{\rho}{2} R_{[\mathfrak{g}, \mathfrak{h}]}, +[R_{𝔤}, R_{𝔥}] + &= -\frac{\rho}{2} R_{[𝔤, 𝔥]}, \\ -[L_{\mathfrak{g}}, R_{\mathfrak{h}}] &= 0. +[L_{𝔤}, R_{𝔥}] &= 0. \end{aligned} ``` -Conventionally, we single out the ``\mathbf{z}`` axis — or -equivalently the generator ``\mathbf{k} = \mathbf{y}\mathbf{x}`` — as -a sort of fiducial axis, and ``L_z = L_\mathbf{k}`` and ``R_z = -R_\mathbf{k}`` as the fiducial operators. Then, *by definition*, +Conventionally, we single out the ``𝐳`` axis — or +equivalently the generator ``𝐤 = 𝐲𝐱`` — as +a sort of fiducial axis, and ``L_z = L_𝐤`` and ``R_z = +R_𝐤`` as the fiducial operators. Then, *by definition*, their raising operators ``L_+`` and ``R_+`` and lowering operators ``L_-`` and ``R_-`` satisfy ```math @@ -733,15 +733,15 @@ their raising operators ``L_+`` and ``R_+`` and lowering operators ``` Assuming that the raising and lowering operators can be written as linear combinations of the basis operators, these equations imply that -they have no component proportional ``L_\mathbf{z}``, and that both of +they have no component proportional ``L_𝐳``, and that both of the remaining components must be nonzero. This actually allows us to deduce that ``\lambda^2 = \rho^2 = -1``. This, in turn, allows us to deduce the values of the raising and lowering operators up to an overall factor. Conventionally the factor is chosen so that ```math \begin{aligned} -L_\pm &= L_\mathbf{x} \pm i L_\mathbf{y}, \\ -R_\pm &= R_\mathbf{x} \pm i R_\mathbf{y}. +L_\pm &= L_𝐱 \pm i L_𝐲, \\ +R_\pm &= R_𝐱 \pm i R_𝐲. \end{aligned} ``` @@ -768,9 +768,9 @@ Using these relations, we can actually solve for the constants - Show for both the three- and two-spheres - Show how they act on functions on the three-sphere -The idea here is to express, e.g., $e^{\theta \mathbf{e}_i / -2}\mathbf{R}_{\alpha, \beta, \gamma}$ in quaternion components, then -solve for the new Euler angles $\mathbf{R}_{\alpha', \beta', \gamma'}$ +The idea here is to express, e.g., $e^{\theta 𝐞_i / +2}𝐑_{\alpha, \beta, \gamma}$ in quaternion components, then +solve for the new Euler angles $𝐑_{\alpha', \beta', \gamma'}$ in terms of the quaternion components, where these new angles all depend on $\theta$. We then use the chain rule to express $\partial_\theta$ in terms of $\partial_{\alpha'}$, etc., which become @@ -779,44 +779,44 @@ $\partial_\alpha$, etc., when $\theta=0$. ```math \begin{aligned} - L_i f(\mathbf{R}_{\alpha, \beta, \gamma}) + L_i f(𝐑_{\alpha, \beta, \gamma}) &= - \left. -\mathbf{z} \frac{\partial} {\partial \theta} f \left( e^{\theta \mathbf{e}_i / 2} \mathbf{R}_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} \\ + \left. -𝐳 \frac{\partial} {\partial \theta} f \left( e^{\theta 𝐞_i / 2} 𝐑_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} \\ &= - \left. -\mathbf{z} \frac{\partial} {\partial \theta} f \left( \mathbf{R}_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ + \left. -𝐳 \frac{\partial} {\partial \theta} f \left( 𝐑_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ &= - \left. -\mathbf{z} \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha'} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta'} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma'} \right] f \left( \mathbf{R}_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ + \left. -𝐳 \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha'} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta'} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma'} \right] f \left( 𝐑_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ &= - -\mathbf{z} \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right) \\ - K_i f(\mathbf{R}_{\alpha, \beta, \gamma}) + -𝐳 \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( 𝐑_{\alpha, \beta, \gamma} \right) \\ + K_i f(𝐑_{\alpha, \beta, \gamma}) &= - -\mathbf{z} \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( \mathbf{R}_{\alpha, \beta, \gamma} \right), + -𝐳 \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( 𝐑_{\alpha, \beta, \gamma} \right), \end{aligned} ``` ```math \begin{aligned} -\mathbf{R}_{\alpha, \beta, \gamma} +𝐑_{\alpha, \beta, \gamma} &= R\, \cos\frac{β}{2} \cos\frac{α+γ}{2} - -R\, \sin\frac{β}{2} \sin\frac{α-γ}{2} \mathbf{i} - + R\, \sin\frac{β}{2} \cos\frac{α-γ}{2} \mathbf{j} - + R\, \cos\frac{β}{2} \sin\frac{α+γ}{2} \mathbf{k}. + -R\, \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 + + R\, \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 + + R\, \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤. \\ -e^{\theta \mathbf{u} / 2} \mathbf{R}_{\alpha, \beta, \gamma} -&= \left(\cos\frac{\theta}{2} + \mathbf{u} \sin\frac{\theta}{2}\right) \mathbf{R}_{\alpha, \beta, \gamma} +e^{\theta 𝐮 / 2} 𝐑_{\alpha, \beta, \gamma} +&= \left(\cos\frac{\theta}{2} + 𝐮 \sin\frac{\theta}{2}\right) 𝐑_{\alpha, \beta, \gamma} \\ &= R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \cos\frac{α+γ}{2} - -R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \sin\frac{α-γ}{2} \mathbf{i} - + R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \cos\frac{α-γ}{2} \mathbf{j} - + R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \sin\frac{α+γ}{2} \mathbf{k} + -R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 + + R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 + + R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤 \\ &\quad + - R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \cos\frac{α+γ}{2} \mathbf{u} - -R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \sin\frac{α-γ}{2} \mathbf{u}\mathbf{i} - + R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \cos\frac{α-γ}{2} \mathbf{u}\mathbf{j} - + R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \sin\frac{α+γ}{2} \mathbf{u}\mathbf{k} + R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \cos\frac{α+γ}{2} 𝐮 + -R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐮𝐢 + + R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐮𝐣 + + R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐮𝐤 \end{aligned} ``` @@ -847,7 +847,7 @@ and ``` These imply that the restriction to the space of unit quaternions is not harmonic with respect to the Laplacian on the 3-sphere, but is an -eigenfunction with eigenvalue ``-\ell(\ell+2)``. +eigenfunction with eigenvalue ``-ℓ(ℓ+2)``. ```math \frac{1}{r^{n-1}} \frac{\partial}{\partial r} \left( r^{n-1} \frac{\partial f}{\partial r} \right) @@ -870,21 +870,21 @@ eigenfunction with eigenvalue ``-\ell(\ell+2)``. + \frac{n-1}{r} \frac{\partial f}{\partial r} = -\frac{f}{r^\ell} \frac{\partial^2 r^\ell}{\partial r^2} +\frac{f}{r^ℓ} \frac{\partial^2 r^ℓ}{\partial r^2} + -\frac{f}{r^\ell} \frac{n-1}{r} \frac{\partial r^\ell}{\partial r} +\frac{f}{r^ℓ} \frac{n-1}{r} \frac{\partial r^ℓ}{\partial r} = -\ell(\ell-1) \frac{f}{r^\ell} r^{\ell-2} +ℓ(ℓ-1) \frac{f}{r^ℓ} r^{ℓ-2} + -\ell \frac{f}{r^\ell} \frac{n-1}{r} r^{\ell-1} +ℓ \frac{f}{r^ℓ} \frac{n-1}{r} r^{ℓ-1} = -\ell(\ell-1) \frac{f}{r^2} +ℓ(ℓ-1) \frac{f}{r^2} + -\ell (n-1) \frac{f}{r^2} +ℓ (n-1) \frac{f}{r^2} = -\ell(\ell+n-2) \frac{f}{r^2} +ℓ(ℓ+n-2) \frac{f}{r^2} \to -\ell(\ell+2) \frac{f}{r^2} +ℓ(ℓ+2) \frac{f}{r^2} ``` Note that [Lee_2012](@citet) points out that there is a sign ambiguity @@ -909,8 +909,8 @@ functions are preferred to Chebyshev polynomials for the spherical harmonics. They also mention that since the Laplacian measures curvature, and spherical harmonics of a given degree have the same Laplacian eigenvalue, they all have the same measure of curvature. -So, for example, the ``\ell = m`` mode varies most rapidly with -longitude but not at all with latitude, while the ``\ell = 0`` mode +So, for example, the ``ℓ = m`` mode varies most rapidly with +longitude but not at all with latitude, while the ``ℓ = 0`` mode varies just as rapidly with latitude but not at all with longitude. [Vasil_2019](@citet) use spin-weighted spherical harmonics to do @@ -956,37 +956,37 @@ f\left(𝐑\right) - i \epsilon L_𝐮 f\left(𝐑\right). ``` This final expression is precisely equivalent to Sakurai's Eq. (3.1.15): ```math -\mathscr{D}\left(\hat{\mathbf{n}}, d\phi \right) +\mathscr{D}\left(\hat{𝐧}, d\phi \right) = -1 - i \left( \mathbf{J} \cdot \hat{\mathbf{n}} \right) d\phi. +1 - i \left( 𝐉 \cdot \hat{𝐧} \right) d\phi. ``` -Now, we can write the eigenkets of ``L^2`` and ``L_z`` as ``|\ell, -m\rangle``, where the eigenvalues are ``\ell(\ell+1)`` and ``m``, +Now, we can write the eigenkets of ``L^2`` and ``L_z`` as ``|ℓ, +m\rangle``, where the eigenvalues are ``ℓ(ℓ+1)`` and ``m``, respectively. Finally, define the 𝔇 matrix as (Eq. 3.5.42) ```math -𝔇^{(\ell)}_{m',m}(R) +𝔇^{(ℓ)}_{m',m}(R) = -\langle \ell, m' | 𝔇(R) | \ell, m \rangle. +\langle ℓ, m' | 𝔇(R) | ℓ, m \rangle. ``` Sakurai notes the important result that (Eq. 3.5.46) ```math -𝔇^{(\ell)}_{m'',m}(R_1\, R_2) +𝔇^{(ℓ)}_{m'',m}(R_1\, R_2) = -\sum_{m'} 𝔇^{(\ell)}_{m'',m'}(R_1) 𝔇^{(\ell)}_{m',m}(R_2), +\sum_{m'} 𝔇^{(ℓ)}_{m'',m'}(R_1) 𝔇^{(ℓ)}_{m',m}(R_2), ``` and we can readily find the essential behavior with respect to the first and last Euler angles (Eq. 3.5.50): ```math \begin{aligned} -𝔇^{(\ell)}_{m',m}(\alpha, \beta, \gamma) +𝔇^{(ℓ)}_{m',m}(\alpha, \beta, \gamma) &= -\langle \ell, m' | +\langle ℓ, m' | \exp[-iL_z \alpha]\exp[-iL_y \beta]\exp[-iL_z \gamma] -| \ell, m \rangle \\ +| ℓ, m \rangle \\ &= \exp[-i(m' \alpha+m\gamma)] -\langle \ell, m' | \exp[-iL_y \beta] | \ell, m \rangle. +\langle ℓ, m' | \exp[-iL_y \beta] | ℓ, m \rangle. \end{aligned} ``` To belabor this point, recall that in general @@ -1010,9 +1010,9 @@ the first and last operators. Now we are left with the middle operator, which we use to define ```math \begin{aligned} -d^{(\ell)}_{m',m}(\beta) +d^{(ℓ)}_{m',m}(\beta) &= -\langle \ell, m' | \exp[-iL_y \beta] | \ell, m \rangle. +\langle ℓ, m' | \exp[-iL_y \beta] | ℓ, m \rangle. \end{aligned} ``` Using @@ -1034,7 +1034,7 @@ Now, writing ``d_+(X) = [L_+, X]``, Eq. (9) of https://arxiv.org/pdf/1707.03861 ``` The sum will automatically be zero unless ``m+k-j ≤ ℓ`` — which means ``j ≥ m+k-ℓ`` ```math -(-L₊)^{k-j}|ℓ,m\rangle = (-1)^{k-j} \sqrt{\frac{(\ell+m+k-j)!}{(\ell+m)!}\,\frac{(\ell-m)!}{(\ell-m-k+j)!}} |ℓ,m+k-j\rangle +(-L₊)^{k-j}|ℓ,m\rangle = (-1)^{k-j} \sqrt{\frac{(ℓ+m+k-j)!}{(ℓ+m)!}\,\frac{(ℓ-m)!}{(ℓ-m-k+j)!}} |ℓ,m+k-j\rangle ``` ``[L₊, L₋] = 2 L_z`` @@ -1077,7 +1077,7 @@ presumably simpler to compute. See, e.g., Varshalovich's Eq. - Representation theory of ``\mathbf{Spin}(3)`` - Show how the Lie algebra is represented by the angular-momentum operators - Show how the Lie group is represented by the Wigner D-matrices - - Demonstrate that ``\mathfrak{D}`` is a representation + - Demonstrate that ``𝔇`` is a representation - Demonstrate its behavior under left and right rotation - Demonstrate orthonormality - Representation theory of ``\mathbf{SO}(3)`` @@ -1133,12 +1133,12 @@ recursion relations by differentiating solutions of the Helmholtz equation ``\nabla^2 \psi + k^2 \psi = 0`` as ``\tfrac{1}{k} \nabla \psi``. More precisely, they differentiate both sides of the equation relating one solution to its rotated form — which naturally involves -Wigner's ``\mathfrak{D}`` matrix. Using orthogonal basis functions +Wigner's ``𝔇`` matrix. Using orthogonal basis functions for the solution, this allows them to equate terms on the two sides proportional to a given basis function, which leaves them with -expressions involving sums of only the ``\mathfrak{D}`` matrices and +expressions involving sums of only the ``𝔇`` matrices and some coefficients depending on the indices of the basis functions (and -hence of ``\mathfrak{D}``) on both sides of the equation. Since +hence of ``𝔇``) on both sides of the equation. Since ``\nabla`` is a 3-vector operator, this gives them three relations. This, of course, is happening in 3-D space, since ``\psi`` is a @@ -1154,6 +1154,6 @@ moving off of the sphere. Maybe we'd need to move off of the sphere in 4-D space to get comparable results. Or maybe just use something like ``𝐫 ∧ L``, which should also have 3 degrees of freedom. -The SWSHs/``\mathfrak{D}`` functions can be naturally promoted to +The SWSHs/``𝔇`` functions can be naturally promoted to functions not just on the 3-sphere, but also in 4-D space just by allowing the quaternions to be non-unit quaternions. diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index 44d9a064..e5713249 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -39,7 +39,7 @@ - Representation theory of ``\mathbf{Spin}(3)`` - Show how the Lie algebra is represented by the angular-momentum operators - Show how the Lie group is represented by the Wigner D-matrices - - Demonstrate that ``\mathfrak{D}`` is a representation + - Demonstrate that ``𝔇`` is a representation - Demonstrate its behavior under left and right rotation - Demonstrate orthonormality - Representation theory of ``\mathbf{SO}(3)`` @@ -112,8 +112,8 @@ formula for ``Y``. Then, we can simply follow Wigner around Eq. (15.21) to derived a transformation law in the form ```math -{}_sY_{\ell,m'}(R_{\theta', \phi'}) = \sum_m M_{m',m}(R) -{}_sY_{\ell,m}(R_{\theta, \phi}), +{}_sY_{ℓ,m'}(R_{\theta', \phi'}) = \sum_m M_{m',m}(R) +{}_sY_{ℓ,m}(R_{\theta, \phi}), ``` for some matrix ``M``. Note that I have written this as if the ``{}_sY`` functions are column vectors. The reason this happens is @@ -129,7 +129,7 @@ just consider the inverse rotation, so that they can work with the conjugate transpose, which is why we see the relative conjugate. * Since ``Y`` is universal, let's start with that as non-negotiable, - and see if we can derive the relationship to ``\mathfrak{D}``. + and see if we can derive the relationship to ``𝔇``. * ``R_{\theta, \phi}`` is a unit quaternion that rotates the point described by Cartesian coordinates (0,0,1) onto the point described by spherical coordinates ``(\theta, \phi)``. @@ -140,20 +140,20 @@ conjugate transpose, which is why we see the relative conjugate. for some rotation ``R``. Now, we just need to interpret ``R``. * Again, just textually, it makes the most sense to write ```math - Y_{\ell,m'}(\theta', \phi') = \sum_m \mathfrak{D}^{(\ell)}_{m',m}(R) - Y_{\ell,m}(\theta, \phi), + Y_{ℓ,m'}(\theta', \phi') = \sum_m 𝔇^{(ℓ)}_{m',m}(R) + Y_{ℓ,m}(\theta, \phi), ``` or, generalizing to spin-weighted spherical harmonics ```math - {}_{s}Y_{\ell,m'}(R_{\theta', \phi'}) = \sum_m \mathfrak{D}^{(\ell)}_{m',m}(R) - {}_{s}Y_{\ell,m}(R_{\theta, \phi}). + {}_{s}Y_{ℓ,m'}(R_{\theta', \phi'}) = \sum_m 𝔇^{(ℓ)}_{m',m}(R) + {}_{s}Y_{ℓ,m}(R_{\theta, \phi}). ``` -* We also have that ``\mathfrak{D}`` obeys the representation +* We also have that ``𝔇`` obeys the representation property, so ```math - \mathfrak{D}^{(\ell)}_{m',m''}(R_{\theta', \phi'}) - = \sum_{m} \mathfrak{D}^{(\ell)}_{m',m}(R) - \mathfrak{D}^{(\ell)}_{m,m''}(R_{\theta, \phi}). + 𝔇^{(ℓ)}_{m',m''}(R_{\theta', \phi'}) + = \sum_{m} 𝔇^{(ℓ)}_{m',m}(R) + 𝔇^{(ℓ)}_{m,m''}(R_{\theta, \phi}). ``` - There is no reason that I can see to introduce a conjugation - The fact that ``m''`` appears on both sides of the equation means @@ -161,9 +161,9 @@ conjugate transpose, which is why we see the relative conjugate. behavior under final rotation to determine the sign. ```math -{}_{s}Y_{\ell,m}(R_{\theta, \phi}) +{}_{s}Y_{ℓ,m}(R_{\theta, \phi}) \propto -\mathfrak{D}^{(\ell)}_{m,\propto s}(R_{\theta, \phi}) +𝔇^{(ℓ)}_{m,\propto s}(R_{\theta, \phi}) ``` @@ -202,7 +202,7 @@ The [Condon-Shortley](@cite CondonShortley_1935) phase convention is a choice of phase factors in the definition of the spherical harmonics that requires the coefficients in ```math -L_{\pm} |\ell,m\rangle = \alpha^{\pm}_{\ell,m} |\ell, m \pm 1\rangle +L_{\pm} |ℓ,m\rangle = \alpha^{\pm}_{ℓ,m} |ℓ, m \pm 1\rangle ``` to be real and positive. The reasoning behind this choice is explained more fully in Section 2 of [Ufford and Shortley @@ -211,16 +211,16 @@ Condon-Shortley phase describes signs chosen in the expression for spherical harmonics. The key expression is Eq. (15) of section 4³ (page 52) of [Condon-Shortley](@cite CondonShortley_1935): ```math -\Theta(\ell, m) = (-1)^\ell \sqrt{\frac{2\ell+1}{2} \frac{(\ell+m)!}{(\ell-m)!}} -\frac{1}{2^\ell \ell!} \frac{1}{\sin^m\theta} -\frac{d^{\ell-m}}{d(\cos\theta)^{\ell-m}} \sin^{2\ell}\theta. +\Theta(ℓ, m) = (-1)^ℓ \sqrt{\frac{2ℓ+1}{2} \frac{(ℓ+m)!}{(ℓ-m)!}} +\frac{1}{2^ℓ ℓ!} \frac{1}{\sin^m\theta} +\frac{d^{ℓ-m}}{d(\cos\theta)^{ℓ-m}} \sin^{2ℓ}\theta. ``` When multiplied by Eq. (5) ``\Phi(m) = e^{im\phi} / \sqrt{2\pi}``, this gives the spherical harmonic function. The right-hand side of the expression above is usually immediately replaced by a simpler expression using Legendre polynomials, but this just shifts sign ambiguity into the definition of the Legendre polynomials. Instead, -we can expand the above expression directly for the first few ``\ell`` +we can expand the above expression directly for the first few ``ℓ`` values and/or use automatic differentiation to actually test their original expression as such against the function implemented in this package. The first few values are given in a footnote to Condon and @@ -274,8 +274,8 @@ conventions for the ``𝔇`` matrices, we need to understand conventions for the angular-momentum operators. There is universal agreement that the angular momentum is defined as -``\mathbf{L} = \mathbf{x} \times \mathbf{p}``, where ``\mathbf{x}`` is -the position vector and ``\mathbf{p}`` is the momentum vector. In +``𝐋 = 𝐱 \times 𝐩``, where ``𝐱`` is +the position vector and ``𝐩`` is the momentum vector. In quantum mechanics, there is further agreement that the momentum operator becomes ``-i\hbar\nabla``. Thus, in operator form, the angular momentum can be decomposed as @@ -303,13 +303,13 @@ coefficients. I defined these in Eqs. (42) and (43) of [Boyle (2016)](@cite Boyle_2016) as ```math \begin{aligned} -L_{j} f(\mathbf{R}) &\colonequals -z \left. \frac{\partial}{\partial \theta} -f\left(e^{\theta \mathbf{e}_j / 2} \mathbf{R} \right) \right|_{\theta=0}, \\ -K_{j} f(\mathbf{R}) &\colonequals -z \left. \frac{\partial}{\partial \theta} -f\left(\mathbf{R} e^{\theta \mathbf{e}_j / 2}\right) \right|_{\theta=0}, +L_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial \theta} +f\left(e^{\theta 𝐞_j / 2} 𝐑 \right) \right|_{\theta=0}, \\ +K_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial \theta} +f\left(𝐑 e^{\theta 𝐞_j / 2}\right) \right|_{\theta=0}, \end{aligned} ``` -where ``\mathbf{e}_j`` is the unit vector in the ``j`` direction. +where ``𝐞_j`` is the unit vector in the ``j`` direction. Surprisingly, I found that [Edmonds](@cite Edmonds_2016) expresses essentially the same thing in the equations following his Eq. (4.1.5). @@ -341,46 +341,46 @@ L_z &= -i\hbar \frac{\partial}{\partial\alpha}. ## Wigner ``𝔇`` and ``d`` matrices Wigner's Eqs. (11.18) and (11.19) define the real orthogonal -transformation ``\mathbf{R}`` by +transformation ``𝐑`` by ```math x'_i = R_{ij} x_j ``` -and the operator ``\mathbf{P}_{\mathbf{R}}`` to act on a function +and the operator ``𝐏_{𝐑}`` to act on a function ``f`` such that ```math -\mathbf{P}_{\mathbf{R}} f(x'_1, \ldots) = f(x_1, \ldots). +𝐏_{𝐑} f(x'_1, \ldots) = f(x_1, \ldots). ``` Then, his Eq. (15.5) presumably implies ```math -Y_{\ell,m}(\vartheta', \varphi') -= \mathbf{P}_{\{\alpha, \beta, \gamma\}} Y_{\ell,m}(\vartheta, \varphi) -= \sum_{m'} \mathfrak{D}^{(\ell)}(\{\alpha, \beta, \gamma\})_{m',m} - Y_{\ell,m'}(\vartheta, \varphi), +Y_{ℓ,m}(\vartheta', \varphi') += 𝐏_{\{\alpha, \beta, \gamma\}} Y_{ℓ,m}(\vartheta, \varphi) += \sum_{m'} 𝔇^{(ℓ)}(\{\alpha, \beta, \gamma\})_{m',m} + Y_{ℓ,m'}(\vartheta, \varphi), ``` where ``\{\alpha, \beta, \gamma\}`` takes ``(\vartheta, \varphi)`` to ``(\vartheta', \varphi')``. In any case, we can now leave behind this -``\mathbf{P}`` notation and just look at the beginning and end of the +``𝐏`` notation and just look at the beginning and end of the equation above as the critical relationship in Wigner's notation. Eq. (44b) of [Boyle (2016)](@cite Boyle_2016) says ```math -L_{\pm} \mathfrak{D}^{(\ell)}_{m',m}(\mathbf{R}) -= \sqrt{(\ell \mp m')(\ell \pm m' + 1)} \mathfrak{D}^{(\ell)}_{m' \pm 1, m}(\mathbf{R}). +L_{\pm} 𝔇^{(ℓ)}_{m',m}(𝐑) += \sqrt{(ℓ \mp m')(ℓ \pm m' + 1)} 𝔇^{(ℓ)}_{m' \pm 1, m}(𝐑). ``` while Eq. (21) relates the Wigner D-matrix to the spin-weighted spherical harmonics as ```math -{}_{s}Y_{\ell,m}(\mathbf{R}) -= (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \mathfrak{D}^{(\ell)}_{m,-s}(\mathbf{R}). +{}_{s}Y_{ℓ,m}(𝐑) += (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} 𝔇^{(ℓ)}_{m,-s}(𝐑). ``` Plugging the latter into the former, we get ```math -L_{\pm} {}_{s}Y_{\ell,m}(\mathbf{R}) -= \sqrt{(\ell \mp m)(\ell \pm m + 1)} {}_{s}Y_{\ell,m \pm 1}(\mathbf{R}). +L_{\pm} {}_{s}Y_{ℓ,m}(𝐑) += \sqrt{(ℓ \mp m)(ℓ \pm m + 1)} {}_{s}Y_{ℓ,m \pm 1}(𝐑). ``` That is, in our conventions we have ```math -\alpha^{\pm}_{\ell,m} = \sqrt{(\ell \mp m)(\ell \pm m + 1)}, +\alpha^{\pm}_{ℓ,m} = \sqrt{(ℓ \mp m)(ℓ \pm m + 1)}, ``` which is always real and positive, and thus consistent with the Condon-Shortley phase convention. @@ -410,17 +410,17 @@ d_{m',m}^{j}(-\beta) &= d_{m,m'}^{j}(\beta) = (-1)^{m'-m} d_{m',m}^{j}(\beta ```math \begin{gather} -R = \cos\epsilon + \sin\epsilon\, \hat{\mathfrak{r}} \\ -R𝐯 = \cos\epsilon 𝐯 + \sin\epsilon\, \hat{\mathfrak{r}}𝐯 \\ -R𝐯R^{-1} = (𝐯\cos\epsilon + \sin\epsilon\, \hat{\mathfrak{r}}𝐯)(\cos\epsilon - \sin\epsilon\, \hat{\mathfrak{r}}) \\ -R𝐯R^{-1} = 𝐯\cos^2\epsilon + \sin^2\epsilon\, \hat{\mathfrak{r}}𝐯\hat{\mathfrak{r}}^{-1} + \sin\epsilon \cos\epsilon\, (\hat{\mathfrak{r}}𝐯 - 𝐯\hat{\mathfrak{r}}) \\ +R = \cos\epsilon + \sin\epsilon\, \hat{𝔯} \\ +R𝐯 = \cos\epsilon 𝐯 + \sin\epsilon\, \hat{𝔯}𝐯 \\ +R𝐯R^{-1} = (𝐯\cos\epsilon + \sin\epsilon\, \hat{𝔯}𝐯)(\cos\epsilon - \sin\epsilon\, \hat{𝔯}) \\ +R𝐯R^{-1} = 𝐯\cos^2\epsilon + \sin^2\epsilon\, \hat{𝔯}𝐯\hat{𝔯}^{-1} + \sin\epsilon \cos\epsilon\, (\hat{𝔯}𝐯 - 𝐯\hat{𝔯}) \\ R𝐯R^{-1} = \begin{cases} -𝐯 & 𝐯 \hat{\mathfrak{r}} = \hat{\mathfrak{r}}𝐯 \\ -𝐯(\cos^2\epsilon - \sin^2\epsilon) + 2 \sin\epsilon \cos\epsilon\, \frac{[\hat{\mathfrak{r}}, 𝐯]}{2} & 𝐯 \hat{\mathfrak{r}} = -\hat{\mathfrak{r}}𝐯 \\ +𝐯 & 𝐯 \hat{𝔯} = \hat{𝔯}𝐯 \\ +𝐯(\cos^2\epsilon - \sin^2\epsilon) + 2 \sin\epsilon \cos\epsilon\, \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ \end{cases} \\ R𝐯R^{-1} = \begin{cases} -𝐯 & 𝐯 \hat{\mathfrak{r}} = \hat{\mathfrak{r}}𝐯 \\ -\cos2\epsilon 𝐯 + \sin2\epsilon \frac{[\hat{\mathfrak{r}}, 𝐯]}{2} & 𝐯 \hat{\mathfrak{r}} = -\hat{\mathfrak{r}}𝐯 \\ +𝐯 & 𝐯 \hat{𝔯} = \hat{𝔯}𝐯 \\ +\cos2\epsilon 𝐯 + \sin2\epsilon \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ \end{cases} \\ \end{gather} ``` diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index 5849e79e..d0b9ae8c 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -205,8 +205,8 @@ package defines the spherical harmonics in terms of Wigner's 𝔇 matrices, by way of the spin-weighted spherical harmonics, as a function of a quaternion: ```math -Y_{l,m}(\mathbf{Q}) = \sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} - D^{(l)}_{m,0}(\mathbf{Q}), +Y_{l,m}(𝐐) = \sqrt{\frac{2ℓ+1}{4\pi}} e^{im\phi} + D^{(l)}_{m,0}(𝐐), ``` where ``D^{(l)}_{m,0}`` is the Wigner 𝔇 matrix. This is a @@ -219,17 +219,17 @@ terms of spherical coordinates, that expression is \begin{align} Y_{l,m} &= - \sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + \sqrt{\frac{2ℓ+1}{4\pi}} e^{im\phi} \sum_{k = k_1}^{k_2} - \frac{(-1)^k \ell! [(\ell+m)!(\ell-m)!]^{1/2}} - {(\ell+m-k)!(\ell-k)!k!(k-m)!} + \frac{(-1)^k ℓ! [(ℓ+m)!(ℓ-m)!]^{1/2}} + {(ℓ+m-k)!(ℓ-k)!k!(k-m)!} \\ &\qquad \times - \left(\cos\left(\frac{\theta}{2}\right)\right)^{2\ell+m-2k} + \left(\cos\left(\frac{\theta}{2}\right)\right)^{2ℓ+m-2k} \left(\sin\left(\frac{\theta}{2}\right)\right)^{2k-m} \end{align} ``` -where ``k_1 = \textrm{max}(0, m)`` and ``k_2=\textrm{min}(\ell+m, -\ell)``. Again, we must emphasize that this package does not actually +where ``k_1 = \textrm{max}(0, m)`` and ``k_2=\textrm{min}(ℓ+m, +ℓ)``. Again, we must emphasize that this package does not actually use this form; it is just shown here to make it easier to compare to other sources. @@ -287,7 +287,7 @@ We can make this a little less dependent on the choice of Euler angles by writing ``\eta`` not as a function of Euler angles, but as a function of a quaternion. We then have ```math -\eta(\mathbf{Q}\, e^{\gamma 𝐤/2}) = e^{-is\gamma} \eta(\mathbf{Q}), +\eta(𝐐\, e^{\gamma 𝐤/2}) = e^{-is\gamma} \eta(𝐐), ``` which means that spin-weighted functions are eigenfunctions of the operator ``R_𝐤`` with eigenvalue ``s``. @@ -330,17 +330,17 @@ of comparison with other sources. The expression is \begin{align} {}_{s}Y_{l,m} &= - (-1)^s\sqrt{\frac{2\ell+1}{4\pi}} e^{im\phi} + (-1)^s\sqrt{\frac{2ℓ+1}{4\pi}} e^{im\phi} \sum_{k = k_1}^{k_2} - \frac{(-1)^k[(\ell+m)!(\ell-m)!(\ell-s)!(\ell+s)!]^{1/2}} - {(\ell+m-k)!(\ell+s-k)!k!(k-s-m)!} + \frac{(-1)^k[(ℓ+m)!(ℓ-m)!(ℓ-s)!(ℓ+s)!]^{1/2}} + {(ℓ+m-k)!(ℓ+s-k)!k!(k-s-m)!} \\ &\qquad \times - \left(\cos\left(\frac{\iota}{2}\right)\right)^{2\ell+m+s-2k} + \left(\cos\left(\frac{\iota}{2}\right)\right)^{2ℓ+m+s-2k} \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-s-m} \end{align} ``` -where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(\ell+m, -\ell+s)``. +where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(ℓ+m, +ℓ+s)``. ## Wigner D-matrices diff --git a/docs/src/index.md b/docs/src/index.md index a4346ae1..cd93cdf5 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -18,7 +18,7 @@ This is a Julia package for evaluating and transforming Wigner's 𝔇 -matrices, and spin-weighted spherical harmonics ``{}_{s}Y_{\ell,m}`` +matrices, and spin-weighted spherical harmonics ``{}_{s}Y_{ℓ,m}`` (which includes the ordinary scalar spherical harmonics). Because [*both* 𝔇 *and* the harmonics are most correctly considered](@cite Boyle_2016) functions on the rotation group ``𝐒𝐎(3)`` — or more diff --git a/docs/src/index_of_docstrings.md b/docs/src/index_of_docstrings.md new file mode 100644 index 00000000..5671b5a4 --- /dev/null +++ b/docs/src/index_of_docstrings.md @@ -0,0 +1,8 @@ +# Index of docstrings + +Here, we collect links to all docstrings provided throughout the +documentation. + +```@index +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +``` diff --git a/docs/src/notes/H_recurrence.md b/docs/src/notes/H_recurrence.md index 286bf15f..7c320e54 100644 --- a/docs/src/notes/H_recurrence.md +++ b/docs/src/notes/H_recurrence.md @@ -1,11 +1,11 @@ # Algorithm for computing ``H`` (redesigned) The ``H`` array, as given by [Gumerov_2015](@citet), is related to Wigner's (small) ``d`` matrices — -which is itself related to the (big) ``\mathfrak{D}`` matrices and the various spin-weighted -spherical harmonics ``{}_{s}Y_{\ell,m}`` — via +which is itself related to the (big) ``𝔇`` matrices and the various spin-weighted +spherical harmonics ``{}_{s}Y_{ℓ,m}`` — via ```math -d_{\ell}^{m',m} = \epsilon_{m'} \epsilon_{-m} H^{\ell}_{m',m}, +d_{ℓ}^{m',m} = \epsilon_{m'} \epsilon_{-m} H^{ℓ}_{m',m}, ``` where @@ -22,16 +22,16 @@ where compared to Gumerov and Duraiswami's paper, to be consistent with the rest of this documentation.) -``H`` has various advantages over ``d`` and ``\mathfrak{D}``, +``H`` has various advantages over ``d`` and ``𝔇``, including the fact that it can be efficiently and robustly calculated via recurrence relations, and the following symmetry relations: ```math \begin{aligned} - H_{m', m}^\ell(β) &= H_{m, m'}^\ell(β) \\ - H_{m', m}^\ell(β) &= H_{-m', -m}^\ell(β) \\ - H_{m', m}^\ell(β) &= (-1)^{\ell+m+m'} H_{-m', m}^\ell(π - β) \\ - H_{m', m}^\ell(β) &= (-1)^{m+m'} H_{m', m}^\ell(-β) + H_{m', m}^ℓ(β) &= H_{m, m'}^ℓ(β) \\ + H_{m', m}^ℓ(β) &= H_{-m', -m}^ℓ(β) \\ + H_{m', m}^ℓ(β) &= (-1)^{ℓ+m+m'} H_{-m', m}^ℓ(π - β) \\ + H_{m', m}^ℓ(β) &= (-1)^{m+m'} H_{m', m}^ℓ(-β) \end{aligned} ``` @@ -47,7 +47,7 @@ correctly by [Gumerov_2015](@citet). All equation numbers refer to that paper unless otherwise noted. Because of the symmetries noted above, we only compute ``H_{m', -m}^\ell`` with ``m ≥ |m'|`` — roughly one quarter of all possible +m}^ℓ`` with ``m ≥ |m'|`` — roughly one quarter of all possible values. Furthermore, for spin-weighted spherical harmonics of weight ``s``, we only need to compute values with ``|m'| ≤ |s|``, which constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. @@ -58,17 +58,17 @@ constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. Set ``H^{0}_{0,0}=1``. -### Step 2: ``H_{0,m}^{\ell} \to H_{0,m}^{\ell+1}`` for ``m \geq 0`` +### Step 2: ``H_{0,m}^{ℓ} \to H_{0,m}^{ℓ+1}`` for ``m \geq 0`` -### Step 3: ``H_{0,m}^{\ell+1} \to H_{1,m}^{\ell}`` for ``m \geq 0`` +### Step 3: ``H_{0,m}^{ℓ+1} \to H_{1,m}^{ℓ}`` for ``m \geq 0`` -### Step 4: ``H_{m',m-1}^{\ell}, H_{m'-1,m}^{\ell}, H_{m',m+1}^{\ell} \to H_{m'+1,m}^{\ell}`` for ``m' > 1`` and ``m > m'`` +### Step 4: ``H_{m',m-1}^{ℓ}, H_{m'-1,m}^{ℓ}, H_{m',m+1}^{ℓ} \to H_{m'+1,m}^{ℓ}`` for ``m' > 1`` and ``m > m'`` -### Step 5: ``H_{m',m-1}^{\ell}, H_{m'+1,m}^{\ell}, H_{m',m+1}^{\ell} \to H_{m'-1,m}^{\ell}`` for ``m' \leq 0`` and ``m > -m'`` +### Step 5: ``H_{m',m-1}^{ℓ}, H_{m'+1,m}^{ℓ}, H_{m',m+1}^{ℓ} \to H_{m'-1,m}^{ℓ}`` for ``m' \leq 0`` and ``m > -m'`` ### Step 6: Use symmetries to fill in the rest of ``H`` -### Step 7: Include phases to obtain ``d`` or ``\mathfrak{D}`` +### Step 7: Include phases to obtain ``d`` or ``𝔇`` Compute values ``H^{0,m}_{n}(β)`` for ``m=0,\ldots,n`` and diff --git a/docs/src/notes/H_recursions.md b/docs/src/notes/H_recursions.md index 5f90b693..72e3c15b 100644 --- a/docs/src/notes/H_recursions.md +++ b/docs/src/notes/H_recursions.md @@ -1,11 +1,11 @@ # Algorithm for computing ``H`` The ``H`` array, as given by [Gumerov_2015](@citet), is related to Wigner's (small) ``d`` matrices — -which is itself related to the (big) ``\mathfrak{D}`` matrices and the various spin-weighted -spherical harmonics ``{}_{s}Y_{\ell,m}`` — via +which is itself related to the (big) ``𝔇`` matrices and the various spin-weighted +spherical harmonics ``{}_{s}Y_{ℓ,m}`` — via ```math -d_{\ell}^{m',m} = \epsilon_{m'} \epsilon_{-m} H_{\ell}^{m',m}, +d_{ℓ}^{m',m} = \epsilon_{m'} \epsilon_{-m} H_{ℓ}^{m',m}, ``` where @@ -46,7 +46,7 @@ to compute values with ``|m'| ≤ |s|``, which constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. The data are stored in the array `Hwedge`. However, some parts of this calculation require calculating terms with -``m=n+1`` — whereas such elements of ``d`` and ``\mathfrak{D}`` are considered +``m=n+1`` — whereas such elements of ``d`` and ``𝔇`` are considered zero. For this purpose, we need additional storage. Rather than allocating extra space, or requiring some additional workspace to be passed in, we can actually use parts of the input ``H`` data space for temporary storage while diff --git a/docs/src/notes/sampling_theorems.md b/docs/src/notes/sampling_theorems.md index 6495b635..f2b5d3ff 100644 --- a/docs/src/notes/sampling_theorems.md +++ b/docs/src/notes/sampling_theorems.md @@ -26,29 +26,29 @@ changes). We begin by defining {}_{s}\tilde{f}_{\theta}(m) := \int_0^{2\pi} {}_sf(\theta, \phi)\, e^{-im\phi}\, d\phi. ``` We will denote the vector of these quantities for all values of -``\theta`` as ``{}_{s}\tilde{\mathbf{f}}_m``. Inserting the -``{}_sY_{\ell,m}`` expansion for ``{}_sf(\theta, \phi)``, and +``\theta`` as ``{}_{s}\tilde{𝐟}_m``. Inserting the +``{}_sY_{ℓ,m}`` expansion for ``{}_sf(\theta, \phi)``, and performing the integration using orthogonality of complex exponentials, we can find that ```math - {}_{s}\tilde{f}_{\theta}(m) = (-1)^s\, 2\pi \sum_{\ell=\Delta}^L \sqrt{\frac{2\ell+1}{4\pi}}\, d_{m,-s}^{\ell}(\theta)\, {}_sf_{\ell,m}. + {}_{s}\tilde{f}_{\theta}(m) = (-1)^s\, 2\pi \sum_{ℓ=\Delta}^L \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{m,-s}^{ℓ}(\theta)\, {}_sf_{ℓ,m}. ``` -Now, denoting the vector of ``{}_sf_{\ell,m}`` for all values of -``\ell`` as ``{}_s\mathbf{f}_m``, we can write this as a matrix-vector +Now, denoting the vector of ``{}_sf_{ℓ,m}`` for all values of +``ℓ`` as ``{}_s𝐟_m``, we can write this as a matrix-vector equation: ```math - {}_{s}\tilde{\mathbf{f}}_m = (-1)^s\, 2\pi\, {}_s\mathbf{d}_{m}\, {}_s\mathbf{f}_m. + {}_{s}\tilde{𝐟}_m = (-1)^s\, 2\pi\, {}_s𝐝_{m}\, {}_s𝐟_m. ``` -We are effectively measuring the ``{}_{s}\tilde{\mathbf{f}}_m`` -values, we can easily construct the ``{}_s\mathbf{d}_{m}`` matrix, and -we are seeking the ``{}_s\mathbf{f}_m`` values, so we can just invert +We are effectively measuring the ``{}_{s}\tilde{𝐟}_m`` +values, we can easily construct the ``{}_s𝐝_{m}`` matrix, and +we are seeking the ``{}_s𝐟_m`` values, so we can just invert this equation to solve for the latter. ## Discretizing the Fourier transform Now, the only flaw in this analysis is that we have undersampled -everywhere except ``\ell = L``, which means that the second equation +everywhere except ``ℓ = L``, which means that the second equation (re-expressing the Fourier transforms as a sum using orthogonality of complex exponentials) isn't quite right; in general there is some folding due to aliasing of higher-frequency modes, so we need an @@ -68,9 +68,9 @@ expansion for ``{}_sf(\theta, \phi)``: ```math \begin{aligned} {}_{s}\tilde{f}_{j}(m) - &= \sum_{k=0}^{2j} \sum_{\ell,m'} {}_sf_{\ell,m'}\, {}_sY_{\ell,m'}(\theta_j, \phi_k)\, e^{-im\phi_k}\, \Delta \phi \\ - &= \sum_{k=0}^{2j} \sum_{\ell,m'} {}_sf_{\ell,m'}\, (-1)^{s}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j) e^{i m' \phi_k}\, e^{-im\phi_k}\, \frac{2\pi}{2j+1} \\ - &= (-1)^{s}\, \frac{2\pi}{2j+1} \sum_{\ell,m'} {}_sf_{\ell,m'}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j) \sum_{k=0}^{2j}e^{i (m'-m) \phi_k}. + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, {}_sY_{ℓ,m'}(\theta_j, \phi_k)\, e^{-im\phi_k}\, \Delta \phi \\ + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, (-1)^{s}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(\theta_j) e^{i m' \phi_k}\, e^{-im\phi_k}\, \frac{2\pi}{2j+1} \\ + &= (-1)^{s}\, \frac{2\pi}{2j+1} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(\theta_j) \sum_{k=0}^{2j}e^{i (m'-m) \phi_k}. \end{aligned} ``` We can evaluate this last sum easily: @@ -84,35 +84,35 @@ This allows us to simplify as ```math \begin{aligned} - {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{\ell,m'} {}_sf_{\ell,m'}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j), + {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(\theta_j), \end{aligned} ``` -where ``m'`` ranges over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq \ell`` +where ``m'`` ranges over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq ℓ`` — that is, all ``n\in \mathbb{Z}`` such that ```math - \left \lceil \frac{-\ell-m}{2j+1} \right \rceil \leq n \leq \left \lfloor \frac{\ell-m}{2j+1} \right \rfloor. + \left \lceil \frac{-ℓ-m}{2j+1} \right \rceil \leq n \leq \left \lfloor \frac{ℓ-m}{2j+1} \right \rfloor. ``` ## Matrix representation -Usually, we would take the sum over ``\ell`` ranging from ``\mathrm{max}(|m|,|s|)`` to ``L``, and the sum -over ``m'`` ranging over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq \ell``. +Usually, we would take the sum over ``ℓ`` ranging from ``\mathrm{max}(|m|,|s|)`` to ``L``, and the sum +over ``m'`` ranging over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq ℓ``. However, we can also consider these sums to range over all possible -values of ``\ell, m'``, and just set the coefficient to zero whenever +values of ``ℓ, m'``, and just set the coefficient to zero whenever these conditions are not satisfied. In that case, we can again think of this as a (much larger) vector-matrix equation reading ```math - {}_s\tilde{\mathbf{f}} = (-1)^s\, 2\pi\, {}_s\mathbf{d}\, {}_s\mathbf{f}, + {}_s\tilde{𝐟} = (-1)^s\, 2\pi\, {}_s𝐝\, {}_s𝐟, ``` -where the index on ``{}_s\tilde{\mathbf{f}}`` loops over ``j`` and -``m``, the index on ``{}_s\mathbf{f}`` loops over ``\ell`` and ``m'``, -and the indices on ``{}_s\mathbf{d}`` loop over each of those pairs. +where the index on ``{}_s\tilde{𝐟}`` loops over ``j`` and +``m``, the index on ``{}_s𝐟`` loops over ``ℓ`` and ``m'``, +and the indices on ``{}_s𝐝`` loop over each of those pairs. ## De-aliasing -While it is *far* simpler to simply invert the full ``{}_s\mathbf{d}`` +While it is *far* simpler to simply invert the full ``{}_s𝐝`` matrix, its size scales as ``L^4``, which means that it very quickly becomes impractical to store and manipulate the full matrix. In CMB astronomy, for example, it is not uncommon to use ``L`` into the tens diff --git a/docs/src/operators.md b/docs/src/operators.md new file mode 100644 index 00000000..22fc2d22 --- /dev/null +++ b/docs/src/operators.md @@ -0,0 +1,254 @@ +# Differential operators + +Spin-weighted spherical functions *cannot* be defined on the sphere +``𝕊²``, but are well defined on the sphere ``𝕊³`` or its projective +version ``ℝℙ³``. See [Boyle_2016](@citet) for the explanation. These +are the spaces underlying the Lie groups ``\mathrm{Spin}(3) \cong +\mathrm{SU}(2)`` or its projective version ``\mathrm{SO}(3)``. As a +result, we can define a variety of differential operators acting on +these functions, relating to infinitesimal motions in these groups, +acting either from the left or the right on their arguments. Right or +left matters because the groups mentioned above are all +non-commutative groups. + +In general, the *left* Lie derivative of a function ``f(Q)`` over the +unit quaternions with respect to a generator of rotation ``g`` is +defined as +```math +L_g(f)\{Q\} := -\frac{i}{2} + \left. \frac{df\left(\exp(t\,g)\, Q\right)}{dt} \right|_{t=0}. +``` +Note that the exponential multiplies ``Q`` *on the left* — hence the +name. We will see below that this agrees with the usual definition of +the angular-momentum from physics, except that in *quantum* physics a +factor of ``\hbar`` is usually included. + +So, for example, a rotation about the ``z`` axis has the quaternion +``z`` as its generator of rotation, and ``L_z`` defined in this way +agrees with [the usual angular-momentum +operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) +``L_z`` familiar from spherical-harmonic theory, and reduces to it +when the function has spin weight 0, but also applies to functions of +general spin weight. Similarly, we can compute ``L_x`` and ``L_y``, +and take appropriate combinations to find [the usual raising and +lowering (ladder) +operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) +``L_+`` and ``L_-``. + +In just the same way, we can define the *right* Lie derivative of a +function ``f(Q)`` over the unit quaternions with respect to a +generator of rotation ``g`` as +```math +R_g(f)\{Q\} := -\frac{i}{2} + \left. \frac{df\left(Q\, \exp(t\,g)\right)}{dt} \right|_{t=0}. +``` +Note that the exponential multiplies ``Q`` *on the right* — hence the +name. + +This operator is less common in physics, because it represents the +dependence of the function on the choice of frame (or coordinate +system), which is not usually of interest. Multiplication on the left +represents a rotation of the physical system, while rotation on the +right represents a rotation of the coordinate system. However, this +dependence on coordinate system is precisely what defines the *spin +weight* of a function, so this class of operators is relevant in +discussions of spin-weighted spherical functions. In particular, the +operators ``R_\pm`` correspond (up to a sign) to the spin-raising and +-lowering operators ``\eth`` and ``\bar{\eth}`` originally introduced +by [Newman_1966](@citet), as explained in greater detail by +[Boyle_2016](@citet). + +Note that these definitions are *extremely* general, in that they can +be used for *any* Lie group, and for any complex-valued function on +that group. And in full generality, we have the useful properties of +linearity: +```math +L_{s𝐚} = sL_{𝐚} +\qquad \text{and} \qquad +R_{s𝐚} = sR_{𝐚}, +``` +and +```math +L_{𝐚+𝐛} = L_{𝐚} + L_{𝐛} +\qquad \text{and} \qquad +R_{𝐚+𝐛} = R_{𝐚} + R_{𝐛}, +``` +for any scalar ``s`` and any elements of the Lie algebra +``𝐚`` and ``𝐛``. In particular, if the Lie algebra +has a basis ``𝐞_{(j)}``, we use the shorthand ``L_j`` and +``R_j`` for ``L_{𝐞_{(j)}}`` and ``R_{𝐞_{(j)}}``, +respectively, and we can expand any operator in terms of these basis +operators: +```math +L_{𝐚} = \sum_{j} a_j L_j +\qquad \text{and} \qquad +R_{𝐚} = \sum_{j} a_j R_j. +``` + + +## Commutators + +In general, for generators ``a`` and ``b``, we have the commutator +relations +```math +\left[ L_a, L_b \right] = \frac{i}{2} L_{[a,b]} +\qquad +\left[ R_a, R_b \right] = -\frac{i}{2} R_{[a,b]}, +``` +where ``[a,b]`` is the commutator of the two generators, which can be +obtained directly as the commutator of the corresponding quaternions. +Note the sign difference between these two equations. The factors of +``\pm i/2`` are inherited directly from the definitions of ``L_g`` and +``R_g`` given above, but they appear there with the *same* sign. The +sign difference between these two commutator equations results from +the fact that the quaternions are multiplied in opposite orders in the +two cases. It *could* be absorbed by defining the operators with +opposite signs.[^1] The arbitrary sign choices used above are purely +for historical reasons. + +Again, these results are valid for general (finite-dimensional) Lie +groups, but a particularly interesting case is in application to the +three-dimensional rotation group. In the following, we will apply our +results to this group. + +The commutator relations for ``L`` are consistent — except for the +differing use of ``\hbar`` — with the usual relations from quantum +mechanics: +```math +\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. +``` +Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and +index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we +represent an arbitrary basis vector as ``\hat{e}_j``, then the +quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` +becomes +```math +[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. +``` +Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} +L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version +frequently seen in quantum physics. + + +[^1]: + In fact, we can define the left and right Lie derivative operators + quite generally, for functions on *any* Lie group and for the + corresponding Lie algebra. And in all cases (at least for + finite-dimensional Lie algebras) we obtain the same commutator + relations. The only potential difference is that it may not make + sense to use the coefficient ``i/2`` in general; it was chosen + here for consistency with the standard angular-momentum operators. + If that coefficient is changed in the definitions of the Lie + derivatives, the only change to the commutator relations would the + substitution of that coefficient. + +The raising and lowering operators relative to ``L_z`` and ``R_z`` +satisfy — by definition of raising and lowering operators — the +relations +```math +[L_z, L_\pm] = \pm L_\pm +\qquad +[R_z, R_\pm] = \pm R_\pm. +``` +These allow us to solve — up to an overall factor — for those +operators in terms of the basic generators (again, noting the sign +difference): +```math +L_\pm = L_x \pm i L_y +\qquad +R_\pm = R_x \mp i R_y. +``` +(Interestingly, this procedure also shows that rasing and lowering +operators can only exist if the factor in front of the derivatives in +the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In +particular, this results in the commutator relations +```math +[L_+, L_-] = 2L_z +\qquad +[R_+, R_-] = 2R_z. +``` +Here, the signs are *similar* because the two sign differences noted +above essentially cancel each other out. + +In the functions [listed below](#Module-functions), these operators +are returned as matrices acting on vectors of mode weights. As such, +we can actually evaluate these commutators as given to cross-validate +the expressions and those functions. + + +## Transformations of functions vs. mode weights + +One important point to note is that mode weights transform +"contravariantly" (very loosely speaking) relative to the +spin-weighted spherical functions under some operators. For example, +take the action of the ``L_+`` operator, which acts on a SWSH as +```math +L_+ \left\{{}_{s}Y_{ℓ,m}\right\} (R) = \sqrt{(ℓ-m)(ℓ+m+1)} {}_{s}Y_{ℓ,m+1}(R). +``` +We can use this to derive the mode weights of a general spin-weighted +function ``f`` under the action of this operator:[^2] +```math +\begin{aligned} +\left\{L_+ f\right\}_{ℓ,m} +&= +\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{L_+ \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \sum_{ℓ',m'} f_{ℓ',m'}\, \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\sqrt{(ℓ'-m')(ℓ'+m'+1)} {}_{s}Y_{ℓ',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \delta_{ℓ,ℓ'} \delta_{m,m'+1} \\ +&= +f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} +\end{aligned} +``` +Note that this expression (and in particular its signs) more resembles +the expression for ``L_- \left\{{}_{s}Y_{ℓ,m}\right\}`` than for +``L_+ \left\{{}_{s}Y_{ℓ,m}\right\}``. Similar relations hold for +the action of ``L_-``. + +[^2]: + A technical note about the integrals above: the integrals should be taken + over the appropriate space and with the appropriate weight such that the + SWSHs are orthonormal. In general, this integral should be over + ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be + either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when + integrated over an ``𝕊²`` subspace (nor even is the integral invariant). + However, if we know that the spins are the same in both cases, it *is* + possible to integrate over an ``𝕊²`` subspace. + +However, it is important to note that the same "contravariance" is not +present for the spin-raising and -lowering operators: +```math +\begin{aligned} +\left\{\eth f\right\}_{ℓ,m} +&= +\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{\eth \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\eth {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \delta_{ℓ,ℓ'} \delta_{m,m'} \\ +&= +f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} +\end{aligned} +``` +Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more +"covariant" form of transformation. + + +## Docstrings + +```@autodocs +Modules = [SphericalFunctions] +Pages = ["operators.jl"] +``` diff --git a/src/deprecated/evaluate.jl b/src/deprecated/evaluate.jl index df50c046..c73cb11d 100644 --- a/src/deprecated/evaluate.jl +++ b/src/deprecated/evaluate.jl @@ -14,9 +14,9 @@ using Quaternionic: Quaternionic, AbstractQuaternion, to_euler_phases d_matrices(β, ℓₘₐₓ) d_matrices(expiβ, ℓₘₐₓ) -Compute Wigner's ``d^{(\ell)}`` matrices with elements ``d^{(\ell)}_{m',m}(\beta)`` for all -``\ell \leq \ell_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" -Wigner matrices, in contrast to the full ``\mathfrak{D}`` matrices. +Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(\beta)`` for all +``ℓ \leq ℓ_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" +Wigner matrices, in contrast to the full ``𝔇`` matrices. See [`d_matrices!`](@ref) for details about the input and output values. @@ -35,9 +35,9 @@ d_matrices(β::Real, ℓₘₐₓ) = d_matrices(cis(β), ℓₘₐₓ) d_matrices!(d, β, ℓₘₐₓ) d_matrices!(d, expiβ, ℓₘₐₓ) -Compute Wigner's ``d^{(\ell)}`` matrices with elements ``d^{(\ell)}_{m',m}(\beta)`` for all -``\ell \leq \ell_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" -Wigner matrices, in contrast to the full ``\mathfrak{D}`` matrices. +Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(\beta)`` for all +``ℓ \leq ℓ_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" +Wigner matrices, in contrast to the full ``𝔇`` matrices. In all cases, the result is returned in a 1-dimensional array ordered as @@ -188,7 +188,7 @@ package's Wigner ``d`` function and other references' more clear. It is ineffic in terms of memory and computation time, and should generally not be used in production code. -Computes a single (complex) value of the ``d`` matrix ``(\ell, m', m)`` at the given +Computes a single (complex) value of the ``d`` matrix ``(ℓ, m', m)`` at the given angle ``(\iota)``. """ function d(ℓ, m′, m, β) @@ -200,8 +200,8 @@ end D_matrices(R, ℓₘₐₓ) D_matrices(α, β, γ, ℓₘₐₓ) -Compute Wigner's 𝔇 matrices ``\mathfrak{D}^{(\ell)}_{m',m}(\beta)`` for all ``\ell \leq -\ell_\mathrm{max}``. +Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(\beta)`` for all ``ℓ \leq +ℓ_\mathrm{max}``. See [`D_matrices!`](@ref) for details about the input and output values. @@ -230,8 +230,8 @@ end D_matrices!(D, R, ℓₘₐₓ) D_matrices!(D, α, β, γ, ℓₘₐₓ) -Compute Wigner's 𝔇 matrices ``\mathfrak{D}^{(\ell)}_{m',m}(\beta)`` for all ``\ell \leq -\ell_\mathrm{max}``. +Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(\beta)`` for all ``ℓ \leq +ℓ_\mathrm{max}``. In all cases, the result is returned in a 1-dimensional array ordered as @@ -298,7 +298,7 @@ end D_prep(ℓₘₐₓ, [T=Float64]) Construct storage space and pre-compute recursion coefficients to compute Wigner's -``\mathfrak{D}`` matrix in place. +``𝔇`` matrix in place. This returns the `D_storage` arguments needed by [`D_matrices!`](@ref). @@ -395,7 +395,7 @@ package's Wigner ``D`` function and other references' more clear. It is ineffic in terms of memory and computation time, and should generally not be used in production code. -Computes a single (complex) value of the ``D`` matrix ``(\ell, m', m)`` at the given +Computes a single (complex) value of the ``D`` matrix ``(ℓ, m', m)`` at the given angle ``(\iota)``. """ function D(ℓ, m′, m, α, β, γ) @@ -414,12 +414,12 @@ end sYlm_values(R, ℓₘₐₓ, spin) sYlm_values(θ, ϕ, ℓₘₐₓ, spin) -Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{\ell, m}(R)`` for all -``\ell \leq \ell_\mathrm{max}``. +Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{ℓ, m}(R)`` for all +``ℓ \leq ℓ_\mathrm{max}``. See [`sYlm_values!`](@ref) for details about the input and output values. -This function only appropriate when you need to evaluate the ``{}_{s}Y_{\ell, m}`` for a +This function only appropriate when you need to evaluate the ``{}_{s}Y_{ℓ, m}`` for a single value of `R` or `θ, ϕ` because it allocates large arrays and performs many calculations that could be reused. If you need to evaluate the matrices for many values of `R` or `θ, ϕ`, you should pre-allocate the storage with [`sYlm_prep`](@ref), and then call @@ -442,16 +442,16 @@ end sYlm_values!(sYlm, R, ℓₘₐₓ, spin) sYlm_values!(sYlm, θ, ϕ, ℓₘₐₓ, spin) -Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{\ell, m}(R)`` for all -``\ell \leq \ell_\mathrm{max}``. +Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{ℓ, m}(R)`` for all +``ℓ \leq ℓ_\mathrm{max}``. -The spherical harmonics of spin weight ``s`` are related to Wigner's ``\mathfrak{D}`` matrix +The spherical harmonics of spin weight ``s`` are related to Wigner's ``𝔇`` matrix as ```math \begin{aligned} -{}_{s}Y_{\ell, m}(R) - &= (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \mathfrak{D}^{(\ell)}_{m, -s}(R) \\ - &= (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \bar{\mathfrak{D}}^{(\ell)}_{-s, m}(\bar{R}). +{}_{s}Y_{ℓ, m}(R) + &= (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} 𝔇^{(ℓ)}_{m, -s}(R) \\ + &= (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} \bar{𝔇}^{(ℓ)}_{-s, m}(\bar{R}). \end{aligned} ``` @@ -480,7 +480,7 @@ The `θ, ϕ` arguments are spherical coordinates as described in the documentati [`Quaternionic.from_spherical_coordinates`](https://moble.github.io/Quaternionic.jl/dev/manual/#Quaternionic.from_spherical_coordinates-Tuple{Any,%20Any}). See also [`sYlm_values`](@ref) for a simpler function call when you only need to evaluate -the ``{}_{s}Y_{\ell, m}`` for a single value of `R` or `θ, ϕ`. +the ``{}_{s}Y_{ℓ, m}`` for a single value of `R` or `θ, ϕ`. # Examples @@ -540,14 +540,14 @@ end sYlm_prep(ℓₘₐₓ, sₘₐₓ, [T=Float64, [ℓₘᵢₙ=0]]) Construct storage space and pre-compute recursion coefficients to compute spin-weighted -spherical-harmonic values ``{}_{s}Y_{\ell, m}`` in place. +spherical-harmonic values ``{}_{s}Y_{ℓ, m}`` in place. This returns the `sYlm_storage` arguments needed by [`sYlm_values!`](@ref). Note that the result of this function can be passed to `sYlm_values!`, even if the value of `spin` passed to that function is smaller (in absolute value) than the `sₘₐₓ` passed to this function. That is, the `sYlm_storage` returned by this function can be used to compute -``{}_{s}Y_{\ell, m}`` values for numerous values of the spin. +``{}_{s}Y_{ℓ, m}`` values for numerous values of the spin. """ function sYlm_prep(ℓₘₐₓ, sₘₐₓ, ::Type{T}=Float64, ℓₘᵢₙ=0) where {T<:Real} @@ -611,7 +611,7 @@ end ₛ𝐘(s, ℓₘₐₓ, [T=Float64], [Rθϕ=golden_ratio_spiral_rotors(s, ℓₘₐₓ, T)]) -Construct a matrix of ``ₛYₗₘ(Rθϕ)`` values for the input `s` and all nontrivial ``(\ell, +Construct a matrix of ``ₛYₗₘ(Rθϕ)`` values for the input `s` and all nontrivial ``(ℓ, m)`` up to `ℓₘₐₓ`. This is a fast and accurate method for mapping between the vector of spin-weighted @@ -655,7 +655,7 @@ NOTE: This function is primarily a test function just to make comparisons betwee package's spherical harmonics and other references' more clear. It is inefficient, both in terms of memory and computation time, and should generally not be used in production code. -Computes a single (complex) value of the spherical harmonic ``(\ell, m)`` at the given +Computes a single (complex) value of the spherical harmonic ``(ℓ, m)`` at the given spherical coordinate ``(\theta, \phi)``. """ function Y(s::Int, ℓ::Int, m::Int, θ, ϕ) diff --git a/src/deprecated/iterators.jl b/src/deprecated/iterators.jl index e2506e9b..d60460ae 100644 --- a/src/deprecated/iterators.jl +++ b/src/deprecated/iterators.jl @@ -261,14 +261,14 @@ Base.size(Yi::sYlm_iterator, dim) = dim > 1 ? 1 : length(Yi) λ_recursion_initialize(cosθ, sin½θ, cos½θ, s, ℓ, m) This provides initial values for the recursion to find -``{}_{s}\lambda_{\ell,m}`` along indices of increasing ``\ell``, due to +``{}_{s}\lambda_{ℓ,m}`` along indices of increasing ``ℓ``, due to [Kostelec & Rockmore](@cite Kostelec_2008) Specifically, this function computes -values with ``\ell=m``. +values with ``ℓ=m``. ```math -{}_{s}\lambda_{\ell,m}(\theta) - := {}_{s}Y_{\ell,m}(\theta, 0) - = (-1)^m\, \sqrt{\frac{2\ell+1}{4\pi}} d^\ell_{-m,s}(\theta) +{}_{s}\lambda_{ℓ,m}(\theta) + := {}_{s}Y_{ℓ,m}(\theta, 0) + = (-1)^m\, \sqrt{\frac{2ℓ+1}{4\pi}} d^ℓ_{-m,s}(\theta) ``` """ function λ_recursion_initialize(sin½θ::T, cos½θ::T, s, ℓ, m) where T diff --git a/src/deprecated/map2salm.jl b/src/deprecated/map2salm.jl index 5d032da2..1ddaddf3 100644 --- a/src/deprecated/map2salm.jl +++ b/src/deprecated/map2salm.jl @@ -2,7 +2,7 @@ map2salm(map, spin, ℓmax) map2salm(map, plan) -Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes. +Transform `map` values sampled on the sphere to ``{}_sa_{ℓ, m}`` modes. The `map` array should have size Nφ along its first dimension and Nϑ along its second; any number of dimensions may follow. The `spin` must be entered explicitly, and `ℓmax` is the @@ -28,7 +28,7 @@ end map2salm!(salm, map, spin, ℓmax) map2salm!(salm, map, plan) -Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes in place. +Transform `map` values sampled on the sphere to ``{}_sa_{ℓ, m}`` modes in place. For details, see [`map2salm`](@ref). diff --git a/src/ssht/huffenberger_wandelt.jl b/src/ssht/huffenberger_wandelt.jl index bf3c35e3..0a56c838 100644 --- a/src/ssht/huffenberger_wandelt.jl +++ b/src/ssht/huffenberger_wandelt.jl @@ -35,61 +35,61 @@ e^{\frac{\pi}{2} 𝐤/ 2}\, e^{-\frac{\pi}{2} 𝐣/ 2}\, e^{-\frac{\pi}{2} 𝐤/ Now, we can use this expansion to find an expression for the ``d`` matrix value: ```math \begin{align} -d^{\ell}_{m', m}(\beta) +d^{ℓ}_{m', m}(\beta) &= -\mathfrak{D}^{\ell}_{m', m}\left(e^{\beta 𝐣 / 2}\right) \\ +𝔇^{ℓ}_{m', m}\left(e^{\beta 𝐣 / 2}\right) \\ &= -\mathfrak{D}^{\ell}_{m', m_1}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right)\, -\mathfrak{D}^{\ell}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -\mathfrak{D}^{\ell}_{m_2, m_3}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, -\mathfrak{D}^{\ell}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ +𝔇^{ℓ}_{m', m_1}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_2, m_3}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ &\quad \times -\mathfrak{D}^{\ell}_{m_4, m_5}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, -\mathfrak{D}^{\ell}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, -\mathfrak{D}^{\ell}_{m_6, m}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right) \\ +𝔇^{ℓ}_{m_4, m_5}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_6, m}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right) \\ &= \delta_{m', m_1} e^{im'\frac{\pi}{2}}\, -\mathfrak{D}^{\ell}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, \delta_{m_2, m_3} e^{-im_2\frac{\pi}{2}}\, -\mathfrak{D}^{\ell}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ +𝔇^{ℓ}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ &\quad \times \delta_{m_4, m_5} e^{-im_4\frac{\pi}{2}}\, -\mathfrak{D}^{\ell}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, \delta_{m_6, m} e^{im\frac{\pi}{2}} \\ &= e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, &\quad \times -\mathfrak{D}^{\ell}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -\mathfrak{D}^{\ell}_{m'', m'''}\left(e^{\beta 𝐤 / 2}\right)\, \\ -\mathfrak{D}^{\ell}_{m''', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +𝔇^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m'', m'''}\left(e^{\beta 𝐤 / 2}\right)\, \\ +𝔇^{ℓ}_{m''', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ &= e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, &\quad \times -\mathfrak{D}^{\ell}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, e^{-im''\beta}\, -\mathfrak{D}^{\ell}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +𝔇^{ℓ}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ &= i^{m'+m-2m''}\, -d^{\ell}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +d^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, e^{-im''\beta}\, -d^{\ell}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +d^{ℓ}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ &= i^{m'+m}(-1)^{m''}\, -d^{\ell}_{m', m''}\left(\frac{\pi}{2}\right)\, +d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, e^{-im''\beta}\, -d^{\ell}_{m'', m}\left(-\frac{\pi}{2}\right) \\ +d^{ℓ}_{m'', m}\left(-\frac{\pi}{2}\right) \\ &= i^{m'+m}(-1)^{m}\, -d^{\ell}_{m', m''}\left(\frac{\pi}{2}\right)\, +d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, e^{-im''\beta}\, -d^{\ell}_{m'', m}\left(\frac{\pi}{2}\right) \\ +d^{ℓ}_{m'', m}\left(\frac{\pi}{2}\right) \\ &= i^{m'-m}\, -d^{\ell}_{m', m''}\left(\frac{\pi}{2}\right)\, +d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, e^{-im''\beta}\, -d^{\ell}_{m'', m}\left(\frac{\pi}{2}\right) +d^{ℓ}_{m'', m}\left(\frac{\pi}{2}\right) \end{align} ``` diff --git a/src/utilities/operators.jl b/src/utilities/operators.jl index 2193b64c..01c47608 100644 --- a/src/utilities/operators.jl +++ b/src/utilities/operators.jl @@ -16,7 +16,7 @@ Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``L^2`` as ```math -L^2 {}_{s}Y_{\ell,m} = \ell\,(\ell+1) {}_{s}Y_{\ell,m} +L^2 {}_{s}Y_{ℓ,m} = ℓ\,(ℓ+1) {}_{s}Y_{ℓ,m} ``` See also [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -38,7 +38,7 @@ Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``L_z`` as ```math -L_z {}_{s}Y_{\ell,m} = m\, {}_{s}Y_{\ell,m} +L_z {}_{s}Y_{ℓ,m} = m\, {}_{s}Y_{ℓ,m} ``` See also [`L²`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -63,11 +63,11 @@ L_+] = L_+``, which allows us to derive ``L_+ = L_x + i\, L_y.`` In terms of the SWSHs, we can write the action of ``L_+`` as ```math -L_+ {}_{s}Y_{\ell,m} = \sqrt{(\ell-m)(\ell+m+1)}\, {}_{s}Y_{\ell,m+1}. +L_+ {}_{s}Y_{ℓ,m} = \sqrt{(ℓ-m)(ℓ+m+1)}\, {}_{s}Y_{ℓ,m+1}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{L_+(f)\right\}_{s,\ell,m} = \sqrt{(\ell+m)(\ell-m+1)}\,\left\{f\right\}_{s,\ell,m-1}. +\left\{L_+(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ+m)(ℓ-m+1)}\,\left\{f\right\}_{s,ℓ,m-1}. ``` See also [`L²`](@ref), [`Lz`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -99,11 +99,11 @@ L_-] = -L_-``, which allows us to derive ``L_- = L_x - i\, L_y.`` In terms of the SWSHs, we can write the action of ``L_-`` as ```math -L_- {}_{s}Y_{\ell,m} = \sqrt{(\ell+m)(\ell-m+1)}\, {}_{s}Y_{\ell,m-1}. +L_- {}_{s}Y_{ℓ,m} = \sqrt{(ℓ+m)(ℓ-m+1)}\, {}_{s}Y_{ℓ,m-1}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{L_-(f)\right\}_{s,\ell,m} = \sqrt{(\ell-m)(\ell+m+1)}\,\left\{f\right\}_{s,\ell,m+1}. +\left\{L_-(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ-m)(ℓ+m+1)}\,\left\{f\right\}_{s,ℓ,m+1}. ``` See also [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), @@ -141,7 +141,7 @@ Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``R^2`` as ```math -R^2 {}_{s}Y_{\ell,m} = \ell\,(\ell+1) {}_{s}Y_{\ell,m} +R^2 {}_{s}Y_{ℓ,m} = ℓ\,(ℓ+1) {}_{s}Y_{ℓ,m} ``` See also [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -164,7 +164,7 @@ operators") or [Boyle](@cite Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``R_z`` as ```math -R_z {}_{s}Y_{\ell,m} = -s\, {}_{s}Y_{\ell,m} +R_z {}_{s}Y_{ℓ,m} = -s\, {}_{s}Y_{ℓ,m} ``` Note the unfortunate sign of ``s``, which seems to be opposite to what we expect, and arises from the choice of definition of ``s`` in the original paper by [Newman and Penrose](@cite @@ -194,11 +194,11 @@ R_+] = R_+``, which allows us to derive ``R_+ = R_x - i\, R_y.`` In terms of the SWSHs, we can write the action of ``R_+`` as ```math -R_+ {}_{s}Y_{\ell,m} = \sqrt{(\ell+s)(\ell-s+1)}\, {}_{s-1}Y_{\ell,m} +R_+ {}_{s}Y_{ℓ,m} = \sqrt{(ℓ+s)(ℓ-s+1)}\, {}_{s-1}Y_{ℓ,m} ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{R_+(f)\right\}_{s,\ell,m} = \sqrt{(\ell+s)(\ell-s+1)}\,\left\{f\right\}_{s-1,\ell,m}. +\left\{R_+(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ+s)(ℓ-s+1)}\,\left\{f\right\}_{s-1,ℓ,m}. ``` Because of the unfortunate sign of ``s`` arising from the choice of definition of ``s`` in the original paper by [Newman and Penrose](@cite Newman_1966), this is a *lowering* operator @@ -235,11 +235,11 @@ R_-] = -R_-``, which allows us to derive ``R_- = R_x + i\, R_y.`` In terms of the SWSHs, we can write the action of ``R_-`` as ```math -R_- {}_{s}Y_{\ell,m} = \sqrt{(\ell-s)(\ell+s+1)}\, {}_{s+1}Y_{\ell,m} +R_- {}_{s}Y_{ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)}\, {}_{s+1}Y_{ℓ,m} ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{R_-(f)\right\}_{s,\ell,m} = \sqrt{(\ell-s)(\ell+s+1)}\,\left\{f\right\}_{s+1,\ell,m}. +\left\{R_-(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)}\,\left\{f\right\}_{s+1,ℓ,m}. ``` Because of the unfortunate sign of ``s`` arising from the choice of definition of ``s`` in the original paper by [Newman and Penrose](@cite Newman_1966), this is a *raising* operator @@ -276,11 +276,11 @@ By definition, the spin-raising operator satisfies the commutator relation ``[S, \eth`` (where ``S`` is the spin operator, which just multiplies the function by its spin). In terms of the SWSHs, we can write the action of ``\eth`` as ```math - \eth {}_{s}Y_{\ell,m} = \sqrt{(\ell-s)(\ell+s+1)} {}_{s+1}Y_{\ell,m}. + \eth {}_{s}Y_{ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)} {}_{s+1}Y_{ℓ,m}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{\eth f\right\}_{s,\ell,m} = \sqrt{(\ell-s)(\ell+s+1)}\,\left\{f\right\}_{s+1,\ell,m}. +\left\{\eth f\right\}_{s,ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)}\,\left\{f\right\}_{s+1,ℓ,m}. ``` See also [`ð̄`](@ref), [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), @@ -305,12 +305,12 @@ By definition, the spin-lowering operator satisfies the commutator relation ``[S \bar{\eth}] = -\bar{\eth}`` (where ``S`` is the spin operator, which just multiplies the function by its spin). In terms of the SWSHs, we can write the action of ``\bar{\eth}`` as ```math -\bar{\eth} {}_{s}Y_{\ell,m} = -\sqrt{(\ell+s)(\ell-s+1)} {}_{s-1}Y_{\ell,m}. +\bar{\eth} {}_{s}Y_{ℓ,m} = -\sqrt{(ℓ+s)(ℓ-s+1)} {}_{s-1}Y_{ℓ,m}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{\bar{\eth} f\right\}_{s,\ell,m} -= -\sqrt{(\ell-s)(\ell+s+1)}\,\left\{f\right\}_{s+1,\ell,m}. +\left\{\bar{\eth} f\right\}_{s,ℓ,m} += -\sqrt{(ℓ-s)(ℓ+s+1)}\,\left\{f\right\}_{s+1,ℓ,m}. ``` See also [`ð`](@ref), [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), diff --git a/src/utilities/pixelizations.jl b/src/utilities/pixelizations.jl index 73e4f3da..49bdd60b 100644 --- a/src/utilities/pixelizations.jl +++ b/src/utilities/pixelizations.jl @@ -202,11 +202,11 @@ driscoll_healy_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_ Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid: > We adopt an equiangular sampling of the sphere with sample positions given by ``\theta_t = -> \frac{\pi(2t+1)}{2\ell_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, \ell_\mathrm{max}-1\}`` -> and ``\phi_p = \frac{2 \pi p}{2\ell_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, -> 2\ell_\mathrm{max}-2\}``. In order to extend the ``\theta`` domain to ``[0, 2\pi)`` we -> simply extend the domain of the ``\theta`` index to include ``\{\ell_\mathrm{max}, -> \ell_\mathrm{max}+1, \dotsc, 2\ell_\mathrm{max}-1\}``. +> \frac{\pi(2t+1)}{2ℓ_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, ℓ_\mathrm{max}-1\}`` +> and ``\phi_p = \frac{2 \pi p}{2ℓ_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, +> 2ℓ_\mathrm{max}-2\}``. In order to extend the ``\theta`` domain to ``[0, 2\pi)`` we +> simply extend the domain of the ``\theta`` index to include ``\{ℓ_\mathrm{max}, +> ℓ_\mathrm{max}+1, \dotsc, 2ℓ_\mathrm{max}-1\}``. !!! note The `s` argument is not used in this function, but is included for consistency with diff --git a/src/wigner/recurrence.jl b/src/wigner/recurrence.jl index 4a175aaa..e1c7e41c 100644 --- a/src/wigner/recurrence.jl +++ b/src/wigner/recurrence.jl @@ -30,7 +30,7 @@ end @doc raw""" recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) -Compute the values of ``H^{\ell}_{0,m}``, from the values of ``H^{\ell-1}_{0,m}`` for all +Compute the values of ``H^{ℓ}_{0,m}``, from the values of ``H^{ℓ-1}_{0,m}`` for all ``m \geq 0``. """ @@ -85,7 +85,7 @@ end @doc raw""" recurrence_step3!(Hˡ, Hˡ⁺¹, sinβ, cosβ) -Compute the values of ``H^{\ell}_{1,m}``, from the values of ``H^{\ell+1}_{0,m}`` for all +Compute the values of ``H^{ℓ}_{1,m}``, from the values of ``H^{ℓ+1}_{0,m}`` for all ``m \geq 0``. """ @@ -114,8 +114,8 @@ end @doc raw""" recurrence_step4!(Hˡ, sinβ, cosβ) -Compute the values of ``H^{\ell}_{m'+1,m}``, from the values of ``H^{\ell}_{m',m-1}``, -``H^{\ell}_{m'-1,m}``, and ``H^{\ell}_{m',m+1}``, for all ``m' > 1`` and ``m \geq m'``. +Compute the values of ``H^{ℓ}_{m'+1,m}``, from the values of ``H^{ℓ}_{m',m-1}``, +``H^{ℓ}_{m'-1,m}``, and ``H^{ℓ}_{m',m+1}``, for all ``m' > 1`` and ``m \geq m'``. """ function recurrence_step4!( @@ -151,8 +151,8 @@ end @doc raw""" recurrence_step5!(Hˡ, sinβ, cosβ) -Compute the values of ``H^{\ell}_{m'-1,m}``, from the values of ``H^{\ell}_{m',m-1}``, -``H^{\ell}_{m'+1,m}``, and ``H^{\ell}_{m',m+1}``, for all ``m' \leq 0`` and ``m > -m'``. +Compute the values of ``H^{ℓ}_{m'-1,m}``, from the values of ``H^{ℓ}_{m',m-1}``, +``H^{ℓ}_{m'+1,m}``, and ``H^{ℓ}_{m',m+1}``, for all ``m' \leq 0`` and ``m > -m'``. """ function recurrence_step5!( diff --git a/test/conventions/NIST_DLMF.jl b/test/conventions/NIST_DLMF.jl index 3d43cf3c..cbe47a15 100644 --- a/test/conventions/NIST_DLMF.jl +++ b/test/conventions/NIST_DLMF.jl @@ -27,11 +27,11 @@ or [Eq. 14.7.14](http://dlmf.nist.gov/14.7#E14) ``` And for the spherical harmonics, [Eq. 14.30.1](http://dlmf.nist.gov/14.30#E1) gives ```math - Y_{\ell, m}\left(\theta,\phi\right) + Y_{ℓ, m}\left(\theta,\phi\right) = - \left(\frac{(\ell-m)!(2\ell+1)}{4\pi(\ell+m)!}\right)^{1/2} + \left(\frac{(ℓ-m)!(2ℓ+1)}{4\pi(ℓ+m)!}\right)^{1/2} \mathsf{e}^{im\phi} - \mathsf{P}_{\ell}^{m}\left(\cos\theta\right). + \mathsf{P}_{ℓ}^{m}\left(\cos\theta\right). ``` """ diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl index a4d4f8e6..ceafee34 100644 --- a/test/conventions/edmonds.jl +++ b/test/conventions/edmonds.jl @@ -39,7 +39,7 @@ end Eqs. (4.1.12) of [Edmonds](@cite Edmonds_2016), implementing ```math - \mathcal{D}^{(j)}_{m',m}(\alpha, \beta, \gamma). + 𝒟^{(j)}_{m',m}(\alpha, \beta, \gamma). ``` See also [`d`](@ref) for Edmonds' version the Wigner d-function. diff --git a/test/conventions/goldbergetal.jl b/test/conventions/goldbergetal.jl index 2b89498c..c47f5e81 100644 --- a/test/conventions/goldbergetal.jl +++ b/test/conventions/goldbergetal.jl @@ -63,7 +63,7 @@ end Eq. (3.1) of [Goldberg et al.](@cite GoldbergEtAl_1967), implementing ```math - {}_sY_{\ell,m}(\theta, \phi). + {}_sY_{ℓ,m}(\theta, \phi). ``` Note that there is a difference in conventions between the ``Y`` of Goldberg et al. and diff --git a/test/conventions/sakurai.jl b/test/conventions/sakurai.jl index 810f0a8a..ba5ffa0d 100644 --- a/test/conventions/sakurai.jl +++ b/test/conventions/sakurai.jl @@ -6,17 +6,17 @@ The conclusion here is that Sakurai's Yₗᵐ(θ, ϕ) is the same as ours, but h - On p. 154 he says that "a rotation operation affects the physical system itself, ..., while the coordinate axes remain *unchanged*." -- On p. 156 he poses "``|\alpha\rangle_R = \mathcal{D}(R) | \alpha \rangle``, where +- On p. 156 he poses "``|\alpha\rangle_R = 𝒟(R) | \alpha \rangle``, where ``|\alpha\rangle_R`` and ``|\alpha \rangle`` stand for the kets of the rotated and original system, respectively." -- On p. 157 he says "``\mathcal{D}(\hat{\mathbf{n}}, d\phi) = 1 - i\left( \frac{\mathbf{J} - \cdot \hat{\mathbf{n}}} {\hbar} \right) d\phi``" +- On p. 157 he says "``𝒟(\hat{𝐧}, d\phi) = 1 - i\left( \frac{𝐉 + \cdot \hat{𝐧}} {\hbar} \right) d\phi``" - On p. 173 he defines his Euler angles in the same way as Quaternionic. -- On p. 192 he defines "``\mathcal{D}^{(j)}_{m',m}(R) = \langle j,m'| \exp \left( - \frac{-i\mathbf{J} \cdot \hat{\mathbf{n}} \phi} {\hbar} \right) |j, m\rangle``". +- On p. 192 he defines "``𝒟^{(j)}_{m',m}(R) = \langle j,m'| \exp \left( + \frac{-i𝐉 \cdot \hat{𝐧} \phi} {\hbar} \right) |j, m\rangle``". - On p. 194 he gives the expression in terms of Euler angles. - On p. 223 he gives an explicit formula for ``d``. -- On p. 203 he relates ``\mathcal{D} to Y_{\ell}^m$ (note the upper index of ``m``). +- On p. 203 he relates ``𝒟 to Y_{ℓ}^m$ (note the upper index of ``m``). Below (1.6.14), we find the translation operator acts as @@ -66,7 +66,7 @@ import .NaiveFactorials: ❗ Eqs. (3.5.50)-(3.5.51) of [Sakurai](@cite Sakurai_1994), p. 194, implementing ```math - \mathcal{D}^{(j)}_{m',m}(\alpha, \beta, \gamma). + 𝒟^{(j)}_{m',m}(\alpha, \beta, \gamma). ``` See also [`d`](@ref) for Sakurai's version the Wigner d-function. @@ -125,7 +125,7 @@ end Eqs. (3.6.51) of [Sakurai](@cite Sakurai_1994), p. 203, implementing ```math - Y_{\ell}^m(\theta, \phi). + Y_{ℓ}^m(\theta, \phi). ``` """ function Y(ℓ, m, θ, ϕ) diff --git a/test/conventions/thorne.jl b/test/conventions/thorne.jl index 3f10e107..47050c98 100644 --- a/test/conventions/thorne.jl +++ b/test/conventions/thorne.jl @@ -33,7 +33,7 @@ end Eqs. (2.7) of [Thorne](@cite Thorne_1980), implementing ```math - Y^{\ell,m}(\theta, \phi). + Y^{ℓ,m}(\theta, \phi). ``` """ function Y(ℓ, m, θ, ϕ) diff --git a/test/conventions/wigner.jl b/test/conventions/wigner.jl index 2b3a5db7..f94de7d9 100644 --- a/test/conventions/wigner.jl +++ b/test/conventions/wigner.jl @@ -19,7 +19,7 @@ with respect to ours. For example, note that the position of ``z'`` is independ which appears to represent a final rotation about the ``z'`` axis. In our convention, this rotation would be described by the final Euler angle, γ. -On the other hand, on page 156, if ``𝔇^{(\ell)}`` obeys the representation-composition +On the other hand, on page 156, if ``𝔇^{(ℓ)}`` obeys the representation-composition property, then {α, β, γ} represents the rotation {α, 0, 0}∘{0, β, 0}∘{0, 0, γ}, which is the same as our convention. @@ -49,7 +49,7 @@ conjugation of the D function, which is consistent with our convention. # Eq. (15.8) of [Wigner](@cite Wigner_1959), implementing # ```math -# D^\ell_{m',m}(\alpha, \beta, \gamma). +# D^ℓ_{m',m}(\alpha, \beta, \gamma). # ``` # """ # function D(ℓ, m′, m, α, β, γ) diff --git a/test/deprecated/wigner_matrices/H.jl b/test/deprecated/wigner_matrices/H.jl index 91aee9e4..91f2fd18 100644 --- a/test/deprecated/wigner_matrices/H.jl +++ b/test/deprecated/wigner_matrices/H.jl @@ -58,7 +58,7 @@ end import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the explicit Wigner d - # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, + # d_{ℓ}^{n,m} = \epsilon_n \epsilon_{-m} H_{ℓ}^{n,m}, for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:2 # 2 is the max explicitly coded ℓ @@ -111,7 +111,7 @@ end import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the formulaic Wigner d - # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, + # d_{ℓ}^{n,m} = \epsilon_n \epsilon_{-m} H_{ℓ}^{n,m}, tol = ifelse(T ∈ (BigFloat, Float32), 100, 1) * 30eps(T) for β in βrange(T) expiβ = cis(β) From f795b1d6eb9416cd30288842fc10ea40f65f0ada Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 7 Jan 2026 15:54:50 -0500 Subject: [PATCH 323/329] Simplify some latex --- .../calculations/euler_angular_momentum.jl | 138 +++--- .../conventions/comparisons/blanchet_2024.jl | 8 +- .../comparisons/cohen_tannoudji_1991.jl | 30 +- .../comparisons/condon_shortley_1935.jl | 50 +- .../conventions/comparisons/ninja_2011.jl | 12 +- .../conventions/comparisons/tait_1868.jl | 16 +- .../conventions/comparisons/whittaker_1947.jl | 2 +- docs/src/background/operators.md | 8 +- docs/src/conventions/comparisons.md | 454 +++++++++--------- docs/src/conventions/details.md | 282 +++++------ docs/src/conventions/outline.md | 136 +++--- docs/src/conventions/summary.md | 158 +++--- docs/src/notes/H_recurrence.md | 6 +- docs/src/notes/H_recursions.md | 6 +- docs/src/notes/sampling_theorems.md | 28 +- docs/src/operators.md | 8 +- src/deprecated/evaluate.jl | 10 +- src/deprecated/iterators.jl | 6 +- src/ssht/huffenberger_wandelt.jl | 38 +- src/utilities/pixelizations.jl | 16 +- src/utilities/weights.jl | 6 +- test/conventions/NIST_DLMF.jl | 6 +- test/conventions/edmonds.jl | 4 +- test/conventions/goldbergetal.jl | 4 +- test/conventions/sakurai.jl | 36 +- test/conventions/thorne.jl | 2 +- test/conventions/torresdelcastillo.jl | 6 +- test/conventions/varshalovich.jl | 4 +- test/conventions/wigner.jl | 4 +- test/deprecated/wigner_matrices/H.jl | 4 +- 30 files changed, 744 insertions(+), 744 deletions(-) diff --git a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl index 9f394f68..154481fe 100644 --- a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl +++ b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl @@ -4,11 +4,11 @@ md""" This package defines the angular-momentum operators ``L_j`` and ``R_j`` in terms of elements of the Lie group and algebra: ```math -L_𝐮 f(𝐑) = \left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} -f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) +L_𝐮 f(𝐑) = \left. i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}\, 𝐑\right) \qquad \text{and} \qquad -R_𝐮 f(𝐑) = -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} -f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), +R_𝐮 f(𝐑) = -\left. i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(𝐑\, e^{-ϵ 𝐮/2}\right), ``` This is certainly the natural realm for these operators, but it is not the common one. In particular, virtually all textbooks and papers on the subject define these operators in @@ -28,8 +28,8 @@ important results that help make contact with more standard expressions: the standard expressions. 3. We also find ``[R_x, R_y] = i R_z`` and cyclic permutations. 4. We can explicitly compute ``[L_i, R_j] = 0``, as expected. - 5. Using the natural extension of Goldberg et al.'s SWSHs to include ``\gamma``, we can - see that the natural spin-weight operator is ``R_z = i \partial_\gamma``. Thus, we + 5. Using the natural extension of Goldberg et al.'s SWSHs to include ``γ``, we can + see that the natural spin-weight operator is ``R_z = i \partial_γ``. Thus, we define ``R_z = s`` for a function with spin weight ``s``. 6. The spin-raising operator for ``R_z`` is ``\eth = R_x + i R_y``; the spin-lowering operator is ``\bar{\eth} = R_x - i R_y``. @@ -38,67 +38,67 @@ important results that help make contact with more standard expressions: We start by defining a new set of Euler angles according to ```math -𝐑_{\alpha', \beta', \gamma'} -= e^{-\epsilon 𝐮 / 2} 𝐑_{\alpha, \beta, \gamma} +𝐑_{α', β', γ'} += e^{-ϵ 𝐮 / 2} 𝐑_{α, β, γ} \qquad \text{or} \qquad -𝐑_{\alpha', \beta', \gamma'} -= 𝐑_{\alpha, \beta, \gamma} e^{-\epsilon 𝐮 / 2} +𝐑_{α', β', γ'} += 𝐑_{α, β, γ} e^{-ϵ 𝐮 / 2} ``` -where ``𝐮`` will be each of the basis quaternions, and each of ``\alpha'``, -``\beta'``, and ``\gamma'`` is a function of ``\alpha``, ``\beta``, ``\gamma``, and -``\epsilon``. Then, we note that the chain rule tells us that +where ``𝐮`` will be each of the basis quaternions, and each of ``α'``, +``β'``, and ``γ'`` is a function of ``α``, ``β``, ``γ``, and +``ϵ``. Then, we note that the chain rule tells us that ```math -\frac{\partial}{\partial \epsilon} +\frac{\partial}{\partial ϵ} = -\frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha'} -+ \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta'} -+ \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma'}, +\frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α'} ++ \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β'} ++ \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ'}, ``` which we will use to convert the general expression for the angular-momentum operators in -terms of ``\partial_\epsilon`` into an expression in terms of derivatives with respect to +terms of ``\partial_ϵ`` into an expression in terms of derivatives with respect to these new Euler angles: ```math \begin{align} - L_j f(𝐑_{\alpha, \beta, \gamma}) + L_j f(𝐑_{α, β, γ}) &= - \left. i \frac{\partial} {\partial \epsilon} f \left( e^{-\epsilon 𝐞_j / 2} - 𝐑_{\alpha, \beta, \gamma} \right) \right|_{\epsilon=0} + \left. i \frac{\partial} {\partial ϵ} f \left( e^{-ϵ 𝐞_j / 2} + 𝐑_{α, β, γ} \right) \right|_{ϵ=0} \\ &= i \left[ \left( - \frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha'} - + \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta'} - + \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma'} - \right) f \left(\alpha', \beta', \gamma'\right) \right]_{\epsilon=0} + \frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α'} + + \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β'} + + \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ'} + \right) f \left(α', β', γ'\right) \right]_{ϵ=0} \\ &= i \left[ \left( - \frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha} - + \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta} - + \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma} - \right) f \left(\alpha, \beta, \gamma\right) \right]_{\epsilon=0}, + \frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α} + + \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β} + + \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ} + \right) f \left(α, β, γ\right) \right]_{ϵ=0}, \end{align} ``` or for ``R_j``: ```math \begin{align} - R_j f(𝐑_{\alpha, \beta, \gamma}) + R_j f(𝐑_{α, β, γ}) &= - -\left. i \frac{\partial} {\partial \epsilon} f \left( 𝐑_{\alpha, \beta, \gamma} - e^{-\epsilon 𝐞_j / 2} \right) \right|_{\epsilon=0} + -\left. i \frac{\partial} {\partial ϵ} f \left( 𝐑_{α, β, γ} + e^{-ϵ 𝐞_j / 2} \right) \right|_{ϵ=0} \\ &= -i \left[ \left( - \frac{\partial \alpha'}{\partial \epsilon} \frac{\partial}{\partial \alpha} - + \frac{\partial \beta'}{\partial \epsilon} \frac{\partial}{\partial \beta} - + \frac{\partial \gamma'}{\partial \epsilon} \frac{\partial}{\partial \gamma} - \right) f \left(\alpha, \beta, \gamma\right) \right]_{\epsilon=0}. + \frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α} + + \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β} + + \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ} + \right) f \left(α, β, γ\right) \right]_{ϵ=0}. \end{align} ``` So the objective is to find the new Euler angles, differentiate with respect to -``\epsilon``, and then evaluate at ``\epsilon = 0``. We do this by first multiplying -``𝐑_{\alpha, \beta, \gamma}`` and ``e^{-\epsilon 𝐮 / 2}`` in the desired +``ϵ``, and then evaluate at ``ϵ = 0``. We do this by first multiplying +``𝐑_{α, β, γ}`` and ``e^{-ϵ 𝐮 / 2}`` in the desired order, then expanding the results in terms of its quaternion components, and then computing the new Euler angles in terms of those components according to the usual expression. @@ -191,9 +191,9 @@ macro display(expr) ∂α′∂ϵ, ∂β′∂ϵ, ∂γ′∂ϵ = latex.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = i\left[ - %$(∂α′∂ϵ) \frac{\partial}{\partial \alpha} - + %$(∂β′∂ϵ) \frac{\partial}{\partial \beta} - + %$(∂γ′∂ϵ) \frac{\partial}{\partial \gamma} + %$(∂α′∂ϵ) \frac{\partial}{\partial α} + + %$(∂β′∂ϵ) \frac{\partial}{\partial β} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial γ} \right]""" # Display the result in LaTeX form end else @@ -201,9 +201,9 @@ macro display(expr) ∂α′∂ϵ, ∂β′∂ϵ, ∂γ′∂ϵ = latex.($expr) # Call expr; format results as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = -i\left[ - %$(∂α′∂ϵ) \frac{\partial}{\partial \alpha} - + %$(∂β′∂ϵ) \frac{\partial}{\partial \beta} - + %$(∂γ′∂ϵ) \frac{\partial}{\partial \gamma} + %$(∂α′∂ϵ) \frac{\partial}{\partial α} + + %$(∂β′∂ϵ) \frac{\partial}{\partial β} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial γ} \right]""" # Display the result in LaTeX form end end @@ -228,9 +228,9 @@ macro display2(expr) ) expr = $op * "_" * $arg # Standard form of the operator expsign = ($arg=="+" ? "" : "-") - L"""%$expr = e^{%$expsign i \phi} \left[ - %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} - + %$(∂φ′∂ϵ) \frac{\partial}{\partial \phi} + L"""%$expr = e^{%$expsign i ϕ} \left[ + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial θ} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial ϕ} \right]""" # Display the result in LaTeX form end elseif op == "L" @@ -238,8 +238,8 @@ macro display2(expr) ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = latex.($conversion.($expr)) # Call expr; format as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = i\left[ - %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} - + %$(∂φ′∂ϵ) \frac{\partial}{\partial \phi} + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial θ} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial ϕ} \right]""" # Display the result in LaTeX form end else @@ -247,9 +247,9 @@ macro display2(expr) ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = latex.($conversion.($expr)) # Call expr; format as LaTeX expr = $op * "_" * $arg # Standard form of the operator L"""%$expr = -i\left[ - %$(∂ϑ′∂ϵ) \frac{\partial}{\partial \theta} - + %$(∂φ′∂ϵ) \frac{\partial}{\partial \phi} - + %$(∂γ′∂ϵ) \frac{\partial}{\partial \gamma} + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial θ} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial ϕ} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial γ} \right]""" # Display the result in LaTeX form end end @@ -291,13 +291,13 @@ nothing #hide # \begin{align} # R_𝐮 f(𝐑) # &= -# -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} f\left(𝐑\, e^{-\epsilon 𝐮/2}\right) \\ +# -\left. i \frac{d}{dϵ}\right|_{ϵ=0} f\left(𝐑\, e^{-ϵ 𝐮/2}\right) \\ # &= -# -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} -# f\left(𝐑\, e^{-\epsilon 𝐮/2}\, 𝐑^{-1}\, 𝐑\right) \\ +# -\left. i \frac{d}{dϵ}\right|_{ϵ=0} +# f\left(𝐑\, e^{-ϵ 𝐮/2}\, 𝐑^{-1}\, 𝐑\right) \\ # &= -# -\left. i \frac{d}{d\epsilon}\right|_{\epsilon=0} -# f\left(e^{-\epsilon 𝐑\, 𝐮\, 𝐑^{-1}/2}\, 𝐑\right) \\ +# -\left. i \frac{d}{dϵ}\right|_{ϵ=0} +# f\left(e^{-ϵ 𝐑\, 𝐮\, 𝐑^{-1}/2}\, 𝐑\right) \\ # &= # -L_{𝐑\, 𝐮\, 𝐑^{-1}} f(𝐑), # \end{align} @@ -396,8 +396,8 @@ commutator(Rz, Rx) # 2-sphere, as seen in numerous references, so we can declare compatibility between our # unusual definition of ``L`` and more standard definitions. # -# Now, note that including ``\partial_\gamma`` for an expression on the 2-sphere doesn't -# actually make any sense: ``\gamma`` isn't even a coordinate for the 2-sphere! However, +# Now, note that including ``\partial_γ`` for an expression on the 2-sphere doesn't +# actually make any sense: ``γ`` isn't even a coordinate for the 2-sphere! However, # for historical reasons, we include it here when showing the results of the ``R`` operator # in Euler angles. @@ -408,26 +408,26 @@ commutator(Rz, Rx) #- @display2 R(𝐤) -# We get nonzero components of ``\partial_\gamma``, showing that these operators really *do +# We get nonzero components of ``\partial_γ``, showing that these operators really *do # not* make sense for the 2-sphere, and therefore that it doesn't actually make sense to # define spin-weighted spherical functions on the 2-sphere; they really only make sense on # the 3-sphere. Nonetheless, if we stipulate that the function ``\eta`` has a specific spin # weight, that means that *on the 3-sphere* it is an eigenfunction with ``R_z\eta = -# i\partial_\gamma \eta = s\eta``. So we could just substitute ``-i s`` for -# ``\partial_\gamma`` in the expressions above, and recover the standard spin-weight +# i\partial_γ \eta = s\eta``. So we could just substitute ``-i s`` for +# ``\partial_γ`` in the expressions above, and recover the standard spin-weight # operators. We get # ```math # \left[R_x + i R_y\right] \eta # = \left[ -# -i \frac{1}{\sin\theta} \frac{\partial}{\partial \phi} -# + \frac{s}{\tan \theta} -# - \frac{\partial}{\partial \theta} +# -i \frac{1}{\sin θ} \frac{\partial}{\partial ϕ} +# + \frac{s}{\tan θ} +# - \frac{\partial}{\partial θ} # \right] \eta -# = -(\sin \theta)^s \left\{ -# \frac{\partial}{\partial \theta} -# +i \frac{1}{\sin\theta} \frac{\partial}{\partial \phi} +# = -(\sin θ)^s \left\{ +# \frac{\partial}{\partial θ} +# +i \frac{1}{\sin θ} \frac{\partial}{\partial ϕ} # \right\} -# \left\{ (\sin \theta)^{-s} \eta \right\}. +# \left\{ (\sin θ)^{-s} \eta \right\}. # ``` # And in the latter form, we can see that ``R_x + i R_y`` is exactly the spin-raising # operator ``\eth`` as originally defined by [Newman_1966](@citet) in their Eq. (3.8). The diff --git a/docs/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl index 4b24f4e3..89596951 100644 --- a/docs/literate_input/conventions/comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -12,7 +12,7 @@ up-to-date with the latest developments. The spherical coordinates are standard physicists' coordinates, implicitly defined by the direction vector below Eq. (188b): ```math - N_i = \left(\sin\theta\cos\phi, \sin\theta\sin\phi, \cos\theta\right). + N_i = \left(\sin θ\cos ϕ, \sin θ\sin ϕ, \cos θ\right). ``` @@ -35,7 +35,7 @@ import ..ConventionsUtilities: 𝒾, ❗ # The ``s=-2`` spin-weighted spherical harmonics are defined in Eq. (184a) as # ```math -# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4\pi}} d^{ℓ m}(\theta) e^{im\phi}. +# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4\pi}} d^{ℓ m}(θ) e^{imϕ}. # ``` function Yˡᵐ₋₂(l, m, θ::T, ϕ::T) where {T<:Real} √((2l + 1) / (4T(π))) * d(l, m, θ) * exp(𝒾 * m * ϕ) @@ -49,8 +49,8 @@ end # \sum_{k = k_1}^{k_2} # \frac{(-)^k}{k!} # e_k^{ℓ m} -# \left(\cos\frac{\theta}{2}\right)^{2ℓ+m-2k-2} -# \left(\sin\frac{\theta}{2}\right)^{2k-m+2}, +# \left(\cos\frac{θ}{2}\right)^{2ℓ+m-2k-2} +# \left(\sin\frac{θ}{2}\right)^{2k-m+2}, # ``` # with ``k_1 = \textrm{max}(0, m-2)`` and ``k_2=\textrm{min}(l+m, l-2)``. function d(l, m, θ::T) where {T<:Real} diff --git a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl index df639823..f2c60038 100644 --- a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl @@ -15,29 +15,29 @@ compute the angular-momentum operators as [Eqs. (D-5)] ```math \begin{aligned} L_x &= i \hbar \left( - \sin\phi \frac{\partial} {\partial \theta} - + \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} + \sin ϕ \frac{\partial} {\partial θ} + + \frac{\cos ϕ}{\tan θ} \frac{\partial} {\partial ϕ} \right), \\ L_y &= i \hbar \left( - -\cos\phi \frac{\partial} {\partial \theta} - + \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} + -\cos ϕ \frac{\partial} {\partial θ} + + \frac{\sin ϕ}{\tan θ} \frac{\partial} {\partial ϕ} \right), \\ -L_z &= \frac{\hbar}{i} \frac{\partial} {\partial \phi}. +L_z &= \frac{\hbar}{i} \frac{\partial} {\partial ϕ}. \end{aligned} ``` In Complement ``\mathrm{B}_{\mathrm{VI}}`` they define a rotation operator ``R`` as acting on a state such that [Eq. (21)] ```math -\langle 𝐫 | R | \psi \rangle +\langle 𝐫 | R | ψ \rangle = -\langle \mathscr{R}^{-1} 𝐫 | \psi \rangle. +\langle \mathscr{R}^{-1} 𝐫 | ψ \rangle. ``` -For an infinitesimal rotation through angle ``d\alpha`` about the axis ``𝐮``, he +For an infinitesimal rotation through angle ``dα`` about the axis ``𝐮``, he shows [Eq. (49)] ```math -R_{𝐮}(d\alpha) = 1 - \frac{i}{\hbar} d\alpha 𝐋.𝐮. +R_{𝐮}(dα) = 1 - \frac{i}{\hbar} dα 𝐋.𝐮. ``` @@ -60,11 +60,11 @@ import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ # They derive the spherical harmonics in two ways and get two different, but equivalent, # expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) # ```math -# Y_{l}^{m}(\theta, \phi) +# Y_{l}^{m}(θ, ϕ) # = # \frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l+m)!}{(l-m)!}} -# e^{i m \phi} (\sin \theta)^{-m} -# \frac{d^{l-m}}{d(\cos \theta)^{l-m}} (\sin \theta)^{2l}, +# e^{i m ϕ} (\sin θ)^{-m} +# \frac{d^{l-m}}{d(\cos θ)^{l-m}} (\sin θ)^{2l}, # ``` function Y₁(l, m, θ::T, ϕ::T) where {T<:Real} ( @@ -78,11 +78,11 @@ end # while the second is Eq. (30) # ```math -# Y_{l}^{m}(\theta, \phi) +# Y_{l}^{m}(θ, ϕ) # = # \frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l-m)!}{(l+m)!}} -# e^{i m \phi} (\sin \theta)^m -# \frac{d^{l+m}}{d(\cos \theta)^{l+m}} (\sin \theta)^{2l}. +# e^{i m ϕ} (\sin θ)^m +# \frac{d^{l+m}}{d(\cos θ)^{l+m}} (\sin θ)^{2l}. # ``` function Y₂(l, m, θ::T, ϕ::T) where {T<:Real} ( diff --git a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl index 4e11bb3e..d0e49f98 100644 --- a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl @@ -12,14 +12,14 @@ standard reference for the "Condon-Shortley phase convention". Though some refe not very clear about precisely what they mean by that phrase, it seems clear that the original meaning revolved around the idea that the angular-momentum raising and lowering operators have eigenvalues that are *real and positive* when acting on the spherical -harmonics. Specifically, they discuss the phase ambiguity of the eigenfunction ``\psi`` — +harmonics. Specifically, they discuss the phase ambiguity of the eigenfunction ``ψ`` — which includes spherical harmonics indexed by ``j`` and ``m`` for the angular part — in section 3³ (page 48). This culminates in Eq. (3) of that section, which is as explicit as they get: ```math -\left( J_x \pm i J_y \right) \psi(\gamma j m) +\left( J_x \pm i J_y \right) ψ(γ j m) = -\hbar \sqrt{(j \mp m)(j \pm m + 1)} \psi(\gamma j m \pm 1). +\hbar \sqrt{(j \mp m)(j \pm m + 1)} ψ(γ j m \pm 1). ``` This eliminates any *relative* phase ambiguity between modes with neighboring ``m`` values, and specifically determines what factors of ``(-1)^m`` should be included in the definition @@ -33,23 +33,23 @@ polynomials — which would subject us to another round of ambiguity — the fun module use automatic differentiation to compute the derivatives explicitly. Condon and Shortley are not very explicit about the meaning of the spherical coordinates, -but they do describe them as "spherical polar coordinates ``r, \theta, \varphi``". +but they do describe them as "spherical polar coordinates ``r, θ, φ``". Immediately before equation (1) of section 4³ (page 50), they define the angular-momentum operator ```math -L_z = -i \hbar \frac{\partial}{\partial \varphi}, +L_z = -i \hbar \frac{\partial}{\partial φ}, ``` which agrees with [our expression](@ref "``L`` operators in spherical coordinates"). This is followed by equation (8): ```math \begin{aligned} -L_x + i L_y &= \hbar e^{i\varphi} \left( - \frac{\partial}{\partial \theta} - + i \cot\theta \frac{\partial}{\partial \varphi} +L_x + i L_y &= \hbar e^{iφ} \left( + \frac{\partial}{\partial θ} + + i \cot θ \frac{\partial}{\partial φ} \right) \\ -L_x - i L_y &= \hbar e^{-i\varphi} \left( - -\frac{\partial}{\partial \theta} - + i \cot\theta \frac{\partial}{\partial \varphi} +L_x - i L_y &= \hbar e^{-iφ} \left( + -\frac{\partial}{\partial θ} + + i \cot θ \frac{\partial}{\partial φ} \right), \end{aligned} ``` @@ -78,47 +78,47 @@ import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ # Equation (12) of section 4³ (page 51) writes the solution to the three-dimensional Laplace # equation in spherical coordinates as # ```math -# \psi(\gamma, ℓ, m_ℓ) +# ψ(γ, ℓ, m_ℓ) # = -# B(\gamma, ℓ) \Theta(ℓ, m_ℓ) \Phi(m_ℓ), +# B(γ, ℓ) \Theta(ℓ, m_ℓ) \Phi(m_ℓ), # ``` -# where ``B`` is independent of ``\theta`` and ``\varphi``, and ``\gamma`` represents any +# where ``B`` is independent of ``θ`` and ``φ``, and ``γ`` represents any # number of eigenvalues required to specify the state. More explicitly, below Eq. (5) of # section 5⁵ (page 127), they specifically define the spherical harmonics as # ```math -# \phi(ℓ, m_ℓ) = \Theta(ℓ, m_ℓ) \Phi(m_ℓ). +# ϕ(ℓ, m_ℓ) = \Theta(ℓ, m_ℓ) \Phi(m_ℓ). # ``` -# One quirk of their notation is that the dependence on ``\theta`` and ``\varphi`` is +# One quirk of their notation is that the dependence on ``θ`` and ``φ`` is # implicit in their functions; we make it explicit, as Julia requires: function 𝜙(ℓ, mₗ, 𝜃, φ) Θ(ℓ, mₗ, 𝜃) * Φ(mₗ, φ) end #+ -# The ``\varphi`` part is given by equation (5) of section 4³ (page 50): +# The ``φ`` part is given by equation (5) of section 4³ (page 50): # ```math # \Phi(m_ℓ) # = -# \frac{1}{\sqrt{2\pi}} e^{i m_ℓ \varphi}. +# \frac{1}{\sqrt{2\pi}} e^{i m_ℓ φ}. # ``` -# Again, we make the dependence on ``\varphi`` explicit, and we capture its type to ensure +# Again, we make the dependence on ``φ`` explicit, and we capture its type to ensure # that we don't lose precision when converting π to a floating-point number. function Φ(mₗ, φ::T) where {T} 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) end #+ -# Equation (15) of section 4³ (page 52) gives the ``\theta`` dependence as +# Equation (15) of section 4³ (page 52) gives the ``θ`` dependence as # ```math # \Theta(ℓ, m) # = # (-1)^ℓ # \sqrt{\frac{(2ℓ+1)}{2} \frac{(ℓ+m)!}{(ℓ-m)!}} # \frac{1}{2^ℓ ℓ!} -# \frac{1}{\sin^m \theta} -# \frac{d^{ℓ-m}}{d(\cos\theta)^{ℓ-m}} \sin^{2ℓ}\theta. +# \frac{1}{\sin^m θ} +# \frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} \sin^{2ℓ}θ. # ``` -# Again, we make the dependence on ``\theta`` explicit, and we capture its type to ensure +# Again, we make the dependence on ``θ`` explicit, and we capture its type to ensure # that we don't lose precision when converting the factorials to a floating-point number. function Θ(ℓ, m, 𝜃::T) where {T} (-1)^ℓ * T(√(((2ℓ+1) * (ℓ+m)❗) / (2 * (ℓ - m)❗)) * (1 / (2^ℓ * (ℓ)❗))) * @@ -170,7 +170,7 @@ end # module CondonShortley #+ # so we test up to that point, and just compare the general form to the explicit formulas — # again, noting the subtle difference between the characters `Θ` and `ϴ`. Note that the -# ``1/\sin\theta`` factor in the general form will cause problems at the poles, so we avoid +# ``1/\sin θ`` factor in the general form will cause problems at the poles, so we avoid # the poles by using `βrange` with a small offset: for θ ∈ θrange(; avoid_poles=ϵₐ/10) for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) @@ -192,7 +192,7 @@ for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) end #+ -# This successful test shows that the function ``\phi`` defined by Condon and Shortley +# This successful test shows that the function ``ϕ`` defined by Condon and Shortley # agrees with the spherical harmonics defined by the `SphericalFunctions` package. end #hide diff --git a/docs/literate_input/conventions/comparisons/ninja_2011.jl b/docs/literate_input/conventions/comparisons/ninja_2011.jl index e6f11d73..74d43ee5 100644 --- a/docs/literate_input/conventions/comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions/comparisons/ninja_2011.jl @@ -39,12 +39,12 @@ import ..ConventionsUtilities: 𝒾, ❗ # The spin-weighted spherical harmonics are defined in Eq. (II.7) as # ```math # {}^{-s}Y_{l,m} = (-1)^s \sqrt{\frac{2l+1}{4\pi}} -# d^{l}_{m,s}(\iota) e^{im\phi}. +# d^{l}_{m,s}(\iota) e^{imϕ}. # ``` # Just for convenience, we eliminate the negative sign on the left-hand side: # ```math # {}^{s}Y_{l,m} = (-1)^{-s} \sqrt{\frac{2l+1}{4\pi}} -# d^{l}_{m,-s}(\iota) e^{im\phi}. +# d^{l}_{m,-s}(\iota) e^{imϕ}. # ``` function ₛYₗₘ(s, l, m, ι::T, ϕ::T) where {T<:Real} (-1)^(-s) * √((2l + 1) / (4T(π))) * d(l, m, -s, ι) * exp(𝒾 * m * ϕ) @@ -84,11 +84,11 @@ end # For reference, several explicit formulas are also provided in Eqs. (II.9)--(II.13): # ```math # \begin{aligned} -# {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}} (1+\cos\iota)^2 e^{2i\phi},\\ -# {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 + \cos\iota) e^{i\phi},\\ +# {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}} (1+\cos\iota)^2 e^{2iϕ},\\ +# {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 + \cos\iota) e^{iϕ},\\ # {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ -# {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 - \cos\iota) e^{-i\phi},\\ -# {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64\pi}} (1-\cos\iota)^2 e^{-2i\phi}. +# {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 - \cos\iota) e^{-iϕ},\\ +# {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64\pi}} (1-\cos\iota)^2 e^{-2iϕ}. # \end{aligned} # ``` ₋₂Y₂₂(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 + cos(ι))^2 * exp(2𝒾*ϕ) diff --git a/docs/literate_input/conventions/comparisons/tait_1868.jl b/docs/literate_input/conventions/comparisons/tait_1868.jl index e5046389..54894a9b 100644 --- a/docs/literate_input/conventions/comparisons/tait_1868.jl +++ b/docs/literate_input/conventions/comparisons/tait_1868.jl @@ -45,22 +45,22 @@ terms of a single quaternion. > Here the vectors ``i``, ``j``, ``k`` in the original position of the body correspond to ``\overline{OA}``, ``\overline{OB}``, ``\overline{OC}`` respectively, at time ``t``. The -transposition is effected by — *first*, a rotation ``\psi`` about ``k``; *second*, a -rotation ``\theta`` about the new position of the line originally coinciding with ``j``; -*third*, a rotation ``\phi`` about the final position of the line at first coinciding with +transposition is effected by — *first*, a rotation ``ψ`` about ``k``; *second*, a +rotation ``θ`` about the new position of the line originally coinciding with ``j``; +*third*, a rotation ``ϕ`` about the final position of the line at first coinciding with ``k'``. So this is what would probably now be called the ``z-y'-z''`` convention for Euler angles -``(\psi, \theta, \phi)``, which is equivalent to ``(\phi, \theta, \psi)`` in the ``z-y-z`` +``(ψ, θ, ϕ)``, which is equivalent to ``(ϕ, θ, ψ)`` in the ``z-y-z`` convention used here. Indeed, Tait goes on to derive (somewhat laboriously) the expression for the quaternion: ```math -q = \cos \frac{\phi + \psi}{2} \cos \frac{\theta}{2} - + i \sin \frac{\phi - \psi}{2} \sin \frac{\theta}{2} - + j \cos \frac{\phi - \psi}{2} \sin \frac{\theta}{2} - + k \sin \frac{\phi + \psi}{2} \cos \frac{\theta}{2}, +q = \cos \frac{ϕ + ψ}{2} \cos \frac{θ}{2} + + i \sin \frac{ϕ - ψ}{2} \sin \frac{θ}{2} + + j \cos \frac{ϕ - ψ}{2} \sin \frac{θ}{2} + + k \sin \frac{ϕ + ψ}{2} \cos \frac{θ}{2}, ``` which is exactly the same as our expression from `from_euler_angles(ψ, θ, ϕ)`. diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl index 4f9d6f15..369c13c4 100644 --- a/docs/literate_input/conventions/comparisons/whittaker_1947.jl +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -166,7 +166,7 @@ axis_angle_rotation(ω, α, β, γ) = exp((ω / 2) * line(α, β, γ)) # > perpendicular to the plane ``zOZ``, drawn so that if ``OZ`` is directed to the vertical # > and the projection of ``Oz`` perpendicular to ``OZ`` is directed to the south, then # > ``OK`` is directed to the east. Denote the angles ``z\hat{O}Z``, ``Y\hat{O}K``, -# > ``y\hat{O}K`` by ``\theta``, ``\phi``, ``\psi``, respectively: these are known as the +# > ``y\hat{O}K`` by ``θ``, ``ϕ``, ``ψ``, respectively: these are known as the # > three *Eulerian angles* defining the position of the axes ``Oxyz`` with reference to the # > axes ``OXYZ``. # diff --git a/docs/src/background/operators.md b/docs/src/background/operators.md index 3f18592d..e5a5dbf0 100644 --- a/docs/src/background/operators.md +++ b/docs/src/background/operators.md @@ -116,7 +116,7 @@ The commutator relations for ``L`` are consistent — except for the differing use of ``\hbar`` — with the usual relations from quantum mechanics: ```math -\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. +\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} ε_{jkl} L_l. ``` Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we @@ -124,7 +124,7 @@ represent an arbitrary basis vector as ``\hat{e}_j``, then the quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` becomes ```math -[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. +[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} ε_{jkl} \hat{e}_l. ``` Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version @@ -204,7 +204,7 @@ function ``f`` under the action of this operator:[^2] &= \sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \delta_{ℓ,ℓ'} \delta_{m,m'+1} \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} δ_{ℓ,ℓ'} δ_{m,m'+1} \\ &= f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} \end{aligned} @@ -238,7 +238,7 @@ present for the spin-raising and -lowering operators: &= \sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \delta_{ℓ,ℓ'} \delta_{m,m'} \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} δ_{ℓ,ℓ'} δ_{m,m'} \\ &= f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} \end{aligned} diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 3d910395..15f87498 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -25,7 +25,7 @@ actually used by any of these sources: * Spin-weighted spherical harmonics - Behavior under rotation * Wigner D-matrices - - Representation à la $\langle ℓ, m' | e^{-i \alpha J_z} e^{-i \beta J_y} e^{-i \gamma J_z} | ℓ, m \rangle$ + - Representation à la $\langle ℓ, m' | e^{-i α J_z} e^{-i β J_y} e^{-i γ J_z} | ℓ, m \rangle$ - Rotation of spherical harmonics - Order of indices - Conjugation @@ -74,47 +74,47 @@ The upshot is that his definition agrees with ours, though he uses the "active" definition style. That is, the rotations are to be performed successively in order: -> 1. A rotation ``\alpha(0 \leq \alpha < 2\pi)`` about the ``z``-axis, +> 1. A rotation ``α(0 \leq α < 2\pi)`` about the ``z``-axis, > bringing the frame of axes from the initial position ``S`` into > the position ``S'``. The axis of this rotation is commonly > called the *vertical*. > -> 2. A rotation ``\beta(0 \leq \beta < \pi)`` about the ``y``-axis of +> 2. A rotation ``β(0 \leq β < \pi)`` about the ``y``-axis of > the frame ``S'``, called the *line of nodes*. Note that its > position is in general different from the initial position of the > ``y``-axis of the frame ``S``. The resulting position of the > frame of axes is symbolized by ``S''``. > -> 3. A rotation ``\gamma(0 \leq \gamma < 2\pi)`` about the ``z``-axis +> 3. A rotation ``γ(0 \leq γ < 2\pi)`` about the ``z``-axis > of the frame of axes ``S''``, called the *figure axis*; the > position of this axis depends on the previous rotations -> ``\alpha`` and ``\beta``. The final position of the frame is +> ``α`` and ``β``. The final position of the frame is > symbolized by ``S'''``. I would simply write the "``y``-axis of the frame ``S'``" as ``y'``, and so on. In quaternionic language, I would write these rotations as -``\exp[\gamma 𝐤''/2]\, \exp[\beta 𝐣'/2]\, \exp[\alpha 𝐤/2]``. But +``\exp[γ 𝐤''/2]\, \exp[β 𝐣'/2]\, \exp[α 𝐤/2]``. But we also have ```math -\exp[\beta 𝐣'/2] = \exp[\alpha 𝐤/2]\, \exp[\beta 𝐣/2]\, \exp[-\alpha 𝐤/2] +\exp[β 𝐣'/2] = \exp[α 𝐤/2]\, \exp[β 𝐣/2]\, \exp[-α 𝐤/2] ``` -so we can just swap the ``\alpha`` rotation with the ``\beta`` +so we can just swap the ``α`` rotation with the ``β`` rotation while dropping the prime from ``𝐣'``. We can do a similar -trick swapping the ``\alpha`` and ``\beta`` rotations with the -``\gamma`` rotation while dropping the double prime from ``𝐤''``. +trick swapping the ``α`` and ``β`` rotations with the +``γ`` rotation while dropping the double prime from ``𝐤''``. That is, an easy calculation shows that ```math -\exp[\gamma 𝐤''/2]\, \exp[\beta 𝐣'/2]\, \exp[\alpha 𝐤/2] +\exp[γ 𝐤''/2]\, \exp[β 𝐣'/2]\, \exp[α 𝐤/2] = -\exp[\alpha 𝐤/2]\, \exp[\beta 𝐣/2]\, \exp[\gamma 𝐤/2], +\exp[α 𝐤/2]\, \exp[β 𝐣/2]\, \exp[γ 𝐤/2], ``` which is precisely our definition. The spherical coordinates are implicitly defined by this statement: -> It should be noted that the polar coordinates ``\varphi, \theta`` +> It should be noted that the polar coordinates ``φ, θ`` > with respect to the original frame ``S`` of the ``z``-axis in its -> final position are identical with the Euler angles ``\alpha, \beta`` +> final position are identical with the Euler angles ``α, β`` > respectively. Again, this agrees with our definition. @@ -124,18 +124,18 @@ His expression for the angular-momentum operator in Euler angles — Eq. ```math \begin{aligned} L_x &= -i \hbar \left\{ - -\frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - - \sin\alpha \frac{\partial} {\partial \beta} - + \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + -\frac{\cos α}{\tan β} \frac{\partial} {\partial α} + - \sin α \frac{\partial} {\partial β} + + \frac{\cos α}{\sin β} \frac{\partial} {\partial γ} \right\}, \\ L_y &= -i \hbar \left\{ - -\frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - + \cos\alpha \frac{\partial} {\partial \beta} - +\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + -\frac{\sin α}{\tan β} \frac{\partial} {\partial α} + + \cos α \frac{\partial} {\partial β} + +\frac{\sin α}{\sin β} \frac{\partial} {\partial γ} \right\}, \\ -L_z &= -i \hbar \frac{\partial} {\partial \alpha}. +L_z &= -i \hbar \frac{\partial} {\partial α}. \end{aligned} ``` (The corresponding restriction to spherical coordinates also precisely @@ -144,10 +144,10 @@ agrees with our results, with the extra factor of ``\hbar``.) Unfortunately, there is disagreement over the definition of the Wigner D-matrices. In Eq. (4.1.12) he defines ```math -𝒟_{\alpha \beta \gamma} = -\exp\big( \frac{i\alpha}{\hbar} J_z\big) -\exp\big( \frac{i\beta}{\hbar} J_y\big) -\exp\big( \frac{i\gamma}{\hbar} J_z\big), +𝒟_{α β γ} = +\exp\big( \frac{iα}{\hbar} J_z\big) +\exp\big( \frac{iβ}{\hbar} J_y\big) +\exp\big( \frac{iγ}{\hbar} J_z\big), ``` which is the *conjugate* of most other definitions. @@ -167,45 +167,45 @@ Y_{ℓ,m}(x') = \sum_{m'} Y_{ℓ,m'}(x) D^{ℓ}_{m',m}\left( R^{-1} \right). ``` They then define the Euler angles as we do, and write [Eq. (3.4)] ```math -D^{ℓ}_{m', m}(\alpha, \beta, \gamma) +D^{ℓ}_{m', m}(α, β, γ) \equiv -D^{ℓ}_{m', m}\left( R(\alpha \beta \gamma)^{-1} \right) +D^{ℓ}_{m', m}\left( R(α β γ)^{-1} \right) = -e^{i m' \gamma} d^{ℓ}_{m', m}(\beta) e^{i m \alpha}. +e^{i m' γ} d^{ℓ}_{m', m}(β) e^{i m α}. ``` Finally, they derive [Eq. (3.9)] ```math -D^{j}_{m', m}(\alpha, \beta, \gamma) +D^{j}_{m', m}(α, β, γ) = \left[\frac{(j+m)!(j-m)!}{(j+m')!(j-m')!}\right]^{1/2} -(\sin \frac{1}{2}\beta)^{2j} +(\sin \frac{1}{2}β)^{2j} \sum_r \binom{j+m'}{r} \binom{j-m'}{r-m-m'} (-1)^{j+m'-r} -e^{im\alpha} -(\cot \tfrac{1}{2}\beta)^{2r-m-m'} -e^{im'\gamma}. +e^{imα} +(\cot \tfrac{1}{2}β)^{2r-m-m'} +e^{im'γ}. ``` Equation (3.11) naturally extends to ```math - {}_sY_{ℓ, m}(\theta, \phi, \gamma) + {}_sY_{ℓ, m}(θ, ϕ, γ) = \left[ \left(2ℓ+1\right) / 4\pi \right]^{1/2} - D^{ℓ}_{-s,m}(\phi, \theta, \gamma), + D^{ℓ}_{-s,m}(ϕ, θ, γ), ``` -where Eq. (3.4) also shows that ``D^{ℓ}_{m', m}(\alpha, \beta, -\gamma) = D^{ℓ}_{m', m}(\alpha, \beta, 0) e^{i m' \gamma}``, +where Eq. (3.4) also shows that ``D^{ℓ}_{m', m}(α, β, +γ) = D^{ℓ}_{m', m}(α, β, 0) e^{i m' γ}``, so we have ```math - {}_sY_{ℓ, m}(\theta, \phi, \gamma) + {}_sY_{ℓ, m}(θ, ϕ, γ) = - {}_sY_{ℓ, m}(\theta, \phi)\, e^{-i s \gamma}. + {}_sY_{ℓ, m}(θ, ϕ)\, e^{-i s γ}. ``` This is the most natural extension of the standard spin-weighted spherical harmonics to ``\mathrm{Spin}(3)``. In particular, the -spin-weight operator is ``i \partial_\gamma``, which suggests that it +spin-weight operator is ``i \partial_γ``, which suggests that it will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i -\partial_\gamma``. +\partial_γ``. ## Griffiths (1995) @@ -228,27 +228,27 @@ P_{ℓ}(x) ``` Then, (4.32) gives the spherical harmonics as ```math -Y_{ℓ}^{m}(\theta, \phi) +Y_{ℓ}^{m}(θ, ϕ) = -\epsilon +ϵ \sqrt{\frac{2ℓ+1}{4\pi} \frac{(ℓ-|m|)!}{(ℓ+|m|)!}} -e^{im\phi} P_{ℓ}^{m}(\cos\theta), +e^{imϕ} P_{ℓ}^{m}(\cos θ), ``` -where ``\epsilon = (-1)^m`` for ``m\geq 0`` and ``\epsilon = 1`` for +where ``ϵ = (-1)^m`` for ``m\geq 0`` and ``ϵ = 1`` for ``m\leq 0``. In Table 4.2, he explicitly lists the first few spherical harmonics: ```math \begin{aligned} Y_{0}^{0} &= \left(\frac{1}{4\pi}\right)^{1/2},\\ - Y_{1}^{0} &= \left(\frac{3}{4\pi}\right)^{1/2} \cos\theta,\\ - Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8\pi}\right)^{1/2} \sin\theta e^{\pm i\phi},\\ - Y_{2}^{0} &= \left(\frac{5}{16\pi}\right)^{1/2} \left(3\cos^2\theta - 1\right),\\ - Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8\pi}\right)^{1/2} \sin\theta \cos\theta e^{\pm i\phi},\\ - Y_{2}^{\pm 2} &= \left(\frac{15}{32\pi}\right)^{1/2} \sin^2\theta e^{\pm 2i\phi},\\ - Y_{3}^{0} &= \left(\frac{7}{16\pi}\right)^{1/2} \left(5\cos^3\theta - 3\cos\theta\right),\\ - Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64\pi}\right)^{1/2} \sin\theta \left(5\cos^2\theta - 1\right) e^{\pm i\phi},\\ - Y_{3}^{\pm 2} &= \left(\frac{105}{32\pi}\right)^{1/2} \sin^2\theta \cos\theta e^{\pm 2i\phi},\\ - Y_{3}^{\pm 3} &= \mp \left(\frac{35}{64\pi}\right)^{1/2} \sin^3\theta e^{\pm 3i\phi}. + Y_{1}^{0} &= \left(\frac{3}{4\pi}\right)^{1/2} \cos θ,\\ + Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8\pi}\right)^{1/2} \sin θ e^{\pm iϕ},\\ + Y_{2}^{0} &= \left(\frac{5}{16\pi}\right)^{1/2} \left(3\cos^2θ - 1\right),\\ + Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8\pi}\right)^{1/2} \sin θ \cos θ e^{\pm iϕ},\\ + Y_{2}^{\pm 2} &= \left(\frac{15}{32\pi}\right)^{1/2} \sin^2θ e^{\pm 2iϕ},\\ + Y_{3}^{0} &= \left(\frac{7}{16\pi}\right)^{1/2} \left(5\cos^3θ - 3\cos θ\right),\\ + Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64\pi}\right)^{1/2} \sin θ \left(5\cos^2θ - 1\right) e^{\pm iϕ},\\ + Y_{3}^{\pm 2} &= \left(\frac{105}{32\pi}\right)^{1/2} \sin^2θ \cos θ e^{\pm 2iϕ},\\ + Y_{3}^{\pm 3} &= \mp \left(\frac{35}{64\pi}\right)^{1/2} \sin^3θ e^{\pm 3iϕ}. \end{aligned} ``` In Eqs. (4.127)—(4.129), he gives the angular-momentum operators in @@ -256,14 +256,14 @@ terms of spherical coordinates: ```math \begin{aligned} L_x &= \frac{\hbar}{i} \left( - -\sin\phi \frac{\partial} {\partial \theta} - - \cos\phi \cot\theta \frac{\partial} {\partial \phi} + -\sin ϕ \frac{\partial} {\partial θ} + - \cos ϕ \cot θ \frac{\partial} {\partial ϕ} \right), \\ L_y &= \frac{\hbar}{i} \left( - \cos\phi \frac{\partial} {\partial \theta} - - \sin\phi \cot\theta \frac{\partial} {\partial \phi} + \cos ϕ \frac{\partial} {\partial θ} + - \sin ϕ \cot θ \frac{\partial} {\partial ϕ} \right), \\ -L_z &= -i \hbar \frac{\partial} {\partial \phi}. +L_z &= -i \hbar \frac{\partial} {\partial ϕ}. \end{aligned} ``` @@ -277,13 +277,13 @@ L_z &= -i \hbar \frac{\partial} {\partial \phi}. [LeBellac_2006](@citet) (with Foreword by Cohen-Tannoudji) takes an odd approach, defining [Eq. (10.32)] ```math -D^{(j)}_{m', m} \left[ ℛ(\theta, \phi) \right] +D^{(j)}_{m', m} \left[ ℛ(θ, ϕ) \right] = -\langle j, m' | e^{-i\phi J_z} e^{-i\theta J_y} | j, m \rangle, +\langle j, m' | e^{-iϕ J_z} e^{-iθ J_y} | j, m \rangle, ``` -but later allowing that ``e^{-i \psi J_z}`` usually goes on the -right-hand side of the others, in which case ``D^{(j)}(\theta, \phi) -\to D^{(j)}(\phi, \theta, \psi)``. Figure 10.1 shows that the +but later allowing that ``e^{-i ψ J_z}`` usually goes on the +right-hand side of the others, in which case ``D^{(j)}(θ, ϕ) +\to D^{(j)}(ϕ, θ, ψ)``. Figure 10.1 shows that the spherical coordinates are standard (physicist's) coordinates. Equation (10.65) shows the rotation law: @@ -295,9 +295,9 @@ Y_{ℓ}^{m}\left( ℛ^{-1} \hat{r} \right) and Eq. (10.66) relates the spherical harmonics to the Wigner D-matrices: ```math -D^{(ℓ)}_{m, 0}(\theta, \phi) +D^{(ℓ)}_{m, 0}(θ, ϕ) = -\sqrt{\frac{4\pi}{2ℓ+1}} \left[Y_{ℓ}^{m}(\theta, \phi)\right]^\ast. +\sqrt{\frac{4\pi}{2ℓ+1}} \left[Y_{ℓ}^{m}(θ, ϕ)\right]^\ast. ``` @@ -331,10 +331,10 @@ expressions in terms of Euler angles. We can find conventions at [this page](https://reference.wolfram.com/language/ref/WignerD.html). -> The Wolfram Language uses phase conventions where ``D^j_{m_1, m_2}(\psi, \theta, \phi) = \exp(i m_1 \psi + i m_2 \phi) D^j_{m_1, m_2}(0, \theta, 0)``. +> The Wolfram Language uses phase conventions where ``D^j_{m_1, m_2}(ψ, θ, ϕ) = \exp(i m_1 ψ + i m_2 ϕ) D^j_{m_1, m_2}(0, θ, 0)``. -> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` = ``-\sqrt{2} e^{i \phi} \cos\frac{\theta}{2} -> \sin\frac{\theta}{2}`` +> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` = ``-\sqrt{2} e^{i ϕ} \cos\frac{θ}{2} +> \sin\frac{θ}{2}`` > `WignerD[{𝓁, 0, m}, θ, ϕ] == Sqrt[(4 π)/(2 𝓁 + 1)] SphericalHarmonicY[𝓁, m, θ, ϕ]` @@ -342,7 +342,7 @@ page](https://reference.wolfram.com/language/ref/WignerD.html). > ϕ]]` -> For ``ℓ \geq 0``, ``Y_ℓ^m = \sqrt{(2ℓ+1)/(4\pi)} \sqrt{(ℓ-m)! / (ℓ+m)!} P_ℓ^m(\cos \theta) e^{im\phi}`` where ``P_ℓ^m`` is the associated Legendre function. +> For ``ℓ \geq 0``, ``Y_ℓ^m = \sqrt{(2ℓ+1)/(4\pi)} \sqrt{(ℓ-m)! / (ℓ+m)!} P_ℓ^m(\cos θ) e^{imϕ}`` where ``P_ℓ^m`` is the associated Legendre function. > The associated Legendre polynomials are defined by ``P_n^m(x) = (-1)^m (1-x^2)^{m/2}(d^m/dx^m)P_n(x)`` where ``P_n(x)`` is the Legendre polynomial. @@ -358,16 +358,16 @@ In their 1966 paper, [Newman_1966](@citet), Newman and Penrose first introduced the spin-weighted spherical harmonics, ``{}_sY_{ℓ m}``. They use the standard (physicists') convention for spherical coordinates and introduce the stereographic coordinate ``\zeta = -e^{i\phi} \cot\frac{\theta}{2}``. They define the spin-raising +e^{iϕ} \cot\frac{θ}{2}``. They define the spin-raising operator ``\eth`` acting on a function of spin weight ``s`` as ```math \eth \eta = --\left(\sin\theta\right)^s +-\left(\sin θ\right)^s \left\{ - \frac{\partial}{\partial\theta} - + \frac{i}{\sin\theta} \frac{\partial}{\partial\phi} -\right\} \left\{\left(\sin\theta\right)^{-s} \eta\right\}, + \frac{\partial}{\partial θ} + + \frac{i}{\sin θ} \frac{\partial}{\partial ϕ} +\right\} \left\{\left(\sin θ\right)^{-s} \eta\right\}, ``` They then compute ```math @@ -388,49 +388,49 @@ basis vector ``m^\mu`` to the coordinates. > orthogonal tangent vectors (of length ``2^{-1/2}``) at each point of > the surface. [...] If spherical polar coordinates are used, a > natural choice for ``m^\mu`` is to make ``\Re(m^\mu)`` and -> ``\Im(m^\mu)`` tangential, respectively, to the curves ``\phi = -> \mathrm{const}`` and ``\theta = \mathrm{const}.`` +> ``\Im(m^\mu)`` tangential, respectively, to the curves ``ϕ = +> \mathrm{const}`` and ``θ = \mathrm{const}.`` The ambiguity is in the sign implied by "tangential", but the natural choice is to assume they mean that the components are *positive* -multiples of ``\partial_\theta`` and ``\partial_\phi`` respectively, +multiples of ``\partial_θ`` and ``\partial_ϕ`` respectively, in which case we have ```math m^\mu = \frac{1}{\sqrt{2}} -\left[ \partial_\theta + i \csc\theta \partial_\phi \right]. +\left[ \partial_θ + i \csc θ \partial_ϕ \right]. ``` They define the spin weight in terms of behavior of a quantity under rotation of ``m^\mu`` in its own plane as ```math (m^\mu)' = -e^{i\psi} m^\mu +e^{iψ} m^\mu = \frac{1}{\sqrt{2}} \left[ - \left(\cos\psi\partial_\theta - \sin\psi\csc\theta \partial_\phi\right) - + i \left(\cos\psi\csc\theta \partial_\phi + \sin\psi\partial_\theta\right) + \left(\cos ψ\partial_θ - \sin ψ\csc θ \partial_ϕ\right) + + i \left(\cos ψ\csc θ \partial_ϕ + \sin ψ\partial_θ\right) \right]. ``` -Raising the spherical coordinates ``(\theta, \phi)`` to Euler angles -``(\phi, \theta, -\psi)``, we see that the rotor ``R_{\phi, \theta, --\psi}`` rotates the ``𝐳`` basis vector to the point ``(\theta, -\phi)``, and it rotates ``(𝐱 + i 𝐲) / \sqrt{2}`` onto ``(m^\mu)'``. +Raising the spherical coordinates ``(θ, ϕ)`` to Euler angles +``(ϕ, θ, -ψ)``, we see that the rotor ``R_{ϕ, θ, +-ψ}`` rotates the ``𝐳`` basis vector to the point ``(θ, +ϕ)``, and it rotates ``(𝐱 + i 𝐲) / \sqrt{2}`` onto ``(m^\mu)'``. Under this rotation, a quantity ``\eta`` has spin weight ``s`` if it transforms as ```math -\eta' = e^{i s \psi} \eta. +\eta' = e^{i s ψ} \eta. ``` Now, supposing that these quantities are functions of Euler angles, we can write ```math -\eta(\phi, \theta, -\psi) = e^{i s \psi} \eta(\phi, \theta, 0), +\eta(ϕ, θ, -ψ) = e^{i s ψ} \eta(ϕ, θ, 0), ``` or ```math -\eta(\phi, \theta, \gamma) = e^{-i s \gamma} \eta(\phi, \theta, 0). +\eta(ϕ, θ, γ) = e^{-i s γ} \eta(ϕ, θ, 0). ``` -Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. +Thus, the operator with eigenvalue ``s`` is ``i \partial_γ``. ## NINJA @@ -454,51 +454,51 @@ Thus, the operator with eigenvalue ``s`` is ``i \partial_\gamma``. [Shankar_1994](@citet) writes in Eq. (12.5.35) the spherical harmonics as ```math -Y_{ℓ}^{m}(\theta, \phi) +Y_{ℓ}^{m}(θ, ϕ) = (-1)^ℓ \left[ \frac{(2ℓ+1)!}{4\pi} \right]^{1/2} \frac{1}{2^ℓ ℓ!} \left[ \frac{(ℓ+m)!}{(2ℓ)!(ℓ-m)!} \right]^{1/2} -e^{i m \phi} -(\sin \theta)^{-m} -\frac{d^{ℓ-m}}{d(\cos\theta)^{ℓ-m}} -(\sin\theta)^{2ℓ} +e^{i m ϕ} +(\sin θ)^{-m} +\frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} +(\sin θ)^{2ℓ} ``` for ``m \geq 0``, with (12.5.40) giving the expression ```math -Y_{ℓ}^{-m}(\theta, \phi) +Y_{ℓ}^{-m}(θ, ϕ) = -(-1)^m \left( Y_{ℓ}^{m}(\theta, \phi) \right)^\ast. +(-1)^m \left( Y_{ℓ}^{m}(θ, ϕ) \right)^\ast. ``` The angular-momentum operators are given below (12.5.27) as ```math \begin{aligned} L_x &= i \hbar \left( - \sin\phi \frac{\partial} {\partial \theta} - + \cos\phi \cot\theta \frac{\partial} {\partial \phi} + \sin ϕ \frac{\partial} {\partial θ} + + \cos ϕ \cot θ \frac{\partial} {\partial ϕ} \right), \\ L_y &= i \hbar \left( - -\cos\phi \frac{\partial} {\partial \theta} - + \sin\phi \cot\theta \frac{\partial} {\partial \phi} + -\cos ϕ \frac{\partial} {\partial θ} + + \sin ϕ \cot θ \frac{\partial} {\partial ϕ} \right), \\ -L_z &= -i \hbar \frac{\partial} {\partial \phi}. +L_z &= -i \hbar \frac{\partial} {\partial ϕ}. \end{aligned} ``` In Exercise 12.5.7, the rotation operator is defined by ```math -U\left[ R(\alpha, \beta, \gamma) \right] +U\left[ R(α, β, γ) \right] = -e^{-i \alpha J_z/\hbar} -e^{-i \beta J_y/\hbar} -e^{-i \gamma J_z/\hbar}, +e^{-i α J_z/\hbar} +e^{-i β J_y/\hbar} +e^{-i γ J_z/\hbar}, ``` That ``U`` becomes a ``D^{(j)}`` when the operator is acting on the states ``|j, m\rangle`` for a given ``j``. Thus, while Shankar never actually uses notation like ``D^{(j)}_{m', m}``, he does talk about -``\langle j, m' | D^{(j)}\left[ R(\alpha, \beta, \gamma) \right] | j, +``\langle j, m' | D^{(j)}\left[ R(α, β, γ) \right] | j, m \rangle``. @@ -513,21 +513,21 @@ Specifically, the [source](https://github.com/sympy/sympy/blob/b4ce69ad5d40e4e545614b6c76ca9b0be0b98f0b/sympy/physics/wigner.py#L1136-L1191) cites [Edmonds_2016](@citet) when defining ```math -𝒟_{\alpha \beta \gamma} = -\exp\big( \frac{i\alpha}{\hbar} J_z\big) -\exp\big( \frac{i\beta}{\hbar} J_y\big) -\exp\big( \frac{i\gamma}{\hbar} J_z\big). +𝒟_{α β γ} = +\exp\big( \frac{iα}{\hbar} J_z\big) +\exp\big( \frac{iβ}{\hbar} J_y\big) +\exp\big( \frac{iγ}{\hbar} J_z\big). ``` But that is an incorrect copy of Edmonds' Eq. (4.1.9), in which the -``\alpha`` and ``\gamma`` on the right-hand side are swapped. The +``α`` and ``γ`` on the right-hand side are swapped. The code also implements D in the `wigner_d` function as (essentially) ```python exp(I*mprime*alpha)*d[i, j]*exp(I*m*gamma) ``` even though the actual equation Eq. (4.1.12) of Edmonds says ```math -\mathscr{D}^{(j)}_{m' m}(\alpha \beta \gamma) = -\exp i m' \gamma d^{(j)}_{m' m}(\alpha, \beta) \exp(i m \alpha). +\mathscr{D}^{(j)}_{m' m}(α β γ) = +\exp i m' γ d^{(j)}_{m' m}(α, β) \exp(i m α). ``` The ``d`` matrix appears to be implemented consistently with Edmonds, and thus not affected. @@ -555,9 +555,9 @@ D-matrix to satisfy [Eq. (2.45)] ``` Including the arguments to the spherical harmonics, this becomes ```math -Y_{l,m}\big(ℛ^{-1} R_{\theta, \phi}\big) +Y_{l,m}\big(ℛ^{-1} R_{θ, ϕ}\big) = -\sum_{m} D^l_{m',m}(ℛ) Y_{l,m'}\big(R_{\theta, \phi}\big). +\sum_{m} D^l_{m',m}(ℛ) Y_{l,m'}\big(R_{θ, ϕ}\big). ``` In this form, we have [Eq. (2.46)] ```math @@ -567,31 +567,31 @@ D^l_{m'',m}(ℛ_1 ℛ_2) ``` He computes [Eq. (2.53)] ```math -D^l_{m',m}(\phi, \theta, \chi) +D^l_{m',m}(ϕ, θ, \chi) = -e^{-i m' \phi} d^l_{m',m}(\theta) e^{-i m \chi}, +e^{-i m' ϕ} d^l_{m',m}(θ) e^{-i m \chi}, ``` where the ``d`` matrix is given by ```math -d^l_{m',m}(\theta) +d^l_{m',m}(θ) = \sqrt{(l+m)!(l-m)!(l+m')!(l-m')!} \sum_{k} \frac{ (-1)^k - (\sin \tfrac{1}{2} \theta)^{m-m'+2k} - (\cos \tfrac{1}{2} \theta)^{2l-m+m'-2k} + (\sin \tfrac{1}{2} θ)^{m-m'+2k} + (\cos \tfrac{1}{2} θ)^{2l-m+m'-2k} } { k!(l+m'-k)!(l-m-k)!(m-m'+k)! }, ``` and the spin-weighted spherical harmonic is related to ``D`` by ```math -{}_{s}Y_{j,m}(\theta, \phi) +{}_{s}Y_{j,m}(θ, ϕ) = (-1)^m \sqrt{\frac{2j+1}{4\pi}} -d^j_{-m,s}(\theta) -e^{i m \phi}. +d^j_{-m,s}(θ) +e^{i m ϕ}. ``` @@ -602,11 +602,11 @@ definitions related to the rotation matrix by previous authors. Eq. 1.4.(31) defines the operator ```math -\hat{D}(\alpha, \beta, \gamma) +\hat{D}(α, β, γ) = -e^{-i\alpha \hat{J}_z} -e^{-i\beta \hat{J}_y} -e^{-i\gamma \hat{J}_z}, +e^{-iα \hat{J}_z} +e^{-iβ \hat{J}_y} +e^{-iγ \hat{J}_z}, ``` where the ``\hat{J}`` operators are defined in @@ -617,25 +617,25 @@ where the ``\hat{J}`` operators are defined in > > A transformation of an arbitrary wave function ``\Psi`` under > rotation of the coordinate system through an infinitesimal angle -> ``\delta \omega`` about an axis ``𝐧`` may be written as +> ``δ \omega`` about an axis ``𝐧`` may be written as > ```math -> \Psi \to \Psi' = \left(1 - i \delta \omega 𝐧 \cdot \hat{J} \right)\Psi, +> \Psi \to \Psi' = \left(1 - i δ \omega 𝐧 \cdot \hat{J} \right)\Psi, > ``` > where ``\hat{J}`` is the total angular momentum operator. Eq. 4.1.(1) defines the Wigner D-functions according to ```math -\langle J M | \hat{D}(\alpha, \beta, \gamma) | J' M' \rangle +\langle J M | \hat{D}(α, β, γ) | J' M' \rangle = -\delta_{J J'} D^J_{M M'}(\alpha, \beta, \gamma). +δ_{J J'} D^J_{M M'}(α, β, γ). ``` Eq. 4.3.(1) states ```math -D^J_{M M'}(\alpha, \beta, \gamma) +D^J_{M M'}(α, β, γ) = -e^{-i M \alpha} -d^J_{M M'}(\beta) -e^{-i M' \gamma} +e^{-i M α} +d^J_{M M'}(β) +e^{-i M' γ} ``` @@ -661,24 +661,24 @@ angular momentum of the rigid symmetric top. They then give in Eq. non-rotating (lab-fixed) system" as ```math \begin{gather} - \hat{J}_{\pm 1} = \frac{i}{\sqrt{2}} e^{\pm i \alpha} \left[ - \mp \cot\beta \frac{\partial}{\partial \alpha} - + i \frac{\partial}{\partial \beta} - \pm \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + \hat{J}_{\pm 1} = \frac{i}{\sqrt{2}} e^{\pm i α} \left[ + \mp \cot β \frac{\partial}{\partial α} + + i \frac{\partial}{\partial β} + \pm \frac{1}{\sin β} \frac{\partial}{\partial γ} \right] \\ - \hat{J}_0 = - i \frac{\partial}{\partial \alpha}, + \hat{J}_0 = - i \frac{\partial}{\partial α}, \end{gather} ``` and "contravariant components of ``\hat{𝐉}`` in the rotating (body-fixed) system" as ```math \begin{gather} - \hat{J}'^{\pm 1} = \frac{i}{\sqrt{2}} e^{\mp i \gamma} \left[ - \pm \cot\beta \frac{\partial}{\partial \gamma} - + i \frac{\partial}{\partial \beta} - \mp \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + \hat{J}'^{\pm 1} = \frac{i}{\sqrt{2}} e^{\mp i γ} \left[ + \pm \cot β \frac{\partial}{\partial γ} + + i \frac{\partial}{\partial β} + \mp \frac{1}{\sin β} \frac{\partial}{\partial α} \right] \\ - \hat{J}'^0 = - i \frac{\partial}{\partial \gamma}. + \hat{J}'^0 = - i \frac{\partial}{\partial γ}. \end{gather} ``` (Note the prime in the last two equations.) We can expand these in @@ -689,46 +689,46 @@ covariant components: \hat{J}_{x} &= -\frac{1}{\sqrt{2}} \left( \hat{J}_{+1} - \hat{J}_{-1} \right) \\ % &= -\frac{1}{\sqrt{2}} \left( - % \frac{i}{\sqrt{2}} e^{i \alpha} \left[ - % - \cot\beta \frac{\partial}{\partial \alpha} - % + i \frac{\partial}{\partial \beta} - % + \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \frac{i}{\sqrt{2}} e^{i α} \left[ + % - \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial γ} % \right] % - - % \frac{i}{\sqrt{2}} e^{-i \alpha} \left[ - % + \cot\beta \frac{\partial}{\partial \alpha} - % + i \frac{\partial}{\partial \beta} - % - \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \frac{i}{\sqrt{2}} e^{-i α} \left[ + % + \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial γ} % \right] % \right) \\ &= i\left[ - \frac{\cos\alpha}{\tan\beta} \frac{\partial}{\partial \alpha} - + \sin\alpha \frac{\partial}{\partial \beta} - - \frac{\cos\alpha}{\sin\beta} \frac{\partial}{\partial \gamma} + \frac{\cos α}{\tan β} \frac{\partial}{\partial α} + + \sin α \frac{\partial}{\partial β} + - \frac{\cos α}{\sin β} \frac{\partial}{\partial γ} \right] \\ \hat{J}_{y} &= -\frac{1}{i\sqrt{2}} \left( \hat{J}_{+1} + \hat{J}_{-1} \right) \\ % &= -\frac{1}{i\sqrt{2}} \left( - % \frac{i}{\sqrt{2}} e^{i \alpha} \left[ - % - \cot\beta \frac{\partial}{\partial \alpha} - % + i \frac{\partial}{\partial \beta} - % + \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \frac{i}{\sqrt{2}} e^{i α} \left[ + % - \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial γ} % \right] % + - % \frac{i}{\sqrt{2}} e^{-i \alpha} \left[ - % + \cot\beta \frac{\partial}{\partial \alpha} - % + i \frac{\partial}{\partial \beta} - % - \frac{1}{\sin\beta} \frac{\partial}{\partial \gamma} + % \frac{i}{\sqrt{2}} e^{-i α} \left[ + % + \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial γ} % \right] % \right) \\ &= i \left[ - \frac{\sin\alpha}{\tan\beta} \frac{\partial}{\partial \alpha} - - \cos\alpha \frac{\partial}{\partial \beta} - - \frac{\sin\alpha}{\sin\beta} \frac{\partial}{\partial \gamma} + \frac{\sin α}{\tan β} \frac{\partial}{\partial α} + - \cos α \frac{\partial}{\partial β} + - \frac{\sin α}{\sin β} \frac{\partial}{\partial γ} \right] \\ \hat{J}_{z} &= \hat{J}_{0} \\ - &= -i \frac{\partial}{\partial \alpha} + &= -i \frac{\partial}{\partial α} \end{aligned} ``` We can compare these to the [Full expressions on ``𝕊³``]() `@ref`, and find @@ -741,46 +741,46 @@ Next, the contravariant components: \hat{J}'_{x} &= -\frac{1}{\sqrt{2}} \left( \hat{J}'^{+1} - \hat{J}'^{-1} \right) \\ % &= -\frac{1}{\sqrt{2}} \left( - % \frac{i}{\sqrt{2}} e^{- i \gamma} \left[ - % + \cot\beta \frac{\partial}{\partial \gamma} - % + i \frac{\partial}{\partial \beta} - % - \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \frac{i}{\sqrt{2}} e^{- i γ} \left[ + % + \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial α} % \right] % - - % \frac{i}{\sqrt{2}} e^{+ i \gamma} \left[ - % - \cot\beta \frac{\partial}{\partial \gamma} - % + i \frac{\partial}{\partial \beta} - % + \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \frac{i}{\sqrt{2}} e^{+ i γ} \left[ + % - \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial α} % \right] % \right) \\ &= -i \left( - \frac{\cos\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - + \sin\gamma \frac{\partial}{\partial \beta} - - \frac{\cos\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \frac{\cos γ}{\tan β} \frac{\partial}{\partial γ} + + \sin γ \frac{\partial}{\partial β} + - \frac{\cos γ}{\sin β} \frac{\partial}{\partial α} \right) \\ \hat{J}'_{y} &= \frac{1}{i\sqrt{2}} \left( \hat{J}'^{+1} + \hat{J}'^{-1} \right) \\ % &= \frac{1}{i\sqrt{2}} \left( - % \frac{i}{\sqrt{2}} e^{-i \gamma} \left[ - % + \cot\beta \frac{\partial}{\partial \gamma} - % + i \frac{\partial}{\partial \beta} - % - \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \frac{i}{\sqrt{2}} e^{-i γ} \left[ + % + \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial α} % \right] % + - % \frac{i}{\sqrt{2}} e^{+ i \gamma} \left[ - % - \cot\beta \frac{\partial}{\partial \gamma} - % + i \frac{\partial}{\partial \beta} - % + \frac{1}{\sin\beta} \frac{\partial}{\partial \alpha} + % \frac{i}{\sqrt{2}} e^{+ i γ} \left[ + % - \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial α} % \right] % \right) \\ &= -i \left( - \frac{\sin\gamma}{\tan\beta} \frac{\partial}{\partial \gamma} - - \cos\gamma \frac{\partial}{\partial \beta} - - \frac{\sin\gamma}{\sin\beta} \frac{\partial}{\partial \alpha} + \frac{\sin γ}{\tan β} \frac{\partial}{\partial γ} + - \cos γ \frac{\partial}{\partial β} + - \frac{\sin γ}{\sin β} \frac{\partial}{\partial α} \right) \\ \hat{J}'_{z} &= \hat{J}'^{0} \\ - &= -i \frac{\partial}{\partial \gamma} + &= -i \frac{\partial}{\partial γ} \end{aligned} ``` Unfortunately, while we have agreement on ``\hat{J}'^{y} = R_y``, we @@ -809,11 +809,11 @@ Spin-weighted spherical harmonics Defining the operator ```math -ℛ(\alpha,\beta,\gamma) = e^{-i\alpha J_z}e^{-i\beta J_y}e^{-i\gamma J_z}, +ℛ(α,β,γ) = e^{-iα J_z}e^{-iβ J_y}e^{-iγ J_z}, ``` [Wikipedia expresses the Wigner D-matrix](https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix) as ```math -D^j_{m'm}(\alpha,\beta,\gamma) \equiv \langle jm' | ℛ(\alpha,\beta,\gamma)| jm \rangle =e^{-im'\alpha } d^j_{m'm}(\beta)e^{-i m\gamma}. +D^j_{m'm}(α,β,γ) \equiv \langle jm' | ℛ(α,β,γ)| jm \rangle =e^{-im'α } d^j_{m'm}(β)e^{-i mγ}. ``` @@ -833,7 +833,7 @@ In Appendix B.1, we find that the spherical coordinates are related to Cartesian coordinates in the usual (physicist's) way. Equation (5.132) gives the angular-momentum operator ```math -\hat{L}_z = -i \hbar \frac{\partial}{\partial \varphi}, +\hat{L}_z = -i \hbar \frac{\partial}{\partial φ}, ``` which agrees with [our expression](@ref "``L`` operators in spherical coordinates"). This is followed by equation (5.134): @@ -842,9 +842,9 @@ coordinates"). This is followed by equation (5.134): = \hat{L}_x \pm i \hat{L}_y = -\pm \hbar e^{\pm i\varphi} \left( - \frac{\partial}{\partial \theta} - \pm i \cot\theta \frac{\partial}{\partial \varphi} +\pm \hbar e^{\pm iφ} \left( + \frac{\partial}{\partial θ} + \pm i \cot θ \frac{\partial}{\partial φ} \right), ``` which also agrees with [our results.](@ref "``L_{\pm}`` operators in @@ -852,77 +852,77 @@ spherical coordinates") Equation (5.180) gives the spherical harmonics as ```math -Y_{l, m}(\theta, \varphi) +Y_{l, m}(θ, φ) = \frac{(-1)^l}{2^l l!} \sqrt{\frac{2l+1}{4\pi} \frac{(l+m)!}{(l-m)!}} -e^{im\varphi} -\frac{1}{\sin^m \theta} -\frac{d^{l-m}}{d(\cos\theta)^{l-m}} -(\sin \theta)^{2l}. +e^{imφ} +\frac{1}{\sin^m θ} +\frac{d^{l-m}}{d(\cos θ)^{l-m}} +(\sin θ)^{2l}. ``` -Section 7.2.1 denotes by ``\hat{R}_z(\delta \phi)`` the +Section 7.2.1 denotes by ``\hat{R}_z(δ ϕ)`` the > rotation of the coordinates of a *spinless* particle over an -> *infinitesimal* angle ``\delta \phi`` about the ``z``-axis +> *infinitesimal* angle ``δ ϕ`` about the ``z``-axis and shows its action [Eq. (7.16)] ```math -\hat{R}_z (\delta \phi) \psi(r, \theta, \phi) +\hat{R}_z (δ ϕ) ψ(r, θ, ϕ) = -\psi(r, \theta, \phi - \delta \phi). +ψ(r, θ, ϕ - δ ϕ). ``` -> We may generalize this relation to a rotation of angle ``\delta -> \phi`` about an arbitrary axis whose direction is given by the unit +> We may generalize this relation to a rotation of angle ``δ +> ϕ`` about an arbitrary axis whose direction is given by the unit > vector ``\vec{n}``: ```math -\hat{R}(\delta \phi) +\hat{R}(δ ϕ) = -1 - \frac{i}{\hbar} \delta \phi \vec{n} \cdot \hat{\vec{L}}. +1 - \frac{i}{\hbar} δ ϕ \vec{n} \cdot \hat{\vec{L}}. ``` This extends to finite rotation by defining the operator [Eq. (7.48)] ```math -\hat{R}(\alpha, \beta, \gamma) +\hat{R}(α, β, γ) = -e^{-i\alpha J_z / \hbar} e^{-i\beta J_y / \hbar} e^{-i\gamma J_z / \hbar}. +e^{-iα J_z / \hbar} e^{-iβ J_y / \hbar} e^{-iγ J_z / \hbar}. ``` Equation (7.52) then defines ```math -D^{(j)}_{m', m}(\alpha, \beta, \gamma) +D^{(j)}_{m', m}(α, β, γ) = -\langle j, m' | \hat{R}(\alpha, \beta, \gamma) | j, m \rangle, +\langle j, m' | \hat{R}(α, β, γ) | j, m \rangle, ``` So that [Eq. (7.54)] ```math -D^{(j)}_{m', m}(\alpha, \beta, \gamma) +D^{(j)}_{m', m}(α, β, γ) = -e^{-i (m' \alpha + m \gamma)} d^{(j)}_{m', m}(\beta), +e^{-i (m' α + m γ)} d^{(j)}_{m', m}(β), ``` where [Eq. (7.55)] ```math -d^{(j)}_{m', m}(\beta) +d^{(j)}_{m', m}(β) = -\langle j, m' | e^{-i\beta J_y / \hbar} | j, m \rangle. +\langle j, m' | e^{-iβ J_y / \hbar} | j, m \rangle. ``` The explicit expression for ``d`` is [Eq. (7.56)] ```math -d^{(j)}_{m', m}(\beta) +d^{(j)}_{m', m}(β) = \sum_k (-1)^{k+m'-m} \frac{\sqrt{(j+m)!(j-m)!(j+m')!(j-m')!}} {(j-m'-k)!(j+m-k)!(k+m'-m)!k!} -\left(\cos\frac{\beta}{2}\right)^{2j+m-m'-2k} -\left(\sin\frac{\beta}{2}\right)^{m'-m+2k}. +\left(\cos\frac{β}{2}\right)^{2j+m-m'-2k} +\left(\sin\frac{β}{2}\right)^{m'-m+2k}. ``` -In Sec. 7.2.6, we find that if the operator ``\hat{R}(\alpha, \beta, -\gamma)`` rotates a vector pointing in the ``(\theta, \phi)`` to a -vector pointing in the ``(\theta', \phi')`` direction, then the +In Sec. 7.2.6, we find that if the operator ``\hat{R}(α, β, +γ)`` rotates a vector pointing in the ``(θ, ϕ)`` to a +vector pointing in the ``(θ', ϕ')`` direction, then the spherical harmonics transform as [Eq. (7.70)] ```math -Y_{ℓ, m}^\ast (\theta', \phi') +Y_{ℓ, m}^\ast (θ', ϕ') = -\sum_{m'} D^{(ℓ)}_{m, m'}(\alpha, \beta, \gamma) Y_{ℓ, m'}^\ast (\theta, \phi). +\sum_{m'} D^{(ℓ)}_{m, m'}(α, β, γ) Y_{ℓ, m'}^\ast (θ, ϕ). ``` diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 7511db06..6ab9ca72 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -44,24 +44,24 @@ correspondence with the vectors, we will frequently use a vector to label a point in space. We will be working on the sphere, so it will be very convenient to use -spherical coordinates ``(r, \theta, \phi)``. We choose the standard +spherical coordinates ``(r, θ, ϕ)``. We choose the standard "physics" conventions for these, in which we relate to the Cartesian coordinates by ```math \begin{aligned} r &= \sqrt{x^2 + y^2 + z^2} &&\in [0, \infty), \\ -\theta &= \arccos\left(\frac{z}{r}\right) &&\in [0, \pi], \\ -\phi &= \arctan\left(\frac{y}{x}\right) &&\in [0, 2\pi), +θ &= \arccos\left(\frac{z}{r}\right) &&\in [0, \pi], \\ +ϕ &= \arctan\left(\frac{y}{x}\right) &&\in [0, 2\pi), \end{aligned} ``` -where we assume the ``\arctan`` in the expression for ``\phi`` is +where we assume the ``\arctan`` in the expression for ``ϕ`` is really the two-argument form that gives the correct quadrant. The inverse transformation is given by ```math \begin{aligned} -x &= r \sin\theta \cos\phi, \\ -y &= r \sin\theta \sin\phi, \\ -z &= r \cos\theta. +x &= r \sin θ \cos ϕ, \\ +y &= r \sin θ \sin ϕ, \\ +z &= r \cos θ. \end{aligned} ``` We can use this to find the components of the metric in spherical @@ -72,15 +72,15 @@ g_{i'j'} = \left( \begin{array}{ccc} 1 & 0 & 0 \\ 0 & r^2 & 0 \\ - 0 & 0 & r^2 \sin^2\theta + 0 & 0 & r^2 \sin^2θ \end{array} \right)_{i'j'}. ``` The unit coordinate vectors in spherical coordinates are then ```math \begin{aligned} -𝐫 &= \sin\theta \cos\phi 𝐱 + \sin\theta \sin\phi 𝐲 + \cos\theta 𝐳, \\ -\boldsymbol{\theta} &= \cos\theta \cos\phi 𝐱 + \cos\theta \sin\phi 𝐲 - \sin\theta 𝐳, \\ -\boldsymbol{\phi} &= -\sin\phi 𝐱 + \cos\phi 𝐲, +𝐫 &= \sin θ \cos ϕ 𝐱 + \sin θ \sin ϕ 𝐲 + \cos θ 𝐳, \\ +\boldsymbol{θ} &= \cos θ \cos ϕ 𝐱 + \cos θ \sin ϕ 𝐲 - \sin θ 𝐳, \\ +\boldsymbol{ϕ} &= -\sin ϕ 𝐱 + \cos ϕ 𝐲, \end{aligned} ``` where, again, we omit the hats on the unit vectors to keep the @@ -88,19 +88,19 @@ notation simple. Conversely, we can express the Cartesian basis vectors in terms of the spherical basis vectors as ```math \begin{aligned} -𝐱 &= \sin\theta \cos\phi 𝐫 + \cos\theta \cos\phi \boldsymbol{\theta} - \sin\phi \boldsymbol{\phi}, +𝐱 &= \sin θ \cos ϕ 𝐫 + \cos θ \cos ϕ \boldsymbol{θ} - \sin ϕ \boldsymbol{ϕ}, \\ -𝐲 &= \sin\theta \sin\phi 𝐫 + \cos\theta \sin\phi \boldsymbol{\theta} + \cos\phi \boldsymbol{\phi}, +𝐲 &= \sin θ \sin ϕ 𝐫 + \cos θ \sin ϕ \boldsymbol{θ} + \cos ϕ \boldsymbol{ϕ}, \\ -𝐳 &= \cos\theta 𝐫 - \sin\theta \boldsymbol{\theta}. +𝐳 &= \cos θ 𝐫 - \sin θ \boldsymbol{θ}. \end{aligned} ``` One seemingly obvious — but extremely important — fact is that the unit basis frame ``(𝐱, 𝐲, 𝐳)`` can be rotated onto -``(\boldsymbol{\theta}, \boldsymbol{\phi}, 𝐫)`` by first -rotating through the "polar" angle ``\theta`` about the ``𝐲`` -axis, and then through the "azimuthal" angle ``\phi`` about the +``(\boldsymbol{θ}, \boldsymbol{ϕ}, 𝐫)`` by first +rotating through the "polar" angle ``θ`` about the ``𝐲`` +axis, and then through the "azimuthal" angle ``ϕ`` about the ``𝐳`` axis. This becomes important when we consider spin-weighted functions. @@ -111,12 +111,12 @@ Integration in Cartesian coordinates is, of course, trivial as In spherical coordinates, the integrand involves the square-root of the determinant of the metric, so we have ```math -\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin\theta\, dr\, d\theta\, d\phi. +\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin θ\, dr\, dθ\, dϕ. ``` Restricting to the unit sphere, and normalizing so that the integral of 1 over the sphere is 1, we can simplify this to ```math -\int_{𝕊²} f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin\theta\, d\theta\, d\phi. +\int_{𝕊²} f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin θ\, dθ\, dϕ. ``` @@ -278,20 +278,20 @@ the relation: ```math \begin{aligned} R &= \sqrt{W^2 + X^2 + Y^2 + Z^2} &&\in [0, \infty), \\ -\alpha &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ -\beta &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ -\gamma &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), +α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), \end{aligned} ``` where we again assume the ``\arctan`` in the expressions for -``\alpha`` and ``\gamma`` is really the two-argument form that gives -the correct quadrant. Note that here, ``\beta`` ranges up to ``2\pi`` +``α`` and ``γ`` is really the two-argument form that gives +the correct quadrant. Note that here, ``β`` ranges up to ``2\pi`` rather than just ``\pi``, as in the standard Euler angles. This is because we are describing the space of quaternions, rather than just the space of rotations. If we restrict to ``R=1``, we have exactly the group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, which is a double cover of the rotation group ``\mathrm{SO}(3)``. -This extended range for ``\beta`` is necessary to cover the entire +This extended range for ``β`` is necessary to cover the entire space of quaternions; if we further restrict to ``[0, \pi)``, we would only cover the space of rotations. This and the inclusion of ``R`` identify precisely how this coordinate system extends the standard @@ -313,9 +313,9 @@ g_{i'j'} = \sum_{i,j} \frac{\partial X^i}{\partial X^{i'}} \frac{\partial X^j}{\partial X^{j'}} g_{ij} = \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ - 0 & \frac{R^2}{4} & 0 & \frac{R^2 \cos\beta}{4} \\ + 0 & \frac{R^2}{4} & 0 & \frac{R^2 \cos β}{4} \\ 0 & 0 & \frac{R^2}{4} & 0 \\ - 0 & \frac{R^2 \cos\beta}{4} & 0 & \frac{R^2}{4} + 0 & \frac{R^2 \cos β}{4} & 0 & \frac{R^2}{4} \end{array} \right)_{i'j'}. ``` The unit basis vectors in extended Euler coordinates in terms of the @@ -323,36 +323,36 @@ unit basis vectors in quaternion coordinates are ```math \begin{aligned} 𝐑 &= \frac{1}{R} \left( - \cos \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝟏 - - \sin \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐢 - + \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 - + \cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝐤 + \cos \frac{β}{2} \cos \frac{α+γ}{2} 𝟏 + - \sin \frac{β}{2} \sin \frac{α-γ}{2} 𝐢 + + \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐣 + + \cos \frac{β}{2} \sin \frac{α+γ}{2} 𝐤 \right), \\ -\boldsymbol{\alpha} &= \frac{R}{2} \left( - -\cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝟏 - - \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐢 - - \sin \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐣 - + \cos \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝐤 +\boldsymbol{α} &= \frac{R}{2} \left( + -\cos \frac{β}{2} \sin \frac{α+γ}{2} 𝟏 + - \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐢 + - \sin \frac{β}{2} \sin \frac{α-γ}{2} 𝐣 + + \cos \frac{β}{2} \cos \frac{α+γ}{2} 𝐤 \right), \\ -\boldsymbol{\beta} &= \frac{R}{2} \left( - -\sin \frac{\beta}{2} \cos \frac{\alpha+\gamma}{2} 𝟏 - - \cos \frac{\beta}{2} \sin \frac{\alpha-\gamma}{2} 𝐢 - + \cos \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 - - \sin \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝐤 +\boldsymbol{β} &= \frac{R}{2} \left( + -\sin \frac{β}{2} \cos \frac{α+γ}{2} 𝟏 + - \cos \frac{β}{2} \sin \frac{α-γ}{2} 𝐢 + + \cos \frac{β}{2} \cos \frac{α-γ}{2} 𝐣 + - \sin \frac{β}{2} \sin \frac{α+γ}{2} 𝐤 \right), \\ -\boldsymbol{\gamma} &= \frac{R}{2} \left( - -\cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝟏 - + \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐢 - - \sin \frac{\beta}{2} \cos \frac{\alpha-\gamma}{2} 𝐣 - - \cos \frac{\beta}{2} \sin \frac{\alpha+\gamma}{2} 𝐤 +\boldsymbol{γ} &= \frac{R}{2} \left( + -\cos \frac{β}{2} \sin \frac{α+γ}{2} 𝟏 + + \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐢 + - \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐣 + - \cos \frac{β}{2} \sin \frac{α+γ}{2} 𝐤 \right). \end{aligned} ``` Again, integration involves a square-root of the determinant of the -metric, which reduces to ``R^3 |\sin\beta| / 8``. Note that — unlike +metric, which reduces to ``R^3 |\sin β| / 8``. Note that — unlike with standard spherical coordinates — the absolute value is necessary -because ``\beta`` ranges over the entire interval ``[0, 2\pi]``. The +because ``β`` ranges over the entire interval ``[0, 2\pi]``. The integral over the entire space of quaternions is then ```math \int_{\mathbb{R}^4} f\, d^4𝐐 @@ -429,28 +429,28 @@ opposite) quaternions that represent it. Now that we understand how rotations work, we can provide geometric intuition for the expressions given above for Euler angles. The Euler angles *in our convention* represent an initial rotation through -``\gamma`` about the ``𝐳`` axis, followed by a rotation through -``\beta`` about the ``𝐲`` axis, and finally a rotation through -``\alpha`` about the ``𝐳`` axis. Note that the axes are fixed, and +``γ`` about the ``𝐳`` axis, followed by a rotation through +``β`` about the ``𝐲`` axis, and finally a rotation through +``α`` about the ``𝐳`` axis. Note that the axes are fixed, and not subject to any preceding rotations. More precisely, we can write the unit quaternion as ```math -𝐑 = \exp\left(\frac{\alpha}{2} 𝐤\right) - \exp\left(\frac{\beta}{2} 𝐣\right) - \exp\left(\frac{\gamma}{2} 𝐤\right). +𝐑 = \exp\left(\frac{α}{2} 𝐤\right) + \exp\left(\frac{β}{2} 𝐣\right) + \exp\left(\frac{γ}{2} 𝐤\right). ``` One of the more important interpretations of a rotor is considering what it does to the basis triad ``(𝐱, 𝐲, 𝐳)``. In particular, the vector ``𝐳`` is rotated onto the point given by spherical coordinates -``(\theta, \phi) = (\beta, \alpha)``, while ``𝐱`` and ``𝐲`` are +``(θ, ϕ) = (β, α)``, while ``𝐱`` and ``𝐲`` are rotated into the plane spanned by the unit basis vectors -``\boldsymbol{\theta}`` and ``\boldsymbol{\phi}`` corresponding to -that point. If ``\gamma = 0`` the rotation is precise, meaning that -``𝐱`` is rotated onto ``\boldsymbol{\theta}`` and ``𝐲`` onto -``\boldsymbol{\phi}``; if ``\gamma ≠ 0`` then they are rotated within -that plane by the angle ``\gamma`` about the ``𝐫`` axis. -Thus, we identify the spherical coordinates ``(\theta, \phi)`` with -the Euler angles ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. +``\boldsymbol{θ}`` and ``\boldsymbol{ϕ}`` corresponding to +that point. If ``γ = 0`` the rotation is precise, meaning that +``𝐱`` is rotated onto ``\boldsymbol{θ}`` and ``𝐲`` onto +``\boldsymbol{ϕ}``; if ``γ ≠ 0`` then they are rotated within +that plane by the angle ``γ`` about the ``𝐫`` axis. +Thus, we identify the spherical coordinates ``(θ, ϕ)`` with +the Euler angles ``(α, β, γ) = (ϕ, θ, 0)``. ## Rotation and angular-momentum operators @@ -482,7 +482,7 @@ spin-weighted functions are generally written as functions of functions on ``𝕊²`` itself; some notion of a reference tangent direction is needed at each point. The difference is that spherical *coordinates* supply a natural choice for the reference tangent -direction: the unit vector in the ``\boldsymbol{\theta}`` direction. +direction: the unit vector in the ``\boldsymbol{θ}`` direction. This supplies just enough information to define the spin-weighted functions — though this ends up not being a useful form when more general transformations or deeper understanding are needed. @@ -578,15 +578,15 @@ To validate the signs here, it may be helpful to work through a simple example involving the sphere ``𝕊²``. We define a function on spherical coordinates as ```math -f(\theta, \phi) = \sin\theta \sin\phi. +f(θ, ϕ) = \sin θ \sin ϕ. ``` Recall that we can map the spherical coordinates into the Euler angles, and the Euler angles into the quaternion ```math -(\theta, \phi) \mapsto (\phi, \theta, 0) \mapsto 𝐐 +(θ, ϕ) \mapsto (ϕ, θ, 0) \mapsto 𝐐 = -\exp\left(\frac{\phi}{2} 𝐤\right) -\exp\left(\frac{\theta}{2} 𝐣\right). +\exp\left(\frac{ϕ}{2} 𝐤\right) +\exp\left(\frac{θ}{2} 𝐣\right). ``` It is straightforward to see that we can write ``f`` as a function of ``𝐐`` as @@ -597,23 +597,23 @@ where the angle brackets and subscript indicate that we are taking the ``𝐣`` component. That is, ``f`` is the ``y`` component of the vector ``𝐳`` rotated by ``𝐐``. -Now, we imagine rotating the field by an angle ``\alpha`` in the +Now, we imagine rotating the field by an angle ``α`` in the positive sense about the ``z`` axis. Visualizing the situation, we can see that the rotated field should be represented by ```math -f'(\theta, \phi) = \sin\theta \sin(\phi - \alpha). +f'(θ, ϕ) = \sin θ \sin(ϕ - α). ``` -For example, the rotated field evaluated at the point ``(\theta, \phi) +For example, the rotated field evaluated at the point ``(θ, ϕ) = (\pi/2, 0)`` along the positive ``x`` axis should correspond to the -original field evaluated at the point ``(\theta, \phi) = (\pi/2, --\alpha)``. This rotation is generated by ``𝔤 = \alpha +original field evaluated at the point ``(θ, ϕ) = (\pi/2, +-α)``. This rotation is generated by ``𝔤 = α 𝐤 / 2``, which allows us to immediately calculate ```math \begin{aligned} -f(e^𝔤 𝐐) &= \sin\theta \sin(\phi + \alpha) &&& -f(𝐐 e^𝔤) &= \sin\theta \sin\phi \\ -f(e^{-𝔤} 𝐐) &= \sin\theta \sin(\phi - \alpha) &&& -f(𝐐 e^{-𝔤}) &= \sin\theta \sin\phi. +f(e^𝔤 𝐐) &= \sin θ \sin(ϕ + α) &&& +f(𝐐 e^𝔤) &= \sin θ \sin ϕ \\ +f(e^{-𝔤} 𝐐) &= \sin θ \sin(ϕ - α) &&& +f(𝐐 e^{-𝔤}) &= \sin θ \sin ϕ. \end{aligned} ``` Thus, we see that left-multiplication by ``e^{-𝔤}`` @@ -640,10 +640,10 @@ f(𝐐 e^{-𝔤}) ``` where ``𝔤' = 𝐐 𝔤 𝐐^{-1}``. In this example, ``𝔤'`` generates a rotation by an angle -``\alpha`` about the point in question, which leaves that point fixed, +``α`` about the point in question, which leaves that point fixed, and since this is a scalar function it has no effect on the value. Of course, we will see below that changing by a phase proportional to -``\alpha`` is the defining feature of a *spin-weighted* function. +``α`` is the defining feature of a *spin-weighted* function. ### Differential rotations @@ -652,8 +652,8 @@ respect to infinitesimal rotations we apply to the functions themselves: ```math \begin{aligned} -L_{𝔤} f(𝐐) &= \lambda \left. \frac{\partial} {\partial \theta} f \left( e^{-\theta 𝔤 / 2} 𝐐 \right) \right|_{\theta=0}, \\ -R_{𝔤} f(𝐐) &= \rho \left. \frac{\partial} {\partial \theta} f \left( 𝐐 e^{-\theta 𝔤 / 2} \right) \right|_{\theta=0}. +L_{𝔤} f(𝐐) &= \lambda \left. \frac{\partial} {\partial θ} f \left( e^{-θ 𝔤 / 2} 𝐐 \right) \right|_{θ=0}, \\ +R_{𝔤} f(𝐐) &= \rho \left. \frac{\partial} {\partial θ} f \left( 𝐐 e^{-θ 𝔤 / 2} \right) \right|_{θ=0}. \end{aligned} ``` Here, we have introduced the constants ``\lambda`` and ``\rho`` @@ -668,11 +668,11 @@ the order of operations, which may look slightly unnatural: ```math \begin{aligned} L_𝔤 L_𝔥 f(𝐐) - % &= \left. \lambda \frac{\partial} {\partial \gamma} f'\left(e^{-\gamma 𝔤 / 2} 𝐐 \right) \right|_{\gamma=0}, \\ - &= \left. \lambda^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left(e^{-\eta 𝔥 / 2} e^{-\gamma 𝔤 / 2} 𝐐 \right) \right|_{\gamma=\eta=0}, \\ + % &= \left. \lambda \frac{\partial} {\partial γ} f'\left(e^{-γ 𝔤 / 2} 𝐐 \right) \right|_{γ=0}, \\ + &= \left. \lambda^2 \frac{\partial} {\partial γ} \frac{\partial} {\partial \eta} f\left(e^{-\eta 𝔥 / 2} e^{-γ 𝔤 / 2} 𝐐 \right) \right|_{γ=\eta=0}, \\ R_𝔤 R_𝔥 f(𝐐) - % &= \rho \left. \frac{\partial} {\partial \gamma} f' \left( 𝐐 e^{-\gamma 𝔤 / 2} \right) \right|_{\gamma=0} \\ - &= \left. \rho^2 \frac{\partial} {\partial \gamma} \frac{\partial} {\partial \eta} f\left( 𝐐 e^{-\gamma 𝔤 / 2} e^{-\eta 𝔥 / 2} \right) \right|_{\gamma=\eta=0}. + % &= \rho \left. \frac{\partial} {\partial γ} f' \left( 𝐐 e^{-γ 𝔤 / 2} \right) \right|_{γ=0} \\ + &= \left. \rho^2 \frac{\partial} {\partial γ} \frac{\partial} {\partial \eta} f\left( 𝐐 e^{-γ 𝔤 / 2} e^{-\eta 𝔥 / 2} \right) \right|_{γ=\eta=0}. \end{aligned} ``` We can prove the first of these, for example, by defining @@ -763,68 +763,68 @@ Using these relations, we can actually solve for the constants - Express angular momentum operators in terms of Euler angles - We just rewrite the ``R`` in the Lie definitions in terms of - Euler angles, multiply by ``\exp(\theta/2)``, rederive the new + Euler angles, multiply by ``\exp(θ/2)``, rederive the new Euler angles from that result, and use the chain rule - Show for both the three- and two-spheres - Show how they act on functions on the three-sphere -The idea here is to express, e.g., $e^{\theta 𝐞_i / -2}𝐑_{\alpha, \beta, \gamma}$ in quaternion components, then -solve for the new Euler angles $𝐑_{\alpha', \beta', \gamma'}$ +The idea here is to express, e.g., $e^{θ 𝐞_i / +2}𝐑_{α, β, γ}$ in quaternion components, then +solve for the new Euler angles $𝐑_{α', β', γ'}$ in terms of the quaternion components, where these new angles all -depend on $\theta$. We then use the chain rule to express -$\partial_\theta$ in terms of $\partial_{\alpha'}$, etc., which become -$\partial_\alpha$, etc., when $\theta=0$. +depend on $θ$. We then use the chain rule to express +$\partial_θ$ in terms of $\partial_{α'}$, etc., which become +$\partial_α$, etc., when $θ=0$. ```math \begin{aligned} - L_i f(𝐑_{\alpha, \beta, \gamma}) + L_i f(𝐑_{α, β, γ}) &= - \left. -𝐳 \frac{\partial} {\partial \theta} f \left( e^{\theta 𝐞_i / 2} 𝐑_{\alpha, \beta, \gamma} \right) \right|_{\theta=0} \\ + \left. -𝐳 \frac{\partial} {\partial θ} f \left( e^{θ 𝐞_i / 2} 𝐑_{α, β, γ} \right) \right|_{θ=0} \\ &= - \left. -𝐳 \frac{\partial} {\partial \theta} f \left( 𝐑_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ + \left. -𝐳 \frac{\partial} {\partial θ} f \left( 𝐑_{α', β', γ'} \right) \right|_{θ=0} \\ &= - \left. -𝐳 \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha'} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta'} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma'} \right] f \left( 𝐑_{\alpha', \beta', \gamma'} \right) \right|_{\theta=0} \\ + \left. -𝐳 \left[ \frac{\partial α'} {\partial θ}\frac{\partial} {\partial α'} + \frac{\partial β'} {\partial θ}\frac{\partial} {\partial β'} + \frac{\partial γ'} {\partial θ}\frac{\partial} {\partial γ'} \right] f \left( 𝐑_{α', β', γ'} \right) \right|_{θ=0} \\ &= - -𝐳 \left[ \frac{\partial \alpha'} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta'} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma'} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( 𝐑_{\alpha, \beta, \gamma} \right) \\ - K_i f(𝐑_{\alpha, \beta, \gamma}) + -𝐳 \left[ \frac{\partial α'} {\partial θ}\frac{\partial} {\partial α} + \frac{\partial β'} {\partial θ}\frac{\partial} {\partial β} + \frac{\partial γ'} {\partial θ}\frac{\partial} {\partial γ} \right]_{θ=0} f \left( 𝐑_{α, β, γ} \right) \\ + K_i f(𝐑_{α, β, γ}) &= - -𝐳 \left[ \frac{\partial \alpha''} {\partial \theta}\frac{\partial} {\partial \alpha} + \frac{\partial \beta''} {\partial \theta}\frac{\partial} {\partial \beta} + \frac{\partial \gamma''} {\partial \theta}\frac{\partial} {\partial \gamma} \right]_{\theta=0} f \left( 𝐑_{\alpha, \beta, \gamma} \right), + -𝐳 \left[ \frac{\partial α''} {\partial θ}\frac{\partial} {\partial α} + \frac{\partial β''} {\partial θ}\frac{\partial} {\partial β} + \frac{\partial γ''} {\partial θ}\frac{\partial} {\partial γ} \right]_{θ=0} f \left( 𝐑_{α, β, γ} \right), \end{aligned} ``` ```math \begin{aligned} -𝐑_{\alpha, \beta, \gamma} +𝐑_{α, β, γ} &= R\, \cos\frac{β}{2} \cos\frac{α+γ}{2} -R\, \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 + R\, \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 + R\, \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤. \\ -e^{\theta 𝐮 / 2} 𝐑_{\alpha, \beta, \gamma} -&= \left(\cos\frac{\theta}{2} + 𝐮 \sin\frac{\theta}{2}\right) 𝐑_{\alpha, \beta, \gamma} +e^{θ 𝐮 / 2} 𝐑_{α, β, γ} +&= \left(\cos\frac{θ}{2} + 𝐮 \sin\frac{θ}{2}\right) 𝐑_{α, β, γ} \\ &= - R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \cos\frac{α+γ}{2} - -R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 - + R\, \cos\frac{\theta}{2} \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 - + R\, \cos\frac{\theta}{2} \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤 + R\, \cos\frac{θ}{2} \cos\frac{β}{2} \cos\frac{α+γ}{2} + -R\, \cos\frac{θ}{2} \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 + + R\, \cos\frac{θ}{2} \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 + + R\, \cos\frac{θ}{2} \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤 \\ &\quad + - R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \cos\frac{α+γ}{2} 𝐮 - -R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐮𝐢 - + R\, \sin\frac{\theta}{2}\sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐮𝐣 - + R\, \sin\frac{\theta}{2}\cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐮𝐤 + R\, \sin\frac{θ}{2}\cos\frac{β}{2} \cos\frac{α+γ}{2} 𝐮 + -R\, \sin\frac{θ}{2}\sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐮𝐢 + + R\, \sin\frac{θ}{2}\sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐮𝐣 + + R\, \sin\frac{θ}{2}\cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐮𝐤 \end{aligned} ``` ```math \begin{aligned} -\alpha &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ -\beta &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ -\gamma &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), +α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), \end{aligned} ``` @@ -934,31 +934,31 @@ derivatives, as a result. > ``R``, we associate an operator ``\mathscr{D}(R)`` in the > appropriate ket space such that > ```math -> |\alpha\rangle_R = \mathscr{D}(R) |\alpha\rangle, +> |α\rangle_R = \mathscr{D}(R) |α\rangle, > ``` -> ``|\alpha\rangle_R`` and ``|\alpha\rangle`` stand for the kets of +> ``|α\rangle_R`` and ``|α\rangle`` stand for the kets of > the rotated and original system, respectively. If the field is represented as a function ``f(𝐑)``, then rotating the -field by ``e^{\epsilon 𝐮/2}`` is equivalent to rotating the argument -of the function by ``e^{-\epsilon 𝐮/2}``: +field by ``e^{ϵ 𝐮/2}`` is equivalent to rotating the argument +of the function by ``e^{-ϵ 𝐮/2}``: ```math \begin{aligned} f\left(𝐑\right) &\to -f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ &\approx -f\left(𝐑\right) + \epsilon \left. \frac{d}{d\epsilon} \right|_{\epsilon=0} -f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +f\left(𝐑\right) + ϵ \left. \frac{d}{dϵ} \right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ &= -f\left(𝐑\right) - i \epsilon L_𝐮 f\left(𝐑\right). +f\left(𝐑\right) - i ϵ L_𝐮 f\left(𝐑\right). \end{aligned} ``` This final expression is precisely equivalent to Sakurai's Eq. (3.1.15): ```math -\mathscr{D}\left(\hat{𝐧}, d\phi \right) +\mathscr{D}\left(\hat{𝐧}, dϕ \right) = -1 - i \left( 𝐉 \cdot \hat{𝐧} \right) d\phi. +1 - i \left( 𝐉 \cdot \hat{𝐧} \right) dϕ. ``` Now, we can write the eigenkets of ``L^2`` and ``L_z`` as ``|ℓ, @@ -979,29 +979,29 @@ and we can readily find the essential behavior with respect to the first and last Euler angles (Eq. 3.5.50): ```math \begin{aligned} -𝔇^{(ℓ)}_{m',m}(\alpha, \beta, \gamma) +𝔇^{(ℓ)}_{m',m}(α, β, γ) &= \langle ℓ, m' | - \exp[-iL_z \alpha]\exp[-iL_y \beta]\exp[-iL_z \gamma] + \exp[-iL_z α]\exp[-iL_y β]\exp[-iL_z γ] | ℓ, m \rangle \\ &= -\exp[-i(m' \alpha+m\gamma)] -\langle ℓ, m' | \exp[-iL_y \beta] | ℓ, m \rangle. +\exp[-i(m' α+mγ)] +\langle ℓ, m' | \exp[-iL_y β] | ℓ, m \rangle. \end{aligned} ``` To belabor this point, recall that in general ```math -\left(\left\langle \psi | A\, B\, C | \chi \right\rangle\right)^\ast +\left(\left\langle ψ | A\, B\, C | \chi \right\rangle\right)^\ast = -\left\langle \chi | C^\dag\, B^\dag\, A^\dag | \psi \right\rangle, +\left\langle \chi | C^\dag\, B^\dag\, A^\dag | ψ \right\rangle, ``` and ```math -\left( e^{-i \epsilon L_u} \right)^\dag +\left( e^{-i ϵ L_u} \right)^\dag = -e^{i \epsilon L_u^\dag} +e^{i ϵ L_u^\dag} = -e^{i \epsilon L_u}. +e^{i ϵ L_u}. ``` Together with the eigenvalue property for the ``L_z`` operator acting on a ket, this allows us to derive the above result by factoring out @@ -1010,9 +1010,9 @@ the first and last operators. Now we are left with the middle operator, which we use to define ```math \begin{aligned} -d^{(ℓ)}_{m',m}(\beta) +d^{(ℓ)}_{m',m}(β) &= -\langle ℓ, m' | \exp[-iL_y \beta] | ℓ, m \rangle. +\langle ℓ, m' | \exp[-iL_y β] | ℓ, m \rangle. \end{aligned} ``` Using @@ -1045,11 +1045,11 @@ I wonder if there's a nicer approach using the symmetry transformation Edmonds notes in Sec. 4.5 (and credits to Wigner) — or the presumably equivalent one McEwen and Wiaux use (and credit to Risbo): ```math -\exp\left[ \beta 𝐣 / 2 \right] +\exp\left[ β 𝐣 / 2 \right] = \exp\left[ \pi 𝐤 / 4 \right] \exp\left[ \pi 𝐣 / 4 \right] -\exp\left[ \beta 𝐤 / 2 \right] +\exp\left[ β 𝐤 / 2 \right] \exp\left[ -\pi 𝐣 / 4 \right] \exp\left[ -\pi 𝐤 / 4 \right] ``` @@ -1116,9 +1116,9 @@ Theorem 2.16 of [Hanson-Yakovlev](@cite HansonYakovlev_2002) says that an orthonormal basis of a product of ``L^2`` spaces is given by the product of the orthonormal bases of the individual spaces. Furthermore, on page 354, they point out that ``\{(1/\sqrt{2\pi}) -e^{im\phi}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the -set ``\{1/c_{n,m} P_n^m(\cos\theta)`` is an orthonormal basis of -``L^2(0, \pi)`` in the ``\theta`` coordinate. Therefore, the product +e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the +set ``\{1/c_{n,m} P_n^m(\cos θ)`` is an orthonormal basis of +``L^2(0, \pi)`` in the ``θ`` coordinate. Therefore, the product of these two sets is an orthonormal basis of the product space ``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate space for ``𝕊²``. I would probably modify this to point out that @@ -1130,8 +1130,8 @@ which happens to give us the Wigner D-matrices. [Gumerov and Duraiswami (2001)](@cite Gumerov_2001) derive their recursion relations by differentiating solutions of the Helmholtz -equation ``\nabla^2 \psi + k^2 \psi = 0`` as ``\tfrac{1}{k} \nabla -\psi``. More precisely, they differentiate both sides of the equation +equation ``\nabla^2 ψ + k^2 ψ = 0`` as ``\tfrac{1}{k} \nabla +ψ``. More precisely, they differentiate both sides of the equation relating one solution to its rotated form — which naturally involves Wigner's ``𝔇`` matrix. Using orthogonal basis functions for the solution, this allows them to equate terms on the two sides @@ -1141,7 +1141,7 @@ some coefficients depending on the indices of the basis functions (and hence of ``𝔇``) on both sides of the equation. Since ``\nabla`` is a 3-vector operator, this gives them three relations. -This, of course, is happening in 3-D space, since ``\psi`` is a +This, of course, is happening in 3-D space, since ``ψ`` is a function of location in the Helmholtz equation. It seems likely to me, however, that we could use the 4-D (quaternionic) version of the functions. Note that G&D use ``\partial_z`` and ``\partial_x \pm i diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index e5713249..43d7ad28 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -86,9 +86,9 @@ Theorem 2.16 of [Hanson-Yakovlev](@cite HansonYakovlev_2002) says that an orthonormal basis of a product of ``L^2`` spaces is given by the product of the orthonormal bases of the individual spaces. Furthermore, on page 354, they point out that ``\{(1/\sqrt{2\pi}) -e^{im\phi}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the -set ``\{1/c_{n,m} P_n^m(\cos\theta)`` is an orthonormal basis of -``L^2(0, \pi)`` in the ``\theta`` coordinate. Therefore, the product +e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the +set ``\{1/c_{n,m} P_n^m(\cos θ)`` is an orthonormal basis of +``L^2(0, \pi)`` in the ``θ`` coordinate. Therefore, the product of these two sets is an orthonormal basis of the product space ``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate space for ``𝕊²``. I would probably modify this to point out that @@ -97,10 +97,10 @@ out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, which happens to give us the Wigner D-matrices. We first define the rotor that takes ``(\hat{x}, \hat{y}, \hat{z})`` -onto ``(\hat{\theta}, \hat{\phi}, \hat{r})``. Then, we can invert +onto ``(\hat{θ}, \hat{ϕ}, \hat{r})``. Then, we can invert that, so that given a rotor that specifies such a rotation exactly, we -can get the spherical coordinates — or specifically ``\sin\theta``, -``\cos\theta``, and ``\exp(i\phi)``. +can get the spherical coordinates — or specifically ``\sin θ``, +``\cos θ``, and ``\exp(iϕ)``. Then, with the universally agreed-upon ``Y`` as given in terms of spherical coordinates, we can rewrite it directly to work with @@ -112,12 +112,12 @@ formula for ``Y``. Then, we can simply follow Wigner around Eq. (15.21) to derived a transformation law in the form ```math -{}_sY_{ℓ,m'}(R_{\theta', \phi'}) = \sum_m M_{m',m}(R) -{}_sY_{ℓ,m}(R_{\theta, \phi}), +{}_sY_{ℓ,m'}(R_{θ', ϕ'}) = \sum_m M_{m',m}(R) +{}_sY_{ℓ,m}(R_{θ, ϕ}), ``` for some matrix ``M``. Note that I have written this as if the ``{}_sY`` functions are column vectors. The reason this happens is -because I want to write ``R_{\theta', \phi'} = R\, R_{\theta, \phi}``, +because I want to write ``R_{θ', ϕ'} = R\, R_{θ, ϕ}``, rather than swapping the order of the rotations on the right-hand side. @@ -130,30 +130,30 @@ conjugate transpose, which is why we see the relative conjugate. * Since ``Y`` is universal, let's start with that as non-negotiable, and see if we can derive the relationship to ``𝔇``. -* ``R_{\theta, \phi}`` is a unit quaternion that rotates the point +* ``R_{θ, ϕ}`` is a unit quaternion that rotates the point described by Cartesian coordinates (0,0,1) onto the point described - by spherical coordinates ``(\theta, \phi)``. + by spherical coordinates ``(θ, ϕ)``. * Just textually, it makes the most sense to write ```math - R_{\theta', \phi'} = R\, R_{\theta, \phi} + R_{θ', ϕ'} = R\, R_{θ, ϕ} ``` for some rotation ``R``. Now, we just need to interpret ``R``. * Again, just textually, it makes the most sense to write ```math - Y_{ℓ,m'}(\theta', \phi') = \sum_m 𝔇^{(ℓ)}_{m',m}(R) - Y_{ℓ,m}(\theta, \phi), + Y_{ℓ,m'}(θ', ϕ') = \sum_m 𝔇^{(ℓ)}_{m',m}(R) + Y_{ℓ,m}(θ, ϕ), ``` or, generalizing to spin-weighted spherical harmonics ```math - {}_{s}Y_{ℓ,m'}(R_{\theta', \phi'}) = \sum_m 𝔇^{(ℓ)}_{m',m}(R) - {}_{s}Y_{ℓ,m}(R_{\theta, \phi}). + {}_{s}Y_{ℓ,m'}(R_{θ', ϕ'}) = \sum_m 𝔇^{(ℓ)}_{m',m}(R) + {}_{s}Y_{ℓ,m}(R_{θ, ϕ}). ``` * We also have that ``𝔇`` obeys the representation property, so ```math - 𝔇^{(ℓ)}_{m',m''}(R_{\theta', \phi'}) + 𝔇^{(ℓ)}_{m',m''}(R_{θ', ϕ'}) = \sum_{m} 𝔇^{(ℓ)}_{m',m}(R) - 𝔇^{(ℓ)}_{m,m''}(R_{\theta, \phi}). + 𝔇^{(ℓ)}_{m,m''}(R_{θ, ϕ}). ``` - There is no reason that I can see to introduce a conjugation - The fact that ``m''`` appears on both sides of the equation means @@ -161,9 +161,9 @@ conjugate transpose, which is why we see the relative conjugate. behavior under final rotation to determine the sign. ```math -{}_{s}Y_{ℓ,m}(R_{\theta, \phi}) +{}_{s}Y_{ℓ,m}(R_{θ, ϕ}) \propto -𝔇^{(ℓ)}_{m,\propto s}(R_{\theta, \phi}) +𝔇^{(ℓ)}_{m,\propto s}(R_{θ, ϕ}) ``` @@ -202,7 +202,7 @@ The [Condon-Shortley](@cite CondonShortley_1935) phase convention is a choice of phase factors in the definition of the spherical harmonics that requires the coefficients in ```math -L_{\pm} |ℓ,m\rangle = \alpha^{\pm}_{ℓ,m} |ℓ, m \pm 1\rangle +L_{\pm} |ℓ,m\rangle = α^{\pm}_{ℓ,m} |ℓ, m \pm 1\rangle ``` to be real and positive. The reasoning behind this choice is explained more fully in Section 2 of [Ufford and Shortley @@ -212,10 +212,10 @@ spherical harmonics. The key expression is Eq. (15) of section 4³ (page 52) of [Condon-Shortley](@cite CondonShortley_1935): ```math \Theta(ℓ, m) = (-1)^ℓ \sqrt{\frac{2ℓ+1}{2} \frac{(ℓ+m)!}{(ℓ-m)!}} -\frac{1}{2^ℓ ℓ!} \frac{1}{\sin^m\theta} -\frac{d^{ℓ-m}}{d(\cos\theta)^{ℓ-m}} \sin^{2ℓ}\theta. +\frac{1}{2^ℓ ℓ!} \frac{1}{\sin^mθ} +\frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} \sin^{2ℓ}θ. ``` -When multiplied by Eq. (5) ``\Phi(m) = e^{im\phi} / \sqrt{2\pi}``, +When multiplied by Eq. (5) ``\Phi(m) = e^{imϕ} / \sqrt{2\pi}``, this gives the spherical harmonic function. The right-hand side of the expression above is usually immediately replaced by a simpler expression using Legendre polynomials, but this just shifts sign @@ -229,15 +229,15 @@ computation with SymPy): ```math \begin{aligned} \Theta(0,0) &= \sqrt{\frac{1}{2}} \\ -\Theta(1,0) &= \sqrt{\frac{3}{2}} \cos\theta & -\Theta(1,\pm1) &= \mp \sqrt{\frac{3}{4}} \sin\theta \\ -\Theta(2,0) &= \sqrt{\frac{5}{8}} (2\cos^2\theta - \sin^2\theta) & -\Theta(2,\pm1) &= \mp \sqrt{\frac{15}{4}} \cos\theta \sin\theta & -\Theta(2,\pm2) &= \sqrt{\frac{15}{16}} \sin^2\theta \\ -\Theta(3,0) &= \sqrt{\frac{7}{8}} (2\cos^3\theta - 3\cos\theta\sin^2\theta) & -\Theta(3,\pm1) &= \mp \sqrt{\frac{21}{32}} (4\cos^2\theta\sin\theta - \sin^3\theta) & -\Theta(3,\pm2) &= \sqrt{\frac{105}{16}} \cos\theta \sin^2\theta & -\Theta(3,\pm3) &= \mp \sqrt{\frac{35}{32}} \sin^3\theta +\Theta(1,0) &= \sqrt{\frac{3}{2}} \cos θ & +\Theta(1,\pm1) &= \mp \sqrt{\frac{3}{4}} \sin θ \\ +\Theta(2,0) &= \sqrt{\frac{5}{8}} (2\cos^2θ - \sin^2θ) & +\Theta(2,\pm1) &= \mp \sqrt{\frac{15}{4}} \cos θ \sin θ & +\Theta(2,\pm2) &= \sqrt{\frac{15}{16}} \sin^2θ \\ +\Theta(3,0) &= \sqrt{\frac{7}{8}} (2\cos^3θ - 3\cos θ\sin^2θ) & +\Theta(3,\pm1) &= \mp \sqrt{\frac{21}{32}} (4\cos^2θ\sin θ - \sin^3θ) & +\Theta(3,\pm2) &= \sqrt{\frac{105}{16}} \cos θ \sin^2θ & +\Theta(3,\pm3) &= \mp \sqrt{\frac{35}{32}} \sin^3θ \end{aligned} ``` These are tested, along with the results from automatic @@ -249,7 +249,7 @@ Condon-Shortley phase convention.*** ## Angular-momentum operators * First, a couple points about ``-i\hbar``: - - The finite transformations look like ``\exp[-i \theta L_j]``, but + - The finite transformations look like ``\exp[-i θ L_j]``, but the factor of ``i`` introduced here just cancels the one in the ``L_j``, and the sign is just chosen to make the result consistent with our notion of active or passive transformations. @@ -258,7 +258,7 @@ Condon-Shortley phase convention.*** - The factor ``i`` comes from plain functional analysis: We need a self-adjoint operator, and ``\partial_x`` by itself is anti-self-adjoint (as can be verified by evaluating on ``\langle - x' | x \rangle = \delta(x-x')``, which switches sign based on + x' | x \rangle = δ(x-x')``, which switches sign based on which is being differentiated). We want self-adjoint operators so that we get purely real eigenvalues. [Van Neerven](@cite vanNeerven_2022) cites this in a more rigorous context in his @@ -289,9 +289,9 @@ L_z &= -i\hbar \left( x \frac{\partial}{\partial y} - y \frac{\partial}{\partial We can transform these to use spherical coordinates and obtain ```math \begin{aligned} -L_x &= -i\hbar \left( \sin\phi \frac{\partial}{\partial\theta} + \cot\theta \cos\phi \frac{\partial}{\partial\phi} \right), \\ -L_y &= -i\hbar \left( \cos\phi \frac{\partial}{\partial\theta} - \cot\theta \sin\phi \frac{\partial}{\partial\phi} \right), \\ -L_z &= -i\hbar \frac{\partial}{\partial\phi}. +L_x &= -i\hbar \left( \sin ϕ \frac{\partial}{\partial θ} + \cot θ \cos ϕ \frac{\partial}{\partial ϕ} \right), \\ +L_y &= -i\hbar \left( \cos ϕ \frac{\partial}{\partial θ} - \cot θ \sin ϕ \frac{\partial}{\partial ϕ} \right), \\ +L_z &= -i\hbar \frac{\partial}{\partial ϕ}. \end{aligned} ``` The conventions we choose *must* be chosen to agree with these — @@ -303,10 +303,10 @@ coefficients. I defined these in Eqs. (42) and (43) of [Boyle (2016)](@cite Boyle_2016) as ```math \begin{aligned} -L_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial \theta} -f\left(e^{\theta 𝐞_j / 2} 𝐑 \right) \right|_{\theta=0}, \\ -K_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial \theta} -f\left(𝐑 e^{\theta 𝐞_j / 2}\right) \right|_{\theta=0}, +L_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial θ} +f\left(e^{θ 𝐞_j / 2} 𝐑 \right) \right|_{θ=0}, \\ +K_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial θ} +f\left(𝐑 e^{θ 𝐞_j / 2}\right) \right|_{θ=0}, \end{aligned} ``` where ``𝐞_j`` is the unit vector in the ``j`` direction. @@ -315,13 +315,13 @@ essentially the same thing in the equations following his Eq. (4.1.5). Condon and Shortley's Eq. (1) of section 4³ (page 50) defines ```math -L_z = -i \hbar \frac{\partial}{\partial \phi}, +L_z = -i \hbar \frac{\partial}{\partial ϕ}, ``` while Eq. (8) on the following page defines ```math \begin{aligned} -L_x + i L_y &= \hbar e^{i\phi} \left( \frac{\partial}{\partial \theta} + i \cot\theta \frac{\partial}{\partial \phi} \right), \\ -L_x - i L_y &= \hbar e^{-i\phi} \left(-\frac{\partial}{\partial \theta} + i \cot\theta \frac{\partial}{\partial \phi} \right). +L_x + i L_y &= \hbar e^{iϕ} \left( \frac{\partial}{\partial θ} + i \cot θ \frac{\partial}{\partial ϕ} \right), \\ +L_x - i L_y &= \hbar e^{-iϕ} \left(-\frac{\partial}{\partial θ} + i \cot θ \frac{\partial}{\partial ϕ} \right). \end{aligned} ``` Note that one is not the conjugate of the other! This is because of @@ -331,9 +331,9 @@ the factors of ``-i`` in the definitions of ``L_x`` and ``L_y``. operator for a rigid body in Eq. (2.2.2) as ```math \begin{aligned} -L_x &= -i\hbar \left(-\cos \alpha \cot\beta \frac{\partial}{\partial\alpha} - \sin\alpha \frac{\partial}{\partial\beta} + \frac{\cos\alpha}{\sin\beta} \frac{\partial}{\partial\gamma} \right), \\ -L_y &= -i\hbar \left(-\sin\alpha \cot\beta \frac{\partial}{\partial \alpha} + \cos\alpha \frac{\partial}{\partial\beta} + \frac{\sin\alpha}{\sin\beta} \frac{\partial}{\partial\gamma} \right), \\ -L_z &= -i\hbar \frac{\partial}{\partial\alpha}. +L_x &= -i\hbar \left(-\cos α \cot β \frac{\partial}{\partial α} - \sin α \frac{\partial}{\partial β} + \frac{\cos α}{\sin β} \frac{\partial}{\partial γ} \right), \\ +L_y &= -i\hbar \left(-\sin α \cot β \frac{\partial}{\partial α} + \cos α \frac{\partial}{\partial β} + \frac{\sin α}{\sin β} \frac{\partial}{\partial γ} \right), \\ +L_z &= -i\hbar \frac{\partial}{\partial α}. \end{aligned} ``` @@ -352,13 +352,13 @@ and the operator ``𝐏_{𝐑}`` to act on a function ``` Then, his Eq. (15.5) presumably implies ```math -Y_{ℓ,m}(\vartheta', \varphi') -= 𝐏_{\{\alpha, \beta, \gamma\}} Y_{ℓ,m}(\vartheta, \varphi) -= \sum_{m'} 𝔇^{(ℓ)}(\{\alpha, \beta, \gamma\})_{m',m} - Y_{ℓ,m'}(\vartheta, \varphi), +Y_{ℓ,m}(ϑ', φ') += 𝐏_{\{α, β, γ\}} Y_{ℓ,m}(ϑ, φ) += \sum_{m'} 𝔇^{(ℓ)}(\{α, β, γ\})_{m',m} + Y_{ℓ,m'}(ϑ, φ), ``` -where ``\{\alpha, \beta, \gamma\}`` takes ``(\vartheta, \varphi)`` to -``(\vartheta', \varphi')``. In any case, we can now leave behind this +where ``\{α, β, γ\}`` takes ``(ϑ, φ)`` to +``(ϑ', φ')``. In any case, we can now leave behind this ``𝐏`` notation and just look at the beginning and end of the equation above as the critical relationship in Wigner's notation. @@ -380,7 +380,7 @@ L_{\pm} {}_{s}Y_{ℓ,m}(𝐑) ``` That is, in our conventions we have ```math -\alpha^{\pm}_{ℓ,m} = \sqrt{(ℓ \mp m)(ℓ \pm m + 1)}, +α^{\pm}_{ℓ,m} = \sqrt{(ℓ \mp m)(ℓ \pm m + 1)}, ``` which is always real and positive, and thus consistent with the Condon-Shortley phase convention. @@ -388,17 +388,17 @@ convention. ### Properties -* ``D^j_{m'm}(\alpha,\beta,\gamma) = (-1)^{m'-m} D^j_{-m',-m}(\alpha,\beta,\gamma)^*`` -* ``(-1)^{m'-m}D^{j}_{mm'}(\alpha,\beta,\gamma)=D^{j}_{m'm}(\gamma,\beta,\alpha)`` +* ``D^j_{m'm}(α,β,γ) = (-1)^{m'-m} D^j_{-m',-m}(α,β,γ)^*`` +* ``(-1)^{m'-m}D^{j}_{mm'}(α,β,γ)=D^{j}_{m'm}(γ,β,α)`` * ``d_{m',m}^{j}=(-1)^{m-m'}d_{m,m'}^{j}=d_{-m,-m'}^{j}`` ```math \begin{aligned} -d_{m',m}^{j}(\pi) &= (-1)^{j-m} \delta_{m',-m} \\[6pt] -d_{m',m}^{j}(\pi-\beta) &= (-1)^{j+m'} d_{m',-m}^{j}(\beta)\\[6pt] -d_{m',m}^{j}(\pi+\beta) &= (-1)^{j-m} d_{m',-m}^{j}(\beta)\\[6pt] -d_{m',m}^{j}(2\pi+\beta) &= (-1)^{2j} d_{m',m}^{j}(\beta)\\[6pt] -d_{m',m}^{j}(-\beta) &= d_{m,m'}^{j}(\beta) = (-1)^{m'-m} d_{m',m}^{j}(\beta) +d_{m',m}^{j}(\pi) &= (-1)^{j-m} δ_{m',-m} \\[6pt] +d_{m',m}^{j}(\pi-β) &= (-1)^{j+m'} d_{m',-m}^{j}(β)\\[6pt] +d_{m',m}^{j}(\pi+β) &= (-1)^{j-m} d_{m',-m}^{j}(β)\\[6pt] +d_{m',m}^{j}(2\pi+β) &= (-1)^{2j} d_{m',m}^{j}(β)\\[6pt] +d_{m',m}^{j}(-β) &= d_{m,m'}^{j}(β) = (-1)^{m'-m} d_{m',m}^{j}(β) \end{aligned} ``` @@ -410,17 +410,17 @@ d_{m',m}^{j}(-\beta) &= d_{m,m'}^{j}(\beta) = (-1)^{m'-m} d_{m',m}^{j}(\beta ```math \begin{gather} -R = \cos\epsilon + \sin\epsilon\, \hat{𝔯} \\ -R𝐯 = \cos\epsilon 𝐯 + \sin\epsilon\, \hat{𝔯}𝐯 \\ -R𝐯R^{-1} = (𝐯\cos\epsilon + \sin\epsilon\, \hat{𝔯}𝐯)(\cos\epsilon - \sin\epsilon\, \hat{𝔯}) \\ -R𝐯R^{-1} = 𝐯\cos^2\epsilon + \sin^2\epsilon\, \hat{𝔯}𝐯\hat{𝔯}^{-1} + \sin\epsilon \cos\epsilon\, (\hat{𝔯}𝐯 - 𝐯\hat{𝔯}) \\ +R = \cos ϵ + \sin ϵ\, \hat{𝔯} \\ +R𝐯 = \cos ϵ 𝐯 + \sin ϵ\, \hat{𝔯}𝐯 \\ +R𝐯R^{-1} = (𝐯\cos ϵ + \sin ϵ\, \hat{𝔯}𝐯)(\cos ϵ - \sin ϵ\, \hat{𝔯}) \\ +R𝐯R^{-1} = 𝐯\cos^2ϵ + \sin^2ϵ\, \hat{𝔯}𝐯\hat{𝔯}^{-1} + \sin ϵ \cos ϵ\, (\hat{𝔯}𝐯 - 𝐯\hat{𝔯}) \\ R𝐯R^{-1} = \begin{cases} 𝐯 & 𝐯 \hat{𝔯} = \hat{𝔯}𝐯 \\ -𝐯(\cos^2\epsilon - \sin^2\epsilon) + 2 \sin\epsilon \cos\epsilon\, \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ +𝐯(\cos^2ϵ - \sin^2ϵ) + 2 \sin ϵ \cos ϵ\, \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ \end{cases} \\ R𝐯R^{-1} = \begin{cases} 𝐯 & 𝐯 \hat{𝔯} = \hat{𝔯}𝐯 \\ -\cos2\epsilon 𝐯 + \sin2\epsilon \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ +\cos2ϵ 𝐯 + \sin2ϵ \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ \end{cases} \\ \end{gather} ``` diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index d0b9ae8c..e0e80938 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -19,14 +19,14 @@ We use standard right-handed Cartesian coordinates ``(x, y, z)`` and unit basis vectors ``(𝐱, 𝐲, 𝐳)``. ## Spherical coordinates -We define spherical coordinates ``(r, \theta, \phi)`` and unit basis -vectors ``(𝐧, \boldsymbol{\theta}, \boldsymbol{\phi})``. The "polar -angle" ``\theta \in [0, \pi]`` measures the angle between the +We define spherical coordinates ``(r, θ, ϕ)`` and unit basis +vectors ``(𝐧, \boldsymbol{θ}, \boldsymbol{ϕ})``. The "polar +angle" ``θ \in [0, \pi]`` measures the angle between the specified direction and the positive ``𝐳`` axis. The "azimuthal -angle" ``\phi \in [0, 2\pi)`` measures the angle between the +angle" ``ϕ \in [0, 2\pi)`` measures the angle between the projection of the specified direction onto the ``𝐱``-``𝐲`` plane and the positive ``𝐱`` axis, with the positive ``𝐲`` axis corresponding -to the positive angle ``\phi = \pi/2``. +to the positive angle ``ϕ = \pi/2``. ## Quaternions A quaternion is written ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢^2 = @@ -45,82 +45,82 @@ vectors as if they were quaternions, and vice versa. A rotation represented by the unit quaternion ``𝐑`` acts on a vector ``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. Where relevant, rotations will be assumed to be right-handed, so that a quaternion characterizing the -rotation through an angle ``\vartheta`` about a unit vector ``𝐮`` can -be expressed as ``𝐑 = \exp(\vartheta 𝐮/2)``. Note that ``-𝐑`` +rotation through an angle ``ϑ`` about a unit vector ``𝐮`` can +be expressed as ``𝐑 = \exp(ϑ 𝐮/2)``. Note that ``-𝐑`` would deliver the same *rotation*, which means that the group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* of the group of rotations ``\mathrm{SO}(3)``. Nonetheless, ``𝐑`` and ``-𝐑`` are distinct quaternions, and represent distinct "spinors". ## Spherical coordinates as quaternions -A point on the unit sphere with spherical coordinates ``(\theta, -\phi)`` can be represented by the unit quaternion +A point on the unit sphere with spherical coordinates ``(θ, +ϕ)`` can be represented by the unit quaternion ```math -𝐑_{\theta, \phi} +𝐑_{θ, ϕ} = -\exp(\phi 𝐤/2)\, \exp(\theta 𝐣/2). +\exp(ϕ 𝐤/2)\, \exp(θ 𝐣/2). ``` This not only takes the positive ``𝐳`` axis to the specified direction, but also takes the ``𝐱`` and ``𝐲`` axes onto the unit basis vectors of the spherical coordinate system: ```math \begin{aligned} -𝐧 &= 𝐑_{\theta, \phi}\, 𝐳\, 𝐑_{\theta, \phi}^{-1}, \\ -\boldsymbol{\theta} &= 𝐑_{\theta, \phi}\, 𝐱\, 𝐑_{\theta, \phi}^{-1}, \\ -\boldsymbol{\phi} &= 𝐑_{\theta, \phi}\, 𝐲\, 𝐑_{\theta, \phi}^{-1}. +𝐧 &= 𝐑_{θ, ϕ}\, 𝐳\, 𝐑_{θ, ϕ}^{-1}, \\ +\boldsymbol{θ} &= 𝐑_{θ, ϕ}\, 𝐱\, 𝐑_{θ, ϕ}^{-1}, \\ +\boldsymbol{ϕ} &= 𝐑_{θ, ϕ}\, 𝐲\, 𝐑_{θ, ϕ}^{-1}. \end{aligned} ``` ## Euler angles (and spherical coordinates) Euler angles parametrize a unit quaternion as ```math -𝐑_{\alpha, \beta, \gamma} +𝐑_{α, β, γ} = -\exp(\alpha 𝐤/2)\, \exp(\beta 𝐣/2)\, \exp(\gamma 𝐤/2). +\exp(α 𝐤/2)\, \exp(β 𝐣/2)\, \exp(γ 𝐤/2). ``` -The angles ``\alpha`` and ``\gamma`` take values in ``[0, 2\pi)``. -The angle ``\beta`` takes values in ``[0, 2\pi]`` to parametrize the +The angles ``α`` and ``γ`` take values in ``[0, 2\pi)``. +The angle ``β`` takes values in ``[0, 2\pi]`` to parametrize the group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in ``[0, \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. By comparison, we can immediately see that spherical coordinates -``(\theta, \phi)`` can be represented as Euler angles with the -equivalence ``(\alpha, \beta, \gamma) = (\phi, \theta, 0)``. In +``(θ, ϕ)`` can be represented as Euler angles with the +equivalence ``(α, β, γ) = (ϕ, θ, 0)``. In particular, any function of spherical coordinates can be promoted to a function on Euler angles using this identification. It's worth noting that the action of Euler angles on the Cartesian basis is similar to the action of the spherical-coordinate quaternion, -but rotates the tangent basis ($\boldsymbol{\theta}, -\boldsymbol{\phi}$). That is, we still have +but rotates the tangent basis ($\boldsymbol{θ}, +\boldsymbol{ϕ}$). That is, we still have ```math -𝐧 = 𝐑_{\phi, \theta, \gamma}\, 𝐳\, 𝐑_{\phi, \theta, \gamma}^{-1}, +𝐧 = 𝐑_{ϕ, θ, γ}\, 𝐳\, 𝐑_{ϕ, θ, γ}^{-1}, ``` but the action on the ``𝐱`` and ``𝐲`` axes is a little more -complicated due to the initial rotation by ``\exp(\gamma 𝐤/2)``, -which is equivalent to a *final* rotation through ``\gamma`` about +complicated due to the initial rotation by ``\exp(γ 𝐤/2)``, +which is equivalent to a *final* rotation through ``γ`` about ``𝐧``. It's easier to write this down if we form the combination ```math 𝐦 = \frac{1}{\sqrt{2}} \left( - \boldsymbol{\theta} + i \boldsymbol{\phi} + \boldsymbol{θ} + i \boldsymbol{ϕ} \right), ``` and find that ```math -𝐦 = e^{-i\gamma} 𝐑_{\phi, \theta, \gamma}\, \frac{1}{\sqrt{2}} \left( +𝐦 = e^{-iγ} 𝐑_{ϕ, θ, γ}\, \frac{1}{\sqrt{2}} \left( 𝐱 + i 𝐲 -\right)\, 𝐑_{\phi, \theta, \gamma}^{-1}. +\right)\, 𝐑_{ϕ, θ, γ}^{-1}. ``` ## Left and right angular-momentum operators For a complex-valued function ``f(𝐑)``, we define two operators, the left and right angular-momentum operators: ```math -L_𝐮 f(𝐑) = \left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} -f\left(e^{-\epsilon 𝐮/2}\, 𝐑\right) +L_𝐮 f(𝐑) = \left.i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}\, 𝐑\right) \qquad \text{and} \qquad -R_𝐮 f(𝐑) = -\left.i \frac{d}{d\epsilon}\right|_{\epsilon=0} -f\left(𝐑\, e^{-\epsilon 𝐮/2}\right), +R_𝐮 f(𝐑) = -\left.i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(𝐑\, e^{-ϵ 𝐮/2}\right), ``` where ``𝐮`` can be any quaternion, though unit pure-vector quaternions are the most common. In particular, ``L`` represents the @@ -129,32 +129,32 @@ expressions in Euler angles for the basis vectors: ```math \begin{aligned} L_𝐢 &= i \left\{ - \frac{\cos\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - + \sin\alpha \frac{\partial} {\partial \beta} - - \frac{\cos\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \frac{\cos α}{\tan β} \frac{\partial} {\partial α} + + \sin α \frac{\partial} {\partial β} + - \frac{\cos α}{\sin β} \frac{\partial} {\partial γ} \right\}, & R_𝐢 &= i \left\{ - -\frac{\cos\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} - +\sin\gamma \frac{\partial} {\partial \beta} - +\frac{\cos\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} + -\frac{\cos γ}{\sin β} \frac{\partial} {\partial α} + +\sin γ \frac{\partial} {\partial β} + +\frac{\cos γ}{\tan β} \frac{\partial} {\partial γ} \right\}, \\ L_𝐣 &= i \left\{ - \frac{\sin\alpha}{\tan\beta} \frac{\partial} {\partial \alpha} - - \cos\alpha \frac{\partial} {\partial \beta} - -\frac{\sin\alpha}{\sin\beta} \frac{\partial} {\partial \gamma} + \frac{\sin α}{\tan β} \frac{\partial} {\partial α} + - \cos α \frac{\partial} {\partial β} + -\frac{\sin α}{\sin β} \frac{\partial} {\partial γ} \right\}, & R_𝐣 &= i \left\{ - \frac{\sin\gamma}{\sin\beta} \frac{\partial} {\partial \alpha} - +\cos\gamma \frac{\partial} {\partial \beta} - -\frac{\sin\gamma}{\tan\beta} \frac{\partial} {\partial \gamma} + \frac{\sin γ}{\sin β} \frac{\partial} {\partial α} + +\cos γ \frac{\partial} {\partial β} + -\frac{\sin γ}{\tan β} \frac{\partial} {\partial γ} \right\}, \\ -L_𝐤 &= -i \frac{\partial} {\partial \alpha}, +L_𝐤 &= -i \frac{\partial} {\partial α}, & -R_𝐤 &= i \frac{\partial} {\partial \gamma}. +R_𝐤 &= i \frac{\partial} {\partial γ}. \end{aligned} ``` These correspond precisely to the standard expressions for the @@ -169,29 +169,29 @@ find that [L_𝐮, R_𝐯] = 0. ``` Restricting to just the basis vectors, indexed as ``a,b,c``, the first -of these reduces to ``[L_a, L_b] = i \epsilon_{abc} L_c``, which is +of these reduces to ``[L_a, L_b] = i ϵ_{abc} L_c``, which is precisely the standard result. We can also lift any function on ``𝕊²`` to a function on ``𝕊³`` — or more precisely any function on spherical coordinates to a function on the space of Euler angles — by -the correspondence ``(\theta, \phi) \mapsto (\alpha, \beta, \gamma) = -(\phi, \theta, 0)``. We can then express the angular-momentum +the correspondence ``(θ, ϕ) \mapsto (α, β, γ) = +(ϕ, θ, 0)``. We can then express the angular-momentum operators in their more common form, in terms of spherical coordinates: ```math L_x = i \left\{ - \frac{\cos\phi}{\tan\theta} \frac{\partial} {\partial \phi} - + \sin\phi \frac{\partial} {\partial \theta} + \frac{\cos ϕ}{\tan θ} \frac{\partial} {\partial ϕ} + + \sin ϕ \frac{\partial} {\partial θ} \right\} \qquad L_y = i \left\{ - \frac{\sin\phi}{\tan\theta} \frac{\partial} {\partial \phi} - - \cos\phi \frac{\partial} {\partial \theta} + \frac{\sin ϕ}{\tan θ} \frac{\partial} {\partial ϕ} + - \cos ϕ \frac{\partial} {\partial θ} \right\} \qquad -L_z = -i \frac{\partial} {\partial \phi} +L_z = -i \frac{\partial} {\partial ϕ} ``` The ``R`` operators make less sense for a function of spherical -coordinates, because of their inherent dependence on ``\gamma.`` We +coordinates, because of their inherent dependence on ``γ.`` We will come back to them, however, when we consider spin-weighted functions — which are inherently ill-defined on the 2-sphere, but can be interpreted as restrictions of functions on the 3-sphere with this @@ -205,7 +205,7 @@ package defines the spherical harmonics in terms of Wigner's 𝔇 matrices, by way of the spin-weighted spherical harmonics, as a function of a quaternion: ```math -Y_{l,m}(𝐐) = \sqrt{\frac{2ℓ+1}{4\pi}} e^{im\phi} +Y_{l,m}(𝐐) = \sqrt{\frac{2ℓ+1}{4\pi}} e^{imϕ} D^{(l)}_{m,0}(𝐐), ``` where ``D^{(l)}_{m,0}`` is the Wigner 𝔇 matrix. This is a @@ -219,13 +219,13 @@ terms of spherical coordinates, that expression is \begin{align} Y_{l,m} &= - \sqrt{\frac{2ℓ+1}{4\pi}} e^{im\phi} + \sqrt{\frac{2ℓ+1}{4\pi}} e^{imϕ} \sum_{k = k_1}^{k_2} \frac{(-1)^k ℓ! [(ℓ+m)!(ℓ-m)!]^{1/2}} {(ℓ+m-k)!(ℓ-k)!k!(k-m)!} \\ &\qquad \times - \left(\cos\left(\frac{\theta}{2}\right)\right)^{2ℓ+m-2k} - \left(\sin\left(\frac{\theta}{2}\right)\right)^{2k-m} + \left(\cos\left(\frac{θ}{2}\right)\right)^{2ℓ+m-2k} + \left(\sin\left(\frac{θ}{2}\right)\right)^{2k-m} \end{align} ``` where ``k_1 = \textrm{max}(0, m)`` and ``k_2=\textrm{min}(ℓ+m, @@ -238,18 +238,18 @@ other sources. as ```math m^\mu = \frac{1}{\sqrt{2}} \left( - \boldsymbol{\theta} + i \boldsymbol{\phi} + \boldsymbol{θ} + i \boldsymbol{ϕ} \right)^\mu ``` and discuss spin weight in terms of the rotation ```math -(m^\mu)' = e^{i\psi} m^\mu, +(m^\mu)' = e^{iψ} m^\mu, ``` where the tangent basis rotates but we are "keeping the coordinates fixed". They then define a function to have spin weight ``s`` if it transforms as ```math -\eta' = e^{is\psi} \eta. +\eta' = e^{isψ} \eta. ``` Such functions are generally the result of contracting a tensor field with some number of ``m^\mu`` and some number of ``\bar{m}^\mu`` @@ -269,25 +269,25 @@ quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, and frequently discuss them in terms of Euler angles. As we saw [above](@ref "Euler angles (and spherical coordinates)"), -``m^\mu`` corresponds to the Euler angles ``(\phi, \theta, 0)``, while -``(m^\mu)'`` corresponds to the Euler angles ``(\phi, \theta, --\psi)``. The function, written in terms of Euler angles, becomes +``m^\mu`` corresponds to the Euler angles ``(ϕ, θ, 0)``, while +``(m^\mu)'`` corresponds to the Euler angles ``(ϕ, θ, +-ψ)``. The function, written in terms of Euler angles, becomes ```math -\eta(\phi, \theta, -\psi) = e^{is\psi} \eta(\phi, \theta, 0), +\eta(ϕ, θ, -ψ) = e^{isψ} \eta(ϕ, θ, 0), ``` or ```math -\eta(\alpha, \beta, \gamma) = e^{-is\gamma} \eta(\alpha, \beta, 0). +\eta(α, β, γ) = e^{-isγ} \eta(α, β, 0). ``` This is the crucial definition giving us the behavior of spin-weighted functions: they are eigenfunctions of the operator -``R_z = i \partial_\gamma`` with eigenvalue ``s``. +``R_z = i \partial_γ`` with eigenvalue ``s``. We can make this a little less dependent on the choice of Euler angles by writing ``\eta`` not as a function of Euler angles, but as a function of a quaternion. We then have ```math -\eta(𝐐\, e^{\gamma 𝐤/2}) = e^{-is\gamma} \eta(𝐐), +\eta(𝐐\, e^{γ 𝐤/2}) = e^{-isγ} \eta(𝐐), ``` which means that spin-weighted functions are eigenfunctions of the operator ``R_𝐤`` with eigenvalue ``s``. @@ -298,20 +298,20 @@ commutator relations for ``R``: ```math \begin{aligned} \eth \eta &= \left(R_x + i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - + \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right), \\ + = -\sin^s θ \left\{ + \frac{\partial}{\partial θ} + + \frac{i}{\sin θ} \frac{\partial}{\partial ϕ} + \right\} \left(\eta \sin^{-s} θ\right), \\ \bar{\eth} \eta &= \left(R_x - i R_y\right)\eta - = -\sin^s \theta \left\{ - \frac{\partial}{\partial \theta} - - \frac{i}{\sin\theta} \frac{\partial}{\partial \phi} - \right\} \left(\eta \sin^{-s} \theta\right). + = -\sin^s θ \left\{ + \frac{\partial}{\partial θ} + - \frac{i}{\sin θ} \frac{\partial}{\partial ϕ} + \right\} \left(\eta \sin^{-s} θ\right). \end{aligned} ``` Here, we have used the full expressions for ``R_x`` and ``R_y`` given above in terms of Euler angles, replacing the derivatives -with respect to ``\gamma`` by a factor of ``-i s``, and converting +with respect to ``γ`` by a factor of ``-i s``, and converting the remaining Euler angles to spherical coordinates. This allows us to write them as if they were operators on the 2-sphere, even though this is mathematically ill-defined and spin-weighted @@ -330,7 +330,7 @@ of comparison with other sources. The expression is \begin{align} {}_{s}Y_{l,m} &= - (-1)^s\sqrt{\frac{2ℓ+1}{4\pi}} e^{im\phi} + (-1)^s\sqrt{\frac{2ℓ+1}{4\pi}} e^{imϕ} \sum_{k = k_1}^{k_2} \frac{(-1)^k[(ℓ+m)!(ℓ-m)!(ℓ-s)!(ℓ+s)!]^{1/2}} {(ℓ+m-k)!(ℓ+s-k)!k!(k-s-m)!} diff --git a/docs/src/notes/H_recurrence.md b/docs/src/notes/H_recurrence.md index 7c320e54..352ed4d9 100644 --- a/docs/src/notes/H_recurrence.md +++ b/docs/src/notes/H_recurrence.md @@ -5,13 +5,13 @@ which is itself related to the (big) ``𝔇`` matrices and the various spin-weig spherical harmonics ``{}_{s}Y_{ℓ,m}`` — via ```math -d_{ℓ}^{m',m} = \epsilon_{m'} \epsilon_{-m} H^{ℓ}_{m',m}, +d_{ℓ}^{m',m} = ϵ_{m'} ϵ_{-m} H^{ℓ}_{m',m}, ``` where ```math -\epsilon_k = +ϵ_k = \begin{cases} 1 & k\leq 0, \\ (-1)^k & k > 0. @@ -118,7 +118,7 @@ where the coefficients are given by b_n &= \sqrt{\frac{2(n-1)(2n+1)}{n(2n-1)}} \\ c_{n,m} &= \frac{1}{n} \sqrt{\frac{(n+m)(n-m)(2n+1)}{2n-1}} \\ d_{n,m} &= \frac{1}{2n} \sqrt{\frac{(n-m)(n-m-1)(2n+1)}{2n-1}} \\ - e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-\delta_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. + e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-δ_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. \end{aligned} ``` diff --git a/docs/src/notes/H_recursions.md b/docs/src/notes/H_recursions.md index 72e3c15b..17202cb7 100644 --- a/docs/src/notes/H_recursions.md +++ b/docs/src/notes/H_recursions.md @@ -5,13 +5,13 @@ which is itself related to the (big) ``𝔇`` matrices and the various spin-weig spherical harmonics ``{}_{s}Y_{ℓ,m}`` — via ```math -d_{ℓ}^{m',m} = \epsilon_{m'} \epsilon_{-m} H_{ℓ}^{m',m}, +d_{ℓ}^{m',m} = ϵ_{m'} ϵ_{-m} H_{ℓ}^{m',m}, ``` where ```math -\epsilon_k = +ϵ_k = \begin{cases} 1 & k\leq 0, \\ (-1)^k & k > 0. @@ -113,7 +113,7 @@ where the coefficients are given by b_n &= \sqrt{\frac{2(n-1)(2n+1)}{n(2n-1)}} \\ c_{n,m} &= \frac{1}{n} \sqrt{\frac{(n+m)(n-m)(2n+1)}{2n-1}} \\ d_{n,m} &= \frac{1}{2n} \sqrt{\frac{(n-m)(n-m-1)(2n+1)}{2n-1}} \\ - e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-\delta_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. + e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-δ_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. \end{aligned} ``` diff --git a/docs/src/notes/sampling_theorems.md b/docs/src/notes/sampling_theorems.md index f2b5d3ff..aa8d486b 100644 --- a/docs/src/notes/sampling_theorems.md +++ b/docs/src/notes/sampling_theorems.md @@ -23,15 +23,15 @@ has slowly growing errors through ``L = 4096``. The EKKM analysis looks like the following (with some notational changes). We begin by defining ```math - {}_{s}\tilde{f}_{\theta}(m) := \int_0^{2\pi} {}_sf(\theta, \phi)\, e^{-im\phi}\, d\phi. + {}_{s}\tilde{f}_{θ}(m) := \int_0^{2\pi} {}_sf(θ, ϕ)\, e^{-imϕ}\, dϕ. ``` We will denote the vector of these quantities for all values of -``\theta`` as ``{}_{s}\tilde{𝐟}_m``. Inserting the -``{}_sY_{ℓ,m}`` expansion for ``{}_sf(\theta, \phi)``, and +``θ`` as ``{}_{s}\tilde{𝐟}_m``. Inserting the +``{}_sY_{ℓ,m}`` expansion for ``{}_sf(θ, ϕ)``, and performing the integration using orthogonality of complex exponentials, we can find that ```math - {}_{s}\tilde{f}_{\theta}(m) = (-1)^s\, 2\pi \sum_{ℓ=\Delta}^L \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{m,-s}^{ℓ}(\theta)\, {}_sf_{ℓ,m}. + {}_{s}\tilde{f}_{θ}(m) = (-1)^s\, 2\pi \sum_{ℓ=\Delta}^L \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{m,-s}^{ℓ}(θ)\, {}_sf_{ℓ,m}. ``` Now, denoting the vector of ``{}_sf_{ℓ,m}`` for all values of ``ℓ`` as ``{}_s𝐟_m``, we can write this as a matrix-vector @@ -56,26 +56,26 @@ additional sum over ``|m'|>|m|``. Or perhaps more precisely, the first equation isn't actually what we implement. It should look more like this: ```math - {}_{s}\tilde{f}_{j}(m) := \sum_{k=0}^{2j} {}_sf(\theta_j, \phi_k)\, e^{-im\phi_k}\, \Delta \phi, + {}_{s}\tilde{f}_{j}(m) := \sum_{k=0}^{2j} {}_sf(θ_j, ϕ_k)\, e^{-imϕ_k}\, \Delta ϕ, ``` -where ``\phi_k = \frac{2\pi k}{2j+1}``, and ``\Delta \phi = +where ``ϕ_k = \frac{2\pi k}{2j+1}``, and ``\Delta ϕ = \frac{2\pi}{2j+1}``. (Recall the subtle notational distinction common in time-frequency analysis that ``\tilde{s}(t_j) = \Delta t \tilde{s}_j``, which would suggest we use ``{}_{s}\tilde{f}_{j}(m) = -\Delta \phi\, {}_{s}\tilde{f}_{j,m}``.) Next, we can insert the -expansion for ``{}_sf(\theta, \phi)``: +\Delta ϕ\, {}_{s}\tilde{f}_{j,m}``.) Next, we can insert the +expansion for ``{}_sf(θ, ϕ)``: ```math \begin{aligned} {}_{s}\tilde{f}_{j}(m) - &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, {}_sY_{ℓ,m'}(\theta_j, \phi_k)\, e^{-im\phi_k}\, \Delta \phi \\ - &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, (-1)^{s}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(\theta_j) e^{i m' \phi_k}\, e^{-im\phi_k}\, \frac{2\pi}{2j+1} \\ - &= (-1)^{s}\, \frac{2\pi}{2j+1} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(\theta_j) \sum_{k=0}^{2j}e^{i (m'-m) \phi_k}. + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, {}_sY_{ℓ,m'}(θ_j, ϕ_k)\, e^{-imϕ_k}\, \Delta ϕ \\ + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, (-1)^{s}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(θ_j) e^{i m' ϕ_k}\, e^{-imϕ_k}\, \frac{2\pi}{2j+1} \\ + &= (-1)^{s}\, \frac{2\pi}{2j+1} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(θ_j) \sum_{k=0}^{2j}e^{i (m'-m) ϕ_k}. \end{aligned} ``` We can evaluate this last sum easily: ```math - \sum_{k=0}^{2j}e^{i (m'-m) \phi_k} = \begin{cases} + \sum_{k=0}^{2j}e^{i (m'-m) ϕ_k} = \begin{cases} 2j+1 & m'-m = n(2j+1)\ \mathrm{for}\ n\in\mathbb{Z}, \\ 0 & \mathrm{otherwise}. \end{cases} @@ -84,7 +84,7 @@ This allows us to simplify as ```math \begin{aligned} - {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(\theta_j), + {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(θ_j), \end{aligned} ``` where ``m'`` ranges over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq ℓ`` @@ -136,7 +136,7 @@ structure to solve the linear equation in a more piecewise fashion, with fairly low memory overhead. Essentially, we start with the highest-``|k|`` values, and solve for the corresponding highest-``|m|`` values. Those harmonics will alias to other -frequencies in ``\theta_j`` rings with ``j < |k|``. But crucially, we +frequencies in ``θ_j`` rings with ``j < |k|``. But crucially, we know *how* they alias, and can simply remove them from the Fourier transforms of those rings. We then repeat, solving for the next-highest ``|k|`` values, and so on. diff --git a/docs/src/operators.md b/docs/src/operators.md index 22fc2d22..ef98403e 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -115,7 +115,7 @@ The commutator relations for ``L`` are consistent — except for the differing use of ``\hbar`` — with the usual relations from quantum mechanics: ```math -\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. +\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} ε_{jkl} L_l. ``` Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we @@ -123,7 +123,7 @@ represent an arbitrary basis vector as ``\hat{e}_j``, then the quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` becomes ```math -[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. +[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} ε_{jkl} \hat{e}_l. ``` Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version @@ -203,7 +203,7 @@ function ``f`` under the action of this operator:[^2] &= \sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \delta_{ℓ,ℓ'} \delta_{m,m'+1} \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} δ_{ℓ,ℓ'} δ_{m,m'+1} \\ &= f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} \end{aligned} @@ -237,7 +237,7 @@ present for the spin-raising and -lowering operators: &= \sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \delta_{ℓ,ℓ'} \delta_{m,m'} \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} δ_{ℓ,ℓ'} δ_{m,m'} \\ &= f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} \end{aligned} diff --git a/src/deprecated/evaluate.jl b/src/deprecated/evaluate.jl index c73cb11d..abdedf48 100644 --- a/src/deprecated/evaluate.jl +++ b/src/deprecated/evaluate.jl @@ -14,7 +14,7 @@ using Quaternionic: Quaternionic, AbstractQuaternion, to_euler_phases d_matrices(β, ℓₘₐₓ) d_matrices(expiβ, ℓₘₐₓ) -Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(\beta)`` for all +Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(β)`` for all ``ℓ \leq ℓ_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" Wigner matrices, in contrast to the full ``𝔇`` matrices. @@ -35,7 +35,7 @@ d_matrices(β::Real, ℓₘₐₓ) = d_matrices(cis(β), ℓₘₐₓ) d_matrices!(d, β, ℓₘₐₓ) d_matrices!(d, expiβ, ℓₘₐₓ) -Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(\beta)`` for all +Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(β)`` for all ``ℓ \leq ℓ_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" Wigner matrices, in contrast to the full ``𝔇`` matrices. @@ -200,7 +200,7 @@ end D_matrices(R, ℓₘₐₓ) D_matrices(α, β, γ, ℓₘₐₓ) -Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(\beta)`` for all ``ℓ \leq +Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(β)`` for all ``ℓ \leq ℓ_\mathrm{max}``. See [`D_matrices!`](@ref) for details about the input and output values. @@ -230,7 +230,7 @@ end D_matrices!(D, R, ℓₘₐₓ) D_matrices!(D, α, β, γ, ℓₘₐₓ) -Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(\beta)`` for all ``ℓ \leq +Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(β)`` for all ``ℓ \leq ℓ_\mathrm{max}``. In all cases, the result is returned in a 1-dimensional array ordered as @@ -656,7 +656,7 @@ package's spherical harmonics and other references' more clear. It is inefficie terms of memory and computation time, and should generally not be used in production code. Computes a single (complex) value of the spherical harmonic ``(ℓ, m)`` at the given -spherical coordinate ``(\theta, \phi)``. +spherical coordinate ``(θ, ϕ)``. """ function Y(s::Int, ℓ::Int, m::Int, θ, ϕ) θ, ϕ = promote(θ, ϕ) diff --git a/src/deprecated/iterators.jl b/src/deprecated/iterators.jl index d60460ae..d8ea11eb 100644 --- a/src/deprecated/iterators.jl +++ b/src/deprecated/iterators.jl @@ -266,9 +266,9 @@ This provides initial values for the recursion to find values with ``ℓ=m``. ```math -{}_{s}\lambda_{ℓ,m}(\theta) - := {}_{s}Y_{ℓ,m}(\theta, 0) - = (-1)^m\, \sqrt{\frac{2ℓ+1}{4\pi}} d^ℓ_{-m,s}(\theta) +{}_{s}\lambda_{ℓ,m}(θ) + := {}_{s}Y_{ℓ,m}(θ, 0) + = (-1)^m\, \sqrt{\frac{2ℓ+1}{4\pi}} d^ℓ_{-m,s}(θ) ``` """ function λ_recursion_initialize(sin½θ::T, cos½θ::T, s, ℓ, m) where T diff --git a/src/ssht/huffenberger_wandelt.jl b/src/ssht/huffenberger_wandelt.jl index 0a56c838..adfe5cd8 100644 --- a/src/ssht/huffenberger_wandelt.jl +++ b/src/ssht/huffenberger_wandelt.jl @@ -2,7 +2,7 @@ module HuffenbergerWandelt raw""" The Wigner ``d`` matrix corresponds to a rotation about the ``y`` axis by an angle ``β``: -``\exp\left[ \beta 𝐣 / 2 \right]``. But computing the ``d`` matrix for a general angle is +``\exp\left[ β 𝐣 / 2 \right]``. But computing the ``d`` matrix for a general angle is a little awkward. However, computing the ``d`` matrix for a rotation by ``π/2`` is somewhat simpler, and computing the full ``D`` matrix for a rotation about the ``z`` axis is trivial — it's just a phase factor. Thus, we can re-express ``d`` for a general angle ``β`` in @@ -15,9 +15,9 @@ the ``x`` axis: ``` So we have ```math -e^{\beta 𝐣 / 2} +e^{β 𝐣 / 2} = -e^{\frac{\pi}{2} 𝐢/ 2}\, e^{\beta 𝐤 / 2}\, e^{-\frac{\pi}{2} 𝐢/ 2}. +e^{\frac{\pi}{2} 𝐢/ 2}\, e^{β 𝐤 / 2}\, e^{-\frac{\pi}{2} 𝐢/ 2}. ``` Unfortunately, this expression involves the ``x`` axis, which we don't want. But we can similarly express the ``x`` axis in terms of the ``y`` axis rotated about the ``z`` axis: @@ -26,69 +26,69 @@ similarly express the ``x`` axis in terms of the ``y`` axis rotated about the `` ``` And now we can use this in our first expression to find ```math -e^{\beta 𝐣 / 2} +e^{β 𝐣 / 2} = e^{-\frac{\pi}{2} 𝐤/ 2}\, e^{\frac{\pi}{2} 𝐣/ 2}\, e^{\frac{\pi}{2} 𝐤/ 2}\, -e^{\beta 𝐤 / 2}\, +e^{β 𝐤 / 2}\, e^{\frac{\pi}{2} 𝐤/ 2}\, e^{-\frac{\pi}{2} 𝐣/ 2}\, e^{-\frac{\pi}{2} 𝐤/ 2}. ``` Now, we can use this expansion to find an expression for the ``d`` matrix value: ```math \begin{align} -d^{ℓ}_{m', m}(\beta) +d^{ℓ}_{m', m}(β) &= -𝔇^{ℓ}_{m', m}\left(e^{\beta 𝐣 / 2}\right) \\ +𝔇^{ℓ}_{m', m}\left(e^{β 𝐣 / 2}\right) \\ &= 𝔇^{ℓ}_{m', m_1}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right)\, 𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, 𝔇^{ℓ}_{m_2, m_3}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, -𝔇^{ℓ}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ +𝔇^{ℓ}_{m_3, m_4}\left(e^{β 𝐤 / 2}\right)\, \\ &\quad \times 𝔇^{ℓ}_{m_4, m_5}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, 𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, 𝔇^{ℓ}_{m_6, m}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right) \\ &= -\delta_{m', m_1} e^{im'\frac{\pi}{2}}\, +δ_{m', m_1} e^{im'\frac{\pi}{2}}\, 𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -\delta_{m_2, m_3} e^{-im_2\frac{\pi}{2}}\, -𝔇^{ℓ}_{m_3, m_4}\left(e^{\beta 𝐤 / 2}\right)\, \\ +δ_{m_2, m_3} e^{-im_2\frac{\pi}{2}}\, +𝔇^{ℓ}_{m_3, m_4}\left(e^{β 𝐤 / 2}\right)\, \\ &\quad \times -\delta_{m_4, m_5} e^{-im_4\frac{\pi}{2}}\, +δ_{m_4, m_5} e^{-im_4\frac{\pi}{2}}\, 𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, -\delta_{m_6, m} e^{im\frac{\pi}{2}} \\ +δ_{m_6, m} e^{im\frac{\pi}{2}} \\ &= e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, &\quad \times 𝔇^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -𝔇^{ℓ}_{m'', m'''}\left(e^{\beta 𝐤 / 2}\right)\, \\ +𝔇^{ℓ}_{m'', m'''}\left(e^{β 𝐤 / 2}\right)\, \\ 𝔇^{ℓ}_{m''', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ &= e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, &\quad \times 𝔇^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -e^{-im''\beta}\, +e^{-im''β}\, 𝔇^{ℓ}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ &= i^{m'+m-2m''}\, d^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -e^{-im''\beta}\, +e^{-im''β}\, d^{ℓ}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ &= i^{m'+m}(-1)^{m''}\, d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, -e^{-im''\beta}\, +e^{-im''β}\, d^{ℓ}_{m'', m}\left(-\frac{\pi}{2}\right) \\ &= i^{m'+m}(-1)^{m}\, d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, -e^{-im''\beta}\, +e^{-im''β}\, d^{ℓ}_{m'', m}\left(\frac{\pi}{2}\right) \\ &= i^{m'-m}\, d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, -e^{-im''\beta}\, +e^{-im''β}\, d^{ℓ}_{m'', m}\left(\frac{\pi}{2}\right) \end{align} ``` diff --git a/src/utilities/pixelizations.jl b/src/utilities/pixelizations.jl index 49bdd60b..89866f1a 100644 --- a/src/utilities/pixelizations.jl +++ b/src/utilities/pixelizations.jl @@ -154,10 +154,10 @@ end Cover the sphere 𝕊² with pixels given by the [DriscollHealy_1994](@citet) equiangular grid: -> Let ``f(\theta, \phi)`` be a band-limited function such that ``\hat{f}(l, m) = 0`` for ``l -> ≥ b``. We sample the function at the equiangular grid of points ``(\theta_i, \phi_j)``, -> ``i = 0, \ldots, 2b-1``, ``j = 0, \ldots, 2b-1``, where ``\theta_i = \pi i/2b`` and -> ``\phi_j = \pi j/b``. +> Let ``f(θ, ϕ)`` be a band-limited function such that ``\hat{f}(l, m) = 0`` for ``l +> ≥ b``. We sample the function at the equiangular grid of points ``(θ_i, ϕ_j)``, +> ``i = 0, \ldots, 2b-1``, ``j = 0, \ldots, 2b-1``, where ``θ_i = \pi i/2b`` and +> ``ϕ_j = \pi j/b``. The returned quantity is a vector of 2-SVectors providing the spherical coordinates of each pixel. See also [`driscoll_healy_rotors`](@ref) for the corresponding `Rotor`s. @@ -201,11 +201,11 @@ driscoll_healy_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_ Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid: -> We adopt an equiangular sampling of the sphere with sample positions given by ``\theta_t = +> We adopt an equiangular sampling of the sphere with sample positions given by ``θ_t = > \frac{\pi(2t+1)}{2ℓ_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, ℓ_\mathrm{max}-1\}`` -> and ``\phi_p = \frac{2 \pi p}{2ℓ_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, -> 2ℓ_\mathrm{max}-2\}``. In order to extend the ``\theta`` domain to ``[0, 2\pi)`` we -> simply extend the domain of the ``\theta`` index to include ``\{ℓ_\mathrm{max}, +> and ``ϕ_p = \frac{2 \pi p}{2ℓ_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, +> 2ℓ_\mathrm{max}-2\}``. In order to extend the ``θ`` domain to ``[0, 2\pi)`` we +> simply extend the domain of the ``θ`` index to include ``\{ℓ_\mathrm{max}, > ℓ_\mathrm{max}+1, \dotsc, 2ℓ_\mathrm{max}-1\}``. !!! note diff --git a/src/utilities/weights.jl b/src/utilities/weights.jl index ff827528..d0010078 100644 --- a/src/utilities/weights.jl +++ b/src/utilities/weights.jl @@ -4,7 +4,7 @@ Compute `n` weights for Fejér's first rule, corresponding to `n` evenly spaced nodes from 0 to π inclusive. That is, the nodes are located at ```math -\theta_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. +θ_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). @@ -39,7 +39,7 @@ end Compute `n` weights for Fejér's second rule, corresponding to `n` evenly spaced nodes between 0 and π exclusive. That is, the nodes are located at ```math -\theta_k = k \frac{\pi}{n+1} \quad k=1, \ldots, n. +θ_k = k \frac{\pi}{n+1} \quad k=1, \ldots, n. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). However, @@ -92,7 +92,7 @@ end Compute `n` weights for the Clenshaw-Curtis rule, corresponding to `n` evenly spaced nodes from 0 to π inclusive. That is, the nodes are located at ```math -\theta_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. +θ_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). diff --git a/test/conventions/NIST_DLMF.jl b/test/conventions/NIST_DLMF.jl index cbe47a15..43ac296a 100644 --- a/test/conventions/NIST_DLMF.jl +++ b/test/conventions/NIST_DLMF.jl @@ -27,11 +27,11 @@ or [Eq. 14.7.14](http://dlmf.nist.gov/14.7#E14) ``` And for the spherical harmonics, [Eq. 14.30.1](http://dlmf.nist.gov/14.30#E1) gives ```math - Y_{ℓ, m}\left(\theta,\phi\right) + Y_{ℓ, m}\left(θ,ϕ\right) = \left(\frac{(ℓ-m)!(2ℓ+1)}{4\pi(ℓ+m)!}\right)^{1/2} - \mathsf{e}^{im\phi} - \mathsf{P}_{ℓ}^{m}\left(\cos\theta\right). + \mathsf{e}^{imϕ} + \mathsf{P}_{ℓ}^{m}\left(\cos θ\right). ``` """ diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl index ceafee34..88a9e3c0 100644 --- a/test/conventions/edmonds.jl +++ b/test/conventions/edmonds.jl @@ -39,7 +39,7 @@ end Eqs. (4.1.12) of [Edmonds](@cite Edmonds_2016), implementing ```math - 𝒟^{(j)}_{m',m}(\alpha, \beta, \gamma). + 𝒟^{(j)}_{m',m}(α, β, γ). ``` See also [`d`](@ref) for Edmonds' version the Wigner d-function. @@ -54,7 +54,7 @@ end Eqs. (4.1.15) of [Edmonds](@cite Edmonds_2016), implementing ```math - d^{(j)}_{m',m}(\beta). + d^{(j)}_{m',m}(β). ``` See also [`𝒟`](@ref) for Edmonds' version the Wigner D-function. diff --git a/test/conventions/goldbergetal.jl b/test/conventions/goldbergetal.jl index c47f5e81..e5a61700 100644 --- a/test/conventions/goldbergetal.jl +++ b/test/conventions/goldbergetal.jl @@ -22,7 +22,7 @@ import .NaiveFactorials: ❗ Eq. (3.9) of [Goldberg et al.](@cite GoldbergEtAl_1967), implementing ```math - D^j_{m',m}(\alpha, \beta, \gamma). + D^j_{m',m}(α, β, γ). ``` """ function D(j, m′, m, α, β, γ) @@ -63,7 +63,7 @@ end Eq. (3.1) of [Goldberg et al.](@cite GoldbergEtAl_1967), implementing ```math - {}_sY_{ℓ,m}(\theta, \phi). + {}_sY_{ℓ,m}(θ, ϕ). ``` Note that there is a difference in conventions between the ``Y`` of Goldberg et al. and diff --git a/test/conventions/sakurai.jl b/test/conventions/sakurai.jl index ba5ffa0d..493d516a 100644 --- a/test/conventions/sakurai.jl +++ b/test/conventions/sakurai.jl @@ -6,36 +6,36 @@ The conclusion here is that Sakurai's Yₗᵐ(θ, ϕ) is the same as ours, but h - On p. 154 he says that "a rotation operation affects the physical system itself, ..., while the coordinate axes remain *unchanged*." -- On p. 156 he poses "``|\alpha\rangle_R = 𝒟(R) | \alpha \rangle``, where - ``|\alpha\rangle_R`` and ``|\alpha \rangle`` stand for the kets of the rotated and +- On p. 156 he poses "``|α\rangle_R = 𝒟(R) | α \rangle``, where + ``|α\rangle_R`` and ``|α \rangle`` stand for the kets of the rotated and original system, respectively." -- On p. 157 he says "``𝒟(\hat{𝐧}, d\phi) = 1 - i\left( \frac{𝐉 - \cdot \hat{𝐧}} {\hbar} \right) d\phi``" +- On p. 157 he says "``𝒟(\hat{𝐧}, dϕ) = 1 - i\left( \frac{𝐉 + \cdot \hat{𝐧}} {\hbar} \right) dϕ``" - On p. 173 he defines his Euler angles in the same way as Quaternionic. - On p. 192 he defines "``𝒟^{(j)}_{m',m}(R) = \langle j,m'| \exp \left( - \frac{-i𝐉 \cdot \hat{𝐧} \phi} {\hbar} \right) |j, m\rangle``". + \frac{-i𝐉 \cdot \hat{𝐧} ϕ} {\hbar} \right) |j, m\rangle``". - On p. 194 he gives the expression in terms of Euler angles. - On p. 223 he gives an explicit formula for ``d``. - On p. 203 he relates ``𝒟 to Y_{ℓ}^m$ (note the upper index of ``m``). Below (1.6.14), we find the translation operator acts as -``\mathscr{T}_{dx'} \alpha(x') = \alpha(x' - dx')``. Then Eq. +``\mathscr{T}_{dx'} α(x') = α(x' - dx')``. Then Eq. (1.6.32) ```math \mathscr{T}_{dx'} = 1 - i p\, dx', ``` for infinitesimal ``dx'``. Eq. (1.7.17) gives the momentum operator -as ``p \alpha(x') = -i \partial_{x'} \alpha(x')``. Combining these, +as ``p α(x') = -i \partial_{x'} α(x')``. Combining these, we can verify consistency: ```math -\mathscr{T}_{dx'} \alpha(x') +\mathscr{T}_{dx'} α(x') = -\alpha(x' - dx') +α(x' - dx') = -\alpha(x') - \partial_{x'}\, \alpha(x')\, dx', +α(x') - \partial_{x'}\, α(x')\, dx', ``` -which is exactly what we expect from Taylor expanding ``\alpha(x' - +which is exactly what we expect from Taylor expanding ``α(x' - dx')``. @@ -43,12 +43,12 @@ dx')``. \begin{aligned} f\left(𝐑\right) &\to -f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ &\approx -f\left(𝐑\right) + \epsilon \left. \frac{d}{d\epsilon} \right|_{\epsilon=0} -f\left(e^{-\epsilon 𝐮/2}𝐑\right) \\ +f\left(𝐑\right) + ϵ \left. \frac{d}{dϵ} \right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ &= -f\left(𝐑\right) - i \epsilon L_𝐮 f\left(𝐑\right) +f\left(𝐑\right) - i ϵ L_𝐮 f\left(𝐑\right) ``` """ @@ -66,7 +66,7 @@ import .NaiveFactorials: ❗ Eqs. (3.5.50)-(3.5.51) of [Sakurai](@cite Sakurai_1994), p. 194, implementing ```math - 𝒟^{(j)}_{m',m}(\alpha, \beta, \gamma). + 𝒟^{(j)}_{m',m}(α, β, γ). ``` See also [`d`](@ref) for Sakurai's version the Wigner d-function. @@ -81,7 +81,7 @@ end Eqs. (3.5.50)-(3.5.51) of [Sakurai](@cite Sakurai_1994), p. 194 (or Eq. (3.8.33), p. 223), implementing ```math - d^{(j)}_{m',m}(\beta). + d^{(j)}_{m',m}(β). ``` See also [`𝒟`](@ref) for Sakurai's version the Wigner D-function. @@ -125,7 +125,7 @@ end Eqs. (3.6.51) of [Sakurai](@cite Sakurai_1994), p. 203, implementing ```math - Y_{ℓ}^m(\theta, \phi). + Y_{ℓ}^m(θ, ϕ). ``` """ function Y(ℓ, m, θ, ϕ) diff --git a/test/conventions/thorne.jl b/test/conventions/thorne.jl index 47050c98..8250b3b7 100644 --- a/test/conventions/thorne.jl +++ b/test/conventions/thorne.jl @@ -33,7 +33,7 @@ end Eqs. (2.7) of [Thorne](@cite Thorne_1980), implementing ```math - Y^{ℓ,m}(\theta, \phi). + Y^{ℓ,m}(θ, ϕ). ``` """ function Y(ℓ, m, θ, ϕ) diff --git a/test/conventions/torresdelcastillo.jl b/test/conventions/torresdelcastillo.jl index c2635614..f660ba02 100644 --- a/test/conventions/torresdelcastillo.jl +++ b/test/conventions/torresdelcastillo.jl @@ -19,7 +19,7 @@ import .NaiveFactorials: ❗ Eq. (2.52) of [Torres del Castillo](@cite TorresDelCastillo_2003), implementing ```math - D^l_{m',m}(\phi, \theta, \chi). + D^l_{m',m}(ϕ, θ, \chi). ``` """ function D(l, m′, m, ϕ, θ, χ) @@ -47,7 +47,7 @@ end The equation following Eq. (2.53) of [Torres del Castillo](@cite TorresDelCastillo_2003), implementing ```math - {}_sY_{j,m}(\theta, \phi). + {}_sY_{j,m}(θ, ϕ). ``` """ function Y(s, j, m, θ, ϕ) @@ -75,7 +75,7 @@ end Second equation below Eq. (2.53) of [Torres del Castillo](@cite TorresDelCastillo_2003), implementing ```math - d^l_{m',m}(\theta). + d^l_{m',m}(θ). ``` """ function d(l, m′, m, θ) diff --git a/test/conventions/varshalovich.jl b/test/conventions/varshalovich.jl index fca806cc..5d38e576 100644 --- a/test/conventions/varshalovich.jl +++ b/test/conventions/varshalovich.jl @@ -40,7 +40,7 @@ import .NaiveFactorials: ❗ Eq. 4.3(1) of [Varshalovich](@cite Varshalovich_1988), implementing ```math - D^{J}_{M,M'}(\alpha, \beta, \gamma). + D^{J}_{M,M'}(α, β, γ). ``` See also [`d`](@ref) for Varshalovich's version the Wigner d-function. @@ -55,7 +55,7 @@ end Eqs. 4.3.1(2) of [Varshalovich](@cite Varshalovich_1988), implementing ```math - d^{J}_{M,M'}(\beta). + d^{J}_{M,M'}(β). ``` See also [`D`](@ref) for Varshalovich's version the Wigner D-function. diff --git a/test/conventions/wigner.jl b/test/conventions/wigner.jl index f94de7d9..363a9faa 100644 --- a/test/conventions/wigner.jl +++ b/test/conventions/wigner.jl @@ -49,7 +49,7 @@ conjugation of the D function, which is consistent with our convention. # Eq. (15.8) of [Wigner](@cite Wigner_1959), implementing # ```math -# D^ℓ_{m',m}(\alpha, \beta, \gamma). +# D^ℓ_{m',m}(α, β, γ). # ``` # """ # function D(ℓ, m′, m, α, β, γ) @@ -62,7 +62,7 @@ conjugation of the D function, which is consistent with our convention. Eq. (15.27) of [Wigner](@cite Wigner_1959), implementing ```math - D^{(j)}(\alpha, \beta, \gamma)_{\mu',\mu}. + D^{(j)}(α, β, γ)_{\mu',\mu}. ``` """ function D(j, μ′, μ, α, β, γ) diff --git a/test/deprecated/wigner_matrices/H.jl b/test/deprecated/wigner_matrices/H.jl index 91f2fd18..2e95e70d 100644 --- a/test/deprecated/wigner_matrices/H.jl +++ b/test/deprecated/wigner_matrices/H.jl @@ -58,7 +58,7 @@ end import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the explicit Wigner d - # d_{ℓ}^{n,m} = \epsilon_n \epsilon_{-m} H_{ℓ}^{n,m}, + # d_{ℓ}^{n,m} = ϵ_n ϵ_{-m} H_{ℓ}^{n,m}, for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:2 # 2 is the max explicitly coded ℓ @@ -111,7 +111,7 @@ end import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the formulaic Wigner d - # d_{ℓ}^{n,m} = \epsilon_n \epsilon_{-m} H_{ℓ}^{n,m}, + # d_{ℓ}^{n,m} = ϵ_n ϵ_{-m} H_{ℓ}^{n,m}, tol = ifelse(T ∈ (BigFloat, Float32), 100, 1) * 30eps(T) for β in βrange(T) expiβ = cis(β) From 56d2964ce48561b29a06e392f1a6b7ce665e7ec6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 8 Jan 2026 16:19:11 -0500 Subject: [PATCH 324/329] Use standard range of extended Euler angles --- .../calculations/metrics_and_integration.jl | 21 ++++--- docs/src/conventions/details.md | 62 +++++++++++-------- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/docs/literate_input/conventions/calculations/metrics_and_integration.jl b/docs/literate_input/conventions/calculations/metrics_and_integration.jl index 8ba5170a..d8b5f175 100644 --- a/docs/literate_input/conventions/calculations/metrics_and_integration.jl +++ b/docs/literate_input/conventions/calculations/metrics_and_integration.jl @@ -177,33 +177,34 @@ metric_extended_euler = sympy.simplify((jacobian_4.T * metric_cartesian_4 * jaco # metric: four_volume_form_factor = sympy.simplify(sympy.sqrt(metric_extended_euler.det())) -# Again, SymPy correctly includes the absolute value of the ``\sin β`` factor, but in this -# case, it can actually be negative, since ``β`` is in ``[0, 2π]``, so it is correct for -# integration over ``\mathrm{Spin}(3)`` to include the absolute value here. +# Again, SymPy correctly includes the absolute value of the ``\sin β`` factor. However, +# with our chosen Euler-angle ranges for ``\mathrm{Spin}(3)`` we take ``β ∈ [0, π]``, so +# ``\sin β ≥ 0`` and we can drop the absolute value for integration. # # Restricting to the unit sphere (``R=1``), we integrate naively to find the surface area: S3_surface_area = sympy.integrate( sympy.integrate( sympy.integrate( - four_volume_form_factor.subs(R, 1), - (γ, 0, 2π), + four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1), + (γ, 0, 4π), ), - (β, 0, 2π), + (β, 0, π), ), (α, 0, 2π) ) # Therefore, the normalized volume-form factor on the unit sphere 𝕊³ is S3_normalized_volume_form_factor = sympy.simplify( - four_volume_form_factor.subs(R, 1) / S3_surface_area + four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1) + / S3_surface_area ) -# And finally, we can restrict back to ``\mathrm{SO}(3)`` by taking ``β ∈ [0, π]``, and -# integrating over that range: +# And finally, we can restrict back to ``\mathrm{SO}(3)`` by taking ``γ ∈ [0, 2π]`` (while +# keeping ``α ∈ [0, 2π]`` and ``β ∈ [0, π]``), and integrating over that range: SO3_volume = sympy.integrate( sympy.integrate( sympy.integrate( - four_volume_form_factor.subs(R, 1), + four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1), (γ, 0, 2π), ), (β, 0, π), diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 6ab9ca72..34f87ed7 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -113,11 +113,11 @@ the determinant of the metric, so we have ```math \int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin θ\, dr\, dθ\, dϕ. ``` -Restricting to the unit sphere, and normalizing so that the integral -of 1 over the sphere is 1, we can simplify this to +Restricting to the unit sphere, we obtain the usual surface element ```math -\int_{𝕊²} f\, d^2\Omega = \frac{1}{4\pi} \int_0^\pi \int_0^{2\pi} f\, \sin θ\, dθ\, dϕ. +\int_{𝕊²} f\, d^2\Omega = \int_0^\pi \int_0^{2\pi} f\, \sin θ\, dθ\, dϕ. ``` +Note that ``\int_{𝕊²} d^2\Omega = 4\pi``. ## Four-dimensional space: Quaternions and rotations @@ -279,24 +279,34 @@ the relation: \begin{aligned} R &= \sqrt{W^2 + X^2 + Y^2 + Z^2} &&\in [0, \infty), \\ α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ -β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ -γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, \pi], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 4\pi), \end{aligned} ``` -where we again assume the ``\arctan`` in the expressions for -``α`` and ``γ`` is really the two-argument form that gives -the correct quadrant. Note that here, ``β`` ranges up to ``2\pi`` -rather than just ``\pi``, as in the standard Euler angles. This is -because we are describing the space of quaternions, rather than just -the space of rotations. If we restrict to ``R=1``, we have exactly -the group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, -which is a double cover of the rotation group ``\mathrm{SO}(3)``. -This extended range for ``β`` is necessary to cover the entire -space of quaternions; if we further restrict to ``[0, \pi)``, we would -only cover the space of rotations. This and the inclusion of ``R`` +where we again assume the ``\arctan`` in the expressions for ``α`` and +``γ`` is really the two-argument form that gives the correct quadrant, +and if relevant, we use `mod` to limit the values on output. Note +that here, ``γ`` ranges up to ``4\pi`` rather than just ``2\pi``, as +in the standard Euler angles. This is because we are describing the +space of quaternions, rather than just the space of rotations. If we +restrict to quaternions with magnitude ``R=1``, we have exactly the +group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, which +is a double cover of the rotation group ``\mathrm{SO}(3)``. This +extended range for ``γ`` is necessary to cover the entire space of +quaternions; if we further restrict to ``[0, 2\pi)``, we would only +cover the space of rotations. This and the inclusion of ``R`` identify precisely how this coordinate system extends the standard Euler angles. +Note that it would also be reasonable to limit ``γ`` to ``2\pi``, +while allowing ``β`` to range up to ``2\pi`` to cover the entire space +of quaternions. This is just somewhat more delicate to compute, and +is simply not conventional. Also, using ``γ ∈ [0,4\pi)`` integrates +nicely with our [framework of a telescope](@ref Domain) with ``γ`` +representing the rotation about its line of sight; a full ``4\pi`` +rotation is required for the polarizer to explore the full range of +states of half-integer spin fields. + The inverse transformation is given by ```math \begin{aligned} @@ -350,27 +360,25 @@ unit basis vectors in quaternion coordinates are ``` Again, integration involves a square-root of the determinant of the -metric, which reduces to ``R^3 |\sin β| / 8``. Note that — unlike -with standard spherical coordinates — the absolute value is necessary -because ``β`` ranges over the entire interval ``[0, 2\pi]``. The -integral over the entire space of quaternions is then +metric, which reduces to ``R^3 \sin β / 8``. The integral over the +entire space of quaternions is then ```math \int_{\mathbb{R}^4} f\, d^4𝐐 = \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty f\, dW\, dX\, dY\, dZ -= \int_0^\infty \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, \frac{R^3}{8} |\sin β|\, dR\, dα\, dβ\, dγ. += \int_0^\infty \int_0^{2\pi} \int_0^{\pi} \int_0^{4\pi} f\, \frac{R^3}{8} \sin β\, dR\, dα\, dβ\, dγ. ``` -Restricting to the unit sphere, and normalizing so that the integral -of 1 over the sphere is 1, we can simplify this to +Restricting to the unit sphere, we can simplify this to ```math \int_{\mathrm{Spin}(3)} f\, d^3\Omega -= \frac{1}{16\pi^2} \int_0^{2\pi} \int_0^{2\pi} \int_0^{2\pi} f\, |\sin β|\, dα\, dβ\, dγ. += \int_0^{2\pi} \int_0^{\pi} \int_0^{4\pi} f\, \sin β\, dα\, dβ\, dγ, ``` -Finally, restricting to the space of rotations, we can further -simplify this to +where ``\int_{\mathrm{Spin}(3)} d^3\Omega = 16\pi^2``. Finally, +restricting to the space of rotations, we can further simplify this to ```math \int_{\mathrm{SO}(3)} f\, d^3\Omega -= \frac{1}{8\pi^2} \int_0^{2\pi} \int_0^{\pi} \int_0^{2\pi} f\, \sin β\, dα\, dβ\, dγ. += \int_0^{2\pi} \int_0^{\pi} \int_0^{2\pi} f\, \sin β\, dα\, dβ\, dγ, ``` +where ``\int_{\mathrm{SO}(3)} d^3\Omega = 8\pi^2``. ## Rotations From d17f1a75771c4d1fca8fe51413fcb9105e471f67 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 8 Jan 2026 16:19:22 -0500 Subject: [PATCH 325/329] Update more background info --- docs/make.jl | 4 +- docs/src/background/domain.md | 6 +- docs/src/background/mode_weights.md | 103 +++++++++++++++ docs/src/background/operators.md | 178 ++++++++------------------ docs/src/background/sYlm.md | 9 -- docs/src/background/sYlm_and_Dlmpm.md | 134 +++++++++++++++++++ 6 files changed, 298 insertions(+), 136 deletions(-) create mode 100644 docs/src/background/mode_weights.md delete mode 100644 docs/src/background/sYlm.md create mode 100644 docs/src/background/sYlm_and_Dlmpm.md diff --git a/docs/make.jl b/docs/make.jl index 718c627f..4ced5ff5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -52,8 +52,8 @@ makedocs( "Background" => [ "background/domain.md", "background/operators.md", - "background/wigner_matrices.md", - "background/sYlm.md", + "background/sYlm_and_Dlmpm.md", + "background/mode_weights.md", "background/transformations.md", ], "Interface" => [ diff --git a/docs/src/background/domain.md b/docs/src/background/domain.md index 0a4fbd41..a0c51a3e 100644 --- a/docs/src/background/domain.md +++ b/docs/src/background/domain.md @@ -121,9 +121,9 @@ So the conclusion is clear: we should represent the arguments of all these functions as unit quaternions, which is what this package does: ```math \begin{gathered} -Y_{ℓ,m}(Q), \\ -{}_sY_{ℓ,m}(Q), \\ -𝔇^{(ℓ)}_{m', m}(Q). +Y_{ℓ,m}(𝐐), \\ +{}_sY_{ℓ,m}(𝐐), \\ +𝔇^{(ℓ)}_{m', m}(𝐐). \end{gathered} ``` We can still provide convenience methods that convert from spherical diff --git a/docs/src/background/mode_weights.md b/docs/src/background/mode_weights.md new file mode 100644 index 00000000..b128ca8b --- /dev/null +++ b/docs/src/background/mode_weights.md @@ -0,0 +1,103 @@ +# Mode weights + +On the [previous page](@ref sYlm_and_Dlmpm), we introduced the +eigenfunctions of the differential operators defined on +``\mathrm{Spin}(3)``, the spin-weighted spherical harmonics (SWSHs) +``{}_{s}Y_{ℓ,m}(R)`` — or equivalently Wigner's 𝔇 matrices. + +These eigenfunctions form a complete orthonormal basis for the space +of square-integrable functions defined on ``\mathrm{Spin}(3)``. Thus, +any such function ``f(R)`` with spin weight ``s`` can be expressed as +a linear combination of these harmonics: +```math +f(R) = \sum_{ℓ=0}^{∞} \sum_{m=-ℓ}^{ℓ} f_{ℓ,m}\, {}_{s}Y_{ℓ,m}(R), +``` +where the coefficients ``f_{ℓ,m}`` are called the *mode weights* of +the function ``f``. These mode weights can be computed from the function +using the orthonormality of the SWSHs: +```math +f_{ℓ,m} = \int f(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR, +``` +where the integral is taken over ``\mathrm{Spin}(3)`` with the +appropriate measure to ensure orthonormality. In the special case +``s=0``, + +Now that we have introduced the spin-weighted spherical harmonics +(SWSHs) as eigenfunctions of the relevant differential operators, we +can define mode weights of a general spin-weighted function in terms of +these harmonics. + + +## Differential operators + +Mode weights and functions transform in related but different ways +under the action of the differential operators. + +One important point to note is that mode weights transform +"contravariantly" (very loosely speaking) relative to the +spin-weighted spherical functions under some operators. For example, +take the action of the ``L_+`` operator, which acts on a SWSH as +```math +L_+ \left\{{}_{s}Y_{ℓ,m}\right\} (R) += \sqrt{(ℓ-m)(ℓ+m+1)}\ {}_{s}Y_{ℓ,m+1}(R). +``` +We can use this to derive mode weights of a general spin-weighted +function ``f`` under the action of this operator:[^2] +```math +\begin{aligned} +\left\{L_+ f\right\}_{ℓ,m} +&= +\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{L_+ \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \sum_{ℓ',m'} f_{ℓ',m'}\, \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\sqrt{(ℓ'-m')(ℓ'+m'+1)} {}_{s}Y_{ℓ',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} δ_{ℓ,ℓ'} δ_{m,m'+1} \\ +&= +f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} +\end{aligned} +``` +Note that this expression (and in particular its signs) more resembles +the expression for ``L_- \left\{{}_{s}Y_{ℓ,m}\right\}`` than for +``L_+ \left\{{}_{s}Y_{ℓ,m}\right\}``. Similar relations hold for +the action of ``L_-``. + +[^2]: + A technical note about the integrals above: the integrals should be taken + over the appropriate space and with the appropriate weight such that the + SWSHs are orthonormal. In general, this integral should be over + ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be + either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when + integrated over an ``𝕊²`` subspace (nor even is the integral invariant). + However, if we know that the spins are the same in both cases, it *is* + possible to integrate over an ``𝕊²`` subspace. + +However, it is important to note that the same "contravariance" is not +present for the spin-raising and -lowering operators: +```math +\begin{aligned} +\left\{\eth f\right\}_{ℓ,m} +&= +\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{\eth \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\eth {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} δ_{ℓ,ℓ'} δ_{m,m'} \\ +&= +f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} +\end{aligned} +``` +Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more +"covariant" form of transformation. + diff --git a/docs/src/background/operators.md b/docs/src/background/operators.md index e5a5dbf0..41b6fe16 100644 --- a/docs/src/background/operators.md +++ b/docs/src/background/operators.md @@ -7,22 +7,28 @@ unit quaternions, ``\mathrm{Spin}(3) \cong \mathrm{SU}(2)``. As a result, we can define a variety of differential operators acting on these functions, relating to infinitesimal motions in this group, acting either from the left or the right on their arguments. Right or -left matters because this group is non-commutative. +left matters because this group is non-commutative. The left +derivative operator we define here turns out to be exactly the +angular-momentum operator familiar from physics. In the coming pages, +we will use these operators to define the spin-weighted spherical +harmonics and Wigner's 𝔇 matrices as eigenfunctions. -In general, the *left* Lie derivative of a function ``f(Q)`` over the -unit quaternions with respect to a generator of rotation ``g`` is +## Definitions and properties + +In general, the *left* Lie derivative of a function ``f(𝐐)`` over the +unit quaternions with respect to a generator of rotation ``𝐠`` is defined as ```math -L_g(f)\{Q\} := -\frac{i}{2} - \left. \frac{df\left(\exp(t\,g)\, Q\right)}{dt} \right|_{t=0}. +L_𝐠(f)\{𝐐\} := -\frac{i}{2} + \left. \frac{df\left(e^{ϵ\,𝐠}\, 𝐐\right)}{d ϵ} \right|_{ϵ=0}. ``` -Note that the exponential multiplies ``Q`` *on the left* — hence the +Note that the exponential multiplies ``𝐐`` *on the left* — hence the name. We will see below that this agrees with the usual definition of the angular-momentum from physics, except that in *quantum* physics a factor of ``\hbar`` is usually included. -So, for example, a rotation about the ``z`` axis has the quaternion -``z`` as its generator of rotation, and ``L_z`` defined in this way +So, for example, a rotation about the ``𝐳`` axis has the quaternion +``𝐤`` as its generator of rotation, and ``L_𝐤`` defined in this way agrees with [the usual angular-momentum operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) ``L_z`` familiar from spherical-harmonic theory, and reduces to it @@ -34,13 +40,13 @@ operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) ``L_+`` and ``L_-``. In just the same way, we can define the *right* Lie derivative of a -function ``f(Q)`` over the unit quaternions with respect to a -generator of rotation ``g`` as +function ``f(𝐐)`` over the unit quaternions with respect to a +generator of rotation ``𝐠`` as ```math -R_g(f)\{Q\} := -\frac{i}{2} - \left. \frac{df\left(Q\, \exp(t\,g)\right)}{dt} \right|_{t=0}. +R_𝐠(f)\{𝐐\} := -\frac{i}{2} + \left. \frac{df\left(𝐐\, e^{-ϵ\,𝐠}\right)}{d ϵ} \right|_{ϵ=0}. ``` -Note that the exponential multiplies ``Q`` *on the right* — hence the +Note that the exponential multiplies ``𝐐`` *on the right* — hence the name. This operator is less common in physics, because it represents the @@ -87,25 +93,40 @@ R_{𝐚} = \sum_{j} a_j R_j. ``` -## Commutators +## Commutators and angular momentum -In general, for generators ``a`` and ``b``, we have the commutator +In general, for generators ``𝐚`` and ``𝐛``, we have the commutator relations ```math -\left[ L_a, L_b \right] = \frac{i}{2} L_{[a,b]} +\left[ L_𝐚, L_𝐛 \right] = \frac{i}{2} L_{[𝐚,𝐛]} \qquad -\left[ R_a, R_b \right] = -\frac{i}{2} R_{[a,b]}, +\left[ R_𝐚, R_𝐛 \right] = \frac{i}{2} R_{[𝐚,𝐛]}, ``` -where ``[a,b]`` is the commutator of the two generators, which can be -obtained directly as the commutator of the corresponding quaternions. -Note the sign difference between these two equations. The factors of -``\pm i/2`` are inherited directly from the definitions of ``L_g`` and -``R_g`` given above, but they appear there with the *same* sign. The -sign difference between these two commutator equations results from -the fact that the quaternions are multiplied in opposite orders in the -two cases. It *could* be absorbed by defining the operators with -opposite signs.[^1] The arbitrary sign choices used above are purely -for historical reasons. +where ``[𝐚,𝐛]`` is the commutator of the two generators, which can +be obtained directly as the commutator of the corresponding +quaternions. Note that these two equations have the same signs. The +factors of ``i/2`` are inherited directly from the definitions of +``L_𝐠`` and ``R_𝐠`` given above. Note the subtle sign difference in +the exponents in those definitions. The fact that these two +commutator relations have the same sign results from the fact that the +quaternions are multiplied in opposite orders in the two cases. There +are overall arbitrary sign choices; we choose these purely for +conventional reasons, to reproduce the standard ``L`` operator and to +produce similar commutators for ``R``.[^1] + +[^1]: + In fact, we can define the left and right Lie derivative operators + quite generally, for functions on *any* Lie group and for the + corresponding Lie algebra. And in all cases (at least for + finite-dimensional Lie algebras) we obtain the same commutator + relations. The only potential difference is that it may not make + sense to use the coefficient ``i/2`` in general; it was chosen + here for consistency with the standard angular-momentum operators. + If that coefficient is changed in the definitions of the Lie + derivatives, the only change to the commutator relations would the + substitution of that coefficient. The presence of an ``i`` is + important to ensure that the operators are Hermitian when acting on + appropriate function spaces. Again, these results are valid for general (finite-dimensional) Lie groups, but a particularly interesting case is in application to the @@ -121,43 +142,29 @@ mechanics: Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we represent an arbitrary basis vector as ``\hat{e}_j``, then the -quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` +quaternion commutator ``[𝐚,𝐛]`` in the expression for ``[L_𝐚, L_𝐛]`` becomes ```math [\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} ε_{jkl} \hat{e}_l. ``` -Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} -L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version -frequently seen in quantum physics. - - -[^1]: - In fact, we can define the left and right Lie derivative operators - quite generally, for functions on *any* Lie group and for the - corresponding Lie algebra. And in all cases (at least for - finite-dimensional Lie algebras) we obtain the same commutator - relations. The only potential difference is that it may not make - sense to use the coefficient ``i/2`` in general; it was chosen - here for consistency with the standard angular-momentum operators. - If that coefficient is changed in the definitions of the Lie - derivatives, the only change to the commutator relations would the - substitution of that coefficient. +Plugging this into the general expression ``[L_𝐚, L_𝐛] = \frac{i}{2} +L_{[𝐚,𝐛]}``, we obtain (except for that factor of ``\hbar``) the +version used in quantum physics. The raising and lowering operators relative to ``L_z`` and ``R_z`` -satisfy — by definition of raising and lowering operators — the +satisfy — *by definition of raising and lowering operators* — the relations ```math [L_z, L_\pm] = \pm L_\pm \qquad [R_z, R_\pm] = \pm R_\pm. ``` -These allow us to solve — up to an overall factor — for those -operators in terms of the basic generators (again, noting the sign -difference): +These allow us to solve, up to an overall factor, for those operators +in terms of the basic generators: ```math L_\pm = L_x \pm i L_y \qquad -R_\pm = R_x \mp i R_y. +R_\pm = R_x \pm i R_y. ``` (Interestingly, this procedure also shows that rasing and lowering operators can only exist if the factor in front of the derivatives in @@ -168,81 +175,8 @@ particular, this results in the commutator relations \qquad [R_+, R_-] = 2R_z. ``` -Here, the signs are *similar* because the two sign differences noted -above essentially cancel each other out. In the functions [listed below](#Module-functions), these operators are returned as matrices acting on vectors of mode weights. As such, we can actually evaluate these commutators as given to cross-validate the expressions and those functions. - - -## Transformations of functions vs. mode weights - -One important point to note is that mode weights transform -"contravariantly" (very loosely speaking) relative to the -spin-weighted spherical functions under some operators. For example, -take the action of the ``L_+`` operator, which acts on a SWSH as -```math -L_+ \left\{{}_{s}Y_{ℓ,m}\right\} (R) = \sqrt{(ℓ-m)(ℓ+m+1)} {}_{s}Y_{ℓ,m+1}(R). -``` -We can use this to derive the mode weights of a general spin-weighted -function ``f`` under the action of this operator:[^2] -```math -\begin{aligned} -\left\{L_+ f\right\}_{ℓ,m} -&= -\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\int \left\{L_+ \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\int \sum_{ℓ',m'} f_{ℓ',m'}\, \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\sqrt{(ℓ'-m')(ℓ'+m'+1)} {}_{s}Y_{ℓ',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} δ_{ℓ,ℓ'} δ_{m,m'+1} \\ -&= -f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} -\end{aligned} -``` -Note that this expression (and in particular its signs) more resembles -the expression for ``L_- \left\{{}_{s}Y_{ℓ,m}\right\}`` than for -``L_+ \left\{{}_{s}Y_{ℓ,m}\right\}``. Similar relations hold for -the action of ``L_-``. - -[^2]: - A technical note about the integrals above: the integrals should be taken - over the appropriate space and with the appropriate weight such that the - SWSHs are orthonormal. In general, this integral should be over - ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be - either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when - integrated over an ``𝕊²`` subspace (nor even is the integral invariant). - However, if we know that the spins are the same in both cases, it *is* - possible to integrate over an ``𝕊²`` subspace. - -However, it is important to note that the same "contravariance" is not -present for the spin-raising and -lowering operators: -```math -\begin{aligned} -\left\{\eth f\right\}_{ℓ,m} -&= -\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\int \left\{\eth \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\eth {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ -&= -\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} δ_{ℓ,ℓ'} δ_{m,m'} \\ -&= -f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} -\end{aligned} -``` -Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more -"covariant" form of transformation. - diff --git a/docs/src/background/sYlm.md b/docs/src/background/sYlm.md deleted file mode 100644 index 8503face..00000000 --- a/docs/src/background/sYlm.md +++ /dev/null @@ -1,9 +0,0 @@ -# [``{}_{s}Y_{ℓ,m}`` functions](@id background_sYlm) - -The [previous page](@ref background_differential_operators) introduced -the left and right Lie derivative operators acting on functions of a -quaternion argument. The familiar way of arriving at the standard -(scalar) spherical harmonics is to consider functions that are -eigenfunctions of the *left* Lie derivative operators. - - diff --git a/docs/src/background/sYlm_and_Dlmpm.md b/docs/src/background/sYlm_and_Dlmpm.md new file mode 100644 index 00000000..8efa588e --- /dev/null +++ b/docs/src/background/sYlm_and_Dlmpm.md @@ -0,0 +1,134 @@ +# [``{}_{s}Y_{ℓ,m}`` and ``𝔇^{(ℓ)}_{m', m}``](@id sYlm_and_Dlmpm) + +The [previous page](@ref background_differential_operators) introduced +the left and right Lie derivative operators acting on functions of a +quaternion argument. Here, we show how the spin-weighted spherical +harmonics arise naturally as eigenfunctions of these operators. + +The familiar way of arriving at the standard (scalar, ``s=0``) +spherical harmonics is to consider solutions to the Laplace equation +in three-dimensional space, then to separate variables in spherical +coordinates, and finally to identify the angular part of the solution +as the spherical harmonics. Since scalar spherical harmonics really +are just functions on the sphere ``𝕊²``, rather than the full group +``\mathrm{Spin}(3)``, they can be sensibly written as functions of +spherical coordinates ``(θ, ϕ)`` alone. We'll suspend for a moment +what, exactly, the angular-momentum operators mean for functions on +the sphere, and just accept the usual definition of ``L`` as the +relevant operator in this case. Recall that we can only +simultaneously diagonalize operators (find nontrivial functions that +are eigenfunctions of both operators at the same time) if they +commute. In this case, the only two commuting operators are ``L^2 = +L_x^2 + L_y^2 + L_z^2`` and any given component of ``L`` — +conventionally chosen to be ``L_z``. Specifically, the spherical +harmonics are defined to satisfy +```math +\begin{aligned} +L² \left\{ Y_{ℓ,m} \right\}(θ, ϕ) +&= ℓ(ℓ+1) \left\{ Y_{ℓ,m} \right\}(θ, ϕ), +\\ +L_z \left\{ Y_{ℓ,m} \right\}(θ, ϕ) +&= m \left\{ Y_{ℓ,m} \right\}(θ, ϕ). +\end{aligned} +``` +(As usual, we do not include the factors of ``\hbar`` that would +normally be included in quantum mechanics texts.) + +But now, we consider functions defined on the full group +``\mathrm{Spin}(3)``. In that case, we have both left and right Lie +derivative operators available. As before, we can choose +eigenfunctions of ``L²`` and ``L_z``. It turns out that ``[L_g, R_h] += 0``, which means that we could also simultaneously diagonalize with +respect to any given component of ``R`` — which again is +conventionally chosen to be ``R_z``. We might also expect to be able +to diagonalize with respect to ``R²``, but that turns out to equal +``L²``, so while it is true that the spin-weighted spherical harmonics +are also eigenvalues of ``R²``, that statement contains no additional +information. Thus, we select the spin-weighted spherical harmonics as +functions satisfying +```math +\begin{aligned} +L² \left\{ {}_{s}Y_{ℓ,m} \right\}(Q) +&= ℓ(ℓ+1) \left\{ {}_{s}Y_{ℓ,m} \right\}(Q), +\\ +L_z \left\{ {}_{s}Y_{ℓ,m} \right\}(Q) +&= m \left\{ {}_{s}Y_{ℓ,m} \right\}(Q), +\\ +R_z \left\{ {}_{s}Y_{ℓ,m} \right\}(Q) +&= s \left\{ {}_{s}Y_{ℓ,m} \right\}(Q). +\end{aligned} +``` +These conditions only determine the functions up to a normalization +factor, which is conventionally chosen so that the functions are +orthonormal with respect to the natural measure on +``\mathrm{Spin}(3)`` — which we can implement using [extended Euler +angles](@ref Quaternions-and-Euler-angles). This still leaves an +overall complex phase freedom, which is conventionally fixed by +requiring that ``{}_{s}Y_{ℓ,m}(I)``, where ``I`` is the identity +rotor, be real and nonnegative. + +Now, we can return to the idea of functions on ``𝕊²``. We can +identify each point in ``\mathrm{Spin}(3)`` with a point on ``𝕊²`` by +considering the direction that a given rotor takes the ``z`` axis. +This loses information regarding rotation *about* that point. A +function on ``\mathrm{Spin}(3)`` can then be converted into a function +on ``𝕊²`` if and only if the function does not depend on the rotation +about that point, which is described by ``R_z``. Thus, only functions +with ``s=0`` can be considered as functions on ``𝕊²``. In fact, the +scalar spherical harmonics are precisely the spin-weighted spherical +harmonics with ``s=0``. + +If we alter notation slightly and write the spherical harmonics as +functions of a unit vector to a point rather than the spherical +coordinates describing the same point, we can define scalar spherical +harmonics as functions on ``\mathrm{Spin}(3)`` as well: +```math +Y_{ℓ,m}(𝐐) = Y_{ℓ,m}\left(𝐐\, 𝐳\, 𝐐⁻¹\right) +``` +where ``𝐳`` is the unit vector in the ``z`` direction. The +right-hand side is just the usual scalar spherical harmonic evaluated +at the point on ``𝕊²`` corresponding to the rotation of the ``z`` +axis by the rotor ``𝐐``. Now, we can explicitly write +```math +Y_{ℓ,m}(𝐐) = {}_0Y_{ℓ,m}(𝐐); +``` +the scalar spherical harmonics are *precisely* the spin-weighted +spherical harmonics with spin weight ``s=0``. + +Finally, we can consider the Wigner ``𝔇`` matrices. These are +usually defined as +```math +𝔇^{(ℓ)}_{m', m}(α, β, γ) += \big\langle ℓ, m' \big| + e^{-iL_z α} e^{-iL_y β} e^{-iL_z γ} + \big| ℓ, m \big\rangle. +``` +We can rewrite this as +```math +𝔇^{(ℓ)}_{m', m}(𝐐) += \int_{\mathrm{Spin}(3)} + \bar{Y}_{ℓ,m'}(𝐐')\, + Y_{ℓ,m}\left(𝐐⁻¹ 𝐐'\right)\, + d𝐐', +``` +where the integral is taken over ``\mathrm{Spin}(3)`` with the +appropriate measure to ensure orthonormality. We can evaluate the +action of the differential operators on this function pretty easily, +noting that the derivative ``d/dϵ`` can pass through the integral sign +by the Leibniz integral rule. The result is that ``𝔇`` satisfies +```math +\begin{aligned} +L² \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) +&= ℓ(ℓ+1) \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐), +\\ +L_z \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) +&= m' \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐), +\\ +R_z \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) +&= m \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐). +\end{aligned} +``` +That is, Wigner's ``𝔇`` matrices are proportional to the +spin-weighted spherical harmonics. We can get the proportionality +factor from ``𝔇^{(ℓ)}_{m', m}(1) = \delta_{m', m}``. + From e29627c36780e39f1b3c2afd7a6cfda513e8e587 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 8 Jan 2026 21:27:21 -0500 Subject: [PATCH 326/329] Simplify more latex --- .../conventions/comparisons/blanchet_2024.jl | 2 +- .../comparisons/cohen_tannoudji_1991.jl | 4 +- .../comparisons/condon_shortley_1935.jl | 2 +- .../conventions/comparisons/ninja_2011.jl | 14 ++-- docs/src/background/domain.md | 2 +- docs/src/background/mode_weights.md | 2 +- docs/src/conventions/comparisons.md | 40 +++++------ docs/src/conventions/details.md | 70 +++++++++---------- docs/src/conventions/outline.md | 24 +++---- docs/src/conventions/summary.md | 18 ++--- docs/src/interface/sYlm.md | 2 +- docs/src/notes/sampling_theorems.md | 18 ++--- docs/src/operators.md | 2 +- src/deprecated/evaluate.jl | 4 +- src/deprecated/iterators.jl | 2 +- src/ssht/huffenberger_wandelt.jl | 66 ++++++++--------- src/utilities/pixelizations.jl | 10 +-- src/utilities/weights.jl | 6 +- test/conventions/NIST_DLMF.jl | 2 +- 19 files changed, 145 insertions(+), 145 deletions(-) diff --git a/docs/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl index 89596951..24326099 100644 --- a/docs/literate_input/conventions/comparisons/blanchet_2024.jl +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -35,7 +35,7 @@ import ..ConventionsUtilities: 𝒾, ❗ # The ``s=-2`` spin-weighted spherical harmonics are defined in Eq. (184a) as # ```math -# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4\pi}} d^{ℓ m}(θ) e^{imϕ}. +# Y^{l,m}_{-2} = \sqrt{\frac{2l+1}{4π}} d^{ℓ m}(θ) e^{imϕ}. # ``` function Yˡᵐ₋₂(l, m, θ::T, ϕ::T) where {T<:Real} √((2l + 1) / (4T(π))) * d(l, m, θ) * exp(𝒾 * m * ϕ) diff --git a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl index f2c60038..03fdf38e 100644 --- a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl +++ b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl @@ -62,7 +62,7 @@ import ..ConventionsUtilities: 𝒾, ❗, dʲsin²ᵏθdcosθʲ # ```math # Y_{l}^{m}(θ, ϕ) # = -# \frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l+m)!}{(l-m)!}} +# \frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4π} \frac{(l+m)!}{(l-m)!}} # e^{i m ϕ} (\sin θ)^{-m} # \frac{d^{l-m}}{d(\cos θ)^{l-m}} (\sin θ)^{2l}, # ``` @@ -80,7 +80,7 @@ end # ```math # Y_{l}^{m}(θ, ϕ) # = -# \frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4\pi} \frac{(l-m)!}{(l+m)!}} +# \frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4π} \frac{(l-m)!}{(l+m)!}} # e^{i m ϕ} (\sin θ)^m # \frac{d^{l+m}}{d(\cos θ)^{l+m}} (\sin θ)^{2l}. # ``` diff --git a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl index d0e49f98..cae737be 100644 --- a/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl +++ b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl @@ -99,7 +99,7 @@ end # ```math # \Phi(m_ℓ) # = -# \frac{1}{\sqrt{2\pi}} e^{i m_ℓ φ}. +# \frac{1}{\sqrt{2π}} e^{i m_ℓ φ}. # ``` # Again, we make the dependence on ``φ`` explicit, and we capture its type to ensure # that we don't lose precision when converting π to a floating-point number. diff --git a/docs/literate_input/conventions/comparisons/ninja_2011.jl b/docs/literate_input/conventions/comparisons/ninja_2011.jl index 74d43ee5..d6c8f776 100644 --- a/docs/literate_input/conventions/comparisons/ninja_2011.jl +++ b/docs/literate_input/conventions/comparisons/ninja_2011.jl @@ -38,12 +38,12 @@ import ..ConventionsUtilities: 𝒾, ❗ # The spin-weighted spherical harmonics are defined in Eq. (II.7) as # ```math -# {}^{-s}Y_{l,m} = (-1)^s \sqrt{\frac{2l+1}{4\pi}} +# {}^{-s}Y_{l,m} = (-1)^s \sqrt{\frac{2l+1}{4π}} # d^{l}_{m,s}(\iota) e^{imϕ}. # ``` # Just for convenience, we eliminate the negative sign on the left-hand side: # ```math -# {}^{s}Y_{l,m} = (-1)^{-s} \sqrt{\frac{2l+1}{4\pi}} +# {}^{s}Y_{l,m} = (-1)^{-s} \sqrt{\frac{2l+1}{4π}} # d^{l}_{m,-s}(\iota) e^{imϕ}. # ``` function ₛYₗₘ(s, l, m, ι::T, ϕ::T) where {T<:Real} @@ -84,11 +84,11 @@ end # For reference, several explicit formulas are also provided in Eqs. (II.9)--(II.13): # ```math # \begin{aligned} -# {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64\pi}} (1+\cos\iota)^2 e^{2iϕ},\\ -# {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 + \cos\iota) e^{iϕ},\\ -# {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32\pi}} \sin^2\iota,\\ -# {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16\pi}} \sin\iota (1 - \cos\iota) e^{-iϕ},\\ -# {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64\pi}} (1-\cos\iota)^2 e^{-2iϕ}. +# {}^{-2}Y_{2,2} &= \sqrt{\frac{5}{64π}} (1+\cos\iota)^2 e^{2iϕ},\\ +# {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16π}} \sin\iota (1 + \cos\iota) e^{iϕ},\\ +# {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32π}} \sin^2\iota,\\ +# {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16π}} \sin\iota (1 - \cos\iota) e^{-iϕ},\\ +# {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64π}} (1-\cos\iota)^2 e^{-2iϕ}. # \end{aligned} # ``` ₋₂Y₂₂(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 + cos(ι))^2 * exp(2𝒾*ϕ) diff --git a/docs/src/background/domain.md b/docs/src/background/domain.md index a0c51a3e..8a514bc9 100644 --- a/docs/src/background/domain.md +++ b/docs/src/background/domain.md @@ -53,7 +53,7 @@ spherical harmonics [Newman_1966](@cite) are simply proportional to Wigner's 𝔇 matrices, ```math {}_{s}Y_{ℓ,m}(θ, ϕ, ψ) -= (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} \, 𝔇^{(ℓ)}_{m, -s}(ϕ, θ, ψ), += (-1)^s \sqrt{\frac{2ℓ+1}{4π}} \, 𝔇^{(ℓ)}_{m, -s}(ϕ, θ, ψ), ``` so we might as well think of them as being parameterized in the same way. diff --git a/docs/src/background/mode_weights.md b/docs/src/background/mode_weights.md index b128ca8b..49006725 100644 --- a/docs/src/background/mode_weights.md +++ b/docs/src/background/mode_weights.md @@ -73,7 +73,7 @@ the action of ``L_-``. A technical note about the integrals above: the integrals should be taken over the appropriate space and with the appropriate weight such that the SWSHs are orthonormal. In general, this integral should be over - ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be + ``\mathrm{Spin}(3)`` and weighted by ``1/2π`` so that the result will be either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when integrated over an ``𝕊²`` subspace (nor even is the integral invariant). However, if we know that the spins are the same in both cases, it *is* diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md index 15f87498..6d668066 100644 --- a/docs/src/conventions/comparisons.md +++ b/docs/src/conventions/comparisons.md @@ -74,18 +74,18 @@ The upshot is that his definition agrees with ours, though he uses the "active" definition style. That is, the rotations are to be performed successively in order: -> 1. A rotation ``α(0 \leq α < 2\pi)`` about the ``z``-axis, +> 1. A rotation ``α(0 \leq α < 2π)`` about the ``z``-axis, > bringing the frame of axes from the initial position ``S`` into > the position ``S'``. The axis of this rotation is commonly > called the *vertical*. > -> 2. A rotation ``β(0 \leq β < \pi)`` about the ``y``-axis of +> 2. A rotation ``β(0 \leq β < π)`` about the ``y``-axis of > the frame ``S'``, called the *line of nodes*. Note that its > position is in general different from the initial position of the > ``y``-axis of the frame ``S``. The resulting position of the > frame of axes is symbolized by ``S''``. > -> 3. A rotation ``γ(0 \leq γ < 2\pi)`` about the ``z``-axis +> 3. A rotation ``γ(0 \leq γ < 2π)`` about the ``z``-axis > of the frame of axes ``S''``, called the *figure axis*; the > position of this axis depends on the previous rotations > ``α`` and ``β``. The final position of the frame is @@ -190,7 +190,7 @@ Equation (3.11) naturally extends to ```math {}_sY_{ℓ, m}(θ, ϕ, γ) = - \left[ \left(2ℓ+1\right) / 4\pi \right]^{1/2} + \left[ \left(2ℓ+1\right) / 4π \right]^{1/2} D^{ℓ}_{-s,m}(ϕ, θ, γ), ``` where Eq. (3.4) also shows that ``D^{ℓ}_{m', m}(α, β, @@ -231,7 +231,7 @@ Then, (4.32) gives the spherical harmonics as Y_{ℓ}^{m}(θ, ϕ) = ϵ -\sqrt{\frac{2ℓ+1}{4\pi} \frac{(ℓ-|m|)!}{(ℓ+|m|)!}} +\sqrt{\frac{2ℓ+1}{4π} \frac{(ℓ-|m|)!}{(ℓ+|m|)!}} e^{imϕ} P_{ℓ}^{m}(\cos θ), ``` where ``ϵ = (-1)^m`` for ``m\geq 0`` and ``ϵ = 1`` for @@ -239,16 +239,16 @@ where ``ϵ = (-1)^m`` for ``m\geq 0`` and ``ϵ = 1`` for spherical harmonics: ```math \begin{aligned} - Y_{0}^{0} &= \left(\frac{1}{4\pi}\right)^{1/2},\\ - Y_{1}^{0} &= \left(\frac{3}{4\pi}\right)^{1/2} \cos θ,\\ - Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8\pi}\right)^{1/2} \sin θ e^{\pm iϕ},\\ - Y_{2}^{0} &= \left(\frac{5}{16\pi}\right)^{1/2} \left(3\cos^2θ - 1\right),\\ - Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8\pi}\right)^{1/2} \sin θ \cos θ e^{\pm iϕ},\\ - Y_{2}^{\pm 2} &= \left(\frac{15}{32\pi}\right)^{1/2} \sin^2θ e^{\pm 2iϕ},\\ - Y_{3}^{0} &= \left(\frac{7}{16\pi}\right)^{1/2} \left(5\cos^3θ - 3\cos θ\right),\\ - Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64\pi}\right)^{1/2} \sin θ \left(5\cos^2θ - 1\right) e^{\pm iϕ},\\ - Y_{3}^{\pm 2} &= \left(\frac{105}{32\pi}\right)^{1/2} \sin^2θ \cos θ e^{\pm 2iϕ},\\ - Y_{3}^{\pm 3} &= \mp \left(\frac{35}{64\pi}\right)^{1/2} \sin^3θ e^{\pm 3iϕ}. + Y_{0}^{0} &= \left(\frac{1}{4π}\right)^{1/2},\\ + Y_{1}^{0} &= \left(\frac{3}{4π}\right)^{1/2} \cos θ,\\ + Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8π}\right)^{1/2} \sin θ e^{\pm iϕ},\\ + Y_{2}^{0} &= \left(\frac{5}{16π}\right)^{1/2} \left(3\cos^2θ - 1\right),\\ + Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8π}\right)^{1/2} \sin θ \cos θ e^{\pm iϕ},\\ + Y_{2}^{\pm 2} &= \left(\frac{15}{32π}\right)^{1/2} \sin^2θ e^{\pm 2iϕ},\\ + Y_{3}^{0} &= \left(\frac{7}{16π}\right)^{1/2} \left(5\cos^3θ - 3\cos θ\right),\\ + Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64π}\right)^{1/2} \sin θ \left(5\cos^2θ - 1\right) e^{\pm iϕ},\\ + Y_{3}^{\pm 2} &= \left(\frac{105}{32π}\right)^{1/2} \sin^2θ \cos θ e^{\pm 2iϕ},\\ + Y_{3}^{\pm 3} &= \mp \left(\frac{35}{64π}\right)^{1/2} \sin^3θ e^{\pm 3iϕ}. \end{aligned} ``` In Eqs. (4.127)—(4.129), he gives the angular-momentum operators in @@ -297,7 +297,7 @@ D-matrices: ```math D^{(ℓ)}_{m, 0}(θ, ϕ) = -\sqrt{\frac{4\pi}{2ℓ+1}} \left[Y_{ℓ}^{m}(θ, ϕ)\right]^\ast. +\sqrt{\frac{4π}{2ℓ+1}} \left[Y_{ℓ}^{m}(θ, ϕ)\right]^\ast. ``` @@ -342,7 +342,7 @@ page](https://reference.wolfram.com/language/ref/WignerD.html). > ϕ]]` -> For ``ℓ \geq 0``, ``Y_ℓ^m = \sqrt{(2ℓ+1)/(4\pi)} \sqrt{(ℓ-m)! / (ℓ+m)!} P_ℓ^m(\cos θ) e^{imϕ}`` where ``P_ℓ^m`` is the associated Legendre function. +> For ``ℓ \geq 0``, ``Y_ℓ^m = \sqrt{(2ℓ+1)/(4π)} \sqrt{(ℓ-m)! / (ℓ+m)!} P_ℓ^m(\cos θ) e^{imϕ}`` where ``P_ℓ^m`` is the associated Legendre function. > The associated Legendre polynomials are defined by ``P_n^m(x) = (-1)^m (1-x^2)^{m/2}(d^m/dx^m)P_n(x)`` where ``P_n(x)`` is the Legendre polynomial. @@ -457,7 +457,7 @@ as Y_{ℓ}^{m}(θ, ϕ) = (-1)^ℓ -\left[ \frac{(2ℓ+1)!}{4\pi} \right]^{1/2} +\left[ \frac{(2ℓ+1)!}{4π} \right]^{1/2} \frac{1}{2^ℓ ℓ!} \left[ \frac{(ℓ+m)!}{(2ℓ)!(ℓ-m)!} \right]^{1/2} e^{i m ϕ} @@ -589,7 +589,7 @@ and the spin-weighted spherical harmonic is related to ``D`` by {}_{s}Y_{j,m}(θ, ϕ) = (-1)^m -\sqrt{\frac{2j+1}{4\pi}} +\sqrt{\frac{2j+1}{4π}} d^j_{-m,s}(θ) e^{i m ϕ}. ``` @@ -855,7 +855,7 @@ Equation (5.180) gives the spherical harmonics as Y_{l, m}(θ, φ) = \frac{(-1)^l}{2^l l!} -\sqrt{\frac{2l+1}{4\pi} \frac{(l+m)!}{(l-m)!}} +\sqrt{\frac{2l+1}{4π} \frac{(l+m)!}{(l-m)!}} e^{imφ} \frac{1}{\sin^m θ} \frac{d^{l-m}}{d(\cos θ)^{l-m}} diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 34f87ed7..8bc5a43b 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -50,8 +50,8 @@ coordinates by ```math \begin{aligned} r &= \sqrt{x^2 + y^2 + z^2} &&\in [0, \infty), \\ -θ &= \arccos\left(\frac{z}{r}\right) &&\in [0, \pi], \\ -ϕ &= \arctan\left(\frac{y}{x}\right) &&\in [0, 2\pi), +θ &= \arccos\left(\frac{z}{r}\right) &&\in [0, π], \\ +ϕ &= \arctan\left(\frac{y}{x}\right) &&\in [0, 2π), \end{aligned} ``` where we assume the ``\arctan`` in the expression for ``ϕ`` is @@ -111,13 +111,13 @@ Integration in Cartesian coordinates is, of course, trivial as In spherical coordinates, the integrand involves the square-root of the determinant of the metric, so we have ```math -\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^\pi \int_0^{2\pi} f\, r^2 \sin θ\, dr\, dθ\, dϕ. +\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^π \int_0^{2π} f\, r^2 \sin θ\, dr\, dθ\, dϕ. ``` Restricting to the unit sphere, we obtain the usual surface element ```math -\int_{𝕊²} f\, d^2\Omega = \int_0^\pi \int_0^{2\pi} f\, \sin θ\, dθ\, dϕ. +\int_{𝕊²} f\, d^2\Omega = \int_0^π \int_0^{2π} f\, \sin θ\, dθ\, dϕ. ``` -Note that ``\int_{𝕊²} d^2\Omega = 4\pi``. +Note that ``\int_{𝕊²} d^2\Omega = 4π``. ## Four-dimensional space: Quaternions and rotations @@ -278,32 +278,32 @@ the relation: ```math \begin{aligned} R &= \sqrt{W^2 + X^2 + Y^2 + Z^2} &&\in [0, \infty), \\ -α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ -β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, \pi], \\ -γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 4\pi), +α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2π), \\ +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, π], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 4π), \end{aligned} ``` where we again assume the ``\arctan`` in the expressions for ``α`` and ``γ`` is really the two-argument form that gives the correct quadrant, and if relevant, we use `mod` to limit the values on output. Note -that here, ``γ`` ranges up to ``4\pi`` rather than just ``2\pi``, as +that here, ``γ`` ranges up to ``4π`` rather than just ``2π``, as in the standard Euler angles. This is because we are describing the space of quaternions, rather than just the space of rotations. If we restrict to quaternions with magnitude ``R=1``, we have exactly the group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, which is a double cover of the rotation group ``\mathrm{SO}(3)``. This extended range for ``γ`` is necessary to cover the entire space of -quaternions; if we further restrict to ``[0, 2\pi)``, we would only +quaternions; if we further restrict to ``[0, 2π)``, we would only cover the space of rotations. This and the inclusion of ``R`` identify precisely how this coordinate system extends the standard Euler angles. -Note that it would also be reasonable to limit ``γ`` to ``2\pi``, -while allowing ``β`` to range up to ``2\pi`` to cover the entire space +Note that it would also be reasonable to limit ``γ`` to ``2π``, +while allowing ``β`` to range up to ``2π`` to cover the entire space of quaternions. This is just somewhat more delicate to compute, and -is simply not conventional. Also, using ``γ ∈ [0,4\pi)`` integrates +is simply not conventional. Also, using ``γ ∈ [0,4π)`` integrates nicely with our [framework of a telescope](@ref Domain) with ``γ`` -representing the rotation about its line of sight; a full ``4\pi`` +representing the rotation about its line of sight; a full ``4π`` rotation is required for the polarizer to explore the full range of states of half-integer spin fields. @@ -365,20 +365,20 @@ entire space of quaternions is then ```math \int_{\mathbb{R}^4} f\, d^4𝐐 = \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty f\, dW\, dX\, dY\, dZ -= \int_0^\infty \int_0^{2\pi} \int_0^{\pi} \int_0^{4\pi} f\, \frac{R^3}{8} \sin β\, dR\, dα\, dβ\, dγ. += \int_0^\infty \int_0^{2π} \int_0^{π} \int_0^{4π} f\, \frac{R^3}{8} \sin β\, dR\, dα\, dβ\, dγ. ``` Restricting to the unit sphere, we can simplify this to ```math \int_{\mathrm{Spin}(3)} f\, d^3\Omega -= \int_0^{2\pi} \int_0^{\pi} \int_0^{4\pi} f\, \sin β\, dα\, dβ\, dγ, += \int_0^{2π} \int_0^{π} \int_0^{4π} f\, \sin β\, dα\, dβ\, dγ, ``` -where ``\int_{\mathrm{Spin}(3)} d^3\Omega = 16\pi^2``. Finally, +where ``\int_{\mathrm{Spin}(3)} d^3\Omega = 16π^2``. Finally, restricting to the space of rotations, we can further simplify this to ```math \int_{\mathrm{SO}(3)} f\, d^3\Omega -= \int_0^{2\pi} \int_0^{\pi} \int_0^{2\pi} f\, \sin β\, dα\, dβ\, dγ, += \int_0^{2π} \int_0^{π} \int_0^{2π} f\, \sin β\, dα\, dβ\, dγ, ``` -where ``\int_{\mathrm{SO}(3)} d^3\Omega = 8\pi^2``. +where ``\int_{\mathrm{SO}(3)} d^3\Omega = 8π^2``. ## Rotations @@ -612,8 +612,8 @@ can see that the rotated field should be represented by f'(θ, ϕ) = \sin θ \sin(ϕ - α). ``` For example, the rotated field evaluated at the point ``(θ, ϕ) -= (\pi/2, 0)`` along the positive ``x`` axis should correspond to the -original field evaluated at the point ``(θ, ϕ) = (\pi/2, += (π/2, 0)`` along the positive ``x`` axis should correspond to the +original field evaluated at the point ``(θ, ϕ) = (π/2, -α)``. This rotation is generated by ``𝔤 = α 𝐤 / 2``, which allows us to immediately calculate ```math @@ -830,9 +830,9 @@ e^{θ 𝐮 / 2} 𝐑_{α, β, γ} ```math \begin{aligned} -α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2\pi), \\ -β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2\pi], \\ -γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2\pi), +α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2π), \\ +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2π], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2π), \end{aligned} ``` @@ -1055,15 +1055,15 @@ equivalent one McEwen and Wiaux use (and credit to Risbo): ```math \exp\left[ β 𝐣 / 2 \right] = -\exp\left[ \pi 𝐤 / 4 \right] -\exp\left[ \pi 𝐣 / 4 \right] +\exp\left[ π 𝐤 / 4 \right] +\exp\left[ π 𝐣 / 4 \right] \exp\left[ β 𝐤 / 2 \right] -\exp\left[ -\pi 𝐣 / 4 \right] -\exp\left[ -\pi 𝐤 / 4 \right] +\exp\left[ -π 𝐣 / 4 \right] +\exp\left[ -π 𝐤 / 4 \right] ``` The 𝔇 matrices corresponding to the ``𝐤`` rotations are simple phases, which converts the problem into one of finding the 𝔇 matrices -for the ``𝐣`` rotations through angles of ``\pm\pi/2`` — which are +for the ``𝐣`` rotations through angles of ``\pm π/2`` — which are presumably simpler to compute. See, e.g., Varshalovich's Eq. 4.16.(5), where they are given by purely combinatorial terms. @@ -1100,7 +1100,7 @@ presumably simpler to compute. See, e.g., Varshalovich's Eq. group is just ``\{1, -1\}``. Presumably, every representation acting on ``1`` will give the identity matrix, so that's trivial. So we just need a criterion for when a representation - is trivial on ``-1``. Noting that ``\exp(\pi \vec{v}) = -1`` + is trivial on ``-1``. Noting that ``\exp(π \vec{v}) = -1`` for any ``\vec{v}``, I think we can show that this requires ``m \in \mathbb{Z}``. - Basically, the point is that the representations of @@ -1123,14 +1123,14 @@ presumably simpler to compute. See, e.g., Varshalovich's Eq. Theorem 2.16 of [Hanson-Yakovlev](@cite HansonYakovlev_2002) says that an orthonormal basis of a product of ``L^2`` spaces is given by the product of the orthonormal bases of the individual spaces. -Furthermore, on page 354, they point out that ``\{(1/\sqrt{2\pi}) -e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the +Furthermore, on page 354, they point out that ``\{(1/\sqrt{2π}) +e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2π)``, while the set ``\{1/c_{n,m} P_n^m(\cos θ)`` is an orthonormal basis of -``L^2(0, \pi)`` in the ``θ`` coordinate. Therefore, the product +``L^2(0, π)`` in the ``θ`` coordinate. Therefore, the product of these two sets is an orthonormal basis of the product space -``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate +``L^2\left((0,2π) \times (0, π)\right)``, which forms a coordinate space for ``𝕊²``. I would probably modify this to point out that -``(0,2\pi)`` is really ``𝕊¹``, and then we could extend it to point +``(0,2π)`` is really ``𝕊¹``, and then we could extend it to point out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, which happens to give us the Wigner D-matrices. diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md index 43d7ad28..22e9be29 100644 --- a/docs/src/conventions/outline.md +++ b/docs/src/conventions/outline.md @@ -54,7 +54,7 @@ group is just ``\{1, -1\}``. Presumably, every representation acting on ``1`` will give the identity matrix, so that's trivial. So we just need a criterion for when a representation - is trivial on ``-1``. Noting that ``\exp(\pi \vec{v}) = -1`` + is trivial on ``-1``. Noting that ``\exp(π \vec{v}) = -1`` for any ``\vec{v}``, I think we can show that this requires ``m \in \mathbb{Z}``. - Basically, the point is that the representations of @@ -85,14 +85,14 @@ discussion; maybe the paper has better references. Theorem 2.16 of [Hanson-Yakovlev](@cite HansonYakovlev_2002) says that an orthonormal basis of a product of ``L^2`` spaces is given by the product of the orthonormal bases of the individual spaces. -Furthermore, on page 354, they point out that ``\{(1/\sqrt{2\pi}) -e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2\pi)``, while the +Furthermore, on page 354, they point out that ``\{(1/\sqrt{2π}) +e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2π)``, while the set ``\{1/c_{n,m} P_n^m(\cos θ)`` is an orthonormal basis of -``L^2(0, \pi)`` in the ``θ`` coordinate. Therefore, the product +``L^2(0, π)`` in the ``θ`` coordinate. Therefore, the product of these two sets is an orthonormal basis of the product space -``L^2\left((0,2\pi) \times (0, \pi)\right)``, which forms a coordinate +``L^2\left((0,2π) \times (0, π)\right)``, which forms a coordinate space for ``𝕊²``. I would probably modify this to point out that -``(0,2\pi)`` is really ``𝕊¹``, and then we could extend it to point +``(0,2π)`` is really ``𝕊¹``, and then we could extend it to point out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, which happens to give us the Wigner D-matrices. @@ -215,7 +215,7 @@ spherical harmonics. The key expression is Eq. (15) of section 4³ \frac{1}{2^ℓ ℓ!} \frac{1}{\sin^mθ} \frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} \sin^{2ℓ}θ. ``` -When multiplied by Eq. (5) ``\Phi(m) = e^{imϕ} / \sqrt{2\pi}``, +When multiplied by Eq. (5) ``\Phi(m) = e^{imϕ} / \sqrt{2π}``, this gives the spherical harmonic function. The right-hand side of the expression above is usually immediately replaced by a simpler expression using Legendre polynomials, but this just shifts sign @@ -371,7 +371,7 @@ L_{\pm} 𝔇^{(ℓ)}_{m',m}(𝐑) while Eq. (21) relates the Wigner D-matrix to the spin-weighted spherical harmonics as ```math {}_{s}Y_{ℓ,m}(𝐑) -= (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} 𝔇^{(ℓ)}_{m,-s}(𝐑). += (-1)^s \sqrt{\frac{2ℓ+1}{4π}} 𝔇^{(ℓ)}_{m,-s}(𝐑). ``` Plugging the latter into the former, we get ```math @@ -394,10 +394,10 @@ convention. ```math \begin{aligned} -d_{m',m}^{j}(\pi) &= (-1)^{j-m} δ_{m',-m} \\[6pt] -d_{m',m}^{j}(\pi-β) &= (-1)^{j+m'} d_{m',-m}^{j}(β)\\[6pt] -d_{m',m}^{j}(\pi+β) &= (-1)^{j-m} d_{m',-m}^{j}(β)\\[6pt] -d_{m',m}^{j}(2\pi+β) &= (-1)^{2j} d_{m',m}^{j}(β)\\[6pt] +d_{m',m}^{j}(π) &= (-1)^{j-m} δ_{m',-m} \\[6pt] +d_{m',m}^{j}(π-β) &= (-1)^{j+m'} d_{m',-m}^{j}(β)\\[6pt] +d_{m',m}^{j}(π+β) &= (-1)^{j-m} d_{m',-m}^{j}(β)\\[6pt] +d_{m',m}^{j}(2π+β) &= (-1)^{2j} d_{m',m}^{j}(β)\\[6pt] d_{m',m}^{j}(-β) &= d_{m,m'}^{j}(β) = (-1)^{m'-m} d_{m',m}^{j}(β) \end{aligned} ``` diff --git a/docs/src/conventions/summary.md b/docs/src/conventions/summary.md index e0e80938..05f120ef 100644 --- a/docs/src/conventions/summary.md +++ b/docs/src/conventions/summary.md @@ -21,12 +21,12 @@ unit basis vectors ``(𝐱, 𝐲, 𝐳)``. ## Spherical coordinates We define spherical coordinates ``(r, θ, ϕ)`` and unit basis vectors ``(𝐧, \boldsymbol{θ}, \boldsymbol{ϕ})``. The "polar -angle" ``θ \in [0, \pi]`` measures the angle between the +angle" ``θ \in [0, π]`` measures the angle between the specified direction and the positive ``𝐳`` axis. The "azimuthal -angle" ``ϕ \in [0, 2\pi)`` measures the angle between the +angle" ``ϕ \in [0, 2π)`` measures the angle between the projection of the specified direction onto the ``𝐱``-``𝐲`` plane and the positive ``𝐱`` axis, with the positive ``𝐲`` axis corresponding -to the positive angle ``ϕ = \pi/2``. +to the positive angle ``ϕ = π/2``. ## Quaternions A quaternion is written ``𝐐 = W + X𝐢 + Y𝐣 + Z𝐤``, where ``𝐢^2 = @@ -78,10 +78,10 @@ Euler angles parametrize a unit quaternion as = \exp(α 𝐤/2)\, \exp(β 𝐣/2)\, \exp(γ 𝐤/2). ``` -The angles ``α`` and ``γ`` take values in ``[0, 2\pi)``. -The angle ``β`` takes values in ``[0, 2\pi]`` to parametrize the +The angles ``α`` and ``γ`` take values in ``[0, 2π)``. +The angle ``β`` takes values in ``[0, 2π]`` to parametrize the group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in -``[0, \pi]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. +``[0, π]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. By comparison, we can immediately see that spherical coordinates ``(θ, ϕ)`` can be represented as Euler angles with the @@ -205,7 +205,7 @@ package defines the spherical harmonics in terms of Wigner's 𝔇 matrices, by way of the spin-weighted spherical harmonics, as a function of a quaternion: ```math -Y_{l,m}(𝐐) = \sqrt{\frac{2ℓ+1}{4\pi}} e^{imϕ} +Y_{l,m}(𝐐) = \sqrt{\frac{2ℓ+1}{4π}} e^{imϕ} D^{(l)}_{m,0}(𝐐), ``` where ``D^{(l)}_{m,0}`` is the Wigner 𝔇 matrix. This is a @@ -219,7 +219,7 @@ terms of spherical coordinates, that expression is \begin{align} Y_{l,m} &= - \sqrt{\frac{2ℓ+1}{4\pi}} e^{imϕ} + \sqrt{\frac{2ℓ+1}{4π}} e^{imϕ} \sum_{k = k_1}^{k_2} \frac{(-1)^k ℓ! [(ℓ+m)!(ℓ-m)!]^{1/2}} {(ℓ+m-k)!(ℓ-k)!k!(k-m)!} @@ -330,7 +330,7 @@ of comparison with other sources. The expression is \begin{align} {}_{s}Y_{l,m} &= - (-1)^s\sqrt{\frac{2ℓ+1}{4\pi}} e^{imϕ} + (-1)^s\sqrt{\frac{2ℓ+1}{4π}} e^{imϕ} \sum_{k = k_1}^{k_2} \frac{(-1)^k[(ℓ+m)!(ℓ-m)!(ℓ-s)!(ℓ+s)!]^{1/2}} {(ℓ+m-k)!(ℓ+s-k)!k!(k-s-m)!} diff --git a/docs/src/interface/sYlm.md b/docs/src/interface/sYlm.md index c1b2a91e..4d4d0b94 100644 --- a/docs/src/interface/sYlm.md +++ b/docs/src/interface/sYlm.md @@ -15,7 +15,7 @@ introduced by [Newman_1966](@citet), they are essentially components of Wigner's ``𝔇`` matrices: ```math {}_{s}Y_{ℓ,m}(𝐑) - = (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} \, 𝔇^{(ℓ)}_{m, -s}(𝐑). + = (-1)^s \sqrt{\frac{2ℓ+1}{4π}} \, 𝔇^{(ℓ)}_{m, -s}(𝐑). ``` As such, they can be computed using the same [``H`` recursion](@ref "Algorithm for computing ``H``") algorithm as the Wigner diff --git a/docs/src/notes/sampling_theorems.md b/docs/src/notes/sampling_theorems.md index aa8d486b..82c7fa40 100644 --- a/docs/src/notes/sampling_theorems.md +++ b/docs/src/notes/sampling_theorems.md @@ -23,7 +23,7 @@ has slowly growing errors through ``L = 4096``. The EKKM analysis looks like the following (with some notational changes). We begin by defining ```math - {}_{s}\tilde{f}_{θ}(m) := \int_0^{2\pi} {}_sf(θ, ϕ)\, e^{-imϕ}\, dϕ. + {}_{s}\tilde{f}_{θ}(m) := \int_0^{2π} {}_sf(θ, ϕ)\, e^{-imϕ}\, dϕ. ``` We will denote the vector of these quantities for all values of ``θ`` as ``{}_{s}\tilde{𝐟}_m``. Inserting the @@ -31,13 +31,13 @@ We will denote the vector of these quantities for all values of performing the integration using orthogonality of complex exponentials, we can find that ```math - {}_{s}\tilde{f}_{θ}(m) = (-1)^s\, 2\pi \sum_{ℓ=\Delta}^L \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{m,-s}^{ℓ}(θ)\, {}_sf_{ℓ,m}. + {}_{s}\tilde{f}_{θ}(m) = (-1)^s\, 2π \sum_{ℓ=\Delta}^L \sqrt{\frac{2ℓ+1}{4π}}\, d_{m,-s}^{ℓ}(θ)\, {}_sf_{ℓ,m}. ``` Now, denoting the vector of ``{}_sf_{ℓ,m}`` for all values of ``ℓ`` as ``{}_s𝐟_m``, we can write this as a matrix-vector equation: ```math - {}_{s}\tilde{𝐟}_m = (-1)^s\, 2\pi\, {}_s𝐝_{m}\, {}_s𝐟_m. + {}_{s}\tilde{𝐟}_m = (-1)^s\, 2π\, {}_s𝐝_{m}\, {}_s𝐟_m. ``` We are effectively measuring the ``{}_{s}\tilde{𝐟}_m`` values, we can easily construct the ``{}_s𝐝_{m}`` matrix, and @@ -58,8 +58,8 @@ like this: ```math {}_{s}\tilde{f}_{j}(m) := \sum_{k=0}^{2j} {}_sf(θ_j, ϕ_k)\, e^{-imϕ_k}\, \Delta ϕ, ``` -where ``ϕ_k = \frac{2\pi k}{2j+1}``, and ``\Delta ϕ = -\frac{2\pi}{2j+1}``. (Recall the subtle notational distinction common +where ``ϕ_k = \frac{2π k}{2j+1}``, and ``\Delta ϕ = +\frac{2π}{2j+1}``. (Recall the subtle notational distinction common in time-frequency analysis that ``\tilde{s}(t_j) = \Delta t \tilde{s}_j``, which would suggest we use ``{}_{s}\tilde{f}_{j}(m) = \Delta ϕ\, {}_{s}\tilde{f}_{j,m}``.) Next, we can insert the @@ -69,8 +69,8 @@ expansion for ``{}_sf(θ, ϕ)``: \begin{aligned} {}_{s}\tilde{f}_{j}(m) &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, {}_sY_{ℓ,m'}(θ_j, ϕ_k)\, e^{-imϕ_k}\, \Delta ϕ \\ - &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, (-1)^{s}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(θ_j) e^{i m' ϕ_k}\, e^{-imϕ_k}\, \frac{2\pi}{2j+1} \\ - &= (-1)^{s}\, \frac{2\pi}{2j+1} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(θ_j) \sum_{k=0}^{2j}e^{i (m'-m) ϕ_k}. + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, (-1)^{s}\, \sqrt{\frac{2ℓ+1}{4π}}\, d_{ℓ}^{m',-s}(θ_j) e^{i m' ϕ_k}\, e^{-imϕ_k}\, \frac{2π}{2j+1} \\ + &= (-1)^{s}\, \frac{2π}{2j+1} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4π}}\, d_{ℓ}^{m',-s}(θ_j) \sum_{k=0}^{2j}e^{i (m'-m) ϕ_k}. \end{aligned} ``` We can evaluate this last sum easily: @@ -84,7 +84,7 @@ This allows us to simplify as ```math \begin{aligned} - {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4\pi}}\, d_{ℓ}^{m',-s}(θ_j), + {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2π \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4π}}\, d_{ℓ}^{m',-s}(θ_j), \end{aligned} ``` where ``m'`` ranges over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq ℓ`` @@ -103,7 +103,7 @@ values of ``ℓ, m'``, and just set the coefficient to zero whenever these conditions are not satisfied. In that case, we can again think of this as a (much larger) vector-matrix equation reading ```math - {}_s\tilde{𝐟} = (-1)^s\, 2\pi\, {}_s𝐝\, {}_s𝐟, + {}_s\tilde{𝐟} = (-1)^s\, 2π\, {}_s𝐝\, {}_s𝐟, ``` where the index on ``{}_s\tilde{𝐟}`` loops over ``j`` and ``m``, the index on ``{}_s𝐟`` loops over ``ℓ`` and ``m'``, diff --git a/docs/src/operators.md b/docs/src/operators.md index ef98403e..32c0dd79 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -217,7 +217,7 @@ the action of ``L_-``. A technical note about the integrals above: the integrals should be taken over the appropriate space and with the appropriate weight such that the SWSHs are orthonormal. In general, this integral should be over - ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be + ``\mathrm{Spin}(3)`` and weighted by ``1/2π`` so that the result will be either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when integrated over an ``𝕊²`` subspace (nor even is the integral invariant). However, if we know that the spins are the same in both cases, it *is* diff --git a/src/deprecated/evaluate.jl b/src/deprecated/evaluate.jl index abdedf48..a8348b06 100644 --- a/src/deprecated/evaluate.jl +++ b/src/deprecated/evaluate.jl @@ -450,8 +450,8 @@ as ```math \begin{aligned} {}_{s}Y_{ℓ, m}(R) - &= (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} 𝔇^{(ℓ)}_{m, -s}(R) \\ - &= (-1)^s \sqrt{\frac{2ℓ+1}{4\pi}} \bar{𝔇}^{(ℓ)}_{-s, m}(\bar{R}). + &= (-1)^s \sqrt{\frac{2ℓ+1}{4π}} 𝔇^{(ℓ)}_{m, -s}(R) \\ + &= (-1)^s \sqrt{\frac{2ℓ+1}{4π}} \bar{𝔇}^{(ℓ)}_{-s, m}(\bar{R}). \end{aligned} ``` diff --git a/src/deprecated/iterators.jl b/src/deprecated/iterators.jl index d8ea11eb..a8f21a91 100644 --- a/src/deprecated/iterators.jl +++ b/src/deprecated/iterators.jl @@ -268,7 +268,7 @@ values with ``ℓ=m``. ```math {}_{s}\lambda_{ℓ,m}(θ) := {}_{s}Y_{ℓ,m}(θ, 0) - = (-1)^m\, \sqrt{\frac{2ℓ+1}{4\pi}} d^ℓ_{-m,s}(θ) + = (-1)^m\, \sqrt{\frac{2ℓ+1}{4π}} d^ℓ_{-m,s}(θ) ``` """ function λ_recursion_initialize(sin½θ::T, cos½θ::T, s, ℓ, m) where T diff --git a/src/ssht/huffenberger_wandelt.jl b/src/ssht/huffenberger_wandelt.jl index adfe5cd8..f3179bbc 100644 --- a/src/ssht/huffenberger_wandelt.jl +++ b/src/ssht/huffenberger_wandelt.jl @@ -11,26 +11,26 @@ terms of ``d`` for the angle ``π/2`` and a phase factor. We start with the fact that the ``y`` axis equals the ``z`` axis rotated by ``π/2`` about the ``x`` axis: ```math -𝐣 = e^{\frac{\pi}{2} 𝐢/ 2}\, 𝐤\, e^{-\frac{\pi}{2} 𝐢/ 2}. +𝐣 = e^{\frac{π}{2} 𝐢/ 2}\, 𝐤\, e^{-\frac{π}{2} 𝐢/ 2}. ``` So we have ```math e^{β 𝐣 / 2} = -e^{\frac{\pi}{2} 𝐢/ 2}\, e^{β 𝐤 / 2}\, e^{-\frac{\pi}{2} 𝐢/ 2}. +e^{\frac{π}{2} 𝐢/ 2}\, e^{β 𝐤 / 2}\, e^{-\frac{π}{2} 𝐢/ 2}. ``` Unfortunately, this expression involves the ``x`` axis, which we don't want. But we can similarly express the ``x`` axis in terms of the ``y`` axis rotated about the ``z`` axis: ```math -𝐢 = e^{-\frac{\pi}{2} 𝐤/ 2}\, 𝐣\, e^{\frac{\pi}{2} 𝐤/ 2}. +𝐢 = e^{-\frac{π}{2} 𝐤/ 2}\, 𝐣\, e^{\frac{π}{2} 𝐤/ 2}. ``` And now we can use this in our first expression to find ```math e^{β 𝐣 / 2} = -e^{-\frac{\pi}{2} 𝐤/ 2}\, e^{\frac{\pi}{2} 𝐣/ 2}\, e^{\frac{\pi}{2} 𝐤/ 2}\, +e^{-\frac{π}{2} 𝐤/ 2}\, e^{\frac{π}{2} 𝐣/ 2}\, e^{\frac{π}{2} 𝐤/ 2}\, e^{β 𝐤 / 2}\, -e^{\frac{\pi}{2} 𝐤/ 2}\, e^{-\frac{\pi}{2} 𝐣/ 2}\, e^{-\frac{\pi}{2} 𝐤/ 2}. +e^{\frac{π}{2} 𝐤/ 2}\, e^{-\frac{π}{2} 𝐣/ 2}\, e^{-\frac{π}{2} 𝐤/ 2}. ``` Now, we can use this expansion to find an expression for the ``d`` matrix value: ```math @@ -39,57 +39,57 @@ d^{ℓ}_{m', m}(β) &= 𝔇^{ℓ}_{m', m}\left(e^{β 𝐣 / 2}\right) \\ &= -𝔇^{ℓ}_{m', m_1}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right)\, -𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -𝔇^{ℓ}_{m_2, m_3}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m', m_1}\left(e^{-\frac{π}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_2, m_3}\left(e^{\frac{π}{2} 𝐤/ 2}\right)\, 𝔇^{ℓ}_{m_3, m_4}\left(e^{β 𝐤 / 2}\right)\, \\ &\quad \times -𝔇^{ℓ}_{m_4, m_5}\left(e^{\frac{\pi}{2} 𝐤/ 2}\right)\, -𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, -𝔇^{ℓ}_{m_6, m}\left(e^{-\frac{\pi}{2} 𝐤/ 2}\right) \\ +𝔇^{ℓ}_{m_4, m_5}\left(e^{\frac{π}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{π}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_6, m}\left(e^{-\frac{π}{2} 𝐤/ 2}\right) \\ &= -δ_{m', m_1} e^{im'\frac{\pi}{2}}\, -𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, -δ_{m_2, m_3} e^{-im_2\frac{\pi}{2}}\, +δ_{m', m_1} e^{im'\frac{π}{2}}\, +𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +δ_{m_2, m_3} e^{-im_2\frac{π}{2}}\, 𝔇^{ℓ}_{m_3, m_4}\left(e^{β 𝐤 / 2}\right)\, \\ &\quad \times -δ_{m_4, m_5} e^{-im_4\frac{\pi}{2}}\, -𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right)\, -δ_{m_6, m} e^{im\frac{\pi}{2}} \\ +δ_{m_4, m_5} e^{-im_4\frac{π}{2}}\, +𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{π}{2} 𝐣/ 2}\right)\, +δ_{m_6, m} e^{im\frac{π}{2}} \\ &= -e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, -e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, +e^{im'\frac{π}{2}}\, e^{-im''\frac{π}{2}}\, +e^{-im'''\frac{π}{2}}\, e^{im\frac{π}{2}}\, &\quad \times -𝔇^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m', m''}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, 𝔇^{ℓ}_{m'', m'''}\left(e^{β 𝐤 / 2}\right)\, \\ -𝔇^{ℓ}_{m''', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +𝔇^{ℓ}_{m''', m}\left(e^{-\frac{π}{2} 𝐣/ 2}\right) \\ &= -e^{im'\frac{\pi}{2}}\, e^{-im''\frac{\pi}{2}}\, -e^{-im'''\frac{\pi}{2}}\, e^{im\frac{\pi}{2}}\, +e^{im'\frac{π}{2}}\, e^{-im''\frac{π}{2}}\, +e^{-im'''\frac{π}{2}}\, e^{im\frac{π}{2}}\, &\quad \times -𝔇^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m', m''}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, e^{-im''β}\, -𝔇^{ℓ}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +𝔇^{ℓ}_{m'', m}\left(e^{-\frac{π}{2} 𝐣/ 2}\right) \\ &= i^{m'+m-2m''}\, -d^{ℓ}_{m', m''}\left(e^{\frac{\pi}{2} 𝐣/ 2}\right)\, +d^{ℓ}_{m', m''}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, e^{-im''β}\, -d^{ℓ}_{m'', m}\left(e^{-\frac{\pi}{2} 𝐣/ 2}\right) \\ +d^{ℓ}_{m'', m}\left(e^{-\frac{π}{2} 𝐣/ 2}\right) \\ &= i^{m'+m}(-1)^{m''}\, -d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, +d^{ℓ}_{m', m''}\left(\frac{π}{2}\right)\, e^{-im''β}\, -d^{ℓ}_{m'', m}\left(-\frac{\pi}{2}\right) \\ +d^{ℓ}_{m'', m}\left(-\frac{π}{2}\right) \\ &= i^{m'+m}(-1)^{m}\, -d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, +d^{ℓ}_{m', m''}\left(\frac{π}{2}\right)\, e^{-im''β}\, -d^{ℓ}_{m'', m}\left(\frac{\pi}{2}\right) \\ +d^{ℓ}_{m'', m}\left(\frac{π}{2}\right) \\ &= i^{m'-m}\, -d^{ℓ}_{m', m''}\left(\frac{\pi}{2}\right)\, +d^{ℓ}_{m', m''}\left(\frac{π}{2}\right)\, e^{-im''β}\, -d^{ℓ}_{m'', m}\left(\frac{\pi}{2}\right) +d^{ℓ}_{m'', m}\left(\frac{π}{2}\right) \end{align} ``` diff --git a/src/utilities/pixelizations.jl b/src/utilities/pixelizations.jl index 89866f1a..a78f30df 100644 --- a/src/utilities/pixelizations.jl +++ b/src/utilities/pixelizations.jl @@ -156,8 +156,8 @@ Cover the sphere 𝕊² with pixels given by the [DriscollHealy_1994](@citet) eq > Let ``f(θ, ϕ)`` be a band-limited function such that ``\hat{f}(l, m) = 0`` for ``l > ≥ b``. We sample the function at the equiangular grid of points ``(θ_i, ϕ_j)``, -> ``i = 0, \ldots, 2b-1``, ``j = 0, \ldots, 2b-1``, where ``θ_i = \pi i/2b`` and -> ``ϕ_j = \pi j/b``. +> ``i = 0, \ldots, 2b-1``, ``j = 0, \ldots, 2b-1``, where ``θ_i = π i/2b`` and +> ``ϕ_j = π j/b``. The returned quantity is a vector of 2-SVectors providing the spherical coordinates of each pixel. See also [`driscoll_healy_rotors`](@ref) for the corresponding `Rotor`s. @@ -202,9 +202,9 @@ driscoll_healy_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_ Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid: > We adopt an equiangular sampling of the sphere with sample positions given by ``θ_t = -> \frac{\pi(2t+1)}{2ℓ_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, ℓ_\mathrm{max}-1\}`` -> and ``ϕ_p = \frac{2 \pi p}{2ℓ_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, -> 2ℓ_\mathrm{max}-2\}``. In order to extend the ``θ`` domain to ``[0, 2\pi)`` we +> \frac{π(2t+1)}{2ℓ_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, ℓ_\mathrm{max}-1\}`` +> and ``ϕ_p = \frac{2 π p}{2ℓ_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, +> 2ℓ_\mathrm{max}-2\}``. In order to extend the ``θ`` domain to ``[0, 2π)`` we > simply extend the domain of the ``θ`` index to include ``\{ℓ_\mathrm{max}, > ℓ_\mathrm{max}+1, \dotsc, 2ℓ_\mathrm{max}-1\}``. diff --git a/src/utilities/weights.jl b/src/utilities/weights.jl index d0010078..3abcb6b7 100644 --- a/src/utilities/weights.jl +++ b/src/utilities/weights.jl @@ -4,7 +4,7 @@ Compute `n` weights for Fejér's first rule, corresponding to `n` evenly spaced nodes from 0 to π inclusive. That is, the nodes are located at ```math -θ_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. +θ_k = k \frac{π}{n-1} \quad k=0, \ldots, n-1. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). @@ -39,7 +39,7 @@ end Compute `n` weights for Fejér's second rule, corresponding to `n` evenly spaced nodes between 0 and π exclusive. That is, the nodes are located at ```math -θ_k = k \frac{\pi}{n+1} \quad k=1, \ldots, n. +θ_k = k \frac{π}{n+1} \quad k=1, \ldots, n. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). However, @@ -92,7 +92,7 @@ end Compute `n` weights for the Clenshaw-Curtis rule, corresponding to `n` evenly spaced nodes from 0 to π inclusive. That is, the nodes are located at ```math -θ_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. +θ_k = k \frac{π}{n-1} \quad k=0, \ldots, n-1. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). diff --git a/test/conventions/NIST_DLMF.jl b/test/conventions/NIST_DLMF.jl index 43ac296a..18a7062b 100644 --- a/test/conventions/NIST_DLMF.jl +++ b/test/conventions/NIST_DLMF.jl @@ -29,7 +29,7 @@ And for the spherical harmonics, [Eq. 14.30.1](http://dlmf.nist.gov/14.30#E1) gi ```math Y_{ℓ, m}\left(θ,ϕ\right) = - \left(\frac{(ℓ-m)!(2ℓ+1)}{4\pi(ℓ+m)!}\right)^{1/2} + \left(\frac{(ℓ-m)!(2ℓ+1)}{4π(ℓ+m)!}\right)^{1/2} \mathsf{e}^{imϕ} \mathsf{P}_{ℓ}^{m}\left(\cos θ\right). ``` From ee235696d6d092d902ef381408edd43523b61772 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 13 Jan 2026 16:30:08 -0500 Subject: [PATCH 327/329] Don't bother with normalized volume forms --- .../calculations/metrics_and_integration.jl | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/literate_input/conventions/calculations/metrics_and_integration.jl b/docs/literate_input/conventions/calculations/metrics_and_integration.jl index d8b5f175..34f2f699 100644 --- a/docs/literate_input/conventions/calculations/metrics_and_integration.jl +++ b/docs/literate_input/conventions/calculations/metrics_and_integration.jl @@ -193,12 +193,6 @@ S3_surface_area = sympy.integrate( (α, 0, 2π) ) -# Therefore, the normalized volume-form factor on the unit sphere 𝕊³ is -S3_normalized_volume_form_factor = sympy.simplify( - four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1) - / S3_surface_area -) - # And finally, we can restrict back to ``\mathrm{SO}(3)`` by taking ``γ ∈ [0, 2π]`` (while # keeping ``α ∈ [0, 2π]`` and ``β ∈ [0, π]``), and integrating over that range: SO3_volume = sympy.integrate( @@ -211,8 +205,3 @@ SO3_volume = sympy.integrate( ), (α, 0, 2π) ) - -# So the normalized volume-form factor on ``\mathrm{SO}(3)`` is -SO3_normalized_volume_form_factor = sympy.simplify( - four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1) / SO3_volume -) From 9501bafc6abc5b3d14d09fb51eb257bfbef148f5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 13 Jan 2026 16:30:24 -0500 Subject: [PATCH 328/329] Flesh out the background --- docs/src/background/domain.md | 158 +++++++++++++++++++++--- docs/src/background/operators.md | 17 +-- docs/src/background/sYlm_and_Dlmpm.md | 160 +++++++++++++++++-------- docs/src/background/transformations.md | 2 + docs/src/conventions/details.md | 4 +- docs/src/references.bib | 26 ++++ 6 files changed, 295 insertions(+), 72 deletions(-) diff --git a/docs/src/background/domain.md b/docs/src/background/domain.md index 8a514bc9..312479a9 100644 --- a/docs/src/background/domain.md +++ b/docs/src/background/domain.md @@ -6,6 +6,8 @@ what domains these functions are defined on — what their arguments are. We will discover that it's best to use quaternions for all three. +## Three functions on two domains + Usually, these are written as functions of spherical coordinates for both types of harmonics, and Euler angles for the 𝔇 matrices: ```math @@ -58,6 +60,8 @@ Wigner's 𝔇 matrices, so we might as well think of them as being parameterized in the same way. +## The geometric picture + It might be helpful to pause for a moment and consider a more intuitive physical picture. Consider a telescope. We can point it any which way we want, which corresponds to choosing a point on the @@ -68,27 +72,36 @@ That is, it's a function of ``𝕊²``. But now, suppose we put a polarizer on the telescope. The pixel's value will now depend not only on where the telescope is pointed, but also on the orientation of the polarizer about the optical axis. This extra degree of freedom is -exactly the extra angle ``ψ`` we mentioned above. But really, what -we're doing is rotating our telescope in three-dimensional space, so -the pixel's value is a function on the *rotation group* -``\mathrm{SO}(3)``, not just the sphere ``𝕊²``. Now, with light, we -know every possible polarized value once we measure the value with one -polarization and the value with the orthogonal polarization, which are -combined into a single complex number. (Compare [Jones +exactly the extra angle ``ψ`` we mentioned above. + +But really, what we're doing is rotating our telescope in +three-dimensional space, so the pixel's value is a function on the +*rotation group* ``\mathrm{SO}(3)``, not just the sphere ``𝕊²``. +Now, with light, we know every possible polarized value once we +measure the value with one polarization and the value with the +orthogonal polarization, which are combined into a single complex +number. (Compare [Jones vectors](https://en.wikipedia.org/wiki/Jones_calculus).) As we rotate our choice of the first polarization by an angle ``ψ``, the complex combination varies as ``e^{iψ}``. This is exactly what it means to have spin weight ``s=1`` — which is, not coincidentally, the spin of a -photon. Imagining that we had a polarizing telescope measuring some -other kind of field with spin ``s``, the complex combination would -vary as ``e^{isψ}``. And since we could have half-integer spins, we -actually need to consider not just the rotation group -``\mathrm{SO}(3)``, but its double cover ``\mathrm{Spin}(3) \cong -\mathrm{SU}(2)``, to fully capture the behavior of general fields. +photon. + +Imagining that we had a polarizing telescope measuring some other kind +of field with spin ``s``, the complex combination would vary as +``e^{isψ}``. (This is important for gravitational-wave astronomy with +spin-2 fields. In principle, we could also consider neutrino +telescopes with spin-1/2 polarization.) And since we could have +half-integer spins, we actually need to consider not just the rotation +group ``\mathrm{SO}(3)``, but its double cover ``\mathrm{Spin}(3) +\cong \mathrm{SU}(2)``, to fully capture the behavior of general +fields. + +## Unification in ``\mathrm{Spin}(3)`` So, at this point, we see that all three of the functions we care about — standard spherical harmonics, spin-weighted spherical -harmonics, and Wigner's 𝔇 matrices — are most naturally thought of as +harmonics, and Wigner's 𝔇 matrices — are naturally thought of as functions on ``\mathrm{Spin}(3)``. The traditional way to parameterize this would be with (extended) Euler angles. However, as mentioned above, Euler angles are a poor choice for many purposes. @@ -99,8 +112,8 @@ the problem of composing rotations; given rotations described by Euler angles ``(α, β, γ)`` and ``(α', β', γ')``, finding another set of angles ``(α'', β'', γ'')`` representing their composition is a nasty, nonlinear problem. With quaternions, composition is just quaternion -multiplication: ``R'' = R'\, R``. Third, and most importantly, we have -the problem of generators of rotations. With Euler angles, +multiplication: ``R'' = R'\, R``. Third, and most importantly, we +have the problem of generators of rotations. With Euler angles, infinitesimal rotations are described by complicated derivatives with bizarre trigonometric factors. With quaternions, infinitesimal rotations are described by "pure-vector" quaternions that can be @@ -131,3 +144,116 @@ coordinates or Euler angles to quaternions, but internally everything is done with quaternions, and the documentation will generally be written in terms of quaternions. See [Boyle_2016](@citet) for full details. + +## Pushing forward to ``𝕊²`` + +Assuming the functions have been defined on ``\mathrm{Spin}(3)``, we +can push them forward to functions on the sphere ``𝕊²``.[^1] Given a +choice of a special point in ``𝕊²`` — conventionally the north pole +``𝐳`` — we can map ``𝐐 ∈ \mathrm{Spin}(3)`` to ``𝕊²`` simply by +using it to rotate ``𝐳`` to ``π(𝐐) = 𝐐\, 𝐳\, 𝐐⁻¹``. For any +particular point ``𝐧 ∈ 𝕊²``, the set of all rotors that map to that +point (its preimage) is of the form +```math +π^{-1}(𝐧) = \left\{𝐐\, e^{𝐤 θ/2} \,\middle|\, θ ∈ [0, 4π)\right\}, +``` +where ``𝐐`` is any particular rotor that maps ``𝐳`` to ``𝐧``, and +``𝐤`` is the generator of rotations about the ``z`` axis. In terms +of the telescope analogy presented above, ``θ`` represents rotation of +the telescope polarizer about the optical axis. + +[^1]: Okay, technically we'll use more structure than just ``𝕊²``. + We're picking out a special point in that space, and assuming the + action of ``\mathrm{Spin}(3)`` on that point to define the + mapping. Elsewhere, I criticize the usual approach because it + ignores the fundamental importance of the choice of tangent basis + at each point. This pushforward to ``𝕊²`` also uses some extra + structure — perhaps reinforcing the notion that spin-weighted + functions really should just be thought of as functions on + ``\mathrm{Spin}(3)``. + +Now, for any function ``f(𝐐)`` on ``\mathrm{Spin}(3)``, we can define +the pushforward function on ``𝕊²`` by taking a point to the average +value of ``f`` over the preimage. (Such "integration along the fiber" +is a standard concept in differential geometry [BottTu_1982](@cite).) +By abuse of notation, we will use the same symbol ``f`` for this new +function, with the understanding that the symbol will be reinterpreted +as needed. Thus, for ``𝐧 ∈ 𝕊²``, we define +```math +f(𝐧) = \frac{1}{4π} \int_0^{4π} f\left(𝐐\, e^{𝐤 θ/2}\right) \, dθ, +``` +where ``𝐐`` is any rotor such that ``π(𝐐) = 𝐧``. The choice of +``𝐐`` does not matter because the integral averages over all possible +choices. Also, given the behavior ``e^{isψ}`` described above, we +know that only fields with ``s=0`` will have nonzero values under this +operation. Thus, only functions with spin weight 0 can be pushed +forward to nontrivial functions on ``𝕊²``. And again, because the +standard scalar spherical harmonics are precisely the spin-weighted +spherical harmonics with ``s=0``, they can be pushed forward in this +way. + +This is effectively the only continuous way to push functions forward +to *all of* ``𝕊²``. For functions with nonzero spin-weight, we need +a reference direction to specify the tangent basis. However, the +[hairy-ball theorem](https://en.wikipedia.org/wiki/Hairy_ball_theorem) +tells us this cannot be done continuously over the entire sphere. +Originally, the spin-weighted spherical harmonics were not defined on +``𝕊²``, but the spherical coordinates — which are topologically the +cylinder ``I×𝕊¹``. + +## Pulling back to ``I×𝕊¹`` + +Finally, we can come back to spherical coordinates: the coordinates +``(θ, ϕ)``, with ``θ ∈ [0, π]`` and ``ϕ ∈ [0, 2π)``. Note that this +range for ``ϕ`` is half open, only because it is parameterizing a +circle; ``0`` and ``2π`` represent the same point. Thus, we see that +the spherical coordinates do not actually parameterize a sphere, but +rather a cylinder. Of course, that cylinder is usually mapped onto +the sphere, shrinking the top and bottom edges to points which +represent the poles of the sphere and therefore the singularities of +the spherical coordinates. Nonetheless, a function defined on +spherical coordinates is really a function on the cylinder ``I×𝕊¹``. + +When we write *spin-weighted* functions in terms of spherical +coordinates, we are not only quietly using the "wrong" domain, but +also implicitly choosing a tangent basis at each point. Specifically, +the ``\boldsymbol{θ}`` unit vector points directly down the cylinder, +mapping to a vector field that everywhere points toward the south pole +on the sphere. This mapping fails to give a unique tangent vector at +both poles, of course, which is consistent with the hairy-ball +theorem. Still, it gives us a well defined function *almost +everywhere* on the sphere. Interestingly, the function is well +defined *everywhere* on the cylinder. These two considerations — the +actual topology and the implicit choice of tangent basis — show that +it really is more correct to think of spin-weighted spherical +functions written in spherical coordinates as being defined on the +cylinder ``I×𝕊¹``. + +Now, interestingly, once we've picked out a basis, we can actually +define the *unique* rotor +```math +𝐐(θ, ϕ) = e^{ϕ𝐤/2}\, e^{θ𝐣/2}. +``` +Conflating spherical coordinates with geometry of ``𝕊²`` again for a +moment, we can think of this mapping as taking the north pole ``𝐳`` +to the point on the sphere with coordinates ``(θ, ϕ)``, and the vector +``𝐱`` onto ``\boldsymbol{θ}`` at that point. + +The key point is that, in this case, the mapping doesn't *start* with +``\mathrm{Spin}(3)``, but rather *ends* there. Thus, we can define a +pullback of a function ``f(𝐐)`` to a function ``f(θ, ϕ)`` in the +usual way — simple composition: +```math +f(θ, ϕ) = f\big(𝐐(θ, ϕ)\big). +``` +This is defined for all spin weights, because we've chosen a tangent +basis at each point. However, if we then try to "wrap" the cylinder +back onto the sphere, we cannot define the function at the poles for +nonzero spin weights, because the tangent basis is not uniquely +defined there. That is, the function value will not be independent of +the choice of ``ϕ`` at the poles, so the "wrapping" is not well +defined. + +This is how we can quite reasonably write spin-weighted spherical +harmonics as functions of spherical coordinates, even though they +cannot be defined as functions on the sphere itself. diff --git a/docs/src/background/operators.md b/docs/src/background/operators.md index 41b6fe16..4c6d856d 100644 --- a/docs/src/background/operators.md +++ b/docs/src/background/operators.md @@ -147,9 +147,11 @@ becomes ```math [\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} ε_{jkl} \hat{e}_l. ``` -Plugging this into the general expression ``[L_𝐚, L_𝐛] = \frac{i}{2} -L_{[𝐚,𝐛]}``, we obtain (except for that factor of ``\hbar``) the -version used in quantum physics. +That is, the commutator ``[𝐚,𝐛]`` is essentially twice the cross +product of the corresponding vectors. Plugging this into the general +expression ``[L_𝐚, L_𝐛] = \frac{i}{2} L_{[𝐚,𝐛]}``, we obtain +(except for that factor of ``\hbar``) the version used in quantum +physics. The raising and lowering operators relative to ``L_z`` and ``R_z`` satisfy — *by definition of raising and lowering operators* — the @@ -166,10 +168,11 @@ L_\pm = L_x \pm i L_y \qquad R_\pm = R_x \pm i R_y. ``` -(Interestingly, this procedure also shows that rasing and lowering -operators can only exist if the factor in front of the derivatives in -the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In -particular, this results in the commutator relations +(Interestingly, the solution process also shows that rasing and +lowering operators can only exist if the factor in front of the +derivatives in the definitions of ``L_g`` and ``R_g`` are pure +imaginary numbers.) In particular, this results in the commutator +relations ```math [L_+, L_-] = 2L_z \qquad diff --git a/docs/src/background/sYlm_and_Dlmpm.md b/docs/src/background/sYlm_and_Dlmpm.md index 8efa588e..f7ed0cdb 100644 --- a/docs/src/background/sYlm_and_Dlmpm.md +++ b/docs/src/background/sYlm_and_Dlmpm.md @@ -5,6 +5,8 @@ the left and right Lie derivative operators acting on functions of a quaternion argument. Here, we show how the spin-weighted spherical harmonics arise naturally as eigenfunctions of these operators. +## ``{}_{s}Y_{ℓ,m}`` as eigenfunctions + The familiar way of arriving at the standard (scalar, ``s=0``) spherical harmonics is to consider solutions to the Laplace equation in three-dimensional space, then to separate variables in spherical @@ -48,52 +50,110 @@ information. Thus, we select the spin-weighted spherical harmonics as functions satisfying ```math \begin{aligned} -L² \left\{ {}_{s}Y_{ℓ,m} \right\}(Q) -&= ℓ(ℓ+1) \left\{ {}_{s}Y_{ℓ,m} \right\}(Q), +L² \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= ℓ(ℓ+1) \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐), \\ -L_z \left\{ {}_{s}Y_{ℓ,m} \right\}(Q) -&= m \left\{ {}_{s}Y_{ℓ,m} \right\}(Q), +L_z \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= m \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐), \\ -R_z \left\{ {}_{s}Y_{ℓ,m} \right\}(Q) -&= s \left\{ {}_{s}Y_{ℓ,m} \right\}(Q). +R_z \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= s \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐). \end{aligned} ``` These conditions only determine the functions up to a normalization -factor, which is conventionally chosen so that the functions are -orthonormal with respect to the natural measure on -``\mathrm{Spin}(3)`` — which we can implement using [extended Euler -angles](@ref Quaternions-and-Euler-angles). This still leaves an -overall complex phase freedom, which is conventionally fixed by -requiring that ``{}_{s}Y_{ℓ,m}(I)``, where ``I`` is the identity -rotor, be real and nonnegative. +factor, which we discuss in the next section. -Now, we can return to the idea of functions on ``𝕊²``. We can -identify each point in ``\mathrm{Spin}(3)`` with a point on ``𝕊²`` by -considering the direction that a given rotor takes the ``z`` axis. -This loses information regarding rotation *about* that point. A -function on ``\mathrm{Spin}(3)`` can then be converted into a function -on ``𝕊²`` if and only if the function does not depend on the rotation -about that point, which is described by ``R_z``. Thus, only functions -with ``s=0`` can be considered as functions on ``𝕊²``. In fact, the -scalar spherical harmonics are precisely the spin-weighted spherical -harmonics with ``s=0``. +First, though, we take a moment to consider the effect of the raising +and lowering operators. We know the commutators from the previous +page, and we now know the eigenvalues, so we can compute just like we +do in elementary treatments of scalar spherical harmonics. ``L_±`` +operates just the same as usual, modifying the ``m`` index, while +``R_±`` in an exactly analogous way, but modifies the ``s`` index. +Thus, we can follow the standard derivation to find the standard +ladder relations: +```math +\begin{aligned} +L_± \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= \sqrt{(ℓ ∓ m)(ℓ ± m + 1)}\, \left\{ {}_{s}Y_{ℓ,m±1} \right\}(𝐐), +\\ +R_± \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= \sqrt{(ℓ ∓ s)(ℓ ± s + 1)}\, \left\{ {}_{s±1}Y_{ℓ,m} \right\}(𝐐). +\end{aligned} +``` +As usual, we have chosen the coefficients to be real and positive. -If we alter notation slightly and write the spherical harmonics as -functions of a unit vector to a point rather than the spherical -coordinates describing the same point, we can define scalar spherical -harmonics as functions on ``\mathrm{Spin}(3)`` as well: +## Integration and normalization + +It is natural to define an inner product on the space of functions on +``\mathrm{Spin}(3)`` using integration over the group itself. That is, +for two functions ``f`` and ``g``, we define[^1] ```math -Y_{ℓ,m}(𝐐) = Y_{ℓ,m}\left(𝐐\, 𝐳\, 𝐐⁻¹\right) +⟨f|g⟩_{\mathrm{Spin}(3)} = \int_{\mathrm{Spin}(3)} f̄(𝐐)\, g(𝐐)\, d𝐐, ``` -where ``𝐳`` is the unit vector in the ``z`` direction. The -right-hand side is just the usual scalar spherical harmonic evaluated -at the point on ``𝕊²`` corresponding to the rotation of the ``z`` -axis by the rotor ``𝐐``. Now, we can explicitly write +where the integration can be implemented using [extended Euler +angles](@ref Quaternions-and-Euler-angles). This induces a norm on +the space of functions on ``\mathrm{Spin}(3)``: ```math -Y_{ℓ,m}(𝐐) = {}_0Y_{ℓ,m}(𝐐); +\left\| f \right\|²_{\mathrm{Spin}(3)} +=⟨f|f⟩_{\mathrm{Spin}(3)} +=\int_{\mathrm{Spin}(3)} |f(𝐐)|²\, d𝐐. ``` -the scalar spherical harmonics are *precisely* the spin-weighted -spherical harmonics with spin weight ``s=0``. +One important point is the norm of the constant function ``f=1``, +which gives the total volume of ``\mathrm{Spin}(3)``. We can compute +it explicitly using extended Euler angles [and find](@ref +Quaternions-and-Euler-angles) that the result is ``2π²``. Recalling +that ``\mathrm{Spin}(3)`` considered as a subset of ``ℝ⁴`` is just the +unit three-sphere ``𝕊³``, which has volume ``2π²``, this makes sense. + +[^1]: Note that we are using the physicists' bra-ket notation here + [Dirac_1939](@cite). Specifically, the inner product is + conjugate-linear in its first argument and linear in its second + argument: for complex numbers ``a`` and ``b``, we have + ``⟨af|bg⟩=ā⟨f|g⟩b``. This convention is common in physics, + engineering, and computer science. But note that mathematicians + more commonly write ``⟨f, g⟩`` (with a comma instead of a vertical + bar) and define the inner product with opposite linearity: linear + in its first argument and conjugate-linear in its second argument. + +Unfortunately, these are not actually the conventional inner product +and norm used in the literature. The difference is not too difficult +to understand or deal with, however. Note that ``|f(𝐐)|²`` is +actually a field of spin weight 0, no matter what the spin weight of +``f`` itself is.[^2] Therefore, we can push it forward to a +nontrivial function on ``𝕊²`` as described [previously](@ref +Pushing-forward-to-𝕊), and then integrate over ``𝕊²`` instead of +``\mathrm{Spin}(3)``. Thus, we can define a distinct second norm +```math +\left\| f \right\|²_{𝕊²} = \int_{𝕊²} |f|²\, dΩ. +``` +This is actually the conventional norm used in the literature on +spin-weighted spherical functions. Again, we can compute the norm of +the constant function ``f=1`` and find that it is just the area of the +unit 2-sphere, ``4π``. We can relate the two norms with a simple +constant factor: +```math +\left\| f \right\|²_{𝕊²} +=\frac{2}{π} \left\| f \right\|²_{\mathrm{Spin}(3)}. +``` +Clearly, we must choose one or the other of these norms when +normalizing the spin-weighted spherical harmonics. To agree with +standard scalar spherical harmonics, the conventional choice is to use +the ``𝕊²`` norm. + +[^2]: It is possible for a function to have no definite spin weight; + to simply not be an eigenfunction of ``R_z``. In that case, + ``|f(𝐐)|²`` can also have indefinite spin weight. Nonetheless, + its integral over ``\mathrm{Spin}(3)`` will only pick up + contributions from the part of that function with spin weight 0. + + +* Note that SWSHs with different spin weights are not orthogonal under + the ``𝕊²`` inner product. +* "Reproducing kernel" and invariance of integral implies factors of + 2ell+1/4pi + + +## Defining ``𝔇^{(ℓ)}_{m', m}`` Finally, we can consider the Wigner ``𝔇`` matrices. These are usually defined as @@ -103,19 +163,21 @@ usually defined as e^{-iL_z α} e^{-iL_y β} e^{-iL_z γ} \big| ℓ, m \big\rangle. ``` -We can rewrite this as +Note that the bra-ket notation usually represents integration over +``𝕊²``. We can extend that to obtain the corresponding norm, while +integrating over ``\mathrm{Spin}(3)`` instead. Thus, we can rewrite +this as ```math 𝔇^{(ℓ)}_{m', m}(𝐐) -= \int_{\mathrm{Spin}(3)} - \bar{Y}_{ℓ,m'}(𝐐')\, - Y_{ℓ,m}\left(𝐐⁻¹ 𝐐'\right)\, - d𝐐', += \frac{2}{π} \int_{\mathrm{Spin}(3)} + \bar{Y}_{ℓ,m'}(𝐏)\, + Y_{ℓ,m}\left(𝐐⁻¹ 𝐏\right)\, + d𝐏. ``` -where the integral is taken over ``\mathrm{Spin}(3)`` with the -appropriate measure to ensure orthonormality. We can evaluate the -action of the differential operators on this function pretty easily, -noting that the derivative ``d/dϵ`` can pass through the integral sign -by the Leibniz integral rule. The result is that ``𝔇`` satisfies +We can evaluate the action of the differential operators on this +function pretty easily, noting that the derivative ``d/dϵ`` can pass +through the integral sign by the Leibniz integral rule. The result is +that ``𝔇`` satisfies ```math \begin{aligned} L² \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) @@ -130,5 +192,9 @@ R_z \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) ``` That is, Wigner's ``𝔇`` matrices are proportional to the spin-weighted spherical harmonics. We can get the proportionality -factor from ``𝔇^{(ℓ)}_{m', m}(1) = \delta_{m', m}``. - +factor by applying the definition above with ``𝐐=𝟏``, in which case +the integral simplifies to the orthonormality condition for the +spherical harmonics: +```math +𝔇^{(ℓ)}_{m', m}(𝟏) = \delta_{m', m}. +``` diff --git a/docs/src/background/transformations.md b/docs/src/background/transformations.md index e69de29b..535b13e9 100644 --- a/docs/src/background/transformations.md +++ b/docs/src/background/transformations.md @@ -0,0 +1,2 @@ +# Transforming between mode weights and values + diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md index 8bc5a43b..8e96d502 100644 --- a/docs/src/conventions/details.md +++ b/docs/src/conventions/details.md @@ -372,13 +372,13 @@ Restricting to the unit sphere, we can simplify this to \int_{\mathrm{Spin}(3)} f\, d^3\Omega = \int_0^{2π} \int_0^{π} \int_0^{4π} f\, \sin β\, dα\, dβ\, dγ, ``` -where ``\int_{\mathrm{Spin}(3)} d^3\Omega = 16π^2``. Finally, +where ``\int_{\mathrm{Spin}(3)} d^3\Omega = 2π^2``. Finally, restricting to the space of rotations, we can further simplify this to ```math \int_{\mathrm{SO}(3)} f\, d^3\Omega = \int_0^{2π} \int_0^{π} \int_0^{2π} f\, \sin β\, dα\, dβ\, dγ, ``` -where ``\int_{\mathrm{SO}(3)} d^3\Omega = 8π^2``. +where ``\int_{\mathrm{SO}(3)} d^3\Omega = π^2``. ## Rotations diff --git a/docs/src/references.bib b/docs/src/references.bib index 17eb96e0..09e257ba 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -64,6 +64,18 @@ @article{Blanchet_2024 doi = {10.1007/s41114-024-00050-z}, } +@book{BottTu_1982, + address = {New York, {NY}}, + series = {Graduate Texts in Mathematics}, + title = {Differential Forms in Algebraic Topology}, + isbn = {978-1-4757-3951-0}, + url = {https://doi.org/10.1007/978-1-4757-3951-0}, + publisher = {Springer}, + author = {Bott, Raoul and Tu, Loring W.}, + year = {1982}, + doi = {10.1007/978-1-4757-3951-0} +} + @article{BoydPetschek_2014, title = {The Relationships Between {C}hebyshev, {L}egendre and {J}acobi Polynomials: The Generic Superiority of {C}hebyshev Polynomials and Three Important Exceptions}, @@ -155,6 +167,20 @@ @article{Davenport_1973 pages = {853--857} } +@article{Dirac_1939, + title = {A new notation for quantum mechanics}, + volume = {35}, + issn = {1469-8064, 0305-0041}, + url = {https://www.cambridge.org/core/journals/mathematical-proceedings-of-the-cambridge-philosophical-society/article/new-notation-for-quantum-mechanics/4631DB9213D680D6332BA11799D76AFB}, + doi = {10.1017/S0305004100021162}, + number = {3}, + journal = {Mathematical Proceedings of the Cambridge Philosophical Society}, + author = {Dirac, P. a. M.}, + month = jul, + year = {1939}, + pages = {416--418} +} + @book{DoranLasenby_2010, address = {Cambridge}, title = {Geometric Algebra for Physicists}, From e8b1c670bfe8b168d708eb25879c642f8f2e511d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 22 Jan 2026 15:58:43 -0500 Subject: [PATCH 329/329] More documentation of background --- docs/src/background/domain.md | 22 ++++-- docs/src/background/mode_weights.md | 62 +++++++++++---- docs/src/background/operators.md | 71 +++++++++++------ docs/src/background/sYlm_and_Dlmpm.md | 32 +++++--- docs/src/notes/normalization.md | 107 ++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 55 deletions(-) create mode 100644 docs/src/notes/normalization.md diff --git a/docs/src/background/domain.md b/docs/src/background/domain.md index 312479a9..426b0a9b 100644 --- a/docs/src/background/domain.md +++ b/docs/src/background/domain.md @@ -3,8 +3,11 @@ This package deals with standard spherical harmonics, spin-weighted spherical harmonics, and Wigner's 𝔇 matrices. The key question is what domains these functions are defined on — what their arguments -are. We will discover that it's best to use quaternions for all -three. +are. Here, we will argue that it's best to use quaternions for all +three. The rest of this package and documentation use the language of +quaternions, though we make contact with the more traditional +parameterizations in terms of spherical coordinates and Euler angles +as needed. ## Three functions on two domains @@ -254,6 +257,15 @@ defined there. That is, the function value will not be independent of the choice of ``ϕ`` at the poles, so the "wrapping" is not well defined. -This is how we can quite reasonably write spin-weighted spherical -harmonics as functions of spherical coordinates, even though they -cannot be defined as functions on the sphere itself. +Still, this approach does give us the value of the function for any +particular point on ``𝕊²`` *and* a particular choice of tangent +direction at that point. Moreover, for a function with a definite +spin weight ``s``, this is enough information to reconstruct the value +of the function for *any other* choice of tangent direction at that +same point. This is how we can quite reasonably write spin-weighted +spherical harmonics as functions of spherical coordinates, even though +they cannot be defined as functions on the sphere itself. Of course, +using the fundamental domain is a much more powerful and flexible +approach; among other things, it allows us to define differential +operators acting on these functions, which we will do [next](@ref +background_differential_operators). diff --git a/docs/src/background/mode_weights.md b/docs/src/background/mode_weights.md index 49006725..60bfcc33 100644 --- a/docs/src/background/mode_weights.md +++ b/docs/src/background/mode_weights.md @@ -1,31 +1,59 @@ # Mode weights On the [previous page](@ref sYlm_and_Dlmpm), we introduced the -eigenfunctions of the differential operators defined on -``\mathrm{Spin}(3)``, the spin-weighted spherical harmonics (SWSHs) -``{}_{s}Y_{ℓ,m}(R)`` — or equivalently Wigner's 𝔇 matrices. +eigenfunctions of [the differential operators](@ref +background_differential_operators) defined on ``\mathrm{Spin}(3)``. +These eigenfunctions are the spin-weighted spherical harmonics (SWSHs) +``{}_{s}Y_{ℓ,m}(R)``, or equivalently Wigner's 𝔇 matrices. + +Now that we have introduced the spin-weighted spherical harmonics +(SWSHs) as eigenfunctions of the relevant differential operators, we +can define mode weights of a general spin-weighted function in terms +of these harmonics. These eigenfunctions form a complete orthonormal basis for the space of square-integrable functions defined on ``\mathrm{Spin}(3)``. Thus, -any such function ``f(R)`` with spin weight ``s`` can be expressed as -a linear combination of these harmonics: +*any* such function ``f(R)`` can be expressed as a linear combination +of these harmonics: ```math -f(R) = \sum_{ℓ=0}^{∞} \sum_{m=-ℓ}^{ℓ} f_{ℓ,m}\, {}_{s}Y_{ℓ,m}(R), +f(R) = \sum_{ℓ=0}^{∞} \sum_{s=-ℓ}^{ℓ} \sum_{m=-ℓ}^{ℓ} +{}_{s}f_{ℓ,m}\, {}_{s}Y_{ℓ,m}(R), ``` -where the coefficients ``f_{ℓ,m}`` are called the *mode weights* of -the function ``f``. These mode weights can be computed from the function -using the orthonormality of the SWSHs: +where the coefficients ``{}_{s}f_{ℓ,m}`` are called the *mode weights* +of the function ``f``. These mode weights can be computed from the +function using the orthogonality of the SWSHs: ```math -f_{ℓ,m} = \int f(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR, +{}_{s}f_{ℓ,m} += \frac{π}{2} \int_{\mathrm{Spin}(3)} f(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR, ``` -where the integral is taken over ``\mathrm{Spin}(3)`` with the -appropriate measure to ensure orthonormality. In the special case -``s=0``, +where the factor in front of the integral is explained [here](@ref +Integration-and-normalization). Note that we have not restricted the +spin weight ``s`` of the function ``f``; a general function on +``\mathrm{Spin}(3)`` can have contributions from SWSHs of any spin +weight, so ``s`` was included in the sum above. In fact, those spin +weights may have half-integral values as well, in which case the sum +over ``ℓ`` must include all positive half-integral values as well as +the integral values. -Now that we have introduced the spin-weighted spherical harmonics -(SWSHs) as eigenfunctions of the relevant differential operators, we -can define mode weights of a general spin-weighted function in terms of -these harmonics. +However, if we restrict to functions with a *specific* spin weight +``s``, only the SWSHs with *that* spin weight will contribute to the +expansion, and we can simplify the expressions above to +```math +f(R) = \sum_{ℓ=|s|}^{∞} \sum_{m=-ℓ}^{ℓ} +{}_{s}f_{ℓ,m}\, {}_{s}Y_{ℓ,m}(R) +``` +and +```math +{}_{s}f_{ℓ,m} += \frac{π}{2} \int_{\mathrm{Spin}(3)} f(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR. +``` + +!!! danger "#TODO" + + Finish this section, citing the section on [functions + of spherical coordinates](@ref Pulling-back-to-I𝕊) and + reducing to the integral over ``𝕊²``. Refine the + following section. ## Differential operators diff --git a/docs/src/background/operators.md b/docs/src/background/operators.md index 4c6d856d..1297ae8b 100644 --- a/docs/src/background/operators.md +++ b/docs/src/background/operators.md @@ -47,21 +47,43 @@ R_𝐠(f)\{𝐐\} := -\frac{i}{2} \left. \frac{df\left(𝐐\, e^{-ϵ\,𝐠}\right)}{d ϵ} \right|_{ϵ=0}. ``` Note that the exponential multiplies ``𝐐`` *on the right* — hence the -name. +name. Also observe that +```math +𝐐\, e^{-ϵ\,𝐠} += 𝐐\, e^{-ϵ\,𝐠}\, 𝐐⁻¹\, 𝐐 += e^{-ϵ\,𝐐\, 𝐠\, 𝐐⁻¹}\, 𝐐 +``` +and +```math +e^{ϵ\,𝐠}\, 𝐐 += 𝐐\, 𝐐⁻¹\, e^{-ϵ\,𝐠}\, 𝐐 += 𝐐\, e^{-ϵ\,𝐐⁻¹\, 𝐠\, 𝐐}, +``` +which mean that +```math +R_𝐠(f)\{𝐐\} = L_{-𝐐\, 𝐠\, 𝐐⁻¹}(f)\{𝐐\} +\qquad \text{and} \qquad +L_𝐠(f)\{𝐐\} = R_{-𝐐⁻¹\, 𝐠\, 𝐐}(f)\{𝐐\}. +``` +That is, the right Lie derivative with respect to ``𝐠`` is equal to +the left Lie derivative with respect to the "rotated" version of the +algebra element ``-𝐠``, and vice versa. But the arguments *depend on +the value* ``𝐐``, which means, for example, that commutator +expressions can't be evaluated by simple static substitution. -This operator is less common in physics, because it represents the -dependence of the function on the choice of frame (or coordinate -system), which is not usually interesting. Multiplication on the left -represents a rotation of the physical system, while rotation on the -right represents a rotation of the coordinate system. However, this -dependence on coordinate system is precisely what defines the *spin -weight* of a function, so this class of operators is relevant in -discussions of spin-weighted spherical functions. In particular, -``R_z`` is the spin-weight operator — meaning that when it acts on a -spin-weighted spherical harmonic of spin weight ``s``, it returns -``s`` times that same function. Moreover, the operators ``R_\pm`` -correspond (up to a sign) to the spin-raising and -lowering operators -``\eth`` and ``\bar{\eth}`` originally introduced by +This ``R_𝐠`` operator is less common in physics, because it +represents the dependence of the function on the choice of frame (or +coordinate system), which is not usually interesting. Multiplication +on the left represents a rotation of the physical system, while +rotation on the right represents a rotation of the coordinate system. +However, this dependence on coordinate system is precisely what +defines the *spin weight* of a function, so this class of operators is +relevant in discussions of spin-weighted spherical functions. In +particular, ``R_z`` is the spin-weight operator — meaning that when it +acts on a spin-weighted spherical harmonic of spin weight ``s``, it +returns ``s`` times that same function. Moreover, the operators +``R_\pm`` correspond (up to a sign) to the spin-raising and -lowering +operators ``\eth`` and ``\bar{\eth}`` originally introduced by [Newman_1966](@citet), as explained in greater detail by [Boyle_2016](@citet). @@ -80,12 +102,11 @@ L_{𝐚+𝐛} = L_{𝐚} + L_{𝐛} \qquad \text{and} \qquad R_{𝐚+𝐛} = R_{𝐚} + R_{𝐛}, ``` -for any scalar ``s`` and any elements of the Lie algebra -``𝐚`` and ``𝐛``. In particular, if the Lie algebra -has a basis ``𝐞_{(j)}``, we use the shorthand ``L_j`` and -``R_j`` for ``L_{𝐞_{(j)}}`` and ``R_{𝐞_{(j)}}``, -respectively, and we can expand any operator in terms of these basis -operators: +for any *real* scalar ``s`` and any elements of the Lie algebra ``𝐚`` +and ``𝐛``. In particular, if the Lie algebra has a basis +``𝐞_{(j)}``, we use the shorthand ``L_j`` and ``R_j`` for +``L_{𝐞_{(j)}}`` and ``R_{𝐞_{(j)}}``, respectively, and we can expand +any operator in terms of these basis operators: ```math L_{𝐚} = \sum_{j} a_j L_j \qquad \text{and} \qquad @@ -100,7 +121,9 @@ relations ```math \left[ L_𝐚, L_𝐛 \right] = \frac{i}{2} L_{[𝐚,𝐛]} \qquad -\left[ R_𝐚, R_𝐛 \right] = \frac{i}{2} R_{[𝐚,𝐛]}, +\left[ R_𝐚, R_𝐛 \right] = \frac{i}{2} R_{[𝐚,𝐛]} +\qquad +\left[ L_𝐚, R_𝐛 \right] = 0, ``` where ``[𝐚,𝐛]`` is the commutator of the two generators, which can be obtained directly as the commutator of the corresponding @@ -123,8 +146,8 @@ produce similar commutators for ``R``.[^1] sense to use the coefficient ``i/2`` in general; it was chosen here for consistency with the standard angular-momentum operators. If that coefficient is changed in the definitions of the Lie - derivatives, the only change to the commutator relations would the - substitution of that coefficient. The presence of an ``i`` is + derivatives, the only change to the commutator relations would be + the substitution of that coefficient. The presence of an ``i`` is important to ensure that the operators are Hermitian when acting on appropriate function spaces. @@ -168,7 +191,7 @@ L_\pm = L_x \pm i L_y \qquad R_\pm = R_x \pm i R_y. ``` -(Interestingly, the solution process also shows that rasing and +(Interestingly, the solution process also shows that raising and lowering operators can only exist if the factor in front of the derivatives in the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In particular, this results in the commutator diff --git a/docs/src/background/sYlm_and_Dlmpm.md b/docs/src/background/sYlm_and_Dlmpm.md index f7ed0cdb..bee2573f 100644 --- a/docs/src/background/sYlm_and_Dlmpm.md +++ b/docs/src/background/sYlm_and_Dlmpm.md @@ -34,7 +34,11 @@ L_z \left\{ Y_{ℓ,m} \right\}(θ, ϕ) \end{aligned} ``` (As usual, we do not include the factors of ``\hbar`` that would -normally be included in quantum mechanics texts.) +normally be included in quantum mechanics texts.) Familiar arguments +— either from spectra of operators, or continuity of the functions — +tell us that the allowed values of ``ℓ`` are non-negative integers, +and for each ``ℓ``, the allowed values of ``m`` are integers +satisfying ``-ℓ ≤ m ≤ ℓ``. But now, we consider functions defined on the full group ``\mathrm{Spin}(3)``. In that case, we have both left and right Lie @@ -60,8 +64,12 @@ R_z \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) &= s \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐). \end{aligned} ``` -These conditions only determine the functions up to a normalization -factor, which we discuss in the next section. +In this case, similar arguments show that the allowed values of ``ℓ`` +are non-negative integers or half-integers, and for each ``ℓ``, the +allowed values of ``m`` and ``s`` are correspondingly integers or +half-integers satisfying ``-ℓ ≤ m ≤ ℓ`` and ``-ℓ ≤ s ≤ ℓ``. These +conditions only determine the functions up to a normalization factor, +which we discuss in the next section. First, though, we take a moment to consider the effect of the raising and lowering operators. We know the commutators from the previous @@ -113,7 +121,8 @@ unit three-sphere ``𝕊³``, which has volume ``2π²``, this makes sense. engineering, and computer science. But note that mathematicians more commonly write ``⟨f, g⟩`` (with a comma instead of a vertical bar) and define the inner product with opposite linearity: linear - in its first argument and conjugate-linear in its second argument. + in its first argument and conjugate-linear in its second argument, + so that ``⟨af|bg⟩=a⟨f|g⟩b̄``. Unfortunately, these are not actually the conventional inner product and norm used in the literature. The difference is not too difficult @@ -146,11 +155,11 @@ the ``𝕊²`` norm. its integral over ``\mathrm{Spin}(3)`` will only pick up contributions from the part of that function with spin weight 0. +!!! danger "#TODO" -* Note that SWSHs with different spin weights are not orthogonal under - the ``𝕊²`` inner product. -* "Reproducing kernel" and invariance of integral implies factors of - 2ell+1/4pi + Finish this section, noting that SWSHs with different + spin weights are not orthogonal under + the ``𝕊²`` inner product. ## Defining ``𝔇^{(ℓ)}_{m', m}`` @@ -194,7 +203,12 @@ That is, Wigner's ``𝔇`` matrices are proportional to the spin-weighted spherical harmonics. We can get the proportionality factor by applying the definition above with ``𝐐=𝟏``, in which case the integral simplifies to the orthonormality condition for the -spherical harmonics: +spin-weighted spherical harmonics: ```math 𝔇^{(ℓ)}_{m', m}(𝟏) = \delta_{m', m}. ``` + +!!! danger "#TODO" + + Check the signs on the eigenvalues above, and refer to + the Notes page on normalization to relate Y to D. diff --git a/docs/src/notes/normalization.md b/docs/src/notes/normalization.md new file mode 100644 index 00000000..0fb56b30 --- /dev/null +++ b/docs/src/notes/normalization.md @@ -0,0 +1,107 @@ +# Normalization + +For any fixed values of ``ℓ`` and ``s``, the spin-weighted spherical +harmonics normalized as usual, satisfy the relation +```math +\sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|² = \frac{(2ℓ+1)}{4π}, +``` +for every ``𝐐 ∈ \mathrm{Spin}(3)``. This result is crucial for +actually obtaining a specific value for the spin-weighted spherical +harmonics, and we will derive it here. + +--- + +Begin by fixing ``ℓ`` and ``s``, and define the space of functions +with these eigenvalues: +```math +ℋ_{ℓ,s} = \left\{ + f : \mathrm{Spin}(3) → ℂ + \middle| + L² f = ℓ(ℓ+1) f + \mathrm{\ \ and\ \ } + R_z f = s f +\right\}. +``` +Now, for a given point ``𝐏 ∈ \mathrm{Spin}(3)``, we define the +["reproducing +kernel"](https://en.wikipedia.org/wiki/Reproducing_kernel_Hilbert_space) +``K_𝐏`` as a function in ``ℋ_{ℓ,s}`` such that for *every* +square-integrable function ``f ∈ ℋ_{ℓ,s}``, +```math +f(𝐏) = \int_{\mathrm{Spin}(3)} K̄_𝐏(𝐐)\, f(𝐐)\, d𝐐. +``` +Now, we want to expand this kernel in terms of the basis functions +``{}_{s}Y_{ℓ,m}`` of this space. Recall that these are not +ortho*normal* over ``\mathrm{Spin}(3)``; they are only orthogonal. +They are normalized over ``𝕊²`` in the restricted sense discussed +[here](@ref sYlm_and_Dlmpm) so that, when integrating over +``\mathrm{Spin}(3)``, we get an extra factor of ``π/2``: +```math +\int_{\mathrm{Spin}(3)} {}_{s}Ȳ_{ℓ,m'}(𝐐)\, {}_{s}Y_{ℓ,m}(𝐐)\, d𝐐 += \frac{π}{2} δ_{m',m}. +``` +Now, if we expand ``K_𝐏`` and ``f`` in terms of these basis +functions, we can calculate +```math +\begin{aligned} +f(𝐏) +&= \int_{\mathrm{Spin}(3)} K̄_𝐏(𝐐)\, f(𝐐)\, d𝐐 \\ +&= \int_{\mathrm{Spin}(3)} + \sum_{m',m} K̄_{𝐏,m'}\, {}_{s}Ȳ_{ℓ,m'}(𝐐)\, + f_{m}\, {}_{s}Y_{ℓ,m}(𝐐)\, d𝐐 \\ +&= \frac{π}{2} \sum_{m} K̄_{𝐏,m}\, f_{m}, +\end{aligned} +``` +the last of which implies that +```math +K_{𝐏,m} = \frac{2}{π} {}_{s}Ȳ_{ℓ,m}(𝐏) +``` +for every ``m``. That is, +```math +K_𝐏(𝐐) = \sum_m \frac{2}{π} {}_{s}Ȳ_{ℓ,m}(𝐏)\; {}_{s}Y_{ℓ,m}(𝐐). +``` +Now we take the norm of the kernel function: +```math +\begin{aligned} +\|K_𝐏\|²_{\mathrm{Spin}(3)} +&= \int_{\mathrm{Spin}(3)} |K_𝐏(𝐐)|²\, d𝐐 \\ +&= \int_{\mathrm{Spin}(3)} + \sum_{m',m} \frac{4}{π²} + {}_{s}Y_{ℓ,m'}(𝐏)\, {}_{s}Ȳ_{ℓ,m'}(𝐐)\, {}_{s}Ȳ_{ℓ,m}(𝐏)\, {}_{s}Y_{ℓ,m}(𝐐)\, d𝐐 \\ +&= \frac{2}{π} \sum_{m} |{}_{s}Y_{ℓ,m}(𝐏)|². +\end{aligned} +``` +Now, we are integrating with respect to a [Haar +measure](https://en.wikipedia.org/wiki/Haar_measure) on the group +``\mathrm{Spin}(3)``, which means that the integral must be invariant +under group actions. In particular, this means that the norm of the +kernel function cannot depend on the choice of ``𝐏``, which means +that the sum in the last expression is independent of ``𝐏``: +```math +\sum_{m} |{}_{s}Y_{ℓ,m}(𝐏)|² = \sum_{m} |{}_{s}Y_{ℓ,m}(𝐏')|² +``` +for *all* ``𝐏, 𝐏' ∈ \mathrm{Spin}(3)``. + +The last trick is to just integrate this expression over +``\mathrm{Spin}(3)`` again, and evaluate it in two different ways. On +one hand, we have +```math +\int_{\mathrm{Spin}(3)} \sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|²\, d𝐐 += \sum_{m} \int_{\mathrm{Spin}(3)} |{}_{s}Y_{ℓ,m}(𝐐)|²\, d𝐐 += \sum_{m} \frac{π}{2} += (2ℓ+1) \frac{π}{2}, +``` +since there are ``2ℓ+1`` values of ``m`` in the sum. On the other +hand, since the sum is independent of ``𝐐``, we can change to +integrating over a dummy variable and pull the sum out of the +integral: +```math +\int_{\mathrm{Spin}(3)} \sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|²\, d𝐐 += \sum_{m} |{}_{s}Y_{ℓ,m}(1)|²\, \int_{\mathrm{Spin}(3)} d𝐐' += 2π² \sum_{m} |{}_{s}Y_{ℓ,m}(1)|². +``` +Equating these two expressions, we find that +```math +\sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|² = \frac{(2ℓ+1)}{4π}, +``` +for arbitrary ``ℓ``, ``s``, and ``𝐐 ∈ \mathrm{Spin}(3)``.