← All posts

SPT-3G D1 high-ℓ CMB power spectra vs. Planck 2018

AI-generated notebook — This notebook was created by GitHub Copilot (Claude Sonnet 4.6) from the following prompts:

  1. “read https://lambda.gsfc.nasa.gov/product/spt/spt3g_d1_high_ell_info.html and the relative paper, then create a jupyter notebook based post that loads the spectra and compares them with the latest spectra from planck. cite stuff properly. be didactic. run it using a uv venv. find a way of specifying requirements inside the notebook itself. the most standard way.”
  2. “I look at the band powers. In the original paper, the error bars are a lot larger. They are increased by a factor of 10, do that also here and mention it.”

SPT-3G D1 high-ℓ CMB power spectra vs. Planck 2018

Background

The Cosmic Microwave Background (CMB) temperature anisotropy power spectrum is usually expressed as Dℓ=ℓ(ℓ+1)Cℓ/(2π)D_\ell = \ell(\ell+1)C_\ell/(2\pi) in units of μK2\mu\mathrm{K}^2. It encodes the statistical properties of tiny temperature fluctuations imprinted on the CMB at recombination (z≈1100z \approx 1100) and provides our most precise probe of cosmological parameters.

The power spectrum can be divided into two regimes by the physical processes responsible for the signal:

Primary anisotropies (ℓ≲2500\ell \lesssim 2500)

These arise from density and velocity perturbations in the primordial plasma. The characteristic acoustic peaks at ℓ≈200,500,800,…\ell \approx 200, 500, 800, \ldots are perfectly measured by Planck (Planck Collaboration 2020).

Secondary anisotropies (ℓ≳2000\ell \gtrsim 2000)

At smaller angular scales, primary CMB fluctuations are exponentially suppressed by Silk damping. A rich variety of physical processes contributes instead:

EffectAbbreviationPhysical origin
Thermal Sunyaev–Zel’dovichtSZInverse Compton scattering off hot ICM electrons
Kinematic Sunyaev–Zel’dovichkSZDoppler shift from bulk motions of ionized gas
Cosmic Infrared BackgroundCIBCumulative emission from dusty star-forming galaxies
Radio galaxiesRGSynchrotron emission from AGN

The tSZ effect has a distinctive frequency spectrum (decreasing below 217 GHz, rising above), which means multi-frequency observations can separate it from the CIB. The kSZ effect has the same frequency spectrum as the CMB and can only be separated statistically.

The SPT-3G D1 data set

The South Pole Telescope (SPT) is a 10-meter microwave telescope at the South Pole, operated by a collaboration led by the University of Chicago. The SPT-3G receiver observes simultaneously at 95, 150, and 220 GHz with ~16,000 detectors.

Chaubal et al. (2026) present the SPT-3G D1 TT high-ℓ power spectra using observations from 2019 and 2020 of the 1646 deg² SPT-3G Main field. The measurements span 1700 ≤ ℓ ≤ 11,000 across six frequency auto- and cross-spectra: 95×95, 95×150, 95×220, 150×150, 150×220, 220×220 GHz.

Citation: Chaubal P. et al. 2026, SPT-3G D1: A Measurement of Secondary Cosmic Microwave Background Anisotropy Power, arXiv:2601.20551. Data hosted at LAMBDA and UChicago SPT site.

In this notebook we:

  1. Download and parse the SPT-3G D1 band powers.
  2. Download the Planck 2018 (PR3) binned TT spectrum.
  3. Compare the two datasets to visualise how SPT-3G extends the CMB power spectrum into the secondary-anisotropy regime.

Requirements

This notebook uses only the standard scientific Python stack. The cell below installs the dependencies with pip (the standard way to declare and install notebook requirements).

To run this notebook in an isolated uv virtual environment:

uv venv .venv
source .venv/bin/activate
uv pip install notebook
jupyter execute 2026-04-21-spt3g-d1-planck-comparison.ipynb

The %pip install cell below is the standard Jupyter mechanism for embedding package requirements directly inside a notebook.

%pip install numpy matplotlib requests
/home/zonca/p/software/zonca.dev/posts/.venv-spt3g/bin/python: No module named pip
Note: you may need to restart the kernel to use updated packages.
import io
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import requests

