Skip to content

Commit cdbd629

Browse files
authored
Merge pull request #36 from hersle/lensing
Add CMB lensing power spectrum
2 parents 993d17b + cdcaf9f commit cdbd629

File tree

6 files changed

+117
-53
lines changed

6 files changed

+117
-53
lines changed

docs/src/comparison.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function solve_class(pars, k = nothing)
3232
"write_background" => "yes",
3333
"write_thermodynamics" => "yes",
3434
"background_verbose" => 2,
35-
"output" => "mPk, tCl, pCl", # need one to evolve perturbations
35+
"output" => "mPk, tCl, pCl, lCl", # need one to evolve perturbations
3636
3737
"k_output_values" => isnothing(k) ? "" : NoUnits(k / u"1/Mpc"),
3838
"ic" => "ad",
@@ -406,16 +406,25 @@ function Dl_symboltz(modes, jl, pars; kwargs...)
406406
end
407407
408408
l = 20:20:2000 # CLASS default is lmax = 2500
409-
Dl1 = Dl_class([:TT, :TE, :EE], l, pars)
409+
Dl1 = Dl_class([:TT, :TE, :EE, :phiphi, :TPhi, :Ephi], l, pars)
410410
jl = SphericalBesselCache(l)
411-
Dl2 = Dl_symboltz([:TT, :TE, :EE], jl, pars)
412-
plot_compare(l, l, Dl1[:, 1], Dl2[:, 1], "l", "Dₗ(TT)"; tol = 9e-13)
411+
Dl2 = Dl_symboltz([:TT, :TE, :EE, :ψψ, :ψT, :ψE], jl, pars)
412+
plot_compare(l, l, Dl1[:, 1], Dl2[:, 1], "l", "Dₗ(TT)"; tol = 2e-12)
413413
```
414414
```@example class
415-
plot_compare(l, l, Dl1[:, 2], Dl2[:, 2], "l", "Dₗ(TE)"; tol = 2e-14)
415+
plot_compare(l, l, Dl1[:, 2], Dl2[:, 2], "l", "Dₗ(TE)"; tol = 3e-14)
416416
```
417417
```@example class
418-
plot_compare(l, l, Dl1[:, 3], Dl2[:, 3], "l", "Dₗ(EE)"; tol = 6e-15)
418+
plot_compare(l, l, Dl1[:, 3], Dl2[:, 3], "l", "Dₗ(EE)"; tol = 8e-15)
419+
```
420+
```@example class
421+
plot_compare(l, l, Dl1[:, 4], Dl2[:, 4], "l", "Dₗ(ψψ)"; tol = 5e-13)
422+
```
423+
```@example class
424+
plot_compare(l, l, Dl1[:, 5], Dl2[:, 5], "l", "Dₗ(ψT)"; tol = 3e-13)
425+
```
426+
```@example class
427+
plot_compare(l, l, Dl1[:, 6], Dl2[:, 6], "l", "Dₗ(ψE)"; tol = 3e-15)
419428
```
420429
```@example class
421430
diffpars = [M.c.Ω₀, M.b.Ω₀, M.g.h]

docs/src/observables.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,17 @@ SymBoltz.spectrum_cmb
5555

5656
```@example
5757
# TODO: more generic, source functions, ... # hide
58-
using SymBoltz, Unitful, UnitfulAstro, Plots
58+
using SymBoltz, Plots
5959
M = SymBoltz.ΛCDM()
6060
pars = SymBoltz.parameters_Planck18(M)
6161
prob = CosmologyProblem(M, pars)
62-
ls = 10:5:1500
6362
63+
ls = 25:25:3000 # 25, 50, ..., 3000
6464
jl = SphericalBesselCache(ls)
65-
Dls = spectrum_cmb([:TT, :EE, :TE], prob, jl; normalization = :Dl, unit = u"μK")
66-
pTT = plot(ls, Dls[:, 1]; ylabel = "Dₗᵀᵀ")
67-
pEE = plot(ls, Dls[:, 2]; ylabel = "Dₗᴱᴱ")
68-
pTE = plot(ls, Dls[:, 3]; ylabel = "Dₗᵀᴱ", xlabel = "l")
69-
plot(pTT, pEE, pTE, layout = (3, 1), size = (600, 700), legend = nothing)
65+
modes = [:TT, :EE, :TE, :ψψ, :ψT, :ψE]
66+
Dls = spectrum_cmb(modes, prob, jl; normalization = :Dl)
67+
68+
Plots.plot(ls, log10.(abs.(Dls)); xlabel = "l", ylabel = "lg(Dₗ)", label = permutedims(String.(modes)))
7069
```
7170

