cmbstack 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cmbstack/__init__.py +1 -0
- cmbstack/main.py +103 -0
- cmbstack/maps.py +136 -0
- cmbstack/stacking.py +198 -0
- cmbstack-0.0.1.dist-info/METADATA +171 -0
- cmbstack-0.0.1.dist-info/RECORD +8 -0
- cmbstack-0.0.1.dist-info/WHEEL +5 -0
- cmbstack-0.0.1.dist-info/top_level.txt +1 -0
cmbstack/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
cmbstack/main.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""High-level stacking pipeline for HEALPix CMB maps.
|
|
2
|
+
|
|
3
|
+
:class:`StackingPipeline` runs the full analysis in one place:
|
|
4
|
+
simulate (or load) a map, detect peaks, extract gnomonic patches, stack them,
|
|
5
|
+
and compute a radial profile. The pipeline stores every intermediate product
|
|
6
|
+
as an attribute so individual steps can be inspected after the run.
|
|
7
|
+
|
|
8
|
+
Typical use
|
|
9
|
+
-----------
|
|
10
|
+
>>> pipeline = StackingPipeline.from_cl("path/to/cl.txt", nside=1024, seed=42)
|
|
11
|
+
>>> pipeline.run()
|
|
12
|
+
>>> plt.imshow(pipeline.stacked)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from . import maps, stacking
|
|
16
|
+
import numpy as np
|
|
17
|
+
import healpy as hp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StackingPipeline:
|
|
21
|
+
"""End-to-end stacking pipeline.
|
|
22
|
+
|
|
23
|
+
Construct from a power spectrum (:meth:`from_cl`) or from an existing map
|
|
24
|
+
(:meth:`from_map`), then call :meth:`run`.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
sky_map : numpy.ndarray
|
|
29
|
+
The HEALPix map to stack on.
|
|
30
|
+
nside : int
|
|
31
|
+
Resolution parameter of the map.
|
|
32
|
+
|
|
33
|
+
Attributes
|
|
34
|
+
----------
|
|
35
|
+
normalized : numpy.ndarray or None
|
|
36
|
+
Set after run(); the normalized map.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, sky_map, nside):
|
|
40
|
+
self.map = sky_map
|
|
41
|
+
self.nside = nside
|
|
42
|
+
self.normalized = None
|
|
43
|
+
self.positions = None
|
|
44
|
+
self.patches = None
|
|
45
|
+
self.stacked = None
|
|
46
|
+
self.radius = None
|
|
47
|
+
self.profile = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_cl(cls, cl_path, nside=128, seed=None):
|
|
52
|
+
"""Build a pipeline by simulating a map from a power-spectrum file."""
|
|
53
|
+
cl = maps.load_cl(cl_path)
|
|
54
|
+
|
|
55
|
+
sky_map = maps.simulate_map(cl, nside, seed)
|
|
56
|
+
return cls(sky_map, nside)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_map(cls, sky_map):
|
|
60
|
+
"""Build a pipeline from a HEALPix map array already in memory. nside is inferred from the map length, so the caller doesn't have to supply it.
|
|
61
|
+
"""
|
|
62
|
+
sky_map = np.asarray(sky_map)
|
|
63
|
+
nside = hp.npix2nside(sky_map.size)
|
|
64
|
+
return cls(sky_map, nside)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_fits(cls, path, field=0):
|
|
68
|
+
"""Build a pipeline from any HEALPix FITS file on disk."""
|
|
69
|
+
sky_map = maps.load_map(path, field=field)
|
|
70
|
+
return cls.from_map(sky_map)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run(self, size_deg=10.0, reso_arcmin=3.0, profile=True, threshold=3.0, n_peaks=None):
|
|
74
|
+
"""Run the full stacking loop.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
size_deg, reso_arcmin : float
|
|
79
|
+
Patch geometry.
|
|
80
|
+
profile : bool
|
|
81
|
+
Whether to also compute the radial profile.
|
|
82
|
+
threshold : float
|
|
83
|
+
Peak-finding threshold in units of the map std.
|
|
84
|
+
n_peaks : int or None
|
|
85
|
+
Maximum number of peaks to use; None means use all.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
result : cmbstack.stack.StackResult
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
self.normalized = maps.normalize_map(self.map)
|
|
93
|
+
|
|
94
|
+
self.positions = stacking.find_peaks(self.normalized, self.nside, threshold=threshold, n_peaks=n_peaks)
|
|
95
|
+
|
|
96
|
+
self.patches = stacking.extract_patches(self.normalized,self.positions,size_deg=size_deg,reso_arcmin=reso_arcmin)
|
|
97
|
+
|
|
98
|
+
self.stacked = stacking.stack_patches(self.patches)
|
|
99
|
+
|
|
100
|
+
if profile:
|
|
101
|
+
self.radius, self.profile = stacking.radial_profile(self.stacked,reso_arcmin=reso_arcmin)
|
|
102
|
+
|
|
103
|
+
return self
|
cmbstack/maps.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Map simulation and preprocessing utilities for HEALPix CMB maps.
|
|
2
|
+
|
|
3
|
+
Provides the building blocks for turning a raw power spectrum into a
|
|
4
|
+
normalised HEALPix map ready for stacking:
|
|
5
|
+
|
|
6
|
+
1. :func:`load_cl` — read a D_ell spectrum file and convert to C_ell
|
|
7
|
+
2. :func:`simulate_map` — draw a Gaussian random realisation with ``healpy.synfast``
|
|
8
|
+
3. :func:`normalize_map`— subtract the monopole and divide by the std
|
|
9
|
+
|
|
10
|
+
These functions are intentionally field-agnostic: they work on any scalar
|
|
11
|
+
power spectrum (temperature TT, lensing convergence, y-map, ...).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import healpy as hp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dl_to_cl(ell, dl,lmax):
|
|
19
|
+
"""Convert D_ell = ell(ell+1) C_ell / (2 pi) to C_ell.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
ell : array_like
|
|
24
|
+
Multipole values. May start at 0; the ell=0 and ell=1 entries are set
|
|
25
|
+
to zero in the output to avoid division by zero (they carry no usable
|
|
26
|
+
power for this purpose).
|
|
27
|
+
dl : array_like
|
|
28
|
+
D_ell values in the same units you want C_ell returned in (e.g. uK^2).
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
cl : numpy.ndarray
|
|
33
|
+
The angular power spectrum C_ell, same shape as ``dl``.
|
|
34
|
+
|
|
35
|
+
Notes
|
|
36
|
+
-----
|
|
37
|
+
The inverse normalization is ``C_ell = D_ell * 2*pi / (ell*(ell+1))``.
|
|
38
|
+
"""
|
|
39
|
+
# Convert D_ell -> C_ell
|
|
40
|
+
cl_vals = 2.0 * np.pi * dl / (ell * (ell + 1.0))
|
|
41
|
+
|
|
42
|
+
# Build a full array indexed from ell=0, with 0,1 set to zero
|
|
43
|
+
lmax = np.max(ell)
|
|
44
|
+
cl = np.zeros(lmax + 1)
|
|
45
|
+
cl[ell] = cl_vals
|
|
46
|
+
|
|
47
|
+
return cl
|
|
48
|
+
|
|
49
|
+
# Function 1
|
|
50
|
+
def load_cl(path):
|
|
51
|
+
"""Load a power-spectrum file and return C_ell for the requested spectrum.
|
|
52
|
+
|
|
53
|
+
The expected file columns are ell, Dl_TT, Dl_TE, Dl_EE, Dl_BB, Dl_dd, with
|
|
54
|
+
D_ell in uK^2. The chosen column is converted from D_ell to C_ell via
|
|
55
|
+
:func:`dl_to_cl` before being returned.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
path : str
|
|
60
|
+
Path to the whitespace-delimited spectrum file.
|
|
61
|
+
column : str, optional
|
|
62
|
+
Which spectrum to return: one of "TT", "TE", "EE", "BB", "dd".
|
|
63
|
+
Default "TT".
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
cl : numpy.ndarray
|
|
68
|
+
C_ell array indexed from ell=0, suitable for passing to
|
|
69
|
+
:func:`simulate_map`.
|
|
70
|
+
"""
|
|
71
|
+
ell, dl = np.loadtxt(path,usecols=(0,1),unpack=True)
|
|
72
|
+
ell = ell.astype(int)
|
|
73
|
+
lmax = ell.max()
|
|
74
|
+
cl = dl_to_cl(ell,dl,lmax)
|
|
75
|
+
|
|
76
|
+
return cl
|
|
77
|
+
|
|
78
|
+
# Function 2
|
|
79
|
+
def simulate_map(cl, nside=128, seed=None):
|
|
80
|
+
"""Simulate a Gaussian random HEALPix map from a power spectrum.
|
|
81
|
+
|
|
82
|
+
Thin wrapper over ``healpy.synfast`` with an optional seed so results are
|
|
83
|
+
reproducible in tests.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
cl : array_like
|
|
88
|
+
Angular power spectrum C_ell (not D_ell).
|
|
89
|
+
nside : int, optional
|
|
90
|
+
HEALPix resolution parameter. Default 128.
|
|
91
|
+
seed : int or None, optional
|
|
92
|
+
Seed for the random number generator. If None, the draw is random.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
m : numpy.ndarray
|
|
97
|
+
A HEALPix map (RING ordering) of length ``12 * nside**2``.
|
|
98
|
+
"""
|
|
99
|
+
if seed:
|
|
100
|
+
np.random.seed(seed)
|
|
101
|
+
|
|
102
|
+
map = hp.synfast(cl, nside=nside)
|
|
103
|
+
|
|
104
|
+
return map
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def normalize_map(m):
|
|
108
|
+
"""Subtract the monopole and divide by the standard deviation.
|
|
109
|
+
|
|
110
|
+
After this, peak thresholds can be expressed in units of sigma, which is the
|
|
111
|
+
natural convention for peak statistics.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
m : array_like
|
|
116
|
+
Input HEALPix map. May contain UNSEEN/NaN pixels, which are ignored in
|
|
117
|
+
the mean and standard deviation.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
m_norm : numpy.ndarray
|
|
122
|
+
The normalized map, with mean ~0 and std ~1.
|
|
123
|
+
"""
|
|
124
|
+
m_clean = hp.remove_monopole(m)
|
|
125
|
+
m_norm = m_clean / m_clean.std()
|
|
126
|
+
return m_norm
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_map(path, field=0):
|
|
130
|
+
"""Wraps ``hp.read_map``"""
|
|
131
|
+
return hp.read_map(path, field=field)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_map(path, sky_map, overwrite=True):
|
|
135
|
+
"""Wraps ``hp.write_map``"""
|
|
136
|
+
hp.write_map(path, sky_map, overwrite=overwrite)
|
cmbstack/stacking.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Stacking of patches around positions on a HEALPix map.
|
|
2
|
+
|
|
3
|
+
The pipeline is field-agnostic: it operates on any scalar HEALPix map (CMB
|
|
4
|
+
temperature, lensing convergence, a y-map, galaxy density, ...). Positions to
|
|
5
|
+
stack on can either be auto-detected peaks (local maxima) or supplied as an
|
|
6
|
+
external catalogue, so the same code serves peak stacking and
|
|
7
|
+
stacking-on-catalogue (clusters, voids, filaments, ...).
|
|
8
|
+
|
|
9
|
+
Typical use
|
|
10
|
+
-----------
|
|
11
|
+
>>> peaks = find_peaks(m, nside, threshold=3.0) # positions in (theta, phi)
|
|
12
|
+
>>> patches = extract_patches(m, peaks) # fixed-grid 2D cutouts
|
|
13
|
+
>>> stacked = stack_patches(patches) # mean 2D image
|
|
14
|
+
>>> r, profile = radial_profile(stacked, reso_arcmin=3.0)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import healpy as hp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Position finding
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
def find_peaks(sky_map, nside, threshold=None, n_peaks=None):
|
|
25
|
+
"""Find local maxima of a HEALPix map and return their sky positions.
|
|
26
|
+
|
|
27
|
+
A pixel is a local maximum if its value is strictly greater than all of its
|
|
28
|
+
immediate HEALPix neighbours. Peaks can be filtered by a significance
|
|
29
|
+
threshold and/or capped at the ``n_peaks`` highest.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
sky_map : numpy.ndarray
|
|
34
|
+
Input HEALPix map (RING ordering). For a normalised map, values are in
|
|
35
|
+
units of sigma, so ``threshold`` is a significance nu.
|
|
36
|
+
nside : int
|
|
37
|
+
HEALPix resolution parameter of ``sky_map``.
|
|
38
|
+
threshold : float, optional
|
|
39
|
+
If given, keep only peaks with value greater than this (e.g. 3.0 for
|
|
40
|
+
3-sigma peaks on a normalised map). Default None (no threshold).
|
|
41
|
+
n_peaks : int, optional
|
|
42
|
+
If given, keep only the ``n_peaks`` highest peaks (applied after the
|
|
43
|
+
threshold). Default None (keep all).
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
positions : numpy.ndarray, shape (N, 2)
|
|
48
|
+
Sky positions of the selected peaks as ``(theta, phi)`` in radians, the
|
|
49
|
+
same format accepted by :func:`extract_patches`.
|
|
50
|
+
"""
|
|
51
|
+
# hp.hotspots returns (max_map, minima_pix, maxima_pix); we want the maxima.
|
|
52
|
+
_, _, maxima_pix = hp.hotspots(sky_map)
|
|
53
|
+
maxima_pix = np.asarray(maxima_pix)
|
|
54
|
+
|
|
55
|
+
values = sky_map[maxima_pix]
|
|
56
|
+
|
|
57
|
+
if threshold is not None: # Add check for minimmun threshold/n_peaks and ratio between threshold and n_peaks
|
|
58
|
+
keep = values > threshold
|
|
59
|
+
maxima_pix = maxima_pix[keep]
|
|
60
|
+
values = values[keep]
|
|
61
|
+
|
|
62
|
+
# Sort highest-first so n_peaks keeps the most significant.
|
|
63
|
+
order = np.argsort(values)[::-1]
|
|
64
|
+
maxima_pix = maxima_pix[order]
|
|
65
|
+
|
|
66
|
+
if n_peaks is not None:
|
|
67
|
+
maxima_pix = maxima_pix[:n_peaks]
|
|
68
|
+
|
|
69
|
+
theta, phi = hp.pix2ang(nside, maxima_pix)
|
|
70
|
+
return np.column_stack([theta, phi])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Patch extraction
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
def extract_patches(sky_map, positions, size_deg=10.0, reso_arcmin=3.0):
|
|
77
|
+
"""Extract fixed-grid gnomonic patches centred on each position.
|
|
78
|
+
|
|
79
|
+
Each patch is a square 2D array produced by a gnomonic (tangent-plane)
|
|
80
|
+
projection centred on the position, so every patch shares the same grid and
|
|
81
|
+
the centre pixel always corresponds to the position itself.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
sky_map : numpy.ndarray
|
|
86
|
+
Input HEALPix map (RING ordering). Any scalar field.
|
|
87
|
+
positions : array_like, shape (N, 2)
|
|
88
|
+
Sky positions as ``(theta, phi)`` in radians (e.g. the output of
|
|
89
|
+
:func:`find_peaks`, or an external catalogue converted to this format).
|
|
90
|
+
size_deg : float, optional
|
|
91
|
+
Full side length of the square patch in degrees. Default 10.0.
|
|
92
|
+
reso_arcmin : float, optional
|
|
93
|
+
Pixel size of the projected patch in arcminutes. Default 3.0.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
patches : list of numpy.ndarray
|
|
98
|
+
One square 2D array per position, all of identical shape
|
|
99
|
+
``(xsize, xsize)`` with ``xsize = size_deg * 60 / reso_arcmin``.
|
|
100
|
+
"""
|
|
101
|
+
positions = np.atleast_2d(positions)
|
|
102
|
+
xsize = int(round(size_deg * 60.0 / reso_arcmin))
|
|
103
|
+
|
|
104
|
+
patches = []
|
|
105
|
+
for theta, phi in positions:
|
|
106
|
+
# gnomview's rot expects (lon, lat) in degrees.
|
|
107
|
+
lon = np.degrees(phi)
|
|
108
|
+
lat = 90.0 - np.degrees(theta)
|
|
109
|
+
patch = hp.gnomview(
|
|
110
|
+
sky_map,
|
|
111
|
+
rot=(lon, lat),
|
|
112
|
+
xsize=xsize,
|
|
113
|
+
reso=reso_arcmin,
|
|
114
|
+
return_projected_map=True,
|
|
115
|
+
no_plot=True,
|
|
116
|
+
)
|
|
117
|
+
patches.append(np.asarray(patch))
|
|
118
|
+
|
|
119
|
+
return patches
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Stacking
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
def stack_patches(patches):
|
|
126
|
+
"""Average patches pixel-by-pixel into a single stacked image.
|
|
127
|
+
|
|
128
|
+
Because every patch shares the same fixed grid (see :func:`extract_patches`),
|
|
129
|
+
the mean is a genuine stacked image: incoherent noise averages towards zero
|
|
130
|
+
while the coherent central profile survives. NaN/UNSEEN pixels (patch edges
|
|
131
|
+
that fall off the map near the poles) are ignored.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
patches : sequence of numpy.ndarray
|
|
136
|
+
Patches of identical shape, e.g. the output of :func:`extract_patches`.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
stacked : numpy.ndarray
|
|
141
|
+
The mean 2D patch. Display with ``plt.imshow(stacked)``.
|
|
142
|
+
"""
|
|
143
|
+
stack = np.array(patches, dtype=float)
|
|
144
|
+
mean_stacked = np.nanmean(stack, axis=0)
|
|
145
|
+
return mean_stacked
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Profile characterisation
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
def radial_profile(stacked, reso_arcmin=3.0, n_bins=None):
|
|
152
|
+
"""Azimuthally average a stacked patch into a 1D radial profile.
|
|
153
|
+
|
|
154
|
+
Collapses the 2D stacked image to value-versus-radius by averaging in
|
|
155
|
+
concentric annuli about the centre. This 1D profile is the characterisation
|
|
156
|
+
of the mean peak: a central maximum, and (for CMB temperature) a faint
|
|
157
|
+
acoustic ring further out.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
stacked : numpy.ndarray
|
|
162
|
+
Square 2D stacked patch from :func:`stack_patches`.
|
|
163
|
+
reso_arcmin : float, optional
|
|
164
|
+
Pixel size in arcminutes, so the returned radius is in physical angular
|
|
165
|
+
units. Default 3.0.
|
|
166
|
+
n_bins : int, optional
|
|
167
|
+
Number of radial bins. Default is half the patch side length.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
radius_arcmin : numpy.ndarray
|
|
172
|
+
Bin-centre radii in arcminutes.
|
|
173
|
+
profile : numpy.ndarray
|
|
174
|
+
Mean value in each annulus.
|
|
175
|
+
"""
|
|
176
|
+
ny, nx = stacked.shape
|
|
177
|
+
cy, cx = (ny - 1) / 2.0, (nx - 1) / 2.0
|
|
178
|
+
|
|
179
|
+
y, x = np.indices(stacked.shape)
|
|
180
|
+
r_pix = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
|
|
181
|
+
|
|
182
|
+
if n_bins is None:
|
|
183
|
+
n_bins = nx // 2
|
|
184
|
+
|
|
185
|
+
r_max = nx // 2
|
|
186
|
+
bin_edges = np.linspace(0, r_max, n_bins + 1)
|
|
187
|
+
bin_centres = 0.5 * (bin_edges[:-1] + bin_edges[1:])
|
|
188
|
+
|
|
189
|
+
profile = np.full(n_bins, np.nan)
|
|
190
|
+
flat_r = r_pix.ravel()
|
|
191
|
+
flat_v = stacked.ravel()
|
|
192
|
+
for i in range(n_bins):
|
|
193
|
+
in_bin = (flat_r >= bin_edges[i]) & (flat_r < bin_edges[i + 1])
|
|
194
|
+
if np.any(in_bin):
|
|
195
|
+
profile[i] = np.nanmean(flat_v[in_bin])
|
|
196
|
+
|
|
197
|
+
radius_arcmin = bin_centres * reso_arcmin
|
|
198
|
+
return radius_arcmin, profile
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmbstack
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Stack patches of the CMB temperature sky around local maxima
|
|
5
|
+
Author-email: Isaac Alexis López Paredes <first@example.com>, Anushka Sanjay Tilekar <anushka.tilekar.23@alumni.ucl.ac.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/IsaacLP/cmbstack
|
|
8
|
+
Project-URL: Repository, https://github.com/IsaacLP/cmbstack.git
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: healpy
|
|
15
|
+
Requires-Dist: matplotlib
|
|
16
|
+
|
|
17
|
+
[](https://semaphorep.github.io/codeastro/)
|
|
18
|
+
|
|
19
|
+
# CMB Peak Stacking Pipeline
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
`cmbstack` is a Python package for stacking patches of the Cosmic Microwave Background (CMB) temperature sky. It accepts input as a theoretical power spectrum, a [HEALPix](https://healpy.readthedocs.io/en/latest/index.html) FITS file, or a map array already in memory. From there it detects local maxima, extracts gnomonic (flat-sky) patches around each peak, and averages them. This stacking procedure enhances the coherent peak profile while suppressing uncorrelated noise.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install cmbstack
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
**From a power spectrum file:**
|
|
34
|
+
```python
|
|
35
|
+
from cmbstack.main import StackingPipeline
|
|
36
|
+
|
|
37
|
+
pipeline = StackingPipeline.from_cl("path/to/spectrum.dat", nside=1024, seed=42)
|
|
38
|
+
pipeline.run()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**From an existing FITS map:**
|
|
42
|
+
```python
|
|
43
|
+
pipeline = StackingPipeline.from_fits("path/to/map.fits", field=0)
|
|
44
|
+
pipeline.run()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**From a map array already in memory:**
|
|
48
|
+
```python
|
|
49
|
+
pipeline = StackingPipeline.from_map(sky_map)
|
|
50
|
+
pipeline.run()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
See `examples/from_theoretical_cl.ipynb` for a full worked example.
|
|
54
|
+
|
|
55
|
+
## Package Structure
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
cmbstack/
|
|
59
|
+
├── maps.py — power-spectrum loading, map simulation, normalisation
|
|
60
|
+
├── stacking.py — peak finding, patch extraction, stacking, radial profile
|
|
61
|
+
└── main.py — StackingPipeline high-level class
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Workflow
|
|
65
|
+
|
|
66
|
+
> **Note:** Steps 1–2 apply when starting from a power spectrum. Use `StackingPipeline.from_fits` or `from_map` to skip them when working with a real or pre-simulated map.
|
|
67
|
+
|
|
68
|
+
### 1. Load the Power Spectrum — `maps.load_cl`
|
|
69
|
+
|
|
70
|
+
The input file contains the power spectrum as $D_\ell^{TT}$:
|
|
71
|
+
|
|
72
|
+
$$D_\ell \equiv \frac{\ell(\ell+1)}{2\pi} C_\ell$$
|
|
73
|
+
|
|
74
|
+
`load_cl` reads columns $(\ell, D_\ell)$ and converts to $C_\ell$:
|
|
75
|
+
|
|
76
|
+
$$C_\ell = \frac{2\pi}{\ell(\ell+1)} D_\ell, \qquad C_0 = C_1 = 0$$
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### 2. Simulate a Sky Map — `maps.simulate_map`
|
|
81
|
+
|
|
82
|
+
A Gaussian random realization is drawn by sampling spherical harmonic coefficients $a_{\ell m}$ with variance $C_\ell$:
|
|
83
|
+
|
|
84
|
+
$$T(\hat{n}) = \sum_{\ell,m} a_{\ell m} \, Y_{\ell m}(\hat{n}), \qquad \langle |a_{\ell m}|^2 \rangle = C_\ell$$
|
|
85
|
+
|
|
86
|
+
This calls `healpy.synfast` internally. An optional `seed` makes runs reproducible.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### 3. Normalise the Map — `maps.normalize_map`
|
|
91
|
+
|
|
92
|
+
Before peak detection, the map is standardised so that thresholds have a clear statistical meaning:
|
|
93
|
+
|
|
94
|
+
$$T_{\text{norm}}(\hat{n}) = \frac{T(\hat{n}) - \langle T \rangle}{\sigma}$$
|
|
95
|
+
|
|
96
|
+
After this step the map has mean $\approx 0$ and standard deviation $= 1$, so peaks are measured in units of $\sigma$.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### 4. Detect Peaks — `stacking.find_peaks`
|
|
101
|
+
|
|
102
|
+
Local maxima are identified with `healpy.hotspots`: a pixel is a maximum if its value exceeds every immediate HEALPix neighbour. Peaks are filtered by a significance threshold $\nu$ (default $\nu = 3\sigma$) and optionally capped at the $N$ highest:
|
|
103
|
+
|
|
104
|
+
$$\text{Peaks} = \{\hat{n}_p \in \text{Maxima} \mid T_{\text{norm}}(\hat{n}_p) > \nu\}$$
|
|
105
|
+
|
|
106
|
+
Returns sky positions as $(θ, φ)$ pairs in radians.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 5. Extract Patches — `stacking.extract_patches`
|
|
111
|
+
|
|
112
|
+
For each peak a square patch is cut using a gnomonic (tangent-plane) projection centred on $\hat{n}_p$. Every patch shares the same fixed pixel grid (side length `size_deg`, pixel scale `reso_arcmin`), so the centre pixel always corresponds to the peak itself and patches can be co-added directly.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### 6. Stack — `stacking.stack_patches`
|
|
117
|
+
|
|
118
|
+
Patches are averaged pixel-by-pixel:
|
|
119
|
+
|
|
120
|
+
$$S = \frac{1}{N} \sum_{i=1}^{N} P_i$$
|
|
121
|
+
|
|
122
|
+
Incoherent noise averages towards zero; the coherent central profile survives.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 7. Radial Profile — `stacking.radial_profile`
|
|
127
|
+
|
|
128
|
+
The 2D stacked image is collapsed to a 1D profile by azimuthal averaging in concentric annuli about the centre. Returns bin-centre radii in arcminutes and the mean temperature in each annulus.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Pipeline Constructors
|
|
133
|
+
|
|
134
|
+
`StackingPipeline` provides three entry points depending on where your data comes from:
|
|
135
|
+
|
|
136
|
+
| Constructor | Input | Notes |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `from_cl(path, nside, seed)` | Power-spectrum file | Simulates a Gaussian random map via `healpy.synfast` |
|
|
139
|
+
| `from_fits(path, field=0)` | HEALPix FITS file | Loads the map with `maps.load_map`; `nside` is inferred automatically |
|
|
140
|
+
| `from_map(sky_map)` | NumPy array | Accepts any in-memory HEALPix map; `nside` is inferred automatically |
|
|
141
|
+
|
|
142
|
+
All three store the map in `pipeline.map` and share the same `run()` interface.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Map I/O Utilities
|
|
147
|
+
|
|
148
|
+
`maps.load_map` and `maps.save_map` wrap the healpy FITS readers for convenience:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from cmbstack import maps
|
|
152
|
+
|
|
153
|
+
m = maps.load_map("map.fits", field=0) # wraps hp.read_map
|
|
154
|
+
maps.save_map("out.fits", m) # wraps hp.write_map (overwrite=True by default)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Pipeline Object
|
|
160
|
+
|
|
161
|
+
`StackingPipeline` stores every intermediate product as an attribute:
|
|
162
|
+
|
|
163
|
+
| Attribute | Content |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `pipeline.map` | Raw simulated map |
|
|
166
|
+
| `pipeline.normalized` | Normalised map (units of $\sigma$) |
|
|
167
|
+
| `pipeline.positions` | Peak positions $(θ, φ)$ in radians |
|
|
168
|
+
| `pipeline.patches` | List of 2D gnomonic patches |
|
|
169
|
+
| `pipeline.stacked` | Mean stacked 2D image |
|
|
170
|
+
| `pipeline.radius` | Radial bin centres (arcmin) |
|
|
171
|
+
| `pipeline.profile` | Mean temperature per radial bin |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cmbstack/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
cmbstack/main.py,sha256=MsEC7R4N14LM6YqsHssYCCL8L27G43eSTteSSTuX29M,3213
|
|
3
|
+
cmbstack/maps.py,sha256=ri6jBpWe0SRZZsFg-FQ-_y_sqZA6fLiaIZk_mX4VYr0,3893
|
|
4
|
+
cmbstack/stacking.py,sha256=P6aXH9xiYWVSj7xD7Zlwg3FQZ0MObKJNj9Hw1dG4NO4,7366
|
|
5
|
+
cmbstack-0.0.1.dist-info/METADATA,sha256=1vSxDLa-V1CIfhLXk6LWqMnBSOGEg-BiaW-lmydcYpY,6056
|
|
6
|
+
cmbstack-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
cmbstack-0.0.1.dist-info/top_level.txt,sha256=QFyCKT2_L8ZxFKUUzMWPSV1hORigTGvvQCvzshyGJvw,9
|
|
8
|
+
cmbstack-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cmbstack
|