# Consistent, publication-friendly style
plt.rcParams.update({
    'figure.dpi': 120,
    'axes.grid': True,
    'grid.alpha': 0.3,
    'font.size': 11,
})

1. Load the SPT-3G D1 band powers

The released text file dls.txt contains 144 rows (24 per frequency combination) with four columns:

ColumnMeaning
indexBand index (0-based within each spectrum)
ℓEffective multipole of the band
DℓD_\ellBand power ℓ(ℓ+1)Cℓ/(2π)\ell(\ell+1)C_\ell/(2\pi) in μK2\mu\mathrm{K}^2
σDℓ\sigma D_\ell1σ1\sigma uncertainty (diagonal covariance)

Rows are ordered: all 95×95 bands, then 95×150, 95×220, 150×150, 150×220, 220×220.

SPT_URL = "https://pole.uchicago.edu/public/data/chaubal26/dls.txt"

resp = requests.get(SPT_URL, timeout=30)
resp.raise_for_status()
raw = np.loadtxt(io.StringIO(resp.text))

print(f"Shape: {raw.shape}  →  {raw.shape[0]} rows, {raw.shape[1]} columns")
print(f"Rows per frequency combination: {raw.shape[0] // 6}")
Shape: (144, 4)  →  144 rows, 4 columns
Rows per frequency combination: 24
FREQS = ["95×95", "95×150", "95×220", "150×150", "150×220", "220×220"]
N_BANDS = raw.shape[0] // len(FREQS)  # 24 bands per spectrum

# Split into a dict keyed by frequency label
spt = {}
for i, name in enumerate(FREQS):
    chunk = raw[i * N_BANDS : (i + 1) * N_BANDS]
    spt[name] = {
        "ell":    chunk[:, 1],
        "Dl":     chunk[:, 2],
        "sigmaDl": chunk[:, 3],
    }

# Sanity check
for name, d in spt.items():
    print(f"{name}: ℓ = {d['ell'][0]:.0f} – {d['ell'][-1]:.0f}  "
          f"({N_BANDS} bands)")
95×95: ℓ = 1751 – 10379  (24 bands)
95×150: ℓ = 1752 – 10402  (24 bands)
95×220: ℓ = 1751 – 10411  (24 bands)
150×150: ℓ = 1752 – 10430  (24 bands)
150×220: ℓ = 1751 – 10449  (24 bands)
220×220: ℓ = 1752 – 10436  (24 bands)

2. Visualise all six SPT-3G D1 spectra

Each panel shows DℓD_\ell with ±1σ\pm 1\sigma error bars multiplied by 10 (following the convention of Fig. 2 in Chaubal et al. 2026, where uncertainties are inflated by a factor of ten to be visible on a log scale). The colour coding follows frequency: blue → 95 GHz, green → 150 GHz, red → 220 GHz.

Key features to notice:

  • Declining slope at low ℓ (~2000–3000): residual primary CMB power (the damping tail).

  • Minimum near ℓ ≈ 3000: tSZ has a null at ~217 GHz and suppresses the 150×150 spectrum, creating a local minimum.

  • Rising power at high ℓ (>4000): CIB dominates the 220 GHz channels; radio galaxies (flat or rising spectra) appear in lower frequency channels.

  • Cross-spectra help separate components: the 95×220 cross-spectrum,

    for example, is sensitive to the CIB without the noise penalty of a 220×220 auto-spectrum.

# Colour map: one colour per unique frequency pair
palette = {
    "95×95":   "#1f77b4",   # blue
    "95×150":  "#2ca02c",   # green
    "95×220":  "#9467bd",   # purple
    "150×150": "#ff7f0e",   # orange
    "150×220": "#d62728",   # red
    "220×220": "#8c564b",   # brown
}

fig, axes = plt.subplots(2, 3, figsize=(14, 7), sharey=False, sharex=True)
axes = axes.flatten()

for ax, name in zip(axes, FREQS):
    d = spt[name]
    ax.errorbar(
        d["ell"], d["Dl"], yerr=10 * d["sigmaDl"],
        fmt="o", ms=3, lw=0.8, color=palette[name], label=name,
        capsize=2, elinewidth=0.8,
    )
    ax.set_title(f"{name} GHz", fontsize=12)
    ax.set_yscale("log")
    ax.set_xlabel(r"Multipole $\ell$")
    ax.set_ylabel(r"$D_\ell\;[\mu\mathrm{K}^2]$")
    ax.set_xlim(1500, 11500)

