PSFCraft

Home

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

Tutorials

  • PSFCraft — Introduction
  • Getting Started with PSFCraft
    • 1. Configuring the Telescope
    • 2. Inspecting the Optical System
    • 3. Wavelength Dependence
    • 4. Saving and Loading a PSF
    • Key Takeaways
  • Aperture Geometry and Pupil Configurations
  • Wavefront Aberrations with Zernike Polynomials
  • Encircled Energy Analysis
  • 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
  • Getting Started with PSFCraft

Getting Started with PSFCraft¶

What you will learn:

  • How to configure a telescope (mirror sizes, pixel scale, field of view)
  • How to inspect the optical system before computing a PSF
  • How to save and reload PSF FITS files
  • How PSF size changes with wavelength

Prerequisites: 01_introduction.ipynb

In [1]:
Copied!
%matplotlib inline
import psfcraft
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline import psfcraft import numpy as np import matplotlib.pyplot as plt
pysynphot is not installed. Please install it to use PSF generation with polychromatic sources.

1. Configuring the Telescope¶

NewtonianTelescope accepts several parameters at construction time.
The most important ones are:

Parameter Default Unit Description
primary_radius 0.5 m Radius of the primary mirror
secondary_radius 0.1 m Radius of the secondary obstruction
version '1' — Spider geometry (see notebook 03)
wfe_coefficients [0.] m Zernike WFE coefficients

After construction, pixelscale (arcsec/pixel) can be set freely.

In [2]:
Copied!
# Build a telescope with explicit parameters
tel = psfcraft.NewtonianTelescope(
    primary_radius=0.5,     # 1-metre diameter primary
    secondary_radius=0.1,   # 0.2-metre diameter secondary obstruction
)
tel.pixelscale = 0.05       # arcsec/pixel  (fine sampling for illustration)

print(tel)
print(f"Primary radius   : {tel.primary_radius} m")
print(f"Secondary radius : {tel.secondary_radius} m")
print(f"Pixel scale      : {tel.pixelscale} arcsec/pixel")
# Build a telescope with explicit parameters tel = psfcraft.NewtonianTelescope( primary_radius=0.5, # 1-metre diameter primary secondary_radius=0.1, # 0.2-metre diameter secondary obstruction ) tel.pixelscale = 0.05 # arcsec/pixel (fine sampling for illustration) print(tel) print(f"Primary radius : {tel.primary_radius} m") print(f"Secondary radius : {tel.secondary_radius} m") print(f"Pixel scale : {tel.pixelscale} arcsec/pixel")
<NewtonianTelescope: V1>
Primary radius   : 0.5 m
Secondary radius : 0.1 m
Pixel scale      : 0.05 arcsec/pixel

2. Inspecting the Optical System¶

Before computing a PSF you can visualise the pupil (amplitude mask) and the corresponding wavefront (phase map). Call calc_psf() first — it populates tel.optsys.

In [3]:
Copied!
psf = tel.calc_psf(monochromatic=1e-6, fov_pixels=64)

# Display the pupil (intensity = amplitude squared) and the resulting PSF side-by-side
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
tel.optsys.display(what="intensity", ax=axes[0], title="Pupil plane (amplitude)")
psfcraft.display_psf(psf, ax=axes[1], title="PSF  (log scale, oversampled)")
plt.tight_layout()
plt.show()
psf = tel.calc_psf(monochromatic=1e-6, fov_pixels=64) # Display the pupil (intensity = amplitude squared) and the resulting PSF side-by-side fig, axes = plt.subplots(1, 2, figsize=(10, 4)) tel.optsys.display(what="intensity", ax=axes[0], title="Pupil plane (amplitude)") psfcraft.display_psf(psf, ax=axes[1], title="PSF (log scale, oversampled)") plt.tight_layout() plt.show()
No description has been provided for this image

3. Wavelength Dependence¶

The PSF width scales with $\lambda / D$ (diffraction limit).
Longer wavelengths produce broader PSFs.

In [4]:
Copied!
wavelengths_um = [0.3, 0.5, 0.7, 1.0, 1.3, 1.6]   # µm
cmaps = ["Blues", "Greens", "Reds", "Purples", "Oranges", "Greys"]

fig, axes = plt.subplots(1, len(wavelengths_um), figsize=(18, 3))
for ax, wl, cmap in zip(axes, wavelengths_um, cmaps):
    p = tel.calc_psf(monochromatic=wl * 1e-6, fov_pixels=64)
    psfcraft.display_psf(p, ax=ax, title=f"{wl} µm", cmap=cmap, colorbar=False)

plt.suptitle("PSF vs. wavelength  (same telescope, same pixel scale)", y=1.02)
plt.tight_layout()
plt.show()
wavelengths_um = [0.3, 0.5, 0.7, 1.0, 1.3, 1.6] # µm cmaps = ["Blues", "Greens", "Reds", "Purples", "Oranges", "Greys"] fig, axes = plt.subplots(1, len(wavelengths_um), figsize=(18, 3)) for ax, wl, cmap in zip(axes, wavelengths_um, cmaps): p = tel.calc_psf(monochromatic=wl * 1e-6, fov_pixels=64) psfcraft.display_psf(p, ax=ax, title=f"{wl} µm", cmap=cmap, colorbar=False) plt.suptitle("PSF vs. wavelength (same telescope, same pixel scale)", y=1.02) plt.tight_layout() plt.show()
No description has been provided for this image

4. Saving and Loading a PSF¶

PSFs are standard FITS files and can be written with astropy or the built-in POPPY convenience function.

In [5]:
Copied!
import os
from astropy.io import fits

# --- Save ---
output_path = "/tmp/my_psf.fits"
psf.writeto(output_path, overwrite=True)
print(f"PSF saved to {output_path}  ({os.path.getsize(output_path) // 1024} KB)")

# --- Reload ---
psf_loaded = fits.open(output_path)
psf_loaded.info()
import os from astropy.io import fits # --- Save --- output_path = "/tmp/my_psf.fits" psf.writeto(output_path, overwrite=True) print(f"PSF saved to {output_path} ({os.path.getsize(output_path) // 1024} KB)") # --- Reload --- psf_loaded = fits.open(output_path) psf_loaded.info()
PSF saved to /tmp/my_psf.fits  (559 KB)
Filename: /tmp/my_psf.fits
No.    Name      Ver    Type      Cards   Dimensions   Format
  0  OVERSAMP      1 PrimaryHDU      37   (256, 256)   float64   
  1  DET_SAMP      1 ImageHDU        39   (64, 64)   float64   

Key Takeaways¶

  • Set tel.pixelscale (arcsec/pixel) before calling calc_psf().
  • fov_pixels controls the image size; fov_arcsec is an alternative.
  • tel.optsys.display() lets you inspect the pupil and wavefront planes.
  • The PSF broadens proportionally to wavelength — a direct consequence of diffraction.
  • PSFs are plain FITS files; save/load them with astropy.

Next: 03_aperture_and_pupil.ipynb — explore different aperture geometries and spider configurations.

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 »