PSFCraft

Home

  • Overview
  • Getting Started
  • Web Interface
  • Generation Scripts
  • Publications
  • People
  • Funding

Tutorials

  • PSFCraft — Introduction
  • Getting Started with PSFCraft
  • Aperture Geometry and Pupil Configurations
  • Wavefront Aberrations with Zernike Polynomials
  • Encircled Energy Analysis
    • Background
    • 1. EE as a Function of Wavelength
    • 2. Extracting Numerical EE Values
    • 3. EE Degradation Due to Aberrations
    • Key Takeaways
  • Polychromatic PSF Simulation
  • End-to-End PSF Dataset Generation Pipeline

API Reference

  • Overview
  • Telescope Models
  • Aperture & Optics
  • Display & Metrics
  • Source Building
  • Batch Generation
  • Constants
PSFCraft
  • Tutorials
  • Encircled Energy Analysis

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.

In [1]:
Copied!
%matplotlib inline
import psfcraft
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
%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.

In [2]:
Copied!
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()
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()
No description has been provided for this image

2. Extracting Numerical EE Values¶

psfcraft.measure_ee(psf) returns a callable $f(r)$ → EE at radius $r$ (arcsec).

In [3]:
Copied!
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))
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.

In [4]:
Copied!
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()
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()
No description has been provided for this image

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.

Previous Next

Copyright © CNRS 2022–2026 — PSFCraft is part of the ANR DISPERS project (ANR-22-CE46-0009). Maintained by Lucas Sauniere, William Gillard, and Julien Zoubian.

Built with MkDocs using a theme provided by Read the Docs.
« Previous Next »