fig.suptitle(
    "SPT-3G D1 high-ℓ TT band powers  (Chaubal et al. 2026, arXiv:2601.20551)\nError bars ×10 for visibility",
    fontsize=13, y=1.01,
)
fig.tight_layout()
plt.savefig("spt3g_d1_six_spectra.png", bbox_inches="tight")
plt.show()
output 1

3. Load the Planck 2018 TT spectrum

We use the Planck PR3 binned TT spectrum, which covers 47≲ℓ≲250047 \lesssim \ell \lesssim 2500 with five columns:

ColumnMeaning
ℓEffective multipole
DℓD_\ellMeasured band power (μK2\mu\mathrm{K}^2)
−δDℓ-\delta D_\ellLower 1σ error
+δDℓ+\delta D_\ellUpper 1σ error
BestFitBest-fit ΛCDM model

Citation: Planck Collaboration V (2020), Planck 2018 results. V. Power spectra and likelihoods, A&A 641, A5. DOI: 10.1051/0004-6361/201936386. Data: Planck Legacy Archive (IRSA).

PLANCK_URL = (
    "https://irsa.ipac.caltech.edu/data/Planck/release_3/"
    "ancillary-data/cosmoparams/COM_PowerSpect_CMB-TT-binned_R3.01.txt"
)

resp_pl = requests.get(PLANCK_URL, timeout=30)
resp_pl.raise_for_status()

planck = np.loadtxt(io.StringIO(resp_pl.text), comments="#")
pl = {
    "ell":     planck[:, 0],
    "Dl":      planck[:, 1],
    "err_lo":  planck[:, 2],
    "err_hi":  planck[:, 3],
    "bestfit": planck[:, 4],
}

print(f"Planck PR3 TT spectrum: {len(pl['ell'])} bins")
print(f"ℓ range: {pl['ell'][0]:.0f} – {pl['ell'][-1]:.0f}")
Planck PR3 TT spectrum: 83 bins
ℓ range: 48 – 2499

4. Full ℓ range: Planck + SPT-3G D1 (150×150 GHz)

Planck’s 143 GHz channel is the closest match to SPT-3G’s 150 GHz band. Although the two instruments use slightly different effective frequencies and source-masking thresholds, the 150×150 SPT-3G spectrum is the most natural counterpart to the Planck TT spectrum for a direct comparison.

The plot below shows:

  • Grey shaded region (Planck PR3): primary CMB spectrum measured with full-sky coverage and multiple frequencies for foreground separation. Plotted up to ℓ ≈ 2500 where Planck signal-to-noise remains adequate.
  • Blue curve (Planck best-fit ΛCDM): smooth theoretical prediction continuing into the damping tail.
  • Orange points (SPT-3G D1 150×150): SPT-3G measurements at 1700 ≤ ℓ ≤ 11,000, showing both the damping tail region and the foreground-dominated regime.

Note on error bars: Following Fig. 2 of Chaubal et al. (2026), all plotted uncertainties are inflated by a factor of 10 to be visible on the logarithmic scale. The actual 1σ1\sigma errors are 10× smaller.

Note the transition near ℓ ≈ 3000–4000: below this range, SPT-3G roughlyDℓD_\ell above the pure-CMB prediction.

follows the Planck best-fit damping tail; above it, foreground power (CIBfrom dusty galaxies, SZ effects) begins to dominate and push the measured

fig, ax = plt.subplots(figsize=(12, 5))

# ── Planck PR3 band powers (error bars ×10 for visibility) ──────────────────
ax.errorbar(
    pl["ell"], pl["Dl"],
    yerr=[10 * pl["err_lo"], 10 * pl["err_hi"]],
    fmt="none", ecolor="#aaaaaa", elinewidth=0.6, alpha=0.7,
    label="Planck PR3 TT (2018)",
)
ax.scatter(
    pl["ell"], pl["Dl"],
    s=5, color="#555555", zorder=3,
)

# ── Planck best-fit ΛCDM (extended to high ℓ as reference) ──────────────────
ax.plot(
    pl["ell"], pl["bestfit"],
    color="royalblue", lw=1.4, ls="--",
    label="Planck best-fit ΛCDM",
)

