Skip to content

Commit 22ad408

Browse files
authored
Add tutorial to docs (#14)
* add tutorial to docs * expand input types * test with Symbolics * removed unused polytype method * fix negative m norm * update tutorial
1 parent 113afa1 commit 22ad408

9 files changed

+529
-371
lines changed

Project.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "LegendrePolynomials"
22
uuid = "3db4a2ba-fc88-11e8-3e01-49c72059a882"
3-
version = "0.4.1"
3+
version = "0.4.2"
44

55
[deps]
66
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
@@ -10,15 +10,19 @@ SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
1010
Aqua = "0.5"
1111
HyperDualNumbers = "4"
1212
OffsetArrays = "1"
13+
PerformanceTestTools = "0.1"
1314
QuadGK = "2"
1415
SpecialFunctions = "1, 2"
16+
Symbolics = "4"
1517
julia = "1.6"
1618

1719
[extras]
1820
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
1921
HyperDualNumbers = "50ceba7f-c3ee-5a84-a6e8-3ad40456ec97"
22+
PerformanceTestTools = "dc46b164-d16f-48ec-a853-60448fc869fe"
2023
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
24+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
2125
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2226

2327
[targets]
24-
test = ["Aqua", "HyperDualNumbers", "Test", "QuadGK"]
28+
test = ["Aqua", "HyperDualNumbers", "PerformanceTestTools", "Symbolics", "Test", "QuadGK"]

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
33
HyperDualNumbers = "50ceba7f-c3ee-5a84-a6e8-3ad40456ec97"
44
LegendrePolynomials = "3db4a2ba-fc88-11e8-3e01-49c72059a882"
5+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
56

67
[compat]
78
Documenter = "0.27"
89
HyperDualNumbers = "4"
10+
Symbolics = "4"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ makedocs(;
1515
),
1616
pages=[
1717
"Introduction" => "index.md",
18+
"Tutorial" => "tutorial.md",
1819
"Derivatives" => "derivatives.md",
1920
],
2021
)

