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()
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()
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 callingcalc_psf(). fov_pixelscontrols the image size;fov_arcsecis 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.