# ── SPT-3G D1 150×150 GHz (error bars ×10 for visibility) ──────────────────
d150 = spt["150×150"]
ax.errorbar(
    d150["ell"], d150["Dl"], yerr=10 * d150["sigmaDl"],
    fmt="o", ms=4, color="#ff7f0e", elinewidth=0.9,
    capsize=2, label="SPT-3G D1  150×150 GHz  (err ×10)",
    zorder=5,
)

# ── Annotations ──────────────────────────────────────────────────────────────
ax.axvspan(1700, 2500, alpha=0.05, color="green", label="Overlap region")
ax.annotate(
    "tSZ minimum\n@ 150 GHz",
    xy=(3200, 32), xytext=(4200, 60),
    fontsize=9, color="#cc4400",
    arrowprops=dict(arrowstyle="->", color="#cc4400", lw=0.8),
)
ax.annotate(
    "CIB + radio\ngalaxies rising",
    xy=(7000, 180), xytext=(6000, 450),
    fontsize=9, color="#cc4400",
    arrowprops=dict(arrowstyle="->", color="#cc4400", lw=0.8),
)

ax.set_yscale("log")
ax.set_xlabel(r"Multipole  $\ell$", fontsize=13)
ax.set_ylabel(r"$D_\ell = \ell(\ell+1)C_\ell / 2\pi\;\;[\mu\mathrm{K}^2]$", fontsize=13)
ax.set_title(
    "CMB TT power spectrum: Planck 2018 PR3  +  SPT-3G D1 (150×150 GHz)",
    fontsize=13,
)
ax.set_xlim(30, 12000)
ax.set_ylim(10, 1e5)
ax.legend(fontsize=10, loc="upper right")

# Secondary x-axis showing angular scale θ ≈ 180°/ℓ
ax2 = ax.twiny()
ax2.set_xlim(ax.get_xlim())
tick_ells = [100, 500, 1000, 3000, 6000, 11000]
ax2.set_xticks(tick_ells)
ax2.set_xticklabels([f"{180/l:.1f}°" for l in tick_ells], fontsize=9)
ax2.set_xlabel(r"Angular scale  $\theta = 180°/\ell$", fontsize=11)

plt.tight_layout()
plt.savefig("spt3g_d1_planck_comparison.png", bbox_inches="tight")
plt.show()
output 2

5. Zoom into the overlap region

Planck measures up to ℓ≈2500\ell \approx 2500 and SPT-3G D1 starts at ℓ=1700\ell = 1700, giving a ~800-multipole overlap window where both experiments should agree on the primary CMB power.

Any systematic offset between the two would signal calibration differences or residual foreground contamination in one (or both) data sets.

fig, ax = plt.subplots(figsize=(9, 4))

# Planck points in overlap range (error bars ×10 for visibility)
mask_pl = pl["ell"] >= 1600
ax.errorbar(
    pl["ell"][mask_pl], pl["Dl"][mask_pl],
    yerr=[10 * pl["err_lo"][mask_pl], 10 * pl["err_hi"][mask_pl]],
    fmt="s", ms=4, color="#555555", elinewidth=0.8, capsize=2,
    label="Planck PR3 TT (2018)  (err ×10)",
)
ax.plot(pl["ell"][mask_pl], pl["bestfit"][mask_pl],
        color="royalblue", lw=1.2, ls="--", label="Planck best-fit ΛCDM")

# SPT-3G D1 low-ell portion (error bars ×10 for visibility)
mask_spt = d150["ell"] <= 2700
ax.errorbar(
    d150["ell"][mask_spt], d150["Dl"][mask_spt],
    yerr=10 * d150["sigmaDl"][mask_spt],
    fmt="o", ms=5, color="#ff7f0e", elinewidth=1.0, capsize=3,
    label="SPT-3G D1  150×150 GHz  (err ×10)",
)

ax.axvspan(1700, 2500, alpha=0.08, color="green", label="Overlap window")
ax.set_xlabel(r"Multipole  $\ell$", fontsize=12)
ax.set_ylabel(r"$D_\ell\;[\mu\mathrm{K}^2]$", fontsize=12)
ax.set_title("Overlap region: Planck PR3 vs. SPT-3G D1 (150×150 GHz)", fontsize=12)
ax.set_xlim(1550, 2800)
ax.legend(fontsize=10)
plt.tight_layout()
plt.savefig("spt3g_d1_planck_overlap.png", bbox_inches="tight")
plt.show()
output 3

