From bf4bb96a1f862a610e358d51093731cc876ddcee Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 4 Dec 2024 23:40:10 -0500 Subject: [PATCH 001/183] 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 01705b20..4d9a63d4 100644 --- a/Project.toml +++ b/Project.toml @@ -26,6 +26,7 @@ Coverage = "1.6" DoubleFloats = "1" ForwardDiff = "0.10" FFTW = "1" +FastDifferentiation = "0.3.17" FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16" 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 0162c8d6e6b148ede56ba55bebb21a35445b5d95 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 5 Dec 2024 09:44:42 -0500 Subject: [PATCH 002/183] 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 5bb3ca78ed72b03bc64ca84af295ede9a5ef5c35 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 13:35:40 -0500 Subject: [PATCH 003/183] 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 100c7b1fa0e252c17ac7ff718d0002c771bdda89 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 13:35:54 -0500 Subject: [PATCH 004/183] 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 8a3d7653947e3aba42795747e939e80f5de10612 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 13:36:07 -0500 Subject: [PATCH 005/183] 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 9a92295e25e0ef38122c8dcc5038390c2601aaaf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 6 Dec 2024 22:58:21 -0500 Subject: [PATCH 006/183] 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 f71f63ba79a20b689732fe12a524be0c32641279 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 10 Dec 2024 13:30:05 -0500 Subject: [PATCH 007/183] 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 396c574b2e03aa5094cef78844384a1855f0e5a2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 10 Dec 2024 13:30:35 -0500 Subject: [PATCH 008/183] 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 f05ddbb0acf899f2d669de09737f33136dad42cc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 11 Dec 2024 09:52:00 -0500 Subject: [PATCH 009/183] 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 ca8b6ae1cb876b2d1733876a48751fe8d67657e3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 11 Dec 2024 11:18:23 -0500 Subject: [PATCH 010/183] 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 2b78fcde3eb1546a2a8bc49e403df19f1231331f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 11 Dec 2024 11:19:56 -0500 Subject: [PATCH 011/183] 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 8a08398e3897f28bc2686604a26f4f13885b18bc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:20:50 -0500 Subject: [PATCH 012/183] 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 0987cdd1e9272cbbc636a6e742c653e1e19bcb5d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:21:27 -0500 Subject: [PATCH 013/183] 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 ead68edda5bc6f67776696ff8682ee66c1cd627a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:21:56 -0500 Subject: [PATCH 014/183] 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 21939c31bfab6f595461207b9fdb523ca92dce96 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:22:10 -0500 Subject: [PATCH 015/183] 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 478544abb29389234595fb83f84b9afc3d820a0c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:58:01 -0500 Subject: [PATCH 016/183] 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 5e577bd9866bdee52a404d57c6e900509d47df46 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 12 Dec 2024 11:58:25 -0500 Subject: [PATCH 017/183] 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 9f0bffad28d1a482ef3deea48fbe192534b5c332 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 20 Dec 2024 02:10:52 -0500 Subject: [PATCH 018/183] 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 a0a413d39a506b9a897e10f23f9b6f21a9a9be4d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 23 Dec 2024 23:42:13 -0600 Subject: [PATCH 019/183] 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 78d6db9db33b08216e22d657e9b66a300b1ff1bc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 23 Dec 2024 23:43:07 -0600 Subject: [PATCH 020/183] 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 b44370cde0cf64f07b538cdd6a22e720f27fe056 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 23 Dec 2024 23:44:57 -0600 Subject: [PATCH 021/183] 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 1e5ec3e0f96799d7aa1828d85c0bbdcc6279b7fd Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 27 Dec 2024 17:03:01 -0600 Subject: [PATCH 022/183] 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 11fb1195b2718fb8e8589cc6184e356e27b688db Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 28 Dec 2024 10:07:30 -0600 Subject: [PATCH 023/183] 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 f0830c16f4cf8e4f7437bd5b6626dfb96b8df93a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 28 Dec 2024 10:08:04 -0600 Subject: [PATCH 024/183] 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 aed6119d0e5cbb3352d077036fdea6f2a13f9517 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 28 Dec 2024 10:18:10 -0600 Subject: [PATCH 025/183] 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 a618bc3bf34e6d73d2220019d7085b28e21dcab1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 29 Dec 2024 00:53:42 -0600 Subject: [PATCH 026/183] 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 3c9b0ac3d18807ac9631996b56d0872ae281d38c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 29 Dec 2024 00:56:20 -0600 Subject: [PATCH 027/183] 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 9e49c7010d9dbe30f9a498197d54e0dc5a1f0ba6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 31 Dec 2024 16:37:48 -0500 Subject: [PATCH 028/183] 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 f2d6df00886f45b522cedec94bafaef480d9ec40 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:56:09 -0500 Subject: [PATCH 029/183] 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 624884dc81cf5895bbe5e041961a94d9fe4b1423 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:57:30 -0500 Subject: [PATCH 030/183] 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 af60ff3847da3269f80c1a6ee5a2e0a47bcc194b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:59:10 -0500 Subject: [PATCH 031/183] 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 23d619887a10993cd61711684bf13c2ac1a5e2c7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 14:59:36 -0500 Subject: [PATCH 032/183] 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 e88e06f1407d89ef8095a1b2200e44644f9cbf82 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 15:15:56 -0500 Subject: [PATCH 033/183] 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 1046f5f5a31d80346e9b0c3da4a9a4e11c52c407 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 12 Jan 2025 15:16:59 -0500 Subject: [PATCH 034/183] 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 c4e9d70a77ef34d62400332f4cfc1b306ac7585b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Jan 2025 14:49:40 -0500 Subject: [PATCH 035/183] 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 3af0ae3f97f240479318aec6abc3a084ae81a8da Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 13 Jan 2025 14:50:05 -0500 Subject: [PATCH 036/183] 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 af6a461736f518c24c18da20d9b8027c772b0f82 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 14 Jan 2025 23:03:27 -0500 Subject: [PATCH 037/183] 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 a4bf0750a5a2c1cba015430df25ba2b59f6d38af Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 16 Jan 2025 15:43:14 -0500 Subject: [PATCH 038/183] 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 e8d941ac5e326d82a806634013eba6fc39bf9d12 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 00:29:32 -0500 Subject: [PATCH 039/183] 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 ef3a9267cb80ed6ad3bc4250589ef3ebe2d3282a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 09:40:34 -0500 Subject: [PATCH 040/183] 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 a79c2fa9ad1428e06f27cfbb2d7834db3d4dbe0b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 09:40:48 -0500 Subject: [PATCH 041/183] 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 4d9a63d4..2f5e0ff9 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" FFTW = "1" FastDifferentiation = "0.3.17" FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16" +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 f9448124abf65a21452ec1bf3f20acfa56d5e309 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 17 Jan 2025 09:57:54 -0500 Subject: [PATCH 042/183] 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 12aa826b5f9c0d1d2224779436c250c9a15de7c9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:49:00 -0500 Subject: [PATCH 043/183] 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 62287909246a15d691d867ce0810a40d2b158ad5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:49:28 -0500 Subject: [PATCH 044/183] 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 417e1f64b69bc3dd71ee5ef01c51600916cb01a2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:51:17 -0500 Subject: [PATCH 045/183] 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 547e0185be47db83464fa34c0ed5ef6a548f8992 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 11:52:19 -0500 Subject: [PATCH 046/183] 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 974964c6053035a12a41002710eeedc2d741daf6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 14:25:25 -0500 Subject: [PATCH 047/183] 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 98204e9e2c98206cbf8c323ba6edf6ff7a42f446 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 18 Jan 2025 21:43:26 -0500 Subject: [PATCH 048/183] 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 dc160a4a3d52cf4e814c6b4a6786da451dd7e744 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 20 Jan 2025 13:33:13 -0500 Subject: [PATCH 049/183] 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 3f973ccaab3cbaac82193ad2db2bf44d0ee1c796 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 20 Jan 2025 13:33:23 -0500 Subject: [PATCH 050/183] 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 f6ef9065615b2c979b7482ec734de8935563f38b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Feb 2025 21:12:00 -0500 Subject: [PATCH 051/183] 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 d613e340925e0199a33bbaa3319e5f9192478058 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Feb 2025 21:12:20 -0500 Subject: [PATCH 052/183] 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 d77d930ba4a19bcf5d5d387d66c63c8ae388e6b6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Feb 2025 21:14:22 -0500 Subject: [PATCH 053/183] 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 cbee017d69f2f46bb7bd05b209827d773c01df2f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Feb 2025 23:33:46 -0500 Subject: [PATCH 054/183] 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 c1ced694fb60cb352ca60987e2b716280efa8dc2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 16:49:33 -0500 Subject: [PATCH 055/183] 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 bdc263cda2b990a33eaf18fdbeb94a7af50ca816 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 16:50:21 -0500 Subject: [PATCH 056/183] 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 cb71aeba59b1d93adf84d8caf9f3e66d8b87593c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 17:17:44 -0500 Subject: [PATCH 057/183] 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 7d96c50d9c58ffcf22d25f1b1268789b135447c4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Feb 2025 22:26:32 -0500 Subject: [PATCH 058/183] 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 1e7ed68c9a44bc78196583ea0cb26020870dcfa9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 07:37:57 -0500 Subject: [PATCH 059/183] 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 804ed8022cb162fe907f0c829f65ec6b00f0d507 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 07:39:45 -0500 Subject: [PATCH 060/183] 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 9a1eb8511b990a29cca201c626043e02ded99120 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 08:36:03 -0500 Subject: [PATCH 061/183] 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 82ae32c3f3a56ea0b012964896769353ec8fe3e5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 08:43:37 -0500 Subject: [PATCH 062/183] 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 840227e8f74dbcae3c89976a231dd9929a408f10 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:30:45 -0500 Subject: [PATCH 063/183] 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 9a3620c2d8ebd3da08d8295c9f2d74ba9fb74ee8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:37:05 -0500 Subject: [PATCH 064/183] Pause tests on 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 d3b60a57..16f69e3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: matrix: version: - '1' - - 'nightly' + # - 'nightly' os: - ubuntu-latest include: From 799a673b2d30b0e29fc5b84ff1be31997250d598 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:38:09 -0500 Subject: [PATCH 065/183] 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 788da5d26476e7fb7688f1ee2eb487b5b4ab41c6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 09:52:47 -0500 Subject: [PATCH 066/183] 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 e32d8b26b6bf70415978854c9d04f933eea5fc4c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 11:26:48 -0500 Subject: [PATCH 067/183] 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 a84103e2cc7846ba6b4bcecda517dc07b64e657b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 14:30:05 -0500 Subject: [PATCH 068/183] 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 6a2cdcfdba5baca614db64c7aeeb91ec286b789d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 6 Feb 2025 14:30:42 -0500 Subject: [PATCH 069/183] 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 b8b2d99b3600d684900f358e736ad94b118b0562 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 8 Feb 2025 22:48:59 -0500 Subject: [PATCH 070/183] 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 1bd69dac80045773db6b6afd22cb21dfd50ca872 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 9 Feb 2025 11:14:07 -0500 Subject: [PATCH 071/183] 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 b25ced6fb4a953654c1f820c66d5d7cd7c349981 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 10 Feb 2025 13:49:03 -0500 Subject: [PATCH 072/183] 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 910afcb7b8343bf387ca076d23087de016f750cc Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 10 Feb 2025 13:49:59 -0500 Subject: [PATCH 073/183] 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 e02dbb40c3695e3cf0ac10a9fcf8e535e6a42970 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 10 Feb 2025 13:50:13 -0500 Subject: [PATCH 074/183] 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 b33e83efb341ffd6b307f8bdd4d38198cb79d889 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 12 Feb 2025 09:39:16 -0500 Subject: [PATCH 075/183] 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 02984ca36f237b7170075693e3b409dc25f849bf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 12 Feb 2025 09:39:43 -0500 Subject: [PATCH 076/183] 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 2ffa7de4f3d6eefa87af91847eb46d2f1943fd10 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 14 Feb 2025 11:06:14 -0500 Subject: [PATCH 077/183] Add option to quiet iterator warnings --- src/iterators.jl | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/iterators.jl b/src/iterators.jl index 2105242a..e2506e9b 100644 --- a/src/iterators.jl +++ b/src/iterators.jl @@ -5,7 +5,9 @@ As a result, a warning is issued every time this function is called. Rather tha *fixing* this bug in this minor/patch version — which would be a breaking change — this is a final release in this major version of the package to notify users of this function that there is a problem. The next major version of the package will likely change the actual -behavior to the one implied by the documentation. To quiet these warnings, you can use +behavior to the one implied by the documentation. To quiet these warnings, you can +temporarily pass the keyword argument `warn=false`, though this will probably be removed in +the next major version. Alternatively, use something like import Logging: with_logger, NullLogger Dit = with_logger(NullLogger()) do D_iterator(...) end @@ -26,8 +28,10 @@ is 0. breaking change — this is a final release in this major version of the package to notify users of this function (and `d_iterator`) that there is a problem. The next major version of the package will likely change the actual behavior to the one implied by this - docstring. To quiet these warnings, you can use `Dit = with_logger(NullLogger()) do - D_iterator(...) end`. + docstring. To quiet these warnings, you can temporarily pass the keyword argument + `warn=false`, though this will probably be removed in the next major version. + Alternatively, use `Dit = with_logger(NullLogger()) do D_iterator(...) end` to catch any + warnings. Note that the returned objects are *views* into the original `D` data — meaning that you may alter their values. @@ -53,13 +57,15 @@ struct D_iterator{VT<:Vector} D::VT ℓₘₐₓ::Int ℓₘᵢₙ::Int - function D_iterator{VT}(D, ℓₘₐₓ, ℓₘᵢₙ=0) where VT + function D_iterator{VT}(D, ℓₘₐₓ, ℓₘᵢₙ=0; warn=true) where VT #@assert ℓₘₐₓ ≥ ℓₘᵢₙ ≥ 0 - @warn iterator_warning + if warn + @warn iterator_warning + end new{VT}(D, ℓₘₐₓ, ℓₘᵢₙ) end end -D_iterator(D::VT, ℓₘₐₓ, ℓₘᵢₙ=0) where VT = D_iterator{VT}(D, ℓₘₐₓ, ℓₘᵢₙ) +D_iterator(D::VT, ℓₘₐₓ, ℓₘᵢₙ=0; warn=true) where VT = D_iterator{VT}(D, ℓₘₐₓ, ℓₘᵢₙ; warn) const Diterator = D_iterator function Base.iterate( @@ -98,8 +104,10 @@ is 0. breaking change — this is a final release in this major version of the package to notify users of this function (and `D_iterator`) that there is a problem. The next major version of the package will likely change the actual behavior to the one implied by this - docstring. To quiet these warnings, you can use `Dit = with_logger(NullLogger()) do - D_iterator(...) end`. + docstring. To quiet these warnings, you can temporarily pass the keyword argument + `warn=false`, though this will probably be removed in the next major version. + Alternatively, use `Dit = with_logger(NullLogger()) do D_iterator(...) end` to catch any + warnings. Note that the returned objects are *views* into the original `d` data — meaning that you may alter their values. @@ -125,13 +133,15 @@ struct d_iterator{VT<:Vector} d::VT ℓₘₐₓ::Int ℓₘᵢₙ::Int - function d_iterator{VT}(d, ℓₘₐₓ, ℓₘᵢₙ=0) where VT + function d_iterator{VT}(d, ℓₘₐₓ, ℓₘᵢₙ=0; warn=true) where VT #@assert ℓₘₐₓ ≥ ℓₘᵢₙ ≥ 0 - @warn iterator_warning + if warn + @warn iterator_warning + end new{VT}(d, ℓₘₐₓ, ℓₘᵢₙ) end end -d_iterator(d::VT, ℓₘₐₓ, ℓₘᵢₙ=0) where VT = d_iterator{VT}(d, ℓₘₐₓ, ℓₘᵢₙ) +d_iterator(d::VT, ℓₘₐₓ, ℓₘᵢₙ=0; warn=true) where VT = d_iterator{VT}(d, ℓₘₐₓ, ℓₘᵢₙ; warn) const diterator = d_iterator function Base.iterate( From e0fe8b691e1d4ec8eba83ad9ed9198e027773319 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 14 Feb 2025 11:45:38 -0500 Subject: [PATCH 078/183] Update @showprogress syntax --- test/wigner_matrices/big_D.jl | 6 +++--- test/wigner_matrices/sYlm.jl | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/wigner_matrices/big_D.jl b/test/wigner_matrices/big_D.jl index 3e1b8173..860d4747 100644 --- a/test/wigner_matrices/big_D.jl +++ b/test/wigner_matrices/big_D.jl @@ -68,7 +68,7 @@ end Random.seed!(123) ℓₘₐₓ = T===BigFloat ? 4 : 8 D_storage = D_prep(ℓₘₐₓ, T) - @showprogress "Compare 𝔇 to formulaic 𝔇 ($T)" for α in αrange(T, 5) + @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(α, β, γ) @@ -103,7 +103,7 @@ end ℓₘₐₓ = T===BigFloat ? 10 : 20 D_storage = D_prep(ℓₘₐₓ, T) d_storage = d_prep(ℓₘₐₓ, T) - @showprogress "Group characters $T" for β in βrange(T) + @showprogress desc="Group characters $T" for β in βrange(T) expiβ = cis(β) d = d_matrices!(d_storage, expiβ) for j in 0:ℓₘₐₓ @@ -142,7 +142,7 @@ end D₂_storage = (𝔇₂, D₁_storage[2:end]...) 𝔇₁₂ = similar(𝔇₁) D₁₂_storage = (𝔇₁₂, D₁_storage[2:end]...) - @showprogress "Representation property ($T)" for R₁ in Rrange(T) + @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₂) diff --git a/test/wigner_matrices/sYlm.jl b/test/wigner_matrices/sYlm.jl index 4d411bb1..d4203661 100644 --- a/test/wigner_matrices/sYlm.jl +++ b/test/wigner_matrices/sYlm.jl @@ -8,7 +8,7 @@ end ## 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 "Test NINJA expressions ($T)" for ι in βrange(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 @@ -35,7 +35,7 @@ end @test_throws ErrorException sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) end - @showprogress "Compare to NINJA expressions ($T)" for spin in -sₘₐₓ:sₘₐₓ + @showprogress desc="Compare to NINJA expressions ($T)" for spin in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) R = from_spherical_coordinates(ι, ϕ) @@ -79,7 +79,7 @@ end ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) - @showprogress "Spin property ($T)" for spin in -sₘₐₓ:sₘₐₓ + @showprogress desc="Spin property ($T)" for spin in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) for γ in γrange(T) @@ -106,7 +106,7 @@ end tol = 4ℓₘₐₓ * eps(T) D_storage = D_prep(ℓₘₐₓ, T) sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) - @showprogress "sYlm vs WignerD ($T)" for s in -sₘₐₓ:sₘₐₓ + @showprogress desc="sYlm vs WignerD ($T)" for s in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) R = from_spherical_coordinates(ι, ϕ) @@ -148,7 +148,7 @@ end ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) - @showprogress "sYlm conjugation ($T)" for ι in βrange(T) + @showprogress desc="sYlm conjugation ($T)" for ι in βrange(T) for ϕ in αrange(T) for γ in γrange(T) for s in -sₘₐₓ:sₘₐₓ From d715a36125a2246007511db60f08db55995703c8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:04:55 -0500 Subject: [PATCH 079/183] 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 4c5a769324656badcd07f48fcf5dd75c110e1469 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:05:15 -0500 Subject: [PATCH 080/183] 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 3bd3a39837f026f8ebd993d853000b39ca199db7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:06:35 -0500 Subject: [PATCH 081/183] 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 5254f9ca4b376c828545dcf7a559e07e2a8602de Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:06:50 -0500 Subject: [PATCH 082/183] 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 88ef4f607c96d5338b5ec080e55e04416e584fae Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 11:11:12 -0500 Subject: [PATCH 083/183] 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 2ef6dcb1bd2c8b214007bb8666d083d0d152a0d5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 12:34:15 -0500 Subject: [PATCH 084/183] 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 c232154ec4b51df0efa7f67b3081da163d76d5f2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 13:06:45 -0500 Subject: [PATCH 085/183] 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 31f7418996b3fca736f02110f14e2c2d470215ea Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 14:56:49 -0500 Subject: [PATCH 086/183] 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 b19e52adfe1d3b356cbf6fb52b529cd36078167d Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 15:06:05 -0500 Subject: [PATCH 087/183] 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 3e0e325872421ce270ce337574eb8f1d586f4ebf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 17 Feb 2025 21:06:20 -0500 Subject: [PATCH 088/183] 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 6f1326e7a5e56f599e2d5bd1a44aabac3288334a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:13:32 -0500 Subject: [PATCH 089/183] 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 fa26c100538f096469c6f7149250d6b74e491e5e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:13:51 -0500 Subject: [PATCH 090/183] 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 ae873162650e211247033767a7d7616803148ed1 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:14:50 -0500 Subject: [PATCH 091/183] 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 4f7a392b8b1003a2e2c935726fca8d6464103cd2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:51:59 -0500 Subject: [PATCH 092/183] 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 4b650711263acc51770edcf0f9101d84634ce5f4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:52:23 -0500 Subject: [PATCH 093/183] 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 5effeaa5837b4350e6273650bc33919ef14c68c3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 11:55:56 -0500 Subject: [PATCH 094/183] 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 36feb361858d109ac6437359cc7af117d4ab6ee3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 13:02:09 -0500 Subject: [PATCH 095/183] 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 9448055db0d603b12bf874f006549b92de86d49e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 18 Feb 2025 14:52:34 -0500 Subject: [PATCH 096/183] 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 6000fc7e30b171df9670951a34979beff369b36f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 24 Feb 2025 21:37:57 -0500 Subject: [PATCH 097/183] 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 3b4a6f39f5d36a04b2a8528361da71c45b1f0347 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 12:17:54 -0500 Subject: [PATCH 098/183] 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 65880feebcb2d752bcc2c87aeabd258699ce735a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 12:41:07 -0500 Subject: [PATCH 099/183] 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 ddd6cc5c68c1419c98a06813ccbaddfc6b42a18c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 12:43:25 -0500 Subject: [PATCH 100/183] 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 3b8fc808d3280ccbe2a20f1b3f5500a6e5c287b6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 13:05:03 -0500 Subject: [PATCH 101/183] 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 9192ecff6b6a3b2359470febce02f7985579c10b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 13:05:29 -0500 Subject: [PATCH 102/183] 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 4d0e1c6e84eca499039bade168d398d9040d0c5b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:22:20 -0500 Subject: [PATCH 103/183] 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 33734074f0dba727abf97fb9548a114275cca607 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:25:21 -0500 Subject: [PATCH 104/183] 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 68d10e9216603ba0be76607cdbb5b7c5a4dd421e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:26:03 -0500 Subject: [PATCH 105/183] 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 c4e59fa4b6405da30406d73144db308f6b5d7416 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:26:26 -0500 Subject: [PATCH 106/183] 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 92e5c19fdd87a662854f8afb7e67d368a453e223 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:26:48 -0500 Subject: [PATCH 107/183] 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 71c7fb9e184a5a1974808752ee02e03178a3f5f5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:28:37 -0500 Subject: [PATCH 108/183] 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 52a8467b501e9214a628c94d9c771c0b430dc693 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 16:29:10 -0500 Subject: [PATCH 109/183] 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 b8364575d990556bdefee71403182e9df09f7f7f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Tue, 25 Feb 2025 17:04:55 -0500 Subject: [PATCH 110/183] 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 1d58d30e515b0e6b5bb2e077c257bf03f14fcc68 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 09:45:16 -0500 Subject: [PATCH 111/183] 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 ef95e1769895bcd754385d18fade7aa05186c10b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 09:46:36 -0500 Subject: [PATCH 112/183] 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 8a2b5b896772c8069a4a24bb064f649f3f807cf2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:26:07 -0500 Subject: [PATCH 113/183] 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 e11b80b7..4d083638 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 b01f4c9c288a8d1c311b655b0a6485d6d89305f8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:27:56 -0500 Subject: [PATCH 114/183] 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 27334c242e8e8e50da58b6e9610f53125f3c523a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:31:19 -0500 Subject: [PATCH 115/183] 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 1122b51c0d0b6ce5be636e98d109381cdcb5f190 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 12:32:49 -0500 Subject: [PATCH 116/183] 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 63c8a23fdad5919731b0df6da46c8a991d2c61af Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:25:24 -0500 Subject: [PATCH 117/183] 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 a026b8332e6d39e4faca8073fb981040fd6aafea Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:25:46 -0500 Subject: [PATCH 118/183] 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 4d083638..1fb65b37 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 6ee64514b2d7102ff89b3fea7a41d439cb0a349b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:30:48 -0500 Subject: [PATCH 119/183] 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 5a18f219a402817eaef8c0565c12eb47ba597da8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:31:43 -0500 Subject: [PATCH 120/183] 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 11d81ef68a67c8ac9e4109e765be6247c9269346 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 13:45:23 -0500 Subject: [PATCH 121/183] 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 2f5e0ff9..bd2e8f83 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 6527bd8ea29beeb8a4a4114c00d0777f60ce21aa Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 14:44:34 -0500 Subject: [PATCH 122/183] 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 c6de08c8ee6b62b5c87f006e3a6b67475f031151 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 14:50:28 -0500 Subject: [PATCH 123/183] 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 17eca1efbf674e250244baea34b436a8c888b372 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 14:54:45 -0500 Subject: [PATCH 124/183] 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 ee5003babbf1b16758b13d61fb7a0d657827b9f9 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Thu, 27 Feb 2025 15:38:13 -0500 Subject: [PATCH 125/183] 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 e0374aacff0afbacf78e94e0a92d23e60223e631 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 28 Feb 2025 20:57:01 -0500 Subject: [PATCH 126/183] 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 df2a85612650c8b166f5cbb8ed1e1184fb746deb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Mar 2025 13:12:59 -0500 Subject: [PATCH 127/183] 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 1fb65b37..50bf0f91 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 869809a652a37f4d4ab7265e3c714990fc205eaa Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 1 Mar 2025 16:13:03 -0500 Subject: [PATCH 128/183] 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 de2b8e92fe80dde40ef298fd2f156651a71d1c4c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 09:12:08 -0500 Subject: [PATCH 129/183] 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 1f9b2ce9c3dba7ee34da16c07fee0c3bd09a73ef Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:46:37 -0500 Subject: [PATCH 130/183] 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 50bf0f91..16d7532b 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 0b676f020951a2996ba6ffbcc11231fd330ed6b6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:47:56 -0500 Subject: [PATCH 131/183] 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 59cc8a3a12ffa827ecfba4d7a88861d098c84947 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:56:00 -0500 Subject: [PATCH 132/183] =?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 1243c33d0924031e077f2ec249dcf236769099be Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 13:57:00 -0500 Subject: [PATCH 133/183] 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 face66ce..cc6f94cd 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 = "0.2, 0.3, 1, 2" 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 4ccf81fec73f78f9387fc195be29bb7a876cb1ab Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 14:27:29 -0500 Subject: [PATCH 134/183] 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 539d6690a164daf24e57d8bf4595d59d1b3886f5 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 16:11:32 -0500 Subject: [PATCH 135/183] 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 b7166960595825e1b42c1b0e8c16e9b3851aa94a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 17:04:54 -0500 Subject: [PATCH 136/183] 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 ab050d9ffb05df38ae3c8c8c871491411b4d511b Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 17:37:23 -0500 Subject: [PATCH 137/183] 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 81a863887a7d812b3dbe3116c6641cb83f0a1d20 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 18:46:15 -0500 Subject: [PATCH 138/183] 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 e855782d12775c2ad7ed52cd5661f0728c6f696e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 19:12:47 -0500 Subject: [PATCH 139/183] 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 0932d9d05bc34876fc47387c1eca845cf18c1a76 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 2 Mar 2025 19:27:58 -0500 Subject: [PATCH 140/183] 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 9b1a4fbf346fd4444235ebdf445c6ead26a1e142 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 00:55:31 -0500 Subject: [PATCH 141/183] 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 54ee30bc8002f5613a27a11d96424086f2fd79ed Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 00:55:39 -0500 Subject: [PATCH 142/183] 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 d54ad7f636c86256bc4d6410bec14130c76e5f76 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 00:56:15 -0500 Subject: [PATCH 143/183] 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 6c99bd116fccf581ab7d40c5a4ec9bf1ddd6a5e3 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 08:00:03 -0500 Subject: [PATCH 144/183] 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 c1cfd52005bc1c5ad346804a1881e22f66b824ef Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 09:02:45 -0500 Subject: [PATCH 145/183] 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 03ed42aee31e0f3898234865a14dba95ba5b5b77 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 09:03:03 -0500 Subject: [PATCH 146/183] 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 c1e83fbda2554571a582d36bed37addb83488bb6 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 10:05:20 -0500 Subject: [PATCH 147/183] 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 98b2761b8e9aeb89557a49c07f2d7837d66b8418 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 10:21:32 -0500 Subject: [PATCH 148/183] 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 bbf82a2f95d18ee72437884cc4b1bb528a36303a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 11:16:48 -0500 Subject: [PATCH 149/183] 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 8e840c2a4509dac760c621e057add6e578d0a9cb Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 11:34:59 -0500 Subject: [PATCH 150/183] 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 a6265701d879427a59553c12a76a9a76444a4e52 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 3 Mar 2025 11:54:40 -0500 Subject: [PATCH 151/183] 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 b6013b0dd63bada86ba4042c8f1005c4aebe02c8 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 5 Mar 2025 09:48:05 -0500 Subject: [PATCH 152/183] 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 220df7fd9471acd564deee9e4efebb4417b1bbe7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 5 Mar 2025 10:55:36 -0500 Subject: [PATCH 153/183] 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 79aacbf6cad59929d5f3dcb124a5dbf73427e26c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 5 Mar 2025 15:00:32 -0500 Subject: [PATCH 154/183] 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 5f1b89c18d45756267b9e94a08331e01eb935822 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 14 Mar 2025 17:29:27 -0400 Subject: [PATCH 155/183] 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 d6f761f2717dfd9d8cb8f7050a95655b51b581be Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 14 Mar 2025 17:29:43 -0400 Subject: [PATCH 156/183] 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 f2f838ef2faab64f4363f0897af7f334fcc415a4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 4 Apr 2025 20:38:43 -0400 Subject: [PATCH 157/183] 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 5853656da71462de93fa5c28b4090c436298d889 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 09:54:47 -0400 Subject: [PATCH 158/183] 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 7be0354f3dede0733c541041da3591d0251f8b55 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 09:59:52 -0400 Subject: [PATCH 159/183] 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 d47ce3b5bdce7d79d405fe4862655397e0b9579f Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 10:00:41 -0400 Subject: [PATCH 160/183] 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 a488e02c4a4ca50f2b3aab2b9272a9f6f993dcc2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 10:09:33 -0400 Subject: [PATCH 161/183] 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 fbc115b67e14e44adb1b1e366019c9e9a8c209e7 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 12:19:20 -0400 Subject: [PATCH 162/183] 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 d86847b27eb9979dac4a45549224cc86f610b7bf Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 13:52:50 -0400 Subject: [PATCH 163/183] 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 4b8ec458f2d600b907303554be9c3823545c9e3c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 14:53:03 -0400 Subject: [PATCH 164/183] Bump compat for Quaternionic and FastTransforms --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index cc6f94cd..687dbf64 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" +FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17" ForwardDiff = "0.10" Hwloc = "2, 3" LinearAlgebra = "1" @@ -34,7 +34,7 @@ LoopVectorization = "0.12" OffsetArrays = "1.10" Printf = "1.11.0" ProgressMeter = "1" -Quaternionic = "0.2, 0.3, 1, 2" +Quaternionic = "0.2, 0.3, 1, 2, 3" Random = "1" SpecialFunctions = "2" StaticArrays = "1" From d9503f1970c6235dbea4135038c2a35bb138ebac Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 17:04:09 -0400 Subject: [PATCH 165/183] 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 687dbf64..5b7b25cc 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 1f70548bb90e9d648de5ca9622558376943c4718 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 17:10:13 -0400 Subject: [PATCH 166/183] 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 0017938914e55217fda24fa14e3a4b214162a983 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sat, 5 Apr 2025 20:44:30 -0400 Subject: [PATCH 167/183] 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 73a81b42f1539a0583ff6ab9ee83b5854a4b2cfe Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 01:50:11 -0400 Subject: [PATCH 168/183] 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 3bde903b9fde84cafe53b4a9674a401f1558a8db Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 02:20:10 -0400 Subject: [PATCH 169/183] 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 c0058f823053e30644fe06fe44e02d7c5a7419bd Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 02:45:41 -0400 Subject: [PATCH 170/183] 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 18fe51f86381988cd2fe50b41fec4fc0612eaf65 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 02:59:14 -0400 Subject: [PATCH 171/183] Keep up with Quaternionic --- Project.toml | 2 +- src/SphericalFunctions.jl | 2 +- src/evaluate.jl | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index 5b7b25cc..130a8ea2 100644 --- a/Project.toml +++ b/Project.toml @@ -34,7 +34,7 @@ LoopVectorization = "0.12" OffsetArrays = "1.10" Printf = "1.11.0" ProgressMeter = "1" -Quaternionic = "0.2, 0.3, 1, 2, 3" +Quaternionic = "3" Random = "1" SpecialFunctions = "2" StaticArrays = "1" 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 diff --git a/src/evaluate.jl b/src/evaluate.jl index 16d7532b..8002a753 100644 --- a/src/evaluate.jl +++ b/src/evaluate.jl @@ -213,13 +213,13 @@ with the result instead. """ function D_matrices(R, ℓₘₐₓ) - D_storage = D_prep(ℓₘₐₓ, eltype(R)) + D_storage = D_prep(ℓₘₐₓ, basetype(R)) D_matrices!(D_storage, R) end function D_matrices(α, β, γ, ℓₘₐₓ) R = Quaternionic.from_euler_angles(α, β, γ) - T = eltype(R) + T = basetype(R) D_storage = D_prep(ℓₘₐₓ, T) D_matrices!(D_storage, R) end @@ -274,7 +274,7 @@ D = D_matrices!(D_storage, R) """ function D_matrices!(D, R, ℓₘₐₓ) - D_storage = (D, Dworkspace(ℓₘₐₓ, eltype(R))...) + D_storage = (D, Dworkspace(ℓₘₐₓ, basetype(R))...) D_matrices!(D_storage, R) end @@ -427,7 +427,7 @@ calculations that could be reused. If you need to evaluate the matrices for man """ function sYlm_values(R::AbstractQuaternion, ℓₘₐₓ, spin) - sYlm_storage = sYlm_prep(ℓₘₐₓ, spin, eltype(R), abs(spin)) + sYlm_storage = sYlm_prep(ℓₘₐₓ, spin, basetype(R), abs(spin)) sYlm_values!(sYlm_storage, R, spin) end @@ -496,7 +496,7 @@ sYlm = sYlm_values!(sYlm_storage, R, spin) """ function sYlm_values!(Y, R::AbstractQuaternion, ℓₘₐₓ, spin) - sYlm_storage = (Y, Y_workspace(ℓₘₐₓ, spin, eltype(R), abs(spin))...) + sYlm_storage = (Y, Y_workspace(ℓₘₐₓ, spin, basetype(R), abs(spin))...) sYlm_values!(sYlm_storage, R, spin) end From 48db00109d6dbcdb5f317ba92f81b1767eea5a16 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 03:08:44 -0400 Subject: [PATCH 172/183] Include compat values for CondaPkg and PythonCall --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 130a8ea2..138c96ed 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 f1e00d135e1008393c7d9b189c82ae2b2eb1cee2 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 11:13:42 -0400 Subject: [PATCH 173/183] 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 27d6a28e88ba64a9d2347291e8248f05e1c567f4 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Sun, 6 Apr 2025 12:46:53 -0400 Subject: [PATCH 174/183] 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 598e59e9efbf3a651cef8301d65728269d734807 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 7 Apr 2025 11:48:52 -0400 Subject: [PATCH 175/183] 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 0d7b0593f09f99d7fff076d1a56b15a8210f2352 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 7 Apr 2025 11:49:46 -0400 Subject: [PATCH 176/183] 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 cff0ace298a28597941d15a74018b269c9341888 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Mon, 7 Apr 2025 13:24:23 -0400 Subject: [PATCH 177/183] 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 9dbda1cd6ebb3415c979e5e8feaaac754d665fc0 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 30 Apr 2025 22:44:39 -0400 Subject: [PATCH 178/183] 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 138c96ed..8619ba75 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 2f89828a0c7c83673dfac2cd744aab9ba91ab38e Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Wed, 30 Apr 2025 22:53:29 -0400 Subject: [PATCH 179/183] =?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 d47c951e8f5917bfb610a33059250e7268ab8781 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 01:06:49 -0400 Subject: [PATCH 180/183] 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 867d2bccf0205d9eaa7fc9fb671b3c03bd08246a Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 01:07:25 -0400 Subject: [PATCH 181/183] 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 958d243f4e691ac9095eb098729fadf33397c196 Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 09:45:26 -0400 Subject: [PATCH 182/183] 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 0d7ebbd4067f30b0dcc7ac0e8c6a58eba6ad2f0c Mon Sep 17 00:00:00 2001 From: Mike Boyle Date: Fri, 2 May 2025 11:00:02 -0400 Subject: [PATCH 183/183] 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