docs/src/index.md

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ P_{\ell}^{m}\left(x\right)=\sqrt{\frac{2\left(\ell+m\right)!}{\left(2\ell+1\righ
4848

4949
There are six main functions:
5050

51-
* [`Pl(x,l; [norm])`](@ref Pl): this evaluates the Legendre polynomial for a given degree `l` at the argument `x`. The argument needs to satisfy `-1 <= x <= 1`.
52-
* [`collectPl(x; lmax, [norm])`](@ref collectPl): this evaluates all the polynomials for `l` lying in `0:lmax` at the argument `x`. As before the argument needs to lie in the domain of validity. Functionally this is equivalent to `Pl.(x, 0:lmax)`, except `collectPl` evaluates the result in one pass, and is therefore faster. There is also the in-place version [`collectPl!`](@ref) that uses a pre-allocated array.
53-
* [`Plm(x, l, m; [norm])`](@ref Plm): this evaluates the associated Legendre polynomial ``P_\ell^m(x)`` at the argument ``x``. The argument needs to satisfy `-1 <= x <= 1`.
54-
* [`collectPlm(x; m, lmax, [norm])`](@ref collectPlm): this evaluates the associated Legendre polynomials with coefficient `m` for `l = abs(m):lmax`. There is also an in-place version [`collectPlm!`](@ref) that uses a pre-allocated array.
51+
* [`Pl(x,l; [norm = Val(:standard)])`](@ref Pl): this evaluates the Legendre polynomial for a given degree `l` at the argument `x`. The argument needs to satisfy `-1 <= x <= 1`.
52+
* [`collectPl(x; lmax, [lmin = 0], [norm = Val(:standard)])`](@ref collectPl): this evaluates all the polynomials for `l` lying in `0:lmax` at the argument `x`. As before the argument needs to lie in the domain of validity. Functionally this is equivalent to `Pl.(x, lmin:lmax)`, except `collectPl` evaluates the result in one pass, and is therefore faster. There is also the in-place version [`collectPl!`](@ref) that uses a pre-allocated array.
53+
* [`Plm(x, l, m; [norm = Val(:standard)])`](@ref Plm): this evaluates the associated Legendre polynomial ``P_\ell^m(x)`` at the argument ``x``. The argument needs to satisfy `-1 <= x <= 1`.
54+
* [`collectPlm(x; m, lmax, [lmin = abs(m)], [norm = Val(:standard)])`](@ref collectPlm): this evaluates the associated Legendre polynomials with coefficient `m` for `l = lmin:lmax`. There is also an in-place version [`collectPlm!`](@ref) that uses a pre-allocated array.
5555
* [`dnPl(x, l, n)`](@ref dnPl): this evaluates the ``n``-th derivative of the Legendre polynomial ``P_\ell(x)`` at the argument ``x``. The argument needs to satisfy `-1 <= x <= 1`.
5656
* [`collectdnPl(x; n, lmax)`](@ref collectdnPl): this evaluates the ``n``-th derivative of all the Legendre polynomials for `l = 0:lmax`. There is also an in-place version [`collectdnPl!`](@ref) that uses a pre-allocated array.
5757

@@ -77,7 +77,7 @@ julia> p ≈ 45/8 # analytical value
7777
true
7878
```
7979

80-
Evaluate the `n`th derivative for one `l` as `dnPl(x, l, n)`:
80+
Evaluate the `n`th derivative of `Pl` for one `l` as `dnPl(x, l, n)`:
8181

8282
```jldoctest
8383
julia> dnPl(0.5, 3, 2)
@@ -107,7 +107,7 @@ julia> collectPlm(0.5, lmax = 5, m = 3)
107107
-42.62468784251535
108108
```
109109

110-
Evaluate all the `n`th derivatives as `collectdnPl(x; lmax, n)`:
110+
Evaluate all the `n`th derivatives of `Pl` as `collectdnPl(x; lmax, n)`:
111111

112112
```jldoctest
113113
julia> collectdnPl(0.5, lmax = 5, n = 3)
@@ -120,43 +120,6 @@ julia> collectdnPl(0.5, lmax = 5, n = 3)
120120
65.625
121121
```
122122

123-
# Increase precision
124-
125-
The precision of the result may be changed by using arbitrary-precision types such as `BigFloat`. For example, using `Float64` arguments we obtain
126-
127-
```jldoctest
128-
julia> Pl(1/3, 5)
129-
0.33333333333333337
130-
```
131-
132-
whereas using `BigFloat`, we obtain
133-
134-
```jldoctest
135-
julia> Pl(big(1)/3, 5)
136-
0.3333333333333333333333333333333333333333333333333333333333333333333333333333305
137-
```
138-
139-
The precision of the latter may be altered using `setprecision`, as
140-
141-
```jldoctest
142-
julia> setprecision(300) do
143-
Pl(big(1)/3, 5)
144-
end
145-
0.33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333317
146-
```
147-
148-
This is particularly important to avoid overflow while computing high-order derivatives. For example:
149-
150-
```jldoctest
151-
julia> dnPl(0.5, 300, 200) # Float64
152-
NaN
153-
154-
julia> dnPl(big(1)/2, 300, 200) # BigFloat
155-
1.738632750542319394663553898425873258768856732308227932150592526951212145232716e+499
156-
```
157-
158-
In general, one would need to use higher precision for both the argument `x` and the degree `l` to obtain accurate results.
159-
160123
# Reference
161124

162125
```@autodocs

docs/src/tutorial.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
```@meta
2+
DocTestSetup = :(using LegendrePolynomials)
3+
```
4+
5+
## Computing normalized Legendre polynomials
6+
7+
By default, the Legendre and associated Legendre polynomials are not normalized.
8+
One may specify the normalization through the keyword argument `norm`.
9+
The normalization options accepted are
10+
11+
* `norm = Val(:standard)`: standard, unnormalized polynomials. This is the default option.
12+
* `norm = Val(:normalized)`: fully normalized polynomials with an L2 norm of 1
13+
14+
!!! note
15+
Irrespective of the norm specified, the 3-term recursion relations used are stable ones,
16+
so polynomials of high degrees may be computed without overflow.
17+
18+
```jldoctest
19+
julia> l = 3;
20+
21+
julia> Pl(0.5, l)
22+
-0.4375
23+
24+
julia> Pl(0.5, l, norm = Val(:normalized))
25+
-0.8184875533567997
26+
27+
julia> Pl(0.5, l, norm = Val(:normalized)) ≈ √((2l+1)/2) * Pl(0.5, l)
28+
true
29+
30+
julia> l = m = 3000;
31+
32+
julia> Plm(0.5, l, m, norm = Val(:normalized))
33+
2.172276347346834e-187
34+
```
35+
36+
## Condon-Shortley phase
37+
38+
By default, the associated Legendre polynomials are computed by including the Condon-Shortley phase ``(-1)^m``.
39+
This may be disabled by passing the flag `csphase = false` to the various `Plm` functions.
40+
41+
!!! note
42+
The `Pl` functions do not accept the keyword argument `csphase`.
43+
44+
```jldoctest
45+
julia> Plm(0.5, 3, 3)
46+
-9.742785792574933
47+
48+
julia> Plm(0.5, 3, 3, csphase = false)
49+
9.742785792574933
50+
51+
julia> all(Plm(0.5, 3, m, csphase = false) ≈ (-1)^m * Plm(0.5, 3, m) for m in -3:3)
52+
true
53+
```
54+
55+
## Polynomials at multiple points
56+
57+
The `collectPl(m)` functions return a vector of polynomials evaluated for one ``\theta``. To evaluate the polynomials for multiple ``\theta`` in one go, one may run
58+
59+
```jldoctest multipletheta
60+
julia> θr = range(0, pi, length=6);
61+
62+
julia> collectPl.(cos.(θr), lmax=3)
63+
6-element Vector{OffsetArrays.OffsetVector{Float64, Vector{Float64}}}:
64+
[1.0, 1.0, 1.0, 1.0]
65+
[1.0, 0.8090169943749475, 0.4817627457812106, 0.1102457514062632]
66+
[1.0, 0.30901699437494745, -0.35676274578121053, -0.38975424859373686]
67+
[1.0, -0.30901699437494734, -0.35676274578121064, 0.3897542485937368]
68+
[1.0, -0.8090169943749473, 0.48176274578121037, -0.11024575140626285]
69+
[1.0, -1.0, 1.0, -1.0]
70+
```
71+
72+
This returns a vector of vectors, where each element corresponds to one ``\theta``. Often, one wants a `Matrix` where
73+
each column corresponds to one ``\theta``. We may obtain this as
74+
75+
```jldoctest multipletheta
76+
julia> mapreduce(hcat, θr) do θ
77+
collectPl(cos(θ), lmax=3)
78+
end
79+
4×6 Matrix{Float64}:
80+
1.0 1.0 1.0 1.0 1.0 1.0
81+
1.0 0.809017 0.309017 -0.309017 -0.809017 -1.0
82+
1.0 0.481763 -0.356763 -0.356763 0.481763 1.0
83+
1.0 0.110246 -0.389754 0.389754 -0.110246 -1.0
84+
```
85+
86+
As of Julia `v1.7.2` and `OffsetArrays` `v1.10.8`, this strips off the axis information, so one would need to wrap the result in an `OffsetArray` again to index the `Matrix` using the degrees ``\ell``.
87+
88+
## Increasing precision
89+
90+
The precision of the result may be changed by using arbitrary-precision types such as `BigFloat`. For example, using `Float64` arguments we obtain
91+
92+
```jldoctest
93+
julia> Pl(1/3, 5)
94+
0.33333333333333337
95+
```
96+
97+
whereas using `BigFloat`, we obtain
98+
99+
```jldoctest
100+
julia> Pl(big(1)/3, 5)
101+
0.3333333333333333333333333333333333333333333333333333333333333333333333333333305
102+
```
103+
104+
This is particularly important to avoid overflow while computing high-order derivatives. For example:
105+
106+
```jldoctest
107+
julia> dnPl(0.5, 300, 200) # Float64
108+
NaN
109+
110+
julia> dnPl(big(1)/2, 300, 200) # BigFloat
111+
1.738632750542319394663553898425873258768856732308227932150592526951212145232716e+499
112+
```
113+
114+
In general, one would need to use higher precision for both the argument `x` and the degrees `l` to obtain accurate results. For example:
115+
116+
```jldoctest
117+
julia> Plm(big(1)/2, big(3000), 3000)
118+
2.05451584347939475644802290993338963448971107938391335496027846832433343889916e+9844
119+
```
120+
121+
## Symbolic evaluation
122+
123+
It's possible to symbolically evaluate Legendre or associated Legendre polynomials using [`Symbolics.jl`](https://github.com/JuliaSymbolics/Symbolics.jl). An exmaple is:
124+
125+
```jldoctest
126+
julia> using Symbolics
127+
128+
julia> @variables x;
129+
130+
julia> Pl(x, 3)
131+
(5//3)*x*((3//2)*(x^2) - (1//2)) - (2//3)*x
132+
133+
julia> myf = eval(build_function(Pl(x, 3), [x]));
134+
135+
julia> myf(0.4) == Pl(0.4, 3)
136+
true
137+
```

src/LegendrePolynomials.jl

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ function __init__()
2828
end
2929
end
3030

31-
function checkdomain(x)
32-
abs(x) > 1 && throw(DomainError(x,
31+
checkdomain(x) = true
32+
function checkdomain(x::Number)
33+
f = abs(x) > 1
34+
if f isa Bool
35+
f && throw(DomainError(x,
3336
"Legendre Polynomials are defined for arguments lying in -1 ⩽ x ⩽ 1"))
37+
end
38+
return nothing
3439
end
3540

3641
assertnonnegative(l::Integer) = l >= 0 || throw(ArgumentError("l must be >= 0, received " * string(l)))
@@ -119,9 +124,13 @@ function coeff(l, m, T = float(promote_type(typeof(l), typeof(m))))
119124
((2T(l) - 1)/((T(l)-m)*(T(l)+m))*(2T(l)+1))
120125
end
121126

122-
polytype(x) = float(typeof(x))
123-
polytype(m::Nothing) = Float64
124-
polytype(x, m) = promote_type(polytype(x), polytype(m))
127+
polytype(x) = typeof(float.(x))
128+
polytype(x::Array{<:Number}) = Array{promote_type(eltype(x), Float64), ndims(x)}
129+
polytype(x::Number) = float(typeof(x))
130+
polytype(x::Number, m::Number) = promote_type(polytype(x), polytype(m))
131+
polytype(x, m::Number) = typeof(float.(x) .* m)
132+
polytype(x::Array{<:Number}, m::Number) = Array{promote_type(eltype(x), Float64, typeof(m)), ndims(x)}
133+
polytype(x, ::Nothing) = polytype(x)
125134

126135
struct LegendrePolynomialIterator{T, M<:Union{Integer, Nothing}, V}
127136
x :: V
@@ -145,8 +154,8 @@ Base.IteratorSize(::Type{<:LegendrePolynomialIterator}) = Base.IsInfinite()
145154
# Iteration for Legendre polynomials
146155
# starting value, l == 0 for m == 0
147156
function Base.iterate(iter::LegendrePolynomialIterator{T,Nothing}) where {T}
148-
Pl = one(T)
149-
Plm1 = zero(T)
157+
Pl = convert(T, one(iter.x))
158+
Plm1 = convert(T, zero(iter.x))
150159
nextstate = (Pl, Plm1, nothing)
151160
return Pl, (1, nextstate)
152161
end
@@ -166,13 +175,13 @@ function Base.iterate(iter::LegendrePolynomialIterator{T,<:Integer}) where {T}
166175
m = iter.m
167176
x = iter.x
168177
if m == 0
169-
Pl = one(T)
178+
Pl = convert(T, one(x))
170179
else
171180
f = pll_prefactor(m, csphase = iter.csphase)
172-
t = f * ((1-x^2))^m
181+
t = f * ((one(x) - x^2))^m
173182
Pl = convert(T, t)
174183
end
175-
Plm1 = zero(T)
184+
Plm1 = convert(T, zero(x))
176185
α_ℓ_m = Inf
177186
return Pl, (m+1, (Pl, Plm1, α_ℓ_m))
178187
end
@@ -206,7 +215,7 @@ function maybenormalize(P, l, m, norm::Val{:standard}, csphase = true)
206215
if m == 0
207216
return maybenormalize(P, l, norm)
208217
else
209-
return (m < 0 && csphase ? neg1pow(m) : 1) * plm_norm(l, m) * P
218+
return (m < 0 ? neg1pow(m) : 1) * plm_norm(l, m) * P
210219
end
211220
end
212221
maybenormalize(P, l, m, norm, args...) = throw(ArgumentError("norm = $norm undefined, valid norms are Val(:standard) and Val(:normalized)"))

test/Project.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
[deps]
22
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
33
HyperDualNumbers = "50ceba7f-c3ee-5a84-a6e8-3ad40456ec97"
4+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
45
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
6+
PerformanceTestTools = "dc46b164-d16f-48ec-a853-60448fc869fe"
57
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
68
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
9+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
710
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
811

912
[compat]
1013
Aqua = "0.5"
1114
HyperDualNumbers = "4"
15+
PerformanceTestTools = "0.1"
1216
QuadGK = "2"
17+
Symbolics = "4"

0 commit comments

Comments
 (0)