7271
## Two-point correlation function

src/models/cosmologies.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ function ΛCDM(;
4646
pars = @parameters begin
4747
C, [description = "Initial conditions integration constant"]
4848
τ0, [description = "Conformal time today"]
49+
τrec, [description = "Conformal time of recombination"]
4950
end
5051
vars = @variables begin
5152
χ(τ), [description = "Conformal lookback time from today"]
@@ -56,10 +57,12 @@ function ΛCDM(;
5657
ST0_ISW(τ, k), ST1_ISW(τ, k),
5758
ST0_Doppler(τ, k), ST1_Doppler(τ, k),
5859
ST0_polarization(τ, k), ST1_polarization(τ, k), ST2_polarization(τ, k), SE_kχ²(τ, k)
60+
(τ, k), [description = "Lensing source function"]
5961
end
6062
defs = Dict(
6163
C => 1//2,
6264
τ0 => NaN,
65+
τrec => NaN,
6366
D(g.a) => g.a / τ, # ℋ ≈ 1 / τ
6467
)
6568
guesses = Dict(
@@ -101,6 +104,7 @@ function ΛCDM(;
101104
ST0 ~ ST0_SW + ST0_ISW + ST0_Doppler + ST0_polarization
102105
ST1 ~ 0
103106
SE_kχ² ~ 3/16 * b.v*γ.Π
107+
~ ifelse τrec, -(g.Ψ+g.Φ) *-τrec)/(τ0-τrec)/(τ0-τ), 0)
104108
])
105109
# TODO: do various initial condition types (adiabatic, isocurvature, ...) from here?
106110
# TODO: automatically solve for initial conditions following e.g. https://arxiv.org/pdf/1012.0569 eq. (1)?

src/observables/angular.jl

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct SphericalBesselCache{Tx}
1414
x::Tx
1515
end
1616

17-
function SphericalBesselCache(ls::AbstractVector; xmax = 3*ls[end], dx = 2π/48)
17+
function SphericalBesselCache(ls::AbstractVector; xmax = 10*ls[end], dx = 2π/48)
1818
xmin = 0.0
1919
xs = range(xmin, xmax, length = trunc(Int, (xmax - xmin) / dx)) # fixed length (so endpoints are exact) that gives step as close to dx as possible
2020
invdx = 1.0 / step(xs) # using the resulting step, which need not be exactly dx
@@ -78,22 +78,29 @@ ChainRulesCore.frule((_, _, Δx), ::typeof(jl), l, x) = jl(l, x), jl′(l, x) *
7878
# TODO: use u = k*χ as integration variable, so oscillations of Bessel functions are the same for every k?
7979
# TODO: define and document symbolic dispatch!
8080
"""
81-
los_integrate(Ss::AbstractMatrix{T}, ls::AbstractVector, τs::AbstractVector, ks::AbstractVector, jl::SphericalBesselCache; integrator = TrapezoidalRule(), verbose = false) where {T <: Real}
81+
los_integrate(Ss::AbstractMatrix{T}, ls::AbstractVector, τs::AbstractVector, ks::AbstractVector, jl::SphericalBesselCache; l_limber = typemax(Int), integrator = TrapezoidalRule(), verbose = false) where {T <: Real}
8282
8383
For the given `ls` and `ks`, compute the line-of-sight-integrals
8484
```math
8585
Iₗ(k) = ∫dτ S(k,τ) jₗ(k(τ₀-τ))
8686
```
8787
over the source function values `Ss` against the spherical Bessel functions ``jₗ(x)`` cached in `jl`.
8888
The element `Ss[i,j]` holds the source function value ``S(kᵢ, τⱼ)``.
89+
The limber approximation
90+
```math
91+
Iₗ ≈ √(π/(2l+1)) S(k,τ₀-(l+1/2)/k)
92+
```
93+
is used for `l ≥ l_limber`.
8994
"""
90-
function los_integrate(Ss::AbstractMatrix{T}, ls::AbstractVector, τs::AbstractVector, ks::AbstractVector, jl::SphericalBesselCache; integrator = TrapezoidalRule(), verbose = false) where {T <: Real}
95+
function los_integrate(Ss::AbstractMatrix{T}, ls::AbstractVector, τs::AbstractVector, ks::AbstractVector, jl::SphericalBesselCache; l_limber = typemax(Int), integrator = TrapezoidalRule(), verbose = false) where {T <: Real}
9196
# Julia is column-major; make sure innermost loop indices appear first in slice expressions (https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-column-major)
9297
@assert size(Ss) == (length(τs), length(ks)) "size(Ss) = $(size(Ss))$((length(τs), length(ks)))"
9398
τs = collect(τs) # force array to avoid floating point errors with ranges in following χs due to (e.g. tiny negative χ)
9499
χs = τs[end] .- τs
95100
Is = similar(Ss, length(ks), length(ls))
96101

