diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..3877f000 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs/literate_input/conventions/comparisons/lalsuite_SphericalHarmonics.c text eol=lf \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aa159a82..e44a3ecc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: - uses: julia-actions/julia-runtest@latest - uses: julia-actions/julia-processcoverage@latest - uses: codecov/codecov-action@v5 - if: "matrix.version == 'nightly' && matrix.os == 'ubuntu-latest'" + if: "matrix.version == '1' && matrix.os == 'ubuntu-latest'" with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 710fb36f..c1b7c259 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,27 @@ benchmark/results.md # Ignore my notes and settings /notes .vscode - +conventions.slides.json rotate.jl +docs/.CondaPkg + +## 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 +docs/src/conventions/calculations/metrics_and_integration.md +docs/src/conventions/comparisons/euler_1767.md +docs/src/conventions/comparisons/clifford_1878.md +docs/src/conventions/comparisons/hamilton_1844.md +docs/src/conventions/comparisons/tait_1868.md +docs/src/conventions/comparisons/gibbs_1881.md +docs/src/conventions/comparisons/wilson_1921.md +docs/src/conventions/comparisons/whittaker_1947.md diff --git a/Project.toml b/Project.toml index e6d4df97..2d9c7edd 100644 --- a/Project.toml +++ b/Project.toml @@ -1,14 +1,17 @@ name = "SphericalFunctions" uuid = "af6d55de-b1f7-4743-b797-0829a72cf84e" +version = "3.0.0-dev" authors = ["Michael Boyle "] -version = "2.2.8" + +[workspace] +projects = ["docs", "test"] [deps] 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" +FixedSizeArrays = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" @@ -22,38 +25,51 @@ TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] AbstractFFTs = "1" Aqua = "0.8" +ArgParse = "1.2" +CondaPkg = "0.2" Coverage = "1.6" DoubleFloats = "1" -ForwardDiff = "0.10, 1" FFTW = "1" +FastDifferentiation = "0.3.17" FastTransforms = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17" +FixedSizeArrays = "1.2.0" +ForwardDiff = "0.10" Hwloc = "2, 3" LinearAlgebra = "1" +Literate = "2.20" Logging = "1.11" LoopVectorization = "0.12" OffsetArrays = "1.10" +Printf = "1.11.0" ProgressMeter = "1" +PythonCall = "0.9" Quaternionic = "3" Random = "1" SpecialFunctions = "2" StaticArrays = "1" Test = "1.11" TestItemRunner = "1" -TestItems = "1" +TestItems = "1.0.0" julia = "1.6" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" 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" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" @@ -61,4 +77,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", "ArgParse", "CondaPkg", "Coverage", "DoubleFloats", "FFTW", "FastDifferentiation", "FastTransforms", "ForwardDiff", "LinearAlgebra", "Literate", "Logging", "OffsetArrays", "Printf", "ProgressMeter", "PythonCall", "Quaternionic", "Random", "StaticArrays", "Test", "TestItemRunner"] diff --git a/docs/CondaPkg.toml b/docs/CondaPkg.toml new file mode 100644 index 00000000..41a8f2df --- /dev/null +++ b/docs/CondaPkg.toml @@ -0,0 +1,3 @@ + +[pip.deps] +galgebra = "" diff --git a/docs/Project.toml b/docs/Project.toml index 528a1a65..733d4f26 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,8 +3,15 @@ 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" +Memoization = "6fafb56a-5788-4b4e-91ca-c0cea6611c73" 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" + +[sources] +SphericalFunctions = {path = ".."} diff --git a/docs/literate_input/conventions/calculations/euler_angular_momentum.jl b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl new file mode 100644 index 00000000..154481fe --- /dev/null +++ b/docs/literate_input/conventions/calculations/euler_angular_momentum.jl @@ -0,0 +1,458 @@ +md""" +# ``L_j`` and ``R_j`` with Euler angles + +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ϵ}\right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}\, 𝐑\right) +\qquad \text{and} \qquad +R_𝐮 f(𝐑) = -\left. i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(𝐑\, e^{-ϵ 𝐮/2}\right), +``` +This is certainly the natural realm for these operators, but it is not the common one. In +particular, virtually all textbooks and papers on the subject define these operators in +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, 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 ``γ``, we can + see that the natural spin-weight operator is ``R_z = i \partial_γ``. Thus, we + define ``R_z = s`` for a function with spin weight ``s``. + 6. The spin-raising operator for ``R_z`` is ``\eth = R_x + i R_y``; the spin-lowering + operator is ``\bar{\eth} = R_x - i R_y``. + +## Analytical groundwork + +We start by defining a new set of Euler angles according to +```math +𝐑_{α', β', γ'} += e^{-ϵ 𝐮 / 2} 𝐑_{α, β, γ} +\qquad \text{or} \qquad +𝐑_{α', β', γ'} += 𝐑_{α, β, γ} e^{-ϵ 𝐮 / 2} +``` +where ``𝐮`` will be each of the basis quaternions, and each of ``α'``, +``β'``, and ``γ'`` is a function of ``α``, ``β``, ``γ``, and +``ϵ``. Then, we note that the chain rule tells us that +```math +\frac{\partial}{\partial ϵ} += +\frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α'} ++ \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β'} ++ \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ'}, +``` +which we will use to convert the general expression for the angular-momentum operators in +terms of ``\partial_ϵ`` into an expression in terms of derivatives with respect to +these new Euler angles: +```math +\begin{align} + L_j f(𝐑_{α, β, γ}) + &= + \left. i \frac{\partial} {\partial ϵ} f \left( e^{-ϵ 𝐞_j / 2} + 𝐑_{α, β, γ} \right) \right|_{ϵ=0} + \\ + &= + i \left[ \left( + \frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α'} + + \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β'} + + \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ'} + \right) f \left(α', β', γ'\right) \right]_{ϵ=0} + \\ + &= + i \left[ \left( + \frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α} + + \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β} + + \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ} + \right) f \left(α, β, γ\right) \right]_{ϵ=0}, +\end{align} +``` +or for ``R_j``: +```math +\begin{align} + R_j f(𝐑_{α, β, γ}) + &= + -\left. i \frac{\partial} {\partial ϵ} f \left( 𝐑_{α, β, γ} + e^{-ϵ 𝐞_j / 2} \right) \right|_{ϵ=0} + \\ + &= + -i \left[ \left( + \frac{\partial α'}{\partial ϵ} \frac{\partial}{\partial α} + + \frac{\partial β'}{\partial ϵ} \frac{\partial}{\partial β} + + \frac{\partial γ'}{\partial ϵ} \frac{\partial}{\partial γ} + \right) f \left(α, β, γ\right) \right]_{ϵ=0}. +\end{align} +``` + +So the objective is to find the new Euler angles, differentiate with respect to +``ϵ``, and then evaluate at ``ϵ = 0``. We do this by first multiplying +``𝐑_{α, β, γ}`` and ``e^{-ϵ 𝐮 / 2}`` in the desired +order, then expanding the results in terms of its quaternion components, and then computing +the new Euler angles in terms of those components according to the usual expression. + +""" + +#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. +import Memoization: @memoize +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 π = sympy.pi +const I = sympy.I +nothing #hide + +# 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` +# 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 +@memoize function 𝒪(u, side) + ## 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₀ = 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 + ))) + + ## 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 + +## 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 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]] + 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 α} + + %$(∂β′∂ϵ) \frac{\partial}{\partial β} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial γ} + \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 α} + + %$(∂β′∂ϵ) \frac{\partial}{\partial β} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial γ} + \right]""" # Display the result in LaTeX form + end + end +end +nothing #hide + +# And we'll need another for the angular-momentum operators in standard ``𝕊²`` form. +conversion(∂) = ∂.subs(Dict(α => ϕ, β => θ, γ => 0)).simplify() +macro display2(expr) + op = string(expr.args[1]) + element = expr.args[2] + arg = Dict(:𝐢 => "x", :𝐣 => "y", :𝐤 => "z", :+ => "+", :- => "-")[element] + @info element + if op == "L" && arg ∈ ("+", "-") + quote + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = ( + ( + ($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 ϕ} \left[ + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial θ} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial ϕ} + \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 θ} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial ϕ} + \right]""" # Display the result in LaTeX form + end + else + quote + ∂φ′∂ϵ, ∂ϑ′∂ϵ, ∂γ′∂ϵ = latex.($conversion.($expr)) # Call expr; format as LaTeX + expr = $op * "_" * $arg # Standard form of the operator + L"""%$expr = -i\left[ + %$(∂ϑ′∂ϵ) \frac{\partial}{\partial θ} + + %$(∂φ′∂ϵ) \frac{\partial}{\partial ϕ} + + %$(∂γ′∂ϵ) \frac{\partial}{\partial γ} + \right]""" # Display the result in LaTeX form + end + end +end +nothing #hide + +#md # ```@raw html +#md #
+#md # ``` + +# ## Full expressions on ``𝕊³`` +# Finally, we can actually compute the Euler components of the angular momentum operators. + +#md # ### ``L`` operators in terms of Euler angles +@display L(𝐢) +#- +@display L(𝐣) +#- +@display L(𝐤) +#- +#md # ### ``R`` operators in terms of Euler angles +@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 — 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{𝒫}`` 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ϵ}\right|_{ϵ=0} f\left(𝐑\, e^{-ϵ 𝐮/2}\right) \\ +# &= +# -\left. i \frac{d}{dϵ}\right|_{ϵ=0} +# f\left(𝐑\, e^{-ϵ 𝐮/2}\, 𝐑^{-1}\, 𝐑\right) \\ +# &= +# -\left. i \frac{d}{dϵ}\right|_{ϵ=0} +# f\left(e^{-ϵ 𝐑\, 𝐮\, 𝐑^{-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. 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 + +# 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 +# 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) * ( + O[1] * f(α, β, γ).diff(α) + + O[2] * f(α, β, γ).diff(β) + + O[3] * f(α, β, γ).diff(γ) + ) + end +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 + +nothing #hide + +# And finally, evaluate each in turn. We expect ``[L_x, L_y] = i L_z`` and cyclic +# permutations: + +#md # ### ``L`` commutators in Euler angles +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 # ### ``R`` commutators in Euler angles +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. + +#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. + + +# ## Standard expressions on ``𝕊²`` +# We can substitute ``(α, β, γ) \to (φ, θ, 0)`` to get the standard expressions for the +# angular momentum operators on the 2-sphere. + +#md # ### ``L`` operators in spherical coordinates +@display2 L(𝐢) +#- +@display2 L(𝐣) +#- +@display2 L(𝐤) + +# 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 # ### ``L_{\pm}`` operators in spherical coordinates +@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_γ`` for an expression on the 2-sphere doesn't +# actually make any sense: ``γ`` isn't even a coordinate for the 2-sphere! However, +# for historical reasons, we include it here when showing the results of the ``R`` operator +# in Euler angles. + +#md # ### ``R`` operators in spherical coordinates +@display2 R(𝐢) +#- +@display2 R(𝐣) +#- +@display2 R(𝐤) + +# We get nonzero components of ``\partial_γ``, showing that these operators really *do +# not* make sense for the 2-sphere, and therefore that it doesn't actually make sense to +# define spin-weighted spherical functions on the 2-sphere; they really only make sense on +# the 3-sphere. Nonetheless, if we stipulate that the function ``\eta`` has a specific spin +# weight, that means that *on the 3-sphere* it is an eigenfunction with ``R_z\eta = +# i\partial_γ \eta = s\eta``. So we could just substitute ``-i s`` for +# ``\partial_γ`` in the expressions above, and recover the standard spin-weight +# operators. We get +# ```math +# \left[R_x + i R_y\right] \eta +# = \left[ +# -i \frac{1}{\sin θ} \frac{\partial}{\partial ϕ} +# + \frac{s}{\tan θ} +# - \frac{\partial}{\partial θ} +# \right] \eta +# = -(\sin θ)^s \left\{ +# \frac{\partial}{\partial θ} +# +i \frac{1}{\sin θ} \frac{\partial}{\partial ϕ} +# \right\} +# \left\{ (\sin θ)^{-s} \eta \right\}. +# ``` +# And in the latter form, we can see that ``R_x + i R_y`` is exactly the spin-raising +# operator ``\eth`` as originally defined by [Newman_1966](@citet) in their Eq. (3.8). The +# 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} +# ``` +# +# 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. diff --git a/docs/literate_input/conventions/calculations/metrics_and_integration.jl b/docs/literate_input/conventions/calculations/metrics_and_integration.jl new file mode 100644 index 00000000..34f2f699 --- /dev/null +++ b/docs/literate_input/conventions/calculations/metrics_and_integration.jl @@ -0,0 +1,207 @@ +md""" +# Metrics and integration + +As something of an exercise, we just work through the basic definitions of metrics and +volume forms in spherical coordinates using SymPy, to verify the volume-form factor that we +use when integrating on the sphere. We then extend this to three-dimensional spherical +coordinates. + + +""" + +#src # Do this first just to hide stdout of the conda installation step. +#src # Note that we can't just use `#hide` because that still shows stdout. +# ````@setup metrics_and_integration +# import PythonCall +# import SymPyPythonCall +# ```` + +# We'll use SymPy (via Julia) since `Symbolics.jl` isn't very good at trig yet. +import PythonCall +import SymPyPythonCall: sympy + +const π = sympy.pi + +## And here are some handy definitions to make manipulations look more natural: +Base.exp(x::PythonCall.Py) = x.exp() +Base.:*(x::SymPyPythonCall.Sym{PythonCall.Py}, q::PythonCall.Py) = x.o * q +(⋅)(a::PythonCall.Py, b::Int) = sympy.S(a.scalar() * b).simplify() +(⋅)(a::PythonCall.Py, b::PythonCall.Py) = sympy.S((a | b.rev()).scalar()).simplify() +nothing #hide + +# ## Three-dimensional space +# Define symbols we will use throughout, including Cartesian coordinates in terms of +# spherical +r, θ, ϕ = sympy.symbols("r θ ϕ", real=true, positive=true, zero=false) +x = r * sympy.sin(θ) * sympy.cos(ϕ) +y = r * sympy.sin(θ) * sympy.sin(ϕ) +z = r * sympy.cos(θ) +nothing #hide + +# The Cartesian metric is just the 3x3 identity matrix +metric_cartesian_3 = sympy.eye(3) + +# so the Cartesian basis ``(𝐱, 𝐲, 𝐳)`` is also an *orthonormal* basis. A point with +# coordinates ``(r, θ, ϕ)`` is separated from the origin by the vector +# ```math +# 𝐩 = r \sin θ \cos ϕ \, 𝐱 + r \sin θ \sin ϕ \, 𝐲 + r \cos θ \, 𝐳, +# ``` +# If we treat a SymPy matrix as containing the components of a vector in the Cartesian +# basis, then we write this as +𝐩 = sympy.Matrix([x, y, z]) +# and the coordinate basis vectors for spherical coordinates are given by differentiating +# this with respect to each of the spherical coordinates: +𝐫 = 𝐩.diff(r) +𝛉 = 𝐩.diff(θ) +𝛟 = 𝐩.diff(ϕ) +nothing #hide + +# We can compute the Jacobian of the coordinate transformation from Cartesian to spherical +# coordinates, and then transform to obtain the spherical metric — the metric *with respect +# to the spherical coordinate basis*: +jacobian_3 = sympy.Matrix([x, y, z]).jacobian([r, θ, ϕ]) +metric_spherical = sympy.simplify((jacobian_3.T * metric_cartesian_3 * jacobian_3)) + +# Recall that there is also an associated *orthonormal* basis, which we can obtain by +# normalizing each of the coordinate basis vectors, which just involves dividing ``𝐫``, +# ``\mathbb{θ}``, and ``\mathbb{ϕ}`` by ``1``, ``r``, and ``r \sin θ`` respectively. But +# that is not the basis we are implicitly using when — say — integrating with spherical +# coordinates, so we must use the above metric. +# +# The volume-form factor is given by the square root of the determinant of the metric: +three_volume_form_factor = sympy.simplify(sympy.sqrt(metric_spherical.det())) + +# Note that SymPy correctly includes the absolute value of the ``\sin θ`` factor, but we +# can drop the absolute value since ``θ`` is in ``(0, π)``: +three_volume_form_factor = three_volume_form_factor.subs(abs(sympy.sin(θ)), sympy.sin(θ)) + +# Restricting to the unit sphere (``r=1``), we integrate naively to find the surface area: +S2_surface_area = sympy.integrate( + sympy.integrate( + three_volume_form_factor.subs(r, 1), + (ϕ, 0, 2π), + ), + (θ, 0, π), +) + +# Therefore, the normalized volume-form factor on the unit sphere 𝕊² is +S2_normalized_volume_form_factor = sympy.simplify( + three_volume_form_factor.subs(r, 1) / S2_surface_area +) + +# ## Four-dimensional space +# +# ### Geometric algebra +# +# Below, we will relate the extended Euler coordinates to Cartesian coordinates in four +# dimensions via the quaternions. First, we set up the geometric algebra of +# three-dimensional space to check our conventions for the quaternion basis elements against +# the `galgebra` Python package. We first import the package and set up a 3D Euclidean +# space with orthonormal basis vectors ``(𝐱, 𝐲, 𝐳)``: + +## This is equivalent to `from galgebra.ga import Ga` in Python: +const Ga = PythonCall.pyimport("galgebra.ga" => "Ga") + +o3d, 𝐱, 𝐲, 𝐳 = Ga.build("𝐱 𝐲 𝐳", g=[1, 1, 1]) +nothing #hide + +# Now we define the quaternion basis elements and the pseudoscalar: +𝐢 = 𝐳 * 𝐲 +𝐣 = 𝐱 * 𝐳 +𝐤 = 𝐲 * 𝐱 +𝐈 = 𝐱 * 𝐲 * 𝐳 +nothing #hide + +# Now we check some basic identities related to these definitions: +PythonCall.pyall([ + 𝐢 == 𝐳*𝐲, + 𝐣 == 𝐱*𝐳, + 𝐤 == 𝐲*𝐱, + 𝐳*𝐲 == -𝐲*𝐳, + 𝐱*𝐳 == -𝐳*𝐱, + 𝐲*𝐱 == -𝐱*𝐲, + 𝐈 == 𝐱*𝐲*𝐳, + 𝐈 == 𝐲*𝐳*𝐱, + 𝐈 == 𝐳*𝐱*𝐲, + 𝐈 == -𝐱*𝐳*𝐲, + 𝐈 == -𝐲*𝐱*𝐳, + 𝐈 == -𝐳*𝐲*𝐱, +]) + +# Next the basic quaternion relations: +PythonCall.pyall([ + 𝐢*𝐣 == 𝐤, + 𝐣*𝐤 == 𝐢, + 𝐤*𝐢 == 𝐣, + 𝐢*𝐣*𝐤 == -1, +]) + +# And the duality relations: +PythonCall.pyall([ + 𝐢 == 𝐈.inv() * 𝐱, + 𝐣 == 𝐈.inv() * 𝐲, + 𝐤 == 𝐈.inv() * 𝐳, +]) + + +# ### Extended-Euler coordinates +# We first define the four-dimensional Cartesian coordinates in terms of Euler angles and a +# radius ``R``: +R, α, β, γ = sympy.symbols("R α β γ", real=true, positive=true, zero=false) +nothing #hide + +# The corresponding quaternion can be written as +𝐐 = R * exp(α * 𝐤 / 2) * exp(β * 𝐣 / 2) * exp(γ * 𝐤 / 2) +nothing #hide + +# Now we define the Cartesian coordinates in four dimensions via the quaternion +# components: +W = 𝐐 ⋅ 1 +#- +X = 𝐐 ⋅ 𝐢 +#- +Y = 𝐐 ⋅ 𝐣 +#- +Z = 𝐐 ⋅ 𝐤 + +# The Cartesian metric in four dimensions is just the 4x4 identity matrix +metric_cartesian_4 = sympy.eye(4) + +# and again we can compute the Jacobian of the coordinate transformation from Cartesian to +# extended-Euler coordinates, and transform to obtain the metric with respect to the +# extended-Euler coordinate basis: +jacobian_4 = sympy.Matrix([W, X, Y, Z]).jacobian([R, α, β, γ]) +metric_extended_euler = sympy.simplify((jacobian_4.T * metric_cartesian_4 * jacobian_4)) + +# And again, the volume-form factor is given by the square root of the determinant of the +# metric: +four_volume_form_factor = sympy.simplify(sympy.sqrt(metric_extended_euler.det())) + +# Again, SymPy correctly includes the absolute value of the ``\sin β`` factor. However, +# with our chosen Euler-angle ranges for ``\mathrm{Spin}(3)`` we take ``β ∈ [0, π]``, so +# ``\sin β ≥ 0`` and we can drop the absolute value for integration. +# +# Restricting to the unit sphere (``R=1``), we integrate naively to find the surface area: +S3_surface_area = sympy.integrate( + sympy.integrate( + sympy.integrate( + four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1), + (γ, 0, 4π), + ), + (β, 0, π), + ), + (α, 0, 2π) +) + +# And finally, we can restrict back to ``\mathrm{SO}(3)`` by taking ``γ ∈ [0, 2π]`` (while +# keeping ``α ∈ [0, 2π]`` and ``β ∈ [0, π]``), and integrating over that range: +SO3_volume = sympy.integrate( + sympy.integrate( + sympy.integrate( + four_volume_form_factor.subs(abs(sympy.sin(β)), sympy.sin(β)).subs(R, 1), + (γ, 0, 2π), + ), + (β, 0, π), + ), + (α, 0, 2π) +) 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/literate_input/conventions/comparisons/blanchet_2024.jl b/docs/literate_input/conventions/comparisons/blanchet_2024.jl new file mode 100644 index 00000000..24326099 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/blanchet_2024.jl @@ -0,0 +1,122 @@ +md""" +# Blanchet (2024) + +!!! info "Summary" + 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 +up-to-date with the latest developments. + +The spherical coordinates are standard physicists' coordinates, implicitly defined by the +direction vector below Eq. (188b): +```math + N_i = \left(\sin θ\cos ϕ, \sin θ\sin ϕ, \cos θ\right). +``` + + +## Implementing formulas + +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. + +""" +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π}} d^{ℓ m}(θ) e^{imϕ}. +# ``` +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^{ℓ m} +# = +# \sum_{k = k_1}^{k_2} +# \frac{(-)^k}{k!} +# e_k^{ℓ m} +# \left(\cos\frac{θ}{2}\right)^{2ℓ+m-2k-2} +# \left(\sin\frac{θ}{2}\right)^{2k-m+2}, +# ``` +# with ``k_1 = \textrm{max}(0, m-2)`` and ``k_2=\textrm{min}(l+m, l-2)``. +function d(l, m, θ::T) where {T<:Real} + 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 +#+ + +# The ``e_k^{ℓ m}`` symbol is defined in Eq. (184c) as +# ```math +# e_k^{ℓ m} = \frac{ +# \sqrt{(ℓ+m)!(ℓ-m)!(ℓ+2)!(ℓ-2)!} +# }{ +# (k-m+2)!(ℓ+m-k)!(ℓ-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 +#+ + +# 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 +#+ + +# ## Tests +# +# We can now test the functions against the equivalent functions from the +# `SphericalFunctions` package. We will test up to +ℓₘₐₓ = 8 +#+ + +# because that's the maximum ``ℓ`` used for PN results — and that's roughly the limit to +# which I'd trust these expressions anyway. We will also only test the +s = -2 +#+ + +# 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: +ϵₐ = 30eps() +ϵᵣ = 1500eps() +#+ + +# This loose relative tolerance is necessary because the numerical errors in Blanchet's +# explicit expressions grow rapidly with ``ℓ``. +for (θ, ϕ) ∈ θϕrange() + for (ℓ, m) ∈ ℓmrange(abs(s), ℓₘₐₓ) + @test Blanchet.Yˡᵐ₋₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(s, ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# These successful tests show that Blanchet's expression agrees with ours. + + +end #hide diff --git a/docs/literate_input/conventions/comparisons/clifford_1878.jl b/docs/literate_input/conventions/comparisons/clifford_1878.jl new file mode 100644 index 00000000..548b79d7 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/clifford_1878.jl @@ -0,0 +1,47 @@ +md""" +# Clifford (1878) + +[Clifford_1878](@citet) introduced Clifford algebras in 1878. The paper starts off with an +introduction to quaternions — though interestingly from a "projective" perspective.[^1] This +first section is a bit muddled from a modern perspective, but Clifford is using it to +develop his ideas. Eventually, he gets to a definition that says + +> the symbols ``ι₁, ι₂, ι₃`` may be taken to mean unit vectors along the axes. + +[^1]: + + That is, in addition to what we would recognize as basis vectors ``ι₁, ι₂, ι₃``, he + includes an element ``ι₀`` that is "at an infinite distance" from the others. This + appears to be because he recognizes the importance of what we now call the even + subalgebra of the Clifford algebra, and wants to be able to include the pseudoscalar in + that subalgebra — which cannot happen with a three-dimensional vector space alone. + +But he actually settles on *negative* squares for these basis elements: + +> We are therefore obliged to write ``ι₂² = -1``, and in a similar way we may find ``ι₁² = +> ι₃² = -1``. + +This differs from our modern treatments that usually assume that the basis vectors square to +``+1``. To compensate for this, he goes on to identify Hamilton's quaternions with products +of pairs of ``ι₁, ι₂, ι₃`` that happen to be exactly the *negative* of modern +definitions — or at least the ones we use here: + +```math +i = ι₂ ι₃, +\quad +j = ι₃ ι₁, +\quad +k = ι₁ ι₂. +``` + +Again, only in later sections does the exposition become clearer and more modern. There, he +returns to quaternions briefly, and manages to describe them in exactly the way we would +today, in terms of the even subalgebra of the Clifford algebra of three-dimensional space. +(He even notes that they are isomorphic to the *full* Clifford algebra of two-dimensional +space.) + +Clifford — like Hamilton and Tait — failed to recognize the vital importance of conjugation +(or "sandwiching") when using quaternions to represent rotations; presumably by analogy with +complex numbers, he only considered left-multiplication by a quaternion as the operation + +""" diff --git a/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl new file mode 100644 index 00000000..03fdf38e --- /dev/null +++ b/docs/literate_input/conventions/comparisons/cohen_tannoudji_1991.jl @@ -0,0 +1,132 @@ +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. + +[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 ϕ \frac{\partial} {\partial θ} + + \frac{\cos ϕ}{\tan θ} \frac{\partial} {\partial ϕ} +\right), +\\ +L_y &= i \hbar \left( + -\cos ϕ \frac{\partial} {\partial θ} + + \frac{\sin ϕ}{\tan θ} \frac{\partial} {\partial ϕ} +\right), +\\ +L_z &= \frac{\hbar}{i} \frac{\partial} {\partial ϕ}. +\end{aligned} +``` +In Complement ``\mathrm{B}_{\mathrm{VI}}`` they define a rotation operator ``R`` as acting +on a state such that [Eq. (21)] +```math +\langle 𝐫 | R | ψ \rangle += +\langle \mathscr{R}^{-1} 𝐫 | ψ \rangle. +``` +For an infinitesimal rotation through angle ``dα`` about the axis ``𝐮``, he +shows [Eq. (49)] +```math +R_{𝐮}(dα) = 1 - \frac{i}{\hbar} dα 𝐋.𝐮. +``` + + +## 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 +#+ + +# 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 get two different, but equivalent, +# expressions in Complement ``\mathrm{A}_{\mathrm{VI}}``. The first is Eq. (26) +# ```math +# Y_{l}^{m}(θ, ϕ) +# = +# \frac{(-1)^l}{2^l l!} \sqrt{\frac{(2l+1)}{4π} \frac{(l+m)!}{(l-m)!}} +# e^{i m ϕ} (\sin θ)^{-m} +# \frac{d^{l-m}}{d(\cos θ)^{l-m}} (\sin θ)^{2l}, +# ``` +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}(θ, ϕ) +# = +# \frac{(-1)^{l+m}}{2^l l!} \sqrt{\frac{(2l+1)}{4π} \frac{(l-m)!}{(l+m)!}} +# e^{i m ϕ} (\sin θ)^m +# \frac{d^{l+m}}{d(\cos θ)^{l+m}} (\sin θ)^{2l}. +# ``` +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.Deprecated.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + @test CohenTannoudji.Y₂(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.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/literate_input/conventions/comparisons/condon_shortley_1935.jl b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl new file mode 100644 index 00000000..cae737be --- /dev/null +++ b/docs/literate_input/conventions/comparisons/condon_shortley_1935.jl @@ -0,0 +1,198 @@ +md""" +# Condon-Shortley (1935) + +!!! info "Summary" + 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 +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 ``ψ`` — +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) ψ(γ j m) += +\hbar \sqrt{(j \mp m)(j \pm m + 1)} ψ(γ j m \pm 1). +``` +This eliminates any *relative* phase ambiguity between modes with neighboring ``m`` values, +and specifically determines what factors of ``(-1)^m`` should be included in the definition +of the spherical harmonics. + +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, θ, φ``". +Immediately before equation (1) of section 4³ (page 50), they define the angular-momentum +operator +```math +L_z = -i \hbar \frac{\partial}{\partial φ}, +``` +which agrees with [our expression](@ref "``L`` operators in spherical coordinates"). This +is followed by equation (8): +```math +\begin{aligned} +L_x + i L_y &= \hbar e^{iφ} \left( + \frac{\partial}{\partial θ} + + i \cot θ \frac{\partial}{\partial φ} +\right) \\ +L_x - i L_y &= \hbar e^{-iφ} \left( + -\frac{\partial}{\partial θ} + + i \cot θ \frac{\partial}{\partial φ} +\right), +\end{aligned} +``` +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. + +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. +""" + +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θʲ +#+ + +# Equation (12) of section 4³ (page 51) writes the solution to the three-dimensional Laplace +# equation in spherical coordinates as +# ```math +# ψ(γ, ℓ, m_ℓ) +# = +# B(γ, ℓ) \Theta(ℓ, m_ℓ) \Phi(m_ℓ), +# ``` +# where ``B`` is independent of ``θ`` and ``φ``, and ``γ`` represents any +# number of eigenvalues required to specify the state. More explicitly, below Eq. (5) of +# section 5⁵ (page 127), they specifically define the spherical harmonics as +# ```math +# ϕ(ℓ, m_ℓ) = \Theta(ℓ, m_ℓ) \Phi(m_ℓ). +# ``` +# One quirk of their notation is that the dependence on ``θ`` and ``φ`` is +# implicit in their functions; we make it explicit, as Julia requires: +function 𝜙(ℓ, mₗ, 𝜃, φ) + Θ(ℓ, mₗ, 𝜃) * Φ(mₗ, φ) +end +#+ + +# The ``φ`` part is given by equation (5) of section 4³ (page 50): +# ```math +# \Phi(m_ℓ) +# = +# \frac{1}{\sqrt{2π}} e^{i m_ℓ φ}. +# ``` +# Again, we make the dependence on ``φ`` explicit, and we capture its type to ensure +# that we don't lose precision when converting π to a floating-point number. +function Φ(mₗ, φ::T) where {T} + 1 / √(2T(π)) * exp(𝒾 * mₗ * φ) +end +#+ + +# Equation (15) of section 4³ (page 52) gives the ``θ`` dependence as +# ```math +# \Theta(ℓ, m) +# = +# (-1)^ℓ +# \sqrt{\frac{(2ℓ+1)}{2} \frac{(ℓ+m)!}{(ℓ-m)!}} +# \frac{1}{2^ℓ ℓ!} +# \frac{1}{\sin^m θ} +# \frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} \sin^{2ℓ}θ. +# ``` +# Again, we make the dependence on ``θ`` explicit, and we capture its type to ensure +# that we don't lose precision when converting the factorials to a floating-point number. +function Θ(ℓ, m, 𝜃::T) where {T} + (-1)^ℓ * T(√(((2ℓ+1) * (ℓ+m)❗) / (2 * (ℓ - m)❗)) * (1 / (2^ℓ * (ℓ)❗))) * + (1 / sin(𝜃)^T(m)) * dʲsin²ᵏθdcosθʲ(j=ℓ-m, k=ℓ, θ=𝜃) +end +#+ + +# It may be helpful to check some values against explicit formulas for the first few +# spherical harmonics as given by Condon-Shortley in the footnote to Eq. (15) of Sec. 4³ +# (page 52). Note the subtle difference between the character `Θ` defining the function +# above and its variant, the character `ϴ`, defining the function below. +ϴ(ℓ, m, 𝜃) = ϴ(Val(ℓ), Val(m), 𝜃) +ϴ(::Val{0}, ::Val{0}, 𝜃) = √(1/2) +ϴ(::Val{1}, ::Val{0}, 𝜃) = √(3/2) * cos(𝜃) +ϴ(::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 # 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: +ϵₐ = 100eps() +ϵᵣ = 1000eps() +#+ + +# 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 θ`` factor in the general form will cause problems at the poles, so we avoid +# the poles by using `βrange` with a small offset: +for θ ∈ θrange(; avoid_poles=ϵₐ/10) + for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) + @test CondonShortley.ϴ(ℓ, m, θ) ≈ CondonShortley.Θ(ℓ, m, θ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# Finally, we can test Condon-Shortley's full expressions for spherical harmonics against +# 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 +# normalization differences, which are the most likely source of error. +for (θ, ϕ) ∈ θϕrange(; avoid_poles=ϵₐ/40) + for (ℓ, m) ∈ ℓmrange(ℓₘₐₓ) + @test CondonShortley.𝜙(ℓ, m, θ, ϕ) ≈ SphericalFunctions.Deprecated.Y(ℓ, m, θ, ϕ) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# This successful test shows that the function ``ϕ`` defined by Condon and Shortley +# agrees with the spherical harmonics defined by the `SphericalFunctions` package. + +end #hide diff --git a/docs/literate_input/conventions/comparisons/euler_1767.jl b/docs/literate_input/conventions/comparisons/euler_1767.jl new file mode 100644 index 00000000..661a0eb7 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/euler_1767.jl @@ -0,0 +1,162 @@ +md""" +# Euler (1767 and 1776) + +!!! info "Summary" + + Euler introduced his eponymous angles in a somewhat more generic form than we use them + today — without actually specifying reference axes or a rotated frame of axes. + Nevertheless, we can interpret his construction in modern terms, corresponding to the + conventions used in this package, though with a curious offset in the ``γ`` angle. + He also used spherical coordinates consistent with our conventions. + +Euler angles are a sequence of three angles representing three *successive rotations* about +a set of orthogonal axes. Several more modern sources incorrectly attribute the +introduction of "Euler angles" to Euler's 1776 papers on rigid-body dynamics [Euler_1776a, +Euler_1776b](@cite). In fact, those papers never used Euler angles at all; they used the +axis-angle representation and the direction-cosine matrix. Euler actually introduced what +we would now call Euler angles in his 1767 paper "Du mouvement d'un corps solide quelconque +lorsqu'il tourne autour d'un axe mobile" ("On the motion of an arbitrary rigid body rotating +about a moving axis") [Euler_1767](@cite). + +This paper was important because Euler fully distinguished between a space-fixed frame and a +body-fixed frame. The space-fixed frame was viewed in reference to the "celestial sphere", +with its origin ``O`` at the center of gravity of the body. Euler's description was +ambiguous, but he did include a figure, reproduced here: + +```@raw html +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + O + C + A + P + Q + L + D + R + B + M + N + Z + Y + X + v + + +
+``` + +```@raw html + +``` +*Euler's Figure 2.* This reproduces a figure from Euler's 1767 paper [Euler_1767](@cite), +showing his conventions for angles describing the orientation of a rigid body in space — +which we now know as Euler angles. He also used spherical coordinates for a point relative +to the body. + +From the figure, we can see that he introduced an orthogonal set of axes ``OA``, ``OD``, and +``OC`` corresponding to what we would now label as ``x``, ``y``, and ``z`` as a right-handed +coordinate system fixed in space. He also assigned the axis ``OM`` to be the body's +principal axis, which we would label as the body-fixed axis ``z''``. While he didn't +introduce a full body-fixed frame at this point,[^1] he did discuss a reference plane +spanned by ``MOL``. We can think of this as the rotated plane spanned by ``COB``, where +``L`` is simply the point at which this rotated plane intersects the "horizon" plane spanned +by ``AOD``. Thus, ``MOL`` tracks the rotation of the body about its axis. + +[^1]: + + Euler later defined an orthogonal set of body-fixed axes ``OS``, ``OT``, and ``OM``. He + never related the former two to the Euler angles, though; they are just used to define + moments of inertia. + +Finally, Euler defines a set of three angles, which we can interpret as corresponding to +three successive rotations. Here we translate from his original: +> - the angle ``ACM`` or the arc ``AP`` = ``p``, +> - the distance of the point ``M`` from the zenith ``C`` or the arc ``CM`` = ``q``, +> - and the angle ``CML`` = ``r``. + +We readily see that these correspond *roughly* to our angles ``α``, ``β``, ``γ``, +respectively — with the important exception that the origin of ``γ`` is different from the +origin of ``r``. Specifically, since ``r`` is measured by ``CML``, ``r=0`` when the +reference direction points back toward the zenith ``C`` from ``M``. This is opposite to +modern conventions, where the reference direction points toward the nadir when ``γ=0``. So, +putting it all together, we have: +```math +\begin{align*} + α &= p, \\ + β &= q, \\ + γ &= π - r. +\end{align*} +``` + +Euler then goes on to define the position of an arbitrary point, labeled ``Z`` in the +figure, by what we would now call Cartesian coordinates relative to the fixed frame, and +also spherical coordinates *relative to the body frame*. First, he progresses from the +origin ``O`` along the ``OA`` axis to the point ``X``, then parallel to ``OD`` to the point +``Y``, and finally parallel to ``OC`` to the point ``Z``. He specifically lists the +distances ``OX=x``, ``XY=y``, and ``YZ=z``, which are precisely what we would label them as. +Then, he defines the distance ``OZ`` as ``s``, the arc ``MN`` as ``u``, and the angle +``LMN`` as ``v``. That is, we would denote these quantities as +```math +\begin{align*} + r &= s, \\ + θ &= u, \\ + ϕ &= v, +\end{align*} +``` +but *only in the body-fixed frame*. + +""" diff --git a/docs/literate_input/conventions/comparisons/gibbs_1881.jl b/docs/literate_input/conventions/comparisons/gibbs_1881.jl new file mode 100644 index 00000000..fde51c8b --- /dev/null +++ b/docs/literate_input/conventions/comparisons/gibbs_1881.jl @@ -0,0 +1,24 @@ +md""" + +# Gibbs (1881) + +Josiah Willard Gibbs was a pioneering American physicist and mathematician, and his work on +vector analysis was influential in the development of modern vector calculus. His 1881 +pamphlet on vector analysis [Gibbs_1881](@cite) was one of the earliest texts to present +vector analysis in a form similar to what we use today. + + +> The letters ``i``, ``j``, ``k`` are appropriated to the designation of a *normal system of +> unit vectors*, i. e., three unit vectors, each of which is at right angles to the other +> two and determined in direction by them in a perfectly definite manner. We shall always +> suppose that ``k`` is on the side of the ``i``-``j`` plane on which a rotation from ``i`` to +> ``j`` (through one right angle) appears counter-clock-wise. In other words, the directions +> of ``i``, ``j``, and ``k`` are to be so determined that if they be turned (remaining +> rigidly connected with each other) so that ``i`` points to the east, and ``j`` to the +> north, ``k`` will point upward. When rectangular axes of ``X``, ``Y``, and ``Z`` are +> employed, their directions will be conformed to a similar condition, and ``i``, ``j``, +> ``k`` (when the contrary is not stated) will be supposed parallel to these axes +> respectively. + + +""" diff --git a/docs/literate_input/conventions/comparisons/hamilton_1844.jl b/docs/literate_input/conventions/comparisons/hamilton_1844.jl new file mode 100644 index 00000000..95d3b4bb --- /dev/null +++ b/docs/literate_input/conventions/comparisons/hamilton_1844.jl @@ -0,0 +1,58 @@ +md""" +# Hamilton (1844 and 1853) + +[Hamilton_1844](@cite) is the foundational work on quaternions, where William Rowan Hamilton +first introduced them in 1844. On page 492, he introduces the quaternion, denoted as ``a + +i b + j c + k d``, or ``(a, b, c, d)``. He quickly establishes the multiplication rules for +the basis elements ``(i, j, k)`` as +```math +\begin{gathered} +i^2 = j^2 = k^2 = -1; +\quad +ij = -ji = k; +\quad +jk = -kj = i; +\quad +ki = -ik = j. +\end{gathered} +``` +He doesn't include the common ``ijk = -1`` explicitly, but it follows from the above. + +He also introduces a parameterization: +```math +a = μ \cos ϱ, +b = μ \sin ϱ \cos ϕ, +c = μ \sin ϱ \sin ϕ \cos ψ, +d = μ \sin ϱ \sin ϕ \sin ψ. +``` +He then says (emphasis in original) + +> I would call ``ϱ`` the *amplitude* of the quaternion; ``ϕ`` its *colatitude*; and ``ψ`` +> its *longitude*. The *modulus* is ``μ``. + +(Note that Hamilton is using the archaic meaning of the word "amplitude", which was the +standard term from complex analysis in the early 1800s, but which we would now call the +"argument" or "angle"; he uses "modulus" where physicists and engineers would now frequently +use "amplitude" or "magnitude".) This is similar to the axis-angle representation of +rotations, where ``ϱ`` is the (whole, not half) angle of rotation, and ``(ϕ, ψ)`` are +spherical coordinates for the axis of rotation — except that this would imply that ``i`` +corresponds to the ``z`` axis if we take the name "colatitude" seriously. And the angle +``ψ`` is measured from the ``j`` axis, so we would probably identify ``j`` with the ``x`` +axis. So ``i, j, k`` correspond to the axes ``z, x, y``. + +There is no mention of "Euler angles" in Hamilton's introduction of quaternions. + + +[Hamilton_1853](@cite) is Hamilton's later monograph on quaternions, published in 1853. In +the table of contents — which is really a collection of summaries of each section — we find +in Lecture II §x this statement (emphasis in original): + +> the symbols ``i, j, k`` come to denote here *three rectangular vector-units* (supposed +> usually, in these Lectures, to be in the directions of *south*, *west*, and *up*) + +This is a *left*-handed system. One sometimes sees claims that the quaternions are somehow +"inherently" left-handed, but this is simply not true; Hamilton could have just as easily +chosen a right-handed system, and later authors generally did. + + +""" diff --git a/docs/literate_input/conventions/comparisons/lalsuite_2025.jl b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl new file mode 100644 index 00000000..15ba491e --- /dev/null +++ b/docs/literate_input/conventions/comparisons/lalsuite_2025.jl @@ -0,0 +1,223 @@ +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` (the LVK Algorithm Library Suite)](@cite LALSuite_2018) is a collection of +routines, comprising the primary official software used by the LIGO-Virgo-KAGRA +Collaboration to detect and characterize gravitational waves. As far as I can tell, the +ultimate source for all spin-weighted spherical harmonics used in `LALSuite` is the function +[`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 + +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 ); +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 +`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 + +import SphericalFunctions: Deprecated + +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 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", + + ## 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) +end +#+ + +# Finally, we just parse the code to turn it into a runnable Julia expression, evaluate it +# inside this module, and we are done defining the module. +eval(Meta.parseall(lalsource)) + +end # module LALSuite +#+ + + +# ## Tests +# +# 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() +ϵᵣ = 100eps() +#+ + +# 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) ≈ + Deprecated.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, β) ≈ + Deprecated.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 ); +# ``` +# Note that this package changed conventions in 2025 to use these signs. +for (α,β,γ) ∈ αβγrange() + for (ℓ, m′, m) ∈ ℓm′mrange(ℓₘₐₓ) + @test LALSuite.XLALWignerDMatrix(ℓ, m′, m, α, β, γ) ≈ + conj(Deprecated.D(ℓ, m′, m, α, β, γ)) atol=ϵₐ rtol=ϵᵣ + end +end +#+ + +# 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 +# ``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 new file mode 100644 index 00000000..d6c8f776 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/ninja_2011.jl @@ -0,0 +1,154 @@ +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. + +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 +#+ + +# We'll also use some predefined utilities to make the code look more like the equations. +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π}} +# d^{l}_{m,s}(\iota) e^{imϕ}. +# ``` +# Just for convenience, we eliminate the negative sign on the left-hand side: +# ```math +# {}^{s}Y_{l,m} = (-1)^{-s} \sqrt{\frac{2l+1}{4π}} +# d^{l}_{m,-s}(\iota) e^{imϕ}. +# ``` +function ₛYₗₘ(s, l, m, ι::T, ϕ::T) where {T<:Real} + (-1)^(-s) * √((2l + 1) / (4T(π))) * d(l, m, -s, ι) * exp(𝒾 * m * ϕ) +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π}} (1+\cos\iota)^2 e^{2iϕ},\\ +# {}^{-2}Y_{2,1} &= \sqrt{\frac{5}{16π}} \sin\iota (1 + \cos\iota) e^{iϕ},\\ +# {}^{-2}Y_{2,0} &= \sqrt{\frac{15}{32π}} \sin^2\iota,\\ +# {}^{-2}Y_{2,-1} &= \sqrt{\frac{5}{16π}} \sin\iota (1 - \cos\iota) e^{-iϕ},\\ +# {}^{-2}Y_{2,-2} &= \sqrt{\frac{5}{64π}} (1-\cos\iota)^2 e^{-2iϕ}. +# \end{aligned} +# ``` +₋₂Y₂₂(ι::T, ϕ::T) where {T<:Real} = √(5 / (64T(π))) * (1 + cos(ι))^2 * exp(2𝒾*ϕ) +₋₂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() + @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.Deprecated.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.Deprecated.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/literate_input/conventions/comparisons/tait_1868.jl b/docs/literate_input/conventions/comparisons/tait_1868.jl new file mode 100644 index 00000000..54894a9b --- /dev/null +++ b/docs/literate_input/conventions/comparisons/tait_1868.jl @@ -0,0 +1,68 @@ +md""" +# Tait (1867 and 1868) + +[Tait_1867](@cite) provided an elementary introduction to quaternions, which was full of +examples and applications. He was quite disinterested in mathematics *per se*; his focus +was on applying mathematics in general — and quaternions specifically — to physics. Recall +that, at this time, vector algebra had not yet been developed, so quaternions were one of +the few available tools for dealing with three-dimensional quantities. (The tragic +quaternion-vector wars would play out over the next few decades, even though Clifford really +unified the two notions.) + +But, as such, this book was a broadly influential introduction to quaternions for many +years. + +In it, he defines the quaternion basis elements ``(i, j, k)`` with multiplication rules +```math +\begin{gathered} +i^2 = j^2 = k^2 = -1, +\qquad +ij = -ji = k, +\qquad +jk = -kj = i, +\qquad +ki = -ik = j, +\\ +ijk = -1. +\end{gathered} +``` +Note that Tait specifically writes expressions like ``xi + yj + zk`` and ``\xi i + \eta j + +\zeta k``; these suggest that Tait does think of ``i`` as corresponding to the ``x`` axis, +``j`` to the ``y`` axis, and ``k`` to the ``z`` axis — evidently unlike Hamilton himself. +Moreover, Tait even says + +> Suppose ``i`` to be drawn eastwards, ``j`` northwards, and ``k`` upwards. + +This is consistent with our standard right-handed system, with the ``k`` axis pointing out +of the page. + + + +[Tait_1868](@cite) discusses the decomposition of a general rotation into three rotations +about successive axes, which we now call "Euler angles". Tait uses the symbols ``(ψ, θ, +ϕ)`` for the three angles. He actually does so just to express that set of rotations in +terms of a single quaternion. + +> Here the vectors ``i``, ``j``, ``k`` in the original position of the body correspond to +``\overline{OA}``, ``\overline{OB}``, ``\overline{OC}`` respectively, at time ``t``. The +transposition is effected by — *first*, a rotation ``ψ`` about ``k``; *second*, a +rotation ``θ`` about the new position of the line originally coinciding with ``j``; +*third*, a rotation ``ϕ`` about the final position of the line at first coinciding with +``k'``. + +So this is what would probably now be called the ``z-y'-z''`` convention for Euler angles +``(ψ, θ, ϕ)``, which is equivalent to ``(ϕ, θ, ψ)`` in the ``z-y-z`` +convention used here. + +Indeed, Tait goes on to derive (somewhat laboriously) the expression for the quaternion: + +```math +q = \cos \frac{ϕ + ψ}{2} \cos \frac{θ}{2} + + i \sin \frac{ϕ - ψ}{2} \sin \frac{θ}{2} + + j \cos \frac{ϕ - ψ}{2} \sin \frac{θ}{2} + + k \sin \frac{ϕ + ψ}{2} \cos \frac{θ}{2}, +``` + +which is exactly the same as our expression from `from_euler_angles(ψ, θ, ϕ)`. + +""" diff --git a/docs/literate_input/conventions/comparisons/whittaker_1947.jl b/docs/literate_input/conventions/comparisons/whittaker_1947.jl new file mode 100644 index 00000000..369c13c4 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/whittaker_1947.jl @@ -0,0 +1,402 @@ +md""" +# Whittaker (1904) + +!!! info "Summary" + + Whittaker consolidated conventions that are still used by physicists today. That + includes a right-handed orthogonal coordinate system labeled ``(x, y, z)``; "Eulerian + angles" that he labels ``(θ, ϕ, ψ)`` but which we would label ``(ϕ, θ, ψ)``, meaning + that we use the same symbols for the same angles, but in different orders; the resulting + spherical coordinates as we now use them, with ``θ`` being colatitude, ``ϕ`` being the + azimuthal angle from the ``x``-axis, and ``ψ=0``; and the standard quaternion basis + elements ``(i, j, k)``, with rotation of a vector ``v`` by a quaternion ``q`` is given + by ``q v q⁻¹``. + +!!! warning + + Whittaker cited the wrong paper for the Euler angles, referring to Euler's 1776 paper on + rigid-body dynamics [Euler_1776a](@cite), when in fact Euler introduced these angles + in his 1767 paper on rigid-body dynamics [Euler_1767](@cite). + +Whittaker's "Analytical Dynamics" [Whittaker_1947](@cite) was the most influential book on +classical mechanics and mathematical physics of the first half of the 20th century. In +particular, quantum physicists found Whittaker's approach to be helpful when inventing their +new branch of physics. It was originally published in 1904, with the 4th edition coming out +in 1947, which was reissued in 1988, with reprintings as late as 1993 — which perhaps says +something about its influence. Sommerfeld's "Lectures on Theoretical Physics" came out +starting in 1943, and Goldstein's "Classical Mechanics" in 1950, marking the end of +Whittaker's dominance. + +For better or for worse, essentially all of the basic conventions used by physicists today +(including many used by this package) were consolidated here. Tragically, that includes a +dismissive snarkiness toward quaternions — presumably because Whittaker, like so many +physicists of his time, was poisoned by the harsh invective against quaternions coming +mostly from Gibbs and Heaviside, belying the impoverished understanding at the turn of the +20th century of the close *interdependence* between quaternions and vectors. Whittaker +often declines to refer to them as "quaternions", referring to them simply as a set of +parameters. He even goes so far as to make the petty and false claim that quaternion +multiplication had been discovered independently by three other people in addition to +Hamilton. His muddled and tendentious treatment of quaternions surely sealed their fate +until computer graphics, aerospace, and robotics applications came to rescue them from +obscurity. + + +## Implementing expressions + +We begin by writing code that implements the concepts described by Ref. +[Whittaker_1947](@cite). We encapsulate the formulas in a module so that we can test them +against the `Quaternionic` and `SphericalFunctions` package. + +""" + +using TestItems: @testitem #hide +@testitem "Whittaker conventions" setup=[ConventionsSetup, Utilities] begin #hide + +module Whittaker +#+ + +# ### Basis vectors and handedness +# +# We start with Sec. I.7, where Whittaker introduces the axis-angle representation: + +# > Let rectangular axes ``Oxyz`` be taken, fixed in space: these will be supposed to form a +# > right-handed system, i.e. if the axes are so placed that ``Oz`` is directed vertically +# > upwards and ``Oy`` is directed to the northern horizon, then ``Ox`` will be directed to +# > the east. + +# (Note that Whittaker will soon distinguish between the system ``OXYZ`` which will be fixed +# in space, and the system ``Oxyz`` which will be fixed in the rotating body.) This is a +# right-handed orthogonal system, which we will represent by the corresponding unit vectors +# as `QuatVec`s: + +import Quaternionic: Quaternionic, 𝐢, 𝐣, 𝐤, ⋅, ×̂ + +const Ox = Quaternionic.𝐢 +const Oy = Quaternionic.𝐣 +const Oz = Quaternionic.𝐤 + +const east = Ox +const north = Oy +const up = Oz + +const south = -north +const west = -east +const down = -up +#+ + +# Whittaker continues: +# +# > Let the displacement considered be equivalent to a rotation through an angle ``ω`` about +# > a line whose direction-angles are ``(α, β, γ)``[...] +# +# The "direction-angles" are the angles that the line makes with the ``Ox``, ``Oy``, and +# ``Oz`` axes, respectively. In modern parlance, we would typically represent this "line" +# with a unit vector, which would have components ``(\cos α, \cos β, \cos γ)``. +line(α, β, γ) = cos(α) * Ox + cos(β) * Oy + cos(γ) * Oz +#+ + +# He then specifies the handedness of the rotation as follows: +# +# > The angle ``ω`` must be taken with its appropriate sign, the sign being positive when +# > the line ``(α, β, γ)`` being directed vertically upwards, the rotation from the southern +# > horizon to the northern is round by the east. +# +# This supposes that ``(α, β, γ) = (π/2, π/2, 0)``, so the "line" is ``𝐳``, and we rotate +# the vector ``-𝐣`` by *increasing* ``ω`` from ``ω=0`` until it reaches ``+𝐣`` at ``ω=π``, +# then we pass through ``+𝐢`` on the way. This is just a standard right-handed rotation, so +# we would write the rotation about this line as +axis_angle_rotation(ω, α, β, γ) = exp((ω / 2) * line(α, β, γ)) +#+ + +# We will test below that the angles the line makes with the axes are what Whittaker +# intended, and that the behavior in this scenario describing handedness is as expected. + + +# ### Quaternions +# +# Section I.9 introduces quaternions in a very awkward way, evidently hobbled by Whittaker's +# spite, driving what we might presume to be his intentional obfuscation of the elegance of +# quaternions. He starts out by arbitrarily introducing four combinations of the axis-angle +# parameters, which so happen to add in quadrature to 1. He then works through some very +# tedious math to eventually show that these four parameters happen to obey the laws of +# quaternion multiplication. He denotes the quaternion (after some evident indecision as to +# whether the scalar component should come first or last) as ``χ + ξi + ηj + ζk``, where +# ``i``, ``j``, ``k`` satisfy +# ```math +# i^2 = j^2 = k^2 = -1, +# \quad +# ij = -ji = k, +# \quad +# jk = -kj = i, +# \quad +# ki = -ik = j. +# ``` +# These are exactly our conventions. Whittaker goes on to say +# +# > The reader who is acquainted with quaternions will observe that the effect of the +# > rotation on any vector ``ρ`` is to convert it into the vector ``qρq⁻¹``, where ``q`` +# > denotes the quaternion ``χ + ξi + ηj + ζk``; the quaternion itself is *not* the +# > rotational operator. +# +# Note the petty little dig attempting to diminish quaternions in the last clause. This is +# exactly how we apply quaternions as well; certain other conventions use ``q⁻¹ρq``. + + +# ### "The Eulerian angles" +# +# Section I.10 is titled "The Eulerian angles". Whittaker is not just saying that the three +# angles he introduces in this section are *akin* to angles that Euler used; he is +# specifically crediting Euler with the particular construction he uses: +# +# > The most practically useful of the various methods of representing parametrically the +# > displacement of a rigid body due to a rotation round a fixed point is likewise due to +# > Euler†: it has the disadvantage of being unsymmetrical, but is otherwise very simple and +# > convenient. +# +# That dagger cites [Euler_1776a](@citet), which discusses the axis-angle representation, +# the direction-cosine matrix, and a reparameterization of the direction-cosine matrix with +# just three angles — which proves that the rotation group is three-dimensional, but *does +# not* provide a parameterization at all related to the one Whittaker now gives, nor can I +# find Euler ever using a similar one: +# +# > Let ``O`` be the fixed point round which the rotation takes place, and let ``OXYZ`` be a +# > right-handed system of rectangular axes fixed in space. Let ``Oxyz`` be rectangular axes +# > fixed relatively to the body and moving with it, and such that before the displacement +# > the two sets of axes ``OXYZ`` and ``Oxyz`` are coincident in position. Let ``OK`` be +# > perpendicular to the plane ``zOZ``, drawn so that if ``OZ`` is directed to the vertical +# > and the projection of ``Oz`` perpendicular to ``OZ`` is directed to the south, then +# > ``OK`` is directed to the east. Denote the angles ``z\hat{O}Z``, ``Y\hat{O}K``, +# > ``y\hat{O}K`` by ``θ``, ``ϕ``, ``ψ``, respectively: these are known as the +# > three *Eulerian angles* defining the position of the axes ``Oxyz`` with reference to the +# > axes ``OXYZ``. +# +# The line ``OK`` is often called the "line of nodes", and is not a very natural object to +# define, except in the case of successive rotations — which, again, is not something that +# Euler did. Nonetheless, Whittaker does not actually specify a sequence of rotations, so +# we have to devise one that will result in these angles between the various vectors (and we +# will test below that this works). +# +# The use of the angle between the initial and final positions of the ``OZ`` axis suggests +# that the direct rotation from one to the other should be one of our sequence, and the two +# in our sequence should be rotations about the inital ``OZ`` axis and the final ``Oz`` +# axis. Moreover, the use of the angles between ``OK`` and the initial and final positions +# of the ``OY`` axis suggest that ``OK`` is really the intermediate position of the ``OY`` +# axis. +# +# So essentially, this rotation is equivalent to an initial rotation about ``OZ`` by ``ϕ``, +# which takes ``OY`` to ``OY'=OK`` (hence ``YÔK=ϕ``); then a rotation about ``OY'=OK`` by +# ``θ`` to take ``OZ`` onto ``OZ'=OZ''=Oz`` (hence ``zÔZ=θ``); finally a rotation about +# ``OZ'=Oz`` by ``ψ``, which takes ``OY'=OK`` onto ``OY''=Oy`` (hence ``yÔK=ψ``). That is, +# in modern parlance this is a ``z``-``y'``-``z''`` rotation. We might write this in +# quaternion notation as +# ```math +# \begin{aligned} +# \exp\left[ \frac{ψ}{2} Oz \right]\, +# \exp\left[ \frac{θ}{2} OK \right]\, +# \exp\left[ \frac{ϕ}{2} OZ \right] +# &= +# \exp\left[ \frac{ϕ}{2} OZ \right]\, +# \exp\left[ \frac{θ}{2} OY \right]\, +# \exp\left[ \frac{ψ}{2} OZ \right] \\ +# &= +# \exp\left[ \frac{ϕ}{2} 𝐤 \right]\, +# \exp\left[ \frac{θ}{2} 𝐣 \right]\, +# \exp\left[ \frac{ψ}{2} 𝐤 \right]. +# \end{aligned} +# ``` +function eulerian_rotation(θ, ϕ, ψ) + OX, OY, OZ = 𝐢, 𝐣, 𝐤 + R₁ = exp((ϕ/2) * OZ) + OK = R₁(OY) + R₂ = exp((θ/2) * OK) + Oz = R₂(R₁(OZ)) + R₃ = exp((ψ/2) * Oz) + R₃ * R₂ * R₁ +end +#+ + +# We'll also need a function to find what Whittaker means by ``OK``, which is the normalized +# cross product ``OZ × Oz``: +function OK(θ, ϕ, ψ) + OZ = 𝐤 + Oz = eulerian_rotation(θ, ϕ, ψ)(OZ) + OZ ×̂ Oz ## Normalized cross product +end +#+ + +# We also implement, for testing purposes, functions to evaluate the three angles Whittaker +# refers to: +function YÔK(θ, ϕ, ψ) + OY = 𝐣 + let OK=OK(θ, ϕ, ψ) + acos(clamp(OY ⋅ OK, -1, 1)) + end +end +function zÔZ(θ, ϕ, ψ) + OZ = 𝐤 + let OK=OK(θ, ϕ, ψ) + Oz = eulerian_rotation(θ, ϕ, ψ)(OZ) + acos(clamp(Oz ⋅ OZ, -1, 1)) + end +end +function yÔK(θ, ϕ, ψ) + OY = 𝐣 + let OK=OK(θ, ϕ, ψ) + Oy = eulerian_rotation(θ, ϕ, ψ)(OY) + acos(clamp(Oy ⋅ OK, -1, 1)) + end +end +#+ + +# Finally, Whittaker provides a table of values for the "direction-cosines" — meaning the +# projections of the rotated basis vectors onto the fixed basis vectors: +function direction_cosine(θ, ϕ, ψ) + [ + ## X Y Z + #= x =# cos(ϕ)cos(θ)cos(ψ)-sin(ϕ)sin(ψ) sin(ϕ)cos(θ)cos(ψ)+cos(ϕ)sin(ψ) -sin(θ)cos(ψ); + #= y =# -cos(ϕ)cos(θ)sin(ψ)-sin(ϕ)cos(ψ) -sin(ϕ)cos(θ)sin(ψ)+cos(ϕ)cos(ψ) sin(θ)sin(ψ); + #= z =# cos(ϕ)sin(θ) sin(ϕ)sin(θ) cos(θ) + ] +end +#+ + +# ### Connecting Eulerian angles to quaternions +# +# In section I.11, Whittaker derives the relationship between the Eulerian angles and the +# quaternion components: +function quaternion_from_eulerian(θ, ϕ, ψ) + ξ = sin(θ/2) * sin((ψ - ϕ)/2) + η = sin(θ/2) * cos((ψ - ϕ)/2) + ζ = cos(θ/2) * sin((ψ + ϕ)/2) + χ = cos(θ/2) * cos((ψ + ϕ)/2) + χ + ξ*𝐢 + η*𝐣 + ζ*𝐤 +end +#+ + +# These are all the conventions we will need from Whittaker. + +end #module Whittaker +#+ + +# ## 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() +#+ + +# ### Basis vectors and handedness +# +# We'll test that the angles the line makes with the axes are what Whittaker intended. +# First, we need a sampling of "direction angles", which should be in the range ``[0, π]``. +# This range is supplied by the `θrange` function: +import Quaternionic: ⋅ +for (α,β,γ) ∈ Iterators.product(θrange(), θrange(), θrange()) + l = Whittaker.line(α, β, γ) + @test acos(l ⋅ Whittaker.Ox) ≈ α atol=ϵₐ rtol=ϵᵣ + @test acos(l ⋅ Whittaker.Oy) ≈ β atol=ϵₐ rtol=ϵᵣ + @test acos(l ⋅ Whittaker.Oz) ≈ γ atol=ϵₐ rtol=ϵᵣ +end +#+ + +# Now we'll test the behavior that +# > the line ``(α, β, γ)`` being directed vertically upwards, the rotation from the southern +# > horizon to the northern is round by the east. +let α=π/2, β=π/2, γ=0 + ω = range(0, π, length=21) + ## Check that the first element takes `south` to `south` + let R = Whittaker.axis_angle_rotation(ω[begin], α, β, γ) + @test R(Whittaker.south) ≈ Whittaker.south atol=ϵₐ rtol=ϵᵣ + end + ## Check that the final element takes `south` to `north` + let R = Whittaker.axis_angle_rotation(ω[end], α, β, γ) + @test R(Whittaker.south) ≈ Whittaker.north atol=ϵₐ rtol=ϵᵣ + end + ## Check that everything in between is "round by the east" by testing that every + ## intermediate point is closer to east than to west + for ωᵢ ∈ ω[begin+1:end-1] + let R = Whittaker.axis_angle_rotation(ωᵢ, α, β, γ) + p = R(Whittaker.south) + @test abs2(p - Whittaker.east) < abs2(p - Whittaker.west) + end + end +end +#+ + +# ### Quaternions +# +# We simply test the multiplication rules: +import Quaternionic: Quaternionic, 𝐢, 𝐣, 𝐤 + +@test 𝐢^2 == 𝐣^2 == 𝐤^2 == -1 +@test 𝐢 * 𝐣 == -𝐣 * 𝐢 == 𝐤 +@test 𝐣 * 𝐤 == -𝐤 * 𝐣 == 𝐢 +@test 𝐤 * 𝐢 == -𝐢 * 𝐤 == 𝐣 +#+ + +# ### "The Eulerian angles" +# The discussion above shows that the angles ``ϕ,θ,ψ`` in that order correspond to what we +# would denote as ``α,β,γ`` in that order. So we rename our utility function for generating +# a variety of angles: +const ϕθψrange = αβγrange +#+ + +# First, we test that the rotation as we've implemented it does correspond to the Euler +# rotation implemented by `Quaternionic`: +for (ϕ,θ,ψ) ∈ ϕθψrange() + @test Whittaker.eulerian_rotation(θ, ϕ, ψ) ≈ Quaternionic.from_euler_angles(ϕ, θ, ψ) +end +#+ + +# Next, we test that it results in the angles between axes that Whittaker described. Note +# that this test is very susceptible to gimbal lock when ``θ`` is near ``0`` or ``π``, so we +# avoid the poles by moving ``θ`` slightly away from them: +avoid_poles = √(eps(Float64)) +#+ + +# Also, because we have to use "direction angles" again, we need to ensure that ϕ and ψ +# are in ``[-π, π]``, so we just subtract ``π`` from each to use the usual generator, and +# only test that the absolute values of those two angles are correct: +for (ϕ,θ,ψ) ∈ ϕθψrange(;avoid_poles) + ϕ = ϕ - π + ψ = ψ - π + @test Whittaker.YÔK(θ, ϕ, ψ) ≈ abs(ϕ) atol=√ϵₐ rtol=√ϵᵣ + @test Whittaker.zÔZ(θ, ϕ, ψ) ≈ θ atol=√ϵₐ rtol=√ϵᵣ + @test Whittaker.yÔK(θ, ϕ, ψ) ≈ abs(ψ) atol=√ϵₐ rtol=√ϵᵣ +end +#+ + +# Now, we'll test that the rotated axes project onto the fixed axes according to Whittaker's +# table of direction-cosines: +for (ϕ,θ,ψ) ∈ ϕθψrange() + X = Whittaker.Ox + Y = Whittaker.Oy + Z = Whittaker.Oz + R = Whittaker.eulerian_rotation(θ, ϕ, ψ) + x = R(X) + y = R(Y) + z = R(Z) + projections = [ + ## X Y Z + #= x =# x ⋅ X x ⋅ Y x ⋅ Z ; + #= y =# y ⋅ X y ⋅ Y y ⋅ Z ; + #= z =# z ⋅ X z ⋅ Y z ⋅ Z + ] + dcm = Whittaker.direction_cosine(θ, ϕ, ψ) + @test projections ≈ dcm atol=10ϵₐ rtol=10ϵᵣ +end +#+ + +# ### Connecting Eulerian angles to quaternions +# +# Finally, we can just test that the quaternion components Whittaker derived are the +# components we've been using. +for (ϕ,θ,ψ) ∈ ϕθψrange() + R₁ = Whittaker.eulerian_rotation(θ, ϕ, ψ) + R₂ = Whittaker.quaternion_from_eulerian(θ, ϕ, ψ) + @test R₁ ≈ R₂ atol=ϵₐ rtol=ϵᵣ +end + +end #@testitem #hide diff --git a/docs/literate_input/conventions/comparisons/wilson_1921.jl b/docs/literate_input/conventions/comparisons/wilson_1921.jl new file mode 100644 index 00000000..a5e61de5 --- /dev/null +++ b/docs/literate_input/conventions/comparisons/wilson_1921.jl @@ -0,0 +1,61 @@ +md""" + +# Wilson (1929) + +Wilson's "Vector analysis" [Wilson_1929](@cite) was, historically, one of the most +influential textbooks on vector analysis in the early 20th century. It was based on the +lectures of [Gibbs](@ref Gibbs-(1881)), and it helped to popularize all aspects of what we +now understand as vector algebra — including the notion of a right-handed coordinate system. +He begins simply: + +> By the ``X``-, ``Y``-, or ``Z``-axis the *positive* half of that axis is meant. The ``X +> Y``-plane means the plane which contains the ``X``- and ``Y``-axis, i.e., the plane +> ``z=0``. + +Now, he gets to the heart of it: + +> In one case (Fig. 8, first part) the ``Z``-axis lies upon that side of the ``X Y``-plane +> on which rotation through a right angle from the ``Z``-axis to the ``Y``-axis appears +> *counterclockwise* or *positive* according to the convention adopted in Trigonometry. +> This relation may be stated in another form. If the ``X``-axis be directed to the right +> and the ``Y``-axis vertically, the ``Z``-axis will be directed toward the observer. Or if +> the ``X``-axis point toward the observer and the ``Y``-axis to the right, the ``Z``-axis +> will point upward. Still another method of statement is common in mathematical physics +> and engineering. If a right-handed screw be turned from the ``X``-axis to the ``Y``-axis +> it will advance along the (positive) ``Z``-axis. Such a system of axes is called +> right-handed, positive, or counterclockwise. + +This is followed by a footnote given another description that is more common now: + +> A convenient right-handed system and one which is always available consists of the thumb, +> first finger, and second finger of the right hand. If the thumb and first finger be +> stretched out from the palm perpendicular to each other, and if the second finger be bent +> over toward the palm at right angles to first finger, a right-handed system is formed by +> the fingers taken in the order thumb, first finger, second finger. + +I've left out some more discussion; in the above, he is just drawing the distinction between +left- and right-handed systems. But eventually, he gets to the final statement of his +choice: + +> In this book the right-handed or counterclockwise system will be invariably employed. + +Finally, he makes the important connection between these vectors and Cartesian coordinates: + +> *Definition:* The three letters ``𝐢``, ``𝐣``, ``𝐤`` will be reserved to denote three +> vectors of unit length drawn respectively in the directions of the ``X``-, ``Y``-, and +> ``Z``-axes of a right-handed rectangular system. +> +> In terms of these vectors, any vector may be expressed as +> ```math +> 𝐫 = x 𝐢 + y 𝐣 + z 𝐤. +> ``` +> The coefficients ``x``, ``y``, ``z`` are the ordinary Cartesian codrdinates of the +> terminus of ``𝐫`` if its origin be situated at the origin of coördinates. The components +> of ``𝐫`` parallel to the ``X``-, ``Y``-, and ``Z``-axes are respectively +> ```math +> x 𝐢, y 𝐣, z 𝐤. +> ``` +> The rotations about ``𝐢`` from ``𝐣`` to ``𝐤``, about ``𝐣`` from ``𝐤`` to ``𝐢``, and +> about ``𝐤`` from ``𝐢`` to ``𝐣`` are all positive. + +""" diff --git a/docs/make.jl b/docs/make.jl index b6eec523..4ced5ff5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,47 +1,107 @@ # 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 + +# 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 -using SphericalFunctions using Documenter +using Literate using DocumenterCitations + +docs_src_dir = joinpath(@__DIR__, "src") +package_root = dirname(@__DIR__) + +# Run `make_literate.jl` to generate the literate files +include(joinpath(@__DIR__, "make_literate.jl")) + + bib = CitationBibliography( - joinpath(@__DIR__, "src", "references.bib"); + joinpath(docs_src_dir, "references.bib"); #style=:authoryear, ) -DocMeta.setdocmeta!(SphericalFunctions, :DocTestSetup, :(using SphericalFunctions); recursive=true) +using SphericalFunctions +using SphericalFunctions.Deprecated + +DocMeta.setdocmeta!( + SphericalFunctions, + :DocTestSetup, + :(using SphericalFunctions; using SphericalFunctions.Deprecated); + recursive=true, + warn=false, +) makedocs( plugins=[bib], sitename="SphericalFunctions.jl", - modules = [SphericalFunctions], + modules = [SphericalFunctions, SphericalFunctions.Deprecated], format = Documenter.HTML( prettyurls = !("local" in ARGS), # Use clean URLs, unless built as a "local" build edit_link = "main", # Link out to "main" branch on github canonical = "https://moble.github.io/SphericalFunctions.jl/stable/", - assets = String["assets/citations.css"], + assets = String["assets/citations.css", "assets/extras.css"], ), pages = [ "index.md", - "transformations.md", - "wigner_matrices.md", - "sYlm.md", - "operators.md", - "utilities.md", + "Background" => [ + "background/domain.md", + "background/operators.md", + "background/sYlm_and_Dlmpm.md", + "background/mode_weights.md", + "background/transformations.md", + ], + "Interface" => [ + "interface/wigner_matrices.md", + "interface/sYlm.md", + "interface/transformations.md", + "interface/operators.md", + "interface/utilities.md", + ], + "Conventions" => [ + "conventions/summary.md", + "conventions/details.md", + "conventions/comparisons.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"))) + ), + ], "API" => [ - "internal.md", - "functions.md", + "api/internal.md", + "api/functions.md", ], "Notes" => map( - s -> "notes/$(s)", - sort(readdir(joinpath(@__DIR__, "src/notes"))) + s -> joinpath("notes", s), + sort(readdir(joinpath(docs_src_dir, "notes"))) ), + "Development" => [ + "development/index.md", + "development/literate_testitems.md", + ], + "Deprecated" => [ + "deprecated/index.md", + ], + "index_of_docstrings.md", "References" => "references.md", ], - #warnonly=true, - #doctest = false + warnonly=true, + #doctest = false, + #draft=true, # Skips running code in the docs for speed ) deploydocs( @@ -49,3 +109,5 @@ deploydocs( devbranch="main", push_preview=true ) + +println("Docs built in ", time() - start, " seconds.\n") diff --git a/docs/make_literate.jl b/docs/make_literate.jl new file mode 100644 index 00000000..ad26ac2d --- /dev/null +++ b/docs/make_literate.jl @@ -0,0 +1,75 @@ +# 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`. + +# 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 +) +literate_input = joinpath(@__DIR__, "literate_input") + +# 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 + +# 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 the literate script + inputfile = joinpath(root, file) + # 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 diff --git a/docs/src/functions.md b/docs/src/api/functions.md similarity index 100% rename from docs/src/functions.md rename to docs/src/api/functions.md diff --git a/docs/src/api/internal.md b/docs/src/api/internal.md new file mode 100644 index 00000000..24f02b6d --- /dev/null +++ b/docs/src/api/internal.md @@ -0,0 +1,61 @@ +# Internal functions + +There are various functions that are only used internally, some of which are likely +to be deprecated in the near future. These are documented here for completeness. + +## ``H`` recursion and ALFs + +The fundamental algorithm is the ``H`` recursion, which is the core computation +needed for Wigner's ``d`` and ``𝔇`` matrices, and the spin-weighted spherical +harmonics ``{}_{s}Y_{ℓ,m}``, as well as `map2salm` functions. + +```@autodocs +Modules = [SphericalFunctions.Deprecated] +Pages = ["deprecated/Hrecursion.jl"] +``` + +Internally, the ``H`` recursion relies on calculation of the Associated Legendre +Functions (ALFs), which can also be called on their own: + +```@autodocs +Modules = [SphericalFunctions.Deprecated] +Pages = ["deprecated/associated_legendre.jl"] +``` + +The function ``{}_{s}\lambda_{ℓ,m}`` is defined as essentially ``{}_{s}Y_{ℓ,0}``, and is important internally for computing the ALFs. We have some important utilities for computing it: + +```@docs +SphericalFunctions.Deprecated.λ_recursion_initialize +SphericalFunctions.Deprecated.λ_iterator +SphericalFunctions.Deprecated.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 +only one with remaining documentation is [`ₛ𝐘`](@ref SphericalFunctions.Deprecated.ₛ𝐘), which could probably be +replaced by [`sYlm_values`](@ref SphericalFunctions.Deprecated.sYlm_values), except that the default pixelization is +[`golden_ratio_spiral_rotors`](@ref), which makes it very convenient for +interacting with [`SSHT`](@ref SphericalFunctions.Deprecated.SSHT). + +```@docs +SphericalFunctions.Deprecated.ₛ𝐘 +SphericalFunctions.Deprecated.Y +SphericalFunctions.Deprecated.d +SphericalFunctions.Deprecated.D +``` + + +# Transformation + +The newer [`SSHT`](@ref SphericalFunctions.Deprecated.SSHT) interface is more efficient for most purposes, but this +package used to use functions named `map2salm`, which is still present, but may +be deprecated. + +```@autodocs +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/map2salm.jl"] +``` diff --git a/docs/src/assets/Makefile b/docs/src/assets/Makefile new file mode 100644 index 00000000..58f52cb0 --- /dev/null +++ b/docs/src/assets/Makefile @@ -0,0 +1,28 @@ +# Define variables +TEX=composition_diagram.tex +STANDARD_DVI=composition_diagram.dvi +STANDARD_SVG=composition_diagram.svg + +.PHONY: all clean allclean + +all: $(STANDARD_SVG) clean + +$(STANDARD_DVI): $(TEX) + @echo "Compiling diagram to DVI..." + lualatex -output-format=dvi $(TEX) + +$(STANDARD_SVG): $(STANDARD_DVI) + @echo "Converting DVI to SVG..." + dvisvgm --libgs=/opt/homebrew/lib/libgs.dylib -o $(STANDARD_SVG) $(STANDARD_DVI) + +clean: + @echo "Cleaning up extra generated files..." + rm -f composition_diagram*.aux composition_diagram*.log composition_diagram*.out + 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..." + rm -f composition_diagram*.svg diff --git a/docs/src/assets/citations.css b/docs/src/assets/citations.css index 7bb7d01c..0bb3e83f 100644 --- a/docs/src/assets/citations.css +++ b/docs/src/assets/citations.css @@ -12,7 +12,7 @@ .citation ul { padding: 0 0 2.25em 0; margin: 0; - list-style: none; + list-style: none !important; } .citation ul li { text-indent: -2.25em; diff --git a/docs/src/assets/composition_diagram.tex b/docs/src/assets/composition_diagram.tex new file mode 100644 index 00000000..4c03a39a --- /dev/null +++ b/docs/src/assets/composition_diagram.tex @@ -0,0 +1,27 @@ +\documentclass[tikz]{standalone} +\usepackage{tikz-cd} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{amsfonts} +\usepackage{xcolor} +%\usepackage{fontspec} % For custom fonts + +% 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}[every label/.append style = {font = \normalsize}] + 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/extras.css b/docs/src/assets/extras.css new file mode 100644 index 00000000..ecbf9dc5 --- /dev/null +++ b/docs/src/assets/extras.css @@ -0,0 +1,97 @@ +.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; +} + +div .composition-diagram { + display: block; + text-align: center; + margin-top: 60px; + height: 130px; + transform: scale(1.94326); + transform-origin: center; +} + +.composition-diagram svg text.f0 { + fill: currentColor; + font-family: 'KaTeX_AMS'; + font-size: 9.96264px; /* Adjust as needed */ +} + +.composition-diagram svg text.f1 { + fill: currentColor; + font-family: 'KaTeX_Math'; + font-style: italic; + font-size: 9.96264px; /* Adjust as needed */ +} + +.composition-diagram svg path { + stroke: currentColor; + fill: none; +} + +.euler-figure2 { + display: block; + text-align: center; + margin: 0 auto; +} + +/* Make the inline SVG scale responsively */ +.euler-figure2 svg { + width: min(900px, 100%); + height: auto; +} + +/* Ensure all strokes/fills follow the page's current text color */ +.euler-figure2 svg path { + stroke: currentColor; +} + +.euler-figure2 svg text, +.euler-figure2 svg circle, +.euler-figure2 svg rect { + fill: currentColor; + stroke: currentColor; +} + +.euler-figure2 svg text.label { + font-family: 'KaTeX_Math', 'KaTeX_Main', 'KaTeX_AMS', serif !important; + font-style: italic !important; + font-weight: 400 !important; /* avoid bold */ + font-size: 14px !important; /* adjust to taste */ + fill: currentColor !important; + dominant-baseline: middle !important; +} + +.euler-figure2 svg text { + font-weight: 400 !important; +} + +.euler-figure2 + p { + text-align: center; + font-size: 0.95em; + margin-top: 0.4rem; + margin-bottom: 1.2rem; + margin-left: 5rem; + margin-right: 5rem; + max-width: 100%; +} + +.euler-figure2 + p em { + font-style: italic; +} diff --git a/docs/src/background/domain.md b/docs/src/background/domain.md new file mode 100644 index 00000000..426b0a9b --- /dev/null +++ b/docs/src/background/domain.md @@ -0,0 +1,271 @@ +# [Domain](@id background_domain) + +This package deals with standard spherical harmonics, spin-weighted +spherical harmonics, and Wigner's 𝔇 matrices. The key question is +what domains these functions are defined on — what their arguments +are. Here, we will argue that it's best to use quaternions for all +three. The rest of this package and documentation use the language of +quaternions, though we make contact with the more traditional +parameterizations in terms of spherical coordinates and Euler angles +as needed. + +## Three functions on two domains + +Usually, these are written as functions of spherical coordinates for +both types of harmonics, and Euler angles for the 𝔇 matrices: +```math +\begin{gathered} +Y_{ℓ,m}(θ, ϕ), \\ +{}_sY_{ℓ,m}(θ, ϕ), \\ +𝔇^{(ℓ)}_{m', m}(α, β, γ). +\end{gathered} +``` +While that's good enough for many purposes, it obscures the true +nature of these functions, and makes certain manipulations more +difficult or even impossible. We can make a few observations: + + 1. Standard (scalar) spherical harmonics are just a special case of + spin-weighted spherical harmonics with spin weight ``s=0``. + 2. Spin-weighted spherical harmonics *cannot* be defined as + functions on the sphere ``𝕊²`` alone; they depend on an + additional choice of frame at each point on the sphere — a choice + of basis for the tangent space at that point. This extra + structure is necessary to define what "spin weight" means at all. + (We'll define it [soon](@ref background_differential_operators).) + 3. Euler angles are a bad way to deal with rotations. Besides the + coordinate singularities at the poles, they make composing + rotations far more difficult. More importantly, they make it + hard to deal with *generators* of rotations, which are crucial + for angular-momentum operators. + +The second point especially requires a little more explanation. In +fact, I wrote an entire paper that gets very deep into the details +about this point [Boyle_2016](@cite). It may seem odd that +spin-weighted spherical functions cannot be defined on the sphere +``𝕊²``, when they're written in terms of spherical coordinates. The +key subtlety here is that the spherical coordinates ``(θ, ϕ)`` +implicitly define a conventional choice of tangent basis. But spin +weight is *defined* in terms of what happens to a function when you +rotate that tangent basis. Thus, if we start from spherical +coordinates, we are naturally led to introduce a third angle ``ψ`` +describing rotation of the tangent basis about the radial direction. +And those three angles together are just Euler angles. In fact, ``(θ, +ϕ, ψ)`` is *precisely* the set of "Eulerian" angles defined by the +very influential [book by Whittaker](@ref Whittaker-(1904)) +[Whittaker_1947](@cite). It turns out [GoldbergEtAl_1967](@cite) that +with this extra angle included explicitly, the original spin-weighted +spherical harmonics [Newman_1966](@cite) are simply proportional to +Wigner's 𝔇 matrices, +```math +{}_{s}Y_{ℓ,m}(θ, ϕ, ψ) += (-1)^s \sqrt{\frac{2ℓ+1}{4π}} \, 𝔇^{(ℓ)}_{m, -s}(ϕ, θ, ψ), +``` +so we might as well think of them as being parameterized in the same +way. + +## The geometric picture + +It might be helpful to pause for a moment and consider a more +intuitive physical picture. Consider a telescope. We can point it +any which way we want, which corresponds to choosing a point on the +sphere ``𝕊²``. Now, imagine an ideal pixel at the very center of the +image formed by that telescope. The entire sky can be described in +terms of that pixel as a function of where the telescope is pointed. +That is, it's a function of ``𝕊²``. But now, suppose we put a +polarizer on the telescope. The pixel's value will now depend not +only on where the telescope is pointed, but also on the orientation of +the polarizer about the optical axis. This extra degree of freedom is +exactly the extra angle ``ψ`` we mentioned above. + +But really, what we're doing is rotating our telescope in +three-dimensional space, so the pixel's value is a function on the +*rotation group* ``\mathrm{SO}(3)``, not just the sphere ``𝕊²``. +Now, with light, we know every possible polarized value once we +measure the value with one polarization and the value with the +orthogonal polarization, which are combined into a single complex +number. (Compare [Jones +vectors](https://en.wikipedia.org/wiki/Jones_calculus).) As we rotate +our choice of the first polarization by an angle ``ψ``, the complex +combination varies as ``e^{iψ}``. This is exactly what it means to +have spin weight ``s=1`` — which is, not coincidentally, the spin of a +photon. + +Imagining that we had a polarizing telescope measuring some other kind +of field with spin ``s``, the complex combination would vary as +``e^{isψ}``. (This is important for gravitational-wave astronomy with +spin-2 fields. In principle, we could also consider neutrino +telescopes with spin-1/2 polarization.) And since we could have +half-integer spins, we actually need to consider not just the rotation +group ``\mathrm{SO}(3)``, but its double cover ``\mathrm{Spin}(3) +\cong \mathrm{SU}(2)``, to fully capture the behavior of general +fields. + +## Unification in ``\mathrm{Spin}(3)`` + +So, at this point, we see that all three of the functions we care +about — standard spherical harmonics, spin-weighted spherical +harmonics, and Wigner's 𝔇 matrices — are naturally thought of as +functions on ``\mathrm{Spin}(3)``. The traditional way to +parameterize this would be with (extended) Euler angles. However, as +mentioned above, Euler angles are a poor choice for many purposes. +First, we have the problem of coordinate singularities at the "poles" +(aka ["gimbal lock"](https://en.wikipedia.org/wiki/Gimbal_lock)). +This is not a problem if we use quaternions instead. Second, we have +the problem of composing rotations; given rotations described by Euler +angles ``(α, β, γ)`` and ``(α', β', γ')``, finding another set of +angles ``(α'', β'', γ'')`` representing their composition is a nasty, +nonlinear problem. With quaternions, composition is just quaternion +multiplication: ``R'' = R'\, R``. Third, and most importantly, we +have the problem of generators of rotations. With Euler angles, +infinitesimal rotations are described by complicated derivatives with +bizarre trigonometric factors. With quaternions, infinitesimal +rotations are described by "pure-vector" quaternions that can be +exponentiated almost as simply as imaginary numbers. That is, if +``𝐮`` is a unit quaternion with 0 scalar part, and ``θ`` is a real +number, then +```math +\exp \left[ 𝐮 \frac{θ}{2} \right] += \cos \frac{θ}{2} + 𝐮 \sin \frac{θ}{2} +``` +is a unit quaternion representing a rotation by ``θ`` about ``𝐮``. +Note the similarity to Euler's formula for complex exponentials. +Alternatively, going from a unit quaternion to its generator just uses +the logarithm, which is only slightly more complicated than the +complex logarithm. + +So the conclusion is clear: we should represent the arguments of all +these functions as unit quaternions, which is what this package does: +```math +\begin{gathered} +Y_{ℓ,m}(𝐐), \\ +{}_sY_{ℓ,m}(𝐐), \\ +𝔇^{(ℓ)}_{m', m}(𝐐). +\end{gathered} +``` +We can still provide convenience methods that convert from spherical +coordinates or Euler angles to quaternions, but internally everything +is done with quaternions, and the documentation will generally be +written in terms of quaternions. See [Boyle_2016](@citet) for full +details. + +## Pushing forward to ``𝕊²`` + +Assuming the functions have been defined on ``\mathrm{Spin}(3)``, we +can push them forward to functions on the sphere ``𝕊²``.[^1] Given a +choice of a special point in ``𝕊²`` — conventionally the north pole +``𝐳`` — we can map ``𝐐 ∈ \mathrm{Spin}(3)`` to ``𝕊²`` simply by +using it to rotate ``𝐳`` to ``π(𝐐) = 𝐐\, 𝐳\, 𝐐⁻¹``. For any +particular point ``𝐧 ∈ 𝕊²``, the set of all rotors that map to that +point (its preimage) is of the form +```math +π^{-1}(𝐧) = \left\{𝐐\, e^{𝐤 θ/2} \,\middle|\, θ ∈ [0, 4π)\right\}, +``` +where ``𝐐`` is any particular rotor that maps ``𝐳`` to ``𝐧``, and +``𝐤`` is the generator of rotations about the ``z`` axis. In terms +of the telescope analogy presented above, ``θ`` represents rotation of +the telescope polarizer about the optical axis. + +[^1]: Okay, technically we'll use more structure than just ``𝕊²``. + We're picking out a special point in that space, and assuming the + action of ``\mathrm{Spin}(3)`` on that point to define the + mapping. Elsewhere, I criticize the usual approach because it + ignores the fundamental importance of the choice of tangent basis + at each point. This pushforward to ``𝕊²`` also uses some extra + structure — perhaps reinforcing the notion that spin-weighted + functions really should just be thought of as functions on + ``\mathrm{Spin}(3)``. + +Now, for any function ``f(𝐐)`` on ``\mathrm{Spin}(3)``, we can define +the pushforward function on ``𝕊²`` by taking a point to the average +value of ``f`` over the preimage. (Such "integration along the fiber" +is a standard concept in differential geometry [BottTu_1982](@cite).) +By abuse of notation, we will use the same symbol ``f`` for this new +function, with the understanding that the symbol will be reinterpreted +as needed. Thus, for ``𝐧 ∈ 𝕊²``, we define +```math +f(𝐧) = \frac{1}{4π} \int_0^{4π} f\left(𝐐\, e^{𝐤 θ/2}\right) \, dθ, +``` +where ``𝐐`` is any rotor such that ``π(𝐐) = 𝐧``. The choice of +``𝐐`` does not matter because the integral averages over all possible +choices. Also, given the behavior ``e^{isψ}`` described above, we +know that only fields with ``s=0`` will have nonzero values under this +operation. Thus, only functions with spin weight 0 can be pushed +forward to nontrivial functions on ``𝕊²``. And again, because the +standard scalar spherical harmonics are precisely the spin-weighted +spherical harmonics with ``s=0``, they can be pushed forward in this +way. + +This is effectively the only continuous way to push functions forward +to *all of* ``𝕊²``. For functions with nonzero spin-weight, we need +a reference direction to specify the tangent basis. However, the +[hairy-ball theorem](https://en.wikipedia.org/wiki/Hairy_ball_theorem) +tells us this cannot be done continuously over the entire sphere. +Originally, the spin-weighted spherical harmonics were not defined on +``𝕊²``, but the spherical coordinates — which are topologically the +cylinder ``I×𝕊¹``. + +## Pulling back to ``I×𝕊¹`` + +Finally, we can come back to spherical coordinates: the coordinates +``(θ, ϕ)``, with ``θ ∈ [0, π]`` and ``ϕ ∈ [0, 2π)``. Note that this +range for ``ϕ`` is half open, only because it is parameterizing a +circle; ``0`` and ``2π`` represent the same point. Thus, we see that +the spherical coordinates do not actually parameterize a sphere, but +rather a cylinder. Of course, that cylinder is usually mapped onto +the sphere, shrinking the top and bottom edges to points which +represent the poles of the sphere and therefore the singularities of +the spherical coordinates. Nonetheless, a function defined on +spherical coordinates is really a function on the cylinder ``I×𝕊¹``. + +When we write *spin-weighted* functions in terms of spherical +coordinates, we are not only quietly using the "wrong" domain, but +also implicitly choosing a tangent basis at each point. Specifically, +the ``\boldsymbol{θ}`` unit vector points directly down the cylinder, +mapping to a vector field that everywhere points toward the south pole +on the sphere. This mapping fails to give a unique tangent vector at +both poles, of course, which is consistent with the hairy-ball +theorem. Still, it gives us a well defined function *almost +everywhere* on the sphere. Interestingly, the function is well +defined *everywhere* on the cylinder. These two considerations — the +actual topology and the implicit choice of tangent basis — show that +it really is more correct to think of spin-weighted spherical +functions written in spherical coordinates as being defined on the +cylinder ``I×𝕊¹``. + +Now, interestingly, once we've picked out a basis, we can actually +define the *unique* rotor +```math +𝐐(θ, ϕ) = e^{ϕ𝐤/2}\, e^{θ𝐣/2}. +``` +Conflating spherical coordinates with geometry of ``𝕊²`` again for a +moment, we can think of this mapping as taking the north pole ``𝐳`` +to the point on the sphere with coordinates ``(θ, ϕ)``, and the vector +``𝐱`` onto ``\boldsymbol{θ}`` at that point. + +The key point is that, in this case, the mapping doesn't *start* with +``\mathrm{Spin}(3)``, but rather *ends* there. Thus, we can define a +pullback of a function ``f(𝐐)`` to a function ``f(θ, ϕ)`` in the +usual way — simple composition: +```math +f(θ, ϕ) = f\big(𝐐(θ, ϕ)\big). +``` +This is defined for all spin weights, because we've chosen a tangent +basis at each point. However, if we then try to "wrap" the cylinder +back onto the sphere, we cannot define the function at the poles for +nonzero spin weights, because the tangent basis is not uniquely +defined there. That is, the function value will not be independent of +the choice of ``ϕ`` at the poles, so the "wrapping" is not well +defined. + +Still, this approach does give us the value of the function for any +particular point on ``𝕊²`` *and* a particular choice of tangent +direction at that point. Moreover, for a function with a definite +spin weight ``s``, this is enough information to reconstruct the value +of the function for *any other* choice of tangent direction at that +same point. This is how we can quite reasonably write spin-weighted +spherical harmonics as functions of spherical coordinates, even though +they cannot be defined as functions on the sphere itself. Of course, +using the fundamental domain is a much more powerful and flexible +approach; among other things, it allows us to define differential +operators acting on these functions, which we will do [next](@ref +background_differential_operators). diff --git a/docs/src/background/mode_weights.md b/docs/src/background/mode_weights.md new file mode 100644 index 00000000..60bfcc33 --- /dev/null +++ b/docs/src/background/mode_weights.md @@ -0,0 +1,131 @@ +# Mode weights + +On the [previous page](@ref sYlm_and_Dlmpm), we introduced the +eigenfunctions of [the differential operators](@ref +background_differential_operators) defined on ``\mathrm{Spin}(3)``. +These eigenfunctions are the spin-weighted spherical harmonics (SWSHs) +``{}_{s}Y_{ℓ,m}(R)``, or equivalently Wigner's 𝔇 matrices. + +Now that we have introduced the spin-weighted spherical harmonics +(SWSHs) as eigenfunctions of the relevant differential operators, we +can define mode weights of a general spin-weighted function in terms +of these harmonics. + +These eigenfunctions form a complete orthonormal basis for the space +of square-integrable functions defined on ``\mathrm{Spin}(3)``. Thus, +*any* such function ``f(R)`` can be expressed as a linear combination +of these harmonics: +```math +f(R) = \sum_{ℓ=0}^{∞} \sum_{s=-ℓ}^{ℓ} \sum_{m=-ℓ}^{ℓ} +{}_{s}f_{ℓ,m}\, {}_{s}Y_{ℓ,m}(R), +``` +where the coefficients ``{}_{s}f_{ℓ,m}`` are called the *mode weights* +of the function ``f``. These mode weights can be computed from the +function using the orthogonality of the SWSHs: +```math +{}_{s}f_{ℓ,m} += \frac{π}{2} \int_{\mathrm{Spin}(3)} f(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR, +``` +where the factor in front of the integral is explained [here](@ref +Integration-and-normalization). Note that we have not restricted the +spin weight ``s`` of the function ``f``; a general function on +``\mathrm{Spin}(3)`` can have contributions from SWSHs of any spin +weight, so ``s`` was included in the sum above. In fact, those spin +weights may have half-integral values as well, in which case the sum +over ``ℓ`` must include all positive half-integral values as well as +the integral values. + +However, if we restrict to functions with a *specific* spin weight +``s``, only the SWSHs with *that* spin weight will contribute to the +expansion, and we can simplify the expressions above to +```math +f(R) = \sum_{ℓ=|s|}^{∞} \sum_{m=-ℓ}^{ℓ} +{}_{s}f_{ℓ,m}\, {}_{s}Y_{ℓ,m}(R) +``` +and +```math +{}_{s}f_{ℓ,m} += \frac{π}{2} \int_{\mathrm{Spin}(3)} f(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR. +``` + +!!! danger "#TODO" + + Finish this section, citing the section on [functions + of spherical coordinates](@ref Pulling-back-to-I𝕊) and + reducing to the integral over ``𝕊²``. Refine the + following section. + + +## Differential operators + +Mode weights and functions transform in related but different ways +under the action of the differential operators. + +One important point to note is that mode weights transform +"contravariantly" (very loosely speaking) relative to the +spin-weighted spherical functions under some operators. For example, +take the action of the ``L_+`` operator, which acts on a SWSH as +```math +L_+ \left\{{}_{s}Y_{ℓ,m}\right\} (R) += \sqrt{(ℓ-m)(ℓ+m+1)}\ {}_{s}Y_{ℓ,m+1}(R). +``` +We can use this to derive mode weights of a general spin-weighted +function ``f`` under the action of this operator:[^2] +```math +\begin{aligned} +\left\{L_+ f\right\}_{ℓ,m} +&= +\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{L_+ \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \sum_{ℓ',m'} f_{ℓ',m'}\, \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\sqrt{(ℓ'-m')(ℓ'+m'+1)} {}_{s}Y_{ℓ',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} δ_{ℓ,ℓ'} δ_{m,m'+1} \\ +&= +f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} +\end{aligned} +``` +Note that this expression (and in particular its signs) more resembles +the expression for ``L_- \left\{{}_{s}Y_{ℓ,m}\right\}`` than for +``L_+ \left\{{}_{s}Y_{ℓ,m}\right\}``. Similar relations hold for +the action of ``L_-``. + +[^2]: + A technical note about the integrals above: the integrals should be taken + over the appropriate space and with the appropriate weight such that the + SWSHs are orthonormal. In general, this integral should be over + ``\mathrm{Spin}(3)`` and weighted by ``1/2π`` so that the result will be + either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when + integrated over an ``𝕊²`` subspace (nor even is the integral invariant). + However, if we know that the spins are the same in both cases, it *is* + possible to integrate over an ``𝕊²`` subspace. + +However, it is important to note that the same "contravariance" is not +present for the spin-raising and -lowering operators: +```math +\begin{aligned} +\left\{\eth f\right\}_{ℓ,m} +&= +\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\int \left\{\eth \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\eth {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ +&= +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} δ_{ℓ,ℓ'} δ_{m,m'} \\ +&= +f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} +\end{aligned} +``` +Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more +"covariant" form of transformation. + diff --git a/docs/src/background/operators.md b/docs/src/background/operators.md new file mode 100644 index 00000000..1297ae8b --- /dev/null +++ b/docs/src/background/operators.md @@ -0,0 +1,208 @@ +# [Differential operators](@id background_differential_operators) + +On [the previous page](@ref background_domain), we saw that the domain +on which our most important functions — spherical harmonics and Wigner +𝔇 matrices — are defined is most naturally regarded as the group of +unit quaternions, ``\mathrm{Spin}(3) \cong \mathrm{SU}(2)``. As a +result, we can define a variety of differential operators acting on +these functions, relating to infinitesimal motions in this group, +acting either from the left or the right on their arguments. Right or +left matters because this group is non-commutative. The left +derivative operator we define here turns out to be exactly the +angular-momentum operator familiar from physics. In the coming pages, +we will use these operators to define the spin-weighted spherical +harmonics and Wigner's 𝔇 matrices as eigenfunctions. + +## Definitions and properties + +In general, the *left* Lie derivative of a function ``f(𝐐)`` over the +unit quaternions with respect to a generator of rotation ``𝐠`` is +defined as +```math +L_𝐠(f)\{𝐐\} := -\frac{i}{2} + \left. \frac{df\left(e^{ϵ\,𝐠}\, 𝐐\right)}{d ϵ} \right|_{ϵ=0}. +``` +Note that the exponential multiplies ``𝐐`` *on the left* — hence the +name. We will see below that this agrees with the usual definition of +the angular-momentum from physics, except that in *quantum* physics a +factor of ``\hbar`` is usually included. + +So, for example, a rotation about the ``𝐳`` axis has the quaternion +``𝐤`` as its generator of rotation, and ``L_𝐤`` defined in this way +agrees with [the usual angular-momentum +operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) +``L_z`` familiar from spherical-harmonic theory, and reduces to it +when the function has spin weight 0, but also applies to functions of +general spin weight. Similarly, we can compute ``L_x`` and ``L_y``, +and take appropriate combinations to find [the usual raising and +lowering (ladder) +operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) +``L_+`` and ``L_-``. + +In just the same way, we can define the *right* Lie derivative of a +function ``f(𝐐)`` over the unit quaternions with respect to a +generator of rotation ``𝐠`` as +```math +R_𝐠(f)\{𝐐\} := -\frac{i}{2} + \left. \frac{df\left(𝐐\, e^{-ϵ\,𝐠}\right)}{d ϵ} \right|_{ϵ=0}. +``` +Note that the exponential multiplies ``𝐐`` *on the right* — hence the +name. Also observe that +```math +𝐐\, e^{-ϵ\,𝐠} += 𝐐\, e^{-ϵ\,𝐠}\, 𝐐⁻¹\, 𝐐 += e^{-ϵ\,𝐐\, 𝐠\, 𝐐⁻¹}\, 𝐐 +``` +and +```math +e^{ϵ\,𝐠}\, 𝐐 += 𝐐\, 𝐐⁻¹\, e^{-ϵ\,𝐠}\, 𝐐 += 𝐐\, e^{-ϵ\,𝐐⁻¹\, 𝐠\, 𝐐}, +``` +which mean that +```math +R_𝐠(f)\{𝐐\} = L_{-𝐐\, 𝐠\, 𝐐⁻¹}(f)\{𝐐\} +\qquad \text{and} \qquad +L_𝐠(f)\{𝐐\} = R_{-𝐐⁻¹\, 𝐠\, 𝐐}(f)\{𝐐\}. +``` +That is, the right Lie derivative with respect to ``𝐠`` is equal to +the left Lie derivative with respect to the "rotated" version of the +algebra element ``-𝐠``, and vice versa. But the arguments *depend on +the value* ``𝐐``, which means, for example, that commutator +expressions can't be evaluated by simple static substitution. + +This ``R_𝐠`` operator is less common in physics, because it +represents the dependence of the function on the choice of frame (or +coordinate system), which is not usually interesting. Multiplication +on the left represents a rotation of the physical system, while +rotation on the right represents a rotation of the coordinate system. +However, this dependence on coordinate system is precisely what +defines the *spin weight* of a function, so this class of operators is +relevant in discussions of spin-weighted spherical functions. In +particular, ``R_z`` is the spin-weight operator — meaning that when it +acts on a spin-weighted spherical harmonic of spin weight ``s``, it +returns ``s`` times that same function. Moreover, the operators +``R_\pm`` correspond (up to a sign) to the spin-raising and -lowering +operators ``\eth`` and ``\bar{\eth}`` originally introduced by +[Newman_1966](@citet), as explained in greater detail by +[Boyle_2016](@citet). + +Note that these definitions are *extremely* general, in that they can +be used for *any* Lie group, and for any complex-valued function on +that group. And in full generality, we have the useful properties of +linearity: +```math +L_{s𝐚} = sL_{𝐚} +\qquad \text{and} \qquad +R_{s𝐚} = sR_{𝐚}, +``` +and +```math +L_{𝐚+𝐛} = L_{𝐚} + L_{𝐛} +\qquad \text{and} \qquad +R_{𝐚+𝐛} = R_{𝐚} + R_{𝐛}, +``` +for any *real* scalar ``s`` and any elements of the Lie algebra ``𝐚`` +and ``𝐛``. In particular, if the Lie algebra has a basis +``𝐞_{(j)}``, we use the shorthand ``L_j`` and ``R_j`` for +``L_{𝐞_{(j)}}`` and ``R_{𝐞_{(j)}}``, respectively, and we can expand +any operator in terms of these basis operators: +```math +L_{𝐚} = \sum_{j} a_j L_j +\qquad \text{and} \qquad +R_{𝐚} = \sum_{j} a_j R_j. +``` + + +## Commutators and angular momentum + +In general, for generators ``𝐚`` and ``𝐛``, we have the commutator +relations +```math +\left[ L_𝐚, L_𝐛 \right] = \frac{i}{2} L_{[𝐚,𝐛]} +\qquad +\left[ R_𝐚, R_𝐛 \right] = \frac{i}{2} R_{[𝐚,𝐛]} +\qquad +\left[ L_𝐚, R_𝐛 \right] = 0, +``` +where ``[𝐚,𝐛]`` is the commutator of the two generators, which can +be obtained directly as the commutator of the corresponding +quaternions. Note that these two equations have the same signs. The +factors of ``i/2`` are inherited directly from the definitions of +``L_𝐠`` and ``R_𝐠`` given above. Note the subtle sign difference in +the exponents in those definitions. The fact that these two +commutator relations have the same sign results from the fact that the +quaternions are multiplied in opposite orders in the two cases. There +are overall arbitrary sign choices; we choose these purely for +conventional reasons, to reproduce the standard ``L`` operator and to +produce similar commutators for ``R``.[^1] + +[^1]: + In fact, we can define the left and right Lie derivative operators + quite generally, for functions on *any* Lie group and for the + corresponding Lie algebra. And in all cases (at least for + finite-dimensional Lie algebras) we obtain the same commutator + relations. The only potential difference is that it may not make + sense to use the coefficient ``i/2`` in general; it was chosen + here for consistency with the standard angular-momentum operators. + If that coefficient is changed in the definitions of the Lie + derivatives, the only change to the commutator relations would be + the substitution of that coefficient. The presence of an ``i`` is + important to ensure that the operators are Hermitian when acting on + appropriate function spaces. + +Again, these results are valid for general (finite-dimensional) Lie +groups, but a particularly interesting case is in application to the +three-dimensional rotation group. In the following, we will apply our +results to this group. + +The commutator relations for ``L`` are consistent — except for the +differing use of ``\hbar`` — with the usual relations from quantum +mechanics: +```math +\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} ε_{jkl} L_l. +``` +Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and +index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we +represent an arbitrary basis vector as ``\hat{e}_j``, then the +quaternion commutator ``[𝐚,𝐛]`` in the expression for ``[L_𝐚, L_𝐛]`` +becomes +```math +[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} ε_{jkl} \hat{e}_l. +``` +That is, the commutator ``[𝐚,𝐛]`` is essentially twice the cross +product of the corresponding vectors. Plugging this into the general +expression ``[L_𝐚, L_𝐛] = \frac{i}{2} L_{[𝐚,𝐛]}``, we obtain +(except for that factor of ``\hbar``) the version used in quantum +physics. + +The raising and lowering operators relative to ``L_z`` and ``R_z`` +satisfy — *by definition of raising and lowering operators* — the +relations +```math +[L_z, L_\pm] = \pm L_\pm +\qquad +[R_z, R_\pm] = \pm R_\pm. +``` +These allow us to solve, up to an overall factor, for those operators +in terms of the basic generators: +```math +L_\pm = L_x \pm i L_y +\qquad +R_\pm = R_x \pm i R_y. +``` +(Interestingly, the solution process also shows that raising and +lowering operators can only exist if the factor in front of the +derivatives in the definitions of ``L_g`` and ``R_g`` are pure +imaginary numbers.) In particular, this results in the commutator +relations +```math +[L_+, L_-] = 2L_z +\qquad +[R_+, R_-] = 2R_z. +``` + +In the functions [listed below](#Module-functions), these operators +are returned as matrices acting on vectors of mode weights. As such, +we can actually evaluate these commutators as given to cross-validate +the expressions and those functions. diff --git a/docs/src/background/sYlm_and_Dlmpm.md b/docs/src/background/sYlm_and_Dlmpm.md new file mode 100644 index 00000000..bee2573f --- /dev/null +++ b/docs/src/background/sYlm_and_Dlmpm.md @@ -0,0 +1,214 @@ +# [``{}_{s}Y_{ℓ,m}`` and ``𝔇^{(ℓ)}_{m', m}``](@id sYlm_and_Dlmpm) + +The [previous page](@ref background_differential_operators) introduced +the left and right Lie derivative operators acting on functions of a +quaternion argument. Here, we show how the spin-weighted spherical +harmonics arise naturally as eigenfunctions of these operators. + +## ``{}_{s}Y_{ℓ,m}`` as eigenfunctions + +The familiar way of arriving at the standard (scalar, ``s=0``) +spherical harmonics is to consider solutions to the Laplace equation +in three-dimensional space, then to separate variables in spherical +coordinates, and finally to identify the angular part of the solution +as the spherical harmonics. Since scalar spherical harmonics really +are just functions on the sphere ``𝕊²``, rather than the full group +``\mathrm{Spin}(3)``, they can be sensibly written as functions of +spherical coordinates ``(θ, ϕ)`` alone. We'll suspend for a moment +what, exactly, the angular-momentum operators mean for functions on +the sphere, and just accept the usual definition of ``L`` as the +relevant operator in this case. Recall that we can only +simultaneously diagonalize operators (find nontrivial functions that +are eigenfunctions of both operators at the same time) if they +commute. In this case, the only two commuting operators are ``L^2 = +L_x^2 + L_y^2 + L_z^2`` and any given component of ``L`` — +conventionally chosen to be ``L_z``. Specifically, the spherical +harmonics are defined to satisfy +```math +\begin{aligned} +L² \left\{ Y_{ℓ,m} \right\}(θ, ϕ) +&= ℓ(ℓ+1) \left\{ Y_{ℓ,m} \right\}(θ, ϕ), +\\ +L_z \left\{ Y_{ℓ,m} \right\}(θ, ϕ) +&= m \left\{ Y_{ℓ,m} \right\}(θ, ϕ). +\end{aligned} +``` +(As usual, we do not include the factors of ``\hbar`` that would +normally be included in quantum mechanics texts.) Familiar arguments +— either from spectra of operators, or continuity of the functions — +tell us that the allowed values of ``ℓ`` are non-negative integers, +and for each ``ℓ``, the allowed values of ``m`` are integers +satisfying ``-ℓ ≤ m ≤ ℓ``. + +But now, we consider functions defined on the full group +``\mathrm{Spin}(3)``. In that case, we have both left and right Lie +derivative operators available. As before, we can choose +eigenfunctions of ``L²`` and ``L_z``. It turns out that ``[L_g, R_h] += 0``, which means that we could also simultaneously diagonalize with +respect to any given component of ``R`` — which again is +conventionally chosen to be ``R_z``. We might also expect to be able +to diagonalize with respect to ``R²``, but that turns out to equal +``L²``, so while it is true that the spin-weighted spherical harmonics +are also eigenvalues of ``R²``, that statement contains no additional +information. Thus, we select the spin-weighted spherical harmonics as +functions satisfying +```math +\begin{aligned} +L² \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= ℓ(ℓ+1) \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐), +\\ +L_z \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= m \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐), +\\ +R_z \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= s \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐). +\end{aligned} +``` +In this case, similar arguments show that the allowed values of ``ℓ`` +are non-negative integers or half-integers, and for each ``ℓ``, the +allowed values of ``m`` and ``s`` are correspondingly integers or +half-integers satisfying ``-ℓ ≤ m ≤ ℓ`` and ``-ℓ ≤ s ≤ ℓ``. These +conditions only determine the functions up to a normalization factor, +which we discuss in the next section. + +First, though, we take a moment to consider the effect of the raising +and lowering operators. We know the commutators from the previous +page, and we now know the eigenvalues, so we can compute just like we +do in elementary treatments of scalar spherical harmonics. ``L_±`` +operates just the same as usual, modifying the ``m`` index, while +``R_±`` in an exactly analogous way, but modifies the ``s`` index. +Thus, we can follow the standard derivation to find the standard +ladder relations: +```math +\begin{aligned} +L_± \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= \sqrt{(ℓ ∓ m)(ℓ ± m + 1)}\, \left\{ {}_{s}Y_{ℓ,m±1} \right\}(𝐐), +\\ +R_± \left\{ {}_{s}Y_{ℓ,m} \right\}(𝐐) +&= \sqrt{(ℓ ∓ s)(ℓ ± s + 1)}\, \left\{ {}_{s±1}Y_{ℓ,m} \right\}(𝐐). +\end{aligned} +``` +As usual, we have chosen the coefficients to be real and positive. + +## Integration and normalization + +It is natural to define an inner product on the space of functions on +``\mathrm{Spin}(3)`` using integration over the group itself. That is, +for two functions ``f`` and ``g``, we define[^1] +```math +⟨f|g⟩_{\mathrm{Spin}(3)} = \int_{\mathrm{Spin}(3)} f̄(𝐐)\, g(𝐐)\, d𝐐, +``` +where the integration can be implemented using [extended Euler +angles](@ref Quaternions-and-Euler-angles). This induces a norm on +the space of functions on ``\mathrm{Spin}(3)``: +```math +\left\| f \right\|²_{\mathrm{Spin}(3)} +=⟨f|f⟩_{\mathrm{Spin}(3)} +=\int_{\mathrm{Spin}(3)} |f(𝐐)|²\, d𝐐. +``` +One important point is the norm of the constant function ``f=1``, +which gives the total volume of ``\mathrm{Spin}(3)``. We can compute +it explicitly using extended Euler angles [and find](@ref +Quaternions-and-Euler-angles) that the result is ``2π²``. Recalling +that ``\mathrm{Spin}(3)`` considered as a subset of ``ℝ⁴`` is just the +unit three-sphere ``𝕊³``, which has volume ``2π²``, this makes sense. + +[^1]: Note that we are using the physicists' bra-ket notation here + [Dirac_1939](@cite). Specifically, the inner product is + conjugate-linear in its first argument and linear in its second + argument: for complex numbers ``a`` and ``b``, we have + ``⟨af|bg⟩=ā⟨f|g⟩b``. This convention is common in physics, + engineering, and computer science. But note that mathematicians + more commonly write ``⟨f, g⟩`` (with a comma instead of a vertical + bar) and define the inner product with opposite linearity: linear + in its first argument and conjugate-linear in its second argument, + so that ``⟨af|bg⟩=a⟨f|g⟩b̄``. + +Unfortunately, these are not actually the conventional inner product +and norm used in the literature. The difference is not too difficult +to understand or deal with, however. Note that ``|f(𝐐)|²`` is +actually a field of spin weight 0, no matter what the spin weight of +``f`` itself is.[^2] Therefore, we can push it forward to a +nontrivial function on ``𝕊²`` as described [previously](@ref +Pushing-forward-to-𝕊), and then integrate over ``𝕊²`` instead of +``\mathrm{Spin}(3)``. Thus, we can define a distinct second norm +```math +\left\| f \right\|²_{𝕊²} = \int_{𝕊²} |f|²\, dΩ. +``` +This is actually the conventional norm used in the literature on +spin-weighted spherical functions. Again, we can compute the norm of +the constant function ``f=1`` and find that it is just the area of the +unit 2-sphere, ``4π``. We can relate the two norms with a simple +constant factor: +```math +\left\| f \right\|²_{𝕊²} +=\frac{2}{π} \left\| f \right\|²_{\mathrm{Spin}(3)}. +``` +Clearly, we must choose one or the other of these norms when +normalizing the spin-weighted spherical harmonics. To agree with +standard scalar spherical harmonics, the conventional choice is to use +the ``𝕊²`` norm. + +[^2]: It is possible for a function to have no definite spin weight; + to simply not be an eigenfunction of ``R_z``. In that case, + ``|f(𝐐)|²`` can also have indefinite spin weight. Nonetheless, + its integral over ``\mathrm{Spin}(3)`` will only pick up + contributions from the part of that function with spin weight 0. + +!!! danger "#TODO" + + Finish this section, noting that SWSHs with different + spin weights are not orthogonal under + the ``𝕊²`` inner product. + + +## Defining ``𝔇^{(ℓ)}_{m', m}`` + +Finally, we can consider the Wigner ``𝔇`` matrices. These are +usually defined as +```math +𝔇^{(ℓ)}_{m', m}(α, β, γ) += \big\langle ℓ, m' \big| + e^{-iL_z α} e^{-iL_y β} e^{-iL_z γ} + \big| ℓ, m \big\rangle. +``` +Note that the bra-ket notation usually represents integration over +``𝕊²``. We can extend that to obtain the corresponding norm, while +integrating over ``\mathrm{Spin}(3)`` instead. Thus, we can rewrite +this as +```math +𝔇^{(ℓ)}_{m', m}(𝐐) += \frac{2}{π} \int_{\mathrm{Spin}(3)} + \bar{Y}_{ℓ,m'}(𝐏)\, + Y_{ℓ,m}\left(𝐐⁻¹ 𝐏\right)\, + d𝐏. +``` +We can evaluate the action of the differential operators on this +function pretty easily, noting that the derivative ``d/dϵ`` can pass +through the integral sign by the Leibniz integral rule. The result is +that ``𝔇`` satisfies +```math +\begin{aligned} +L² \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) +&= ℓ(ℓ+1) \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐), +\\ +L_z \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) +&= m' \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐), +\\ +R_z \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐) +&= m \left\{ 𝔇^{(ℓ)}_{m', m} \right\}(𝐐). +\end{aligned} +``` +That is, Wigner's ``𝔇`` matrices are proportional to the +spin-weighted spherical harmonics. We can get the proportionality +factor by applying the definition above with ``𝐐=𝟏``, in which case +the integral simplifies to the orthonormality condition for the +spin-weighted spherical harmonics: +```math +𝔇^{(ℓ)}_{m', m}(𝟏) = \delta_{m', m}. +``` + +!!! danger "#TODO" + + Check the signs on the eigenvalues above, and refer to + the Notes page on normalization to relate Y to D. diff --git a/docs/src/background/transformations.md b/docs/src/background/transformations.md new file mode 100644 index 00000000..535b13e9 --- /dev/null +++ b/docs/src/background/transformations.md @@ -0,0 +1,2 @@ +# Transforming between mode weights and values + diff --git a/docs/src/background/wigner_matrices.md b/docs/src/background/wigner_matrices.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/conventions/comparisons.md b/docs/src/conventions/comparisons.md new file mode 100644 index 00000000..6d668066 --- /dev/null +++ b/docs/src/conventions/comparisons.md @@ -0,0 +1,928 @@ +# 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 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 ℓ, m' | e^{-i α J_z} e^{-i β J_y} e^{-i γ J_z} | ℓ, m \rangle$ + - Rotation of spherical harmonics + - 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. + +When choosing conventions for this package, I intend to prioritize +consistency (to the extent that any of these references actually have +anything to say about the above items) with the following sources, in +order: + +1. LALSuite +2. NINJA +3. Newman-Penrose +4. Goldberg +5. Thorne / MTW +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. + + +## Cohen-Tannoudji (1991) + +(moved) + + +## Condon-Shortley (1935) + +(moved) + +## Edmonds (1960) + +[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 ``α(0 \leq α < 2π)`` about the ``z``-axis, +> bringing the frame of axes from the initial position ``S`` into +> the position ``S'``. The axis of this rotation is commonly +> called the *vertical*. +> +> 2. A rotation ``β(0 \leq β < π)`` about the ``y``-axis of +> the frame ``S'``, called the *line of nodes*. Note that its +> position is in general different from the initial position of the +> ``y``-axis of the frame ``S``. The resulting position of the +> frame of axes is symbolized by ``S''``. +> +> 3. A rotation ``γ(0 \leq γ < 2π)`` about the ``z``-axis +> of the frame of axes ``S''``, called the *figure axis*; the +> position of this axis depends on the previous rotations +> ``α`` and ``β``. The final position of the frame is +> 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[γ 𝐤''/2]\, \exp[β 𝐣'/2]\, \exp[α 𝐤/2]``. But +we also have +```math +\exp[β 𝐣'/2] = \exp[α 𝐤/2]\, \exp[β 𝐣/2]\, \exp[-α 𝐤/2] +``` +so we can just swap the ``α`` rotation with the ``β`` +rotation while dropping the prime from ``𝐣'``. We can do a similar +trick swapping the ``α`` and ``β`` rotations with the +``γ`` rotation while dropping the double prime from ``𝐤''``. +That is, an easy calculation shows that +```math +\exp[γ 𝐤''/2]\, \exp[β 𝐣'/2]\, \exp[α 𝐤/2] += +\exp[α 𝐤/2]\, \exp[β 𝐣/2]\, \exp[γ 𝐤/2], +``` +which is precisely our definition. + +The spherical coordinates are implicitly defined by this statement: + +> It should be noted that the polar coordinates ``φ, θ`` +> with respect to the original frame ``S`` of the ``z``-axis in its +> final position are identical with the Euler angles ``α, β`` +> 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 α}{\tan β} \frac{\partial} {\partial α} + - \sin α \frac{\partial} {\partial β} + + \frac{\cos α}{\sin β} \frac{\partial} {\partial γ} +\right\}, +\\ +L_y &= -i \hbar \left\{ + -\frac{\sin α}{\tan β} \frac{\partial} {\partial α} + + \cos α \frac{\partial} {\partial β} + +\frac{\sin α}{\sin β} \frac{\partial} {\partial γ} +\right\}, +\\ +L_z &= -i \hbar \frac{\partial} {\partial α}. +\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 +𝒟_{α β γ} = +\exp\big( \frac{iα}{\hbar} J_z\big) +\exp\big( \frac{iβ}{\hbar} J_y\big) +\exp\big( \frac{iγ}{\hbar} J_z\big), +``` +which is the *conjugate* of most other definitions. + + +## Goldberg et al. (1967) + +[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_{ℓ,m}(x') = \sum_{m'} Y_{ℓ,m'}(x) D^{ℓ}_{m',m}\left( R^{-1} \right). +``` +They then define the Euler angles as we do, and write [Eq. (3.4)] +```math +D^{ℓ}_{m', m}(α, β, γ) +\equiv +D^{ℓ}_{m', m}\left( R(α β γ)^{-1} \right) += +e^{i m' γ} d^{ℓ}_{m', m}(β) e^{i m α}. +``` +Finally, they derive [Eq. (3.9)] +```math +D^{j}_{m', m}(α, β, γ) += +\left[\frac{(j+m)!(j-m)!}{(j+m')!(j-m')!}\right]^{1/2} +(\sin \frac{1}{2}β)^{2j} +\sum_r \binom{j+m'}{r} \binom{j-m'}{r-m-m'} +(-1)^{j+m'-r} +e^{imα} +(\cot \tfrac{1}{2}β)^{2r-m-m'} +e^{im'γ}. +``` + +Equation (3.11) naturally extends to +```math + {}_sY_{ℓ, m}(θ, ϕ, γ) + = + \left[ \left(2ℓ+1\right) / 4π \right]^{1/2} + D^{ℓ}_{-s,m}(ϕ, θ, γ), +``` +where Eq. (3.4) also shows that ``D^{ℓ}_{m', m}(α, β, +γ) = D^{ℓ}_{m', m}(α, β, 0) e^{i m' γ}``, +so we have +```math + {}_sY_{ℓ, m}(θ, ϕ, γ) + = + {}_sY_{ℓ, m}(θ, ϕ)\, e^{-i s γ}. +``` +This is the most natural extension of the standard spin-weighted +spherical harmonics to ``\mathrm{Spin}(3)``. In particular, the +spin-weight operator is ``i \partial_γ``, which suggests that it +will be most natural to choose the sign of ``R_𝐮`` so that ``R_z = i +\partial_γ``. + + +## 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_{ℓ}^{m}(x) += +(1-x^2)^{|m|/2} \left(\frac{d}{dx}\right)^{|m|} P_{ℓ}(x), +``` +and (4.28) gives the Legendre polynomial as +```math +P_{ℓ}(x) += +\frac{1}{2^ℓ ℓ!} \left(\frac{d}{dx}\right)^ℓ (x^2-1)^ℓ. +``` +Then, (4.32) gives the spherical harmonics as +```math +Y_{ℓ}^{m}(θ, ϕ) += +ϵ +\sqrt{\frac{2ℓ+1}{4π} \frac{(ℓ-|m|)!}{(ℓ+|m|)!}} +e^{imϕ} P_{ℓ}^{m}(\cos θ), +``` +where ``ϵ = (-1)^m`` for ``m\geq 0`` and ``ϵ = 1`` for +``m\leq 0``. In Table 4.2, he explicitly lists the first few +spherical harmonics: +```math +\begin{aligned} + Y_{0}^{0} &= \left(\frac{1}{4π}\right)^{1/2},\\ + Y_{1}^{0} &= \left(\frac{3}{4π}\right)^{1/2} \cos θ,\\ + Y_{1}^{\pm 1} &= \mp \left(\frac{3}{8π}\right)^{1/2} \sin θ e^{\pm iϕ},\\ + Y_{2}^{0} &= \left(\frac{5}{16π}\right)^{1/2} \left(3\cos^2θ - 1\right),\\ + Y_{2}^{\pm 1} &= \mp \left(\frac{15}{8π}\right)^{1/2} \sin θ \cos θ e^{\pm iϕ},\\ + Y_{2}^{\pm 2} &= \left(\frac{15}{32π}\right)^{1/2} \sin^2θ e^{\pm 2iϕ},\\ + Y_{3}^{0} &= \left(\frac{7}{16π}\right)^{1/2} \left(5\cos^3θ - 3\cos θ\right),\\ + Y_{3}^{\pm 1} &= \mp \left(\frac{21}{64π}\right)^{1/2} \sin θ \left(5\cos^2θ - 1\right) e^{\pm iϕ},\\ + Y_{3}^{\pm 2} &= \left(\frac{105}{32π}\right)^{1/2} \sin^2θ \cos θ e^{\pm 2iϕ},\\ + Y_{3}^{\pm 3} &= \mp \left(\frac{35}{64π}\right)^{1/2} \sin^3θ e^{\pm 3iϕ}. +\end{aligned} +``` +In Eqs. (4.127)—(4.129), he gives the angular-momentum operators in +terms of spherical coordinates: +```math +\begin{aligned} +L_x &= \frac{\hbar}{i} \left( + -\sin ϕ \frac{\partial} {\partial θ} + - \cos ϕ \cot θ \frac{\partial} {\partial ϕ} +\right), \\ +L_y &= \frac{\hbar}{i} \left( + \cos ϕ \frac{\partial} {\partial θ} + - \sin ϕ \cot θ \frac{\partial} {\partial ϕ} +\right), \\ +L_z &= -i \hbar \frac{\partial} {\partial ϕ}. +\end{aligned} +``` + + +## LALSuite + +(moved) + +## 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[ ℛ(θ, ϕ) \right] += +\langle j, m' | e^{-iϕ J_z} e^{-iθ J_y} | j, m \rangle, +``` +but later allowing that ``e^{-i ψ J_z}`` usually goes on the +right-hand side of the others, in which case ``D^{(j)}(θ, ϕ) +\to D^{(j)}(ϕ, θ, ψ)``. Figure 10.1 shows that the +spherical coordinates are standard (physicist's) coordinates. + +Equation (10.65) shows the rotation law: +```math +Y_{ℓ}^{m}\left( ℛ^{-1} \hat{r} \right) += +\sum_{m'} D^{(ℓ)}_{m', m}(ℛ) Y_{ℓ}^{m'}(\hat{r}), +``` +and Eq. (10.66) relates the spherical harmonics to the Wigner +D-matrices: +```math +D^{(ℓ)}_{m, 0}(θ, ϕ) += +\sqrt{\frac{4π}{2ℓ+1}} \left[Y_{ℓ}^{m}(θ, ϕ)\right]^\ast. +``` + + +## 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}(ψ, θ, ϕ) = \exp(i m_1 ψ + i m_2 ϕ) D^j_{m_1, m_2}(0, θ, 0)``. + +> `WignerD[{1, 0, 1}, ψ, θ, ϕ]` = ``-\sqrt{2} e^{i ϕ} \cos\frac{θ}{2} +> \sin\frac{θ}{2}`` + +> `WignerD[{𝓁, 0, m}, θ, ϕ] == Sqrt[(4 π)/(2 𝓁 + 1)] SphericalHarmonicY[𝓁, m, θ, ϕ]` + +> `WignerD[{j, m1, m2},ψ, θ, ϕ] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, ψ, θ, +> ϕ]]` + + +> For ``ℓ \geq 0``, ``Y_ℓ^m = \sqrt{(2ℓ+1)/(4π)} \sqrt{(ℓ-m)! / (ℓ+m)!} P_ℓ^m(\cos θ) e^{imϕ}`` where ``P_ℓ^m`` is the associated Legendre function. + +> The associated Legendre polynomials are defined by ``P_n^m(x) = (-1)^m (1-x^2)^{m/2}(d^m/dx^m)P_n(x)`` where ``P_n(x)`` is the Legendre polynomial. + +[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 +introduced the spin-weighted spherical harmonics, ``{}_sY_{ℓ m}``. +They use the standard (physicists') convention for spherical +coordinates and introduce the stereographic coordinate ``\zeta = +e^{iϕ} \cot\frac{θ}{2}``. They define the spin-raising +operator ``\eth`` acting on a function of spin weight ``s`` as +```math +\eth \eta += +-\left(\sin θ\right)^s +\left\{ + \frac{\partial}{\partial θ} + + \frac{i}{\sin θ} \frac{\partial}{\partial ϕ} +\right\} \left\{\left(\sin θ\right)^{-s} \eta\right\}, +``` +They then compute +```math +{}_sY_{ℓ, m} +\propto +\frac{1}{\left[(ℓ-s)! (ℓ+s)!\right]^{1/2}} +\left(1 + \zeta \bar{\zeta}\right)^{-ℓ} +\sum_p \zeta^p (-\bar{\zeta})^{p+s-m} +\binom{ℓ-s}{p} \binom{ℓ+s}{p+s-m}, +``` +where the sum is over all integers ``p`` such that the factorials are +nonzero. + +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 ``ϕ = +> \mathrm{const}`` and ``θ = \mathrm{const}.`` + +The ambiguity is in the sign implied by "tangential", but the natural +choice is to assume they mean that the components are *positive* +multiples of ``\partial_θ`` and ``\partial_ϕ`` respectively, +in which case we have +```math +m^\mu = \frac{1}{\sqrt{2}} +\left[ \partial_θ + i \csc θ \partial_ϕ \right]. +``` +They define the spin weight in terms of behavior of a quantity under +rotation of ``m^\mu`` in its own plane as +```math +(m^\mu)' += +e^{iψ} m^\mu += +\frac{1}{\sqrt{2}} +\left[ + \left(\cos ψ\partial_θ - \sin ψ\csc θ \partial_ϕ\right) + + i \left(\cos ψ\csc θ \partial_ϕ + \sin ψ\partial_θ\right) +\right]. +``` +Raising the spherical coordinates ``(θ, ϕ)`` to Euler angles +``(ϕ, θ, -ψ)``, we see that the rotor ``R_{ϕ, θ, +-ψ}`` rotates the ``𝐳`` basis vector to the point ``(θ, +ϕ)``, and it rotates ``(𝐱 + i 𝐲) / \sqrt{2}`` onto ``(m^\mu)'``. +Under this rotation, a quantity ``\eta`` has spin weight ``s`` if it +transforms as +```math +\eta' = e^{i s ψ} \eta. +``` +Now, supposing that these quantities are functions of Euler angles, we +can write +```math +\eta(ϕ, θ, -ψ) = e^{i s ψ} \eta(ϕ, θ, 0), +``` +or +```math +\eta(ϕ, θ, γ) = e^{-i s γ} \eta(ϕ, θ, 0). +``` +Thus, the operator with eigenvalue ``s`` is ``i \partial_γ``. + + +## NINJA + +(moved) + + +## Sakurai (1994) + + + + +## Scipy + + +[`scipy.special.sph_harm_y`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.sph_harm_y.html) + + +## Shankar (1994) + +[Shankar_1994](@citet) writes in Eq. (12.5.35) the spherical harmonics +as +```math +Y_{ℓ}^{m}(θ, ϕ) += +(-1)^ℓ +\left[ \frac{(2ℓ+1)!}{4π} \right]^{1/2} +\frac{1}{2^ℓ ℓ!} +\left[ \frac{(ℓ+m)!}{(2ℓ)!(ℓ-m)!} \right]^{1/2} +e^{i m ϕ} +(\sin θ)^{-m} +\frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} +(\sin θ)^{2ℓ} +``` +for ``m \geq 0``, with (12.5.40) giving the expression +```math +Y_{ℓ}^{-m}(θ, ϕ) += +(-1)^m \left( Y_{ℓ}^{m}(θ, ϕ) \right)^\ast. +``` +The angular-momentum operators are given below (12.5.27) as +```math +\begin{aligned} +L_x &= i \hbar \left( + \sin ϕ \frac{\partial} {\partial θ} + + \cos ϕ \cot θ \frac{\partial} {\partial ϕ} +\right), +\\ +L_y &= i \hbar \left( + -\cos ϕ \frac{\partial} {\partial θ} + + \sin ϕ \cot θ \frac{\partial} {\partial ϕ} +\right), +\\ +L_z &= -i \hbar \frac{\partial} {\partial ϕ}. +\end{aligned} +``` +In Exercise 12.5.7, the rotation operator is defined by +```math +U\left[ R(α, β, γ) \right] += +e^{-i α J_z/\hbar} +e^{-i β J_y/\hbar} +e^{-i γ J_z/\hbar}, +``` +That ``U`` becomes a ``D^{(j)}`` when the operator is acting on the +states ``|j, m\rangle`` for a given ``j``. Thus, while Shankar never +actually uses notation like ``D^{(j)}_{m', m}``, he does talk about +``\langle j, m' | D^{(j)}\left[ R(α, β, γ) \right] | j, +m \rangle``. + + +## SymPy + +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) when defining +```math +𝒟_{α β γ} = +\exp\big( \frac{iα}{\hbar} J_z\big) +\exp\big( \frac{iβ}{\hbar} J_y\big) +\exp\big( \frac{iγ}{\hbar} J_z\big). +``` +But that is an incorrect copy of Edmonds' Eq. (4.1.9), in which the +``α`` and ``γ`` on the right-hand side are swapped. The +code also implements D in the `wigner_d` function as (essentially) +```python +exp(I*mprime*alpha)*d[i, j]*exp(I*m*gamma) +``` +even though the actual equation Eq. (4.1.12) of Edmonds says +```math +\mathscr{D}^{(j)}_{m' m}(α β γ) = +\exp i m' γ d^{(j)}_{m' m}(α, β) \exp(i m α). +``` +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. + +## Thorne + +## Torres del Castillo (2003) + +[TorresDelCastillo_2003](@citet) starts by defining a rotation +``ℛ`` 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' = +ℛ 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 +ℛ Y_{l,m} = \sum_{m} D^l_{m',m}(ℛ) Y_{l,m'}. +``` +Including the arguments to the spherical harmonics, this becomes +```math +Y_{l,m}\big(ℛ^{-1} R_{θ, ϕ}\big) += +\sum_{m} D^l_{m',m}(ℛ) Y_{l,m'}\big(R_{θ, ϕ}\big). +``` +In this form, we have [Eq. (2.46)] +```math +D^l_{m'',m}(ℛ_1 ℛ_2) += +\sum_{m'} D^l_{m'',m'}(ℛ_1) D^l_{m',m}(ℛ_2). +``` +He computes [Eq. (2.53)] +```math +D^l_{m',m}(ϕ, θ, \chi) += +e^{-i m' ϕ} d^l_{m',m}(θ) e^{-i m \chi}, +``` +where the ``d`` matrix is given by +```math +d^l_{m',m}(θ) += +\sqrt{(l+m)!(l-m)!(l+m')!(l-m')!} +\sum_{k} \frac{ + (-1)^k + (\sin \tfrac{1}{2} θ)^{m-m'+2k} + (\cos \tfrac{1}{2} θ)^{2l-m+m'-2k} +} { + k!(l+m'-k)!(l-m-k)!(m-m'+k)! +}, +``` +and the spin-weighted spherical harmonic is related to ``D`` by +```math +{}_{s}Y_{j,m}(θ, ϕ) += +(-1)^m +\sqrt{\frac{2j+1}{4π}} +d^j_{-m,s}(θ) +e^{i m ϕ}. +``` + + +## Varshalovich et al. (1988) + +[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}(α, β, γ) += +e^{-iα \hat{J}_z} +e^{-iβ \hat{J}_y} +e^{-iγ \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 +> ``δ \omega`` about an axis ``𝐧`` may be written as +> ```math +> \Psi \to \Psi' = \left(1 - i δ \omega 𝐧 \cdot \hat{J} \right)\Psi, +> ``` +> where ``\hat{J}`` is the total angular momentum operator. + +Eq. 4.1.(1) defines the Wigner D-functions according to +```math +\langle J M | \hat{D}(α, β, γ) | J' M' \rangle += +δ_{J J'} D^J_{M M'}(α, β, γ). +``` +Eq. 4.3.(1) states +```math +D^J_{M M'}(α, β, γ) += +e^{-i M α} +d^J_{M M'}(β) +e^{-i M' γ} +``` + + +Page 155 has a table of values for ``ℓ \leq 5`` + +[Varshalovich_1988](@citet) distinguish in Sec. 1.1.3 between +*covariant* and *contravariant* spherical coordinates and the +corresponding basis vectors, which they define as +```math +\begin{aligned} + 𝐞_{+1} &= - \frac{1}{\sqrt{2}} \left( 𝐞_x + i 𝐞_y\right) + &&& + 𝐞^{+1} &= - \frac{1}{\sqrt{2}} \left( 𝐞_x - i 𝐞_y\right) \\ + 𝐞_{0} &= 𝐞_z &&& 𝐞^{0} &= 𝐞_z \\ + 𝐞_{-1} &= \frac{1}{\sqrt{2}} \left( 𝐞_x - i 𝐞_y\right) + &&& + 𝐞^{-1} &= \frac{1}{\sqrt{2}} \left( 𝐞_x + i 𝐞_y\right). +\end{aligned} +``` +Then, in Sec. 4.2 they define ``\hat{𝐉}`` as the operator of +angular momentum of the rigid symmetric top. They then give in Eq. +(6) the "covariant spherical coordinates of ``\hat{𝐉}`` in the +non-rotating (lab-fixed) system" as +```math +\begin{gather} + \hat{J}_{\pm 1} = \frac{i}{\sqrt{2}} e^{\pm i α} \left[ + \mp \cot β \frac{\partial}{\partial α} + + i \frac{\partial}{\partial β} + \pm \frac{1}{\sin β} \frac{\partial}{\partial γ} + \right] \\ + \hat{J}_0 = - i \frac{\partial}{\partial α}, +\end{gather} +``` +and "contravariant components of ``\hat{𝐉}`` in the rotating +(body-fixed) system" as +```math +\begin{gather} + \hat{J}'^{\pm 1} = \frac{i}{\sqrt{2}} e^{\mp i γ} \left[ + \pm \cot β \frac{\partial}{\partial γ} + + i \frac{\partial}{\partial β} + \mp \frac{1}{\sin β} \frac{\partial}{\partial α} + \right] \\ + \hat{J}'^0 = - i \frac{\partial}{\partial γ}. +\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{aligned} + \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 α} \left[ + % - \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial γ} + % \right] + % - + % \frac{i}{\sqrt{2}} e^{-i α} \left[ + % + \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial γ} + % \right] + % \right) \\ + &= i\left[ + \frac{\cos α}{\tan β} \frac{\partial}{\partial α} + + \sin α \frac{\partial}{\partial β} + - \frac{\cos α}{\sin β} \frac{\partial}{\partial γ} + \right] \\ + \hat{J}_{y} + &= -\frac{1}{i\sqrt{2}} \left( \hat{J}_{+1} + \hat{J}_{-1} \right) \\ + % &= -\frac{1}{i\sqrt{2}} \left( + % \frac{i}{\sqrt{2}} e^{i α} \left[ + % - \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial γ} + % \right] + % + + % \frac{i}{\sqrt{2}} e^{-i α} \left[ + % + \cot β \frac{\partial}{\partial α} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial γ} + % \right] + % \right) \\ + &= i \left[ + \frac{\sin α}{\tan β} \frac{\partial}{\partial α} + - \cos α \frac{\partial}{\partial β} + - \frac{\sin α}{\sin β} \frac{\partial}{\partial γ} + \right] \\ + \hat{J}_{z} + &= \hat{J}_{0} \\ + &= -i \frac{\partial}{\partial α} +\end{aligned} +``` +We can compare these to the [Full expressions on ``𝕊³``]() `@ref`, and find +that they are precisely equivalent to expressions for ``L_j`` computed in +this package's conventions. + +Next, the contravariant components: +```math +\begin{aligned} + \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 γ} \left[ + % + \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial α} + % \right] + % - + % \frac{i}{\sqrt{2}} e^{+ i γ} \left[ + % - \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial α} + % \right] + % \right) \\ + &= -i \left( + \frac{\cos γ}{\tan β} \frac{\partial}{\partial γ} + + \sin γ \frac{\partial}{\partial β} + - \frac{\cos γ}{\sin β} \frac{\partial}{\partial α} + \right) \\ + \hat{J}'_{y} + &= \frac{1}{i\sqrt{2}} \left( \hat{J}'^{+1} + \hat{J}'^{-1} \right) \\ + % &= \frac{1}{i\sqrt{2}} \left( + % \frac{i}{\sqrt{2}} e^{-i γ} \left[ + % + \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % - \frac{1}{\sin β} \frac{\partial}{\partial α} + % \right] + % + + % \frac{i}{\sqrt{2}} e^{+ i γ} \left[ + % - \cot β \frac{\partial}{\partial γ} + % + i \frac{\partial}{\partial β} + % + \frac{1}{\sin β} \frac{\partial}{\partial α} + % \right] + % \right) \\ + &= -i \left( + \frac{\sin γ}{\tan β} \frac{\partial}{\partial γ} + - \cos γ \frac{\partial}{\partial β} + - \frac{\sin γ}{\sin β} \frac{\partial}{\partial α} + \right) \\ + \hat{J}'_{z} + &= \hat{J}'^{0} \\ + &= -i \frac{\partial}{\partial γ} +\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} = +-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. 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. + +## Wikipedia + +Euler angles + +Angular-momentum operators + +Spherical harmonics + +Spin-weighted spherical harmonics + +Defining the operator +```math +ℛ(α,β,γ) = e^{-iα J_z}e^{-iβ J_y}e^{-iγ J_z}, +``` +[Wikipedia expresses the Wigner D-matrix](https://en.wikipedia.org/wiki/Wigner_D-matrix#Definition_of_the_Wigner_D-matrix) as +```math +D^j_{m'm}(α,β,γ) \equiv \langle jm' | ℛ(α,β,γ)| jm \rangle =e^{-im'α } d^j_{m'm}(β)e^{-i mγ}. +``` + + +## Wigner + + + + +## Zettili (2009) + +[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 φ}, +``` +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φ} \left( + \frac{\partial}{\partial θ} + \pm i \cot θ \frac{\partial}{\partial φ} +\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}(θ, φ) += +\frac{(-1)^l}{2^l l!} +\sqrt{\frac{2l+1}{4π} \frac{(l+m)!}{(l-m)!}} +e^{imφ} +\frac{1}{\sin^m θ} +\frac{d^{l-m}}{d(\cos θ)^{l-m}} +(\sin θ)^{2l}. +``` + +Section 7.2.1 denotes by ``\hat{R}_z(δ ϕ)`` the + +> rotation of the coordinates of a *spinless* particle over an +> *infinitesimal* angle ``δ ϕ`` about the ``z``-axis + +and shows its action [Eq. (7.16)] +```math +\hat{R}_z (δ ϕ) ψ(r, θ, ϕ) += +ψ(r, θ, ϕ - δ ϕ). +``` + +> We may generalize this relation to a rotation of angle ``δ +> ϕ`` about an arbitrary axis whose direction is given by the unit +> vector ``\vec{n}``: + +```math +\hat{R}(δ ϕ) += +1 - \frac{i}{\hbar} δ ϕ \vec{n} \cdot \hat{\vec{L}}. +``` +This extends to finite rotation by defining the operator [Eq. (7.48)] +```math +\hat{R}(α, β, γ) += +e^{-iα J_z / \hbar} e^{-iβ J_y / \hbar} e^{-iγ J_z / \hbar}. +``` +Equation (7.52) then defines +```math +D^{(j)}_{m', m}(α, β, γ) += +\langle j, m' | \hat{R}(α, β, γ) | j, m \rangle, +``` +So that [Eq. (7.54)] +```math +D^{(j)}_{m', m}(α, β, γ) += +e^{-i (m' α + m γ)} d^{(j)}_{m', m}(β), +``` +where [Eq. (7.55)] +```math +d^{(j)}_{m', m}(β) += +\langle j, m' | e^{-iβ J_y / \hbar} | j, m \rangle. +``` +The explicit expression for ``d`` is [Eq. (7.56)] +```math +d^{(j)}_{m', m}(β) += +\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{β}{2}\right)^{2j+m-m'-2k} +\left(\sin\frac{β}{2}\right)^{m'-m+2k}. +``` +In Sec. 7.2.6, we find that if the operator ``\hat{R}(α, β, +γ)`` rotates a vector pointing in the ``(θ, ϕ)`` to a +vector pointing in the ``(θ', ϕ')`` direction, then the +spherical harmonics transform as [Eq. (7.70)] +```math +Y_{ℓ, m}^\ast (θ', ϕ') += +\sum_{m'} D^{(ℓ)}_{m, m'}(α, β, γ) Y_{ℓ, m'}^\ast (θ, ϕ). +``` diff --git a/docs/src/conventions/details.md b/docs/src/conventions/details.md new file mode 100644 index 00000000..8e96d502 --- /dev/null +++ b/docs/src/conventions/details.md @@ -0,0 +1,1167 @@ +# 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") 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. + + +## Three-dimensional space + +The space we are working in is naturally three-dimensional Euclidean +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 +𝐯 = v_x 𝐱 + v_y 𝐲 + v_z 𝐳, +``` +in which case the Euclidean norm is given by +```math +\| 𝐯 \| = \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, θ, ϕ)``. 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), \\ +θ &= \arccos\left(\frac{z}{r}\right) &&\in [0, π], \\ +ϕ &= \arctan\left(\frac{y}{x}\right) &&\in [0, 2π), +\end{aligned} +``` +where we assume the ``\arctan`` in the expression for ``ϕ`` is +really the two-argument form that gives the correct quadrant. The +inverse transformation is given by +```math +\begin{aligned} +x &= r \sin θ \cos ϕ, \\ +y &= r \sin θ \sin ϕ, \\ +z &= r \cos θ. +\end{aligned} +``` +We can use this to find the components of the metric in spherical +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θ +\end{array} \right)_{i'j'}. +``` +The unit coordinate vectors in spherical coordinates are then +```math +\begin{aligned} +𝐫 &= \sin θ \cos ϕ 𝐱 + \sin θ \sin ϕ 𝐲 + \cos θ 𝐳, \\ +\boldsymbol{θ} &= \cos θ \cos ϕ 𝐱 + \cos θ \sin ϕ 𝐲 - \sin θ 𝐳, \\ +\boldsymbol{ϕ} &= -\sin ϕ 𝐱 + \cos ϕ 𝐲, +\end{aligned} +``` +where, again, we omit the hats on the unit vectors to keep the +notation simple. Conversely, we can express the Cartesian basis +vectors in terms of the spherical basis vectors as +```math +\begin{aligned} +𝐱 &= \sin θ \cos ϕ 𝐫 + \cos θ \cos ϕ \boldsymbol{θ} - \sin ϕ \boldsymbol{ϕ}, +\\ +𝐲 &= \sin θ \sin ϕ 𝐫 + \cos θ \sin ϕ \boldsymbol{θ} + \cos ϕ \boldsymbol{ϕ}, +\\ +𝐳 &= \cos θ 𝐫 - \sin θ \boldsymbol{θ}. +\end{aligned} +``` + +One seemingly obvious — but extremely important — fact is that the +unit basis frame ``(𝐱, 𝐲, 𝐳)`` can be rotated onto +``(\boldsymbol{θ}, \boldsymbol{ϕ}, 𝐫)`` by first +rotating through the "polar" angle ``θ`` about the ``𝐲`` +axis, and then through the "azimuthal" angle ``ϕ`` about the +``𝐳`` axis. This becomes important when we consider +spin-weighted functions. + +Integration in Cartesian coordinates is, of course, trivial as +```math +\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f\, dx\, dy\, dz. +``` +In spherical coordinates, the integrand involves the square-root of +the determinant of the metric, so we have +```math +\int_{\mathbb{R}^3} f\, d^3𝐫 = \int_0^\infty \int_0^π \int_0^{2π} f\, r^2 \sin θ\, dr\, dθ\, dϕ. +``` +Restricting to the unit sphere, we obtain the usual surface element +```math +\int_{𝕊²} f\, d^2\Omega = \int_0^π \int_0^{2π} f\, \sin θ\, dθ\, dϕ. +``` +Note that ``\int_{𝕊²} d^2\Omega = 4π``. + + +## 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 +feature is the geometric product, which we could define 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 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} +𝟏, \\ +𝐱, 𝐲, 𝐳,\\ +𝐱𝐲, 𝐱𝐳, 𝐲𝐳, \\ +𝐱𝐲𝐳. +\end{gather} +``` +The standard presentation of quaternions (including the confused +historical development) uses different symbols for these last four +basis elements: +```math +\begin{gather} +𝐢 = 𝐳𝐲 = -𝐲𝐳, \\ +𝐣 = 𝐱𝐳 = -𝐳𝐱, \\ +𝐤 = 𝐲𝐱 = -𝐱𝐲, \\ +𝐈 = 𝐱𝐲𝐳. +\end{gather} +``` +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} +𝐢 𝐣 &= 𝐤, \\ +𝐣 𝐤 &= 𝐢, \\ +𝐤 𝐢 &= 𝐣. +\end{aligned} +``` +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 a quaternion would be written as +```math +𝐐 = W𝟏 + X𝐢 + Y𝐣 + Z𝐤, +``` +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. Any nonzero quaternion has an inverse, which is 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), \\ +α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2π), \\ +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, π], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 4π), +\end{aligned} +``` +where we again assume the ``\arctan`` in the expressions for ``α`` and +``γ`` is really the two-argument form that gives the correct quadrant, +and if relevant, we use `mod` to limit the values on output. Note +that here, ``γ`` ranges up to ``4π`` rather than just ``2π``, as +in the standard Euler angles. This is because we are describing the +space of quaternions, rather than just the space of rotations. If we +restrict to quaternions with magnitude ``R=1``, we have exactly the +group of unit quaternions ``\mathrm{Spin}(3)=\mathrm{SU}(2)``, which +is a double cover of the rotation group ``\mathrm{SO}(3)``. This +extended range for ``γ`` is necessary to cover the entire space of +quaternions; if we further restrict to ``[0, 2π)``, we would only +cover the space of rotations. This and the inclusion of ``R`` +identify precisely how this coordinate system extends the standard +Euler angles. + +Note that it would also be reasonable to limit ``γ`` to ``2π``, +while allowing ``β`` to range up to ``2π`` to cover the entire space +of quaternions. This is just somewhat more delicate to compute, and +is simply not conventional. Also, using ``γ ∈ [0,4π)`` integrates +nicely with our [framework of a telescope](@ref Domain) with ``γ`` +representing the rotation about its line of sight; a full ``4π`` +rotation is required for the polarizer to explore the full range of +states of half-integer spin fields. + +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 β}{4} \\ + 0 & 0 & \frac{R^2}{4} & 0 \\ + 0 & \frac{R^2 \cos β}{4} & 0 & \frac{R^2}{4} +\end{array} \right)_{i'j'}. +``` +The unit basis vectors in extended Euler coordinates in terms of the +unit basis vectors in quaternion coordinates are +```math +\begin{aligned} +𝐑 &= \frac{1}{R} \left( + \cos \frac{β}{2} \cos \frac{α+γ}{2} 𝟏 + - \sin \frac{β}{2} \sin \frac{α-γ}{2} 𝐢 + + \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐣 + + \cos \frac{β}{2} \sin \frac{α+γ}{2} 𝐤 +\right), \\ +\boldsymbol{α} &= \frac{R}{2} \left( + -\cos \frac{β}{2} \sin \frac{α+γ}{2} 𝟏 + - \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐢 + - \sin \frac{β}{2} \sin \frac{α-γ}{2} 𝐣 + + \cos \frac{β}{2} \cos \frac{α+γ}{2} 𝐤 +\right), \\ +\boldsymbol{β} &= \frac{R}{2} \left( + -\sin \frac{β}{2} \cos \frac{α+γ}{2} 𝟏 + - \cos \frac{β}{2} \sin \frac{α-γ}{2} 𝐢 + + \cos \frac{β}{2} \cos \frac{α-γ}{2} 𝐣 + - \sin \frac{β}{2} \sin \frac{α+γ}{2} 𝐤 +\right), \\ +\boldsymbol{γ} &= \frac{R}{2} \left( + -\cos \frac{β}{2} \sin \frac{α+γ}{2} 𝟏 + + \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐢 + - \sin \frac{β}{2} \cos \frac{α-γ}{2} 𝐣 + - \cos \frac{β}{2} \sin \frac{α+γ}{2} 𝐤 +\right). +\end{aligned} +``` + +Again, integration involves a square-root of the determinant of the +metric, which reduces to ``R^3 \sin β / 8``. The integral over the +entire space of quaternions is then +```math +\int_{\mathbb{R}^4} f\, d^4𝐐 += \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty \int_{-\infty}^\infty f\, dW\, dX\, dY\, dZ += \int_0^\infty \int_0^{2π} \int_0^{π} \int_0^{4π} f\, \frac{R^3}{8} \sin β\, dR\, dα\, dβ\, dγ. +``` +Restricting to the unit sphere, we can simplify this to +```math +\int_{\mathrm{Spin}(3)} f\, d^3\Omega += \int_0^{2π} \int_0^{π} \int_0^{4π} f\, \sin β\, dα\, dβ\, dγ, +``` +where ``\int_{\mathrm{Spin}(3)} d^3\Omega = 2π^2``. Finally, +restricting to the space of rotations, we can further simplify this to +```math +\int_{\mathrm{SO}(3)} f\, d^3\Omega += \int_0^{2π} \int_0^{π} \int_0^{2π} f\, \sin β\, dα\, dβ\, dγ, +``` +where ``\int_{\mathrm{SO}(3)} d^3\Omega = π^2``. + +## 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{𝔯}\right) += \cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{𝔯}, +``` +where ``\rho`` is an angle of rotation and ``\hat{𝔯}`` is a +unit "pure-vector" quaternion. We can multiply a vector ``𝐯`` as +```math +𝐑\, 𝐯\, 𝐑^{-1}. +``` +Splitting ``𝐯 = 𝐯_⟂ + 𝐯_∥`` into components perpendicular and +parallel to ``\hat{𝔯}``, we see that ``𝐯_∥`` commutes with +``𝐑`` and ``𝐑^{-1}``, while ``𝐯_⟂`` anticommutes with +``\hat{𝔯}``. To find the full rotation, we expand the +product: +```math +\begin{aligned} +𝐑\, 𝐯\, 𝐑^{-1} +&= 𝐯_∥ + + \left(\cos\frac{\rho}{2} + \sin\frac{\rho}{2}\, \hat{𝔯}\right) + 𝐯_⟂ + \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{𝔯}\right) \\ +&= 𝐯_∥ + + \left(\cos\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \hat{𝔯}\, 𝐯_⟂\right) + \left(\cos\frac{\rho}{2} - \sin\frac{\rho}{2}\, \hat{𝔯}\right) \\ +&= 𝐯_∥ + + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, \hat{𝔯}\, 𝐯_⟂ + - \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, 𝐯_⟂ \, \hat{𝔯} - \sin^2\frac{\rho}{2}\, \hat{𝔯}\, 𝐯_⟂\, \hat{𝔯} \\ +&= 𝐯_∥ + + \cos^2\frac{\rho}{2}\, 𝐯_⟂ + \sin\frac{\rho}{2}\, \cos\frac{\rho}{2}\, [\hat{𝔯}, 𝐯_⟂] - \sin^2\frac{\rho}{2}\, 𝐯_⟂ \\ +&= 𝐯_∥ + + \cos\rho\, 𝐯_⟂ + \sin\rho\, \hat{𝔯}\times 𝐯_⟂ +\end{aligned} +``` +The final expression shows that this is precisely what we expect when +rotating ``𝐯`` through an angle ``\rho`` (in a positive, right-handed +sense) about the axis ``\hat{𝔯}``. + +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 + +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 +``γ`` about the ``𝐳`` axis, followed by a rotation through +``β`` about the ``𝐲`` axis, and finally a rotation through +``α`` about the ``𝐳`` axis. Note that the axes are fixed, and +not subject to any preceding rotations. More precisely, we can write +the unit quaternion as +```math +𝐑 = \exp\left(\frac{α}{2} 𝐤\right) + \exp\left(\frac{β}{2} 𝐣\right) + \exp\left(\frac{γ}{2} 𝐤\right). +``` +One of the more important interpretations of a rotor is considering +what it does to the basis triad ``(𝐱, 𝐲, 𝐳)``. In particular, the +vector ``𝐳`` is rotated onto the point given by spherical coordinates +``(θ, ϕ) = (β, α)``, while ``𝐱`` and ``𝐲`` are +rotated into the plane spanned by the unit basis vectors +``\boldsymbol{θ}`` and ``\boldsymbol{ϕ}`` corresponding to +that point. If ``γ = 0`` the rotation is precise, meaning that +``𝐱`` is rotated onto ``\boldsymbol{θ}`` and ``𝐲`` onto +``\boldsymbol{ϕ}``; if ``γ ≠ 0`` then they are rotated within +that plane by the angle ``γ`` about the ``𝐫`` axis. +Thus, we identify the spherical coordinates ``(θ, ϕ)`` with +the Euler angles ``(α, β, γ) = (ϕ, θ, 0)``. + + +## Rotation and angular-momentum operators + +### 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 ``𝕊³``), ``\mathrm{SO}(3)`` +(topologically ``\mathbb{RP}^3``), and ``𝕊²``. We will be defining +complex-valued functions on these spaces, and defining operators to +construct and classify them. In particular, because we have +constructed the spaces, they are naturally supplied with coordinates +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 ``𝕊¹ \times I +\times 𝕊¹`` instead of the ``𝕊³`` and ``\mathbb{RP}^3`` topologies +of the spaces they represent; spherical coordinates have topology +``𝕊¹ \times I`` instead of ``𝕊²``. + +Defining functions on the coordinate system of a space is subtly +different from defining functions on the space itself. For example, +spin-weighted functions are generally written as functions of +(``𝕊²``) spherical coordinates. However, they *cannot* be defined as +functions on ``𝕊²`` itself; some notion of a reference tangent +direction is needed at each point. The difference is that spherical +*coordinates* supply a natural choice for the reference tangent +direction: the unit vector in the ``\boldsymbol{θ}`` 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. + +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 +
+ + + + +A +B +C + + +m + + +f + + +F + + +
+``` +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^𝔤`` for +some finite quaternion ``𝔤``, which is referred to as the +"generator" of the action of ``e^𝔤``. This can act on a +function ``f`` by multiplying the argument by ``e^𝔤``. +However, there is an ambiguity: we could multiply either on the left +or the right:[^2] +```math +f\left(𝐐\right) \mapsto f\left(e^𝔤 𝐐\right) +\qquad \text{or} \qquad +f\left(𝐐\right) \mapsto f\left(𝐐 e^𝔤\right). +``` +There is an additional ambiguity, in that this action rotates the +*argument* of the function, whereas we will often prefer to think in +terms of rotating the *function* itself. For example, our function +may describe the measurement of some field in a particular coordinate +system. Here, the argument ``𝐐`` describes a particular +value of the coordinates, and ``e^𝔤`` changes the point +under consideration. If, on the other hand ``e^𝔤`` +describes how the field itself is rotated, then we can write the +rotated field as a function ``f'`` which is related to the original +function ``f`` by +```math +f'\left(𝐐\right) = f\left(e^{-𝔤} 𝐐\right) +\qquad \text{or} \qquad +f'\left(𝐐\right) = f\left(𝐐 e^{-𝔤}\right). +``` +Note that the exponent is negated, because the action of +``e^𝔤`` on the argument is the inverse of the action of +``e^{-𝔤}`` on the function. This is a general property of +the action of a group on a space, and is a consequence of the group +action being a homomorphism. + +[^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 ``𝕊²``. We define a function on +spherical coordinates as +```math +f(θ, ϕ) = \sin θ \sin ϕ. +``` +Recall that we can map the spherical coordinates into the Euler +angles, and the Euler angles into the quaternion +```math +(θ, ϕ) \mapsto (ϕ, θ, 0) \mapsto 𝐐 += +\exp\left(\frac{ϕ}{2} 𝐤\right) +\exp\left(\frac{θ}{2} 𝐣\right). +``` +It is straightforward to see that we can write ``f`` as a function of +``𝐐`` as +```math +f(𝐐) = \left\langle 𝐐\, 𝐤\, 𝐐^{-1} \right\rangle_{𝐣}, +``` +where the angle brackets and subscript indicate that we are taking the +``𝐣`` component. That is, ``f`` is the ``y`` component of +the vector ``𝐳`` rotated by ``𝐐``. + +Now, we imagine rotating the field by an angle ``α`` in the +positive sense about the ``z`` axis. Visualizing the situation, we +can see that the rotated field should be represented by +```math +f'(θ, ϕ) = \sin θ \sin(ϕ - α). +``` +For example, the rotated field evaluated at the point ``(θ, ϕ) += (π/2, 0)`` along the positive ``x`` axis should correspond to the +original field evaluated at the point ``(θ, ϕ) = (π/2, +-α)``. This rotation is generated by ``𝔤 = α +𝐤 / 2``, which allows us to immediately calculate +```math +\begin{aligned} +f(e^𝔤 𝐐) &= \sin θ \sin(ϕ + α) &&& +f(𝐐 e^𝔤) &= \sin θ \sin ϕ \\ +f(e^{-𝔤} 𝐐) &= \sin θ \sin(ϕ - α) &&& +f(𝐐 e^{-𝔤}) &= \sin θ \sin ϕ. +\end{aligned} +``` +Thus, we see that left-multiplication by ``e^{-𝔤}`` +corresponds to rotation of the field while leaving the coordinates +fixed; left-multiplication by ``e^𝔤`` corresponds to +rotation of the coordinates while leaving the field fixed; and +right-multiplication by either doesn't affect this function at all. + +Of course, right-multiplication using other choices for +``𝔤`` could certainly have some effect on this function, +and this choice of ``𝔤`` could have an effect on other +functions. Note that right-multiplication can also be interpreted as +left-multiplication, where the generator itself is rotated by the +argument to the function. That is, +```math +\begin{aligned} +f(𝐐 e^𝔤) + &= f(𝐐 e^{𝔤} 𝐐^{-1} 𝐐) + = f(e^{𝔤'} 𝐐) \\ +f(𝐐 e^{-𝔤}) + &= f(𝐐 e^{-𝔤} 𝐐^{-1} 𝐐) + = f(e^{-𝔤'} 𝐐), +\end{aligned} +``` +where ``𝔤' = 𝐐 𝔤 𝐐^{-1}``. In +this example, ``𝔤'`` generates a rotation by an angle +``α`` 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 +``α`` is the defining feature of a *spin-weighted* function. + +### Differential rotations + +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_{𝔤} f(𝐐) &= \lambda \left. \frac{\partial} {\partial θ} f \left( e^{-θ 𝔤 / 2} 𝐐 \right) \right|_{θ=0}, \\ +R_{𝔤} f(𝐐) &= \rho \left. \frac{\partial} {\partial θ} f \left( 𝐐 e^{-θ 𝔤 / 2} \right) \right|_{θ=0}. +\end{aligned} +``` +Here, we have introduced the constants ``\lambda`` and ``\rho`` +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 +the order of operations, which may look slightly unnatural: +```math +\begin{aligned} + L_𝔤 L_𝔥 f(𝐐) + % &= \left. \lambda \frac{\partial} {\partial γ} f'\left(e^{-γ 𝔤 / 2} 𝐐 \right) \right|_{γ=0}, \\ + &= \left. \lambda^2 \frac{\partial} {\partial γ} \frac{\partial} {\partial \eta} f\left(e^{-\eta 𝔥 / 2} e^{-γ 𝔤 / 2} 𝐐 \right) \right|_{γ=\eta=0}, \\ + R_𝔤 R_𝔥 f(𝐐) + % &= \rho \left. \frac{\partial} {\partial γ} f' \left( 𝐐 e^{-γ 𝔤 / 2} \right) \right|_{γ=0} \\ + &= \left. \rho^2 \frac{\partial} {\partial γ} \frac{\partial} {\partial \eta} f\left( 𝐐 e^{-γ 𝔤 / 2} e^{-\eta 𝔥 / 2} \right) \right|_{γ=\eta=0}. +\end{aligned} +``` +We can prove the first of these, for example, by defining +``f'(𝐐) = L_𝔥 f(𝐐)``, then applying the +definition of ``L_𝔤`` to ``f'(𝐐)``, and finally +substituting the definition of ``f'`` back in. If we failed to use +the correct order of operations, we would get sign errors when trying +to evaluate the commutators. + +These operators have some nice properties. For any scalar ``s``, we have +```math +\begin{aligned} +L_{s 𝔤} &= s L_{𝔤}, \\ +R_{s 𝔤} &= s R_{𝔤}. +\end{aligned} +``` +Given any basis ``𝐞_n`` for the quaternions, we can use +the multivariable chain rule to expand the operators in terms of +components: +```math +\begin{aligned} +L_{𝔤} &= \sum_n g_n\, L_{𝐞_n}, \\ +R_{𝔤} &= \sum_n g_n\, R_{𝐞_n}. +\end{aligned} +``` +This implies that vector addition holds more generally: +```math +\begin{aligned} +L_{𝔤 + 𝔥} &= L_{𝔤} + L_{𝔥} \\ +R_{𝔤 + 𝔥} &= R_{𝔤} + R_{𝔥}. +\end{aligned} +``` +Moreover, we can show that these operators form a Lie algebra with the +commutator as the Lie bracket. That is, we have +```math +\begin{aligned} +[L_{𝔤}, L_{𝔥}] + &= \frac{\lambda}{2} L_{[𝔤, 𝔥]}, +\\ +[R_{𝔤}, R_{𝔥}] + &= -\frac{\rho}{2} R_{[𝔤, 𝔥]}, +\\ +[L_{𝔤}, R_{𝔥}] &= 0. +\end{aligned} +``` + +Conventionally, we single out the ``𝐳`` axis — or +equivalently the generator ``𝐤 = 𝐲𝐱`` — as +a sort of fiducial axis, and ``L_z = L_𝐤`` and ``R_z = +R_𝐤`` as the fiducial operators. Then, *by definition*, +their raising operators ``L_+`` and ``R_+`` and lowering operators +``L_-`` and ``R_-`` satisfy +```math +\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_𝐳``, 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_𝐱 \pm i L_𝐲, \\ +R_\pm &= R_𝐱 \pm i R_𝐲. +\end{aligned} +``` + +* 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 +```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(θ/2)``, rederive the new + Euler angles from that result, and use the chain rule + - Show for both the three- and two-spheres + - Show how they act on functions on the three-sphere + +The idea here is to express, e.g., $e^{θ 𝐞_i / +2}𝐑_{α, β, γ}$ in quaternion components, then +solve for the new Euler angles $𝐑_{α', β', γ'}$ +in terms of the quaternion components, where these new angles all +depend on $θ$. We then use the chain rule to express +$\partial_θ$ in terms of $\partial_{α'}$, etc., which become +$\partial_α$, etc., when $θ=0$. + + +```math +\begin{aligned} + L_i f(𝐑_{α, β, γ}) + &= + \left. -𝐳 \frac{\partial} {\partial θ} f \left( e^{θ 𝐞_i / 2} 𝐑_{α, β, γ} \right) \right|_{θ=0} \\ + &= + \left. -𝐳 \frac{\partial} {\partial θ} f \left( 𝐑_{α', β', γ'} \right) \right|_{θ=0} \\ + &= + \left. -𝐳 \left[ \frac{\partial α'} {\partial θ}\frac{\partial} {\partial α'} + \frac{\partial β'} {\partial θ}\frac{\partial} {\partial β'} + \frac{\partial γ'} {\partial θ}\frac{\partial} {\partial γ'} \right] f \left( 𝐑_{α', β', γ'} \right) \right|_{θ=0} \\ + &= + -𝐳 \left[ \frac{\partial α'} {\partial θ}\frac{\partial} {\partial α} + \frac{\partial β'} {\partial θ}\frac{\partial} {\partial β} + \frac{\partial γ'} {\partial θ}\frac{\partial} {\partial γ} \right]_{θ=0} f \left( 𝐑_{α, β, γ} \right) \\ + K_i f(𝐑_{α, β, γ}) + &= + -𝐳 \left[ \frac{\partial α''} {\partial θ}\frac{\partial} {\partial α} + \frac{\partial β''} {\partial θ}\frac{\partial} {\partial β} + \frac{\partial γ''} {\partial θ}\frac{\partial} {\partial γ} \right]_{θ=0} f \left( 𝐑_{α, β, γ} \right), +\end{aligned} +``` + +```math +\begin{aligned} +𝐑_{α, β, γ} +&= + R\, \cos\frac{β}{2} \cos\frac{α+γ}{2} + -R\, \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 + + R\, \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 + + R\, \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤. +\\ +e^{θ 𝐮 / 2} 𝐑_{α, β, γ} +&= \left(\cos\frac{θ}{2} + 𝐮 \sin\frac{θ}{2}\right) 𝐑_{α, β, γ} +\\ +&= + R\, \cos\frac{θ}{2} \cos\frac{β}{2} \cos\frac{α+γ}{2} + -R\, \cos\frac{θ}{2} \sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐢 + + R\, \cos\frac{θ}{2} \sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐣 + + R\, \cos\frac{θ}{2} \cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐤 +\\ +&\quad + + R\, \sin\frac{θ}{2}\cos\frac{β}{2} \cos\frac{α+γ}{2} 𝐮 + -R\, \sin\frac{θ}{2}\sin\frac{β}{2} \sin\frac{α-γ}{2} 𝐮𝐢 + + R\, \sin\frac{θ}{2}\sin\frac{β}{2} \cos\frac{α-γ}{2} 𝐮𝐣 + + R\, \sin\frac{θ}{2}\cos\frac{β}{2} \sin\frac{α+γ}{2} 𝐮𝐤 +\end{aligned} +``` + +```math +\begin{aligned} +α &= \arctan\frac{Z}{W} + \arctan\frac{-X}{Y} &&\in [0, 2π), \\ +β &= 2\arccos\sqrt{\frac{W^2+Z^2}{W^2+X^2+Y^2+Z^2}} &&\in [0, 2π], \\ +γ &= \arctan\frac{Z}{W} - \arctan\frac{-X}{Y} &&\in [0, 2π), +\end{aligned} +``` + + +### 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_{𝕊^{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_{𝕊^{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 ``-ℓ(ℓ+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^ℓ} \frac{\partial^2 r^ℓ}{\partial r^2} ++ +\frac{f}{r^ℓ} \frac{n-1}{r} \frac{\partial r^ℓ}{\partial r} += +ℓ(ℓ-1) \frac{f}{r^ℓ} r^{ℓ-2} ++ +ℓ \frac{f}{r^ℓ} \frac{n-1}{r} r^{ℓ-1} += +ℓ(ℓ-1) \frac{f}{r^2} ++ +ℓ (n-1) \frac{f}{r^2} += +ℓ(ℓ+n-2) \frac{f}{r^2} +\to +ℓ(ℓ+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``. + +[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 ``ℓ = m`` mode varies most rapidly with +longitude but not at all with latitude, while the ``ℓ = 0`` mode +varies just as rapidly with latitude but not at all with longitude. + +[Vasil_2019](@citet) use spin-weighted spherical harmonics to do +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 + 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 +> |α\rangle_R = \mathscr{D}(R) |α\rangle, +> ``` +> ``|α\rangle_R`` and ``|α\rangle`` stand for the kets of +> the rotated and original system, respectively. + +If the field is represented as a function ``f(𝐑)``, then rotating the +field by ``e^{ϵ 𝐮/2}`` is equivalent to rotating the argument +of the function by ``e^{-ϵ 𝐮/2}``: +```math +\begin{aligned} +f\left(𝐑\right) +&\to +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ +&\approx +f\left(𝐑\right) + ϵ \left. \frac{d}{dϵ} \right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ +&= +f\left(𝐑\right) - i ϵ L_𝐮 f\left(𝐑\right). +\end{aligned} +``` +This final expression is precisely equivalent to Sakurai's Eq. (3.1.15): +```math +\mathscr{D}\left(\hat{𝐧}, dϕ \right) += +1 - i \left( 𝐉 \cdot \hat{𝐧} \right) dϕ. +``` + +Now, we can write the eigenkets of ``L^2`` and ``L_z`` as ``|ℓ, +m\rangle``, where the eigenvalues are ``ℓ(ℓ+1)`` and ``m``, +respectively. Finally, define the 𝔇 matrix as (Eq. 3.5.42) +```math +𝔇^{(ℓ)}_{m',m}(R) += +\langle ℓ, m' | 𝔇(R) | ℓ, m \rangle. +``` +Sakurai notes the important result that (Eq. 3.5.46) +```math +𝔇^{(ℓ)}_{m'',m}(R_1\, R_2) += +\sum_{m'} 𝔇^{(ℓ)}_{m'',m'}(R_1) 𝔇^{(ℓ)}_{m',m}(R_2), +``` +and we can readily find the essential behavior with respect to the +first and last Euler angles (Eq. 3.5.50): +```math +\begin{aligned} +𝔇^{(ℓ)}_{m',m}(α, β, γ) +&= +\langle ℓ, m' | + \exp[-iL_z α]\exp[-iL_y β]\exp[-iL_z γ] +| ℓ, m \rangle \\ +&= +\exp[-i(m' α+mγ)] +\langle ℓ, m' | \exp[-iL_y β] | ℓ, m \rangle. +\end{aligned} +``` +To belabor this point, recall that in general +```math +\left(\left\langle ψ | A\, B\, C | \chi \right\rangle\right)^\ast += +\left\langle \chi | C^\dag\, B^\dag\, A^\dag | ψ \right\rangle, +``` +and +```math +\left( e^{-i ϵ L_u} \right)^\dag += +e^{i ϵ L_u^\dag} += +e^{i ϵ L_u}. +``` +Together with the eigenvalue property for the ``L_z`` operator acting +on a ket, this allows us to derive the above result by factoring out +the first and last operators. + +Now we are left with the middle operator, which we use to define +```math +\begin{aligned} +d^{(ℓ)}_{m',m}(β) +&= +\langle ℓ, m' | \exp[-iL_y β] | ℓ, m \rangle. +\end{aligned} +``` +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} \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{(ℓ+m+k-j)!}{(ℓ+m)!}\,\frac{(ℓ-m)!}{(ℓ-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 McEwen and Wiaux use (and credit to Risbo): +```math +\exp\left[ β 𝐣 / 2 \right] += +\exp\left[ π 𝐤 / 4 \right] +\exp\left[ π 𝐣 / 4 \right] +\exp\left[ β 𝐤 / 2 \right] +\exp\left[ -π 𝐣 / 4 \right] +\exp\left[ -π 𝐤 / 4 \right] +``` +The 𝔇 matrices corresponding to the ``𝐤`` rotations are simple +phases, which converts the problem into one of finding the 𝔇 matrices +for the ``𝐣`` rotations through angles of ``\pm π/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 + - 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 ``𝔇`` 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(π \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 + +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π}) +e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2π)``, while the +set ``\{1/c_{n,m} P_n^m(\cos θ)`` is an orthonormal basis of +``L^2(0, π)`` in the ``θ`` coordinate. Therefore, the product +of these two sets is an orthonormal basis of the product space +``L^2\left((0,2π) \times (0, π)\right)``, which forms a coordinate +space for ``𝕊²``. I would probably modify this to point out that +``(0,2π)`` is really ``𝕊¹``, and then we could extend it to point +out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, +which happens to give us the Wigner D-matrices. + +## Recursion relations + +[Gumerov and Duraiswami (2001)](@cite Gumerov_2001) derive their +recursion relations by differentiating solutions of the Helmholtz +equation ``\nabla^2 ψ + k^2 ψ = 0`` as ``\tfrac{1}{k} \nabla +ψ``. More precisely, they differentiate both sides of the equation +relating one solution to its rotated form — which naturally involves +Wigner's ``𝔇`` matrix. Using orthogonal basis functions +for the solution, this allows them to equate terms on the two sides +proportional to a given basis function, which leaves them with +expressions involving sums of only the ``𝔇`` matrices and +some coefficients depending on the indices of the basis functions (and +hence of ``𝔇``) on both sides of the equation. Since +``\nabla`` is a 3-vector operator, this gives them three relations. + +This, of course, is happening in 3-D space, since ``ψ`` is a +function of location in the Helmholtz equation. It seems likely to +me, however, that we could use the 4-D (quaternionic) version of the +functions. Note that G&D use ``\partial_z`` and ``\partial_x \pm i +\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. Or maybe just use something +like ``𝐫 ∧ L``, which should also have 3 degrees of freedom. + +The SWSHs/``𝔇`` functions can be naturally promoted to +functions not just on the 3-sphere, but also in 4-D space just by +allowing the quaternions to be non-unit quaternions. diff --git a/docs/src/conventions/outline.md b/docs/src/conventions/outline.md new file mode 100644 index 00000000..22e9be29 --- /dev/null +++ b/docs/src/conventions/outline.md @@ -0,0 +1,445 @@ +# 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 ``𝔇`` 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(π \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 + + + +# Notes + +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π}) +e^{imϕ}\}`` is an orthonormal basis of ``L^2(0,2π)``, while the +set ``\{1/c_{n,m} P_n^m(\cos θ)`` is an orthonormal basis of +``L^2(0, π)`` in the ``θ`` coordinate. Therefore, the product +of these two sets is an orthonormal basis of the product space +``L^2\left((0,2π) \times (0, π)\right)``, which forms a coordinate +space for ``𝕊²``. I would probably modify this to point out that +``(0,2π)`` is really ``𝕊¹``, and then we could extend it to point +out that you can throw on another factor of ``𝕊¹`` to cover ``𝕊³``, +which happens to give us the Wigner D-matrices. + +We first define the rotor that takes ``(\hat{x}, \hat{y}, \hat{z})`` +onto ``(\hat{θ}, \hat{ϕ}, \hat{r})``. Then, we can invert +that, so that given a rotor that specifies such a rotation exactly, we +can get the spherical coordinates — or specifically ``\sin θ``, +``\cos θ``, and ``\exp(iϕ)``. + +Then, with the universally agreed-upon ``Y`` as given in terms of +spherical coordinates, we can rewrite it directly to work with +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_{ℓ,m'}(R_{θ', ϕ'}) = \sum_m M_{m',m}(R) +{}_sY_{ℓ,m}(R_{θ, ϕ}), +``` +for some matrix ``M``. Note that I have written this as if the +``{}_sY`` functions are column vectors. The reason this happens is +because I want to write ``R_{θ', ϕ'} = R\, R_{θ, ϕ}``, +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 ``𝔇``. +* ``R_{θ, ϕ}`` is a unit quaternion that rotates the point + described by Cartesian coordinates (0,0,1) onto the point described + by spherical coordinates ``(θ, ϕ)``. +* Just textually, it makes the most sense to write + ```math + R_{θ', ϕ'} = R\, R_{θ, ϕ} + ``` + for some rotation ``R``. Now, we just need to interpret ``R``. +* Again, just textually, it makes the most sense to write + ```math + Y_{ℓ,m'}(θ', ϕ') = \sum_m 𝔇^{(ℓ)}_{m',m}(R) + Y_{ℓ,m}(θ, ϕ), + ``` + or, generalizing to spin-weighted spherical harmonics + ```math + {}_{s}Y_{ℓ,m'}(R_{θ', ϕ'}) = \sum_m 𝔇^{(ℓ)}_{m',m}(R) + {}_{s}Y_{ℓ,m}(R_{θ, ϕ}). + ``` +* We also have that ``𝔇`` obeys the representation + property, so + ```math + 𝔇^{(ℓ)}_{m',m''}(R_{θ', ϕ'}) + = \sum_{m} 𝔇^{(ℓ)}_{m',m}(R) + 𝔇^{(ℓ)}_{m,m''}(R_{θ, ϕ}). + ``` + - There is no reason that I can see to introduce a conjugation + - The fact that ``m''`` appears on both sides of the equation means + that it must correspond to ``s`` — though we have to check the + behavior under final rotation to determine the sign. + +```math +{}_{s}Y_{ℓ,m}(R_{θ, ϕ}) +\propto +𝔇^{(ℓ)}_{m,\propto s}(R_{θ, ϕ}) +``` + + +## collapsible markdown? + +```@raw html +
CLICK ME +``` +#### yes, even hidden code blocks! + +```julia +println("hello world!") +``` +```@raw html +
+``` + + +# More notes + + +## 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} |ℓ,m\rangle = α^{\pm}_{ℓ,m} |ℓ, m \pm 1\rangle +``` +to be real and positive. The reasoning behind this choice is +explained more fully in Section 2 of [Ufford and Shortley +(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(ℓ, m) = (-1)^ℓ \sqrt{\frac{2ℓ+1}{2} \frac{(ℓ+m)!}{(ℓ-m)!}} +\frac{1}{2^ℓ ℓ!} \frac{1}{\sin^mθ} +\frac{d^{ℓ-m}}{d(\cos θ)^{ℓ-m}} \sin^{2ℓ}θ. +``` +When multiplied by Eq. (5) ``\Phi(m) = e^{imϕ} / \sqrt{2π}``, +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 ``ℓ`` +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(1,\pm1) &= \mp \sqrt{\frac{3}{4}} \sin θ \\ +\Theta(2,0) &= \sqrt{\frac{5}{8}} (2\cos^2θ - \sin^2θ) & +\Theta(2,\pm1) &= \mp \sqrt{\frac{15}{4}} \cos θ \sin θ & +\Theta(2,\pm2) &= \sqrt{\frac{15}{16}} \sin^2θ \\ +\Theta(3,0) &= \sqrt{\frac{7}{8}} (2\cos^3θ - 3\cos θ\sin^2θ) & +\Theta(3,\pm1) &= \mp \sqrt{\frac{21}{32}} (4\cos^2θ\sin θ - \sin^3θ) & +\Theta(3,\pm2) &= \sqrt{\frac{105}{16}} \cos θ \sin^2θ & +\Theta(3,\pm3) &= \mp \sqrt{\frac{35}{32}} \sin^3θ +\end{aligned} +``` +These are tested, along with the results from automatic +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 + +* First, a couple points about ``-i\hbar``: + - The finite transformations look like ``\exp[-i θ L_j]``, but + the factor of ``i`` introduced here just cancels the one in the + ``L_j``, and the sign is just chosen to make the result consistent + with our notion of active or passive transformations. + - 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 = δ(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 +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 +``𝐋 = 𝐱 \times 𝐩``, where ``𝐱`` is +the position vector and ``𝐩`` is the momentum vector. In +quantum mechanics, there is further agreement that the momentum +operator becomes ``-i\hbar\nabla``. Thus, in operator form, the +angular momentum can be decomposed as +```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 ϕ \frac{\partial}{\partial θ} + \cot θ \cos ϕ \frac{\partial}{\partial ϕ} \right), \\ +L_y &= -i\hbar \left( \cos ϕ \frac{\partial}{\partial θ} - \cot θ \sin ϕ \frac{\partial}{\partial ϕ} \right), \\ +L_z &= -i\hbar \frac{\partial}{\partial ϕ}. +\end{aligned} +``` +The conventions we choose *must* be chosen to agree with these — +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(𝐑) &\colonequals -z \left. \frac{\partial}{\partial θ} +f\left(e^{θ 𝐞_j / 2} 𝐑 \right) \right|_{θ=0}, \\ +K_{j} f(𝐑) &\colonequals -z \left. \frac{\partial}{\partial θ} +f\left(𝐑 e^{θ 𝐞_j / 2}\right) \right|_{θ=0}, +\end{aligned} +``` +where ``𝐞_j`` is the unit vector in the ``j`` direction. +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 ϕ}, +``` +while Eq. (8) on the following page defines +```math +\begin{aligned} +L_x + i L_y &= \hbar e^{iϕ} \left( \frac{\partial}{\partial θ} + i \cot θ \frac{\partial}{\partial ϕ} \right), \\ +L_x - i L_y &= \hbar e^{-iϕ} \left(-\frac{\partial}{\partial θ} + i \cot θ \frac{\partial}{\partial ϕ} \right). +\end{aligned} +``` +Note that one is not the conjugate of the other! This is because of +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 α \cot β \frac{\partial}{\partial α} - \sin α \frac{\partial}{\partial β} + \frac{\cos α}{\sin β} \frac{\partial}{\partial γ} \right), \\ +L_y &= -i\hbar \left(-\sin α \cot β \frac{\partial}{\partial α} + \cos α \frac{\partial}{\partial β} + \frac{\sin α}{\sin β} \frac{\partial}{\partial γ} \right), \\ +L_z &= -i\hbar \frac{\partial}{\partial α}. +\end{aligned} +``` + + +## Wigner ``𝔇`` and ``d`` matrices + +Wigner's Eqs. (11.18) and (11.19) define the real orthogonal +transformation ``𝐑`` by +```math +x'_i = R_{ij} x_j +``` +and the operator ``𝐏_{𝐑}`` to act on a function +``f`` such that +```math +𝐏_{𝐑} f(x'_1, \ldots) = f(x_1, \ldots). +``` +Then, his Eq. (15.5) presumably implies +```math +Y_{ℓ,m}(ϑ', φ') += 𝐏_{\{α, β, γ\}} Y_{ℓ,m}(ϑ, φ) += \sum_{m'} 𝔇^{(ℓ)}(\{α, β, γ\})_{m',m} + Y_{ℓ,m'}(ϑ, φ), +``` +where ``\{α, β, γ\}`` takes ``(ϑ, φ)`` to +``(ϑ', φ')``. In any case, we can now leave behind this +``𝐏`` notation and just look at the beginning and end of the +equation above as the critical relationship in Wigner's notation. + + +Eq. (44b) of [Boyle (2016)](@cite Boyle_2016) says +```math +L_{\pm} 𝔇^{(ℓ)}_{m',m}(𝐑) += \sqrt{(ℓ \mp m')(ℓ \pm m' + 1)} 𝔇^{(ℓ)}_{m' \pm 1, m}(𝐑). +``` +while Eq. (21) relates the Wigner D-matrix to the spin-weighted spherical harmonics as +```math +{}_{s}Y_{ℓ,m}(𝐑) += (-1)^s \sqrt{\frac{2ℓ+1}{4π}} 𝔇^{(ℓ)}_{m,-s}(𝐑). +``` +Plugging the latter into the former, we get +```math +L_{\pm} {}_{s}Y_{ℓ,m}(𝐑) += \sqrt{(ℓ \mp m)(ℓ \pm m + 1)} {}_{s}Y_{ℓ,m \pm 1}(𝐑). +``` +That is, in our conventions we have +```math +α^{\pm}_{ℓ,m} = \sqrt{(ℓ \mp m)(ℓ \pm m + 1)}, +``` +which is always real and positive, and thus consistent with the Condon-Shortley phase +convention. + + +### Properties + +* ``D^j_{m'm}(α,β,γ) = (-1)^{m'-m} D^j_{-m',-m}(α,β,γ)^*`` +* ``(-1)^{m'-m}D^{j}_{mm'}(α,β,γ)=D^{j}_{m'm}(γ,β,α)`` +* ``d_{m',m}^{j}=(-1)^{m-m'}d_{m,m'}^{j}=d_{-m,-m'}^{j}`` + +```math +\begin{aligned} +d_{m',m}^{j}(π) &= (-1)^{j-m} δ_{m',-m} \\[6pt] +d_{m',m}^{j}(π-β) &= (-1)^{j+m'} d_{m',-m}^{j}(β)\\[6pt] +d_{m',m}^{j}(π+β) &= (-1)^{j-m} d_{m',-m}^{j}(β)\\[6pt] +d_{m',m}^{j}(2π+β) &= (-1)^{2j} d_{m',m}^{j}(β)\\[6pt] +d_{m',m}^{j}(-β) &= d_{m,m'}^{j}(β) = (-1)^{m'-m} d_{m',m}^{j}(β) +\end{aligned} +``` + + + + + + + +```math +\begin{gather} +R = \cos ϵ + \sin ϵ\, \hat{𝔯} \\ +R𝐯 = \cos ϵ 𝐯 + \sin ϵ\, \hat{𝔯}𝐯 \\ +R𝐯R^{-1} = (𝐯\cos ϵ + \sin ϵ\, \hat{𝔯}𝐯)(\cos ϵ - \sin ϵ\, \hat{𝔯}) \\ +R𝐯R^{-1} = 𝐯\cos^2ϵ + \sin^2ϵ\, \hat{𝔯}𝐯\hat{𝔯}^{-1} + \sin ϵ \cos ϵ\, (\hat{𝔯}𝐯 - 𝐯\hat{𝔯}) \\ +R𝐯R^{-1} = \begin{cases} +𝐯 & 𝐯 \hat{𝔯} = \hat{𝔯}𝐯 \\ +𝐯(\cos^2ϵ - \sin^2ϵ) + 2 \sin ϵ \cos ϵ\, \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ +\end{cases} \\ +R𝐯R^{-1} = \begin{cases} +𝐯 & 𝐯 \hat{𝔯} = \hat{𝔯}𝐯 \\ +\cos2ϵ 𝐯 + \sin2ϵ \frac{[\hat{𝔯}, 𝐯]}{2} & 𝐯 \hat{𝔯} = -\hat{𝔯}𝐯 \\ +\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/conventions/summary.md b/docs/src/conventions/summary.md new file mode 100644 index 00000000..05f120ef --- /dev/null +++ b/docs/src/conventions/summary.md @@ -0,0 +1,348 @@ +# Summary + +This page lists the most important conventions used in this package. +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 +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. + +## Fundamental coordinates +We use standard right-handed Cartesian coordinates ``(x, y, z)`` and +unit basis vectors ``(𝐱, 𝐲, 𝐳)``. + +## Spherical coordinates +We define spherical coordinates ``(r, θ, ϕ)`` and unit basis +vectors ``(𝐧, \boldsymbol{θ}, \boldsymbol{ϕ})``. The "polar +angle" ``θ \in [0, π]`` measures the angle between the +specified direction and the positive ``𝐳`` axis. The "azimuthal +angle" ``ϕ \in [0, 2π)`` measures the angle between the +projection of the specified direction onto the ``𝐱``-``𝐲`` plane and +the positive ``𝐱`` axis, with the positive ``𝐲`` axis corresponding +to the positive angle ``ϕ = π/2``. + +## Quaternions +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 +``𝐯`` as ``𝐑\, 𝐯\, 𝐑^{-1}``. Where relevant, rotations will be +assumed to be right-handed, so that a quaternion characterizing the +rotation through an angle ``ϑ`` about a unit vector ``𝐮`` can +be expressed as ``𝐑 = \exp(ϑ 𝐮/2)``. Note that ``-𝐑`` +would deliver the same *rotation*, which means that the group of unit +quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)`` is a *double cover* +of the group of rotations ``\mathrm{SO}(3)``. Nonetheless, ``𝐑`` and +``-𝐑`` are distinct quaternions, and represent distinct "spinors". + +## Spherical coordinates as quaternions +A point on the unit sphere with spherical coordinates ``(θ, +ϕ)`` can be represented by the unit quaternion +```math +𝐑_{θ, ϕ} += +\exp(ϕ 𝐤/2)\, \exp(θ 𝐣/2). +``` +This not only takes the positive ``𝐳`` axis to the specified +direction, but also takes the ``𝐱`` and ``𝐲`` axes onto the unit +basis vectors of the spherical coordinate system: +```math +\begin{aligned} +𝐧 &= 𝐑_{θ, ϕ}\, 𝐳\, 𝐑_{θ, ϕ}^{-1}, \\ +\boldsymbol{θ} &= 𝐑_{θ, ϕ}\, 𝐱\, 𝐑_{θ, ϕ}^{-1}, \\ +\boldsymbol{ϕ} &= 𝐑_{θ, ϕ}\, 𝐲\, 𝐑_{θ, ϕ}^{-1}. +\end{aligned} +``` + +## Euler angles (and spherical coordinates) +Euler angles parametrize a unit quaternion as +```math +𝐑_{α, β, γ} += +\exp(α 𝐤/2)\, \exp(β 𝐣/2)\, \exp(γ 𝐤/2). +``` +The angles ``α`` and ``γ`` take values in ``[0, 2π)``. +The angle ``β`` takes values in ``[0, 2π]`` to parametrize the +group of unit quaternions ``\mathrm{Spin}(3) = \mathrm{SU}(2)``, or in +``[0, π]`` to parametrize the group of rotations ``\mathrm{SO}(3)``. + +By comparison, we can immediately see that spherical coordinates +``(θ, ϕ)`` can be represented as Euler angles with the +equivalence ``(α, β, γ) = (ϕ, θ, 0)``. In +particular, any function of spherical coordinates can be promoted to a +function on Euler angles using this identification. + +It's worth noting that the action of Euler angles on the Cartesian +basis is similar to the action of the spherical-coordinate quaternion, +but rotates the tangent basis ($\boldsymbol{θ}, +\boldsymbol{ϕ}$). That is, we still have +```math +𝐧 = 𝐑_{ϕ, θ, γ}\, 𝐳\, 𝐑_{ϕ, θ, γ}^{-1}, +``` +but the action on the ``𝐱`` and ``𝐲`` axes is a little more +complicated due to the initial rotation by ``\exp(γ 𝐤/2)``, +which is equivalent to a *final* rotation through ``γ`` about +``𝐧``. It's easier to write this down if we form the combination +```math +𝐦 = \frac{1}{\sqrt{2}} \left( + \boldsymbol{θ} + i \boldsymbol{ϕ} +\right), +``` +and find that +```math +𝐦 = e^{-iγ} 𝐑_{ϕ, θ, γ}\, \frac{1}{\sqrt{2}} \left( + 𝐱 + i 𝐲 +\right)\, 𝐑_{ϕ, θ, γ}^{-1}. +``` + +## Left and right angular-momentum operators +For a complex-valued function ``f(𝐑)``, we define two operators, the +left and right angular-momentum operators: +```math +L_𝐮 f(𝐑) = \left.i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}\, 𝐑\right) +\qquad \text{and} \qquad +R_𝐮 f(𝐑) = -\left.i \frac{d}{dϵ}\right|_{ϵ=0} +f\left(𝐑\, e^{-ϵ 𝐮/2}\right), +``` +where ``𝐮`` can be any quaternion, though unit pure-vector +quaternions are the most common. In particular, ``L`` represents the +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 α}{\tan β} \frac{\partial} {\partial α} + + \sin α \frac{\partial} {\partial β} + - \frac{\cos α}{\sin β} \frac{\partial} {\partial γ} +\right\}, +& +R_𝐢 &= i \left\{ + -\frac{\cos γ}{\sin β} \frac{\partial} {\partial α} + +\sin γ \frac{\partial} {\partial β} + +\frac{\cos γ}{\tan β} \frac{\partial} {\partial γ} +\right\}, +\\ +L_𝐣 &= i \left\{ + \frac{\sin α}{\tan β} \frac{\partial} {\partial α} + - \cos α \frac{\partial} {\partial β} + -\frac{\sin α}{\sin β} \frac{\partial} {\partial γ} +\right\}, +& +R_𝐣 &= i \left\{ + \frac{\sin γ}{\sin β} \frac{\partial} {\partial α} + +\cos γ \frac{\partial} {\partial β} + -\frac{\sin γ}{\tan β} \frac{\partial} {\partial γ} +\right\}, +\\ +L_𝐤 &= -i \frac{\partial} {\partial α}, +& +R_𝐤 &= i \frac{\partial} {\partial γ}. +\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 ϵ_{abc} L_c``, which is +precisely the standard result. We can also lift any function on +``𝕊²`` to a function on ``𝕊³`` — or more precisely any function on +spherical coordinates to a function on the space of Euler angles — by +the correspondence ``(θ, ϕ) \mapsto (α, β, γ) = +(ϕ, θ, 0)``. We can then express the angular-momentum +operators in their more common form, in terms of spherical +coordinates: +```math +L_x = i \left\{ + \frac{\cos ϕ}{\tan θ} \frac{\partial} {\partial ϕ} + + \sin ϕ \frac{\partial} {\partial θ} +\right\} +\qquad +L_y = i \left\{ + \frac{\sin ϕ}{\tan θ} \frac{\partial} {\partial ϕ} + - \cos ϕ \frac{\partial} {\partial θ} +\right\} +\qquad +L_z = -i \frac{\partial} {\partial ϕ} +``` +The ``R`` operators make less sense for a function of spherical +coordinates, because of their inherent dependence on ``γ.`` We +will come back to them, however, when we consider spin-weighted +functions — which are inherently ill-defined on the 2-sphere, but can +be interpreted as restrictions of functions on the 3-sphere with this +special "weight" property. + +## Spherical harmonics +There is essentially no disagreement in the literature about the +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: +```math +Y_{l,m}(𝐐) = \sqrt{\frac{2ℓ+1}{4π}} e^{imϕ} + D^{(l)}_{m,0}(𝐐), +``` +where ``D^{(l)}_{m,0}`` is the Wigner 𝔇 matrix. This is a + +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 +\begin{align} + Y_{l,m} + &= + \sqrt{\frac{2ℓ+1}{4π}} e^{imϕ} + \sum_{k = k_1}^{k_2} + \frac{(-1)^k ℓ! [(ℓ+m)!(ℓ-m)!]^{1/2}} + {(ℓ+m-k)!(ℓ-k)!k!(k-m)!} + \\ &\qquad \times + \left(\cos\left(\frac{θ}{2}\right)\right)^{2ℓ+m-2k} + \left(\sin\left(\frac{θ}{2}\right)\right)^{2k-m} +\end{align} +``` +where ``k_1 = \textrm{max}(0, m)`` and ``k_2=\textrm{min}(ℓ+m, +ℓ)``. 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 +as +```math +m^\mu = \frac{1}{\sqrt{2}} \left( + \boldsymbol{θ} + i \boldsymbol{ϕ} +\right)^\mu +``` +and discuss spin weight in terms of the rotation +```math +(m^\mu)' = e^{iψ} m^\mu, +``` +where the tangent basis rotates but we are "keeping the +coordinates fixed". They then define a function to have +spin weight ``s`` if it transforms as +```math +\eta' = e^{isψ} \eta. +``` +Such functions are generally the result of contracting a tensor field +with some number of ``m^\mu`` and some number of ``\bar{m}^\mu`` +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 ``(ϕ, θ, 0)``, while +``(m^\mu)'`` corresponds to the Euler angles ``(ϕ, θ, +-ψ)``. The function, written in terms of Euler angles, becomes +```math +\eta(ϕ, θ, -ψ) = e^{isψ} \eta(ϕ, θ, 0), +``` +or +```math +\eta(α, β, γ) = e^{-isγ} \eta(α, β, 0). +``` +This is the crucial definition giving us the behavior of +spin-weighted functions: they are eigenfunctions of the operator +``R_z = i \partial_γ`` with eigenvalue ``s``. + +We can make this a little less dependent on the choice of Euler +angles by writing ``\eta`` not as a function of Euler angles, but as +a function of a quaternion. We then have +```math +\eta(𝐐\, e^{γ 𝐤/2}) = e^{-isγ} \eta(𝐐), +``` +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} +\eth \eta &= \left(R_x + i R_y\right)\eta + = -\sin^s θ \left\{ + \frac{\partial}{\partial θ} + + \frac{i}{\sin θ} \frac{\partial}{\partial ϕ} + \right\} \left(\eta \sin^{-s} θ\right), \\ +\bar{\eth} \eta &= \left(R_x - i R_y\right)\eta + = -\sin^s θ \left\{ + \frac{\partial}{\partial θ} + - \frac{i}{\sin θ} \frac{\partial}{\partial ϕ} + \right\} \left(\eta \sin^{-s} θ\right). +\end{aligned} +``` +Here, we have used the full expressions for ``R_x`` and ``R_y`` +given above in terms of Euler angles, replacing the derivatives +with respect to ``γ`` 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. + +## 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ℓ+1}{4π}} e^{imϕ} + \sum_{k = k_1}^{k_2} + \frac{(-1)^k[(ℓ+m)!(ℓ-m)!(ℓ-s)!(ℓ+s)!]^{1/2}} + {(ℓ+m-k)!(ℓ+s-k)!k!(k-s-m)!} + \\ &\qquad \times + \left(\cos\left(\frac{\iota}{2}\right)\right)^{2ℓ+m+s-2k} + \left(\sin\left(\frac{\iota}{2}\right)\right)^{2k-s-m} +\end{align} +``` +where ``k_1 = \textrm{max}(0, m+s)`` and ``k_2=\textrm{min}(ℓ+m, +ℓ+s)``. + + +## Wigner D-matrices + + diff --git a/docs/src/deprecated/index.md b/docs/src/deprecated/index.md new file mode 100644 index 00000000..37c8e389 --- /dev/null +++ b/docs/src/deprecated/index.md @@ -0,0 +1,2 @@ +# Deprecations + diff --git a/docs/src/development/index.md b/docs/src/development/index.md new file mode 100644 index 00000000..90c273cd --- /dev/null +++ b/docs/src/development/index.md @@ -0,0 +1,90 @@ +# Common development tasks + +## Running tests + +You can run all tests with coverage and process that coverage *from +the package root* with: + +```bash +julia --project=. scripts/test.jl +``` + +Optionally, at the end of this command, specify names of individual +tests to run (in quotes if there are spaces), tags of tests to run +(which must start with a colon), or files to run all tests in (which +must end with `.jl`). If any are specified, only matching tests or +files will be run. By default, all tests in all files will be run. + +Optionally, either with or without any of the above specifications, +add `--skip` followed by one or more tests, tags, or files to skip. +These override any inclusion criteria specified earlier in the +command. Note that everything before `--skip` constitutes inclusion +criteria; everything after constitutes exclusion criteria. + +The names of individual tests or files can be given as regex patterns +(probably in quotes), and all such matches will be via `occursin` +matching, so that partial matches will work. Tags must be given +exactly as they appear in the code (including the colon). + +Note that any test with the `:skipci` tag will be skipped whenever the +environment variable `CI` is set to "true" (which is the case on +GitHub Actions), unless it is explicitly included in the command line +as a test to run. + +Here are some example invocations: + +```bash +# Run all tests in all files (except those tagged :skipci if CI=true) +julia --project=. scripts/test.jl + +# Run everything in complex_powers.jl +julia --project=. scripts/test.jl complex_powers.jl + +# Run only the ComplexPowers test +julia --project=. scripts/test.jl ComplexPowers + +# Run everything in complex_powers.jl except ComplexPowers (or :skipci if CI=true) +julia --project=. scripts/test.jl complex_powers.jl --skip ComplexPowers + +# Run everything in every file except ComplexPowers (or :skipci if CI=true) +julia --project=. scripts/test.jl --skip ComplexPowers + +# Run only tests tagged :fast +julia --project=. scripts/test.jl :fast + +# Run everything except tests tagged :slow (or :skipci if CI=true) +julia --project=. scripts/test.jl --skip :slow +``` + + +## Writing tests and coverage + +Tags can be added to individual test items, which can then be used either in the VS Code interface or the command line to include or exclude certain tests. + +```julia +@testitem "My testitem" tags=[:skipci, :slow] begin + @test my_function() == expected_value +end +``` + +It's a well hidden fact that you can turn coverage on and off by +adding certain comments around the code you don't want to check: + +```julia +# COV_EXCL_START +untested_code_that_wont_show_up_in_coverage() +# COV_EXCL_STOP +``` + + +## Building the documentation + +To build the documentation locally, run the following command from the +package root: + +```bash +julia --project=. scripts/docs.jl +``` + +By default, this will build the documentation, run the doctests, and +launch a local server to view the docs in your web browser. diff --git a/docs/src/development/literate_testitems.md b/docs/src/development/literate_testitems.md new file mode 100644 index 00000000..bfca5221 --- /dev/null +++ b/docs/src/development/literate_testitems.md @@ -0,0 +1,116 @@ +# Literate TestItems + +* Use TestItems to be able to run tests via VS Code +* Use Documenter (and DocumenterCitations) to build docs +* Write pages that extensively document *and test* different + conventions from the literature against this package + +* Use Literate to write files that generate pages for Documenter + +Because the conventions pages require significant amounts of code, +including as their own modules, it is helpful to be able to run the +tests in those pages separately from the main test suite — i.e., with +more granularity. This cannot be done with simple doctests, but can +be done with TestItems, which necessitates using Literate. + +I want these pages to stand on their own as documentation of the +various conventions, while also actively being used to test the +package's conventions. + +So, basically, I couldn't use doctests for two reasons: the sheer +complexity of the code and tests, and the need for modularity and +granularity in running the tests. + +Also note that these particular conventions often define things that +need to be differentiated, for which FastDifferentiation does a very +good job of converting those general formulas to useful +implementations. + + + + + +This package computes things like Wigner's 𝔇 matrices. If you've +ever dealt with these things — or even just rotations generally — +you'll know that the literature is an absolute quagmire of subtly +differing conventions wrapped up in terminology and notation from +hundreds of years ago. In an effort to sort some of this out, I +decided to carefully compare the conventions of as many significant +sources as I could — everything from Wikipedia and Mathematica, to +current quantum-mechanics textbooks, all the way back to the original +books and papers that introduced some of these concepts. So I went +through each reference, and wrote a documentation page for each one +that carefully laid out the conventions used in that source. + + + + + + + + + + +The +documentation for my package has a whole section collecting all of +these different pages. + + +TestItemRunner.jl + Literate.jl = 💪 + +(With honorable mention going to the excellent +[DocumenterCitations.jl](https://juliadocs.org/DocumenterCitations.jl/stable/) +and +[FastDifferentiation.jl](https://github.com/brianguenter/FastDifferentiation.jl) +packages.) + + +But I didn't just want to *document* these conventions; I wanted to +*test* how they compared to the implementations in my package. I +wanted to feed actual numbers into the actual expressions written down +by all these sources, and see if they agreed (or all too commonly, how +they disagreed) with my package's output. But I wanted thorough tests +— too much for simple doctests. + +All these expressions are, naturally, given very similar or identical +names in the literature, which could lead to collisions; we have to +define them in a separate module for each reference. Of course, I +couldn't have all these extra inefficient and inaccurate functions +cluttering up my actual package, so these modules would have to be +defined elsewhere. + +I wanted to be able to run the tests automatically, as part of the +documentation-build process and on their own. Of course, I also +wanted to be able to test what I was writing *as I was going through* +and writing the page for any given reference, because the results of +those tests would determine what I should write in the documentation. +This meant that I needed the test suite to be as modular and fast as +possible — preferably testing just the reference I was working on, +rather than my entire test suite, so that I could iterate quickly. + +* The critical feature of Literate.jl is that it takes actual runnable + Julia scripts as input — rather than markdown that has fenced Julia + code, for example — which allows TestItemRunner.jl to find the tests + and run them properly. This does not appear to be possible with + Quarto. It looks like it *would* work with Weave.jl; I just happen + to not use it in my workflow (because I'm not sure how easy it is to + integrate with Documenter.jl and DocumenterCitations.jl). +* TODO: Use TestItemRunner as part of doc building process, with + [filtering](https://www.julia-vscode.org/docs/stable/userguide/testitems/#Filtering-support-in-TestItemRunner.jl) + to select tests in the docs. +* TODO: I'm adding `using TestItems: @testmodule, @testitem #hide` at + the top of each page; could this be put somewhere else? +* `@testmodule` and `@testitem` and their corresponding `end` + statements go on their own lines, followed by `#hide` to make the + output look nicer, while still functioning properly. Note that + `#src` is *not* the right thing to do, as it will fail to provide + those lines to Literate's execution, which needs them in order to + skip them — which we presumably want to do so that the tests won't + run and possibly pollute the namespace. +* Inside those blocks, all the code output has to end up in the same + `@example` section, because they're all part of the same `@testitem` + or whatever, which means we have to put `#+` on its own line + immediately after any code and before any non-code (markdown). +* Mention how FastDifferentiation makes it easy to test expressions + that explicitly use derivatives. +* Mention DocumenterCitations.jl for managing citations in the docs. diff --git a/docs/src/index.md b/docs/src/index.md index c03f94eb..cd93cdf5 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,46 +1,70 @@ # 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) +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_{ℓ,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) \cong 𝐒𝐔(2)`` that covers it +— these functions are evaluated directly in terms of quaternions. +Concessions are also made for more standard forms of spherical +coordinates and Euler angles.[^1] Among other applications, those +functions permit "synthesis" (evaluation of the spin-weighted +spherical functions) of spin-weighted spherical harmonic coefficients +on regular or distorted grids. This package also includes functions +enabling efficient "analysis" (decomposition into mode coefficients) of functions evaluated on regular grids to high order and accuracy. -These quantities are computed using recursion relations, which makes it possible -to compute to very high ℓ values. Unlike direct 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 +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 underlying float type. + +The conventions for this package diverge from its predecessors found [here](https://moble.github.io/spherical_functions/) and -[here](https://moble.github.io/spherical/). +[here](https://moble.github.io/spherical/), but are described in +detail on [this page](@ref Summary) and the following pages, including +detailed comparisons to other sources that are tested automatically +with each change to this code. -Note that numerous other packages cover some of these use cases, including +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]: diff --git a/docs/src/index_of_docstrings.md b/docs/src/index_of_docstrings.md new file mode 100644 index 00000000..5671b5a4 --- /dev/null +++ b/docs/src/index_of_docstrings.md @@ -0,0 +1,8 @@ +# Index of docstrings + +Here, we collect links to all docstrings provided throughout the +documentation. + +```@index +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +``` diff --git a/docs/src/interface/operators.md b/docs/src/interface/operators.md new file mode 100644 index 00000000..c8e6f4e1 --- /dev/null +++ b/docs/src/interface/operators.md @@ -0,0 +1,9 @@ +# [Differential operators](@id interface_differential_operators) + + +## Docstrings + +```@autodocs +Modules = [SphericalFunctions] +Pages = ["operators.jl"] +``` diff --git a/docs/src/interface/sYlm.md b/docs/src/interface/sYlm.md new file mode 100644 index 00000000..4d4d0b94 --- /dev/null +++ b/docs/src/interface/sYlm.md @@ -0,0 +1,70 @@ +# [``{}_{s}Y_{ℓ,m}`` functions](@id interface_sYlm) + +```@meta +CurrentModule = SphericalFunctions.Deprecated +``` + +The spin-weighted spherical harmonics are an [important set of +functions defined on](@cite Boyle_2016) the rotation group +``𝐒𝐎(3)``, or more generally, the spin group ``𝐒𝐩𝐢𝐧(3)`` that +covers it. They are eigenfunctions of [the left- and right-Lie +derivatives](@ref "Differential operators"), and are particularly +useful in describing the angular dependence of polarized fields, like +the electromagnetic field and gravitational-wave field. Originally +introduced by [Newman_1966](@citet), they are essentially components +of Wigner's ``𝔇`` matrices: +```math +{}_{s}Y_{ℓ,m}(𝐑) + = (-1)^s \sqrt{\frac{2ℓ+1}{4π}} \, 𝔇^{(ℓ)}_{m, -s}(𝐑). +``` +As such, they can be computed using the same [``H`` recursion](@ref +"Algorithm for computing ``H``") algorithm as the Wigner +``𝔇^{(ℓ)}_{m, -s}`` matrices. But because not all values of ``s \in +-ℓ:ℓ`` are used, we can be much more efficient in both storage and +computation time. + +The user interface is very similar to the one for [Wigner's ``𝔇`` and +``d`` matrices](@ref): +```julia +using Quaternionic +using SphericalFunctions +using SphericalFunctions.Deprecated + +R = randn(RotorF64) +ℓₘₐₓ = 8 +s = -2 +Y = sYlm_values(R, ℓₘₐₓ, s) +``` +Again, the results can take up a lot of memory, so for maximum +efficiency when calling this function repeatedly with different `R` +values, it is best to pre-allocate the necessary memory with the +[`sYlm_prep`](@ref) function, and the pass that in as an argument to +[`sYlm_values!`](@ref): +```julia +Y_storage = sYlm_prep(ℓₘₐₓ, s) +Y = sYlm_values!(Y_storage, R, s) +``` +(Beware that, as noted in the documentation for +[`sYlm_values!`](@ref), the output `Y` is just a reference to part of +the `Y_storage` object, so you should not reuse `Y_storage` until you +have copied or otherwise finished using `Y`.) + +The output `Y` is a single vector of `Complex` numbers with the same +base type as `R`. The ordering of the elements is described in the +documentation for [`sYlm_values!`](@ref). It is also possible to +efficiently view slices of this vector as a series of individual +vectors using a [`sYlm_iterator`](@ref): +```julia +for (ℓ, Yˡ) in zip(0:ℓₘₐₓ, sYlm_iterator(Y, ℓₘₐₓ)) + # Do something with the matrix Yˡ[ℓ+m′+1, ℓ+m+1] +end +``` + +## Docstrings + +```@docs +sYlm_values +sYlm_values! +sYlm_prep +sYlm_iterator +``` diff --git a/docs/src/interface/transformations.md b/docs/src/interface/transformations.md new file mode 100644 index 00000000..8856a19b --- /dev/null +++ b/docs/src/interface/transformations.md @@ -0,0 +1,252 @@ +# ``s``-SHT Transformations + +Any square-integrable function on the sphere 𝕊² or 𝕊³ can be +represented as an expansion in spherical harmonics or spin-weighted +spherical harmonics, respectively. For a particular spin-weight +``s``, we can restrict the spin-weighted spherical harmonics to 𝕊² +(because its behavior on the remaining [𝕊¹ factor of +𝕊³](https://en.wikipedia.org/wiki/Hopf_fibration) is determined by +the spin). So, for example, if ``f`` is a function with spin weight +``s``, we will frequently write +```math +\begin{gathered} +f(θ, ϕ) = \sum_{ℓ = |s|}^{ℓₘₐₓ} \sum_{m = -ℓ}^{ℓ} f̃_{ℓ, m} + \, {}_{s}Y_{ℓ, m}(θ, ϕ) \\ +f(𝐑) = \sum_{ℓ = |s|}^{ℓₘₐₓ} \sum_{m = -ℓ}^{ℓ} f̃_{ℓ, m} + \, {}_{s}Y_{ℓ, m}(𝐑), +\end{gathered} +``` +where ``(θ, ϕ)`` are spherical coordinates on 𝕊², and we use the +rotor ``𝐑`` to describe a point on 𝕊³. The upper limit ``ℓₘₐₓ`` is +— in principle — infinite, but because we are finite ``ℓₘₐₓ`` will +also be finite in all applications here. Similarly, the set of points +on which we evaluate the function will also be finite. A little +terminology will be helpful: + + 1. Values ``f`` of the function evaluated on a set of points or + "pixels" in the domain of the function — sometimes called "nodes" + or the "map" values. + 2. Values ``f̃`` of the mode weights (coefficients) of the expansion + — usually called "modes" or sometimes "salm" (for ``_sa_{ℓ,m}``). + +Here, we are concerned with transformations between these two +representations of the function. In the literature, the +transformation ``f ↦ f̃`` is usually called "analysis" or `map2salm`, +while the inverse transformation ``f̃ ↦ f`` is called "synthesis" or +`salm2map`. These are both referred to as spin-spherical-harmonic +transforms, or ``s``-SHTs. + +## Synthesis + +The expressions written above already show us one way to transform +*from* modes ``f̃`` *to* function values ``f``. If the pixels on +which we evaluate are indexed by ``k``, then we can write +```math +f(𝐑_k) = \sum_{ℓ,m} f̃_{ℓ, m}\, {}_{s}Y_{ℓ, m}(𝐑_k). +``` +Considering the ``(ℓ,m)`` pairs as a single index, this is just a +matrix-vector multiplication, where the matrix elements are given by +```math +𝒯_{k, (ℓ,m)} = {}_{s}Y_{ℓ, m}(𝐑_k), +``` +and we might more compactly write +```math +f = 𝒯 \, f̃. +``` +In this expression, ``f̃`` is being treated as a column vector of mode +weights, and ``f`` is being treated as a column vector of function +values at the sequence of pixels. For instance, we will frequently +store the values ``f̃`` as the (column) vector +```julia +f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] +``` +(Here, `mode_weight` is just for illustration.) If this +transformation will be performed repeatedly, it can be very efficient +to pre-compute the matrix ``𝒯``, and capitalize on the impressive +efficiency of linear-algebra libraries to perform the transformation. +And indeed, this is the approach taken by the [`SSHTDirect`](@ref SphericalFunctions.Deprecated.SSHTDirect) +method. + +However, we must consider the memory requirements of this approach. +The number of nonzero mode weights is ``M = (ℓₘₐₓ+1)^2 - s^2``. If +there are ``N`` pixels in the pixelization, then the matrix ``𝒯`` has +size ``N × M``. Typically, we will use roughly the same number of +pixels as there are mode weights to be able to express roughly the +same number of degrees of freedom, so that ``N ≈ M``. Therefore, the +size of this matrix will typically be ``N × M ∼ ℓₘₐₓ^4``. For +standard complex numbers stored in 128 bits, this will exceed 1 GiB of +memory for ``ℓₘₐₓ ≳ 90``, and grow rapidly. The computational cost of +the matrix-vector multiplication will scale just as poorly. This +full-storage-matrix approach is very attractive, but only for +relatively small ``ℓₘₐₓ``. + +Another approach would be to compute the matrix elements as needed. +Given that the SWSH values are best computed via recurrence relations, +it would make sense to compute them row-by-row. This would +essentially eliminate the memory requirements. Though the +computational cost would still scale poorly, it would parallelize very +well, since each row is independent until a final summation. This is +not currently implemented, but the pieces are all in place, and it +will be implemented in the future. + +However, we can achieve significantly better performance at high +``ℓₘₐₓ`` if we select the pixelization carefully. Recall that a +spherical harmonic of *any* spin weight still varies azimuthally as +``e^{i m ϕ}``. Therefore, if we select a pixelization made up of a +series of rings, each of which is at a constant ``θ`` and has pixels +equally spaced in ``ϕ``, then we can use Fast Fourier Transforms +(FFTs) to perform the azimuthal integration very quickly. + + + + +# Analysis + +Analytically, we use orthogonality of the spin-weighted spherical +harmonics to compute the mode weights from the function values as +```math +\begin{gathered} +f̃_{ℓ, m} = \int_{𝕊²} f(θ, ϕ)\, {}_{s}Ȳ_{ℓ, m}(θ, ϕ) \, d^2Ω, \\ +f̃_{ℓ, m} = \int_{𝕊³} f(𝐑)\, {}_{s}Ȳ_{ℓ, m}(𝐑) \, d^3Ω. +\end{gathered} +``` +But — again because we are finite — we will only be able to evaluate +the function at a finite number of points, and so we will need to use +discrete quadrature to evaluate the integrals. + + + +To describe the mode weights of a spin-``s`` function up to (and +including) some maximum angular resolution ``ℓ_\mathrm{max}``, +there are ``(ℓ_\mathrm{max}+1)^2 - s^2`` mode weights. We assume +throughout that the values `f̃` are stored as the (column) vector +```julia +f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] +``` +(Here, `mode_weight` is a made-up function for schematic purposes.) In +particular, the ``m`` index varies most rapidly, and the ``ℓ`` +index varies most slowly. Correspondingly, there must be *at least* +``(ℓ_\mathrm{max}+1)^2 - s^2`` function values `f`. However, some +``s``-SHT algorithms require more function values — usually by a +factor of 2 or 4 — trading off between speed and memory usage. + +The `SSHT` object implements these transformations, storing +pre-computed constants and pre-allocated workspace for the +transformations. The interface is designed to be similar to that of +`AbstractFFTs.jl`, whereby an `SSHT` object `𝒯` can be used to +perform the transformation as either +```julia +f = 𝒯 * f̃ +``` +or +```julia +f̃ = 𝒯 \ f +``` +Currently, there are three algorithms implemented, each having different +advantages and disadvantages: + + 1. The "Direct" algorithm (introduced here for the first time), which is the + default, but should only be used up to ``ℓ_\mathrm{max} \lesssim 50`` + because its intermediate storage requirements scale as + ``ℓ_\mathrm{max}^4``. This algorithm is the fastest for small + ``ℓ_\mathrm{max}``, it can be used with arbitrary (non-degenerate) + pixelizations, and achieves optimal dimensionality. + 2. The "Minimal" algorithm due to [Elahi_2018](@citet), with some minor + improvements. This algorithm is fast and — as the name implies — also + achieves optimal dimensionality, and its storage scales as + ``ℓ_\mathrm{max}^3``. However, its pixelization is restricted, and its + accuracy at very high ``ℓ_\mathrm{max}`` is not as good as the "RS" + algorithm. The algorithm itself is not actually fully specified by Elahi + et al., and leaves out some relatively simple improvements, so I have had + to take some liberties with my interpretation. + 3. The "RS" algorithm due to [Reinecke_2013](@citet). This forms the basis + for the [`libsharp`](https://gitlab.mpcdf.mpg.de/mtr/libsharp) and + [`ducc.sht`](https://gitlab.mpcdf.mpg.de/mtr/ducc#duccsht) packages. It + requires pixelizations on "iso-latitude rings", and does not achieve + optimal dimensionality. However, it is very fast, and its accuracy is + excellent at extremely high ``ℓ_\mathrm{max}``. + + +## `SSHT` objects + +```@autodocs +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/ssht.jl", "deprecated/ssht/direct.jl", "deprecated/ssht/minimal.jl", "deprecated/ssht/rs.jl"] +``` + +## Pixelizations + +The algorithms implemented here require pixelizations. While the +"Direct" algorithm can be used with arbitrary pixelizations, the +"Minimal" and "RS" algorithms require more specific choices, as noted +in their docstrings. + +Typically, "pixelization" refers exclusively to a choice of points on +the sphere 𝕊² at which to compute function values. Of course, as +mentioned [elsewhere](@cite Boyle_2016), it is not *technically +possible* to define spin-weighted functions as functions of a point on +𝕊² alone; we also need some sense of reference direction in the +tangent space. Quite generally, we can define spin-weighted functions +on the group 𝐒𝐎(3) or 𝐒𝐩𝐢𝐧(3), so we will also refer to a choice +of a set of points in 𝐒𝐩𝐢𝐧(3) (which is essentially the group of +unit quaternions) as a "pixelization". However, assuming spherical +coordinates, a choice of *coordinates* on the sphere almost everywhere +induces a choice of the reference direction in the tangent space, so +it is *almost* possible to define pixelizations just in terms of +points on 𝕊². But using spherical coordinates is actually enough to +fully specify the pixelization, because the degeneracies at the poles +also allow us to define the reference direction. + +In principle, we could be concerned about the choice of reference +direction in the tangent space. That is, we might expect to care +about pixelizations over 𝕊³. However, we are dealing with +spin-weighted functions, which are eigenfunctions of a final rotation +about the reference direction. This means that once we choose any +reference direction at each point, we know the function values for any +other reference direction at those points. In particular, an +important property of a pixelization is the condition number of the +transformation matrix between the function values and the mode +weights. If we rotate the reference direction at a single point, this +is equivalent to multiplying the matrix by a diagonal matrix with +entries of 1 everywhere except the entry corresponding to that point, +where the entry is some complex phase. This does not change the +condition number of the matrix, so we can ignore the choice of +reference direction at every point. For other situations, where we +might care about the choice of reference direction, it might be +interesting to consider [this work by Marc +Alexa](https://github.com/moble/superfibonacci), and references +therein. + +Interesting discussions of various pixelizations and metrics can be +found in [Saff and Kuijlaars (1997)](@cite SaffKuijlaars_1997) and +[Brauchart and Grabner (2015)](@cite BrauchartGrabner_2015), as well +as blog posts +[here](https://web.archive.org/web/20220303150307/https://www.maths.unsw.edu.au/about/distributing-points-sphere) +and +[here](https://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/). +Note that the "equal-area" pixelizations of +[Healpix](https://healpix.sourceforge.io/) are very restrictive—only +being available for very specific numbers of points—and do not provide +any obvious advantages over the more flexible pixelizations available +here. + +The various pixelizations may be computed as follows: + +```@autodocs +Modules = [SphericalFunctions] +Pages = ["utilities/pixelizations.jl"] +``` + + +## Quadrature weights + +The "RS" algorithm requires quadrature weights corresponding to the input +pixelization. Though there is a working default choice, it is possible to use +others. There are several that are currently implemented, along with their +corresponding pixelizations: + +```@autodocs +Modules = [SphericalFunctions] +Pages = ["utilities/weights.jl"] +Order = [:module, :type, :constant, :function, :macro] +``` diff --git a/docs/src/utilities.md b/docs/src/interface/utilities.md similarity index 78% rename from docs/src/utilities.md rename to docs/src/interface/utilities.md index 2d9b8500..cab03f0f 100644 --- a/docs/src/utilities.md +++ b/docs/src/interface/utilities.md @@ -13,20 +13,20 @@ do a little better with a specialized routine. ```@autodocs Modules = [SphericalFunctions] -Pages = ["complex_powers.jl"] +Pages = ["utilities/complex_powers.jl"] Order = [:module, :type, :constant, :function, :macro] ``` ## Sizes of and indexing into ``𝔇``, ``d``, and ``Y`` data -By ``Y`` data, we mean anything indexed like ``Y_{\ell, m}`` modes; by ``D`` -data, we mean anything indexed like Wigner's ``\mathfrak{D}`` matrices, or +By ``Y`` data, we mean anything indexed like ``Y_{ℓ, m}`` modes; by ``D`` +data, we mean anything indexed like Wigner's ``𝔇`` matrices, or special subsets of them, like the ``H`` matrices. ```@autodocs -Modules = [SphericalFunctions] -Pages = ["indexing.jl", "wigner_matrices/indexing.jl", "wigner_matrices/Hrecursions.jl"] +Modules = [SphericalFunctions, SphericalFunctions.Deprecated] +Pages = ["deprecated/indexing.jl"] Order = [:module, :type, :constant, :function, :macro] ``` @@ -40,7 +40,7 @@ methods. Here, we collect any specialized methods that help us beat the limits. ```@autodocs Modules = [SphericalFunctions] -Pages = ["utils.jl"] +Pages = ["utilities/utils.jl"] ``` diff --git a/docs/src/wigner_matrices.md b/docs/src/interface/wigner_matrices.md similarity index 92% rename from docs/src/wigner_matrices.md rename to docs/src/interface/wigner_matrices.md index 0f9c8b02..50e31477 100644 --- a/docs/src/wigner_matrices.md +++ b/docs/src/interface/wigner_matrices.md @@ -1,5 +1,9 @@ # Wigner's ``𝔇`` and ``d`` matrices +```@meta +CurrentModule = SphericalFunctions.Deprecated +``` + Wigner's ``𝔇`` matrices — and to a lesser extent, the related ``d`` matrices — are extremely important in the theory of rotations. Each element is, itself, a special function of the rotation group: in particular, an eigenfunction of [the @@ -14,6 +18,7 @@ The actual computations can be done with the [`D_matrices`](@ref) function: ```julia using Quaternionic using SphericalFunctions +using SphericalFunctions.Deprecated R = randn(RotorF64) ℓₘₐₓ = 8 @@ -47,6 +52,7 @@ which can be computed directly in some cases), and the output is real-valued: ```julia using Quaternionic using SphericalFunctions +using SphericalFunctions.Deprecated β = π * rand(Float64) ℓₘₐₓ = 8 @@ -74,3 +80,15 @@ d_matrices! d_prep d_iterator ``` + + +## Workspaces + +```@meta +CurrentModule = SphericalFunctions +``` + +```@docs +HWedge +HAxis +``` diff --git a/docs/src/internal.md b/docs/src/internal.md deleted file mode 100644 index c6d88cd2..00000000 --- a/docs/src/internal.md +++ /dev/null @@ -1,58 +0,0 @@ -# Internal functions - -There are various functions that are only used internally, some of which are likely -to be deprecated in the near future. These are documented here for completeness. - -## ``H`` recursion and ALFs - -The fundamental algorithm is the ``H`` recursion, which is the core computation -needed for Wigner's ``d`` and ``𝔇`` matrices, and the spin-weighted spherical -harmonics ``{}_{s}Y_{\ell,m}``, as well as `map2salm` functions. - -```@autodocs -Modules = [SphericalFunctions] -Pages = ["Hrecursion.jl"] -``` - -Internally, the ``H`` recursion relies on calculation of the Associated Legendre -Functions (ALFs), which can also be called on their own: - -```@autodocs -Modules = [SphericalFunctions] -Pages = ["associated_legendre.jl"] -``` - -The function ``{}_{s}\lambda_{\ell,m}`` is defined as essentially ``{}_{s}Y_{\ell,0}``, and is important internally for computing the ALFs. We have some important utilities for computing it: - -```@docs -SphericalFunctions.λ_recursion_initialize -SphericalFunctions.λ_iterator -SphericalFunctions.AlternatingCountdown -``` - - - -## ₛ𝐘 - -Various `d`, `D`, and `sYlm` functions are important in the main API. Their -names and signatures have been tweaked from older versions of this package. The -only one with remaining documentation is [`ₛ𝐘`](@ref), which could probably be -replaced by [`sYlm_values`](@ref), except that the default pixelization is -[`golden_ratio_spiral_rotors`](@ref), which makes it very convenient for -interacting with [`SSHT`](@ref). - -```@docs -ₛ𝐘 -``` - - -# Transformation - -The newer [`SSHT`](@ref) interface is more efficient for most purposes, but this -package used to use functions named `map2salm`, which is still present, but may -be deprecated. - -```@autodocs -Modules = [SphericalFunctions] -Pages = ["map2salm.jl"] -``` diff --git a/docs/src/notes/H_recurrence.md b/docs/src/notes/H_recurrence.md new file mode 100644 index 00000000..352ed4d9 --- /dev/null +++ b/docs/src/notes/H_recurrence.md @@ -0,0 +1,253 @@ +# Algorithm for computing ``H`` (redesigned) + +The ``H`` array, as given by [Gumerov_2015](@citet), is related to Wigner's (small) ``d`` matrices — +which is itself related to the (big) ``𝔇`` matrices and the various spin-weighted +spherical harmonics ``{}_{s}Y_{ℓ,m}`` — via + +```math +d_{ℓ}^{m',m} = ϵ_{m'} ϵ_{-m} H^{ℓ}_{m',m}, +``` + +where + +```math +ϵ_k = + \begin{cases} + 1 & k\leq 0, \\ + (-1)^k & k > 0. + \end{cases} +``` + +(Note that I have swapped superscripts and subscripts on ``H`` +compared to Gumerov and Duraiswami's paper, to be consistent with the +rest of this documentation.) + +``H`` has various advantages over ``d`` and ``𝔇``, +including the fact that it can be efficiently and robustly calculated +via recurrence relations, and the following symmetry relations: + +```math +\begin{aligned} + H_{m', m}^ℓ(β) &= H_{m, m'}^ℓ(β) \\ + H_{m', m}^ℓ(β) &= H_{-m', -m}^ℓ(β) \\ + H_{m', m}^ℓ(β) &= (-1)^{ℓ+m+m'} H_{-m', m}^ℓ(π - β) \\ + H_{m', m}^ℓ(β) &= (-1)^{m+m'} H_{m', m}^ℓ(-β) +\end{aligned} +``` + +Because of these symmetries — specifically the first two — we only +need to evaluate about 1/4 of all the elements for a given value of +``β``. + + +## Steps to compute ``H`` (redesigned) + +The following describes various details that are not spelled out +correctly by [Gumerov_2015](@citet). All equation numbers refer to +that paper unless otherwise noted. + +Because of the symmetries noted above, we only compute ``H_{m', +m}^ℓ`` with ``m ≥ |m'|`` — roughly one quarter of all possible +values. Furthermore, for spin-weighted spherical harmonics of weight +``s``, we only need to compute values with ``|m'| ≤ |s|``, which +constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. + + +### Step 1: Initialize ``H^{0}_{0,0}`` + +Set ``H^{0}_{0,0}=1``. + + +### Step 2: ``H_{0,m}^{ℓ} \to H_{0,m}^{ℓ+1}`` for ``m \geq 0`` + +### Step 3: ``H_{0,m}^{ℓ+1} \to H_{1,m}^{ℓ}`` for ``m \geq 0`` + +### Step 4: ``H_{m',m-1}^{ℓ}, H_{m'-1,m}^{ℓ}, H_{m',m+1}^{ℓ} \to H_{m'+1,m}^{ℓ}`` for ``m' > 1`` and ``m > m'`` + +### Step 5: ``H_{m',m-1}^{ℓ}, H_{m'+1,m}^{ℓ}, H_{m',m+1}^{ℓ} \to H_{m'-1,m}^{ℓ}`` for ``m' \leq 0`` and ``m > -m'`` + +### Step 6: Use symmetries to fill in the rest of ``H`` + +### Step 7: Include phases to obtain ``d`` or ``𝔇`` + + +Compute values ``H^{0,m}_{n}(β)`` for ``m=0,\ldots,n`` and +``H^{0,m}_{n+1}(β)`` for ``m=0,\ldots,n+1``. Using Eq. (32), we see +that within Gumerov and Duraiswami's conventions +```math +\begin{aligned} + H^{0,m}_{n}(β) &= (-1)^m \sqrt{\frac{(n-|m|)!}{(n+|m|)!}} P^{|m|}_{n}(\cos β) \\ + &= \frac{1}{\sqrt{k_m (2n+1)}} P̄_{n,|m|}(\cos β). +\end{aligned} +``` +Here, ``k_0=1`` and ``k_m=2`` for ``m>0``, and ``P̄`` is defined as +```math + P̄_{n,|m|} = \sqrt{\frac{k_m(2n+1)(n-m)!}{(n+m)!}} P_{n,|m|}. +``` +Note that the factor of ``(-1)^m`` in the first equation above is +different from the convention used here, and is related to the +[Condon-Shortley +phase](https://en.wikipedia.org/wiki/Spherical_harmonics#Condon%E2%80%93Shortley_phase). +Note that Gumerov and Duraiswami use the notation ``P^{|m|}_{n}``, +whereas we are using the notation ``P_{n,|m|}`` — which usually differ +by a factor of ``(-1)^m``. + +We use the "fully normalized" associated Legendre functions (fnALF) +``P̄`` because, as explained by [Xing_2019](@citet), it is possible to +compute these values very efficiently and accurately, while also +delaying the onset of overflow and underflow. + +The algorithm Xing et al. describe as the best for computing ``P̄`` is +due to [Strakhov_1980](@citet) via [Belikov_1991](@citet), and is +given by them as +```math +\begin{aligned} + P̄_{0,0} &= 1 \\ + P̄_{1,0} &= \sqrt{3} \cos β \\ + P̄_{1,1} &= \sqrt{3} \sin β \\ + P̄_{n,0} &= a_n \cos β P̄_{n-1,0} - b_n \frac{\sin β}{2} P̄_{n-1,1} \\ + P̄_{n,m} &= + c_{n,m} \cos β P̄_{n-1,m} + - \sin β \left[ d_{n,m} P̄_{n-1,m+1} - e_{n,m} P̄_{n-1,m-1} \right], +\end{aligned} +``` +where the coefficients are given by +```math +\begin{aligned} + a_n &= \sqrt{\frac{2n+1}{2n-1}} \\ + b_n &= \sqrt{\frac{2(n-1)(2n+1)}{n(2n-1)}} \\ + c_{n,m} &= \frac{1}{n} \sqrt{\frac{(n+m)(n-m)(2n+1)}{2n-1}} \\ + d_{n,m} &= \frac{1}{2n} \sqrt{\frac{(n-m)(n-m-1)(2n+1)}{2n-1}} \\ + e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-δ_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. +\end{aligned} +``` + +Now, we can directly obtain a recurrence relation for +``H^{0,m}_{n} = P̄_{n,|m|} / \sqrt{k_m (2n+1)} `` from those expressions: +```math +\begin{aligned} + H^{0,0}_{0} &= 1 \\ + H^{0,0}_{1} &= \cos β \\ + H^{0,1}_{1} &= \sqrt{1/2} \sin β \\ + H^{0,0}_{n} &= \cos β H^{0,0}_{n-1} - b̄_n \sin β H^{0,1}_{n-1} \\ + H^{0,m}_{n} &= + c̄_{n,m} \cos β H^{0,m}_{n-1} + - \sin β \left[ d̄_{n,m} H^{0,m+1}_{n-1} - ē_{n,m} H^{0,m-1}_{n-1} \right], +\end{aligned} +``` +where the coefficients are given by +```math +\begin{aligned} + b̄_n &= \sqrt{\frac{n-1}{n}} \\ + c̄_{n,m} &= \frac{1}{n} \sqrt{(n+m)(n-m)} \\ + d̄_{n,m} &= \frac{1}{2n} \sqrt{(n-m)(n-m-1)} \\ + ē_{n,m} &= \frac{1}{2n} \sqrt{(n+m)(n+m-1)}. +\end{aligned} +``` +Note that the coefficients all simplified (in fact, ``a_n`` disappeared), without any +increase in the complexity of the recurrence relations themselves. Rewriting Belikov's +algorithm explicitly in terms of the ``H^{0,m}_{n}`` also allows us to avoid an extra +normalization step. + +### Step 3 +Compute ``H^{1,m}_{n}(β)`` for ``m=1,\ldots,n`` using relation (41). Symmetry +and shift of the indices allow this relation to be written as +```math +b^{0}_{n+1} H^{1, m}_{n} + = \frac{b^{−m−1}_{n+1} (1−\cos β)}{2} H^{0, m+1}_{n+1} + − \frac{b^{ m−1}_{n+1} (1+\cos β)}{2} H^{0, m−1}_{n+1} + − a^{m}_{n} \sin β H^{0, m}_{n+1}. +``` +Here the constants are defined by +```math +a^{m}_{n} = \sqrt{\frac{(n+m+1)(n-m+1)} {(2n+1)(2n+3)}}, +``` +```math +b^{m}_{n} = \mathrm{sgn}(m) \sqrt{\frac{(n-m-1)(n-m)} {(2n-1)(2n+1)}}. +``` +Note that all values are assumed to be zero whenever ``|m| > n``, we use +``\mathrm{sgn}(0)=1`` (unlike the common convention that +``\mathrm{sgn}(0)=0``), and we have ``a^{m}_{n} = a^{-m}_{n}``. Also note +that these coefficients *only* appear in this step, and because of how they +appear (specifically, because ``b`` always appears with argument ``n+1``), we +can factor out the denominators in the definitions of the constants. We +obtain this simplified formula +```math +H^{1, m}_{n} + = -\frac{1}{\sqrt{n(n+1)}} \left[ + \frac{\bar{b}^{−m−1}_{n+1} (1−\cos β)}{2} H^{0, m+1}_{n+1} + + \frac{\bar{b}^{ m−1}_{n+1} (1+\cos β)}{2} H^{0, m−1}_{n+1} + + \bar{a}^{m}_{n} \sin β H^{0, m}_{n+1} + \right], +``` +with +```math +\bar{a}^{m}_{n} = \sqrt{(n+m+1)(n-m+1)}, +``` +```math +\bar{b}^{m}_{n+1} = \sqrt{(n-m)(n-m+1)}. +``` + + +### Step 4 +Recursively compute ``H^{m'+1, m}_{n}(β)`` for ``m'=1,\ldots,n−1``, +``m=m',...,n`` using relation (50) resolved with respect to ``H^{m'+1, +m}_{n}``: +```math +d^{m'}_{n} H^{m'+1, m}_{n} + = d^{m'−1}_{n} H^{m'−1, m}_{n} + − d^{m−1}_{n} H^{m', m−1}_{n} + + d^{m}_{n} H^{m', m+1}_{n} +``` +(where the last term drops out for ``m=n``). The constants are defined by +```math +d^{m}_{n} = \frac{\mathrm{sgn}(m)}{2} \sqrt{(n-m)(n+m+1)}. +``` +Note that we can drop the factor of ``1/2``, and *for this case only* the sign +is always +1. + + +### Step 5 +Recursively compute ``H^{m'−1, m}_{n}(β)`` for ``m'=0,\ldots,−n+1``, +``m=−m',\ldots,n`` using relation (50) resolved with respect to ``H^{m'−1, +m}_{n}``: +```math +d^{m'−1}_{n} H^{m'−1, m}_{n} + = d^{m'}_{n} H^{m'+1, m}_{n} + + d^{m−1}_{n} H^{m', m−1}_{n} + − d^{m}_{n} H^{m', m+1}_{n} +``` +(where the last term drops out for ``m=n``). + +NOTE: Although Gumerov and Duraiswami specify the loop over ``m'`` to start at +-1, I find it necessary to start at 0, or there will be missing information. +This also requires setting the ``H^{0, -1}_{n}`` components (for all ``n``) +before beginning this loop. + + +## Pre-computing constants versus computing on the fly + +Each of the constants ``a^{m}_{n}``, ``b^{m}_{n}``, and ``c^{m}_{n}`` involves +divisions and square-roots, which can be very costly to compute. It can be +advantageous to pre-compute the constants, and simply index the pre-computed +arrays rather than re-computing them on each recursion. + +*If* we include the cost of computing all these constants in a single call to +the ``H`` recurrence, it can be much cheaper to compute each constant as needed +within the algorithm, rather than computing them all at once at the beginning +of the algorithm — but only for very small computations, such as those +involving ``n_{\mathrm{max}} ≈ 10``. Beyond this, despite the storage +penalties for all those constants, it turns out to be better to pre-compute +them. However, it should be noted that the fractional cost of storing the +constants is ``\sim 3/n_{\mathrm{max}}`` compared to just storing ``H`` itself, +so this will never be a very significant amount of space. + +On the other hand, if we can pre-compute the constants just once, and store +them between multiple calls to the ``H`` recurrence, then it is always +advantageous to do so — typically by factors of 2 or 3 in speed. The only +difficulty here is ensuring that each call to the recurrence has access to the +constants, which can be a little awkward when using multiple processes and/or +threads. However, it should be thread safe, since we only need to read those +constants within the ``H`` recurrence. All in all, I conclude that it is +probably not worth the effort to maintain separate versions of the recurrence +for pre-computed and on-the-fly constants. diff --git a/docs/src/notes/H_recursions.md b/docs/src/notes/H_recursions.md index 7e8c80f2..17202cb7 100644 --- a/docs/src/notes/H_recursions.md +++ b/docs/src/notes/H_recursions.md @@ -1,17 +1,17 @@ # Algorithm for computing ``H`` The ``H`` array, as given by [Gumerov_2015](@citet), is related to Wigner's (small) ``d`` matrices — -which is itself related to the (big) ``\mathfrak{D}`` matrices and the various spin-weighted -spherical harmonics ``{}_{s}Y_{\ell,m}`` — via +which is itself related to the (big) ``𝔇`` matrices and the various spin-weighted +spherical harmonics ``{}_{s}Y_{ℓ,m}`` — via ```math -d_{\ell}^{m',m} = \epsilon_{m'} \epsilon_{-m} H_{\ell}^{m',m}, +d_{ℓ}^{m',m} = ϵ_{m'} ϵ_{-m} H_{ℓ}^{m',m}, ``` where ```math -\epsilon_k = +ϵ_k = \begin{cases} 1 & k\leq 0, \\ (-1)^k & k > 0. @@ -19,7 +19,7 @@ where ``` ``H`` has various advantages over ``d``, including the fact that it can be efficiently -and robustly valculated via recurrence relations, and the following symmetry +and robustly calculated via recurrence relations, and the following symmetry relations: ```math @@ -46,7 +46,7 @@ to compute values with ``|m'| ≤ |s|``, which constitutes a dramatic savings when ``|s| ≪ ℓₘₐₓ``. The data are stored in the array `Hwedge`. However, some parts of this calculation require calculating terms with -``m=n+1`` — whereas such elements of ``d`` and ``\mathfrak{D}`` are considered +``m=n+1`` — whereas such elements of ``d`` and ``𝔇`` are considered zero. For this purpose, we need additional storage. Rather than allocating extra space, or requiring some additional workspace to be passed in, we can actually use parts of the input ``H`` data space for temporary storage while @@ -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``. +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. +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 +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 \\ @@ -109,7 +113,7 @@ where the coefficients are given by b_n &= \sqrt{\frac{2(n-1)(2n+1)}{n(2n-1)}} \\ c_{n,m} &= \frac{1}{n} \sqrt{\frac{(n+m)(n-m)(2n+1)}{2n-1}} \\ d_{n,m} &= \frac{1}{2n} \sqrt{\frac{(n-m)(n-m-1)(2n+1)}{2n-1}} \\ - e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-\delta_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. + e_{n,m} &= \frac{1}{2n} \sqrt{\frac{2}{2-δ_0^{m-1}}} \sqrt{\frac{(n+m)(n+m-1)(2n+1)}{2n-1}}. \end{aligned} ``` diff --git a/docs/src/notes/normalization.md b/docs/src/notes/normalization.md new file mode 100644 index 00000000..0fb56b30 --- /dev/null +++ b/docs/src/notes/normalization.md @@ -0,0 +1,107 @@ +# Normalization + +For any fixed values of ``ℓ`` and ``s``, the spin-weighted spherical +harmonics normalized as usual, satisfy the relation +```math +\sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|² = \frac{(2ℓ+1)}{4π}, +``` +for every ``𝐐 ∈ \mathrm{Spin}(3)``. This result is crucial for +actually obtaining a specific value for the spin-weighted spherical +harmonics, and we will derive it here. + +--- + +Begin by fixing ``ℓ`` and ``s``, and define the space of functions +with these eigenvalues: +```math +ℋ_{ℓ,s} = \left\{ + f : \mathrm{Spin}(3) → ℂ + \middle| + L² f = ℓ(ℓ+1) f + \mathrm{\ \ and\ \ } + R_z f = s f +\right\}. +``` +Now, for a given point ``𝐏 ∈ \mathrm{Spin}(3)``, we define the +["reproducing +kernel"](https://en.wikipedia.org/wiki/Reproducing_kernel_Hilbert_space) +``K_𝐏`` as a function in ``ℋ_{ℓ,s}`` such that for *every* +square-integrable function ``f ∈ ℋ_{ℓ,s}``, +```math +f(𝐏) = \int_{\mathrm{Spin}(3)} K̄_𝐏(𝐐)\, f(𝐐)\, d𝐐. +``` +Now, we want to expand this kernel in terms of the basis functions +``{}_{s}Y_{ℓ,m}`` of this space. Recall that these are not +ortho*normal* over ``\mathrm{Spin}(3)``; they are only orthogonal. +They are normalized over ``𝕊²`` in the restricted sense discussed +[here](@ref sYlm_and_Dlmpm) so that, when integrating over +``\mathrm{Spin}(3)``, we get an extra factor of ``π/2``: +```math +\int_{\mathrm{Spin}(3)} {}_{s}Ȳ_{ℓ,m'}(𝐐)\, {}_{s}Y_{ℓ,m}(𝐐)\, d𝐐 += \frac{π}{2} δ_{m',m}. +``` +Now, if we expand ``K_𝐏`` and ``f`` in terms of these basis +functions, we can calculate +```math +\begin{aligned} +f(𝐏) +&= \int_{\mathrm{Spin}(3)} K̄_𝐏(𝐐)\, f(𝐐)\, d𝐐 \\ +&= \int_{\mathrm{Spin}(3)} + \sum_{m',m} K̄_{𝐏,m'}\, {}_{s}Ȳ_{ℓ,m'}(𝐐)\, + f_{m}\, {}_{s}Y_{ℓ,m}(𝐐)\, d𝐐 \\ +&= \frac{π}{2} \sum_{m} K̄_{𝐏,m}\, f_{m}, +\end{aligned} +``` +the last of which implies that +```math +K_{𝐏,m} = \frac{2}{π} {}_{s}Ȳ_{ℓ,m}(𝐏) +``` +for every ``m``. That is, +```math +K_𝐏(𝐐) = \sum_m \frac{2}{π} {}_{s}Ȳ_{ℓ,m}(𝐏)\; {}_{s}Y_{ℓ,m}(𝐐). +``` +Now we take the norm of the kernel function: +```math +\begin{aligned} +\|K_𝐏\|²_{\mathrm{Spin}(3)} +&= \int_{\mathrm{Spin}(3)} |K_𝐏(𝐐)|²\, d𝐐 \\ +&= \int_{\mathrm{Spin}(3)} + \sum_{m',m} \frac{4}{π²} + {}_{s}Y_{ℓ,m'}(𝐏)\, {}_{s}Ȳ_{ℓ,m'}(𝐐)\, {}_{s}Ȳ_{ℓ,m}(𝐏)\, {}_{s}Y_{ℓ,m}(𝐐)\, d𝐐 \\ +&= \frac{2}{π} \sum_{m} |{}_{s}Y_{ℓ,m}(𝐏)|². +\end{aligned} +``` +Now, we are integrating with respect to a [Haar +measure](https://en.wikipedia.org/wiki/Haar_measure) on the group +``\mathrm{Spin}(3)``, which means that the integral must be invariant +under group actions. In particular, this means that the norm of the +kernel function cannot depend on the choice of ``𝐏``, which means +that the sum in the last expression is independent of ``𝐏``: +```math +\sum_{m} |{}_{s}Y_{ℓ,m}(𝐏)|² = \sum_{m} |{}_{s}Y_{ℓ,m}(𝐏')|² +``` +for *all* ``𝐏, 𝐏' ∈ \mathrm{Spin}(3)``. + +The last trick is to just integrate this expression over +``\mathrm{Spin}(3)`` again, and evaluate it in two different ways. On +one hand, we have +```math +\int_{\mathrm{Spin}(3)} \sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|²\, d𝐐 += \sum_{m} \int_{\mathrm{Spin}(3)} |{}_{s}Y_{ℓ,m}(𝐐)|²\, d𝐐 += \sum_{m} \frac{π}{2} += (2ℓ+1) \frac{π}{2}, +``` +since there are ``2ℓ+1`` values of ``m`` in the sum. On the other +hand, since the sum is independent of ``𝐐``, we can change to +integrating over a dummy variable and pull the sum out of the +integral: +```math +\int_{\mathrm{Spin}(3)} \sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|²\, d𝐐 += \sum_{m} |{}_{s}Y_{ℓ,m}(1)|²\, \int_{\mathrm{Spin}(3)} d𝐐' += 2π² \sum_{m} |{}_{s}Y_{ℓ,m}(1)|². +``` +Equating these two expressions, we find that +```math +\sum_{m} |{}_{s}Y_{ℓ,m}(𝐐)|² = \frac{(2ℓ+1)}{4π}, +``` +for arbitrary ``ℓ``, ``s``, and ``𝐐 ∈ \mathrm{Spin}(3)``. diff --git a/docs/src/notes/sampling_theorems.md b/docs/src/notes/sampling_theorems.md index e4df7995..82c7fa40 100644 --- a/docs/src/notes/sampling_theorems.md +++ b/docs/src/notes/sampling_theorems.md @@ -1,67 +1,81 @@ # 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``. +[McEwenWiaux_2011](@citet) (MW) provide a very thorough review of the +literature on sampling theorems related to spin-weighted spherical +harmonics up to 2011. [Reinecke_2013](@citet) (RS) outlined one of +the more efficient and accurate implementations of spin-weighted +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. + {}_{s}\tilde{f}_{θ}(m) := \int_0^{2π} {}_sf(θ, ϕ)\, e^{-imϕ}\, dϕ. ``` -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 +``θ`` as ``{}_{s}\tilde{𝐟}_m``. Inserting the +``{}_sY_{ℓ,m}`` expansion for ``{}_sf(θ, ϕ)``, and +performing the integration using orthogonality of complex +exponentials, we can find that ```math - {}_{s}\tilde{f}_{\theta}(m) = (-1)^s\, 2\pi \sum_{\ell=\Delta}^L \sqrt{\frac{2\ell+1}{4\pi}}\, d_{m,-s}^{\ell}(\theta)\, {}_sf_{\ell,m}. + {}_{s}\tilde{f}_{θ}(m) = (-1)^s\, 2π \sum_{ℓ=\Delta}^L \sqrt{\frac{2ℓ+1}{4π}}\, d_{m,-s}^{ℓ}(θ)\, {}_sf_{ℓ,m}. ``` -Now, denoting the vector of ${}_sf_{\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_{ℓ,m}`` for all values of +``ℓ`` as ``{}_s𝐟_m``, we can write this as a matrix-vector +equation: ```math - {}_{s}\tilde{\mathbf{f}}_m = (-1)^s\, 2\pi\, {}_s\mathbf{d}_{m}\, {}_s\mathbf{f}_m. + {}_{s}\tilde{𝐟}_m = (-1)^s\, 2π\, {}_s𝐝_{m}\, {}_s𝐟_m. ``` -We are effectively measuring the ${}_{s}\tilde{\mathbf{f}}_m$ values, we can easily construct the -${}_s\mathbf{d}_{m}$ matrix, and we are seeking the ${}_s\mathbf{f}_m$ values, so we can just invert +We are effectively measuring the ``{}_{s}\tilde{𝐟}_m`` +values, we can easily construct the ``{}_s𝐝_{m}`` matrix, and +we are seeking the ``{}_s𝐟_m`` values, so we can just invert this equation to solve for the latter. ## Discretizing the Fourier transform -Now, the only flaw in this analysis is that we have undersampled everywhere except $\ell = L$, which -means that the second equation (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 ``ℓ = 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, + {}_{s}\tilde{f}_{j}(m) := \sum_{k=0}^{2j} {}_sf(θ_j, ϕ_k)\, e^{-imϕ_k}\, \Delta ϕ, ``` -where $\phi_k = \frac{2\pi k}{2j+1}$, and $\Delta \phi = \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 ``ϕ_k = \frac{2π k}{2j+1}``, and ``\Delta ϕ = +\frac{2π}{2j+1}``. (Recall the subtle notational distinction common +in time-frequency analysis that ``\tilde{s}(t_j) = \Delta t +\tilde{s}_j``, which would suggest we use ``{}_{s}\tilde{f}_{j}(m) = +\Delta ϕ\, {}_{s}\tilde{f}_{j,m}``.) Next, we can insert the +expansion for ``{}_sf(θ, ϕ)``: ```math \begin{aligned} {}_{s}\tilde{f}_{j}(m) - &= \sum_{k=0}^{2j} \sum_{\ell,m'} {}_sf_{\ell,m'}\, {}_sY_{\ell,m'}(\theta_j, \phi_k)\, e^{-im\phi_k}\, \Delta \phi \\ - &= \sum_{k=0}^{2j} \sum_{\ell,m'} {}_sf_{\ell,m'}\, (-1)^{s}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j) e^{i m' \phi_k}\, e^{-im\phi_k}\, \frac{2\pi}{2j+1} \\ - &= (-1)^{s}\, \frac{2\pi}{2j+1} \sum_{\ell,m'} {}_sf_{\ell,m'}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j) \sum_{k=0}^{2j}e^{i (m'-m) \phi_k}. + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, {}_sY_{ℓ,m'}(θ_j, ϕ_k)\, e^{-imϕ_k}\, \Delta ϕ \\ + &= \sum_{k=0}^{2j} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, (-1)^{s}\, \sqrt{\frac{2ℓ+1}{4π}}\, d_{ℓ}^{m',-s}(θ_j) e^{i m' ϕ_k}\, e^{-imϕ_k}\, \frac{2π}{2j+1} \\ + &= (-1)^{s}\, \frac{2π}{2j+1} \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4π}}\, d_{ℓ}^{m',-s}(θ_j) \sum_{k=0}^{2j}e^{i (m'-m) ϕ_k}. \end{aligned} ``` We can evaluate this last sum easily: ```math - \sum_{k=0}^{2j}e^{i (m'-m) \phi_k} = \begin{cases} + \sum_{k=0}^{2j}e^{i (m'-m) ϕ_k} = \begin{cases} 2j+1 & m'-m = n(2j+1)\ \mathrm{for}\ n\in\mathbb{Z}, \\ 0 & \mathrm{otherwise}. \end{cases} @@ -70,54 +84,65 @@ This allows us to simplify as ```math \begin{aligned} - {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2\pi \sum_{\ell,m'} {}_sf_{\ell,m'}\, \sqrt{\frac{2\ell+1}{4\pi}}\, d_{\ell}^{m',-s}(\theta_j), + {}_{s}\tilde{f}_{j}(m) = (-1)^{s}\, 2π \sum_{ℓ,m'} {}_sf_{ℓ,m'}\, \sqrt{\frac{2ℓ+1}{4π}}\, d_{ℓ}^{m',-s}(θ_j), \end{aligned} ``` -where $m'$ ranges over $m + n(2j+1)$ for all $n\in \mathbb{Z}$ such that $|m + n(2j+1)| \leq \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 ℓ`` +— that is, all ``n\in \mathbb{Z}`` such that ```math - \left \lceil \frac{-\ell-m}{2j+1} \right \rceil \leq n \leq \left \lfloor \frac{\ell-m}{2j+1} \right \rfloor. + \left \lceil \frac{-ℓ-m}{2j+1} \right \rceil \leq n \leq \left \lfloor \frac{ℓ-m}{2j+1} \right \rfloor. ``` ## Matrix representation -Usually, we would take the sum over $\ell$ ranging from $\mathrm{max}(|m|,|s|)$ to $L$, and the sum -over $m'$ ranging over $m + n(2j+1)$ for all $n\in \mathbb{Z}$ such that $|m + n(2j+1)| \leq \ell$. -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 ``ℓ`` ranging from ``\mathrm{max}(|m|,|s|)`` to ``L``, and the sum +over ``m'`` ranging over ``m + n(2j+1)`` for all ``n\in \mathbb{Z}`` such that ``|m + n(2j+1)| \leq ℓ``. +However, we can also consider these sums to range over all possible +values of ``ℓ, m'``, and just set the coefficient to zero whenever +these conditions are not satisfied. In that case, we can again think +of this as a (much larger) vector-matrix equation reading ```math - {}_s\tilde{\mathbf{f}} = (-1)^s\, 2\pi\, {}_s\mathbf{d}\, {}_s\mathbf{f}, + {}_s\tilde{𝐟} = (-1)^s\, 2π\, {}_s𝐝\, {}_s𝐟, ``` -where the index on ${}_s\tilde{\mathbf{f}}$ loops over $j$ and $m$, the index on ${}_s\mathbf{f}$ -loops over $\ell$ and $m'$, and the indices on ${}_s\mathbf{d}$ loop over each of those pairs. +where the index on ``{}_s\tilde{𝐟}`` loops over ``j`` and +``m``, the index on ``{}_s𝐟`` loops over ``ℓ`` and ``m'``, +and the indices on ``{}_s𝐝`` loop over each of those pairs. ## De-aliasing -While it is *far* simpler to simply invert the full ${}_s\mathbf{d}$ 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𝐝`` +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 ``θ_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..32c0dd79 100644 --- a/docs/src/operators.md +++ b/docs/src/operators.md @@ -1,232 +1,249 @@ # Differential operators -Spin-weighted spherical functions *cannot* be defined on the sphere $S^2$, but -are well defined on the group ``\mathrm{Spin}(3) \cong \mathrm{SU}(2)`` or the -rotation group ``\mathrm{SO}(3)``. (See [Boyle_2016](@citet) for the -explanation.) However, this also allows us to define a variety of differential -operators acting on these functions, relating to infinitesimal motions in these -groups, acting either from the left or the right on their arguments. Right or -left matters because the groups mentioned above are all non-commutative groups. - -In general, the *left* Lie derivative of a function ``f(Q)`` over the unit -quaternions with respect to a generator of rotation ``g`` is defined as +Spin-weighted spherical functions *cannot* be defined on the sphere +``𝕊²``, but are well defined on the sphere ``𝕊³`` or its projective +version ``ℝℙ³``. See [Boyle_2016](@citet) for the explanation. These +are the spaces underlying the Lie groups ``\mathrm{Spin}(3) \cong +\mathrm{SU}(2)`` or its projective version ``\mathrm{SO}(3)``. As a +result, we can define a variety of differential operators acting on +these functions, relating to infinitesimal motions in these groups, +acting either from the left or the right on their arguments. Right or +left matters because the groups mentioned above are all +non-commutative groups. + +In general, the *left* Lie derivative of a function ``f(Q)`` over the +unit quaternions with respect to a generator of rotation ``g`` is +defined as ```math L_g(f)\{Q\} := -\frac{i}{2} \left. \frac{df\left(\exp(t\,g)\, Q\right)}{dt} \right|_{t=0}. ``` -Note that the exponential multiplies ``Q`` *on the left* — hence the name. We -will see below that this agrees with the usual definition of the -angular-momentum from physics, except that in *quantum* physics a factor of -``\hbar`` is usually included. - -So, for example, a rotation about the ``z`` axis has the quaternion ``z`` as its -generator of rotation, and ``L_z`` defined in this way agrees with [the usual -angular-momentum -operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) ``L_z`` -familiar from spherical-harmonic theory, and reduces to it when the function has -spin weight 0, but also applies to functions of general spin weight. Similarly, -we can compute ``L_x`` and ``L_y``, and take appropriate combinations to find -[the usual raising and lowering (ladder) +Note that the exponential multiplies ``Q`` *on the left* — hence the +name. We will see below that this agrees with the usual definition of +the angular-momentum from physics, except that in *quantum* physics a +factor of ``\hbar`` is usually included. + +So, for example, a rotation about the ``z`` axis has the quaternion +``z`` as its generator of rotation, and ``L_z`` defined in this way +agrees with [the usual angular-momentum +operator](https://en.wikipedia.org/wiki/Angular_momentum_operator) +``L_z`` familiar from spherical-harmonic theory, and reduces to it +when the function has spin weight 0, but also applies to functions of +general spin weight. Similarly, we can compute ``L_x`` and ``L_y``, +and take appropriate combinations to find [the usual raising and +lowering (ladder) operators](https://en.wikipedia.org/wiki/Ladder_operator#Angular_momentum) ``L_+`` and ``L_-``. -In just the same way, we can define the *right* Lie derivative of a function -``f(Q)`` over the unit quaternions with respect to a generator of rotation ``g`` -as +In just the same way, we can define the *right* Lie derivative of a +function ``f(Q)`` over the unit quaternions with respect to a +generator of rotation ``g`` as ```math R_g(f)\{Q\} := -\frac{i}{2} \left. \frac{df\left(Q\, \exp(t\,g)\right)}{dt} \right|_{t=0}. ``` -Note that the exponential multiplies ``Q`` *on the right* — hence the name. - -This operator is less common in physics, because it represents the dependence of -the function on the choice of frame (or coordinate system), which is not usually -of interest. Multiplication on the left represents a rotation of the physical -system, while rotation on the right represents a rotation of the coordinate -system. However, this dependence on coordinate system is precisely what defines -the *spin weight* of a function, so this class of operators is relevant in -discussions of spin-weighted spherical functions. In particular, the operators -``R_\pm`` correspond (up to a sign) to the spin-raising and -lowering operators -``\eth`` and ``\bar{\eth}`` originally introduced by [Newman_1966](@citet), as -explained in greater detail by [Boyle_2016](@citet). - -Note that these definitions are *extremely* general, in that they can be used -for *any* Lie group, and for any complex-valued function on that group. And in -full generality, we have the useful properties of linearity: +Note that the exponential multiplies ``Q`` *on the right* — hence the +name. + +This operator is less common in physics, because it represents the +dependence of the function on the choice of frame (or coordinate +system), which is not usually of interest. Multiplication on the left +represents a rotation of the physical system, while rotation on the +right represents a rotation of the coordinate system. However, this +dependence on coordinate system is precisely what defines the *spin +weight* of a function, so this class of operators is relevant in +discussions of spin-weighted spherical functions. In particular, the +operators ``R_\pm`` correspond (up to a sign) to the spin-raising and +-lowering operators ``\eth`` and ``\bar{\eth}`` originally introduced +by [Newman_1966](@citet), as explained in greater detail by +[Boyle_2016](@citet). + +Note that these definitions are *extremely* general, in that they can +be used for *any* Lie group, and for any complex-valued function on +that group. And in full generality, we have the useful properties of +linearity: ```math -L_{s\mathbf{a}} = sL_{\mathbf{a}} +L_{s𝐚} = sL_{𝐚} \qquad \text{and} \qquad -R_{s\mathbf{a}} = sR_{\mathbf{a}}, +R_{s𝐚} = sR_{𝐚}, ``` and ```math -L_{\mathbf{a}+\mathbf{b}} = L_{\mathbf{a}} + L_{\mathbf{b}} +L_{𝐚+𝐛} = L_{𝐚} + L_{𝐛} \qquad \text{and} \qquad -R_{\mathbf{a}+\mathbf{b}} = R_{\mathbf{a}} + R_{\mathbf{b}}, +R_{𝐚+𝐛} = R_{𝐚} + R_{𝐛}, ``` -for any scalar ``s`` and any elements of the Lie algebra ``\mathbf{a}`` and -``\mathbf{b}``. In particular, if the Lie algebra has a basis -``\mathbf{e}_{(j)}``, we use the shorthand ``L_j`` and ``R_j`` for -``L_{\mathbf{e}_{(j)}}`` and ``R_{\mathbf{e}_{(j)}}``, respectively, and we can -expand any operator in terms of these basis operators: +for any scalar ``s`` and any elements of the Lie algebra +``𝐚`` and ``𝐛``. In particular, if the Lie algebra +has a basis ``𝐞_{(j)}``, we use the shorthand ``L_j`` and +``R_j`` for ``L_{𝐞_{(j)}}`` and ``R_{𝐞_{(j)}}``, +respectively, and we can expand any operator in terms of these basis +operators: ```math -L_{\mathbf{a}} = \sum_{j} a_j L_j +L_{𝐚} = \sum_{j} a_j L_j \qquad \text{and} \qquad -R_{\mathbf{a}} = \sum_{j} a_j R_j. +R_{𝐚} = \sum_{j} a_j R_j. ``` ## Commutators -In general, for generators ``a`` and ``b``, we have the commutator relations +In general, for generators ``a`` and ``b``, we have the commutator +relations ```math \left[ L_a, L_b \right] = \frac{i}{2} L_{[a,b]} \qquad \left[ R_a, R_b \right] = -\frac{i}{2} R_{[a,b]}, ``` -where ``[a,b]`` is the commutator of the two generators, which can be obtained -directly as the commutator of the corresponding quaternions. Note the sign -difference between these two equations. The factors of ``\pm i/2`` are inherited -directly from the definitions of ``L_g`` and ``R_g`` given above, but they -appear there with the *same* sign. The sign difference between these two -commutator equations results from the fact that the quaternions are multiplied -in opposite orders in the two cases. It *could* be absorbed by defining the -operators with opposite signs.[^1] The arbitrary sign choices used above are -purely for historical reasons. - -Again, these results are valid for general (finite-dimensional) Lie groups, but -a particularly interesting case is in application to the three-dimensional -rotation group. In the following, we will apply our results to this group. - -The commutator relations for ``L`` are consistent — except for -the differing use of ``\hbar`` — with the usual relations from quantum +where ``[a,b]`` is the commutator of the two generators, which can be +obtained directly as the commutator of the corresponding quaternions. +Note the sign difference between these two equations. The factors of +``\pm i/2`` are inherited directly from the definitions of ``L_g`` and +``R_g`` given above, but they appear there with the *same* sign. The +sign difference between these two commutator equations results from +the fact that the quaternions are multiplied in opposite orders in the +two cases. It *could* be absorbed by defining the operators with +opposite signs.[^1] The arbitrary sign choices used above are purely +for historical reasons. + +Again, these results are valid for general (finite-dimensional) Lie +groups, but a particularly interesting case is in application to the +three-dimensional rotation group. In the following, we will apply our +results to this group. + +The commutator relations for ``L`` are consistent — except for the +differing use of ``\hbar`` — with the usual relations from quantum mechanics: ```math -\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} \varepsilon_{jkl} L_l. +\left[ L_j, L_k \right] = i \hbar \sum_{l=1}^{3} ε_{jkl} L_l. ``` -Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and index the -set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we represent an -arbitrary basis vector as ``\hat{e}_j``, then the quaternion commutator -``[a,b]`` in the expression for ``[L_a, L_b]`` becomes +Here, ``j``, ``k``, and ``l`` are indices that run from 1 to 3, and +index the set of basis vectors ``(\hat{x}, \hat{y}, \hat{z})``. If we +represent an arbitrary basis vector as ``\hat{e}_j``, then the +quaternion commutator ``[a,b]`` in the expression for ``[L_a, L_b]`` +becomes ```math -[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} \varepsilon_{jkl} \hat{e}_l. +[\hat{e}_j, \hat{e}_k] = 2 \sum_{l=1}^{3} ε_{jkl} \hat{e}_l. ``` Plugging this into the general expression ``[L_a, L_b] = \frac{i}{2} -L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version frequently -seen in quantum physics. +L_{[a,b]}``, we obtain (up to the factor of ``\hbar``) the version +frequently seen in quantum physics. [^1]: - In fact, we can define the left and right Lie derivative operators quite - generally, for functions on *any* Lie group and for the corresponding Lie - algebra. And in all cases (at least for finite-dimensional Lie algebras) we - obtain the same commutator relations. The only potential difference is that - it may not make sense to use the coefficient ``i/2`` in general; it was - chosen here for consistency with the standard angular-momentum operators. - If that coefficient is changed in the definitions of the Lie derivatives, - the only change to the commutator relations would the substitution of that - coefficient. - -The raising and lowering operators relative to ``L_z`` and ``R_z`` satisfy — by -definition of raising and lowering operators — the relations + In fact, we can define the left and right Lie derivative operators + quite generally, for functions on *any* Lie group and for the + corresponding Lie algebra. And in all cases (at least for + finite-dimensional Lie algebras) we obtain the same commutator + relations. The only potential difference is that it may not make + sense to use the coefficient ``i/2`` in general; it was chosen + here for consistency with the standard angular-momentum operators. + If that coefficient is changed in the definitions of the Lie + derivatives, the only change to the commutator relations would the + substitution of that coefficient. + +The raising and lowering operators relative to ``L_z`` and ``R_z`` +satisfy — by definition of raising and lowering operators — the +relations ```math [L_z, L_\pm] = \pm L_\pm \qquad [R_z, R_\pm] = \pm R_\pm. ``` -These allow us to solve — up to an overall factor — for those operators in terms -of the basic generators (again, noting the sign difference): +These allow us to solve — up to an overall factor — for those +operators in terms of the basic generators (again, noting the sign +difference): ```math L_\pm = L_x \pm i L_y \qquad R_\pm = R_x \mp i R_y. ``` -(Interestingly, this procedure also shows that rasing and lowering operators can -only exist if the factor in front of the derivatives in the definitions of -``L_g`` and ``R_g`` are pure imaginary numbers.) In particular, this results in -the commutator relations +(Interestingly, this procedure also shows that rasing and lowering +operators can only exist if the factor in front of the derivatives in +the definitions of ``L_g`` and ``R_g`` are pure imaginary numbers.) In +particular, this results in the commutator relations ```math [L_+, L_-] = 2L_z \qquad [R_+, R_-] = 2R_z. ``` -Here, the signs are *similar* because the two sign differences noted above -essentially cancel each other out. +Here, the signs are *similar* because the two sign differences noted +above essentially cancel each other out. -In the functions [listed below](#Module-functions), these operators are returned -as matrices acting on vectors of mode weights. As such, we can actually -evaluate these commutators as given to cross-validate the expressions and those -functions. +In the functions [listed below](#Module-functions), these operators +are returned as matrices acting on vectors of mode weights. As such, +we can actually evaluate these commutators as given to cross-validate +the expressions and those functions. ## Transformations of functions vs. mode weights -One important point to note is that mode weights transform "contravariantly" -(very loosely speaking) relative to the spin-weighted spherical functions under -some operators. For example, take the action of the ``L_+`` operator, which -acts on a SWSH as +One important point to note is that mode weights transform +"contravariantly" (very loosely speaking) relative to the +spin-weighted spherical functions under some operators. For example, +take the action of the ``L_+`` operator, which acts on a SWSH as ```math -L_+ \left\{{}_{s}Y_{\ell,m}\right\} (R) = \sqrt{(\ell-m)(\ell+m+1)} {}_{s}Y_{\ell,m+1}(R). +L_+ \left\{{}_{s}Y_{ℓ,m}\right\} (R) = \sqrt{(ℓ-m)(ℓ+m+1)} {}_{s}Y_{ℓ,m+1}(R). ``` -We can use this to derive the mode weights of a general spin-weighted function -``f`` under the action of this operator:[^2] +We can use this to derive the mode weights of a general spin-weighted +function ``f`` under the action of this operator:[^2] ```math \begin{aligned} -\left\{L_+ f\right\}_{\ell,m} +\left\{L_+ f\right\}_{ℓ,m} &= -\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ +\int \left\{L_+ f(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\int \left\{L_+ \sum_{\ell',m'}f_{\ell',m'}\, {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ +\int \left\{L_+ \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\int \sum_{\ell',m'} f_{\ell',m'}\, \left\{L_+ {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ +\int \sum_{ℓ',m'} f_{ℓ',m'}\, \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \int \left\{L_+ {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{L_+ {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \int \left\{\sqrt{(\ell'-m')(\ell'+m'+1)} {}_{s}Y_{\ell',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\sqrt{(ℓ'-m')(ℓ'+m'+1)} {}_{s}Y_{ℓ',m'+1}(R)\right\}\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-m')(\ell'+m'+1)} \int {}_{s}Y_{\ell',m'+1}(R)\, {}_{s}\bar{Y}_{\ell,m}(R)\, dR \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} \int {}_{s}Y_{ℓ',m'+1}(R)\, {}_{s}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-m')(\ell'+m'+1)} \delta_{\ell,\ell'} \delta_{m,m'+1} \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-m')(ℓ'+m'+1)} δ_{ℓ,ℓ'} δ_{m,m'+1} \\ &= -f_{\ell,m-1}\, \sqrt{(\ell-m+1)(\ell+m)} +f_{ℓ,m-1}\, \sqrt{(ℓ-m+1)(ℓ+m)} \end{aligned} ``` -Note that this expression (and in particular its signs) more resembles the -expression for ``L_- \left\{{}_{s}Y_{\ell,m}\right\}`` than for ``L_+ -\left\{{}_{s}Y_{\ell,m}\right\}``. Similar relations hold for the action of -``L_-``. +Note that this expression (and in particular its signs) more resembles +the expression for ``L_- \left\{{}_{s}Y_{ℓ,m}\right\}`` than for +``L_+ \left\{{}_{s}Y_{ℓ,m}\right\}``. Similar relations hold for +the action of ``L_-``. [^2]: A technical note about the integrals above: the integrals should be taken over the appropriate space and with the appropriate weight such that the SWSHs are orthonormal. In general, this integral should be over - ``\mathrm{Spin}(3)`` and weighted by ``1/2\pi`` so that the result will be + ``\mathrm{Spin}(3)`` and weighted by ``1/2π`` so that the result will be either ``0`` or ``1``; in general the SWSHs are not truly orthonormal when - integrated over an ``S^2`` subspace (nor even is the integral invariant). + integrated over an ``𝕊²`` subspace (nor even is the integral invariant). However, if we know that the spins are the same in both cases, it *is* - possible to integrate over an ``S^2`` subspace. + possible to integrate over an ``𝕊²`` subspace. -However, it is important to note that the same "contravariance" is not present -for the spin-raising and -lowering operators: +However, it is important to note that the same "contravariance" is not +present for the spin-raising and -lowering operators: ```math \begin{aligned} -\left\{\eth f\right\}_{\ell,m} +\left\{\eth f\right\}_{ℓ,m} &= -\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ +\int \left\{\eth f(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\int \left\{\eth \sum_{\ell',m'}f_{\ell',m'}\, {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ +\int \left\{\eth \sum_{ℓ',m'}f_{ℓ',m'}\, {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \int \left\{\eth {}_{s}Y_{\ell',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \int \left\{\eth {}_{s}Y_{ℓ',m'}(R)\right\}\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-s)(\ell'+s+1)} \int {}_{s+1}Y_{\ell',m'}(R)\, {}_{s+1}\bar{Y}_{\ell,m}(R)\, dR \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} \int {}_{s+1}Y_{ℓ',m'}(R)\, {}_{s+1}\bar{Y}_{ℓ,m}(R)\, dR \\ &= -\sum_{\ell',m'} f_{\ell',m'}\, \sqrt{(\ell'-s)(\ell'+s+1)} \delta_{\ell,\ell'} \delta_{m,m'} \\ +\sum_{ℓ',m'} f_{ℓ',m'}\, \sqrt{(ℓ'-s)(ℓ'+s+1)} δ_{ℓ,ℓ'} δ_{m,m'} \\ &= -f_{\ell,m}\, \sqrt{(\ell-s)(\ell+s+1)} +f_{ℓ,m}\, \sqrt{(ℓ-s)(ℓ+s+1)} \end{aligned} ``` -Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more "covariant" -form of transformation. +Similarly ``\bar{\eth}`` — and ``R_\pm`` of course — obey this more +"covariant" form of transformation. ## Docstrings diff --git a/docs/src/references.bib b/docs/src/references.bib index 7fde3911..09e257ba 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -1,6 +1,6 @@ -@misc{Ajith_2007, +@misc{AjithEtAl_2011, 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,86 @@ @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, + 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}, + 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} +} + +@misc{BelknerEtAl_2024, + title = {{$\texttt{cunuSHT}$:} {GPU} Accelerated Spherical Harmonic Transforms on Arbitrary Pixelizations}, + url = {http://arxiv.org/abs/2406.14542}, + doi = {10.48550/arXiv.2406.14542}, + publisher = {{arXiv}}, + author = {Belkner, Sebastian and Duivenvoorden, Adriaan J. and Carron, Julien and Schaeffer, Nathanael and Reinecke, Martin}, + month = jun, + year = {2024}, +} + +@article{Blanchet_2024, + author = {Blanchet, Luc}, + year = {2024}, + 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}, +} + +@book{BottTu_1982, + address = {New York, {NY}}, + series = {Graduate Texts in Mathematics}, + title = {Differential Forms in Algebraic Topology}, + isbn = {978-1-4757-3951-0}, + url = {https://doi.org/10.1007/978-1-4757-3951-0}, + publisher = {Springer}, + author = {Bott, Raoul and Tu, Loring W.}, + year = {1982}, + doi = {10.1007/978-1-4757-3951-0} +} + +@article{BoydPetschek_2014, + title = {The Relationships Between {C}hebyshev, {L}egendre and {J}acobi Polynomials: The + Generic Superiority of {C}hebyshev Polynomials and Three Important Exceptions}, + 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, @@ -41,6 +121,30 @@ @article{BrauchartGrabner_2015 author = {Johann S. Brauchart and Peter J. Grabner} } +@article{Clifford_1878, + title = {Applications of Grassmann's Extensive Algebra}, + volume = {1}, + url = {http://www.jstor.org/stable/2369379}, + doi = {10.2307/2369379}, + number = {4}, + journal = {American Journal of Mathematics}, + author = {Clifford, William Kingdon}, + month = jan, + year = {1878}, + pages = {350--358} +} + +@book{CohenTannoudji_1991, + address = {New York}, + edition = {1st}, + 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 +} + @book{CondonShortley_1935, address = {London}, title = {The Theory Of Atomic Spectra}, @@ -50,6 +154,71 @@ @book{CondonShortley_1935 url = {https://archive.org/details/in.ernet.dli.2015.212979} } +@article{Davenport_1973, + title = {Rotations about nonorthogonal axes}, + volume = {11}, + issn = {0001-1452}, + url = {https://doi.org/10.2514/3.6842}, + doi = {10.2514/3.6842}, + number = {6}, + journal = {{AIAA} Journal}, + author = {Davenport, Paul B.}, + year = {1973}, + pages = {853--857} +} + +@article{Dirac_1939, + title = {A new notation for quantum mechanics}, + volume = {35}, + issn = {1469-8064, 0305-0041}, + url = {https://www.cambridge.org/core/journals/mathematical-proceedings-of-the-cambridge-philosophical-society/article/new-notation-for-quantum-mechanics/4631DB9213D680D6332BA11799D76AFB}, + doi = {10.1017/S0305004100021162}, + number = {3}, + journal = {Mathematical Proceedings of the Cambridge Philosophical Society}, + author = {Dirac, P. a. M.}, + month = jul, + year = {1939}, + pages = {416--418} +} + +@book{DoranLasenby_2010, + address = {Cambridge}, + title = {Geometric Algebra for Physicists}, + 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} +} + +@article{DriscollHealy_1994, + title = {Computing Fourier Transforms and Convolutions on the {2-Sphere}}, + volume = {15}, + issn = {0196-8858}, + url = {http://www.sciencedirect.com/science/article/pii/S0196885884710086}, + doi = {10.1006/aama.1994.1008}, + number = {2}, + journal = {Advances in Applied Mathematics}, + author = {Driscoll, J. R. and Healy, D. M.}, + month = jun, + year = {1994}, + pages = {202--250} +} + +@book{Edmonds_2016, + title = {Angular Momentum in Quantum Mechanics}, + isbn = {978-1-4008-8418-6}, + 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 = 1960, + edition = {2nd}, +} + @article{Elahi_2018, doi = {10.1109/lsp.2018.2865676}, url = {https://doi.org/10.1109/lsp.2018.2865676}, @@ -67,6 +236,53 @@ @article{Elahi_2018 primaryClass = "astro-ph.IM", } +@article{Euler_1767, + title = {Du mouvement d'un corps solide quelconque lorsqu'il tourne autour d'un axe mobile}, + volume = {16}, + url = {https://bibliothek.bbaw.de/digitalisierte-sammlungen/akademieschriften/ansicht-akademieschriften?tx_bbaw_academicpublicationshow%5Baction%5D=show&tx_bbaw_academicpublicationshow%5Bcontroller%5D=AcademicPublication%5CVolume&tx_bbaw_academicpublicationshow%5Bpage%5D=186&tx_bbaw_academicpublicationshow%5Bvolume%5D=16&cHash=0d077822c83d8525b438a27723d92c19}, + journal = {Histoire de {l'Acad\'{e}mie} Royale des Sciences et des Belles-Lettres de Berlin}, + author = {Euler, Leonhard}, + year = {1767}, + note = {Academy year 1760. Enestr\"{o}m Number: E336. Figures are on a separate plate facing page 228.}, + pages = {176--227} +} + +@article{Euler_1776a, + title = {Formulae generales pro translatione quacunque corporum rigidorum}, + volume = {20}, + location = {Euler Archive}, + url = {https://scholarlycommons.pacific.edu/euler-works/478}, + journal = {Novi Commentarii Academiae Scientiarum Imperialis Petropolitanae}, + author = {Euler, Leonhard}, + year = {1776}, + note = {Enestr\"{o}m Number: E478}, + pages = {189--207} +} + +@article{Euler_1776b, + title = {Nova methodus motum corporum rigidorum determinandi}, + volume = {20}, + location = {Euler Archive}, + url = {https://scholarlycommons.pacific.edu/euler-works/479}, + journal = {Novi Commentarii Academiae Scientiarum Imperialis Petropolitanae}, + author = {Euler, Leonhard}, + year = {1776}, + note = {Enestr\"{o}m Number: E479}, + pages = {208--238} +} + +@book{Folland_2016, + address = {New York}, + edition = {2nd}, + 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}, @@ -82,6 +298,29 @@ @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} +} + +@book{Gibbs_1881, + title = {Elements of vector analysis : arranged for the use of students in physics}, + shorttitle = {Elements of vector analysis}, + url = {http://archive.org/details/elementsvectora00gibb}, + publisher = {Tuttle, Morehouse \& Taylor}, + address = {New Haven}, + author = {Gibbs, Josiah Willard}, + year = {1881} +} + @article{GoldbergEtAl_1967, author = {Goldberg, J. N. and Macfarlane, A. J. and Newman, E. T. and Rohrlich, F. and Sudarshan, E. C. G.}, @@ -97,6 +336,41 @@ @article{GoldbergEtAl_1967 url = {https://doi.org/10.1063/1.1705135}, } +@book{Griffiths_1995, + address = {Upper Saddle River, {NJ}}, + edition = {1st}, + 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 + 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}, @@ -111,6 +385,43 @@ @incollection{Gumerov_2015 primaryClass = "math.NA", } +@article{Hamilton_1844, + title = {{LXXVIII.} On quaternions; or on a new system of imaginaries in Algebra: To the editors of the Philosophical Magazine and Journal}, + volume = {25}, + issn = {1941-5966}, + shorttitle = {{LXXVIII.} On quaternions; or on a new system of imaginaries in Algebra}, + url = {https://doi.org/10.1080/14786444408645047}, + doi = {10.1080/14786444408645047}, + number = {169}, + journal = {The London, Edinburgh, and Dublin Philosophical Magazine and Journal of Science}, + author = {Hamilton, William Rowan}, + month = jan, + year = {1844}, + pages = {489--495} +} + +@book{Hamilton_1853, + title = {Lectures on quaternions : containing a systematic statement of a new mathematical method, of which the principles were communicated in 1843 to the Royal Irish academy, and which has since formed the subject of successive courses of lectures, delivered in 1848 and subsequent years, in the halls of Trinity college, Dublin}, + lccn = {b17315189}, + shorttitle = {Lectures on quaternions}, + url = {http://archive.org/details/lecturesonquater00hami}, + publisher = {Hodges and Smith}, + address = {Dublin}, + author = {Hamilton, William Rowan}, + year = {1853} +} + +@book{HansonYakovlev_2002, + address = {New York, {NY}}, + title = {Operator Theory for Electromagnetics}, + 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}, @@ -126,6 +437,18 @@ @article{Holmes_2002 journal = {Journal of Geodesy} } +@article{Ishioka_2018, + title = {A New Recurrence Formula for Efficient Computation of Spherical Harmonic Transform}, + volume = {96}, + url = {https://www.jstage.jst.go.jp/article/jmsj/96/2/96_2018-019/_article}, + doi = {10.2151/jmsj.2018-019}, + number = {2}, + journal = {Journal of the Meteorological Society of Japan. Ser. {II}}, + author = {Ishioka, Keiichi}, + year = {2018}, + pages = {241--249} +} + @article{Kostelec_2008, doi = {10.1007/s00041-008-9013-5}, url = {https://doi.org/10.1007/s00041-008-9013-5}, @@ -140,7 +463,42 @@ @article{Kostelec_2008 journal = {Journal of Fourier Analysis and Applications} } -@article{McEwen_2011, +@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}, + 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}, + 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{McEwenWiaux_2011, doi = {10.1109/tsp.2011.2166394}, url = {https://doi.org/10.1109/tsp.2011.2166394}, year = 2011, @@ -157,6 +515,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}, @@ -171,6 +538,19 @@ @article{Newman_1966 journal = {Journal of Mathematical Physics} } +@article{PriceMcEwen_2024, + title = {Differentiable and accelerated spherical harmonic and Wigner transforms}, + volume = {510}, + issn = {0021-9991}, + url = {https://www.sciencedirect.com/science/article/pii/S0021999124003589}, + doi = {10.1016/j.jcp.2024.113109}, + journal = {Journal of Computational Physics}, + author = {Price, Matthew A. and {McEwen}, Jason D.}, + month = aug, + year = {2024}, + pages = {113109} +} + @article{Reinecke_2013, doi = {10.1051/0004-6361/201321494}, url = {https://doi.org/10.1051/0004-6361/201321494}, @@ -203,7 +583,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}, @@ -211,6 +591,105 @@ @book{Sakurai_1994 year = 1994 } +@book{Shankar_1994, + address = {New York}, + edition = {2nd}, + title = {Principles of Quantum Mechanics}, + publisher = {Plenum Press}, + author = {Shankar, Ramamurti}, + year = 1994 +} + +@article{Shukowsky_1986, + title = {A quadrature formula over the sphere with application to high resolution spherical harmonic analysis}, + volume = {60}, + issn = {1432-1394}, + url = {https://doi.org/10.1007/BF02519350}, + doi = {10.1007/BF02519350}, + number = {1}, + journal = {Bulletin g\'{e}od\'{e}sique}, + author = {Shukowsky, Wladimir}, + month = mar, + year = {1986}, + pages = {1--14} +} + +@misc{SommerEtAl_2018, + title = {Why and How to Avoid the Flipped Quaternion Multiplication}, + url = {http://arxiv.org/abs/1801.07478}, + 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}, +} + +@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, + issn = {0002-3264}, + number = 4, + journal = {Doklady Akademii nauk {SSSR}}, + author = {{VN} Strakhov}, + pages = {839---841}, + year = 1980 +} + +@book{Tait_1867, + title = {An elementary treatise on quaternions}, + url = {http://archive.org/details/anelementarytre03taitgoog}, + publisher = {Oxford, Clarendon Press}, + author = {Tait, Peter Guthrie}, + year = {1867}, +} + +@article{Tait_1868, + title = {On the Rotation of a Rigid Body about a Fixed Point}, + volume = {25}, + url = {http://archive.org/details/scientificpapers029742mbp}, + journal = {Transactions of the Royal Society of Edinburgh}, + author = {Tait, Peter Guthrie}, + year = {1868}, + note = {Also found in "Scientific Papers by Peter Guthrie Tait", Vol. I.} +} + +@book{Tait_1898, + title = {Scientific Papers by Peter Guthrie Tait}, + volume = {1}, + lccn = {29742}, + url = {http://archive.org/details/scientificpapers029742mbp}, + publisher = {Cambridge University Press}, + author = {Tait, Peter Guthrie}, + year = {1898} +} + +@article{Thorne_1980, + title = {Multipole expansions of gravitational radiation}, + volume = 52, + 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}, @@ -221,6 +700,32 @@ @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} +} + +@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}, @@ -259,3 +764,64 @@ @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-02678-6}, + shorttitle = {Quantum Mechanics}, + 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, + edition = {2nd}, +} + +@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{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} +} + +@book{Whittaker_1947, + address = {Cambridge}, + series = {Cambridge Mathematical Library}, + title = {A Treatise on the Analytical Dynamics of Particles and Rigid Bodies}, + isbn = {978-0-521-35883-5}, + url = {https://www.cambridge.org/core/books/treatise-on-the-analytical-dynamics-of-particles-and-rigid-bodies/E4CEF3F091F516792B0D30FA0DE9C2BB}, + publisher = {Cambridge University Press}, + author = {Whittaker, E. T.}, + year = {1947}, + edition = {4th} +} + +@book{Wilson_1929, + title = {Vector Analysis : A {Text-Book} for the Use of Students of Mathematics \& Physics: Founded Upon the Lectures of J. W. Gibbs}, + shorttitle = {Vector Analysis}, + url = {http://archive.org/details/vectoranalysiste0000unse}, + publisher = {Yale University Press}, + address = {New Haven}, + author = {Wilson, Edwin Bidwell}, + year = {1929} +} \ No newline at end of file diff --git a/docs/src/sYlm.md b/docs/src/sYlm.md deleted file mode 100644 index f3695535..00000000 --- a/docs/src/sYlm.md +++ /dev/null @@ -1,60 +0,0 @@ -# ``{}_{s}Y_{\ell,m}`` functions - -The spin-weighted spherical harmonics are an [important set of functions defined -on](@cite Boyle_2016) the rotation group ``𝐒𝐎(3)``, or more generally, the -spin group ``𝐒𝐩𝐢𝐧(3)`` that covers it. They are eigenfunctions of [the -left- and right-Lie derivatives](@ref "Differential operators"), and are -particularly useful in describing the angular dependence of polarized fields, -like the electromagnetic field and gravitational-wave field. Originally -introduced by [Newman_1966](@citet), they are essentially components of Wigner's -``\frak{D}`` matrices: -```math -{}_{s}Y_{\ell,m}(\mathbf{R}) - = (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \, \frak{D}^{(\ell)}_{m, -s}(\mathbf{R}). -``` -As such, they can be computed using the same [``H`` recursion](@ref "Algorithm -for computing ``H``") algorithm as the Wigner ``\frak{D}^{(\ell)}_{m, -s}`` -matrices. But because not all values of ``s \in -\ell:\ell`` are used, we can -be much more efficient in both storage and computation time. - -The user interface is very similar to the one for [Wigner's ``𝔇`` and ``d`` -matrices](@ref): -```julia -using Quaternionic -using SphericalFunctions - -R = randn(RotorF64) -ℓₘₐₓ = 8 -s = -2 -Y = sYlm_values(R, ℓₘₐₓ, s) -``` -Again, the results can take up a lot of memory, so for maximum efficiency when -calling this function repeatedly with different `R` values, it is best to -pre-allocate the necessary memory with the [`sYlm_prep`](@ref) function, and the -pass that in as an argument to [`sYlm_values!`](@ref): -```julia -Y_storage = sYlm_prep(ℓₘₐₓ, s) -Y = sYlm_values!(Y_storage, R, s) -``` -(Beware that, as noted in the documentation for [`sYlm_values!`](@ref), the -output `Y` is just a reference to part of the `Y_storage` object, so you should -not reuse `Y_storage` until you have copied or otherwise finished using `Y`.) - -The output `Y` is a single vector of `Complex` numbers with the same base type -as `R`. The ordering of the elements is described in the documentation for -[`sYlm_values!`](@ref). It is also possible to efficiently view slices of this -vector as a series of individual vectors using a [`sYlm_iterator`](@ref): -```julia -for (ℓ, Yˡ) in zip(0:ℓₘₐₓ, sYlm_iterator(Y, ℓₘₐₓ)) - # Do something with the matrix Yˡ[ℓ+m′+1, ℓ+m+1] -end -``` - -## Docstrings - -```@docs -sYlm_values -sYlm_values! -sYlm_prep -sYlm_iterator -``` diff --git a/docs/src/transformations.md b/docs/src/transformations.md deleted file mode 100644 index ac873328..00000000 --- a/docs/src/transformations.md +++ /dev/null @@ -1,147 +0,0 @@ -# ``s``-SHT Transformations - -One important capability of this package is the transformation between the two -representations of a spin-weighted spherical function: - - 1. Values `f` of the function evaluated on a set of points or "pixels" in the - domain of the function. - 2. Values `f̃` of the mode weights (coefficients) of an expansion in the - standard spin-weighted spherical-harmonic basis. - -In the literature, the transformation `f` ↦ `f̃` is usually called "analysis" or -`map2salm`, while the inverse transformation `f` ↦ `f̃` is called "synthesis" or -`salm2map`. These are both referred to as spin-spherical-harmonic transforms, -or ``s``-SHTs. - -To describe the values of a spin-``s`` function up to some maximum angular -resolution ``\ell_\mathrm{max}``, we need ``(\ell_\mathrm{max}+1)^2 - s^2`` mode -weights. We assume throughout that the values `f̃` are stored as -```julia -f̃ = [mode_weight(ℓ, m) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] -``` -(Here, `mode_weight` is a made-up function intended to provide a schematic.) In -particular, the ``m`` index varies most rapidly, and the ``\ell`` index varies -most slowly. Correspondingly, there must be *at least* -``(\ell_\mathrm{max}+1)^2 - s^2`` function values `f`. However, some ``s``-SHT -algorithms require more function values — usually by a factor of 2 or 4 — -trading off between speed and memory usage. - -The `SSHT` object implements these transformations, storing pre-computed -constants and pre-allocated workspace for the transformations. The interface is -designed to be similar to that of `FFTW.jl`, whereby an `SSHT` object `𝒯` can -be used to perform the transformation as either -```julia -f = 𝒯 * f̃ -``` -or -```julia -f̃ = 𝒯 \ f -``` -Currently, there are three algorithms implemented, each having different -advantages and disadvantages: - - 1. The "Direct" algorithm (introduced here for the first time), which is the - default, but should only be used up to ``\ell_\mathrm{max} \lesssim 50`` - because its intermediate storage requirements scale as - ``\ell_\mathrm{max}^4``. This algorithm is the fastest for small - ``\ell_\mathrm{max}``, it can be used with arbitrary (non-degenerate) - pixelizations, and achieves optimal dimensionality. - 2. The "Minimal" algorithm due to [Elahi_2018](@citet), with some minor - improvements. This algorithm is fast and — as the name implies — also - achieves optimal dimensionality, and its storage scales as - ``\ell_\mathrm{max}^3``. However, its pixelization is restricted, and its - accuracy at very high ``\ell_\mathrm{max}`` is not as good as the "RS" - algorithm. The algorithm itself is not actually fully specified by Elahi - et al., and leaves out some relatively simple improvements, so I have had - to take some liberties with my interpretation. - 3. The "RS" algorithm due to [Reinecke_2013](@citet). This forms the basis - for the [`libsharp`](https://gitlab.mpcdf.mpg.de/mtr/libsharp) and - [`ducc.sht`](https://gitlab.mpcdf.mpg.de/mtr/ducc#duccsht) packages. It - requires pixelizations on "iso-latitude rings", and does not achieve - optimal dimensionality. However, it is very fast, and its accuracy is - excellent at extremely high ``\ell_\mathrm{max}``. - - -## `SSHT` objects - -```@autodocs -Modules = [SphericalFunctions] -Pages = ["ssht.jl", "ssht/direct.jl", "ssht/minimal.jl", "ssht/rs.jl"] -``` - -## Pixelizations - -The algorithms implemented here require pixelizations. While the -"Direct" algorithm can be used with arbitrary pixelizations, the -"Minimal" and "RS" algorithms require more specific choices, as noted -in their docstrings. - -Typically, "pixelization" refers exclusively to a choice of points on -the sphere 𝕊² at which to compute function values. Of course, as -mentioned [elsewhere](@cite Boyle_2016), it is not *technically -possible* to define spin-weighted functions as functions of a point on -𝕊² alone; we also need some sense of reference direction in the -tangent space. Quite generally, we can define spin-weighted functions -on the group 𝐒𝐎(3) or 𝐒𝐩𝐢𝐧(3), so we will also refer to a choice -of a set of points in 𝐒𝐩𝐢𝐧(3) (which is essentially the group of -unit quaternions) as a "pixelization". However, assuming spherical -coordinates, a choice of *coordinates* on the sphere almost everywhere -induces a choice of the reference direction in the tangent space, so -it is *almost* possible to define pixelizations just in terms of -points on 𝕊². But using spherical coordinates is actually enough to -fully specify the pixelization, because the degeneracies at the poles -also allow us to define the reference direction. - -In principle, we could be concerned about the choice of reference -direction in the tangent space. That is, we might expect to care -about pixelizations over 𝕊³. However, we are dealing with -spin-weighted functions, which are eigenfunctions of a final rotation -about the reference direction. This means that once we choose any -reference direction at each point, we know the function values for any -other reference direction at those points. In particular, an -important property of a pixelization is the condition number of the -transformation matrix between the function values and the mode -weights. If we rotate the reference direction at a single point, this -is equivalent to multiplying the matrix by a diagonal matrix with -entries of 1 everywhere except the entry corresponding to that point, -where the entry is some complex phase. This does not change the -condition number of the matrix, so we can ignore the choice of -reference direction at every point. For other situations, where we -might care about the choice of reference direction, it might be -interesting to consider [this work by Marc -Alexa](https://github.com/moble/superfibonacci), and references -therein. - -Interesting discussions of various pixelizations and metrics can be -found in [Saff and Kuijlaars (1997)](@cite SaffKuijlaars_1997) and -[Brauchart and Grabner (2015)](@cite BrauchartGrabner_2015), as well -as blog posts -[here](https://web.archive.org/web/20220303150307/https://www.maths.unsw.edu.au/about/distributing-points-sphere) -and -[here](https://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/). -Note that the "equal-area" pixelizations of -[Healpix](https://healpix.sourceforge.io/) are very restrictive—only -being available for very specific numbers of points—and do not provide -any obvious advantages over the more flexible pixelizations available -here. - -The various pixelizations may be computed as follows: - -```@autodocs -Modules = [SphericalFunctions] -Pages = ["pixelizations.jl"] -``` - - -## Quadrature weights - -The "RS" algorithm requires quadrature weights corresponding to the input -pixelization. Though there is a working default choice, it is possible to use -others. There are several that are currently implemented, along with their -corresponding pixelizations: - -```@autodocs -Modules = [SphericalFunctions] -Pages = ["weights.jl"] -Order = [:module, :type, :constant, :function, :macro] -``` diff --git a/scripts/docs.jl b/scripts/docs.jl index f0b47455..32d674c9 100644 --- a/scripts/docs.jl +++ b/scripts/docs.jl @@ -6,14 +6,23 @@ # 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 -servedocs(launch_browser=true) +import LiveServer: servedocs +literate_input = joinpath(pwd(), "docs", "literate_input") +@info "Using input for Literate.jl from $literate_input" +servedocs( + 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, +) diff --git a/scripts/test.jl b/scripts/test.jl index aecfa8c1..c14c7b66 100644 --- a/scripts/test.jl +++ b/scripts/test.jl @@ -1,8 +1,8 @@ # Call this from the top-level directory as # julia -t auto scripts/test.jl -# Optionally, specify the name of a top-level test group — e.g., ssht — -# at the end of this command to only run the tests in that group. Or add -# --help at the end to see the possibilities. +# or to run with coverage as +# julia -t auto scripts/test.jl --coverage +# See docs/src/development/index.md for more information. import Dates println("Running tests starting at ", Dates.format(Dates.now(), "HH:MM:SS"), ".") @@ -15,16 +15,25 @@ using LinearAlgebra using Base.Threads LinearAlgebra.BLAS.set_num_threads(nthreads()) +# Check for the `--coverage` flag +coverage = any(ARGS .== "--coverage") + try - Δt = @elapsed Pkg.test("SphericalFunctions"; coverage=true, test_args=ARGS) + Δt = @elapsed Pkg.test("SphericalFunctions"; coverage, test_args=ARGS) println("Running tests took $Δt seconds.") catch e - println("Tests failed; proceeding to coverage") + if coverage + println("Tests failed; proceeding to coverage") + else + println("Tests failed.") + end end -Pkg.activate() # Activate Julia's base (home) directory -using Coverage -cd((@__DIR__) * "/..") -coverage = Coverage.process_folder("src") -Coverage.writefile("lcov.info", coverage) -Coverage.clean_folder(".") +if coverage + Pkg.activate() # Activate Julia's base (home) directory + using Coverage + cd((@__DIR__) * "/..") + coverage = Coverage.process_folder("src") + Coverage.writefile("lcov.info", coverage) + Coverage.clean_folder(".") +end diff --git a/src/SphericalFunctions.jl b/src/SphericalFunctions.jl index 5f0a2bde..3e79b01a 100644 --- a/src/SphericalFunctions.jl +++ b/src/SphericalFunctions.jl @@ -1,70 +1,44 @@ module SphericalFunctions -using FastTransforms: FFTW, fft, fftshift!, ifft, ifftshift!, irfft, - plan_bfft!, plan_fft, plan_fft! -using LinearAlgebra: LinearAlgebra, Bidiagonal, Diagonal, convert, ldiv!, mul! -using OffsetArrays: OffsetArray, OffsetVector -using ProgressMeter: Progress, next! -using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, - to_euler_phases, to_spherical_coordinates +using TestItems: @testitem, @testsnippet +using FastTransforms: ifft, irfft +using Quaternionic: Quaternionic, 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 +using SpecialFunctions +using DoubleFloats +using LinearAlgebra: Diagonal, Bidiagonal +using FixedSizeArrays: FixedSizeVectorDefault, FixedSizeVector -const MachineFloat = Union{Float16, Float32, Float64} +# Base.IEEEFloat is not public, so we just define our own +const IEEEFloat = Union{Float16, Float32, Float64} -include("utils.jl") +# Utilities (kept top-level; code lives in `src/utilities/`) +include("utilities/utils.jl") -include("pixelizations.jl") +include("utilities/pixelizations.jl") export golden_ratio_spiral_pixels, golden_ratio_spiral_rotors export sorted_rings, sorted_ring_pixels, sorted_ring_rotors export fejer1_rings, fejer2_rings, clenshaw_curtis_rings -include("complex_powers.jl") -export complex_powers, complex_powers! +include("utilities/complex_powers.jl") +export complex_powers, complex_powers!, ComplexPowers -include("indexing.jl") -export Ysize, Yrange, Yindex, deduce_limits, theta_phi, phi_theta -export WignerHsize, WignerHindex, _WignerHindex, WignerHrange -export WignerDsize, WignerDindex, WignerDrange - -include("iterators.jl") -export D_iterator, d_iterator, sYlm_iterator, λ_iterator -# Legacy API: -export Diterator, diterator, Yiterator, λiterator - -include("associated_legendre.jl") -export ALFRecursionCoefficients, ALFrecurse!, ALFcompute!, ALFcompute - -include("Hrecursion.jl") -export H!, H_recursion_coefficients - -include("evaluate.jl") -export d_prep, d_matrices, d_matrices! -export D_prep, D_matrices, D_matrices! -export sYlm_prep, sYlm_values, sYlm_values! -# Legacy API: -export d!, d, D!, Y!, dprep, Dprep, Yprep, ₛ𝐘 - -include("weights.jl") +include("utilities/weights.jl") export fejer1, fejer2, clenshaw_curtis -include("ssht.jl") -export SSHT, pixels, rotors - -include("map2salm.jl") -export map2salm, map2salm!, plan_map2salm - -include("operators.jl") +include("utilities/operators.jl") export L², Lz, L₊, L₋, R², Rz, R₊, R₋, ð, ð̄ -#include("rotate.jl") -#export rotate! - +include("wigner/wigner.jl") +export AbstractWignerMatrix, WignerMatrix, WignerDMatrix, WignerdMatrix +export WignerCalculator, WignerDCalculator, WignerdCalculator, WignerHCalculator +export recurrence! +public ℓ, ℓₘᵢₙ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ +public ell, ellmin, mpmax, mpmin, mmax, mmin +# Legacy API (no legacy names exported from the top-level module) +include("deprecated/Deprecated.jl") +export Deprecated -end # module +end # module SphericalFunctions diff --git a/src/complex_powers.jl b/src/complex_powers.jl deleted file mode 100644 index a5464c8c..00000000 --- a/src/complex_powers.jl +++ /dev/null @@ -1,82 +0,0 @@ -""" - complex_powers!(zpowers, z) - -Compute integer powers of `z` from `z^0` through `z^m`, recursively, where `m` is -one less than the length of the input `zpowers` vector. - -Note that `z` is assumed to be normalized, with complex amplitude approximately 1. - -See also: [`complex_powers`](@ref) -""" -function complex_powers!(zpowers, z) - Base.require_one_based_indexing(zpowers) - @fastmath @inbounds begin - M = length(zpowers) - if M == 0 - return zpowers - end - M -= 1 - θ = one(z) - zpowers[1] = θ - if M == 0 - return zpowers - end - if M == 1 - zpowers[2] = z - return zpowers - end - while z.re<0 || z.im<0 - θ *= 1im - z /= 1im - end - zpowers[2] = z - clock = θ - dc = -2 * sqrt(z).im^2 - t = 2 * dc - dz = dc * (1 + 2 * z) + 1im * sqrt(-dc * (2 + dc)) - for m in 2:M - zpowers[m+1] = zpowers[m] + dz - zpowers[m] *= clock - clock *= θ - dz += t * zpowers[m+1] - end - zpowers[M+1] *= clock - end - zpowers -end - - -""" - complex_powers(z, m) - -Compute integer powers of `z` from `z^0` through `z^m`, recursively. - -Note that `z` is assumed to be normalized, with complex amplitude approximately 1. - -This algorithm is mostly due to Stoer and Bulirsch in "Introduction to -Numerical Analysis" (page 24) — with a little help from de Moivre's formula, -which is essentially exp(iθ)ⁿ = exp(inθ), as well as my own alterations to deal -with different behaviors in different quadrants. - -There isn't usually a huge advantage to using this specialized function. If -you just need a particular power, it will generally be far more efficient and -just as accurate to compute either exp(iθ)ⁿ or exp(inθ) explicitly. However, -if you need all powers from 0 to m, this function is about 10 or 5 times faster -than those options, respectively, for large m. Like those options, this function -is numerically stable, in the sense that its errors are usually smaller than `m` -times the error from machine-precision errors in the input argument — or at worst -about 50% larger, which occurs as the phase approaches multiples of π/2. - -See also: [`complex_powers!`](@ref) -""" -function complex_powers(z, m::Int) - if abs(z) ≉ one(z) - throw(DomainError("z = $z", - "This function is only valid for `z` with complex amplitude approximately 1; " - * "abs(z) = $(abs(z))" - )) - end - zpowers = zeros(typeof(z), m+1) - complex_powers!(zpowers, z) -end - diff --git a/src/deprecated/Deprecated.jl b/src/deprecated/Deprecated.jl new file mode 100644 index 00000000..6fd94ce1 --- /dev/null +++ b/src/deprecated/Deprecated.jl @@ -0,0 +1,64 @@ +module Deprecated + +using FastTransforms: FFTW, fft, fftshift!, ifft, ifftshift!, irfft, + plan_bfft!, plan_fft, plan_fft! +using LinearAlgebra: LinearAlgebra, Bidiagonal, Diagonal, convert, ldiv!, mul! +using OffsetArrays: OffsetArray, OffsetVector +using ProgressMeter: Progress, next! +using Quaternionic: Quaternionic, Rotor, from_spherical_coordinates, + to_euler_phases, to_spherical_coordinates, basetype +using StaticArrays: @SVector +using SpecialFunctions, DoubleFloats +using LoopVectorization: @turbo +using Base.Threads: @threads, threadpoolsize + +# Pull in shared utility functionality from the parent module (code lives in `src/utilities/`). +using ..SphericalFunctions: IEEEFloat +using ..SphericalFunctions: OffsetVec, OffsetMat, offset +using ..SphericalFunctions: sqrtbinomial +using ..SphericalFunctions: golden_ratio_spiral_pixels, golden_ratio_spiral_rotors +using ..SphericalFunctions: sorted_rings, sorted_ring_pixels, sorted_ring_rotors +using ..SphericalFunctions: fejer1_rings, fejer2_rings, clenshaw_curtis_rings +using ..SphericalFunctions: complex_powers, complex_powers!, ComplexPowers +using ..SphericalFunctions: fejer1, fejer2, clenshaw_curtis + +include("indexing.jl") +export Ysize, Yrange, Yindex, deduce_limits, theta_phi, phi_theta +export WignerHsize, WignerHindex, _WignerHindex, WignerHrange +export WignerDsize, WignerDindex, WignerDrange + +include("iterators.jl") +export D_iterator, d_iterator, sYlm_iterator, λ_iterator +# Legacy API: +export Diterator, diterator, Yiterator, λiterator + +include("associated_legendre.jl") +export ALFRecursionCoefficients, ALFrecurse!, ALFcompute!, ALFcompute + +include("Hrecursion.jl") +export H!, H_recursion_coefficients + +include("evaluate.jl") +export d_prep, d_matrices, d_matrices! +export D_prep, D_matrices, D_matrices! +export sYlm_prep, sYlm_values, sYlm_values! +# Legacy API: +export d!, d, D!, D, Y!, Y, dprep, Dprep, Yprep, ₛ𝐘 + +# Legacy utilities / helpers that were historically exported from the top-level module. +export golden_ratio_spiral_pixels, golden_ratio_spiral_rotors +export sorted_rings, sorted_ring_pixels, sorted_ring_rotors +export fejer1_rings, fejer2_rings, clenshaw_curtis_rings +export complex_powers, complex_powers!, ComplexPowers +export fejer1, fejer2, clenshaw_curtis + +include("ssht.jl") +export SSHT, pixels, rotors + +include("map2salm.jl") +export map2salm, map2salm!, plan_map2salm + +# include("rotate.jl") +# export rotate! + +end # module Deprecated diff --git a/src/Hrecursion.jl b/src/deprecated/Hrecursion.jl similarity index 100% rename from src/Hrecursion.jl rename to src/deprecated/Hrecursion.jl diff --git a/src/associated_legendre.jl b/src/deprecated/associated_legendre.jl similarity index 100% rename from src/associated_legendre.jl rename to src/deprecated/associated_legendre.jl diff --git a/src/evaluate.jl b/src/deprecated/evaluate.jl similarity index 87% rename from src/evaluate.jl rename to src/deprecated/evaluate.jl index c9e3d234..a8348b06 100644 --- a/src/evaluate.jl +++ b/src/deprecated/evaluate.jl @@ -4,7 +4,7 @@ ### as ℓ increases, reaching ~4% around ℓ=512. ### 2. This is probably a much more significant advantage for ALFs. -using .SphericalFunctions: complex_powers! +using ..SphericalFunctions: complex_powers! using Quaternionic: Quaternionic, AbstractQuaternion, to_euler_phases @inline ϵ(m) = ifelse(m > 0 && isodd(m), -1, 1) @@ -14,9 +14,9 @@ using Quaternionic: Quaternionic, AbstractQuaternion, to_euler_phases d_matrices(β, ℓₘₐₓ) d_matrices(expiβ, ℓₘₐₓ) -Compute Wigner's ``d^{(\ell)}`` matrices with elements ``d^{(\ell)}_{m',m}(\beta)`` for all -``\ell \leq \ell_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" -Wigner matrices, in contrast to the full ``\mathfrak{D}`` matrices. +Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(β)`` for all +``ℓ \leq ℓ_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" +Wigner matrices, in contrast to the full ``𝔇`` matrices. See [`d_matrices!`](@ref) for details about the input and output values. @@ -29,16 +29,15 @@ with the result instead. """ d_matrices(β::Real, ℓₘₐₓ) = d_matrices(cis(β), ℓₘₐₓ) - @doc raw""" d_matrices!(d_storage, β) d_matrices!(d_storage, expiβ) d_matrices!(d, β, ℓₘₐₓ) d_matrices!(d, expiβ, ℓₘₐₓ) -Compute Wigner's ``d^{(\ell)}`` matrices with elements ``d^{(\ell)}_{m',m}(\beta)`` for all -``\ell \leq \ell_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" -Wigner matrices, in contrast to the full ``\mathfrak{D}`` matrices. +Compute Wigner's ``d^{(ℓ)}`` matrices with elements ``d^{(ℓ)}_{m',m}(β)`` for all +``ℓ \leq ℓ_\mathrm{max}``. The ``d`` matrices are sometimes called the "reduced" +Wigner matrices, in contrast to the full ``𝔇`` matrices. In all cases, the result is returned in a 1-dimensional array ordered as @@ -180,13 +179,29 @@ 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 ``(ℓ, m', m)`` at the given +angle ``(\iota)``. +""" +function d(ℓ, m′, m, β) + d(β, ℓ)[WignerDindex(ℓ, m′, m)] +end + @doc raw""" D_matrices(R, ℓₘₐₓ) D_matrices(α, β, γ, ℓₘₐₓ) -Compute Wigner's 𝔇 matrices ``\mathfrak{D}^{(\ell)}_{m',m}(\beta)`` for all ``\ell \leq -\ell_\mathrm{max}``. +Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(β)`` for all ``ℓ \leq +ℓ_\mathrm{max}``. See [`D_matrices!`](@ref) for details about the input and output values. @@ -215,8 +230,8 @@ end D_matrices!(D, R, ℓₘₐₓ) D_matrices!(D, α, β, γ, ℓₘₐₓ) -Compute Wigner's 𝔇 matrices ``\mathfrak{D}^{(\ell)}_{m',m}(\beta)`` for all ``\ell \leq -\ell_\mathrm{max}``. +Compute Wigner's 𝔇 matrices ``𝔇^{(ℓ)}_{m',m}(β)`` for all ``ℓ \leq +ℓ_\mathrm{max}``. In all cases, the result is returned in a 1-dimensional array ordered as @@ -283,7 +298,7 @@ end D_prep(ℓₘₐₓ, [T=Float64]) Construct storage space and pre-compute recursion coefficients to compute Wigner's -``\mathfrak{D}`` matrix in place. +``𝔇`` matrix in place. This returns the `D_storage` arguments needed by [`D_matrices!`](@ref). @@ -371,18 +386,40 @@ 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 ``(ℓ, 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""" sYlm_values(R, ℓₘₐₓ, spin) sYlm_values(θ, ϕ, ℓₘₐₓ, spin) -Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{\ell, m}(R)`` for all -``\ell \leq \ell_\mathrm{max}``. +Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{ℓ, m}(R)`` for all +``ℓ \leq ℓ_\mathrm{max}``. See [`sYlm_values!`](@ref) for details about the input and output values. -This function only appropriate when you need to evaluate the ``{}_{s}Y_{\ell, m}`` for a +This function only appropriate when you need to evaluate the ``{}_{s}Y_{ℓ, m}`` for a single value of `R` or `θ, ϕ` because it allocates large arrays and performs many calculations that could be reused. If you need to evaluate the matrices for many values of `R` or `θ, ϕ`, you should pre-allocate the storage with [`sYlm_prep`](@ref), and then call @@ -405,16 +442,16 @@ end sYlm_values!(sYlm, R, ℓₘₐₓ, spin) sYlm_values!(sYlm, θ, ϕ, ℓₘₐₓ, spin) -Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{\ell, m}(R)`` for all -``\ell \leq \ell_\mathrm{max}``. +Compute values of the spin-weighted spherical harmonic ``{}_{s}Y_{ℓ, m}(R)`` for all +``ℓ \leq ℓ_\mathrm{max}``. -The spherical harmonics of spin weight ``s`` are related to Wigner's ``\mathfrak{D}`` matrix +The spherical harmonics of spin weight ``s`` are related to Wigner's ``𝔇`` matrix as ```math \begin{aligned} -{}_{s}Y_{\ell, m}(R) - &= (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \mathfrak{D}^{(\ell)}_{m, -s}(R) \\ - &= (-1)^s \sqrt{\frac{2\ell+1}{4\pi}} \bar{\mathfrak{D}}^{(\ell)}_{-s, m}(\bar{R}). +{}_{s}Y_{ℓ, m}(R) + &= (-1)^s \sqrt{\frac{2ℓ+1}{4π}} 𝔇^{(ℓ)}_{m, -s}(R) \\ + &= (-1)^s \sqrt{\frac{2ℓ+1}{4π}} \bar{𝔇}^{(ℓ)}_{-s, m}(\bar{R}). \end{aligned} ``` @@ -443,7 +480,7 @@ The `θ, ϕ` arguments are spherical coordinates as described in the documentati [`Quaternionic.from_spherical_coordinates`](https://moble.github.io/Quaternionic.jl/dev/manual/#Quaternionic.from_spherical_coordinates-Tuple{Any,%20Any}). See also [`sYlm_values`](@ref) for a simpler function call when you only need to evaluate -the ``{}_{s}Y_{\ell, m}`` for a single value of `R` or `θ, ϕ`. +the ``{}_{s}Y_{ℓ, m}`` for a single value of `R` or `θ, ϕ`. # Examples @@ -503,14 +540,14 @@ end sYlm_prep(ℓₘₐₓ, sₘₐₓ, [T=Float64, [ℓₘᵢₙ=0]]) Construct storage space and pre-compute recursion coefficients to compute spin-weighted -spherical-harmonic values ``{}_{s}Y_{\ell, m}`` in place. +spherical-harmonic values ``{}_{s}Y_{ℓ, m}`` in place. This returns the `sYlm_storage` arguments needed by [`sYlm_values!`](@ref). Note that the result of this function can be passed to `sYlm_values!`, even if the value of `spin` passed to that function is smaller (in absolute value) than the `sₘₐₓ` passed to this function. That is, the `sYlm_storage` returned by this function can be used to compute -``{}_{s}Y_{\ell, m}`` values for numerous values of the spin. +``{}_{s}Y_{ℓ, m}`` values for numerous values of the spin. """ function sYlm_prep(ℓₘₐₓ, sₘₐₓ, ::Type{T}=Float64, ℓₘᵢₙ=0) where {T<:Real} @@ -574,7 +611,7 @@ end ₛ𝐘(s, ℓₘₐₓ, [T=Float64], [Rθϕ=golden_ratio_spiral_rotors(s, ℓₘₐₓ, T)]) -Construct a matrix of ``ₛYₗₘ(Rθϕ)`` values for the input `s` and all nontrivial ``(\ell, +Construct a matrix of ``ₛYₗₘ(Rθϕ)`` values for the input `s` and all nontrivial ``(ℓ, m)`` up to `ℓₘₐₓ`. This is a fast and accurate method for mapping between the vector of spin-weighted @@ -608,3 +645,24 @@ function ₛ𝐘(s, ℓₘₐₓ, ::Type{T}=Float64, Rθϕ=golden_ratio_spiral_r end ₛ𝐘 end + + +@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 ``(ℓ, m)`` at the given +spherical coordinate ``(θ, ϕ)``. +""" +function Y(s::Int, ℓ::Int, m::Int, θ, ϕ) + θ, ϕ = promote(θ, ϕ) + Rθϕ = Quaternionic.from_spherical_coordinates(θ, ϕ) + ₛ𝐘(s, ℓ, typeof(θ), [Rθϕ])[1, Yindex(ℓ, m, abs(s))] +end +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/src/indexing.jl b/src/deprecated/indexing.jl similarity index 100% rename from src/indexing.jl rename to src/deprecated/indexing.jl diff --git a/src/iterators.jl b/src/deprecated/iterators.jl similarity index 98% rename from src/iterators.jl rename to src/deprecated/iterators.jl index e2506e9b..a8f21a91 100644 --- a/src/iterators.jl +++ b/src/deprecated/iterators.jl @@ -261,14 +261,14 @@ Base.size(Yi::sYlm_iterator, dim) = dim > 1 ? 1 : length(Yi) λ_recursion_initialize(cosθ, sin½θ, cos½θ, s, ℓ, m) This provides initial values for the recursion to find -``{}_{s}\lambda_{\ell,m}`` along indices of increasing ``\ell``, due to +``{}_{s}\lambda_{ℓ,m}`` along indices of increasing ``ℓ``, due to [Kostelec & Rockmore](@cite Kostelec_2008) Specifically, this function computes -values with ``\ell=m``. +values with ``ℓ=m``. ```math -{}_{s}\lambda_{\ell,m}(\theta) - := {}_{s}Y_{\ell,m}(\theta, 0) - = (-1)^m\, \sqrt{\frac{2\ell+1}{4\pi}} d^\ell_{-m,s}(\theta) +{}_{s}\lambda_{ℓ,m}(θ) + := {}_{s}Y_{ℓ,m}(θ, 0) + = (-1)^m\, \sqrt{\frac{2ℓ+1}{4π}} d^ℓ_{-m,s}(θ) ``` """ function λ_recursion_initialize(sin½θ::T, cos½θ::T, s, ℓ, m) where T diff --git a/src/deprecated/map2salm.jl b/src/deprecated/map2salm.jl new file mode 100644 index 00000000..1ddaddf3 --- /dev/null +++ b/src/deprecated/map2salm.jl @@ -0,0 +1,222 @@ +@doc raw""" + map2salm(map, spin, ℓmax) + map2salm(map, plan) + +Transform `map` values sampled on the sphere to ``{}_sa_{ℓ, m}`` modes. + +The `map` array should have size Nφ along its first dimension and Nϑ along its second; any +number of dimensions may follow. The `spin` must be entered explicitly, and `ℓmax` is the +highest ℓ value you want in the output. + +For repeated applications of this function with different values of `map`, it is more +efficient to pre-compute `plan` using [`plan_map2salm`](@ref). These functions will create +a new `salm` array on each call. To operate in place on a pre-allocated `salm` array, use +[`map2salm!`](@ref). + +The core of this function follows the method described by [Reinecke and Seljebotn](@cite +Reinecke_2013). + +""" +function map2salm(map::AbstractArray{Complex{T}}, spin::Int, ℓmax::Int, show_progress=false) where {T<:Real} + Nφ, Nϑ, Nextra... = size(map) + salm = zeros(complex(T), (Ysize(ℓmax), Nextra...)) + map2salm!(salm, map, spin, ℓmax, show_progress) + return salm +end + +@doc raw""" + map2salm!(salm, map, spin, ℓmax) + map2salm!(salm, map, plan) + +Transform `map` values sampled on the sphere to ``{}_sa_{ℓ, m}`` modes in place. + +For details, see [`map2salm`](@ref). + +""" +function map2salm!( + salm::AbstractArray{Complex{T}}, + map::AbstractArray{Complex{T}}, + spin::Int, ℓmax::Int, show_progress=false +) where {T<:Real} + plan = plan_map2salm(map, spin, ℓmax) + map2salm!(salm, map, plan, show_progress) +end + +# Per-worker workspace of preallocated memory to modify during map2salm computations. +struct Workspace_map2salm{T<:Real,P} + G::Vector{Complex{T}} + fftplan::P +end + +@inline function with_workspace(f, pool::Channel) + ws = take!(pool) + try + return f(ws) + finally + put!(pool, ws) + end +end + +""" + plan_map2salm(map, spin, ℓmax) + +Precompute values to use in executing [`map2salm`](@ref) or [`map2salm!`](@ref). + +The arguments to this function exactly mirror those of the first form of [`map2salm`](@ref), +and all but the first argument in the first form of [`map2salm!`](@ref). The `plan` +returned by this function can be passed to the second forms of those functions to avoid some +computation and allocation costs. + +Note that the `plan` object is not thread safe; a separate `plan` should be created for each +thread that will use one, or locks/Channels should be used to ensure that a single `plan` is +not used at the same time on different threads. + +""" +function plan_map2salm(map_data::AbstractArray{Complex{T}}, spin::Int, ℓmax::Int) where {T<:Real} + Nφ, Nϑ, Nextra... = size(map_data) + m′max = abs(spin) + Hwedge = Array{T}(undef, WignerHsize(ℓmax, m′max)) + H_rec_coeffs = H_recursion_coefficients(ℓmax, T) + weight = clenshaw_curtis(Nϑ, T) + expiθ = complex_powers(cis(π / T(Nϑ-1)), Nϑ-1) + ϵs = ϵ(-spin) + + # Indexable without allocation (unlike collect(product(...))). + extra_dims = CartesianIndices(Nextra) + + # Number of workers seen by `@thread` + Nworkers = threadpoolsize(:default) + + workspace_pool = if T <: IEEEFloat + # Build FFT plan from a representative slice + proto_idx = extra_dims[1] + proto = @views map_data[:, 1, proto_idx.I...] + fftplan₁ = plan_fft(proto) + P = typeof(fftplan₁) + workspace_pool = Channel{Workspace_map2salm{T,P}}(Nworkers) + for i ∈ 1:Nworkers + Gᵢ = Vector{Complex{T}}(undef, Nφ) + planᵢ = plan_fft(proto) + put!(workspace_pool, Workspace_map2salm{T,P}(Gᵢ, planᵢ)) + end + workspace_pool + else + P = Nothing + workspace_pool = Channel{Workspace_map2salm{T,Nothing}}(Nworkers) + for i ∈ 1:Nworkers + Gᵢ = Vector{Complex{T}}(undef, Nφ) + planᵢ = nothing + put!(workspace_pool, Workspace_map2salm{T,P}(Gᵢ, planᵢ)) + end + workspace_pool + end + + return ( + spin, ℓmax, Nφ, Nϑ, Nextra, workspace_pool, m′max, + Hwedge, H_rec_coeffs, weight, expiθ, ϵs, extra_dims + ) +end + + +function computeG!( + G::AbstractArray{Complex{T}}, + map::AbstractArray{Complex{T}}, + weight::T, fftplan +) where {T<:Real} + G[:] = weight * fft(map) +end + + +function computeG!( + G::AbstractArray{Complex{T}}, + map::AbstractArray{Complex{T}}, + weight::T, fftplan +) where {T<:IEEEFloat} + @views mul!(G[:], fftplan, map) + @views G[:] *= weight +end + + +function map2salm!( + salm::AbstractArray{Complex{T}}, map::AbstractArray{Complex{T}}, + ( + spin, ℓmax, Nφ, Nϑ, Nextra, pool, m′max, Hwedge, + H_rec_coeffs, weight, expiθ, ϵs, extra_dims + ), + show_progress=false +) where {T<:Real} + s1 = size(salm) + s2 = (Ysize(ℓmax), Nextra...) + @assert s1==s2 "size(salm)=$s1 != (Ysize(ℓmax), Nextra...)=$s2" + + absspin = abs(spin) + progress = Progress(Nϑ * prod(Nextra); showspeed=true, enabled=show_progress) + proglock = ReentrantLock() + + @inbounds for ϑ ∈ 1:Nϑ + H!(Hwedge, expiθ[ϑ], ℓmax, m′max, H_rec_coeffs, WignerHindex) + + # NOTE: We can't thread at a higher level because each thread could access the + # same element of `salm` simultaneously below; by threading at this level, we + # are assured that the `Tuple(extra)...` index used below is unique to each thread. + @threads for extra ∈ extra_dims + # extra is a CartesianIndex; indices are in Tuple(extra) + with_workspace(pool) do ws + G = ws.G + @views computeG!(G, map[:, ϑ, Tuple(extra)...], weight[ϑ], ws.fftplan) + for ℓ ∈ absspin:ℓmax + λ_factor = ϵs * √((2ℓ+1)*T(π)) / Nφ + + i₀ = WignerHindex(ℓ, spin, 0, m′max) + + let m=0 + salm[Yindex(ℓ, m), Tuple(extra)...] += + G[m+1] * λ_factor * Hwedge[i₀] + end + + i₊ = i₀ + i₋ = i₀ + if !signbit(spin) + for m ∈ 1:min(ℓ, absspin) + i₊ -= ℓ-m+2 + i₋ += ℓ-m+1 + salm[Yindex(ℓ, m), Tuple(extra)...] += + G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] + salm[Yindex(ℓ, -m), Tuple(extra)...] += + G[Nφ-m+1] * λ_factor * Hwedge[i₋] + end + else + for m ∈ 1:min(ℓ, absspin) + i₊ += ℓ-m+1 + i₋ -= ℓ-m+2 + salm[Yindex(ℓ, m), Tuple(extra)...] += + G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] + salm[Yindex(ℓ, -m), Tuple(extra)...] += + G[Nφ-m+1] * λ_factor * Hwedge[i₋] + end + end + for m ∈ absspin+1:ℓ + i₊ += 1 + i₋ += 1 + salm[Yindex(ℓ, m), Tuple(extra)...] += + G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] + salm[Yindex(ℓ, -m), Tuple(extra)...] += + G[Nφ-m+1] * λ_factor * Hwedge[i₋] + end + end + end + + if show_progress + lock(proglock) do + next!(progress) + end + end + end + end +end + +function map2salm(map::AbstractArray{Complex{T}}, plan, show_progress=false) where {T<:Real} + salm = zeros(complex(T), (Ysize(plan[2]), plan[5]...)) + map2salm!(salm, map, plan, show_progress) + return salm +end diff --git a/src/ssht.jl b/src/deprecated/ssht.jl similarity index 100% rename from src/ssht.jl rename to src/deprecated/ssht.jl diff --git a/src/deprecated/ssht/direct.jl b/src/deprecated/ssht/direct.jl new file mode 100644 index 00000000..782e21e8 --- /dev/null +++ b/src/deprecated/ssht/direct.jl @@ -0,0 +1,98 @@ +""" + SSHTDirect(s, ℓₘₐₓ; decomposition=LinearAlgebra.qr, T=Float64, Rθϕ=golden_ratio_spiral_rotors(s, ℓₘₐₓ, T), inplace=true) + +Construct an ``s``-SHT object that uses the "Direct" method; see [`ₛ𝐘`](@ref) for details about the +method and optional arguments. Also see [`SSHT`](@ref) for general information about how to use +these objects. + +By default, this uses precisely optimal sampling — meaning that the number of points on which the +function is evaluated, represented by `Rθϕ`, is *equal to* the number of modes. However, it is +equally possible to evaluate on *more* points than there are modes. This can be useful, for +example, when processing multiple fields with different spin weights; the function could be +evaluated on points appropriate for the lowest value of ``|s|``, and therefore could also be used to +solve for fields of all other spin weights. + +Note that in-place operation is possible for this type when the length of the input `Rθϕ` is equal +to the number of modes given `s` and `ℓₘₐₓ` — and is the default behavior when possible. See +[`SSHT`](@ref) for description of in-place operation. + +This method is typically better than other current implementations for ``ℓₘₐₓ ≲ 24``, both in terms +of speed and accuracy. However, this advantage quickly falls away. A warning will be issued if +`ℓₘₐₓ` is greater than about 64, because this method is not likely to be the most efficient or most +accurate choice. + +""" +struct SSHTDirect{T<:Real, Inplace, Tdecomp} <: SSHT{T} + """Spin weight""" + s::Integer + + """Highest ℓ value present in the data""" + ℓₘₐₓ::Integer + + """Rotors at which to evalue the ``s``-SH""" + Rθϕ::Vector{Rotor{T}} + + """Spin-weighted spherical harmonic values""" + ₛ𝐘::Matrix{Complex{T}} + + """Decomposed ₛ𝐘 matrix used in inversion + + LU decomposition (`LinearAlgebra.lu`) is used by default, but any function that decomposes ₛ𝐘 + into something capable of solving the linear problem may be passed to the constructor. In + particular, using QR decomposition (`LinearAlgebra.qr`) will typically be around 3 times slower, + but have an error level roughly 10 times lower. Also note that QR decomposition does not + currently scale effectively with multiple threads. + """ + ₛ𝐘decomposition::Tdecomp +end + +function SSHTDirect( + s, ℓₘₐₓ; + decomposition=LinearAlgebra.lu, + T::Type{TT}=Float64, Rθϕ=golden_ratio_spiral_rotors(s, ℓₘₐₓ, T), + inplace=inplaceable(s, ℓₘₐₓ, Rθϕ) +) where TT + if ((ℓₘₐₓ+1)^2-s^2)^2 > 65^4 + @warn """ + The "Direct" method for s-SHT is only recommended for fairly small ℓ values (or comparably large s values). + Using it with ℓₘₐₓ=$ℓₘₐₓ and s=$s will be slow due to large memory requirements, and may be inaccurate. + You will likely benefit from trying other methods for these parameters. + """ + end + let ₛ𝐘 = ₛ𝐘(s, ℓₘₐₓ, TT, Rθϕ) + ₛ𝐘decomp = decomposition(ₛ𝐘) + SSHTDirect{TT, inplace, typeof(ₛ𝐘decomp)}(s, ℓₘₐₓ, Rθϕ, ₛ𝐘, ₛ𝐘decomp) + end +end + +function pixels(𝒯::SSHTDirect) + to_spherical_coordinates.(rotors(𝒯)) +end + +function rotors(𝒯::SSHTDirect) + 𝒯.Rθϕ +end + +function Base.:*(𝒯::SSHTDirect, f̃) + 𝒯.ₛ𝐘 * f̃ +end + +function LinearAlgebra.mul!(f, 𝒯::SSHTDirect, f̃) + mul!(f, 𝒯.ₛ𝐘, f̃) +end + +function Base.:\(𝒯::SSHTDirect, f) + 𝒯.ₛ𝐘decomposition \ f +end + +function Base.:\(𝒯::SSHTDirect{T, true}, ff̃) where {T} + ldiv!(𝒯.ₛ𝐘decomposition, ff̃) +end + +function LinearAlgebra.ldiv!(f̃, 𝒯::SSHTDirect, f) + ldiv!(f̃, 𝒯.ₛ𝐘decomposition, f) +end + +function LinearAlgebra.ldiv!(𝒯::SSHTDirect, ff̃) + ldiv!(𝒯.ₛ𝐘decomposition, ff̃) +end diff --git a/src/deprecated/ssht/minimal.jl b/src/deprecated/ssht/minimal.jl new file mode 100644 index 00000000..54d94a4b --- /dev/null +++ b/src/deprecated/ssht/minimal.jl @@ -0,0 +1,343 @@ + +"""Storage for Minimal spin-spherical-harmonic transform + +The Minimal algorithm was described by [Elahi et al.](@cite Elahi_2018), and +allows for the minimal number of function samples. + +""" +struct SSHTMinimal{T<:Real, Inplace} <: SSHT{T} + """Spin weight""" + s::Integer + + """Highest ℓ value present in the data""" + ℓₘₐₓ::Integer + + """OffsetVector of colatitudes of sampling rings + + These are the coordinates of the rings, where ring `j` contains 2j+1 + equally spaced points. The index ranges over j ∈ abs(s):ℓₘₐₓ. + """ + θ::OffsetVector + + """OffsetVector of index ranges for each θ ring + + This OffsetVector provides a series of `UnitRange`s indexing each + successive colatitude ring (as given by `θ`), so that the data along + the first such ring will be given by `f[θindices[abs(s)]]`, and the + last `f[θindices[ℓₘₐₓ]]`, where `f` is a standard (non-offset) + `Vector`. + """ + θindices::OffsetVector + + """OffsetVector of Fourier-transform plans + + These transform physical-space function values to the Fourier domain on + each ring of colatitude. The indices range over j ∈ abs(s):ℓₘₐₓ. + """ + plans::OffsetVector + bplans::OffsetVector + + """OffsetVector of matrices used to compute mode weights + + There is one matrix for each value of m = k ∈ -ℓₘₐₓ:ℓₘₐₓ. Indexing should + look like ₛΛ[m][j, ℓ], where the rows in each matrix correspond to + j ∈ abs(s):abs(m) and the columns correspond to ℓ ∈ max(abs(s),abs(m)):ℓₘₐₓ. + In particular, note that j ≤ |m| in these matrices; the opposite is true for + the `luₛΛ` variable. + """ + ₛΛ::OffsetVector + + """OffsetVector of LU-decomposed ₛΛ matrices, used to solve for mode weights + + There is one matrix for each value of m = k ∈ -ℓₘₐₓ:ℓₘₐₓ. Indexing should + look like ₛΛ[m][j, ℓ], where the rows in each matrix correspond to + j ∈ max(abs(s),abs(m)):ℓₘₐₓ and the columns correspond to ℓ over the same + range as j. In particular, note that j ≥ |m| in these matrices; the opposite + is true for the `ₛΛ` variable. + """ + luₛΛ::OffsetVector + + """Preallocated storage for all FT modes with a given ``m``""" + ₛfₘ::OffsetVector + + """Preallocated storage for all SH modes with a given positive ``m`` value""" + ₛf̃ₘ::OffsetVector + + """Preallocated storage for function values / Fourier modes on a given ring""" + ₛf̃ⱼ +end + + + +""" + SSHTMinimal(s, ℓₘₐₓ; kwargs...) + +Construct a `SSHTMinimal` object directly. This may also be achieved by calling the main +`SSHT` function with the same keywords, along with `method="Minimal"`. + +This object uses the algorithm described by [Elahi et al](@cite Elahi_2018). + +The basic floating-point number type may be adjusted with the keyword argument `T`, which +defaults to `Float64`. + +The SSHs are evaluated on a series of "rings" at constant colatitude. Their locations are +specified by the `θ` keyword argument, which defaults to [`sorted_rings(s, ℓₘₐₓ, T)`](@ref +sorted_rings). The first element of `θ` is the colatitude of the smallest ring (containing +``2s+1`` elements), and so on to the last element of `θ`, which is the colatitude of the +largest ring (containing ``2ℓₘₐₓ+1`` elements). + +Whenever `T` is either `Float64` or `Float32`, the keyword arguments `plan_fft_flags` and +`plan_fft_timelimit` may also be useful for obtaining more efficient FFTs. They default to +`FFTW.ESTIMATE` and `Inf`, respectively. They are passed to +[`AbstractFFTs.plan_fft`](https://juliamath.github.io/AbstractFFTs.jl/stable/api/#AbstractFFTs.plan_fft). + +Note that, because this algorithm achieves optimal dimensionality, the transformation will +be performed in place by default. If this is not desired, pass the keyword argument +`inplace=false`. This will cause the algorithm to copy the input and perform in-place +transformation on that copy. +""" +function SSHTMinimal( + s, ℓₘₐₓ; + T::Type{TT}=Float64, θ=sorted_rings(s, ℓₘₐₓ, T), + plan_fft_flags=FFTW.ESTIMATE, plan_fft_timelimit=Inf, + inplace=true +) where TT + @assert length(θ) == ℓₘₐₓ-abs(s)+1 """ + Length of `θ` ($(length(θ))) must equal `ℓₘₐₓ-abs(s)+1` ($(ℓₘₐₓ-abs(s)+1)) + """ + @assert TT === eltype(θ) + + θindices = let iθ = cumsum([2j+1 for j ∈ abs(s):ℓₘₐₓ]) + [a:b for (a,b) in eachrow(hcat([1; iθ[begin:end-1].+1], iθ))] + end + + ₛfₘ = OffsetVector(Vector{Complex{TT}}(undef, ℓₘₐₓ+1), 0:ℓₘₐₓ) + ₛf̃ₘ = OffsetVector(Vector{Complex{TT}}(undef, ℓₘₐₓ+1), 0:ℓₘₐₓ) + ₛf̃ⱼ = OffsetVector([Vector{Complex{TT}}(undef, 2j+1) for j ∈ abs(s):ℓₘₐₓ], abs(s):ℓₘₐₓ) + + plans = OffsetVector( + if TT ∈ [Float64, Float32] # Only supported types in FFTW + [ + plan_fft!( + ₛf̃ⱼ[j], + flags=plan_fft_flags, + timelimit=plan_fft_timelimit + ) + for j ∈ abs(s):ℓₘₐₓ + ] + else + [plan_fft!(ₛf̃ⱼ[j]) for j ∈ abs(s):ℓₘₐₓ] + end, + abs(s):ℓₘₐₓ # This is the range of valid indices + ) + bplans = OffsetVector( + if TT ∈ [Float64, Float32] # Only supported types in FFTW + [ + plan_bfft!( + Vector{Complex{TT}}(undef, 2j+1), + flags=plan_fft_flags, + timelimit=plan_fft_timelimit + ) + for j ∈ abs(s):ℓₘₐₓ + ] + else + [plan_bfft!(Vector{Complex{TT}}(undef, 2j+1)) for j ∈ abs(s):ℓₘₐₓ] + end, + abs(s):ℓₘₐₓ # This is the range of valid indices + ) + + # Compute ₛ𝐝 as a series of LU-decomposed matrices — one for each m=k value + ₛΛ, luₛΛ = let π=TT(π) + d, H_rec_coeffs = dprep(ℓₘₐₓ, TT) + ₛΛ = OffsetVector( + [ + let J = abs(s):abs(m), Δ = max(abs(s), abs(m)):ℓₘₐₓ + OffsetArray(zeros(T, length(J), length(Δ)), J, Δ) + end + for m ∈ -ℓₘₐₓ:ℓₘₐₓ + ], + -ℓₘₐₓ:ℓₘₐₓ + ) + luₛΛ = OffsetVector( + [ + let Δ = max(abs(s), abs(m)):ℓₘₐₓ + OffsetArray(zeros(T, length(Δ), length(Δ)), Δ, Δ) + end + for m ∈ -ℓₘₐₓ:ℓₘₐₓ + ], + -ℓₘₐₓ:ℓₘₐₓ + ) + for j ∈ abs(s):ℓₘₐₓ + θⱼ = θ[j-abs(s)+1] + d!(d, θⱼ, ℓₘₐₓ, H_rec_coeffs) + for ℓ ∈ abs(s):ℓₘₐₓ + coefficient = (-1)^s * √((2ℓ+1)/4π) + for m ∈ [-ℓ:-j... ; j:ℓ...] + ₛΛ[m][j, ℓ] = coefficient * d[WignerDindex(ℓ, m, -s)] + end + coefficient *= 2π + for m ∈ -min(j,ℓ):min(j,ℓ) + luₛΛ[m][j, ℓ] = coefficient * d[WignerDindex(ℓ, m, -s)] + end + end + end + ₛΛ, OffsetVector([LinearAlgebra.lu(luₛΛ[m]) for m ∈ -ℓₘₐₓ:ℓₘₐₓ], -ℓₘₐₓ:ℓₘₐₓ) + end + + SSHTMinimal{TT, inplace}( + s, ℓₘₐₓ, OffsetVector(θ, abs(s):ℓₘₐₓ), OffsetVector(θindices, abs(s):ℓₘₐₓ), + plans, bplans, ₛΛ, luₛΛ, ₛfₘ, ₛf̃ₘ, ₛf̃ⱼ + ) +end + +function pixels(𝒯::SSHTMinimal{T}) where {T} + let π = convert(T, π) + [ + @SVector [𝒯.θ[j], iϕ * 2π / (2j+1)] + for j ∈ abs(𝒯.s):𝒯.ℓₘₐₓ + for iϕ ∈ 0:2j + ] + end +end + +function rotors(𝒯::SSHTMinimal) + from_spherical_coordinates.(pixels(𝒯)) +end + +function Base.:*(𝒯::SSHTMinimal, f̃) + mul!(𝒯, copy(f̃)) +end + +function Base.:*(𝒯::SSHTMinimal{T, true}, f̃) where {T} + mul!(𝒯, f̃) +end + +function LinearAlgebra.mul!(f, 𝒯::SSHTMinimal, f̃) + f .= f̃ + mul!(𝒯, f) +end + +function LinearAlgebra.mul!(𝒯::SSHTMinimal{T}, ff̃) where {T} + s1 = size(ff̃, 1) + s2 = Ysize(abs(𝒯.s), 𝒯.ℓₘₐₓ) + @assert s1==s2 """ + Size of input `ff̃` along first dimension is incorrect for spins `s`=$s and + `ℓₘₐₓ`=$ℓₘₐₓ; it has size $s1, but should be $s2. + """ + s = 𝒯.s + ℓₘₐₓ = 𝒯.ℓₘₐₓ + ff̃′ = reshape(ff̃, size(ff̃, 1), :) + + @inbounds let π = T(π) + for ₛf̃ ∈ eachcol(ff̃′) + for m ∈ AlternatingCountup(ℓₘₐₓ) # Iterate over +m, then -m, up from m=0 + Δ = max(abs(s), abs(m)) + + # Iterate over rings, combining contributions for this `m` value + @threads for j ∈ Δ:ℓₘₐₓ + # We will accumulate into 𝒯.ₛfₘ, and write it out at the end of the loop + 𝒯.ₛfₘ[j] = false + + # Direct (non-aliased) contributions from m′ == m + λ = λ_iterator(𝒯.θ[j], s, m) + for (ℓ, ₛλₗₘ) ∈ zip(Δ:ℓₘₐₓ, λ) + 𝒯.ₛfₘ[j] += ₛf̃[Yindex(ℓ, m, abs(s))] * ₛλₗₘ + end + + # Aliased contributions from |m′| > j > |m| + for ℓ′ ∈ j:ℓₘₐₓ + for n ∈ cld(-ℓ′-m, 2j+1):fld(ℓ′-m, 2j+1) + m′ = m + n*(2j+1) + if abs(m′) > j + ₛλₗ′ₘ′ = 𝒯.ₛΛ[m′][j,ℓ′] + 𝒯.ₛfₘ[j] += ₛf̃[Yindex(ℓ′, m′, abs(s))] * ₛλₗ′ₘ′ + end + end + end + + end # j + + # Distribute the data back into the output + @threads for j ∈ Δ:ℓₘₐₓ + ₛf̃[Yindex(j, m, abs(s))] = 𝒯.ₛfₘ[j] + end + + end # m + + # Iterate over rings, doing Fourier decompositions on each + @threads for j ∈ abs(s):ℓₘₐₓ + jk = 𝒯.θindices[j] + @views ifftshift!(𝒯.ₛf̃ⱼ[j], ₛf̃[jk]) # Cycle order of modes in place to match order of FFT elements + @views 𝒯.bplans[j] * 𝒯.ₛf̃ⱼ[j] # Perform in-place BFFT + @. ₛf̃[jk] = 𝒯.ₛf̃ⱼ[j] # Copy data back into main array + end + + end # ₛf̃ + end # π + ff̃ +end + +function Base.:\(𝒯::SSHTMinimal, f) + ldiv!(𝒯, copy(f)) +end + +function Base.:\(𝒯::SSHTMinimal{T, true}, ff̃) where {T} + ldiv!(𝒯, ff̃) +end + +function LinearAlgebra.ldiv!(f̃, 𝒯::SSHTMinimal, f) + f̃ .= f + ldiv!(𝒯, f̃) +end + +function LinearAlgebra.ldiv!(𝒯::SSHTMinimal{T}, ff̃) where {T} + s1 = size(ff̃, 1) + s2 = Ysize(abs(𝒯.s), 𝒯.ℓₘₐₓ) + @assert s1==s2 """ + Size of input `ff̃` along first dimension is incorrect for spins `s`=$s and + `ℓₘₐₓ`=$ℓₘₐₓ; it has size $s1, but should be $s2. + """ + s = 𝒯.s + ℓₘₐₓ = 𝒯.ℓₘₐₓ + ff̃′ = reshape(ff̃, size(ff̃, 1), :) + + @inbounds let π = T(π) + for ₛf ∈ eachcol(ff̃′) + # Iterate over rings, doing Fourier decompositions on each + for j ∈ abs(s):ℓₘₐₓ + jk = 𝒯.θindices[j] + @. 𝒯.ₛf̃ⱼ[j] = ₛf[jk] * 2π / (2j+1) # Copy data from main array and normalize + @views 𝒯.plans[j] * 𝒯.ₛf̃ⱼ[j] # Perform in-place IFFT + @views fftshift!(ₛf[jk], 𝒯.ₛf̃ⱼ[j]) # Cycle order of modes in place to match order of FFT elements + end + + for m ∈ AlternatingCountdown(ℓₘₐₓ) + Δ = max(abs(s), abs(m)) + + # Gather the `m` data from each ring into a temporary workspace + @threads for j ∈ Δ:ℓₘₐₓ + 𝒯.ₛfₘ[j] = ₛf[Yindex(j, m, abs(s))] + end + + # Solve for the mode weights from the Fourier components + @views ldiv!(𝒯.ₛf̃ₘ.parent[Δ+1:ℓₘₐₓ+1], 𝒯.luₛΛ[m], 𝒯.ₛfₘ.parent[Δ+1:ℓₘₐₓ+1]) + + # Distribute the data back into the output + @threads for ℓ ∈ Δ:ℓₘₐₓ + ₛf[Yindex(ℓ, m, abs(s))] = 𝒯.ₛf̃ₘ[ℓ] + end + + # De-alias Fourier components from rings with values of j < Δ + @threads for j′ ∈ abs(s):abs(m)-1 + m′ = mod(j′+m, 2j′+1)-j′ # `m` aliases into `(j′, m′)` + α = 2π * sum( + 𝒯.ₛf̃ₘ[ℓ] * ₛλₗₘ + for (ℓ, ₛλₗₘ) ∈ zip(Δ:ℓₘₐₓ, λ_iterator(𝒯.θ[j′], s, m)) + ) + ₛf[Yindex(j′, m′, abs(s))] -= α + end # j′ + end # m + end # ₛf + end # π + ff̃ +end diff --git a/src/ssht/rs.jl b/src/deprecated/ssht/rs.jl similarity index 100% rename from src/ssht/rs.jl rename to src/deprecated/ssht/rs.jl diff --git a/src/map2salm.jl b/src/map2salm.jl deleted file mode 100644 index 50fc7cfc..00000000 --- a/src/map2salm.jl +++ /dev/null @@ -1,167 +0,0 @@ -@doc raw""" - map2salm(map, spin, ℓmax) - map2salm(map, plan) - -Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes. - -The `map` array should have size Nφ along its first dimension and Nϑ along its -second; any number of dimensions may follow. The `spin` must be entered -explicitly, and `ℓmax` is the highest ℓ value you want in the output. - -For repeated applications of this function with different values of `map`, it -is more efficient to pre-compute `plan` using [`plan_map2salm`](@ref). These -functions will create a new `salm` array on each call. To operate in place on -a pre-allocated `salm` array, use [`map2salm!`](@ref). - -The core of this function follows the method described by [Reinecke and Seljebotn](@cite -Reinecke_2013). - -""" -function map2salm(map::AbstractArray{Complex{T}}, spin::Int, ℓmax::Int, show_progress=false) where {T<:Real} - Nφ, Nϑ, Nextra... = size(map) - salm = zeros(complex(T), (Ysize(ℓmax), Nextra...)) - map2salm!(salm, map, spin, ℓmax, show_progress) - return salm -end - -@doc raw""" - map2salm!(salm, map, spin, ℓmax) - map2salm!(salm, map, plan) - -Transform `map` values sampled on the sphere to ``{}_sa_{\ell, m}`` modes in -place. - -For details, see [`map2salm`](@ref). - -""" -function map2salm!( - salm::AbstractArray{Complex{T}}, - map::AbstractArray{Complex{T}}, - spin::Int, ℓmax::Int, show_progress=false -) where {T<:Real} - plan = plan_map2salm(map, spin, ℓmax) - map2salm!(salm, map, plan, show_progress) -end - -""" - plan_map2salm(map, spin, ℓmax) - -Precompute values to use in executing [`map2salm`](@ref) or -[`map2salm!`](@ref). - -The arguments to this function exactly mirror those of the first form of -[`map2salm`](@ref), and all but the first argument in the first form of -[`map2salm!`](@ref). The `plan` returned by this function can be passed to the -second forms of those functions to avoid some computation and allocation costs. - -Note that the `plan` object is not thread safe; a separate `plan` should be -created for each thread that will use one, or locks should be used to ensure -that a single `plan` is not used at the same time on different threads. - -""" -function plan_map2salm(map_data::AbstractArray{Complex{T}}, spin::Int, ℓmax::Int) where {T<:Real} - Nφ, Nϑ, Nextra... = size(map_data) - Gs = [Array{complex(T)}(undef, (Nφ,)) for i = 1:nthreads()] - m′max = abs(spin) - Hwedge = Array{T}(undef, WignerHsize(ℓmax, m′max)) - H_rec_coeffs = H_recursion_coefficients(ℓmax, T) - weight = clenshaw_curtis(Nϑ, T) - expiθ = complex_powers(cis(π / T(Nϑ-1)), Nϑ-1) - ϵs = ϵ(-spin) - extra_dims = Base.Iterators.product((1:e for e in Nextra)...) - fftplan = T<:MachineFloat ? plan_fft(map_data[:, 1, first(extra_dims)...]) : nothing - - return (spin, ℓmax, Nφ, Nϑ, Nextra, Gs, m′max, Hwedge, H_rec_coeffs, weight, expiθ, ϵs, extra_dims, fftplan) -end - - -function computeG!( - G::AbstractArray{Complex{T}}, - map::AbstractArray{Complex{T}}, - weight::T, fftplan -) where {T<:Real} - G[:] = weight * fft(map) -end - - -function computeG!( - G::AbstractArray{Complex{T}}, - map::AbstractArray{Complex{T}}, - weight::T, fftplan -) where {T<:MachineFloat} - @views mul!(G[:], fftplan, map) - @views G[:] *= weight -end - - -function map2salm!( - salm::AbstractArray{Complex{T}}, map::AbstractArray{Complex{T}}, - (spin, ℓmax, Nφ, Nϑ, Nextra, Gs, m′max, Hwedge, H_rec_coeffs, weight, expiθ, ϵs, extra_dims, fftplan), - show_progress=false -) where {T<:Real} - s1 = size(salm) - s2 = (Ysize(ℓmax), Nextra...) - @assert s1==s2 "size(salm)=$s1 != (Ysize(ℓmax), Nextra...)=$s2" - - absspin = abs(spin) - progress = Progress(Nϑ * prod(Nextra); showspeed=true, enabled=show_progress) - - @inbounds for ϑ ∈ 1:Nϑ - H!(Hwedge, expiθ[ϑ], ℓmax, m′max, H_rec_coeffs, WignerHindex) - @threads for extra ∈ collect(extra_dims) - # NOTE: We can't thread at a higher level because each thread could access the - # same element of `salm` simultaneously below; by threading at this level, we - # are assured that the `extra...` index used below is unique to each thread. - iₜ = Threads.threadid() - G = Gs[iₜ] - computeG!(G, map[:, ϑ, extra...], weight[ϑ], fftplan) - for ℓ ∈ absspin:ℓmax - λ_factor = ϵs * √((2ℓ+1)*T(π)) / Nφ - - i₀ = WignerHindex(ℓ, spin, 0, m′max) - - let m=0 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * λ_factor * Hwedge[i₀] - end - - i₊ = i₀ - i₋ = i₀ - if !signbit(spin) - for m ∈ 1:min(ℓ, absspin) - i₊ -= ℓ-m+2 - i₋ += ℓ-m+1 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra...] += - G[Nφ-m+1] * λ_factor * Hwedge[i₋] - end - else - for m ∈ 1:min(ℓ, absspin) - i₊ += ℓ-m+1 - i₋ -= ℓ-m+2 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra...] += - G[Nφ-m+1] * λ_factor * Hwedge[i₋] - end - end - for m ∈ absspin+1:ℓ - i₊ += 1 - i₋ += 1 - salm[Yindex(ℓ, m), extra...] += - G[m+1] * ϵ(m) * λ_factor * Hwedge[i₊] - salm[Yindex(ℓ, -m), extra...] += - G[Nφ-m+1] * λ_factor * Hwedge[i₋] - end - end - next!(progress) - end - end -end - -function map2salm(map::AbstractArray{Complex{T}}, plan, show_progress=false) where {T<:Real} - salm = zeros(complex(T), (Ysize(plan[2]), plan[5]...)) - map2salm!(salm, map, plan, show_progress) - return salm -end diff --git a/src/pixelizations.jl b/src/pixelizations.jl deleted file mode 100644 index 920fe963..00000000 --- a/src/pixelizations.jl +++ /dev/null @@ -1,155 +0,0 @@ -@doc raw""" - golden_ratio_spiral_pixels(s, ℓₘₐₓ, [T=Float64]) - -Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. -Successive pixels are separated by the azimuthal angle ``Δϕ = -2π(2-φ)``, and are uniformly distributed in ``\cos θ``. - -This is also known as the "Fibonacci sphere" or "Fibonacci lattice". - -Visually, this is a very reasonable-looking pixelization, with fairly uniform -distance between neighbors, and approximate isotropy. No two pixels will share -the same values of either ``θ`` or ``ϕ``. Also note that no point is present -on either the North or South poles. - -The returned quantity is a vector of 2-SVectors providing the spherical -coordinates of each pixel. See also [`golden_ratio_spiral_rotors`](@ref) for -the corresponding `Rotor`s. -""" -function golden_ratio_spiral_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T - let π = T(π), φ = T(MathConstants.φ) - N = (ℓₘₐₓ+1)^2 - s^2 - # Note: the formula used here looks different from some sources, - # but just represents spiraling in the opposite direction. - Δϕ = 2π * (2 - φ) - ϕ = (0:N-1) * Δϕ - cosθ = range(1, -1, length=N+1)[begin:end-1] .- 1/T(N) - [@SVector [acos(cosθ), ϕ] for (cosθ, ϕ) in zip(cosθ, ϕ)] - end -end - -@doc raw""" - golden_ratio_spiral_rotors(s, ℓₘₐₓ, [T=Float64]) - -Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. - -See [`golden_ratio_spiral_pixels`](@ref) for more detailed explanation. The -quantity returned by this function is a vector of `Rotor`s providing each pixel. -""" -function golden_ratio_spiral_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T - from_spherical_coordinates.(golden_ratio_spiral_pixels(s, ℓₘₐₓ, T)) -end - -@doc raw""" - sorted_rings(s, ℓₘₐₓ, [T=Float64]) - -Compute locations of a series of rings labelled by ``j ∈ |s|:ℓₘₐₓ`` (analogous -to ``ℓ``), where each ring will contain ``k = 2j+1`` (analogous to ``m``) -pixels distributed evenly around the ring. These rings are then sorted, so -that the ring with the most pixels (``j = ℓₘₐₓ``) is closest to the equator, -and the next-largest ring is placed just above or below the equator (depending -on the sign of ``s``), the next just below or above, and so on. This is -generally a fairly good first guess when minimizing the condition number of -matrices used to solve for mode weights from function values. In particular, I -use this to initialize the Minimal algorithm, which is then fed into an -optimizer to fine-tune the positions of the rings. - -This function does not provide the individual pixels; it just provides the -colatitude values of the rings on which the pixels will be placed. The pixels -themselves are provided by [`sorted_ring_pixels`](@ref). -""" -function sorted_rings(s, ℓₘₐₓ, ::Type{T}=Float64) where T - let πo2 = prevfloat(T(π)/2, s) - sort( - collect(LinRange(T(0), T(π), 2+ℓₘₐₓ-abs(s)+1))[begin+1:end-1], - lt=(x,y)->(abs(x-πo2) 65^4 - @warn """ - The "Direct" method for s-SHT is only recommended for fairly small ℓ values (or comparably large s values). - Using it with ℓₘₐₓ=$ℓₘₐₓ and s=$s will be slow due to large memory requirements, and may be inaccurate. - You will likely benefit from trying other methods for these parameters. - """ - end - let ₛ𝐘 = ₛ𝐘(s, ℓₘₐₓ, TT, Rθϕ) - ₛ𝐘decomp = decomposition(ₛ𝐘) - SSHTDirect{TT, inplace, typeof(ₛ𝐘decomp)}(s, ℓₘₐₓ, Rθϕ, ₛ𝐘, ₛ𝐘decomp) - end -end - -function pixels(𝒯::SSHTDirect) - to_spherical_coordinates.(rotors(𝒯)) -end - -function rotors(𝒯::SSHTDirect) - 𝒯.Rθϕ -end - -function Base.:*(𝒯::SSHTDirect, f̃) - 𝒯.ₛ𝐘 * f̃ -end - -function LinearAlgebra.mul!(f, 𝒯::SSHTDirect, f̃) - mul!(f, 𝒯.ₛ𝐘, f̃) -end - -function Base.:\(𝒯::SSHTDirect, f) - 𝒯.ₛ𝐘decomposition \ f -end - -function Base.:\(𝒯::SSHTDirect{T, true}, ff̃) where {T} - ldiv!(𝒯.ₛ𝐘decomposition, ff̃) -end - -function LinearAlgebra.ldiv!(f̃, 𝒯::SSHTDirect, f) - ldiv!(f̃, 𝒯.ₛ𝐘decomposition, f) -end - -function LinearAlgebra.ldiv!(𝒯::SSHTDirect, ff̃) - ldiv!(𝒯.ₛ𝐘decomposition, ff̃) -end +end # module Direct diff --git a/src/ssht/huffenberger_wandelt.jl b/src/ssht/huffenberger_wandelt.jl new file mode 100644 index 00000000..f3179bbc --- /dev/null +++ b/src/ssht/huffenberger_wandelt.jl @@ -0,0 +1,288 @@ +module HuffenbergerWandelt + +raw""" +The Wigner ``d`` matrix corresponds to a rotation about the ``y`` axis by an angle ``β``: +``\exp\left[ β 𝐣 / 2 \right]``. But computing the ``d`` matrix for a general angle is +a little awkward. However, computing the ``d`` matrix for a rotation by ``π/2`` is somewhat +simpler, and computing the full ``D`` matrix for a rotation about the ``z`` axis is trivial +— it's just a phase factor. Thus, we can re-express ``d`` for a general angle ``β`` in +terms of ``d`` for the angle ``π/2`` and a phase factor. + +We start with the fact that the ``y`` axis equals the ``z`` axis rotated by ``π/2`` about +the ``x`` axis: +```math +𝐣 = e^{\frac{π}{2} 𝐢/ 2}\, 𝐤\, e^{-\frac{π}{2} 𝐢/ 2}. +``` +So we have +```math +e^{β 𝐣 / 2} += +e^{\frac{π}{2} 𝐢/ 2}\, e^{β 𝐤 / 2}\, e^{-\frac{π}{2} 𝐢/ 2}. +``` +Unfortunately, this expression involves the ``x`` axis, which we don't want. But we can +similarly express the ``x`` axis in terms of the ``y`` axis rotated about the ``z`` axis: +```math +𝐢 = e^{-\frac{π}{2} 𝐤/ 2}\, 𝐣\, e^{\frac{π}{2} 𝐤/ 2}. +``` +And now we can use this in our first expression to find +```math +e^{β 𝐣 / 2} += +e^{-\frac{π}{2} 𝐤/ 2}\, e^{\frac{π}{2} 𝐣/ 2}\, e^{\frac{π}{2} 𝐤/ 2}\, +e^{β 𝐤 / 2}\, +e^{\frac{π}{2} 𝐤/ 2}\, e^{-\frac{π}{2} 𝐣/ 2}\, e^{-\frac{π}{2} 𝐤/ 2}. +``` +Now, we can use this expansion to find an expression for the ``d`` matrix value: +```math +\begin{align} +d^{ℓ}_{m', m}(β) +&= +𝔇^{ℓ}_{m', m}\left(e^{β 𝐣 / 2}\right) \\ +&= +𝔇^{ℓ}_{m', m_1}\left(e^{-\frac{π}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_2, m_3}\left(e^{\frac{π}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_3, m_4}\left(e^{β 𝐤 / 2}\right)\, \\ +&\quad \times +𝔇^{ℓ}_{m_4, m_5}\left(e^{\frac{π}{2} 𝐤/ 2}\right)\, +𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{π}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m_6, m}\left(e^{-\frac{π}{2} 𝐤/ 2}\right) \\ +&= +δ_{m', m_1} e^{im'\frac{π}{2}}\, +𝔇^{ℓ}_{m_1, m_2}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +δ_{m_2, m_3} e^{-im_2\frac{π}{2}}\, +𝔇^{ℓ}_{m_3, m_4}\left(e^{β 𝐤 / 2}\right)\, \\ +&\quad \times +δ_{m_4, m_5} e^{-im_4\frac{π}{2}}\, +𝔇^{ℓ}_{m_5, m_6}\left(e^{-\frac{π}{2} 𝐣/ 2}\right)\, +δ_{m_6, m} e^{im\frac{π}{2}} \\ +&= +e^{im'\frac{π}{2}}\, e^{-im''\frac{π}{2}}\, +e^{-im'''\frac{π}{2}}\, e^{im\frac{π}{2}}\, +&\quad \times +𝔇^{ℓ}_{m', m''}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +𝔇^{ℓ}_{m'', m'''}\left(e^{β 𝐤 / 2}\right)\, \\ +𝔇^{ℓ}_{m''', m}\left(e^{-\frac{π}{2} 𝐣/ 2}\right) \\ +&= +e^{im'\frac{π}{2}}\, e^{-im''\frac{π}{2}}\, +e^{-im'''\frac{π}{2}}\, e^{im\frac{π}{2}}\, +&\quad \times +𝔇^{ℓ}_{m', m''}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +e^{-im''β}\, +𝔇^{ℓ}_{m'', m}\left(e^{-\frac{π}{2} 𝐣/ 2}\right) \\ +&= +i^{m'+m-2m''}\, +d^{ℓ}_{m', m''}\left(e^{\frac{π}{2} 𝐣/ 2}\right)\, +e^{-im''β}\, +d^{ℓ}_{m'', m}\left(e^{-\frac{π}{2} 𝐣/ 2}\right) \\ +&= +i^{m'+m}(-1)^{m''}\, +d^{ℓ}_{m', m''}\left(\frac{π}{2}\right)\, +e^{-im''β}\, +d^{ℓ}_{m'', m}\left(-\frac{π}{2}\right) \\ +&= +i^{m'+m}(-1)^{m}\, +d^{ℓ}_{m', m''}\left(\frac{π}{2}\right)\, +e^{-im''β}\, +d^{ℓ}_{m'', m}\left(\frac{π}{2}\right) \\ +&= +i^{m'-m}\, +d^{ℓ}_{m', m''}\left(\frac{π}{2}\right)\, +e^{-im''β}\, +d^{ℓ}_{m'', m}\left(\frac{π}{2}\right) +\end{align} +``` + +""" + + + +function deducelmax(salm) + N = length(salm) + lmax = floor(Int, sqrt(N) - 1) + if (lmax + 1)^2 != N + error("Cannot deduce lmax from salm length $N") + end + return lmax +end + + +""" + map2salm(𝒯, f) + +Map function values `f` to spin-weighted spherical-harmonic mode weights `f̃` using the +`SSHT` object `𝒯`. +""" +function map2salm end +function map2salm! end + +""" + salm2map(salm, s, [lmax=deducelmax(salm), Ntheta=2lmax+1, Nphi=2lmax+1]) + +Compatibility function for `spinsfast` package. Converts mode weights of spin-weighted +function, stored in the array `salm`, to values on an equiangular grid with `Ntheta` points +in θ and `Nphi` points in ϕ. The spin weight is `s` and the maximum ℓ value in the input +data is `lmax`. + +The optional arguments `lmax`, `Ntheta`, and `Nphi` are available for backwards +compatibility, but if they are given values that are inconsistent with the defaults, an +error will be thrown. + +See also [`map2salm`](@ref) for the (rough) inverse operation. + + +# Notes + +The input `salm` data should be given in increasing order of ℓ value, always starting with +`(ℓ, m) = (0, 0)` even if `s` is nonzero, proceeding to `(1, -1)`, `(1, 0)`, `(1, 1)`, etc. +Explicitly, the ordering should match this: + + [f̃[ℓ, m] for ℓ ∈ 0:lmax for m ∈ -ℓ:ℓ] + +The output data are presented on this grid of spherical coordinates: + + [f(θ, ϕ) for θ ∈ LinRange(0, π, Ntheta), ϕ ∈ LinRange(0, 2π, Nphi+1)[begin:end-1]] + +Note that `map2salm` and `salm2map` are not true inverses of each other for several reasons. +First, modes with `ell < |s|` should always be zero; they are simply assumed to be zero on +input to `salm2map`. It is also possible to define a `map` function that violates this +assumption -- for example, having a nonzero average value over the sphere; if the function +has nonzero spin `s`, this is impossible. Also, it is possible to define a map of a +function with so much angular dependence that it cannot be captured with the given `lmax` +value. For example, a discontinuous function will never be perfectly resolved. + +""" +function salm2map(salm, s, lmax=deducelmax(salm), Ntheta=2lmax+1, Nphi=2lmax+1) + # Not implemented + error("salm2map is not yet implemented") +end + +using FFTW +using LinearAlgebra + +""" + lm_index(ℓ, m, lmax) -> Int + +Map (ℓ, m) to the flattened index matching spinsfast ordering. +Inspired by Python wrapper ordering in `python/__init__.py` lines 31–49 and C helper `ind_lm` in `cextension.c` lines ~20–53. +""" +lm_index(ℓ, m, lmax) = ℓ^2 + (m + ℓ) + 1 # 1-based for Julia + +""" + ind_lm(idx, lmax) -> (ℓ, m) + +Inverse of `lm_index`. +""" +function ind_lm(idx, lmax) + i0 = idx - 1 + ℓ = floor(Int, sqrt(i0)) + while (ℓ + 1)^2 <= i0 + ℓ += 1 + end + m = i0 - ℓ^2 - ℓ + return ℓ, m +end + +""" + salm2map(salm, s, lmax; Nθ = 2lmax+1, Nφ = 2lmax+1) + +Spin-weighted (s) spherical-harmonic synthesis to grid samples. +Follows structure of `spinsfast_salm2map` in `code/spinsfast_backward_transform.c` lines 131–200 and wrapper in `python/cextension.c` lines 56–90. +""" +function salm2map(salm::AbstractVector{<:Complex}, s::Integer, lmax::Integer; + Nθ::Integer = 2lmax + 1, Nφ::Integer = 2lmax + 1) + + Nm = 2lmax + 1 + + # Placeholder Wigner d(π/2); replace with stable helper like `wdhp_TN_helper` (see `spinsfast_backward_transform.c` lines 151–160). + wig_d_halfpi(ℓ, m, mp) = wigner_d_halfpi(ℓ, m, mp) + + # G matrix (mode-coupled intermediate), cf. `Gmm` in `spinsfast_backward_transform.c` lines 151–186. + G = zeros(ComplexF64, Nm, Nm) + + # Build G via Δ products, cf. `spinsfast_backward_Gmm` call in `spinsfast_salm2map` lines 151–160. + for ℓ in 0:lmax + for m in -ℓ:ℓ + alm = salm[lm_index(ℓ, m, lmax)] + for mp in -ℓ:ℓ + Δ1 = wig_d_halfpi(ℓ, m, mp) + Δ2 = wig_d_halfpi(ℓ, mp, s) + G[m + lmax + 1, mp + lmax + 1] += alm * Δ1 * Δ2 + end + end + end + + # FFT work array F, matching the quadrant placement in `spinsfast_backward_transform.c` lines 151–186. + F = zeros(ComplexF64, Nθ, Nφ) + limit = lmax + for mp in 0:limit + for m in 0:limit + F[mp + 1, m + 1] = G[mp + 1, m + 1] # ++ + if m > 0 + F[mp + 1, Nφ - m + 1] = G[mp + 1, Nm - m + 1] # +- + end + if mp > 0 + F[Nθ - mp + 1, m + 1] = G[Nm - mp + 1, m + 1] # -+ + end + if mp > 0 && m > 0 + F[Nθ - mp + 1, Nφ - m + 1] = G[Nm - mp + 1, Nm - m + 1] # -- + end + end + end + + # Inverse FFT (2D), mirroring `fftw_execute` usage in `spinsfast_backward_transform.c` lines 188–198. + f = ifft(ifft(F, 1), 2) .* Nm # scale to align with C conventions + return f +end + +""" + map2salm(f, s, lmax) + +Spin-weighted analysis: grid samples -> spherical-harmonic coefficients. +Inspired by forward path (`spinsfast_map2salm`) inverse of the above; quadrant extraction mirrors `spinsfast_backward_transform.c` lines 151–186. +""" +function map2salm(f::AbstractMatrix{<:Complex}, s::Integer, lmax::Integer) + Nθ, Nφ = size(f) + Nm = 2lmax + 1 + + # Forward FFT (2D), inverse of salm2map’s iFFT; normalization mirrors scaling above. + F = fft(fft(f, 1), 2) ./ Nm + + # Reconstruct G from quadrants, inverse of the packing in `salm2map`. + G = zeros(ComplexF64, Nm, Nm) + for mp in 0:lmax + for m in 0:lmax + G[mp + 1, m + 1] = F[mp + 1, m + 1] # ++ + if m > 0 + G[mp + 1, Nm - m + 1] = F[mp + 1, Nφ - m + 1] # +- + end + if mp > 0 + G[Nm - mp + 1, m + 1] = F[Nθ - mp + 1, m + 1] # -+ + end + if mp > 0 && m > 0 + G[Nm - mp + 1, Nm - m + 1] = F[Nθ - mp + 1, Nφ - m + 1] # -- + end + end + end + + # Placeholder Wigner d(π/2); replace with stable helper (see `spinsfast_backward_transform.c` lines 151–160). + wig_d_halfpi(ℓ, m, mp) = wigner_d_halfpi(ℓ, m, mp) + + # Project G back to alm, paralleling the inverse of `spinsfast_backward_Gmm`. + salm = zeros(ComplexF64, (lmax + 1)^2) + for ℓ in 0:lmax + for m in -ℓ:ℓ + acc = 0.0 + 0im + for mp in -ℓ:ℓ + Δ1 = wig_d_halfpi(ℓ, m, mp) + Δ2 = wig_d_halfpi(ℓ, mp, s) + acc += conj(Δ1) * conj(Δ2) * G[m + lmax + 1, mp + lmax + 1] + end + salm[lm_index(ℓ, m, lmax)] = acc + end + end + return salm +end + + +end # module HuffenbergerWandelt diff --git a/src/ssht/minimal.jl b/src/ssht/minimal.jl index 54d94a4b..5f1b6a74 100644 --- a/src/ssht/minimal.jl +++ b/src/ssht/minimal.jl @@ -1,343 +1,5 @@ +module Minimal -"""Storage for Minimal spin-spherical-harmonic transform -The Minimal algorithm was described by [Elahi et al.](@cite Elahi_2018), and -allows for the minimal number of function samples. -""" -struct SSHTMinimal{T<:Real, Inplace} <: SSHT{T} - """Spin weight""" - s::Integer - - """Highest ℓ value present in the data""" - ℓₘₐₓ::Integer - - """OffsetVector of colatitudes of sampling rings - - These are the coordinates of the rings, where ring `j` contains 2j+1 - equally spaced points. The index ranges over j ∈ abs(s):ℓₘₐₓ. - """ - θ::OffsetVector - - """OffsetVector of index ranges for each θ ring - - This OffsetVector provides a series of `UnitRange`s indexing each - successive colatitude ring (as given by `θ`), so that the data along - the first such ring will be given by `f[θindices[abs(s)]]`, and the - last `f[θindices[ℓₘₐₓ]]`, where `f` is a standard (non-offset) - `Vector`. - """ - θindices::OffsetVector - - """OffsetVector of Fourier-transform plans - - These transform physical-space function values to the Fourier domain on - each ring of colatitude. The indices range over j ∈ abs(s):ℓₘₐₓ. - """ - plans::OffsetVector - bplans::OffsetVector - - """OffsetVector of matrices used to compute mode weights - - There is one matrix for each value of m = k ∈ -ℓₘₐₓ:ℓₘₐₓ. Indexing should - look like ₛΛ[m][j, ℓ], where the rows in each matrix correspond to - j ∈ abs(s):abs(m) and the columns correspond to ℓ ∈ max(abs(s),abs(m)):ℓₘₐₓ. - In particular, note that j ≤ |m| in these matrices; the opposite is true for - the `luₛΛ` variable. - """ - ₛΛ::OffsetVector - - """OffsetVector of LU-decomposed ₛΛ matrices, used to solve for mode weights - - There is one matrix for each value of m = k ∈ -ℓₘₐₓ:ℓₘₐₓ. Indexing should - look like ₛΛ[m][j, ℓ], where the rows in each matrix correspond to - j ∈ max(abs(s),abs(m)):ℓₘₐₓ and the columns correspond to ℓ over the same - range as j. In particular, note that j ≥ |m| in these matrices; the opposite - is true for the `ₛΛ` variable. - """ - luₛΛ::OffsetVector - - """Preallocated storage for all FT modes with a given ``m``""" - ₛfₘ::OffsetVector - - """Preallocated storage for all SH modes with a given positive ``m`` value""" - ₛf̃ₘ::OffsetVector - - """Preallocated storage for function values / Fourier modes on a given ring""" - ₛf̃ⱼ -end - - - -""" - SSHTMinimal(s, ℓₘₐₓ; kwargs...) - -Construct a `SSHTMinimal` object directly. This may also be achieved by calling the main -`SSHT` function with the same keywords, along with `method="Minimal"`. - -This object uses the algorithm described by [Elahi et al](@cite Elahi_2018). - -The basic floating-point number type may be adjusted with the keyword argument `T`, which -defaults to `Float64`. - -The SSHs are evaluated on a series of "rings" at constant colatitude. Their locations are -specified by the `θ` keyword argument, which defaults to [`sorted_rings(s, ℓₘₐₓ, T)`](@ref -sorted_rings). The first element of `θ` is the colatitude of the smallest ring (containing -``2s+1`` elements), and so on to the last element of `θ`, which is the colatitude of the -largest ring (containing ``2ℓₘₐₓ+1`` elements). - -Whenever `T` is either `Float64` or `Float32`, the keyword arguments `plan_fft_flags` and -`plan_fft_timelimit` may also be useful for obtaining more efficient FFTs. They default to -`FFTW.ESTIMATE` and `Inf`, respectively. They are passed to -[`AbstractFFTs.plan_fft`](https://juliamath.github.io/AbstractFFTs.jl/stable/api/#AbstractFFTs.plan_fft). - -Note that, because this algorithm achieves optimal dimensionality, the transformation will -be performed in place by default. If this is not desired, pass the keyword argument -`inplace=false`. This will cause the algorithm to copy the input and perform in-place -transformation on that copy. -""" -function SSHTMinimal( - s, ℓₘₐₓ; - T::Type{TT}=Float64, θ=sorted_rings(s, ℓₘₐₓ, T), - plan_fft_flags=FFTW.ESTIMATE, plan_fft_timelimit=Inf, - inplace=true -) where TT - @assert length(θ) == ℓₘₐₓ-abs(s)+1 """ - Length of `θ` ($(length(θ))) must equal `ℓₘₐₓ-abs(s)+1` ($(ℓₘₐₓ-abs(s)+1)) - """ - @assert TT === eltype(θ) - - θindices = let iθ = cumsum([2j+1 for j ∈ abs(s):ℓₘₐₓ]) - [a:b for (a,b) in eachrow(hcat([1; iθ[begin:end-1].+1], iθ))] - end - - ₛfₘ = OffsetVector(Vector{Complex{TT}}(undef, ℓₘₐₓ+1), 0:ℓₘₐₓ) - ₛf̃ₘ = OffsetVector(Vector{Complex{TT}}(undef, ℓₘₐₓ+1), 0:ℓₘₐₓ) - ₛf̃ⱼ = OffsetVector([Vector{Complex{TT}}(undef, 2j+1) for j ∈ abs(s):ℓₘₐₓ], abs(s):ℓₘₐₓ) - - plans = OffsetVector( - if TT ∈ [Float64, Float32] # Only supported types in FFTW - [ - plan_fft!( - ₛf̃ⱼ[j], - flags=plan_fft_flags, - timelimit=plan_fft_timelimit - ) - for j ∈ abs(s):ℓₘₐₓ - ] - else - [plan_fft!(ₛf̃ⱼ[j]) for j ∈ abs(s):ℓₘₐₓ] - end, - abs(s):ℓₘₐₓ # This is the range of valid indices - ) - bplans = OffsetVector( - if TT ∈ [Float64, Float32] # Only supported types in FFTW - [ - plan_bfft!( - Vector{Complex{TT}}(undef, 2j+1), - flags=plan_fft_flags, - timelimit=plan_fft_timelimit - ) - for j ∈ abs(s):ℓₘₐₓ - ] - else - [plan_bfft!(Vector{Complex{TT}}(undef, 2j+1)) for j ∈ abs(s):ℓₘₐₓ] - end, - abs(s):ℓₘₐₓ # This is the range of valid indices - ) - - # Compute ₛ𝐝 as a series of LU-decomposed matrices — one for each m=k value - ₛΛ, luₛΛ = let π=TT(π) - d, H_rec_coeffs = dprep(ℓₘₐₓ, TT) - ₛΛ = OffsetVector( - [ - let J = abs(s):abs(m), Δ = max(abs(s), abs(m)):ℓₘₐₓ - OffsetArray(zeros(T, length(J), length(Δ)), J, Δ) - end - for m ∈ -ℓₘₐₓ:ℓₘₐₓ - ], - -ℓₘₐₓ:ℓₘₐₓ - ) - luₛΛ = OffsetVector( - [ - let Δ = max(abs(s), abs(m)):ℓₘₐₓ - OffsetArray(zeros(T, length(Δ), length(Δ)), Δ, Δ) - end - for m ∈ -ℓₘₐₓ:ℓₘₐₓ - ], - -ℓₘₐₓ:ℓₘₐₓ - ) - for j ∈ abs(s):ℓₘₐₓ - θⱼ = θ[j-abs(s)+1] - d!(d, θⱼ, ℓₘₐₓ, H_rec_coeffs) - for ℓ ∈ abs(s):ℓₘₐₓ - coefficient = (-1)^s * √((2ℓ+1)/4π) - for m ∈ [-ℓ:-j... ; j:ℓ...] - ₛΛ[m][j, ℓ] = coefficient * d[WignerDindex(ℓ, m, -s)] - end - coefficient *= 2π - for m ∈ -min(j,ℓ):min(j,ℓ) - luₛΛ[m][j, ℓ] = coefficient * d[WignerDindex(ℓ, m, -s)] - end - end - end - ₛΛ, OffsetVector([LinearAlgebra.lu(luₛΛ[m]) for m ∈ -ℓₘₐₓ:ℓₘₐₓ], -ℓₘₐₓ:ℓₘₐₓ) - end - - SSHTMinimal{TT, inplace}( - s, ℓₘₐₓ, OffsetVector(θ, abs(s):ℓₘₐₓ), OffsetVector(θindices, abs(s):ℓₘₐₓ), - plans, bplans, ₛΛ, luₛΛ, ₛfₘ, ₛf̃ₘ, ₛf̃ⱼ - ) -end - -function pixels(𝒯::SSHTMinimal{T}) where {T} - let π = convert(T, π) - [ - @SVector [𝒯.θ[j], iϕ * 2π / (2j+1)] - for j ∈ abs(𝒯.s):𝒯.ℓₘₐₓ - for iϕ ∈ 0:2j - ] - end -end - -function rotors(𝒯::SSHTMinimal) - from_spherical_coordinates.(pixels(𝒯)) -end - -function Base.:*(𝒯::SSHTMinimal, f̃) - mul!(𝒯, copy(f̃)) -end - -function Base.:*(𝒯::SSHTMinimal{T, true}, f̃) where {T} - mul!(𝒯, f̃) -end - -function LinearAlgebra.mul!(f, 𝒯::SSHTMinimal, f̃) - f .= f̃ - mul!(𝒯, f) -end - -function LinearAlgebra.mul!(𝒯::SSHTMinimal{T}, ff̃) where {T} - s1 = size(ff̃, 1) - s2 = Ysize(abs(𝒯.s), 𝒯.ℓₘₐₓ) - @assert s1==s2 """ - Size of input `ff̃` along first dimension is incorrect for spins `s`=$s and - `ℓₘₐₓ`=$ℓₘₐₓ; it has size $s1, but should be $s2. - """ - s = 𝒯.s - ℓₘₐₓ = 𝒯.ℓₘₐₓ - ff̃′ = reshape(ff̃, size(ff̃, 1), :) - - @inbounds let π = T(π) - for ₛf̃ ∈ eachcol(ff̃′) - for m ∈ AlternatingCountup(ℓₘₐₓ) # Iterate over +m, then -m, up from m=0 - Δ = max(abs(s), abs(m)) - - # Iterate over rings, combining contributions for this `m` value - @threads for j ∈ Δ:ℓₘₐₓ - # We will accumulate into 𝒯.ₛfₘ, and write it out at the end of the loop - 𝒯.ₛfₘ[j] = false - - # Direct (non-aliased) contributions from m′ == m - λ = λ_iterator(𝒯.θ[j], s, m) - for (ℓ, ₛλₗₘ) ∈ zip(Δ:ℓₘₐₓ, λ) - 𝒯.ₛfₘ[j] += ₛf̃[Yindex(ℓ, m, abs(s))] * ₛλₗₘ - end - - # Aliased contributions from |m′| > j > |m| - for ℓ′ ∈ j:ℓₘₐₓ - for n ∈ cld(-ℓ′-m, 2j+1):fld(ℓ′-m, 2j+1) - m′ = m + n*(2j+1) - if abs(m′) > j - ₛλₗ′ₘ′ = 𝒯.ₛΛ[m′][j,ℓ′] - 𝒯.ₛfₘ[j] += ₛf̃[Yindex(ℓ′, m′, abs(s))] * ₛλₗ′ₘ′ - end - end - end - - end # j - - # Distribute the data back into the output - @threads for j ∈ Δ:ℓₘₐₓ - ₛf̃[Yindex(j, m, abs(s))] = 𝒯.ₛfₘ[j] - end - - end # m - - # Iterate over rings, doing Fourier decompositions on each - @threads for j ∈ abs(s):ℓₘₐₓ - jk = 𝒯.θindices[j] - @views ifftshift!(𝒯.ₛf̃ⱼ[j], ₛf̃[jk]) # Cycle order of modes in place to match order of FFT elements - @views 𝒯.bplans[j] * 𝒯.ₛf̃ⱼ[j] # Perform in-place BFFT - @. ₛf̃[jk] = 𝒯.ₛf̃ⱼ[j] # Copy data back into main array - end - - end # ₛf̃ - end # π - ff̃ -end - -function Base.:\(𝒯::SSHTMinimal, f) - ldiv!(𝒯, copy(f)) -end - -function Base.:\(𝒯::SSHTMinimal{T, true}, ff̃) where {T} - ldiv!(𝒯, ff̃) -end - -function LinearAlgebra.ldiv!(f̃, 𝒯::SSHTMinimal, f) - f̃ .= f - ldiv!(𝒯, f̃) -end - -function LinearAlgebra.ldiv!(𝒯::SSHTMinimal{T}, ff̃) where {T} - s1 = size(ff̃, 1) - s2 = Ysize(abs(𝒯.s), 𝒯.ℓₘₐₓ) - @assert s1==s2 """ - Size of input `ff̃` along first dimension is incorrect for spins `s`=$s and - `ℓₘₐₓ`=$ℓₘₐₓ; it has size $s1, but should be $s2. - """ - s = 𝒯.s - ℓₘₐₓ = 𝒯.ℓₘₐₓ - ff̃′ = reshape(ff̃, size(ff̃, 1), :) - - @inbounds let π = T(π) - for ₛf ∈ eachcol(ff̃′) - # Iterate over rings, doing Fourier decompositions on each - for j ∈ abs(s):ℓₘₐₓ - jk = 𝒯.θindices[j] - @. 𝒯.ₛf̃ⱼ[j] = ₛf[jk] * 2π / (2j+1) # Copy data from main array and normalize - @views 𝒯.plans[j] * 𝒯.ₛf̃ⱼ[j] # Perform in-place IFFT - @views fftshift!(ₛf[jk], 𝒯.ₛf̃ⱼ[j]) # Cycle order of modes in place to match order of FFT elements - end - - for m ∈ AlternatingCountdown(ℓₘₐₓ) - Δ = max(abs(s), abs(m)) - - # Gather the `m` data from each ring into a temporary workspace - @threads for j ∈ Δ:ℓₘₐₓ - 𝒯.ₛfₘ[j] = ₛf[Yindex(j, m, abs(s))] - end - - # Solve for the mode weights from the Fourier components - @views ldiv!(𝒯.ₛf̃ₘ.parent[Δ+1:ℓₘₐₓ+1], 𝒯.luₛΛ[m], 𝒯.ₛfₘ.parent[Δ+1:ℓₘₐₓ+1]) - - # Distribute the data back into the output - @threads for ℓ ∈ Δ:ℓₘₐₓ - ₛf[Yindex(ℓ, m, abs(s))] = 𝒯.ₛf̃ₘ[ℓ] - end - - # De-alias Fourier components from rings with values of j < Δ - @threads for j′ ∈ abs(s):abs(m)-1 - m′ = mod(j′+m, 2j′+1)-j′ # `m` aliases into `(j′, m′)` - α = 2π * sum( - 𝒯.ₛf̃ₘ[ℓ] * ₛλₗₘ - for (ℓ, ₛλₗₘ) ∈ zip(Δ:ℓₘₐₓ, λ_iterator(𝒯.θ[j′], s, m)) - ) - ₛf[Yindex(j′, m′, abs(s))] -= α - end # j′ - end # m - end # ₛf - end # π - ff̃ -end +end # module Minimal diff --git a/src/ssht/reinecke_seljebotn.jl b/src/ssht/reinecke_seljebotn.jl new file mode 100644 index 00000000..ab5da841 --- /dev/null +++ b/src/ssht/reinecke_seljebotn.jl @@ -0,0 +1,5 @@ +module ReineckeSeljebotn + + + +end # module ReineckeSeljebotn diff --git a/src/ssht/ssht.jl b/src/ssht/ssht.jl new file mode 100644 index 00000000..c553cc2c --- /dev/null +++ b/src/ssht/ssht.jl @@ -0,0 +1,66 @@ +"""Supertype of storage for spin-spherical-harmonic transforms""" +abstract type SSHT{T<:Real} end + +""" + pixels(𝒯) + +Return the spherical coordinates (θ, ϕ) on which the spin-weighted spherical harmonics are +evaluated. See also [`rotors`](@ref), which provides the actual `Rotor`s on which they are +evaluated. +""" +function pixels end + + +""" + rotors(𝒯) + +Return the `Rotor`s on which the spin-weighted spherical harmonics are evaluated. See also +[`pixels`](@ref), which provides the corresponding spherical coordinates. +""" +function rotors end + + +# mul!, ldiv!, synthesis, analysis, map2salm, salm2map + + +""" + synthesis(𝒯, f̃) + synthesis!(f, 𝒯, f̃) + synthesize(𝒯, f̃) + synthesize!(f, 𝒯, f̃) + +Synthesize function values `f` from spin-weighted spherical-harmonic mode weights `f̃` using +the `SSHT` object `𝒯`. +""" +function synthesis end +function synthesis! end +function synthesize end +function synthesize! end + +""" + analysis(𝒯, f) + analysis!(f̃, 𝒯, f) + analyze(𝒯, f) + analyze!(f̃, 𝒯, f) + +Analyze function values `f` to obtain spin-weighted spherical-harmonic mode weights `f̃` +using the `SSHT` object `𝒯`. +""" +function analysis end +function analysis! end +function analyze end +function analyze! end + + +function Base.show(io::IO, 𝒯::SSHT) + print(io, typeof(𝒯), "($(𝒯.s), $(𝒯.ℓₘₐₓ))") +end + + +inplaceable(s, ℓₘₐₓ, Rθϕ) = (ℓₘₐₓ + 1)^2 - s^2 == length(Rθϕ) + + +include("ssht/direct.jl") +include("ssht/minimal.jl") +include("ssht/reinecke_seljebotn.jl") +include("ssht/huffenberger_wandelt.jl") diff --git a/src/utilities/complex_powers.jl b/src/utilities/complex_powers.jl new file mode 100644 index 00000000..12cb5165 --- /dev/null +++ b/src/utilities/complex_powers.jl @@ -0,0 +1,192 @@ +""" + complex_powers!(zpowers, z) + +Compute integer powers of `z` from `z^0` through `z^m`, recursively, where `m` is +one less than the length of the input `zpowers` vector. + +Note that `z` is assumed to be normalized, with complex amplitude approximately 1. + +See also: [`complex_powers`](@ref) +""" +function complex_powers!(zpowers, z) + Base.require_one_based_indexing(zpowers) + @fastmath @inbounds begin + M = length(zpowers) + if M == 0 + return zpowers + end + M -= 1 + θ = one(z) + zpowers[1] = θ + if M == 0 + return zpowers + end + if M == 1 + zpowers[2] = z + return zpowers + end + while z.re<0 || z.im<0 + θ *= 1im + z /= 1im + end + zpowers[2] = z + clock = θ + dc = -2 * sqrt(z).im^2 + t = 2 * dc + dz = dc * (1 + 2 * z) + 1im * sqrt(-dc * (2 + dc)) + for m in 2:M + zpowers[m+1] = zpowers[m] + dz + zpowers[m] *= clock + clock *= θ + dz += t * zpowers[m+1] + end + zpowers[M+1] *= clock + end + zpowers +end + + +""" + complex_powers(z, m) + +Compute integer powers of `z` from `z^0` through `z^m`, recursively. + +Note that `z` is assumed to be normalized, with complex amplitude approximately 1. + +This algorithm is mostly due to Stoer and Bulirsch in "Introduction to +Numerical Analysis" (page 24) — with a little help from de Moivre's formula, +which is essentially exp(iθ)ⁿ = exp(inθ), as well as my own alterations to deal +with different behaviors in different quadrants. + +There isn't usually a huge advantage to using this specialized function. If +you just need a particular power, it will generally be far more efficient and +just as accurate to compute either exp(iθ)ⁿ or exp(inθ) explicitly. However, +if you need all powers from 0 to m, this function is about 10 or 5 times faster +than those options, respectively, for large m. Like those options, this function +is numerically stable, in the sense that its errors are usually smaller than `m` +times the error from machine-precision errors in the input argument — or at worst +about 50% larger, which occurs as the phase approaches multiples of π/2. + +See also: [`complex_powers!`](@ref) +""" +function complex_powers(z, m::Int) + if abs(z) ≉ one(z) + throw(DomainError("z = $z", + "This function is only valid for `z` with complex amplitude approximately 1; " + * "abs(z) = $(abs(z))" + )) + end + zpowers = zeros(typeof(z), m+1) + 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 + + +@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 diff --git a/src/operators.jl b/src/utilities/operators.jl similarity index 91% rename from src/operators.jl rename to src/utilities/operators.jl index 2193b64c..01c47608 100644 --- a/src/operators.jl +++ b/src/utilities/operators.jl @@ -16,7 +16,7 @@ Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``L^2`` as ```math -L^2 {}_{s}Y_{\ell,m} = \ell\,(\ell+1) {}_{s}Y_{\ell,m} +L^2 {}_{s}Y_{ℓ,m} = ℓ\,(ℓ+1) {}_{s}Y_{ℓ,m} ``` See also [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -38,7 +38,7 @@ Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``L_z`` as ```math -L_z {}_{s}Y_{\ell,m} = m\, {}_{s}Y_{\ell,m} +L_z {}_{s}Y_{ℓ,m} = m\, {}_{s}Y_{ℓ,m} ``` See also [`L²`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -63,11 +63,11 @@ L_+] = L_+``, which allows us to derive ``L_+ = L_x + i\, L_y.`` In terms of the SWSHs, we can write the action of ``L_+`` as ```math -L_+ {}_{s}Y_{\ell,m} = \sqrt{(\ell-m)(\ell+m+1)}\, {}_{s}Y_{\ell,m+1}. +L_+ {}_{s}Y_{ℓ,m} = \sqrt{(ℓ-m)(ℓ+m+1)}\, {}_{s}Y_{ℓ,m+1}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{L_+(f)\right\}_{s,\ell,m} = \sqrt{(\ell+m)(\ell-m+1)}\,\left\{f\right\}_{s,\ell,m-1}. +\left\{L_+(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ+m)(ℓ-m+1)}\,\left\{f\right\}_{s,ℓ,m-1}. ``` See also [`L²`](@ref), [`Lz`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -99,11 +99,11 @@ L_-] = -L_-``, which allows us to derive ``L_- = L_x - i\, L_y.`` In terms of the SWSHs, we can write the action of ``L_-`` as ```math -L_- {}_{s}Y_{\ell,m} = \sqrt{(\ell+m)(\ell-m+1)}\, {}_{s}Y_{\ell,m-1}. +L_- {}_{s}Y_{ℓ,m} = \sqrt{(ℓ+m)(ℓ-m+1)}\, {}_{s}Y_{ℓ,m-1}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{L_-(f)\right\}_{s,\ell,m} = \sqrt{(\ell-m)(\ell+m+1)}\,\left\{f\right\}_{s,\ell,m+1}. +\left\{L_-(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ-m)(ℓ+m+1)}\,\left\{f\right\}_{s,ℓ,m+1}. ``` See also [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), [`Rz`](@ref), @@ -141,7 +141,7 @@ Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``R^2`` as ```math -R^2 {}_{s}Y_{\ell,m} = \ell\,(\ell+1) {}_{s}Y_{\ell,m} +R^2 {}_{s}Y_{ℓ,m} = ℓ\,(ℓ+1) {}_{s}Y_{ℓ,m} ``` See also [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`Rz`](@ref), [`R₊`](@ref), @@ -164,7 +164,7 @@ operators") or [Boyle](@cite Boyle_2016) for more details. In terms of the SWSHs, we can write the action of ``R_z`` as ```math -R_z {}_{s}Y_{\ell,m} = -s\, {}_{s}Y_{\ell,m} +R_z {}_{s}Y_{ℓ,m} = -s\, {}_{s}Y_{ℓ,m} ``` Note the unfortunate sign of ``s``, which seems to be opposite to what we expect, and arises from the choice of definition of ``s`` in the original paper by [Newman and Penrose](@cite @@ -194,11 +194,11 @@ R_+] = R_+``, which allows us to derive ``R_+ = R_x - i\, R_y.`` In terms of the SWSHs, we can write the action of ``R_+`` as ```math -R_+ {}_{s}Y_{\ell,m} = \sqrt{(\ell+s)(\ell-s+1)}\, {}_{s-1}Y_{\ell,m} +R_+ {}_{s}Y_{ℓ,m} = \sqrt{(ℓ+s)(ℓ-s+1)}\, {}_{s-1}Y_{ℓ,m} ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{R_+(f)\right\}_{s,\ell,m} = \sqrt{(\ell+s)(\ell-s+1)}\,\left\{f\right\}_{s-1,\ell,m}. +\left\{R_+(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ+s)(ℓ-s+1)}\,\left\{f\right\}_{s-1,ℓ,m}. ``` Because of the unfortunate sign of ``s`` arising from the choice of definition of ``s`` in the original paper by [Newman and Penrose](@cite Newman_1966), this is a *lowering* operator @@ -235,11 +235,11 @@ R_-] = -R_-``, which allows us to derive ``R_- = R_x + i\, R_y.`` In terms of the SWSHs, we can write the action of ``R_-`` as ```math -R_- {}_{s}Y_{\ell,m} = \sqrt{(\ell-s)(\ell+s+1)}\, {}_{s+1}Y_{\ell,m} +R_- {}_{s}Y_{ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)}\, {}_{s+1}Y_{ℓ,m} ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{R_-(f)\right\}_{s,\ell,m} = \sqrt{(\ell-s)(\ell+s+1)}\,\left\{f\right\}_{s+1,\ell,m}. +\left\{R_-(f)\right\}_{s,ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)}\,\left\{f\right\}_{s+1,ℓ,m}. ``` Because of the unfortunate sign of ``s`` arising from the choice of definition of ``s`` in the original paper by [Newman and Penrose](@cite Newman_1966), this is a *raising* operator @@ -276,11 +276,11 @@ By definition, the spin-raising operator satisfies the commutator relation ``[S, \eth`` (where ``S`` is the spin operator, which just multiplies the function by its spin). In terms of the SWSHs, we can write the action of ``\eth`` as ```math - \eth {}_{s}Y_{\ell,m} = \sqrt{(\ell-s)(\ell+s+1)} {}_{s+1}Y_{\ell,m}. + \eth {}_{s}Y_{ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)} {}_{s+1}Y_{ℓ,m}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{\eth f\right\}_{s,\ell,m} = \sqrt{(\ell-s)(\ell+s+1)}\,\left\{f\right\}_{s+1,\ell,m}. +\left\{\eth f\right\}_{s,ℓ,m} = \sqrt{(ℓ-s)(ℓ+s+1)}\,\left\{f\right\}_{s+1,ℓ,m}. ``` See also [`ð̄`](@ref), [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), @@ -305,12 +305,12 @@ By definition, the spin-lowering operator satisfies the commutator relation ``[S \bar{\eth}] = -\bar{\eth}`` (where ``S`` is the spin operator, which just multiplies the function by its spin). In terms of the SWSHs, we can write the action of ``\bar{\eth}`` as ```math -\bar{\eth} {}_{s}Y_{\ell,m} = -\sqrt{(\ell+s)(\ell-s+1)} {}_{s-1}Y_{\ell,m}. +\bar{\eth} {}_{s}Y_{ℓ,m} = -\sqrt{(ℓ+s)(ℓ-s+1)} {}_{s-1}Y_{ℓ,m}. ``` Consequently, the *mode weights* of a function are affected as ```math -\left\{\bar{\eth} f\right\}_{s,\ell,m} -= -\sqrt{(\ell-s)(\ell+s+1)}\,\left\{f\right\}_{s+1,\ell,m}. +\left\{\bar{\eth} f\right\}_{s,ℓ,m} += -\sqrt{(ℓ-s)(ℓ+s+1)}\,\left\{f\right\}_{s+1,ℓ,m}. ``` See also [`ð`](@ref), [`L²`](@ref), [`Lz`](@ref), [`L₊`](@ref), [`L₋`](@ref), [`R²`](@ref), diff --git a/src/utilities/pixelizations.jl b/src/utilities/pixelizations.jl new file mode 100644 index 00000000..a78f30df --- /dev/null +++ b/src/utilities/pixelizations.jl @@ -0,0 +1,241 @@ +@doc raw""" + golden_ratio_spiral_pixels(s, ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. Successive pixels +are separated by the azimuthal angle ``Δϕ = 2π(2-φ)``, and are uniformly distributed in +``\cos θ``. + +This is a common angular pattern of phyllotaxis in plants, found in sunflowers, pine cones, +and artichokes, for example. Wrapped around 𝕊², this is a very reasonable-looking +pixelization, with fairly uniform distance between neighbors, and approximate isotropy. No +two pixels will share the same values of either ``θ`` or ``ϕ``. Also note that no point is +present on either the North or South poles. + +This is also known as the "Fibonacci sphere" or "Fibonacci lattice" — though he had nothing +to do with it; later authors pointed out relationships between his sequence and the golden +ratio. + +The returned quantity is a vector of 2-SVectors providing the spherical coordinates of each +pixel. See also [`golden_ratio_spiral_rotors`](@ref) for the corresponding `Rotor`s. +""" +function golden_ratio_spiral_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T + let π = T(π), φ = T(MathConstants.φ) + N = (ℓₘₐₓ+1)^2 - s^2 + # Note: the formula used here looks different from some sources, + # but just represents spiraling in the opposite direction. + Δϕ = 2π * (2 - φ) + ϕ = (0:N-1) * Δϕ + cosθ = LinRange{T}(1, -1, N+1)[begin:end-1] .- 1/T(N) + [@SVector [acos(cosθ), ϕ] for (cosθ, ϕ) in zip(cosθ, ϕ)] + end +end + +@doc raw""" + golden_ratio_spiral_rotors(s, ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels generated by the golden-ratio spiral. + +See [`golden_ratio_spiral_pixels`](@ref) for more detailed explanation. The quantity +returned by this function is a vector of `Rotor`s providing each pixel. +""" +function golden_ratio_spiral_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T + from_spherical_coordinates.(golden_ratio_spiral_pixels(s, ℓₘₐₓ, T)) +end + +@doc raw""" + sorted_rings(s, ℓₘₐₓ, [T=Float64]) + +Compute locations of a series of rings labelled by ``j ∈ |s|:ℓₘₐₓ`` (analogous +to ``ℓ``), where each ring will contain ``k = 2j+1`` (analogous to ``m``) pixels distributed +evenly around the ring. These rings are then sorted, so that the ring with the most pixels +(``j = ℓₘₐₓ``) is closest to the equator, and the next-largest ring is placed just above or +below the equator (depending on the sign of ``s``), the next just below or above, and so on. +This is generally a fairly good first guess when minimizing the condition number of matrices +used to solve for mode weights from function values. In particular, I use this to +initialize the Minimal algorithm, which is then fed into an optimizer to fine-tune the +positions of the rings. + +This function does not provide the individual pixels; it just provides the colatitude values +of the rings on which the pixels will be placed. The pixels themselves are provided by +[`sorted_ring_pixels`](@ref). +""" +function sorted_rings(s, ℓₘₐₓ, ::Type{T}=Float64) where T + let πo2 = prevfloat(T(π)/2, s) + sort( + collect(LinRange{T}(0, π, 2+ℓₘₐₓ-abs(s)+1))[begin+1:end-1], + lt=(x,y)->(abs(x-πo2) Let ``f(θ, ϕ)`` be a band-limited function such that ``\hat{f}(l, m) = 0`` for ``l +> ≥ b``. We sample the function at the equiangular grid of points ``(θ_i, ϕ_j)``, +> ``i = 0, \ldots, 2b-1``, ``j = 0, \ldots, 2b-1``, where ``θ_i = π i/2b`` and +> ``ϕ_j = π j/b``. + +The returned quantity is a vector of 2-SVectors providing the spherical coordinates of each +pixel. See also [`driscoll_healy_rotors`](@ref) for the corresponding `Rotor`s. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function driscoll_healy_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T + let π = T(π) + b = ℓₘₐₓ + 1 + [ + @SVector [π*i/2b, π*j/b] + for i ∈ 0:(2b-1) + for j ∈ 0:(2b-1) + ] + end +end +driscoll_healy_pixels(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_pixels(0, ℓₘₐₓ, T) + +@doc raw""" + driscoll_healy_rotors(s, ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels given by the [DriscollHealy_1994](@citet) equiangular grid. + +See [`driscoll_healy_pixels`](@ref) for more detailed explanation. The quantity returned by +this function is a vector of `Rotor`s providing each pixel. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function driscoll_healy_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T + from_spherical_coordinates.(driscoll_healy_pixels(s, ℓₘₐₓ, T)) +end +driscoll_healy_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = driscoll_healy_rotors(0, ℓₘₐₓ, T) + + +@doc raw""" + mcewen_wiaux_pixels([s], ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid: + +> We adopt an equiangular sampling of the sphere with sample positions given by ``θ_t = +> \frac{π(2t+1)}{2ℓ_{\max}-1}``, where ``t ∈ \{0, 1, \dotsc, ℓ_\mathrm{max}-1\}`` +> and ``ϕ_p = \frac{2 π p}{2ℓ_\mathrm{max}-1}``, where ``p ∈ \{0, 1, \dotsc, +> 2ℓ_\mathrm{max}-2\}``. In order to extend the ``θ`` domain to ``[0, 2π)`` we +> simply extend the domain of the ``θ`` index to include ``\{ℓ_\mathrm{max}, +> ℓ_\mathrm{max}+1, \dotsc, 2ℓ_\mathrm{max}-1\}``. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function mcewen_wiaux_pixels(s, ℓₘₐₓ, ::Type{T}=Float64) where T + let π = T(π) + [ + @SVector [π*(2t+1) / (2ℓₘₐₓ - 1), 2π*p / (2ℓₘₐₓ - 1)] + for t ∈ 0:(2ℓₘₐₓ - 1) + for p ∈ 0:(2ℓₘₐₓ - 2) + ] + end +end +mcewen_wiaux_pixels(ℓₘₐₓ, ::Type{T}=Float64) where T = mcewen_wiaux_pixels(0, ℓₘₐₓ, T) + +@doc raw""" + mcewen_wiaux_rotors([s], ℓₘₐₓ, [T=Float64]) + +Cover the sphere 𝕊² with pixels given by the [McEwenWiaux_2011](@citet) equiangular grid. + +See [`mcewen_wiaux_pixels`](@ref) for more detailed explanation. The quantity returned by +this function is a vector of `Rotor`s providing each pixel. + +!!! note + The `s` argument is not used in this function, but is included for consistency with + other pixelization functions. +""" +function mcewen_wiaux_rotors(s, ℓₘₐₓ, ::Type{T}=Float64) where T + from_spherical_coordinates.(mcewen_wiaux_pixels(s, ℓₘₐₓ, T)) +end +mcewen_wiaux_rotors(ℓₘₐₓ, ::Type{T}=Float64) where T = mcewen_wiaux_rotors(0, ℓₘₐₓ, T) diff --git a/src/utils.jl b/src/utilities/utils.jl similarity index 87% rename from src/utils.jl rename to src/utilities/utils.jl index 4bc6fae6..06e64c5f 100644 --- a/src/utils.jl +++ b/src/utilities/utils.jl @@ -26,6 +26,8 @@ Base.parent(M::OffsetMat) = M.parent #@propagate_inbounds Base.getindex(M::OffsetMat, ::Colon, j::Int) = parent(M)[:, j-offset2(M)] +# Note that the loggamma and logbinomial functions below are not used directly in the +# package, but are used in sqrtbinomial, which *is* used. loggamma(a, ::Type{T}) where {T<:DoubleFloats.MultipartFloat} = DoubleFloats.loggamma(T(a)) loggamma(a, ::Type{T}) where T = SpecialFunctions.loggamma(T(a)) function logbinomial(n::T, k::T, S=float(T)) where {T<:Integer} @@ -38,7 +40,10 @@ function logbinomial(n::T, k::T, S=float(T)) where {T<:Integer} if k == 1 return log(S(n)) else - return -log1p(S(n)) - loggamma(n - k + one(T), S) - loggamma(k + one(T), S) + loggamma(n + 2one(T), S) + return ( + -log1p(S(n)) - loggamma(n - k + one(T), S) + - loggamma(k + one(T), S) + loggamma(n + 2one(T), S) + ) end end diff --git a/src/weights.jl b/src/utilities/weights.jl similarity index 92% rename from src/weights.jl rename to src/utilities/weights.jl index 4949b8c8..3abcb6b7 100644 --- a/src/weights.jl +++ b/src/utilities/weights.jl @@ -4,7 +4,7 @@ Compute `n` weights for Fejér's first rule, corresponding to `n` evenly spaced nodes from 0 to π inclusive. That is, the nodes are located at ```math -\theta_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. +θ_k = k \frac{π}{n-1} \quad k=0, \ldots, n-1. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). @@ -39,7 +39,7 @@ end Compute `n` weights for Fejér's second rule, corresponding to `n` evenly spaced nodes between 0 and π exclusive. That is, the nodes are located at ```math -\theta_k = k \frac{\pi}{n+1} \quad k=1, \ldots, n. +θ_k = k \frac{π}{n+1} \quad k=1, \ldots, n. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). However, @@ -68,7 +68,7 @@ function fejer2(n, ::Type{T}) where {T<:AbstractFloat} w[2:end] end -function fejer2(n, ::Type{T}=Float64) where {T<:MachineFloat} +function fejer2(n, ::Type{T}=Float64) where {T<:IEEEFloat} # Specialized to "machine" floats; significant reduction in memory and increase in speed v = Vector{T}(undef, (n+1)÷2 + 1) @inbounds begin @@ -92,7 +92,7 @@ end Compute `n` weights for the Clenshaw-Curtis rule, corresponding to `n` evenly spaced nodes from 0 to π inclusive. That is, the nodes are located at ```math -\theta_k = k \frac{\pi}{n-1} \quad k=0, \ldots, n-1. +θ_k = k \frac{π}{n-1} \quad k=0, \ldots, n-1. ``` This function uses [Waldvogel's method](@cite Waldvogel_2006). @@ -122,7 +122,7 @@ function clenshaw_curtis(n, ::Type{T}) where {T<:AbstractFloat} [w; w[1]] end -function clenshaw_curtis(n, ::Type{T}=Float64) where {T<:MachineFloat} +function clenshaw_curtis(n, ::Type{T}=Float64) where {T<:IEEEFloat} # Specialized to "machine" floats; significant reduction in memory and increase in speed nmod2 = mod(n-1, 2) w₀ᶜᶜ = inv(T((n-1)^2 - 1 + nmod2)) diff --git a/src/wigner/recurrence.jl b/src/wigner/recurrence.jl new file mode 100644 index 00000000..e1c7e41c --- /dev/null +++ b/src/wigner/recurrence.jl @@ -0,0 +1,270 @@ +# 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) = ifelse(m ≥ 0, 1, -1) + +# Eq. (7) in Gumerov and Duraiswami (2015) +# ϵ(m) = (m ≥ 0 ? (-1)^m : 1) +ϵ(m) = ifelse(m > 0 && isodd(m), -1, 1) + + +@doc raw""" + recurrence_step1!(H⁰) + +Initialize the Wigner matrix `H⁰` for the recurrence relations. This only sets the values +`H⁰[0,0]=1`. + +Note that `H⁰` can be any `AbstractWignerMatrix` with integer indices. In particular, it +can be a `D` matrix or a `d` matrix. +""" +function recurrence_step1!(H⁰::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT} + @inbounds let ℓ=ℓ(H⁰) + if ℓ == 0 + H⁰[0, 0] = 1 + else + error("Trying to initialize ℓ=$ℓ; only ℓ=0 is supported.") + end + end + H⁰ +end + +@doc raw""" + recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + +Compute the values of ``H^{ℓ}_{0,m}``, from the values of ``H^{ℓ-1}_{0,m}`` for all +``m \geq 0``. + +""" +function recurrence_step2!( + Hˡ::AbstractWignerMatrix{IT, NT}, Hˡ⁻¹::AbstractWignerMatrix{IT, NT2}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, NT2, T} + @assert ℓ(Hˡ⁻¹) == ℓ(Hˡ) - 1 + # Note that in this step only, we use notation derived from Xing et al., denoting the + # 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 + # The ℓ>1 branch would try to access invalid indices of H⁰; if we treat those + # elements as zero, we can simplify that branch to just the following much + # simpler code anyway. So fundamentally, this branch is the same as the other + # branch. + Hˡ[0, 0] = cosβ + Hˡ[0, 1] = sinβ / √2 + elseif ℓ > 1 + b̄ₗ = √(T(ℓ-1)/ℓ) + Hˡ[0, 0] = cosβ * Hˡ⁻¹[0, 0] - b̄ₗ * sinβ * Hˡ⁻¹[0, 1] + for m ∈ 1:ℓ-2 + 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 + else + error("Tried to recurse with ℓ=$ℓ; only integer ℓ ≥ 1 is supported.") + end + end + Hˡ +end + +@doc raw""" + recurrence_step3!(Hˡ, Hˡ⁺¹, sinβ, cosβ) + +Compute the values of ``H^{ℓ}_{1,m}``, from the values of ``H^{ℓ+1}_{0,m}`` for all +``m \geq 0``. + +""" +function recurrence_step3!( + Hˡ::AbstractWignerMatrix{IT, NT}, Hˡ⁺¹::AbstractWignerMatrix{IT, NT2}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, NT2, T} + @assert ℓ(Hˡ⁺¹) == ℓ(Hˡ) + 1 + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + if ℓ > 0 && m′ₘₐₓ ≥ 1 + c = 1 / √(ℓ*(ℓ+1)) + for m ∈ 1:ℓ + āₗᵐ = √((ℓ+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 + Hˡ +end + +@doc raw""" + recurrence_step4!(Hˡ, sinβ, cosβ) + +Compute the values of ``H^{ℓ}_{m'+1,m}``, from the values of ``H^{ℓ}_{m',m-1}``, +``H^{ℓ}_{m'-1,m}``, and ``H^{ℓ}_{m',m+1}``, for all ``m' > 1`` and ``m \geq m'``. + +""" +function recurrence_step4!( + Hˡ::AbstractWignerMatrix{IT, NT}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, T} + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 + # 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):ℓ-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 + Hˡ +end + +@doc raw""" + recurrence_step5!(Hˡ, sinβ, cosβ) + +Compute the values of ``H^{ℓ}_{m'-1,m}``, from the values of ``H^{ℓ}_{m',m-1}``, +``H^{ℓ}_{m'+1,m}``, and ``H^{ℓ}_{m',m+1}``, for all ``m' \leq 0`` and ``m > -m'``. + +""" +function recurrence_step5!( + Hˡ::AbstractWignerMatrix{IT, NT}, sinβ::T, cosβ::T +) where {IT<:Signed, NT, T} + @inbounds let √=sqrt∘T, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 + d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) + for m ∈ -(m′-1):ℓ-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 + Hˡ +end + +@doc raw""" + recurrence_step6!(Hˡ) + +Impose the symmetries of the Wigner matrix `Hˡ` to fill in all the values that have not yet +been computed. + +Assuming that `Hˡ` has already been computed as much as possible by the recurrence +relations, this function imposes the symmetries, rather than recalculating terms. +Specifically, the recurrence relations will calculate the terms for all `m`, and `m′ ≥ +abs(m)`, and this function will complete the calculations using the symmetries +```math +\begin{aligned} +H^ℓ_{m′, m} &= H^ℓ_{m, m′}, \\ +H^ℓ_{m′, m} &= H^ℓ_{-m′, -m}. +\end{aligned} +``` + +""" +function recurrence_step6!(Hˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT} + @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + # The idea here is to impose + # Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[-m′, -m] = Hˡ[m′, m] + # without double-counting any entries, and accounting for m′ₘₐₓ. + for m ∈ 1:ℓ + for m′ ∈ -min(m′ₘₐₓ, m):min(m′ₘₐₓ, m) + Hˡ[-m′, -m] = Hˡ[m′, m] + end + for m′ ∈ -min(m′ₘₐₓ, m-1):min(m′ₘₐₓ, m-1) + Hˡ[m, m′] = Hˡ[-m, -m′] = Hˡ[m′, m] + end + end + end + Hˡ +end + + +""" + 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ˡ::AbstractWignerMatrix{IT, NT}) where {IT<:Signed, NT<:Real} + @inbounds let ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + for m ∈ -ℓ:ℓ + for m′ ∈ -m′ₘₐₓ:m′ₘₐₓ + Hˡ[m′, m] *= ϵ(m′) * ϵ(-m) + end + end + end + Hˡ +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ˡ::AbstractWignerMatrix{IT, NT}, eⁱᵅ::NT, eⁱᵞ::NT) where {IT<:Signed, NT<:Complex} + # NOTE: This function will have to be modified to work for Rational indices because the + # phases will not be integer powers; we'll have to incorporate √eⁱᵅ and √eⁱᵞ. + @inbounds let ℓ=ℓ(Hˡ), ℓₘᵢₙ=ℓₘᵢₙ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + ϕᵞ = ComplexPowers(eⁱᵞ) + ϕᵅ = ComplexPowers(eⁱᵅ) + for (m, eⁱᵐᵞ) ∈ zip(ℓₘᵢₙ:ℓ, ϕᵞ) + for (m′, eⁱᵐ′ᵅ) ∈ zip(ℓₘᵢₙ:m′ₘₐₓ, ϕᵅ) + Hˡ[m′, m] *= ϵ(m′) * ϵ(-m) * conj(eⁱᵐ′ᵅ) * conj(eⁱᵐᵞ) + if m′ ≠ 0 + Hˡ[-m′, m] *= ϵ(-m′) * ϵ(-m) * eⁱᵐ′ᵅ * conj(eⁱᵐᵞ) + if m ≠ 0 + Hˡ[-m′, -m] *= ϵ(-m′) * ϵ(m) * eⁱᵐ′ᵅ * eⁱᵐᵞ + end + end + if m ≠ 0 + Hˡ[m′, -m] *= ϵ(m′) * ϵ(m) * conj(eⁱᵐ′ᵅ) * eⁱᵐᵞ + end + end + end + end + Hˡ +end diff --git a/src/wigner/wigner.jl b/src/wigner/wigner.jl new file mode 100644 index 00000000..a67a14a2 --- /dev/null +++ b/src/wigner/wigner.jl @@ -0,0 +1,5 @@ +include("wigner_matrix.jl") +include("wigner_calculator.jl") +include("wigner_H.jl") +include("wigner_H_calculator.jl") +include("recurrence.jl") diff --git a/src/wigner/wigner_H.jl b/src/wigner/wigner_H.jl new file mode 100644 index 00000000..06fb8444 --- /dev/null +++ b/src/wigner/wigner_H.jl @@ -0,0 +1,318 @@ +""" + HWedge{IT, RT, ST} <: AbstractWignerMatrix{IT, RT, ST} + +The ``Hˡ`` matrix is critical to efficient and stable computation of the Wigner ``D`` and +``d`` matrices — in fact, it essentially *is* the ``d`` matrix with signs adjusted to avoid +numerical problems with alternating signs. This gives it additional symmetries that reduce +the amount of data that needs to be stored to about 1/4 of the total ``d`` size. + +The purpose of an `HWedge` is to provide a workspace for the Wigner recurrences that is +efficient, both in terms of the size of memory used, and the implications for vectorization +and threading. Specifically, the data is stored as strictly `Real` values, in contiguous +storage. Indexing is performed efficiently via precomputed row offsets. Once the full +recurrence is done, the data can be used directly — computing phases and symmetry on the fly +— or copied into a full explicit matrix with the appropriate phases. + +The recurrences require ``m`` in the full range from 0 (or 1/2) to ``ℓ``, but ``m'`` only +needs to include the axis ``m'=0`` or 1/2. Thus, we store `m′ₘᵢₙ`and `m′ₘₐₓ` as fields, and +only require enough storage for those ranges. Specifically, an `HWedge` will store elements +in a vector as if they were components of the `Hˡ` matrix: + + [ + Hˡ[m′, m] + for m′ ∈ max(-ℓ, m′ₘᵢₙ):min(ℓ, m′ₘₐₓ) + for m ∈ abs(m′):ℓ + ] + +However, for further efficiency when vectorizing and threading over multiple rotors, the +data is stored as a 1-dimensional vector, though it can be indexed as if it were a +three-dimensional array, with the first dimension indexing `Nᵣ` different rotors, and the +second dimension indexing `m′`, and the third dimension indexing `m`. Thus, this object can +be indexed as `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value for rotor index `iᵣ`, and matrix element +`(m′, m)`. + +Because of this complicated layout, the constructor is fairly restrictive, but will do all +the allocation needed. To avoid multiple allocations, it is advisable to first construct an +instance with the maximum `ℓ` value that will be needed, and then change the `ℓ` field as +needed to compute different orders. That is, if `H isa HWedge`, then `H.ℓ = new_ell` can be +used to change the current order being computed. The constructor starts out with the +smallest `ℓ` value possible (0 or 1/2), which is the natural choice for recurrence. + +!!! warning "Thread safety" + + The `HWedge` object is not thread safe. Its internal storage is intended to be changed + by different threads, but the code must be designed carefully to avoid accessing the + same memory locations from different threads. In particular, note that changing the `ℓ` + field changes internal storage, and is *not* thread-safe. It may be better to allocate + separate `HWedge` objects for each thread. + +""" +mutable struct HWedge{IT, RT<:Real, ST} <: AbstractWignerMatrix{IT, RT, ST} + const parent::ST + const row_index::FixedSizeVectorDefault{Int} + const Nᵣ::Int + const maxℓ::IT + const maxm′ₘₐₓ::IT + const minm′ₘᵢₙ::IT + ℓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + function HWedge(Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT} + HWedge(Float64, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + end + function HWedge(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ) where {IT, RT<:Real} + if Nᵣ < 1 + error("Number of rotors Nᵣ=$Nᵣ must be at least 1.") + end + validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + + # Set up storage for the biggest these values will ever be + parent = FixedSizeVector{RT}(undef, Nᵣ * HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ)) + row_index = FixedSizeVector{Int}(undef, Int(m′ₘₐₓ - m′ₘᵢₙ) + 1) + + # But start out assuming ℓ is the smallest it can be + maxℓ = ℓₘₐₓ + maxm′ₘₐₓ = m′ₘₐₓ + minm′ₘᵢₙ = m′ₘᵢₙ + ℓ = ℓₘᵢₙ(ℓₘₐₓ) + m′ₘₐₓ = min(ℓ, m′ₘₐₓ) + m′ₘᵢₙ = max(-ℓ, m′ₘᵢₙ) + HWedge_row_index!(row_index, Nᵣ, ℓ, m′ₘₐₓ, m′ₘᵢₙ) + + new{IT, RT, typeof(parent)}( + parent, row_index, Nᵣ, maxℓ, maxm′ₘₐₓ, minm′ₘᵢₙ, ℓ, m′ₘₐₓ, m′ₘᵢₙ + ) + end +end + +mₘₐₓ(w::HWedge{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::HWedge{IT}) where {IT} = ℓₘᵢₙ(w) + +row_index(w::HWedge{IT}) where {IT} = w.row_index +row_index(w::HWedge{IT}, m′::IT) where {IT} = row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] +Nᵣ(w::HWedge{IT}) where {IT} = w.Nᵣ +maxℓ(w::HWedge{IT}) where {IT} = w.maxℓ +maxm′ₘₐₓ(w::HWedge{IT}) where {IT} = w.maxm′ₘₐₓ +minm′ₘᵢₙ(w::HWedge{IT}) where {IT} = w.minm′ₘᵢₙ + +function Base.setproperty!(H::HWedge{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + if IIT !== IT + error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") + end + if IT <: Rational && denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end + if ℓ < ℓₘᵢₙ(IT) + error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") + end + if ℓ > maxℓ(H) + error("Cannot set ℓ=$ℓ greater than maxℓ=$(maxℓ(H)).") + end + m′ₘₐₓ = min(ℓ, maxm′ₘₐₓ(H)) + m′ₘᵢₙ = max(-ℓ, minm′ₘᵢₙ(H)) + HWedge_row_index!(row_index(H), Nᵣ(H), ℓ, m′ₘₐₓ, m′ₘᵢₙ) + Base.setfield!(H, :ℓ, ℓ) + Base.setfield!(H, :m′ₘₐₓ, m′ₘₐₓ) + Base.setfield!(H, :m′ₘᵢₙ, m′ₘᵢₙ) + ℓ + else + error("Cannot set property `$s` on HWedge; only `ℓ` is allowed to be changed.") + end +end + +function HWedge_row_index!(row_index, Nᵣ::Int, ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} + index = 1 + for (i, m′) ∈ enumerate(m′ₘᵢₙ:m′ₘₐₓ) + @inbounds row_index[i] = index + index += Nᵣ * (Int(ℓ - abs(m′)) + 1) + end + row_index +end + +function HWedge_size(ℓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where {IT} + let ℓₘᵢₙ = ℓₘᵢₙ(IT) + Int( + (ℓₘᵢₙ - m′ₘᵢₙ) * (2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) + - (ℓₘᵢₙ - m′ₘₐₓ - 1) * (2ℓ - m′ₘₐₓ - ℓₘᵢₙ + 2) + ) ÷ 2 + end +end + +function Base.axes(w::HWedge{IT}) where {IT} + (1:Nᵣ(w), WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) +end + +function Base.checkbounds(::Type{Bool}, w::HWedge, i::Int) + i ≥ 1 && i ≤ length(w) +end +function Base.checkbounds(::Type{Bool}, w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + iᵣ > 0 && iᵣ ≤ Nᵣ(w) && m ≥ abs(m′) && m′ ≥ m′ₘᵢₙ(w) && m′ ≤ m′ₘₐₓ(w) +end + +@propagate_inbounds function Base.getindex(w::HWedge, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HWedge{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] + @inbounds Base.parent(w)[i] +end + +@propagate_inbounds function Base.setindex!(w::HWedge, v, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HWedge{IT}, v, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = @inbounds (iᵣ - 1) + Nᵣ(w) * Int(m - abs(m′)) + row_index(w)[Int(m′ - m′ₘᵢₙ(w)) + 1] + @inbounds Base.parent(w)[i] = v +end + +function Base.show(io::IO, ::MIME"text/plain", H::HWedge{IT, RT, ST}) where {IT, RT, ST} + let ℓ = ℓ(H), m′ₘᵢₙ = m′ₘᵢₙ(H), m′ₘₐₓ = m′ₘₐₓ(H), Nᵣ = Nᵣ(H) + print( + io, + "SphericalFunctions.HWedge{$IT, $RT} for ℓ=$(ℓ) with m′=$(m′ₘᵢₙ:m′ₘₐₓ), ", + "m=abs(m′):$(ℓ), and iᵣ=1:$(Nᵣ) stored in\n", + summary(parent(H)), ", currently using\n" + ) + i = row_index(H)[Int(m′ₘₐₓ - m′ₘᵢₙ) + 1] + Nᵣ * (Int(ℓ - abs(m′ₘₐₓ)) + 1) - 1 + show(io, MIME("text/plain"), parent(H)[begin:i]) + end +end + +# Explicit HWedge index formula, assuming no iᵣ: +# ( +# Int(ℓₘᵢₙ - m′ₘᵢₙ) * Int(2ℓ + m′ₘᵢₙ + ℓₘᵢₙ + 1) +# - +# Int(ℓₘᵢₙ - m′) * Int(2ℓ - abs(m′ + ℓₘᵢₙ - 1) + 2) +# ) ÷ 2 + 1 + + +""" + HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} + +The `HAxis` type represents the ``m'=0``, ``m≥0`` axis of the `Hˡ` matrix used in +calculation of the Wigner `D` and `d` matrices. + +As with [`HWedge`](@ref), the data is stored as a 1-dimensional vector, though it can be +indexed as if it were a two-dimensional array, with the first dimension indexing `Nᵣ` +different rotors, and the second dimension indexing `m` — or alternatively as if it were a +three-dimensional array with the second dimension indexing `m′` and the third indexing `m`. +Thus, this object can be indexed as `Hˡ₀[iᵣ, m]` or `Hˡ[iᵣ, m′, m]` to get the `Hˡ` value +for rotor index `iᵣ`, and matrix element `(m′, m)`. + +""" +mutable struct HAxis{IT, RT} <: AbstractWignerMatrix{IT, RT, FixedSizeVectorDefault{RT}} + const parent::FixedSizeVectorDefault{RT} + const Nᵣ::Int + const maxℓ::IT + ℓ::IT + function HAxis(::Type{RT}, Nᵣ::Int, ℓₘₐₓ::IT) where {IT, RT<:Real} + H = FixedSizeVector{RT}(undef, Nᵣ * (Int(ℓₘₐₓ - ℓₘᵢₙ(IT)) + 1)) + new{IT, RT}(H, Nᵣ, ℓₘₐₓ, ℓₘᵢₙ(IT)) + end +end + +m′ₘₐₓ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) +m′ₘᵢₙ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) +mₘₐₓ(w::HAxis{IT}) where {IT} = ℓ(w) +mₘᵢₙ(w::HAxis{IT}) where {IT} = ℓₘᵢₙ(IT) + +Nᵣ(w::HAxis{IT}) where {IT} = w.Nᵣ +maxℓ(w::HAxis{IT}) where {IT} = w.maxℓ + +function Base.setproperty!(H::HAxis{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + if IIT !== IT + error("Cannot change ℓ from type $IT to type $IIT; they must be the same.") + end + if IT <: Rational && denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end + if ℓ < ℓₘᵢₙ(IT) + error("Cannot set ℓ=$ℓ less than ℓₘᵢₙ=$(ℓₘᵢₙ(IT)).") + end + if ℓ > maxℓ(H) + error("Cannot set ℓ=$ℓ greater than maxℓ=$(maxℓ(H)).") + end + Base.setfield!(H, :ℓ, ℓ) + ℓ + else + error("Cannot set property `$s` on HAxis; only `ℓ` is allowed to be changed.") + end +end + +function Base.checkbounds(::Type{Bool}, w::HAxis, i::Int) + i ≥ 1 && i ≤ length(w) +end +function Base.checkbounds(::Type{Bool}, w::HAxis{IT}, iᵣ::Int, m::IT) where {IT} + iᵣ ≤ Nᵣ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓ(w) +end +function Base.checkbounds(::Type{Bool}, w::HAxis{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + iᵣ ≤ Nᵣ(w) && m′ == ℓₘᵢₙ(w) && ℓₘᵢₙ(w) ≤ m ≤ ℓ(w) +end + +@propagate_inbounds function Base.getindex(w::HAxis, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HAxis{IT}, iᵣ::Int, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m) + throw(BoundsError(w, (iᵣ, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::HAxis{IT}, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] +end + +@propagate_inbounds function Base.setindex!(w::HAxis, v, i::Int) + @boundscheck if !checkbounds(Bool, w, i) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HAxis{IT}, v, iᵣ::Int, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m) + throw(BoundsError(w, (iᵣ, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::HAxis{IT}, v, iᵣ::Int, m′::IT, m::IT) where {IT} + @boundscheck if !checkbounds(Bool, w, iᵣ, m′, m) + throw(BoundsError(w, (iᵣ, m′, m))) + end + i = iᵣ + Nᵣ(w) * Int(m - ℓₘᵢₙ(w)) + @inbounds Base.parent(w)[i] = v +end + +function Base.show(io::IO, ::MIME"text/plain", H::HAxis{IT, RT}) where {IT, RT} + let ℓ = ℓ(H), ℓₘᵢₙ = ℓₘᵢₙ(H), Nᵣ = Nᵣ(H) + print( + io, + "SphericalFunctions.HAxis{$IT, $RT} for ℓ=$(ℓ) with ", + "m=$(ℓₘᵢₙ):$(ℓ), and iᵣ=1:$(Nᵣ)\n", + "Stored in ", + ) + show(io, MIME("text/plain"), parent(H)) + end +end diff --git a/src/wigner/wigner_H_calculator.jl b/src/wigner/wigner_H_calculator.jl new file mode 100644 index 00000000..7518445e --- /dev/null +++ b/src/wigner/wigner_H_calculator.jl @@ -0,0 +1,532 @@ +struct WignerHCalculator{IT, RT<:Real, ST} + h⃗ᵃ::HAxis{IT, RT} + h⃗ᵇ::HAxis{IT, RT} + Hˡ::HWedge{IT, RT, ST} + eⁱᵝ::FixedSizeVectorDefault{Complex{RT}} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + swapH::Base.RefValue{Bool} # h⃗ˡ(w) returns h⃗ᵃ if `false`, otherwise h⃗ᵇ; and vice versa for h⃗ˡ⁺¹(w) + + function WignerHCalculator( + eⁱᵝ::AbstractVector{Complex{RT}}, ℓₘₐₓ::IT, m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ + ) where {IT, RT<:Real} + eⁱᵝ = FixedSizeVector(eⁱᵝ) + Nᵣ = length(eⁱᵝ) + # One of the H matrices will (eventually) be required to store all the coefficients + # for Hˡ⁺¹₀ₘ with non-negative `m` (and that will be strictly necessary), so we give + # it one extra column. Since we may not know which one that will be, we give both + # of them that extra column. + h⃗ᵃ = HAxis(RT, Nᵣ, ℓₘₐₓ+1) + h⃗ᵇ = HAxis(RT, Nᵣ, ℓₘₐₓ+1) + Hˡ = HWedge(RT, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + h⃗ᵇ.ℓ = ℓ(h⃗ᵇ) + 1 + WignerHCalculator(h⃗ᵃ, h⃗ᵇ, Hˡ, eⁱᵝ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, Ref(false)) + end + function WignerHCalculator( + h⃗ᵃ::HAxis{IT, RT}, + h⃗ᵇ::HAxis{IT, RT}, + Hˡ::HWedge{IT, RT, ST}, + eⁱᵝ::FixedSizeVectorDefault{Complex{RT}}, + ℓₘₐₓ::IT, + m′ₘₐₓ::IT, + m′ₘᵢₙ::IT, + swapH::Base.RefValue{Bool} + ) where {IT, RT<:Real, ST} + if !(Nᵣ(h⃗ᵃ) == Nᵣ(h⃗ᵇ) == Nᵣ(Hˡ) == length(eⁱᵝ)) + error( + "Inconsistent Nᵣ values in WignerHCalculator constructor:\n" + * " Nᵣ(h⃗ᵃ)=$(Nᵣ(h⃗ᵃ)), Nᵣ(h⃗ᵇ)=$(Nᵣ(h⃗ᵇ)), Nᵣ(Hˡ)=$(Nᵣ(Hˡ))," + * " length(eⁱᵝ)=$(length(eⁱᵝ))." + ) + end + new{IT, RT, ST}(h⃗ᵃ, h⃗ᵇ, Hˡ, eⁱᵝ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, swapH) + end +end + +consistent_ℓ(w::WignerHCalculator{IT}) where {IT} = ℓ(h⃗ˡ(w)) == ℓ(Hˡ(w)) == ℓ(h⃗ˡ⁺¹(w)) - 1 + +function ℓ(w::WignerHCalculator{IT}) where {IT} + if !consistent_ℓ(w) + error( + "Inconsistent ℓ values in WignerHCalculator:\n" + * " ℓ(h⃗ˡ)=$(ℓ(h⃗ˡ(w))), ℓ(h⃗ˡ⁺¹)=$(ℓ(h⃗ˡ⁺¹(w))), ℓ(Hˡ)=$(ℓ(Hˡ(w)))." + ) + end + ℓ(Hˡ(w)) +end +ℓₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = ℓₘᵢₙ(w.ℓₘₐₓ) +ℓₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.ℓₘₐₓ +m′ₘₐₓ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerHCalculator{IT}) where {IT} = w.m′ₘᵢₙ +Nᵣ(w::WignerHCalculator{IT}) where {IT} = Nᵣ(Hˡ(w)) + +h⃗ˡ(w::WignerHCalculator) = swapH(w) ? w.h⃗ᵇ : w.h⃗ᵃ +h⃗ˡ⁺¹(w::WignerHCalculator) = swapH(w) ? w.h⃗ᵃ : w.h⃗ᵇ +Hˡ(w::WignerHCalculator) = w.Hˡ +eⁱᵝ(w::WignerHCalculator) = w.eⁱᵝ + +function Base.fill!(w::WignerHCalculator{IT, RT}, v::Real) where {IT, RT} + let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), Hˡ = Hˡ(w), v = convert(RT, v) + fill!(parent(h⃗ˡ), v) + fill!(parent(h⃗ˡ⁺¹), v) + fill!(parent(Hˡ), v) + end + w +end + +function swapH(w::WignerHCalculator) + w.swapH[] +end + +function increment_axes!(w::WignerHCalculator) + # The data that is now stored as h⃗ˡ(w) will get swapped below so that it will be + # returned by h⃗ˡ⁺¹(w), so we need to increment its ℓ value twice. + let h⃗ˡ = h⃗ˡ(w) + h⃗ˡ.ℓ = ℓ(h⃗ˡ) + 2 + end + # The data that is now stored as h⃗ˡ⁺¹(w) will get swapped below so that it will be + # returned by h⃗ˡ(w), which will already be correct for the next ℓ value. + w.swapH[] = !w.swapH[] + w +end + +function increment_ℓ!(w::WignerHCalculator) + increment_axes!(w) + Hˡ = Hˡ(w) + Hˡ.ℓ = ℓ(Hˡ) + 1 + w +end + +function fillHˡ₀ₘ!(w::WignerHCalculator{IT}) where {IT} + let h⃗ˡ = h⃗ˡ(w), Hˡ = Hˡ(w) + if ℓ(h⃗ˡ) != ℓ(Hˡ) + error("Cannot fill Hˡ₀ₘ for ℓ=$(ℓ(Hˡ)) from h⃗ˡ for ℓ=$(ℓ(h⃗ˡ)).") + end + # Get the index to the start of the central row, m′ = 0 or 1//2 + iˡ₀₀ = row_index(Hˡ, ℓₘᵢₙ(Hˡ)) + # Figure out how many entries to copy + N = Nᵣ(Hˡ) * (Int(ℓ(Hˡ) - ℓₘᵢₙ(Hˡ)) + 1) + # Now just copy that many entries from h⃗ˡ₀ₘ into row Hˡ₀ₘ + copyto!(parent(Hˡ), iˡ₀₀, parent(h⃗ˡ), 1, N) + end + w +end + +function Base.setproperty!(w::WignerHCalculator{IT}, s::Symbol, ℓ::IIT) where {IT, IIT} + if s === :ℓ + h⃗ˡ(w).ℓ = ℓ + h⃗ˡ⁺¹(w).ℓ = ℓ+1 + Hˡ(w).ℓ = ℓ + ℓ + else + error("Cannot set property `$s` on HWedge; only `ℓ` is allowed to be changed.") + end +end + +function Base.show(io::IO, ::MIME"text/plain", w::WignerHCalculator{IT, RT, ST}) where {IT, RT, ST} + let ℓ = ℓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w), m′ₘₐₓ = m′ₘₐₓ(w), Nᵣ = Nᵣ(w) + print( + io, + "SphericalFunctions.WignerHCalculator{$IT, $RT} for ℓ=$(ℓ) with ", + "m′=$(m′ₘᵢₙ:m′ₘₐₓ) and iᵣ=1:$(Nᵣ) up to ℓₘₐₓ=$(ℓₘₐₓ(w)), ", + "and swapH=$(swapH(w))."#\nStored in ", + ) + # show(io, MIME("text/plain"), Hˡ(w)) + end +end + +function recurrence_step1!(w::WignerHCalculator{IT}) where {IT<:Signed} + let h⃗⁰ = h⃗ˡ(w), ℓ = ℓ(h⃗⁰) + if ℓ ≠ ℓₘᵢₙ(w) + error("recurrence_step1! can only be called for ℓ=$(ℓₘᵢₙ(w)); current ℓ=$ℓ.") + end + parent(h⃗⁰)[:, 1] .= 1 + end + w +end + +function recurrence_step2!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} + let h⃗ˡ = h⃗ˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), eⁱᵝ = eⁱᵝ(w) + if ℓ(h⃗ˡ) < ℓₘᵢₙ(IT) + error( + "recurrence_step2! can only be called for ℓ≥ℓₘᵢₙ=$(ℓₘᵢₙ(IT)); current ℓ=$ℓ." + ) + end + + # Note that in this step only, we use notation derived from (but not the same as) + # Xing et al., denoting the coefficients as b̄ₗ, c̄ₗₘ, d̄ₗₘ, ēₗₘ. In the following + # steps, we will use notation from Gumerov and Duraiswami, who denote their + # different coefficients aₗᵐ, etc. + @inbounds let √=sqrt∘RT, ℓ = ℓ(h⃗ˡ), Nᵣ = Nᵣ(h⃗ˡ) + if ℓ == 0 + # The ℓ>1 branch would try to access invalid indices of H⁰; if we treat + # those elements as zero, we can simplify that branch to just the following + # much simpler code anyway (because we know that h⃗ˡ₀₀=1). So + # fundamentally, this branch is the same as the other branch. + i⁰⁰ = 0 + i⁰¹ = Nᵣ + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + # h⃗ˡ⁺¹[i, 0, 0] = cosβ + # h⃗ˡ⁺¹[i, 0, 1] = sinβ / √2 + h⃗ˡ⁺¹[i⁰⁰ + i] = cosβ + h⃗ˡ⁺¹[i⁰¹ + i] = sinβ / √2 + end + else + b̄ₗ = √(RT(ℓ-1)/ℓ) + i⁰⁰ = 0 + i⁰¹ = Nᵣ + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + # h⃗ˡ⁺¹[i, 0, 0] = cosβ * h⃗ˡ[i, 0, 0] - b̄ₗ * sinβ * h⃗ˡ[i, 0, 1] + h⃗ˡ⁺¹[i⁰⁰ + i] = cosβ * h⃗ˡ[i⁰⁰ + i] - b̄ₗ * sinβ * h⃗ˡ[i⁰¹ + i] + end + for m ∈ 1:ℓ-2 + c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ + d̄ₗₘ = √((ℓ-m)*(ℓ-m-1)) / 2ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + + i⁰ᵐ = Nᵣ * m + i⁰ᵐ⁺¹ = Nᵣ * (m + 1) + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + # h⃗ˡ⁺¹[i, 0, m] = ( + # c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] + # - sinβ * (d̄ₗₘ * h⃗ˡ[i, 0, m+1] - ēₗₘ * h⃗ˡ[i, 0, m-1]) + # ) + h⃗ˡ⁺¹[i⁰ᵐ + i] = ( + c̄ₗₘ * cosβ * h⃗ˡ[i⁰ᵐ + i] + - sinβ * (d̄ₗₘ * h⃗ˡ[i⁰ᵐ⁺¹ + i] - ēₗₘ * h⃗ˡ[i⁰ᵐ⁻¹ + i]) + ) + end + end + let m = ℓ-1 + c̄ₗₘ = √((ℓ+m)*(ℓ-m)) / ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + + i⁰ᵐ = Nᵣ * m + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + # h⃗ˡ⁺¹[i, 0, m] = ( + # c̄ₗₘ * cosβ * h⃗ˡ[i, 0, m] + # - sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1]) + # ) + h⃗ˡ⁺¹[i⁰ᵐ + i] = ( + c̄ₗₘ * cosβ * h⃗ˡ[i⁰ᵐ + i] + - sinβ * (- ēₗₘ * h⃗ˡ[i⁰ᵐ⁻¹ + i]) + ) + end + end + let m = ℓ + ēₗₘ = √((ℓ+m)*(ℓ+m-1)) / 2ℓ + + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + i⁰ᵐ = Nᵣ * m + + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + # h⃗ˡ⁺¹[i, 0, m] = (- sinβ * (- ēₗₘ * h⃗ˡ[i, 0, m-1])) + h⃗ˡ⁺¹[i⁰ᵐ + i] = (- sinβ * (- ēₗₘ * h⃗ˡ[i⁰ᵐ⁻¹ + i])) + end + end + end + end + end + w +end + +function recurrence_step3!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} + let Hˡ = Hˡ(w), h⃗ˡ⁺¹ = h⃗ˡ⁺¹(w), eⁱᵝ = eⁱᵝ(w) + @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), Nᵣ = Nᵣ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ) + if ℓ > 0 && m′ₘₐₓ ≥ 1 + c = 1 / √(ℓ*(ℓ+1)) + + # Precompute base offset for m′=1 row in Hˡ + r¹ = row_index(Hˡ, 1) - 1 + + for m ∈ 1:ℓ + āₗᵐ = √((ℓ+m+1)*(ℓ-m+1)) + b̄ₗ₊₁ᵐ⁻¹ = √((ℓ-m+1)*(ℓ-m+2)) + b̄ₗ₊₁⁻ᵐ⁻¹ = √((ℓ+m+1)*(ℓ+m+2)) + + # Column offsets in Hˡ row 1 and h⃗ˡ⁺¹ row 0 + c¹ᵐ = Nᵣ * Int(m - 1) # Hˡ[i, 1, m] has m′=1, so column is m-abs(1)=m-1 + i¹ᵐ = r¹ + c¹ᵐ + i⁰ᵐ⁺¹ = Nᵣ * (m + 1) + i⁰ᵐ⁻¹ = Nᵣ * (m - 1) + i⁰ᵐ = Nᵣ * m + + for i ∈ 1:Nᵣ + cosβ, sinβ = reim(eⁱᵝ[i]) + # Hˡ[i, 1, m] = -c * ( + # b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m+1] + # + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * h⃗ˡ⁺¹[i, 0, m-1] + # + āₗᵐ * sinβ * h⃗ˡ⁺¹[i, 0, m] + # ) + Hˡ[i¹ᵐ + i] = -c * ( + b̄ₗ₊₁⁻ᵐ⁻¹ * (1 - cosβ) / 2 * h⃗ˡ⁺¹[i⁰ᵐ⁺¹ + i] + + b̄ₗ₊₁ᵐ⁻¹ * (1 + cosβ) / 2 * h⃗ˡ⁺¹[i⁰ᵐ⁻¹ + i] + + āₗᵐ * sinβ * h⃗ˡ⁺¹[i⁰ᵐ + i] + ) + end + end + end + end + end + w +end + +function recurrence_step4!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} + let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) + @inbounds @fastmath let √=sqrt∘RT, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + for m′ ∈ 1:min(ℓ, m′ₘₐₓ)-1 + # Note that the signs of m′ and m are always +1, so we leave them out of the + # calculations of d̄ in this function. + d̄ₗᵐ′ = √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = √((ℓ-m′+1)*(ℓ+m′)) + + # Precompute base offsets for m′-1, m′, m′+1 rows. Note that we subtract 1 + # here because row_index points to the beginning of the desired row, but we + # just want the offset. + rᵐ′⁻¹ = row_index(Hˡ, m′ - 1) - 1 + rᵐ′ = row_index(Hˡ, m′) - 1 + rᵐ′⁺¹ = row_index(Hˡ, m′ + 1) - 1 + + for m ∈ (m′+1):ℓ-1 + d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) + d̄ₗᵐ = √((ℓ-m)*(ℓ+m+1)) + + # Compute column offsets within each row. For row m′, column m has + # offset Nᵣ * (m - abs(m′)). + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′ᵐ⁺¹ = Nᵣ * Int(m + 1 - abs(m′)) + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + + # Final 1D index offsets (0-based for the loop) + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′ᵐ⁺¹ = rᵐ′ + cᵐ′ᵐ⁺¹ + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′+1, m] = ( + # d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] + # - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # + d̄ₗᵐ * Hˡ[i, m′, m+1] + # ) / d̄ₗᵐ′ + Hˡ[iᵐ′⁺¹ᵐ + i] = ( + d̄ₗᵐ′⁻¹ * Hˡ[iᵐ′⁻¹ᵐ + i] + - d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + + d̄ₗᵐ * Hˡ[iᵐ′ᵐ⁺¹ + i] + ) / d̄ₗᵐ′ + end + end + + # Now, we do the m=ℓ case separately, since there is no m+1, so we would get + # out-of-bounds accesses; we just copy the body of the loop above, but + # remove anything that involves m+1. + let m = ℓ + d̄ₗᵐ⁻¹ = √((ℓ-m+1)*(ℓ+m)) + + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′+1, m] = ( + # d̄ₗᵐ′⁻¹ * Hˡ[i, m′-1, m] + # - d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # ) / d̄ₗᵐ′ + Hˡ[iᵐ′⁺¹ᵐ + i] = ( + d̄ₗᵐ′⁻¹ * Hˡ[iᵐ′⁻¹ᵐ + i] + - d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + ) / d̄ₗᵐ′ + end + end + end + end + end + w +end + +function recurrence_step5!(w::WignerHCalculator{IT, RT}) where {IT<:Signed, RT} + let Hˡ = Hˡ(w), eⁱᵝ = eⁱᵝ(w) + @inbounds let √=sqrt∘RT, ℓ=ℓ(Hˡ), m′ₘₐₓ=m′ₘₐₓ(Hˡ), Nᵣ=Nᵣ(Hˡ) + for m′ ∈ 0:-1:-min(ℓ, m′ₘₐₓ)+1 + d̄ₗᵐ′ = sgn(m′) * √((ℓ-m′)*(ℓ+m′+1)) + d̄ₗᵐ′⁻¹ = sgn(m′-1) * √((ℓ-m′+1)*(ℓ+m′)) + + # Precompute base offsets for m′-1, m′, m′+1 rows + rᵐ′⁻¹ = row_index(Hˡ, m′ - 1) - 1 + rᵐ′ = row_index(Hˡ, m′) - 1 + rᵐ′⁺¹ = row_index(Hˡ, m′ + 1) - 1 + + for m ∈ -(m′-1):ℓ-1 + d̄ₗᵐ = sgn(m) * √((ℓ-m)*(ℓ+m+1)) + d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + + # Compute column offsets within each row + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′ᵐ⁺¹ = Nᵣ * Int(m + 1 - abs(m′)) + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′ᵐ⁺¹ = rᵐ′ + cᵐ′ᵐ⁺¹ + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′-1, m] = ( + # d̄ₗᵐ′ * Hˡ[i, m′+1, m] + # + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # - d̄ₗᵐ * Hˡ[i, m′, m+1] + # ) / d̄ₗᵐ′⁻¹ + Hˡ[iᵐ′⁻¹ᵐ + i] = ( + d̄ₗᵐ′ * Hˡ[iᵐ′⁺¹ᵐ + i] + + d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + - d̄ₗᵐ * Hˡ[iᵐ′ᵐ⁺¹ + i] + ) / d̄ₗᵐ′⁻¹ + end + end + let m = ℓ + d̄ₗᵐ⁻¹ = sgn(m-1) * √((ℓ-m+1)*(ℓ+m)) + + cᵐ′⁺¹ᵐ = Nᵣ * Int(m - abs(m′ + 1)) + cᵐ′ᵐ⁻¹ = Nᵣ * Int(m - 1 - abs(m′)) + cᵐ′⁻¹ᵐ = Nᵣ * Int(m - abs(m′ - 1)) + + iᵐ′⁺¹ᵐ = rᵐ′⁺¹ + cᵐ′⁺¹ᵐ + iᵐ′ᵐ⁻¹ = rᵐ′ + cᵐ′ᵐ⁻¹ + iᵐ′⁻¹ᵐ = rᵐ′⁻¹ + cᵐ′⁻¹ᵐ + + for i ∈ 1:Nᵣ + # Hˡ[i, m′-1, m] = ( + # d̄ₗᵐ′ * Hˡ[i, m′+1, m] + # + d̄ₗᵐ⁻¹ * Hˡ[i, m′, m-1] + # ) / d̄ₗᵐ′⁻¹ + Hˡ[iᵐ′⁻¹ᵐ + i] = ( + d̄ₗᵐ′ * Hˡ[iᵐ′⁺¹ᵐ + i] + + d̄ₗᵐ⁻¹ * Hˡ[iᵐ′ᵐ⁻¹ + i] + ) / d̄ₗᵐ′⁻¹ + end + end + end + end + end + w +end + +# function recurrence_step6!(w::WignerHCalculator{IT}, ℓ) where {IT<:Signed} +# let Hˡ = Hˡ(w) +# recurrence_step6!(Hˡ) +# end +# w +# end + +# function recurrence!( +# w::WignerHCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT +# ) where {IT<:Signed, RT, NT<:Complex} +# eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) +# recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ) +# end + +# function recurrence!( +# w::WignerHCalculator{IT, RT, NT}, β::RT, ℓ::IT +# ) where {IT<:Signed, RT, NT<:Real} +# eⁱᵝ = cis(β) +# recurrence!(w, eⁱᵝ, ℓ) +# end + +# function recurrence!( +# w::WignerHCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, +# ℓ::IT +# ) where {IT<:Signed, RT, NT<:Complex} +# _recurrence!(w, eⁱᵝ, ℓ) +# let Hˡ = Hˡ(w) +# convert_H_to_D!(Hˡ, eⁱᵅ, eⁱᵞ) +# Hˡ +# end +# end + +function recurrence!( + w::WignerHCalculator{IT, RT, ST}, ℓ::IT +) where {IT<:Signed, RT, ST} + _recurrence!(w, ℓ) + # let Hˡ = Hˡ(w) + # convert_H_to_d!(Hˡ) + # Hˡ + # end +end + +function _recurrence!( + w::WignerHCalculator{IT, RT, NT}, ℓ::IT +) where {IT<:Signed, RT, NT} + # NOTE: In the comments explaining the recurrence steps below, we use notation with + # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. + + if ℓ < ℓₘᵢₙ(w) || ℓ > ℓₘₐₓ(w) + error( + "Requested ℓ=$(ℓ) is out of bounds [$(ℓₘᵢₙ(w)), $(ℓₘₐₓ(w))] for WignerHCalculator." + ) + end + + let h⃗ˡ=h⃗ˡ(w), h⃗ˡ⁺¹=h⃗ˡ⁺¹(w), Hˡ=Hˡ(w) + + if ℓ == ℓₘᵢₙ(w) + w.ℓ = ℓ + recurrence_step1!(w) # H⁰₀₀ = 1 + fillHˡ₀ₘ!(w) # Record the result in Hˡ (it was computed in h⃗ˡ) + + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ. + recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ + else + if !consistent_ℓ(w) || h⃗ˡ.ℓ ≠ ℓ-1 + # We need to start over, but will only be using the axes, so we only reset them. + h⃗ˡ.ℓ = ℓₘᵢₙ(w) + h⃗ˡ⁺¹.ℓ = ℓₘᵢₙ(w)+1 + + recurrence_step1!(w) # H⁰₀₀ = 1 + recurrence_step2!(w) # H⁰₀ₘ -> H¹₀ₘ + + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ-1 + increment_axes!(w) + recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + end + end + + # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not the + # `if` block above was executed. If not, it was set in a previous call to this + # function, but is currently stored in h⃗ˡ⁺¹ so we have to swap and increment the + # axes first. If so, that block just got us to the same point. + increment_axes!(w) + recurrence_step2!(w) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + + # So far, we've only used h⃗ˡ and h⃗ˡ⁺¹. Now we're ready to start using Hˡ. + Hˡ.ℓ = ℓ + + # The h⃗ˡ and h⃗ˡ⁺¹ are what we need to start the recurrence for Hˡ + fillHˡ₀ₘ!(w) # Copy h⃗ˡ₀ₘ to Hˡ₀ₘ + recurrence_step3!(w) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ + + # # Impose symmetries + # recurrence_step6!(w, ℓ) + + # # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ + # increment_axes!(w) + end + Hˡ + end +end diff --git a/src/wigner/wigner_calculator.jl b/src/wigner/wigner_calculator.jl new file mode 100644 index 00000000..2faeb309 --- /dev/null +++ b/src/wigner/wigner_calculator.jl @@ -0,0 +1,228 @@ +struct WignerCalculator{IT, RT<:Real, NT<:Union{RT, Complex{RT}}} + ℓₘₐₓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + mₘₐₓ::IT + mₘᵢₙ::IT + Hᵃ::Matrix{RT} + Hᵇ::Matrix{RT} + Wˡ::Matrix{NT} + swapH::Base.RefValue{Bool} # w(ℓ) returns (Wˡ, Hᵇ, Hᵃ) if `true`, otherwise (Wˡ, Hᵃ, Hᵇ) + function WignerCalculator( + ℓₘₐₓ::IT, rt::Type{RT}, ::Type{NT}=rt; + m′ₘₐₓ::IT=ℓₘₐₓ, m′ₘᵢₙ::IT=-ℓₘₐₓ, mₘₐₓ::IT=ℓₘₐₓ, mₘᵢₙ::IT=-ℓₘₐₓ + ) where {IT, RT, NT} + if real(NT) ≠ RT + error("RT=$RT is supposed to be the real type of NT=$NT.") + end + validate_index_ranges(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + # One of the H matrices will (eventually) be required to store all the coefficients + # for Hˡ⁺¹₀ₘ with non-negative `m` (and that will be strictly necessary), so we give + # it one extra column. Since we may not know which one that will be, we give both + # of them that extra column. + Hᵃp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + Hᵇp = Matrix{RT}(undef, 1, Int(mₘₐₓ-ℓₘᵢₙ(ℓₘₐₓ))+2) + Wˡp = Matrix{NT}(undef, Int(m′ₘₐₓ-m′ₘᵢₙ)+1, Int(mₘₐₓ-mₘᵢₙ)+1) + new{IT, RT, NT}(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, Hᵃp, Hᵇp, Wˡp, Ref(false)) + end +end + +ℓₘᵢₙ(w::WignerCalculator{IT}) where {IT} = ℓₘᵢₙ(w.ℓₘₐₓ) +ℓₘₐₓ(w::WignerCalculator{IT}) where {IT} = w.ℓₘₐₓ +m′ₘₐₓ(w::WignerCalculator{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::WignerCalculator{IT}) where {IT} = w.m′ₘᵢₙ +mₘₐₓ(w::WignerCalculator{IT}) where {IT} = w.mₘₐₓ +mₘᵢₙ(w::WignerCalculator{IT}) where {IT} = w.mₘᵢₙ + +Hᵃ(w::WignerCalculator) = swapH(w) ? w.Hᵇ : w.Hᵃ +Hᵇ(w::WignerCalculator) = swapH(w) ? w.Hᵃ : w.Hᵇ +Wˡ(w::WignerCalculator) = w.Wˡ + +function Base.fill!(w::WignerCalculator{IT, RT}, v::Real) where {IT, RT} + let Wˡ = Wˡ(w), Hᵃ = Hᵃ(w), Hᵇ = Hᵇ(w) + fill!(Wˡ, eltype(Wˡ)(v)) + fill!(Hᵃ, eltype(Hᵃ)(v)) + fill!(Hᵇ, eltype(Hᵇ)(v)) + end + w +end + +function swapH(w::WignerCalculator) + w.swapH[] +end + +function swapH!(w::WignerCalculator) + w.swapH[] = !w.swapH[] + w +end + +function fillW!(w::WignerCalculator{IT}, ℓ::IT) where {IT} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + @views copyto!(Wˡ[0:0, 0:ℓ], Hˡ[0:0, 0:ℓ]) + w +end + +function (w::WignerCalculator{IT, RT, NT})(ℓ::IT) where {IT, RT, NT} + if IT <: Rational + if denominator(ℓ) ≠ 2 + error("For IT=$IT <: Rational, ℓ=$ℓ must have denominator 2") + end + end + if ℓ < ℓₘᵢₙ(w) || ℓ > ℓₘₐₓ(w) + error( + "ℓ=$ℓ is out of range for this WignerCalculator " * + "(which has ℓₘᵢₙ=$(ℓₘᵢₙ(w)) and ℓₘₐₓ=$(ℓₘₐₓ(w)))." + ) + end + let ℓₘᵢₙ=ℓₘᵢₙ(w), m′ₘₐₓ=min(ℓ, m′ₘₐₓ(w)), m′ₘᵢₙ=max(-ℓ, m′ₘᵢₙ(w)), + mₘₐₓ=min(ℓ, mₘₐₓ(w)), mₘᵢₙ=max(-ℓ, mₘᵢₙ(w)), + Wˡ=WignerMatrix(Wˡ(w), ℓ; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ), + Hˡ=WignerMatrix(Hᵃ(w), ℓ; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ, mₘᵢₙ=ℓₘᵢₙ), + Hˡ⁺¹=WignerMatrix(Hᵇ(w), ℓ+1; m′ₘₐₓ=ℓₘᵢₙ, m′ₘᵢₙ=ℓₘᵢₙ, mₘₐₓ=mₘₐₓ+1, mₘᵢₙ=ℓₘᵢₙ) + + Wˡ, Hˡ, Hˡ⁺¹ + end +end + +function WignerDCalculator( + ℓₘₐₓ::IT, ::Type{RT}; + mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min +) where {IT, RT<:Real} + NT = complex(RT) + WignerCalculator(ℓₘₐₓ, RT, NT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) +end + +function WignerdCalculator( + ℓₘₐₓ::IT, ::Type{RT}; + mp_max::IT=ℓₘₐₓ, mp_min::IT=-ℓₘₐₓ, m_max::IT=ℓₘₐₓ, m_min::IT=-ℓₘₐₓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min +) where {IT, RT<:Real} + WignerCalculator(ℓₘₐₓ, RT, RT; m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) +end + +function recurrence_step1!(w::WignerCalculator{IT}) where {IT<:Signed} + ℓ = ℓₘᵢₙ(w) + W⁰, H⁰, H¹ = w(ℓ) + recurrence_step1!(H⁰) + fillW!(w, ℓ) + w +end + +function recurrence_step2!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ⁻¹, Hˡ⁻¹, Hˡ = w(ℓ-1) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step2!(Hˡ, Hˡ⁻¹, sinβ, cosβ) + w +end + +function recurrence_step3!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step3!(Wˡ, Hˡ⁺¹, sinβ, cosβ) + w +end + +function recurrence_step4!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step4!(Wˡ, sinβ, cosβ) + w +end + +function recurrence_step5!(w::WignerCalculator{IT}, eⁱᵝ, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + cosβ, sinβ = reim(eⁱᵝ) + recurrence_step5!(Wˡ, sinβ, cosβ) + w +end + +function recurrence_step6!(w::WignerCalculator{IT}, ℓ) where {IT<:Signed} + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + recurrence_step6!(Wˡ) + w +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, α::RT, β::RT, γ::RT, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Complex} + eⁱᵅ, eⁱᵝ, eⁱᵞ = cis(α), cis(β), cis(γ) + recurrence!(w, eⁱᵅ, eⁱᵝ, eⁱᵞ, ℓ, skip_ℓ_recurrence) +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, β::RT, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Real} + eⁱᵝ = cis(β) + recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, eⁱᵅ::Complex{RT}, eⁱᵝ::Complex{RT}, eⁱᵞ::Complex{RT}, + ℓ::IT, skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Complex} + _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_D!(Wˡ, eⁱᵅ, eⁱᵞ) + Wˡ +end + +function recurrence!( + w::WignerCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT<:Real} + _recurrence!(w, eⁱᵝ, ℓ, skip_ℓ_recurrence) + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + convert_H_to_d!(Wˡ) + Wˡ +end + +function _recurrence!( + w::WignerCalculator{IT, RT, NT}, eⁱᵝ::Complex{RT}, ℓ::IT, + skip_ℓ_recurrence::Bool=false +) where {IT<:Signed, RT, NT} + # NOTE: In the comments explaining the recurrence steps below, we use notation with + # ℓₘᵢₙ=0 for simplicity, but this sequence may work for ℓₘᵢₙ=1//2 as well. + + if ℓ == ℓₘᵢₙ(w) + recurrence_step1!(w) # H⁰₀₀ = 1 + fillW!(w, ℓ) # Record the result in Wˡ + + # Now, to leave `w` in a good state for the next ℓ, compute H¹₀ₘ and swap. + recurrence_step2!(w, eⁱᵝ, ℓ+1) # H⁰₀ₘ -> H¹₀ₘ + swapH!(w) + else + if !skip_ℓ_recurrence + recurrence_step1!(w) # H⁰₀₀ = 1 + + for ℓ′ in ℓₘᵢₙ(w)+1:ℓ + recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ⁻¹₀ₘ -> Hˡ₀ₘ + swapH!(w) # Prepare for the next iteration of ℓ′ + end + end + + # Do one more step of the recurrence to get Hˡ⁺¹₀ₘ, regardless of whether or not we + # asked to skip the ℓ recurrence. If we did, the user is responsible for having + # already set Hˡ₀ₘ correctly; if we didn't, we just set it in the loop above. + let ℓ′ = ℓ+1 + recurrence_step2!(w, eⁱᵝ, ℓ′) # Hˡ₀ₘ -> Hˡ⁺¹₀ₘ + end + + fillW!(w, ℓ) # Copy Hˡ₀ₘ to Wˡ₀ₘ + recurrence_step3!(w, eⁱᵝ, ℓ) # Hˡ⁺¹₀ₘ -> Hˡ₁ₘ + recurrence_step4!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₋₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₊₁ₘ + recurrence_step5!(w, eⁱᵝ, ℓ) # Hˡₘ′ₘ₋₁, Hˡₘ′₊₁ₘ, Hˡₘ′ₘ₊₁ -> Hˡₘ′₋₁ₘ + + # Impose symmetries + recurrence_step6!(w, ℓ) + + # Swap the H matrices once more so that the current Hˡ⁺¹ is the next loop's Hˡ + swapH!(w) + end + + Wˡ, Hˡ, Hˡ⁺¹ = w(ℓ) + Wˡ + +end diff --git a/src/wigner/wigner_matrix.jl b/src/wigner/wigner_matrix.jl new file mode 100644 index 00000000..4e471f5b --- /dev/null +++ b/src/wigner/wigner_matrix.jl @@ -0,0 +1,468 @@ +import Base: @propagate_inbounds + +""" + AbstractWignerMatrix{IT, NT, ST} + +Abstract base type for Wigner rotation‐matrix objects of a specific ``ℓ`` value. +- `IT` is the index type (an `Integer` or half-integer `Rational`), governing the allowed + ranges of `m′` and `m`. +- `NT` is the number type (e.g., `ComplexF64` for D-matrices or `Float64` for d-matrices). +- `ST` is the storage type (typically `Matrix{NT}`, but other `AbstractMatrix{NT}` storage + can be used). + +The basic concrete subtypes (`WignerDMatrix`, `WignerdMatrix`) default to storing their data +in a `Matrix{NT}` and implement the usual `size`, `getindex` and `setindex!` so that one can +use `w[m′,m]`. Specifically, these indices can be negative or positive, and must obey +`abs(m′) ≤ m′ₘₐₓ` and `abs(m) ≤ ℓ`. + +# Methods + +Methods defined for `AbstractWignerMatrix` objects include: +- `parent(w)`: the underlying data array. +- `ℓ(w)` or `ell(w)`: the value of ``ℓ``. +- `m′ₘₐₓ(w)` or `mpmax(w)`: the maximum value of ``m′``. +- `mₘₐₓ(w)` or `mmax(w)`: the maximum value of ``m``. +- `ℓₘᵢₙ(w)` or `ellmin(w)`: the minimum value of ``ℓ``, which is either 0 or 1//2. +- `isrational(w)`: whether the indices are rational (i.e., half‐integer). +- `size(w)`: the size of the underlying data array. +- `length(w)`: the length of the underlying data array. +- `getindex(w, i)`: get the value at index `i` in the underlying data array. +- `getindex(w, m′, m)`: get the value at index `(m′, m)`. +- `setindex!(w, v, i)`: set the value at index `i` in the underlying data array to `v`. +- `setindex!(w, v, m′, m)`: set the value at index `(m′, m)`. +- `axes(w)`: the axes of the matrix, which are 2-tuples of ranges for the `m′` and `m` + indices. + +# Implementation + +Any new subtypes of `AbstractWignerMatrix` should inherit from this type and re-implement any of the +methods mentioned above that are not appropriate for the new type. Specifically, the +default implementations assume that subtypes store the fields +- `parent::ST`: the underlying storage type. +- `ℓ::IT`: the value of ``ℓ``. +- `m′ₘₐₓ::IT`: the maximum value of ``m′``. + +For example, if the parent Matrix is not stored as the `parent` field, then the `parent(w)` +method should be re-implemented to return the correct parent object. The `getindex` and +`setindex!` +""" +abstract type AbstractWignerMatrix{IT<:Union{Integer,Rational}, NT, ST<:AbstractArray{NT}} <: AbstractMatrix{NT} end + +### General methods for all AbstractWignerMatrix types + +Base.parent(w::AbstractWignerMatrix) = w.parent + +ℓ(w::AbstractWignerMatrix{IT}) where {IT} = w.ℓ +ℓₘᵢₙ(::IT) where {IT} = ℓₘᵢₙ(IT) +ℓₘᵢₙ(::Type{IT}) where {IT} = error("No method defined for `ℓₘᵢₙ(::Type{$IT})`.") +ℓₘᵢₙ(::Type{IT}) where {IT<:Integer} = zero(IT) +ℓₘᵢₙ(::Type{IT}) where {IT<:Rational} = IT(1//2) +ℓₘᵢₙ(::AbstractWignerMatrix{IT}) where {IT} = ℓₘᵢₙ(IT) + +m′ₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = w.m′ₘₐₓ +m′ₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = w.m′ₘᵢₙ +mₘₐₓ(w::AbstractWignerMatrix{IT}) where {IT} = w.mₘₐₓ +mₘᵢₙ(w::AbstractWignerMatrix{IT}) where {IT} = w.mₘᵢₙ + +const ell = ℓ +const ellmin = ℓₘᵢₙ +const mpmax = m′ₘₐₓ +const mpmin = m′ₘᵢₙ +const mmax = mₘₐₓ +const mmin = mₘᵢₙ + +isrational(::AbstractWignerMatrix{IT}) where {IT<:Integer} = false +isrational(::AbstractWignerMatrix{IT}) where {IT<:Rational} = true + +Base.eltype(::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = NT +Base.size(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = size(parent(w)) +Base.length(w::AbstractWignerMatrix{IT, NT, ST}) where {IT, NT, ST} = length(parent(w)) + +struct WignerRange{T<:Union{Integer,Rational}} <: AbstractUnitRange{T} + start::T + stop::T + + WignerRange(r::UnitRange{T}) where {T} = new{T}(r.start, r.stop) +end +@inline Base.axes(r::WignerRange) = (axes1(r),) +@inline axes1(r::WignerRange) = WignerRange(r.start:r.stop) +if VERSION < v"1.8.2" + Base.axes1(r::WignerRange) = axes1(r) +end +Base.inds2string(inds::NTuple{2, WignerRange}) = + string( + "(", inds[1].start, ":", inds[1].stop, ")", + "×", + "(", inds[2].start, ":", inds[2].stop, ")" + ) +Base.inds2string(inds::Tuple{UnitRange, WignerRange, WignerRange}) = + string( + "(", inds[1].start, ":", inds[1].stop, ")", + "×", + "(", inds[2].start, ":", inds[2].stop, ")", + "×", + "(", inds[3].start, ":", inds[3].stop, ")" + ) +Base.firstindex(r::WignerRange) = 1 +Base.lastindex(r::WignerRange) = length(r) +function Base.getindex(v::WignerRange, i::Bool) + throw(ArgumentError("invalid index: $i of type Bool")) +end +@propagate_inbounds function Base.getindex(v::WignerRange{T}, i::Integer) where {T} + val = convert(T, v.start + (i - oneunit(i))) + @boundscheck (i>0 && val <= v.stop && val >= v.start) || throw(BoundsError(v, i)) + val +end + +function Base.axes(w::AbstractWignerMatrix{IT}) where {IT} + (WignerRange(m′ₘᵢₙ(w):m′ₘₐₓ(w)), WignerRange(mₘᵢₙ(w):mₘₐₓ(w))) +end + + +# We don't have to override Base.show; most of its machinery works just fine, except that +# printing the data itself gets screwed up when the indices are Rational. So we override +# this core part of the printing machinery to just print the parent matrix as usual. The +# only other thing show really does is add a "summary" line, for which the `axes` and thence +# `inds2string` methods above are used. +Base.print_array(io::IO, w::AbstractWignerMatrix{<:Rational}) = Base.print_array(io, parent(w)) + +@propagate_inbounds function Base.getindex(w::AbstractWignerMatrix, i::Int) + @boundscheck if i<1 || i>length(w) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] +end +@propagate_inbounds function Base.getindex(w::AbstractWignerMatrix{IT}, m′::IT, m::IT) where {IT} + @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) + throw(BoundsError(w, (m′, m))) + end + @inbounds Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] +end + +@propagate_inbounds function Base.setindex!(w::AbstractWignerMatrix, v, i::Int) + @boundscheck if i<1 || i>length(w) + throw(BoundsError(w, i)) + end + @inbounds Base.parent(w)[i] = v +end +@propagate_inbounds function Base.setindex!(w::AbstractWignerMatrix{IT}, v, m′::IT, m::IT) where {IT} + @boundscheck if m′ ∉ axes(w, 1) || m ∉ axes(w, 2) + throw(BoundsError(w, (m′, m))) + end + @inbounds Base.parent(w)[Int(m′-m′ₘᵢₙ(w))+1, Int(m-mₘᵢₙ(w))+1] = v +end + + +function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT, mₘₐₓ::IT, mₘᵢₙ::IT) where + {IT<:Union{Signed, Rational}} + if IT <: Rational + if ( + denominator(ℓₘₐₓ) ≠ 2 || + denominator(m′ₘᵢₙ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 || + denominator(mₘᵢₙ) ≠ 2 || denominator(mₘₐₓ) ≠ 2 + ) + error( + "For IT=$IT <: Rational, indices must have denominator 2:\n" + * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ, mₘᵢₙ=$mₘᵢₙ, mₘₐₓ=$mₘₐₓ.\n" + * "If you want an integer index type, use IT=<:Integer instead." + ) + end + end + + # ℓₘₐₓ must be at least as big as ℓₘᵢₙ(ℓₘₐₓ) + if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") + end + + # The m′ and m ranges must be ordered correctly + if m′ₘₐₓ < m′ₘᵢₙ + error("m′ₘₐₓ=$m′ₘₐₓ is less than m′ₘᵢₙ=$m′ₘᵢₙ.") + end + if mₘₐₓ < mₘᵢₙ + error("mₘₐₓ=$mₘₐₓ is less than mₘᵢₙ=$mₘᵢₙ.") + end + + # The m′ and m values must bracket ℓₘᵢₙ + if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type, $IT.") + end + if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type, $IT.") + end + if mₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("mₘᵢₙ=$mₘᵢₙ is too large for this index type, $IT.") + end + if mₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("mₘₐₓ=$mₘₐₓ is too small for this index type, $IT.") + end + + # The m′ and m values must be in range for ℓₘₐₓ + if abs(m′ₘᵢₙ) > ℓₘₐₓ + error("|m′ₘᵢₙ|=|$m′ₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(m′ₘₐₓ) > ℓₘₐₓ + error("|m′ₘₐₓ|=|$m′ₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(mₘᵢₙ) > ℓₘₐₓ + error("|mₘᵢₙ|=|$mₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(mₘₐₓ) > ℓₘₐₓ + error("|mₘₐₓ|=|$mₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + +end + +function validate_index_ranges(ℓₘₐₓ::IT, m′ₘₐₓ::IT, m′ₘᵢₙ::IT) where + {IT<:Union{Signed, Rational}} + if IT <: Rational + if ( + denominator(ℓₘₐₓ) ≠ 2 || + denominator(m′ₘᵢₙ) ≠ 2 || denominator(m′ₘₐₓ) ≠ 2 + ) + error( + "For IT=$IT <: Rational, indices must have denominator 2:\n" + * "\tℓₘₐₓ=$ℓₘₐₓ, m′ₘᵢₙ=$m′ₘᵢₙ, m′ₘₐₓ=$m′ₘₐₓ.\n" + * "If you want an integer index type, use IT=<:Integer instead." + ) + end + end + + # ℓₘₐₓ must be at least as big as ℓₘᵢₙ(ℓₘₐₓ) + if ℓₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("ℓₘₐₓ=$ℓₘₐₓ must be non-negative.") + end + + # The m′ range must be ordered correctly + if m′ₘₐₓ < m′ₘᵢₙ + error("m′ₘₐₓ=$m′ₘₐₓ is less than m′ₘᵢₙ=$m′ₘᵢₙ.") + end + + # The m′ values must bracket ℓₘᵢₙ + if m′ₘᵢₙ > ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘᵢₙ=$m′ₘᵢₙ is too large for this index type, $IT.") + end + if m′ₘₐₓ < ℓₘᵢₙ(ℓₘₐₓ) + error("m′ₘₐₓ=$m′ₘₐₓ is too small for this index type, $IT.") + end + + # The m′ values must be in range for ℓₘₐₓ + if abs(m′ₘᵢₙ) > ℓₘₐₓ + error("|m′ₘᵢₙ|=|$m′ₘᵢₙ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + if abs(m′ₘₐₓ) > ℓₘₐₓ + error("|m′ₘₐₓ|=|$m′ₘₐₓ| is too large for ℓₘₐₓ=$ℓₘₐₓ.") + end + +end + + +""" + WignerMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + +General concrete subtype of [`AbstractWignerMatrix`](@ref) for Wigner rotation matrices, +which can include D-matrices (when `NT` is complex) or d-matrices (when `NT` is real). + +In general, the storage type `ST` can be any `AbstractMatrix{NT}`, but should be 1-based. +That is, the storage should generally be either a `Matrix` or a view. That matrix will +represent a rectangular array of values representing some or all of the Wigner matrix for a +specific ``ℓ`` value. The first dimension corresponds to the `m′` index, and the second +dimension corresponds to the `m` index. The allowed ranges of `m′` and `m` are governed by +the fields `m′ₘₐₓ`, `m′ₘᵢₙ`, `mₘₐₓ`, and `mₘᵢₙ`, which must satisfy +```math +\begin{aligned} +-ℓₘₐₓ &≤ m′ₘᵢₙ ≤ ℓₘᵢₙ ≤ m′ₘₐₓ ≤ ℓₘₐₓ, \\ +-ℓₘₐₓ &≤ mₘᵢₙ ≤ ℓₘᵢₙ ≤ mₘₐₓ ≤ ℓₘₐₓ, +\end{aligned} +``` +where `ℓₘᵢₙ` is either 0 or 1//2 depending on whether `IT` is an integer or rational type. + +""" +struct WignerMatrix{IT, NT, ST} <: AbstractWignerMatrix{IT, NT, ST} + parent::ST + ℓ::IT + m′ₘₐₓ::IT + m′ₘᵢₙ::IT + mₘₐₓ::IT + mₘᵢₙ::IT +end + +function WignerMatrix( + parent::ST, ℓ::IT; + mp_max::IT=ℓ, mp_min::IT=-ℓ, m_max::IT=ℓ, m_min::IT=-ℓ, + m′ₘₐₓ::IT=mp_max, m′ₘᵢₙ::IT=mp_min, mₘₐₓ::IT=m_max, mₘᵢₙ::IT=m_min +) where {IT, NT, ST<:AbstractMatrix{NT}} + validate_index_ranges(ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) + s₁, s₂ = size(parent) + if s₁ < Int(m′ₘₐₓ - m′ₘᵢₙ + 1) + error( + "The extent of the first dimension in the input data must be at least " + * "m′ₘₐₓ-m′ₘᵢₙ+1=$m′ₘₐₓ-$m′ₘᵢₙ+1=$(Int(m′ₘₐₓ - m′ₘᵢₙ + 1)); it is $s₁." + ) + end + if s₂ < Int(mₘₐₓ - mₘᵢₙ + 1) + error( + "The extent of the second dimension in the input data must be at least " + * "mₘₐₓ-mₘᵢₙ+1=$mₘₐₓ-$mₘᵢₙ+1=$(Int(mₘₐₓ - mₘᵢₙ + 1)); it is $s₂." + ) + end + WignerMatrix{IT, NT, ST}(parent, ℓ, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ) +end + + +""" + WignerDMatrix{IT, RT, ST} + +Type alias for [`WignerMatrix`](@ref) with complex number type `Complex{RT}`. +Represents Wigner D-matrices (complex rotation matrices). + +# Example +```julia +D = WignerDMatrix(ComplexF64, 2) # Creates WignerMatrix with NT=ComplexF64 +""" +const WignerDMatrix{IT, RT, ST} = WignerMatrix{IT, Complex{RT}, ST} where {IT, RT<:Real, ST<:AbstractMatrix{Complex{RT}}} + +const WignerdMatrix{IT, RT, ST} = WignerMatrix{IT, RT, ST} where {IT, RT<:Real, ST<:AbstractMatrix{RT}} + +# Constructors for WignerDMatrix (complex) +function WignerDMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{Complex{RT}}} + WignerMatrix(parent, ℓ; kwargs...) +end +function WignerDMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{RT}} + error( + "WignerDMatrix only supports complex types; the input type is $RT.\n" + * "Perhaps you meant to use WignerdMatrix?\n" + ) +end +function WignerDMatrix(::Type{Complex{RT}}, ℓ::IT, m′ₘₐₓ::IT=ℓ; kwargs...) where {RT<:Real, IT} + parent = Matrix{Complex{RT}}(undef, Int(m′ₘₐₓ - (-m′ₘₐₓ) + 1), Int(2ℓ + 1)) + WignerMatrix(parent, ℓ; m′ₘₐₓ=m′ₘₐₓ, m′ₘᵢₙ=-m′ₘₐₓ, kwargs...) +end + +# Constructors for WignerdMatrix (real) +function WignerdMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{RT}} + WignerMatrix(parent, ℓ; kwargs...) +end +function WignerdMatrix(parent::ST, ℓ::IT; kwargs...) where {IT, RT<:Real, ST<:AbstractMatrix{Complex{RT}}} + error( + "WignerdMatrix only supports real types; the input type is Complex{$RT}.\n" + * "Perhaps you meant to use WignerDMatrix?" + ) +end +function WignerdMatrix(::Type{RT}, ℓ::IT, m′ₘₐₓ::IT=ℓ; kwargs...) where {RT<:Real, IT} + parent = Matrix{RT}(undef, Int(m′ₘₐₓ - (-m′ₘₐₓ) + 1), Int(2ℓ + 1)) + WignerMatrix(parent, ℓ; m′ₘₐₓ=m′ₘₐₓ, m′ₘᵢₙ=-m′ₘₐₓ, kwargs...) +end + + +@testitem "WignerMatrix" begin + import SphericalFunctions: WignerDMatrix, WignerdMatrix, + parent, ell, mpmax, mpmin, mmax, mmin, m′ₘₐₓ, m′ₘᵢₙ, mₘₐₓ, mₘᵢₙ, ℓₘᵢₙ + + # Check that mixed-up types throw an error + @test_throws "WignerDMatrix only supports complex types" WignerDMatrix(rand(Float64, 3, 3), 1) + @test_throws "WignerdMatrix only supports real types" WignerdMatrix(rand(ComplexF64, 3, 3), 1) + @test_throws "WignerDMatrix only supports complex types" WignerDMatrix(rand(Float64, 2, 2), 1//2) + @test_throws "WignerdMatrix only supports real types" WignerdMatrix(rand(ComplexF64, 2, 2), 1//2) + + # Check that a negative ℓ value throws an error + @test_throws "ℓₘₐₓ=-1 must be non-negative." WignerDMatrix(rand(ComplexF64, 3, 3), -1) + @test_throws "ℓₘₐₓ=-1 must be non-negative." WignerdMatrix(rand(Float64, 3, 3), -1) + @test_throws "ℓₘₐₓ=-1//2 must be non-negative." WignerDMatrix(rand(ComplexF64, 2, 2), -1//2) + @test_throws "ℓₘₐₓ=-1//2 must be non-negative." WignerdMatrix(rand(Float64, 2, 2), -1//2) + + # Check that a non-half-integer ℓ value throws an error + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 3, 3), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 3, 3), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 2, 2), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 2, 2), 1//3) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 3, 3), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 3, 3), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerDMatrix(rand(ComplexF64, 2, 2), 2//2) + @test_throws "For IT=Rational{Int64} <: Rational, indices must have denominator 2:" WignerdMatrix(rand(Float64, 2, 2), 2//2) + + #for ℓ ∈ Any[collect(0:8); collect(1//2:15//2)] + ℓₘₐₓ = 2 + encode(ℓ, m′, m) = (ℓ+ℓₘₐₓ) + (m′+ℓₘₐₓ)*(4ℓₘₐₓ+1) + (m+ℓₘₐₓ)*(4ℓₘₐₓ+1)^2 + for ℓ ∈ Any[collect(0:ℓₘₐₓ); collect(1//2:(ℓₘₐₓ+1//2))] + mₘ = ℓ + + # These tests are old; the input array can be larger than necessary now. + # # Check that ℓ < m′ₘₐₓ and ℓ ≠ mₘₐₓ throw errors + # @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) + # @test_throws "greater than 0 and less than or equal to 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+2, Int(2ℓ)+1), ℓ) + # @test_throws "in the input data must be 2ℓ+1=" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) + # @test_throws "in the input data must be 2ℓ+1=" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+2), ℓ) + + # # Check that the input is at least as big as needed for the given ℓ + @test_throws "The extent of the first dimension" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) + @test_throws "The extent of the first dimension" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+0, Int(2ℓ)+1), ℓ) + @test_throws "The extent of the second dimension" WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + @test_throws "The extent of the second dimension" WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, Int(2ℓ)+0), ℓ) + + # Check that a mismatch between integer/half-integer throws an error + if ℓ>0 && ℓ isa Int + @test_throws "The extent of the first dimension" WignerDMatrix(rand(ComplexF64, 2ℓ, 2ℓ+1), ℓ) + @test_throws "The extent of the first dimension" WignerdMatrix(rand(Float64, 2ℓ, 2ℓ+1), ℓ) + elseif ℓ isa Rational + @test_throws "The extent of the first dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ), Int(2ℓ+1)), ℓ) + @test_throws "The extent of the first dimension" WignerdMatrix(rand(Float64, Int(2ℓ), Int(2ℓ+1)), ℓ) + end + @test_throws "The extent of the second dimension" WignerDMatrix(rand(ComplexF64, Int(2ℓ+1), Int(2ℓ)), ℓ) + @test_throws "The extent of the second dimension" WignerdMatrix(rand(Float64, Int(2ℓ+1), Int(2ℓ)), ℓ) + + # Check that a data array with a dimension of 0 extent throws an error. + @test_throws r"The extent of the second dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, Int(2ℓ)+1, 0), ℓ) + @test_throws r"The extent of the first dimension.*; it is 0." WignerDMatrix(Array{ComplexF64}(undef, 0, Int(2ℓ)+1), ℓ) + @test_throws r"The extent of the second dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, Int(2ℓ)+1, 0), ℓ) + @test_throws r"The extent of the first dimension.*; it is 0." WignerdMatrix(Array{Float64}(undef, 0, Int(2ℓ)+1), ℓ) + + for m′ₘ ∈ ℓₘᵢₙ(ℓ):ℓ + # Make a big, dumb array full of the explicit indices. + data = [ + encode(ℓ, m′, m) + for m′ ∈ -m′ₘ:m′ₘ, m ∈ -mₘ:mₘ + ] + # Check that indexing works as expected. + for (WignerMatrixType, NT) ∈ ((WignerDMatrix, ComplexF64), (WignerdMatrix, Float64)) + w = WignerMatrixType(NT.(data), ℓ; m′ₘₐₓ=m′ₘ, m′ₘᵢₙ=-m′ₘ, mₘₐₓ=mₘ, mₘᵢₙ=-mₘ) + @test Base.parent(w) == data + @test ell(w) == ℓ + @test mpmax(w) == m′ₘ + @test mmax(w) == ℓ + @test mpmin(w) == -mpmax(w) + @test mmin(w) == -mmax(w) + for m ∈ -mₘ:mₘ + for m′ ∈ -m′ₘ:m′ₘ + @test w[m′, m] == encode(ℓ, m′, m) + end + end + end + end + + for m′ₘ ∈ ℓₘᵢₙ(ℓ):ℓ + for WignerMatrixType ∈ (WignerDMatrix, WignerdMatrix) + data = rand( + WignerMatrixType<:WignerDMatrix ? ComplexF64 : Float64, + Int(2m′ₘ)+1, Int(2mₘ)+1 + ) + w = WignerMatrixType(data, ℓ; m′ₘₐₓ=m′ₘ, m′ₘᵢₙ=-m′ₘ, mₘₐₓ=mₘ, mₘᵢₙ=-mₘ) + + # Check that the data array is stored correctly. + @test Base.parent(w) == data + @test ell(w) == ℓ + @test m′ₘₐₓ(w) == m′ₘ + @test mₘₐₓ(w) == ℓ + @test m′ₘᵢₙ(w) == -m′ₘₐₓ(w) + @test mₘᵢₙ(w) == -mₘₐₓ(w) + + # The Julia docs say that the `axes` function should + # > Return a tuple of `AbstractUnitRange{<:Integer}` of valid indices. + # > The axes should be their own axes, that is `axes.(axes(A),1) == + # > axes(A)` should be satisfied. + # https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array + @test typeof(axes(w)) <: NTuple{2, AbstractUnitRange} + @test axes.(axes(w),1) == axes(w) + end + end + end +end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 00000000..c00d978f --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,26 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" +FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FastDifferentiation = "eb9bf01b-bf85-4b60-bf87-ee5de06c00be" +FastTransforms = "057dd010-8810-581a-b7be-e3fc3b93f78c" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +Quaternionic = "0756cd96-85bf-4b6f-a009-b5012ea7a443" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SphericalFunctions = "af6d55de-b1f7-4743-b797-0829a72cf84e" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" + +[sources] +SphericalFunctions = {path = ".."} diff --git a/test/conventions/NIST_DLMF.jl b/test/conventions/NIST_DLMF.jl new file mode 100644 index 00000000..18a7062b --- /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_{ℓ, m}\left(θ,ϕ\right) + = + \left(\frac{(ℓ-m)!(2ℓ+1)}{4π(ℓ+m)!}\right)^{1/2} + \mathsf{e}^{imϕ} + \mathsf{P}_{ℓ}^{m}\left(\cos θ\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 diff --git a/test/conventions/boyle2016.jl b/test/conventions/boyle2016.jl new file mode 100644 index 00000000..8fd50735 --- /dev/null +++ b/test/conventions/boyle2016.jl @@ -0,0 +1,183 @@ +@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 + import SphericalFunctions: Deprecated + 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(Deprecated.WignerDrange(ℓₘₐₓ)) + ] + 𝔇2 = Deprecated.D_matrices(R, ℓₘₐₓ) + @test 𝔇1 ≈ 𝔇2 + end + end + +end # @testitem "WignerDElement" diff --git a/test/conventions/edmonds.jl b/test/conventions/edmonds.jl new file mode 100644 index 00000000..88a9e3c0 --- /dev/null +++ b/test/conventions/edmonds.jl @@ -0,0 +1,183 @@ +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. 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 + +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 + 𝒟^{(j)}_{m',m}(α, β, γ). +``` + +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}(β). +``` + +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 + using SphericalFunctions: Deprecated + + Random.seed!(1234) + const T = Float64 + const ℓₘₐₓ = 3 + ϵₐ = 8eps(T) + ϵᵣ = 20eps(T) + + # Tests for Y(ℓ, m, θ, ϕ) + for θ ∈ βrange(T, 3) + if abs(sin(θ)) ≤ eps(T) + continue + end + + 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=ϵᵣ + end + end + end + + # Compare to SphericalFunctions + let s=0 + Y = Deprecated.ₛ𝐘(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 + + # Tests for 𝒟(j, m′, m, α, β, γ) + let ϵₐ=√ϵᵣ, ϵᵣ=√ϵᵣ, 𝒟=Edmonds.𝒟 + for α ∈ αrange(T) + for β ∈ βrange(T) + if abs(sin(β)) ≤ eps(T) + continue + end + + for γ ∈ γrange(T) + D = Deprecated.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 diff --git a/test/conventions/goldbergetal.jl b/test/conventions/goldbergetal.jl index 2714a6f9..e5a61700 100644 --- a/test/conventions/goldbergetal.jl +++ b/test/conventions/goldbergetal.jl @@ -22,7 +22,7 @@ import .NaiveFactorials: ❗ Eq. (3.9) of [Goldberg et al.](@cite GoldbergEtAl_1967), implementing ```math - D^j_{m',m}(\alpha, \beta, \gamma). + D^j_{m',m}(α, β, γ). ``` """ function D(j, m′, m, α, β, γ) @@ -63,7 +63,7 @@ end Eq. (3.1) of [Goldberg et al.](@cite GoldbergEtAl_1967), implementing ```math - {}_sY_{\ell,m}(\theta, \phi). + {}_sY_{ℓ,m}(θ, ϕ). ``` Note that there is a difference in conventions between the ``Y`` of Goldberg et al. and @@ -112,6 +112,7 @@ end # @testmodule GoldbergEtAl @testitem "GoldbergEtAl conventions" setup=[Utilities, GoldbergEtAl] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -133,9 +134,9 @@ end # @testmodule GoldbergEtAl end end - # Compare to SphericalHarmonics Y + # Compare to SphericalFunctions Y for s ∈ -ℓₘₐₓ:ℓₘₐₓ - Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + Y₁ = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] Y₂ = [(-1)^m * Y(s, ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ end @@ -147,7 +148,7 @@ end # @testmodule GoldbergEtAl for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/lal.jl b/test/conventions/lal.jl deleted file mode 100644 index 106fae00..00000000 --- a/test/conventions/lal.jl +++ /dev/null @@ -1,205 +0,0 @@ -@testmodule LAL begin - - """ - Reproduces the XLALSpinWeightedSphericalHarmonic function from the LALSuite C library: - https://lscsoft.docs.ligo.org/lalsuite/lal/_spherical_harmonics_8c_source.html#l00042 - """ - function LALSpinWeightedSphericalHarmonic( - 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 - -end # module LAL diff --git a/test/conventions/mathematica.jl b/test/conventions/mathematica.jl new file mode 100644 index 00000000..fe665efc --- /dev/null +++ b/test/conventions/mathematica.jl @@ -0,0 +1,4 @@ +raw""" + + +""" diff --git a/test/conventions/ninja.jl b/test/conventions/ninja.jl deleted file mode 100644 index 937a9a14..00000000 --- a/test/conventions/ninja.jl +++ /dev/null @@ -1,83 +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 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 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 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/conventions/sakurai.jl b/test/conventions/sakurai.jl index d767c7ea..493d516a 100644 --- a/test/conventions/sakurai.jl +++ b/test/conventions/sakurai.jl @@ -6,17 +6,51 @@ The conclusion here is that Sakurai's Yₗᵐ(θ, ϕ) is the same as ours, but h - On p. 154 he says that "a rotation operation affects the physical system itself, ..., while the coordinate axes remain *unchanged*." -- On p. 156 he poses "``|\alpha\rangle_R = \mathcal{D}(R) | \alpha \rangle``, where - ``|\alpha\rangle_R`` and ``|\alpha \rangle`` stand for the kets of the rotated and +- On p. 156 he poses "``|α\rangle_R = 𝒟(R) | α \rangle``, where + ``|α\rangle_R`` and ``|α \rangle`` stand for the kets of the rotated and original system, respectively." -- On p. 157 he says "``\mathcal{D}(\hat{\mathbf{n}}, d\phi) = 1 - i\left( \frac{\mathbf{J} - \cdot \hat{\mathbf{n}}} {\hbar} \right) d\phi``" +- On p. 157 he says "``𝒟(\hat{𝐧}, dϕ) = 1 - i\left( \frac{𝐉 + \cdot \hat{𝐧}} {\hbar} \right) dϕ``" - On p. 173 he defines his Euler angles in the same way as Quaternionic. -- On p. 192 he defines "``\mathcal{D}^{(j)}_{m',m}(R) = \langle j,m'| \exp \left( - \frac{-i\mathbf{J} \cdot \hat{\mathbf{n}} \phi} {\hbar} \right) |j, m\rangle``". +- On p. 192 he defines "``𝒟^{(j)}_{m',m}(R) = \langle j,m'| \exp \left( + \frac{-i𝐉 \cdot \hat{𝐧} ϕ} {\hbar} \right) |j, m\rangle``". - On p. 194 he gives the expression in terms of Euler angles. - On p. 223 he gives an explicit formula for ``d``. -- On p. 203 he relates ``\mathcal{D} to Y_{\ell}^m$ (note the upper index of ``m``). +- On p. 203 he relates ``𝒟 to Y_{ℓ}^m$ (note the upper index of ``m``). + + +Below (1.6.14), we find the translation operator acts as +``\mathscr{T}_{dx'} α(x') = α(x' - dx')``. Then Eq. +(1.6.32) +```math +\mathscr{T}_{dx'} = 1 - i p\, dx', +``` +for infinitesimal ``dx'``. Eq. (1.7.17) gives the momentum operator +as ``p α(x') = -i \partial_{x'} α(x')``. Combining these, +we can verify consistency: +```math +\mathscr{T}_{dx'} α(x') += +α(x' - dx') += +α(x') - \partial_{x'}\, α(x')\, dx', +``` +which is exactly what we expect from Taylor expanding ``α(x' - +dx')``. + + +```math +\begin{aligned} +f\left(𝐑\right) +&\to +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ +&\approx +f\left(𝐑\right) + ϵ \left. \frac{d}{dϵ} \right|_{ϵ=0} +f\left(e^{-ϵ 𝐮/2}𝐑\right) \\ +&= +f\left(𝐑\right) - i ϵ L_𝐮 f\left(𝐑\right) +``` + """ @testmodule Sakurai begin @@ -32,7 +66,7 @@ import .NaiveFactorials: ❗ Eqs. (3.5.50)-(3.5.51) of [Sakurai](@cite Sakurai_1994), p. 194, implementing ```math - \mathcal{D}^{(j)}_{m',m}(\alpha, \beta, \gamma). + 𝒟^{(j)}_{m',m}(α, β, γ). ``` See also [`d`](@ref) for Sakurai's version the Wigner d-function. @@ -44,10 +78,10 @@ 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). + d^{(j)}_{m',m}(β). ``` See also [`𝒟`](@ref) for Sakurai's version the Wigner D-function. @@ -91,7 +125,7 @@ end Eqs. (3.6.51) of [Sakurai](@cite Sakurai_1994), p. 203, implementing ```math - Y_{\ell}^m(\theta, \phi). + Y_{ℓ}^m(θ, ϕ). ``` """ function Y(ℓ, m, θ, ϕ) @@ -115,6 +149,7 @@ end # @testmodule Sakurai @testitem "Sakurai conventions" setup=[Utilities, Sakurai] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -145,7 +180,7 @@ end # @testmodule Sakurai # Compare to SphericalFunctions let s=0 - Y = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) + Y = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)]) i = 1 for ℓ in 0:ℓₘₐₓ for m in -ℓ:ℓ @@ -162,7 +197,7 @@ end # @testmodule Sakurai for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/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 diff --git a/test/conventions/thorne.jl b/test/conventions/thorne.jl new file mode 100644 index 00000000..8250b3b7 --- /dev/null +++ b/test/conventions/thorne.jl @@ -0,0 +1,91 @@ +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^{ℓ,m}(θ, ϕ). +``` +""" +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 + using SphericalFunctions: Deprecated + + 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 = Deprecated.ₛ𝐘(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 diff --git a/test/conventions/torresdelcastillo.jl b/test/conventions/torresdelcastillo.jl index f9666c59..f660ba02 100644 --- a/test/conventions/torresdelcastillo.jl +++ b/test/conventions/torresdelcastillo.jl @@ -19,7 +19,7 @@ import .NaiveFactorials: ❗ Eq. (2.52) of [Torres del Castillo](@cite TorresDelCastillo_2003), implementing ```math - D^l_{m',m}(\phi, \theta, \chi). + D^l_{m',m}(ϕ, θ, \chi). ``` """ function D(l, m′, m, ϕ, θ, χ) @@ -47,7 +47,7 @@ end The equation following Eq. (2.53) of [Torres del Castillo](@cite TorresDelCastillo_2003), implementing ```math - {}_sY_{j,m}(\theta, \phi). + {}_sY_{j,m}(θ, ϕ). ``` """ function Y(s, j, m, θ, ϕ) @@ -75,7 +75,7 @@ end Second equation below Eq. (2.53) of [Torres del Castillo](@cite TorresDelCastillo_2003), implementing ```math - d^l_{m',m}(\theta). + d^l_{m',m}(θ). ``` """ function d(l, m′, m, θ) @@ -115,6 +115,7 @@ end # @testmodule TorresDelCastillo @testitem "TorresDelCastillo conventions" setup=[Utilities, TorresDelCastillo] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -127,7 +128,7 @@ end # @testmodule TorresDelCastillo for θ ∈ βrange(T) for ϕ ∈ αrange(T) for s ∈ -ℓₘₐₓ:ℓₘₐₓ - Y₁ = ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] + Y₁ = Deprecated.ₛ𝐘(s, ℓₘₐₓ, T, [from_spherical_coordinates(θ, ϕ)])[1,:] Y₂ = [Y(s, ℓ, m, θ, ϕ) for ℓ ∈ abs(s):ℓₘₐₓ for m ∈ -ℓ:ℓ] @test Y₁ ≈ Y₂ atol=ϵₐ rtol=ϵᵣ end @@ -139,7 +140,7 @@ end # @testmodule TorresDelCastillo for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/conventions/varshalovich.jl b/test/conventions/varshalovich.jl new file mode 100644 index 00000000..5d38e576 --- /dev/null +++ b/test/conventions/varshalovich.jl @@ -0,0 +1,351 @@ +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'}(α, β, γ). +``` + +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'}(β). +``` + +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 + +""" + 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 + + +@testitem "Varshalovich conventions" setup=[Utilities, Varshalovich] begin + using Random + using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated + + 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 = Deprecated.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 + + 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 diff --git a/test/conventions/wigner.jl b/test/conventions/wigner.jl index 26aca927..363a9faa 100644 --- a/test/conventions/wigner.jl +++ b/test/conventions/wigner.jl @@ -19,7 +19,7 @@ with respect to ours. For example, note that the position of ``z'`` is independ which appears to represent a final rotation about the ``z'`` axis. In our convention, this rotation would be described by the final Euler angle, γ. -On the other hand, on page 156, if ``𝔇^{(\ell)}`` obeys the representation-composition +On the other hand, on page 156, if ``𝔇^{(ℓ)}`` obeys the representation-composition property, then {α, β, γ} represents the rotation {α, 0, 0}∘{0, β, 0}∘{0, 0, γ}, which is the same as our convention. @@ -49,7 +49,7 @@ conjugation of the D function, which is consistent with our convention. # Eq. (15.8) of [Wigner](@cite Wigner_1959), implementing # ```math -# D^\ell_{m',m}(\alpha, \beta, \gamma). +# D^ℓ_{m',m}(α, β, γ). # ``` # """ # function D(ℓ, m′, m, α, β, γ) @@ -62,7 +62,7 @@ conjugation of the D function, which is consistent with our convention. Eq. (15.27) of [Wigner](@cite Wigner_1959), implementing ```math - D^{(j)}(\alpha, \beta, \gamma)_{\mu',\mu}. + D^{(j)}(α, β, γ)_{\mu',\mu}. ``` """ function D(j, μ′, μ, α, β, γ) @@ -104,6 +104,7 @@ end # @testmodule Wigner @testitem "Wigner conventions" setup=[Utilities, Wigner] begin using Random using Quaternionic: from_spherical_coordinates + using SphericalFunctions: Deprecated Random.seed!(1234) const T = Float64 @@ -116,7 +117,7 @@ end # @testmodule Wigner for α ∈ αrange(T) for β ∈ βrange(T) for γ ∈ γrange(T) - D = D_matrices(α, β, γ, ℓₘₐₓ) + D = Deprecated.D_matrices(α, β, γ, ℓₘₐₓ) i = 1 for j in 0:ℓₘₐₓ for m′ in -j:j diff --git a/test/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 diff --git a/test/associated_legendre.jl b/test/deprecated/associated_legendre.jl similarity index 73% rename from test/associated_legendre.jl rename to test/deprecated/associated_legendre.jl index 4a61e5c5..945c508f 100644 --- a/test/associated_legendre.jl +++ b/test/deprecated/associated_legendre.jl @@ -1,4 +1,5 @@ @testitem "Associated Legendre Functions" setup=[Utilities] begin + import SphericalFunctions: Deprecated using OffsetArrays nmax = 1_000 @@ -8,17 +9,17 @@ ϵ = eps(T) P̄ = fill(T(NaN), min_length) P̄′ = Vector{T}(undef, min_length) - recursion_coefficients = ALFRecursionCoefficients(nmax, T) + recursion_coefficients = Deprecated.ALFRecursionCoefficients(nmax, T) for β in βrange(T) expiβ = cis(β) # Make sure we're checking the size of P̄ against nmax - @test_throws ArgumentError ALFcompute!(P̄, expiβ, nmax+1, recursion_coefficients) + @test_throws ArgumentError Deprecated.ALFcompute!(P̄, expiβ, nmax+1, recursion_coefficients) - ALFcompute!(P̄, expiβ, nmax, recursion_coefficients) - ALFcompute!(P̄′, expiβ, nmax) - P̄″ = ALFcompute(expiβ, nmax) + Deprecated.ALFcompute!(P̄, expiβ, nmax, recursion_coefficients) + Deprecated.ALFcompute!(P̄′, expiβ, nmax) + P̄″ = Deprecated.ALFcompute(expiβ, nmax) @test eltype(P̄″) === eltype(P̄) offset = -1 diff --git a/test/indexing.jl b/test/deprecated/indexing.jl similarity index 67% rename from test/indexing.jl rename to test/deprecated/indexing.jl index 25588d29..c6ec75ae 100644 --- a/test/indexing.jl +++ b/test/deprecated/indexing.jl @@ -1,5 +1,5 @@ @testitem "WignerHrange" begin - import SphericalFunctions: WignerHrange + import SphericalFunctions: Deprecated ell_max = 16 r1(mp_max, ell_max) = hcat([ [ell, mp, m] for ell in 0:ell_max @@ -7,11 +7,11 @@ for m in abs(mp):ell ]...)' for ell_max in 0:ell_max - a = WignerHrange(ell_max) # Implicitly, mp_max=ell_max + a = Deprecated.WignerHrange(ell_max) # Implicitly, mp_max=ell_max b = r1(ell_max, ell_max) @test a == b for mp_max in 0:ell_max - a = WignerHrange(ell_max, mp_max) + a = Deprecated.WignerHrange(ell_max, mp_max) b = r1(mp_max, ell_max) @test a == b end @@ -19,24 +19,24 @@ end @testitem "WignerHsize" begin - import SphericalFunctions: WignerHsize, WignerHrange + import SphericalFunctions: Deprecated ell_max = 16 for i in -5:-1 - @test WignerHsize(i) == 0 + @test Deprecated.WignerHsize(i) == 0 for j in -5:5 - @test WignerHsize(i, j) == 0 + @test Deprecated.WignerHsize(i, j) == 0 end end for ell_max in 0:ell_max - @test WignerHsize(ell_max) == size(WignerHrange(ell_max, ell_max), 1) + @test Deprecated.WignerHsize(ell_max) == size(Deprecated.WignerHrange(ell_max, ell_max), 1) for mp_max in ell_max - @test WignerHsize(ell_max, mp_max) == size(WignerHrange(ell_max, mp_max), 1) + @test Deprecated.WignerHsize(ell_max, mp_max) == size(Deprecated.WignerHrange(ell_max, mp_max), 1) end end end @testitem "WignerHindex" begin - import SphericalFunctions: WignerHrange, WignerHindex + import SphericalFunctions: Deprecated ell_max = 16 ell_max_slow = ell_max ÷ 2 function fold_H_indices(ell, mp, m) @@ -56,24 +56,24 @@ end end for ell_max_i in 0:ell_max_slow - r = WignerHrange(ell_max_i) + r = Deprecated.WignerHrange(ell_max_i) for ell in 0:ell_max_i for mp in -ell:ell for m in -ell:ell - i = WignerHindex(ell, mp, m) + i = Deprecated.WignerHindex(ell, mp, m) @test r[i, :] == fold_H_indices(ell, mp, m) end end end for mp_max in 0:ell_max_i - r = WignerHrange(ell_max_i, mp_max) + r = Deprecated.WignerHrange(ell_max_i, mp_max) for ell in 0:ell_max_i for mp in -ell:ell for m in -ell:ell if abs(mp) > mp_max && abs(m) > mp_max continue end - i = WignerHindex(ell, mp, m, mp_max) + i = Deprecated.WignerHindex(ell, mp, m, mp_max) @test r[i, :] == fold_H_indices(ell, mp, m) end end @@ -83,7 +83,7 @@ end end @testitem "WignerDrange" begin - import SphericalFunctions: WignerDrange + import SphericalFunctions: Deprecated ell_max = 16 ell_max_slow = ell_max ÷ 2 function r2(ell_min, mp_max, ell_max) @@ -98,11 +98,11 @@ end for ell_max in 0:ell_max_slow÷2 let ell_min = 0 - a = WignerDrange(ell_max) # Implicitly, mp_max=ell_max + a = Deprecated.WignerDrange(ell_max) # Implicitly, mp_max=ell_max b = r2(ell_min, ell_max, ell_max) @test a == b for mp_max in 0:ell_max - a = WignerDrange(ell_max, mp_max) + a = Deprecated.WignerDrange(ell_max, mp_max) b = r2(ell_min, mp_max, ell_max) @test a == b end @@ -111,13 +111,13 @@ end end @testitem "WignerDsize" begin - import SphericalFunctions: WignerDsize, WignerDrange + import SphericalFunctions: Deprecated ell_max = 16 for ell_max in 0:ell_max let ell_min = 0 for mp_max in 0:ell_max - a = WignerDsize(ell_max, mp_max) - b = size(WignerDrange(ell_max, mp_max), 1) + a = Deprecated.WignerDsize(ell_max, mp_max) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -125,8 +125,8 @@ end for ell_max in 0:ell_max let ell_min = 0 for mp_max in 0:ell_max - a = WignerDsize(ell_max, mp_max) - b = size(WignerDrange(ell_max, mp_max), 1) + a = Deprecated.WignerDsize(ell_max, mp_max) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -134,9 +134,9 @@ end for ell_max in 0:ell_max let ell_min = 0 for mp_max in [ell_max] - a = WignerDsize(ell_max, mp_max) + a = Deprecated.WignerDsize(ell_max, mp_max) # a = WignerDsize_ellmin(ell_min, ell_max) - b = size(WignerDrange(ell_max, mp_max), 1) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -144,9 +144,9 @@ end for ell_max in 0:ell_max let ell_min = 0 for mp_max in [ell_max] - a = WignerDsize(ell_max, mp_max) + a = Deprecated.WignerDsize(ell_max, mp_max) # a = WignerDsize(ell_max) - b = size(WignerDrange(ell_max, mp_max), 1) + b = size(Deprecated.WignerDrange(ell_max, mp_max), 1) @test a == b end end @@ -154,35 +154,35 @@ end end @testitem "WignerDindex" begin - import SphericalFunctions: WignerDrange, WignerDindex + import SphericalFunctions: Deprecated ell_max = 16 ell_max_slow = ell_max ÷ 2 for ellmax in 0:ell_max_slow - r = WignerDrange(ellmax) + r = Deprecated.WignerDrange(ellmax) for ell in 0:ellmax for mp in -ell:ell for m in -ell:ell - i = WignerDindex(ell, mp, m) + i = Deprecated.WignerDindex(ell, mp, m) @test r[i, :] == [ell, mp, m] end end end let ell_min = 0 - r = WignerDrange(ellmax) + r = Deprecated.WignerDrange(ellmax) for ell in ell_min:ellmax for mp in -ell:ell for m in -ell:ell - i = WignerDindex(ell, mp, m) + i = Deprecated.WignerDindex(ell, mp, m) @test r[i, :] == [ell, mp, m] end end end for mp_max in 0:ellmax - r = WignerDrange(ellmax, mp_max) + r = Deprecated.WignerDrange(ellmax, mp_max) for ell in ell_min:ellmax for mp in -min(ell, mp_max):min(ell, mp_max) for m in -ell:ell - i = WignerDindex(ell, mp, m, mp_max) + i = Deprecated.WignerDindex(ell, mp, m, mp_max) @test r[i, :] == [ell, mp, m] end end @@ -193,7 +193,7 @@ end end @testitem "Yrange" begin - import SphericalFunctions: Yrange + import SphericalFunctions: Deprecated ell_max = 16 function r3(ell_min, ell_max) a = [ @@ -205,7 +205,7 @@ end end for ell_max in 0:ell_max for ell_min in 0:ell_max - a = Yrange(ell_min, ell_max) + a = Deprecated.Yrange(ell_min, ell_max) b = r3(ell_min, ell_max) @test a == b end @@ -213,68 +213,68 @@ end end @testitem "Ysize" begin - import SphericalFunctions: Ysize, Yrange + import SphericalFunctions: Deprecated ell_max = 16 for ell_max in 0:ell_max for ell_min in 0:ell_max - a = Ysize(ell_min, ell_max) - b = size(Yrange(ell_min, ell_max))[1] + a = Deprecated.Ysize(ell_min, ell_max) + b = size(Deprecated.Yrange(ell_min, ell_max))[1] @test a == b end end end @testitem "deduce_limits" begin - import SphericalFunctions: Ysize, deduce_limits + import SphericalFunctions: Deprecated ell_max = 16 for ℓmax in 0:4096 for ℓmin in 0:min(2, ℓmax) - deduced = deduce_limits(Ysize(ℓmin, ℓmax), nothing) + deduced = Deprecated.deduce_limits(Deprecated.Ysize(ℓmin, ℓmax), nothing) @test deduced == (ℓmin, ℓmax) end end for ℓmax in 0:ell_max for ℓmin in [0] - deduced = deduce_limits(Ysize(ℓmin, ℓmax)) + deduced = Deprecated.deduce_limits(Deprecated.Ysize(ℓmin, ℓmax)) @test deduced == (ℓmin, ℓmax) end end for ℓmax in 0:ell_max for ℓmin in 0:ℓmax - deduced = deduce_limits(Ysize(ℓmin, ℓmax), ℓmin) + deduced = Deprecated.deduce_limits(Deprecated.Ysize(ℓmin, ℓmax), ℓmin) @test deduced == (ℓmin, ℓmax) end end for ℓmax in 1:ell_max - prev_size = Ysize(ℓmax-1) - this_size = Ysize(ℓmax) + prev_size = Deprecated.Ysize(ℓmax-1) + this_size = Deprecated.Ysize(ℓmax) if abs(prev_size - this_size) > 1 mid_size = (prev_size + this_size) ÷ 2 - @test_throws ErrorException deduce_limits(mid_size) + @test_throws ErrorException Deprecated.deduce_limits(mid_size) end end end @testitem "Yindex" setup=[Utilities] begin - import SphericalFunctions: Yrange, Yindex + import SphericalFunctions: Deprecated ell_max = 16 for ell_max in 0:ell_max let ell_min = 0 - r = Yrange(ell_min, ell_max) + r = Deprecated.Yrange(ell_min, ell_max) for ell in ell_min:ell_max for m in -ell:ell - i = Yindex(ell, m) + i = Deprecated.Yindex(ell, m) @test r[i, :] == [ell, m] end end - s = Yrange(ell_max) + s = Deprecated.Yrange(ell_max) @test array_equal(r, s) end for ell_min in 0:ell_max - r = Yrange(ell_min, ell_max) + r = Deprecated.Yrange(ell_min, ell_max) for ell in ell_min:ell_max for m in -ell:ell - i = Yindex(ell, m, ell_min) + i = Deprecated.Yindex(ell, m, ell_min) @test r[i, :] == [ell, m] end end @@ -284,7 +284,7 @@ end @testitem "phi_theta <-> theta_phi" setup=[Utilities] begin for T in [Float64, Float32, BigFloat] - import SphericalFunctions: theta_phi + import SphericalFunctions.Deprecated: theta_phi, phi_theta ell_max = 16 for nθ in 2:(2ell_max+1) for nϕ in 2:(2ell_max+1) diff --git a/test/iterators.jl b/test/deprecated/iterators.jl similarity index 88% rename from test/iterators.jl rename to test/deprecated/iterators.jl index 38e03207..c2842e72 100644 --- a/test/iterators.jl +++ b/test/deprecated/iterators.jl @@ -1,15 +1,16 @@ @testitem "D iterators" begin + import SphericalFunctions: Deprecated import Logging: with_logger, NullLogger ℓₘₐₓ = 20 Drange = mapslices( ℓm′m -> tuple(ℓm′m...), - SphericalFunctions.WignerDrange(ℓₘₐₓ); + Deprecated.WignerDrange(ℓₘₐₓ); dims=[2] )[:, 1] let 𝔇 = with_logger(NullLogger()) do - D_iterator(Drange, ℓₘₐₓ) + Deprecated.D_iterator(Drange, ℓₘₐₓ) end for (ℓ, 𝔇ˡ) in enumerate(𝔇) ℓ -= 1 @@ -48,7 +49,7 @@ for ℓₘᵢₙ in 0:ℓₘₐₓ 𝔇 = with_logger(NullLogger()) do - D_iterator(Drange, ℓₘₐₓ, ℓₘᵢₙ) + Deprecated.D_iterator(Drange, ℓₘₐₓ, ℓₘᵢₙ) end for (ℓ, 𝔇ˡ) in enumerate(𝔇) ℓ += ℓₘᵢₙ - 1 @@ -79,17 +80,18 @@ end @testitem "d iterators" begin + import SphericalFunctions: Deprecated import Logging: with_logger, NullLogger ℓₘₐₓ = 20 drange = mapslices( ℓm′m -> tuple(ℓm′m...), - SphericalFunctions.WignerDrange(ℓₘₐₓ); + Deprecated.WignerDrange(ℓₘₐₓ); dims=[2] )[:, 1] let 𝔡 = with_logger(NullLogger()) do - d_iterator(drange, ℓₘₐₓ) + Deprecated.d_iterator(drange, ℓₘₐₓ) end for (ℓ, 𝔡ˡ) in enumerate(𝔡) ℓ -= 1 @@ -129,7 +131,7 @@ end for ℓₘᵢₙ in 0:ℓₘₐₓ 𝔡 = with_logger(NullLogger()) do - d_iterator(drange, ℓₘₐₓ, ℓₘᵢₙ) + Deprecated.d_iterator(drange, ℓₘₐₓ, ℓₘᵢₙ) end for (ℓ, 𝔡ˡ) in enumerate(𝔡) ℓ += ℓₘᵢₙ - 1 @@ -162,13 +164,14 @@ end end @testitem "Y iterators" begin + import SphericalFunctions: Deprecated ℓₘₐₓ = 20 Yrange = mapslices( ℓm -> tuple(ℓm...), - SphericalFunctions.Yrange(ℓₘₐₓ); + Deprecated.Yrange(ℓₘₐₓ); dims=[2] )[:, 1] - 𝔜 = sYlm_iterator(Yrange, ℓₘₐₓ) + 𝔜 = Deprecated.sYlm_iterator(Yrange, ℓₘₐₓ) for (ℓ, 𝔜ˡ) in enumerate(𝔜) ℓ -= 1 Yˡ = [ @@ -186,7 +189,7 @@ end @test eltype(collection) == eltype(𝔜) for ℓₘᵢₙ in 0:ℓₘₐₓ - local 𝔜 = sYlm_iterator(Yrange, ℓₘₐₓ, ℓₘᵢₙ) + local 𝔜 = Deprecated.sYlm_iterator(Yrange, ℓₘₐₓ, ℓₘᵢₙ) for (ℓ, 𝔜ˡ) in enumerate(𝔜) ℓ += ℓₘᵢₙ - 1 Yˡ = [ @@ -195,9 +198,9 @@ end ] @test 𝔜ˡ == Yˡ end - iₘᵢₙ = Ysize(ℓₘᵢₙ-1)+1 + iₘᵢₙ = Deprecated.Ysize(ℓₘᵢₙ-1)+1 @test Yrange[iₘᵢₙ] == (ℓₘᵢₙ, -ℓₘᵢₙ) - 𝔜 = sYlm_iterator(Yrange[iₘᵢₙ:end], ℓₘₐₓ, ℓₘᵢₙ, 1) + 𝔜 = Deprecated.sYlm_iterator(Yrange[iₘᵢₙ:end], ℓₘₐₓ, ℓₘᵢₙ, 1) for (ℓ, 𝔜ˡ) in enumerate(𝔜) ℓ += ℓₘᵢₙ - 1 Yˡ = [ diff --git a/test/map2salm.jl b/test/deprecated/map2salm.jl similarity index 60% rename from test/map2salm.jl rename to test/deprecated/map2salm.jl index 1045986a..7e7a358d 100644 --- a/test/map2salm.jl +++ b/test/deprecated/map2salm.jl @@ -1,25 +1,8 @@ # 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 + import SphericalFunctions: Deprecated for T in [BigFloat, Float64, Float32] # These test the ability of map2salm to precisely decompose the results of `sYlm`. ℓmax = 7 @@ -32,12 +15,12 @@ end for m in -ℓ:ℓ f = mapslices( ϕθ -> sYlm(s, ℓ, m, ϕθ[2], ϕθ[1]), - phi_theta(Nφ, Nϑ, T), + Deprecated.phi_theta(Nφ, Nϑ, T), dims=[3] ) - computed = map2salm(f, s, ℓmax) + computed = Deprecated.map2salm(f, s, ℓmax) expected = zeros(Complex{T}, size(computed)) - expected[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + expected[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) if ≉(computed, expected, atol=30eps(T), rtol=30eps(T)) @show T @show ℓmax @@ -54,8 +37,8 @@ end end @test computed ≈ expected atol=30eps(T) rtol=30eps(T) - plan = plan_map2salm(f, s, ℓmax) - computed2 = map2salm(f, plan) + plan = Deprecated.plan_map2salm(f, s, ℓmax) + computed2 = Deprecated.map2salm(f, plan) @test array_equal(computed, computed2) end end diff --git a/test/operators.jl b/test/deprecated/operators.jl similarity index 69% rename from test/operators.jl rename to test/deprecated/operators.jl index 5b04d61b..7375f794 100644 --- a/test/operators.jl +++ b/test/deprecated/operators.jl @@ -11,43 +11,46 @@ end @testitem "Explicit definition" setup=[ExplicitOperators] begin + import SphericalFunctions: Deprecated 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 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] @test R(imz, f)(Q) ≈ m′ * f(Q) atol=ϵ rtol=ϵ @test L(imz, f)(Q) ≈ m * f(Q) atol=ϵ rtol=ϵ if ℓ ≥ abs(m+1) L₊1 = L(imx, f)(Q) + im * L(imy, f)(Q) - L₊2 = √T((ℓ-m)*(ℓ+m+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m+1, m′)] + L₊2 = √T((ℓ-m)*(ℓ+m+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m+1, m′)] @test L₊1 ≈ L₊2 atol=ϵ rtol=ϵ end if ℓ ≥ abs(m-1) L₋1 = L(imx, f)(Q) - im * L(imy, f)(Q) - L₋2 = √T((ℓ+m)*(ℓ-m+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m-1, m′)] + L₋2 = √T((ℓ+m)*(ℓ-m+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m-1, m′)] @test L₋1 ≈ L₋2 atol=ϵ rtol=ϵ end if ℓ ≥ abs(m′+1) K₊1 = R(imx, f)(Q) - im * R(imy, f)(Q) - K₊2 = √T((ℓ-m′)*(ℓ+m′+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′+1)] + K₊2 = √T((ℓ-m′)*(ℓ+m′+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′+1)] @test K₊1 ≈ K₊2 atol=ϵ rtol=ϵ end if ℓ ≥ abs(m′-1) K₋1 = R(imx, f)(Q) + im * R(imy, f)(Q) - K₋2 = √T((ℓ+m′)*(ℓ-m′+1)) * D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′-1)] + K₋2 = √T((ℓ+m′)*(ℓ-m′+1)) * Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′-1)] @test K₋1 ≈ K₋2 atol=ϵ rtol=ϵ end end @@ -57,7 +60,64 @@ 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) + import SphericalFunctions: Deprecated + + 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) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] + for n ∈ N + for m ∈ M + @test L(m, L(n, f))(Q) ≈ LL(m, n, f, Q) atol=ϵ rtol=ϵ + @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 + import SphericalFunctions: Deprecated using Quaternionic using DoubleFloats const L = ExplicitOperators.L @@ -71,7 +131,7 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] for s ∈ Ss for g ∈ Gs @test L(s*g, f)(Q) ≈ s*L(g, f)(Q) atol=ϵ rtol=ϵ @@ -86,6 +146,7 @@ end end @testitem "Additivity" setup=[ExplicitOperators] begin + import SphericalFunctions: Deprecated using Quaternionic using DoubleFloats const L = ExplicitOperators.L @@ -98,7 +159,7 @@ end for ℓ ∈ 0:4 for m ∈ -ℓ:ℓ for m′ ∈ -ℓ:ℓ - f(Q) = D_matrices(Q, ℓ)[WignerDindex(ℓ, m, m′)] + f(Q) = Deprecated.D_matrices(Q, ℓ)[Deprecated.WignerDindex(ℓ, m, m′)] for g₁ ∈ Gs for g₂ ∈ Gs @test L(g₁+g₂, f)(Q) ≈ L(g₁, f)(Q) + L(g₂, f)(Q) atol=ϵ rtol=ϵ @@ -112,67 +173,35 @@ 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) + import SphericalFunctions: Deprecated -@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) = Deprecated.D_matrices(Q, ℓ)[Deprecated.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 @@ -182,6 +211,7 @@ end end @testitem "Commutators" begin + import SphericalFunctions: Deprecated using DoubleFloats for T ∈ [Float32, Float64, Double64, BigFloat] # Test the following relations: @@ -257,8 +287,80 @@ end end end +@testitem "Casimir" begin + import SphericalFunctions: Deprecated + using DoubleFloats + for T ∈ [Float32, Float64, Double64, BigFloat] + # Test that L² = (L₊L₋ + L₋L₊ + 2Lz²)/2 = R² = (R₊R₋ + R₋R₊ + 2Rz²)/2 + ϵ = 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 + import SphericalFunctions: Deprecated + 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 + 𝒯₊ = Deprecated.SSHT(s+1, ℓₘₐₓ; T=T, method="Direct", inplace=false) + 𝒯₋ = Deprecated.SSHT(s-1, ℓₘₐₓ; T=T, method="Direct", inplace=false) + i₊ = Deprecated.Yindex(abs(s+1), -abs(s+1), ℓₘᵢₙ) + i₋ = Deprecated.Yindex(abs(s-1), -abs(s-1), ℓₘᵢₙ) + Y = zeros(Complex{T}, Deprecated.Ysize(ℓₘᵢₙ, ℓₘₐₓ)) + for ℓ in abs(s):ℓₘₐₓ + for m in -ℓ:ℓ + Y[:] .= zero(T) + Y[Deprecated.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 diff --git a/test/ssht.jl b/test/deprecated/ssht.jl similarity index 73% rename from test/ssht.jl rename to test/deprecated/ssht.jl index 90ab6ed9..23b13be9 100644 --- a/test/ssht.jl +++ b/test/deprecated/ssht.jl @@ -21,13 +21,14 @@ end # Preliminary check that `sqrtbinomial` works as expected @testitem "Preliminaries: sqrtbinomial" begin + import SphericalFunctions: Deprecated using DoubleFloats for T ∈ [Float16, Float32, Float64, Double64, BigFloat] for ℓ ∈ [1, 2, 3, 4, 5, 13, 64, 1025] for s ∈ -2:2 # Note that `ℓ-abs(s)` is more relevant, but we test without `abs` here # to exercise more code paths - a = SphericalFunctions.sqrtbinomial(2ℓ, ℓ-s, T) + a = Deprecated.sqrtbinomial(2ℓ, ℓ-s, T) b = T(√binomial(big(2ℓ), big(ℓ-s))) @test a ≈ b end @@ -38,17 +39,19 @@ end # Check that an error results from a nonsense method request @testitem "Preliminaries: Nonsense method" begin let s=-2, ℓmax=8 - @test_throws ErrorException SSHT(s, ℓmax; method="NonsenseGarbage") + import SphericalFunctions: Deprecated + @test_throws ErrorException Deprecated.SSHT(s, ℓmax; method="NonsenseGarbage") end end # Check what `show` looks like @testitem "Preliminaries: SSHT show" begin let io=IOBuffer(), s=-2, ℓmax=8, T=Float64, method="Direct" + import SphericalFunctions: Deprecated TD = "LinearAlgebra.LU{ComplexF64, Matrix{ComplexF64}, Vector{Int64}}" for inplace ∈ [true, false] - expected = "SphericalFunctions.SSHT$method{$T, $inplace, $TD}($s, $ℓmax)" - 𝒯 = SSHT(s, ℓmax; T, method, inplace) + expected = "SphericalFunctions.Deprecated.SSHT$method{$T, $inplace, $TD}($s, $ℓmax)" + 𝒯 = Deprecated.SSHT(s, ℓmax; T, method, inplace) Base.show(io, MIME("text/plain"), 𝒯) @test String(take!(io)) == expected end @@ -58,18 +61,20 @@ end # Check that SSHTDirect warns if ℓₘₐₓ is too large @testitem "Preliminaries: Direct ℓₘₐₓ" begin let s=0, ℓₘₐₓ=65 - @test_warn """ "Direct" method for s-SHT is only """ SSHT(s, ℓₘₐₓ; method="Direct") + import SphericalFunctions: Deprecated + @test_warn """ "Direct" method for s-SHT is only """ Deprecated.SSHT(s, ℓₘₐₓ; method="Direct") end end # Check pixels and rotors of Minimal @testitem "Preliminaries: Minimal pixels" setup=[SSHT] begin + import SphericalFunctions: Deprecated for T ∈ FloatTypes for ℓmax ∈ [3, 4, 5, 13, 64] for s ∈ -min(2,abs(ℓmax)-1):min(2,abs(ℓmax)-1) - 𝒯 = SSHT(s, ℓmax; T=T, method="Minimal") - @test pixels(𝒯) ≈ sorted_ring_pixels(s, ℓmax, T) - @test rotors(𝒯) ≈ sorted_ring_rotors(s, ℓmax, T) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method="Minimal") + @test Deprecated.pixels(𝒯) ≈ sorted_ring_pixels(s, ℓmax, T) + @test Deprecated.rotors(𝒯) ≈ sorted_ring_rotors(s, ℓmax, T) end end end @@ -77,27 +82,27 @@ end # These test the ability of ssht to precisely reconstruct a pure `sYlm`. -@testitem "Synthesis" setup=[NINJA,SSHT] begin +@testitem "Synthesis" setup=[SSHT] begin + import SphericalFunctions: Deprecated 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 - 𝒯 = SSHT(s, ℓmax; T=T, method=method) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method) #for ℓmin in 0:abs(s) let ℓmin = abs(s) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) computed = 𝒯 * f - expected = NINJA.sYlm.(s, ℓ, m, pixels(𝒯)) + expected = Deprecated.Y.(s, ℓ, m, Deprecated.pixels(𝒯)) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @test computed ≈ expected atol=ϵ rtol=ϵ end @@ -110,28 +115,28 @@ 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 + import SphericalFunctions: Deprecated 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 end for s in -2:2 - 𝒯 = SSHT(s, ℓmax; T=T, method=method) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method) let ℓmin = abs(s) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ - f = NINJA.sYlm.(s, ℓ, m, pixels(𝒯)) + f = Deprecated.Y.(s, ℓ, m, Deprecated.pixels(𝒯)) computed = 𝒯 \ f expected = zeros(Complex{T}, size(computed)) - expected[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + expected[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @test computed ≈ expected atol=ϵ rtol=ϵ end @@ -145,6 +150,7 @@ end # These test the ability of ssht to precisely reconstruct a pure `sYlm`, # and then reverse that process to find the pure mode again. @testitem "A ∘ S" setup=[SSHT] begin + import SphericalFunctions: Deprecated for (method, T) in cases # Note that the number of tests here scales as ℓmax^2, and # the time needed for each scales as (ℓmax log(ℓmax))^2, @@ -156,13 +162,13 @@ end ϵ *= 50 end for s in -2:2 - 𝒯 = SSHT(s, ℓmax; T=T, method=method) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method) let ℓmin = abs(s) - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ f[:] .= false - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) expected = copy(f) computed = 𝒯 \ (𝒯 * f) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @@ -177,6 +183,7 @@ end # These test A ∘ S in the RS method when using different quadratures @testitem "RS quadratures" setup=[SSHT] begin + import SphericalFunctions: Deprecated using StaticArrays using Quaternionic for T in FloatTypes @@ -190,23 +197,23 @@ end (fejer2_rings(2ℓmax+1, T), fejer2(2ℓmax+1, T)), (clenshaw_curtis_rings(2ℓmax+1, T), clenshaw_curtis(2ℓmax+1, T)) ] - 𝒯 = SSHT(s, ℓmax; T=T, θ=θ, quadrature_weights=w, method="RS") + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, θ=θ, quadrature_weights=w, method="RS") p1 = [ @SVector [θi, ϕi] for θi ∈ θ for ϕi ∈ LinRange(T(0), 2T(π), 2ℓmax+2)[begin:end-1] ] - p2 = pixels(𝒯) + p2 = Deprecated.pixels(𝒯) @test p1 ≈ p2 r1 = [from_spherical_coordinates(θϕ...) for θϕ ∈ p1] - r2 = rotors(𝒯) + r2 = Deprecated.rotors(𝒯) @test r1 ≈ r2 let ℓmin = abs(s) - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ f[:] .= false - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) expected = copy(f) computed = 𝒯 \ (𝒯 * f) explain(computed, expected, method, T, ℓmax, s, ℓ, m, ϵ) @@ -223,6 +230,7 @@ end # These test that the non-inplace versions of transformers that *can* work in place # still work. @testitem "Non-inplace" setup=[SSHT] begin + import SphericalFunctions: Deprecated using LinearAlgebra @testset verbose=false "Non-inplace: $T $method" for (method, T) in inplacecases @testset "$ℓmax" for ℓmax ∈ [4,5] @@ -232,13 +240,13 @@ end ϵ *= 50 end for s in [-1, 1] - 𝒯 = SSHT(s, ℓmax; T=T, method=method, inplace=false) + 𝒯 = Deprecated.SSHT(s, ℓmax; T=T, method=method, inplace=false) let ℓmin = abs(s) - f = zeros(Complex{T}, SphericalFunctions.Ysize(ℓmin, ℓmax)) + f = zeros(Complex{T}, Deprecated.Ysize(ℓmin, ℓmax)) for ℓ in abs(s):ℓmax for m in -ℓ:ℓ f[:] .= false - f[SphericalFunctions.Yindex(ℓ, m, ℓmin)] = one(T) + f[Deprecated.Yindex(ℓ, m, ℓmin)] = one(T) expected = f f′ = similar(f) LinearAlgebra.mul!(f′, 𝒯, f) diff --git a/test/wigner_matrices.jl b/test/deprecated/wigner_matrices.jl similarity index 100% rename from test/wigner_matrices.jl rename to test/deprecated/wigner_matrices.jl diff --git a/test/wigner_matrices/H.jl b/test/deprecated/wigner_matrices/H.jl similarity index 76% rename from test/wigner_matrices/H.jl rename to test/deprecated/wigner_matrices/H.jl index 46cfbe71..2e95e70d 100644 --- a/test/wigner_matrices/H.jl +++ b/test/deprecated/wigner_matrices/H.jl @@ -1,16 +1,17 @@ @testitem "H(0)" begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # We have H_{n}^{m′,m}(0) = (-1)^m′ δ_{m′,m} let β = zero(T) expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in abs(m′):n - H_rec = Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + H_rec = Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] if m′ == m @test H_rec ≈ T(-1)^m′ atol=eps(T) else @@ -26,18 +27,19 @@ end @testitem "H(π)" begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # We have H_{n}^{m′,m}(0) = (-1)^m′ δ_{m′,m} let β = T(π) expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in abs(m′):n - H_rec = Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + H_rec = Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] if m′ == -m @test H_rec ≈ T(-1)^(n+m′) atol=eps(T(π)) else @@ -53,20 +55,21 @@ end end @testitem "Compare H to explicit d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the explicit Wigner d - # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, + # d_{ℓ}^{n,m} = ϵ_n ϵ_{-m} H_{ℓ}^{n,m}, for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:2 # 2 is the max explicitly coded ℓ for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in -n:n d_expl = ExplicitWignerMatrices.d_explicit(n, m′, m, expiβ) - d_rec = epsilon(m′) * epsilon(-m) * Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + d_rec = epsilon(m′) * epsilon(-m) * Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] @test d_rec ≈ d_expl atol=30eps(T) rtol=30eps(T) end end @@ -105,21 +108,22 @@ end end @testitem "Compare H to formulaic d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This compares the H obtained via recurrence with the formulaic Wigner d - # d_{\ell}^{n,m} = \epsilon_n \epsilon_{-m} H_{\ell}^{n,m}, - tol = ifelse(T === BigFloat, 100, 1) * 30eps(T) + # d_{ℓ}^{n,m} = ϵ_n ϵ_{-m} H_{ℓ}^{n,m}, + tol = ifelse(T ∈ (BigFloat, Float32), 100, 1) * 30eps(T) for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:6 # Expect overflows for higher ℓ with Float32 for m′ₘₐₓ in 0:ℓₘₐₓ - Hw = fill(T(NaN), WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) + Hw = fill(T(NaN), Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβ, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in -n:n d_form = ExplicitWignerMatrices.d_formula(n, m′, m, expiβ) - d_rec = epsilon(m′) * epsilon(-m) * Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] + d_rec = epsilon(m′) * epsilon(-m) * Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] @test d_rec ≈ d_form atol=tol rtol=tol end end diff --git a/test/wigner_matrices/big_D.jl b/test/deprecated/wigner_matrices/big_D.jl similarity index 70% rename from test/wigner_matrices/big_D.jl rename to test/deprecated/wigner_matrices/big_D.jl index 860d4747..ab5be769 100644 --- a/test/wigner_matrices/big_D.jl +++ b/test/deprecated/wigner_matrices/big_D.jl @@ -1,4 +1,5 @@ @testitem "Compare H/D indexing" setup=[NaNChecker] begin + import SphericalFunctions: Deprecated const NaNCheck = NaNChecker.NaNCheck @testset "$T" for T in [Float64, Float32] # Here, we check that we can pass in either an "H wedge" array to be used with @@ -9,18 +10,18 @@ expiβ = cis(rand(0:eps(T):π)) expiβNaNCheck = complex(NaNCheck{T}(expiβ.re), NaNCheck{T}(expiβ.im)) NCTN = NaNCheck{T}(NaN) - Hw = fill(NCTN, WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) - H!(Hw, expiβNaNCheck, ℓₘₐₓ, m′ₘₐₓ, H_recursion_coefficients(ℓₘₐₓ, T)) - 𝔇 = fill(NCTN, WignerDsize(ℓₘₐₓ, m′ₘₐₓ)) - H!( + Hw = fill(NCTN, Deprecated.WignerHsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!(Hw, expiβNaNCheck, ℓₘₐₓ, m′ₘₐₓ, Deprecated.H_recursion_coefficients(ℓₘₐₓ, T)) + 𝔇 = fill(NCTN, Deprecated.WignerDsize(ℓₘₐₓ, m′ₘₐₓ)) + Deprecated.H!( 𝔇, expiβNaNCheck, ℓₘₐₓ, m′ₘₐₓ, - H_recursion_coefficients(ℓₘₐₓ, T), WignerDindex + Deprecated.H_recursion_coefficients(ℓₘₐₓ, T), Deprecated.WignerDindex ) for n in 0:ℓₘₐₓ for m′ in -min(n, m′ₘₐₓ):min(n, m′ₘₐₓ) for m in abs(m′):n - Hnm′m = Hw[WignerHindex(n, m′, m, m′ₘₐₓ)] - 𝔇nm′m = 𝔇[WignerDindex(n, m′, m, m′ₘₐₓ)] + Hnm′m = Hw[Deprecated.WignerHindex(n, m′, m, m′ₘₐₓ)] + 𝔇nm′m = 𝔇[Deprecated.WignerDindex(n, m′, m, m′ₘₐₓ)] @test Hnm′m == 𝔇nm′m end end @@ -30,26 +31,27 @@ end @testitem "Compare 𝔇 to formulaic d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic @testset "$T" for T in [BigFloat, Float64, Float32] # Now, we're ready to check that d_{n}^{m′,m}(β) matches the expected values # for a range of β values for ℓₘₐₓ in 0:4 - D_storage = D_prep(ℓₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) expiα = complex(one(T)) expiγ = complex(one(T)) for β in βrange(T) expiβ = cis(β) R = from_euler_angles(zero(T), β, zero(T)) - 𝔇 = D_matrices!(D_storage, R) + 𝔇 = Deprecated.D_matrices!(D_storage, R) for n in 0:ℓₘₐₓ for m′ in -n:n for m in -n:n 𝔇_formula = ExplicitWignerMatrices.D_formula( n, m′, m, expiα, expiβ, expiγ ) - 𝔇_recurrence = 𝔇[WignerDindex(n, m′, m)] - @test 𝔇_formula ≈ 𝔇_recurrence atol=200eps(T) rtol=200eps(T) + 𝔇_recurrence = 𝔇[Deprecated.WignerDindex(n, m′, m)] + @test conj(𝔇_formula) ≈ 𝔇_recurrence atol=200eps(T) rtol=200eps(T) end end end @@ -59,6 +61,7 @@ end end @testitem "Compare 𝔇 to formulaic 𝔇" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic using ProgressMeter using Random @@ -67,21 +70,21 @@ end # for a range of α, β, γ values Random.seed!(123) ℓₘₐₓ = T===BigFloat ? 4 : 8 - D_storage = D_prep(ℓₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) @showprogress desc="Compare 𝔇 to formulaic 𝔇 ($T)" for α in αrange(T, 5) for β in βrange(T, 5) for γ in γrange(T, 5) R = from_euler_angles(α, β, γ) - expiα, expiβ, expiγ = to_euler_phases(R) - 𝔇 = D_matrices!(D_storage, R) + expiα, expiβ, expiγ = Deprecated.to_euler_phases(R) + 𝔇 = Deprecated.D_matrices!(D_storage, R) for n in 0:ℓₘₐₓ for m′ in -n:n for m in -n:n 𝔇_formula = ExplicitWignerMatrices.D_formula( n, m′, m, expiα, expiβ, expiγ ) - 𝔇_recurrence = 𝔇[WignerDindex(n, m′, m)] - @test 𝔇_formula ≈ 𝔇_recurrence atol=400eps(T) rtol=400eps(T) + 𝔇_recurrence = 𝔇[Deprecated.WignerDindex(n, m′, m)] + @test conj(𝔇_formula) ≈ 𝔇_recurrence atol=400eps(T) rtol=400eps(T) end end end @@ -92,6 +95,7 @@ end end @testitem "Group characters" setup=[Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic using ProgressMeter using Random @@ -101,11 +105,11 @@ end # conjugacy classes of SO(3) are rotations through the same angle about any axis. Random.seed!(123) ℓₘₐₓ = T===BigFloat ? 10 : 20 - D_storage = D_prep(ℓₘₐₓ, T) - d_storage = d_prep(ℓₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) + d_storage = Deprecated.d_prep(ℓₘₐₓ, T) @showprogress desc="Group characters $T" for β in βrange(T) expiβ = cis(β) - d = d_matrices!(d_storage, expiβ) + d = Deprecated.d_matrices!(d_storage, expiβ) for j in 0:ℓₘₐₓ sin_ratio = if abs(β) < 10eps(T) T(2j+1) @@ -114,12 +118,12 @@ end else sin((2j+1)*β/2) / sin(β/2) end - χʲ = sum(d[WignerDindex(j, m, m)] for m in -j:j) + χʲ = sum(d[Deprecated.WignerDindex(j, m, m)] for m in -j:j) @test χʲ ≈ sin_ratio atol=500eps(T) rtol=500eps(T) for v̂ in v̂range(T) R = exp(β/2 * v̂) - 𝔇 = D_matrices!(D_storage, R) - χʲ = sum(𝔇[WignerDindex(j, m, m)] for m in -j:j) + 𝔇 = Deprecated.D_matrices!(D_storage, R) + χʲ = sum(𝔇[Deprecated.WignerDindex(j, m, m)] for m in -j:j) @test χʲ ≈ sin_ratio atol=500eps(T) rtol=500eps(T) end end @@ -128,6 +132,7 @@ end end @testitem "Representation property" setup=[Utilities] begin + import SphericalFunctions: Deprecated using Quaternionic using ProgressMeter using Random @@ -136,7 +141,7 @@ end Random.seed!(123) tol = 3eps(T) ℓₘₐₓ = 10 - D₁_storage = D_prep(ℓₘₐₓ, T) + D₁_storage = Deprecated.D_prep(ℓₘₐₓ, T) 𝔇₁ = D₁_storage[1] 𝔇₂ = similar(𝔇₁) D₂_storage = (𝔇₂, D₁_storage[2:end]...) @@ -144,12 +149,12 @@ end D₁₂_storage = (𝔇₁₂, D₁_storage[2:end]...) @showprogress desc="Representation property ($T)" for R₁ in Rrange(T) for R₂ in Rrange(T) - D_matrices!(D₁_storage, R₁) - D_matrices!(D₂_storage, R₂) - D_matrices!(D₁₂_storage, R₁*R₂) + Deprecated.D_matrices!(D₁_storage, R₁) + Deprecated.D_matrices!(D₂_storage, R₂) + Deprecated.D_matrices!(D₁₂_storage, R₁*R₂) for ℓ in 0:ℓₘₐₓ - i = WignerDindex(ℓ, -ℓ, -ℓ) - j = WignerDindex(ℓ, ℓ, ℓ) + i = Deprecated.WignerDindex(ℓ, -ℓ, -ℓ) + j = Deprecated.WignerDindex(ℓ, ℓ, ℓ) 𝔇₁ˡ = transpose(reshape(𝔇₁[i:j], 2ℓ+1, 2ℓ+1)) 𝔇₂ˡ = transpose(reshape(𝔇₂[i:j], 2ℓ+1, 2ℓ+1)) 𝔇₁₂ˡ = transpose(reshape(𝔇₁₂[i:j], 2ℓ+1, 2ℓ+1)) diff --git a/test/wigner_matrices/sYlm.jl b/test/deprecated/wigner_matrices/sYlm.jl similarity index 52% rename from test/wigner_matrices/sYlm.jl rename to test/deprecated/wigner_matrices/sYlm.jl index d4203661..aa4a434f 100644 --- a/test/wigner_matrices/sYlm.jl +++ b/test/deprecated/wigner_matrices/sYlm.jl @@ -1,46 +1,30 @@ @testitem "Issue #40" begin - @test maximum(abs, sYlm_values(0.0, 0.0, 3, -2)) > 0 + import SphericalFunctions: Deprecated + @test maximum(abs, Deprecated.sYlm_values(0.0, 0.0, 3, -2)) > 0 end -@testitem "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 "Internal consistency" setup=[Utilities] begin + import SphericalFunctions: Deprecated 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 - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + tol = ℓₘₐₓ^2 * 2eps(T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) let R = randn(Rotor{T}) - @test_throws ErrorException sYlm_values!(sYlm_storage, R, sₘₐₓ+1) - @test_throws ErrorException sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) + @test_throws ErrorException Deprecated.sYlm_values!(sYlm_storage, R, sₘₐₓ+1) + @test_throws ErrorException Deprecated.sYlm_values!(sYlm_storage, R, -sₘₐₓ-1) end - @showprogress desc="Compare to NINJA expressions ($T)" for spin in -sₘₐₓ:sₘₐₓ + for spin in [0, -1, -2] for ι in βrange(T) for ϕ in αrange(T) - R = from_spherical_coordinates(ι, ϕ) - Y = sYlm_values!(sYlm_storage, R, spin) - Y′ = sYlm_values(ι, ϕ, ℓₘₐₓ, spin) # Also tests issue #40 + R = Deprecated.from_spherical_coordinates(ι, ϕ) + Y = Deprecated.sYlm_values!(sYlm_storage, R, spin) + Y′ = Deprecated.sYlm_values(ι, ϕ, ℓₘₐₓ, spin) # Also tests issue #40 @test Y[(spin^2+1):end] ≈ Y′ atol=tol rtol=tol i = 1 for ℓ in 0:abs(spin)-1 @@ -49,18 +33,6 @@ end i += 1 end 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.LALSpinWeightedSphericalHarmonic(ι, ϕ, spin, ℓ, m) - @test sYlm1 ≈ sYlm3 atol=tol rtol=tol - end - i += 1 - end - end end end end @@ -68,6 +40,7 @@ end end @testitem "Spin property" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64, Float32, BigFloat] @@ -78,14 +51,14 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) @showprogress desc="Spin property ($T)" for spin in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) for γ in γrange(T) - R = from_spherical_coordinates(ι, ϕ) - Y1 = copy(sYlm_values!(sYlm_storage, R * exp(γ*𝐤/2), spin)) - Y2 = sYlm_values!(sYlm_storage, R, spin) + R = Deprecated.from_spherical_coordinates(ι, ϕ) + Y1 = copy(Deprecated.sYlm_values!(sYlm_storage, R * exp(γ*𝐤/2), spin)) + Y2 = Deprecated.sYlm_values!(sYlm_storage, R, spin) @test Y1 ≈ Y2 * cis(-spin*γ) atol=tol rtol=tol end end @@ -95,6 +68,7 @@ end end @testitem "sYlm vs WignerD" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64, Float32, BigFloat] @@ -104,14 +78,14 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) - D_storage = D_prep(ℓₘₐₓ, T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + D_storage = Deprecated.D_prep(ℓₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) @showprogress desc="sYlm vs WignerD ($T)" for s in -sₘₐₓ:sₘₐₓ for ι in βrange(T) for ϕ in αrange(T) - R = from_spherical_coordinates(ι, ϕ) - 𝔇 = D_matrices!(D_storage, R) - Y = sYlm_values!(sYlm_storage, R, s) + R = Deprecated.from_spherical_coordinates(ι, ϕ) + 𝔇 = Deprecated.D_matrices!(D_storage, R) + Y = Deprecated.sYlm_values!(sYlm_storage, R, s) i = 1 for ℓ in 0:abs(s)-1 for m in -ℓ:ℓ @@ -122,7 +96,7 @@ end for ℓ in abs(s):ℓₘₐₓ for m in -ℓ:ℓ sYlm1 = Y[i] - sYlm2 = (-1)^s * √((2ℓ+1)/(4T(π))) * 𝔇[WignerDindex(ℓ, m, -s)] + sYlm2 = (-1)^s * √((2ℓ+1)/(4T(π))) * 𝔇[Deprecated.WignerDindex(ℓ, m, -s)] if ≉(sYlm1, sYlm2, atol=tol, rtol=tol) println("Unequal at i=$i (s,ℓ,m)=$((s,ℓ,m)): ") @show ι ϕ @@ -139,6 +113,7 @@ end end @testitem "sYlm conjugation" setup=[Utilities] begin + import SphericalFunctions: Deprecated using ProgressMeter using Quaternionic @testset "$T" for T in [Float64, Float32, BigFloat] @@ -147,18 +122,18 @@ end sₘₐₓ = 2 ℓₘᵢₙ = 0 tol = 4ℓₘₐₓ * eps(T) - sYlm_storage = sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) + sYlm_storage = Deprecated.sYlm_prep(ℓₘₐₓ, sₘₐₓ, T) @showprogress desc="sYlm conjugation ($T)" for ι in βrange(T) for ϕ in αrange(T) for γ in γrange(T) for s in -sₘₐₓ:sₘₐₓ - R = from_spherical_coordinates(ι, ϕ) - Y1 = copy(sYlm_values!(sYlm_storage, R, s)) - Y2 = sYlm_values!(sYlm_storage, R, -s) + R = Deprecated.from_spherical_coordinates(ι, ϕ) + Y1 = copy(Deprecated.sYlm_values!(sYlm_storage, R, s)) + Y2 = Deprecated.sYlm_values!(sYlm_storage, R, -s) for ℓ in abs(s):ℓₘₐₓ for m in -ℓ:ℓ - sYlm1 = conj(Y1[Yindex(ℓ, m)]) - sYlm2 = (-1)^(s+m) * Y2[Yindex(ℓ, -m)] + sYlm1 = conj(Y1[Deprecated.Yindex(ℓ, m)]) + sYlm2 = (-1)^(s+m) * Y2[Deprecated.Yindex(ℓ, -m)] @test sYlm1 ≈ sYlm2 atol=tol rtol=tol end end diff --git a/test/wigner_matrices/small_d.jl b/test/deprecated/wigner_matrices/small_d.jl similarity index 77% rename from test/wigner_matrices/small_d.jl rename to test/deprecated/wigner_matrices/small_d.jl index eb844e1b..a76b1549 100644 --- a/test/wigner_matrices/small_d.jl +++ b/test/deprecated/wigner_matrices/small_d.jl @@ -1,4 +1,5 @@ @testitem "Compare d expressions" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # This just compares the two versions of the `d` function from test_utilities # to ensure that later tests that use those functions are reliable @@ -23,6 +24,7 @@ end @testitem "Compare d to formulaic d" setup=[ExplicitWignerMatrices,Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # Now, we're ready to check that d_{n}^{m′,m}(β) matches the expected values # for a range of β values @@ -30,13 +32,13 @@ end for β in βrange(T) expiβ = cis(β) for ℓₘₐₓ in 0:4 - d_storage = d_prep(ℓₘₐₓ, T) - d = d_matrices!(d_storage, expiβ) + d_storage = Deprecated.d_prep(ℓₘₐₓ, T) + d = Deprecated.d_matrices!(d_storage, expiβ) for n in 0:ℓₘₐₓ for m′ in -n:n for m in -n:n d_formula = ExplicitWignerMatrices.d_formula(n, m′, m, expiβ) - d_recurrence = d[WignerDindex(n, m′, m)] + d_recurrence = d[Deprecated.WignerDindex(n, m′, m)] @test d_formula ≈ d_recurrence atol=tol rtol=tol end end @@ -47,6 +49,7 @@ end end @testitem "Test d signatures" setup=[Utilities] begin + import SphericalFunctions: Deprecated @testset "$T" for T in [BigFloat, Float64, Float32] # 1 d_matrices(β, ℓₘₐₓ) # 2 d_matrices(expiβ, ℓₘₐₓ) @@ -57,20 +60,20 @@ end ℓₘₐₓ = 8 for β in βrange(T) expiβ = cis(β) - dA = d_matrices(β, ℓₘₐₓ) # 1 - dB = d_matrices(expiβ, ℓₘₐₓ) # 2 + dA = Deprecated.d_matrices(β, ℓₘₐₓ) # 1 + dB = Deprecated.d_matrices(expiβ, ℓₘₐₓ) # 2 @test array_equal(dA, dB) dB .= 0 - d_matrices!(dB, β, ℓₘₐₓ) # 5 + Deprecated.d_matrices!(dB, β, ℓₘₐₓ) # 5 @test array_equal(dA, dB) dB .= 0 - d_matrices!(dB, expiβ, ℓₘₐₓ) # 6 + Deprecated.d_matrices!(dB, expiβ, ℓₘₐₓ) # 6 @test array_equal(dA, dB) - d_storage = d_prep(ℓₘₐₓ, T) - dB = d_matrices!(d_storage, β) # 3 + d_storage = Deprecated.d_prep(ℓₘₐₓ, T) + dB = Deprecated.d_matrices!(d_storage, β) # 3 @test array_equal(dA, dB) dB .= 0 - dB = d_matrices!(d_storage, expiβ) # 4 + dB = Deprecated.d_matrices!(d_storage, expiβ) # 4 @test array_equal(dA, dB) end end diff --git a/test/haxis.jl b/test/haxis.jl new file mode 100644 index 00000000..f9e52594 --- /dev/null +++ b/test/haxis.jl @@ -0,0 +1,146 @@ +@testitem "HAxis" setup=[EncodeDecode] begin + using SphericalFunctions: HAxis, Nᵣ, ℓ, ℓₘᵢₙ, maxℓ, m′ₘᵢₙ, m′ₘₐₓ, mₘᵢₙ, mₘₐₓ + using .EncodeDecode: encode, decode + + # HAxis stores only the m′=ℓₘᵢₙ axis (0 or 1/2), with m ranging from ℓₘᵢₙ to ℓₘₐₓ. + # The data layout is: [value for iᵣ ∈ 1:Nᵣ, m ∈ ℓₘᵢₙ:ℓₘₐₓ] + # We want inner loop over iᵣ, outer loop over m for vectorization. + + function fill_1index!(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + i = 1 + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + h[i] = encode(iᵣ, ℓₘᵢₙ, m) + i += 1 + end + end + end + return h + end + + function fill_2index!(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + h[iᵣ, m] = encode(iᵣ, ℓₘᵢₙ, m) + end + end + end + return h + end + + function fill_3index!(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + h[iᵣ, ℓₘᵢₙ, m] = encode(iᵣ, ℓₘᵢₙ, m) + end + end + end + return h + end + + function test_1index(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + i = 1 + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(h[i]) == (iᵣ, numerator(ℓₘᵢₙ), numerator(m)) + i += 1 + end + end + end + end + + function test_2index(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(h[iᵣ, m]) == (iᵣ, numerator(ℓₘᵢₙ), numerator(m)) + end + end + end + end + + function test_3index(h::HAxis{IT}) where {IT} + let Nᵣ = Nᵣ(h), ℓ = ℓ(h), ℓₘᵢₙ = ℓₘᵢₙ(IT) + for m ∈ ℓₘᵢₙ:ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(h[iᵣ, ℓₘᵢₙ, m]) == (iᵣ, numerator(ℓₘᵢₙ), numerator(m)) + end + end + end + end + + # Test both integer and half-integer ℓ + for ℓₘₐₓ ∈ (5, 9//2) + for Nᵣ ∈ (1, 2, 3, 7) + IT = typeof(ℓₘₐₓ) + RT = Float64 + h = HAxis(RT, Nᵣ, ℓₘₐₓ) + + # Check fields + @test h.Nᵣ == Nᵣ + @test h.maxℓ == ℓₘₐₓ + + # When first created, ℓ should be at its minimum value + @test ℓ(h) == ℓₘᵢₙ(IT) + + # Check index ranges + @test m′ₘᵢₙ(h) == ℓₘᵢₙ(IT) + @test m′ₘₐₓ(h) == ℓₘᵢₙ(IT) + @test mₘᵢₙ(h) == ℓₘᵢₙ(IT) + @test mₘₐₓ(h) == ℓₘᵢₙ(IT) # mₘₐₓ should equal current ℓ, not ℓₘₐₓ + + # Check storage size (allocated for maximum ℓₘₐₓ) + expected_length = Nᵣ * (Int(ℓₘₐₓ - ℓₘᵢₙ(IT)) + 1) + @test length(h.parent) == expected_length + + # Test changing ℓ + for new_ell in (ℓₘᵢₙ(IT):ℓₘₐₓ) + h.ℓ = new_ell + @test h.ℓ == new_ell + @test ℓ(h) == new_ell + @test mₘₐₓ(h) == new_ell # mₘₐₓ should track current ℓ + + # Test all three indexing methods (1D, 2D, 3D) + fill_1index!(h) + test_2index(h) + test_3index(h) + + fill_2index!(h) + test_1index(h) + test_3index(h) + + fill_3index!(h) + test_1index(h) + test_2index(h) + + # Test bounds checking for current ℓ; check just for the string, because + # some tests will throw a `FixedSizeArrays.BoundsErrorLight` instead of a + # standard `BoundsError`. + @test_throws "BoundsError" h[0] + @test_throws "BoundsError" h[length(h.parent) + 1] + @test_throws "BoundsError" h[0, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[Nᵣ + 1, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT) - 1] + @test_throws "BoundsError" h[1, new_ell + 1] # Beyond current ℓ + + # 3D indexing: m′ must equal ℓₘᵢₙ + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT) - 1, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT) + 1, ℓₘᵢₙ(IT)] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT), ℓₘᵢₙ(IT) - 1] + @test_throws "BoundsError" h[1, ℓₘᵢₙ(IT), new_ell + 1] # Beyond current ℓ + end + + # Test error conditions for changing ℓ + @test_throws "greater than maxℓ" h.ℓ = ℓₘₐₓ + 1 + @test_throws "less than ℓₘᵢₙ" h.ℓ = ℓₘᵢₙ(IT) - 1 + + # Test that we can't change other properties + @test_throws "only `ℓ` is allowed to be changed" h.Nᵣ = 10 + @test_throws "only `ℓ` is allowed to be changed" h.maxℓ = ℓₘₐₓ + 1 + end + end +end diff --git a/test/hwedge.jl b/test/hwedge.jl new file mode 100644 index 00000000..f862d501 --- /dev/null +++ b/test/hwedge.jl @@ -0,0 +1,100 @@ +@testitem "HWedge" setup=[EncodeDecode] begin + using SphericalFunctions: HWedge, HWedge_size, Nᵣ, ℓ, ℓₘᵢₙ, m′ₘᵢₙ, m′ₘₐₓ + using .EncodeDecode: encode, decode + + # We will fill the HWedge with integers that encode their indices. By iterating over + # valid indices in order, we can test that the storage layout is correct. Specifically, + # we want an inner loop over iᵣ, then m, then m′ because this is the order in which the + # recurrence relations will fill the data, vectorizing over iᵣ, then iterating most + # quickly over m. We will check that we can fill that data both using 1D indexing and + # 3D indexing, then verify that both methods give the same result. We will also test + # both methods for reading the data back out, which will verify that the indexing logic + # is correct in both setindex! and getindex. + function fill_1index!(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + i = 1 + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + w[i] = encode(iᵣ, m′, m) + i += 1 + end + end + end + end + return w + end + function fill_3index!(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + w[iᵣ, m′, m] = encode(iᵣ, m′, m) + end + end + end + end + return w + end + function test_1index(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + i = 1 + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(w[i]) == (iᵣ, numerator(m′), numerator(m)) + i += 1 + end + end + end + end + end + function test_3index(w::HWedge{IT}) where {IT} + let Nᵣ = Nᵣ(w), ℓ = ℓ(w), m′ₘₐₓ = m′ₘₐₓ(w), m′ₘᵢₙ = m′ₘᵢₙ(w) + for m′ ∈ m′ₘᵢₙ:m′ₘₐₓ + for m ∈ abs(m′):ℓ + for iᵣ ∈ 1:Nᵣ + @test decode(w[iᵣ, m′, m]) == (iᵣ, numerator(m′), numerator(m)) + end + end + end + end + end + + for ℓₘₐₓ ∈ (5, 9//2) + for Nᵣ ∈ (1, 2, 3, 7) + for m′ₘₐₓ ∈ ℓₘᵢₙ(ℓₘₐₓ):ℓₘₐₓ + for m′ₘᵢₙ in -ℓₘₐₓ:-ℓₘᵢₙ(ℓₘₐₓ) + RT = Float64 + H = HWedge(RT, Nᵣ, ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + + @test H.Nᵣ == Nᵣ + @test H.maxℓ == ℓₘₐₓ + @test H.maxm′ₘₐₓ == m′ₘₐₓ + @test H.minm′ₘᵢₙ == m′ₘᵢₙ + + # When first created, the indices should have their smallest values + @test H.ℓ == ℓₘᵢₙ(ℓₘₐₓ) + @test H.m′ₘₐₓ == H.ℓ + @test H.m′ₘᵢₙ == -H.ℓ + + # But the storage should be full size + expected_size = HWedge_size(ℓₘₐₓ, m′ₘₐₓ, m′ₘᵢₙ) + @test size(H.parent) == (Nᵣ * expected_size, ) + + # Test changing ℓ + for new_ell in (ℓₘᵢₙ(ℓₘₐₓ):ℓₘₐₓ) + H.ℓ = new_ell + @test H.ℓ == new_ell + @test H.m′ₘₐₓ == min(new_ell, m′ₘₐₓ) + @test H.m′ₘᵢₙ == max(-new_ell, m′ₘᵢₙ) + fill_1index!(H) + test_3index(H) + fill_3index!(H) + test_1index(H) + end + end + end + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index e73befde..cefef814 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,121 @@ +# See docs/src/development/index.md for details of how to run tests with this script. + +# This is to ensure that, even run as a script, Ctrl-C will actually interrupt the tests. +Base.exit_on_sigint(false) + using TestItemRunner +using ArgParse + +function parse_commandline() + settings = ArgParseSettings( + description = """Run selected tests from the SphericalFunctions.jl test suite. + + \ua0 + + See docs/src/development/index.md for details of how to run tests with this script. + + \ua0 + + The RUN and SKIP arguments may be names of individual tests (in quotes if there are + spaces), tags (which must be prefixed by `:`) that are given in the `@testitem`, or + files (which must end with `.jl`). Note that SKIP takes precedence over RUN if both + are specified; a test matching both a run and a skip filter will be skipped. Any + test with the `:skipci` tag will be skipped whenever the environment variable `CI` + is set to "true" (which is the case on, e.g., GitHub Actions). + """, + usage = "julia test/runtests.jl [-h] [RUN...] [--skip SKIP...]", + ) + @add_arg_table! settings begin + # Collect everything before the optional `--skip` + "run" + nargs = '*' + help = "names, tags, or files to run" + # Collect everything after the optional `--skip` + "--skip" + nargs = '*' + default = String[] + help = "names, tags, or files to skip" + end + parsed_args = parse_args(settings) + run_files = Tuple(Regex(s) for s ∈ parsed_args["run"] if endswith(s, ".jl")) + run_tags = Tuple(Symbol(s[2:end]) for s ∈ parsed_args["run"] if startswith(s, ":")) + run_tests = Tuple(Regex(s) for s ∈ parsed_args["run"] if !endswith(s, ".jl") && !startswith(s, ":")) + skip_files = Tuple(Regex(s) for s ∈ parsed_args["skip"] if endswith(s, ".jl")) + skip_tags = Tuple(Symbol(s[2:end]) for s ∈ parsed_args["skip"] if startswith(s, ":")) + skip_tests = Tuple(Regex(s) for s ∈ parsed_args["skip"] if !endswith(s, ".jl") && !startswith(s, ":")) + return run_files, run_tags, run_tests, skip_files, skip_tags, skip_tests +end + +const run_files, run_tags, run_tests, skip_files, skip_tags, skip_tests = parse_commandline() + +# Get the `CI` environment variable, defaulting to "false" if not set +const CI = get(ENV, "CI", "false") == "true" + +# Create the function that will filter which tests to run +function filter(testitem) + # Destructure the input NamedTuple. Note that `filename` is the full path to the file + # containing the test item, and `name` is the full string used to name the test item, + # while `tags` is a vector of `Symbol`s that can be used to tag test items. + (; filename, name, tags) = testitem + + for skip ∈ skip_files + if occursin(skip, filename) + @info "Skipping test '$name' in file '$(relpath(filename))' " * + "due to skip filter '$skip'." + return false + end + end + + for skip ∈ skip_tags + if skip ∈ tags + @info "Skipping test '$name' tagged '$skip' due to skip filter." + return false + end + end + + for skip ∈ skip_tests + if occursin(skip, name) + @info "Skipping test '$name' due to skip filter '$skip'." + return false + end + end + + if !isempty(run_files) || !isempty(run_tags) || !isempty(run_tests) + for run ∈ run_files + if occursin(run, filename) + # @warn "Dry run: including test '$name' in file '$(relpath(filename))' " * + # "due to run filter '$run'." + # return false + return true + end + end + for run ∈ run_tags + if run ∈ tags + # @warn "Dry run: including test '$name' tagged '$run' due to run filter." + # return false + return true + end + end + for run ∈ run_tests + if occursin(run, name) + # @warn "Dry run: including test '$name' due to run filter '$run'." + # return false + return true + end + end + # @info "Excluding test '$name' in file '$(relpath(filename))' because " * + # "it does not match any requested tests." + return false + end + + if CI && :skipci ∈ tags && :skipci ∉ run_tags + @info "Skipping test '$name' tagged ':skipci' because `CI` is true." + return false + end + + return true +end + +@info "Filtering tests with" run_files run_tags run_tests skip_files skip_tags skip_tests CI -@run_package_tests verbose = true +@run_package_tests verbose=true filter=filter diff --git a/test/utilities/encoder.jl b/test/utilities/encoder.jl new file mode 100644 index 00000000..e504940d --- /dev/null +++ b/test/utilities/encoder.jl @@ -0,0 +1,56 @@ +""" +This module provides functions for encoding a (small) set of (small) integers into a single +integer, and decoding them back again. This is useful for storing those integers in data +structures that only accept single integers or floats, such as HWedge. +""" +@testmodule EncodeDecode begin + encode(i::Int) = i + 50 + encode(i::Rational) = numerator(i) + 50 + function encode(i...) + output = 0 + max_integers = log10(typemax(output)) ÷ 2 + if length(i) > max_integers + error("Can only encode up to $max_integers integers into a single integer") + end + for (j, v) in enumerate(i) + if !(-50 ≤ v ≤ 49) + error("Can only encode integers in -50:49; got $v") + end + output += encode(v) * 10^(2j-2) + end + output + end + + decode(i; pad=2) = decode(Int(i); pad) + function decode(i::Int; pad=2) + d = digits(i; pad) + output = Int[] + for k in 1:2:length(d) + v = (d[k] + 10d[k+1]) - 50 + push!(output, v) + end + return Tuple(output) + end +end + +@testitem "EncodeDecode" setup=[EncodeDecode] begin + using .EncodeDecode: encode, decode + for ℓₘₐₓ ∈ (5, 7//2) + for iᵣ ∈ 1:7 + for m′ ∈ -ℓₘₐₓ:ℓₘₐₓ + for m ∈ -ℓₘₐₓ:ℓₘₐₓ + code = encode(iᵣ, m′, m) + (iᵣ₂, m′₂, m₂) = decode(code; pad=6) + @test iᵣ == iᵣ₂ + if ℓₘₐₓ isa Int + @test m′ == m′₂ + @test m == m₂ + else + @test m′ == m′₂ // 2 + @test m == m₂ // 2 + end + end + end + end + end +end 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 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} diff --git a/test/utilities/utilities.jl b/test/utilities/utilities.jl index a4b3431c..54da4731 100644 --- a/test/utilities/utilities.jl +++ b/test/utilities/utilities.jl @@ -1,13 +1,45 @@ @testsnippet Utilities begin +ℓmrange(ℓₘᵢₙ, ℓₘₐₓ) = eachrow(SphericalFunctions.Deprecated.Yrange(ℓₘᵢₙ, ℓₘₐₓ)) +ℓmrange(ℓₘₐₓ) = ℓmrange(0, ℓₘₐₓ) +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(π); 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_poles=0) where T = T[ + avoid_poles; nextfloat(T(avoid_poles)); + rand(T(0):eps(T(π)):T(π), n); + 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 +θϕ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}[ 𝐢; 𝐣; 𝐤; -𝐢; -𝐣; -𝐤; @@ -29,6 +61,7 @@ function Rrange(::Type{T}, n=15) where T randn(Rotor{T}, n) ] end + epsilon(k) = ifelse(k>0 && isodd(k), -1, 1) """ @@ -50,7 +83,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)