Encircled Energy Analysis¶
What you will learn:
- What Encircled Energy (EE) is and why it is used to characterise PSF quality
- How to compute and plot EE curves with PSFCraft
- How wavelength and optical aberrations affect the EE
- How to extract numerical EE values at specific radii
Prerequisites: 04_wavefront_aberrations.ipynb
Background¶
The Encircled Energy $\mathrm{EE}(r)$ is the fraction of total PSF flux contained within a circle of radius $r$ (in arcseconds) centred on the PSF peak:
$$\mathrm{EE}(r) = \frac{\int_0^r I(\rho)\, 2\pi\rho\, d\rho}{\int_0^\infty I(\rho)\, 2\pi\rho\, d\rho}$$
It ranges from 0 to 1. Common specifications are:
- EE50 — radius enclosing 50 % of the total energy (half-light radius)
- EE80 — radius enclosing 80 % (image quality budget for many space missions)
A sharper PSF reaches high EE values at smaller radii.
%matplotlib inline
import psfcraft
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
pysynphot is not installed. Please install it to use PSF generation with polychromatic sources.
1. EE as a Function of Wavelength¶
Longer wavelengths produce larger PSFs, so the EE curve rises more slowly — you need a larger aperture radius to capture the same energy fraction.
tel = psfcraft.NewtonianTelescope(version="0") # no struts — clean Airy pattern
tel.pixelscale = 0.0298 # ~Euclid pixel scale (arcsec/pixel)
wavelengths_um = np.linspace(0.3, 1.5, 6) # 0.3 … 1.5 µm
cmaps = ["Blues", "Greens", "Reds", "Purples", "Oranges", "Greys"]
line_colors = ["royalblue", "forestgreen", "crimson", "purple", "darkorange", "dimgray"]
psfs = [tel.calc_psf(monochromatic=wl * 1e-6) for wl in wavelengths_um]
# --- Grid layout: PSF images on top, shared EE curve below ---
fig = plt.figure(figsize=(18, 7))
gs = gridspec.GridSpec(2, 6, height_ratios=[1, 1.2])
axes_psf = [fig.add_subplot(gs[0, i]) for i in range(6)]
ax_ee = fig.add_subplot(gs[1, :])
for i, (psf, wl, cmap, color) in enumerate(zip(psfs, wavelengths_um, cmaps, line_colors)):
psfcraft.display_psf(psf, ax=axes_psf[i], colorbar=False,
title=f"{wl:.2f} µm", cmap=cmap)
axes_psf[i].set_xlabel("")
psfcraft.display_ee(psf, ax=ax_ee, mark_levels=False)
axes_psf[0].set_ylabel("arcsec")
# Colour the EE lines to match the PSF images
for i, (line, color) in enumerate(zip(ax_ee.lines, line_colors)):
line.set_color(color)
ax_ee.legend([f"{wl:.2f} µm" for wl in wavelengths_um], title="Wavelength", loc="lower right")
ax_ee.set_xlabel("Radius (arcsec)")
ax_ee.set_ylabel("Encircled Energy")
ax_ee.axhline(0.5, color="gray", linestyle="--", linewidth=0.8, label="EE50")
ax_ee.axhline(0.8, color="gray", linestyle=":", linewidth=0.8, label="EE80")
ax_ee.grid(True, alpha=0.3)
plt.suptitle("PSF and Encircled Energy — wavelength effect", fontsize=13)
plt.tight_layout()
plt.show()
2. Extracting Numerical EE Values¶
psfcraft.measure_ee(psf) returns a callable $f(r)$ → EE at radius $r$ (arcsec).
radii_arcsec = [0.1, 0.2, 0.5, 1.0]
print(f"{'Wavelength':>12} " + " ".join(f"EE({r:.1f}\")" for r in radii_arcsec))
print("-" * 60)
for psf, wl in zip(psfs, wavelengths_um):
ee_fn = psfcraft.measure_ee(psf)
values = [ee_fn(r) for r in radii_arcsec]
print(f"{wl:.2f} µm " + " ".join(f"{v:.3f} " for v in values))
Wavelength EE(0.1") EE(0.2") EE(0.5") EE(1.0") ------------------------------------------------------------ 0.30 µm 0.866 0.938 0.976 nan 0.54 µm 0.803 0.890 0.954 nan 0.78 µm 0.606 0.838 0.937 nan 1.02 µm 0.428 0.817 0.911 nan 1.26 µm 0.308 0.734 0.900 nan 1.50 µm 0.229 0.628 0.862 nan
3. EE Degradation Due to Aberrations¶
Aberrations spread PSF energy away from the core, reducing EE at any given radius.
Here we compare the EE of a perfect and an aberrated PSF.
wavelength = 1e-6 # 1 µm fixed
# Perfect telescope (no WFE)
tel_perfect = psfcraft.NewtonianTelescope(version="0")
tel_perfect.pixelscale = 0.0298
psf_perfect = tel_perfect.calc_psf(monochromatic=wavelength)
# Aberrated — add defocus + coma + spherical
wfe_nm_sets = {
"Perfect (0 nm)": [0] * 11,
"Low WFE (Seidel ~50 nm)": [0, 0, 0, 30, 0, 0, 30, 0, 0, 0, 0],
"High WFE (100 nm mix)": [0, 0, 0, 60, 50, 50, 100, 25, 0, 0, 50],
}
line_styles = ["-", "--", ":"]
fig, ax = plt.subplots(figsize=(8, 5))
for (label, wfe_nm), ls in zip(wfe_nm_sets.items(), line_styles):
wfe_m = [c * 1e-9 for c in wfe_nm]
tel = psfcraft.NewtonianTelescope(version="0", wfe_coefficients=wfe_m)
tel.pixelscale = 0.0298
psf = tel.calc_psf(monochromatic=wavelength)
psfcraft.display_ee(psf, ax=ax, mark_levels=False)
ax.lines[-1].set_linestyle(ls)
ax.lines[-1].set_label(label)
ax.axhline(0.5, color="gray", linestyle="--", linewidth=0.8, alpha=0.5)
ax.axhline(0.8, color="gray", linestyle=":", linewidth=0.8, alpha=0.5)
ax.text(0.02, 0.51, "EE50", fontsize=9, color="gray")
ax.text(0.02, 0.81, "EE80", fontsize=9, color="gray")
ax.legend()
ax.set_xlabel("Radius (arcsec)")
ax.set_ylabel("Encircled Energy")
ax.set_title(f"EE degradation by WFE (λ = {wavelength*1e6:.1f} µm)", fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Key Takeaways¶
psfcraft.display_ee(psf, ax=ax)draws the EE curve on a given axes.psfcraft.measure_ee(psf)returns a callable — evaluate it at any radius in arcsec.- Longer wavelengths → broader PSFs → EE curve rises more slowly.
- Wavefront aberrations redistribute PSF energy outward → EE drops, especially at small radii.
- EE50 and EE80 are standard image-quality figures of merit.
Next: 06_polychromatic_psf.ipynb — simulate PSFs for spectrally broad stellar sources.