102+
verbose && l_limber < typemax(Int) && println("Using Limber approximation for l ≥ $l_limber")
103+
97104
# TODO: skip and set jl to zero if l ≳ kτ0 or another cutoff?
98105
@tasks for il in eachindex(ls) # parallellize independent loop iterations
99106
@local begin # define task-local values (declared once for all loop iterations)
@@ -103,13 +110,30 @@ function los_integrate(Ss::AbstractMatrix{T}, ls::AbstractVector, τs::AbstractV
103110
verbose && print("\rLOS integrating with l = $l")
104111
for ik in eachindex(ks)
105112
k = ks[ik]
106-
forin eachindex(τs)
107-
S = Ss[iτ,ik]
108-
χ = χs[iτ]
109-
= k * χ
110-
∂I_∂τ[iτ] = S * jl(l, kχ) # TODO: rewrite LOS integral to avoid evaluating jl with dual numbers? # TODO: rewrite LOS integral with y = kτ0 and x=τ/τ0 to cache jls independent of cosmology
113+
if l l_limber
114+
χ = (l+1/2) / k
115+
if χ > χs[1]
116+
# χ > χini > χrec, so source function is definitely zero
117+
S = 0.0
118+
else
119+
# interpolate between two closest points in saved array
120+
iχ₋ = searchsortedfirst(χs, χ; rev = true)
121+
iχ₊ = iχ₋ - 1
122+
χ₋, χ₊ = χs[iχ₋], χs[iχ₊] # now χ₋ < χ < χ₊
123+
S₋, S₊ = Ss[iχ₋,ik], Ss[iχ₊,ik]
124+
S = S₋ + (S₊-S₋) *-χ₋) / (χ₊-χ₋)
125+
end
126+
I = /(2l+1)) * S / k
127+
else
128+
forin eachindex(τs)
129+
S = Ss[iτ,ik]
130+
χ = χs[iτ]
131+
= k * χ
132+
∂I_∂τ[iτ] = S * jl(l, kχ) # TODO: rewrite LOS integral to avoid evaluating Rl with dual numbers? # TODO: rewrite LOS integral with y = kτ0 and x=τ/τ0 to cache jls independent of cosmology
133+
end
134+
I = integrate(τs, ∂I_∂τ; integrator) # integrate over τ # TODO: add starting I(τini) to fix small l?
111135
end
112-
Is[ik,il] = integrate(τs, ∂I_∂τ; integrator) # integrate over τ # TODO: add starting I(τini) to fix small l?
136+
Is[ik,il] = I
113137
end
114138
end
115139
verbose && println()
@@ -181,46 +205,67 @@ function spectrum_cmb(ΘlAs::AbstractMatrix, ΘlBs::AbstractMatrix, P0s::Abstrac
181205
end
182206

183207
"""
184-
spectrum_cmb(modes::AbstractVector, prob::CosmologyProblem, jl::SphericalBesselCache; normalization = :Cl, unit = nothing, kτ0s = 0.1*jl.l[begin]:2π/2:2*jl.l[end], u = (τ->tanh(τ)), u⁻¹ = (u->atanh(u)), Nlos = 768, integrator = TrapezoidalRule(), bgopts = (alg = Rodas4P(), reltol = 1e-9, abstol = 1e-9), ptopts = (alg = KenCarp4(), reltol = 1e-8, abstol = 1e-8), sourceopts = (rtol = 1e-2,), verbose = false, kwargs...)
208+
spectrum_cmb(modes::AbstractVector{<:Symbol}, prob::CosmologyProblem, jl::SphericalBesselCache; normalization = :Cl, unit = nothing, kτ0s = 0.1*jl.l[begin]:2π/2:10*jl.l[end], xs = 0.0:0.0008:1.0, l_limber = 50, integrator = TrapezoidalRule(), bgopts = (alg = Rodas4P(), reltol = 1e-9, abstol = 1e-9), ptopts = (alg = KenCarp4(), reltol = 1e-8, abstol = 1e-8), sourceopts = (rtol = 1e-3, atol = 0.9), verbose = false, kwargs...)
185209
186-
Compute the CMB power spectra `modes` (`:TT`, `:EE`, `:TE` or an array thereof) ``C_l^{AB}``'s at angular wavenumbers `ls` from the cosmological solution `sol`.
210+
Compute angular CMB power spectra ``Cₗᴬᴮ`` at angular wavenumbers `ls` from the cosmological problem `prob`.
211+
The requested `modes` are specified as a vector of symbols in the form `:AB`, where `A` and `B` are `T` (temperature), `E` (E-mode polarization) or `ψ` (lensing).
187212
If `unit` is `nothing` the spectra are of dimensionless temperature fluctuations relative to the present photon temperature; while if `unit` is a temperature unit the spectra are of dimensionful temperature fluctuations.
213+
Returns a matrix of ``Cₗ`` if `normalization` is `:Cl`, or ``Dₗ = l(l+1)/2π`` if `normalization` is `:Dl`.
214+
215+
The lensing line-of-sight integral uses the Limber approximation for `l ≥ l_limber`.
216+
217+
# Examples
218+
219+
```julia
220+
using SymBoltz, Unitful
221+
M = ΛCDM()
222+
pars = parameters_Planck18(M)
223+
prob = CosmologyProblem(M, pars)
224+
225+
ls = 10:10:1000
226+
jl = SphericalBesselCache(ls)
227+
modes = [:TT, :TE, :ψψ, :ψT]
228+
Dls = spectrum_cmb(modes, prob, jl; normalization = :Dl, unit = u"μK")
229+
```
188230
"""
189-
function spectrum_cmb(modes::AbstractVector, prob::CosmologyProblem, jl::SphericalBesselCache; normalization = :Cl, unit = nothing, kτ0s = 0.1*jl.l[begin]:2π/2:2*jl.l[end], u = ->tanh(τ)), u⁻¹ = (u->atanh(u)), Nlos = 768, integrator = TrapezoidalRule(), bgopts = (alg = Rodas4P(), reltol = 1e-9, abstol = 1e-9), ptopts = (alg = KenCarp4(), reltol = 1e-8, abstol = 1e-8), sourceopts = (rtol = 1e-2,), verbose = false, kwargs...)
231+
function spectrum_cmb(modes::AbstractVector{<:Symbol}, prob::CosmologyProblem, jl::SphericalBesselCache; normalization = :Cl, unit = nothing, kτ0s = 0.1*jl.l[begin]:2π/2:10*jl.l[end], xs = 0.0:0.0008:1.0, l_limber = 50, integrator = TrapezoidalRule(), bgopts = (alg = Rodas4P(), reltol = 1e-9, abstol = 1e-9), ptopts = (alg = KenCarp4(), reltol = 1e-8, abstol = 1e-8), sourceopts = (rtol = 1e-3, atol = 0.9), verbose = false, kwargs...)
190232
ls = jl.l
191233
sol = solve(prob; bgopts, verbose)
192234
τ0 = getsym(sol, prob.M.τ0)(sol)
193235
ks_fine = collect(kτ0s ./ τ0)
236+
194237
τs = sol.bg.t # by default, use background (thermodynamics) time points for line of sight integration
195-
τi = τs[begin]
196-
if Nlos != 0 # instead choose Nlos time points τ = τ(u) corresponding to uniformly spaced u
197-
τmin, τmax = extrema(τs)
198-
umin, umax = u(τmin), u(τmax)
199-
us = range(umin, umax, length = Nlos)
200-
τs = u⁻¹.(us)
201-
τs[begin] = τi
202-
τs[end] = τ0
238+
if !isnothing(xs)
239+
# use user's array of x = (τ-τi)/(τ0-τi)
240+
xs[begin] == 0 || error("xs begins with $(xs[begin]), but should begin with 0")
241+
xs[end] == 1 || error("xs ends with $(xs[end]), but should end with 1")
242+
τs = τs[begin] .+ (τs[end] .- τs[begin]) .* xs
203243
end
204244