6. Multi-frequency view: foreground separation by frequency

A key advantage of SPT-3G D1 is the simultaneous measurement at three frequencies. The plot below overlays all six spectra on a single panel to illustrate frequency-dependent behaviour:

  • tSZ imprint: the tSZ effect decreases the temperature at 95 and 150 GHz (relative to the CIB) and has a spectral null at ~217 GHz. The 150×150 spectrum is most suppressed relative to 220×220 near ℓ ≈ 3000.
  • CIB rise: The CIB has a steep, rising spectrum, making the 220 GHz channels the most CIB-dominated at high ℓ.
  • Radio galaxies: They appear brightest at 95 GHz and their power remains relatively flat or slightly rising toward low ℓ within the SPT range.

These frequency dependencies allow the SPT-3G collaboration to fit for the individual foreground components and extract, e.g., the tSZ amplitude D3000tSZD^{\mathrm{tSZ}}_{3000} and kSZ amplitude D3000kSZD^{\mathrm{kSZ}}_{3000}.

fig, ax = plt.subplots(figsize=(11, 5))

for name in FREQS:
    d = spt[name]
    ax.errorbar(
        d["ell"], d["Dl"], yerr=10 * d["sigmaDl"],
        fmt="o-", ms=3, lw=0.8, color=palette[name],
        elinewidth=0.5, capsize=1, label=f"{name} GHz",
    )

# Show where tSZ roughly creates its minimum in the 150×150 spectrum
ax.axvline(3000, color="grey", ls=":", lw=1)
ax.text(3100, 8, r"$\ell = 3000$", fontsize=9, color="grey")

ax.set_yscale("log")
ax.set_xlabel(r"Multipole  $\ell$", fontsize=13)
ax.set_ylabel(r"$D_\ell\;[\mu\mathrm{K}^2]$", fontsize=13)
ax.set_title(
    "SPT-3G D1: all six frequency spectra — foreground separation by colour\n(error bars ×10 for visibility)",
    fontsize=12,
)
ax.set_xlim(1500, 11500)
ax.legend(ncol=2, fontsize=10)
plt.tight_layout()
plt.savefig("spt3g_d1_all_spectra_overlay.png", bbox_inches="tight")
plt.show()
output 4

7. Key results from the paper

Chaubal et al. (2026) use these spectra to constrain the amplitudes of the secondary signal components. Selected highlights:

ParameterValueNotes
D3000tSZD^{\mathrm{tSZ}}_{3000}4.91±0.37  μK24.91 \pm 0.37\;\mu\mathrm{K}^2Thermal SZ at 143 GHz
D3000kSZD^{\mathrm{kSZ}}_{3000}1.75±0.86  μK21.75 \pm 0.86\;\mu\mathrm{K}^2Kinematic SZ
Duration of reionization Δ50zre\Delta_{50}z_{\rm re}<3.8< 3.8 (95%)From kSZ
Duration of reionization Δ90zre\Delta_{90}z_{\rm re}<6.1< 6.1 (95%)AMBER simulations

The kSZ power provides the most constraining current bounds on the epoch and duration of reionization from the small-scale CMB power spectrum.

8. Summary

This notebook demonstrated how to:

  1. Download and parse the SPT-3G D1 band-power file from the UChicago public data archive and the Planck Legacy Archive (IRSA mirror).
  2. Compare the high-resolution SPT-3G spectra with the Planck 2018 PR3 measurements in the overlap multipole range 1700≲ℓ≲25001700 \lesssim \ell \lesssim 2500.
  3. Visualise the frequency-dependent foreground signatures (tSZ minimum, CIB rise) that allow SPT-3G D1 to separate astrophysical components.

Together, Planck (primary CMB, large scales) and SPT-3G D1 (secondary CMB, small scales) map the CMB temperature power spectrum across nearly four decades in angular scale, from the Sachs–Wolfe plateau at ℓ∼2\ell \sim 2 to the Poisson-noise floor of extragalactic sources at ℓ∼10,000\ell \sim 10{,}000.

References