Field Basics
using CMBLensing
Base Fields
The basic building blocks of CMBLensing.jl are CMB "fields", like temperature, Q or U polarization, or the lensing potential $\phi$. These types are all encompassed by the abstract type Field
, with some concrete examples including FlatMap
for a flat-sky map projection, or FlatQUMap
for Q/U polarization, etc...
Flat
fields are just thin wrappers around Julia arrays, e.g.
Ix = rand(2,2)
2×2 Matrix{Float64}:
0.791579 0.122001
0.747891 0.484126
f = FlatMap(Ix)
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.7915791207108701
0.7478911009853889
0.12200078444323892
0.4841256229149363
When displayed, you can see the pixels in the 2x2 map have been splayed out into a length-4 array. This is intentional, as even though the maps themselves are two-dimensional, it is extremely useful conceptually to think of fields as vectors (which they are, in fact, as they form an abstract vector space). This tie to vector spaces is deeply rooted in CMBLensing, to the extent that Field
objects are a subtype of Julia's own AbstractVector
type,
f isa AbstractVector
true
The data itself, however, is still stored as the original 2x2 matrix, and can be accessed as follows,
f.Ix
2×2 view(::Matrix{Float64}, :, :) with eltype Float64:
0.791579 0.122001
0.747891 0.484126
But since Fields
are vectors, they can be tranposed,
f'
1×4 adjoint(::LambertMap{Array{Float64, 2}}) with eltype Float64:
0.791579 0.747891 0.122001 0.484126
inner products can be computed,
f' * f
1.4352004134460723
and they can be added with each other as well as multiplied by scalars,
2*f+f
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
2.37473736213261
2.2436733029561666
0.36600235332971676
1.452376868744809
Diagonal operators
Vector spaces have linear operators which act on the vectors. Linear operators correpsond to matrices, thus for a map with $N$ total pixels, a general linear operator would be an $N$-by-$N$ matrix, which for even modest map sizes becomes far too large to actually store. Thus, an important class of linear operators are ones which are diagonal, since these can actually be stored. CMBLensing uses Julia's builtin Diagonal
to represent these. Diagonal(f)
takes a vector f
and puts it on the diagonal of the matrix:
Diagonal(f)
4×4 Diagonal{Float64, BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}}:
0.791579 ⋅ ⋅ ⋅
⋅ 0.747891 ⋅ ⋅
⋅ ⋅ 0.122001 ⋅
⋅ ⋅ ⋅ 0.484126
Multiplying this operator by the original map is then a matrix-vector product:
Diagonal(f) * f
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.6265975043453943
0.5593410989331372
0.014884191404765648
0.23437761876277513
Note that this is also equal to the the pointwise multiplication of f
with itself:
f .* f
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.6265975043453943
0.5593410989331372
0.014884191404765648
0.23437761876277513
Field Tuples
You can put Fields
together into tuples. For example,
a = FlatMap(rand(2,2))
b = FlatMap(rand(2,2));
FieldTuple(a,b)
8-element Field-2-Tuple{BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}}:
0.05553470245958003
0.5613686533003903
0.15598472262801066
0.3990035727328386
0.019565391929818765
0.08390502755152796
0.7022876296827427
0.689956425641495
The components can also have names:
ft = FieldTuple(a=a, b=b)
8-element Field-(a,b)-Tuple{BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}}:
0.05553470245958003
0.5613686533003903
0.15598472262801066
0.3990035727328386
0.019565391929818765
0.08390502755152796
0.7022876296827427
0.689956425641495
which can be accessed later:
ft.a
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.05553470245958003
0.5613686533003903
0.15598472262801066
0.3990035727328386
FieldTuples
have all of the same behavior of individual fields. Indeed, spin fields like QU or IQU are simply special FieldTuples
:
fqu = FlatQUMap(a,b)
fqu isa FieldTuple
false
Field Vectors
in progress
Basis Conversion
All fields are tagged as to which basis they are stored in. You can convert them to other bases by calling the basis type on them:
f
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.7915791207108701
0.7478911009853889
0.12200078444323892
0.4841256229149363
g = Fourier(f)
4-element 2×2-pixel 1.0′-resolution LambertFourier{Array{ComplexF64, 2}}:
2.145596629054434 + 0.0im
-0.3184368187462162 + 0.0im
0.9333438143380838 + 0.0im
0.4058128581971786 + 0.0im
Basis conversion is usually done automatically for you. E.g. here f′
is automatically converted to a FlatMap
before addition:
f + g
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
1.5831582414217402
1.4957822019707778
0.24400156888647778
0.9682512458298727
A key feature of Diagonal
operators is they convert the field they are acting on to the right basis before multiplication:
Diagonal(f) * g
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.6265975043453943
0.5593410989331372
0.014884191404765641
0.2343776187627751
A FlatMap
times a FlatFourier
doesn't have a natural linear algebra meaning so its an error:
f * g
MethodError: no method matching *(::BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, ::BaseField{Fourier, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, ComplexF64, Matrix{ComplexF64}})
Closest candidates are:
*(::Any, ::Any, ::Any, ::Any...)
@ Base operators.jl:578
*(::Field, ::LinearAlgebra.Adjoint{<:Any, <:StaticArraysCore.StaticArray{Tuple{2}, F, 1}} where F<:Union{Field, FieldOp})
@ CMBLensing ~/CMBLensing/src/field_vectors.jl:35
*(::Field, ::StaticArraysCore.StaticArray{Tuple{2}, F, 1} where F<:Union{Field, FieldOp})
@ CMBLensing ~/CMBLensing/src/field_vectors.jl:34
...
Stacktrace:
[1] top-level scope
@ In[21]:1
Properties and indices
FlatMap
and FlatFourier
can be indexed directly like arrays. If given 1D indices, this is the index into the vector representation:
f
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.7915791207108701
0.7478911009853889
0.12200078444323892
0.4841256229149363
f[1], f[2], f[3], f[4]
(0.7915791207108701, 0.7478911009853889, 0.12200078444323892, 0.4841256229149363)
f[5]
BoundsError: attempt to access 2×2 Matrix{Float64} at index [5]
Stacktrace:
[1] getindex
@ ./essentials.jl:13 [inlined]
[2] getindex(f::BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, I::Int64)
@ CMBLensing ~/CMBLensing/src/base_fields.jl:36
[3] top-level scope
@ In[24]:1
Or with a 2D index, this indexes directly into the 2D map:
f[1,1], f[2,1], f[1,2], f[2,2]
(0.7915791207108701, 0.7478911009853889, 0.12200078444323892, 0.4841256229149363)
Note: there is no overhead to indexing f
in this way as compared to working directly on the underlying array.
For other fields which are built on FieldTuples
, 1D indexing will instead index the tuple indices:
ft
8-element Field-(a,b)-Tuple{BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}}:
0.05553470245958003
0.5613686533003903
0.15598472262801066
0.3990035727328386
0.019565391929818765
0.08390502755152796
0.7022876296827427
0.689956425641495
ft[1]
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.05553470245958003
0.5613686533003903
0.15598472262801066
0.3990035727328386
ft[2]
4-element 2×2-pixel 1.0′-resolution LambertMap{Array{Float64, 2}}:
0.019565391929818765
0.08390502755152796
0.7022876296827427
0.689956425641495
ft[3]
BoundsError: attempt to access NamedTuple{(:a, :b), Tuple{BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}}} at index [3]
Stacktrace:
[1] getindex
@ ./namedtuple.jl:136 [inlined]
[2] getindex(f::FieldTuple{NamedTuple{(:a, :b), Tuple{BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}, BaseField{Map, ProjLambert{Float64, Vector{Float64}, Matrix{Float64}}, Float64, Matrix{Float64}}}}, CMBLensing.BasisProd{Tuple{Map, Map}}, Float64}, i::Int64)
@ CMBLensing ~/CMBLensing/src/field_tuples.jl:33
[3] top-level scope
@ In[29]:1
To get the underlying data arrays, use the object's properties:
f.Ix
2×2 view(::Matrix{Float64}, :, :) with eltype Float64:
0.791579 0.122001
0.747891 0.484126
You can always find out what properties are available by typing f.<Tab>
. For example, if you typed ft
then hit <Tab>
you'd get:
ft |> propertynames
(:fs, :a, :b)
For a FieldTuple
like the FlatQUMap
object, fqu
, you can get each individual Q or U field:
fqu.Q
4-element 2×2-pixel 1.0′-resolution LambertMap{SubArray{Float64, 2, Array{Float64, 3}, Tuple{Base.Slice{Base.OneTo{Int64}}, Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
0.05553470245958003
0.5613686533003903
0.15598472262801066
0.3990035727328386
Or fqu.Qx
which is shorthand for fqu.Q.Ix
:
fqu.Q.Ix === fqu.Qx
true
If you convert f
to Fourier space, it would have the Il
property to get the Fourier coefficients of the $I$ component:
Fourier(f).Il
2×2 view(::Matrix{ComplexF64}, :, :) with eltype ComplexF64:
2.1456+0.0im 0.933344+0.0im
-0.318437+0.0im 0.405813+0.0im
For convenience, you can index fields with brackets []
and any necessary conversions will be done automatically:
f[:Il]
2×2 view(::Matrix{ComplexF64}, :, :) with eltype ComplexF64:
2.1456+0.0im 0.933344+0.0im
-0.318437+0.0im 0.405813+0.0im
This works between any bases. For example. fqu
is originally QUMap
but we can convert to EBFourier
and get the El
coefficients:
fqu[:El]
2×2 view(::Array{ComplexF64, 3}, :, :, 1) with eltype ComplexF64:
-1.17189-0.0im -0.0619151+0.0im
-0.748853+0.0im 0.0766708+0.0im
The general rule to keep in mind for these two ways of accessing the underlying data is:
- Properties (i.e.
f.Ix
) are type-stable and get you the underlying data arrays, even recursively from specialFieldTuples
likeFlatQUMap
, etc... If these arrays are modified, they affect the original field. - Indices (i.e.
f[:Ix]
) are not type-stable, and may or may not be one of the underlying data arrays (because a basis conversion may have been performed). They should be used for getting (not setting) data, and in non-performance-critical code.