This is a notebook version of the examples available in the POPPY documentation at http://pythonhosted.org/poppy/examples.html
It differs only cosmetically from the code there: it contains some extra function calls to set an aesthetically pleasing size for each plot, and to save the outputs to PNGs for inclusion in the documentation source code. These lines are left out of the example docs HTML page just to streamline it a bit.
%pylab inline --no-import-all
import poppy
poppy.__version__
Populating the interactive namespace from numpy and matplotlib
'0.8.0'
Let’s dive right in to some example code.
For all of the following examples, you will have more informative text output when running the code if you first enable Python’s logging mechanism to display log messages to screen:
import logging
logging.basicConfig(level=logging.DEBUG)
This is very simple, as it should be:
osys = poppy.OpticalSystem()
osys.add_pupil(poppy.CircularAperture(radius=3)) # pupil radius in meters
osys.add_detector(pixelscale=0.010, fov_arcsec=5.0) # image plane coordinates in arcseconds
psf = osys.calc_psf(2e-6) # wavelength in microns
poppy.display_psf(psf, title='The Airy Function')
plt.savefig('example_airy.png', dpi=100)
By combining multiple analytic optics together it is possible to create quite complex pupils:
plt.figure(figsize=(16,12))
ap = poppy.MultiHexagonAperture(rings=3, flattoflat=2) # 3 rings of 2 m segments yields 14.1 m circumscribed diameter
sec = poppy.SecondaryObscuration(secondary_radius=1.5, n_supports=4, support_width=0.1) # secondary with spiders
atlast = poppy.CompoundAnalyticOptic( opticslist=[ap, sec], name='Mock ATLAST') # combine into one optic
atlast.display(npix=1024, colorbar_orientation='vertical')
plt.savefig('example_atlast_pupil.png', dpi=100)
And here’s the PSF:
plt.figure(figsize=(8,6))
osys = poppy.OpticalSystem()
osys.add_pupil(atlast)
osys.add_detector(pixelscale=0.010, fov_arcsec=2.0)
psf = osys.calc_psf(1e-6)
poppy.display_psf(psf, title="Mock ATLAST PSF")
plt.savefig('example_atlast_psf.png', dpi=100)
Defocus can be added using a lens:
plt.figure(figsize=(16,6))
wavelen=1e-6
nsteps = 4
psfs = []
for nwaves in range(nsteps):
osys = poppy.OpticalSystem("test", oversample=2)
osys.add_pupil( poppy.CircularAperture(radius=3)) # pupil radius in meters
osys.add_pupil( poppy.ThinLens(nwaves=nwaves, reference_wavelength=wavelen, radius=3))
osys.add_detector(pixelscale=0.01, fov_arcsec=4.0)
psf = osys.calc_psf(wavelength=wavelen)
psfs.append(psf)
plt.subplot(1,nsteps, nwaves+1)
poppy.display_psf(psf, title='Defocused by {0} waves'.format(nwaves),
colorbar_orientation='horizontal', imagecrop=1.0)
plt.savefig('example_defocus.png', dpi=100)
As an example of a more complicated calculation, here’s a NIRCam-style band limited coronagraph with the source not precisely centered:
plt.figure(figsize=(12,8))
plt.subplots_adjust(hspace=0.4)
oversample=2
pixelscale = 0.010 #arcsec/pixel
wavelength = 4.6e-6
osys = poppy.OpticalSystem("test", oversample=oversample)
osys.add_pupil(poppy.CircularAperture(radius=6.5/2))
osys.add_image()
osys.add_image(poppy.BandLimitedCoron(kind='circular', sigma=5.0))
osys.add_pupil()
lyot = poppy.CircularAperture(radius=6.5/2)
lyot.wavefront_display_hint='intensity' # optional - just affects the display
osys.add_pupil(lyot)
osys.add_detector(pixelscale=pixelscale, fov_arcsec=3.0)
osys.source_offset_theta = 45.
osys.source_offset_r = 0.1 # arcsec
psf = osys.calc_psf(wavelength=wavelength, display_intermediates=True)
plt.savefig('example_BLC_offset.png', dpi=100)
Four quadrant phase mask coronagraphs are a bit more complicated because one needs to ensure proper alignment of the FFT result with the center of the phase mask. This is done using a virtual optic called an ‘FQPM FFT aligner’ as follows:
plt.figure(figsize=(13,8))
plt.subplots_adjust(hspace=0.4, wspace=0.2)
optsys = poppy.OpticalSystem()
optsys.add_pupil( poppy.CircularAperture( radius=3, pad_factor=1.5)) #pad display area by 50%
optsys.add_pupil( poppy.FQPM_FFT_aligner()) # ensure the PSF is centered on the FQPM cross hairs
optsys.add_image() # empty image plane for "before the mask"
optsys.add_image( poppy.IdealFQPM(wavelength=2e-6))
optsys.add_pupil( poppy.FQPM_FFT_aligner(direction='backward')) # undo the alignment tilt after going back to the pupil plane
optsys.add_pupil( poppy.CircularAperture( radius=3)) # Lyot mask - change radius if desired
optsys.add_detector(pixelscale=0.01, fov_arcsec=10.0)
for plane in optsys.planes[4:6]:
plane.wavefront_display_hint = 'intensity' # display this rather than the default phase
psf = optsys.calc_psf(wavelength=2e-6, display_intermediates=True)
plt.savefig('example_FQPM.png', dpi=100)
As a variation, we can add a secondary obscuration. This can be done by creating a compound optic consisting of the circular outer aperture plus an opaque circular obscuration. The latter we can make using the InverseTransmission class.
plt.figure(figsize=(13,8))
plt.subplots_adjust(hspace=0.4, wspace=0.2)
primary = poppy.CircularAperture( radius=3, pad_factor=1.5)
secondary = poppy.InverseTransmission( poppy.CircularAperture(radius=0.5) )
aperture = poppy.CompoundAnalyticOptic( opticslist = [primary, secondary] )
optsys = poppy.OpticalSystem()
optsys.add_pupil( aperture)
optsys.add_pupil( poppy.FQPM_FFT_aligner()) # ensure the PSF is centered on the FQPM cross hairs
optsys.add_image( poppy.IdealFQPM(wavelength=2e-6))
optsys.add_pupil( poppy.FQPM_FFT_aligner(direction='backward')) # undo the alignment tilt after going back to the pupil plane
optsys.add_pupil( poppy.CircularAperture( radius=3)) # Lyot mask - change radius if desired
optsys.add_detector(pixelscale=0.01, fov_arcsec=10.0)
for plane in optsys.planes[3:5]:
plane.wavefront_display_hint = 'intensity' # display this rather than the default phase
optsys.display()
psf = optsys.calc_psf(wavelength=2e-6, display_intermediates=True)
plt.savefig('example_FQPM_obscured.png', dpi=100)
In some cases, coronagraphy calculations can be sped up significantly using the semi-analytic algorithm of Soummer et al. This is implemented by first creating an OpticalSystem as usual, and then casting it to a SemiAnalyticCoronagraph class (which has a special customized propagation method implementing the alternate algorithm):
The following code performs the same calculation both ways and compares their speeds:
radius = 6.5/2
lyot_radius = 6.5/2.5
pixelscale = 0.060
osys = poppy.OpticalSystem("test", oversample=8)
osys.add_pupil( poppy.CircularAperture(radius=radius), name='Entrance Pupil')
osys.add_image( poppy.CircularOcculter(radius = 0.1) )
osys.add_pupil( poppy.CircularAperture(radius=lyot_radius), name='Lyot Pupil')
osys.add_detector(pixelscale=pixelscale, fov_arcsec=5.0)
plt.figure(1, figsize=(12,4))
sam_osys = poppy.SemiAnalyticCoronagraph(osys, oversample=8, occulter_box=0.15)
import time
t0s = time.time()
psf_sam = sam_osys.calc_psf(display_intermediates=True)
t1s = time.time()
plt.tight_layout()
plt.figure(2, figsize=(12,12))
t0f = time.time()
psf_fft = osys.calc_psf(display_intermediates=True)
t1f = time.time()
plt.tight_layout()
plt.figure(3, figsize=(12,6))
plt.clf()
plt.subplot(121)
poppy.utils.display_psf(psf_fft, title="FFT", imagecrop=1)
plt.subplot(122)
poppy.utils.display_psf(psf_sam, title="SAM", imagecrop=1)
plt.tight_layout()
plt.savefig('example_SAM_comparison.png', dpi=100)
print("Elapsed time, FFT: {:.3f} s".format(t1f-t0f))
print("Elapsed time, SAM: {:.3f} s".format(t1s-t0s))
Elapsed time, FFT: 87.878 s Elapsed time, SAM: 3.092 s
All AnalyticOpticalElements support arbitrary shifts and rotations of the optic. Set the shift_x
, shift_y
or rotation
attributes. The shifts are given in meters for pupil plane optics, or arcseconds for image plane optics.
For instance we can demonstrate the shift invariance of PSFs:
plt.figure(figsize=(6,6))
ap_regular = poppy.CircularAperture(radius=2, pad_factor=1.5) # pad_factor is important here - without it you will
ap_shifted = poppy.CircularAperture(radius=2, pad_factor=1.5) # crop off part of the circle outside the array.
ap_shifted.shift_x =-0.75
ap_shifted.shift_y = 0.25
plt.figure(figsize=(6,6))
for optic, title, i in [(ap_regular, 'Unshifted', 1), (ap_shifted, 'Shifted', 3)]:
sys = poppy.OpticalSystem()
sys.add_pupil(optic)
sys.add_detector(0.010, fov_pixels=100)
psf = sys.calc_psf()
ax1 = plt.subplot(2,2,i)
optic.display(nrows=2, colorbar=False, ax=ax1)
ax1.set_title(title+' pupil')
ax2 = plt.subplot(2,2,i+1)
poppy.display_psf(psf,ax=ax2, colorbar=False)
ax2.set_title(title+' PSF')
plt.savefig('example_shift_invariance.png', dpi=100)
<Figure size 432x432 with 0 Axes>
You can also simulate optics being used at tilted inclinations by setting the inclination_x
or inclination_y
attributes. Specify these as degrees of rotation around the given axis, where 0 represents using the optic face-on and 90 represents edge-on.
Note, these simply apply a cosine stretch factor for the geometric projection, but do not introduce path delays or tilted wavefront error. If desired, you can add that yourself with a Zernike WFE tilt term, or directly apply tilt to the wavefront. Also, though you can choose to tilt around either x
or y
, the code isn't set up to correctly model tilts about both simultaneously and will warn you if you ask it to do so. If you to incline an optic around some other angle, instead you can combine e.g. inclination_x
with rotation
to reorient the optic. .
For example, we can tilt a circular mirror and see the resulting elliptical aperture.
plt.figure(figsize=(6,6))
ap = poppy.CircularAperture()
ap.inclination_x = 30 # degrees.
# Recall, tilting about the x axis has the effect of compressing our view of the y axis
ap.display(colorbar=False)
<matplotlib.axes._subplots.AxesSubplot at 0x1c1b174208>
In addition to setting the attributes as shown in the above example, these options can be set directly in the initialization of such elements::
plt.figure(figsize=(6,6))
ap = poppy.RectangleAperture(rotation=30, shift_x=0.1)
ap.display(colorbar=False)
plt.savefig('example_shift_and_rotate.png', dpi=100)
plt.figure(figsize=(6,6))
ap = poppy.CircularAperture(inclination_x = 75, shift_x=0.25, rotation=5) # degrees.
ap.display()
plt.axvline(0.25)
<matplotlib.lines.Line2D at 0x1c1ca93b00>
If multiple transformations are requested, they are applied in the order: shift, rotate, incline. This ensures that the optic's apparent center is always at the location given by (shift_x, shift_y)
, and the inclination can take place around a rotated axis if desired. For example:
plt.figure(figsize=(6,6))
ap = poppy.CircularAperture(shift_x=0.25, shift_y=-0.3, rotation=45, inclination_y = 45)
# note, meters for shifts, degrees for rotation and inclination
ap.display()
plt.scatter( [0.25], [-0.3], marker='+') # show requested center of shifted aperture
<matplotlib.collections.PathCollection at 0x1c22b8d080>