205245
# Integrate perturbations to calculate source function on coarse k-grid
206246
iT = 'T' in join(modes) ? 1 : 0
207247
iE = 'E' in join(modes) ? iT + 1 : 0
208-
Ss = Num[]
209-
iT > 0 && push!(Ss, prob.M.ST0)
210-
iE > 0 && push!(Ss, prob.M.SE_kχ²)
248+
= 'ψ' in join(modes) ? max(iE, iT) + 1 : 0
249+
Ss = [prob.M.ST0, prob.M.SE_kχ², prob.M.Sψ]
211250
ks_coarse = range(ks_fine[begin], ks_fine[end]; length = 2)
212251
ks_coarse, Ss_coarse = source_grid_adaptive(prob, Ss, τs, ks_coarse; bgopts, ptopts, verbose, sourceopts...) # TODO: thread flag # TODO: pass kτ0 and x # TODO: pass bgsol
213252

214253
# Interpolate source function to finer k-grid
215254
Ss_fine = source_grid(Ss_coarse, ks_coarse, ks_fine)
216-
if iE > 0
217-
χs = τs[end] .- τs
218-
Ss_fine[iE, :, :] ./= (ks_fine' .* χs) .^ 2
219-
end
255+
χs = τs[end] .- τs
256+
Ss_fine[2, :, :] ./= (ks_fine' .* χs) .^ 2
220257
Ss_fine[:, end, :] .= 0.0 # can be Inf, but is always weighted by zero-valued spherical Bessel function in LOS integration
221258

222-
ΘlTs = iT > 0 ? los_integrate(@view(Ss_fine[iT, :, :]), ls, τs, ks_fine, jl; integrator, verbose, kwargs...) : nothing
223-
ΘlEs = iE > 0 ? los_integrate(@view(Ss_fine[iE, :, :]), ls, τs, ks_fine, jl; integrator, verbose, kwargs...) .* transpose(@. ((ls+2)*(ls+1)*(ls+0)*(ls-1))) : nothing
259+
Θls = zeros(eltype(Ss_fine), max(iT, iE, iψ), length(ks_fine), length(ls))
260+
if iT > 0
261+
Θls[iT, :, :] .= los_integrate(@view(Ss_fine[1, :, :]), ls, τs, ks_fine, jl; integrator, verbose, kwargs...)
262+
end
263+
if iE > 0
264+
Θls[iE, :, :] .= los_integrate(@view(Ss_fine[2, :, :]), ls, τs, ks_fine, jl; integrator, verbose, kwargs...) .* transpose(@. ((ls+2)*(ls+1)*(ls+0)*(ls-1)))
265+
end
266+
if> 0
267+
Θls[iψ, :, :] .= los_integrate(@view(Ss_fine[3, :, :]), ls, τs, ks_fine, jl; l_limber, integrator, verbose, kwargs...)
268+
end
224269

225270
P0s = spectrum_primordial(ks_fine, sol) # more accurate
226271

@@ -232,17 +277,21 @@ function spectrum_cmb(modes::AbstractVector, prob::CosmologyProblem, jl::Spheric
232277
error("Requested unit $unit is not a temperature unit")
233278
end
234279

280+
function geti(mode)
281+
mode == :T && return iT
282+
mode == :E && return iE
283+
mode == && return
284+
error("Unknown CMB power spectrum mode $mode")
285+
end
286+
235287
spectra = zeros(eltype(Ss_fine[1,1,1] * P0s[1] * factor^2), length(ls), length(modes)) # Cls or Dls
236288
for (i, mode) in enumerate(modes)
237-
if mode == :TT
238-
spectrum = spectrum_cmb(ΘlTs, ΘlTs, P0s, ls, ks_fine; integrator, normalization)
239-
elseif mode == :EE
240-
spectrum = spectrum_cmb(ΘlEs, ΘlEs, P0s, ls, ks_fine; integrator, normalization)
241-
elseif mode == :TE
242-
spectrum = spectrum_cmb(ΘlTs, ΘlEs, P0s, ls, ks_fine; integrator, normalization)
243-
else
244-
error("Unknown CMB power spectrum mode $mode")
245-
end
289+
mode = String(mode)
290+
iA = geti(Symbol(mode[firstindex(mode)]))
291+
iB = geti(Symbol(mode[lastindex(mode)]))
292+
ΘlAs = @view(Θls[iA, :, :])
293+
ΘlBs = @view(Θls[iB, :, :])
294+
spectrum = spectrum_cmb(ΘlAs, ΘlBs, P0s, ls, ks_fine; integrator, normalization)
246295
spectrum *= factor^2 # possibly make dimensionful
247296
spectra[:, i] .= spectrum
248297
end

src/observables/fourier.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ function source_grid_refine(i1, i2, ks, Ss, Sk, savelock; verbose = false, kwarg
241241

242242
# check if interpolation is close enough for all sources
243243
# (equivalent to finding the source grid of each source separately)
244-
@sync if !all(isapprox(S[iS, :], Sint[iS, :]; kwargs...) for iS in 1:size(Ss, 1))
244+
@sync if !all(isapprox(S[iS, begin:end-1], Sint[iS, begin:end-1]; kwargs...) for iS in 1:size(Ss, 1)) # exclude χ = 0 from comparison, where some sources diverge with 1/χ
245245
@spawn source_grid_refine(i1, i, ks, Ss, Sk, savelock; verbose, kwargs...) # refine left subinterval
246246
@spawn source_grid_refine(i, i2, ks, Ss, Sk, savelock; verbose, kwargs...) # refine right subinterval
247247
end
@@ -272,7 +272,6 @@ function source_grid_adaptive(prob::CosmologyProblem, Ss::AbstractVector, τs, k
272272
ptprob = ptprobgen(ptprob0, k)
273273
ptsol = solvept(ptprob; saveat = τs, ptopts...)
274274
S = transpose(stack(map(getS -> getS(ptsol), getSs)))
275-
S[:, end] .= 0.0 # avoid NaN/Infs from 1/χ today; fine in LOS integration, since always weighted by 0-valued Bessel function # TODO: avoid
276275
return S
277276
end
278277

src/solve.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ function solvebg(bgprob::ODEProblem; alg = DEFAULT_BGALG, reltol = 1e-9, abstol
332332
if !successful_retcode(bgsol)
333333
@warn warning_failed_solution(bgsol, "Background"; verbose)
334334
end
335+
if Symbol("τrec") in Symbol.(parameters(bgprob.f.sys))
336+
τrecidx = ModelingToolkit.parameter_index(bgprob, :τrec)
337+
bgsol.ps[τrecidx] = bgsol[][argmax(bgsol[bgprob.f.sys.b.v])]
338+
end
335339
return bgsol
336340
end
337341
# TODO: more generic shooting method that can do anything (e.g. S8)

0 commit comments

Comments
 (0)